@delorenj/pjangler 1.4.4 → 1.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -1
- package/contracts/fleet-contract.yaml +82 -11
- package/dist/index.js +1416 -158
- package/dist/mcp-server.js +1359 -145
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -219,7 +219,7 @@ var init_types = __esm({
|
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
// src/utils/tree-diff.ts
|
|
222
|
-
import { createHash as
|
|
222
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
223
223
|
import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync as readlinkSync2 } from "node:fs";
|
|
224
224
|
import { join as join6, relative as relative3 } from "node:path";
|
|
225
225
|
function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
|
|
@@ -230,7 +230,7 @@ function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()
|
|
|
230
230
|
if (stat.isSymbolicLink()) {
|
|
231
231
|
snapshot.set(rel, `link:${readlinkSync2(current)}`);
|
|
232
232
|
} else if (stat.isFile()) {
|
|
233
|
-
snapshot.set(rel, `file:${
|
|
233
|
+
snapshot.set(rel, `file:${createHash4("sha256").update(readFileSync5(current)).digest("hex")}:${stat.mode & 511}`);
|
|
234
234
|
} else if (stat.isDirectory()) {
|
|
235
235
|
snapshot.set(rel, `dir:${stat.mode & 511}`);
|
|
236
236
|
for (const name of readdirSync3(current)) snapshotTree(root, join6(current, name), snapshot);
|
|
@@ -249,7 +249,7 @@ var init_tree_diff = __esm({
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
// src/lifecycle/preflight.ts
|
|
252
|
-
import { createHash as
|
|
252
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
253
253
|
import {
|
|
254
254
|
accessSync,
|
|
255
255
|
constants as constants2,
|
|
@@ -267,7 +267,7 @@ function containedBy(parent, candidate) {
|
|
|
267
267
|
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
268
268
|
}
|
|
269
269
|
function sha2562(path) {
|
|
270
|
-
return
|
|
270
|
+
return createHash5("sha256").update(readFileSync6(path)).digest("base64url");
|
|
271
271
|
}
|
|
272
272
|
function fingerprint(path) {
|
|
273
273
|
const absolute = resolve4(path);
|
|
@@ -2760,7 +2760,7 @@ var init_config = __esm({
|
|
|
2760
2760
|
});
|
|
2761
2761
|
|
|
2762
2762
|
// src/notebook/notes.ts
|
|
2763
|
-
import { createHash as
|
|
2763
|
+
import { createHash as createHash6, randomUUID as randomUUID2 } from "node:crypto";
|
|
2764
2764
|
function canonicalJson(value) {
|
|
2765
2765
|
if (value === void 0 || typeof value === "function" || typeof value === "symbol" || typeof value === "bigint") throw new NotebookError("INVALID_INPUT", "Canonical JSON value is not JSON-compatible");
|
|
2766
2766
|
if (value === null || typeof value !== "object") {
|
|
@@ -2773,7 +2773,7 @@ function canonicalJson(value) {
|
|
|
2773
2773
|
return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
2774
2774
|
}
|
|
2775
2775
|
function sha256Hex(value) {
|
|
2776
|
-
return
|
|
2776
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
2777
2777
|
}
|
|
2778
2778
|
function base64UrlEncode(value) {
|
|
2779
2779
|
return Buffer.from(value, "utf8").toString("base64url");
|
|
@@ -3315,7 +3315,7 @@ var init_remote_mutation_schema = __esm({
|
|
|
3315
3315
|
});
|
|
3316
3316
|
|
|
3317
3317
|
// src/notebook/state.ts
|
|
3318
|
-
import { createHash as
|
|
3318
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
3319
3319
|
import {
|
|
3320
3320
|
closeSync as closeSync4,
|
|
3321
3321
|
constants as constants3,
|
|
@@ -3340,7 +3340,7 @@ function notebookStateRoot(env2 = process.env) {
|
|
|
3340
3340
|
}
|
|
3341
3341
|
function deriveSessionKey(projectSlug, client, clientSessionId) {
|
|
3342
3342
|
if (!projectSlug || !client || !clientSessionId) throw new NotebookError("INVALID_INPUT", "project slug, client, and client session id are required");
|
|
3343
|
-
return
|
|
3343
|
+
return createHash7("sha256").update(`pjangler-session-v1\0${projectSlug}\0${client}\0${clientSessionId}`, "utf8").digest("hex");
|
|
3344
3344
|
}
|
|
3345
3345
|
function deriveReceiptId(sessionKey) {
|
|
3346
3346
|
assertDigest(sessionKey, "session key");
|
|
@@ -5842,8 +5842,8 @@ var init_capture = __esm({
|
|
|
5842
5842
|
|
|
5843
5843
|
// src/index.ts
|
|
5844
5844
|
import { spawnSync as spawnSync16 } from "node:child_process";
|
|
5845
|
-
import { existsSync as existsSync30, readFileSync as
|
|
5846
|
-
import { basename as basename10, join as
|
|
5845
|
+
import { existsSync as existsSync30, readFileSync as readFileSync30, statSync as statSync9 } from "node:fs";
|
|
5846
|
+
import { basename as basename10, join as join38, resolve as resolve24 } from "node:path";
|
|
5847
5847
|
import { Command as Command3, CommanderError } from "commander";
|
|
5848
5848
|
|
|
5849
5849
|
// src/commands/hermes/types.ts
|
|
@@ -6551,7 +6551,7 @@ import { existsSync as existsSync3, lstatSync as lstatSync3, mkdirSync as mkdirS
|
|
|
6551
6551
|
import { basename as basename3, dirname as dirname3, join as join4, relative as relative2, resolve as resolve3 } from "node:path";
|
|
6552
6552
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6553
6553
|
import { homedir as homedir3 } from "node:os";
|
|
6554
|
-
import { createHash as
|
|
6554
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
6555
6555
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
6556
6556
|
import YAML from "yaml";
|
|
6557
6557
|
|
|
@@ -6567,9 +6567,149 @@ var SUPPORTED_CLIS = Object.freeze([
|
|
|
6567
6567
|
var SUPPORTED_BMAD_TOOLS = SUPPORTED_CLIS.map((cli) => cli.bmadTool);
|
|
6568
6568
|
var SUPPORTED_CLI_ROOTS = SUPPORTED_CLIS.map((cli) => cli.projectRoot);
|
|
6569
6569
|
|
|
6570
|
+
// src/scaffold/compare.ts
|
|
6571
|
+
import { createHash } from "node:crypto";
|
|
6572
|
+
var PLACEHOLDER = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/gu;
|
|
6573
|
+
var JINJA_DELIMITER = /\{\{|\{%|\{#/u;
|
|
6574
|
+
function renderTemplate(source, inputs) {
|
|
6575
|
+
const stripped = source.replace(PLACEHOLDER, "");
|
|
6576
|
+
if (JINJA_DELIMITER.test(stripped)) {
|
|
6577
|
+
return { ok: false, reason: "render-unsupported", detail: "render-unsupported: template uses Jinja constructs beyond simple substitution" };
|
|
6578
|
+
}
|
|
6579
|
+
const undeclared = /* @__PURE__ */ new Set();
|
|
6580
|
+
const missing = /* @__PURE__ */ new Set();
|
|
6581
|
+
const text3 = source.replace(PLACEHOLDER, (whole, name) => {
|
|
6582
|
+
if (!Object.prototype.hasOwnProperty.call(inputs, name)) {
|
|
6583
|
+
undeclared.add(name);
|
|
6584
|
+
return whole;
|
|
6585
|
+
}
|
|
6586
|
+
const value = inputs[name];
|
|
6587
|
+
if (value === null || value === void 0) {
|
|
6588
|
+
missing.add(name);
|
|
6589
|
+
return whole;
|
|
6590
|
+
}
|
|
6591
|
+
return value;
|
|
6592
|
+
});
|
|
6593
|
+
if (undeclared.size > 0) {
|
|
6594
|
+
return { ok: false, reason: "render-unsupported", detail: `render-unsupported: undeclared placeholder ${[...undeclared].sort().join(", ")}` };
|
|
6595
|
+
}
|
|
6596
|
+
if (missing.size > 0) {
|
|
6597
|
+
return { ok: false, reason: "input-missing", detail: `input-missing: ${[...missing].sort().join(", ")}` };
|
|
6598
|
+
}
|
|
6599
|
+
return { ok: true, text: text3 };
|
|
6600
|
+
}
|
|
6601
|
+
function blobId(bytes) {
|
|
6602
|
+
return createHash("sha1").update(`blob ${bytes.byteLength}\0`, "latin1").update(bytes).digest("hex");
|
|
6603
|
+
}
|
|
6604
|
+
function digestPrefix(id) {
|
|
6605
|
+
return id.slice(0, 12);
|
|
6606
|
+
}
|
|
6607
|
+
function globToRegExp(glob) {
|
|
6608
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/gu, "\\$&").replace(/\*/gu, "[^/]*").replace(/\?/gu, "[^/]");
|
|
6609
|
+
return new RegExp(`^${escaped}$`, "u");
|
|
6610
|
+
}
|
|
6611
|
+
function matchesExcluded(path, patterns) {
|
|
6612
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
6613
|
+
for (const pattern of patterns) {
|
|
6614
|
+
if (pattern === "" || pattern === "/") continue;
|
|
6615
|
+
const directory = pattern.endsWith("/");
|
|
6616
|
+
const glob = directory ? pattern.slice(0, -1) : pattern;
|
|
6617
|
+
if (glob === "") continue;
|
|
6618
|
+
if (glob.includes("/")) {
|
|
6619
|
+
const expression2 = globToRegExp(glob.replace(/^\/+/u, ""));
|
|
6620
|
+
if (directory ? segments.some((_, index) => index < segments.length - 1 && expression2.test(segments.slice(0, index + 1).join("/"))) : expression2.test(segments.join("/"))) return pattern;
|
|
6621
|
+
continue;
|
|
6622
|
+
}
|
|
6623
|
+
const expression = globToRegExp(glob);
|
|
6624
|
+
const candidates = directory ? segments.slice(0, -1) : segments;
|
|
6625
|
+
if (candidates.some((segment) => expression.test(segment))) return pattern;
|
|
6626
|
+
}
|
|
6627
|
+
return null;
|
|
6628
|
+
}
|
|
6629
|
+
function groupFor(path, groups) {
|
|
6630
|
+
let best = null;
|
|
6631
|
+
let bestLength = -1;
|
|
6632
|
+
for (const leaf of Object.keys(groups).sort()) {
|
|
6633
|
+
const rolePath = groups[leaf];
|
|
6634
|
+
const matches = rolePath.endsWith("/") ? path.startsWith(rolePath) : path === rolePath;
|
|
6635
|
+
if (!matches) continue;
|
|
6636
|
+
const length = rolePath.endsWith("/") ? rolePath.length : rolePath.length + 1;
|
|
6637
|
+
if (length > bestLength) {
|
|
6638
|
+
best = leaf;
|
|
6639
|
+
bestLength = length;
|
|
6640
|
+
}
|
|
6641
|
+
}
|
|
6642
|
+
return best;
|
|
6643
|
+
}
|
|
6644
|
+
function typeWord(asset) {
|
|
6645
|
+
return asset.type;
|
|
6646
|
+
}
|
|
6647
|
+
function modeWord(executable) {
|
|
6648
|
+
return executable ? "100755" : "100644";
|
|
6649
|
+
}
|
|
6650
|
+
function compareAssets(desired, observe, options) {
|
|
6651
|
+
const out = [];
|
|
6652
|
+
const compareModes = options.modes !== false;
|
|
6653
|
+
const push = (asset, seen, kind, desiredWord, observedWord, detail = null) => {
|
|
6654
|
+
out.push({ path: asset.path, kind, desired: desiredWord, observed: observedWord, detail, wip: seen.wip });
|
|
6655
|
+
};
|
|
6656
|
+
for (const asset of desired) {
|
|
6657
|
+
const seen = observe(asset);
|
|
6658
|
+
if (asset.incomplete) {
|
|
6659
|
+
push(asset, seen, "incomplete", null, seen.present ? seen.type : "absent", asset.incomplete.detail);
|
|
6660
|
+
continue;
|
|
6661
|
+
}
|
|
6662
|
+
if (!seen.present) {
|
|
6663
|
+
push(asset, seen, "missing", typeWord(asset), "absent");
|
|
6664
|
+
continue;
|
|
6665
|
+
}
|
|
6666
|
+
if (seen.type === "symlink" && seen.unsafeSymlink) {
|
|
6667
|
+
push(asset, seen, "unsafe-symlink", typeWord(asset), "symlink");
|
|
6668
|
+
continue;
|
|
6669
|
+
}
|
|
6670
|
+
if (seen.type !== asset.type) {
|
|
6671
|
+
push(asset, seen, "wrong-type", typeWord(asset), seen.type ?? "unknown");
|
|
6672
|
+
continue;
|
|
6673
|
+
}
|
|
6674
|
+
if (compareModes && asset.type === "file" && seen.executable !== asset.executable) {
|
|
6675
|
+
push(asset, seen, "wrong-mode", modeWord(asset.executable), modeWord(seen.executable));
|
|
6676
|
+
}
|
|
6677
|
+
if (asset.presenceOnly || asset.blobId === null) continue;
|
|
6678
|
+
if (seen.unreadable !== null || seen.blobId === null) {
|
|
6679
|
+
push(asset, seen, "incomplete", digestPrefix(asset.blobId), null, `unreadable: ${seen.unreadable ?? "unread"}`);
|
|
6680
|
+
continue;
|
|
6681
|
+
}
|
|
6682
|
+
if (seen.blobId === asset.blobId) continue;
|
|
6683
|
+
const kind = options.inLineage(seen.blobId, asset.path) ? "stale-content" : "locally-modified";
|
|
6684
|
+
push(asset, seen, kind, digestPrefix(asset.blobId), digestPrefix(seen.blobId));
|
|
6685
|
+
}
|
|
6686
|
+
return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0);
|
|
6687
|
+
}
|
|
6688
|
+
function classifyExtras(input) {
|
|
6689
|
+
const unexpected = [];
|
|
6690
|
+
let foreignTracked = 0;
|
|
6691
|
+
const runtimePrefix = `${input.runtimeDir.replace(/\/+$/u, "")}/`;
|
|
6692
|
+
for (const path of input.trackedPaths) {
|
|
6693
|
+
if (input.ownedPaths.has(path)) continue;
|
|
6694
|
+
if (path.startsWith(runtimePrefix)) {
|
|
6695
|
+
foreignTracked += 1;
|
|
6696
|
+
continue;
|
|
6697
|
+
}
|
|
6698
|
+
const group = groupFor(path, input.groups);
|
|
6699
|
+
if (group === null) {
|
|
6700
|
+
foreignTracked += 1;
|
|
6701
|
+
continue;
|
|
6702
|
+
}
|
|
6703
|
+
const excluded = matchesExcluded(path, input.excludedPatterns);
|
|
6704
|
+
unexpected.push({ path, group, detail: excluded === null ? null : `matches excluded pattern ${excluded}` });
|
|
6705
|
+
}
|
|
6706
|
+
unexpected.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
6707
|
+
return { unexpected, foreignTracked };
|
|
6708
|
+
}
|
|
6709
|
+
|
|
6570
6710
|
// src/parity/pack.ts
|
|
6571
6711
|
import { closeSync, constants, fstatSync, lstatSync as lstatSync2, openSync, readFileSync as readFileSync2, readdirSync } from "node:fs";
|
|
6572
|
-
import { createHash } from "node:crypto";
|
|
6712
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
6573
6713
|
import { basename as basename2, join as join3, relative, resolve as resolve2, sep } from "node:path";
|
|
6574
6714
|
var PackUnavailableError = class extends Error {
|
|
6575
6715
|
constructor(message) {
|
|
@@ -6579,7 +6719,7 @@ var PackUnavailableError = class extends Error {
|
|
|
6579
6719
|
};
|
|
6580
6720
|
var CANONICAL_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
6581
6721
|
function sha256(content) {
|
|
6582
|
-
return
|
|
6722
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
6583
6723
|
}
|
|
6584
6724
|
function readRegularFile(path) {
|
|
6585
6725
|
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
@@ -7891,7 +8031,7 @@ function availableSkillsRegistryRoots(ctx) {
|
|
|
7891
8031
|
);
|
|
7892
8032
|
}
|
|
7893
8033
|
function digestSkillEntry(root) {
|
|
7894
|
-
const hash =
|
|
8034
|
+
const hash = createHash3("sha256");
|
|
7895
8035
|
try {
|
|
7896
8036
|
const stat = lstatSync3(root);
|
|
7897
8037
|
if (stat.isSymbolicLink()) return null;
|
|
@@ -8956,8 +9096,72 @@ ${roleSpecific}
|
|
|
8956
9096
|
Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
8957
9097
|
`;
|
|
8958
9098
|
}
|
|
9099
|
+
function renderScaffoldAsset(templateRoleDir, jinjaRel, inputs) {
|
|
9100
|
+
const result2 = renderTemplate(readText(join4(templateRoleDir, jinjaRel)), inputs);
|
|
9101
|
+
if (!result2.ok) throw new Error(`${jinjaRel}: ${result2.detail}`);
|
|
9102
|
+
return result2.text;
|
|
9103
|
+
}
|
|
8959
9104
|
function renderHermesWrapper(role, templateRoleDir) {
|
|
8960
|
-
return
|
|
9105
|
+
return renderScaffoldAsset(templateRoleDir, "hermes.jinja", { agent_id: role.agentId });
|
|
9106
|
+
}
|
|
9107
|
+
function sentinelPromptInputs(role) {
|
|
9108
|
+
return {
|
|
9109
|
+
agent_id: role.agentId,
|
|
9110
|
+
role: role.role,
|
|
9111
|
+
target_repo: role.repo,
|
|
9112
|
+
display_name: role.displayName || role.agentId,
|
|
9113
|
+
ticket_provider: role.ticketProviderName || "plane"
|
|
9114
|
+
};
|
|
9115
|
+
}
|
|
9116
|
+
function observeScaffoldAsset(path) {
|
|
9117
|
+
const seen = { present: false, type: null, executable: false, blobId: null, unsafeSymlink: false, unreadable: null, wip: false };
|
|
9118
|
+
try {
|
|
9119
|
+
const stat = lstatSync3(path);
|
|
9120
|
+
seen.present = true;
|
|
9121
|
+
if (stat.isSymbolicLink()) {
|
|
9122
|
+
seen.type = "symlink";
|
|
9123
|
+
seen.blobId = blobId(Buffer.from(readlinkSync(path), "utf8"));
|
|
9124
|
+
} else if (stat.isDirectory()) {
|
|
9125
|
+
seen.type = "directory";
|
|
9126
|
+
} else if (stat.isFile()) {
|
|
9127
|
+
seen.type = "file";
|
|
9128
|
+
seen.executable = (stat.mode & 73) !== 0;
|
|
9129
|
+
try {
|
|
9130
|
+
seen.blobId = blobId(readFileSync3(path));
|
|
9131
|
+
} catch {
|
|
9132
|
+
seen.unreadable = "unreadable";
|
|
9133
|
+
}
|
|
9134
|
+
} else {
|
|
9135
|
+
seen.type = "other";
|
|
9136
|
+
}
|
|
9137
|
+
} catch {
|
|
9138
|
+
}
|
|
9139
|
+
return seen;
|
|
9140
|
+
}
|
|
9141
|
+
function scaffoldDesiredForRule(role, templateRoleDir, managedScripts) {
|
|
9142
|
+
const desired = [];
|
|
9143
|
+
const asset = (path, blob, incomplete = null, presenceOnly = false) => {
|
|
9144
|
+
desired.push({ path, type: "file", executable: false, blobId: blob, presenceOnly, incomplete });
|
|
9145
|
+
};
|
|
9146
|
+
for (const rel of ["role.yaml", "SOUL.md", ".runtime-scaffold/README.md"]) asset(rel, null, null, true);
|
|
9147
|
+
const rendered = (path, jinjaRel, inputs) => {
|
|
9148
|
+
const source = join4(templateRoleDir, jinjaRel);
|
|
9149
|
+
if (!existsSync3(source)) {
|
|
9150
|
+
asset(path, null, { reason: "render-unsupported", detail: `render-unsupported: ${jinjaRel} is absent from the template` });
|
|
9151
|
+
return;
|
|
9152
|
+
}
|
|
9153
|
+
const result2 = renderTemplate(readFileSync3(source).toString("utf8"), inputs);
|
|
9154
|
+
if (!result2.ok) {
|
|
9155
|
+
asset(path, null, { reason: result2.reason, detail: result2.detail });
|
|
9156
|
+
return;
|
|
9157
|
+
}
|
|
9158
|
+
asset(path, blobId(Buffer.from(result2.text, "utf8")));
|
|
9159
|
+
};
|
|
9160
|
+
rendered("hermes", "hermes.jinja", { agent_id: role.agentId });
|
|
9161
|
+
rendered(".gitignore", ".gitignore.jinja", { role: role.role });
|
|
9162
|
+
for (const rel of managedScripts) asset(`.scripts/${rel}`, blobId(readFileSync3(join4(templateRoleDir, ".scripts", rel))));
|
|
9163
|
+
rendered(".scripts/sentinel.prompt.md", ".scripts/sentinel.prompt.md.jinja", sentinelPromptInputs(role));
|
|
9164
|
+
return desired;
|
|
8961
9165
|
}
|
|
8962
9166
|
function templateFiles(sourceDir, current = sourceDir) {
|
|
8963
9167
|
if (!existsSync3(current)) return [];
|
|
@@ -9011,7 +9215,7 @@ function managedHermesScaffoldRoles(ctx) {
|
|
|
9011
9215
|
return { roles, blockers };
|
|
9012
9216
|
}
|
|
9013
9217
|
function renderSentinelPrompt(role, templateRoleDir) {
|
|
9014
|
-
return
|
|
9218
|
+
return renderScaffoldAsset(templateRoleDir, join4(".scripts", "sentinel.prompt.md.jinja"), sentinelPromptInputs(role));
|
|
9015
9219
|
}
|
|
9016
9220
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
9017
9221
|
if (!existsSync3(sourceDir)) return;
|
|
@@ -9739,7 +9943,7 @@ function ownedRegistryEntries(registry, repoRoot) {
|
|
|
9739
9943
|
}
|
|
9740
9944
|
return owned;
|
|
9741
9945
|
}
|
|
9742
|
-
function unprovisionedRoleAgents(registry, repoRoot,
|
|
9946
|
+
function unprovisionedRoleAgents(registry, repoRoot, canonical2) {
|
|
9743
9947
|
const blockers = /* @__PURE__ */ new Map();
|
|
9744
9948
|
const record = (agentId, roleDir, source) => {
|
|
9745
9949
|
const current = blockers.get(agentId) ?? { roleDir, sources: /* @__PURE__ */ new Set() };
|
|
@@ -9748,12 +9952,12 @@ function unprovisionedRoleAgents(registry, repoRoot, canonical) {
|
|
|
9748
9952
|
blockers.set(agentId, current);
|
|
9749
9953
|
};
|
|
9750
9954
|
for (const [agentId, entry] of ownedRegistryEntries(registry, repoRoot)) {
|
|
9751
|
-
if (
|
|
9955
|
+
if (canonical2.has(agentId)) continue;
|
|
9752
9956
|
const roleDir = String(entry.role_dir ?? "");
|
|
9753
9957
|
if (declaredRoleIsUnprovisioned(repoRoot, roleDir)) record(agentId, roleDir, "registry");
|
|
9754
9958
|
}
|
|
9755
9959
|
for (const [agentId, entry] of declaredAgentEntries(repoRoot)) {
|
|
9756
|
-
if (
|
|
9960
|
+
if (canonical2.has(agentId)) continue;
|
|
9757
9961
|
const configured = String(entry.role_dir ?? "");
|
|
9758
9962
|
const roleDir = configured ? resolve3(repoRoot, configured) : "";
|
|
9759
9963
|
if (declaredRoleIsUnprovisioned(repoRoot, configured)) record(agentId, roleDir, ".project.json");
|
|
@@ -10356,7 +10560,7 @@ function unsupportedRootAttestation(repoRoot, rootName) {
|
|
|
10356
10560
|
for (const rel of walked.files) {
|
|
10357
10561
|
const expectedHash = inventory.files.get(rel);
|
|
10358
10562
|
if (!expectedHash) return { safe: false, reason: `${rootName}/${rel} is outside the BMAD generated inventory` };
|
|
10359
|
-
const actualHash =
|
|
10563
|
+
const actualHash = createHash3("sha256").update(readFileSync3(join4(root, rel))).digest("hex");
|
|
10360
10564
|
if (actualHash !== expectedHash) return { safe: false, reason: `${rootName}/${rel} was locally modified after generation` };
|
|
10361
10565
|
}
|
|
10362
10566
|
return { safe: true, reason: `${walked.files.length} file(s) match BMAD installer inventory and hashes` };
|
|
@@ -10903,15 +11107,15 @@ function createProjectJsonChecks() {
|
|
|
10903
11107
|
const droppedDetails = [];
|
|
10904
11108
|
const path = join4(ctx.repoRoot, ".project.json");
|
|
10905
11109
|
const existing = readProjectJson(ctx) ?? {};
|
|
10906
|
-
const
|
|
11110
|
+
const canonical2 = canonicalProjectJson(ctx);
|
|
10907
11111
|
const preservedDetails = [];
|
|
10908
|
-
for (const agentId of
|
|
11112
|
+
for (const agentId of canonical2.unprovisioned) {
|
|
10909
11113
|
const roleDir = existing.agents?.[agentId]?.role_dir;
|
|
10910
11114
|
preservedDetails.push(
|
|
10911
11115
|
`preserved unprovisioned declared agent: ${agentId}${typeof roleDir === "string" && roleDir ? ` (${roleDir} has no role.yaml)` : " (no role_dir)"}; provision or restore the role, do not delete its declaration`
|
|
10912
11116
|
);
|
|
10913
11117
|
}
|
|
10914
|
-
for (const agentId of
|
|
11118
|
+
for (const agentId of canonical2.dropped) {
|
|
10915
11119
|
const entry = existing.agents?.[agentId];
|
|
10916
11120
|
const entryRecord = typeof entry === "object" && entry !== null ? entry : void 0;
|
|
10917
11121
|
const declared = {
|
|
@@ -10924,7 +11128,7 @@ function createProjectJsonChecks() {
|
|
|
10924
11128
|
const reason = validation.details.join("; ") || "invalid";
|
|
10925
11129
|
droppedDetails.push(`dropped invalid declared agent: ${agentId} (${reason})`);
|
|
10926
11130
|
}
|
|
10927
|
-
const { dropped: _dropped, unprovisioned: _unprovisioned, ...canonicalJson2 } =
|
|
11131
|
+
const { dropped: _dropped, unprovisioned: _unprovisioned, ...canonicalJson2 } = canonical2;
|
|
10928
11132
|
const merged = { ...existing, ...canonicalJson2 };
|
|
10929
11133
|
const expected = `${JSON.stringify(merged, null, 2)}
|
|
10930
11134
|
`;
|
|
@@ -11622,26 +11826,19 @@ function createHermesChecks() {
|
|
|
11622
11826
|
const managedScripts = templateFiles(join4(templateRoleDir, ".scripts")).filter((rel) => rel !== "sentinel.prompt.md.jinja");
|
|
11623
11827
|
for (const role of selection.roles) {
|
|
11624
11828
|
const prefix = role.agentId || role.role;
|
|
11625
|
-
|
|
11626
|
-
|
|
11627
|
-
|
|
11628
|
-
const
|
|
11629
|
-
|
|
11630
|
-
|
|
11631
|
-
|
|
11632
|
-
|
|
11633
|
-
const
|
|
11634
|
-
|
|
11635
|
-
|
|
11636
|
-
|
|
11637
|
-
const source = join4(templateRoleDir, ".scripts", rel);
|
|
11638
|
-
const target = join4(role.roleDir, ".scripts", rel);
|
|
11639
|
-
if (!existsSync3(target)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, target)}`);
|
|
11640
|
-
else if (safeReadText(target) !== readText(source)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, target)}`);
|
|
11829
|
+
const memory = join4(role.roleDir, "runtime", "memories", "MEMORY.md");
|
|
11830
|
+
if (!existsSync3(memory)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, memory)}`);
|
|
11831
|
+
const desired = scaffoldDesiredForRule(role, templateRoleDir, managedScripts);
|
|
11832
|
+
const findings = compareAssets(
|
|
11833
|
+
desired,
|
|
11834
|
+
(asset) => observeScaffoldAsset(join4(role.roleDir, ...asset.path.split("/"))),
|
|
11835
|
+
{ inLineage: () => true, modes: false }
|
|
11836
|
+
);
|
|
11837
|
+
for (const finding2 of findings) {
|
|
11838
|
+
const word = finding2.kind === "stale-content" ? "stale" : finding2.kind;
|
|
11839
|
+
const shown = relative2(ctx.repoRoot, join4(role.roleDir, ...finding2.path.split("/")));
|
|
11840
|
+
details.push(`${prefix}: ${word} ${shown}${finding2.detail ? ` (${finding2.detail})` : ""}`);
|
|
11641
11841
|
}
|
|
11642
|
-
const promptPath = join4(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
11643
|
-
if (!existsSync3(promptPath)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, promptPath)}`);
|
|
11644
|
-
else if (safeReadText(promptPath) !== renderSentinelPrompt(role, templateRoleDir)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, promptPath)}`);
|
|
11645
11842
|
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) details.push(`${prefix}: .gitmodules contains retired ${role.role} runtime submodule mapping`);
|
|
11646
11843
|
if (!profileMetaInheritsDefault(join4(role.roleDir, "runtime", "profile.yaml"))) details.push(`${prefix}: runtime/profile.yaml missing inherited default config metadata`);
|
|
11647
11844
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -12244,9 +12441,9 @@ function createHermesChecks() {
|
|
|
12244
12441
|
}
|
|
12245
12442
|
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "warn", summary: `registry unreadable at ${registryPath2}`, details: [], fixable: false };
|
|
12246
12443
|
}
|
|
12247
|
-
const
|
|
12444
|
+
const canonical2 = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
12248
12445
|
const owned = ownedRegistryEntries(registry, ctx.repoRoot);
|
|
12249
|
-
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot,
|
|
12446
|
+
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot, canonical2);
|
|
12250
12447
|
if (unprovisioned.length) {
|
|
12251
12448
|
return {
|
|
12252
12449
|
id: "hermes.registry-parity",
|
|
@@ -12259,16 +12456,16 @@ function createHermesChecks() {
|
|
|
12259
12456
|
fixable: false
|
|
12260
12457
|
};
|
|
12261
12458
|
}
|
|
12262
|
-
if (
|
|
12459
|
+
if (canonical2.size === 0) {
|
|
12263
12460
|
return { id: "hermes.registry-parity", title: "Fleet registry matches .project.json (no duplicate or stale agents)", status: "skip", summary: "No Hermes roles, declarations, or registry entries present", details: [], fixable: false };
|
|
12264
12461
|
}
|
|
12265
12462
|
for (const [agentId, entry] of owned) {
|
|
12266
12463
|
const roleDir = String(entry?.role_dir ?? "");
|
|
12267
|
-
if (!
|
|
12268
|
-
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...
|
|
12464
|
+
if (!canonical2.has(agentId)) {
|
|
12465
|
+
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...canonical2].join(", ")})`);
|
|
12269
12466
|
}
|
|
12270
12467
|
}
|
|
12271
|
-
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !
|
|
12468
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical2.has(id))) {
|
|
12272
12469
|
details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
|
|
12273
12470
|
}
|
|
12274
12471
|
for (const role of roles) {
|
|
@@ -12357,8 +12554,8 @@ function createHermesChecks() {
|
|
|
12357
12554
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "registry is not valid YAML", changedFiles, details };
|
|
12358
12555
|
}
|
|
12359
12556
|
const agents = doc?.agents ?? {};
|
|
12360
|
-
const
|
|
12361
|
-
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot,
|
|
12557
|
+
const canonical2 = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
12558
|
+
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot, canonical2);
|
|
12362
12559
|
if (unprovisioned.length) {
|
|
12363
12560
|
return {
|
|
12364
12561
|
id: finding2.id,
|
|
@@ -12373,7 +12570,7 @@ function createHermesChecks() {
|
|
|
12373
12570
|
}
|
|
12374
12571
|
const fleetBin = fleetBinPath(ctx);
|
|
12375
12572
|
let dirty = false;
|
|
12376
|
-
if (
|
|
12573
|
+
if (canonical2.size === 0) {
|
|
12377
12574
|
for (const [agentId] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
12378
12575
|
details.push(`blocked: "${agentId}" has no role.yaml; provision the role instead of pruning the registry`);
|
|
12379
12576
|
}
|
|
@@ -12435,7 +12632,7 @@ function createHermesChecks() {
|
|
|
12435
12632
|
}
|
|
12436
12633
|
}
|
|
12437
12634
|
for (const [agentId, entry] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
12438
|
-
if (
|
|
12635
|
+
if (canonical2.size > 0 && !canonical2.has(agentId)) {
|
|
12439
12636
|
details.push(`drop stale/duplicate registry agent "${agentId}"`);
|
|
12440
12637
|
delete agents[agentId];
|
|
12441
12638
|
dropDeclaredAgent(ctx, agentId, changedFiles, details);
|
|
@@ -12455,8 +12652,8 @@ function createHermesChecks() {
|
|
|
12455
12652
|
dirty = true;
|
|
12456
12653
|
}
|
|
12457
12654
|
}
|
|
12458
|
-
if (
|
|
12459
|
-
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !
|
|
12655
|
+
if (canonical2.size > 0) {
|
|
12656
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical2.has(id))) {
|
|
12460
12657
|
dropDeclaredAgent(ctx, extra, changedFiles, details);
|
|
12461
12658
|
}
|
|
12462
12659
|
}
|
|
@@ -14524,7 +14721,7 @@ init_notes();
|
|
|
14524
14721
|
init_state();
|
|
14525
14722
|
init_types();
|
|
14526
14723
|
import { spawn, spawnSync as spawnSync12 } from "node:child_process";
|
|
14527
|
-
import { createHash as
|
|
14724
|
+
import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
|
|
14528
14725
|
import { chmodSync as chmodSync4, closeSync as closeSync6, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync18, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync11, mkdirSync as mkdirSync7, openSync as openSync6, readFileSync as readFileSync19, 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";
|
|
14529
14726
|
import { basename as basename9, dirname as dirname11, isAbsolute as isAbsolute4, join as join24, parse as parse3, relative as relative11, resolve as resolve12, sep as sep6 } from "node:path";
|
|
14530
14727
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
@@ -14619,7 +14816,7 @@ function enumerateSkillPayload(source) {
|
|
|
14619
14816
|
if (entry.isDirectory()) walk(path);
|
|
14620
14817
|
else if (entry.isFile()) {
|
|
14621
14818
|
const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
|
|
14622
|
-
result2.push({ path: rel, sha256:
|
|
14819
|
+
result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync19(path)).digest("hex"), mode: expectedMode });
|
|
14623
14820
|
} else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
14624
14821
|
}
|
|
14625
14822
|
};
|
|
@@ -14641,7 +14838,7 @@ function parsePackedManifest(source) {
|
|
|
14641
14838
|
const path = join24(source, ...entry.path.split("/"));
|
|
14642
14839
|
const stat = lstatSync11(path);
|
|
14643
14840
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
14644
|
-
const actual =
|
|
14841
|
+
const actual = createHash8("sha256").update(readFileSync19(path)).digest("hex");
|
|
14645
14842
|
if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
|
|
14646
14843
|
const actualMode = stat.mode & 511;
|
|
14647
14844
|
const executable = entry.mode === "0755";
|
|
@@ -14662,7 +14859,7 @@ function expectedPackedSkill() {
|
|
|
14662
14859
|
assertOwnedSkillTree(candidate);
|
|
14663
14860
|
const manifest = parsePackedManifest(candidate);
|
|
14664
14861
|
if (!manifest) continue;
|
|
14665
|
-
const digest =
|
|
14862
|
+
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
14666
14863
|
return { source: candidate, manifest, digest };
|
|
14667
14864
|
}
|
|
14668
14865
|
throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
|
|
@@ -14730,10 +14927,10 @@ function resolveProjectNotebookSkillSource(env2 = process.env) {
|
|
|
14730
14927
|
return explicit;
|
|
14731
14928
|
}
|
|
14732
14929
|
if (env2.PJ_SKILLS_REGISTRY_ROOT) {
|
|
14733
|
-
const
|
|
14734
|
-
if (existsSync18(join24(
|
|
14735
|
-
verifyProjectNotebookSkillExport(
|
|
14736
|
-
return
|
|
14930
|
+
const canonical2 = resolve12(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
|
|
14931
|
+
if (existsSync18(join24(canonical2, "SKILL.md"))) {
|
|
14932
|
+
verifyProjectNotebookSkillExport(canonical2);
|
|
14933
|
+
return canonical2;
|
|
14737
14934
|
}
|
|
14738
14935
|
}
|
|
14739
14936
|
try {
|
|
@@ -14801,7 +14998,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
14801
14998
|
const source = input.source ?? resolveProjectNotebookSkillSource(env2);
|
|
14802
14999
|
if (!source) throw new NotebookError("NOT_CONFIGURED", "Project Notebook skill source is unavailable");
|
|
14803
15000
|
const manifest = verifyProjectNotebookSkillExport(source);
|
|
14804
|
-
const digest =
|
|
15001
|
+
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
14805
15002
|
const home = env2.HOME;
|
|
14806
15003
|
if (!home || !resolve12(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
|
|
14807
15004
|
const skillsRoot = join24(home, ".agents", "skills");
|
|
@@ -14956,7 +15153,7 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
14956
15153
|
if (!existsSync18(path)) return true;
|
|
14957
15154
|
const stat = lstatSync11(path);
|
|
14958
15155
|
if (!stat.isFile()) return true;
|
|
14959
|
-
if (
|
|
15156
|
+
if (createHash8("sha256").update(readFileSync19(path)).digest("hex") !== entry.sha256) return true;
|
|
14960
15157
|
const mode = stat.mode & 511;
|
|
14961
15158
|
const executable = entry.mode === "0755";
|
|
14962
15159
|
return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
|
|
@@ -16391,8 +16588,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
16391
16588
|
} else try {
|
|
16392
16589
|
rmSync5(targetDir, { recursive: true, force: true });
|
|
16393
16590
|
const insideTarget = (path) => {
|
|
16394
|
-
const
|
|
16395
|
-
return
|
|
16591
|
+
const relative14 = relativePath(resolve14(targetDir), resolve14(path));
|
|
16592
|
+
return relative14 === "" || !relative14.startsWith("..") && !isAbsolute6(relative14);
|
|
16396
16593
|
};
|
|
16397
16594
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
16398
16595
|
changedFiles.length = 0;
|
|
@@ -16833,15 +17030,15 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
16833
17030
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
16834
17031
|
if (enterRe.test(content)) {
|
|
16835
17032
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
16836
|
-
const
|
|
16837
|
-
return `${head}${
|
|
17033
|
+
const sep8 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
17034
|
+
return `${head}${sep8}
|
|
16838
17035
|
${enterAdds}${close}`;
|
|
16839
17036
|
});
|
|
16840
17037
|
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
16841
17038
|
if (leaveRe.test(content)) {
|
|
16842
17039
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
16843
|
-
const
|
|
16844
|
-
return `${head}${
|
|
17040
|
+
const sep8 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
17041
|
+
return `${head}${sep8}
|
|
16845
17042
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
16846
17043
|
});
|
|
16847
17044
|
} else {
|
|
@@ -18059,7 +18256,7 @@ function git3(repo, args) {
|
|
|
18059
18256
|
return result2.stdout;
|
|
18060
18257
|
}
|
|
18061
18258
|
function gitAsync(repo, args) {
|
|
18062
|
-
return new Promise((
|
|
18259
|
+
return new Promise((resolve25) => {
|
|
18063
18260
|
const child = spawn2("git", ["-C", repo, ...args], { stdio: ["ignore", "pipe", "ignore"] });
|
|
18064
18261
|
let out = "";
|
|
18065
18262
|
let size = 0;
|
|
@@ -18067,7 +18264,7 @@ function gitAsync(repo, args) {
|
|
|
18067
18264
|
const finish = (value) => {
|
|
18068
18265
|
if (settled) return;
|
|
18069
18266
|
settled = true;
|
|
18070
|
-
|
|
18267
|
+
resolve25(value);
|
|
18071
18268
|
};
|
|
18072
18269
|
const timer = setTimeout(() => {
|
|
18073
18270
|
child.kill("SIGKILL");
|
|
@@ -18976,7 +19173,7 @@ var SHOW_CURSOR = "\x1B[?25h";
|
|
|
18976
19173
|
function runChecklist(options) {
|
|
18977
19174
|
const input = options.input ?? process.stdin;
|
|
18978
19175
|
const output = options.output ?? process.stdout;
|
|
18979
|
-
return new Promise((
|
|
19176
|
+
return new Promise((resolve25) => {
|
|
18980
19177
|
let state = createChecklist(options.items);
|
|
18981
19178
|
let previousLines = 0;
|
|
18982
19179
|
const draw = () => {
|
|
@@ -18996,7 +19193,7 @@ function runChecklist(options) {
|
|
|
18996
19193
|
return;
|
|
18997
19194
|
}
|
|
18998
19195
|
cleanup();
|
|
18999
|
-
|
|
19196
|
+
resolve25({ outcome: state.outcome, selected: state.outcome === "apply" ? selectedIds(state) : [] });
|
|
19000
19197
|
};
|
|
19001
19198
|
const cleanup = () => {
|
|
19002
19199
|
input.removeListener("keypress", onKey);
|
|
@@ -19013,7 +19210,7 @@ function runChecklist(options) {
|
|
|
19013
19210
|
input.once("end", () => {
|
|
19014
19211
|
if (state.outcome !== "pending") return;
|
|
19015
19212
|
cleanup();
|
|
19016
|
-
|
|
19213
|
+
resolve25({ outcome: "cancel", selected: [] });
|
|
19017
19214
|
});
|
|
19018
19215
|
});
|
|
19019
19216
|
}
|
|
@@ -19039,7 +19236,7 @@ function guardBrokenPipe(streams = [process.stdout, process.stderr]) {
|
|
|
19039
19236
|
}
|
|
19040
19237
|
function writeStdout(text3, stream = process.stdout) {
|
|
19041
19238
|
if (text3.length === 0) return Promise.resolve();
|
|
19042
|
-
return new Promise((
|
|
19239
|
+
return new Promise((resolve25, reject) => {
|
|
19043
19240
|
let settled = false;
|
|
19044
19241
|
const finish = (error) => {
|
|
19045
19242
|
if (settled) return;
|
|
@@ -19048,7 +19245,7 @@ function writeStdout(text3, stream = process.stdout) {
|
|
|
19048
19245
|
stream.removeListener("drain", onDrain);
|
|
19049
19246
|
stream.removeListener("close", onClose);
|
|
19050
19247
|
if (error && !isBrokenPipe(error)) reject(error);
|
|
19051
|
-
else
|
|
19248
|
+
else resolve25();
|
|
19052
19249
|
};
|
|
19053
19250
|
const onError = (error) => finish(error);
|
|
19054
19251
|
const onDrain = () => finish();
|
|
@@ -19069,13 +19266,13 @@ function drain(stream) {
|
|
|
19069
19266
|
if (typeof stream?.write !== "function") return Promise.resolve();
|
|
19070
19267
|
if (stream.destroyed || stream.writableEnded) return Promise.resolve();
|
|
19071
19268
|
if ((stream.writableLength ?? 0) === 0) return Promise.resolve();
|
|
19072
|
-
return new Promise((
|
|
19269
|
+
return new Promise((resolve25) => {
|
|
19073
19270
|
const done = () => {
|
|
19074
19271
|
clearTimeout(timer);
|
|
19075
19272
|
stream.removeListener("drain", done);
|
|
19076
19273
|
stream.removeListener("error", done);
|
|
19077
19274
|
stream.removeListener("close", done);
|
|
19078
|
-
|
|
19275
|
+
resolve25();
|
|
19079
19276
|
};
|
|
19080
19277
|
const timer = setTimeout(done, DRAIN_DEADLINE_MS);
|
|
19081
19278
|
stream.on("drain", done);
|
|
@@ -19461,7 +19658,7 @@ import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
|
19461
19658
|
import YAML9 from "yaml";
|
|
19462
19659
|
|
|
19463
19660
|
// src/fleet/types.ts
|
|
19464
|
-
var FLEET_SUPPORTED_SCHEMA_VERSIONS = { min: 1, max:
|
|
19661
|
+
var FLEET_SUPPORTED_SCHEMA_VERSIONS = { min: 1, max: 3 };
|
|
19465
19662
|
var FLEET_SCHEMA_VERSION = 1;
|
|
19466
19663
|
var FLEET_CONTRACT_ROOT_KEYS = [
|
|
19467
19664
|
"schema_version",
|
|
@@ -19473,20 +19670,34 @@ var FLEET_CONTRACT_ROOT_KEYS = [
|
|
|
19473
19670
|
"service_model",
|
|
19474
19671
|
"activation",
|
|
19475
19672
|
"retired",
|
|
19476
|
-
"health_policy"
|
|
19673
|
+
"health_policy",
|
|
19674
|
+
"scaffold_manifest"
|
|
19477
19675
|
];
|
|
19478
|
-
var FLEET_CONTRACT_OPTIONAL_ROOT_KEYS = ["health_policy"];
|
|
19676
|
+
var FLEET_CONTRACT_OPTIONAL_ROOT_KEYS = ["health_policy", "scaffold_manifest"];
|
|
19479
19677
|
var FLEET_HEALTH_POLICY_KEYS = [
|
|
19480
19678
|
"required_domains",
|
|
19481
19679
|
"deferred_capabilities",
|
|
19482
19680
|
"allowed_warnings",
|
|
19483
19681
|
"allowed_skips",
|
|
19484
|
-
"freshness"
|
|
19682
|
+
"freshness",
|
|
19683
|
+
"agent_exceptions"
|
|
19485
19684
|
];
|
|
19486
19685
|
var FLEET_HEALTH_POLICY_DEFERRED_KEYS = ["domain", "capability", "reason", "owner_story"];
|
|
19487
19686
|
var FLEET_HEALTH_POLICY_WARNING_KEYS = ["rule_id", "reason", "owner"];
|
|
19488
19687
|
var FLEET_HEALTH_POLICY_SKIP_KEYS = ["domain", "rule_id", "reason"];
|
|
19489
19688
|
var FLEET_HEALTH_POLICY_FRESHNESS_KEYS = ["field", "max_age_days", "applies_to"];
|
|
19689
|
+
var FLEET_HEALTH_POLICY_AGENT_EXCEPTION_KEYS = ["domain", "agent_id", "reason", "owner"];
|
|
19690
|
+
var FLEET_SCAFFOLD_MANIFEST_KEYS = [
|
|
19691
|
+
"template_submodule",
|
|
19692
|
+
"template_subdirectory",
|
|
19693
|
+
"render_suffix",
|
|
19694
|
+
"render_inputs",
|
|
19695
|
+
"groups",
|
|
19696
|
+
"presence_only",
|
|
19697
|
+
"excluded_patterns",
|
|
19698
|
+
"runtime_dir"
|
|
19699
|
+
];
|
|
19700
|
+
var FLEET_SCAFFOLD_PRESENCE_ONLY_KEYS = ["path", "reason"];
|
|
19490
19701
|
var FLEET_COMPATIBILITY_KEYS = ["min_schema_version", "max_schema_version"];
|
|
19491
19702
|
var FLEET_AUTHORITY_KEYS = ["owner", "store", "store_env", "writable_fields", "read_only", "notes"];
|
|
19492
19703
|
var FLEET_PROJECTION_KEYS = ["field", "source", "target", "direction", "writable_by"];
|
|
@@ -19612,6 +19823,9 @@ var FLEET_STATUS_MAX_OBSERVATIONS_PER_AGENT = 200;
|
|
|
19612
19823
|
var FLEET_STATUS_MAX_FINDINGS = 2e3;
|
|
19613
19824
|
var FLEET_STATUS_MAX_DETAILS = 20;
|
|
19614
19825
|
var FLEET_STATUS_AUDIT_CONCURRENCY = 4;
|
|
19826
|
+
var FLEET_STATUS_SCAFFOLD_CONCURRENCY = 4;
|
|
19827
|
+
var FLEET_STATUS_MAX_ITEMS = 100;
|
|
19828
|
+
var FLEET_STATUS_MAX_WIP_OVERLAP = 20;
|
|
19615
19829
|
var FLEET_STATUS_EVIDENCE = ["direct", "declared", "derived", "absent"];
|
|
19616
19830
|
var FLEET_STATUS_FRESHNESS_PRECEDENCE = [
|
|
19617
19831
|
"stale",
|
|
@@ -19768,7 +19982,8 @@ function validateFleetContract(document) {
|
|
|
19768
19982
|
() => validateAuthorityConflicts(policy),
|
|
19769
19983
|
() => sortDiagnostics(validateClassifications(policy)),
|
|
19770
19984
|
() => sortDiagnostics(validateRetiredModes(policy)),
|
|
19771
|
-
() => sortDiagnostics(validateHealthPolicy(policy))
|
|
19985
|
+
() => sortDiagnostics(validateHealthPolicy(policy)),
|
|
19986
|
+
() => sortDiagnostics(validateScaffoldManifest(policy))
|
|
19772
19987
|
];
|
|
19773
19988
|
for (const stage of stages) {
|
|
19774
19989
|
const findings = stage();
|
|
@@ -20362,11 +20577,146 @@ function validateHealthPolicy(policy) {
|
|
|
20362
20577
|
});
|
|
20363
20578
|
}
|
|
20364
20579
|
}
|
|
20580
|
+
const exceptions = block.agent_exceptions;
|
|
20581
|
+
if (exceptions !== void 0) {
|
|
20582
|
+
if (!Array.isArray(exceptions)) fail("health_policy.agent_exceptions", "agent_exceptions must be a list");
|
|
20583
|
+
else {
|
|
20584
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20585
|
+
exceptions.forEach((entry, index) => {
|
|
20586
|
+
const at = `health_policy.agent_exceptions[${index}]`;
|
|
20587
|
+
if (!isRecord6(entry)) {
|
|
20588
|
+
fail(at, "agent exception must be a mapping");
|
|
20589
|
+
return;
|
|
20590
|
+
}
|
|
20591
|
+
for (const key2 of Object.keys(entry)) {
|
|
20592
|
+
if (!FLEET_HEALTH_POLICY_AGENT_EXCEPTION_KEYS.includes(key2)) fail(`${at}.${key2}`, "unknown agent_exceptions key");
|
|
20593
|
+
}
|
|
20594
|
+
requireDomain(entry.domain, `${at}.domain`);
|
|
20595
|
+
if (typeof entry.agent_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(entry.agent_id)) {
|
|
20596
|
+
fail(`${at}.agent_id`, "agent_id must be a registry key: letters, digits, dots, underscores or hyphens");
|
|
20597
|
+
}
|
|
20598
|
+
for (const key2 of ["reason", "owner"]) {
|
|
20599
|
+
const value = entry[key2];
|
|
20600
|
+
if (typeof value !== "string" || value.trim().length === 0) fail(`${at}.${key2}`, `${key2} must be a non-empty string`);
|
|
20601
|
+
}
|
|
20602
|
+
const key = `${String(entry.domain)}\0${String(entry.agent_id)}`;
|
|
20603
|
+
if (seen.has(key)) fail(at, "duplicate agent exception: the same domain and agent_id is already ruled on");
|
|
20604
|
+
seen.add(key);
|
|
20605
|
+
});
|
|
20606
|
+
}
|
|
20607
|
+
}
|
|
20608
|
+
return findings;
|
|
20609
|
+
}
|
|
20610
|
+
var SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
20611
|
+
var RENDER_INPUT_NAME = /^[a-z_][a-z0-9_]*$/u;
|
|
20612
|
+
function relativeInside(value) {
|
|
20613
|
+
if (typeof value !== "string" || value === "" || value.startsWith("/") || value.includes("\\")) return false;
|
|
20614
|
+
const segments = value.replace(/\/+$/u, "").split("/");
|
|
20615
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
20616
|
+
}
|
|
20617
|
+
function validateScaffoldManifest(policy) {
|
|
20618
|
+
const block = policy.scaffold_manifest;
|
|
20619
|
+
if (block === void 0) return [];
|
|
20620
|
+
const findings = [];
|
|
20621
|
+
const fail = (path, message) => {
|
|
20622
|
+
findings.push({ code: "INVALID_INPUT", path, message });
|
|
20623
|
+
};
|
|
20624
|
+
if (!isRecord6(block)) {
|
|
20625
|
+
fail("scaffold_manifest", "scaffold_manifest must be a mapping");
|
|
20626
|
+
return findings;
|
|
20627
|
+
}
|
|
20628
|
+
for (const key of Object.keys(block)) {
|
|
20629
|
+
if (!FLEET_SCAFFOLD_MANIFEST_KEYS.includes(key)) fail(`scaffold_manifest.${key}`, "unknown scaffold_manifest key");
|
|
20630
|
+
}
|
|
20631
|
+
for (const key of ["template_submodule", "template_subdirectory"]) {
|
|
20632
|
+
if (!relativeInside(block[key]) || block[key].endsWith("/")) fail(`scaffold_manifest.${key}`, `${key} must be a relative path inside the repository`);
|
|
20633
|
+
}
|
|
20634
|
+
const suffix = block.render_suffix;
|
|
20635
|
+
if (typeof suffix !== "string" || !/^\.[A-Za-z0-9]+$/u.test(suffix)) fail("scaffold_manifest.render_suffix", "render_suffix must be a dotted extension such as .jinja");
|
|
20636
|
+
const runtimeDir = block.runtime_dir;
|
|
20637
|
+
if (typeof runtimeDir !== "string" || !SAFE_SEGMENT.test(runtimeDir)) fail("scaffold_manifest.runtime_dir", "runtime_dir must be one safe path segment");
|
|
20638
|
+
const declaredFields = /* @__PURE__ */ new Set();
|
|
20639
|
+
const authorities = policy.authorities;
|
|
20640
|
+
if (isRecord6(authorities)) {
|
|
20641
|
+
for (const entry of Object.values(authorities)) {
|
|
20642
|
+
if (!isRecord6(entry) || !Array.isArray(entry.writable_fields)) continue;
|
|
20643
|
+
for (const field3 of entry.writable_fields) if (typeof field3 === "string") declaredFields.add(field3);
|
|
20644
|
+
}
|
|
20645
|
+
}
|
|
20646
|
+
const inputs = block.render_inputs;
|
|
20647
|
+
if (!isRecord6(inputs)) fail("scaffold_manifest.render_inputs", "render_inputs must be a mapping of placeholder name to field path");
|
|
20648
|
+
else {
|
|
20649
|
+
for (const [name, field3] of Object.entries(inputs)) {
|
|
20650
|
+
const at = `scaffold_manifest.render_inputs.${name}`;
|
|
20651
|
+
if (!RENDER_INPUT_NAME.test(name)) fail(at, "a render input name must be a lower-case identifier");
|
|
20652
|
+
if (typeof field3 !== "string" || !FIELD_PATH.test(field3)) {
|
|
20653
|
+
fail(at, "must be a dotted field path");
|
|
20654
|
+
continue;
|
|
20655
|
+
}
|
|
20656
|
+
const resolvable = field3 === "agents.{agent_id}" || (field3.startsWith("agents.{agent_id}.") || field3.startsWith("projects.{slug}.")) && declaredFields.has(field3);
|
|
20657
|
+
if (!resolvable) fail(at, `${field3} is not agents.{agent_id} or a declared agents.{agent_id}.* / projects.{slug}.* writable field`);
|
|
20658
|
+
}
|
|
20659
|
+
}
|
|
20660
|
+
const groups = block.groups;
|
|
20661
|
+
const groupTable = {};
|
|
20662
|
+
if (!isRecord6(groups) || Object.keys(groups).length === 0) fail("scaffold_manifest.groups", "groups must be a non-empty mapping of declared leaf to role-relative path");
|
|
20663
|
+
else {
|
|
20664
|
+
const seenPaths = /* @__PURE__ */ new Map();
|
|
20665
|
+
for (const [leaf, rolePath] of Object.entries(groups)) {
|
|
20666
|
+
const at = `scaffold_manifest.groups.${leaf}`;
|
|
20667
|
+
if (!declaredFields.has(leaf)) fail(at, `${leaf} is not declared writable by any authority`);
|
|
20668
|
+
if (!relativeInside(rolePath)) {
|
|
20669
|
+
fail(at, "must be a role-relative path; a trailing / owns a directory");
|
|
20670
|
+
continue;
|
|
20671
|
+
}
|
|
20672
|
+
const previous = seenPaths.get(rolePath);
|
|
20673
|
+
if (previous !== void 0) fail(at, `duplicate group path ${rolePath}, already owned by ${previous}`);
|
|
20674
|
+
else seenPaths.set(rolePath, leaf);
|
|
20675
|
+
groupTable[leaf] = rolePath;
|
|
20676
|
+
}
|
|
20677
|
+
}
|
|
20678
|
+
const presence = block.presence_only;
|
|
20679
|
+
if (presence !== void 0) {
|
|
20680
|
+
if (!Array.isArray(presence)) fail("scaffold_manifest.presence_only", "presence_only must be a list");
|
|
20681
|
+
else {
|
|
20682
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20683
|
+
presence.forEach((entry, index) => {
|
|
20684
|
+
const at = `scaffold_manifest.presence_only[${index}]`;
|
|
20685
|
+
if (!isRecord6(entry)) {
|
|
20686
|
+
fail(at, "presence_only entry must be a mapping");
|
|
20687
|
+
return;
|
|
20688
|
+
}
|
|
20689
|
+
for (const key of Object.keys(entry)) {
|
|
20690
|
+
if (!FLEET_SCAFFOLD_PRESENCE_ONLY_KEYS.includes(key)) fail(`${at}.${key}`, "unknown presence_only key");
|
|
20691
|
+
}
|
|
20692
|
+
const path = entry.path;
|
|
20693
|
+
if (!relativeInside(path) || path.endsWith("/")) fail(`${at}.path`, "path must be a role-relative file path");
|
|
20694
|
+
else if (Object.keys(groupTable).length > 0 && groupFor(path, groupTable) === null) fail(`${at}.path`, `${path} resolves to no declared group`);
|
|
20695
|
+
else if (seen.has(path)) fail(`${at}.path`, `duplicate presence_only path ${path}`);
|
|
20696
|
+
else seen.add(path);
|
|
20697
|
+
if (typeof entry.reason !== "string" || entry.reason.trim().length === 0) fail(`${at}.reason`, "reason must be a non-empty string");
|
|
20698
|
+
});
|
|
20699
|
+
}
|
|
20700
|
+
}
|
|
20701
|
+
const excluded = block.excluded_patterns;
|
|
20702
|
+
if (excluded !== void 0) {
|
|
20703
|
+
if (!Array.isArray(excluded)) fail("scaffold_manifest.excluded_patterns", "excluded_patterns must be a list");
|
|
20704
|
+
else {
|
|
20705
|
+
excluded.forEach((pattern, index) => {
|
|
20706
|
+
const at = `scaffold_manifest.excluded_patterns[${index}]`;
|
|
20707
|
+
if (typeof pattern !== "string" || pattern.trim() === "" || pattern === "/" || pattern.startsWith("/")) {
|
|
20708
|
+
fail(at, "an excluded pattern must be a non-empty segment glob");
|
|
20709
|
+
return;
|
|
20710
|
+
}
|
|
20711
|
+
if (/^\*+\/?$/u.test(pattern) || pattern.includes("**")) fail(at, "an excluded pattern may not match every segment");
|
|
20712
|
+
});
|
|
20713
|
+
}
|
|
20714
|
+
}
|
|
20365
20715
|
return findings;
|
|
20366
20716
|
}
|
|
20367
20717
|
|
|
20368
20718
|
// src/fleet/inventory.ts
|
|
20369
|
-
import { createHash as
|
|
20719
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
20370
20720
|
import { existsSync as existsSync27, lstatSync as lstatSync15, readFileSync as readFileSync26, readlinkSync as readlinkSync3, statSync as statSync7 } from "node:fs";
|
|
20371
20721
|
import { homedir as homedir14 } from "node:os";
|
|
20372
20722
|
import { isAbsolute as isAbsolute7, join as join34, posix, relative as relative12, resolve as resolve20 } from "node:path";
|
|
@@ -20383,7 +20733,7 @@ function list(value) {
|
|
|
20383
20733
|
function readHealthPolicy(contract) {
|
|
20384
20734
|
const block = contract.health_policy;
|
|
20385
20735
|
if (!block) {
|
|
20386
|
-
return { declared: false, requiredDomains: /* @__PURE__ */ new Set(), deferred: [], warnings: [], skips: [], freshness: [] };
|
|
20736
|
+
return { declared: false, requiredDomains: /* @__PURE__ */ new Set(), deferred: [], warnings: [], skips: [], freshness: [], agentExceptions: [] };
|
|
20387
20737
|
}
|
|
20388
20738
|
return {
|
|
20389
20739
|
declared: true,
|
|
@@ -20391,7 +20741,8 @@ function readHealthPolicy(contract) {
|
|
|
20391
20741
|
deferred: list(block.deferred_capabilities).map((entry, index) => ({ path: `health_policy.deferred_capabilities[${index}]`, entry })),
|
|
20392
20742
|
warnings: list(block.allowed_warnings).map((entry, index) => ({ path: `health_policy.allowed_warnings[${index}]`, entry })),
|
|
20393
20743
|
skips: list(block.allowed_skips).map((entry, index) => ({ path: `health_policy.allowed_skips[${index}]`, entry })),
|
|
20394
|
-
freshness: list(block.freshness).map((entry, index) => ({ path: `health_policy.freshness[${index}]`, entry }))
|
|
20744
|
+
freshness: list(block.freshness).map((entry, index) => ({ path: `health_policy.freshness[${index}]`, entry })),
|
|
20745
|
+
agentExceptions: list(block.agent_exceptions).map((entry, index) => ({ path: `health_policy.agent_exceptions[${index}]`, entry }))
|
|
20395
20746
|
};
|
|
20396
20747
|
}
|
|
20397
20748
|
function evaluateFreshness(field3, isoValue, referenceMs, policy) {
|
|
@@ -20413,6 +20764,8 @@ var SOURCE_EVIDENCE = Object.freeze({
|
|
|
20413
20764
|
"fleet-inventory": "direct",
|
|
20414
20765
|
"fleet-provenance": "direct",
|
|
20415
20766
|
"recipe-audit": "direct",
|
|
20767
|
+
// The scaffold observer reads git objects and role directories itself.
|
|
20768
|
+
"fleet-scaffold": "direct",
|
|
20416
20769
|
"declared-gap": "absent"
|
|
20417
20770
|
});
|
|
20418
20771
|
var UNREAD_STATES = /* @__PURE__ */ new Set(["unobserved", "unsupported", "error"]);
|
|
@@ -20429,9 +20782,10 @@ function resolveJustification(input, policy) {
|
|
|
20429
20782
|
}
|
|
20430
20783
|
}
|
|
20431
20784
|
if (input.exceptionId !== null) {
|
|
20785
|
+
const policyPath = input.exceptionPolicy ?? `classifications.intentionally_unmanaged.entries.${bounded3(input.exceptionId, 128)}`;
|
|
20432
20786
|
return {
|
|
20433
20787
|
kind: "exception",
|
|
20434
|
-
policy:
|
|
20788
|
+
policy: bounded3(policyPath),
|
|
20435
20789
|
reason: bounded3(input.exceptionReason ?? "a managed exception the contract records for exactly these participants"),
|
|
20436
20790
|
owner: null
|
|
20437
20791
|
};
|
|
@@ -20880,6 +21234,7 @@ var FLEET_COMMAND_DATA_KEYS = {
|
|
|
20880
21234
|
"findings",
|
|
20881
21235
|
"probes",
|
|
20882
21236
|
"transitions",
|
|
21237
|
+
"scaffold",
|
|
20883
21238
|
"truncated"
|
|
20884
21239
|
]
|
|
20885
21240
|
};
|
|
@@ -21363,6 +21718,7 @@ function formatFleetProvenanceReport(provenance) {
|
|
|
21363
21718
|
var REPORT_MAX_AGENTS = 40;
|
|
21364
21719
|
var REPORT_MAX_OBSERVATIONS = 40;
|
|
21365
21720
|
var REPORT_MAX_TRANSITIONS = 30;
|
|
21721
|
+
var REPORT_MAX_ITEMS = 5;
|
|
21366
21722
|
function statusGlyph(state) {
|
|
21367
21723
|
if (state === "pass") return green(glyph.pass);
|
|
21368
21724
|
if (state === "fail" || state === "error") return red(glyph.fail);
|
|
@@ -21409,6 +21765,15 @@ function observationLines(observation3, width) {
|
|
|
21409
21765
|
if (observation3.justification) {
|
|
21410
21766
|
lines.push(` ${dim(glyph.arrow)} ${gray(`authorized by ${observation3.justification.policy}: ${observation3.justification.reason}`)}`);
|
|
21411
21767
|
}
|
|
21768
|
+
const items = observation3.items ?? [];
|
|
21769
|
+
for (const item of items.slice(0, REPORT_MAX_ITEMS)) {
|
|
21770
|
+
const pair = item.desired !== null || item.observed !== null ? ` ${item.desired ?? "-"}${glyph.arrow}${item.observed ?? "-"}` : "";
|
|
21771
|
+
const paint = item.kind === "incomplete" ? red : item.kind === "wrong-mode" || item.kind === "unexpected-owned" ? yellow : red;
|
|
21772
|
+
lines.push(` ${dim(glyph.arrow)} ${paint(item.kind)} ${bounded3(item.path)}${dim(pair)}${item.wip ? ` ${yellow("[wip]")}` : ""}${item.detail ? ` ${dim(`(${item.detail})`)}` : ""}`);
|
|
21773
|
+
}
|
|
21774
|
+
if (items.length > REPORT_MAX_ITEMS) {
|
|
21775
|
+
lines.push(` ${dim(glyph.arrow)} ${dim(`... ${items.length - REPORT_MAX_ITEMS} more item(s); use --json for all of them`)}`);
|
|
21776
|
+
}
|
|
21412
21777
|
if (observation3.state !== "pass" && observation3.state !== "skip") {
|
|
21413
21778
|
lines.push(` ${dim(glyph.arrow)} ${dim(observation3.retrieval)}`);
|
|
21414
21779
|
}
|
|
@@ -21431,6 +21796,18 @@ function agentLine(agent, width, domains) {
|
|
|
21431
21796
|
agent.lifecycle.capability_readiness === "ready" ? green("routing ready") : dim(`routing ${agent.lifecycle.capability_readiness}`),
|
|
21432
21797
|
agent.lifecycle.activation === "granted" ? yellow("activation granted") : dim(`activation ${agent.lifecycle.activation}`)
|
|
21433
21798
|
])}`);
|
|
21799
|
+
if (agent.scaffold) {
|
|
21800
|
+
const assets = agent.scaffold.assets;
|
|
21801
|
+
const cells2 = [
|
|
21802
|
+
dim(`scaffold ${assets.matching}/${assets.owned}`),
|
|
21803
|
+
assets.drifted ? red(`${assets.drifted} drifted`) : dim("0 drifted"),
|
|
21804
|
+
assets.incomplete ? yellow(`${assets.incomplete} undecided`) : dim("0 undecided"),
|
|
21805
|
+
assets.unexpected_owned ? yellow(`${assets.unexpected_owned} unexpected`) : dim("0 unexpected"),
|
|
21806
|
+
dim(`gitlink ${agent.scaffold.source_gitlink ? agent.scaffold.source_gitlink.slice(0, 12) : "unreadable"}`)
|
|
21807
|
+
];
|
|
21808
|
+
if (agent.scaffold.wip_overlap.length) cells2.push(yellow(`${agent.scaffold.wip_overlap.length} wip overlap`));
|
|
21809
|
+
lines.push(` ${dim(glyph.arrow)} ${joinDot(cells2)}`);
|
|
21810
|
+
}
|
|
21434
21811
|
if (agent.truncated) lines.push(` ${dim(glyph.arrow)} ${yellow(`observations clipped; ${agent.retrieval}`)}`);
|
|
21435
21812
|
return lines;
|
|
21436
21813
|
}
|
|
@@ -21481,6 +21858,27 @@ function formatFleetStatusReport(status) {
|
|
|
21481
21858
|
const stateCounts = Object.entries(rollup.counts).filter(([, count]) => count > 0).map(([state, count]) => statusColor2(state)(`${count} ${state}`));
|
|
21482
21859
|
lines.push(` ${statusGlyph(rollup.state)} ${padVisible(rollup.domain, domainWidth)} ${statusColor2(rollup.state)(rollup.state)} ${dim(`${rollup.agents} agent${rollup.agents === 1 ? "" : "s"}`)}`);
|
|
21483
21860
|
lines.push(` ${dim(glyph.arrow)} ${stateCounts.length ? joinDot(stateCounts) : dim("no observations")}`);
|
|
21861
|
+
if (rollup.domain === "template_scaffold" && status.scaffold) {
|
|
21862
|
+
const counts2 = status.scaffold.agents;
|
|
21863
|
+
lines.push(` ${dim(glyph.arrow)} ${joinDot([
|
|
21864
|
+
dim(`scaffold parity over ${counts2.applicable} of ${counts2.selected} selected`),
|
|
21865
|
+
counts2.passing ? green(`${counts2.passing} passing`) : dim("0 passing"),
|
|
21866
|
+
counts2.drifted ? red(`${counts2.drifted} drifted`) : dim("0 drifted"),
|
|
21867
|
+
counts2.incomplete ? yellow(`${counts2.incomplete} incomplete`) : dim("0 incomplete"),
|
|
21868
|
+
counts2.exception_authorized ? gray(`${counts2.exception_authorized} exception`) : dim("0 exception"),
|
|
21869
|
+
counts2.unobserved ? yellow(`${counts2.unobserved} unobserved`) : dim("0 unobserved")
|
|
21870
|
+
])}`);
|
|
21871
|
+
const source = status.scaffold.source;
|
|
21872
|
+
lines.push(` ${dim(glyph.arrow)} ${source.integrity === "ok" ? green("source ok") : red(`source ${source.integrity}`)} ${dim(`gitlink ${source.gitlink ? source.gitlink.slice(0, 12) : "none"}`)}`);
|
|
21873
|
+
const agreement = status.scaffold.rule_agreement;
|
|
21874
|
+
if (agreement.compared || agreement.disagree) {
|
|
21875
|
+
lines.push(` ${dim(glyph.arrow)} ${joinDot([
|
|
21876
|
+
dim(`rule agreement ${agreement.agree}/${agreement.compared}`),
|
|
21877
|
+
agreement.disagree ? red(`${agreement.disagree} disagree`) : dim("0 disagree"),
|
|
21878
|
+
dim(`${agreement.not_compared} not compared`)
|
|
21879
|
+
])}`);
|
|
21880
|
+
}
|
|
21881
|
+
}
|
|
21484
21882
|
for (const observation3 of rollup.observations) {
|
|
21485
21883
|
lines.push(` ${dim(glyph.arrow)} ${statusColor2(observation3.state)(observation3.state)} ${dim(observation3.summary)}`);
|
|
21486
21884
|
}
|
|
@@ -21628,18 +22026,27 @@ async function probe(ctx, argv, cwd) {
|
|
|
21628
22026
|
const { outcome, value } = await runBoundedChild(ctx, command, args, { cwd });
|
|
21629
22027
|
return { outcome, value };
|
|
21630
22028
|
}
|
|
22029
|
+
async function probeRaw(ctx, argv, cwd, input) {
|
|
22030
|
+
const [command, ...args] = argv;
|
|
22031
|
+
if (!command) return { outcome: "failed", value: null };
|
|
22032
|
+
const { outcome, bytes } = await runBoundedChild(ctx, command, args, { cwd, input, raw: true });
|
|
22033
|
+
return { outcome, value: outcome === "ok" ? bytes : null };
|
|
22034
|
+
}
|
|
21631
22035
|
function captureSelf(ctx, entry, args, cwd, env2, timeoutMs) {
|
|
21632
22036
|
return runBoundedChild(ctx, process.execPath, [entry, ...args], { cwd, env: env2, timeoutMs, keepStdoutOnFailure: true });
|
|
21633
22037
|
}
|
|
21634
22038
|
function runBoundedChild(ctx, command, args, options = {}) {
|
|
21635
|
-
const { cwd, env: env2, keepStdoutOnFailure = false, timeoutMs } = options;
|
|
22039
|
+
const { cwd, env: env2, keepStdoutOnFailure = false, timeoutMs, input, raw = false } = options;
|
|
21636
22040
|
const budget = Math.min(timeoutMs ?? ctx.probeTimeoutMs, remainingMs(ctx));
|
|
21637
22041
|
return new Promise((settle) => {
|
|
21638
22042
|
let child;
|
|
21639
22043
|
try {
|
|
21640
22044
|
child = spawn4(command, [...args], {
|
|
21641
22045
|
cwd,
|
|
21642
|
-
|
|
22046
|
+
// stdin is `ignore` unless a caller has bytes to feed it: a child that
|
|
22047
|
+
// could wait on a terminal it never gets is a child the timeout has to
|
|
22048
|
+
// kill, and nothing here should need killing to finish.
|
|
22049
|
+
stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "ignore"],
|
|
21643
22050
|
// Its own process GROUP, so `kill` below can reach the whole tree.
|
|
21644
22051
|
// Measured: a shim that is `#!/bin/sh ... sleep 60` puts the sleep under
|
|
21645
22052
|
// the sh. SIGKILLing the sh alone leaves the sleep holding the write end
|
|
@@ -21655,12 +22062,13 @@ function runBoundedChild(ctx, command, args, options = {}) {
|
|
|
21655
22062
|
env: env2 ?? probeEnv()
|
|
21656
22063
|
});
|
|
21657
22064
|
} catch {
|
|
21658
|
-
settle({ outcome: "failed", value: null, code: null, overflow: false });
|
|
22065
|
+
settle({ outcome: "failed", value: null, code: null, overflow: false, bytes: null });
|
|
21659
22066
|
return;
|
|
21660
22067
|
}
|
|
21661
|
-
|
|
22068
|
+
const chunks = [];
|
|
21662
22069
|
let size = 0;
|
|
21663
22070
|
let settled = false;
|
|
22071
|
+
const collected = () => Buffer.concat(chunks);
|
|
21664
22072
|
const finish = (result2) => {
|
|
21665
22073
|
if (settled) return;
|
|
21666
22074
|
settled = true;
|
|
@@ -21670,8 +22078,21 @@ function runBoundedChild(ctx, command, args, options = {}) {
|
|
|
21670
22078
|
child.stdout?.destroy();
|
|
21671
22079
|
} catch {
|
|
21672
22080
|
}
|
|
22081
|
+
try {
|
|
22082
|
+
child.stdin?.destroy();
|
|
22083
|
+
} catch {
|
|
22084
|
+
}
|
|
21673
22085
|
child.unref();
|
|
21674
|
-
|
|
22086
|
+
const bytes = result2.keep ? collected() : null;
|
|
22087
|
+
settle({
|
|
22088
|
+
outcome: result2.outcome,
|
|
22089
|
+
code: result2.code,
|
|
22090
|
+
overflow: result2.overflow,
|
|
22091
|
+
bytes,
|
|
22092
|
+
// The TEXT value keeps its historical shape for every existing caller:
|
|
22093
|
+
// decoded as utf8 and trimmed. A raw caller reads `bytes` instead.
|
|
22094
|
+
value: bytes === null || raw ? null : bytes.toString("utf8").trim()
|
|
22095
|
+
});
|
|
21675
22096
|
};
|
|
21676
22097
|
const kill = () => {
|
|
21677
22098
|
try {
|
|
@@ -21685,31 +22106,40 @@ function runBoundedChild(ctx, command, args, options = {}) {
|
|
|
21685
22106
|
};
|
|
21686
22107
|
const timer = setTimeout(() => {
|
|
21687
22108
|
kill();
|
|
21688
|
-
finish({ outcome: "timeout",
|
|
22109
|
+
finish({ outcome: "timeout", code: null, overflow: false, keep: false });
|
|
21689
22110
|
}, budget);
|
|
21690
22111
|
timer.unref?.();
|
|
21691
22112
|
const onAbort = () => {
|
|
21692
22113
|
kill();
|
|
21693
|
-
finish({ outcome: "cancelled",
|
|
22114
|
+
finish({ outcome: "cancelled", code: null, overflow: false, keep: false });
|
|
21694
22115
|
};
|
|
21695
22116
|
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
21696
|
-
child.stdout
|
|
21697
|
-
|
|
21698
|
-
|
|
22117
|
+
const stdout = child.stdout;
|
|
22118
|
+
if (!stdout) {
|
|
22119
|
+
finish({ outcome: "failed", code: null, overflow: false, keep: false });
|
|
22120
|
+
return;
|
|
22121
|
+
}
|
|
22122
|
+
stdout.on("data", (chunk) => {
|
|
22123
|
+
size += chunk.byteLength;
|
|
21699
22124
|
if (size > PROBE_MAX_BYTES) {
|
|
21700
22125
|
kill();
|
|
21701
|
-
finish({ outcome: "failed",
|
|
22126
|
+
finish({ outcome: "failed", code: null, overflow: true, keep: false });
|
|
21702
22127
|
return;
|
|
21703
22128
|
}
|
|
21704
|
-
|
|
22129
|
+
chunks.push(chunk);
|
|
21705
22130
|
});
|
|
21706
|
-
child.
|
|
22131
|
+
if (child.stdin) {
|
|
22132
|
+
child.stdin.on("error", () => {
|
|
22133
|
+
});
|
|
22134
|
+
child.stdin.end(input);
|
|
22135
|
+
}
|
|
22136
|
+
child.on("error", () => finish({ outcome: "failed", code: null, overflow: false, keep: false }));
|
|
21707
22137
|
child.on("close", (code) => {
|
|
21708
22138
|
if (code !== 0) {
|
|
21709
|
-
finish({ outcome: "failed",
|
|
22139
|
+
finish({ outcome: "failed", code, overflow: false, keep: keepStdoutOnFailure });
|
|
21710
22140
|
return;
|
|
21711
22141
|
}
|
|
21712
|
-
finish({ outcome: "ok",
|
|
22142
|
+
finish({ outcome: "ok", code, overflow: false, keep: true });
|
|
21713
22143
|
});
|
|
21714
22144
|
});
|
|
21715
22145
|
}
|
|
@@ -22334,7 +22764,7 @@ function normalizePathValue(value) {
|
|
|
22334
22764
|
}
|
|
22335
22765
|
function conflictGroupId(fieldPath, value) {
|
|
22336
22766
|
const normalized = value.normalize("NFC").trim();
|
|
22337
|
-
return `conflict:${fieldPath}:${
|
|
22767
|
+
return `conflict:${fieldPath}:${createHash9("sha256").update(normalized, "utf8").digest("hex").slice(0, 12)}`;
|
|
22338
22768
|
}
|
|
22339
22769
|
var DIMENSIONS = {
|
|
22340
22770
|
agentId: { key: "agent-id", field: "agents.{agent_id}", kind: "agent" },
|
|
@@ -22801,7 +23231,7 @@ function collectFleetInventory(options = {}) {
|
|
|
22801
23231
|
}
|
|
22802
23232
|
|
|
22803
23233
|
// src/fleet/provenance.ts
|
|
22804
|
-
import { createHash as
|
|
23234
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
22805
23235
|
import { existsSync as existsSync28, lstatSync as lstatSync16, readFileSync as readFileSync27, realpathSync as realpathSync9, statSync as statSync8 } from "node:fs";
|
|
22806
23236
|
import { homedir as homedir15 } from "node:os";
|
|
22807
23237
|
import { dirname as dirname17, isAbsolute as isAbsolute8, join as join35, resolve as resolve21 } from "node:path";
|
|
@@ -22823,7 +23253,6 @@ var SOURCE_TEMPLATE_SUBMODULE = "hermes-agent-template-submodule";
|
|
|
22823
23253
|
var SOURCE_AGENT_REGISTRY = "hermes-agent-registry";
|
|
22824
23254
|
var SOURCE_AGENT_CHECKOUT = "hermes-agent-checkout";
|
|
22825
23255
|
var SOURCE_PROVENANCE_POLICY = "pjangler-fleet-provenance";
|
|
22826
|
-
var SOURCE_ROLE_SCAFFOLD = "hermes-agent-role-scaffold";
|
|
22827
23256
|
var SOURCE_PROFILE_TREE = "hermes-profile-tree";
|
|
22828
23257
|
var SHELL_REFERENCE = /\$\{?[A-Za-z_]/u;
|
|
22829
23258
|
function isRecord9(value) {
|
|
@@ -22970,14 +23399,14 @@ async function probeCheckout(ctx, declared, home) {
|
|
|
22970
23399
|
reason: toplevel.outcome === "timeout" ? "probe-timeout" : toplevel.outcome === "cancelled" ? "probe-cancelled" : "probe-failed"
|
|
22971
23400
|
};
|
|
22972
23401
|
}
|
|
22973
|
-
const
|
|
23402
|
+
const canonical2 = (path) => {
|
|
22974
23403
|
try {
|
|
22975
23404
|
return realpathSync9(path);
|
|
22976
23405
|
} catch {
|
|
22977
23406
|
return resolve21(path);
|
|
22978
23407
|
}
|
|
22979
23408
|
};
|
|
22980
|
-
if (
|
|
23409
|
+
if (canonical2(toplevel.value) !== canonical2(expanded)) {
|
|
22981
23410
|
return { ...base, classification, outcome: "skipped", reason: "not-a-repository-root" };
|
|
22982
23411
|
}
|
|
22983
23412
|
const [remote, head, status] = await Promise.all([
|
|
@@ -23247,23 +23676,7 @@ function addAgentRecordFacts(ctx, subject) {
|
|
|
23247
23676
|
});
|
|
23248
23677
|
}
|
|
23249
23678
|
function addUnsupportedFacts(ctx, subject, profileTemplate) {
|
|
23250
|
-
const { agentId,
|
|
23251
|
-
const scaffoldEvidence = roleDir === null ? absent(SOURCE_ROLE_SCAFFOLD, "missing") : (() => {
|
|
23252
|
-
const dir = expandHome3(roleDir, ctx.home);
|
|
23253
|
-
const view = classifyPath(dir, { directory: true });
|
|
23254
|
-
const manifest = join35(dir, "role.yaml");
|
|
23255
|
-
const state = view.classification === "ok" && existsSync28(manifest) ? "role.yaml present" : `role directory is ${view.classification}`;
|
|
23256
|
-
return present(state, SOURCE_ROLE_SCAFFOLD, { classification: view.classification });
|
|
23257
|
-
})();
|
|
23258
|
-
addFact(ctx, {
|
|
23259
|
-
id: "scaffold.template_ref",
|
|
23260
|
-
scope: "agent",
|
|
23261
|
-
agent_id: agentId,
|
|
23262
|
-
field: "scaffold",
|
|
23263
|
-
desired: absent(SOURCE_ROLE_SCAFFOLD, "unsupported"),
|
|
23264
|
-
observed: scaffoldEvidence,
|
|
23265
|
-
detail: "a deployed role scaffold records no template ref -- it renders no .copier-answers.yml and the repo-root one is CommonProject's, with no _commit -- so no comparison is possible and none is invented"
|
|
23266
|
-
});
|
|
23679
|
+
const { agentId, profileName } = subject;
|
|
23267
23680
|
const namedProfile = profileName !== null && /^[A-Za-z0-9._-]+$/u.test(profileName) && profileName !== "." && profileName !== ".." ? profileName : null;
|
|
23268
23681
|
const profilePath = namedProfile && profileTemplate ? profileTemplate.replaceAll("{profile_name}", namedProfile) : null;
|
|
23269
23682
|
const generated = profilePath ? join35(profilePath, "config.yaml") : null;
|
|
@@ -23283,7 +23696,7 @@ function profileDigest(path) {
|
|
|
23283
23696
|
if (stat.isSymbolicLink()) return present("symlink", SOURCE_PROFILE_TREE, { classification: "symlink" });
|
|
23284
23697
|
if (!stat.isFile()) return present(`not-a-file`, SOURCE_PROFILE_TREE, { classification: "not-a-directory" });
|
|
23285
23698
|
if (stat.size > PROFILE_CONFIG_MAX_BYTES) return absent(SOURCE_PROFILE_TREE, "unobserved", { classification: "unreadable" });
|
|
23286
|
-
return present(`sha256:${
|
|
23699
|
+
return present(`sha256:${createHash10("sha256").update(readFileSync27(path)).digest("hex")}`, SOURCE_PROFILE_TREE, { classification: "ok" });
|
|
23287
23700
|
} catch (error) {
|
|
23288
23701
|
const code = error.code;
|
|
23289
23702
|
return absent(SOURCE_PROFILE_TREE, "missing", { classification: code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable" });
|
|
@@ -23441,9 +23854,9 @@ async function collectFleetProvenance(options) {
|
|
|
23441
23854
|
remainingMs(runContext);
|
|
23442
23855
|
const observations = await mapBounded(targets, PROBE_CONCURRENCY, async ([, declared]) => probeCheckout(runContext, declared, home));
|
|
23443
23856
|
const byDeclared = /* @__PURE__ */ new Map();
|
|
23444
|
-
targets.forEach(([
|
|
23857
|
+
targets.forEach(([canonical2, declared], index) => {
|
|
23445
23858
|
const observation3 = observations[index];
|
|
23446
|
-
byDeclared.set(
|
|
23859
|
+
byDeclared.set(canonical2, observation3);
|
|
23447
23860
|
addProbe(ctx, {
|
|
23448
23861
|
id: `checkout:${observation3.target}`,
|
|
23449
23862
|
kind: "checkout",
|
|
@@ -23561,10 +23974,545 @@ async function collectFleetProvenance(options) {
|
|
|
23561
23974
|
}
|
|
23562
23975
|
|
|
23563
23976
|
// src/fleet/status.ts
|
|
23564
|
-
import { createHash as
|
|
23565
|
-
import { existsSync as existsSync29, readFileSync as
|
|
23977
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
23978
|
+
import { existsSync as existsSync29, readFileSync as readFileSync29 } from "node:fs";
|
|
23566
23979
|
import { homedir as homedir16 } from "node:os";
|
|
23567
|
-
import { isAbsolute as
|
|
23980
|
+
import { isAbsolute as isAbsolute10, join as join37, resolve as resolve23 } from "node:path";
|
|
23981
|
+
|
|
23982
|
+
// src/fleet/scaffold.ts
|
|
23983
|
+
import { lstatSync as lstatSync17, readFileSync as readFileSync28, readlinkSync as readlinkSync4, realpathSync as realpathSync10 } from "node:fs";
|
|
23984
|
+
import { dirname as dirname18, isAbsolute as isAbsolute9, join as join36, relative as relative13, resolve as resolve22, sep as sep7 } from "node:path";
|
|
23985
|
+
var SCAFFOLD_MAX_ASSET_BYTES = 4 * 1024 * 1024;
|
|
23986
|
+
var SCAFFOLD_MAX_ROLE_ENTRIES = 5e3;
|
|
23987
|
+
var SCAFFOLD_LINEAGE_DEPTH = 12;
|
|
23988
|
+
var SCAFFOLD_PROBE_KIND = "scaffold";
|
|
23989
|
+
function gitArgv2(path, args) {
|
|
23990
|
+
return ["git", "--no-optional-locks", "-C", path, ...args];
|
|
23991
|
+
}
|
|
23992
|
+
function canonical(path) {
|
|
23993
|
+
try {
|
|
23994
|
+
return realpathSync10(path);
|
|
23995
|
+
} catch {
|
|
23996
|
+
return resolve22(path);
|
|
23997
|
+
}
|
|
23998
|
+
}
|
|
23999
|
+
function within(root, candidate) {
|
|
24000
|
+
return candidate === root || candidate.startsWith(`${root}${sep7}`);
|
|
24001
|
+
}
|
|
24002
|
+
function parseBatch(output, keys) {
|
|
24003
|
+
const out = /* @__PURE__ */ new Map();
|
|
24004
|
+
let offset = 0;
|
|
24005
|
+
for (const key of keys) {
|
|
24006
|
+
const newline = output.indexOf(10, offset);
|
|
24007
|
+
if (newline === -1) {
|
|
24008
|
+
out.set(key, null);
|
|
24009
|
+
continue;
|
|
24010
|
+
}
|
|
24011
|
+
const header = output.subarray(offset, newline).toString("utf8");
|
|
24012
|
+
offset = newline + 1;
|
|
24013
|
+
const parts = header.split(" ");
|
|
24014
|
+
if (parts.length < 3 || parts[1] === "missing" || parts[1] === "ambiguous") {
|
|
24015
|
+
out.set(key, null);
|
|
24016
|
+
continue;
|
|
24017
|
+
}
|
|
24018
|
+
const size = Number(parts[2]);
|
|
24019
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
24020
|
+
out.set(key, null);
|
|
24021
|
+
continue;
|
|
24022
|
+
}
|
|
24023
|
+
out.set(key, output.subarray(offset, offset + size));
|
|
24024
|
+
offset += size + 1;
|
|
24025
|
+
}
|
|
24026
|
+
return out;
|
|
24027
|
+
}
|
|
24028
|
+
async function inspectSource(ctx) {
|
|
24029
|
+
const { manifest } = ctx;
|
|
24030
|
+
const submodule = join36(ctx.pjanglerRoot, manifest.template_submodule);
|
|
24031
|
+
const target = ctx.shown(submodule);
|
|
24032
|
+
const empty = (integrity, detail, gitlink2, outcome = "failed") => ({
|
|
24033
|
+
record: { gitlink: gitlink2, integrity, detail, assets: 0 },
|
|
24034
|
+
submodule,
|
|
24035
|
+
assets: [],
|
|
24036
|
+
renderSources: /* @__PURE__ */ new Map(),
|
|
24037
|
+
probe: { id: `${SCAFFOLD_PROBE_KIND}:${target}`, kind: SCAFFOLD_PROBE_KIND, target, outcome, reason: integrity }
|
|
24038
|
+
});
|
|
24039
|
+
const unobserved = (step, outcome, gitlink2) => empty("source-unobserved", `the ${step} probe ${outcome === "timeout" ? "timed out" : "failed"} before the template source could be read`, gitlink2, outcome);
|
|
24040
|
+
const committed = await probe(ctx.run, gitArgv2(ctx.pjanglerRoot, ["ls-tree", "HEAD", "--", manifest.template_submodule]));
|
|
24041
|
+
throwIfCancelled(ctx.run);
|
|
24042
|
+
if (committed.outcome === "timeout") return unobserved("ls-tree HEAD", "timeout", null);
|
|
24043
|
+
const head = /^160000\s+commit\s+([0-9a-f]{40})\t/u.exec(committed.value ?? "");
|
|
24044
|
+
if (!head) {
|
|
24045
|
+
return empty("gitlink-missing", `HEAD of the package root records no gitlink for ${manifest.template_submodule}; the template has no committed pin to compare against`, null);
|
|
24046
|
+
}
|
|
24047
|
+
const gitlink = head[1];
|
|
24048
|
+
const staged = await probe(ctx.run, gitArgv2(ctx.pjanglerRoot, ["ls-files", "--stage", "--", manifest.template_submodule]));
|
|
24049
|
+
throwIfCancelled(ctx.run);
|
|
24050
|
+
if (staged.outcome === "timeout") return unobserved("ls-files --stage", "timeout", gitlink);
|
|
24051
|
+
const index = /^160000\s+([0-9a-f]{40})\s/u.exec(staged.value ?? "");
|
|
24052
|
+
if (!index || index[1] !== gitlink) {
|
|
24053
|
+
return empty("gitlink-unstable", `the index pins ${manifest.template_submodule} at ${index ? index[1].slice(0, 12) : "nothing"} while HEAD pins ${gitlink.slice(0, 12)}; a staged, uncommitted pin is not a version anything can be compared against`, gitlink);
|
|
24054
|
+
}
|
|
24055
|
+
const toplevel = await probe(ctx.run, gitArgv2(submodule, ["rev-parse", "--show-toplevel"]));
|
|
24056
|
+
throwIfCancelled(ctx.run);
|
|
24057
|
+
if (toplevel.outcome === "timeout") return unobserved("rev-parse --show-toplevel", "timeout", gitlink);
|
|
24058
|
+
if (toplevel.outcome !== "ok" || !toplevel.value || canonical(toplevel.value) !== canonical(submodule)) {
|
|
24059
|
+
return empty("source-uninitialized", `${manifest.template_submodule} is not an initialized submodule checkout; run git submodule update to make the pinned template readable`, gitlink);
|
|
24060
|
+
}
|
|
24061
|
+
const object = await probe(ctx.run, gitArgv2(submodule, ["cat-file", "-e", `${gitlink}^{commit}`]));
|
|
24062
|
+
throwIfCancelled(ctx.run);
|
|
24063
|
+
if (object.outcome === "timeout") return unobserved("cat-file -e", "timeout", gitlink);
|
|
24064
|
+
if (object.outcome !== "ok") {
|
|
24065
|
+
return empty("source-missing-object", `the committed gitlink ${gitlink.slice(0, 12)} names a commit the submodule's object database does not hold; fetch the template before comparing against it`, gitlink);
|
|
24066
|
+
}
|
|
24067
|
+
const worktreeHead = await probe(ctx.run, gitArgv2(submodule, ["rev-parse", "HEAD"]));
|
|
24068
|
+
throwIfCancelled(ctx.run);
|
|
24069
|
+
if (worktreeHead.outcome === "timeout") return unobserved("rev-parse HEAD", "timeout", gitlink);
|
|
24070
|
+
if (worktreeHead.outcome !== "ok" || worktreeHead.value !== gitlink) {
|
|
24071
|
+
return empty("source-mismatched", `the template worktree is at ${worktreeHead.value ? worktreeHead.value.slice(0, 12) : "no commit"} while the parent commits ${gitlink.slice(0, 12)}; the checkout is not canonical`, gitlink);
|
|
24072
|
+
}
|
|
24073
|
+
const dirty = await probe(ctx.run, gitArgv2(submodule, ["status", "--porcelain", "--untracked-files=no"]));
|
|
24074
|
+
throwIfCancelled(ctx.run);
|
|
24075
|
+
if (dirty.outcome === "timeout") return unobserved("status --porcelain", "timeout", gitlink);
|
|
24076
|
+
if (dirty.outcome !== "ok") return unobserved("status --porcelain", "failed", gitlink);
|
|
24077
|
+
if ((dirty.value ?? "") !== "") {
|
|
24078
|
+
return empty("source-dirty", `the template worktree carries modified tracked files; desired bytes are read from the committed gitlink ${gitlink.slice(0, 12)} and never from a dirty checkout, and the checkout must be clean before it is canonical`, gitlink);
|
|
24079
|
+
}
|
|
24080
|
+
const tree = await probeRaw(ctx.run, gitArgv2(submodule, ["ls-tree", "-r", "-z", gitlink, "--", manifest.template_subdirectory]));
|
|
24081
|
+
throwIfCancelled(ctx.run);
|
|
24082
|
+
if (tree.outcome === "timeout") return unobserved("ls-tree -r", "timeout", gitlink);
|
|
24083
|
+
if (tree.outcome !== "ok" || tree.value === null) return unobserved("ls-tree -r", "failed", gitlink);
|
|
24084
|
+
const prefix = `${manifest.template_subdirectory.replace(/\/+$/u, "")}/`;
|
|
24085
|
+
const assets = [];
|
|
24086
|
+
const entries = tree.value.toString("utf8").split("\0").filter((entry) => entry !== "");
|
|
24087
|
+
for (const entry of entries) {
|
|
24088
|
+
const match = /^(\d{6}) (\w+) ([0-9a-f]{40})\t(.+)$/su.exec(entry);
|
|
24089
|
+
if (!match) continue;
|
|
24090
|
+
const [, mode, type, sha, templatePath] = match;
|
|
24091
|
+
if (!templatePath.startsWith(prefix)) continue;
|
|
24092
|
+
const raw = templatePath.slice(prefix.length);
|
|
24093
|
+
if (type !== "blob") {
|
|
24094
|
+
return empty("source-contaminated", `the tree at ${gitlink.slice(0, 12)} carries a ${type} at ${raw}; a template renders blobs only`, gitlink);
|
|
24095
|
+
}
|
|
24096
|
+
const excluded = matchesExcluded(raw, manifest.excluded_patterns);
|
|
24097
|
+
if (excluded !== null) {
|
|
24098
|
+
return empty("source-contaminated", `the tree at ${gitlink.slice(0, 12)} carries ${raw}, which matches the excluded pattern ${excluded}; a contaminated template cannot be the desired state`, gitlink);
|
|
24099
|
+
}
|
|
24100
|
+
const rendered = raw.endsWith(manifest.render_suffix);
|
|
24101
|
+
const rolePath = rendered ? raw.slice(0, -manifest.render_suffix.length) : raw;
|
|
24102
|
+
if (rolePath === "" || rolePath.endsWith("/")) {
|
|
24103
|
+
return empty("source-contaminated", `the tree at ${gitlink.slice(0, 12)} renders an unnamed asset from ${raw}`, gitlink);
|
|
24104
|
+
}
|
|
24105
|
+
const group = groupFor(rolePath, manifest.groups);
|
|
24106
|
+
if (group === null) {
|
|
24107
|
+
return empty(`manifest-uncovered:${rolePath}`, `the tree at ${gitlink.slice(0, 12)} renders ${rolePath}, which no scaffold_manifest.groups entry owns; declare a group for it before it can be compared`, gitlink);
|
|
24108
|
+
}
|
|
24109
|
+
assets.push({
|
|
24110
|
+
templatePath,
|
|
24111
|
+
rolePath,
|
|
24112
|
+
sha,
|
|
24113
|
+
mode,
|
|
24114
|
+
type: mode === "120000" ? "symlink" : "file",
|
|
24115
|
+
executable: mode === "100755",
|
|
24116
|
+
rendered,
|
|
24117
|
+
group,
|
|
24118
|
+
presenceOnly: manifest.presence_only.some((item) => item.path === rolePath)
|
|
24119
|
+
});
|
|
24120
|
+
}
|
|
24121
|
+
if (assets.length === 0) {
|
|
24122
|
+
return empty("source-empty", `the tree at ${gitlink.slice(0, 12)} renders nothing under ${manifest.template_subdirectory}; an empty template is not a desired state`, gitlink);
|
|
24123
|
+
}
|
|
24124
|
+
assets.sort((a, b) => a.rolePath < b.rolePath ? -1 : a.rolePath > b.rolePath ? 1 : 0);
|
|
24125
|
+
const renderSources = /* @__PURE__ */ new Map();
|
|
24126
|
+
const wanted = assets.filter((asset) => asset.rendered && !asset.presenceOnly && asset.type === "file");
|
|
24127
|
+
if (wanted.length > 0) {
|
|
24128
|
+
const ids = [...new Set(wanted.map((asset) => asset.sha))];
|
|
24129
|
+
const batch = await probeRaw(ctx.run, gitArgv2(submodule, ["cat-file", "--batch"]), void 0, `${ids.join("\n")}
|
|
24130
|
+
`);
|
|
24131
|
+
throwIfCancelled(ctx.run);
|
|
24132
|
+
if (batch.outcome === "timeout") return unobserved("cat-file --batch", "timeout", gitlink);
|
|
24133
|
+
if (batch.outcome !== "ok" || batch.value === null) return unobserved("cat-file --batch", "failed", gitlink);
|
|
24134
|
+
const blobs = parseBatch(batch.value, ids);
|
|
24135
|
+
for (const asset of wanted) {
|
|
24136
|
+
const bytes = blobs.get(asset.sha) ?? null;
|
|
24137
|
+
if (bytes === null) {
|
|
24138
|
+
return empty("source-missing-object", `blob ${asset.sha.slice(0, 12)} for ${asset.rolePath} is absent from the submodule's object database`, gitlink);
|
|
24139
|
+
}
|
|
24140
|
+
const text3 = bytes.toString("utf8");
|
|
24141
|
+
renderSources.set(asset.templatePath, Buffer.from(text3, "utf8").equals(bytes) ? { text: text3 } : { unsupported: "render-unsupported: template source is not UTF-8 text" });
|
|
24142
|
+
}
|
|
24143
|
+
}
|
|
24144
|
+
return {
|
|
24145
|
+
record: { gitlink, integrity: "ok", detail: `${assets.length} asset(s) rendered by the template at the committed gitlink ${gitlink.slice(0, 12)}`, assets: assets.length },
|
|
24146
|
+
submodule,
|
|
24147
|
+
assets,
|
|
24148
|
+
renderSources,
|
|
24149
|
+
probe: { id: `${SCAFFOLD_PROBE_KIND}:${target}`, kind: SCAFFOLD_PROBE_KIND, target, outcome: "ok", reason: null }
|
|
24150
|
+
};
|
|
24151
|
+
}
|
|
24152
|
+
function desiredFor(source, input) {
|
|
24153
|
+
return source.assets.map((asset) => {
|
|
24154
|
+
const base = {
|
|
24155
|
+
path: asset.rolePath,
|
|
24156
|
+
type: asset.type,
|
|
24157
|
+
executable: asset.executable,
|
|
24158
|
+
blobId: asset.sha,
|
|
24159
|
+
presenceOnly: asset.presenceOnly,
|
|
24160
|
+
incomplete: null
|
|
24161
|
+
};
|
|
24162
|
+
if (asset.presenceOnly) return { ...base, blobId: null };
|
|
24163
|
+
if (!asset.rendered || asset.type !== "file") return base;
|
|
24164
|
+
const rendered = source.renderSources.get(asset.templatePath);
|
|
24165
|
+
if (!rendered) return { ...base, blobId: null, incomplete: { reason: "render-unsupported", detail: "render-unsupported: source not read" } };
|
|
24166
|
+
if ("unsupported" in rendered) return { ...base, blobId: null, incomplete: { reason: "render-unsupported", detail: rendered.unsupported } };
|
|
24167
|
+
const result2 = renderTemplate(rendered.text, input.inputs);
|
|
24168
|
+
if (!result2.ok) return { ...base, blobId: null, incomplete: { reason: result2.reason, detail: result2.detail } };
|
|
24169
|
+
return { ...base, blobId: blobId(Buffer.from(result2.text, "utf8")) };
|
|
24170
|
+
});
|
|
24171
|
+
}
|
|
24172
|
+
function absent2() {
|
|
24173
|
+
return { present: false, type: null, executable: false, blobId: null, unsafeSymlink: false, unreadable: null, wip: false };
|
|
24174
|
+
}
|
|
24175
|
+
async function scanAgent(ctx, source, input) {
|
|
24176
|
+
const scan = {
|
|
24177
|
+
input,
|
|
24178
|
+
roleDir: null,
|
|
24179
|
+
roleDirSource: input.roleDir !== null ? "registry" : "default",
|
|
24180
|
+
error: null,
|
|
24181
|
+
top: null,
|
|
24182
|
+
observed: /* @__PURE__ */ new Map(),
|
|
24183
|
+
tracked: [],
|
|
24184
|
+
wipPaths: /* @__PURE__ */ new Set(),
|
|
24185
|
+
wipPreserved: 0,
|
|
24186
|
+
ignoredEntries: 0,
|
|
24187
|
+
probe: { id: "", kind: SCAFFOLD_PROBE_KIND, target: "", outcome: "skipped", reason: null }
|
|
24188
|
+
};
|
|
24189
|
+
const fail = (error, outcome = "skipped") => {
|
|
24190
|
+
scan.error = error;
|
|
24191
|
+
scan.probe = { ...scan.probe, outcome, reason: error };
|
|
24192
|
+
return scan;
|
|
24193
|
+
};
|
|
24194
|
+
const setTarget = (path) => {
|
|
24195
|
+
const shown = ctx.shown(path);
|
|
24196
|
+
scan.probe = { ...scan.probe, id: `${SCAFFOLD_PROBE_KIND}:${shown}`, target: shown };
|
|
24197
|
+
};
|
|
24198
|
+
if (input.projectPath === null) {
|
|
24199
|
+
setTarget(input.agentId);
|
|
24200
|
+
return fail("repository-unreadable:no-project-path");
|
|
24201
|
+
}
|
|
24202
|
+
setTarget(input.projectPath);
|
|
24203
|
+
const toplevel = await probe(ctx.run, gitArgv2(input.projectPath, ["rev-parse", "--show-toplevel"]));
|
|
24204
|
+
throwIfCancelled(ctx.run);
|
|
24205
|
+
if (toplevel.outcome === "timeout") return fail("probe-timeout", "timeout");
|
|
24206
|
+
if (toplevel.outcome !== "ok" || !toplevel.value) return fail("repository-unreadable:not-a-repository", "failed");
|
|
24207
|
+
const top = canonical(toplevel.value);
|
|
24208
|
+
if (top !== canonical(input.projectPath)) return fail("repository-unreadable:not-a-repository-root");
|
|
24209
|
+
scan.top = top;
|
|
24210
|
+
let roleDir;
|
|
24211
|
+
if (input.roleDir !== null) {
|
|
24212
|
+
roleDir = isAbsolute9(input.roleDir) ? resolve22(input.roleDir) : resolve22(input.projectPath, input.roleDir);
|
|
24213
|
+
} else if (input.role !== null && input.role !== "" && input.role !== "." && input.role !== ".." && !input.role.includes("/")) {
|
|
24214
|
+
roleDir = join36(input.projectPath, "agents", "hermes", input.role);
|
|
24215
|
+
} else {
|
|
24216
|
+
return fail("role-unresolved");
|
|
24217
|
+
}
|
|
24218
|
+
scan.roleDir = roleDir;
|
|
24219
|
+
setTarget(roleDir);
|
|
24220
|
+
let roleRel;
|
|
24221
|
+
let roleReal;
|
|
24222
|
+
try {
|
|
24223
|
+
const stat = lstatSync17(roleDir);
|
|
24224
|
+
if (stat.isSymbolicLink()) return fail("role-dir-symlink");
|
|
24225
|
+
if (!stat.isDirectory()) return fail("role-dir-not-a-directory");
|
|
24226
|
+
roleReal = canonical(roleDir);
|
|
24227
|
+
if (roleReal === top) return fail("role-dir-is-repository-root");
|
|
24228
|
+
if (!within(top, roleReal)) return fail("role-dir-outside-project");
|
|
24229
|
+
roleRel = relative13(top, roleReal);
|
|
24230
|
+
} catch {
|
|
24231
|
+
const lexical = resolve22(roleDir);
|
|
24232
|
+
const project = resolve22(input.projectPath);
|
|
24233
|
+
if (lexical === project || lexical === top) return fail("role-dir-is-repository-root");
|
|
24234
|
+
if (within(project, lexical)) roleRel = relative13(project, lexical);
|
|
24235
|
+
else if (within(top, lexical)) roleRel = relative13(top, lexical);
|
|
24236
|
+
else return fail("role-dir-outside-project");
|
|
24237
|
+
roleReal = lexical;
|
|
24238
|
+
}
|
|
24239
|
+
const rolePosix = roleRel.split(sep7).join("/");
|
|
24240
|
+
const pathspec = `:(literal)${rolePosix}`;
|
|
24241
|
+
const tracked = await probeRaw(ctx.run, gitArgv2(top, ["ls-files", "-z", "--", pathspec]));
|
|
24242
|
+
throwIfCancelled(ctx.run);
|
|
24243
|
+
if (tracked.outcome === "timeout") return fail("probe-timeout", "timeout");
|
|
24244
|
+
if (tracked.outcome !== "ok" || tracked.value === null) return fail("repository-unreadable:probe-failed", "failed");
|
|
24245
|
+
const status = await probeRaw(ctx.run, gitArgv2(top, ["status", "--porcelain=v1", "-z", "--ignored=matching", "--untracked-files=all", "--", pathspec]));
|
|
24246
|
+
throwIfCancelled(ctx.run);
|
|
24247
|
+
if (status.outcome === "timeout") return fail("probe-timeout", "timeout");
|
|
24248
|
+
if (status.outcome !== "ok" || status.value === null) return fail("repository-unreadable:probe-failed", "failed");
|
|
24249
|
+
const toRole = (path) => path.startsWith(`${rolePosix}/`) ? path.slice(rolePosix.length + 1) : null;
|
|
24250
|
+
const trackedEntries = tracked.value.toString("utf8").split("\0").filter((entry) => entry !== "");
|
|
24251
|
+
const statusEntries = status.value.toString("utf8").split("\0");
|
|
24252
|
+
if (trackedEntries.length + statusEntries.length > SCAFFOLD_MAX_ROLE_ENTRIES) return fail("role-entries-exceeded");
|
|
24253
|
+
for (const entry of trackedEntries) {
|
|
24254
|
+
const rolePath = toRole(entry);
|
|
24255
|
+
if (rolePath !== null) scan.tracked.push(rolePath);
|
|
24256
|
+
}
|
|
24257
|
+
scan.tracked.sort();
|
|
24258
|
+
for (let index = 0; index < statusEntries.length; index += 1) {
|
|
24259
|
+
const entry = statusEntries[index];
|
|
24260
|
+
if (entry.length < 4) continue;
|
|
24261
|
+
const xy = entry.slice(0, 2);
|
|
24262
|
+
const path = entry.slice(3);
|
|
24263
|
+
if (xy[0] === "R" || xy[0] === "C") index += 1;
|
|
24264
|
+
const rolePath = toRole(path.replace(/\/$/u, ""));
|
|
24265
|
+
if (rolePath === null && !path.startsWith(`${rolePosix}/`)) continue;
|
|
24266
|
+
if (xy === "!!") {
|
|
24267
|
+
scan.ignoredEntries += 1;
|
|
24268
|
+
continue;
|
|
24269
|
+
}
|
|
24270
|
+
scan.wipPreserved += 1;
|
|
24271
|
+
if (xy !== "??" && rolePath !== null) scan.wipPaths.add(rolePath);
|
|
24272
|
+
}
|
|
24273
|
+
for (const asset of source.assets) {
|
|
24274
|
+
const full = join36(roleReal, ...asset.rolePath.split("/"));
|
|
24275
|
+
const seen = absent2();
|
|
24276
|
+
seen.wip = scan.wipPaths.has(asset.rolePath);
|
|
24277
|
+
try {
|
|
24278
|
+
const stat = lstatSync17(full);
|
|
24279
|
+
seen.present = true;
|
|
24280
|
+
if (stat.isSymbolicLink()) {
|
|
24281
|
+
seen.type = "symlink";
|
|
24282
|
+
const link = readlinkSync4(full);
|
|
24283
|
+
let resolvedTarget = null;
|
|
24284
|
+
try {
|
|
24285
|
+
resolvedTarget = realpathSync10(resolve22(dirname18(full), link));
|
|
24286
|
+
} catch {
|
|
24287
|
+
resolvedTarget = null;
|
|
24288
|
+
}
|
|
24289
|
+
seen.unsafeSymlink = isAbsolute9(link) || resolvedTarget !== null && !within(top, resolvedTarget);
|
|
24290
|
+
seen.blobId = blobId(Buffer.from(link, "utf8"));
|
|
24291
|
+
} else if (stat.isDirectory()) {
|
|
24292
|
+
seen.type = "directory";
|
|
24293
|
+
} else if (stat.isFile()) {
|
|
24294
|
+
seen.type = "file";
|
|
24295
|
+
seen.executable = (stat.mode & 73) !== 0;
|
|
24296
|
+
if (stat.size > SCAFFOLD_MAX_ASSET_BYTES) seen.unreadable = "too-large";
|
|
24297
|
+
else {
|
|
24298
|
+
try {
|
|
24299
|
+
seen.blobId = blobId(readFileSync28(full));
|
|
24300
|
+
} catch {
|
|
24301
|
+
seen.unreadable = "unreadable";
|
|
24302
|
+
}
|
|
24303
|
+
}
|
|
24304
|
+
} else {
|
|
24305
|
+
seen.type = "other";
|
|
24306
|
+
}
|
|
24307
|
+
} catch {
|
|
24308
|
+
}
|
|
24309
|
+
scan.observed.set(asset.rolePath, seen);
|
|
24310
|
+
}
|
|
24311
|
+
scan.probe = { ...scan.probe, outcome: "ok", reason: null };
|
|
24312
|
+
return scan;
|
|
24313
|
+
}
|
|
24314
|
+
async function resolveLineage(ctx, source, scans, desiredByAgent) {
|
|
24315
|
+
const lineage = { verbatim: /* @__PURE__ */ new Set(), rendered: /* @__PURE__ */ new Map(), unobserved: false };
|
|
24316
|
+
const verbatimIds = /* @__PURE__ */ new Set();
|
|
24317
|
+
const renderedNeeded = /* @__PURE__ */ new Map();
|
|
24318
|
+
const assetByPath = new Map(source.assets.map((asset) => [asset.rolePath, asset]));
|
|
24319
|
+
scans.forEach((scan, index) => {
|
|
24320
|
+
if (scan.error !== null) return;
|
|
24321
|
+
for (const desired of desiredByAgent[index] ?? []) {
|
|
24322
|
+
if (desired.presenceOnly || desired.blobId === null || desired.type !== "file") continue;
|
|
24323
|
+
const seen = scan.observed.get(desired.path);
|
|
24324
|
+
if (!seen || !seen.present || seen.type !== "file" || seen.blobId === null || seen.blobId === desired.blobId) continue;
|
|
24325
|
+
const asset = assetByPath.get(desired.path);
|
|
24326
|
+
if (!asset) continue;
|
|
24327
|
+
if (asset.rendered) {
|
|
24328
|
+
const set = renderedNeeded.get(asset.templatePath) ?? /* @__PURE__ */ new Set();
|
|
24329
|
+
set.add(index);
|
|
24330
|
+
renderedNeeded.set(asset.templatePath, set);
|
|
24331
|
+
} else {
|
|
24332
|
+
verbatimIds.add(seen.blobId);
|
|
24333
|
+
}
|
|
24334
|
+
}
|
|
24335
|
+
});
|
|
24336
|
+
if (verbatimIds.size > 0) {
|
|
24337
|
+
const ids = [...verbatimIds].sort();
|
|
24338
|
+
const check = await probeRaw(ctx.run, gitArgv2(source.submodule, ["cat-file", "--batch-check"]), void 0, `${ids.join("\n")}
|
|
24339
|
+
`);
|
|
24340
|
+
throwIfCancelled(ctx.run);
|
|
24341
|
+
if (check.outcome !== "ok" || check.value === null) lineage.unobserved = true;
|
|
24342
|
+
else {
|
|
24343
|
+
for (const line of check.value.toString("utf8").split("\n")) {
|
|
24344
|
+
const match = /^([0-9a-f]{40}) (\w+)/u.exec(line);
|
|
24345
|
+
if (match && match[2] !== "missing") lineage.verbatim.add(match[1]);
|
|
24346
|
+
}
|
|
24347
|
+
}
|
|
24348
|
+
}
|
|
24349
|
+
if (renderedNeeded.size > 0) {
|
|
24350
|
+
const gitlink = source.record.gitlink;
|
|
24351
|
+
const keys = [];
|
|
24352
|
+
const versions = /* @__PURE__ */ new Map();
|
|
24353
|
+
for (const templatePath of [...renderedNeeded.keys()].sort()) {
|
|
24354
|
+
const log3 = await probe(ctx.run, gitArgv2(source.submodule, ["log", "--format=%H", "-n", String(SCAFFOLD_LINEAGE_DEPTH), gitlink, "--", templatePath]));
|
|
24355
|
+
throwIfCancelled(ctx.run);
|
|
24356
|
+
if (log3.outcome !== "ok") {
|
|
24357
|
+
lineage.unobserved = true;
|
|
24358
|
+
continue;
|
|
24359
|
+
}
|
|
24360
|
+
const commits = (log3.value ?? "").split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/u.test(line));
|
|
24361
|
+
const perPath = commits.map((commit) => `${commit}:${templatePath}`);
|
|
24362
|
+
versions.set(templatePath, perPath);
|
|
24363
|
+
keys.push(...perPath);
|
|
24364
|
+
}
|
|
24365
|
+
if (keys.length > 0) {
|
|
24366
|
+
const batch = await probeRaw(ctx.run, gitArgv2(source.submodule, ["cat-file", "--batch"]), void 0, `${keys.join("\n")}
|
|
24367
|
+
`);
|
|
24368
|
+
throwIfCancelled(ctx.run);
|
|
24369
|
+
if (batch.outcome !== "ok" || batch.value === null) lineage.unobserved = true;
|
|
24370
|
+
else {
|
|
24371
|
+
const blobs = parseBatch(batch.value, keys);
|
|
24372
|
+
for (const [templatePath, agentIndexes] of renderedNeeded) {
|
|
24373
|
+
const asset = source.assets.find((item) => item.templatePath === templatePath);
|
|
24374
|
+
if (!asset) continue;
|
|
24375
|
+
const sources = (versions.get(templatePath) ?? []).map((key) => blobs.get(key) ?? null).filter((bytes) => bytes !== null).map((bytes) => bytes.toString("utf8"));
|
|
24376
|
+
for (const index of agentIndexes) {
|
|
24377
|
+
const scan = scans[index];
|
|
24378
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
24379
|
+
for (const text3 of sources) {
|
|
24380
|
+
const result2 = renderTemplate(text3, scan.input.inputs);
|
|
24381
|
+
if (result2.ok) rendered.add(blobId(Buffer.from(result2.text, "utf8")));
|
|
24382
|
+
}
|
|
24383
|
+
const perAgent = lineage.rendered.get(index) ?? /* @__PURE__ */ new Map();
|
|
24384
|
+
perAgent.set(asset.rolePath, rendered);
|
|
24385
|
+
lineage.rendered.set(index, perAgent);
|
|
24386
|
+
}
|
|
24387
|
+
}
|
|
24388
|
+
}
|
|
24389
|
+
}
|
|
24390
|
+
}
|
|
24391
|
+
return lineage;
|
|
24392
|
+
}
|
|
24393
|
+
function erroredGroups(manifest, assets, detail) {
|
|
24394
|
+
return Object.keys(manifest.groups).sort().map((group) => {
|
|
24395
|
+
const rolePath = manifest.groups[group];
|
|
24396
|
+
const owned = assets.filter((asset) => asset.group === group).length;
|
|
24397
|
+
return {
|
|
24398
|
+
group,
|
|
24399
|
+
role_path: rolePath,
|
|
24400
|
+
state: "error",
|
|
24401
|
+
owned,
|
|
24402
|
+
matching: 0,
|
|
24403
|
+
drifted: 0,
|
|
24404
|
+
incomplete: owned,
|
|
24405
|
+
unexpected: 0,
|
|
24406
|
+
items: [{ path: rolePath, kind: "incomplete", desired: null, observed: null, detail, wip: false }]
|
|
24407
|
+
};
|
|
24408
|
+
});
|
|
24409
|
+
}
|
|
24410
|
+
function emptyAgentResult(input, groups, error, roleDir, roleDirSource) {
|
|
24411
|
+
const owned = groups.reduce((total, group) => total + group.owned, 0);
|
|
24412
|
+
return {
|
|
24413
|
+
agentId: input.agentId,
|
|
24414
|
+
roleDir,
|
|
24415
|
+
roleDirSource,
|
|
24416
|
+
error,
|
|
24417
|
+
groups,
|
|
24418
|
+
assets: { owned, compared: 0, matching: 0, drifted: 0, incomplete: owned, unexpected_owned: 0 },
|
|
24419
|
+
wipOverlap: [],
|
|
24420
|
+
wipPreserved: 0,
|
|
24421
|
+
foreignTracked: 0,
|
|
24422
|
+
ignoredEntries: 0
|
|
24423
|
+
};
|
|
24424
|
+
}
|
|
24425
|
+
async function collectScaffoldParity(ctx) {
|
|
24426
|
+
throwIfCancelled(ctx.run);
|
|
24427
|
+
const source = await inspectSource(ctx);
|
|
24428
|
+
const probes = [source.probe];
|
|
24429
|
+
const manifest = ctx.manifest;
|
|
24430
|
+
if (source.record.integrity !== "ok") {
|
|
24431
|
+
const detail = `source:${source.record.integrity}`;
|
|
24432
|
+
const agents2 = ctx.agents.map((input) => {
|
|
24433
|
+
const roleDir = input.roleDir !== null ? isAbsolute9(input.roleDir) ? resolve22(input.roleDir) : input.projectPath === null ? null : resolve22(input.projectPath, input.roleDir) : input.projectPath !== null && input.role ? join36(input.projectPath, "agents", "hermes", input.role) : null;
|
|
24434
|
+
const target = ctx.shown(roleDir ?? input.projectPath ?? input.agentId);
|
|
24435
|
+
probes.push({ id: `${SCAFFOLD_PROBE_KIND}:${target}`, kind: SCAFFOLD_PROBE_KIND, target, outcome: "skipped", reason: "source-unreadable" });
|
|
24436
|
+
return emptyAgentResult(input, erroredGroups(manifest, [], detail), detail, roleDir, input.roleDir !== null ? "registry" : "default");
|
|
24437
|
+
});
|
|
24438
|
+
return { source: source.record, agents: agents2, probes };
|
|
24439
|
+
}
|
|
24440
|
+
const scans = await mapBounded(ctx.agents, FLEET_STATUS_SCAFFOLD_CONCURRENCY, (input) => scanAgent(ctx, source, input));
|
|
24441
|
+
const desiredByAgent = ctx.agents.map((input) => desiredFor(source, input));
|
|
24442
|
+
const lineage = await resolveLineage(ctx, source, scans, desiredByAgent);
|
|
24443
|
+
const ownedPaths = new Set(source.assets.map((asset) => asset.rolePath));
|
|
24444
|
+
const agents = scans.map((scan, index) => {
|
|
24445
|
+
probes.push(scan.probe);
|
|
24446
|
+
if (scan.error !== null) {
|
|
24447
|
+
return emptyAgentResult(scan.input, erroredGroups(manifest, source.assets, scan.error), scan.error, scan.roleDir, scan.roleDirSource);
|
|
24448
|
+
}
|
|
24449
|
+
const desired = desiredByAgent[index] ?? [];
|
|
24450
|
+
const renderedLineage = lineage.rendered.get(index) ?? /* @__PURE__ */ new Map();
|
|
24451
|
+
let findings = compareAssets(desired, (asset) => scan.observed.get(asset.path) ?? absent2(), {
|
|
24452
|
+
inLineage: (id, path) => lineage.verbatim.has(id) || (renderedLineage.get(path)?.has(id) ?? false)
|
|
24453
|
+
});
|
|
24454
|
+
if (lineage.unobserved) {
|
|
24455
|
+
findings = findings.map((finding2) => finding2.kind === "stale-content" || finding2.kind === "locally-modified" ? { ...finding2, kind: "incomplete", detail: "lineage-unobserved" } : finding2);
|
|
24456
|
+
}
|
|
24457
|
+
const extras = classifyExtras({
|
|
24458
|
+
trackedPaths: scan.tracked,
|
|
24459
|
+
ownedPaths,
|
|
24460
|
+
groups: manifest.groups,
|
|
24461
|
+
runtimeDir: manifest.runtime_dir,
|
|
24462
|
+
excludedPatterns: manifest.excluded_patterns
|
|
24463
|
+
});
|
|
24464
|
+
const groups = Object.keys(manifest.groups).sort().map((group) => {
|
|
24465
|
+
const rolePath = manifest.groups[group];
|
|
24466
|
+
const ownedHere = source.assets.filter((asset) => asset.group === group);
|
|
24467
|
+
const ownedSet = new Set(ownedHere.map((asset) => asset.rolePath));
|
|
24468
|
+
const items = findings.filter((finding2) => ownedSet.has(finding2.path));
|
|
24469
|
+
for (const extra of extras.unexpected) {
|
|
24470
|
+
if (extra.group !== group) continue;
|
|
24471
|
+
items.push({ path: extra.path, kind: "unexpected-owned", desired: null, observed: "tracked", detail: extra.detail, wip: scan.wipPaths.has(extra.path) });
|
|
24472
|
+
}
|
|
24473
|
+
items.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0);
|
|
24474
|
+
const incompletePaths = new Set(items.filter((item) => item.kind === "incomplete").map((item) => item.path));
|
|
24475
|
+
const driftedPaths = new Set(items.filter((item) => item.kind !== "incomplete" && item.kind !== "unexpected-owned" && !incompletePaths.has(item.path)).map((item) => item.path));
|
|
24476
|
+
const unexpected = items.filter((item) => item.kind === "unexpected-owned").length;
|
|
24477
|
+
const owned = ownedHere.length;
|
|
24478
|
+
return {
|
|
24479
|
+
group,
|
|
24480
|
+
role_path: rolePath,
|
|
24481
|
+
state: incompletePaths.size > 0 ? "error" : driftedPaths.size > 0 || unexpected > 0 ? "fail" : "pass",
|
|
24482
|
+
owned,
|
|
24483
|
+
matching: owned - incompletePaths.size - driftedPaths.size,
|
|
24484
|
+
drifted: driftedPaths.size,
|
|
24485
|
+
incomplete: incompletePaths.size,
|
|
24486
|
+
unexpected,
|
|
24487
|
+
items
|
|
24488
|
+
};
|
|
24489
|
+
});
|
|
24490
|
+
const totals = groups.reduce((sum, group) => ({
|
|
24491
|
+
owned: sum.owned + group.owned,
|
|
24492
|
+
compared: sum.compared + group.owned - group.incomplete,
|
|
24493
|
+
matching: sum.matching + group.matching,
|
|
24494
|
+
drifted: sum.drifted + group.drifted,
|
|
24495
|
+
incomplete: sum.incomplete + group.incomplete,
|
|
24496
|
+
unexpected_owned: sum.unexpected_owned + group.unexpected
|
|
24497
|
+
}), { owned: 0, compared: 0, matching: 0, drifted: 0, incomplete: 0, unexpected_owned: 0 });
|
|
24498
|
+
const wipOverlap = [...new Set(groups.flatMap((group) => group.items).filter((item) => item.wip).map((item) => item.path))].sort();
|
|
24499
|
+
return {
|
|
24500
|
+
agentId: scan.input.agentId,
|
|
24501
|
+
roleDir: scan.roleDir,
|
|
24502
|
+
roleDirSource: scan.roleDirSource,
|
|
24503
|
+
error: null,
|
|
24504
|
+
groups,
|
|
24505
|
+
assets: totals,
|
|
24506
|
+
wipOverlap,
|
|
24507
|
+
wipPreserved: scan.wipPreserved,
|
|
24508
|
+
foreignTracked: extras.foreignTracked,
|
|
24509
|
+
ignoredEntries: scan.ignoredEntries
|
|
24510
|
+
};
|
|
24511
|
+
});
|
|
24512
|
+
return { source: source.record, agents, probes };
|
|
24513
|
+
}
|
|
24514
|
+
|
|
24515
|
+
// src/fleet/status.ts
|
|
23568
24516
|
init_project();
|
|
23569
24517
|
var RULE_DOMAIN = Object.freeze({
|
|
23570
24518
|
// Tracked assets the CommonProject / Hermes template owns in a repository.
|
|
@@ -23657,6 +24605,7 @@ var DOMAIN_AUTHORITY_BLOCK = Object.freeze({
|
|
|
23657
24605
|
var CAPABILITY_SYSTEMD = "unit_topology";
|
|
23658
24606
|
var CAPABILITY_LIVE_PROCESS = "process_attribution";
|
|
23659
24607
|
var CAPABILITY_BLOODBANK_LIVENESS = "routing_liveness";
|
|
24608
|
+
var CAPABILITY_SCAFFOLD_MANIFEST = "scaffold.manifest";
|
|
23660
24609
|
var FINDING_SEVERITY = Object.freeze({
|
|
23661
24610
|
error: "critical",
|
|
23662
24611
|
warn: "medium",
|
|
@@ -23681,6 +24630,16 @@ var SOURCE_REGISTRY = "fleet-inventory";
|
|
|
23681
24630
|
var SOURCE_PROVENANCE = "fleet-provenance";
|
|
23682
24631
|
var SOURCE_AUDIT = "recipe-audit";
|
|
23683
24632
|
var SOURCE_DECLARED_GAP = "declared-gap";
|
|
24633
|
+
var SOURCE_SCAFFOLD = "fleet-scaffold";
|
|
24634
|
+
var SCAFFOLD_SOURCE_RULE_ID = "scaffold.source";
|
|
24635
|
+
var RULE_COVERED_CONTENT_GROUPS = /* @__PURE__ */ new Set([
|
|
24636
|
+
"scaffold.hermes",
|
|
24637
|
+
"scaffold.gitignore",
|
|
24638
|
+
"scaffold.scripts",
|
|
24639
|
+
"scaffold.sentinel.prompt.md"
|
|
24640
|
+
]);
|
|
24641
|
+
var RULE_COVERED_PRESENCE_PATHS = /* @__PURE__ */ new Set(["role.yaml", "SOUL.md", ".runtime-scaffold/README.md"]);
|
|
24642
|
+
var RULE_COVERED_KINDS = /* @__PURE__ */ new Set(["missing", "stale-content", "locally-modified"]);
|
|
23684
24643
|
var FLEET_STATUS_AUDIT_TIMEOUT_MS = 2e4;
|
|
23685
24644
|
var CLI_ENTRY_ENV = "PJ_FLEET_CLI_ENTRY";
|
|
23686
24645
|
var AUDIT_CHILD_ENV_KEYS = [
|
|
@@ -23719,7 +24678,7 @@ function nonEmptyString3(value) {
|
|
|
23719
24678
|
}
|
|
23720
24679
|
function expandHome4(path, home) {
|
|
23721
24680
|
if (path === "~") return home;
|
|
23722
|
-
if (path.startsWith("~/")) return
|
|
24681
|
+
if (path.startsWith("~/")) return join37(home, path.slice(2));
|
|
23723
24682
|
return path;
|
|
23724
24683
|
}
|
|
23725
24684
|
function shownPath3(path) {
|
|
@@ -23727,7 +24686,7 @@ function shownPath3(path) {
|
|
|
23727
24686
|
}
|
|
23728
24687
|
function statusFindingId(scope, agentId, domain, ruleId, field3, source) {
|
|
23729
24688
|
const key = [scope, agentId ?? "", domain, ruleId ?? "", field3, source].join("\0");
|
|
23730
|
-
return
|
|
24689
|
+
return createHash11("sha256").update(key, "utf8").digest("hex").slice(0, 12);
|
|
23731
24690
|
}
|
|
23732
24691
|
function retrievalFor(agentId, domain, live) {
|
|
23733
24692
|
const parts = ["pjangler fleet status"];
|
|
@@ -23896,12 +24855,14 @@ function observation2(ctx, input) {
|
|
|
23896
24855
|
fixable: input.fixable ?? null,
|
|
23897
24856
|
exceptionId: input.exceptionId ?? null,
|
|
23898
24857
|
exceptionReason: input.exceptionReason ?? null,
|
|
24858
|
+
exceptionPolicy: input.exceptionPolicy ?? null,
|
|
23899
24859
|
freshness,
|
|
23900
24860
|
repo: input.agentId === null ? null : ctx.repoFor(input.agentId),
|
|
23901
24861
|
retrieval,
|
|
23902
24862
|
activationField: ctx.activationField,
|
|
23903
24863
|
activationOwner: ctx.activationOwner
|
|
23904
24864
|
}, ctx.policy);
|
|
24865
|
+
const items = input.items && input.items.length > 0 ? { items: [...input.items] } : {};
|
|
23905
24866
|
return {
|
|
23906
24867
|
domain: input.domain,
|
|
23907
24868
|
agent_id: input.agentId,
|
|
@@ -23930,7 +24891,8 @@ function observation2(ctx, input) {
|
|
|
23930
24891
|
desired: bounded3(redactHome(input.desired ?? DOMAIN_DESIRED[input.domain])),
|
|
23931
24892
|
next_action: classification.next_action,
|
|
23932
24893
|
next_action_class: classification.next_action_class,
|
|
23933
|
-
justification: classification.justification
|
|
24894
|
+
justification: classification.justification,
|
|
24895
|
+
...items
|
|
23934
24896
|
};
|
|
23935
24897
|
}
|
|
23936
24898
|
function observeFromInventory(ctx, row, domains) {
|
|
@@ -24170,21 +25132,134 @@ function observeFromProvenance(ctx, facts, agentId, domains) {
|
|
|
24170
25132
|
if (!domains.has(domain)) continue;
|
|
24171
25133
|
out.push(factObservation(ctx, fact, domain, agentId));
|
|
24172
25134
|
}
|
|
24173
|
-
for (const domain of ["
|
|
25135
|
+
for (const domain of ["release_provenance"]) {
|
|
24174
25136
|
if (!domains.has(domain) || (byDomain.get(domain) ?? 0) > 0) continue;
|
|
24175
25137
|
out.push(observation2(ctx, {
|
|
24176
25138
|
domain,
|
|
24177
25139
|
agentId,
|
|
24178
25140
|
state: "unobserved",
|
|
24179
25141
|
field: DOMAIN_FIELD[domain],
|
|
24180
|
-
summary:
|
|
25142
|
+
summary: "the provenance core reported no release fact for this agent",
|
|
24181
25143
|
source: SOURCE_PROVENANCE,
|
|
24182
25144
|
observed: "no fact",
|
|
24183
|
-
desired:
|
|
25145
|
+
desired: "at least one release provenance fact for this agent"
|
|
24184
25146
|
}));
|
|
24185
25147
|
}
|
|
24186
25148
|
return out;
|
|
24187
25149
|
}
|
|
25150
|
+
function observeFromScaffold(ctx, input) {
|
|
25151
|
+
const { agentId, result: result2 } = input;
|
|
25152
|
+
if (!input.manifestDeclared) {
|
|
25153
|
+
return {
|
|
25154
|
+
observations: [observation2(ctx, {
|
|
25155
|
+
domain: "template_scaffold",
|
|
25156
|
+
agentId,
|
|
25157
|
+
state: "unsupported",
|
|
25158
|
+
field: DOMAIN_FIELD.template_scaffold,
|
|
25159
|
+
summary: "the fleet contract declares no scaffold_manifest, so no role directory can be compared against the pinned template",
|
|
25160
|
+
details: ["declare scaffold_manifest in the fleet contract (schema 3) to make this domain observable"],
|
|
25161
|
+
source: SOURCE_SCAFFOLD,
|
|
25162
|
+
capability: CAPABILITY_SCAFFOLD_MANIFEST,
|
|
25163
|
+
observed: "not observed",
|
|
25164
|
+
desired: "every owned asset compared against the template at the committed gitlink"
|
|
25165
|
+
})],
|
|
25166
|
+
summary: null
|
|
25167
|
+
};
|
|
25168
|
+
}
|
|
25169
|
+
if (result2 === void 0) {
|
|
25170
|
+
return {
|
|
25171
|
+
observations: [observation2(ctx, {
|
|
25172
|
+
domain: "template_scaffold",
|
|
25173
|
+
agentId,
|
|
25174
|
+
state: "unobserved",
|
|
25175
|
+
field: DOMAIN_FIELD.template_scaffold,
|
|
25176
|
+
summary: "the scaffold observer produced no result for this agent",
|
|
25177
|
+
source: SOURCE_SCAFFOLD,
|
|
25178
|
+
observed: "no result",
|
|
25179
|
+
desired: "eight group observations for this agent"
|
|
25180
|
+
})],
|
|
25181
|
+
summary: null
|
|
25182
|
+
};
|
|
25183
|
+
}
|
|
25184
|
+
const short = input.gitlink === null ? null : input.gitlink.slice(0, 12);
|
|
25185
|
+
const desiredSuffix = short === null ? "at the committed gitlink" : `at gitlink ${short}`;
|
|
25186
|
+
const observations = [];
|
|
25187
|
+
for (const group of result2.groups) {
|
|
25188
|
+
const items = group.items.map((item) => ({
|
|
25189
|
+
path: bounded3(item.path),
|
|
25190
|
+
kind: item.kind,
|
|
25191
|
+
desired: item.desired === null ? null : bounded3(item.desired, 64),
|
|
25192
|
+
observed: item.observed === null ? null : bounded3(item.observed, 64),
|
|
25193
|
+
detail: item.detail === null ? null : bounded3(item.detail),
|
|
25194
|
+
wip: item.wip
|
|
25195
|
+
}));
|
|
25196
|
+
const kept = items.slice(0, FLEET_STATUS_MAX_ITEMS);
|
|
25197
|
+
if (items.length > kept.length) {
|
|
25198
|
+
input.notes.push(
|
|
25199
|
+
`agents.${agentId}.observations[${group.group}].items: ${items.length - kept.length} of ${items.length} items dropped; the counts on this observation and on agents[].scaffold.assets are computed over every item before the cap`
|
|
25200
|
+
);
|
|
25201
|
+
}
|
|
25202
|
+
const details = kept.map((item) => {
|
|
25203
|
+
const pair = item.desired !== null || item.observed !== null ? ` ${item.desired ?? "-"} -> ${item.observed ?? "-"}` : "";
|
|
25204
|
+
return `${item.kind} ${item.path}${pair}${item.detail ? ` (${item.detail})` : ""}${item.wip ? " [wip]" : ""}`;
|
|
25205
|
+
});
|
|
25206
|
+
const summary = group.state === "pass" ? `${group.owned} owned asset(s) in ${group.role_path} match the template ${desiredSuffix}` : group.state === "fail" ? `${group.drifted} drifted, ${group.unexpected} unexpected and ${group.incomplete} undecided asset(s) in ${group.role_path}` : `${group.incomplete} asset(s) in ${group.role_path} could not be compared`;
|
|
25207
|
+
const excepted = group.state === "fail" && input.exception !== null;
|
|
25208
|
+
observations.push(observation2(ctx, {
|
|
25209
|
+
domain: "template_scaffold",
|
|
25210
|
+
agentId,
|
|
25211
|
+
state: group.state,
|
|
25212
|
+
field: group.group,
|
|
25213
|
+
ruleId: null,
|
|
25214
|
+
ruleScope: "project",
|
|
25215
|
+
fixable: false,
|
|
25216
|
+
source: SOURCE_SCAFFOLD,
|
|
25217
|
+
summary,
|
|
25218
|
+
details,
|
|
25219
|
+
observed: `${group.matching}/${group.owned} assets match`,
|
|
25220
|
+
desired: `every asset in ${group.group} ${desiredSuffix}`,
|
|
25221
|
+
items: kept,
|
|
25222
|
+
exceptionId: excepted ? agentId : null,
|
|
25223
|
+
exceptionReason: excepted ? input.exception.reason : null,
|
|
25224
|
+
exceptionPolicy: excepted ? input.exception.path : null
|
|
25225
|
+
}));
|
|
25226
|
+
}
|
|
25227
|
+
const overlap = result2.wipOverlap.slice(0, FLEET_STATUS_MAX_WIP_OVERLAP).map((path) => bounded3(path));
|
|
25228
|
+
if (result2.wipOverlap.length > overlap.length) {
|
|
25229
|
+
input.notes.push(`agents.${agentId}.scaffold.wip_overlap: ${result2.wipOverlap.length - overlap.length} of ${result2.wipOverlap.length} paths dropped`);
|
|
25230
|
+
}
|
|
25231
|
+
return {
|
|
25232
|
+
observations,
|
|
25233
|
+
summary: {
|
|
25234
|
+
source_gitlink: input.gitlink,
|
|
25235
|
+
role_dir: result2.roleDir === null ? null : shownPath3(result2.roleDir),
|
|
25236
|
+
role_dir_source: result2.roleDirSource,
|
|
25237
|
+
assets: { ...result2.assets },
|
|
25238
|
+
wip_overlap: overlap,
|
|
25239
|
+
wip_preserved: result2.wipPreserved,
|
|
25240
|
+
foreign_tracked: result2.foreignTracked,
|
|
25241
|
+
ignored_entries: result2.ignoredEntries
|
|
25242
|
+
}
|
|
25243
|
+
};
|
|
25244
|
+
}
|
|
25245
|
+
function scaffoldRuleVerdict(rule) {
|
|
25246
|
+
if (rule === void 0) return null;
|
|
25247
|
+
if (rule.status === "pass") return "pass";
|
|
25248
|
+
if (rule.status !== "fail") return null;
|
|
25249
|
+
const details = Array.isArray(rule.details) ? rule.details.filter((line) => typeof line === "string") : [];
|
|
25250
|
+
return details.some((line) => /: (?:missing|stale) (?!.*\/runtime\/)/u.test(line)) ? "fail" : null;
|
|
25251
|
+
}
|
|
25252
|
+
function scaffoldObserverDrift(result2) {
|
|
25253
|
+
if (result2 === void 0 || result2.error !== null) return null;
|
|
25254
|
+
if (result2.groups.some((group) => group.state === "error" && (RULE_COVERED_CONTENT_GROUPS.has(group.group) || group.owned > 0))) return null;
|
|
25255
|
+
for (const group of result2.groups) {
|
|
25256
|
+
for (const item of group.items) {
|
|
25257
|
+
if (item.kind === "missing" && RULE_COVERED_PRESENCE_PATHS.has(item.path)) return true;
|
|
25258
|
+
if (RULE_COVERED_CONTENT_GROUPS.has(group.group) && RULE_COVERED_KINDS.has(item.kind)) return true;
|
|
25259
|
+
}
|
|
25260
|
+
}
|
|
25261
|
+
return false;
|
|
25262
|
+
}
|
|
24188
25263
|
function factObservation(ctx, fact, domain, agentId) {
|
|
24189
25264
|
return observation2(ctx, {
|
|
24190
25265
|
domain,
|
|
@@ -24246,8 +25321,8 @@ function agentLifecycle(row, own, domains) {
|
|
|
24246
25321
|
}
|
|
24247
25322
|
function resolveAuditCli(env2 = process.env) {
|
|
24248
25323
|
const override = nonEmptyString3(env2[CLI_ENTRY_ENV]);
|
|
24249
|
-
const entry = override ??
|
|
24250
|
-
if (!
|
|
25324
|
+
const entry = override ?? join37(resolvePjanglerRoot(), "dist", "index.js");
|
|
25325
|
+
if (!isAbsolute10(entry)) return null;
|
|
24251
25326
|
return existsSync29(entry) ? entry : null;
|
|
24252
25327
|
}
|
|
24253
25328
|
function auditChildEnv(base = process.env) {
|
|
@@ -24318,10 +25393,10 @@ async function collectFleetStatus(options) {
|
|
|
24318
25393
|
let baselineText = null;
|
|
24319
25394
|
let baselineShown = "";
|
|
24320
25395
|
if (baselinePath !== null) {
|
|
24321
|
-
const resolved =
|
|
25396
|
+
const resolved = resolve23(expandHome4(baselinePath, home));
|
|
24322
25397
|
baselineShown = shownPath3(resolved);
|
|
24323
25398
|
try {
|
|
24324
|
-
baselineText =
|
|
25399
|
+
baselineText = readFileSync29(resolved, "utf8");
|
|
24325
25400
|
} catch {
|
|
24326
25401
|
throw new FleetError(
|
|
24327
25402
|
"INVALID_INPUT",
|
|
@@ -24364,15 +25439,22 @@ async function collectFleetStatus(options) {
|
|
|
24364
25439
|
const agentRawById = /* @__PURE__ */ new Map();
|
|
24365
25440
|
for (const entry of agentRaw.entries) if (!agentRawById.has(entry.key)) agentRawById.set(entry.key, entry.value);
|
|
24366
25441
|
const repoByAgent = /* @__PURE__ */ new Map();
|
|
25442
|
+
const roleDirByAgent = /* @__PURE__ */ new Map();
|
|
25443
|
+
const rawRowByAgent = /* @__PURE__ */ new Map();
|
|
24367
25444
|
for (const entry of agentRaw.entries) {
|
|
24368
|
-
if (!selectedAgents.has(entry.key)) continue;
|
|
25445
|
+
if (!selectedAgents.has(entry.key) || rawRowByAgent.has(entry.key)) continue;
|
|
24369
25446
|
const raw = isRecord10(entry.value) ? entry.value : {};
|
|
24370
25447
|
const declared = nonEmptyString3(raw.project_path);
|
|
24371
|
-
if (declared !== null) repoByAgent.set(entry.key,
|
|
25448
|
+
if (declared !== null) repoByAgent.set(entry.key, resolve23(expandHome4(declared, home)));
|
|
25449
|
+
const declaredRoleDir = nonEmptyString3(raw.role_dir);
|
|
25450
|
+
roleDirByAgent.set(entry.key, declaredRoleDir === null ? null : expandHome4(declaredRoleDir, home));
|
|
25451
|
+
rawRowByAgent.set(entry.key, raw);
|
|
24372
25452
|
}
|
|
24373
25453
|
const policy = readHealthPolicy(contract);
|
|
25454
|
+
const manifest = domainSet.has("template_scaffold") ? contract.scaffold_manifest ?? null : null;
|
|
24374
25455
|
const projectRawBySlug = /* @__PURE__ */ new Map();
|
|
24375
|
-
|
|
25456
|
+
const manifestNeedsProjects = manifest !== null && Object.values(manifest.render_inputs).some((field3) => field3.startsWith("projects."));
|
|
25457
|
+
if (manifestNeedsProjects || policy.freshness.some(({ entry }) => entry.field.startsWith("projects."))) {
|
|
24376
25458
|
const projectRaw = readProjectRegistryRaw(stores.projects.inspectedPath);
|
|
24377
25459
|
for (const entry of projectRaw.entries) {
|
|
24378
25460
|
if (!projectRawBySlug.has(entry.key)) projectRawBySlug.set(entry.key, entry.value);
|
|
@@ -24484,6 +25566,66 @@ async function collectFleetStatus(options) {
|
|
|
24484
25566
|
provenanceFacts = provenance.facts;
|
|
24485
25567
|
ctx.probes.push(...provenance.probes);
|
|
24486
25568
|
}
|
|
25569
|
+
const scalarAt = (root, segments) => {
|
|
25570
|
+
let node = root;
|
|
25571
|
+
for (const segment of segments) {
|
|
25572
|
+
if (!isRecord10(node)) return null;
|
|
25573
|
+
node = node[segment];
|
|
25574
|
+
}
|
|
25575
|
+
if (typeof node === "string") return nonEmptyString3(node);
|
|
25576
|
+
if (typeof node === "number" || typeof node === "boolean") return String(node);
|
|
25577
|
+
return null;
|
|
25578
|
+
};
|
|
25579
|
+
const renderInputsFor = (agentId) => {
|
|
25580
|
+
const inputs = {};
|
|
25581
|
+
if (manifest === null) return inputs;
|
|
25582
|
+
const rawRow = rawRowByAgent.get(agentId);
|
|
25583
|
+
const slug = rowsById.get(agentId)?.project_id.value ?? null;
|
|
25584
|
+
const project = slug === null ? void 0 : projectRawBySlug.get(slug);
|
|
25585
|
+
for (const [name, field3] of Object.entries(manifest.render_inputs)) {
|
|
25586
|
+
const [root, key, ...rest] = field3.split(".");
|
|
25587
|
+
if (field3 === "agents.{agent_id}") inputs[name] = agentId;
|
|
25588
|
+
else if (root === "agents" && key === "{agent_id}") inputs[name] = scalarAt(rawRow, rest);
|
|
25589
|
+
else if (root === "projects" && key === "{slug}") inputs[name] = scalarAt(project, rest);
|
|
25590
|
+
else inputs[name] = null;
|
|
25591
|
+
}
|
|
25592
|
+
return inputs;
|
|
25593
|
+
};
|
|
25594
|
+
const scaffoldExceptionFor = (agentId) => {
|
|
25595
|
+
const ruling = policy.agentExceptions.find(({ entry }) => entry.domain === "template_scaffold" && entry.agent_id === agentId);
|
|
25596
|
+
return ruling ? { path: ruling.path, reason: ruling.entry.reason } : null;
|
|
25597
|
+
};
|
|
25598
|
+
let scaffoldParity = null;
|
|
25599
|
+
const scaffoldByAgent = /* @__PURE__ */ new Map();
|
|
25600
|
+
if (manifest !== null) {
|
|
25601
|
+
throwIfCancelled(runContext);
|
|
25602
|
+
scaffoldParity = await collectScaffoldParity({
|
|
25603
|
+
run: runContext,
|
|
25604
|
+
pjanglerRoot: resolvePjanglerRoot(),
|
|
25605
|
+
manifest,
|
|
25606
|
+
agents: agentIds.map((agentId) => ({
|
|
25607
|
+
agentId,
|
|
25608
|
+
projectPath: repoByAgent.get(agentId) ?? null,
|
|
25609
|
+
roleDir: roleDirByAgent.get(agentId) ?? null,
|
|
25610
|
+
role: nonEmptyString3(rawRowByAgent.get(agentId)?.role),
|
|
25611
|
+
inputs: renderInputsFor(agentId)
|
|
25612
|
+
})),
|
|
25613
|
+
shown: shownPath3
|
|
25614
|
+
});
|
|
25615
|
+
ctx.probes.push(...scaffoldParity.probes);
|
|
25616
|
+
for (const result2 of scaffoldParity.agents) scaffoldByAgent.set(result2.agentId, result2);
|
|
25617
|
+
}
|
|
25618
|
+
const scaffoldCounts = {
|
|
25619
|
+
total_registered: inventory.totals.registered_agents,
|
|
25620
|
+
selected: agentIds.length,
|
|
25621
|
+
applicable: 0,
|
|
25622
|
+
passing: 0,
|
|
25623
|
+
drifted: 0,
|
|
25624
|
+
incomplete: 0,
|
|
25625
|
+
exception_authorized: 0,
|
|
25626
|
+
unobserved: 0
|
|
25627
|
+
};
|
|
25628
|
+
const ruleAgreement = { compared: 0, agree: 0, disagree: 0, not_compared: 0 };
|
|
24487
25629
|
const auditFedSelected = domains.filter((domain) => AUDIT_PER_AGENT_DOMAINS.has(domain));
|
|
24488
25630
|
const wantsAudit = live && auditFedSelected.length > 0;
|
|
24489
25631
|
const auditByAgent = /* @__PURE__ */ new Map();
|
|
@@ -24565,6 +25707,57 @@ async function collectFleetStatus(options) {
|
|
|
24565
25707
|
}
|
|
24566
25708
|
}
|
|
24567
25709
|
const hostByRule = /* @__PURE__ */ new Map();
|
|
25710
|
+
if (scaffoldParity !== null) {
|
|
25711
|
+
const state = scaffoldParity.source.integrity === "ok" ? "pass" : "error";
|
|
25712
|
+
const field3 = DOMAIN_FIELD.template_scaffold;
|
|
25713
|
+
const hostRetrieval = retrievalFor(null, "template_scaffold", live);
|
|
25714
|
+
const sourceClassification = classifyObservation({
|
|
25715
|
+
domain: "template_scaffold",
|
|
25716
|
+
state,
|
|
25717
|
+
field: field3,
|
|
25718
|
+
ruleId: SCAFFOLD_SOURCE_RULE_ID,
|
|
25719
|
+
ruleScope: "host",
|
|
25720
|
+
source: SOURCE_SCAFFOLD,
|
|
25721
|
+
capability: null,
|
|
25722
|
+
evidence: null,
|
|
25723
|
+
fixable: false,
|
|
25724
|
+
exceptionId: null,
|
|
25725
|
+
exceptionReason: null,
|
|
25726
|
+
freshness: "not_applicable",
|
|
25727
|
+
repo: null,
|
|
25728
|
+
retrieval: hostRetrieval,
|
|
25729
|
+
activationField: ctx.activationField,
|
|
25730
|
+
activationOwner: ctx.activationOwner
|
|
25731
|
+
}, ctx.policy);
|
|
25732
|
+
const sourceSummary = bounded3(redactHome(scaffoldParity.source.detail));
|
|
25733
|
+
hostByRule.set(SCAFFOLD_SOURCE_RULE_ID, {
|
|
25734
|
+
finding: {
|
|
25735
|
+
rule_id: SCAFFOLD_SOURCE_RULE_ID,
|
|
25736
|
+
owner: ctx.domainOwner("template_scaffold"),
|
|
25737
|
+
domain: "template_scaffold",
|
|
25738
|
+
state,
|
|
25739
|
+
summary: sourceSummary,
|
|
25740
|
+
details: boundedDetails([
|
|
25741
|
+
`integrity ${scaffoldParity.source.integrity}`,
|
|
25742
|
+
`gitlink ${scaffoldParity.source.gitlink ?? "none recorded"}`,
|
|
25743
|
+
`${scaffoldParity.source.assets} asset(s) in the manifest`
|
|
25744
|
+
]),
|
|
25745
|
+
finding_id: statusFindingId("host", null, "template_scaffold", SCAFFOLD_SOURCE_RULE_ID, field3, SOURCE_SCAFFOLD),
|
|
25746
|
+
retrieval: hostRetrieval,
|
|
25747
|
+
applicability: sourceClassification.applicability,
|
|
25748
|
+
evidence: sourceClassification.evidence,
|
|
25749
|
+
freshness: sourceClassification.freshness,
|
|
25750
|
+
severity: sourceClassification.severity,
|
|
25751
|
+
repair: sourceClassification.repair,
|
|
25752
|
+
observed: bounded3(scaffoldParity.source.integrity),
|
|
25753
|
+
desired: "the committed gitlink stable, its object present, the worktree at it with no tracked change, and every rendered path owned by a declared group",
|
|
25754
|
+
next_action: sourceClassification.next_action,
|
|
25755
|
+
next_action_class: sourceClassification.next_action_class,
|
|
25756
|
+
justification: sourceClassification.justification
|
|
25757
|
+
},
|
|
25758
|
+
states: /* @__PURE__ */ new Map([[state, 1]])
|
|
25759
|
+
});
|
|
25760
|
+
}
|
|
24568
25761
|
const agentRecords = [];
|
|
24569
25762
|
let totalObservations = 0;
|
|
24570
25763
|
let emittedObservations = 0;
|
|
@@ -24581,8 +25774,34 @@ async function collectFleetStatus(options) {
|
|
|
24581
25774
|
const own = [];
|
|
24582
25775
|
own.push(...observeFromInventory(ctx, row, domainSet));
|
|
24583
25776
|
if (needsProvenance) own.push(...observeFromProvenance(ctx, provenanceFacts, agentId, domainSet));
|
|
25777
|
+
const agentNotes = [];
|
|
25778
|
+
let scaffoldSummary = null;
|
|
25779
|
+
const scaffoldResult = scaffoldByAgent.get(agentId);
|
|
25780
|
+
const scaffoldObserved = manifest !== null && scaffoldResult !== void 0;
|
|
25781
|
+
if (domainSet.has("template_scaffold")) {
|
|
25782
|
+
const scaffold2 = observeFromScaffold(ctx, {
|
|
25783
|
+
agentId,
|
|
25784
|
+
result: scaffoldResult,
|
|
25785
|
+
manifestDeclared: manifest !== null,
|
|
25786
|
+
gitlink: scaffoldParity?.source.gitlink ?? null,
|
|
25787
|
+
exception: scaffoldExceptionFor(agentId),
|
|
25788
|
+
notes: agentNotes
|
|
25789
|
+
});
|
|
25790
|
+
own.push(...scaffold2.observations);
|
|
25791
|
+
scaffoldSummary = scaffold2.summary;
|
|
25792
|
+
if (!scaffoldObserved) scaffoldCounts.unobserved += 1;
|
|
25793
|
+
else {
|
|
25794
|
+
scaffoldCounts.applicable += 1;
|
|
25795
|
+
if (scaffoldResult.groups.some((group) => group.state === "error")) scaffoldCounts.incomplete += 1;
|
|
25796
|
+
else if (scaffoldResult.groups.some((group) => group.state === "fail")) {
|
|
25797
|
+
if (scaffoldExceptionFor(agentId) !== null) scaffoldCounts.exception_authorized += 1;
|
|
25798
|
+
else scaffoldCounts.drifted += 1;
|
|
25799
|
+
} else scaffoldCounts.passing += 1;
|
|
25800
|
+
}
|
|
25801
|
+
}
|
|
24584
25802
|
const audit = auditByAgent.get(agentId);
|
|
24585
25803
|
if (!live) {
|
|
25804
|
+
if (scaffoldObserved) ruleAgreement.not_compared += 1;
|
|
24586
25805
|
for (const domain of auditFedSelected) {
|
|
24587
25806
|
own.push(observation2(ctx, {
|
|
24588
25807
|
domain,
|
|
@@ -24597,6 +25816,7 @@ async function collectFleetStatus(options) {
|
|
|
24597
25816
|
}));
|
|
24598
25817
|
}
|
|
24599
25818
|
} else if (auditEntry === null || audit === void 0 || audit.outcome !== "ok") {
|
|
25819
|
+
if (scaffoldObserved) ruleAgreement.not_compared += 1;
|
|
24600
25820
|
const failed = audit !== void 0 && audit.outcome === "failed";
|
|
24601
25821
|
const state = failed ? "error" : "unobserved";
|
|
24602
25822
|
const reason = auditEntry === null ? "audit-cli-unavailable" : audit === void 0 ? "no-project-path-recorded" : audit.reason ?? audit.outcome;
|
|
@@ -24749,6 +25969,29 @@ async function collectFleetStatus(options) {
|
|
|
24749
25969
|
fixable: typeof rule.fixable === "boolean" ? rule.fixable : null
|
|
24750
25970
|
}));
|
|
24751
25971
|
}
|
|
25972
|
+
if (scaffoldObserved) {
|
|
25973
|
+
const ruleVerdict = scaffoldRuleVerdict((audit.rules ?? []).find((rule) => nonEmptyString3(rule.id) === "hermes.pm-scaffold"));
|
|
25974
|
+
const observerDrift = scaffoldObserverDrift(scaffoldResult);
|
|
25975
|
+
if (ruleVerdict === null || observerDrift === null) ruleAgreement.not_compared += 1;
|
|
25976
|
+
else {
|
|
25977
|
+
ruleAgreement.compared += 1;
|
|
25978
|
+
if (ruleVerdict === "fail" === observerDrift) ruleAgreement.agree += 1;
|
|
25979
|
+
else {
|
|
25980
|
+
ruleAgreement.disagree += 1;
|
|
25981
|
+
addFinding3(ctx, {
|
|
25982
|
+
code: "scaffold-rule-disagreement",
|
|
25983
|
+
domain: "template_scaffold",
|
|
25984
|
+
field: DOMAIN_FIELD.template_scaffold,
|
|
25985
|
+
agent_id: agentId,
|
|
25986
|
+
source: ctx.domainOwner("template_scaffold"),
|
|
25987
|
+
severity: "error",
|
|
25988
|
+
statusSeverity: "high",
|
|
25989
|
+
gating: true,
|
|
25990
|
+
detail: `the recipe rule hermes.pm-scaffold reports ${ruleVerdict} while the scaffold observer finds ${observerDrift ? "drift" : "no drift"} over the assets both compare (hermes, .gitignore, .scripts/** for content; role.yaml, SOUL.md and .runtime-scaffold/README.md for presence); both readings are kept and neither is resolved silently`
|
|
25991
|
+
});
|
|
25992
|
+
}
|
|
25993
|
+
}
|
|
25994
|
+
}
|
|
24752
25995
|
}
|
|
24753
25996
|
own.sort((a, b) => a.domain < b.domain ? -1 : a.domain > b.domain ? 1 : (a.rule_id ?? "") < (b.rule_id ?? "") ? -1 : (a.rule_id ?? "") > (b.rule_id ?? "") ? 1 : a.field < b.field ? -1 : a.field > b.field ? 1 : a.finding_id < b.finding_id ? -1 : a.finding_id > b.finding_id ? 1 : 0);
|
|
24754
25997
|
totalObservations += own.length;
|
|
@@ -24766,6 +26009,7 @@ async function collectFleetStatus(options) {
|
|
|
24766
26009
|
);
|
|
24767
26010
|
members[memberClass] += 1;
|
|
24768
26011
|
if (agentRecords.length >= FLEET_STATUS_MAX_AGENTS) continue;
|
|
26012
|
+
truncated.push(...agentNotes);
|
|
24769
26013
|
let kept = own;
|
|
24770
26014
|
let retrieval = retrievalFor(agentId, domains.length === 1 ? domains[0] : null, live);
|
|
24771
26015
|
if (clipped) {
|
|
@@ -24786,7 +26030,8 @@ async function collectFleetStatus(options) {
|
|
|
24786
26030
|
truncated: clipped,
|
|
24787
26031
|
retrieval,
|
|
24788
26032
|
lifecycle,
|
|
24789
|
-
member_class: memberClass
|
|
26033
|
+
member_class: memberClass,
|
|
26034
|
+
scaffold: scaffoldSummary
|
|
24790
26035
|
});
|
|
24791
26036
|
}
|
|
24792
26037
|
const fleetObservations = [];
|
|
@@ -24875,6 +26120,18 @@ async function collectFleetStatus(options) {
|
|
|
24875
26120
|
policy,
|
|
24876
26121
|
domainStates: new Map(domainRollups.map((rollup) => [rollup.domain, rollup.state]))
|
|
24877
26122
|
});
|
|
26123
|
+
const scaffold = !domainSet.has("template_scaffold") ? null : {
|
|
26124
|
+
source: scaffoldParity === null ? { gitlink: null, integrity: "manifest-undeclared", detail: "the fleet contract declares no scaffold_manifest, so no role directory was compared" } : {
|
|
26125
|
+
gitlink: scaffoldParity.source.gitlink,
|
|
26126
|
+
integrity: bounded3(scaffoldParity.source.integrity),
|
|
26127
|
+
detail: bounded3(redactHome(scaffoldParity.source.detail))
|
|
26128
|
+
},
|
|
26129
|
+
agents: {
|
|
26130
|
+
...scaffoldCounts,
|
|
26131
|
+
unobserved: scaffoldCounts.unobserved + (scaffoldCounts.total_registered - scaffoldCounts.selected)
|
|
26132
|
+
},
|
|
26133
|
+
rule_agreement: { ...ruleAgreement }
|
|
26134
|
+
};
|
|
24878
26135
|
return {
|
|
24879
26136
|
contract_path: shownPath3(contractPath),
|
|
24880
26137
|
contract_version: contract.contract_version,
|
|
@@ -24887,6 +26144,7 @@ async function collectFleetStatus(options) {
|
|
|
24887
26144
|
findings,
|
|
24888
26145
|
probes,
|
|
24889
26146
|
transitions,
|
|
26147
|
+
scaffold,
|
|
24890
26148
|
truncated
|
|
24891
26149
|
};
|
|
24892
26150
|
}
|
|
@@ -24918,13 +26176,13 @@ function emptyInspection(contractPath, diagnostics) {
|
|
|
24918
26176
|
function inspectFleetContract(override) {
|
|
24919
26177
|
const path = resolveFleetContractPath(override);
|
|
24920
26178
|
const shown = redactHome(path);
|
|
24921
|
-
const
|
|
26179
|
+
const canonical2 = override === void 0;
|
|
24922
26180
|
const loaded = loadFleetContract(path);
|
|
24923
26181
|
const { diagnostics, contract, extensions } = validateFleetContract(loaded.document);
|
|
24924
26182
|
if (!contract) return { ...emptyInspection(shown, diagnostics), extensions };
|
|
24925
26183
|
const byteStable = serializeFleetContract(loaded.document) === loaded.text;
|
|
24926
26184
|
const findings = [...diagnostics];
|
|
24927
|
-
if (
|
|
26185
|
+
if (canonical2 && !byteStable) {
|
|
24928
26186
|
findings.push({
|
|
24929
26187
|
code: "INVALID_INPUT",
|
|
24930
26188
|
path: "contract",
|
|
@@ -25248,7 +26506,7 @@ async function promptForRuleIds(rules) {
|
|
|
25248
26506
|
function readJson2(path) {
|
|
25249
26507
|
if (!existsSync30(path)) return void 0;
|
|
25250
26508
|
try {
|
|
25251
|
-
const parsed = JSON.parse(
|
|
26509
|
+
const parsed = JSON.parse(readFileSync30(path, "utf8"));
|
|
25252
26510
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
25253
26511
|
} catch {
|
|
25254
26512
|
return void 0;
|
|
@@ -25257,7 +26515,7 @@ function readJson2(path) {
|
|
|
25257
26515
|
function findGitRoot(cwd) {
|
|
25258
26516
|
const result2 = spawnSync16("git", ["rev-parse", "--show-toplevel"], { cwd, encoding: "utf8" });
|
|
25259
26517
|
if (result2.status !== 0) return void 0;
|
|
25260
|
-
return
|
|
26518
|
+
return resolve24(result2.stdout.trim());
|
|
25261
26519
|
}
|
|
25262
26520
|
function packageNameToProjectName(value) {
|
|
25263
26521
|
if (!value) return void 0;
|
|
@@ -25265,8 +26523,8 @@ function packageNameToProjectName(value) {
|
|
|
25265
26523
|
return name.replace(/[-_]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()).trim();
|
|
25266
26524
|
}
|
|
25267
26525
|
function deriveProjectDefaults(targetDir) {
|
|
25268
|
-
const manifest = readJson2(
|
|
25269
|
-
const pkg = readJson2(
|
|
26526
|
+
const manifest = readJson2(join38(targetDir, ".project.json"));
|
|
26527
|
+
const pkg = readJson2(join38(targetDir, "package.json"));
|
|
25270
26528
|
const name = String(manifest?.project_name ?? "").trim() || packageNameToProjectName(typeof pkg?.name === "string" ? pkg.name : void 0) || packageNameToProjectName(basename10(targetDir)) || "Project";
|
|
25271
26529
|
const ticketProvider = manifest?.ticket_provider && typeof manifest.ticket_provider === "object" ? manifest.ticket_provider : {};
|
|
25272
26530
|
return {
|
|
@@ -25342,7 +26600,7 @@ function actionNeedsRun(plan, kind, syncMode) {
|
|
|
25342
26600
|
if (!action || action.kind !== "project.write-manifest") return false;
|
|
25343
26601
|
const next = `${JSON.stringify(action.manifest, null, 2)}
|
|
25344
26602
|
`;
|
|
25345
|
-
return !existsSync30(action.path) ||
|
|
26603
|
+
return !existsSync30(action.path) || readFileSync30(action.path, "utf8") !== next;
|
|
25346
26604
|
}
|
|
25347
26605
|
if (kind === "copier.copy.commonproject") return true;
|
|
25348
26606
|
if (kind === "ticket-provider.create-or-link") return plan.actions.some((action) => action.kind === kind && action.enabled);
|
|
@@ -25390,27 +26648,27 @@ async function resolveProjectInitTarget(name, options) {
|
|
|
25390
26648
|
const interactive = isInteractiveProjectInit(options);
|
|
25391
26649
|
const cwd = process.cwd();
|
|
25392
26650
|
const cwdGitRoot = findGitRoot(cwd);
|
|
25393
|
-
let targetDir = options.targetDir ?
|
|
26651
|
+
let targetDir = options.targetDir ? resolve24(options.targetDir) : void 0;
|
|
25394
26652
|
if (!targetDir && cwdGitRoot) {
|
|
25395
26653
|
targetDir = cwdGitRoot;
|
|
25396
26654
|
}
|
|
25397
26655
|
if (!targetDir && interactive) {
|
|
25398
26656
|
const defaultName = name ?? basename10(cwd);
|
|
25399
26657
|
const promptedName = name ?? await promptTextValue("Project name", packageNameToProjectName(defaultName));
|
|
25400
|
-
const defaultDir =
|
|
26658
|
+
const defaultDir = join38(cwd, promptedName.replace(/[^A-Za-z0-9._-]/g, "") || promptedName.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
25401
26659
|
targetDir = await promptTextValue("Project directory", defaultDir);
|
|
25402
26660
|
name = promptedName;
|
|
25403
26661
|
}
|
|
25404
26662
|
if (!targetDir) {
|
|
25405
26663
|
if (!name) throw new Error("Project name or --target-dir is required when project init is not run inside a git repo");
|
|
25406
|
-
targetDir =
|
|
26664
|
+
targetDir = resolve24(process.cwd(), name.replace(/[^A-Za-z0-9._-]/g, "") || name.toLowerCase().replace(/[^a-z0-9]+/g, "-"));
|
|
25407
26665
|
}
|
|
25408
26666
|
const targetExists = existsSync30(targetDir);
|
|
25409
26667
|
if (targetExists && !statSync9(targetDir).isDirectory()) throw new Error(`Target path is not a directory: ${targetDir}`);
|
|
25410
26668
|
const targetGitRoot = targetExists ? findGitRoot(targetDir) : void 0;
|
|
25411
|
-
const alreadyScaffolded = targetExists && (existsSync30(
|
|
26669
|
+
const alreadyScaffolded = targetExists && (existsSync30(join38(targetDir, ".project.json")) || existsSync30(join38(targetDir, ".copier-answers.yml")));
|
|
25412
26670
|
const syncMode = Boolean(
|
|
25413
|
-
targetGitRoot &&
|
|
26671
|
+
targetGitRoot && resolve24(targetGitRoot) === resolve24(targetDir) || alreadyScaffolded
|
|
25414
26672
|
);
|
|
25415
26673
|
const defaults = targetExists ? deriveProjectDefaults(targetDir) : { name: packageNameToProjectName(basename10(targetDir)) ?? "Project", description: "" };
|
|
25416
26674
|
if (!name && interactive && !syncMode) {
|