@delorenj/pjangler 1.4.2 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3336 -1359
- package/dist/index.js.map +7 -0
- package/dist/mcp-server.js +1520 -838
- package/dist/mcp-server.js.map +7 -0
- package/dist/prompt.js +2 -1
- package/dist/prompt.js.map +7 -0
- package/package.json +8 -4
- package/templates/hermes-agent/copier.yml +16 -3
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +7 -4
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +88 -94
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +44 -21
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +182 -171
- package/templates/hermes-agent/template/.scripts/31-slack.sh +260 -165
- package/templates/hermes-agent/template/.scripts/40-plane.sh +45 -36
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +210 -41
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +129 -15
- package/templates/hermes-agent/template/.scripts/80-registry.sh +42 -6
- package/templates/hermes-agent/template/.scripts/99-summary.sh +69 -16
- package/templates/hermes-agent/template/.scripts/_lib.sh +773 -0
- package/templates/hermes-agent/template/.scripts/channel-transaction.py +2340 -0
- package/templates/hermes-agent/template/.scripts/config.example.toml +8 -2
- package/templates/hermes-agent/template/.scripts/credential-launch.sh +5 -1
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +2 -3
- package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +182 -0
- package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +108 -0
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +9 -0
- package/templates/hermes-agent/template/.scripts/lib/voice-config.py +546 -0
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +24 -1
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +54 -8
- package/templates/hermes-agent/template/.scripts/providers/trello.sh +25 -2
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +5 -16
- package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +2 -16
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +13 -19
- package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +29 -36
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +7 -8
- package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +260 -0
- package/templates/hermes-agent/template/SOUL.md.jinja +14 -16
- package/templates/hermes-agent/template/hermes.jinja +1 -1
- package/templates/hermes-agent/template/role.yaml.jinja +11 -4
package/dist/mcp-server.js
CHANGED
|
@@ -193,17 +193,17 @@ var init_types = __esm({
|
|
|
193
193
|
|
|
194
194
|
// src/utils/tree-diff.ts
|
|
195
195
|
import { createHash as createHash3 } from "node:crypto";
|
|
196
|
-
import { existsSync as existsSync4, lstatSync as
|
|
196
|
+
import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync as readlinkSync2 } from "node:fs";
|
|
197
197
|
import { join as join6, relative as relative3 } from "node:path";
|
|
198
198
|
function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
|
|
199
199
|
if (!existsSync4(current)) return snapshot;
|
|
200
200
|
const rel = relative3(root, current) || ".";
|
|
201
201
|
if (rel === ".git" || rel.startsWith(`.git${process.platform === "win32" ? "\\" : "/"}`)) return snapshot;
|
|
202
|
-
const stat =
|
|
202
|
+
const stat = lstatSync4(current);
|
|
203
203
|
if (stat.isSymbolicLink()) {
|
|
204
204
|
snapshot.set(rel, `link:${readlinkSync2(current)}`);
|
|
205
205
|
} else if (stat.isFile()) {
|
|
206
|
-
snapshot.set(rel, `file:${createHash3("sha256").update(
|
|
206
|
+
snapshot.set(rel, `file:${createHash3("sha256").update(readFileSync5(current)).digest("hex")}:${stat.mode & 511}`);
|
|
207
207
|
} else if (stat.isDirectory()) {
|
|
208
208
|
snapshot.set(rel, `dir:${stat.mode & 511}`);
|
|
209
209
|
for (const name of readdirSync3(current)) snapshotTree(root, join6(current, name), snapshot);
|
|
@@ -227,14 +227,14 @@ import {
|
|
|
227
227
|
accessSync,
|
|
228
228
|
constants as constants2,
|
|
229
229
|
existsSync as existsSync5,
|
|
230
|
-
lstatSync as
|
|
231
|
-
readFileSync as
|
|
230
|
+
lstatSync as lstatSync5,
|
|
231
|
+
readFileSync as readFileSync6,
|
|
232
232
|
readdirSync as readdirSync4,
|
|
233
|
-
realpathSync as
|
|
233
|
+
realpathSync as realpathSync3,
|
|
234
234
|
statSync
|
|
235
235
|
} from "node:fs";
|
|
236
236
|
import { tmpdir, userInfo } from "node:os";
|
|
237
|
-
import { basename as
|
|
237
|
+
import { basename as basename5, delimiter, isAbsolute, join as join7, relative as relative4, resolve as resolve4 } from "node:path";
|
|
238
238
|
import YAML2 from "yaml";
|
|
239
239
|
function containedBy(parent, candidate) {
|
|
240
240
|
const rel = relative4(resolve4(parent), resolve4(candidate));
|
|
@@ -246,7 +246,7 @@ function firstExecutableOnPath(env2) {
|
|
|
246
246
|
const candidate = resolve4(entry, process.platform === "win32" ? "copier.exe" : "copier");
|
|
247
247
|
try {
|
|
248
248
|
accessSync(candidate, constants2.X_OK);
|
|
249
|
-
const stat =
|
|
249
|
+
const stat = lstatSync5(candidate);
|
|
250
250
|
if (stat.isFile() || stat.isSymbolicLink()) return candidate;
|
|
251
251
|
} catch {
|
|
252
252
|
}
|
|
@@ -254,11 +254,11 @@ function firstExecutableOnPath(env2) {
|
|
|
254
254
|
return void 0;
|
|
255
255
|
}
|
|
256
256
|
function sha2562(path) {
|
|
257
|
-
return createHash4("sha256").update(
|
|
257
|
+
return createHash4("sha256").update(readFileSync6(path)).digest("base64url");
|
|
258
258
|
}
|
|
259
259
|
function fingerprint(path) {
|
|
260
260
|
const absolute = resolve4(path);
|
|
261
|
-
const realPath =
|
|
261
|
+
const realPath = realpathSync3(absolute);
|
|
262
262
|
const stat = statSync(realPath);
|
|
263
263
|
if (!stat.isFile()) throw new Error(`${absolute} is not a regular file`);
|
|
264
264
|
return {
|
|
@@ -292,7 +292,7 @@ function sameFingerprint(expected) {
|
|
|
292
292
|
function consoleScriptContract(path) {
|
|
293
293
|
let text2;
|
|
294
294
|
try {
|
|
295
|
-
text2 =
|
|
295
|
+
text2 = readFileSync6(path, "utf8").slice(0, 32 * 1024);
|
|
296
296
|
} catch (error) {
|
|
297
297
|
return { ok: false, error: `cannot read Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
|
|
298
298
|
}
|
|
@@ -303,7 +303,7 @@ function consoleScriptContract(path) {
|
|
|
303
303
|
return { ok: false, error: "Copier launcher must use one absolute Python interpreter" };
|
|
304
304
|
}
|
|
305
305
|
const interpreterPath = resolve4(shebang[0]);
|
|
306
|
-
const interpreter =
|
|
306
|
+
const interpreter = basename5(interpreterPath);
|
|
307
307
|
if (!/^python(?:\d+(?:\.\d+)*)?$/.test(interpreter)) {
|
|
308
308
|
return { ok: false, error: "Copier launcher is not an absolute Python console script" };
|
|
309
309
|
}
|
|
@@ -327,7 +327,7 @@ function locateUvSitePackages(toolRoot) {
|
|
|
327
327
|
if (existsSync5(sitePackages)) candidates.push(sitePackages);
|
|
328
328
|
}
|
|
329
329
|
if (candidates.length !== 1) throw new Error(`expected one UV Copier site-packages directory, found ${candidates.length}`);
|
|
330
|
-
return
|
|
330
|
+
return realpathSync3(candidates[0]);
|
|
331
331
|
}
|
|
332
332
|
function parseRecordLine(line) {
|
|
333
333
|
if (!line || line.includes('"')) return void 0;
|
|
@@ -356,11 +356,11 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
356
356
|
if (!launcher.ok || !launcher.interpreter) {
|
|
357
357
|
return { ...launcher, executable: realCandidate, realExecutable: realCandidate };
|
|
358
358
|
}
|
|
359
|
-
const expectedInterpreter = join7(toolRoot, "bin",
|
|
359
|
+
const expectedInterpreter = join7(toolRoot, "bin", basename5(launcher.interpreter));
|
|
360
360
|
if (resolve4(launcher.interpreter) !== resolve4(expectedInterpreter)) {
|
|
361
361
|
return { ok: false, error: "UV Copier launcher interpreter is outside the attested tool environment" };
|
|
362
362
|
}
|
|
363
|
-
const interpreterReal =
|
|
363
|
+
const interpreterReal = realpathSync3(launcher.interpreter);
|
|
364
364
|
const uvPythonRoots = [
|
|
365
365
|
join7(home, ".local", "share", "uv", "python"),
|
|
366
366
|
join7(home, "Library", "Application Support", "uv", "python")
|
|
@@ -369,7 +369,7 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
369
369
|
return { ok: false, error: "UV Copier interpreter is not managed by the canonical UV Python installation" };
|
|
370
370
|
}
|
|
371
371
|
const receiptPath = join7(toolRoot, "uv-receipt.toml");
|
|
372
|
-
const receipt =
|
|
372
|
+
const receipt = readFileSync6(receiptPath, "utf8");
|
|
373
373
|
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)) {
|
|
374
374
|
return { ok: false, error: "UV tool receipt does not bind the copier entry point to the Copier package" };
|
|
375
375
|
}
|
|
@@ -378,24 +378,24 @@ function attestUvCopier(candidate, realCandidate, home) {
|
|
|
378
378
|
if (distInfos.length !== 1) {
|
|
379
379
|
return { ok: false, error: `expected one installed Copier distribution, found ${distInfos.length}` };
|
|
380
380
|
}
|
|
381
|
-
const distInfo =
|
|
381
|
+
const distInfo = realpathSync3(distInfos[0]);
|
|
382
382
|
const metadataPath = join7(distInfo, "METADATA");
|
|
383
383
|
const entryPointsPath = join7(distInfo, "entry_points.txt");
|
|
384
384
|
const recordPath = join7(distInfo, "RECORD");
|
|
385
|
-
const metadata =
|
|
385
|
+
const metadata = readFileSync6(metadataPath, "utf8");
|
|
386
386
|
const name = metadata.match(/^Name:\s*(.+)$/mi)?.[1]?.trim();
|
|
387
387
|
const version = metadata.match(/^Version:\s*(.+)$/mi)?.[1]?.trim();
|
|
388
388
|
if (name?.toLowerCase() !== "copier" || !version || !/^9(?:\.|$)/.test(version)) {
|
|
389
389
|
return { ok: false, error: "installed distribution is not a Copier 9 package" };
|
|
390
390
|
}
|
|
391
|
-
const entryPoints =
|
|
391
|
+
const entryPoints = readFileSync6(entryPointsPath, "utf8");
|
|
392
392
|
if (!/^copier\s*=\s*copier\.__main__:CopierApp\.run\s*$/m.test(entryPoints)) {
|
|
393
393
|
return { ok: false, error: "installed Copier distribution has an unexpected console entry point" };
|
|
394
394
|
}
|
|
395
|
-
const recordEntries =
|
|
395
|
+
const recordEntries = readFileSync6(recordPath, "utf8").split(/\r?\n/).map(parseRecordLine).filter((entry) => Boolean(entry));
|
|
396
396
|
const selected = recordEntries.filter((entry) => {
|
|
397
397
|
const normalized = entry.relativePath.replaceAll("\\", "/");
|
|
398
|
-
return normalized.startsWith("copier/") || normalized === `${
|
|
398
|
+
return normalized.startsWith("copier/") || normalized === `${basename5(distInfo)}/METADATA` || normalized === `${basename5(distInfo)}/entry_points.txt` || normalized === "../../../bin/copier";
|
|
399
399
|
});
|
|
400
400
|
if (!selected.some((entry) => entry.relativePath === "../../../bin/copier") || !selected.some((entry) => entry.relativePath.replaceAll("\\", "/") === "copier/__main__.py")) {
|
|
401
401
|
return { ok: false, error: "Copier RECORD does not bind its launcher and package entry point" };
|
|
@@ -456,7 +456,7 @@ function preflightTrustedCopier(options) {
|
|
|
456
456
|
if (!candidate) return { ok: false, error: "copier not found on PATH" };
|
|
457
457
|
let realCandidate;
|
|
458
458
|
try {
|
|
459
|
-
realCandidate =
|
|
459
|
+
realCandidate = realpathSync3(candidate);
|
|
460
460
|
} catch (error) {
|
|
461
461
|
return { ok: false, error: `cannot resolve Copier launcher: ${error instanceof Error ? error.message : String(error)}` };
|
|
462
462
|
}
|
|
@@ -469,10 +469,10 @@ function preflightTrustedCopier(options) {
|
|
|
469
469
|
}
|
|
470
470
|
function regularContainedFile(root, path, label) {
|
|
471
471
|
try {
|
|
472
|
-
const rootReal =
|
|
473
|
-
const fileReal =
|
|
472
|
+
const rootReal = realpathSync3(root);
|
|
473
|
+
const fileReal = realpathSync3(path);
|
|
474
474
|
if (!containedBy(rootReal, fileReal)) return { ok: false, error: `${label} escapes its vendored template root` };
|
|
475
|
-
if (!
|
|
475
|
+
if (!lstatSync5(path).isFile()) return { ok: false, error: `${label} is not a regular file` };
|
|
476
476
|
return { ok: true };
|
|
477
477
|
} catch (error) {
|
|
478
478
|
return { ok: false, error: `${label} is unavailable: ${error instanceof Error ? error.message : String(error)}` };
|
|
@@ -483,7 +483,7 @@ function parseCopierConfig(templateRoot, label) {
|
|
|
483
483
|
const file = regularContainedFile(templateRoot, configPath, `${label} copier.yml`);
|
|
484
484
|
if (!file.ok) return { result: file };
|
|
485
485
|
try {
|
|
486
|
-
const parsed = YAML2.parse(
|
|
486
|
+
const parsed = YAML2.parse(readFileSync6(configPath, "utf8"));
|
|
487
487
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
488
488
|
return { result: { ok: false, error: `${label} copier.yml must contain a mapping` } };
|
|
489
489
|
}
|
|
@@ -518,7 +518,7 @@ function preflightCommonProjectTemplate(pjanglerRoot) {
|
|
|
518
518
|
"template/mise.toml.jinja"
|
|
519
519
|
], "CommonProject template");
|
|
520
520
|
if (!files.ok) return files;
|
|
521
|
-
const projectJson =
|
|
521
|
+
const projectJson = readFileSync6(join7(templateRoot, "template", ".project.json.jinja"), "utf8");
|
|
522
522
|
for (const key of ["project_name", "project_slug", "repo_path", "ticket_provider", "agents"]) {
|
|
523
523
|
if (!projectJson.includes(`"${key}"`)) return { ok: false, error: `CommonProject projection is missing ${key}` };
|
|
524
524
|
}
|
|
@@ -529,7 +529,7 @@ function preflightHermesTemplate(pjanglerRoot, env2 = process.env) {
|
|
|
529
529
|
const explicit = env2.PJANGLER_HERMES_TEMPLATE?.trim();
|
|
530
530
|
if (explicit) {
|
|
531
531
|
try {
|
|
532
|
-
if (
|
|
532
|
+
if (realpathSync3(resolve4(explicit)) !== realpathSync3(templateRoot)) {
|
|
533
533
|
return { ok: false, error: "MCP Hermes apply requires the version-locked vendored template" };
|
|
534
534
|
}
|
|
535
535
|
} catch (error) {
|
|
@@ -547,27 +547,29 @@ function preflightHermesTemplate(pjanglerRoot, env2 = process.env) {
|
|
|
547
547
|
"template/.scripts/05-fleet-env.sh",
|
|
548
548
|
"template/.scripts/10-hermes-profile.sh",
|
|
549
549
|
"template/.scripts/20-runtime-repo.sh",
|
|
550
|
+
"template/.scripts/30-telegram.sh",
|
|
551
|
+
"template/.scripts/31-slack.sh",
|
|
550
552
|
"template/.scripts/42-ticket-provider.sh",
|
|
551
553
|
"template/.scripts/70-systemd.sh",
|
|
552
554
|
"template/.scripts/80-registry.sh"
|
|
553
555
|
], "Hermes template");
|
|
554
556
|
if (!required.ok) return required;
|
|
555
|
-
const role =
|
|
557
|
+
const role = readFileSync6(join7(templateRoot, "template", "role.yaml.jinja"), "utf8");
|
|
556
558
|
if (!/^bloodbank:\s*$[\s\S]*?^\s+enabled:\s+(?:true|false)\s*$/m.test(role)) {
|
|
557
559
|
return { ok: false, error: "Hermes role projection must declare bloodbank.enabled as a strict boolean" };
|
|
558
560
|
}
|
|
559
|
-
const library =
|
|
561
|
+
const library = readFileSync6(join7(templateRoot, "template", ".scripts", "_lib.sh"), "utf8");
|
|
560
562
|
if (!library.includes("PJANGLER_PROJECT_ROOT") || !library.includes('"$explicit"/agents/hermes/*')) {
|
|
561
563
|
return { ok: false, error: "Hermes project-root resolver must honor the explicitly contained MCP target" };
|
|
562
564
|
}
|
|
563
|
-
const skipPlane =
|
|
565
|
+
const skipPlane = readFileSync6(join7(templateRoot, "template", ".scripts", "42-ticket-provider.sh"), "utf8");
|
|
564
566
|
const guard = skipPlane.indexOf('if [[ "${SKIP_PLANE:-0}" == "1" ]]');
|
|
565
567
|
const firstSource = skipPlane.search(/^source\s/m);
|
|
566
568
|
if (guard < 0 || firstSource < 0 || guard > firstSource) {
|
|
567
569
|
return { ok: false, error: "Hermes ticket-provider skip guard must precede all sourced provider/config logic" };
|
|
568
570
|
}
|
|
569
|
-
for (const script of ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "80-registry.sh"]) {
|
|
570
|
-
const text2 =
|
|
571
|
+
for (const script of ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "30-telegram.sh", "31-slack.sh", "80-registry.sh"]) {
|
|
572
|
+
const text2 = readFileSync6(join7(templateRoot, "template", ".scripts", script), "utf8");
|
|
571
573
|
const hostGuard = text2.indexOf('if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]');
|
|
572
574
|
const source = text2.search(/^source\s/m);
|
|
573
575
|
if (hostGuard < 0 || source < 0 || hostGuard > source) {
|
|
@@ -585,7 +587,7 @@ function preflightRenderedHermes(options) {
|
|
|
585
587
|
const roleDir = resolve4(options.roleDir);
|
|
586
588
|
if (!containedBy(target, roleDir)) return { ok: false, error: "rendered Hermes role escapes its project target" };
|
|
587
589
|
try {
|
|
588
|
-
const stat =
|
|
590
|
+
const stat = lstatSync5(roleDir);
|
|
589
591
|
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
590
592
|
return { ok: false, error: "rendered Hermes role must be a real directory" };
|
|
591
593
|
}
|
|
@@ -606,7 +608,7 @@ function preflightRenderedHermes(options) {
|
|
|
606
608
|
if (!required.ok) return required;
|
|
607
609
|
for (const script of ["_lib.sh", "01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "20-runtime-repo.sh", "42-ticket-provider.sh", "70-systemd.sh", "80-registry.sh"]) {
|
|
608
610
|
try {
|
|
609
|
-
if (
|
|
611
|
+
if (readFileSync6(join7(renderedScripts, script), "utf8") !== readFileSync6(join7(templateScripts, script), "utf8")) {
|
|
610
612
|
return { ok: false, error: `rendered Hermes script differs from the attested template: ${script}` };
|
|
611
613
|
}
|
|
612
614
|
} catch (error) {
|
|
@@ -615,7 +617,7 @@ function preflightRenderedHermes(options) {
|
|
|
615
617
|
}
|
|
616
618
|
let role;
|
|
617
619
|
try {
|
|
618
|
-
const parsed = YAML2.parse(
|
|
620
|
+
const parsed = YAML2.parse(readFileSync6(join7(roleDir, "role.yaml"), "utf8"));
|
|
619
621
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
620
622
|
return { ok: false, error: "rendered Hermes role.yaml must contain a mapping" };
|
|
621
623
|
}
|
|
@@ -637,7 +639,7 @@ function preflightRenderedHermes(options) {
|
|
|
637
639
|
const manifestPath = join7(target, ".project.json");
|
|
638
640
|
if (existsSync5(manifestPath)) {
|
|
639
641
|
try {
|
|
640
|
-
const manifest = JSON.parse(
|
|
642
|
+
const manifest = JSON.parse(readFileSync6(manifestPath, "utf8"));
|
|
641
643
|
const agents = manifest.agents;
|
|
642
644
|
const declared = agents?.[options.agentId];
|
|
643
645
|
if (!declared || declared.role !== options.role || declared.role_dir !== relative4(target, roleDir) || declared.provisioning_state !== "provisioned") {
|
|
@@ -828,7 +830,7 @@ var init_RegistryStore = __esm({
|
|
|
828
830
|
async getByRepoPath(repoPath) {
|
|
829
831
|
const registry = await this.load();
|
|
830
832
|
return Object.values(registry.projects).find(
|
|
831
|
-
(
|
|
833
|
+
(p4) => p4.repo_path === repoPath
|
|
832
834
|
);
|
|
833
835
|
}
|
|
834
836
|
async close() {
|
|
@@ -905,7 +907,7 @@ var init_RegistryStore = __esm({
|
|
|
905
907
|
}
|
|
906
908
|
async loadTicketProvider(client, projectId) {
|
|
907
909
|
const { rows } = await client.query(
|
|
908
|
-
`SELECT provider_type, workspace, identifier, board_id, state
|
|
910
|
+
`SELECT provider_type, workspace, identifier, identifier_source, identifier_fetched_at, board_id, board_confirmed_at, state
|
|
909
911
|
FROM public.project_ticket_boards
|
|
910
912
|
WHERE project_id = $1
|
|
911
913
|
LIMIT 1`,
|
|
@@ -915,11 +917,20 @@ var init_RegistryStore = __esm({
|
|
|
915
917
|
return { type: "plane", workspace: "33god", identifier: "", board_id: "", state: "planned" };
|
|
916
918
|
}
|
|
917
919
|
const row = rows[0];
|
|
920
|
+
const fetchedAt = row.identifier_fetched_at;
|
|
921
|
+
const confirmedAt = row.board_confirmed_at;
|
|
918
922
|
return {
|
|
919
923
|
type: row.provider_type,
|
|
920
924
|
workspace: row.workspace ?? void 0,
|
|
921
925
|
identifier: row.identifier ?? void 0,
|
|
926
|
+
// Provenance is a column, not an extension blob: dropping it here would
|
|
927
|
+
// silently demote every provider-confirmed board back to a guess.
|
|
928
|
+
identifier_source: row.identifier_source ?? void 0,
|
|
929
|
+
identifier_fetched_at: fetchedAt ? fetchedAt instanceof Date ? fetchedAt.toISOString() : String(fetchedAt) : void 0,
|
|
922
930
|
board_id: row.board_id ?? void 0,
|
|
931
|
+
// Board-binding provenance is its own column for the same reason
|
|
932
|
+
// identifier provenance is: dropping it demotes every honest link.
|
|
933
|
+
board_confirmed_at: confirmedAt ? confirmedAt instanceof Date ? confirmedAt.toISOString() : String(confirmedAt) : void 0,
|
|
923
934
|
state: row.state ?? void 0
|
|
924
935
|
};
|
|
925
936
|
}
|
|
@@ -947,15 +958,18 @@ var init_RegistryStore = __esm({
|
|
|
947
958
|
);
|
|
948
959
|
await client.query(
|
|
949
960
|
`INSERT INTO public.project_ticket_boards
|
|
950
|
-
(repo_id, project_id, provider_type, workspace, identifier, board_id, state)
|
|
951
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
961
|
+
(repo_id, project_id, provider_type, workspace, identifier, identifier_source, identifier_fetched_at, board_id, board_confirmed_at, state)
|
|
962
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)`,
|
|
952
963
|
[
|
|
953
964
|
repoId,
|
|
954
965
|
projectId,
|
|
955
966
|
tp.type,
|
|
956
967
|
tp.workspace ?? null,
|
|
957
968
|
tp.identifier ?? null,
|
|
969
|
+
tp.identifier_source ?? null,
|
|
970
|
+
tp.identifier_fetched_at ?? null,
|
|
958
971
|
tp.board_id ?? null,
|
|
972
|
+
tp.board_confirmed_at ?? null,
|
|
959
973
|
tp.state ?? null
|
|
960
974
|
]
|
|
961
975
|
);
|
|
@@ -965,18 +979,69 @@ var init_RegistryStore = __esm({
|
|
|
965
979
|
}
|
|
966
980
|
});
|
|
967
981
|
|
|
982
|
+
// src/project/boardUrl.ts
|
|
983
|
+
var init_boardUrl = __esm({
|
|
984
|
+
"src/project/boardUrl.ts"() {
|
|
985
|
+
"use strict";
|
|
986
|
+
}
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
// src/utils/version.ts
|
|
990
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
991
|
+
import { dirname as dirname5, join as join8 } from "node:path";
|
|
992
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
993
|
+
var PJANGLER_VERSION;
|
|
994
|
+
var init_version = __esm({
|
|
995
|
+
"src/utils/version.ts"() {
|
|
996
|
+
"use strict";
|
|
997
|
+
PJANGLER_VERSION = (() => {
|
|
998
|
+
try {
|
|
999
|
+
let dir = dirname5(fileURLToPath2(import.meta.url));
|
|
1000
|
+
for (let i = 0; i < 4; i++) {
|
|
1001
|
+
try {
|
|
1002
|
+
const raw = readFileSync7(join8(dir, "package.json"), "utf8");
|
|
1003
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
1004
|
+
} catch {
|
|
1005
|
+
const parent = dirname5(dir);
|
|
1006
|
+
if (parent === dir) break;
|
|
1007
|
+
dir = parent;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
} catch {
|
|
1011
|
+
}
|
|
1012
|
+
return "0.0.0";
|
|
1013
|
+
})();
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
// src/project/boardQuery.ts
|
|
1018
|
+
function workspaceEnvKey(workspace) {
|
|
1019
|
+
const key = (workspace ?? "default").toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1020
|
+
return `PLANE_${key || "DEFAULT"}_API_KEY`;
|
|
1021
|
+
}
|
|
1022
|
+
var init_boardQuery = __esm({
|
|
1023
|
+
"src/project/boardQuery.ts"() {
|
|
1024
|
+
"use strict";
|
|
1025
|
+
init_boardUrl();
|
|
1026
|
+
init_version();
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
968
1030
|
// src/project/index.ts
|
|
969
|
-
import { spawnSync as
|
|
1031
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
970
1032
|
import { isIP } from "node:net";
|
|
971
|
-
import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync6, fchmodSync, fsyncSync, lstatSync as
|
|
1033
|
+
import { chmodSync as chmodSync2, closeSync as closeSync2, copyFileSync as copyFileSync2, existsSync as existsSync6, fchmodSync, fsyncSync, lstatSync as lstatSync6, mkdirSync as mkdirSync4, mkdtempSync as mkdtempSync2, openSync as openSync2, readFileSync as readFileSync8, realpathSync as realpathSync4, renameSync as renameSync3, rmSync as rmSync3, statSync as statSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
972
1034
|
import { homedir as homedir4, tmpdir as tmpdir2 } from "node:os";
|
|
973
|
-
import { basename as
|
|
974
|
-
import { fileURLToPath as
|
|
1035
|
+
import { basename as basename6, delimiter as delimiter2, dirname as dirname6, isAbsolute as isAbsolute2, join as join9, relative as relative5, resolve as resolve5, sep as sep2, win32 } from "node:path";
|
|
1036
|
+
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
975
1037
|
import YAML3 from "yaml";
|
|
1038
|
+
function providerAssignsIdentifiers(type) {
|
|
1039
|
+
return IDENTIFIER_ASSIGNING_PROVIDERS.includes((type ?? "").trim());
|
|
1040
|
+
}
|
|
976
1041
|
function synchronizeCopierIdentity(manifestPath, manifest) {
|
|
977
|
-
const answersPath =
|
|
1042
|
+
const answersPath = join9(dirname6(manifestPath), ".copier-answers.yml");
|
|
978
1043
|
if (!existsSync6(answersPath)) return [];
|
|
979
|
-
const current =
|
|
1044
|
+
const current = readFileSync8(answersPath, "utf8");
|
|
980
1045
|
const document = YAML3.parseDocument(current);
|
|
981
1046
|
if (document.errors.length) return [];
|
|
982
1047
|
const name = String(document.get("project_name") ?? "");
|
|
@@ -990,7 +1055,7 @@ function synchronizeCopierIdentity(manifestPath, manifest) {
|
|
|
990
1055
|
return [answersPath];
|
|
991
1056
|
}
|
|
992
1057
|
function projectRegistryPath(env2 = process.env) {
|
|
993
|
-
return expandHome(env2[PROJECT_REGISTRY_ENV] ||
|
|
1058
|
+
return expandHome(env2[PROJECT_REGISTRY_ENV] || join9(homedir4(), ".config", "pjangler", "projects.yaml"));
|
|
994
1059
|
}
|
|
995
1060
|
function createSafeRecord(entries = []) {
|
|
996
1061
|
const record = /* @__PURE__ */ Object.create(null);
|
|
@@ -1005,7 +1070,7 @@ function emptyProjectRegistry() {
|
|
|
1005
1070
|
}
|
|
1006
1071
|
function loadProjectRegistry(path = projectRegistryPath()) {
|
|
1007
1072
|
if (!existsSync6(path)) return emptyProjectRegistry();
|
|
1008
|
-
const raw = YAML3.parse(
|
|
1073
|
+
const raw = YAML3.parse(readFileSync8(path, "utf8"));
|
|
1009
1074
|
if (raw == null) return emptyProjectRegistry();
|
|
1010
1075
|
if (!isRecord(raw)) throw new Error(`Project registry must be a mapping: ${path}`);
|
|
1011
1076
|
const registry = raw;
|
|
@@ -1084,14 +1149,14 @@ function fsyncDirectory(path) {
|
|
|
1084
1149
|
}
|
|
1085
1150
|
function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
1086
1151
|
validateProjectRegistry(registry);
|
|
1087
|
-
mkdirSync4(
|
|
1152
|
+
mkdirSync4(dirname6(path), { recursive: true });
|
|
1088
1153
|
let text2;
|
|
1089
1154
|
let mode = 420;
|
|
1090
1155
|
if (existsSync6(path)) {
|
|
1091
|
-
const stat =
|
|
1156
|
+
const stat = lstatSync6(path);
|
|
1092
1157
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Project registry must be a regular file: ${path}`);
|
|
1093
1158
|
mode = stat.mode & 511;
|
|
1094
|
-
const current =
|
|
1159
|
+
const current = readFileSync8(path, "utf8");
|
|
1095
1160
|
const document = YAML3.parseDocument(current);
|
|
1096
1161
|
if (document.errors.length) throw new Error(`Project registry YAML is invalid: ${path}`);
|
|
1097
1162
|
setYamlLeaf(document, ["schema_version"], registry.schema_version);
|
|
@@ -1118,9 +1183,9 @@ function saveProjectRegistry(registry, path = projectRegistryPath()) {
|
|
|
1118
1183
|
fsyncSync(fd);
|
|
1119
1184
|
closeSync2(fd);
|
|
1120
1185
|
fd = void 0;
|
|
1121
|
-
|
|
1186
|
+
renameSync3(temp, path);
|
|
1122
1187
|
chmodSync2(path, mode);
|
|
1123
|
-
fsyncDirectory(
|
|
1188
|
+
fsyncDirectory(dirname6(path));
|
|
1124
1189
|
} catch (error) {
|
|
1125
1190
|
if (fd !== void 0) try {
|
|
1126
1191
|
closeSync2(fd);
|
|
@@ -1142,6 +1207,7 @@ function validateProjectRegistry(registry) {
|
|
|
1142
1207
|
const slugs = /* @__PURE__ */ new Set();
|
|
1143
1208
|
const repoPaths = /* @__PURE__ */ new Map();
|
|
1144
1209
|
const identifiers = /* @__PURE__ */ new Map();
|
|
1210
|
+
const boardIds = /* @__PURE__ */ new Map();
|
|
1145
1211
|
const notebookIds = /* @__PURE__ */ new Map();
|
|
1146
1212
|
const overviewNoteIds = /* @__PURE__ */ new Map();
|
|
1147
1213
|
for (const [slug, project] of Object.entries(registry.projects)) {
|
|
@@ -1154,13 +1220,24 @@ function validateProjectRegistry(registry) {
|
|
|
1154
1220
|
throw new Error(`Duplicate project repo_path: ${project.repo_path} used by ${existingRepoSlug} and ${slug}`);
|
|
1155
1221
|
}
|
|
1156
1222
|
repoPaths.set(repoKey, slug);
|
|
1223
|
+
const scope = ticketProviderScope(project.ticket_provider);
|
|
1224
|
+
const boardId = project.ticket_provider.board_id?.trim();
|
|
1225
|
+
if (boardId) {
|
|
1226
|
+
const boardKey = `${scope}\0${boardId}`;
|
|
1227
|
+
const existingBoardSlug = boardIds.get(boardKey);
|
|
1228
|
+
if (existingBoardSlug && existingBoardSlug !== slug) {
|
|
1229
|
+
throw new Error(`Duplicate project board_id: ${boardId} in ${scope.replace("\0", "/")} used by ${existingBoardSlug} and ${slug}`);
|
|
1230
|
+
}
|
|
1231
|
+
boardIds.set(boardKey, slug);
|
|
1232
|
+
}
|
|
1157
1233
|
const identifier = project.ticket_provider.identifier?.toUpperCase();
|
|
1158
1234
|
if (identifier) {
|
|
1159
|
-
const
|
|
1235
|
+
const identifierKey = `${scope}\0${identifier}`;
|
|
1236
|
+
const existingIdentifierSlug = identifiers.get(identifierKey);
|
|
1160
1237
|
if (existingIdentifierSlug && existingIdentifierSlug !== slug) {
|
|
1161
|
-
throw new Error(`Duplicate project identifier: ${identifier} used by ${existingIdentifierSlug} and ${slug}`);
|
|
1238
|
+
throw new Error(`Duplicate project identifier: ${identifier} in ${scope.replace("\0", "/")} used by ${existingIdentifierSlug} and ${slug}`);
|
|
1162
1239
|
}
|
|
1163
|
-
identifiers.set(
|
|
1240
|
+
identifiers.set(identifierKey, slug);
|
|
1164
1241
|
}
|
|
1165
1242
|
const notebookId = project.notebook?.notebook_id;
|
|
1166
1243
|
if (notebookId) {
|
|
@@ -1176,6 +1253,9 @@ function validateProjectRegistry(registry) {
|
|
|
1176
1253
|
}
|
|
1177
1254
|
}
|
|
1178
1255
|
}
|
|
1256
|
+
function ticketProviderScope(provider) {
|
|
1257
|
+
return `${provider.type ?? ""}\0${(provider.workspace ?? "").toLowerCase()}`;
|
|
1258
|
+
}
|
|
1179
1259
|
function validateGlobalNotebookConfig(value) {
|
|
1180
1260
|
if (value === void 0) return;
|
|
1181
1261
|
if (!isRecord(value)) throw new Error("Project registry notebook must be a mapping");
|
|
@@ -1241,40 +1321,41 @@ function normalizeTicketProvider(value) {
|
|
|
1241
1321
|
function buildTicketProviderBlock(input) {
|
|
1242
1322
|
const type = normalizeTicketProvider(input.type);
|
|
1243
1323
|
const boardId = input.boardId ?? "";
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
identifier: input.identifier,
|
|
1249
|
-
board_id: boardId,
|
|
1250
|
-
state: boardId ? "linked" : "planned"
|
|
1251
|
-
};
|
|
1252
|
-
}
|
|
1253
|
-
const workspace = input.workspace ?? "33god";
|
|
1324
|
+
const identifierSource = input.identifierSource ?? "proposed";
|
|
1325
|
+
const fetchedAt = identifierSource === "provider" ? input.identifierFetchedAt : void 0;
|
|
1326
|
+
const confirmedAt = input.boardConfirmedAt ?? (boardId ? fetchedAt : void 0);
|
|
1327
|
+
const provenClaim = !providerAssignsIdentifiers(type) || identifierSource === "provider";
|
|
1254
1328
|
return {
|
|
1255
1329
|
type,
|
|
1256
|
-
workspace,
|
|
1330
|
+
workspace: input.workspace ?? (type === "trello" ? "" : "33god"),
|
|
1257
1331
|
identifier: input.identifier,
|
|
1332
|
+
identifier_source: identifierSource,
|
|
1333
|
+
...fetchedAt ? { identifier_fetched_at: fetchedAt } : {},
|
|
1258
1334
|
board_id: boardId,
|
|
1259
|
-
|
|
1335
|
+
...confirmedAt ? { board_confirmed_at: confirmedAt } : {},
|
|
1336
|
+
state: boardId && confirmedAt && provenClaim ? "linked" : "planned"
|
|
1260
1337
|
};
|
|
1261
1338
|
}
|
|
1262
|
-
function
|
|
1263
|
-
|
|
1339
|
+
function ticketProviderKeyGroups(provider, workspace) {
|
|
1340
|
+
if (provider === "trello") return [["TRELLO_KEY"], ["TRELLO_TOKEN"]];
|
|
1341
|
+
return [[.../* @__PURE__ */ new Set(["PLANE_API_KEY", workspaceEnvKey(workspace)])]];
|
|
1264
1342
|
}
|
|
1265
|
-
function ticketProviderKeyVars(provider) {
|
|
1266
|
-
return provider
|
|
1343
|
+
function ticketProviderKeyVars(provider, workspace) {
|
|
1344
|
+
return ticketProviderKeyGroups(provider, workspace).flat();
|
|
1267
1345
|
}
|
|
1268
1346
|
function ticketProviderSecretsPath(env2 = process.env) {
|
|
1269
|
-
const base = env2.XDG_CONFIG_HOME ||
|
|
1270
|
-
return
|
|
1347
|
+
const base = env2.XDG_CONFIG_HOME || join9(env2.HOME || homedir4(), ".config");
|
|
1348
|
+
return join9(base, "zshyzsh", "secrets.zsh");
|
|
1349
|
+
}
|
|
1350
|
+
function ticketProviderFleetEnvPath(env2 = process.env) {
|
|
1351
|
+
return env2.HERMES_FLEET_ENV?.trim() || join9(env2.HOME || homedir4(), ".hermes", "fleet.env");
|
|
1271
1352
|
}
|
|
1272
1353
|
function readShellAssignments(path, keys) {
|
|
1273
1354
|
const found = {};
|
|
1274
1355
|
if (!existsSync6(path)) return found;
|
|
1275
1356
|
let text2;
|
|
1276
1357
|
try {
|
|
1277
|
-
text2 =
|
|
1358
|
+
text2 = readFileSync8(path, "utf8");
|
|
1278
1359
|
} catch {
|
|
1279
1360
|
return found;
|
|
1280
1361
|
}
|
|
@@ -1287,8 +1368,8 @@ function readShellAssignments(path, keys) {
|
|
|
1287
1368
|
const key = match[1];
|
|
1288
1369
|
if (!wanted.has(key) || found[key] !== void 0) continue;
|
|
1289
1370
|
let value = match[2].trim();
|
|
1290
|
-
const
|
|
1291
|
-
if ((
|
|
1371
|
+
const quote2 = value[0];
|
|
1372
|
+
if ((quote2 === '"' || quote2 === "'") && value.length > 1 && value.endsWith(quote2)) {
|
|
1292
1373
|
value = value.slice(1, -1);
|
|
1293
1374
|
} else {
|
|
1294
1375
|
value = value.split(/\s+#/)[0].trim();
|
|
@@ -1310,16 +1391,16 @@ function resolveTicketProviderCredentials(input) {
|
|
|
1310
1391
|
}
|
|
1311
1392
|
}
|
|
1312
1393
|
const candidates = [];
|
|
1313
|
-
if (input.repoPath) candidates.push(
|
|
1314
|
-
|
|
1315
|
-
candidates.push(
|
|
1394
|
+
if (input.repoPath) candidates.push(join9(input.repoPath, ".env"));
|
|
1395
|
+
candidates.push(ticketProviderFleetEnvPath(env2));
|
|
1396
|
+
candidates.push(ticketProviderSecretsPath(env2));
|
|
1316
1397
|
for (const candidate of candidates) {
|
|
1317
1398
|
const outstanding = missing();
|
|
1318
1399
|
if (!outstanding.length) break;
|
|
1319
|
-
const assignments = readShellAssignments(candidate
|
|
1400
|
+
const assignments = readShellAssignments(candidate, outstanding);
|
|
1320
1401
|
for (const [key, value] of Object.entries(assignments)) {
|
|
1321
1402
|
values[key] = value;
|
|
1322
|
-
sources[key] = candidate
|
|
1403
|
+
sources[key] = candidate;
|
|
1323
1404
|
}
|
|
1324
1405
|
}
|
|
1325
1406
|
return { values, sources };
|
|
@@ -1328,40 +1409,42 @@ function resolveTicketProviderAdapter(provider, env2 = process.env) {
|
|
|
1328
1409
|
const file = `${provider}.sh`;
|
|
1329
1410
|
const candidates = [];
|
|
1330
1411
|
const override = env2[TICKET_PROVIDER_ADAPTERS_ENV];
|
|
1331
|
-
if (override) candidates.push(
|
|
1412
|
+
if (override) candidates.push(join9(override, file));
|
|
1332
1413
|
const relativeRoots = [
|
|
1333
|
-
|
|
1334
|
-
|
|
1414
|
+
join9("templates", "hermes-agent", "template", ".scripts", "providers"),
|
|
1415
|
+
join9("agents", "hermes", "pm", ".scripts", "providers")
|
|
1335
1416
|
];
|
|
1336
1417
|
try {
|
|
1337
|
-
let dir =
|
|
1418
|
+
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
1338
1419
|
for (let depth = 0; depth < 8; depth++) {
|
|
1339
|
-
for (const relativeRoot of relativeRoots) candidates.push(
|
|
1340
|
-
const parent =
|
|
1420
|
+
for (const relativeRoot of relativeRoots) candidates.push(join9(dir, relativeRoot, file));
|
|
1421
|
+
const parent = dirname6(dir);
|
|
1341
1422
|
if (parent === dir) break;
|
|
1342
1423
|
dir = parent;
|
|
1343
1424
|
}
|
|
1344
1425
|
} catch {
|
|
1345
1426
|
}
|
|
1346
1427
|
for (const relativeRoot of relativeRoots) {
|
|
1347
|
-
candidates.push(
|
|
1428
|
+
candidates.push(join9(homedir4(), "code", "pjangler", relativeRoot, file));
|
|
1348
1429
|
}
|
|
1349
1430
|
return candidates.find((candidate) => existsSync6(candidate));
|
|
1350
1431
|
}
|
|
1351
1432
|
function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
1352
1433
|
const provider = action.provider;
|
|
1353
|
-
const
|
|
1434
|
+
const groups = ticketProviderKeyGroups(provider, action.workspace);
|
|
1354
1435
|
const { values } = resolveTicketProviderCredentials({
|
|
1355
|
-
keys: ticketProviderKeyVars(provider),
|
|
1436
|
+
keys: ticketProviderKeyVars(provider, action.workspace),
|
|
1356
1437
|
repoPath: action.repoPath,
|
|
1357
1438
|
env: env2
|
|
1358
1439
|
});
|
|
1359
|
-
|
|
1440
|
+
const unsatisfied = groups.filter((group) => !group.some((key) => values[key]));
|
|
1441
|
+
if (unsatisfied.length) {
|
|
1442
|
+
const names = unsatisfied.map((group) => group.join(" or ")).join(" and ");
|
|
1360
1443
|
return {
|
|
1361
1444
|
ok: true,
|
|
1362
1445
|
skipped: true,
|
|
1363
1446
|
logs: [
|
|
1364
|
-
`ticket-provider: ${
|
|
1447
|
+
`ticket-provider: ${names} not set; skipping ${provider} board creation (state stays "planned"). Set it in the environment, ${join9(action.repoPath, ".env")}, ${ticketProviderFleetEnvPath(env2)}, or ${ticketProviderSecretsPath(env2)} \u2014 or pass --board-id to link an existing board.`
|
|
1365
1448
|
]
|
|
1366
1449
|
};
|
|
1367
1450
|
}
|
|
@@ -1375,12 +1458,12 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
|
1375
1458
|
};
|
|
1376
1459
|
}
|
|
1377
1460
|
const redact = (text2) => Object.values(values).reduce((acc, secret) => secret ? acc.split(secret).join("***") : acc, text2);
|
|
1378
|
-
const staging = mkdtempSync2(
|
|
1461
|
+
const staging = mkdtempSync2(join9(tmpdir2(), "pjangler-tp-"));
|
|
1379
1462
|
try {
|
|
1380
|
-
const providersDir =
|
|
1463
|
+
const providersDir = join9(staging, "agents", "hermes", "pm", ".scripts", "providers");
|
|
1381
1464
|
mkdirSync4(providersDir, { recursive: true });
|
|
1382
1465
|
writeFileSync4(
|
|
1383
|
-
|
|
1466
|
+
join9(staging, ".project.json"),
|
|
1384
1467
|
`${JSON.stringify(
|
|
1385
1468
|
{
|
|
1386
1469
|
project_name: action.boardName,
|
|
@@ -1399,11 +1482,11 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
|
1399
1482
|
`,
|
|
1400
1483
|
"utf8"
|
|
1401
1484
|
);
|
|
1402
|
-
const staged =
|
|
1485
|
+
const staged = join9(providersDir, `${provider}.sh`);
|
|
1403
1486
|
copyFileSync2(adapter, staged);
|
|
1404
1487
|
const childEnv = { ...env2, ...values, TICKET_PROVIDER: provider };
|
|
1405
1488
|
if (provider === "plane" && action.workspace) childEnv.PLANE_WORKSPACE = action.workspace;
|
|
1406
|
-
const result2 =
|
|
1489
|
+
const result2 = spawnSync3("sh", [staged, "create_board", action.boardName, action.identifier, action.description], {
|
|
1407
1490
|
cwd: existsSync6(action.repoPath) ? action.repoPath : staging,
|
|
1408
1491
|
encoding: "utf8",
|
|
1409
1492
|
env: childEnv
|
|
@@ -1447,16 +1530,26 @@ function provisionTicketProviderBoard(action, env2 = process.env) {
|
|
|
1447
1530
|
error: `ticket-provider: ${provider} create_board returned no board_id`
|
|
1448
1531
|
};
|
|
1449
1532
|
}
|
|
1450
|
-
const
|
|
1533
|
+
const identifier = isRecord(parsed) && typeof parsed.identifier === "string" ? parsed.identifier.trim() : "";
|
|
1534
|
+
if (!identifier) {
|
|
1535
|
+
return {
|
|
1536
|
+
ok: false,
|
|
1537
|
+
skipped: false,
|
|
1538
|
+
logs: [],
|
|
1539
|
+
error: `ticket-provider: ${provider} create_board returned no identifier. The adapter must echo the identifier the provider assigned; pjangler no longer invents one.`
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
const boardUrl2 = isRecord(parsed) && typeof parsed.board_url === "string" ? parsed.board_url : void 0;
|
|
1451
1543
|
return {
|
|
1452
1544
|
ok: true,
|
|
1453
1545
|
skipped: false,
|
|
1454
1546
|
boardId,
|
|
1455
|
-
|
|
1456
|
-
|
|
1547
|
+
identifier,
|
|
1548
|
+
boardUrl: boardUrl2,
|
|
1549
|
+
logs: [`ticket-provider: ${provider} board linked (${identifier} \u2192 ${boardId})`]
|
|
1457
1550
|
};
|
|
1458
1551
|
} finally {
|
|
1459
|
-
|
|
1552
|
+
rmSync3(staging, { recursive: true, force: true });
|
|
1460
1553
|
}
|
|
1461
1554
|
}
|
|
1462
1555
|
function defaultProjectAutomation() {
|
|
@@ -1485,12 +1578,12 @@ function prospectiveRealPath(path) {
|
|
|
1485
1578
|
let cursor = resolve5(path);
|
|
1486
1579
|
const suffix = [];
|
|
1487
1580
|
while (!existsSync6(cursor)) {
|
|
1488
|
-
const parent =
|
|
1581
|
+
const parent = dirname6(cursor);
|
|
1489
1582
|
if (parent === cursor) return resolve5(path);
|
|
1490
|
-
suffix.unshift(
|
|
1583
|
+
suffix.unshift(basename6(cursor));
|
|
1491
1584
|
cursor = parent;
|
|
1492
1585
|
}
|
|
1493
|
-
return resolve5(
|
|
1586
|
+
return resolve5(realpathSync4(cursor), ...suffix);
|
|
1494
1587
|
}
|
|
1495
1588
|
function resolveContainedPath(parentDir, candidate, label) {
|
|
1496
1589
|
const physicalParent = prospectiveRealPath(parentDir);
|
|
@@ -1501,7 +1594,7 @@ function resolveContainedPath(parentDir, candidate, label) {
|
|
|
1501
1594
|
}
|
|
1502
1595
|
return resolve5(candidate);
|
|
1503
1596
|
}
|
|
1504
|
-
function
|
|
1597
|
+
function proposeProjectIdentifier(value) {
|
|
1505
1598
|
const compact = value.replace(/[^A-Za-z0-9]/g, "").toUpperCase();
|
|
1506
1599
|
const identifier = compact.slice(0, 4) || "PROJ";
|
|
1507
1600
|
return identifier.length >= 2 ? identifier : `${identifier}XX`.slice(0, 4);
|
|
@@ -1514,7 +1607,7 @@ function resolveAgentHooksLayer2(input, env2 = process.env) {
|
|
|
1514
1607
|
const override = env2.PJ_AGENT_HOOKS_LAYER;
|
|
1515
1608
|
if (override === "0" || override === "false") return false;
|
|
1516
1609
|
if (override === "1" || override === "true") return true;
|
|
1517
|
-
return !existsSync6(
|
|
1610
|
+
return !existsSync6(join9(homedir4(), ".agents", "hooks"));
|
|
1518
1611
|
}
|
|
1519
1612
|
function jsonStable(value) {
|
|
1520
1613
|
return JSON.stringify(value);
|
|
@@ -1528,7 +1621,7 @@ function projectRecordEquivalent(a, b) {
|
|
|
1528
1621
|
function defaultProjectTargetDir(name, cwd = process.cwd()) {
|
|
1529
1622
|
const compactName = name.replace(/[^A-Za-z0-9._-]/g, "");
|
|
1530
1623
|
const safeName = SAFE_PATH_SEGMENT.test(compactName) ? compactName : slugifyProjectName(name);
|
|
1531
|
-
return resolve5(
|
|
1624
|
+
return resolve5(dirname6(resolve5(cwd)), validateSafePathSegment(safeName, "Generated project directory"));
|
|
1532
1625
|
}
|
|
1533
1626
|
function sourceSkillRoots(env2 = process.env) {
|
|
1534
1627
|
const configuredRoots = (env2[PROJECT_SOURCE_SKILL_ROOTS_ENV] || "").split(delimiter2).map((root) => root.trim()).filter(Boolean);
|
|
@@ -1547,10 +1640,10 @@ function resolveSourceSkillPath(sourceSkill, env2 = process.env) {
|
|
|
1547
1640
|
const expanded = expandHome(sourceSkill);
|
|
1548
1641
|
const direct = resolve5(expanded);
|
|
1549
1642
|
if (existsSync6(direct)) return direct;
|
|
1550
|
-
const name =
|
|
1643
|
+
const name = basename6(sourceSkill);
|
|
1551
1644
|
const roots = sourceSkillRoots(env2);
|
|
1552
1645
|
for (const root of roots) {
|
|
1553
|
-
const candidate =
|
|
1646
|
+
const candidate = join9(root, name);
|
|
1554
1647
|
if (existsSync6(candidate)) return candidate;
|
|
1555
1648
|
}
|
|
1556
1649
|
const searched = roots.length ? ` Searched roots: ${roots.join(", ")}.` : "";
|
|
@@ -1565,8 +1658,14 @@ function planProjectInit(input) {
|
|
|
1565
1658
|
const registry = loadProjectRegistry(registryPath2);
|
|
1566
1659
|
const now = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1567
1660
|
const targetDir = resolve5(input.targetDir ?? defaultProjectTargetDir(input.name, input.cwd));
|
|
1568
|
-
const identifier = (input.projectIdentifier ??
|
|
1661
|
+
const identifier = (input.projectIdentifier ?? proposeProjectIdentifier(input.name)).toUpperCase();
|
|
1569
1662
|
const existing = getOwnRecordValue(registry.projects, slug);
|
|
1663
|
+
const resolvedBoardId = input.boardId ?? input.planeProjectId ?? (existing?.ticket_provider?.board_id || void 0);
|
|
1664
|
+
const inheritedProvenance = existing?.ticket_provider?.identifier_source === "provider" && existing.ticket_provider.identifier?.toUpperCase() === identifier && (existing.ticket_provider.board_id || void 0) === resolvedBoardId ? {
|
|
1665
|
+
identifierSource: "provider",
|
|
1666
|
+
...existing.ticket_provider.identifier_fetched_at ? { identifierFetchedAt: existing.ticket_provider.identifier_fetched_at } : {}
|
|
1667
|
+
} : void 0;
|
|
1668
|
+
const inheritedBoardConfirmation = resolvedBoardId && (existing?.ticket_provider?.board_id || void 0) === resolvedBoardId && existing?.ticket_provider?.board_confirmed_at ? { boardConfirmedAt: existing.ticket_provider.board_confirmed_at } : void 0;
|
|
1570
1669
|
const sourceSkillPath = resolveSourceSkillPath(input.sourceSkill);
|
|
1571
1670
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
1572
1671
|
const agents = createSafeRecord(Object.entries(existing?.agents ?? {}));
|
|
@@ -1601,11 +1700,13 @@ function planProjectInit(input) {
|
|
|
1601
1700
|
ticket_provider: buildTicketProviderBlock({
|
|
1602
1701
|
type: input.ticketProvider ?? "plane",
|
|
1603
1702
|
identifier,
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
//
|
|
1607
|
-
|
|
1608
|
-
|
|
1703
|
+
boardId: resolvedBoardId,
|
|
1704
|
+
workspace: input.boardWorkspace ?? input.planeWorkspace,
|
|
1705
|
+
// Provenance survives a re-plan. Without this, re-running init on an
|
|
1706
|
+
// already-confirmed board would demote it back to "planned" because the
|
|
1707
|
+
// CLI has no way to re-derive where the identifier came from.
|
|
1708
|
+
...inheritedProvenance ?? {},
|
|
1709
|
+
...inheritedBoardConfirmation ?? {}
|
|
1609
1710
|
}),
|
|
1610
1711
|
agents,
|
|
1611
1712
|
automation: existing?.automation ?? defaultProjectAutomation(),
|
|
@@ -1622,14 +1723,13 @@ function planProjectInit(input) {
|
|
|
1622
1723
|
const manifest = projectManifestFromRegistryProject(project);
|
|
1623
1724
|
const apply = input.apply ?? false;
|
|
1624
1725
|
const live = input.live ?? false;
|
|
1625
|
-
const
|
|
1626
|
-
const provisionTicketBoard = input.provisionTicketBoard ?? live;
|
|
1726
|
+
const provisionTicketBoard = input.provisionTicketBoard ?? true;
|
|
1627
1727
|
const enableSystemd = input.enableSystemd ?? live;
|
|
1628
1728
|
const skipPlane = input.skipPlane ?? false;
|
|
1629
|
-
const boardEnabled =
|
|
1630
|
-
const runtimeRepoEnabled = live && provisionRuntimeRepo;
|
|
1729
|
+
const boardEnabled = provisionTicketBoard && !skipPlane;
|
|
1631
1730
|
const systemdEnabled = live && enableSystemd && process.platform !== "darwin";
|
|
1632
|
-
const
|
|
1731
|
+
const agentBoardEffect = live && boardEnabled;
|
|
1732
|
+
const anyExternalAgentEffect = agentBoardEffect || systemdEnabled;
|
|
1633
1733
|
const actions = [
|
|
1634
1734
|
{ kind: "registry.upsert", registryPath: registryPath2, slug, project }
|
|
1635
1735
|
];
|
|
@@ -1652,7 +1752,7 @@ function planProjectInit(input) {
|
|
|
1652
1752
|
}));
|
|
1653
1753
|
}
|
|
1654
1754
|
actions.push(
|
|
1655
|
-
{ kind: "project.write-manifest", path:
|
|
1755
|
+
{ kind: "project.write-manifest", path: join9(targetDir, ".project.json"), manifest },
|
|
1656
1756
|
{
|
|
1657
1757
|
kind: "ticket-provider.create-or-link",
|
|
1658
1758
|
enabled: boardEnabled,
|
|
@@ -1665,7 +1765,7 @@ function planProjectInit(input) {
|
|
|
1665
1765
|
description: project.description || `Ticket board for ${project.slug}`,
|
|
1666
1766
|
boardId: project.ticket_provider.board_id ?? "",
|
|
1667
1767
|
state: project.ticket_provider.board_id ? "linked" : "planned",
|
|
1668
|
-
reason: skipPlane ? "ticket-provider action disabled by skipPlane=true" : project.ticket_provider.board_id ? "board already linked; no provider call" : !
|
|
1768
|
+
reason: skipPlane ? "ticket-provider action disabled by skipPlane=true (--skip-board)" : project.ticket_provider.board_id ? "board already linked; no provider call" : !provisionTicketBoard ? "ticket-provider action requires explicit provisionTicketBoard=true" : `create or link the ${project.ticket_provider.type} board "${project.name}" (${identifier}) via the ticket-provider adapter`
|
|
1669
1769
|
},
|
|
1670
1770
|
{
|
|
1671
1771
|
kind: "hermes.provision-agent",
|
|
@@ -1675,8 +1775,7 @@ function planProjectInit(input) {
|
|
|
1675
1775
|
targetRepo: slug,
|
|
1676
1776
|
role: agentRole,
|
|
1677
1777
|
context: {
|
|
1678
|
-
|
|
1679
|
-
skipPlane: !boardEnabled,
|
|
1778
|
+
skipPlane: !agentBoardEffect,
|
|
1680
1779
|
// Per-agent Bloodbank consumers are retired. Agent ingress always
|
|
1681
1780
|
// stays on the fleet-shared gateway, regardless of live/local mode.
|
|
1682
1781
|
skipBloodbank: true,
|
|
@@ -1696,30 +1795,39 @@ function planProjectInit(input) {
|
|
|
1696
1795
|
...input.boardUrl !== void 0 ? { warnings: [BOARD_URL_DEPRECATION_WARNING] } : {}
|
|
1697
1796
|
};
|
|
1698
1797
|
}
|
|
1699
|
-
function linkTicketProviderBoard(plan, action, boardId) {
|
|
1798
|
+
function linkTicketProviderBoard(plan, action, boardId, identifier, now = /* @__PURE__ */ new Date()) {
|
|
1700
1799
|
const block = buildTicketProviderBlock({
|
|
1701
1800
|
type: action.provider,
|
|
1702
|
-
identifier
|
|
1801
|
+
// The PROVIDER's identifier, not the one we proposed on the way in.
|
|
1802
|
+
identifier,
|
|
1803
|
+
identifierSource: "provider",
|
|
1804
|
+
identifierFetchedAt: now.toISOString(),
|
|
1703
1805
|
boardId,
|
|
1704
1806
|
workspace: action.workspace
|
|
1705
1807
|
});
|
|
1808
|
+
action.identifier = identifier;
|
|
1706
1809
|
plan.project.ticket_provider = block;
|
|
1707
1810
|
const manifestProvider = {
|
|
1708
1811
|
type: block.type,
|
|
1709
1812
|
workspace: block.workspace ?? "",
|
|
1710
1813
|
identifier: block.identifier ?? "",
|
|
1814
|
+
identifier_source: block.identifier_source ?? "proposed",
|
|
1815
|
+
...block.identifier_fetched_at ? { identifier_fetched_at: block.identifier_fetched_at } : {},
|
|
1711
1816
|
board_id: block.board_id ?? "",
|
|
1817
|
+
// The provider just handed this board back, and the manifest is where that
|
|
1818
|
+
// confirmation lives for every later reader of the repo.
|
|
1819
|
+
...block.board_confirmed_at ? { board_confirmed_at: block.board_confirmed_at } : {},
|
|
1712
1820
|
state: block.state ?? "linked"
|
|
1713
1821
|
};
|
|
1714
1822
|
plan.manifest.ticket_provider = manifestProvider;
|
|
1715
1823
|
action.boardId = boardId;
|
|
1716
1824
|
action.state = manifestProvider.state;
|
|
1717
|
-
const manifestPath =
|
|
1825
|
+
const manifestPath = join9(action.repoPath, ".project.json");
|
|
1718
1826
|
let next;
|
|
1719
1827
|
if (existsSync6(manifestPath)) {
|
|
1720
1828
|
let existing = {};
|
|
1721
1829
|
try {
|
|
1722
|
-
const parsed = JSON.parse(
|
|
1830
|
+
const parsed = JSON.parse(readFileSync8(manifestPath, "utf8"));
|
|
1723
1831
|
if (isRecord(parsed)) existing = parsed;
|
|
1724
1832
|
} catch {
|
|
1725
1833
|
existing = {};
|
|
@@ -1727,12 +1835,12 @@ function linkTicketProviderBoard(plan, action, boardId) {
|
|
|
1727
1835
|
const existingProvider = isRecord(existing.ticket_provider) ? existing.ticket_provider : {};
|
|
1728
1836
|
next = { ...existing, ticket_provider: { ...existingProvider, ...manifestProvider } };
|
|
1729
1837
|
} else {
|
|
1730
|
-
mkdirSync4(
|
|
1838
|
+
mkdirSync4(dirname6(manifestPath), { recursive: true });
|
|
1731
1839
|
next = plan.manifest;
|
|
1732
1840
|
}
|
|
1733
1841
|
const text2 = `${JSON.stringify(next, null, 2)}
|
|
1734
1842
|
`;
|
|
1735
|
-
if (!existsSync6(manifestPath) ||
|
|
1843
|
+
if (!existsSync6(manifestPath) || readFileSync8(manifestPath, "utf8") !== text2) {
|
|
1736
1844
|
writeFileSync4(manifestPath, text2, "utf8");
|
|
1737
1845
|
return [manifestPath];
|
|
1738
1846
|
}
|
|
@@ -1761,7 +1869,7 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1761
1869
|
logs.push(
|
|
1762
1870
|
action.data.agent_hooks_layer === "false" ? "commonproject: agent-hooks layer skipped (global ~/.agents/hooks detected \u2014 no per-user CLI injection)" : "commonproject: agent-hooks layer included"
|
|
1763
1871
|
);
|
|
1764
|
-
mkdirSync4(
|
|
1872
|
+
mkdirSync4(dirname6(action.targetDir), { recursive: true });
|
|
1765
1873
|
const before = snapshotTree(action.targetDir);
|
|
1766
1874
|
const copierExecutable = options.trustedCopier?.executable ?? action.command[0];
|
|
1767
1875
|
const copierEnv = options.trustedCopier ? { ...process.env } : void 0;
|
|
@@ -1771,7 +1879,7 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1771
1879
|
copierEnv.PYTHONNOUSERSITE = "1";
|
|
1772
1880
|
copierEnv.PYTHONSAFEPATH = "1";
|
|
1773
1881
|
}
|
|
1774
|
-
const result2 =
|
|
1882
|
+
const result2 = spawnSync3(copierExecutable, action.command.slice(1), {
|
|
1775
1883
|
encoding: "utf8",
|
|
1776
1884
|
cwd: action.cwd,
|
|
1777
1885
|
...copierEnv ? { env: copierEnv } : {}
|
|
@@ -1792,11 +1900,11 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1792
1900
|
break;
|
|
1793
1901
|
}
|
|
1794
1902
|
} else if (action.kind === "project.write-manifest") {
|
|
1795
|
-
mkdirSync4(
|
|
1903
|
+
mkdirSync4(dirname6(action.path), { recursive: true });
|
|
1796
1904
|
let value = action.manifest;
|
|
1797
1905
|
if (existsSync6(action.path)) {
|
|
1798
1906
|
try {
|
|
1799
|
-
const currentValue = JSON.parse(
|
|
1907
|
+
const currentValue = JSON.parse(readFileSync8(action.path, "utf8"));
|
|
1800
1908
|
if (isRecord(currentValue)) {
|
|
1801
1909
|
const currentNotebook = isRecord(currentValue.notebook) ? currentValue.notebook : {};
|
|
1802
1910
|
const desiredNotebook = isRecord(value.notebook) ? value.notebook : {};
|
|
@@ -1820,7 +1928,7 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1820
1928
|
}
|
|
1821
1929
|
const next = `${JSON.stringify(value, null, 2)}
|
|
1822
1930
|
`;
|
|
1823
|
-
const current = existsSync6(action.path) ?
|
|
1931
|
+
const current = existsSync6(action.path) ? readFileSync8(action.path, "utf8") : void 0;
|
|
1824
1932
|
if (current !== next) {
|
|
1825
1933
|
writeFileSync4(action.path, next, "utf8");
|
|
1826
1934
|
changedFiles.push(action.path);
|
|
@@ -1838,8 +1946,8 @@ async function executeProjectInitPlan(plan, options = {}) {
|
|
|
1838
1946
|
logs.push(...outcome.logs);
|
|
1839
1947
|
if (!outcome.ok) {
|
|
1840
1948
|
errors.push(outcome.error ?? `ticket-provider: ${action.provider} board provisioning failed`);
|
|
1841
|
-
} else if (outcome.boardId) {
|
|
1842
|
-
changedFiles.push(...linkTicketProviderBoard(plan, action, outcome.boardId));
|
|
1949
|
+
} else if (outcome.boardId && outcome.identifier) {
|
|
1950
|
+
changedFiles.push(...linkTicketProviderBoard(plan, action, outcome.boardId, outcome.identifier));
|
|
1843
1951
|
pendingRegistryAction ??= {
|
|
1844
1952
|
kind: "registry.upsert",
|
|
1845
1953
|
registryPath: plan.registryPath,
|
|
@@ -1905,7 +2013,7 @@ function getProject(registry, slug) {
|
|
|
1905
2013
|
return project;
|
|
1906
2014
|
}
|
|
1907
2015
|
function buildCommonProjectCopierAction(input) {
|
|
1908
|
-
const templateDir =
|
|
2016
|
+
const templateDir = join9(input.pjanglerRoot, "templates", "commonproject");
|
|
1909
2017
|
const data = {
|
|
1910
2018
|
project_name: input.projectName,
|
|
1911
2019
|
project_description: input.projectDescription ?? "",
|
|
@@ -1932,10 +2040,10 @@ function buildCommonProjectCopierAction(input) {
|
|
|
1932
2040
|
};
|
|
1933
2041
|
}
|
|
1934
2042
|
function resolvePjanglerRoot() {
|
|
1935
|
-
let dir =
|
|
1936
|
-
while (dir !==
|
|
1937
|
-
if (existsSync6(
|
|
1938
|
-
dir =
|
|
2043
|
+
let dir = dirname6(fileURLToPath3(import.meta.url));
|
|
2044
|
+
while (dir !== dirname6(dir)) {
|
|
2045
|
+
if (existsSync6(join9(dir, "package.json")) && existsSync6(join9(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
2046
|
+
dir = dirname6(dir);
|
|
1939
2047
|
}
|
|
1940
2048
|
return resolve5(process.cwd());
|
|
1941
2049
|
}
|
|
@@ -1949,6 +2057,10 @@ function validateNoDuplicateProject(registry, project, overwrite) {
|
|
|
1949
2057
|
if (resolve5(existing.repo_path) === resolve5(project.repo_path)) {
|
|
1950
2058
|
throw new Error(`Project repo_path already registered by ${slug}: ${project.repo_path}`);
|
|
1951
2059
|
}
|
|
2060
|
+
if (ticketProviderScope(existing.ticket_provider) !== ticketProviderScope(project.ticket_provider)) continue;
|
|
2061
|
+
if (existing.ticket_provider.board_id?.trim() && existing.ticket_provider.board_id === project.ticket_provider.board_id) {
|
|
2062
|
+
throw new Error(`Project board_id already registered by ${slug}: ${project.ticket_provider.board_id}`);
|
|
2063
|
+
}
|
|
1952
2064
|
if (existing.ticket_provider.identifier && existing.ticket_provider.identifier.toUpperCase() === project.ticket_provider.identifier?.toUpperCase()) {
|
|
1953
2065
|
throw new Error(`Project identifier already registered by ${slug}: ${project.ticket_provider.identifier}`);
|
|
1954
2066
|
}
|
|
@@ -1964,6 +2076,28 @@ function validateProjectRecord(project, key) {
|
|
|
1964
2076
|
if (!project.repo_path) throw new Error(`Project ${key} missing repo_path`);
|
|
1965
2077
|
if (!Array.isArray(project.source_artifacts)) throw new Error(`Project ${key} source_artifacts must be a list`);
|
|
1966
2078
|
if (!isRecord(project.ticket_provider)) throw new Error(`Project ${key} ticket_provider must be a mapping`);
|
|
2079
|
+
const provider = project.ticket_provider;
|
|
2080
|
+
if (provider.state !== void 0 && !TICKET_PROVIDER_STATES.includes(provider.state)) {
|
|
2081
|
+
throw new Error(`Project ${key} ticket_provider.state must be one of ${TICKET_PROVIDER_STATES.join(" | ")}; got ${JSON.stringify(provider.state)}`);
|
|
2082
|
+
}
|
|
2083
|
+
if (provider.identifier_source !== void 0 && !PROJECT_IDENTIFIER_SOURCES.includes(provider.identifier_source)) {
|
|
2084
|
+
throw new Error(`Project ${key} ticket_provider.identifier_source must be one of ${PROJECT_IDENTIFIER_SOURCES.join(" | ")}; got ${JSON.stringify(provider.identifier_source)}`);
|
|
2085
|
+
}
|
|
2086
|
+
if (provider.state === "linked" && !(provider.board_id && provider.board_confirmed_at)) {
|
|
2087
|
+
throw new Error(
|
|
2088
|
+
`Project ${key} ticket_provider.state is "linked" but its board binding is not provider-confirmed (board_id=${JSON.stringify(provider.board_id ?? "")}, board_confirmed_at=${JSON.stringify(provider.board_confirmed_at ?? "")}). Run \`${IDENTIFIER_REPAIR_COMMAND}\`.`
|
|
2089
|
+
);
|
|
2090
|
+
}
|
|
2091
|
+
if (provider.identifier_source === "provider" && !(provider.identifier && provider.identifier_fetched_at)) {
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
`Project ${key} ticket_provider.identifier_source is "provider" but no identifier was read back (identifier=${JSON.stringify(provider.identifier ?? "")}, identifier_fetched_at=${JSON.stringify(provider.identifier_fetched_at ?? "")}). Run \`${IDENTIFIER_REPAIR_COMMAND}\`.`
|
|
2094
|
+
);
|
|
2095
|
+
}
|
|
2096
|
+
if (provider.state === "linked" && providerAssignsIdentifiers(provider.type) && provider.identifier_source !== "provider") {
|
|
2097
|
+
throw new Error(
|
|
2098
|
+
`Project ${key} ticket_provider.state is "linked" on ${provider.type}, which assigns its own identifiers, but identifier_source=${JSON.stringify(provider.identifier_source ?? "")} (identifier=${JSON.stringify(provider.identifier ?? "")}). Run \`${IDENTIFIER_REPAIR_COMMAND}\`.`
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
1967
2101
|
if (!isRecord(project.agents)) throw new Error(`Project ${key} agents must be a mapping`);
|
|
1968
2102
|
if (project.notebook !== void 0) {
|
|
1969
2103
|
if (!isRecord(project.notebook)) throw new Error(`Project ${key} notebook must be a mapping`);
|
|
@@ -1985,13 +2119,13 @@ function validateProjectRecord(project, key) {
|
|
|
1985
2119
|
}
|
|
1986
2120
|
function expandHome(path) {
|
|
1987
2121
|
if (path === "~") return homedir4();
|
|
1988
|
-
if (path.startsWith("~/")) return
|
|
2122
|
+
if (path.startsWith("~/")) return join9(homedir4(), path.slice(2));
|
|
1989
2123
|
return path;
|
|
1990
2124
|
}
|
|
1991
2125
|
function isRecord(value) {
|
|
1992
2126
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1993
2127
|
}
|
|
1994
|
-
var PROJECT_REGISTRY_ENV, PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, BOARD_URL_DEPRECATION_WARNING, DEFAULT_SOURCE_SKILL_ROOTS, PROJECT_REGISTRY_OWNED_KEYS, PROJECT_NOTEBOOK_OWNED_KEYS, TICKET_PROVIDER_OWNED_KEYS, GLOBAL_NOTEBOOK_OWNED_KEYS, GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS, GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS, GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS, GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS, SAFE_PATH_SEGMENT;
|
|
2128
|
+
var PROJECT_REGISTRY_ENV, PROJECT_SOURCE_SKILL_ROOTS_ENV, TICKET_PROVIDER_ADAPTERS_ENV, PROJECT_REGISTRY_SCHEMA_VERSION, DEFAULT_NEW_PROJECT_STATUS, BOARD_URL_DEPRECATION_WARNING, PROJECT_IDENTIFIER_SOURCES, IDENTIFIER_ASSIGNING_PROVIDERS, TICKET_PROVIDER_STATES, IDENTIFIER_REPAIR_COMMAND, DEFAULT_SOURCE_SKILL_ROOTS, PROJECT_REGISTRY_OWNED_KEYS, PROJECT_NOTEBOOK_OWNED_KEYS, TICKET_PROVIDER_OWNED_KEYS, GLOBAL_NOTEBOOK_OWNED_KEYS, GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS, GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS, GLOBAL_NOTEBOOK_LIMITS_OWNED_KEYS, GLOBAL_NOTEBOOK_SUMMARIZER_OWNED_KEYS, SAFE_PATH_SEGMENT;
|
|
1995
2129
|
var init_project = __esm({
|
|
1996
2130
|
"src/project/index.ts"() {
|
|
1997
2131
|
"use strict";
|
|
@@ -2000,16 +2134,21 @@ var init_project = __esm({
|
|
|
2000
2134
|
init_tree_diff();
|
|
2001
2135
|
init_preflight();
|
|
2002
2136
|
init_RegistryStore();
|
|
2137
|
+
init_boardQuery();
|
|
2003
2138
|
PROJECT_REGISTRY_ENV = "PJ_PROJECT_REGISTRY";
|
|
2004
2139
|
PROJECT_SOURCE_SKILL_ROOTS_ENV = "PJ_SOURCE_SKILL_ROOTS";
|
|
2005
2140
|
TICKET_PROVIDER_ADAPTERS_ENV = "PJ_TICKET_PROVIDER_ADAPTERS";
|
|
2006
2141
|
PROJECT_REGISTRY_SCHEMA_VERSION = 1;
|
|
2007
2142
|
DEFAULT_NEW_PROJECT_STATUS = "active";
|
|
2008
2143
|
BOARD_URL_DEPRECATION_WARNING = "boardUrl is deprecated and ignored; board URLs are derived at runtime and are never persisted.";
|
|
2144
|
+
PROJECT_IDENTIFIER_SOURCES = ["provider", "proposed"];
|
|
2145
|
+
IDENTIFIER_ASSIGNING_PROVIDERS = ["plane", "linear"];
|
|
2146
|
+
TICKET_PROVIDER_STATES = ["planned", "linked", "skipped"];
|
|
2147
|
+
IDENTIFIER_REPAIR_COMMAND = "pj project identity --all --apply";
|
|
2009
2148
|
DEFAULT_SOURCE_SKILL_ROOTS = [
|
|
2010
2149
|
"/home/delorenj/code/skillex/all-skills",
|
|
2011
|
-
|
|
2012
|
-
|
|
2150
|
+
join9(homedir4(), ".agents", "skills"),
|
|
2151
|
+
join9(homedir4(), ".codex", "skills")
|
|
2013
2152
|
];
|
|
2014
2153
|
PROJECT_REGISTRY_OWNED_KEYS = [
|
|
2015
2154
|
"name",
|
|
@@ -2027,7 +2166,7 @@ var init_project = __esm({
|
|
|
2027
2166
|
"updated_at"
|
|
2028
2167
|
];
|
|
2029
2168
|
PROJECT_NOTEBOOK_OWNED_KEYS = ["state", "notebook_id", "notebook_name", "overview_note_id", "blocked_reason"];
|
|
2030
|
-
TICKET_PROVIDER_OWNED_KEYS = ["type", "workspace", "identifier", "board_id", "board_url", "state"];
|
|
2169
|
+
TICKET_PROVIDER_OWNED_KEYS = ["type", "workspace", "identifier", "identifier_source", "identifier_fetched_at", "board_id", "board_confirmed_at", "board_url", "state"];
|
|
2031
2170
|
GLOBAL_NOTEBOOK_OWNED_KEYS = ["base_url", "auth", "defaults", "limits", "summarizer"];
|
|
2032
2171
|
GLOBAL_NOTEBOOK_AUTH_OWNED_KEYS = ["mode", "env_var"];
|
|
2033
2172
|
GLOBAL_NOTEBOOK_DEFAULTS_OWNED_KEYS = ["enabled", "session_start_enabled", "session_capture_enabled", "overview_max_chars", "documentation_globs", "overview_references", "excluded_globs"];
|
|
@@ -2040,21 +2179,21 @@ var init_project = __esm({
|
|
|
2040
2179
|
// src/notebook/config.ts
|
|
2041
2180
|
import { randomUUID } from "node:crypto";
|
|
2042
2181
|
import { isIP as isIP2 } from "node:net";
|
|
2043
|
-
import { closeSync as closeSync3, existsSync as
|
|
2044
|
-
import { dirname as
|
|
2182
|
+
import { closeSync as closeSync3, existsSync as existsSync14, fchmodSync as fchmodSync2, fsyncSync as fsyncSync2, lstatSync as lstatSync8, openSync as openSync3, readFileSync as readFileSync14, realpathSync as realpathSync5, renameSync as renameSync4, writeFileSync as writeFileSync8 } from "node:fs";
|
|
2183
|
+
import { dirname as dirname8, resolve as resolve7 } from "node:path";
|
|
2045
2184
|
function isRecord2(value) {
|
|
2046
2185
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
2047
2186
|
}
|
|
2048
2187
|
function realOrResolved(path) {
|
|
2049
2188
|
const absolute = resolve7(path);
|
|
2050
|
-
return
|
|
2189
|
+
return existsSync14(absolute) ? realpathSync5(absolute) : absolute;
|
|
2051
2190
|
}
|
|
2052
2191
|
function readManifest(repoPath) {
|
|
2053
2192
|
const path = resolve7(repoPath, ".project.json");
|
|
2054
|
-
if (!
|
|
2193
|
+
if (!existsSync14(path)) return null;
|
|
2055
2194
|
let parsed;
|
|
2056
2195
|
try {
|
|
2057
|
-
parsed = JSON.parse(
|
|
2196
|
+
parsed = JSON.parse(readFileSync14(path, "utf8"));
|
|
2058
2197
|
} catch {
|
|
2059
2198
|
throw new NotebookError("INVALID_INPUT", `${path} is not valid JSON`);
|
|
2060
2199
|
}
|
|
@@ -2279,7 +2418,7 @@ function requireRemoteNotebookConfig(config) {
|
|
|
2279
2418
|
function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
2280
2419
|
const changed = [];
|
|
2281
2420
|
const manifestPath = resolve7(resolved.project.repo_path, ".project.json");
|
|
2282
|
-
const manifestRaw =
|
|
2421
|
+
const manifestRaw = existsSync14(manifestPath) ? JSON.parse(readFileSync14(manifestPath, "utf8")) : {};
|
|
2283
2422
|
if (!isRecord2(manifestRaw)) throw new NotebookError("INVALID_INPUT", `${manifestPath} must contain a JSON object`);
|
|
2284
2423
|
validateManifestNotebookSurface(manifestRaw.notebook);
|
|
2285
2424
|
const manifestNotebook2 = isRecord2(manifestRaw.notebook) ? manifestRaw.notebook : {};
|
|
@@ -2296,14 +2435,14 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
|
2296
2435
|
};
|
|
2297
2436
|
const manifestText = `${JSON.stringify(manifestNext, null, 2)}
|
|
2298
2437
|
`;
|
|
2299
|
-
if (!
|
|
2438
|
+
if (!existsSync14(manifestPath) || readFileSync14(manifestPath, "utf8") !== manifestText) {
|
|
2300
2439
|
let mode = 420;
|
|
2301
|
-
if (
|
|
2302
|
-
const current2 =
|
|
2440
|
+
if (existsSync14(manifestPath)) {
|
|
2441
|
+
const current2 = lstatSync8(manifestPath);
|
|
2303
2442
|
if (!current2.isFile() || current2.isSymbolicLink()) throw new NotebookError("CONFLICT", `${manifestPath} must be a regular non-symlink file`);
|
|
2304
2443
|
mode = current2.mode & 511;
|
|
2305
2444
|
}
|
|
2306
|
-
const temp = resolve7(
|
|
2445
|
+
const temp = resolve7(dirname8(manifestPath), `.${process.pid}.${randomUUID()}.project.json.tmp`);
|
|
2307
2446
|
const fd = openSync3(temp, "wx", mode);
|
|
2308
2447
|
try {
|
|
2309
2448
|
writeFileSync8(fd, manifestText, "utf8");
|
|
@@ -2312,9 +2451,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
|
2312
2451
|
} finally {
|
|
2313
2452
|
closeSync3(fd);
|
|
2314
2453
|
}
|
|
2315
|
-
|
|
2454
|
+
renameSync4(temp, manifestPath);
|
|
2316
2455
|
try {
|
|
2317
|
-
const directory = openSync3(
|
|
2456
|
+
const directory = openSync3(dirname8(manifestPath), "r");
|
|
2318
2457
|
try {
|
|
2319
2458
|
fsyncSync2(directory);
|
|
2320
2459
|
} finally {
|
|
@@ -2326,9 +2465,9 @@ function persistProjectNotebookBinding(resolved, binding, policy) {
|
|
|
2326
2465
|
}
|
|
2327
2466
|
const current = resolved.project.notebook;
|
|
2328
2467
|
resolved.project.notebook = { ...isRecord2(current) ? current : { state: "planned" }, ...binding };
|
|
2329
|
-
const registryBefore =
|
|
2468
|
+
const registryBefore = existsSync14(resolved.registry_path) ? readFileSync14(resolved.registry_path, "utf8") : null;
|
|
2330
2469
|
saveProjectRegistry(resolved.registry, resolved.registry_path);
|
|
2331
|
-
const registryAfter =
|
|
2470
|
+
const registryAfter = existsSync14(resolved.registry_path) ? readFileSync14(resolved.registry_path, "utf8") : null;
|
|
2332
2471
|
if (registryBefore !== registryAfter) changed.push(resolved.registry_path);
|
|
2333
2472
|
return changed;
|
|
2334
2473
|
}
|
|
@@ -2474,26 +2613,26 @@ function truncateUtf8(value, maxBytes) {
|
|
|
2474
2613
|
}
|
|
2475
2614
|
return result2.join("");
|
|
2476
2615
|
}
|
|
2477
|
-
function noteDetail(
|
|
2478
|
-
const parsed = parseNoteEnvelope(
|
|
2616
|
+
function noteDetail(note, maxBytes) {
|
|
2617
|
+
const parsed = parseNoteEnvelope(note.content);
|
|
2479
2618
|
return {
|
|
2480
|
-
id:
|
|
2481
|
-
title:
|
|
2482
|
-
note_type:
|
|
2483
|
-
created_at:
|
|
2484
|
-
updated_at:
|
|
2485
|
-
content: truncateUtf8(parsed?.body ??
|
|
2619
|
+
id: note.id,
|
|
2620
|
+
title: note.title,
|
|
2621
|
+
note_type: note.note_type,
|
|
2622
|
+
created_at: note.created_at,
|
|
2623
|
+
updated_at: note.updated_at,
|
|
2624
|
+
content: truncateUtf8(parsed?.body ?? note.content, maxBytes)
|
|
2486
2625
|
};
|
|
2487
2626
|
}
|
|
2488
|
-
function noteSummary(
|
|
2489
|
-
const body = parseNoteEnvelope(
|
|
2627
|
+
function noteSummary(note, excerptMaxChars) {
|
|
2628
|
+
const body = parseNoteEnvelope(note.content)?.body ?? note.content;
|
|
2490
2629
|
const excerpt = Array.from(body.replace(/\s+/gu, " ").trim()).slice(0, excerptMaxChars).join("");
|
|
2491
2630
|
return {
|
|
2492
|
-
id:
|
|
2493
|
-
title:
|
|
2494
|
-
note_type:
|
|
2495
|
-
created_at:
|
|
2496
|
-
updated_at:
|
|
2631
|
+
id: note.id,
|
|
2632
|
+
title: note.title,
|
|
2633
|
+
note_type: note.note_type,
|
|
2634
|
+
created_at: note.created_at,
|
|
2635
|
+
updated_at: note.updated_at,
|
|
2497
2636
|
excerpt
|
|
2498
2637
|
};
|
|
2499
2638
|
}
|
|
@@ -2510,13 +2649,13 @@ function searchNotesLocally(notes, query, limit, excerptMaxChars) {
|
|
|
2510
2649
|
const queryTokens = tokenizeSearch(query);
|
|
2511
2650
|
if (!queryTokens.length) throw new NotebookError("INVALID_INPUT", "Search query must contain at least one letter or number");
|
|
2512
2651
|
if (!Number.isSafeInteger(limit) || limit < 1) throw new NotebookError("INVALID_INPUT", "Search limit must be a positive integer");
|
|
2513
|
-
const scored = notes.flatMap((
|
|
2514
|
-
const body = parseNoteEnvelope(
|
|
2515
|
-
const titleTokens = tokenizeSearch(
|
|
2652
|
+
const scored = notes.flatMap((note) => {
|
|
2653
|
+
const body = parseNoteEnvelope(note.content)?.body ?? note.content;
|
|
2654
|
+
const titleTokens = tokenizeSearch(note.title);
|
|
2516
2655
|
const bodyTokens = tokenizeSearch(body);
|
|
2517
2656
|
if (!queryTokens.every((token) => titleTokens.includes(token) || bodyTokens.includes(token))) return [];
|
|
2518
2657
|
const score = queryTokens.reduce((sum, token) => sum + 10 * countTokens(titleTokens, token) + countTokens(bodyTokens, token), 0);
|
|
2519
|
-
return [{ note
|
|
2658
|
+
return [{ note, body, score }];
|
|
2520
2659
|
});
|
|
2521
2660
|
const timestamp2 = (value) => {
|
|
2522
2661
|
const parsed = Date.parse(value ?? "");
|
|
@@ -2524,12 +2663,12 @@ function searchNotesLocally(notes, query, limit, excerptMaxChars) {
|
|
|
2524
2663
|
};
|
|
2525
2664
|
scored.sort((left, right) => right.score - left.score || timestamp2(right.note.updated_at) - timestamp2(left.note.updated_at) || left.note.id.localeCompare(right.note.id, "en"));
|
|
2526
2665
|
return {
|
|
2527
|
-
items: scored.slice(0, limit).map(({ note
|
|
2666
|
+
items: scored.slice(0, limit).map(({ note, body }) => {
|
|
2528
2667
|
const normalizedBody = body.replace(/\s+/gu, " ").trim().normalize("NFKC");
|
|
2529
2668
|
const lower = normalizedBody.toLocaleLowerCase("und");
|
|
2530
2669
|
const starts = queryTokens.map((token) => lower.indexOf(token)).filter((index) => index >= 0);
|
|
2531
2670
|
const start = starts.length ? Math.min(...starts) : 0;
|
|
2532
|
-
return { ...noteSummary(
|
|
2671
|
+
return { ...noteSummary(note, excerptMaxChars), excerpt: Array.from(normalizedBody.slice(start)).slice(0, excerptMaxChars).join("") };
|
|
2533
2672
|
}),
|
|
2534
2673
|
next_cursor: null,
|
|
2535
2674
|
query_tokens: queryTokens
|
|
@@ -2634,23 +2773,23 @@ import { createHash as createHash6, randomUUID as randomUUID3 } from "node:crypt
|
|
|
2634
2773
|
import {
|
|
2635
2774
|
closeSync as closeSync4,
|
|
2636
2775
|
constants as constants3,
|
|
2637
|
-
existsSync as
|
|
2776
|
+
existsSync as existsSync15,
|
|
2638
2777
|
fchmodSync as fchmodSync3,
|
|
2639
2778
|
fstatSync as fstatSync2,
|
|
2640
2779
|
fsyncSync as fsyncSync3,
|
|
2641
|
-
lstatSync as
|
|
2780
|
+
lstatSync as lstatSync9,
|
|
2642
2781
|
mkdirSync as mkdirSync6,
|
|
2643
2782
|
openSync as openSync4,
|
|
2644
2783
|
readSync,
|
|
2645
|
-
readdirSync as
|
|
2646
|
-
renameSync as
|
|
2647
|
-
unlinkSync as
|
|
2784
|
+
readdirSync as readdirSync7,
|
|
2785
|
+
renameSync as renameSync5,
|
|
2786
|
+
unlinkSync as unlinkSync4,
|
|
2648
2787
|
writeFileSync as writeFileSync9
|
|
2649
2788
|
} from "node:fs";
|
|
2650
2789
|
import { homedir as homedir6 } from "node:os";
|
|
2651
|
-
import { basename as
|
|
2790
|
+
import { basename as basename7, dirname as dirname9, join as join18, parse, relative as relative8, resolve as resolve8, sep as sep3 } from "node:path";
|
|
2652
2791
|
function notebookStateRoot(env2 = process.env) {
|
|
2653
|
-
const base = env2.XDG_STATE_HOME ||
|
|
2792
|
+
const base = env2.XDG_STATE_HOME || join18(env2.HOME || homedir6(), ".local", "state");
|
|
2654
2793
|
return resolve8(base, "pjangler", "notebook", NOTEBOOK_STATE_VERSION);
|
|
2655
2794
|
}
|
|
2656
2795
|
function assertDigest(value, label) {
|
|
@@ -2658,10 +2797,10 @@ function assertDigest(value, label) {
|
|
|
2658
2797
|
}
|
|
2659
2798
|
function projectStateDir(root, projectSlug) {
|
|
2660
2799
|
if (!/^[a-z0-9][a-z0-9._-]{0,127}$/iu.test(projectSlug)) throw new NotebookError("INVALID_INPUT", "Invalid project slug for Notebook state");
|
|
2661
|
-
return
|
|
2800
|
+
return join18(resolve8(root), "projects", sha256Hex(`pjangler-project-state-v1\0${projectSlug}`));
|
|
2662
2801
|
}
|
|
2663
2802
|
function assertContained(root, candidate) {
|
|
2664
|
-
const rel =
|
|
2803
|
+
const rel = relative8(resolve8(root), resolve8(candidate));
|
|
2665
2804
|
if (!rel || !rel.startsWith(`..${sep3}`) && rel !== ".." && !rel.startsWith(sep3)) return;
|
|
2666
2805
|
throw new NotebookError("INTERNAL_ERROR", "Notebook state path escaped its root");
|
|
2667
2806
|
}
|
|
@@ -2669,14 +2808,14 @@ function openPinnedDirectory(path, root, create) {
|
|
|
2669
2808
|
const absolute = resolve8(path);
|
|
2670
2809
|
const absoluteRoot = resolve8(root);
|
|
2671
2810
|
assertContained(absoluteRoot, absolute);
|
|
2672
|
-
if (!
|
|
2811
|
+
if (!existsSync15("/proc/self/fd")) throw new NotebookError("INTERNAL_ERROR", "Descriptor-pinned Notebook state requires procfs");
|
|
2673
2812
|
const parsed = parse(absolute);
|
|
2674
2813
|
let fd = openSync4(parsed.root, DIRECTORY_OPEN_FLAGS);
|
|
2675
2814
|
let cursor = parsed.root;
|
|
2676
2815
|
try {
|
|
2677
2816
|
for (const part of absolute.slice(parsed.root.length).split(sep3).filter(Boolean)) {
|
|
2678
2817
|
const child = `/proc/self/fd/${fd}/${part}`;
|
|
2679
|
-
cursor =
|
|
2818
|
+
cursor = join18(cursor, part);
|
|
2680
2819
|
let childFd;
|
|
2681
2820
|
try {
|
|
2682
2821
|
childFd = openSync4(child, DIRECTORY_OPEN_FLAGS);
|
|
@@ -2693,7 +2832,7 @@ function openPinnedDirectory(path, root, create) {
|
|
|
2693
2832
|
fd = childFd;
|
|
2694
2833
|
const stat = fstatSync2(fd);
|
|
2695
2834
|
if (!stat.isDirectory()) throw new NotebookError("INTERNAL_ERROR", "Notebook state path component is not a directory");
|
|
2696
|
-
if (cursor === absoluteRoot ||
|
|
2835
|
+
if (cursor === absoluteRoot || relative8(absoluteRoot, cursor).startsWith("..") === false) assertOwned(stat);
|
|
2697
2836
|
}
|
|
2698
2837
|
return fd;
|
|
2699
2838
|
} catch (error) {
|
|
@@ -2705,7 +2844,7 @@ function openPinnedDirectory(path, root, create) {
|
|
|
2705
2844
|
}
|
|
2706
2845
|
}
|
|
2707
2846
|
function pinnedLeaf(parentFd, path) {
|
|
2708
|
-
return `/proc/self/fd/${parentFd}/${
|
|
2847
|
+
return `/proc/self/fd/${parentFd}/${basename7(path)}`;
|
|
2709
2848
|
}
|
|
2710
2849
|
function assertOwned(stat) {
|
|
2711
2850
|
if (typeof process.getuid === "function" && stat.uid !== process.getuid()) {
|
|
@@ -2733,18 +2872,18 @@ function notebookStatePaths(root, projectSlug) {
|
|
|
2733
2872
|
return {
|
|
2734
2873
|
root: absoluteRoot,
|
|
2735
2874
|
project,
|
|
2736
|
-
baselines:
|
|
2737
|
-
claims:
|
|
2738
|
-
receipts:
|
|
2739
|
-
refusals:
|
|
2740
|
-
journals:
|
|
2741
|
-
locks:
|
|
2875
|
+
baselines: join18(project, "baselines"),
|
|
2876
|
+
claims: join18(project, "claims"),
|
|
2877
|
+
receipts: join18(project, "receipts"),
|
|
2878
|
+
refusals: join18(project, "refusals"),
|
|
2879
|
+
journals: join18(project, "journals"),
|
|
2880
|
+
locks: join18(project, "locks")
|
|
2742
2881
|
};
|
|
2743
2882
|
}
|
|
2744
2883
|
function ensureNotebookState(root, projectSlug) {
|
|
2745
2884
|
const paths = notebookStatePaths(root, projectSlug);
|
|
2746
2885
|
const { root: absoluteRoot, project } = paths;
|
|
2747
|
-
for (const path of [absoluteRoot,
|
|
2886
|
+
for (const path of [absoluteRoot, join18(absoluteRoot, "projects"), project, paths.baselines, paths.claims, paths.receipts, paths.refusals, paths.journals, paths.locks]) {
|
|
2748
2887
|
ensureDirectory(path, absoluteRoot);
|
|
2749
2888
|
}
|
|
2750
2889
|
return paths;
|
|
@@ -2766,7 +2905,7 @@ function fsyncDirectory2(path) {
|
|
|
2766
2905
|
function readStateDirectory(path, root) {
|
|
2767
2906
|
const fd = openPinnedDirectory(path, root, false);
|
|
2768
2907
|
try {
|
|
2769
|
-
return
|
|
2908
|
+
return readdirSync7(`/proc/self/fd/${fd}`, { withFileTypes: true });
|
|
2770
2909
|
} finally {
|
|
2771
2910
|
closeSync4(fd);
|
|
2772
2911
|
}
|
|
@@ -2777,7 +2916,7 @@ function readNotebookStateDirectory(path, root) {
|
|
|
2777
2916
|
}
|
|
2778
2917
|
function unlinkStateFile(path, root, allowMissing = false) {
|
|
2779
2918
|
assertContained(root, path);
|
|
2780
|
-
const parentFd = openPinnedDirectory(
|
|
2919
|
+
const parentFd = openPinnedDirectory(dirname9(path), root, false);
|
|
2781
2920
|
const target = pinnedLeaf(parentFd, path);
|
|
2782
2921
|
let fileFd;
|
|
2783
2922
|
try {
|
|
@@ -2792,7 +2931,7 @@ function unlinkStateFile(path, root, allowMissing = false) {
|
|
|
2792
2931
|
throw new NotebookError("CONFLICT", "Refusing to remove a suspect Notebook state entry; run pj notebook audit --json");
|
|
2793
2932
|
}
|
|
2794
2933
|
assertOwned(stat);
|
|
2795
|
-
|
|
2934
|
+
unlinkSync4(target);
|
|
2796
2935
|
fsyncSync3(parentFd);
|
|
2797
2936
|
return true;
|
|
2798
2937
|
} finally {
|
|
@@ -2801,8 +2940,8 @@ function unlinkStateFile(path, root, allowMissing = false) {
|
|
|
2801
2940
|
}
|
|
2802
2941
|
}
|
|
2803
2942
|
function renameStateFile(source, target, root) {
|
|
2804
|
-
if (
|
|
2805
|
-
const parentFd = openPinnedDirectory(
|
|
2943
|
+
if (dirname9(source) !== dirname9(target)) throw new NotebookError("INTERNAL_ERROR", "Notebook state rename crossed directories");
|
|
2944
|
+
const parentFd = openPinnedDirectory(dirname9(source), root, false);
|
|
2806
2945
|
const sourcePath = pinnedLeaf(parentFd, source);
|
|
2807
2946
|
const targetPath = pinnedLeaf(parentFd, target);
|
|
2808
2947
|
let sourceFd;
|
|
@@ -2812,12 +2951,12 @@ function renameStateFile(source, target, root) {
|
|
|
2812
2951
|
if (!stat.isFile() || (stat.mode & 511) !== 384) throw new NotebookError("CONFLICT", "Notebook state rename source has an integrity finding");
|
|
2813
2952
|
assertOwned(stat);
|
|
2814
2953
|
try {
|
|
2815
|
-
|
|
2954
|
+
lstatSync9(targetPath);
|
|
2816
2955
|
throw new NotebookError("CONFLICT", "Notebook state rename target already exists");
|
|
2817
2956
|
} catch (error) {
|
|
2818
2957
|
if (error.code !== "ENOENT") throw error;
|
|
2819
2958
|
}
|
|
2820
|
-
|
|
2959
|
+
renameSync5(sourcePath, targetPath);
|
|
2821
2960
|
fsyncSync3(parentFd);
|
|
2822
2961
|
} finally {
|
|
2823
2962
|
if (sourceFd !== void 0) closeSync4(sourceFd);
|
|
@@ -2826,11 +2965,11 @@ function renameStateFile(source, target, root) {
|
|
|
2826
2965
|
}
|
|
2827
2966
|
function atomicWriteJson(path, value, root, afterParentPinned) {
|
|
2828
2967
|
assertContained(root, path);
|
|
2829
|
-
ensureDirectory(
|
|
2968
|
+
ensureDirectory(dirname9(path), root);
|
|
2830
2969
|
const text2 = jsonLine(value);
|
|
2831
|
-
const parentFd = openPinnedDirectory(
|
|
2970
|
+
const parentFd = openPinnedDirectory(dirname9(path), root, false);
|
|
2832
2971
|
const target = pinnedLeaf(parentFd, path);
|
|
2833
|
-
const tempName = `.${
|
|
2972
|
+
const tempName = `.${basename7(path)}.${process.pid}.${randomUUID3()}.tmp`;
|
|
2834
2973
|
const temp = `/proc/self/fd/${parentFd}/${tempName}`;
|
|
2835
2974
|
let existingIdentity = null;
|
|
2836
2975
|
try {
|
|
@@ -2857,18 +2996,18 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
|
|
|
2857
2996
|
closeSync4(fd);
|
|
2858
2997
|
}
|
|
2859
2998
|
try {
|
|
2860
|
-
const current =
|
|
2999
|
+
const current = lstatSync9(target);
|
|
2861
3000
|
if (!existingIdentity || !current.isFile() || current.isSymbolicLink() || current.dev !== existingIdentity.dev || current.ino !== existingIdentity.ino) {
|
|
2862
3001
|
throw new NotebookError("CONFLICT", "Notebook state target changed during atomic update; preserving both entries for audit");
|
|
2863
3002
|
}
|
|
2864
3003
|
} catch (error) {
|
|
2865
3004
|
if (error.code !== "ENOENT" || existingIdentity) throw error;
|
|
2866
3005
|
}
|
|
2867
|
-
|
|
3006
|
+
renameSync5(temp, target);
|
|
2868
3007
|
fsyncSync3(parentFd);
|
|
2869
3008
|
} catch (error) {
|
|
2870
3009
|
try {
|
|
2871
|
-
|
|
3010
|
+
unlinkSync4(temp);
|
|
2872
3011
|
} catch {
|
|
2873
3012
|
}
|
|
2874
3013
|
throw error;
|
|
@@ -2879,7 +3018,7 @@ function atomicWriteJson(path, value, root, afterParentPinned) {
|
|
|
2879
3018
|
}
|
|
2880
3019
|
function exclusiveWrite(path, text2, root) {
|
|
2881
3020
|
assertContained(root, path);
|
|
2882
|
-
const parentFd = openPinnedDirectory(
|
|
3021
|
+
const parentFd = openPinnedDirectory(dirname9(path), root, false);
|
|
2883
3022
|
const target = pinnedLeaf(parentFd, path);
|
|
2884
3023
|
let fd;
|
|
2885
3024
|
try {
|
|
@@ -3036,7 +3175,7 @@ function parseRefusal(value) {
|
|
|
3036
3175
|
function safeReadJson(path, maxBytes) {
|
|
3037
3176
|
let parentFd;
|
|
3038
3177
|
try {
|
|
3039
|
-
parentFd = openPinnedDirectory(
|
|
3178
|
+
parentFd = openPinnedDirectory(dirname9(path), dirname9(path), false);
|
|
3040
3179
|
} catch {
|
|
3041
3180
|
return { reason: "non-regular", bytes: 0 };
|
|
3042
3181
|
}
|
|
@@ -3082,7 +3221,7 @@ function safeEntryId(kind, name) {
|
|
|
3082
3221
|
return `${kind}/${/^[a-zA-Z0-9._-]{1,160}$/u.test(name) ? name : sha256Hex(name).slice(0, 24)}`;
|
|
3083
3222
|
}
|
|
3084
3223
|
function readBaseline(path, maxBytes) {
|
|
3085
|
-
if (!
|
|
3224
|
+
if (!existsSync15(path)) return null;
|
|
3086
3225
|
const read = safeReadJson(path, maxBytes);
|
|
3087
3226
|
return read.value === void 0 ? null : parseBaseline(read.value);
|
|
3088
3227
|
}
|
|
@@ -3092,10 +3231,10 @@ function baselineReceiptByteCeiling(limits) {
|
|
|
3092
3231
|
function readSessionBaseline(root, projectSlug, sessionKey, limits) {
|
|
3093
3232
|
assertDigest(sessionKey, "session key");
|
|
3094
3233
|
const paths = ensureNotebookState(root, projectSlug);
|
|
3095
|
-
return readBaseline(
|
|
3234
|
+
return readBaseline(join18(paths.baselines, `${sessionKey}.json`), baselineReceiptByteCeiling(limits));
|
|
3096
3235
|
}
|
|
3097
3236
|
function acquireLock(paths, maxWaitMs) {
|
|
3098
|
-
const lock =
|
|
3237
|
+
const lock = join18(paths.locks, "admission.lock");
|
|
3099
3238
|
const deadline = Date.now() + Math.max(1, maxWaitMs);
|
|
3100
3239
|
const token = randomUUID3();
|
|
3101
3240
|
const record = () => ({
|
|
@@ -3125,7 +3264,7 @@ function acquireLock(paths, maxWaitMs) {
|
|
|
3125
3264
|
throw new NotebookError("CONFLICT", "Notebook state lock has an integrity finding; preserve it and run pj notebook audit --json");
|
|
3126
3265
|
}
|
|
3127
3266
|
if (Date.now() >= expiresAt) {
|
|
3128
|
-
const recovery =
|
|
3267
|
+
const recovery = join18(paths.locks, `recovery-${heldToken}.lock`);
|
|
3129
3268
|
const recoveryRecord = jsonLine({ schema_version: NOTEBOOK_SCHEMA_VERSION, stale_token: heldToken, recovery_token: token, expires_at: new Date(Date.now() + 5e3).toISOString() });
|
|
3130
3269
|
if (exclusiveWrite(recovery, recoveryRecord, paths.root)) {
|
|
3131
3270
|
try {
|
|
@@ -3133,7 +3272,7 @@ function acquireLock(paths, maxWaitMs) {
|
|
|
3133
3272
|
const currentToken = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? verify.value.token : null;
|
|
3134
3273
|
const currentExpiry = verify.value && typeof verify.value === "object" && !Array.isArray(verify.value) ? Date.parse(String(verify.value.expires_at ?? "")) : Number.NaN;
|
|
3135
3274
|
if (currentToken === heldToken && Number.isFinite(currentExpiry) && Date.now() >= currentExpiry) {
|
|
3136
|
-
const recovered =
|
|
3275
|
+
const recovered = join18(paths.locks, `.recovered-${heldToken}-${randomUUID3()}.json`);
|
|
3137
3276
|
renameStateFile(lock, recovered, paths.root);
|
|
3138
3277
|
unlinkStateFile(recovered, paths.root);
|
|
3139
3278
|
}
|
|
@@ -3199,7 +3338,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3199
3338
|
{ kind: "refusals", dir: paths.refusals, suffix: ".json", maxBytes: limits.receipt_max_bytes, parse: parseRefusal, key: (value) => value.session_key }
|
|
3200
3339
|
];
|
|
3201
3340
|
for (const specification of specifications) {
|
|
3202
|
-
if (!
|
|
3341
|
+
if (!existsSync15(specification.dir)) continue;
|
|
3203
3342
|
let entries;
|
|
3204
3343
|
try {
|
|
3205
3344
|
entries = readStateDirectory(specification.dir, paths.root);
|
|
@@ -3213,7 +3352,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3213
3352
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
|
|
3214
3353
|
continue;
|
|
3215
3354
|
}
|
|
3216
|
-
const read = safeReadJson(
|
|
3355
|
+
const read = safeReadJson(join18(specification.dir, entry.name), specification.maxBytes);
|
|
3217
3356
|
if (read.reason || read.value === void 0) {
|
|
3218
3357
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
|
|
3219
3358
|
continue;
|
|
@@ -3224,7 +3363,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3224
3363
|
if (!parsed || expectedName !== entry.name) addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "invalid-schema" }, read.bytes);
|
|
3225
3364
|
}
|
|
3226
3365
|
}
|
|
3227
|
-
if (
|
|
3366
|
+
if (existsSync15(paths.journals)) {
|
|
3228
3367
|
let entries;
|
|
3229
3368
|
try {
|
|
3230
3369
|
entries = readStateDirectory(paths.journals, paths.root);
|
|
@@ -3238,7 +3377,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3238
3377
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
|
|
3239
3378
|
continue;
|
|
3240
3379
|
}
|
|
3241
|
-
const read = safeReadJson(
|
|
3380
|
+
const read = safeReadJson(join18(paths.journals, entry.name), limits.receipt_max_bytes);
|
|
3242
3381
|
if (read.reason || read.value === void 0) {
|
|
3243
3382
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
|
|
3244
3383
|
continue;
|
|
@@ -3253,7 +3392,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3253
3392
|
if (reference.summary) scan.unresolvedJournals.push(reference.summary);
|
|
3254
3393
|
}
|
|
3255
3394
|
}
|
|
3256
|
-
if (
|
|
3395
|
+
if (existsSync15(paths.locks)) {
|
|
3257
3396
|
let entries;
|
|
3258
3397
|
try {
|
|
3259
3398
|
entries = readStateDirectory(paths.locks, paths.root);
|
|
@@ -3267,7 +3406,7 @@ function scanAuxiliaryState(paths, limits) {
|
|
|
3267
3406
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: "non-regular" });
|
|
3268
3407
|
continue;
|
|
3269
3408
|
}
|
|
3270
|
-
const read = safeReadJson(
|
|
3409
|
+
const read = safeReadJson(join18(paths.locks, entry.name), 8192);
|
|
3271
3410
|
if (read.reason || read.value === void 0) {
|
|
3272
3411
|
addBoundedIntegrity(scan, limits, { entry_id: entryId, reason: read.reason ?? "invalid-json" }, read.bytes);
|
|
3273
3412
|
continue;
|
|
@@ -3296,7 +3435,7 @@ function scanReceipts(paths, limits) {
|
|
|
3296
3435
|
unresolvedBytes += Math.max(0, knownBytes);
|
|
3297
3436
|
if (integrity.length < limits.integrity_max_entries) integrity.push(entry);
|
|
3298
3437
|
};
|
|
3299
|
-
if (!
|
|
3438
|
+
if (!existsSync15(paths.receipts)) return { receipts, unresolvedCount, unresolvedBytes, referencedSessions, integrity, integrityCount };
|
|
3300
3439
|
let entries;
|
|
3301
3440
|
try {
|
|
3302
3441
|
entries = readStateDirectory(paths.receipts, paths.root);
|
|
@@ -3306,7 +3445,7 @@ function scanReceipts(paths, limits) {
|
|
|
3306
3445
|
}
|
|
3307
3446
|
for (const entry of entries) {
|
|
3308
3447
|
const entryId = safeEntryId("receipts", entry.name);
|
|
3309
|
-
const path =
|
|
3448
|
+
const path = join18(paths.receipts, entry.name);
|
|
3310
3449
|
if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) {
|
|
3311
3450
|
addIntegrity({ entry_id: entryId, reason: "non-regular" });
|
|
3312
3451
|
continue;
|
|
@@ -3333,10 +3472,10 @@ function scanReceipts(paths, limits) {
|
|
|
3333
3472
|
function scanBaselines(paths, nowMs, limits, referenced) {
|
|
3334
3473
|
let current = 0;
|
|
3335
3474
|
let stale = 0;
|
|
3336
|
-
if (!
|
|
3475
|
+
if (!existsSync15(paths.baselines)) return { current, stale };
|
|
3337
3476
|
for (const entry of readStateDirectory(paths.baselines, paths.root)) {
|
|
3338
3477
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3339
|
-
const baseline = readBaseline(
|
|
3478
|
+
const baseline = readBaseline(join18(paths.baselines, entry.name), baselineReceiptByteCeiling(limits));
|
|
3340
3479
|
if (!baseline || referenced.has(baseline.session_key)) continue;
|
|
3341
3480
|
const expires = Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3;
|
|
3342
3481
|
if (nowMs >= expires) stale += 1;
|
|
@@ -3346,11 +3485,11 @@ function scanBaselines(paths, nowMs, limits, referenced) {
|
|
|
3346
3485
|
}
|
|
3347
3486
|
function scanRefusals(paths, nowMs, limits, scan) {
|
|
3348
3487
|
const result2 = [];
|
|
3349
|
-
if (!
|
|
3488
|
+
if (!existsSync15(paths.refusals)) return result2;
|
|
3350
3489
|
const entries = readStateDirectory(paths.refusals, paths.root).sort((a, b) => a.name.localeCompare(b.name, "en")).slice(0, limits.refusal_max_entries);
|
|
3351
3490
|
for (const entry of entries) {
|
|
3352
3491
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3353
|
-
const read = safeReadJson(
|
|
3492
|
+
const read = safeReadJson(join18(paths.refusals, entry.name), limits.receipt_max_bytes);
|
|
3354
3493
|
const marker = read.value === void 0 ? null : parseRefusal(read.value);
|
|
3355
3494
|
if (!marker || `${marker.session_key}.json` !== entry.name) continue;
|
|
3356
3495
|
if (nowMs >= Date.parse(marker.baseline_created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
|
|
@@ -3433,8 +3572,8 @@ function listCaptureReceipts(root, projectSlug, limits, state) {
|
|
|
3433
3572
|
function readCaptureReceipt(root, projectSlug, receiptId, limits) {
|
|
3434
3573
|
if (!RECEIPT_ID_RE.test(receiptId)) throw new NotebookError("INVALID_INPUT", "Invalid receipt ID");
|
|
3435
3574
|
const paths = notebookStatePaths(root, projectSlug);
|
|
3436
|
-
const path =
|
|
3437
|
-
if (!
|
|
3575
|
+
const path = join18(paths.receipts, `${receiptId}.json`);
|
|
3576
|
+
if (!existsSync15(path)) throw new NotebookError("NOT_FOUND", `Capture receipt not found: ${receiptId}`);
|
|
3438
3577
|
const read = safeReadJson(path, limits.receipt_max_bytes);
|
|
3439
3578
|
const receipt = read.value === void 0 ? null : parseReceipt(read.value);
|
|
3440
3579
|
if (!receipt || receipt.serialized_bytes !== read.bytes) throw new NotebookError("CONFLICT", `Capture receipt has an integrity finding: ${receiptId}`);
|
|
@@ -3446,7 +3585,7 @@ function writeReceipt(paths, receipt, limit) {
|
|
|
3446
3585
|
const bytes = Buffer.byteLength(text2, "utf8");
|
|
3447
3586
|
if (bytes > limit) throw new NotebookError("CONFLICT", "Receipt transition exceeds its per-receipt ceiling");
|
|
3448
3587
|
if (receipt.serialized_bytes === bytes) {
|
|
3449
|
-
atomicWriteJson(
|
|
3588
|
+
atomicWriteJson(join18(paths.receipts, `${receipt.receipt_id}.json`), receipt, paths.root);
|
|
3450
3589
|
return receipt;
|
|
3451
3590
|
}
|
|
3452
3591
|
receipt.serialized_bytes = bytes;
|
|
@@ -3585,25 +3724,25 @@ function pruneNotebookState(root, projectSlug, limits, now = /* @__PURE__ */ new
|
|
|
3585
3724
|
const successCutoff = now.getTime() - limits.receipt_succeeded_retention_days * 864e5;
|
|
3586
3725
|
for (const receipt of scan.receipts) {
|
|
3587
3726
|
if (receipt.state !== "succeeded" || Date.parse(receipt.updated_at) > successCutoff) continue;
|
|
3588
|
-
const path =
|
|
3727
|
+
const path = join18(paths.receipts, `${receipt.receipt_id}.json`);
|
|
3589
3728
|
unlinkStateFile(path, paths.root);
|
|
3590
|
-
removed.push(safeEntryId("receipts",
|
|
3729
|
+
removed.push(safeEntryId("receipts", basename7(path)));
|
|
3591
3730
|
}
|
|
3592
3731
|
const remaining = scanReceipts(paths, limits);
|
|
3593
3732
|
const remainingAuxiliary = scanAuxiliaryState(paths, limits);
|
|
3594
3733
|
const referenced = /* @__PURE__ */ new Set([...remaining.referencedSessions, ...remainingAuxiliary.referencedSessions]);
|
|
3595
3734
|
for (const entry of readStateDirectory(paths.baselines, paths.root)) {
|
|
3596
3735
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
3597
|
-
const path =
|
|
3736
|
+
const path = join18(paths.baselines, entry.name);
|
|
3598
3737
|
const baseline = readBaseline(path, baselineReceiptByteCeiling(limits));
|
|
3599
3738
|
if (!baseline || referenced.has(baseline.session_key)) continue;
|
|
3600
3739
|
if (now.getTime() < Date.parse(baseline.created_at) + limits.receiptless_session_retention_seconds * 1e3) continue;
|
|
3601
3740
|
unlinkStateFile(path, paths.root);
|
|
3602
3741
|
removed.push(safeEntryId("baselines", entry.name));
|
|
3603
|
-
const claim =
|
|
3604
|
-
if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims",
|
|
3605
|
-
const refusal =
|
|
3606
|
-
if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals",
|
|
3742
|
+
const claim = join18(paths.claims, `${baseline.session_key}.overview`);
|
|
3743
|
+
if (unlinkStateFile(claim, paths.root, true)) removed.push(safeEntryId("claims", basename7(claim)));
|
|
3744
|
+
const refusal = join18(paths.refusals, `${baseline.session_key}.json`);
|
|
3745
|
+
if (unlinkStateFile(refusal, paths.root, true)) removed.push(safeEntryId("refusals", basename7(refusal)));
|
|
3607
3746
|
}
|
|
3608
3747
|
fsyncDirectory2(paths.project);
|
|
3609
3748
|
return removed;
|
|
@@ -3703,9 +3842,9 @@ function parseScopedNoteListItem(value, noteMaxBytes) {
|
|
|
3703
3842
|
return parseNoteRecord(value, noteMaxBytes, true);
|
|
3704
3843
|
}
|
|
3705
3844
|
function parseNote(value, noteMaxBytes) {
|
|
3706
|
-
const
|
|
3707
|
-
if (
|
|
3708
|
-
return
|
|
3845
|
+
const note = parseNoteRecord(value, noteMaxBytes, false);
|
|
3846
|
+
if (note.content === null) throw new NotebookError("REMOTE_PROTOCOL_ERROR", "Open Notebook returned invalid note content");
|
|
3847
|
+
return note;
|
|
3709
3848
|
}
|
|
3710
3849
|
function errorForStatus(status, message) {
|
|
3711
3850
|
if (status === 400 || status === 422) return new NotebookError("INVALID_INPUT", message, false, { http_status: status, definitive_rejection: true });
|
|
@@ -3831,9 +3970,9 @@ var init_open_notebook_client = __esm({
|
|
|
3831
3970
|
}
|
|
3832
3971
|
async getOwnedNote(notebookId, noteId) {
|
|
3833
3972
|
const notes = await this.listNotes(notebookId);
|
|
3834
|
-
const
|
|
3835
|
-
if (!
|
|
3836
|
-
return
|
|
3973
|
+
const note = notes.find((item) => item.id === noteId);
|
|
3974
|
+
if (!note) throw new NotebookError("NOT_FOUND", `Note is not a proven member of the bound notebook: ${noteId}`);
|
|
3975
|
+
return note;
|
|
3837
3976
|
}
|
|
3838
3977
|
async updateOwnedNote(notebookId, noteId, input) {
|
|
3839
3978
|
if (input.content !== void 0 && Buffer.byteLength(input.content, "utf8") > this.config.limits.note_max_bytes) throw new NotebookError("INVALID_INPUT", "Note content exceeds the configured ceiling");
|
|
@@ -3917,8 +4056,8 @@ var init_open_notebook_client = __esm({
|
|
|
3917
4056
|
|
|
3918
4057
|
// src/notebook/git-evidence.ts
|
|
3919
4058
|
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
3920
|
-
import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync3, lstatSync as
|
|
3921
|
-
import { extname, join as
|
|
4059
|
+
import { closeSync as closeSync5, constants as constants4, fstatSync as fstatSync3, lstatSync as lstatSync10, openSync as openSync5, readSync as readSync2, realpathSync as realpathSync6 } from "node:fs";
|
|
4060
|
+
import { extname, join as join20, relative as relative9, resolve as resolve9, sep as sep4 } from "node:path";
|
|
3922
4061
|
function git(repo, args, maxBuffer = 4 * 1024 * 1024, timeout = 5e3) {
|
|
3923
4062
|
const result2 = spawnSync9("git", args, { cwd: repo, encoding: "utf8", maxBuffer, timeout, shell: false });
|
|
3924
4063
|
return { ok: result2.status === 0, stdout: result2.stdout ?? "" };
|
|
@@ -3986,19 +4125,19 @@ function validateCommittedGitRef(repoPath, gitRef) {
|
|
|
3986
4125
|
}
|
|
3987
4126
|
function safeRelative(repoPath, relativePath2) {
|
|
3988
4127
|
if (!relativePath2 || relativePath2.includes("\0") || relativePath2.startsWith("/") || relativePath2.split(/[\\/]/u).includes("..")) return null;
|
|
3989
|
-
const root =
|
|
4128
|
+
const root = realpathSync6(repoPath);
|
|
3990
4129
|
const candidate = resolve9(root, relativePath2);
|
|
3991
|
-
const rel =
|
|
4130
|
+
const rel = relative9(root, candidate);
|
|
3992
4131
|
if (rel === ".." || rel.startsWith(`..${sep4}`) || rel.startsWith(sep4)) return null;
|
|
3993
4132
|
return { root, candidate };
|
|
3994
4133
|
}
|
|
3995
4134
|
function hasSymlinkComponent(root, candidate) {
|
|
3996
|
-
const rel =
|
|
4135
|
+
const rel = relative9(root, candidate);
|
|
3997
4136
|
let cursor = root;
|
|
3998
4137
|
for (const part of rel.split(sep4).filter(Boolean)) {
|
|
3999
|
-
cursor =
|
|
4138
|
+
cursor = join20(cursor, part);
|
|
4000
4139
|
try {
|
|
4001
|
-
if (
|
|
4140
|
+
if (lstatSync10(cursor).isSymbolicLink()) return true;
|
|
4002
4141
|
} catch {
|
|
4003
4142
|
return true;
|
|
4004
4143
|
}
|
|
@@ -4020,11 +4159,11 @@ function readSafeEvidenceText(repoPath, relativePath2, maxBytes) {
|
|
|
4020
4159
|
if (before.size > maxBytes) return { status: "excluded", reason: "oversize" };
|
|
4021
4160
|
let physical;
|
|
4022
4161
|
try {
|
|
4023
|
-
physical =
|
|
4162
|
+
physical = realpathSync6(`/proc/self/fd/${fd}`);
|
|
4024
4163
|
} catch {
|
|
4025
4164
|
return { status: "excluded", reason: "unsafe-path" };
|
|
4026
4165
|
}
|
|
4027
|
-
const physicalRel =
|
|
4166
|
+
const physicalRel = relative9(safe.root, physical);
|
|
4028
4167
|
if (physicalRel === ".." || physicalRel.startsWith(`..${sep4}`) || physicalRel.startsWith(sep4)) return { status: "excluded", reason: "unsafe-path" };
|
|
4029
4168
|
const chunks = [];
|
|
4030
4169
|
let total = 0;
|
|
@@ -4139,10 +4278,10 @@ var init_git_evidence = __esm({
|
|
|
4139
4278
|
|
|
4140
4279
|
// src/notebook/remote-mutation-journal.ts
|
|
4141
4280
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
4142
|
-
import { join as
|
|
4281
|
+
import { join as join21 } from "node:path";
|
|
4143
4282
|
function journalPath(root, projectSlug, operationId) {
|
|
4144
4283
|
if (!/^[a-f0-9-]{16,64}$/iu.test(operationId)) throw new NotebookError("INVALID_INPUT", "Invalid remote mutation operation ID");
|
|
4145
|
-
return
|
|
4284
|
+
return join21(ensureNotebookState(root, projectSlug).journals, `${operationId}.json`);
|
|
4146
4285
|
}
|
|
4147
4286
|
function remoteMutationJournalPath(root, projectSlug, operationId) {
|
|
4148
4287
|
return journalPath(root, projectSlug, operationId);
|
|
@@ -4162,7 +4301,7 @@ function listRemoteMutationJournals(root, projectSlug) {
|
|
|
4162
4301
|
if (!entry.isFile() || entry.isSymbolicLink() || !/^[a-f0-9-]{16,64}\.json$/iu.test(entry.name)) {
|
|
4163
4302
|
throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
|
|
4164
4303
|
}
|
|
4165
|
-
const read = readNotebookStateJson(
|
|
4304
|
+
const read = readNotebookStateJson(join21(paths.journals, entry.name), paths.root, 65536);
|
|
4166
4305
|
const journal = read.value === void 0 ? null : parseRemoteMutationJournal(read.value);
|
|
4167
4306
|
if (!journal || `${journal.operation_id}.json` !== entry.name) {
|
|
4168
4307
|
throw new NotebookError("CONFLICT", `Remote mutation journal state-integrity finding: journals/${entry.name}`);
|
|
@@ -4452,7 +4591,7 @@ async function reconcileManagedNote(input) {
|
|
|
4452
4591
|
const reconcile = async () => {
|
|
4453
4592
|
input.beforeRemote?.();
|
|
4454
4593
|
const notes = await input.client.listNotes(input.notebookId);
|
|
4455
|
-
const candidates2 = notes.filter((
|
|
4594
|
+
const candidates2 = notes.filter((note) => parseNoteEnvelope(note.content)?.envelope.logical_id === input.logicalId);
|
|
4456
4595
|
journal = recordReconciliation({
|
|
4457
4596
|
stateRoot: input.stateRoot,
|
|
4458
4597
|
journal,
|
|
@@ -4798,10 +4937,10 @@ async function processClaimed(module, receipt, leaseUpdated) {
|
|
|
4798
4937
|
content: documentContent,
|
|
4799
4938
|
beforeRemote: renew
|
|
4800
4939
|
});
|
|
4801
|
-
const
|
|
4940
|
+
const note = upserted.note;
|
|
4802
4941
|
if (upserted.journal) journals.push(upserted.journal);
|
|
4803
4942
|
logicalIds.push(logicalId);
|
|
4804
|
-
remoteIds.push(
|
|
4943
|
+
remoteIds.push(note.id);
|
|
4805
4944
|
}
|
|
4806
4945
|
const summary = summarizeCapture(linked.config, {
|
|
4807
4946
|
documents: evidence.documents,
|
|
@@ -4928,8 +5067,8 @@ var init_capture = __esm({
|
|
|
4928
5067
|
});
|
|
4929
5068
|
|
|
4930
5069
|
// src/mcp-server.ts
|
|
4931
|
-
import { existsSync as
|
|
4932
|
-
import { basename as
|
|
5070
|
+
import { existsSync as existsSync23, statSync as statSync5 } from "node:fs";
|
|
5071
|
+
import { basename as basename9, dirname as dirname14, join as join30, resolve as resolve16 } from "node:path";
|
|
4933
5072
|
import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
4934
5073
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4935
5074
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5324,21 +5463,21 @@ function selectPackVersion(packDir) {
|
|
|
5324
5463
|
}
|
|
5325
5464
|
function stripTomlComment(line) {
|
|
5326
5465
|
let out = "";
|
|
5327
|
-
let
|
|
5466
|
+
let quote2 = null;
|
|
5328
5467
|
for (let index = 0; index < line.length; index += 1) {
|
|
5329
5468
|
const ch = line[index];
|
|
5330
|
-
if (
|
|
5331
|
-
if (ch === "\\" &&
|
|
5469
|
+
if (quote2) {
|
|
5470
|
+
if (ch === "\\" && quote2 === '"') {
|
|
5332
5471
|
out += ch + (line[index + 1] ?? "");
|
|
5333
5472
|
index += 1;
|
|
5334
5473
|
continue;
|
|
5335
5474
|
}
|
|
5336
|
-
if (ch ===
|
|
5475
|
+
if (ch === quote2) quote2 = null;
|
|
5337
5476
|
out += ch;
|
|
5338
5477
|
continue;
|
|
5339
5478
|
}
|
|
5340
5479
|
if (ch === '"' || ch === "'") {
|
|
5341
|
-
|
|
5480
|
+
quote2 = ch;
|
|
5342
5481
|
out += ch;
|
|
5343
5482
|
continue;
|
|
5344
5483
|
}
|
|
@@ -5349,15 +5488,15 @@ function stripTomlComment(line) {
|
|
|
5349
5488
|
}
|
|
5350
5489
|
function bracketDepth(text2) {
|
|
5351
5490
|
let depth = 0;
|
|
5352
|
-
let
|
|
5491
|
+
let quote2 = null;
|
|
5353
5492
|
for (let index = 0; index < text2.length; index += 1) {
|
|
5354
5493
|
const ch = text2[index];
|
|
5355
|
-
if (
|
|
5356
|
-
if (ch === "\\" &&
|
|
5357
|
-
else if (ch ===
|
|
5494
|
+
if (quote2) {
|
|
5495
|
+
if (ch === "\\" && quote2 === '"') index += 1;
|
|
5496
|
+
else if (ch === quote2) quote2 = null;
|
|
5358
5497
|
continue;
|
|
5359
5498
|
}
|
|
5360
|
-
if (ch === '"' || ch === "'")
|
|
5499
|
+
if (ch === '"' || ch === "'") quote2 = ch;
|
|
5361
5500
|
else if (ch === "[") depth += 1;
|
|
5362
5501
|
else if (ch === "]") depth -= 1;
|
|
5363
5502
|
}
|
|
@@ -6033,6 +6172,8 @@ function discoverRoles(repoRoot) {
|
|
|
6033
6172
|
ticketProviderIdentifier: yamlGet(text2, "plane.identifier"),
|
|
6034
6173
|
bloodbankEnabled: yamlGet(text2, "bloodbank.enabled"),
|
|
6035
6174
|
deploymentSystemd: yamlGet(text2, "deployment.systemd"),
|
|
6175
|
+
serviceStateGateway: yamlGet(text2, "service_state.gateway"),
|
|
6176
|
+
serviceStateHeartbeat: yamlGet(text2, "service_state.heartbeat"),
|
|
6036
6177
|
legacyReconcileEnabled: yamlGet(text2, "reconcile.enabled"),
|
|
6037
6178
|
legacyReconcileGraceHours: yamlGet(text2, "reconcile.grace_hours"),
|
|
6038
6179
|
legacyReconcileAutoReview: yamlGet(text2, "reconcile.auto_review"),
|
|
@@ -6920,13 +7061,8 @@ ${block}
|
|
|
6920
7061
|
`;
|
|
6921
7062
|
}
|
|
6922
7063
|
var BASE_MISE_PATH_ENTRIES = [".mise/scripts", "agents/hermes/pm"];
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
const required = [...BASE_MISE_PATH_ENTRIES];
|
|
6926
|
-
for (const candidate of CONDITIONAL_HERMES_PATHS) {
|
|
6927
|
-
if (existsSync2(join3(ctx.repoRoot, candidate)) && !required.includes(candidate)) required.push(candidate);
|
|
6928
|
-
}
|
|
6929
|
-
return required;
|
|
7064
|
+
function requiredMisePathEntries(_ctx) {
|
|
7065
|
+
return [...BASE_MISE_PATH_ENTRIES];
|
|
6930
7066
|
}
|
|
6931
7067
|
function upsertMisePath(text2, required = BASE_MISE_PATH_ENTRIES) {
|
|
6932
7068
|
const render = (values) => `_.path = [${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
@@ -7785,6 +7921,94 @@ function checkUnit(unit) {
|
|
|
7785
7921
|
const active = systemctlUser(["is-active", unit]).ok;
|
|
7786
7922
|
return { enabled: enabled2, active };
|
|
7787
7923
|
}
|
|
7924
|
+
function persistRoleServiceState(role, updates) {
|
|
7925
|
+
try {
|
|
7926
|
+
const stat = lstatSync2(role.roleYamlPath);
|
|
7927
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
7928
|
+
return { changed: false, error: `refusing unsafe role manifest ${role.roleYamlPath}` };
|
|
7929
|
+
}
|
|
7930
|
+
const current = readFileSync2(role.roleYamlPath, "utf8");
|
|
7931
|
+
const document = YAML.parseDocument(current);
|
|
7932
|
+
if (document.errors.length) throw document.errors[0];
|
|
7933
|
+
const serviceState = document.get("service_state", true);
|
|
7934
|
+
if (serviceState !== void 0 && serviceState !== null && !YAML.isMap(serviceState)) {
|
|
7935
|
+
return { changed: false, error: `${role.roleYamlPath} service_state must be a YAML mapping` };
|
|
7936
|
+
}
|
|
7937
|
+
for (const [leaf, value] of Object.entries(updates)) {
|
|
7938
|
+
document.setIn(["service_state", leaf], value);
|
|
7939
|
+
}
|
|
7940
|
+
const next = String(document);
|
|
7941
|
+
if (next === current) return { changed: false };
|
|
7942
|
+
const transaction = mkdtempSync(join3(dirname2(role.roleYamlPath), ".pjangler-role-state-"));
|
|
7943
|
+
try {
|
|
7944
|
+
atomicWriteBuffer(
|
|
7945
|
+
role.roleYamlPath,
|
|
7946
|
+
Buffer.from(next),
|
|
7947
|
+
Number(stat.mode) & 511,
|
|
7948
|
+
join3(transaction, "role.yaml")
|
|
7949
|
+
);
|
|
7950
|
+
} finally {
|
|
7951
|
+
rmSync(transaction, { recursive: true, force: true });
|
|
7952
|
+
}
|
|
7953
|
+
return { changed: true };
|
|
7954
|
+
} catch (error) {
|
|
7955
|
+
return { changed: false, error: error instanceof Error ? error.message : String(error) };
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
function reconcileHermesRoleUnits(ctx, role, changedFiles, details) {
|
|
7959
|
+
const gatewayUnit = `hermes-${role.agentId}-gateway.service`;
|
|
7960
|
+
const heartbeatUnit = `hermes-${role.agentId}-heartbeat.timer`;
|
|
7961
|
+
const gatewayDeferred = role.serviceStateGateway === "deferred";
|
|
7962
|
+
const stateUpdates = {};
|
|
7963
|
+
if (role.serviceStateHeartbeat !== "active") stateUpdates.heartbeat = "active";
|
|
7964
|
+
if (!gatewayDeferred && role.serviceStateGateway !== "active") stateUpdates.gateway = "active";
|
|
7965
|
+
if (ctx.dryRun) {
|
|
7966
|
+
details.push("would run: systemctl --user daemon-reload");
|
|
7967
|
+
details.push(`would run: systemctl --user enable --now ${heartbeatUnit}`);
|
|
7968
|
+
details.push(`would run: systemctl --user ${gatewayDeferred ? "disable" : "enable"} --now ${gatewayUnit}`);
|
|
7969
|
+
if (Object.keys(stateUpdates).length) {
|
|
7970
|
+
if (!changedFiles.includes(role.roleYamlPath)) changedFiles.push(role.roleYamlPath);
|
|
7971
|
+
details.push(`would atomically record verified service_state in ${relative2(ctx.repoRoot, role.roleYamlPath)}`);
|
|
7972
|
+
}
|
|
7973
|
+
return true;
|
|
7974
|
+
}
|
|
7975
|
+
const reload = systemctlUser(["daemon-reload"]);
|
|
7976
|
+
if (!reload.ok) {
|
|
7977
|
+
details.push(`script failed: systemctl --user daemon-reload: ${reload.stderr || reload.stdout || "unknown error"}`);
|
|
7978
|
+
return false;
|
|
7979
|
+
}
|
|
7980
|
+
const heartbeat = systemctlUser(["enable", "--now", heartbeatUnit]);
|
|
7981
|
+
const gateway = systemctlUser([gatewayDeferred ? "disable" : "enable", "--now", gatewayUnit]);
|
|
7982
|
+
if (!heartbeat.ok) {
|
|
7983
|
+
details.push(`script failed: could not enable ${heartbeatUnit}: ${heartbeat.stderr || heartbeat.stdout || "unknown error"}`);
|
|
7984
|
+
}
|
|
7985
|
+
if (!gateway.ok) {
|
|
7986
|
+
details.push(`script failed: could not ${gatewayDeferred ? "disable" : "enable"} ${gatewayUnit}: ${gateway.stderr || gateway.stdout || "unknown error"}`);
|
|
7987
|
+
}
|
|
7988
|
+
if (!heartbeat.ok || !gateway.ok) return false;
|
|
7989
|
+
const heartbeatState = checkUnit(heartbeatUnit);
|
|
7990
|
+
const gatewayState = checkUnit(gatewayUnit);
|
|
7991
|
+
const heartbeatHealthy = heartbeatState.enabled && heartbeatState.active;
|
|
7992
|
+
const gatewayHealthy = gatewayDeferred ? !gatewayState.enabled && !gatewayState.active : gatewayState.enabled && gatewayState.active;
|
|
7993
|
+
if (!heartbeatHealthy) {
|
|
7994
|
+
details.push(`script failed: ${heartbeatUnit} did not become enabled+active after systemctl reported success`);
|
|
7995
|
+
}
|
|
7996
|
+
if (!gatewayHealthy) {
|
|
7997
|
+
details.push(`script failed: ${gatewayUnit} did not become ${gatewayDeferred ? "disabled+inactive" : "enabled+active"} after systemctl reported success`);
|
|
7998
|
+
}
|
|
7999
|
+
if (!heartbeatHealthy || !gatewayHealthy) return false;
|
|
8000
|
+
const persisted = persistRoleServiceState(role, stateUpdates);
|
|
8001
|
+
if (persisted.error) {
|
|
8002
|
+
details.push(`script failed: could not update ${relative2(ctx.repoRoot, role.roleYamlPath)}: ${persisted.error}`);
|
|
8003
|
+
return false;
|
|
8004
|
+
}
|
|
8005
|
+
if (persisted.changed) {
|
|
8006
|
+
if (!changedFiles.includes(role.roleYamlPath)) changedFiles.push(role.roleYamlPath);
|
|
8007
|
+
details.push(`atomically recorded verified service_state in ${relative2(ctx.repoRoot, role.roleYamlPath)}`);
|
|
8008
|
+
}
|
|
8009
|
+
details.push(`verified ${heartbeatUnit} enabled+active and ${gatewayUnit} ${gatewayDeferred ? "disabled+inactive" : "enabled+active"}`);
|
|
8010
|
+
return true;
|
|
8011
|
+
}
|
|
7788
8012
|
var BMAD_NPM_PACKAGE = "bmad-method";
|
|
7789
8013
|
var BMAD_INSTALLER_VERSION = "6.11.1-next.1";
|
|
7790
8014
|
var BMAD_TARGET_CHANNEL = "next";
|
|
@@ -9962,20 +10186,52 @@ function createHermesChecks() {
|
|
|
9962
10186
|
}
|
|
9963
10187
|
const probe = systemctlUser(["is-system-running"]);
|
|
9964
10188
|
if (!probe.ok && !/running|degraded|starting|maintenance/.test(`${probe.stdout} ${probe.stderr}`)) {
|
|
9965
|
-
|
|
10189
|
+
const sysDir2 = join3(ctx.homeDir, ".config", "systemd", "user");
|
|
10190
|
+
const details2 = [];
|
|
10191
|
+
for (const role of requiredRoles) {
|
|
10192
|
+
const gateway = role.serviceStateGateway || "active";
|
|
10193
|
+
const heartbeat = role.serviceStateHeartbeat || "active";
|
|
10194
|
+
for (const unit of [`hermes-${role.agentId}-gateway.service`, `hermes-${role.agentId}-heartbeat.timer`]) {
|
|
10195
|
+
if (!existsSync2(join3(sysDir2, unit))) details2.push(`${unit} should be installed`);
|
|
10196
|
+
}
|
|
10197
|
+
if (heartbeat !== "installed") details2.push(`${role.agentId} heartbeat should record installed while systemd --user is unavailable (got ${heartbeat})`);
|
|
10198
|
+
if (gateway !== "installed" && gateway !== "deferred") details2.push(`${role.agentId} gateway should record installed or deferred while systemd --user is unavailable (got ${gateway})`);
|
|
10199
|
+
}
|
|
10200
|
+
return {
|
|
10201
|
+
id: "systemd.sentinel",
|
|
10202
|
+
title: "Hermes systemd/sentinel units enabled + active",
|
|
10203
|
+
status: details2.length ? "warn" : "pass",
|
|
10204
|
+
summary: details2.length ? "systemd --user unavailable and installed-state metadata is incomplete" : "Hermes units are installed; activation is deferred because systemd --user is unavailable",
|
|
10205
|
+
details: details2,
|
|
10206
|
+
fixable: false
|
|
10207
|
+
};
|
|
9966
10208
|
}
|
|
9967
10209
|
const details = [];
|
|
10210
|
+
const sysDir = join3(ctx.homeDir, ".config", "systemd", "user");
|
|
9968
10211
|
for (const role of requiredRoles) {
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
10212
|
+
const gatewayUnit = `hermes-${role.agentId}-gateway.service`;
|
|
10213
|
+
const heartbeatUnit = `hermes-${role.agentId}-heartbeat.timer`;
|
|
10214
|
+
const gatewayState = role.serviceStateGateway || "active";
|
|
10215
|
+
const heartbeatState = role.serviceStateHeartbeat || "active";
|
|
10216
|
+
for (const unit of [gatewayUnit, heartbeatUnit]) {
|
|
10217
|
+
if (!existsSync2(join3(sysDir, unit))) details.push(`${unit} should be installed`);
|
|
10218
|
+
}
|
|
10219
|
+
const heartbeat = checkUnit(heartbeatUnit);
|
|
10220
|
+
if (heartbeatState !== "active" || !heartbeat.enabled || !heartbeat.active) {
|
|
10221
|
+
details.push(`${heartbeatUnit} should be enabled+active (manifest: ${heartbeatState || "missing"})`);
|
|
10222
|
+
}
|
|
10223
|
+
const gateway = checkUnit(gatewayUnit);
|
|
10224
|
+
if (gatewayState === "deferred") {
|
|
10225
|
+
if (gateway.enabled || gateway.active) details.push(`${gatewayUnit} is deferred and should be disabled+inactive`);
|
|
10226
|
+
} else if (gatewayState !== "active" || !gateway.enabled || !gateway.active) {
|
|
10227
|
+
details.push(`${gatewayUnit} should be enabled+active (manifest: ${gatewayState || "missing"})`);
|
|
9972
10228
|
}
|
|
9973
10229
|
}
|
|
9974
10230
|
return {
|
|
9975
10231
|
id: "systemd.sentinel",
|
|
9976
10232
|
title: "Hermes systemd/sentinel units enabled + active",
|
|
9977
10233
|
status: details.length === 0 ? "pass" : "fail",
|
|
9978
|
-
summary: details.length === 0 ? "Hermes user units
|
|
10234
|
+
summary: details.length === 0 ? "Hermes user units match each role's declared service state" : `${details.length} systemd parity issue(s) detected`,
|
|
9979
10235
|
details,
|
|
9980
10236
|
fixable: true
|
|
9981
10237
|
};
|
|
@@ -10000,17 +10256,12 @@ function createHermesChecks() {
|
|
|
10000
10256
|
if (text2 === null) return true;
|
|
10001
10257
|
return text2.includes("/agents/hermes/") && !text2.includes(role.roleDir);
|
|
10002
10258
|
});
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
} else {
|
|
10007
|
-
systemctlUser(["daemon-reload"]);
|
|
10008
|
-
for (const unit of units) {
|
|
10009
|
-
systemctlUser(["enable", "--now", unit]);
|
|
10010
|
-
}
|
|
10011
|
-
}
|
|
10259
|
+
const manifestNeedsReconcile = [role.serviceStateGateway, role.serviceStateHeartbeat].some((state) => state === "pending" || state === "error");
|
|
10260
|
+
if (allUnitsPresent && !unitsStale && !manifestNeedsReconcile) {
|
|
10261
|
+
reconcileHermesRoleUnits(ctx, role, changedFiles, details);
|
|
10012
10262
|
continue;
|
|
10013
10263
|
}
|
|
10264
|
+
let regenerated = false;
|
|
10014
10265
|
for (const script of [join3(role.roleDir, ".scripts", "70-systemd.sh")]) {
|
|
10015
10266
|
if (!existsSync2(script)) {
|
|
10016
10267
|
details.push(`script failed: missing ${script}`);
|
|
@@ -10027,16 +10278,26 @@ function createHermesChecks() {
|
|
|
10027
10278
|
if (result2.status !== 0) {
|
|
10028
10279
|
details.push(`script failed: ${script}: ${result2.stderr.trim() || result2.stdout.trim()}`);
|
|
10029
10280
|
} else {
|
|
10281
|
+
regenerated = true;
|
|
10030
10282
|
details.push(`regenerated systemd units for ${role.agentId} from ${role.roleDir}`);
|
|
10031
10283
|
}
|
|
10032
10284
|
}
|
|
10033
10285
|
}
|
|
10286
|
+
if (ctx.dryRun) continue;
|
|
10287
|
+
if (regenerated) {
|
|
10288
|
+
const refreshed = discoverRoles(ctx.repoRoot).find((candidate) => candidate.agentId === role.agentId);
|
|
10289
|
+
if (!refreshed) {
|
|
10290
|
+
details.push(`script failed: regenerated role ${role.agentId} could not be rediscovered`);
|
|
10291
|
+
} else {
|
|
10292
|
+
reconcileHermesRoleUnits(ctx, refreshed, changedFiles, details);
|
|
10293
|
+
}
|
|
10294
|
+
}
|
|
10034
10295
|
}
|
|
10035
10296
|
return {
|
|
10036
10297
|
id: finding2.id,
|
|
10037
10298
|
title: finding2.title,
|
|
10038
10299
|
status: details.some((detail) => detail.includes("failed:")) ? "blocked" : details.length ? ctx.dryRun ? "skipped" : "applied" : "noop",
|
|
10039
|
-
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "
|
|
10300
|
+
summary: details.length ? ctx.dryRun ? "Planned systemd remediation commands" : "Reconciled and verified systemd service state" : "No changes required",
|
|
10040
10301
|
changedFiles,
|
|
10041
10302
|
details
|
|
10042
10303
|
};
|
|
@@ -10863,8 +11124,21 @@ var DockerRecipe = class extends Recipe {
|
|
|
10863
11124
|
|
|
10864
11125
|
// src/commands/hermes/EnsureTemplateConfig.ts
|
|
10865
11126
|
import { homedir as homedir3, platform } from "node:os";
|
|
10866
|
-
import {
|
|
10867
|
-
|
|
11127
|
+
import {
|
|
11128
|
+
existsSync as existsSync3,
|
|
11129
|
+
lstatSync as lstatSync3,
|
|
11130
|
+
mkdirSync as mkdirSync3,
|
|
11131
|
+
readFileSync as readFileSync3,
|
|
11132
|
+
realpathSync as realpathSync2,
|
|
11133
|
+
renameSync as renameSync2,
|
|
11134
|
+
rmSync as rmSync2,
|
|
11135
|
+
writeFileSync as writeFileSync3
|
|
11136
|
+
} from "node:fs";
|
|
11137
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
11138
|
+
import { basename as basename3, join as join4, dirname as dirname3 } from "node:path";
|
|
11139
|
+
var HERMES_GIT_URL = "https://github.com/delorenj/hermes-agent.git";
|
|
11140
|
+
var HERMES_GIT_REF = "main";
|
|
11141
|
+
var HERMES_GIT_SHA = "0408fec7a153e6c32c064acd2b8053917f1525f1";
|
|
10868
11142
|
function resolveTemplateConfigPath() {
|
|
10869
11143
|
const fromEnv = process.env.HERMES_TEMPLATE_CONFIG;
|
|
10870
11144
|
if (fromEnv && fromEnv.trim()) return fromEnv.trim();
|
|
@@ -10872,69 +11146,395 @@ function resolveTemplateConfigPath() {
|
|
|
10872
11146
|
const base = xdg && xdg.length ? xdg : join4(homedir3(), ".config");
|
|
10873
11147
|
return join4(base, "hermes-agent-template", "config.toml");
|
|
10874
11148
|
}
|
|
10875
|
-
function
|
|
11149
|
+
function quote(value) {
|
|
11150
|
+
return JSON.stringify(value);
|
|
11151
|
+
}
|
|
11152
|
+
function detectHermesInstall(home) {
|
|
11153
|
+
const releaseRoot = join4(home, ".local", "share", "hermes-agent", "releases", HERMES_GIT_SHA);
|
|
11154
|
+
const devRoot = join4(home, "code", "hermes-agent");
|
|
10876
11155
|
const candidates = [
|
|
10877
|
-
join4(
|
|
10878
|
-
join4(home, "
|
|
10879
|
-
join4(
|
|
11156
|
+
join4(releaseRoot, ".venv", "bin", "hermes"),
|
|
11157
|
+
join4(home, ".local", "bin", "hermes"),
|
|
11158
|
+
join4(devRoot, "venv", "bin", "hermes"),
|
|
11159
|
+
join4(devRoot, ".venv", "bin", "hermes")
|
|
10880
11160
|
];
|
|
10881
|
-
|
|
10882
|
-
|
|
11161
|
+
const bin = candidates.find((candidate) => existsSync3(candidate)) ?? candidates[0];
|
|
11162
|
+
try {
|
|
11163
|
+
const resolved = realpathSync2(bin);
|
|
11164
|
+
const environment = dirname3(dirname3(resolved));
|
|
11165
|
+
if (["venv", ".venv"].includes(environment.split("/").at(-1) ?? "")) {
|
|
11166
|
+
return { bin, repo: dirname3(environment) };
|
|
11167
|
+
}
|
|
11168
|
+
} catch {
|
|
10883
11169
|
}
|
|
10884
|
-
return
|
|
11170
|
+
return { bin, repo: bin.startsWith(devRoot) ? devRoot : releaseRoot };
|
|
10885
11171
|
}
|
|
10886
|
-
function
|
|
11172
|
+
function hostSchema() {
|
|
10887
11173
|
const home = homedir3();
|
|
10888
|
-
const
|
|
10889
|
-
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
11174
|
+
const hermes = detectHermesInstall(home);
|
|
11175
|
+
return [
|
|
11176
|
+
{
|
|
11177
|
+
section: "fleet",
|
|
11178
|
+
values: [
|
|
11179
|
+
["hermes_bin", quote(hermes.bin)],
|
|
11180
|
+
["hermes_repo", quote(hermes.repo)],
|
|
11181
|
+
["pjangler_bin", quote("pj")],
|
|
11182
|
+
["hermes_git_url", quote(HERMES_GIT_URL)],
|
|
11183
|
+
["hermes_git_ref", quote(HERMES_GIT_REF)],
|
|
11184
|
+
["hermes_git_sha", quote(HERMES_GIT_SHA)],
|
|
11185
|
+
["runtime_scaffold_dir", quote(join4(home, "code", "hermes-agent-template", "runtime-scaffold"))],
|
|
11186
|
+
["fleet_env", quote("~/.hermes/fleet.env")],
|
|
11187
|
+
["registry_file", quote("~/.hermes/agents-registry.yaml")],
|
|
11188
|
+
["oauth_file", quote("~/.hermes/auth.json")],
|
|
11189
|
+
["codex_home", quote("~/.codex")],
|
|
11190
|
+
["canonical_skills_dir", quote(join4(home, ".agents", "skills"))],
|
|
11191
|
+
["vox_plugin_name", quote("vox")],
|
|
11192
|
+
["vox_plugin_dir", quote(join4(home, "code", "voxxy", "plugins", "tts", "vox"))],
|
|
11193
|
+
["vox_voice", quote("carlin")],
|
|
11194
|
+
["vox_url", quote("https://vox.delo.sh")],
|
|
11195
|
+
["onepassword_vault", quote("DeLoSecrets")],
|
|
11196
|
+
["onepassword_item_prefix", quote("hermes-agent")],
|
|
11197
|
+
[
|
|
11198
|
+
"symlinked_runtime_skills",
|
|
11199
|
+
`[${[
|
|
11200
|
+
"delonet-conventions",
|
|
11201
|
+
"delonet-dotenv",
|
|
11202
|
+
"hermes-pm-template-maintenance",
|
|
11203
|
+
"hindsight",
|
|
11204
|
+
"33god-projects",
|
|
11205
|
+
"subagent-driven-development"
|
|
11206
|
+
].map(quote).join(", ")}]`
|
|
11207
|
+
]
|
|
11208
|
+
]
|
|
11209
|
+
},
|
|
11210
|
+
{ section: "github", values: [["runtime_repo_owner", quote("")]] },
|
|
11211
|
+
{
|
|
11212
|
+
section: "plane",
|
|
11213
|
+
values: [
|
|
11214
|
+
["base", quote("https://plane.delo.sh")],
|
|
11215
|
+
["workspace", quote("33god")]
|
|
11216
|
+
]
|
|
11217
|
+
}
|
|
11218
|
+
];
|
|
11219
|
+
}
|
|
11220
|
+
function renderHostConfig() {
|
|
11221
|
+
const sections = hostSchema().map(({ section: section2, values }) => `[${section2}]
|
|
11222
|
+
${values.map(([key, value]) => `${key} = ${value}`).join("\n")}`).join("\n\n");
|
|
10893
11223
|
return `# hermes-agent-template \u2014 host configuration
|
|
10894
|
-
# Bootstrapped by \`
|
|
10895
|
-
#
|
|
10896
|
-
#
|
|
10897
|
-
#
|
|
10898
|
-
# CLOUD provision (\`pjangler hermes\` without --local); they are unused by the
|
|
10899
|
-
# default local-only provision.
|
|
10900
|
-
#
|
|
10901
|
-
# Resolution precedence per value: env var > ~/.hermes/fleet.env > this file > fallback.
|
|
11224
|
+
# Bootstrapped by \`pj config bootstrap\` for $HOME=${homedir3()} (platform=${platform()}).
|
|
11225
|
+
# Existing values and additional keys are preserved by \`--force\`; it only adds
|
|
11226
|
+
# fields missing from the schema pinned in this pjangler release.
|
|
11227
|
+
# Resolution precedence: env var > ~/.hermes/fleet.env > this file > fallback.
|
|
10902
11228
|
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
|
|
11229
|
+
${sections}
|
|
11230
|
+
`;
|
|
11231
|
+
}
|
|
11232
|
+
function isEscaped(value, index) {
|
|
11233
|
+
let slashes = 0;
|
|
11234
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
|
|
11235
|
+
return slashes % 2 === 1;
|
|
11236
|
+
}
|
|
11237
|
+
function scanMultilineState(line, initial) {
|
|
11238
|
+
let state = initial;
|
|
11239
|
+
let string;
|
|
11240
|
+
for (let index = 0; index < line.length; ) {
|
|
11241
|
+
if (state) {
|
|
11242
|
+
const marker = state === "basic" ? '"""' : "'''";
|
|
11243
|
+
const close = line.indexOf(marker, index);
|
|
11244
|
+
if (close === -1) return state;
|
|
11245
|
+
if (state === "basic" && isEscaped(line, close)) {
|
|
11246
|
+
index = close + marker.length;
|
|
11247
|
+
continue;
|
|
11248
|
+
}
|
|
11249
|
+
state = void 0;
|
|
11250
|
+
index = close + marker.length;
|
|
11251
|
+
continue;
|
|
11252
|
+
}
|
|
11253
|
+
if (string === "basic") {
|
|
11254
|
+
if (line[index] === '"' && !isEscaped(line, index)) string = void 0;
|
|
11255
|
+
index += 1;
|
|
11256
|
+
continue;
|
|
11257
|
+
}
|
|
11258
|
+
if (string === "literal") {
|
|
11259
|
+
if (line[index] === "'") string = void 0;
|
|
11260
|
+
index += 1;
|
|
11261
|
+
continue;
|
|
11262
|
+
}
|
|
11263
|
+
if (line[index] === "#") break;
|
|
11264
|
+
if (line.startsWith('"""', index)) {
|
|
11265
|
+
state = "basic";
|
|
11266
|
+
index += 3;
|
|
11267
|
+
continue;
|
|
11268
|
+
}
|
|
11269
|
+
if (line.startsWith("'''", index)) {
|
|
11270
|
+
state = "literal";
|
|
11271
|
+
index += 3;
|
|
11272
|
+
continue;
|
|
11273
|
+
}
|
|
11274
|
+
if (line[index] === '"') string = "basic";
|
|
11275
|
+
else if (line[index] === "'") string = "literal";
|
|
11276
|
+
index += 1;
|
|
11277
|
+
}
|
|
11278
|
+
return state;
|
|
11279
|
+
}
|
|
11280
|
+
function parseBasicKey(raw) {
|
|
11281
|
+
try {
|
|
11282
|
+
const parsed = JSON.parse(raw);
|
|
11283
|
+
return typeof parsed === "string" ? parsed : void 0;
|
|
11284
|
+
} catch {
|
|
11285
|
+
return void 0;
|
|
11286
|
+
}
|
|
11287
|
+
}
|
|
11288
|
+
function parseDottedKey(raw) {
|
|
11289
|
+
const path = [];
|
|
11290
|
+
let cursor = 0;
|
|
11291
|
+
const whitespace = () => {
|
|
11292
|
+
while (cursor < raw.length && /[ \t]/.test(raw[cursor])) cursor += 1;
|
|
11293
|
+
};
|
|
11294
|
+
whitespace();
|
|
11295
|
+
while (cursor < raw.length) {
|
|
11296
|
+
let key;
|
|
11297
|
+
if (raw[cursor] === '"') {
|
|
11298
|
+
const start = cursor;
|
|
11299
|
+
cursor += 1;
|
|
11300
|
+
while (cursor < raw.length) {
|
|
11301
|
+
if (raw[cursor] === '"' && !isEscaped(raw, cursor)) {
|
|
11302
|
+
cursor += 1;
|
|
11303
|
+
key = parseBasicKey(raw.slice(start, cursor));
|
|
11304
|
+
break;
|
|
11305
|
+
}
|
|
11306
|
+
cursor += 1;
|
|
11307
|
+
}
|
|
11308
|
+
} else if (raw[cursor] === "'") {
|
|
11309
|
+
const end = raw.indexOf("'", cursor + 1);
|
|
11310
|
+
if (end !== -1) {
|
|
11311
|
+
key = raw.slice(cursor + 1, end);
|
|
11312
|
+
cursor = end + 1;
|
|
11313
|
+
}
|
|
11314
|
+
} else {
|
|
11315
|
+
const match = raw.slice(cursor).match(/^[A-Za-z0-9_-]+/);
|
|
11316
|
+
if (match) {
|
|
11317
|
+
key = match[0];
|
|
11318
|
+
cursor += match[0].length;
|
|
11319
|
+
}
|
|
11320
|
+
}
|
|
11321
|
+
if (key === void 0) return void 0;
|
|
11322
|
+
path.push(key);
|
|
11323
|
+
whitespace();
|
|
11324
|
+
if (cursor === raw.length) return path;
|
|
11325
|
+
if (raw[cursor] !== ".") return void 0;
|
|
11326
|
+
cursor += 1;
|
|
11327
|
+
whitespace();
|
|
11328
|
+
if (cursor === raw.length) return void 0;
|
|
11329
|
+
}
|
|
11330
|
+
return path.length ? path : void 0;
|
|
11331
|
+
}
|
|
11332
|
+
function parseHeaderLine(line) {
|
|
11333
|
+
const text2 = line.trimStart();
|
|
11334
|
+
if (!text2.startsWith("[")) return void 0;
|
|
11335
|
+
const kind = text2.startsWith("[[") ? "array-table" : "table";
|
|
11336
|
+
const openLength = kind === "array-table" ? 2 : 1;
|
|
11337
|
+
const close = kind === "array-table" ? "]]" : "]";
|
|
11338
|
+
let string;
|
|
11339
|
+
let closeAt = -1;
|
|
11340
|
+
for (let cursor = openLength; cursor < text2.length; cursor += 1) {
|
|
11341
|
+
if (string === "basic") {
|
|
11342
|
+
if (text2[cursor] === '"' && !isEscaped(text2, cursor)) string = void 0;
|
|
11343
|
+
continue;
|
|
11344
|
+
}
|
|
11345
|
+
if (string === "literal") {
|
|
11346
|
+
if (text2[cursor] === "'") string = void 0;
|
|
11347
|
+
continue;
|
|
11348
|
+
}
|
|
11349
|
+
if (text2[cursor] === '"') {
|
|
11350
|
+
string = "basic";
|
|
11351
|
+
continue;
|
|
11352
|
+
}
|
|
11353
|
+
if (text2[cursor] === "'") {
|
|
11354
|
+
string = "literal";
|
|
11355
|
+
continue;
|
|
11356
|
+
}
|
|
11357
|
+
if (text2.startsWith(close, cursor)) {
|
|
11358
|
+
closeAt = cursor;
|
|
11359
|
+
break;
|
|
11360
|
+
}
|
|
11361
|
+
}
|
|
11362
|
+
if (closeAt === -1) return void 0;
|
|
11363
|
+
const tail = text2.slice(closeAt + close.length);
|
|
11364
|
+
if (!/^[ \t]*(?:#.*)?$/.test(tail)) return void 0;
|
|
11365
|
+
const path = parseDottedKey(text2.slice(openLength, closeAt));
|
|
11366
|
+
return path ? { kind, path } : void 0;
|
|
11367
|
+
}
|
|
11368
|
+
function parseTomlTableHeaders(source) {
|
|
11369
|
+
const parsed = [];
|
|
11370
|
+
let offset = 0;
|
|
11371
|
+
let multiline;
|
|
11372
|
+
for (const match of source.matchAll(/[^\r\n]*(?:\r\n|\n|\r|$)/g)) {
|
|
11373
|
+
const segment = match[0];
|
|
11374
|
+
if (!segment) break;
|
|
11375
|
+
const line = segment.replace(/(?:\r\n|\n|\r)$/, "");
|
|
11376
|
+
if (!multiline) {
|
|
11377
|
+
const header = parseHeaderLine(line);
|
|
11378
|
+
if (header) {
|
|
11379
|
+
parsed.push({
|
|
11380
|
+
...header,
|
|
11381
|
+
headerStart: offset,
|
|
11382
|
+
bodyStart: offset + segment.length
|
|
11383
|
+
});
|
|
11384
|
+
}
|
|
11385
|
+
}
|
|
11386
|
+
multiline = scanMultilineState(line, multiline);
|
|
11387
|
+
offset += segment.length;
|
|
11388
|
+
}
|
|
11389
|
+
return parsed.map((header, index) => ({
|
|
11390
|
+
...header,
|
|
11391
|
+
bodyEnd: parsed[index + 1]?.headerStart ?? source.length
|
|
11392
|
+
}));
|
|
11393
|
+
}
|
|
11394
|
+
function ownedBareKeys(source, table) {
|
|
11395
|
+
const keys = /* @__PURE__ */ new Set();
|
|
11396
|
+
let multiline;
|
|
11397
|
+
const body = source.slice(table.bodyStart, table.bodyEnd);
|
|
11398
|
+
for (const match of body.matchAll(/[^\r\n]*(?:\r\n|\n|\r|$)/g)) {
|
|
11399
|
+
const segment = match[0];
|
|
11400
|
+
if (!segment) break;
|
|
11401
|
+
const line = segment.replace(/(?:\r\n|\n|\r)$/, "");
|
|
11402
|
+
if (!multiline) {
|
|
11403
|
+
const assignment = line.match(/^\s*((?:"(?:\\.|[^"\\])*"|'[^']*'|[A-Za-z0-9_-]+))\s*=/);
|
|
11404
|
+
if (assignment) {
|
|
11405
|
+
const path = parseDottedKey(assignment[1]);
|
|
11406
|
+
if (path?.length === 1) keys.add(path[0]);
|
|
11407
|
+
}
|
|
11408
|
+
}
|
|
11409
|
+
multiline = scanMultilineState(line, multiline);
|
|
11410
|
+
}
|
|
11411
|
+
return keys;
|
|
11412
|
+
}
|
|
11413
|
+
var TOMLLIB_VALIDATE = String.raw`
|
|
11414
|
+
import sys
|
|
10925
11415
|
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
11416
|
+
try:
|
|
11417
|
+
import tomllib
|
|
11418
|
+
except Exception as exc:
|
|
11419
|
+
sys.stderr.write("TOMLLIB_UNAVAILABLE:" + repr(exc))
|
|
11420
|
+
raise SystemExit(2)
|
|
10930
11421
|
|
|
10931
|
-
|
|
10932
|
-
|
|
10933
|
-
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
11422
|
+
try:
|
|
11423
|
+
source = sys.stdin.buffer.read().decode("utf-8", errors="strict")
|
|
11424
|
+
tomllib.loads(source)
|
|
11425
|
+
except (UnicodeDecodeError, tomllib.TOMLDecodeError) as exc:
|
|
11426
|
+
sys.stderr.write("TOML_INVALID:" + exc.__class__.__name__ + ": " + str(exc))
|
|
11427
|
+
raise SystemExit(1)
|
|
11428
|
+
`;
|
|
11429
|
+
var TOMLLIB_VALIDATION_TIMEOUT_MS = 5e3;
|
|
11430
|
+
function isolatedPythonEnvironment() {
|
|
11431
|
+
return Object.fromEntries(
|
|
11432
|
+
Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("PYTHON"))
|
|
11433
|
+
);
|
|
11434
|
+
}
|
|
11435
|
+
function validateTomlBytes(source, label) {
|
|
11436
|
+
const validation = spawnSync2("python3", ["-I", "-S", "-c", TOMLLIB_VALIDATE], {
|
|
11437
|
+
input: source,
|
|
11438
|
+
encoding: "utf8",
|
|
11439
|
+
env: isolatedPythonEnvironment(),
|
|
11440
|
+
maxBuffer: 1024 * 1024,
|
|
11441
|
+
timeout: TOMLLIB_VALIDATION_TIMEOUT_MS,
|
|
11442
|
+
killSignal: "SIGKILL"
|
|
11443
|
+
});
|
|
11444
|
+
if (validation.error) {
|
|
11445
|
+
const code = validation.error.code;
|
|
11446
|
+
if (code === "ETIMEDOUT") {
|
|
11447
|
+
throw new Error(`${label} validation timed out after ${TOMLLIB_VALIDATION_TIMEOUT_MS}ms`);
|
|
11448
|
+
}
|
|
11449
|
+
throw new Error(
|
|
11450
|
+
code === "ENOENT" ? `${label} cannot be validated: python3 with tomllib is required but was not found` : `${label} validation failed to start: ${validation.error.message}`
|
|
11451
|
+
);
|
|
11452
|
+
}
|
|
11453
|
+
if (validation.status === 2 || validation.stderr.startsWith("TOMLLIB_UNAVAILABLE:")) {
|
|
11454
|
+
throw new Error(
|
|
11455
|
+
`${label} cannot be validated: python3 with tomllib is required (${validation.stderr.replace(/^TOMLLIB_UNAVAILABLE:/, "")})`
|
|
11456
|
+
);
|
|
11457
|
+
}
|
|
11458
|
+
if (validation.status !== 0) {
|
|
11459
|
+
const detail = validation.stderr.replace(/^TOML_INVALID:/, "").trim() || `python3 exited ${validation.status ?? "without a status"}`;
|
|
11460
|
+
throw new Error(`${label} is not valid TOML 1.0 for Python tomllib: ${detail}`);
|
|
11461
|
+
}
|
|
11462
|
+
}
|
|
11463
|
+
function assertValidToml(source, label) {
|
|
11464
|
+
validateTomlBytes(Buffer.from(source, "utf8"), label);
|
|
11465
|
+
}
|
|
11466
|
+
function mergeHostConfig(existingBytes) {
|
|
11467
|
+
validateTomlBytes(existingBytes, "Existing Hermes template config");
|
|
11468
|
+
const existing = existingBytes.toString("utf8");
|
|
11469
|
+
if (!Buffer.from(existing, "utf8").equals(existingBytes)) {
|
|
11470
|
+
throw new Error("Existing Hermes template config is not valid UTF-8");
|
|
11471
|
+
}
|
|
11472
|
+
let merged = existing;
|
|
11473
|
+
for (const { section: section2, values } of hostSchema()) {
|
|
11474
|
+
const tables = parseTomlTableHeaders(merged);
|
|
11475
|
+
const matching = tables.filter((table2) => table2.kind === "table" && table2.path.length === 1 && table2.path[0] === section2);
|
|
11476
|
+
if (matching.length > 1) {
|
|
11477
|
+
throw new Error(`Cannot merge [${section2}]: config contains duplicate table headers`);
|
|
11478
|
+
}
|
|
11479
|
+
const table = matching[0];
|
|
11480
|
+
if (!table) {
|
|
11481
|
+
const incompatible = tables.find((candidate) => candidate.path[0] === section2);
|
|
11482
|
+
if (incompatible) {
|
|
11483
|
+
throw new Error(`Cannot merge [${section2}]: config defines ${incompatible.kind === "array-table" ? "an array table" : "a child table"} at [${incompatible.path.join(".")}] without an owning [${section2}] table`);
|
|
11484
|
+
}
|
|
11485
|
+
const prefix = merged.length === 0 ? "" : merged.endsWith("\n") ? "\n" : "\n\n";
|
|
11486
|
+
merged += `${prefix}[${section2}]
|
|
11487
|
+
${values.map(([key, value]) => `${key} = ${value}`).join("\n")}
|
|
10937
11488
|
`;
|
|
11489
|
+
continue;
|
|
11490
|
+
}
|
|
11491
|
+
const existingKeys = ownedBareKeys(merged, table);
|
|
11492
|
+
const missing = values.filter(([key]) => !existingKeys.has(key));
|
|
11493
|
+
if (!missing.length) continue;
|
|
11494
|
+
const body = merged.slice(table.bodyStart, table.bodyEnd);
|
|
11495
|
+
const addition = `${body.length === 0 || body.endsWith("\n") ? "" : "\n"}${missing.map(([key, value]) => `${key} = ${value}`).join("\n")}
|
|
11496
|
+
`;
|
|
11497
|
+
merged = `${merged.slice(0, table.bodyEnd)}${addition}${merged.slice(table.bodyEnd)}`;
|
|
11498
|
+
}
|
|
11499
|
+
assertValidToml(merged, "Merged Hermes template config");
|
|
11500
|
+
return merged;
|
|
11501
|
+
}
|
|
11502
|
+
function describePathType(stats) {
|
|
11503
|
+
if (stats.isSymbolicLink()) return "symbolic link";
|
|
11504
|
+
if (stats.isDirectory()) return "directory";
|
|
11505
|
+
if (stats.isFIFO()) return "FIFO";
|
|
11506
|
+
if (stats.isSocket()) return "socket";
|
|
11507
|
+
if (stats.isCharacterDevice()) return "character device";
|
|
11508
|
+
if (stats.isBlockDevice()) return "block device";
|
|
11509
|
+
return "non-regular file";
|
|
11510
|
+
}
|
|
11511
|
+
function inspectConfigPath(path) {
|
|
11512
|
+
let stats;
|
|
11513
|
+
try {
|
|
11514
|
+
stats = lstatSync3(path);
|
|
11515
|
+
} catch (error) {
|
|
11516
|
+
if (error.code === "ENOENT") return void 0;
|
|
11517
|
+
throw error;
|
|
11518
|
+
}
|
|
11519
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
11520
|
+
throw new Error(`Unsafe Hermes template config path ${path}: expected a regular file, found ${describePathType(stats)}`);
|
|
11521
|
+
}
|
|
11522
|
+
return stats;
|
|
11523
|
+
}
|
|
11524
|
+
function installConfig(path, next, previous) {
|
|
11525
|
+
const dir = dirname3(path);
|
|
11526
|
+
mkdirSync3(dir, { recursive: true });
|
|
11527
|
+
const tmp = join4(dir, `.${basename3(path)}.pjangler-${process.pid}.tmp`);
|
|
11528
|
+
try {
|
|
11529
|
+
writeFileSync3(tmp, next, { mode: previous ? previous.mode & 511 : 384 });
|
|
11530
|
+
renameSync2(tmp, path);
|
|
11531
|
+
} catch (error) {
|
|
11532
|
+
try {
|
|
11533
|
+
rmSync2(tmp, { force: true });
|
|
11534
|
+
} catch {
|
|
11535
|
+
}
|
|
11536
|
+
throw error;
|
|
11537
|
+
}
|
|
10938
11538
|
}
|
|
10939
11539
|
var EnsureTemplateConfig = class extends Command {
|
|
10940
11540
|
async invoke() {
|
|
@@ -10948,33 +11548,75 @@ var EnsureTemplateConfig = class extends Command {
|
|
|
10948
11548
|
}
|
|
10949
11549
|
const force = ctx.forceConfig === true || process.env.PJANGLER_FORCE_CONFIG === "1";
|
|
10950
11550
|
const path = resolveTemplateConfigPath();
|
|
10951
|
-
|
|
11551
|
+
let stats;
|
|
11552
|
+
try {
|
|
11553
|
+
stats = inspectConfigPath(path);
|
|
11554
|
+
} catch (error) {
|
|
11555
|
+
return {
|
|
11556
|
+
success: false,
|
|
11557
|
+
outcome: "failed",
|
|
11558
|
+
message: `Failed to inspect ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
11559
|
+
};
|
|
11560
|
+
}
|
|
11561
|
+
const exists = stats !== void 0;
|
|
10952
11562
|
if (exists && !force) {
|
|
10953
11563
|
if (!ctx.quiet) console.log(`\u2713 Config present: ${path}`);
|
|
10954
|
-
return { success: true, outcome: "unchanged", message:
|
|
11564
|
+
return { success: true, outcome: "unchanged", message: `Config present: ${path}` };
|
|
11565
|
+
}
|
|
11566
|
+
let next = renderHostConfig();
|
|
11567
|
+
let current = "";
|
|
11568
|
+
try {
|
|
11569
|
+
if (exists) {
|
|
11570
|
+
const bytes = readFileSync3(path);
|
|
11571
|
+
next = mergeHostConfig(bytes);
|
|
11572
|
+
current = bytes.toString("utf8");
|
|
11573
|
+
} else {
|
|
11574
|
+
assertValidToml(next, "Rendered Hermes template config");
|
|
11575
|
+
}
|
|
11576
|
+
} catch (error) {
|
|
11577
|
+
return {
|
|
11578
|
+
success: false,
|
|
11579
|
+
outcome: "failed",
|
|
11580
|
+
message: `Failed to prepare ${path}; no changes were applied: ${error instanceof Error ? error.message : String(error)}`
|
|
11581
|
+
};
|
|
11582
|
+
}
|
|
11583
|
+
if (exists && next === current) {
|
|
11584
|
+
return { success: true, outcome: "unchanged", message: `Config schema already current: ${path}` };
|
|
10955
11585
|
}
|
|
10956
11586
|
if (ctx.dryRun) {
|
|
10957
|
-
if (!ctx.quiet) console.log(`[DRY RUN] Would ${exists ? "
|
|
10958
|
-
return {
|
|
11587
|
+
if (!ctx.quiet) console.log(`[DRY RUN] Would ${exists ? "merge missing schema fields into" : "create"} config: ${path}`);
|
|
11588
|
+
return {
|
|
11589
|
+
success: true,
|
|
11590
|
+
outcome: "planned",
|
|
11591
|
+
filePath: path,
|
|
11592
|
+
message: `[DRY RUN] Would ${exists ? "merge missing schema fields into" : "create"} config: ${path}`
|
|
11593
|
+
};
|
|
10959
11594
|
}
|
|
10960
11595
|
try {
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
11596
|
+
installConfig(path, next, stats);
|
|
11597
|
+
} catch (error) {
|
|
11598
|
+
return {
|
|
11599
|
+
success: false,
|
|
11600
|
+
outcome: "failed",
|
|
11601
|
+
message: `Failed to write ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
11602
|
+
};
|
|
10966
11603
|
}
|
|
10967
11604
|
if (!ctx.quiet) {
|
|
10968
|
-
console.log(`\u2713 Bootstrapped config: ${path}`);
|
|
10969
|
-
console.log(" Review [github].runtime_repo_owner + [plane] + [bloodbank] before a cloud provision.");
|
|
11605
|
+
console.log(`\u2713 ${exists ? "Updated" : "Bootstrapped"} config: ${path}`);
|
|
11606
|
+
if (!exists) console.log(" Review [github].runtime_repo_owner + [plane] + [bloodbank] before a cloud provision.");
|
|
10970
11607
|
}
|
|
10971
|
-
return {
|
|
11608
|
+
return {
|
|
11609
|
+
success: true,
|
|
11610
|
+
outcome: "changed",
|
|
11611
|
+
filePath: path,
|
|
11612
|
+
message: `${exists ? "Updated" : "Bootstrapped"} config without replacing existing values: ${path}`
|
|
11613
|
+
};
|
|
10972
11614
|
}
|
|
10973
11615
|
};
|
|
10974
11616
|
|
|
10975
11617
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
10976
|
-
import { basename as
|
|
10977
|
-
import { readFileSync as
|
|
11618
|
+
import { basename as basename4, join as join5 } from "node:path";
|
|
11619
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
10978
11620
|
import * as p from "@clack/prompts";
|
|
10979
11621
|
|
|
10980
11622
|
// src/commands/hermes/types.ts
|
|
@@ -10989,7 +11631,7 @@ function deriveProfileName(repo, role) {
|
|
|
10989
11631
|
// src/commands/hermes/PromptForAgentConfig.ts
|
|
10990
11632
|
function detectTicketProvider(targetDir) {
|
|
10991
11633
|
try {
|
|
10992
|
-
const t = JSON.parse(
|
|
11634
|
+
const t = JSON.parse(readFileSync4(join5(targetDir, ".project.json"), "utf8"))?.ticket_provider?.type;
|
|
10993
11635
|
return t === "plane" || t === "trello" ? t : void 0;
|
|
10994
11636
|
} catch {
|
|
10995
11637
|
return void 0;
|
|
@@ -10998,7 +11640,7 @@ function detectTicketProvider(targetDir) {
|
|
|
10998
11640
|
var PromptForAgentConfig = class extends Command {
|
|
10999
11641
|
async invoke() {
|
|
11000
11642
|
const ctx = this.context;
|
|
11001
|
-
const defaultRepo =
|
|
11643
|
+
const defaultRepo = basename4(ctx.targetDir).toLowerCase();
|
|
11002
11644
|
ctx.targetRepo = (ctx.targetRepo ?? defaultRepo).toLowerCase();
|
|
11003
11645
|
ctx.role ??= "pm";
|
|
11004
11646
|
ctx.agentPurpose ??= `${ctx.role} agent for ${ctx.targetRepo}`;
|
|
@@ -11054,12 +11696,73 @@ var PromptForAgentConfig = class extends Command {
|
|
|
11054
11696
|
}
|
|
11055
11697
|
};
|
|
11056
11698
|
|
|
11699
|
+
// src/commands/hermes/ValidateHermesOptions.ts
|
|
11700
|
+
import { lstatSync as lstatSync7, readdirSync as readdirSync5 } from "node:fs";
|
|
11701
|
+
import { join as join10, relative as relative6 } from "node:path";
|
|
11702
|
+
init_project();
|
|
11703
|
+
var EMAIL_UNSUPPORTED_MESSAGE = "Email provisioning is unavailable: the pinned Hermes template has no supported email provisioner. Omit --email; no files or external state were changed.";
|
|
11704
|
+
var HARMLESS_ROLE_PLACEHOLDERS = /* @__PURE__ */ new Set([".gitkeep", ".DS_Store", "Thumbs.db"]);
|
|
11705
|
+
function existingRoleDirectoryBlockers(roleDir) {
|
|
11706
|
+
let root;
|
|
11707
|
+
try {
|
|
11708
|
+
root = lstatSync7(roleDir);
|
|
11709
|
+
} catch (error) {
|
|
11710
|
+
if (error.code === "ENOENT") return [];
|
|
11711
|
+
throw error;
|
|
11712
|
+
}
|
|
11713
|
+
if (!root.isDirectory() || root.isSymbolicLink()) return ["<target is not a real directory>"];
|
|
11714
|
+
const blockers = [];
|
|
11715
|
+
const visit = (directory) => {
|
|
11716
|
+
for (const entry of readdirSync5(directory, { withFileTypes: true })) {
|
|
11717
|
+
const path = join10(directory, entry.name);
|
|
11718
|
+
const stats = lstatSync7(path);
|
|
11719
|
+
if (stats.isDirectory() && !stats.isSymbolicLink()) {
|
|
11720
|
+
visit(path);
|
|
11721
|
+
} else if (!HARMLESS_ROLE_PLACEHOLDERS.has(entry.name) || !stats.isFile() || stats.isSymbolicLink() || stats.nlink !== 1) {
|
|
11722
|
+
blockers.push(relative6(roleDir, path));
|
|
11723
|
+
}
|
|
11724
|
+
}
|
|
11725
|
+
};
|
|
11726
|
+
visit(roleDir);
|
|
11727
|
+
return blockers.sort();
|
|
11728
|
+
}
|
|
11729
|
+
function existingRoleRefusal(roleDir) {
|
|
11730
|
+
const blockers = existingRoleDirectoryBlockers(roleDir);
|
|
11731
|
+
if (!blockers.length) return void 0;
|
|
11732
|
+
const sample = blockers.slice(0, 3).join(", ");
|
|
11733
|
+
const remainder = blockers.length > 3 ? ` and ${blockers.length - 3} more` : "";
|
|
11734
|
+
return `Hermes target directory is not empty at ${roleDir} (found ${sample}${remainder}); non-interactive mode will not render into it. Re-run with --force to re-render explicitly.`;
|
|
11735
|
+
}
|
|
11736
|
+
var ValidateHermesOptions = class extends Command {
|
|
11737
|
+
async invoke() {
|
|
11738
|
+
const ctx = this.context;
|
|
11739
|
+
if (ctx.skipEmail === false) {
|
|
11740
|
+
return { success: false, outcome: "failed", message: EMAIL_UNSUPPORTED_MESSAGE };
|
|
11741
|
+
}
|
|
11742
|
+
const role = normalizeAgentRole(ctx.role ?? "pm");
|
|
11743
|
+
const roleDir = resolveContainedPath(
|
|
11744
|
+
ctx.targetDir,
|
|
11745
|
+
join10(ctx.targetDir, "agents", "hermes", role),
|
|
11746
|
+
"Hermes role directory"
|
|
11747
|
+
);
|
|
11748
|
+
const refusal = (ctx.yes || ctx.quiet) && !ctx.force ? existingRoleRefusal(roleDir) : void 0;
|
|
11749
|
+
if (refusal) {
|
|
11750
|
+
return {
|
|
11751
|
+
success: false,
|
|
11752
|
+
outcome: "failed",
|
|
11753
|
+
message: refusal
|
|
11754
|
+
};
|
|
11755
|
+
}
|
|
11756
|
+
return { success: true, outcome: "unchanged", message: "" };
|
|
11757
|
+
}
|
|
11758
|
+
};
|
|
11759
|
+
|
|
11057
11760
|
// src/commands/hermes/RunCopierTemplate.ts
|
|
11058
|
-
import { spawnSync as
|
|
11761
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
11059
11762
|
import { homedir as homedir5 } from "node:os";
|
|
11060
|
-
import { join as
|
|
11061
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as
|
|
11062
|
-
import { fileURLToPath as
|
|
11763
|
+
import { join as join11, dirname as dirname7, relative as relative7 } from "node:path";
|
|
11764
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
11765
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
11063
11766
|
import * as p2 from "@clack/prompts";
|
|
11064
11767
|
import YAML4 from "yaml";
|
|
11065
11768
|
init_project();
|
|
@@ -11089,9 +11792,9 @@ function scrubInteractiveChannelCredentials(env2) {
|
|
|
11089
11792
|
delete env2.WIRE_SLACK;
|
|
11090
11793
|
}
|
|
11091
11794
|
function registerRenderedAgent(ctx, roleDir, role) {
|
|
11092
|
-
const manifestPath =
|
|
11795
|
+
const manifestPath = join11(ctx.targetDir, ".project.json");
|
|
11093
11796
|
if (!existsSync7(manifestPath) || !ctx.targetRepo) return;
|
|
11094
|
-
const current =
|
|
11797
|
+
const current = readFileSync9(manifestPath, "utf8");
|
|
11095
11798
|
const parsed = JSON.parse(current);
|
|
11096
11799
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
11097
11800
|
throw new Error(`${manifestPath} must contain a JSON object`);
|
|
@@ -11106,7 +11809,7 @@ function registerRenderedAgent(ctx, roleDir, role) {
|
|
|
11106
11809
|
Object.defineProperty(agents, agentId, {
|
|
11107
11810
|
value: {
|
|
11108
11811
|
role,
|
|
11109
|
-
role_dir:
|
|
11812
|
+
role_dir: relative7(ctx.targetDir, roleDir),
|
|
11110
11813
|
provisioning_state: "provisioned"
|
|
11111
11814
|
},
|
|
11112
11815
|
configurable: true,
|
|
@@ -11121,14 +11824,14 @@ function registerRenderedAgent(ctx, roleDir, role) {
|
|
|
11121
11824
|
function resolveVendoredTemplate(name) {
|
|
11122
11825
|
let dir;
|
|
11123
11826
|
try {
|
|
11124
|
-
dir =
|
|
11827
|
+
dir = dirname7(fileURLToPath4(import.meta.url));
|
|
11125
11828
|
} catch {
|
|
11126
11829
|
return void 0;
|
|
11127
11830
|
}
|
|
11128
11831
|
for (let i = 0; i < 8; i++) {
|
|
11129
|
-
const candidate =
|
|
11130
|
-
if (existsSync7(
|
|
11131
|
-
const parent =
|
|
11832
|
+
const candidate = join11(dir, "templates", name);
|
|
11833
|
+
if (existsSync7(join11(candidate, "copier.yml"))) return candidate;
|
|
11834
|
+
const parent = dirname7(dir);
|
|
11132
11835
|
if (parent === dir) break;
|
|
11133
11836
|
dir = parent;
|
|
11134
11837
|
}
|
|
@@ -11160,11 +11863,14 @@ var RunCopierTemplate = class extends Command {
|
|
|
11160
11863
|
ctx.role = safeRole;
|
|
11161
11864
|
const roleDir = resolveContainedPath(
|
|
11162
11865
|
ctx.targetDir,
|
|
11163
|
-
|
|
11866
|
+
join11(ctx.targetDir, "agents", "hermes", safeRole),
|
|
11164
11867
|
"Hermes role directory"
|
|
11165
11868
|
);
|
|
11166
11869
|
ctx.roleDir = roleDir;
|
|
11167
|
-
ctx.
|
|
11870
|
+
const nonInteractiveRefusal = (ctx.yes || ctx.quiet) && !ctx.force ? existingRoleRefusal(roleDir) : void 0;
|
|
11871
|
+
if (nonInteractiveRefusal) {
|
|
11872
|
+
return { success: false, outcome: "failed", message: nonInteractiveRefusal };
|
|
11873
|
+
}
|
|
11168
11874
|
const trustedCopierRequired = Boolean(ctx.deferredExternalEffects && !ctx.dryRun);
|
|
11169
11875
|
if (trustedCopierRequired && !ctx.trustedCopier) {
|
|
11170
11876
|
return {
|
|
@@ -11174,7 +11880,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
11174
11880
|
};
|
|
11175
11881
|
}
|
|
11176
11882
|
if (!ctx.trustedCopier) {
|
|
11177
|
-
const which =
|
|
11883
|
+
const which = spawnSync4("which", ["copier"], { encoding: "utf8" });
|
|
11178
11884
|
if (which.status !== 0) {
|
|
11179
11885
|
return {
|
|
11180
11886
|
success: false,
|
|
@@ -11183,10 +11889,8 @@ var RunCopierTemplate = class extends Command {
|
|
|
11183
11889
|
};
|
|
11184
11890
|
}
|
|
11185
11891
|
}
|
|
11186
|
-
if (existsSync7(
|
|
11187
|
-
if (ctx.yes) {
|
|
11188
|
-
ctx.force = true;
|
|
11189
|
-
} else {
|
|
11892
|
+
if (existsSync7(join11(roleDir, "role.yaml")) && !ctx.force) {
|
|
11893
|
+
if (!ctx.yes && !ctx.quiet) {
|
|
11190
11894
|
const proceed = await p2.confirm({
|
|
11191
11895
|
message: `${safeRole}/role.yaml already exists \u2014 re-render with --overwrite?`,
|
|
11192
11896
|
initialValue: false
|
|
@@ -11214,9 +11918,10 @@ var RunCopierTemplate = class extends Command {
|
|
|
11214
11918
|
// renders repo-local files first and executes those scripts only after a
|
|
11215
11919
|
// structural lifecycle gate has accepted the render.
|
|
11216
11920
|
SKIP_HOST_STATE: ctx.deferredExternalEffects ? "1" : "0",
|
|
11217
|
-
//
|
|
11218
|
-
//
|
|
11219
|
-
|
|
11921
|
+
// The legacy script/env name now owns only ignored role-local runtime
|
|
11922
|
+
// setup. Structured transactions defer it until rendered eligibility;
|
|
11923
|
+
// ordinary CLI deploys always run it and no flag can create a remote repo.
|
|
11924
|
+
SKIP_RUNTIME_REPO: ctx.deferredExternalEffects ? "1" : "0",
|
|
11220
11925
|
SKIP_PLANE: ctx.deferredExternalEffects ? "1" : ctx.skipPlane ? "1" : "0",
|
|
11221
11926
|
SKIP_BLOODBANK: "1",
|
|
11222
11927
|
SKIP_SYSTEMD: ctx.deferredExternalEffects ? "1" : ctx.skipSystemd ? "1" : "0"
|
|
@@ -11229,9 +11934,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
11229
11934
|
env2.PYTHONNOUSERSITE = "1";
|
|
11230
11935
|
env2.PYTHONSAFEPATH = "1";
|
|
11231
11936
|
}
|
|
11232
|
-
const LOCAL_TEMPLATE =
|
|
11937
|
+
const LOCAL_TEMPLATE = join11(homedir5(), "code", "hermes-agent-template");
|
|
11233
11938
|
const vendored = resolveVendoredTemplate("hermes-agent");
|
|
11234
|
-
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync7(
|
|
11939
|
+
const templateSrc = process.env.PJANGLER_HERMES_TEMPLATE || vendored || (existsSync7(join11(LOCAL_TEMPLATE, "copier.yml")) ? LOCAL_TEMPLATE : HERMES_AGENT_TEMPLATE);
|
|
11235
11940
|
const args = [
|
|
11236
11941
|
"copy",
|
|
11237
11942
|
templateSrc,
|
|
@@ -11261,6 +11966,7 @@ var RunCopierTemplate = class extends Command {
|
|
|
11261
11966
|
"--trust",
|
|
11262
11967
|
"--vcs-ref=HEAD"
|
|
11263
11968
|
];
|
|
11969
|
+
if (ctx.yes || ctx.quiet) args.push("--defaults");
|
|
11264
11970
|
if (ctx.force) args.push("--overwrite");
|
|
11265
11971
|
if (ctx.dryRun) {
|
|
11266
11972
|
return {
|
|
@@ -11280,12 +11986,12 @@ var RunCopierTemplate = class extends Command {
|
|
|
11280
11986
|
};
|
|
11281
11987
|
}
|
|
11282
11988
|
}
|
|
11283
|
-
mkdirSync5(
|
|
11284
|
-
const
|
|
11285
|
-
|
|
11989
|
+
mkdirSync5(join11(ctx.targetDir, "agents", "hermes"), { recursive: true });
|
|
11990
|
+
const spinner3 = ctx.quiet ? void 0 : p2.spinner();
|
|
11991
|
+
spinner3?.start(`Running copier copy (target: agents/hermes/${safeRole})`);
|
|
11286
11992
|
const copierExecutable = ctx.trustedCopier?.executable ?? "copier";
|
|
11287
|
-
const result2 =
|
|
11288
|
-
|
|
11993
|
+
const result2 = spawnSync4(copierExecutable, args, ctx.quiet ? { encoding: "utf8", env: env2, cwd: ctx.targetDir } : { stdio: "inherit", env: env2, cwd: ctx.targetDir });
|
|
11994
|
+
spinner3?.stop(result2.status === 0 ? "\u2713 copier run complete" : "\u2717 copier failed");
|
|
11289
11995
|
if (result2.status !== 0) {
|
|
11290
11996
|
return {
|
|
11291
11997
|
success: false,
|
|
@@ -11293,9 +11999,9 @@ var RunCopierTemplate = class extends Command {
|
|
|
11293
11999
|
message: `copier exited with status ${result2.status}.${ctx.quiet && String(result2.stderr ?? "").trim() ? ` ${String(result2.stderr).trim()}` : " Check the output above; re-run with the same flags after fixing."}`
|
|
11294
12000
|
};
|
|
11295
12001
|
}
|
|
11296
|
-
const roleManifest =
|
|
12002
|
+
const roleManifest = join11(roleDir, "role.yaml");
|
|
11297
12003
|
try {
|
|
11298
|
-
const current =
|
|
12004
|
+
const current = readFileSync9(roleManifest, "utf8");
|
|
11299
12005
|
const document = YAML4.parseDocument(current);
|
|
11300
12006
|
if (document.errors.length) throw document.errors[0];
|
|
11301
12007
|
document.setIn(["deployment", "local_only"], ctx.deferredExternalEffects ? true : Boolean(ctx.local));
|
|
@@ -11312,15 +12018,16 @@ var RunCopierTemplate = class extends Command {
|
|
|
11312
12018
|
}
|
|
11313
12019
|
return {
|
|
11314
12020
|
success: true,
|
|
11315
|
-
|
|
12021
|
+
outcome: "changed",
|
|
12022
|
+
message: `Rendered Hermes role at ${roleDir}; lifecycle postconditions are pending`
|
|
11316
12023
|
};
|
|
11317
12024
|
}
|
|
11318
12025
|
};
|
|
11319
12026
|
|
|
11320
12027
|
// src/commands/hermes/UntrackHermesRuntimes.ts
|
|
11321
|
-
import { existsSync as existsSync8, readFileSync as
|
|
11322
|
-
import { join as
|
|
11323
|
-
import { spawnSync as
|
|
12028
|
+
import { existsSync as existsSync8, readFileSync as readFileSync10, writeFileSync as writeFileSync6, readdirSync as readdirSync6 } from "fs";
|
|
12029
|
+
import { join as join12 } from "path";
|
|
12030
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
11324
12031
|
function sectionHasPath(section2, targetPath) {
|
|
11325
12032
|
return section2.split(/\r?\n/).some((line) => /^\s*path\s*=/.test(line) && line.replace(/^\s*path\s*=\s*/, "").trim() === targetPath);
|
|
11326
12033
|
}
|
|
@@ -11330,14 +12037,14 @@ function removeSubmodulePath(content, targetPath) {
|
|
|
11330
12037
|
var UntrackHermesRuntimes = class extends Command {
|
|
11331
12038
|
async invoke() {
|
|
11332
12039
|
const targetDir = this.context.targetDir;
|
|
11333
|
-
const rolesDir =
|
|
12040
|
+
const rolesDir = join12(targetDir, "agents", "hermes");
|
|
11334
12041
|
if (!existsSync8(rolesDir)) {
|
|
11335
12042
|
return {
|
|
11336
12043
|
success: true,
|
|
11337
12044
|
message: "No Hermes agents found (no agents/hermes directory)."
|
|
11338
12045
|
};
|
|
11339
12046
|
}
|
|
11340
|
-
const roles =
|
|
12047
|
+
const roles = readdirSync6(rolesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
11341
12048
|
if (roles.length === 0) {
|
|
11342
12049
|
return {
|
|
11343
12050
|
success: true,
|
|
@@ -11347,12 +12054,12 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
11347
12054
|
let modifiedAny = false;
|
|
11348
12055
|
const details = [];
|
|
11349
12056
|
for (const role of roles) {
|
|
11350
|
-
const roleDir =
|
|
11351
|
-
const runtimePath =
|
|
11352
|
-
const gitignorePath =
|
|
11353
|
-
const gitmodulesPath =
|
|
12057
|
+
const roleDir = join12("agents", "hermes", role);
|
|
12058
|
+
const runtimePath = join12(roleDir, "runtime");
|
|
12059
|
+
const gitignorePath = join12(roleDir, ".gitignore");
|
|
12060
|
+
const gitmodulesPath = join12(targetDir, ".gitmodules");
|
|
11354
12061
|
let isTracked = false;
|
|
11355
|
-
const lsResult =
|
|
12062
|
+
const lsResult = spawnSync5("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
11356
12063
|
cwd: targetDir,
|
|
11357
12064
|
encoding: "utf8"
|
|
11358
12065
|
});
|
|
@@ -11368,14 +12075,14 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
11368
12075
|
let hasStaleMapping = false;
|
|
11369
12076
|
let gitmodulesContent = "";
|
|
11370
12077
|
if (existsSync8(gitmodulesPath)) {
|
|
11371
|
-
gitmodulesContent =
|
|
12078
|
+
gitmodulesContent = readFileSync10(gitmodulesPath, "utf8");
|
|
11372
12079
|
const sections = gitmodulesContent.match(/^\[submodule "[^"\n]+"\][\s\S]*?(?=^\[submodule "|(?![\s\S]))/gm) ?? [];
|
|
11373
12080
|
hasStaleMapping = sections.some((section2) => sectionHasPath(section2, runtimePath));
|
|
11374
12081
|
}
|
|
11375
12082
|
let isIgnored = false;
|
|
11376
|
-
const fullGitignorePath =
|
|
12083
|
+
const fullGitignorePath = join12(targetDir, gitignorePath);
|
|
11377
12084
|
if (existsSync8(fullGitignorePath)) {
|
|
11378
|
-
const content =
|
|
12085
|
+
const content = readFileSync10(fullGitignorePath, "utf8");
|
|
11379
12086
|
const lines = content.split(/\r?\n/).map((line) => line.trim());
|
|
11380
12087
|
isIgnored = lines.includes("runtime/") || lines.includes("runtime");
|
|
11381
12088
|
}
|
|
@@ -11384,7 +12091,7 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
11384
12091
|
if (isTracked) {
|
|
11385
12092
|
details.push(`untrack agents/hermes/${role}/runtime`);
|
|
11386
12093
|
if (!this.context.dryRun) {
|
|
11387
|
-
const rmResult =
|
|
12094
|
+
const rmResult = spawnSync5("git", ["rm", "--cached", "-r", "-f", "--", runtimePath], {
|
|
11388
12095
|
cwd: targetDir,
|
|
11389
12096
|
encoding: "utf8"
|
|
11390
12097
|
});
|
|
@@ -11394,7 +12101,7 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
11394
12101
|
message: `\u2717 Failed to untrack ${runtimePath}: ${rmResult.stderr.trim() || `exit ${rmResult.status}`}`
|
|
11395
12102
|
};
|
|
11396
12103
|
}
|
|
11397
|
-
const verifyResult =
|
|
12104
|
+
const verifyResult = spawnSync5("git", ["ls-files", "--stage", "--", runtimePath], {
|
|
11398
12105
|
cwd: targetDir,
|
|
11399
12106
|
encoding: "utf8"
|
|
11400
12107
|
});
|
|
@@ -11419,7 +12126,7 @@ var UntrackHermesRuntimes = class extends Command {
|
|
|
11419
12126
|
if (!this.context.dryRun) {
|
|
11420
12127
|
let content = "";
|
|
11421
12128
|
if (existsSync8(fullGitignorePath)) {
|
|
11422
|
-
content =
|
|
12129
|
+
content = readFileSync10(fullGitignorePath, "utf8");
|
|
11423
12130
|
}
|
|
11424
12131
|
if (content && !content.endsWith("\n")) {
|
|
11425
12132
|
content += "\n";
|
|
@@ -11446,8 +12153,8 @@ ${details.map((d) => ` - ${d}`).join("\n")}`
|
|
|
11446
12153
|
};
|
|
11447
12154
|
|
|
11448
12155
|
// src/commands/hermes/WireTelegram.ts
|
|
11449
|
-
import { spawnSync as
|
|
11450
|
-
import { join as
|
|
12156
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
12157
|
+
import { join as join13 } from "node:path";
|
|
11451
12158
|
import { existsSync as existsSync9, unlinkSync as unlinkSync3 } from "node:fs";
|
|
11452
12159
|
import * as p3 from "@clack/prompts";
|
|
11453
12160
|
var WireTelegram = class extends Command {
|
|
@@ -11477,7 +12184,7 @@ var WireTelegram = class extends Command {
|
|
|
11477
12184
|
let token = process.env.TELEGRAM_BOT_TOKEN;
|
|
11478
12185
|
let source = token ? "env" : null;
|
|
11479
12186
|
if (!token) {
|
|
11480
|
-
const tryOp =
|
|
12187
|
+
const tryOp = spawnSync6("op", ["read", vaultRef], { encoding: "utf8" });
|
|
11481
12188
|
if (tryOp.status === 0) {
|
|
11482
12189
|
token = tryOp.stdout.trim();
|
|
11483
12190
|
source = "op";
|
|
@@ -11516,7 +12223,7 @@ var WireTelegram = class extends Command {
|
|
|
11516
12223
|
initialValue: true
|
|
11517
12224
|
});
|
|
11518
12225
|
if (!p3.isCancel(persist) && persist) {
|
|
11519
|
-
const create =
|
|
12226
|
+
const create = spawnSync6(
|
|
11520
12227
|
"op",
|
|
11521
12228
|
[
|
|
11522
12229
|
"item",
|
|
@@ -11543,28 +12250,32 @@ var WireTelegram = class extends Command {
|
|
|
11543
12250
|
if (p3.isCancel(allowedAnswer)) {
|
|
11544
12251
|
return { success: false, message: "\u2717 Aborted; Telegram step deferred." };
|
|
11545
12252
|
}
|
|
11546
|
-
const script =
|
|
12253
|
+
const script = join13(roleDir, ".scripts", "30-telegram.sh");
|
|
11547
12254
|
if (!existsSync9(script)) {
|
|
11548
12255
|
return {
|
|
11549
12256
|
success: false,
|
|
11550
|
-
message: `\u2717 ${script} not found.
|
|
12257
|
+
message: `\u2717 ${script} not found. Did Copier finish rendering the Hermes role?`
|
|
11551
12258
|
};
|
|
11552
12259
|
}
|
|
11553
|
-
const marker =
|
|
12260
|
+
const marker = join13(roleDir, ".scripts", ".done-30-telegram");
|
|
11554
12261
|
if (existsSync9(marker)) unlinkSync3(marker);
|
|
11555
|
-
const
|
|
11556
|
-
|
|
11557
|
-
const result2 =
|
|
12262
|
+
const spinner3 = p3.spinner();
|
|
12263
|
+
spinner3.start("Verifying token + wiring profile");
|
|
12264
|
+
const result2 = spawnSync6("bash", [script], {
|
|
11558
12265
|
stdio: "inherit",
|
|
11559
12266
|
env: {
|
|
11560
12267
|
...process.env,
|
|
12268
|
+
// Interactive wiring is an explicit host-state operation: it writes the
|
|
12269
|
+
// profile. Set the gate rather than inheriting whatever a deferred MCP
|
|
12270
|
+
// render left in the environment, which would silently no-op this run.
|
|
12271
|
+
SKIP_HOST_STATE: "0",
|
|
11561
12272
|
SKIP_TELEGRAM: "0",
|
|
11562
12273
|
TELEGRAM_BOT_TOKEN: token,
|
|
11563
12274
|
TELEGRAM_ALLOWED_USERS: String(allowedAnswer).trim()
|
|
11564
12275
|
},
|
|
11565
12276
|
cwd: roleDir
|
|
11566
12277
|
});
|
|
11567
|
-
|
|
12278
|
+
spinner3.stop(result2.status === 0 ? "\u2713 Telegram wired" : "\u2717 Telegram step failed");
|
|
11568
12279
|
if (result2.status !== 0) {
|
|
11569
12280
|
return { success: false, message: "Telegram wire-up failed. See output above." };
|
|
11570
12281
|
}
|
|
@@ -11577,164 +12288,67 @@ function cap(s) {
|
|
|
11577
12288
|
}
|
|
11578
12289
|
|
|
11579
12290
|
// src/commands/hermes/WireEmail.ts
|
|
11580
|
-
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
11581
|
-
import { join as join12 } from "node:path";
|
|
11582
|
-
import { existsSync as existsSync10, unlinkSync as unlinkSync4 } from "node:fs";
|
|
11583
|
-
import * as p4 from "@clack/prompts";
|
|
11584
12291
|
var WireEmail = class extends Command {
|
|
11585
12292
|
async invoke() {
|
|
11586
12293
|
const ctx = this.context;
|
|
11587
|
-
if (ctx.skipEmail) {
|
|
11588
|
-
return { success: true, message: "" };
|
|
11589
|
-
}
|
|
11590
|
-
if (ctx.quiet) {
|
|
11591
|
-
return {
|
|
11592
|
-
success: false,
|
|
11593
|
-
outcome: "failed",
|
|
11594
|
-
message: "Email wiring is interactive and unavailable during quiet/non-interactive Hermes execution"
|
|
11595
|
-
};
|
|
11596
|
-
}
|
|
11597
|
-
if (ctx.dryRun) {
|
|
11598
|
-
return { success: true, message: this.formatMessage("Would create CF Email Routing rule") };
|
|
11599
|
-
}
|
|
11600
|
-
const { targetRepo, role, roleDir } = ctx;
|
|
11601
|
-
if (!targetRepo || !role || !roleDir) {
|
|
11602
|
-
return { success: false, message: "Cannot wire email: missing target_repo/role/roleDir" };
|
|
11603
|
-
}
|
|
11604
|
-
const script = join12(roleDir, ".scripts", "50-email.sh");
|
|
11605
|
-
if (!existsSync10(script)) {
|
|
11606
|
-
return { success: false, message: `\u2717 ${script} not found` };
|
|
11607
|
-
}
|
|
11608
|
-
let token = process.env.CF_EMAIL_ROUTING_TOKEN;
|
|
11609
|
-
if (!token) {
|
|
11610
|
-
const tryOp = spawnSync6(
|
|
11611
|
-
"op",
|
|
11612
|
-
["read", "op://DeLoSecrets/Cloudflare-EmailRouting/token"],
|
|
11613
|
-
{ encoding: "utf8" }
|
|
11614
|
-
);
|
|
11615
|
-
if (tryOp.status === 0) {
|
|
11616
|
-
token = tryOp.stdout.trim();
|
|
11617
|
-
}
|
|
11618
|
-
}
|
|
11619
|
-
if (!token) {
|
|
11620
|
-
p4.log.warn("CF Email Routing token not found. Required scopes:");
|
|
11621
|
-
p4.log.info(
|
|
11622
|
-
[
|
|
11623
|
-
" Zone (delo.sh) \u2192 Email Routing Rules : Edit",
|
|
11624
|
-
" Zone (delo.sh) \u2192 Email Routing Settings : Read",
|
|
11625
|
-
" Account \u2192 Email Routing Addresses : Read",
|
|
11626
|
-
"Create at: https://dash.cloudflare.com/profile/api-tokens"
|
|
11627
|
-
].join("\n")
|
|
11628
|
-
);
|
|
11629
|
-
const provideNow = await p4.confirm({
|
|
11630
|
-
message: "Paste a token now? (skipping leaves email unwired until you re-run.)",
|
|
11631
|
-
initialValue: false
|
|
11632
|
-
});
|
|
11633
|
-
if (p4.isCancel(provideNow) || !provideNow) {
|
|
11634
|
-
return { success: true, message: "\u2192 Email skipped (no token). Re-run later." };
|
|
11635
|
-
}
|
|
11636
|
-
const tokenAnswer = await p4.password({
|
|
11637
|
-
message: "CF token (will be passed via env, not stored)",
|
|
11638
|
-
mask: "\u2022",
|
|
11639
|
-
validate: (v) => String(v ?? "").trim() ? void 0 : "required"
|
|
11640
|
-
});
|
|
11641
|
-
if (p4.isCancel(tokenAnswer)) {
|
|
11642
|
-
return { success: true, message: "\u2192 Email skipped (cancelled)" };
|
|
11643
|
-
}
|
|
11644
|
-
token = String(tokenAnswer).trim();
|
|
11645
|
-
const persist = await p4.confirm({
|
|
11646
|
-
message: "Save to op://DeLoSecrets/Cloudflare-EmailRouting/token for next time?",
|
|
11647
|
-
initialValue: true
|
|
11648
|
-
});
|
|
11649
|
-
if (!p4.isCancel(persist) && persist) {
|
|
11650
|
-
const create = spawnSync6(
|
|
11651
|
-
"op",
|
|
11652
|
-
[
|
|
11653
|
-
"item",
|
|
11654
|
-
"create",
|
|
11655
|
-
"--category=API Credential",
|
|
11656
|
-
"--vault=DeLoSecrets",
|
|
11657
|
-
"--title=Cloudflare-EmailRouting",
|
|
11658
|
-
`token=${token}`
|
|
11659
|
-
],
|
|
11660
|
-
{ stdio: "inherit" }
|
|
11661
|
-
);
|
|
11662
|
-
if (create.status !== 0) {
|
|
11663
|
-
p4.log.warn("Could not store in 1Password \u2014 token is still set for this run.");
|
|
11664
|
-
}
|
|
11665
|
-
}
|
|
12294
|
+
if (ctx.skipEmail !== false) {
|
|
12295
|
+
return { success: true, outcome: "skipped", message: "" };
|
|
11666
12296
|
}
|
|
11667
|
-
|
|
11668
|
-
if (existsSync10(marker)) unlinkSync4(marker);
|
|
11669
|
-
const spinner4 = p4.spinner();
|
|
11670
|
-
spinner4.start("Creating Cloudflare Email Routing rule");
|
|
11671
|
-
const result2 = spawnSync6("bash", [script], {
|
|
11672
|
-
stdio: "inherit",
|
|
11673
|
-
env: { ...process.env, SKIP_EMAIL: "0", CF_EMAIL_ROUTING_TOKEN: token },
|
|
11674
|
-
cwd: roleDir
|
|
11675
|
-
});
|
|
11676
|
-
spinner4.stop(result2.status === 0 ? "\u2713 Email rule created" : "\u2717 Email step failed");
|
|
11677
|
-
if (result2.status !== 0) {
|
|
11678
|
-
return { success: false, message: "Email rule creation failed. See output above." };
|
|
11679
|
-
}
|
|
11680
|
-
return {
|
|
11681
|
-
success: true,
|
|
11682
|
-
message: `\u2713 Email: ${targetRepo}-${role}@delo.sh \u2192 jaradd@gmail.com`
|
|
11683
|
-
};
|
|
12297
|
+
return { success: false, outcome: "failed", message: EMAIL_UNSUPPORTED_MESSAGE };
|
|
11684
12298
|
}
|
|
11685
12299
|
};
|
|
11686
12300
|
|
|
11687
12301
|
// src/commands/hermes/PrintHermesSummary.ts
|
|
11688
|
-
import
|
|
12302
|
+
import { existsSync as existsSync10, readFileSync as readFileSync11 } from "node:fs";
|
|
12303
|
+
import { join as join14 } from "node:path";
|
|
12304
|
+
import YAML5 from "yaml";
|
|
12305
|
+
function readServiceState(roleDir) {
|
|
12306
|
+
if (!roleDir || !existsSync10(join14(roleDir, "role.yaml"))) return { gateway: "planned", heartbeat: "planned" };
|
|
12307
|
+
try {
|
|
12308
|
+
const role = YAML5.parse(readFileSync11(join14(roleDir, "role.yaml"), "utf8"));
|
|
12309
|
+
return {
|
|
12310
|
+
gateway: typeof role?.service_state?.gateway === "string" ? role.service_state.gateway : "unknown",
|
|
12311
|
+
heartbeat: typeof role?.service_state?.heartbeat === "string" ? role.service_state.heartbeat : "unknown"
|
|
12312
|
+
};
|
|
12313
|
+
} catch {
|
|
12314
|
+
return { gateway: "unknown", heartbeat: "unknown" };
|
|
12315
|
+
}
|
|
12316
|
+
}
|
|
12317
|
+
function renderHermesSummary(ctx) {
|
|
12318
|
+
const outcome = ctx.deploymentOutcome ?? (ctx.dryRun ? "planned" : "failed");
|
|
12319
|
+
const service = readServiceState(ctx.roleDir);
|
|
12320
|
+
const deferrals = [...new Set(ctx.deploymentDeferrals ?? [])];
|
|
12321
|
+
const title = outcome === "planned" ? "Hermes deployment plan (no changes applied)" : outcome === "verified-deferred" ? "Hermes deployment healthy with deferred capabilities" : outcome === "verified" ? "Hermes deployment verified" : "Hermes deployment not verified";
|
|
12322
|
+
const lines = [
|
|
12323
|
+
title,
|
|
12324
|
+
`agent_id: ${ctx.agentId ?? "planned"}`,
|
|
12325
|
+
`role_dir: ${ctx.roleDir ?? join14(ctx.targetDir, "agents", "hermes", ctx.role ?? "pm")}`,
|
|
12326
|
+
`runtime: local role runtime (${join14(ctx.roleDir ?? join14(ctx.targetDir, "agents", "hermes", ctx.role ?? "pm"), "runtime")})`,
|
|
12327
|
+
`heartbeat: ${service.heartbeat}`,
|
|
12328
|
+
`gateway: ${service.gateway}`
|
|
12329
|
+
];
|
|
12330
|
+
if (deferrals.length) lines.push(`deferred: ${deferrals.join(", ")}`);
|
|
12331
|
+
for (const assertion of ctx.deploymentPostconditions ?? []) lines.push(`verified: ${assertion}`);
|
|
12332
|
+
if (outcome === "planned") lines.push("Apply by rerunning without --dry-run.");
|
|
12333
|
+
return lines.join("\n");
|
|
12334
|
+
}
|
|
11689
12335
|
var PrintHermesSummary = class extends Command {
|
|
11690
12336
|
async invoke() {
|
|
11691
12337
|
const ctx = this.context;
|
|
11692
|
-
|
|
11693
|
-
const botHandle = `${targetRepo?.toLowerCase().replace(/-/g, "_")}_${role?.toLowerCase()}_bot`;
|
|
11694
|
-
const email = `${targetRepo}-${role}@delo.sh`;
|
|
11695
|
-
const gw = `hermes-${agentId}-gateway.service`;
|
|
11696
|
-
const hb = `hermes-${agentId}-heartbeat.timer`;
|
|
11697
|
-
const lines = [];
|
|
11698
|
-
lines.push(`agent_id ${agentId}`);
|
|
11699
|
-
lines.push(`role dir ${ctx.roleDir}`);
|
|
11700
|
-
lines.push(`runtime gh:${runtimeRepo}`);
|
|
11701
|
-
lines.push(`telegram @${botHandle}${skipTelegram ? " (NOT yet wired)" : ""}`);
|
|
11702
|
-
if (!skipEmail) lines.push(`email ${email}`);
|
|
11703
|
-
lines.push("");
|
|
11704
|
-
lines.push("Start daemons:");
|
|
11705
|
-
lines.push(` systemctl --user start ${hb}`);
|
|
11706
|
-
if (!skipTelegram) {
|
|
11707
|
-
lines.push(` systemctl --user start ${gw}`);
|
|
11708
|
-
} else {
|
|
11709
|
-
lines.push(` # gateway needs Telegram wired first (re-run with --skip-telegram=0)`);
|
|
11710
|
-
}
|
|
11711
|
-
lines.push(" # Bloodbank commands arrive through the fleet-shared Hermes gateway");
|
|
11712
|
-
lines.push("");
|
|
11713
|
-
lines.push("Talk locally:");
|
|
11714
|
-
lines.push(` ${ctx.roleDir}/hermes chat "status"`);
|
|
11715
|
-
if (skipTelegram) {
|
|
11716
|
-
lines.push("");
|
|
11717
|
-
lines.push("Wire Telegram later:");
|
|
11718
|
-
lines.push(" pjangler hermes-agent # re-run and answer yes when asked");
|
|
11719
|
-
}
|
|
11720
|
-
if (!ctx.quiet) {
|
|
11721
|
-
p5.note(lines.join("\n"), `Provisioned ${agentId}`);
|
|
11722
|
-
p5.outro("Done.");
|
|
11723
|
-
}
|
|
11724
|
-
return { success: true, outcome: "unchanged", message: "" };
|
|
12338
|
+
return { success: ctx.deploymentOutcome !== "failed", outcome: "unchanged", message: renderHermesSummary(ctx) };
|
|
11725
12339
|
}
|
|
11726
12340
|
};
|
|
11727
12341
|
|
|
11728
12342
|
// src/commands/hermes/ApplyDeferredExternalEffects.ts
|
|
11729
12343
|
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
11730
|
-
import { existsSync as existsSync11, readFileSync as
|
|
11731
|
-
import { join as
|
|
11732
|
-
import
|
|
12344
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "node:fs";
|
|
12345
|
+
import { join as join15 } from "node:path";
|
|
12346
|
+
import YAML6 from "yaml";
|
|
11733
12347
|
var ApplyDeferredExternalEffects = class extends Command {
|
|
11734
12348
|
async invoke() {
|
|
11735
12349
|
const ctx = this.context;
|
|
11736
12350
|
const selected = ctx.deferredExternalEffects;
|
|
11737
|
-
if (!selected || !selected.
|
|
12351
|
+
if (!selected || !selected.ticketBoard && !selected.systemd) {
|
|
11738
12352
|
return { success: true, outcome: "unchanged", message: "Hermes external effects not selected" };
|
|
11739
12353
|
}
|
|
11740
12354
|
if (!ctx.roleDir) {
|
|
@@ -11748,24 +12362,25 @@ var ApplyDeferredExternalEffects = class extends Command {
|
|
|
11748
12362
|
SKIP_EMAIL: "1",
|
|
11749
12363
|
SKIP_SLACK: "1",
|
|
11750
12364
|
SKIP_BLOODBANK: "1",
|
|
11751
|
-
|
|
12365
|
+
// Role-local runtime is already converged by ApplyDeferredHostEffects.
|
|
12366
|
+
// External consent can never dispatch the retired GitHub runtime model.
|
|
12367
|
+
SKIP_RUNTIME_REPO: "1",
|
|
11752
12368
|
SKIP_PLANE: selected.ticketBoard ? "0" : "1",
|
|
11753
12369
|
SKIP_SYSTEMD: selected.systemd ? "0" : "1"
|
|
11754
12370
|
};
|
|
11755
12371
|
scrubInteractiveChannelCredentials(env2);
|
|
11756
12372
|
if (!selected.ticketBoard) scrubTicketProviderCredentials(env2);
|
|
11757
|
-
const roleManifest =
|
|
12373
|
+
const roleManifest = join15(ctx.roleDir, "role.yaml");
|
|
11758
12374
|
const scripts = [
|
|
11759
|
-
...selected.runtimeRepo ? ["20-runtime-repo.sh"] : [],
|
|
11760
12375
|
...selected.ticketBoard ? ["42-ticket-provider.sh"] : [],
|
|
11761
12376
|
...selected.systemd ? ["70-systemd.sh"] : [],
|
|
11762
|
-
// Refresh fleet metadata after a board binding or
|
|
12377
|
+
// Refresh fleet metadata after a board binding or systemd state
|
|
11763
12378
|
// changes. 80-registry.sh is idempotent and performs no provider call.
|
|
11764
12379
|
"80-registry.sh"
|
|
11765
12380
|
];
|
|
11766
12381
|
const logs = [];
|
|
11767
12382
|
for (const script of scripts) {
|
|
11768
|
-
const path =
|
|
12383
|
+
const path = join15(ctx.roleDir, ".scripts", script);
|
|
11769
12384
|
if (!existsSync11(path)) {
|
|
11770
12385
|
return { success: false, outcome: "failed", message: `Deferred Hermes script is missing: ${path}` };
|
|
11771
12386
|
}
|
|
@@ -11778,8 +12393,8 @@ var ApplyDeferredExternalEffects = class extends Command {
|
|
|
11778
12393
|
}
|
|
11779
12394
|
}
|
|
11780
12395
|
try {
|
|
11781
|
-
const current =
|
|
11782
|
-
const document =
|
|
12396
|
+
const current = readFileSync12(roleManifest, "utf8");
|
|
12397
|
+
const document = YAML6.parseDocument(current);
|
|
11783
12398
|
if (document.errors.length) throw document.errors[0];
|
|
11784
12399
|
document.setIn(["deployment", "local_only"], Boolean(ctx.local));
|
|
11785
12400
|
document.setIn(["deployment", "systemd"], selected.systemd ? "required" : "deferred");
|
|
@@ -11804,7 +12419,7 @@ ${logs.join("\n")}` : ""}`
|
|
|
11804
12419
|
// src/commands/hermes/ApplyDeferredHostEffects.ts
|
|
11805
12420
|
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
11806
12421
|
import { existsSync as existsSync12 } from "node:fs";
|
|
11807
|
-
import { join as
|
|
12422
|
+
import { join as join16 } from "node:path";
|
|
11808
12423
|
var ApplyDeferredHostEffects = class extends Command {
|
|
11809
12424
|
async invoke() {
|
|
11810
12425
|
const ctx = this.context;
|
|
@@ -11830,16 +12445,18 @@ var ApplyDeferredHostEffects = class extends Command {
|
|
|
11830
12445
|
SKIP_EMAIL: "1",
|
|
11831
12446
|
SKIP_SLACK: "1",
|
|
11832
12447
|
SKIP_BLOODBANK: "1",
|
|
11833
|
-
|
|
12448
|
+
// The legacy name controls role-local runtime/profile convergence only.
|
|
12449
|
+
// It is a required host/local phase, never a GitHub repository effect.
|
|
12450
|
+
SKIP_RUNTIME_REPO: "0",
|
|
11834
12451
|
SKIP_PLANE: "1",
|
|
11835
12452
|
SKIP_SYSTEMD: "1"
|
|
11836
12453
|
};
|
|
11837
12454
|
scrubTicketProviderCredentials(env2);
|
|
11838
12455
|
scrubInteractiveChannelCredentials(env2);
|
|
11839
|
-
const scripts = ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh"];
|
|
12456
|
+
const scripts = ["01-config.sh", "05-fleet-env.sh", "10-hermes-profile.sh", "20-runtime-repo.sh"];
|
|
11840
12457
|
const logs = [];
|
|
11841
12458
|
for (const script of scripts) {
|
|
11842
|
-
const path =
|
|
12459
|
+
const path = join16(ctx.roleDir, ".scripts", script);
|
|
11843
12460
|
if (!existsSync12(path)) {
|
|
11844
12461
|
return { success: false, outcome: "failed", message: `Deferred Hermes host script is missing: ${path}` };
|
|
11845
12462
|
}
|
|
@@ -11863,7 +12480,56 @@ ${logs.join("\n")}` : ""}`
|
|
|
11863
12480
|
// src/recipes/HermesAgentRecipe.ts
|
|
11864
12481
|
init_tree_diff();
|
|
11865
12482
|
init_preflight();
|
|
11866
|
-
import {
|
|
12483
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
|
|
12484
|
+
import { join as join17, resolve as resolve6 } from "node:path";
|
|
12485
|
+
import YAML7 from "yaml";
|
|
12486
|
+
function deploymentDeferrals(ctx) {
|
|
12487
|
+
const deferred = [];
|
|
12488
|
+
if (ctx.local || ctx.skipPlane) deferred.push("ticket-board provisioning");
|
|
12489
|
+
if (ctx.local || ctx.skipSystemd) deferred.push("systemd service activation");
|
|
12490
|
+
const projectManifest = join17(ctx.targetDir, ".project.json");
|
|
12491
|
+
if (!ctx.skipPlane && existsSync13(projectManifest)) {
|
|
12492
|
+
try {
|
|
12493
|
+
const project = JSON.parse(readFileSync13(projectManifest, "utf8"));
|
|
12494
|
+
const state = project.ticket_provider?.state;
|
|
12495
|
+
if (typeof state === "string" && state !== "linked") deferred.push(`ticket board (${state})`);
|
|
12496
|
+
} catch {
|
|
12497
|
+
deferred.push("ticket board state unreadable");
|
|
12498
|
+
}
|
|
12499
|
+
}
|
|
12500
|
+
const rolePath = ctx.roleDir ? join17(ctx.roleDir, "role.yaml") : "";
|
|
12501
|
+
if (rolePath && existsSync13(rolePath)) {
|
|
12502
|
+
try {
|
|
12503
|
+
const role = YAML7.parse(readFileSync13(rolePath, "utf8"));
|
|
12504
|
+
const gateway = role?.service_state?.gateway;
|
|
12505
|
+
const heartbeat = role?.service_state?.heartbeat;
|
|
12506
|
+
if (typeof gateway === "string" && gateway !== "active") deferred.push(`gateway (${gateway})`);
|
|
12507
|
+
if (typeof heartbeat === "string" && heartbeat !== "active") deferred.push(`heartbeat (${heartbeat})`);
|
|
12508
|
+
} catch {
|
|
12509
|
+
deferred.push("service state unreadable");
|
|
12510
|
+
}
|
|
12511
|
+
} else if (!ctx.dryRun) {
|
|
12512
|
+
deferred.push("role service state unavailable");
|
|
12513
|
+
}
|
|
12514
|
+
return [...new Set(deferred)];
|
|
12515
|
+
}
|
|
12516
|
+
async function summaryResult(ctx) {
|
|
12517
|
+
const summary = await new PrintHermesSummary(ctx).invoke();
|
|
12518
|
+
return {
|
|
12519
|
+
recipeId: "hermes-agent",
|
|
12520
|
+
ok: summary.success,
|
|
12521
|
+
dryRun: Boolean(ctx.dryRun),
|
|
12522
|
+
changedFiles: [],
|
|
12523
|
+
logs: summary.message ? [summary.message] : [],
|
|
12524
|
+
errors: summary.success ? [] : [summary.message || "Hermes summary could not be rendered"],
|
|
12525
|
+
phases: [{
|
|
12526
|
+
id: "hermes.summary",
|
|
12527
|
+
status: summary.success ? "unchanged" : "failed",
|
|
12528
|
+
changedFiles: [],
|
|
12529
|
+
message: summary.message || void 0
|
|
12530
|
+
}]
|
|
12531
|
+
};
|
|
12532
|
+
}
|
|
11867
12533
|
var HermesAgentRecipe = class extends Recipe {
|
|
11868
12534
|
checks = createHermesChecks();
|
|
11869
12535
|
metadata = {
|
|
@@ -11872,13 +12538,14 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
11872
12538
|
description: "Add and reconcile a Hermes agent role",
|
|
11873
12539
|
dependencies: [],
|
|
11874
12540
|
commands: [
|
|
11875
|
-
"EnsureTemplateConfig",
|
|
11876
12541
|
"PromptForAgentConfig",
|
|
12542
|
+
"ValidateHermesOptions",
|
|
12543
|
+
"EnsureTemplateConfig",
|
|
11877
12544
|
"RunCopierTemplate",
|
|
11878
12545
|
"UntrackHermesRuntimes",
|
|
11879
12546
|
"WireTelegram",
|
|
11880
12547
|
"WireEmail",
|
|
11881
|
-
"PrintHermesSummary"
|
|
12548
|
+
"PrintHermesSummary (postconditions only)"
|
|
11882
12549
|
],
|
|
11883
12550
|
publicRuleIds: this.checks.map((check) => check.id)
|
|
11884
12551
|
};
|
|
@@ -11889,13 +12556,13 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
11889
12556
|
const errors = [];
|
|
11890
12557
|
const changedFiles = [];
|
|
11891
12558
|
const ingredients = [
|
|
11892
|
-
EnsureTemplateConfig,
|
|
11893
12559
|
PromptForAgentConfig,
|
|
12560
|
+
ValidateHermesOptions,
|
|
12561
|
+
EnsureTemplateConfig,
|
|
11894
12562
|
RunCopierTemplate,
|
|
11895
12563
|
UntrackHermesRuntimes,
|
|
11896
12564
|
WireTelegram,
|
|
11897
|
-
WireEmail
|
|
11898
|
-
PrintHermesSummary
|
|
12565
|
+
WireEmail
|
|
11899
12566
|
];
|
|
11900
12567
|
for (const [ingredientIndex, CommandClass] of ingredients.entries()) {
|
|
11901
12568
|
if (typeof CommandClass !== "function") {
|
|
@@ -11964,30 +12631,46 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
11964
12631
|
if (!commandResult.ok) return commandResult;
|
|
11965
12632
|
const lifecycle = await this.initializeOwnedChecks(ctx);
|
|
11966
12633
|
const localResult = mergeInitResults(this.metadata.id, Boolean(ctx.dryRun), [commandResult, lifecycle]);
|
|
11967
|
-
if (!localResult.ok
|
|
12634
|
+
if (!localResult.ok) return localResult;
|
|
11968
12635
|
const hermesContext = ctx;
|
|
11969
|
-
if (
|
|
12636
|
+
if (ctx.dryRun) {
|
|
12637
|
+
hermesContext.deploymentOutcome = "planned";
|
|
12638
|
+
hermesContext.deploymentDeferrals = deploymentDeferrals(hermesContext);
|
|
12639
|
+
hermesContext.deploymentPostconditions = ["dry-run made no repository or host changes"];
|
|
12640
|
+
return mergeInitResults(this.metadata.id, true, [localResult, await summaryResult(ctx)]);
|
|
12641
|
+
}
|
|
11970
12642
|
const selected = hermesContext.deferredExternalEffects;
|
|
11971
|
-
if (
|
|
11972
|
-
|
|
11973
|
-
|
|
11974
|
-
|
|
11975
|
-
|
|
11976
|
-
|
|
11977
|
-
|
|
11978
|
-
|
|
11979
|
-
|
|
11980
|
-
|
|
11981
|
-
|
|
11982
|
-
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
|
|
11989
|
-
|
|
11990
|
-
|
|
12643
|
+
if (selected?.owner === "project" || ctx.quiet && !selected) return localResult;
|
|
12644
|
+
let appliedResult = localResult;
|
|
12645
|
+
if (selected?.owner === "hermes" && (selected.ticketBoard || selected.systemd)) {
|
|
12646
|
+
const beforeExternal = snapshotTree(ctx.targetDir);
|
|
12647
|
+
const external = await new ApplyDeferredExternalEffects(ctx).invoke();
|
|
12648
|
+
const externalChanges = changedTreePaths(ctx.targetDir, beforeExternal, snapshotTree(ctx.targetDir));
|
|
12649
|
+
const externalResult = {
|
|
12650
|
+
recipeId: this.metadata.id,
|
|
12651
|
+
ok: external.success,
|
|
12652
|
+
dryRun: false,
|
|
12653
|
+
changedFiles: externalChanges,
|
|
12654
|
+
logs: external.message ? [external.message] : [],
|
|
12655
|
+
errors: external.success ? [] : [external.message || "Deferred Hermes external effects failed"],
|
|
12656
|
+
phases: [{
|
|
12657
|
+
id: "hermes.external-effects",
|
|
12658
|
+
status: external.success ? "changed" : "failed",
|
|
12659
|
+
changedFiles: external.success ? externalChanges : [],
|
|
12660
|
+
message: external.message || void 0
|
|
12661
|
+
}]
|
|
12662
|
+
};
|
|
12663
|
+
appliedResult = mergeInitResults(this.metadata.id, false, [localResult, externalResult]);
|
|
12664
|
+
if (!externalResult.ok) return appliedResult;
|
|
12665
|
+
}
|
|
12666
|
+
const crossChecks = [
|
|
12667
|
+
...createMiseChecks().filter((check) => check.id === "mise.config-root"),
|
|
12668
|
+
...createProjectChecks().filter((check) => check.id === "sot.project-json")
|
|
12669
|
+
];
|
|
12670
|
+
const findings = [
|
|
12671
|
+
...this.audit(ctx),
|
|
12672
|
+
...crossChecks.map((check) => check.audit(ctx))
|
|
12673
|
+
].filter((finding2) => finding2.status !== "pass" && finding2.status !== "skip");
|
|
11991
12674
|
const verification = {
|
|
11992
12675
|
recipeId: this.metadata.id,
|
|
11993
12676
|
ok: findings.length === 0,
|
|
@@ -12002,7 +12685,18 @@ var HermesAgentRecipe = class extends Recipe {
|
|
|
12002
12685
|
message: findings.length ? "Hermes postcondition audit failed" : "Hermes postcondition audit passed"
|
|
12003
12686
|
}]
|
|
12004
12687
|
};
|
|
12005
|
-
|
|
12688
|
+
const verifiedResult = mergeInitResults(this.metadata.id, false, [appliedResult, verification]);
|
|
12689
|
+
if (!verification.ok) {
|
|
12690
|
+
hermesContext.deploymentOutcome = "failed";
|
|
12691
|
+
return verifiedResult;
|
|
12692
|
+
}
|
|
12693
|
+
hermesContext.deploymentDeferrals = deploymentDeferrals(hermesContext);
|
|
12694
|
+
hermesContext.deploymentOutcome = hermesContext.deploymentDeferrals.length ? "verified-deferred" : "verified";
|
|
12695
|
+
hermesContext.deploymentPostconditions = [
|
|
12696
|
+
"Hermes lifecycle audit passed",
|
|
12697
|
+
"mise PATH and canonical project manifest passed"
|
|
12698
|
+
];
|
|
12699
|
+
return mergeInitResults(this.metadata.id, false, [verifiedResult, await summaryResult(ctx)]);
|
|
12006
12700
|
}
|
|
12007
12701
|
printNextSteps() {
|
|
12008
12702
|
}
|
|
@@ -12181,22 +12875,22 @@ var NodeRecipe = class extends Recipe {
|
|
|
12181
12875
|
// src/recipes/ProjectRecipe.ts
|
|
12182
12876
|
init_project();
|
|
12183
12877
|
import { spawnSync as spawnSync13 } from "node:child_process";
|
|
12184
|
-
import { existsSync as
|
|
12185
|
-
import { dirname as dirname11, isAbsolute as isAbsolute5, join as
|
|
12878
|
+
import { existsSync as existsSync19, lstatSync as lstatSync12, readFileSync as readFileSync19, rmSync as rmSync5 } from "node:fs";
|
|
12879
|
+
import { dirname as dirname11, isAbsolute as isAbsolute5, join as join24, relative as relativePath, resolve as resolve13 } from "node:path";
|
|
12186
12880
|
init_tree_diff();
|
|
12187
12881
|
|
|
12188
12882
|
// src/recipes/NotebookRecipe.ts
|
|
12189
12883
|
init_project();
|
|
12190
|
-
import { existsSync as
|
|
12191
|
-
import { join as
|
|
12884
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "node:fs";
|
|
12885
|
+
import { join as join23 } from "node:path";
|
|
12192
12886
|
|
|
12193
12887
|
// src/notebook/checks.ts
|
|
12194
12888
|
init_config();
|
|
12195
12889
|
init_notes();
|
|
12196
12890
|
init_output();
|
|
12197
12891
|
init_state();
|
|
12198
|
-
import { existsSync as
|
|
12199
|
-
import { join as
|
|
12892
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "node:fs";
|
|
12893
|
+
import { join as join19 } from "node:path";
|
|
12200
12894
|
var NOTEBOOK_RULE_IDS = [
|
|
12201
12895
|
"notebook.configuration",
|
|
12202
12896
|
"notebook.binding",
|
|
@@ -12213,10 +12907,10 @@ function result(check, status, summary, changedFiles = [], details = []) {
|
|
|
12213
12907
|
return { id: check.id, title: check.title, status, summary, changedFiles, details };
|
|
12214
12908
|
}
|
|
12215
12909
|
function manifestNotebook(repo) {
|
|
12216
|
-
const path =
|
|
12217
|
-
if (!
|
|
12910
|
+
const path = join19(repo, ".project.json");
|
|
12911
|
+
if (!existsSync16(path)) return null;
|
|
12218
12912
|
try {
|
|
12219
|
-
const parsed = JSON.parse(
|
|
12913
|
+
const parsed = JSON.parse(readFileSync16(path, "utf8"));
|
|
12220
12914
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
12221
12915
|
const notebook = parsed.notebook;
|
|
12222
12916
|
return notebook && typeof notebook === "object" && !Array.isArray(notebook) ? notebook : null;
|
|
@@ -12484,8 +13178,8 @@ init_notes();
|
|
|
12484
13178
|
init_git_evidence();
|
|
12485
13179
|
init_types();
|
|
12486
13180
|
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
12487
|
-
import { realpathSync as
|
|
12488
|
-
import { relative as
|
|
13181
|
+
import { realpathSync as realpathSync7 } from "node:fs";
|
|
13182
|
+
import { relative as relative10, resolve as resolve10, sep as sep5 } from "node:path";
|
|
12489
13183
|
var DEFAULT_OVERVIEW_REFERENCES = [".project.json", "README.md", "AGENTS.md", "CLAUDE.md", "docs/architecture.md"];
|
|
12490
13184
|
function git2(repo, args, timeout) {
|
|
12491
13185
|
const result2 = spawnSync10("git", args, { cwd: repo, encoding: "utf8", maxBuffer: 1024 * 1024, timeout, shell: false });
|
|
@@ -12493,9 +13187,9 @@ function git2(repo, args, timeout) {
|
|
|
12493
13187
|
}
|
|
12494
13188
|
function normalizedReference(repo, value) {
|
|
12495
13189
|
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}`);
|
|
12496
|
-
const root =
|
|
13190
|
+
const root = realpathSync7(repo);
|
|
12497
13191
|
const candidate = resolve10(root, value);
|
|
12498
|
-
const rel =
|
|
13192
|
+
const rel = relative10(root, candidate).split(sep5).join("/");
|
|
12499
13193
|
if (!rel || rel === ".." || rel.startsWith("../")) throw new NotebookError("INVALID_INPUT", `Overview reference escapes the repository: ${value}`);
|
|
12500
13194
|
return rel.normalize("NFC");
|
|
12501
13195
|
}
|
|
@@ -12621,44 +13315,21 @@ init_reconcile();
|
|
|
12621
13315
|
init_config();
|
|
12622
13316
|
|
|
12623
13317
|
// src/notebook/hooks.ts
|
|
12624
|
-
|
|
12625
|
-
import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
|
|
12626
|
-
import { chmodSync as chmodSync4, closeSync as closeSync6, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync16, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync9, mkdirSync as mkdirSync7, openSync as openSync6, readFileSync as readFileSync14, readSync as readSync3, readdirSync as readdirSync7, realpathSync as realpathSync7, renameSync as renameSync5, rmSync as rmSync3, symlinkSync as symlinkSync2, unlinkSync as unlinkSync6, writeFileSync as writeFileSync10 } from "node:fs";
|
|
12627
|
-
import { basename as basename7, dirname as dirname10, isAbsolute as isAbsolute3, join as join20, parse as parse3, relative as relative10, resolve as resolve11, sep as sep6 } from "node:path";
|
|
12628
|
-
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
12629
|
-
|
|
12630
|
-
// src/utils/version.ts
|
|
12631
|
-
import { readFileSync as readFileSync13 } from "node:fs";
|
|
12632
|
-
import { dirname as dirname9, join as join19 } from "node:path";
|
|
12633
|
-
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
12634
|
-
var PJANGLER_VERSION = (() => {
|
|
12635
|
-
try {
|
|
12636
|
-
let dir = dirname9(fileURLToPath4(import.meta.url));
|
|
12637
|
-
for (let i = 0; i < 4; i++) {
|
|
12638
|
-
try {
|
|
12639
|
-
const raw = readFileSync13(join19(dir, "package.json"), "utf8");
|
|
12640
|
-
return JSON.parse(raw).version ?? "0.0.0";
|
|
12641
|
-
} catch {
|
|
12642
|
-
const parent = dirname9(dir);
|
|
12643
|
-
if (parent === dir) break;
|
|
12644
|
-
dir = parent;
|
|
12645
|
-
}
|
|
12646
|
-
}
|
|
12647
|
-
} catch {
|
|
12648
|
-
}
|
|
12649
|
-
return "0.0.0";
|
|
12650
|
-
})();
|
|
12651
|
-
|
|
12652
|
-
// src/notebook/hooks.ts
|
|
13318
|
+
init_version();
|
|
12653
13319
|
init_git_evidence();
|
|
12654
13320
|
init_notes();
|
|
12655
13321
|
init_state();
|
|
12656
13322
|
init_types();
|
|
13323
|
+
import { spawn, spawnSync as spawnSync11 } from "node:child_process";
|
|
13324
|
+
import { createHash as createHash7, randomUUID as randomUUID5 } from "node:crypto";
|
|
13325
|
+
import { chmodSync as chmodSync4, closeSync as closeSync6, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync17, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync11, mkdirSync as mkdirSync7, openSync as openSync6, readFileSync as readFileSync17, readSync as readSync3, readdirSync as readdirSync8, realpathSync as realpathSync8, renameSync as renameSync6, rmSync as rmSync4, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
13326
|
+
import { basename as basename8, dirname as dirname10, isAbsolute as isAbsolute3, join as join22, parse as parse3, relative as relative11, resolve as resolve11, sep as sep6 } from "node:path";
|
|
13327
|
+
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
12657
13328
|
function bundledSkillCandidates() {
|
|
12658
13329
|
const candidates = [];
|
|
12659
13330
|
let cursor = dirname10(fileURLToPath5(import.meta.url));
|
|
12660
13331
|
for (let depth = 0; depth < 8; depth++) {
|
|
12661
|
-
candidates.push(
|
|
13332
|
+
candidates.push(join22(cursor, "dist", "assets", "project-notebook-skill"), join22(cursor, "assets", "project-notebook-skill"));
|
|
12662
13333
|
const parent = dirname10(cursor);
|
|
12663
13334
|
if (parent === cursor) break;
|
|
12664
13335
|
cursor = parent;
|
|
@@ -12676,13 +13347,13 @@ function assertOwnedSkillTree(source) {
|
|
|
12676
13347
|
assertNoSymlinkComponents2(source);
|
|
12677
13348
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
12678
13349
|
const walk = (directory) => {
|
|
12679
|
-
const directoryStat =
|
|
13350
|
+
const directoryStat = lstatSync11(directory);
|
|
12680
13351
|
if (!directoryStat.isDirectory() || directoryStat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-directory component");
|
|
12681
13352
|
if (uid !== void 0 && directoryStat.uid !== uid) throw new NotebookError("CONFLICT", "Project Notebook skill source is not owned by the current user");
|
|
12682
13353
|
if (directoryStat.mode & 3586) throw new NotebookError("CONFLICT", "Project Notebook skill source has unsafe directory mode bits");
|
|
12683
|
-
for (const entry of
|
|
12684
|
-
const path =
|
|
12685
|
-
const stat =
|
|
13354
|
+
for (const entry of readdirSync8(directory, { withFileTypes: true })) {
|
|
13355
|
+
const path = join22(directory, entry.name);
|
|
13356
|
+
const stat = lstatSync11(path);
|
|
12686
13357
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a symlink");
|
|
12687
13358
|
if (entry.isDirectory()) walk(path);
|
|
12688
13359
|
else if (!entry.isFile()) throw new NotebookError("CONFLICT", "Project Notebook skill source contains a non-regular entry");
|
|
@@ -12697,17 +13368,17 @@ function assertOwnedSkillTree(source) {
|
|
|
12697
13368
|
function enumerateSkillPayload(source) {
|
|
12698
13369
|
const result2 = [];
|
|
12699
13370
|
const walk = (directory) => {
|
|
12700
|
-
for (const entry of
|
|
12701
|
-
const path =
|
|
12702
|
-
const rel =
|
|
13371
|
+
for (const entry of readdirSync8(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name, "en"))) {
|
|
13372
|
+
const path = join22(directory, entry.name);
|
|
13373
|
+
const rel = relative11(source, path).split(sep6).join("/");
|
|
12703
13374
|
if (!rel.includes("/") && (rel === "export-manifest.json" || rel === "SHA256SUMS" || rel === ".source.yaml")) continue;
|
|
12704
13375
|
if (!safeSkillRelativePath(rel)) throw new NotebookError("CONFLICT", `Project Notebook skill export path is unsafe: ${rel}`);
|
|
12705
|
-
const stat =
|
|
13376
|
+
const stat = lstatSync11(path);
|
|
12706
13377
|
if (stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a symlink");
|
|
12707
13378
|
if (entry.isDirectory()) walk(path);
|
|
12708
13379
|
else if (entry.isFile()) {
|
|
12709
13380
|
const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
|
|
12710
|
-
result2.push({ path: rel, sha256: createHash7("sha256").update(
|
|
13381
|
+
result2.push({ path: rel, sha256: createHash7("sha256").update(readFileSync17(path)).digest("hex"), mode: expectedMode });
|
|
12711
13382
|
} else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
12712
13383
|
}
|
|
12713
13384
|
};
|
|
@@ -12715,9 +13386,9 @@ function enumerateSkillPayload(source) {
|
|
|
12715
13386
|
return result2.sort((a, b) => a.path.localeCompare(b.path, "en"));
|
|
12716
13387
|
}
|
|
12717
13388
|
function parsePackedManifest(source) {
|
|
12718
|
-
const manifestPath =
|
|
12719
|
-
if (!
|
|
12720
|
-
const value = JSON.parse(
|
|
13389
|
+
const manifestPath = join22(source, "export-manifest.json");
|
|
13390
|
+
if (!existsSync17(manifestPath)) return null;
|
|
13391
|
+
const value = JSON.parse(readFileSync17(manifestPath, "utf8"));
|
|
12721
13392
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is invalid");
|
|
12722
13393
|
const manifest = value;
|
|
12723
13394
|
if (manifest.schema_version !== 1 || manifest.skill !== "project-notebook" || !Array.isArray(manifest.files)) throw new NotebookError("CONFLICT", "Project Notebook skill export manifest is incompatible");
|
|
@@ -12726,10 +13397,10 @@ function parsePackedManifest(source) {
|
|
|
12726
13397
|
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");
|
|
12727
13398
|
if (!safeSkillRelativePath(entry.path) || paths.has(entry.path)) throw new NotebookError("CONFLICT", "Project Notebook skill export path is unsafe or duplicated");
|
|
12728
13399
|
paths.add(entry.path);
|
|
12729
|
-
const path =
|
|
12730
|
-
const stat =
|
|
13400
|
+
const path = join22(source, ...entry.path.split("/"));
|
|
13401
|
+
const stat = lstatSync11(path);
|
|
12731
13402
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
12732
|
-
const actual = createHash7("sha256").update(
|
|
13403
|
+
const actual = createHash7("sha256").update(readFileSync17(path)).digest("hex");
|
|
12733
13404
|
if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
|
|
12734
13405
|
const actualMode = stat.mode & 511;
|
|
12735
13406
|
const executable = entry.mode === "0755";
|
|
@@ -12741,12 +13412,12 @@ function parsePackedManifest(source) {
|
|
|
12741
13412
|
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");
|
|
12742
13413
|
const sums = `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
12743
13414
|
`;
|
|
12744
|
-
if (!
|
|
13415
|
+
if (!existsSync17(join22(source, "SHA256SUMS")) || readFileSync17(join22(source, "SHA256SUMS"), "utf8") !== sums) throw new NotebookError("CONFLICT", "Project Notebook skill SHA256SUMS is missing or stale");
|
|
12745
13416
|
return manifest;
|
|
12746
13417
|
}
|
|
12747
13418
|
function expectedPackedSkill() {
|
|
12748
13419
|
for (const candidate of bundledSkillCandidates()) {
|
|
12749
|
-
if (!
|
|
13420
|
+
if (!existsSync17(join22(candidate, "export-manifest.json"))) continue;
|
|
12750
13421
|
assertOwnedSkillTree(candidate);
|
|
12751
13422
|
const manifest = parsePackedManifest(candidate);
|
|
12752
13423
|
if (!manifest) continue;
|
|
@@ -12813,13 +13484,13 @@ function verifyProjectNotebookSkillExport(source) {
|
|
|
12813
13484
|
function resolveProjectNotebookSkillSource(env2 = process.env) {
|
|
12814
13485
|
if (env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT) {
|
|
12815
13486
|
const explicit = resolve11(env2.PJ_PROJECT_NOTEBOOK_SKILL_ROOT);
|
|
12816
|
-
if (!
|
|
13487
|
+
if (!existsSync17(join22(explicit, "SKILL.md"))) throw new NotebookError("NOT_CONFIGURED", "Configured Project Notebook skill source is unavailable");
|
|
12817
13488
|
verifyProjectNotebookSkillExport(explicit);
|
|
12818
13489
|
return explicit;
|
|
12819
13490
|
}
|
|
12820
13491
|
if (env2.PJ_SKILLS_REGISTRY_ROOT) {
|
|
12821
13492
|
const canonical = resolve11(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
|
|
12822
|
-
if (
|
|
13493
|
+
if (existsSync17(join22(canonical, "SKILL.md"))) {
|
|
12823
13494
|
verifyProjectNotebookSkillExport(canonical);
|
|
12824
13495
|
return canonical;
|
|
12825
13496
|
}
|
|
@@ -12834,7 +13505,7 @@ var REPAIR_COMMAND = "pj notebook skill --apply";
|
|
|
12834
13505
|
function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
12835
13506
|
let rootLink;
|
|
12836
13507
|
try {
|
|
12837
|
-
rootLink =
|
|
13508
|
+
rootLink = lstatSync11(skillsRoot);
|
|
12838
13509
|
} catch (error) {
|
|
12839
13510
|
if (error.code === "ENOENT") return { state: "absent" };
|
|
12840
13511
|
throw error;
|
|
@@ -12845,21 +13516,21 @@ function probeCanonicalSkillexRootProjection(skillsRoot, link) {
|
|
|
12845
13516
|
assertNoSymlinkComponents2(dirname10(skillsRoot));
|
|
12846
13517
|
const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
12847
13518
|
if (uid !== void 0 && rootLink.uid !== uid) throw new Error("owner");
|
|
12848
|
-
const globalRoot =
|
|
13519
|
+
const globalRoot = realpathSync8(skillsRoot);
|
|
12849
13520
|
assertNoSymlinkComponents2(globalRoot);
|
|
12850
|
-
const globalStat =
|
|
13521
|
+
const globalStat = lstatSync11(globalRoot);
|
|
12851
13522
|
if (!globalStat.isDirectory() || globalStat.isSymbolicLink()) throw new Error("root-type");
|
|
12852
13523
|
if (uid !== void 0 && globalStat.uid !== uid) throw new Error("root-owner");
|
|
12853
13524
|
if (globalStat.mode & 3586) throw new Error("root-mode");
|
|
12854
13525
|
const skillSetsRoot = dirname10(globalRoot);
|
|
12855
|
-
if (
|
|
13526
|
+
if (basename8(globalRoot) !== "global" || basename8(skillSetsRoot) !== "skill-sets") throw new Error("layout");
|
|
12856
13527
|
const checkoutRoot = dirname10(skillSetsRoot);
|
|
12857
|
-
const expectedSource =
|
|
12858
|
-
const linkStat =
|
|
13528
|
+
const expectedSource = join22(checkoutRoot, "all-skills", "project-notebook");
|
|
13529
|
+
const linkStat = lstatSync11(link);
|
|
12859
13530
|
if (!linkStat.isSymbolicLink()) throw new Error("projection-type");
|
|
12860
13531
|
if (uid !== void 0 && linkStat.uid !== uid) throw new Error("projection-owner");
|
|
12861
|
-
const projectedSource =
|
|
12862
|
-
if (projectedSource !==
|
|
13532
|
+
const projectedSource = realpathSync8(link);
|
|
13533
|
+
if (projectedSource !== realpathSync8(expectedSource)) throw new Error("projection-target");
|
|
12863
13534
|
if (!isVerifiedCanonicalSkillexProjection(projectedSource)) {
|
|
12864
13535
|
return {
|
|
12865
13536
|
state: "declined",
|
|
@@ -12892,27 +13563,27 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
12892
13563
|
const digest = createHash7("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
12893
13564
|
const home = env2.HOME;
|
|
12894
13565
|
if (!home || !resolve11(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
|
|
12895
|
-
const skillsRoot =
|
|
12896
|
-
const link =
|
|
13566
|
+
const skillsRoot = join22(home, ".agents", "skills");
|
|
13567
|
+
const link = join22(skillsRoot, "project-notebook");
|
|
12897
13568
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
12898
13569
|
if (probe.state === "adopted") return { installed: false, path: link, digest };
|
|
12899
13570
|
if (probe.state === "declined") return { installed: false, path: link, digest, blocked: probe.block };
|
|
12900
|
-
const dataRoot = resolve11(env2.XDG_DATA_HOME ||
|
|
12901
|
-
const payload =
|
|
13571
|
+
const dataRoot = resolve11(env2.XDG_DATA_HOME || join22(home, ".local", "share"), "pjangler", "skills", "project-notebook");
|
|
13572
|
+
const payload = join22(dataRoot, `${PJANGLER_VERSION}-${digest}`);
|
|
12902
13573
|
assertNoSymlinkComponents2(dirname10(dataRoot), true);
|
|
12903
13574
|
mkdirSync7(dataRoot, { recursive: true, mode: 448 });
|
|
12904
13575
|
assertNoSymlinkComponents2(dataRoot);
|
|
12905
|
-
const dataStat =
|
|
13576
|
+
const dataStat = lstatSync11(dataRoot);
|
|
12906
13577
|
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");
|
|
12907
13578
|
chmodSync4(dataRoot, 448);
|
|
12908
|
-
if (!
|
|
12909
|
-
const staging =
|
|
13579
|
+
if (!existsSync17(payload)) {
|
|
13580
|
+
const staging = join22(dataRoot, `.staging-${randomUUID5()}`);
|
|
12910
13581
|
mkdirSync7(staging, { recursive: false, mode: 448 });
|
|
12911
13582
|
try {
|
|
12912
13583
|
for (const entry of manifest.files) {
|
|
12913
|
-
const destination =
|
|
13584
|
+
const destination = join22(staging, ...entry.path.split("/"));
|
|
12914
13585
|
mkdirSync7(dirname10(destination), { recursive: true, mode: 493 });
|
|
12915
|
-
copyFileSync3(
|
|
13586
|
+
copyFileSync3(join22(source, ...entry.path.split("/")), destination);
|
|
12916
13587
|
const mode = entry.mode === "0755" ? 493 : 420;
|
|
12917
13588
|
chmodSync4(destination, mode);
|
|
12918
13589
|
const fd = openSync6(destination, constants5.O_RDONLY | (constants5.O_NOFOLLOW ?? 0));
|
|
@@ -12924,27 +13595,27 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
12924
13595
|
closeSync6(fd);
|
|
12925
13596
|
}
|
|
12926
13597
|
}
|
|
12927
|
-
writeFileSync10(
|
|
13598
|
+
writeFileSync10(join22(staging, "export-manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
12928
13599
|
`, { mode: 420, flag: "wx" });
|
|
12929
|
-
writeFileSync10(
|
|
13600
|
+
writeFileSync10(join22(staging, "SHA256SUMS"), `${manifest.files.map((entry) => `${entry.sha256} ${entry.path}`).join("\n")}
|
|
12930
13601
|
`, { mode: 420, flag: "wx" });
|
|
12931
13602
|
verifyProjectNotebookSkillExport(staging);
|
|
12932
|
-
|
|
13603
|
+
renameSync6(staging, payload);
|
|
12933
13604
|
} finally {
|
|
12934
|
-
if (
|
|
13605
|
+
if (existsSync17(staging)) rmSync4(staging, { recursive: true, force: true });
|
|
12935
13606
|
}
|
|
12936
13607
|
} else {
|
|
12937
|
-
const stat =
|
|
13608
|
+
const stat = lstatSync11(payload);
|
|
12938
13609
|
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Installed Project Notebook payload is not a real directory");
|
|
12939
13610
|
verifyProjectNotebookSkillExport(payload);
|
|
12940
13611
|
}
|
|
12941
13612
|
assertNoSymlinkComponents2(skillsRoot, true);
|
|
12942
13613
|
mkdirSync7(skillsRoot, { recursive: true, mode: 448 });
|
|
12943
13614
|
assertNoSymlinkComponents2(skillsRoot);
|
|
12944
|
-
let linkExists =
|
|
13615
|
+
let linkExists = existsSync17(link);
|
|
12945
13616
|
if (!linkExists) {
|
|
12946
13617
|
try {
|
|
12947
|
-
|
|
13618
|
+
lstatSync11(link);
|
|
12948
13619
|
linkExists = true;
|
|
12949
13620
|
} catch {
|
|
12950
13621
|
}
|
|
@@ -12961,10 +13632,10 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
12961
13632
|
repair: REPAIR_COMMAND
|
|
12962
13633
|
}
|
|
12963
13634
|
});
|
|
12964
|
-
const stat =
|
|
13635
|
+
const stat = lstatSync11(link);
|
|
12965
13636
|
if (!stat.isSymbolicLink()) return foreign(["the path is a real file or directory, not a PJ\xE1ngler-owned link"]);
|
|
12966
|
-
const target =
|
|
12967
|
-
if (target !==
|
|
13637
|
+
const target = realpathSync8(link);
|
|
13638
|
+
if (target !== realpathSync8(payload) && !isVerifiedCanonicalSkillexProjection(target)) {
|
|
12968
13639
|
return foreign(isCanonicalSkillexProjectionPath(target) ? describeProjectNotebookSkillDrift(target) : [`the link targets ${target}, which is neither the pinned payload nor a canonical Skillex projection`]);
|
|
12969
13640
|
}
|
|
12970
13641
|
try {
|
|
@@ -12982,25 +13653,25 @@ function collectSupersededPayloads(dataRoot, keep) {
|
|
|
12982
13653
|
const kept = /* @__PURE__ */ new Set();
|
|
12983
13654
|
for (const path of keep) {
|
|
12984
13655
|
try {
|
|
12985
|
-
kept.add(
|
|
13656
|
+
kept.add(realpathSync8(path));
|
|
12986
13657
|
} catch {
|
|
12987
13658
|
}
|
|
12988
13659
|
}
|
|
12989
13660
|
const removed = [];
|
|
12990
13661
|
let entries;
|
|
12991
13662
|
try {
|
|
12992
|
-
entries =
|
|
13663
|
+
entries = readdirSync8(dataRoot);
|
|
12993
13664
|
} catch {
|
|
12994
13665
|
return removed;
|
|
12995
13666
|
}
|
|
12996
13667
|
for (const name of entries) {
|
|
12997
13668
|
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?-[0-9a-f]{64}$/u.test(name)) continue;
|
|
12998
|
-
const candidate =
|
|
13669
|
+
const candidate = join22(dataRoot, name);
|
|
12999
13670
|
try {
|
|
13000
|
-
const stat =
|
|
13671
|
+
const stat = lstatSync11(candidate);
|
|
13001
13672
|
if (!stat.isDirectory() || stat.isSymbolicLink()) continue;
|
|
13002
|
-
if (kept.has(
|
|
13003
|
-
|
|
13673
|
+
if (kept.has(realpathSync8(candidate))) continue;
|
|
13674
|
+
rmSync4(candidate, { recursive: true, force: true });
|
|
13004
13675
|
removed.push(candidate);
|
|
13005
13676
|
} catch {
|
|
13006
13677
|
}
|
|
@@ -13011,8 +13682,8 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
13011
13682
|
const env2 = input.env ?? process.env;
|
|
13012
13683
|
const home = env2.HOME;
|
|
13013
13684
|
if (!home || !resolve11(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to repair the Project Notebook skill projection");
|
|
13014
|
-
const skillsRoot =
|
|
13015
|
-
const link =
|
|
13685
|
+
const skillsRoot = join22(home, ".agents", "skills");
|
|
13686
|
+
const link = join22(skillsRoot, "project-notebook");
|
|
13016
13687
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
13017
13688
|
if (probe.state === "adopted") return { status: "clean", summary: "Canonical Skillex projection already matches the version-pinned export", source: probe.source, drift: [], changed_files: [] };
|
|
13018
13689
|
if (probe.state !== "declined") {
|
|
@@ -13025,9 +13696,9 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
13025
13696
|
assertOwnedSkillTree(source);
|
|
13026
13697
|
const present = [];
|
|
13027
13698
|
const walk = (directory) => {
|
|
13028
|
-
for (const entry of
|
|
13029
|
-
const path =
|
|
13030
|
-
const rel =
|
|
13699
|
+
for (const entry of readdirSync8(directory, { withFileTypes: true })) {
|
|
13700
|
+
const path = join22(directory, entry.name);
|
|
13701
|
+
const rel = relative11(source, path).split(sep6).join("/");
|
|
13031
13702
|
if (entry.isDirectory()) {
|
|
13032
13703
|
walk(path);
|
|
13033
13704
|
continue;
|
|
@@ -13040,11 +13711,11 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
13040
13711
|
walk(source);
|
|
13041
13712
|
const stale = present.filter((rel) => !wanted.has(rel)).sort();
|
|
13042
13713
|
const changed = expected.manifest.files.filter((entry) => {
|
|
13043
|
-
const path =
|
|
13044
|
-
if (!
|
|
13045
|
-
const stat =
|
|
13714
|
+
const path = join22(source, ...entry.path.split("/"));
|
|
13715
|
+
if (!existsSync17(path)) return true;
|
|
13716
|
+
const stat = lstatSync11(path);
|
|
13046
13717
|
if (!stat.isFile()) return true;
|
|
13047
|
-
if (createHash7("sha256").update(
|
|
13718
|
+
if (createHash7("sha256").update(readFileSync17(path)).digest("hex") !== entry.sha256) return true;
|
|
13048
13719
|
const mode = stat.mode & 511;
|
|
13049
13720
|
const executable = entry.mode === "0755";
|
|
13050
13721
|
return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
|
|
@@ -13059,15 +13730,15 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
13059
13730
|
summary: `Would restore ${affected.length} file(s) in ${source} from the version-pinned export`,
|
|
13060
13731
|
source,
|
|
13061
13732
|
drift: probe.block.details,
|
|
13062
|
-
changed_files: affected.map((rel) =>
|
|
13733
|
+
changed_files: affected.map((rel) => join22(source, ...rel.split("/")))
|
|
13063
13734
|
};
|
|
13064
13735
|
}
|
|
13065
|
-
for (const rel of stale)
|
|
13736
|
+
for (const rel of stale) rmSync4(join22(source, ...rel.split("/")), { force: true });
|
|
13066
13737
|
for (const rel of changed) {
|
|
13067
|
-
const destination =
|
|
13738
|
+
const destination = join22(source, ...rel.split("/"));
|
|
13068
13739
|
const entry = expected.manifest.files.find((item) => item.path === rel);
|
|
13069
13740
|
mkdirSync7(dirname10(destination), { recursive: true, mode: 493 });
|
|
13070
|
-
copyFileSync3(
|
|
13741
|
+
copyFileSync3(join22(expected.source, ...rel.split("/")), destination);
|
|
13071
13742
|
chmodSync4(destination, entry.mode === "0755" ? 493 : 420);
|
|
13072
13743
|
}
|
|
13073
13744
|
verifyProjectNotebookSkillExport(source);
|
|
@@ -13076,21 +13747,21 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
13076
13747
|
summary: `Restored ${affected.length} file(s) in ${source} from the version-pinned export; commit the change in the Skillex checkout`,
|
|
13077
13748
|
source,
|
|
13078
13749
|
drift: probe.block.details,
|
|
13079
|
-
changed_files: affected.map((rel) =>
|
|
13750
|
+
changed_files: affected.map((rel) => join22(source, ...rel.split("/")))
|
|
13080
13751
|
};
|
|
13081
13752
|
}
|
|
13082
13753
|
function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
13083
13754
|
const home = env2.HOME;
|
|
13084
13755
|
if (!home) return { skill_installed: false, hooks_projected: false, details: ["HOME is unavailable"] };
|
|
13085
|
-
const skillsRoot =
|
|
13086
|
-
const link =
|
|
13756
|
+
const skillsRoot = join22(home, ".agents", "skills");
|
|
13757
|
+
const link = join22(skillsRoot, "project-notebook");
|
|
13087
13758
|
try {
|
|
13088
13759
|
const expected = expectedPackedSkill();
|
|
13089
|
-
const dataRoot = resolve11(env2.XDG_DATA_HOME ||
|
|
13090
|
-
const expectedPayload =
|
|
13760
|
+
const dataRoot = resolve11(env2.XDG_DATA_HOME || join22(home, ".local", "share"), "pjangler", "skills", "project-notebook");
|
|
13761
|
+
const expectedPayload = join22(dataRoot, `${PJANGLER_VERSION}-${expected.digest}`);
|
|
13091
13762
|
const probe = probeCanonicalSkillexRootProjection(skillsRoot, link);
|
|
13092
13763
|
if (probe.state === "declined") return { skill_installed: false, hooks_projected: false, details: probe.block.details, blocked: probe.block };
|
|
13093
|
-
const stat =
|
|
13764
|
+
const stat = lstatSync11(link);
|
|
13094
13765
|
if (!stat.isSymbolicLink()) {
|
|
13095
13766
|
return {
|
|
13096
13767
|
skill_installed: false,
|
|
@@ -13099,8 +13770,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
|
13099
13770
|
blocked: { code: "projection-foreign", summary: `${link} is not a PJ\xE1ngler-owned link`, details: ["the path is a real file or directory"], repair: REPAIR_COMMAND }
|
|
13100
13771
|
};
|
|
13101
13772
|
}
|
|
13102
|
-
const source =
|
|
13103
|
-
const packedPayloadMatches =
|
|
13773
|
+
const source = realpathSync8(link);
|
|
13774
|
+
const packedPayloadMatches = existsSync17(expectedPayload) && source === realpathSync8(expectedPayload);
|
|
13104
13775
|
if (!packedPayloadMatches && !isVerifiedCanonicalSkillexProjection(source)) {
|
|
13105
13776
|
const details = isCanonicalSkillexProjectionPath(source) ? describeProjectNotebookSkillDrift(source) : [`the link targets ${source}, which is neither the version-pinned payload nor a canonical Skillex projection`];
|
|
13106
13777
|
return {
|
|
@@ -13127,8 +13798,8 @@ function inspectProjectNotebookIntegration(env2 = process.env) {
|
|
|
13127
13798
|
}
|
|
13128
13799
|
}
|
|
13129
13800
|
function projectorArguments(source, command, input) {
|
|
13130
|
-
const script =
|
|
13131
|
-
const args = [script, command, "--master",
|
|
13801
|
+
const script = join22(source, "scripts", "project-hooks.py");
|
|
13802
|
+
const args = [script, command, "--master", join22(source, "hooks", "hooks.master.json"), "--fragment", join22(source, "hooks", "claude.settings.json"), "--target", input.target];
|
|
13132
13803
|
if (command === "check") args.push("--json");
|
|
13133
13804
|
else if (input.stateHome) args.push("--state-home", input.stateHome);
|
|
13134
13805
|
return args;
|
|
@@ -13150,7 +13821,7 @@ function checkProjectNotebookHooks(input = {}) {
|
|
|
13150
13821
|
verifyProjectNotebookSkillExport(source);
|
|
13151
13822
|
const home = env2.HOME;
|
|
13152
13823
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to check Project Notebook hooks");
|
|
13153
|
-
const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ??
|
|
13824
|
+
const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join22(home, ".claude", "settings.json"));
|
|
13154
13825
|
const result2 = spawnSync11("/usr/bin/python3", ["-I", ...projectorArguments(source, "check", { target })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
|
|
13155
13826
|
if (result2.status !== 0 && result2.status !== 1) throw new NotebookError("CONFLICT", (result2.stderr || "Project Notebook projector check failed").trim().slice(0, 512));
|
|
13156
13827
|
try {
|
|
@@ -13165,17 +13836,17 @@ function installProjectNotebookIntegration(input = {}) {
|
|
|
13165
13836
|
const env2 = input.env ?? process.env;
|
|
13166
13837
|
const skill = installPackagedProjectNotebookSkill({ source: input.source, env: env2 });
|
|
13167
13838
|
if (skill.blocked) return { skill, hooksChanged: false, blocked: skill.blocked };
|
|
13168
|
-
const source =
|
|
13839
|
+
const source = realpathSync8(skill.path);
|
|
13169
13840
|
const home = env2.HOME;
|
|
13170
13841
|
if (!home) throw new NotebookError("NOT_CONFIGURED", "HOME is required to install Project Notebook hooks");
|
|
13171
|
-
const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ??
|
|
13172
|
-
const stateHome = resolve11(env2.XDG_STATE_HOME ||
|
|
13842
|
+
const target = resolve11(input.target ?? env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS ?? join22(home, ".claude", "settings.json"));
|
|
13843
|
+
const stateHome = resolve11(env2.XDG_STATE_HOME || join22(home, ".local", "state"));
|
|
13173
13844
|
const result2 = spawnSync11("/usr/bin/python3", ["-I", ...projectorArguments(source, "install", { target, stateHome })], { encoding: "utf8", env: projectorEnvironment(env2), timeout: 5e3, maxBuffer: 1048576 });
|
|
13174
13845
|
if (result2.status !== 0) {
|
|
13175
13846
|
if (skill.installed) {
|
|
13176
13847
|
try {
|
|
13177
|
-
const stat =
|
|
13178
|
-
if (stat.isSymbolicLink() &&
|
|
13848
|
+
const stat = lstatSync11(skill.path);
|
|
13849
|
+
if (stat.isSymbolicLink() && realpathSync8(skill.path) === source) unlinkSync5(skill.path);
|
|
13179
13850
|
} catch {
|
|
13180
13851
|
}
|
|
13181
13852
|
}
|
|
@@ -13192,10 +13863,10 @@ function assertNoSymlinkComponents2(path, allowMissing = false) {
|
|
|
13192
13863
|
const root = parse3(absolute).root;
|
|
13193
13864
|
let cursor = root;
|
|
13194
13865
|
for (const component of absolute.slice(root.length).split(sep6).filter(Boolean)) {
|
|
13195
|
-
cursor =
|
|
13866
|
+
cursor = join22(cursor, component);
|
|
13196
13867
|
let stat;
|
|
13197
13868
|
try {
|
|
13198
|
-
stat =
|
|
13869
|
+
stat = lstatSync11(cursor);
|
|
13199
13870
|
} catch (error) {
|
|
13200
13871
|
if (allowMissing && error.code === "ENOENT") return;
|
|
13201
13872
|
throw error;
|
|
@@ -13281,13 +13952,13 @@ async function prepareNotebookObservationResolved(module, resolved, config, loca
|
|
|
13281
13952
|
health: healthy ? "healthy" : "drifted",
|
|
13282
13953
|
notebook_check: notebookCheck,
|
|
13283
13954
|
notebook,
|
|
13284
|
-
scoped_notes: notes.map((
|
|
13285
|
-
id:
|
|
13286
|
-
title:
|
|
13287
|
-
note_type:
|
|
13288
|
-
created_at:
|
|
13289
|
-
updated_at:
|
|
13290
|
-
envelope_logical_id: parseNoteEnvelope(
|
|
13955
|
+
scoped_notes: notes.map((note) => ({
|
|
13956
|
+
id: note.id,
|
|
13957
|
+
title: note.title,
|
|
13958
|
+
note_type: note.note_type,
|
|
13959
|
+
created_at: note.created_at,
|
|
13960
|
+
updated_at: note.updated_at,
|
|
13961
|
+
envelope_logical_id: parseNoteEnvelope(note.content)?.envelope.logical_id ?? null
|
|
13291
13962
|
})),
|
|
13292
13963
|
overview,
|
|
13293
13964
|
error: null
|
|
@@ -13350,9 +14021,9 @@ function ensureFinalNoteContent(value, maxBytes) {
|
|
|
13350
14021
|
return value;
|
|
13351
14022
|
}
|
|
13352
14023
|
function getScoped(notes, noteId) {
|
|
13353
|
-
const
|
|
13354
|
-
if (!
|
|
13355
|
-
return
|
|
14024
|
+
const note = notes.find((item) => item.id === noteId);
|
|
14025
|
+
if (!note) throw new NotebookError("NOT_FOUND", `Note is not a proven member of this project notebook: ${noteId}`);
|
|
14026
|
+
return note;
|
|
13356
14027
|
}
|
|
13357
14028
|
var NotebookModule = class {
|
|
13358
14029
|
registryPath;
|
|
@@ -13564,8 +14235,8 @@ var NotebookModule = class {
|
|
|
13564
14235
|
async deleteNote(repo, noteId, confirmed) {
|
|
13565
14236
|
if (!confirmed) throw new NotebookError("INVALID_INPUT", "Note deletion requires confirmation or --yes");
|
|
13566
14237
|
const { ctx, notes, notebookId } = await this.scoped(repo);
|
|
13567
|
-
const
|
|
13568
|
-
const parsed = parseNoteEnvelope(
|
|
14238
|
+
const note = getScoped(notes, noteId);
|
|
14239
|
+
const parsed = parseNoteEnvelope(note.content);
|
|
13569
14240
|
if (parsed && parsed.envelope.project_slug !== ctx.config.project_slug) throw new NotebookError("CROSS_PROJECT", "Managed note envelope belongs to a different project");
|
|
13570
14241
|
if (noteId === ctx.config.binding.overview_note_id || parsed?.envelope.kind === "overview") throw new NotebookError("CONFLICT", "The stable Project Overview note cannot be deleted");
|
|
13571
14242
|
await ctx.client.deleteOwnedNote(notebookId, noteId);
|
|
@@ -13674,17 +14345,17 @@ init_remote_mutation_journal();
|
|
|
13674
14345
|
function integrationEnvironment(module, ctx) {
|
|
13675
14346
|
const env2 = { ...module.environment, HOME: ctx.homeDir };
|
|
13676
14347
|
if (module.environment.HOME !== ctx.homeDir) {
|
|
13677
|
-
env2.XDG_DATA_HOME =
|
|
13678
|
-
env2.XDG_STATE_HOME =
|
|
13679
|
-
env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS =
|
|
14348
|
+
env2.XDG_DATA_HOME = join23(ctx.homeDir, ".local", "share");
|
|
14349
|
+
env2.XDG_STATE_HOME = join23(ctx.homeDir, ".local", "state");
|
|
14350
|
+
env2.PJ_PROJECT_NOTEBOOK_CLAUDE_SETTINGS = join23(ctx.homeDir, ".claude", "settings.json");
|
|
13680
14351
|
}
|
|
13681
14352
|
return env2;
|
|
13682
14353
|
}
|
|
13683
14354
|
function resolvedForPlan(plan) {
|
|
13684
|
-
const manifestPath =
|
|
14355
|
+
const manifestPath = join23(plan.project.repo_path, ".project.json");
|
|
13685
14356
|
let manifest = plan.manifest;
|
|
13686
|
-
if (
|
|
13687
|
-
const parsed = JSON.parse(
|
|
14357
|
+
if (existsSync18(manifestPath)) {
|
|
14358
|
+
const parsed = JSON.parse(readFileSync18(manifestPath, "utf8"));
|
|
13688
14359
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${manifestPath} must contain a JSON object`);
|
|
13689
14360
|
manifest = parsed;
|
|
13690
14361
|
}
|
|
@@ -13845,7 +14516,7 @@ function unsafeToRemove(targetDir) {
|
|
|
13845
14516
|
while (!seen.has(cursor)) {
|
|
13846
14517
|
seen.add(cursor);
|
|
13847
14518
|
try {
|
|
13848
|
-
const stat =
|
|
14519
|
+
const stat = lstatSync12(cursor);
|
|
13849
14520
|
if (stat.isSymbolicLink()) {
|
|
13850
14521
|
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`;
|
|
13851
14522
|
}
|
|
@@ -13873,12 +14544,12 @@ function publicMigration(report) {
|
|
|
13873
14544
|
};
|
|
13874
14545
|
}
|
|
13875
14546
|
function hasGitRepository(runtime, targetDir) {
|
|
13876
|
-
if (!
|
|
14547
|
+
if (!existsSync19(join24(targetDir, ".git"))) return false;
|
|
13877
14548
|
return runtime.runGit(targetDir, ["rev-parse", "--is-inside-work-tree"]).status === 0;
|
|
13878
14549
|
}
|
|
13879
14550
|
function refreshPlanFromCanonicalManifest(plan) {
|
|
13880
|
-
const manifestPath =
|
|
13881
|
-
const manifest = JSON.parse(
|
|
14551
|
+
const manifestPath = join24(plan.project.repo_path, ".project.json");
|
|
14552
|
+
const manifest = JSON.parse(readFileSync19(manifestPath, "utf8"));
|
|
13882
14553
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
13883
14554
|
throw new Error(`${manifestPath} must contain a JSON object`);
|
|
13884
14555
|
}
|
|
@@ -13898,12 +14569,25 @@ function refreshPlanFromCanonicalManifest(plan) {
|
|
|
13898
14569
|
if (!manifestTicket || typeof manifestTicket !== "object") {
|
|
13899
14570
|
throw new Error(`${manifestPath} ticket_provider is missing`);
|
|
13900
14571
|
}
|
|
14572
|
+
const manifestIdentifier = String(manifestTicket.identifier ?? "");
|
|
14573
|
+
const manifestBoardId = String(manifestTicket.board_id ?? "");
|
|
14574
|
+
const manifestType = String(manifestTicket.type ?? "");
|
|
14575
|
+
const recorded = plan.project.ticket_provider;
|
|
14576
|
+
const carriesProvenance = (recorded?.identifier ?? "") === manifestIdentifier && (recorded?.board_id ?? "") === manifestBoardId;
|
|
14577
|
+
const identifierSource = (carriesProvenance ? recorded?.identifier_source : void 0) ?? "proposed";
|
|
14578
|
+
const manifestConfirmedAt = typeof manifestTicket.board_confirmed_at === "string" ? manifestTicket.board_confirmed_at.trim() : "";
|
|
14579
|
+
const boardConfirmedAt = manifestConfirmedAt || ((recorded?.board_id ?? "") === manifestBoardId ? recorded?.board_confirmed_at ?? "" : "");
|
|
14580
|
+
const keyIsProven = !providerAssignsIdentifiers(manifestType) || identifierSource === "provider";
|
|
14581
|
+
const manifestState = typeof manifestTicket.state === "string" ? manifestTicket.state : void 0;
|
|
13901
14582
|
const ticketProvider = {
|
|
13902
|
-
type:
|
|
14583
|
+
type: manifestType,
|
|
13903
14584
|
workspace: String(manifestTicket.workspace ?? ""),
|
|
13904
|
-
identifier:
|
|
13905
|
-
|
|
13906
|
-
|
|
14585
|
+
identifier: manifestIdentifier,
|
|
14586
|
+
identifier_source: identifierSource,
|
|
14587
|
+
...carriesProvenance && identifierSource === "provider" && recorded?.identifier_fetched_at ? { identifier_fetched_at: recorded.identifier_fetched_at } : {},
|
|
14588
|
+
board_id: manifestBoardId,
|
|
14589
|
+
...boardConfirmedAt ? { board_confirmed_at: boardConfirmedAt } : {},
|
|
14590
|
+
state: manifestState === "skipped" ? "skipped" : manifestBoardId && boardConfirmedAt && keyIsProven ? "linked" : "planned"
|
|
13907
14591
|
};
|
|
13908
14592
|
plan.manifest = manifest;
|
|
13909
14593
|
plan.project.agents = agents;
|
|
@@ -13950,7 +14634,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
13950
14634
|
const logs = [];
|
|
13951
14635
|
const errors = [];
|
|
13952
14636
|
const changedFiles = [];
|
|
13953
|
-
const targetExistedAtStart =
|
|
14637
|
+
const targetExistedAtStart = existsSync19(targetDir);
|
|
13954
14638
|
const transactionContext = {
|
|
13955
14639
|
...ctx,
|
|
13956
14640
|
targetDir,
|
|
@@ -14039,7 +14723,6 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14039
14723
|
force: Boolean(ctx.force),
|
|
14040
14724
|
skipTelegram: true,
|
|
14041
14725
|
skipEmail: true,
|
|
14042
|
-
skipRuntimeRepo: agentAction.context.skipRuntimeRepo,
|
|
14043
14726
|
skipPlane: agentAction.context.skipPlane,
|
|
14044
14727
|
skipBloodbank: agentAction.context.skipBloodbank,
|
|
14045
14728
|
skipSystemd: agentAction.context.skipSystemd,
|
|
@@ -14113,7 +14796,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14113
14796
|
if (hasGitRepository(this.runtime, targetDir)) {
|
|
14114
14797
|
phases.push({ id: "project.git", status: "unchanged", changedFiles: [], message: "Git repository already initialized" });
|
|
14115
14798
|
} else {
|
|
14116
|
-
const gitPath =
|
|
14799
|
+
const gitPath = join24(targetDir, ".git");
|
|
14117
14800
|
for (const { args, label, options } of [
|
|
14118
14801
|
{ args: ["init", "--initial-branch=main"], label: "git init" },
|
|
14119
14802
|
{ args: ["add", "-A"], label: "git add" },
|
|
@@ -14129,7 +14812,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14129
14812
|
phases.push({ id: `project.git:${label}`, status: "failed", changedFiles: changedFiles.includes(gitPath) ? [gitPath] : [], message: errors.at(-1) });
|
|
14130
14813
|
break;
|
|
14131
14814
|
}
|
|
14132
|
-
if (label === "git init" &&
|
|
14815
|
+
if (label === "git init" && existsSync19(gitPath)) changedFiles.push(gitPath);
|
|
14133
14816
|
logs.push(`${label}: ok`);
|
|
14134
14817
|
}
|
|
14135
14818
|
if (errors.length === 0) {
|
|
@@ -14137,7 +14820,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14137
14820
|
const headReady = repositoryReady && this.runtime.runGit(targetDir, ["rev-parse", "--verify", "HEAD"]).status === 0;
|
|
14138
14821
|
if (!headReady) {
|
|
14139
14822
|
errors.push("git postcondition failed: repository or initial commit is missing");
|
|
14140
|
-
phases.push({ id: "project.git:postcondition", status: "failed", changedFiles:
|
|
14823
|
+
phases.push({ id: "project.git:postcondition", status: "failed", changedFiles: existsSync19(gitPath) ? [gitPath] : [], message: errors.at(-1) });
|
|
14141
14824
|
} else {
|
|
14142
14825
|
if (!changedFiles.includes(gitPath)) changedFiles.push(gitPath);
|
|
14143
14826
|
phases.push({ id: "project.git", status: "changed", changedFiles: [gitPath], message: "Git repository initialized and committed" });
|
|
@@ -14207,7 +14890,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14207
14890
|
}
|
|
14208
14891
|
}
|
|
14209
14892
|
const deferred = provisionedAgentContext?.deferredExternalEffects;
|
|
14210
|
-
if (errors.length === 0 && deferred?.owner === "project" && (deferred.
|
|
14893
|
+
if (errors.length === 0 && deferred?.owner === "project" && (deferred.ticketBoard || deferred.systemd)) {
|
|
14211
14894
|
externalDispatchStarted = true;
|
|
14212
14895
|
rollbackEligible = false;
|
|
14213
14896
|
const beforeExternal = snapshotTree(targetDir);
|
|
@@ -14270,7 +14953,7 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14270
14953
|
message: errors.at(-1)
|
|
14271
14954
|
});
|
|
14272
14955
|
}
|
|
14273
|
-
if (errors.length > 0 && mode === "create" && !targetExistedAtStart && rollbackEligible &&
|
|
14956
|
+
if (errors.length > 0 && mode === "create" && !targetExistedAtStart && rollbackEligible && existsSync19(targetDir)) {
|
|
14274
14957
|
const unsafe = unsafeToRemove(targetDir);
|
|
14275
14958
|
if (unsafe) {
|
|
14276
14959
|
errors.push(`fresh-target rollback refused: ${unsafe}`);
|
|
@@ -14281,10 +14964,10 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14281
14964
|
message: errors.at(-1)
|
|
14282
14965
|
});
|
|
14283
14966
|
} else try {
|
|
14284
|
-
|
|
14967
|
+
rmSync5(targetDir, { recursive: true, force: true });
|
|
14285
14968
|
const insideTarget = (path) => {
|
|
14286
|
-
const
|
|
14287
|
-
return
|
|
14969
|
+
const relative12 = relativePath(resolve13(targetDir), resolve13(path));
|
|
14970
|
+
return relative12 === "" || !relative12.startsWith("..") && !isAbsolute5(relative12);
|
|
14288
14971
|
};
|
|
14289
14972
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
14290
14973
|
changedFiles.length = 0;
|
|
@@ -14626,8 +15309,8 @@ var recipeRegistry = new RecipeRegistry([
|
|
|
14626
15309
|
|
|
14627
15310
|
// src/commands/AgentHooksCommands.ts
|
|
14628
15311
|
import { homedir as homedir8 } from "node:os";
|
|
14629
|
-
import { join as
|
|
14630
|
-
import { existsSync as
|
|
15312
|
+
import { join as join25, dirname as dirname12 } from "node:path";
|
|
15313
|
+
import { existsSync as existsSync20, cpSync as cpSync2, mkdirSync as mkdirSync8, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "node:fs";
|
|
14631
15314
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
14632
15315
|
init_project();
|
|
14633
15316
|
var AGENT_HOOKS_SKIP_MESSAGE = "\u21B7 agent-hooks layer skipped: global ~/.agents/hooks detected (these hooks already run globally).\n Set PJ_AGENT_HOOKS_LAYER=1 to install the project-scoped layer anyway.";
|
|
@@ -14639,16 +15322,16 @@ function resolveTemplateRoot() {
|
|
|
14639
15322
|
try {
|
|
14640
15323
|
let dir = dirname12(fileURLToPath6(import.meta.url));
|
|
14641
15324
|
for (let i = 0; i < 8; i++) {
|
|
14642
|
-
candidates.push(
|
|
15325
|
+
candidates.push(join25(dir, "templates", "commonproject", "template"));
|
|
14643
15326
|
const parent = dirname12(dir);
|
|
14644
15327
|
if (parent === dir) break;
|
|
14645
15328
|
dir = parent;
|
|
14646
15329
|
}
|
|
14647
15330
|
} catch {
|
|
14648
15331
|
}
|
|
14649
|
-
candidates.push(
|
|
15332
|
+
candidates.push(join25(homedir8(), "code", "pjangler", "templates", "commonproject", "template"));
|
|
14650
15333
|
for (const c of candidates) {
|
|
14651
|
-
if (
|
|
15334
|
+
if (existsSync20(join25(c, ".agents", "hooks", "hooks.master.json"))) return c;
|
|
14652
15335
|
}
|
|
14653
15336
|
throw new Error(
|
|
14654
15337
|
"Could not locate the CommonProject template. Set PJANGLER_COMMONPROJECT_TEMPLATE to <repo>/templates/commonproject/template."
|
|
@@ -14674,10 +15357,10 @@ var CopyAgentHooksTree = class extends Command {
|
|
|
14674
15357
|
const created = [];
|
|
14675
15358
|
const skipped = [];
|
|
14676
15359
|
for (const { rel, dir } of items) {
|
|
14677
|
-
const src =
|
|
14678
|
-
const dest =
|
|
14679
|
-
if (!
|
|
14680
|
-
if (
|
|
15360
|
+
const src = join25(templateRoot, rel);
|
|
15361
|
+
const dest = join25(this.context.targetDir, rel);
|
|
15362
|
+
if (!existsSync20(src)) continue;
|
|
15363
|
+
if (existsSync20(dest) && !this.context.force) {
|
|
14681
15364
|
skipped.push(rel);
|
|
14682
15365
|
continue;
|
|
14683
15366
|
}
|
|
@@ -14703,14 +15386,14 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
14703
15386
|
if (!resolveAgentHooksLayer2()) {
|
|
14704
15387
|
return { success: true, message: this.formatMessage(AGENT_HOOKS_SKIP_MESSAGE) };
|
|
14705
15388
|
}
|
|
14706
|
-
const misePath =
|
|
14707
|
-
if (!
|
|
15389
|
+
const misePath = join25(this.context.targetDir, "mise.toml");
|
|
15390
|
+
if (!existsSync20(misePath)) {
|
|
14708
15391
|
return {
|
|
14709
15392
|
success: false,
|
|
14710
15393
|
message: "\u26A0\uFE0F No mise.toml found \u2014 run `pjangler init mise` first, then re-run."
|
|
14711
15394
|
};
|
|
14712
15395
|
}
|
|
14713
|
-
let content =
|
|
15396
|
+
let content = readFileSync20(misePath, "utf8");
|
|
14714
15397
|
if (content.includes(_WireMiseAgentHooks.MARKER)) {
|
|
14715
15398
|
return { success: true, message: this.formatMessage("\u2713 mise.toml already wired for agent-hooks") };
|
|
14716
15399
|
}
|
|
@@ -14910,7 +15593,7 @@ if __name__ == "__main__":
|
|
|
14910
15593
|
|
|
14911
15594
|
// src/commands/AddMiseCodegraphScript.ts
|
|
14912
15595
|
import { chmodSync as chmodSync5 } from "fs";
|
|
14913
|
-
import { join as
|
|
15596
|
+
import { join as join26 } from "path";
|
|
14914
15597
|
var AddMiseCodegraphScript = class extends Command {
|
|
14915
15598
|
async invoke() {
|
|
14916
15599
|
const filePath = ".mise/scripts/codegraph.sh";
|
|
@@ -14964,7 +15647,7 @@ fi
|
|
|
14964
15647
|
`;
|
|
14965
15648
|
this.writeFile(filePath, content);
|
|
14966
15649
|
if (!this.context.dryRun) {
|
|
14967
|
-
chmodSync5(
|
|
15650
|
+
chmodSync5(join26(this.context.targetDir, filePath), 493);
|
|
14968
15651
|
}
|
|
14969
15652
|
return {
|
|
14970
15653
|
success: true,
|
|
@@ -15000,14 +15683,14 @@ SECRET_KEY=""
|
|
|
15000
15683
|
};
|
|
15001
15684
|
|
|
15002
15685
|
// src/parity/index.ts
|
|
15003
|
-
import { existsSync as
|
|
15686
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
15004
15687
|
import { homedir as homedir9 } from "node:os";
|
|
15005
|
-
import { dirname as dirname13, join as
|
|
15688
|
+
import { dirname as dirname13, join as join27, resolve as resolve14 } from "node:path";
|
|
15006
15689
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
15007
15690
|
function resolvePjanglerRoot2() {
|
|
15008
15691
|
let dir = dirname13(fileURLToPath7(import.meta.url));
|
|
15009
15692
|
while (dir !== dirname13(dir)) {
|
|
15010
|
-
if (
|
|
15693
|
+
if (existsSync21(join27(dir, "package.json")) && existsSync21(join27(dir, "templates", "commonproject", "copier.yml"))) return dir;
|
|
15011
15694
|
dir = dirname13(dir);
|
|
15012
15695
|
}
|
|
15013
15696
|
return resolve14(process.cwd());
|
|
@@ -15169,15 +15852,18 @@ function getRecipeInfo(name) {
|
|
|
15169
15852
|
return RECIPE_REGISTRY[name] || null;
|
|
15170
15853
|
}
|
|
15171
15854
|
|
|
15855
|
+
// src/mcp-server.ts
|
|
15856
|
+
init_version();
|
|
15857
|
+
|
|
15172
15858
|
// src/describe/index.ts
|
|
15173
|
-
import { existsSync as
|
|
15174
|
-
import { join as
|
|
15859
|
+
import { existsSync as existsSync22, readFileSync as readFileSync21, readdirSync as readdirSync9, statSync as statSync4 } from "node:fs";
|
|
15860
|
+
import { join as join29, resolve as resolve15 } from "node:path";
|
|
15175
15861
|
init_project();
|
|
15176
15862
|
|
|
15177
15863
|
// src/describe/activity.ts
|
|
15178
15864
|
import { spawn as spawn2, spawnSync as spawnSync14 } from "node:child_process";
|
|
15179
15865
|
import { statSync as statSync3 } from "node:fs";
|
|
15180
|
-
import { join as
|
|
15866
|
+
import { join as join28 } from "node:path";
|
|
15181
15867
|
var ACTIVE_WINDOW_SECONDS = 24 * 60 * 60;
|
|
15182
15868
|
var MAX_DIRTY_STATS = 500;
|
|
15183
15869
|
var GIT_TIMEOUT_MS = 5e3;
|
|
@@ -15306,7 +15992,7 @@ function uncommittedSource(repo, paths) {
|
|
|
15306
15992
|
let newest = 0;
|
|
15307
15993
|
for (const path of paths.slice(0, MAX_DIRTY_STATS)) {
|
|
15308
15994
|
try {
|
|
15309
|
-
const mtime = Math.floor(statSync3(
|
|
15995
|
+
const mtime = Math.floor(statSync3(join28(repo, path)).mtimeMs / 1e3);
|
|
15310
15996
|
if (mtime > newest) newest = mtime;
|
|
15311
15997
|
} catch {
|
|
15312
15998
|
}
|
|
@@ -15415,7 +16101,7 @@ var CONFIG_FILES = [
|
|
|
15415
16101
|
];
|
|
15416
16102
|
function readJson(path) {
|
|
15417
16103
|
try {
|
|
15418
|
-
const parsed = JSON.parse(
|
|
16104
|
+
const parsed = JSON.parse(readFileSync21(path, "utf8"));
|
|
15419
16105
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
15420
16106
|
} catch {
|
|
15421
16107
|
return void 0;
|
|
@@ -15435,39 +16121,39 @@ function describeType(repo) {
|
|
|
15435
16121
|
const languages = [];
|
|
15436
16122
|
const roles = [];
|
|
15437
16123
|
const evidence = [];
|
|
15438
|
-
const
|
|
16124
|
+
const note = (signal, file) => evidence.push(`${signal} (${file})`);
|
|
15439
16125
|
for (const marker of LANGUAGE_MARKERS) {
|
|
15440
|
-
if (!
|
|
16126
|
+
if (!existsSync22(join29(repo, marker.file))) continue;
|
|
15441
16127
|
if (!languages.includes(marker.language)) {
|
|
15442
16128
|
languages.push(marker.language);
|
|
15443
|
-
|
|
16129
|
+
note(marker.language, marker.file);
|
|
15444
16130
|
}
|
|
15445
16131
|
}
|
|
15446
16132
|
try {
|
|
15447
|
-
const dotnet =
|
|
16133
|
+
const dotnet = readdirSync9(repo).find((entry) => entry.endsWith(".csproj") || entry.endsWith(".sln"));
|
|
15448
16134
|
if (dotnet && !languages.includes("dotnet")) {
|
|
15449
16135
|
languages.push("dotnet");
|
|
15450
|
-
|
|
16136
|
+
note("dotnet", dotnet);
|
|
15451
16137
|
}
|
|
15452
16138
|
} catch {
|
|
15453
16139
|
}
|
|
15454
|
-
const pkg = readJson(
|
|
16140
|
+
const pkg = readJson(join29(repo, "package.json"));
|
|
15455
16141
|
if (pkg) {
|
|
15456
|
-
if (
|
|
16142
|
+
if (existsSync22(join29(repo, "tsconfig.json"))) {
|
|
15457
16143
|
const index = languages.indexOf("javascript");
|
|
15458
16144
|
if (index >= 0) languages.splice(index, 1);
|
|
15459
16145
|
if (!languages.includes("typescript")) {
|
|
15460
16146
|
languages.unshift("typescript");
|
|
15461
|
-
|
|
16147
|
+
note("typescript", "tsconfig.json");
|
|
15462
16148
|
}
|
|
15463
16149
|
}
|
|
15464
16150
|
if (pkg.bin) {
|
|
15465
16151
|
roles.push("cli");
|
|
15466
|
-
|
|
16152
|
+
note("cli", "package.json#bin");
|
|
15467
16153
|
}
|
|
15468
16154
|
if (pkg.workspaces) {
|
|
15469
16155
|
roles.push("monorepo");
|
|
15470
|
-
|
|
16156
|
+
note("monorepo", "package.json#workspaces");
|
|
15471
16157
|
}
|
|
15472
16158
|
const dependencies = {
|
|
15473
16159
|
...pkg.dependencies,
|
|
@@ -15475,7 +16161,7 @@ function describeType(repo) {
|
|
|
15475
16161
|
};
|
|
15476
16162
|
if (Object.keys(dependencies).some((name) => name.startsWith("@modelcontextprotocol/"))) {
|
|
15477
16163
|
roles.push("mcp-server");
|
|
15478
|
-
|
|
16164
|
+
note("mcp-server", "package.json#@modelcontextprotocol");
|
|
15479
16165
|
}
|
|
15480
16166
|
}
|
|
15481
16167
|
const roleMarkers = [
|
|
@@ -15487,14 +16173,14 @@ function describeType(repo) {
|
|
|
15487
16173
|
["hermes-fleet-host", "agents/hermes"]
|
|
15488
16174
|
];
|
|
15489
16175
|
for (const [role, marker] of roleMarkers) {
|
|
15490
|
-
if (!
|
|
16176
|
+
if (!existsSync22(join29(repo, marker))) continue;
|
|
15491
16177
|
roles.push(role);
|
|
15492
|
-
|
|
16178
|
+
note(role, marker);
|
|
15493
16179
|
}
|
|
15494
16180
|
return { primaryLanguage: languages[0], languages, roles, evidence };
|
|
15495
16181
|
}
|
|
15496
16182
|
function describeIdentity(repo, registryPath2) {
|
|
15497
|
-
const manifestPath =
|
|
16183
|
+
const manifestPath = join29(repo, ".project.json");
|
|
15498
16184
|
const manifest = readJson(manifestPath);
|
|
15499
16185
|
const drift = [];
|
|
15500
16186
|
let record;
|
|
@@ -15581,12 +16267,12 @@ function describeSubsystems(repo, findings) {
|
|
|
15581
16267
|
const markers = SUBSYSTEM_MARKERS[metadata.id] ?? [];
|
|
15582
16268
|
const evidence = metadata.id === "notebook" ? (() => {
|
|
15583
16269
|
try {
|
|
15584
|
-
const manifest = JSON.parse(
|
|
16270
|
+
const manifest = JSON.parse(readFileSync21(join29(repo, ".project.json"), "utf8"));
|
|
15585
16271
|
return manifest.notebook && typeof manifest.notebook === "object" ? [".project.json#notebook"] : [];
|
|
15586
16272
|
} catch {
|
|
15587
16273
|
return [];
|
|
15588
16274
|
}
|
|
15589
|
-
})() : markers.filter((marker) =>
|
|
16275
|
+
})() : markers.filter((marker) => existsSync22(join29(repo, marker)));
|
|
15590
16276
|
const rules = (byRecipe.get(metadata.id) ?? []).map((finding2) => ({
|
|
15591
16277
|
id: finding2.id,
|
|
15592
16278
|
title: finding2.title,
|
|
@@ -15605,7 +16291,7 @@ function describeNotebook(repo, registryPath2) {
|
|
|
15605
16291
|
try {
|
|
15606
16292
|
const registry = loadProjectRegistry(registryPath2);
|
|
15607
16293
|
const project = Object.values(registry.projects).find((entry) => resolve15(entry.repo_path) === resolve15(repo));
|
|
15608
|
-
const manifest =
|
|
16294
|
+
const manifest = existsSync22(join29(repo, ".project.json")) ? JSON.parse(readFileSync21(join29(repo, ".project.json"), "utf8")) : void 0;
|
|
15609
16295
|
const declared = Boolean(project?.notebook || manifest?.notebook && typeof manifest.notebook === "object");
|
|
15610
16296
|
if (!declared) return { declared: false, bindingState: null, notebookId: null, overviewNoteId: null, health: null, remoteCheck: "skip", captureAdmission: null };
|
|
15611
16297
|
const config = loadEffectiveNotebookConfig(repo, registryPath2);
|
|
@@ -15623,7 +16309,7 @@ function describeNotebook(repo, registryPath2) {
|
|
|
15623
16309
|
}
|
|
15624
16310
|
}
|
|
15625
16311
|
function describeConfigFiles(repo) {
|
|
15626
|
-
return CONFIG_FILES.filter((spec) =>
|
|
16312
|
+
return CONFIG_FILES.filter((spec) => existsSync22(join29(repo, spec.path))).map((spec) => ({ path: spec.path, purpose: spec.purpose, subsystem: spec.subsystem }));
|
|
15627
16313
|
}
|
|
15628
16314
|
function describeNextSteps(description, findings) {
|
|
15629
16315
|
const steps = [];
|
|
@@ -15702,7 +16388,7 @@ function describeNextSteps(description, findings) {
|
|
|
15702
16388
|
}
|
|
15703
16389
|
function describeProject(input = {}) {
|
|
15704
16390
|
const repo = resolve15(input.repoArg ?? process.cwd());
|
|
15705
|
-
if (!
|
|
16391
|
+
if (!existsSync22(repo)) throw new Error(`Path does not exist: ${repo}`);
|
|
15706
16392
|
if (!statSync4(repo).isDirectory()) throw new Error(`Not a directory: ${repo}`);
|
|
15707
16393
|
const registryPath2 = input.registryPath ?? projectRegistryPath();
|
|
15708
16394
|
const report = recipeRegistry.auditRecipes(lifecycleContext(repo, true, false, { registryPath: registryPath2 }));
|
|
@@ -15855,6 +16541,7 @@ var server = new McpServer({
|
|
|
15855
16541
|
});
|
|
15856
16542
|
var TICKET_PROVIDER_SCHEMA = z.enum(["plane", "trello"]);
|
|
15857
16543
|
var BOARD_URL_COMPAT_SCHEMA = z.string().optional().describe("Deprecated compatibility input. Ignored; board URLs are derived at runtime and are never persisted.").meta({ deprecated: true });
|
|
16544
|
+
var RUNTIME_REPO_COMPAT_SCHEMA = z.boolean().optional().describe("Deprecated no-op. Hermes always converges ignored role-local runtime state and never provisions a per-agent GitHub repository.").meta({ deprecated: true });
|
|
15858
16545
|
function safePathSegmentSchema(label) {
|
|
15859
16546
|
return z.string().superRefine((value, context) => {
|
|
15860
16547
|
try {
|
|
@@ -15875,20 +16562,16 @@ var GENERIC_RECIPE_NAMES = getRecipeNames().filter((name) => !INTERACTIVE_RECIPE
|
|
|
15875
16562
|
if (GENERIC_RECIPE_NAMES.length === 0) throw new Error("No non-interactive recipes are registered for generic MCP execution");
|
|
15876
16563
|
function validateExternalEffectConsent(input, options) {
|
|
15877
16564
|
const selected = {
|
|
15878
|
-
runtimeRepo: input.provisionRuntimeRepo === true,
|
|
15879
16565
|
ticketBoard: input.provisionTicketBoard === true,
|
|
15880
16566
|
systemd: input.enableSystemd === true
|
|
15881
16567
|
};
|
|
15882
|
-
const anySelected = selected.
|
|
16568
|
+
const anySelected = selected.ticketBoard || selected.systemd;
|
|
15883
16569
|
if (anySelected && input.live !== true) {
|
|
15884
16570
|
throw new Error("External Hermes effects require live=true in addition to explicit positive opt-ins");
|
|
15885
16571
|
}
|
|
15886
16572
|
if (anySelected && options.requireNonLocal && input.local !== false) {
|
|
15887
16573
|
throw new Error("External Hermes effects require local=false in addition to live=true and explicit positive opt-ins");
|
|
15888
16574
|
}
|
|
15889
|
-
if (selected.runtimeRepo && input.skipRuntimeRepo === true) {
|
|
15890
|
-
throw new Error("provisionRuntimeRepo=true contradicts skipRuntimeRepo=true");
|
|
15891
|
-
}
|
|
15892
16575
|
if (selected.ticketBoard && input.skipPlane === true) {
|
|
15893
16576
|
throw new Error("provisionTicketBoard=true contradicts skipPlane=true");
|
|
15894
16577
|
}
|
|
@@ -15902,7 +16585,7 @@ function validateExternalEffectConsent(input, options) {
|
|
|
15902
16585
|
}
|
|
15903
16586
|
function resolveTargetDir(targetDir) {
|
|
15904
16587
|
const dir = resolve16(targetDir ?? process.cwd());
|
|
15905
|
-
if (!
|
|
16588
|
+
if (!existsSync23(dir)) {
|
|
15906
16589
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
15907
16590
|
}
|
|
15908
16591
|
if (!statSync5(dir).isDirectory()) {
|
|
@@ -15913,7 +16596,7 @@ function resolveTargetDir(targetDir) {
|
|
|
15913
16596
|
function resolvePjanglerRoot3() {
|
|
15914
16597
|
let dir = dirname14(fileURLToPath8(import.meta.url));
|
|
15915
16598
|
while (dir !== dirname14(dir)) {
|
|
15916
|
-
if (
|
|
16599
|
+
if (existsSync23(join30(dir, "package.json")) && existsSync23(join30(dir, "templates", "commonproject", "copier.yml"))) {
|
|
15917
16600
|
return dir;
|
|
15918
16601
|
}
|
|
15919
16602
|
dir = dirname14(dir);
|
|
@@ -15934,7 +16617,6 @@ function publicProjectPlan(plan) {
|
|
|
15934
16617
|
return {
|
|
15935
16618
|
...action,
|
|
15936
16619
|
context: {
|
|
15937
|
-
skipRuntimeRepo: action.context.skipRuntimeRepo,
|
|
15938
16620
|
skipPlane: action.context.skipPlane,
|
|
15939
16621
|
skipSystemd: action.context.skipSystemd
|
|
15940
16622
|
}
|
|
@@ -15949,7 +16631,10 @@ function publicCompositeProjectResponse(payload, plan) {
|
|
|
15949
16631
|
return {
|
|
15950
16632
|
...payload,
|
|
15951
16633
|
...hasNestedPlan ? { plan: projectedPlan } : {},
|
|
15952
|
-
...provisionsAgent ? {
|
|
16634
|
+
...provisionsAgent ? {
|
|
16635
|
+
bloodbankMode: "fleet-shared",
|
|
16636
|
+
runtimeMode: "role-local-ignored"
|
|
16637
|
+
} : {}
|
|
15953
16638
|
};
|
|
15954
16639
|
}
|
|
15955
16640
|
async function executeRegisteredProjectPlan(plan, agentContext, lifecycleOverrides = {}, trustedCopier) {
|
|
@@ -15967,7 +16652,6 @@ async function executeRegisteredProjectPlan(plan, agentContext, lifecycleOverrid
|
|
|
15967
16652
|
...agentContext,
|
|
15968
16653
|
trustedCopier,
|
|
15969
16654
|
deferredExternalEffects: {
|
|
15970
|
-
runtimeRepo: !plannedAgent.context.skipRuntimeRepo,
|
|
15971
16655
|
ticketBoard: !plannedAgent.context.skipPlane,
|
|
15972
16656
|
systemd: !plannedAgent.context.skipSystemd,
|
|
15973
16657
|
owner: "project"
|
|
@@ -16009,7 +16693,7 @@ function projectPreflightFailure(plan, errors, audit) {
|
|
|
16009
16693
|
};
|
|
16010
16694
|
}
|
|
16011
16695
|
function preflightExistingHermesScaffold(targetDir) {
|
|
16012
|
-
if (!
|
|
16696
|
+
if (!existsSync23(join30(targetDir, "agents", "hermes"))) return void 0;
|
|
16013
16697
|
const owner = recipeRegistry.ownerOf("hermes.pm-scaffold");
|
|
16014
16698
|
if (!owner) return "Hermes lifecycle owner is unavailable";
|
|
16015
16699
|
const finding2 = owner.check.audit(lifecycleContext(targetDir, true));
|
|
@@ -16249,7 +16933,7 @@ server.registerTool(
|
|
|
16249
16933
|
agentRole: AGENT_ROLE_SCHEMA.optional(),
|
|
16250
16934
|
agentPurpose: z.string().optional(),
|
|
16251
16935
|
local: z.boolean().optional(),
|
|
16252
|
-
provisionRuntimeRepo:
|
|
16936
|
+
provisionRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
|
|
16253
16937
|
provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; also requires live=true, local=false, and skipPlane!=true."),
|
|
16254
16938
|
enableSystemd: z.boolean().optional().describe("Explicitly opt in to systemd installation/enablement; also requires live=true and local=false."),
|
|
16255
16939
|
force: z.boolean().optional(),
|
|
@@ -16270,10 +16954,10 @@ server.registerTool(
|
|
|
16270
16954
|
const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
|
|
16271
16955
|
const explicitTargetDir = input.targetDir ? resolve16(input.targetDir) : void 0;
|
|
16272
16956
|
const parentDir = resolve16(input.parentDir ?? (explicitTargetDir ? dirname14(explicitTargetDir) : process.cwd()));
|
|
16273
|
-
if (!
|
|
16957
|
+
if (!existsSync23(parentDir) || !statSync5(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
16274
16958
|
const targetDir = resolveContainedPath(
|
|
16275
16959
|
parentDir,
|
|
16276
|
-
explicitTargetDir ??
|
|
16960
|
+
explicitTargetDir ?? join30(parentDir, projectSlug),
|
|
16277
16961
|
"Bootstrap target"
|
|
16278
16962
|
);
|
|
16279
16963
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
@@ -16285,7 +16969,7 @@ server.registerTool(
|
|
|
16285
16969
|
if (externalEffects.ticketBoard && ticketProvider === "plane" && !boardId) {
|
|
16286
16970
|
throw new Error("boardId or planeProjectId is required when skipPlane=false for Plane; keep skipPlane=true for safe local bootstrap");
|
|
16287
16971
|
}
|
|
16288
|
-
if (!dryRun &&
|
|
16972
|
+
if (!dryRun && existsSync23(targetDir) && !overwrite) throw new Error(`Target already exists: ${targetDir} (set force/overwrite=true to re-render)`);
|
|
16289
16973
|
const plan = planProjectInit({
|
|
16290
16974
|
name: input.projectName,
|
|
16291
16975
|
description: input.projectDescription,
|
|
@@ -16297,12 +16981,13 @@ server.registerTool(
|
|
|
16297
16981
|
agentRole: input.agentRole ?? "pm",
|
|
16298
16982
|
apply: !dryRun,
|
|
16299
16983
|
live: input.live ?? false,
|
|
16300
|
-
provisionRuntimeRepo: externalEffects.runtimeRepo,
|
|
16301
16984
|
provisionTicketBoard: externalEffects.ticketBoard,
|
|
16302
16985
|
enableSystemd: externalEffects.systemd,
|
|
16303
16986
|
skipPlane,
|
|
16304
16987
|
registryPath: input.registryPath,
|
|
16305
|
-
|
|
16988
|
+
// A PROPOSAL only. The provider assigns the real identifier and
|
|
16989
|
+
// `pj project identity` reads it back; MCP never mints a board key.
|
|
16990
|
+
projectIdentifier: input.projectIdentifier ?? proposeProjectIdentifier(projectSlug),
|
|
16306
16991
|
ticketProvider,
|
|
16307
16992
|
boardId,
|
|
16308
16993
|
boardUrl: input.boardUrl,
|
|
@@ -16334,7 +17019,6 @@ server.registerTool(
|
|
|
16334
17019
|
force: overwrite,
|
|
16335
17020
|
skipTelegram: true,
|
|
16336
17021
|
skipEmail: true,
|
|
16337
|
-
skipRuntimeRepo: plannedAgent?.kind === "hermes.provision-agent" ? plannedAgent.context.skipRuntimeRepo : true,
|
|
16338
17022
|
skipPlane: plannedAgent?.kind === "hermes.provision-agent" ? plannedAgent.context.skipPlane : true,
|
|
16339
17023
|
skipBloodbank: true,
|
|
16340
17024
|
skipSystemd: plannedAgent?.kind === "hermes.provision-agent" ? plannedAgent.context.skipSystemd : true
|
|
@@ -16381,7 +17065,7 @@ server.registerTool(
|
|
|
16381
17065
|
agentRole: AGENT_ROLE_SCHEMA.optional(),
|
|
16382
17066
|
apply: z.boolean().optional(),
|
|
16383
17067
|
live: z.boolean().optional(),
|
|
16384
|
-
provisionRuntimeRepo:
|
|
17068
|
+
provisionRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
|
|
16385
17069
|
provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; also requires live=true and skipPlane!=true."),
|
|
16386
17070
|
enableSystemd: z.boolean().optional().describe("Explicitly opt in to Hermes systemd installation/enablement; also requires live=true."),
|
|
16387
17071
|
skipPlane: z.boolean().optional().describe("Disable project-board planning and provider invocation even when live=true."),
|
|
@@ -16408,7 +17092,6 @@ server.registerTool(
|
|
|
16408
17092
|
agentRole: input.agentRole,
|
|
16409
17093
|
apply: input.apply ?? false,
|
|
16410
17094
|
live: input.live ?? false,
|
|
16411
|
-
provisionRuntimeRepo: externalEffects.runtimeRepo,
|
|
16412
17095
|
provisionTicketBoard: externalEffects.ticketBoard,
|
|
16413
17096
|
enableSystemd: externalEffects.systemd,
|
|
16414
17097
|
skipPlane: input.skipPlane ?? false,
|
|
@@ -16421,7 +17104,7 @@ server.registerTool(
|
|
|
16421
17104
|
registryPath: input.registryPath,
|
|
16422
17105
|
force: input.force ?? false,
|
|
16423
17106
|
overwrite: input.force ?? false,
|
|
16424
|
-
scaffold: !(input.targetDir &&
|
|
17107
|
+
scaffold: !(input.targetDir && existsSync23(join30(resolve16(input.targetDir), ".git")))
|
|
16425
17108
|
});
|
|
16426
17109
|
if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
|
|
16427
17110
|
const preflight = preflightProjectApply(plan, resolvePjanglerRoot3());
|
|
@@ -16589,11 +17272,11 @@ server.registerTool(
|
|
|
16589
17272
|
local: z.boolean().optional(),
|
|
16590
17273
|
apply: z.boolean().optional(),
|
|
16591
17274
|
live: z.boolean().optional(),
|
|
16592
|
-
provisionRuntimeRepo:
|
|
17275
|
+
provisionRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
|
|
16593
17276
|
provisionTicketBoard: z.boolean().optional().describe("Explicitly opt in to ticket-board provisioning; requires live=true, local=false, and skipPlane!=true."),
|
|
16594
17277
|
enableSystemd: z.boolean().optional().describe("Explicitly opt in to systemd installation/enablement; requires live=true, local=false, and skipSystemd!=true."),
|
|
16595
17278
|
force: z.boolean().optional(),
|
|
16596
|
-
skipRuntimeRepo:
|
|
17279
|
+
skipRuntimeRepo: RUNTIME_REPO_COMPAT_SCHEMA,
|
|
16597
17280
|
skipPlane: z.boolean().optional(),
|
|
16598
17281
|
skipSystemd: z.boolean().optional(),
|
|
16599
17282
|
ticketProvider: TICKET_PROVIDER_SCHEMA.optional()
|
|
@@ -16665,7 +17348,7 @@ server.registerTool(
|
|
|
16665
17348
|
quiet: true,
|
|
16666
17349
|
local,
|
|
16667
17350
|
live,
|
|
16668
|
-
targetRepo: input.targetRepo ??
|
|
17351
|
+
targetRepo: input.targetRepo ?? basename9(resolvedTarget),
|
|
16669
17352
|
role: normalizeAgentRole(input.role),
|
|
16670
17353
|
agentPurpose: input.agentPurpose,
|
|
16671
17354
|
soulTone: input.soulTone,
|
|
@@ -16681,13 +17364,11 @@ server.registerTool(
|
|
|
16681
17364
|
// unreachable and therefore cannot consume JSON-RPC stdin.
|
|
16682
17365
|
skipTelegram: true,
|
|
16683
17366
|
skipEmail: true,
|
|
16684
|
-
skipRuntimeRepo: !externalEffects.runtimeRepo,
|
|
16685
17367
|
skipPlane: !externalEffects.ticketBoard,
|
|
16686
17368
|
skipBloodbank: true,
|
|
16687
17369
|
skipSystemd: !externalEffects.systemd || process.platform === "darwin",
|
|
16688
17370
|
trustedCopier,
|
|
16689
17371
|
deferredExternalEffects: {
|
|
16690
|
-
runtimeRepo: externalEffects.runtimeRepo,
|
|
16691
17372
|
ticketBoard: externalEffects.ticketBoard,
|
|
16692
17373
|
systemd: externalEffects.systemd,
|
|
16693
17374
|
owner: "hermes"
|
|
@@ -16703,6 +17384,7 @@ server.registerTool(
|
|
|
16703
17384
|
apply,
|
|
16704
17385
|
live,
|
|
16705
17386
|
bloodbankMode: "fleet-shared",
|
|
17387
|
+
runtimeMode: "role-local-ignored",
|
|
16706
17388
|
guidance: parityGuidance(),
|
|
16707
17389
|
context: {
|
|
16708
17390
|
targetRepo: context.targetRepo,
|
|
@@ -16711,7 +17393,6 @@ server.registerTool(
|
|
|
16711
17393
|
dryRun: context.dryRun,
|
|
16712
17394
|
quiet: context.quiet,
|
|
16713
17395
|
force: context.force,
|
|
16714
|
-
skipRuntimeRepo: context.skipRuntimeRepo,
|
|
16715
17396
|
skipPlane: context.skipPlane,
|
|
16716
17397
|
skipSystemd: context.skipSystemd
|
|
16717
17398
|
},
|
|
@@ -16729,3 +17410,4 @@ server.registerTool(
|
|
|
16729
17410
|
);
|
|
16730
17411
|
var transport = new StdioServerTransport();
|
|
16731
17412
|
await server.connect(transport);
|
|
17413
|
+
//# sourceMappingURL=mcp-server.js.map
|