@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/mcp-server.js
CHANGED
|
@@ -192,7 +192,7 @@ var init_types = __esm({
|
|
|
192
192
|
});
|
|
193
193
|
|
|
194
194
|
// src/utils/tree-diff.ts
|
|
195
|
-
import { createHash as
|
|
195
|
+
import { createHash as createHash4 } from "node:crypto";
|
|
196
196
|
import { existsSync as existsSync4, lstatSync as lstatSync4, readFileSync as readFileSync5, readdirSync as readdirSync3, readlinkSync as readlinkSync2 } from "node:fs";
|
|
197
197
|
import { join as join6, relative as relative3 } from "node:path";
|
|
198
198
|
function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()) {
|
|
@@ -203,7 +203,7 @@ function snapshotTree(root, current = root, snapshot = /* @__PURE__ */ new Map()
|
|
|
203
203
|
if (stat.isSymbolicLink()) {
|
|
204
204
|
snapshot.set(rel, `link:${readlinkSync2(current)}`);
|
|
205
205
|
} else if (stat.isFile()) {
|
|
206
|
-
snapshot.set(rel, `file:${
|
|
206
|
+
snapshot.set(rel, `file:${createHash4("sha256").update(readFileSync5(current)).digest("hex")}:${stat.mode & 511}`);
|
|
207
207
|
} else if (stat.isDirectory()) {
|
|
208
208
|
snapshot.set(rel, `dir:${stat.mode & 511}`);
|
|
209
209
|
for (const name of readdirSync3(current)) snapshotTree(root, join6(current, name), snapshot);
|
|
@@ -222,7 +222,7 @@ var init_tree_diff = __esm({
|
|
|
222
222
|
});
|
|
223
223
|
|
|
224
224
|
// src/lifecycle/preflight.ts
|
|
225
|
-
import { createHash as
|
|
225
|
+
import { createHash as createHash5 } from "node:crypto";
|
|
226
226
|
import {
|
|
227
227
|
accessSync,
|
|
228
228
|
constants as constants2,
|
|
@@ -254,7 +254,7 @@ function firstExecutableOnPath(env2) {
|
|
|
254
254
|
return void 0;
|
|
255
255
|
}
|
|
256
256
|
function sha2562(path) {
|
|
257
|
-
return
|
|
257
|
+
return createHash5("sha256").update(readFileSync6(path)).digest("base64url");
|
|
258
258
|
}
|
|
259
259
|
function fingerprint(path) {
|
|
260
260
|
const absolute = resolve4(path);
|
|
@@ -2529,7 +2529,7 @@ var init_config = __esm({
|
|
|
2529
2529
|
});
|
|
2530
2530
|
|
|
2531
2531
|
// src/notebook/notes.ts
|
|
2532
|
-
import { createHash as
|
|
2532
|
+
import { createHash as createHash6, randomUUID as randomUUID2 } from "node:crypto";
|
|
2533
2533
|
function canonicalJson(value) {
|
|
2534
2534
|
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");
|
|
2535
2535
|
if (value === null || typeof value !== "object") {
|
|
@@ -2542,7 +2542,7 @@ function canonicalJson(value) {
|
|
|
2542
2542
|
return `{${Object.keys(record).filter((key) => record[key] !== void 0).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
|
|
2543
2543
|
}
|
|
2544
2544
|
function sha256Hex(value) {
|
|
2545
|
-
return
|
|
2545
|
+
return createHash6("sha256").update(value).digest("hex");
|
|
2546
2546
|
}
|
|
2547
2547
|
function base64UrlEncode(value) {
|
|
2548
2548
|
return Buffer.from(value, "utf8").toString("base64url");
|
|
@@ -2799,7 +2799,7 @@ var init_remote_mutation_schema = __esm({
|
|
|
2799
2799
|
});
|
|
2800
2800
|
|
|
2801
2801
|
// src/notebook/state.ts
|
|
2802
|
-
import { createHash as
|
|
2802
|
+
import { createHash as createHash7, randomUUID as randomUUID3 } from "node:crypto";
|
|
2803
2803
|
import {
|
|
2804
2804
|
closeSync as closeSync4,
|
|
2805
2805
|
constants as constants3,
|
|
@@ -5098,7 +5098,7 @@ var init_capture = __esm({
|
|
|
5098
5098
|
|
|
5099
5099
|
// src/mcp-server.ts
|
|
5100
5100
|
import { existsSync as existsSync27, statSync as statSync8 } from "node:fs";
|
|
5101
|
-
import { basename as basename9, dirname as
|
|
5101
|
+
import { basename as basename9, dirname as dirname18, join as join36, resolve as resolve22 } from "node:path";
|
|
5102
5102
|
import { fileURLToPath as fileURLToPath9 } from "node:url";
|
|
5103
5103
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5104
5104
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -5307,7 +5307,7 @@ import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync as mkdirS
|
|
|
5307
5307
|
import { basename as basename2, dirname as dirname2, join as join3, relative as relative2, resolve as resolve3 } from "node:path";
|
|
5308
5308
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5309
5309
|
import { homedir as homedir2 } from "node:os";
|
|
5310
|
-
import { createHash as
|
|
5310
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5311
5311
|
import { spawnSync } from "node:child_process";
|
|
5312
5312
|
import YAML from "yaml";
|
|
5313
5313
|
|
|
@@ -5323,9 +5323,149 @@ var SUPPORTED_CLIS = Object.freeze([
|
|
|
5323
5323
|
var SUPPORTED_BMAD_TOOLS = SUPPORTED_CLIS.map((cli) => cli.bmadTool);
|
|
5324
5324
|
var SUPPORTED_CLI_ROOTS = SUPPORTED_CLIS.map((cli) => cli.projectRoot);
|
|
5325
5325
|
|
|
5326
|
+
// src/scaffold/compare.ts
|
|
5327
|
+
import { createHash } from "node:crypto";
|
|
5328
|
+
var PLACEHOLDER = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/gu;
|
|
5329
|
+
var JINJA_DELIMITER = /\{\{|\{%|\{#/u;
|
|
5330
|
+
function renderTemplate(source, inputs) {
|
|
5331
|
+
const stripped = source.replace(PLACEHOLDER, "");
|
|
5332
|
+
if (JINJA_DELIMITER.test(stripped)) {
|
|
5333
|
+
return { ok: false, reason: "render-unsupported", detail: "render-unsupported: template uses Jinja constructs beyond simple substitution" };
|
|
5334
|
+
}
|
|
5335
|
+
const undeclared = /* @__PURE__ */ new Set();
|
|
5336
|
+
const missing = /* @__PURE__ */ new Set();
|
|
5337
|
+
const text2 = source.replace(PLACEHOLDER, (whole, name) => {
|
|
5338
|
+
if (!Object.prototype.hasOwnProperty.call(inputs, name)) {
|
|
5339
|
+
undeclared.add(name);
|
|
5340
|
+
return whole;
|
|
5341
|
+
}
|
|
5342
|
+
const value = inputs[name];
|
|
5343
|
+
if (value === null || value === void 0) {
|
|
5344
|
+
missing.add(name);
|
|
5345
|
+
return whole;
|
|
5346
|
+
}
|
|
5347
|
+
return value;
|
|
5348
|
+
});
|
|
5349
|
+
if (undeclared.size > 0) {
|
|
5350
|
+
return { ok: false, reason: "render-unsupported", detail: `render-unsupported: undeclared placeholder ${[...undeclared].sort().join(", ")}` };
|
|
5351
|
+
}
|
|
5352
|
+
if (missing.size > 0) {
|
|
5353
|
+
return { ok: false, reason: "input-missing", detail: `input-missing: ${[...missing].sort().join(", ")}` };
|
|
5354
|
+
}
|
|
5355
|
+
return { ok: true, text: text2 };
|
|
5356
|
+
}
|
|
5357
|
+
function blobId(bytes) {
|
|
5358
|
+
return createHash("sha1").update(`blob ${bytes.byteLength}\0`, "latin1").update(bytes).digest("hex");
|
|
5359
|
+
}
|
|
5360
|
+
function digestPrefix(id) {
|
|
5361
|
+
return id.slice(0, 12);
|
|
5362
|
+
}
|
|
5363
|
+
function globToRegExp(glob) {
|
|
5364
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/gu, "\\$&").replace(/\*/gu, "[^/]*").replace(/\?/gu, "[^/]");
|
|
5365
|
+
return new RegExp(`^${escaped}$`, "u");
|
|
5366
|
+
}
|
|
5367
|
+
function matchesExcluded(path, patterns) {
|
|
5368
|
+
const segments = path.split("/").filter((segment) => segment !== "");
|
|
5369
|
+
for (const pattern of patterns) {
|
|
5370
|
+
if (pattern === "" || pattern === "/") continue;
|
|
5371
|
+
const directory = pattern.endsWith("/");
|
|
5372
|
+
const glob = directory ? pattern.slice(0, -1) : pattern;
|
|
5373
|
+
if (glob === "") continue;
|
|
5374
|
+
if (glob.includes("/")) {
|
|
5375
|
+
const expression2 = globToRegExp(glob.replace(/^\/+/u, ""));
|
|
5376
|
+
if (directory ? segments.some((_, index) => index < segments.length - 1 && expression2.test(segments.slice(0, index + 1).join("/"))) : expression2.test(segments.join("/"))) return pattern;
|
|
5377
|
+
continue;
|
|
5378
|
+
}
|
|
5379
|
+
const expression = globToRegExp(glob);
|
|
5380
|
+
const candidates = directory ? segments.slice(0, -1) : segments;
|
|
5381
|
+
if (candidates.some((segment) => expression.test(segment))) return pattern;
|
|
5382
|
+
}
|
|
5383
|
+
return null;
|
|
5384
|
+
}
|
|
5385
|
+
function groupFor(path, groups) {
|
|
5386
|
+
let best = null;
|
|
5387
|
+
let bestLength = -1;
|
|
5388
|
+
for (const leaf of Object.keys(groups).sort()) {
|
|
5389
|
+
const rolePath = groups[leaf];
|
|
5390
|
+
const matches = rolePath.endsWith("/") ? path.startsWith(rolePath) : path === rolePath;
|
|
5391
|
+
if (!matches) continue;
|
|
5392
|
+
const length = rolePath.endsWith("/") ? rolePath.length : rolePath.length + 1;
|
|
5393
|
+
if (length > bestLength) {
|
|
5394
|
+
best = leaf;
|
|
5395
|
+
bestLength = length;
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
return best;
|
|
5399
|
+
}
|
|
5400
|
+
function typeWord(asset) {
|
|
5401
|
+
return asset.type;
|
|
5402
|
+
}
|
|
5403
|
+
function modeWord(executable) {
|
|
5404
|
+
return executable ? "100755" : "100644";
|
|
5405
|
+
}
|
|
5406
|
+
function compareAssets(desired, observe, options) {
|
|
5407
|
+
const out = [];
|
|
5408
|
+
const compareModes = options.modes !== false;
|
|
5409
|
+
const push = (asset, seen, kind, desiredWord, observedWord, detail = null) => {
|
|
5410
|
+
out.push({ path: asset.path, kind, desired: desiredWord, observed: observedWord, detail, wip: seen.wip });
|
|
5411
|
+
};
|
|
5412
|
+
for (const asset of desired) {
|
|
5413
|
+
const seen = observe(asset);
|
|
5414
|
+
if (asset.incomplete) {
|
|
5415
|
+
push(asset, seen, "incomplete", null, seen.present ? seen.type : "absent", asset.incomplete.detail);
|
|
5416
|
+
continue;
|
|
5417
|
+
}
|
|
5418
|
+
if (!seen.present) {
|
|
5419
|
+
push(asset, seen, "missing", typeWord(asset), "absent");
|
|
5420
|
+
continue;
|
|
5421
|
+
}
|
|
5422
|
+
if (seen.type === "symlink" && seen.unsafeSymlink) {
|
|
5423
|
+
push(asset, seen, "unsafe-symlink", typeWord(asset), "symlink");
|
|
5424
|
+
continue;
|
|
5425
|
+
}
|
|
5426
|
+
if (seen.type !== asset.type) {
|
|
5427
|
+
push(asset, seen, "wrong-type", typeWord(asset), seen.type ?? "unknown");
|
|
5428
|
+
continue;
|
|
5429
|
+
}
|
|
5430
|
+
if (compareModes && asset.type === "file" && seen.executable !== asset.executable) {
|
|
5431
|
+
push(asset, seen, "wrong-mode", modeWord(asset.executable), modeWord(seen.executable));
|
|
5432
|
+
}
|
|
5433
|
+
if (asset.presenceOnly || asset.blobId === null) continue;
|
|
5434
|
+
if (seen.unreadable !== null || seen.blobId === null) {
|
|
5435
|
+
push(asset, seen, "incomplete", digestPrefix(asset.blobId), null, `unreadable: ${seen.unreadable ?? "unread"}`);
|
|
5436
|
+
continue;
|
|
5437
|
+
}
|
|
5438
|
+
if (seen.blobId === asset.blobId) continue;
|
|
5439
|
+
const kind = options.inLineage(seen.blobId, asset.path) ? "stale-content" : "locally-modified";
|
|
5440
|
+
push(asset, seen, kind, digestPrefix(asset.blobId), digestPrefix(seen.blobId));
|
|
5441
|
+
}
|
|
5442
|
+
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);
|
|
5443
|
+
}
|
|
5444
|
+
function classifyExtras(input) {
|
|
5445
|
+
const unexpected = [];
|
|
5446
|
+
let foreignTracked = 0;
|
|
5447
|
+
const runtimePrefix = `${input.runtimeDir.replace(/\/+$/u, "")}/`;
|
|
5448
|
+
for (const path of input.trackedPaths) {
|
|
5449
|
+
if (input.ownedPaths.has(path)) continue;
|
|
5450
|
+
if (path.startsWith(runtimePrefix)) {
|
|
5451
|
+
foreignTracked += 1;
|
|
5452
|
+
continue;
|
|
5453
|
+
}
|
|
5454
|
+
const group = groupFor(path, input.groups);
|
|
5455
|
+
if (group === null) {
|
|
5456
|
+
foreignTracked += 1;
|
|
5457
|
+
continue;
|
|
5458
|
+
}
|
|
5459
|
+
const excluded = matchesExcluded(path, input.excludedPatterns);
|
|
5460
|
+
unexpected.push({ path, group, detail: excluded === null ? null : `matches excluded pattern ${excluded}` });
|
|
5461
|
+
}
|
|
5462
|
+
unexpected.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
5463
|
+
return { unexpected, foreignTracked };
|
|
5464
|
+
}
|
|
5465
|
+
|
|
5326
5466
|
// src/parity/pack.ts
|
|
5327
5467
|
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, readdirSync } from "node:fs";
|
|
5328
|
-
import { createHash } from "node:crypto";
|
|
5468
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
5329
5469
|
import { basename, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
5330
5470
|
var PackUnavailableError = class extends Error {
|
|
5331
5471
|
constructor(message) {
|
|
@@ -5335,7 +5475,7 @@ var PackUnavailableError = class extends Error {
|
|
|
5335
5475
|
};
|
|
5336
5476
|
var CANONICAL_NAME_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
|
|
5337
5477
|
function sha256(content) {
|
|
5338
|
-
return
|
|
5478
|
+
return createHash2("sha256").update(content).digest("hex");
|
|
5339
5479
|
}
|
|
5340
5480
|
function readRegularFile(path) {
|
|
5341
5481
|
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
@@ -6647,7 +6787,7 @@ function availableSkillsRegistryRoots(ctx) {
|
|
|
6647
6787
|
);
|
|
6648
6788
|
}
|
|
6649
6789
|
function digestSkillEntry(root) {
|
|
6650
|
-
const hash =
|
|
6790
|
+
const hash = createHash3("sha256");
|
|
6651
6791
|
try {
|
|
6652
6792
|
const stat = lstatSync2(root);
|
|
6653
6793
|
if (stat.isSymbolicLink()) return null;
|
|
@@ -7712,8 +7852,72 @@ ${roleSpecific}
|
|
|
7712
7852
|
Your memory is stored locally at \`./runtime/memories/\`. Use durable memory deliberately and keep \`memories/MEMORY.md\` current.
|
|
7713
7853
|
`;
|
|
7714
7854
|
}
|
|
7855
|
+
function renderScaffoldAsset(templateRoleDir, jinjaRel, inputs) {
|
|
7856
|
+
const result2 = renderTemplate(readText(join3(templateRoleDir, jinjaRel)), inputs);
|
|
7857
|
+
if (!result2.ok) throw new Error(`${jinjaRel}: ${result2.detail}`);
|
|
7858
|
+
return result2.text;
|
|
7859
|
+
}
|
|
7715
7860
|
function renderHermesWrapper(role, templateRoleDir) {
|
|
7716
|
-
return
|
|
7861
|
+
return renderScaffoldAsset(templateRoleDir, "hermes.jinja", { agent_id: role.agentId });
|
|
7862
|
+
}
|
|
7863
|
+
function sentinelPromptInputs(role) {
|
|
7864
|
+
return {
|
|
7865
|
+
agent_id: role.agentId,
|
|
7866
|
+
role: role.role,
|
|
7867
|
+
target_repo: role.repo,
|
|
7868
|
+
display_name: role.displayName || role.agentId,
|
|
7869
|
+
ticket_provider: role.ticketProviderName || "plane"
|
|
7870
|
+
};
|
|
7871
|
+
}
|
|
7872
|
+
function observeScaffoldAsset(path) {
|
|
7873
|
+
const seen = { present: false, type: null, executable: false, blobId: null, unsafeSymlink: false, unreadable: null, wip: false };
|
|
7874
|
+
try {
|
|
7875
|
+
const stat = lstatSync2(path);
|
|
7876
|
+
seen.present = true;
|
|
7877
|
+
if (stat.isSymbolicLink()) {
|
|
7878
|
+
seen.type = "symlink";
|
|
7879
|
+
seen.blobId = blobId(Buffer.from(readlinkSync(path), "utf8"));
|
|
7880
|
+
} else if (stat.isDirectory()) {
|
|
7881
|
+
seen.type = "directory";
|
|
7882
|
+
} else if (stat.isFile()) {
|
|
7883
|
+
seen.type = "file";
|
|
7884
|
+
seen.executable = (stat.mode & 73) !== 0;
|
|
7885
|
+
try {
|
|
7886
|
+
seen.blobId = blobId(readFileSync2(path));
|
|
7887
|
+
} catch {
|
|
7888
|
+
seen.unreadable = "unreadable";
|
|
7889
|
+
}
|
|
7890
|
+
} else {
|
|
7891
|
+
seen.type = "other";
|
|
7892
|
+
}
|
|
7893
|
+
} catch {
|
|
7894
|
+
}
|
|
7895
|
+
return seen;
|
|
7896
|
+
}
|
|
7897
|
+
function scaffoldDesiredForRule(role, templateRoleDir, managedScripts) {
|
|
7898
|
+
const desired = [];
|
|
7899
|
+
const asset = (path, blob, incomplete = null, presenceOnly = false) => {
|
|
7900
|
+
desired.push({ path, type: "file", executable: false, blobId: blob, presenceOnly, incomplete });
|
|
7901
|
+
};
|
|
7902
|
+
for (const rel of ["role.yaml", "SOUL.md", ".runtime-scaffold/README.md"]) asset(rel, null, null, true);
|
|
7903
|
+
const rendered = (path, jinjaRel, inputs) => {
|
|
7904
|
+
const source = join3(templateRoleDir, jinjaRel);
|
|
7905
|
+
if (!existsSync2(source)) {
|
|
7906
|
+
asset(path, null, { reason: "render-unsupported", detail: `render-unsupported: ${jinjaRel} is absent from the template` });
|
|
7907
|
+
return;
|
|
7908
|
+
}
|
|
7909
|
+
const result2 = renderTemplate(readFileSync2(source).toString("utf8"), inputs);
|
|
7910
|
+
if (!result2.ok) {
|
|
7911
|
+
asset(path, null, { reason: result2.reason, detail: result2.detail });
|
|
7912
|
+
return;
|
|
7913
|
+
}
|
|
7914
|
+
asset(path, blobId(Buffer.from(result2.text, "utf8")));
|
|
7915
|
+
};
|
|
7916
|
+
rendered("hermes", "hermes.jinja", { agent_id: role.agentId });
|
|
7917
|
+
rendered(".gitignore", ".gitignore.jinja", { role: role.role });
|
|
7918
|
+
for (const rel of managedScripts) asset(`.scripts/${rel}`, blobId(readFileSync2(join3(templateRoleDir, ".scripts", rel))));
|
|
7919
|
+
rendered(".scripts/sentinel.prompt.md", ".scripts/sentinel.prompt.md.jinja", sentinelPromptInputs(role));
|
|
7920
|
+
return desired;
|
|
7717
7921
|
}
|
|
7718
7922
|
function templateFiles(sourceDir, current = sourceDir) {
|
|
7719
7923
|
if (!existsSync2(current)) return [];
|
|
@@ -7767,7 +7971,7 @@ function managedHermesScaffoldRoles(ctx) {
|
|
|
7767
7971
|
return { roles, blockers };
|
|
7768
7972
|
}
|
|
7769
7973
|
function renderSentinelPrompt(role, templateRoleDir) {
|
|
7770
|
-
return
|
|
7974
|
+
return renderScaffoldAsset(templateRoleDir, join3(".scripts", "sentinel.prompt.md.jinja"), sentinelPromptInputs(role));
|
|
7771
7975
|
}
|
|
7772
7976
|
function copyMissingRecursive(sourceDir, targetDir, changedFiles, dryRun, skip) {
|
|
7773
7977
|
if (!existsSync2(sourceDir)) return;
|
|
@@ -8495,7 +8699,7 @@ function ownedRegistryEntries(registry, repoRoot) {
|
|
|
8495
8699
|
}
|
|
8496
8700
|
return owned;
|
|
8497
8701
|
}
|
|
8498
|
-
function unprovisionedRoleAgents(registry, repoRoot,
|
|
8702
|
+
function unprovisionedRoleAgents(registry, repoRoot, canonical2) {
|
|
8499
8703
|
const blockers = /* @__PURE__ */ new Map();
|
|
8500
8704
|
const record = (agentId, roleDir, source) => {
|
|
8501
8705
|
const current = blockers.get(agentId) ?? { roleDir, sources: /* @__PURE__ */ new Set() };
|
|
@@ -8504,12 +8708,12 @@ function unprovisionedRoleAgents(registry, repoRoot, canonical) {
|
|
|
8504
8708
|
blockers.set(agentId, current);
|
|
8505
8709
|
};
|
|
8506
8710
|
for (const [agentId, entry] of ownedRegistryEntries(registry, repoRoot)) {
|
|
8507
|
-
if (
|
|
8711
|
+
if (canonical2.has(agentId)) continue;
|
|
8508
8712
|
const roleDir = String(entry.role_dir ?? "");
|
|
8509
8713
|
if (declaredRoleIsUnprovisioned(repoRoot, roleDir)) record(agentId, roleDir, "registry");
|
|
8510
8714
|
}
|
|
8511
8715
|
for (const [agentId, entry] of declaredAgentEntries(repoRoot)) {
|
|
8512
|
-
if (
|
|
8716
|
+
if (canonical2.has(agentId)) continue;
|
|
8513
8717
|
const configured = String(entry.role_dir ?? "");
|
|
8514
8718
|
const roleDir = configured ? resolve3(repoRoot, configured) : "";
|
|
8515
8719
|
if (declaredRoleIsUnprovisioned(repoRoot, configured)) record(agentId, roleDir, ".project.json");
|
|
@@ -8760,7 +8964,7 @@ function unsupportedRootAttestation(repoRoot, rootName) {
|
|
|
8760
8964
|
for (const rel of walked.files) {
|
|
8761
8965
|
const expectedHash = inventory.files.get(rel);
|
|
8762
8966
|
if (!expectedHash) return { safe: false, reason: `${rootName}/${rel} is outside the BMAD generated inventory` };
|
|
8763
|
-
const actualHash =
|
|
8967
|
+
const actualHash = createHash3("sha256").update(readFileSync2(join3(root, rel))).digest("hex");
|
|
8764
8968
|
if (actualHash !== expectedHash) return { safe: false, reason: `${rootName}/${rel} was locally modified after generation` };
|
|
8765
8969
|
}
|
|
8766
8970
|
return { safe: true, reason: `${walked.files.length} file(s) match BMAD installer inventory and hashes` };
|
|
@@ -9307,15 +9511,15 @@ function createProjectJsonChecks() {
|
|
|
9307
9511
|
const droppedDetails = [];
|
|
9308
9512
|
const path = join3(ctx.repoRoot, ".project.json");
|
|
9309
9513
|
const existing = readProjectJson(ctx) ?? {};
|
|
9310
|
-
const
|
|
9514
|
+
const canonical2 = canonicalProjectJson(ctx);
|
|
9311
9515
|
const preservedDetails = [];
|
|
9312
|
-
for (const agentId of
|
|
9516
|
+
for (const agentId of canonical2.unprovisioned) {
|
|
9313
9517
|
const roleDir = existing.agents?.[agentId]?.role_dir;
|
|
9314
9518
|
preservedDetails.push(
|
|
9315
9519
|
`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`
|
|
9316
9520
|
);
|
|
9317
9521
|
}
|
|
9318
|
-
for (const agentId of
|
|
9522
|
+
for (const agentId of canonical2.dropped) {
|
|
9319
9523
|
const entry = existing.agents?.[agentId];
|
|
9320
9524
|
const entryRecord = typeof entry === "object" && entry !== null ? entry : void 0;
|
|
9321
9525
|
const declared = {
|
|
@@ -9328,7 +9532,7 @@ function createProjectJsonChecks() {
|
|
|
9328
9532
|
const reason = validation.details.join("; ") || "invalid";
|
|
9329
9533
|
droppedDetails.push(`dropped invalid declared agent: ${agentId} (${reason})`);
|
|
9330
9534
|
}
|
|
9331
|
-
const { dropped: _dropped, unprovisioned: _unprovisioned, ...canonicalJson2 } =
|
|
9535
|
+
const { dropped: _dropped, unprovisioned: _unprovisioned, ...canonicalJson2 } = canonical2;
|
|
9332
9536
|
const merged = { ...existing, ...canonicalJson2 };
|
|
9333
9537
|
const expected = `${JSON.stringify(merged, null, 2)}
|
|
9334
9538
|
`;
|
|
@@ -10026,26 +10230,19 @@ function createHermesChecks() {
|
|
|
10026
10230
|
const managedScripts = templateFiles(join3(templateRoleDir, ".scripts")).filter((rel) => rel !== "sentinel.prompt.md.jinja");
|
|
10027
10231
|
for (const role of selection.roles) {
|
|
10028
10232
|
const prefix = role.agentId || role.role;
|
|
10029
|
-
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
const
|
|
10033
|
-
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
const
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
const source = join3(templateRoleDir, ".scripts", rel);
|
|
10042
|
-
const target = join3(role.roleDir, ".scripts", rel);
|
|
10043
|
-
if (!existsSync2(target)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, target)}`);
|
|
10044
|
-
else if (safeReadText(target) !== readText(source)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, target)}`);
|
|
10233
|
+
const memory = join3(role.roleDir, "runtime", "memories", "MEMORY.md");
|
|
10234
|
+
if (!existsSync2(memory)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, memory)}`);
|
|
10235
|
+
const desired = scaffoldDesiredForRule(role, templateRoleDir, managedScripts);
|
|
10236
|
+
const findings = compareAssets(
|
|
10237
|
+
desired,
|
|
10238
|
+
(asset) => observeScaffoldAsset(join3(role.roleDir, ...asset.path.split("/"))),
|
|
10239
|
+
{ inLineage: () => true, modes: false }
|
|
10240
|
+
);
|
|
10241
|
+
for (const finding2 of findings) {
|
|
10242
|
+
const word = finding2.kind === "stale-content" ? "stale" : finding2.kind;
|
|
10243
|
+
const shown = relative2(ctx.repoRoot, join3(role.roleDir, ...finding2.path.split("/")));
|
|
10244
|
+
details.push(`${prefix}: ${word} ${shown}${finding2.detail ? ` (${finding2.detail})` : ""}`);
|
|
10045
10245
|
}
|
|
10046
|
-
const promptPath = join3(role.roleDir, ".scripts", "sentinel.prompt.md");
|
|
10047
|
-
if (!existsSync2(promptPath)) details.push(`${prefix}: missing ${relative2(ctx.repoRoot, promptPath)}`);
|
|
10048
|
-
else if (safeReadText(promptPath) !== renderSentinelPrompt(role, templateRoleDir)) details.push(`${prefix}: stale ${relative2(ctx.repoRoot, promptPath)}`);
|
|
10049
10246
|
if (hasRuntimeSubmoduleMapping(ctx.repoRoot, role)) details.push(`${prefix}: .gitmodules contains retired ${role.role} runtime submodule mapping`);
|
|
10050
10247
|
if (!profileMetaInheritsDefault(join3(role.roleDir, "runtime", "profile.yaml"))) details.push(`${prefix}: runtime/profile.yaml missing inherited default config metadata`);
|
|
10051
10248
|
const registry = safeReadText(registryPath(ctx.homeDir));
|
|
@@ -10648,9 +10845,9 @@ function createHermesChecks() {
|
|
|
10648
10845
|
}
|
|
10649
10846
|
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 };
|
|
10650
10847
|
}
|
|
10651
|
-
const
|
|
10848
|
+
const canonical2 = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
10652
10849
|
const owned = ownedRegistryEntries(registry, ctx.repoRoot);
|
|
10653
|
-
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot,
|
|
10850
|
+
const unprovisioned = unprovisionedRoleAgents(registry, ctx.repoRoot, canonical2);
|
|
10654
10851
|
if (unprovisioned.length) {
|
|
10655
10852
|
return {
|
|
10656
10853
|
id: "hermes.registry-parity",
|
|
@@ -10663,16 +10860,16 @@ function createHermesChecks() {
|
|
|
10663
10860
|
fixable: false
|
|
10664
10861
|
};
|
|
10665
10862
|
}
|
|
10666
|
-
if (
|
|
10863
|
+
if (canonical2.size === 0) {
|
|
10667
10864
|
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 };
|
|
10668
10865
|
}
|
|
10669
10866
|
for (const [agentId, entry] of owned) {
|
|
10670
10867
|
const roleDir = String(entry?.role_dir ?? "");
|
|
10671
|
-
if (!
|
|
10672
|
-
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...
|
|
10868
|
+
if (!canonical2.has(agentId)) {
|
|
10869
|
+
details.push(`stale/duplicate registry agent "${agentId}" for ${roleDir} (role.yaml declares ${[...canonical2].join(", ")})`);
|
|
10673
10870
|
}
|
|
10674
10871
|
}
|
|
10675
|
-
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !
|
|
10872
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical2.has(id))) {
|
|
10676
10873
|
details.push(`.project.json declares agent "${extra}" that no role.yaml claims`);
|
|
10677
10874
|
}
|
|
10678
10875
|
for (const role of roles) {
|
|
@@ -10761,8 +10958,8 @@ function createHermesChecks() {
|
|
|
10761
10958
|
return { id: finding2.id, title: finding2.title, status: "blocked", summary: "registry is not valid YAML", changedFiles, details };
|
|
10762
10959
|
}
|
|
10763
10960
|
const agents = doc?.agents ?? {};
|
|
10764
|
-
const
|
|
10765
|
-
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot,
|
|
10961
|
+
const canonical2 = new Set(roles.map((role) => role.agentId).filter(Boolean));
|
|
10962
|
+
const unprovisioned = unprovisionedRoleAgents(agents, ctx.repoRoot, canonical2);
|
|
10766
10963
|
if (unprovisioned.length) {
|
|
10767
10964
|
return {
|
|
10768
10965
|
id: finding2.id,
|
|
@@ -10777,7 +10974,7 @@ function createHermesChecks() {
|
|
|
10777
10974
|
}
|
|
10778
10975
|
const fleetBin = fleetBinPath(ctx);
|
|
10779
10976
|
let dirty = false;
|
|
10780
|
-
if (
|
|
10977
|
+
if (canonical2.size === 0) {
|
|
10781
10978
|
for (const [agentId] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
10782
10979
|
details.push(`blocked: "${agentId}" has no role.yaml; provision the role instead of pruning the registry`);
|
|
10783
10980
|
}
|
|
@@ -10839,7 +11036,7 @@ function createHermesChecks() {
|
|
|
10839
11036
|
}
|
|
10840
11037
|
}
|
|
10841
11038
|
for (const [agentId, entry] of ownedRegistryEntries(agents, ctx.repoRoot)) {
|
|
10842
|
-
if (
|
|
11039
|
+
if (canonical2.size > 0 && !canonical2.has(agentId)) {
|
|
10843
11040
|
details.push(`drop stale/duplicate registry agent "${agentId}"`);
|
|
10844
11041
|
delete agents[agentId];
|
|
10845
11042
|
dropDeclaredAgent(ctx, agentId, changedFiles, details);
|
|
@@ -10859,8 +11056,8 @@ function createHermesChecks() {
|
|
|
10859
11056
|
dirty = true;
|
|
10860
11057
|
}
|
|
10861
11058
|
}
|
|
10862
|
-
if (
|
|
10863
|
-
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !
|
|
11059
|
+
if (canonical2.size > 0) {
|
|
11060
|
+
for (const extra of declaredAgentIds(ctx.repoRoot).filter((id) => !canonical2.has(id))) {
|
|
10864
11061
|
dropDeclaredAgent(ctx, extra, changedFiles, details);
|
|
10865
11062
|
}
|
|
10866
11063
|
}
|
|
@@ -13351,7 +13548,7 @@ init_notes();
|
|
|
13351
13548
|
init_state();
|
|
13352
13549
|
init_types();
|
|
13353
13550
|
import { spawn, spawnSync as spawnSync11 } from "node:child_process";
|
|
13354
|
-
import { createHash as
|
|
13551
|
+
import { createHash as createHash8, randomUUID as randomUUID5 } from "node:crypto";
|
|
13355
13552
|
import { chmodSync as chmodSync4, closeSync as closeSync6, constants as constants5, copyFileSync as copyFileSync3, existsSync as existsSync17, fstatSync as fstatSync4, fsyncSync as fsyncSync4, lstatSync as lstatSync11, mkdirSync as mkdirSync7, openSync as openSync6, readFileSync as readFileSync17, readSync as readSync3, readdirSync as readdirSync8, realpathSync as realpathSync8, renameSync as renameSync6, rmSync as rmSync4, symlinkSync as symlinkSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "node:fs";
|
|
13356
13553
|
import { basename as basename8, dirname as dirname11, isAbsolute as isAbsolute4, join as join23, parse as parse3, relative as relative11, resolve as resolve12, sep as sep6 } from "node:path";
|
|
13357
13554
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
@@ -13408,7 +13605,7 @@ function enumerateSkillPayload(source) {
|
|
|
13408
13605
|
if (entry.isDirectory()) walk(path);
|
|
13409
13606
|
else if (entry.isFile()) {
|
|
13410
13607
|
const expectedMode = rel.endsWith(".sh") || rel.startsWith("scripts/") ? "0755" : "0644";
|
|
13411
|
-
result2.push({ path: rel, sha256:
|
|
13608
|
+
result2.push({ path: rel, sha256: createHash8("sha256").update(readFileSync17(path)).digest("hex"), mode: expectedMode });
|
|
13412
13609
|
} else throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
13413
13610
|
}
|
|
13414
13611
|
};
|
|
@@ -13430,7 +13627,7 @@ function parsePackedManifest(source) {
|
|
|
13430
13627
|
const path = join23(source, ...entry.path.split("/"));
|
|
13431
13628
|
const stat = lstatSync11(path);
|
|
13432
13629
|
if (!stat.isFile() || stat.isSymbolicLink()) throw new NotebookError("CONFLICT", "Project Notebook skill export contains a non-regular entry");
|
|
13433
|
-
const actual =
|
|
13630
|
+
const actual = createHash8("sha256").update(readFileSync17(path)).digest("hex");
|
|
13434
13631
|
if (actual !== entry.sha256) throw new NotebookError("CONFLICT", `Project Notebook skill digest mismatch: ${entry.path}`);
|
|
13435
13632
|
const actualMode = stat.mode & 511;
|
|
13436
13633
|
const executable = entry.mode === "0755";
|
|
@@ -13451,7 +13648,7 @@ function expectedPackedSkill() {
|
|
|
13451
13648
|
assertOwnedSkillTree(candidate);
|
|
13452
13649
|
const manifest = parsePackedManifest(candidate);
|
|
13453
13650
|
if (!manifest) continue;
|
|
13454
|
-
const digest =
|
|
13651
|
+
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
13455
13652
|
return { source: candidate, manifest, digest };
|
|
13456
13653
|
}
|
|
13457
13654
|
throw new NotebookError("NOT_CONFIGURED", "PJ\xE1ngler package has no verified Project Notebook skill export");
|
|
@@ -13519,10 +13716,10 @@ function resolveProjectNotebookSkillSource(env2 = process.env) {
|
|
|
13519
13716
|
return explicit;
|
|
13520
13717
|
}
|
|
13521
13718
|
if (env2.PJ_SKILLS_REGISTRY_ROOT) {
|
|
13522
|
-
const
|
|
13523
|
-
if (existsSync17(join23(
|
|
13524
|
-
verifyProjectNotebookSkillExport(
|
|
13525
|
-
return
|
|
13719
|
+
const canonical2 = resolve12(env2.PJ_SKILLS_REGISTRY_ROOT, "all-skills", "project-notebook");
|
|
13720
|
+
if (existsSync17(join23(canonical2, "SKILL.md"))) {
|
|
13721
|
+
verifyProjectNotebookSkillExport(canonical2);
|
|
13722
|
+
return canonical2;
|
|
13526
13723
|
}
|
|
13527
13724
|
}
|
|
13528
13725
|
try {
|
|
@@ -13590,7 +13787,7 @@ function installPackagedProjectNotebookSkill(input = {}) {
|
|
|
13590
13787
|
const source = input.source ?? resolveProjectNotebookSkillSource(env2);
|
|
13591
13788
|
if (!source) throw new NotebookError("NOT_CONFIGURED", "Project Notebook skill source is unavailable");
|
|
13592
13789
|
const manifest = verifyProjectNotebookSkillExport(source);
|
|
13593
|
-
const digest =
|
|
13790
|
+
const digest = createHash8("sha256").update(JSON.stringify(manifest)).digest("hex");
|
|
13594
13791
|
const home = env2.HOME;
|
|
13595
13792
|
if (!home || !resolve12(home).startsWith("/")) throw new NotebookError("NOT_CONFIGURED", "A trusted HOME is required to install the Project Notebook skill");
|
|
13596
13793
|
const skillsRoot = join23(home, ".agents", "skills");
|
|
@@ -13745,7 +13942,7 @@ function repairProjectNotebookSkillProjection(input = {}) {
|
|
|
13745
13942
|
if (!existsSync17(path)) return true;
|
|
13746
13943
|
const stat = lstatSync11(path);
|
|
13747
13944
|
if (!stat.isFile()) return true;
|
|
13748
|
-
if (
|
|
13945
|
+
if (createHash8("sha256").update(readFileSync17(path)).digest("hex") !== entry.sha256) return true;
|
|
13749
13946
|
const mode = stat.mode & 511;
|
|
13750
13947
|
const executable = entry.mode === "0755";
|
|
13751
13948
|
return executable && (mode & 64) === 0 || !executable && (mode & 73) !== 0;
|
|
@@ -14996,8 +15193,8 @@ var ProjectRecipe = class extends Recipe {
|
|
|
14996
15193
|
} else try {
|
|
14997
15194
|
rmSync5(targetDir, { recursive: true, force: true });
|
|
14998
15195
|
const insideTarget = (path) => {
|
|
14999
|
-
const
|
|
15000
|
-
return
|
|
15196
|
+
const relative14 = relativePath(resolve14(targetDir), resolve14(path));
|
|
15197
|
+
return relative14 === "" || !relative14.startsWith("..") && !isAbsolute6(relative14);
|
|
15001
15198
|
};
|
|
15002
15199
|
const orphaned = [...new Set(changedFiles.filter((path) => !insideTarget(path)))].sort();
|
|
15003
15200
|
changedFiles.length = 0;
|
|
@@ -15438,15 +15635,15 @@ var WireMiseAgentHooks = class _WireMiseAgentHooks extends Command {
|
|
|
15438
15635
|
const enterRe = /(enter\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
15439
15636
|
if (enterRe.test(content)) {
|
|
15440
15637
|
content = content.replace(enterRe, (_m, head, close) => {
|
|
15441
|
-
const
|
|
15442
|
-
return `${head}${
|
|
15638
|
+
const sep8 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
15639
|
+
return `${head}${sep8}
|
|
15443
15640
|
${enterAdds}${close}`;
|
|
15444
15641
|
});
|
|
15445
15642
|
const leaveRe = /(leave\s*=\s*\[[\s\S]*?)(\n[ \t]*\])/;
|
|
15446
15643
|
if (leaveRe.test(content)) {
|
|
15447
15644
|
content = content.replace(leaveRe, (_m, head, close) => {
|
|
15448
|
-
const
|
|
15449
|
-
return `${head}${
|
|
15645
|
+
const sep8 = /[,[]\s*$/.test(head) ? "" : ",";
|
|
15646
|
+
return `${head}${sep8}
|
|
15450
15647
|
"${cr}/.agents/hooks/sync.py --uninstall --quiet",${close}`;
|
|
15451
15648
|
});
|
|
15452
15649
|
} else {
|
|
@@ -16570,7 +16767,7 @@ init_preflight();
|
|
|
16570
16767
|
import { z } from "zod";
|
|
16571
16768
|
|
|
16572
16769
|
// src/fleet/inventory.ts
|
|
16573
|
-
import { createHash as
|
|
16770
|
+
import { createHash as createHash9 } from "node:crypto";
|
|
16574
16771
|
import { existsSync as existsSync24, lstatSync as lstatSync13, readFileSync as readFileSync23, readlinkSync as readlinkSync3, statSync as statSync6 } from "node:fs";
|
|
16575
16772
|
import { homedir as homedir12 } from "node:os";
|
|
16576
16773
|
import { isAbsolute as isAbsolute7, join as join32, posix, relative as relative12, resolve as resolve18 } from "node:path";
|
|
@@ -16583,7 +16780,7 @@ import { fileURLToPath as fileURLToPath8 } from "node:url";
|
|
|
16583
16780
|
import YAML8 from "yaml";
|
|
16584
16781
|
|
|
16585
16782
|
// src/fleet/types.ts
|
|
16586
|
-
var FLEET_SUPPORTED_SCHEMA_VERSIONS = { min: 1, max:
|
|
16783
|
+
var FLEET_SUPPORTED_SCHEMA_VERSIONS = { min: 1, max: 3 };
|
|
16587
16784
|
var FLEET_SCHEMA_VERSION = 1;
|
|
16588
16785
|
var FLEET_CONTRACT_ROOT_KEYS = [
|
|
16589
16786
|
"schema_version",
|
|
@@ -16595,20 +16792,34 @@ var FLEET_CONTRACT_ROOT_KEYS = [
|
|
|
16595
16792
|
"service_model",
|
|
16596
16793
|
"activation",
|
|
16597
16794
|
"retired",
|
|
16598
|
-
"health_policy"
|
|
16795
|
+
"health_policy",
|
|
16796
|
+
"scaffold_manifest"
|
|
16599
16797
|
];
|
|
16600
|
-
var FLEET_CONTRACT_OPTIONAL_ROOT_KEYS = ["health_policy"];
|
|
16798
|
+
var FLEET_CONTRACT_OPTIONAL_ROOT_KEYS = ["health_policy", "scaffold_manifest"];
|
|
16601
16799
|
var FLEET_HEALTH_POLICY_KEYS = [
|
|
16602
16800
|
"required_domains",
|
|
16603
16801
|
"deferred_capabilities",
|
|
16604
16802
|
"allowed_warnings",
|
|
16605
16803
|
"allowed_skips",
|
|
16606
|
-
"freshness"
|
|
16804
|
+
"freshness",
|
|
16805
|
+
"agent_exceptions"
|
|
16607
16806
|
];
|
|
16608
16807
|
var FLEET_HEALTH_POLICY_DEFERRED_KEYS = ["domain", "capability", "reason", "owner_story"];
|
|
16609
16808
|
var FLEET_HEALTH_POLICY_WARNING_KEYS = ["rule_id", "reason", "owner"];
|
|
16610
16809
|
var FLEET_HEALTH_POLICY_SKIP_KEYS = ["domain", "rule_id", "reason"];
|
|
16611
16810
|
var FLEET_HEALTH_POLICY_FRESHNESS_KEYS = ["field", "max_age_days", "applies_to"];
|
|
16811
|
+
var FLEET_HEALTH_POLICY_AGENT_EXCEPTION_KEYS = ["domain", "agent_id", "reason", "owner"];
|
|
16812
|
+
var FLEET_SCAFFOLD_MANIFEST_KEYS = [
|
|
16813
|
+
"template_submodule",
|
|
16814
|
+
"template_subdirectory",
|
|
16815
|
+
"render_suffix",
|
|
16816
|
+
"render_inputs",
|
|
16817
|
+
"groups",
|
|
16818
|
+
"presence_only",
|
|
16819
|
+
"excluded_patterns",
|
|
16820
|
+
"runtime_dir"
|
|
16821
|
+
];
|
|
16822
|
+
var FLEET_SCAFFOLD_PRESENCE_ONLY_KEYS = ["path", "reason"];
|
|
16612
16823
|
var FLEET_COMPATIBILITY_KEYS = ["min_schema_version", "max_schema_version"];
|
|
16613
16824
|
var FLEET_AUTHORITY_KEYS = ["owner", "store", "store_env", "writable_fields", "read_only", "notes"];
|
|
16614
16825
|
var FLEET_PROJECTION_KEYS = ["field", "source", "target", "direction", "writable_by"];
|
|
@@ -16703,6 +16914,9 @@ var FLEET_STATUS_MAX_OBSERVATIONS_PER_AGENT = 200;
|
|
|
16703
16914
|
var FLEET_STATUS_MAX_FINDINGS = 2e3;
|
|
16704
16915
|
var FLEET_STATUS_MAX_DETAILS = 20;
|
|
16705
16916
|
var FLEET_STATUS_AUDIT_CONCURRENCY = 4;
|
|
16917
|
+
var FLEET_STATUS_SCAFFOLD_CONCURRENCY = 4;
|
|
16918
|
+
var FLEET_STATUS_MAX_ITEMS = 100;
|
|
16919
|
+
var FLEET_STATUS_MAX_WIP_OVERLAP = 20;
|
|
16706
16920
|
var FLEET_STATUS_EVIDENCE = ["direct", "declared", "derived", "absent"];
|
|
16707
16921
|
var FLEET_STATUS_FRESHNESS_PRECEDENCE = [
|
|
16708
16922
|
"stale",
|
|
@@ -16851,7 +17065,8 @@ function validateFleetContract(document) {
|
|
|
16851
17065
|
() => validateAuthorityConflicts(policy),
|
|
16852
17066
|
() => sortDiagnostics(validateClassifications(policy)),
|
|
16853
17067
|
() => sortDiagnostics(validateRetiredModes(policy)),
|
|
16854
|
-
() => sortDiagnostics(validateHealthPolicy(policy))
|
|
17068
|
+
() => sortDiagnostics(validateHealthPolicy(policy)),
|
|
17069
|
+
() => sortDiagnostics(validateScaffoldManifest(policy))
|
|
16855
17070
|
];
|
|
16856
17071
|
for (const stage of stages) {
|
|
16857
17072
|
const findings = stage();
|
|
@@ -17445,6 +17660,141 @@ function validateHealthPolicy(policy) {
|
|
|
17445
17660
|
});
|
|
17446
17661
|
}
|
|
17447
17662
|
}
|
|
17663
|
+
const exceptions = block.agent_exceptions;
|
|
17664
|
+
if (exceptions !== void 0) {
|
|
17665
|
+
if (!Array.isArray(exceptions)) fail("health_policy.agent_exceptions", "agent_exceptions must be a list");
|
|
17666
|
+
else {
|
|
17667
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17668
|
+
exceptions.forEach((entry, index) => {
|
|
17669
|
+
const at = `health_policy.agent_exceptions[${index}]`;
|
|
17670
|
+
if (!isRecord4(entry)) {
|
|
17671
|
+
fail(at, "agent exception must be a mapping");
|
|
17672
|
+
return;
|
|
17673
|
+
}
|
|
17674
|
+
for (const key2 of Object.keys(entry)) {
|
|
17675
|
+
if (!FLEET_HEALTH_POLICY_AGENT_EXCEPTION_KEYS.includes(key2)) fail(`${at}.${key2}`, "unknown agent_exceptions key");
|
|
17676
|
+
}
|
|
17677
|
+
requireDomain(entry.domain, `${at}.domain`);
|
|
17678
|
+
if (typeof entry.agent_id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/u.test(entry.agent_id)) {
|
|
17679
|
+
fail(`${at}.agent_id`, "agent_id must be a registry key: letters, digits, dots, underscores or hyphens");
|
|
17680
|
+
}
|
|
17681
|
+
for (const key2 of ["reason", "owner"]) {
|
|
17682
|
+
const value = entry[key2];
|
|
17683
|
+
if (typeof value !== "string" || value.trim().length === 0) fail(`${at}.${key2}`, `${key2} must be a non-empty string`);
|
|
17684
|
+
}
|
|
17685
|
+
const key = `${String(entry.domain)}\0${String(entry.agent_id)}`;
|
|
17686
|
+
if (seen.has(key)) fail(at, "duplicate agent exception: the same domain and agent_id is already ruled on");
|
|
17687
|
+
seen.add(key);
|
|
17688
|
+
});
|
|
17689
|
+
}
|
|
17690
|
+
}
|
|
17691
|
+
return findings;
|
|
17692
|
+
}
|
|
17693
|
+
var SAFE_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/u;
|
|
17694
|
+
var RENDER_INPUT_NAME = /^[a-z_][a-z0-9_]*$/u;
|
|
17695
|
+
function relativeInside(value) {
|
|
17696
|
+
if (typeof value !== "string" || value === "" || value.startsWith("/") || value.includes("\\")) return false;
|
|
17697
|
+
const segments = value.replace(/\/+$/u, "").split("/");
|
|
17698
|
+
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
17699
|
+
}
|
|
17700
|
+
function validateScaffoldManifest(policy) {
|
|
17701
|
+
const block = policy.scaffold_manifest;
|
|
17702
|
+
if (block === void 0) return [];
|
|
17703
|
+
const findings = [];
|
|
17704
|
+
const fail = (path, message) => {
|
|
17705
|
+
findings.push({ code: "INVALID_INPUT", path, message });
|
|
17706
|
+
};
|
|
17707
|
+
if (!isRecord4(block)) {
|
|
17708
|
+
fail("scaffold_manifest", "scaffold_manifest must be a mapping");
|
|
17709
|
+
return findings;
|
|
17710
|
+
}
|
|
17711
|
+
for (const key of Object.keys(block)) {
|
|
17712
|
+
if (!FLEET_SCAFFOLD_MANIFEST_KEYS.includes(key)) fail(`scaffold_manifest.${key}`, "unknown scaffold_manifest key");
|
|
17713
|
+
}
|
|
17714
|
+
for (const key of ["template_submodule", "template_subdirectory"]) {
|
|
17715
|
+
if (!relativeInside(block[key]) || block[key].endsWith("/")) fail(`scaffold_manifest.${key}`, `${key} must be a relative path inside the repository`);
|
|
17716
|
+
}
|
|
17717
|
+
const suffix = block.render_suffix;
|
|
17718
|
+
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");
|
|
17719
|
+
const runtimeDir = block.runtime_dir;
|
|
17720
|
+
if (typeof runtimeDir !== "string" || !SAFE_SEGMENT.test(runtimeDir)) fail("scaffold_manifest.runtime_dir", "runtime_dir must be one safe path segment");
|
|
17721
|
+
const declaredFields = /* @__PURE__ */ new Set();
|
|
17722
|
+
const authorities = policy.authorities;
|
|
17723
|
+
if (isRecord4(authorities)) {
|
|
17724
|
+
for (const entry of Object.values(authorities)) {
|
|
17725
|
+
if (!isRecord4(entry) || !Array.isArray(entry.writable_fields)) continue;
|
|
17726
|
+
for (const field3 of entry.writable_fields) if (typeof field3 === "string") declaredFields.add(field3);
|
|
17727
|
+
}
|
|
17728
|
+
}
|
|
17729
|
+
const inputs = block.render_inputs;
|
|
17730
|
+
if (!isRecord4(inputs)) fail("scaffold_manifest.render_inputs", "render_inputs must be a mapping of placeholder name to field path");
|
|
17731
|
+
else {
|
|
17732
|
+
for (const [name, field3] of Object.entries(inputs)) {
|
|
17733
|
+
const at = `scaffold_manifest.render_inputs.${name}`;
|
|
17734
|
+
if (!RENDER_INPUT_NAME.test(name)) fail(at, "a render input name must be a lower-case identifier");
|
|
17735
|
+
if (typeof field3 !== "string" || !FIELD_PATH.test(field3)) {
|
|
17736
|
+
fail(at, "must be a dotted field path");
|
|
17737
|
+
continue;
|
|
17738
|
+
}
|
|
17739
|
+
const resolvable = field3 === "agents.{agent_id}" || (field3.startsWith("agents.{agent_id}.") || field3.startsWith("projects.{slug}.")) && declaredFields.has(field3);
|
|
17740
|
+
if (!resolvable) fail(at, `${field3} is not agents.{agent_id} or a declared agents.{agent_id}.* / projects.{slug}.* writable field`);
|
|
17741
|
+
}
|
|
17742
|
+
}
|
|
17743
|
+
const groups = block.groups;
|
|
17744
|
+
const groupTable = {};
|
|
17745
|
+
if (!isRecord4(groups) || Object.keys(groups).length === 0) fail("scaffold_manifest.groups", "groups must be a non-empty mapping of declared leaf to role-relative path");
|
|
17746
|
+
else {
|
|
17747
|
+
const seenPaths = /* @__PURE__ */ new Map();
|
|
17748
|
+
for (const [leaf, rolePath] of Object.entries(groups)) {
|
|
17749
|
+
const at = `scaffold_manifest.groups.${leaf}`;
|
|
17750
|
+
if (!declaredFields.has(leaf)) fail(at, `${leaf} is not declared writable by any authority`);
|
|
17751
|
+
if (!relativeInside(rolePath)) {
|
|
17752
|
+
fail(at, "must be a role-relative path; a trailing / owns a directory");
|
|
17753
|
+
continue;
|
|
17754
|
+
}
|
|
17755
|
+
const previous = seenPaths.get(rolePath);
|
|
17756
|
+
if (previous !== void 0) fail(at, `duplicate group path ${rolePath}, already owned by ${previous}`);
|
|
17757
|
+
else seenPaths.set(rolePath, leaf);
|
|
17758
|
+
groupTable[leaf] = rolePath;
|
|
17759
|
+
}
|
|
17760
|
+
}
|
|
17761
|
+
const presence = block.presence_only;
|
|
17762
|
+
if (presence !== void 0) {
|
|
17763
|
+
if (!Array.isArray(presence)) fail("scaffold_manifest.presence_only", "presence_only must be a list");
|
|
17764
|
+
else {
|
|
17765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17766
|
+
presence.forEach((entry, index) => {
|
|
17767
|
+
const at = `scaffold_manifest.presence_only[${index}]`;
|
|
17768
|
+
if (!isRecord4(entry)) {
|
|
17769
|
+
fail(at, "presence_only entry must be a mapping");
|
|
17770
|
+
return;
|
|
17771
|
+
}
|
|
17772
|
+
for (const key of Object.keys(entry)) {
|
|
17773
|
+
if (!FLEET_SCAFFOLD_PRESENCE_ONLY_KEYS.includes(key)) fail(`${at}.${key}`, "unknown presence_only key");
|
|
17774
|
+
}
|
|
17775
|
+
const path = entry.path;
|
|
17776
|
+
if (!relativeInside(path) || path.endsWith("/")) fail(`${at}.path`, "path must be a role-relative file path");
|
|
17777
|
+
else if (Object.keys(groupTable).length > 0 && groupFor(path, groupTable) === null) fail(`${at}.path`, `${path} resolves to no declared group`);
|
|
17778
|
+
else if (seen.has(path)) fail(`${at}.path`, `duplicate presence_only path ${path}`);
|
|
17779
|
+
else seen.add(path);
|
|
17780
|
+
if (typeof entry.reason !== "string" || entry.reason.trim().length === 0) fail(`${at}.reason`, "reason must be a non-empty string");
|
|
17781
|
+
});
|
|
17782
|
+
}
|
|
17783
|
+
}
|
|
17784
|
+
const excluded = block.excluded_patterns;
|
|
17785
|
+
if (excluded !== void 0) {
|
|
17786
|
+
if (!Array.isArray(excluded)) fail("scaffold_manifest.excluded_patterns", "excluded_patterns must be a list");
|
|
17787
|
+
else {
|
|
17788
|
+
excluded.forEach((pattern, index) => {
|
|
17789
|
+
const at = `scaffold_manifest.excluded_patterns[${index}]`;
|
|
17790
|
+
if (typeof pattern !== "string" || pattern.trim() === "" || pattern === "/" || pattern.startsWith("/")) {
|
|
17791
|
+
fail(at, "an excluded pattern must be a non-empty segment glob");
|
|
17792
|
+
return;
|
|
17793
|
+
}
|
|
17794
|
+
if (/^\*+\/?$/u.test(pattern) || pattern.includes("**")) fail(at, "an excluded pattern may not match every segment");
|
|
17795
|
+
});
|
|
17796
|
+
}
|
|
17797
|
+
}
|
|
17448
17798
|
return findings;
|
|
17449
17799
|
}
|
|
17450
17800
|
|
|
@@ -17459,7 +17809,7 @@ function list(value) {
|
|
|
17459
17809
|
function readHealthPolicy(contract) {
|
|
17460
17810
|
const block = contract.health_policy;
|
|
17461
17811
|
if (!block) {
|
|
17462
|
-
return { declared: false, requiredDomains: /* @__PURE__ */ new Set(), deferred: [], warnings: [], skips: [], freshness: [] };
|
|
17812
|
+
return { declared: false, requiredDomains: /* @__PURE__ */ new Set(), deferred: [], warnings: [], skips: [], freshness: [], agentExceptions: [] };
|
|
17463
17813
|
}
|
|
17464
17814
|
return {
|
|
17465
17815
|
declared: true,
|
|
@@ -17467,7 +17817,8 @@ function readHealthPolicy(contract) {
|
|
|
17467
17817
|
deferred: list(block.deferred_capabilities).map((entry, index) => ({ path: `health_policy.deferred_capabilities[${index}]`, entry })),
|
|
17468
17818
|
warnings: list(block.allowed_warnings).map((entry, index) => ({ path: `health_policy.allowed_warnings[${index}]`, entry })),
|
|
17469
17819
|
skips: list(block.allowed_skips).map((entry, index) => ({ path: `health_policy.allowed_skips[${index}]`, entry })),
|
|
17470
|
-
freshness: list(block.freshness).map((entry, index) => ({ path: `health_policy.freshness[${index}]`, entry }))
|
|
17820
|
+
freshness: list(block.freshness).map((entry, index) => ({ path: `health_policy.freshness[${index}]`, entry })),
|
|
17821
|
+
agentExceptions: list(block.agent_exceptions).map((entry, index) => ({ path: `health_policy.agent_exceptions[${index}]`, entry }))
|
|
17471
17822
|
};
|
|
17472
17823
|
}
|
|
17473
17824
|
function evaluateFreshness(field3, isoValue, referenceMs, policy) {
|
|
@@ -17489,6 +17840,8 @@ var SOURCE_EVIDENCE = Object.freeze({
|
|
|
17489
17840
|
"fleet-inventory": "direct",
|
|
17490
17841
|
"fleet-provenance": "direct",
|
|
17491
17842
|
"recipe-audit": "direct",
|
|
17843
|
+
// The scaffold observer reads git objects and role directories itself.
|
|
17844
|
+
"fleet-scaffold": "direct",
|
|
17492
17845
|
"declared-gap": "absent"
|
|
17493
17846
|
});
|
|
17494
17847
|
var UNREAD_STATES = /* @__PURE__ */ new Set(["unobserved", "unsupported", "error"]);
|
|
@@ -17505,9 +17858,10 @@ function resolveJustification(input, policy) {
|
|
|
17505
17858
|
}
|
|
17506
17859
|
}
|
|
17507
17860
|
if (input.exceptionId !== null) {
|
|
17861
|
+
const policyPath = input.exceptionPolicy ?? `classifications.intentionally_unmanaged.entries.${bounded2(input.exceptionId, 128)}`;
|
|
17508
17862
|
return {
|
|
17509
17863
|
kind: "exception",
|
|
17510
|
-
policy:
|
|
17864
|
+
policy: bounded2(policyPath),
|
|
17511
17865
|
reason: bounded2(input.exceptionReason ?? "a managed exception the contract records for exactly these participants"),
|
|
17512
17866
|
owner: null
|
|
17513
17867
|
};
|
|
@@ -17996,18 +18350,27 @@ async function probe(ctx, argv, cwd) {
|
|
|
17996
18350
|
const { outcome, value } = await runBoundedChild(ctx, command, args, { cwd });
|
|
17997
18351
|
return { outcome, value };
|
|
17998
18352
|
}
|
|
18353
|
+
async function probeRaw(ctx, argv, cwd, input) {
|
|
18354
|
+
const [command, ...args] = argv;
|
|
18355
|
+
if (!command) return { outcome: "failed", value: null };
|
|
18356
|
+
const { outcome, bytes } = await runBoundedChild(ctx, command, args, { cwd, input, raw: true });
|
|
18357
|
+
return { outcome, value: outcome === "ok" ? bytes : null };
|
|
18358
|
+
}
|
|
17999
18359
|
function captureSelf(ctx, entry, args, cwd, env2, timeoutMs) {
|
|
18000
18360
|
return runBoundedChild(ctx, process.execPath, [entry, ...args], { cwd, env: env2, timeoutMs, keepStdoutOnFailure: true });
|
|
18001
18361
|
}
|
|
18002
18362
|
function runBoundedChild(ctx, command, args, options = {}) {
|
|
18003
|
-
const { cwd, env: env2, keepStdoutOnFailure = false, timeoutMs } = options;
|
|
18363
|
+
const { cwd, env: env2, keepStdoutOnFailure = false, timeoutMs, input, raw = false } = options;
|
|
18004
18364
|
const budget = Math.min(timeoutMs ?? ctx.probeTimeoutMs, remainingMs(ctx));
|
|
18005
18365
|
return new Promise((settle) => {
|
|
18006
18366
|
let child;
|
|
18007
18367
|
try {
|
|
18008
18368
|
child = spawn3(command, [...args], {
|
|
18009
18369
|
cwd,
|
|
18010
|
-
|
|
18370
|
+
// stdin is `ignore` unless a caller has bytes to feed it: a child that
|
|
18371
|
+
// could wait on a terminal it never gets is a child the timeout has to
|
|
18372
|
+
// kill, and nothing here should need killing to finish.
|
|
18373
|
+
stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "ignore"],
|
|
18011
18374
|
// Its own process GROUP, so `kill` below can reach the whole tree.
|
|
18012
18375
|
// Measured: a shim that is `#!/bin/sh ... sleep 60` puts the sleep under
|
|
18013
18376
|
// the sh. SIGKILLing the sh alone leaves the sleep holding the write end
|
|
@@ -18023,12 +18386,13 @@ function runBoundedChild(ctx, command, args, options = {}) {
|
|
|
18023
18386
|
env: env2 ?? probeEnv()
|
|
18024
18387
|
});
|
|
18025
18388
|
} catch {
|
|
18026
|
-
settle({ outcome: "failed", value: null, code: null, overflow: false });
|
|
18389
|
+
settle({ outcome: "failed", value: null, code: null, overflow: false, bytes: null });
|
|
18027
18390
|
return;
|
|
18028
18391
|
}
|
|
18029
|
-
|
|
18392
|
+
const chunks = [];
|
|
18030
18393
|
let size = 0;
|
|
18031
18394
|
let settled = false;
|
|
18395
|
+
const collected = () => Buffer.concat(chunks);
|
|
18032
18396
|
const finish = (result2) => {
|
|
18033
18397
|
if (settled) return;
|
|
18034
18398
|
settled = true;
|
|
@@ -18038,8 +18402,21 @@ function runBoundedChild(ctx, command, args, options = {}) {
|
|
|
18038
18402
|
child.stdout?.destroy();
|
|
18039
18403
|
} catch {
|
|
18040
18404
|
}
|
|
18405
|
+
try {
|
|
18406
|
+
child.stdin?.destroy();
|
|
18407
|
+
} catch {
|
|
18408
|
+
}
|
|
18041
18409
|
child.unref();
|
|
18042
|
-
|
|
18410
|
+
const bytes = result2.keep ? collected() : null;
|
|
18411
|
+
settle({
|
|
18412
|
+
outcome: result2.outcome,
|
|
18413
|
+
code: result2.code,
|
|
18414
|
+
overflow: result2.overflow,
|
|
18415
|
+
bytes,
|
|
18416
|
+
// The TEXT value keeps its historical shape for every existing caller:
|
|
18417
|
+
// decoded as utf8 and trimmed. A raw caller reads `bytes` instead.
|
|
18418
|
+
value: bytes === null || raw ? null : bytes.toString("utf8").trim()
|
|
18419
|
+
});
|
|
18043
18420
|
};
|
|
18044
18421
|
const kill = () => {
|
|
18045
18422
|
try {
|
|
@@ -18053,31 +18430,40 @@ function runBoundedChild(ctx, command, args, options = {}) {
|
|
|
18053
18430
|
};
|
|
18054
18431
|
const timer = setTimeout(() => {
|
|
18055
18432
|
kill();
|
|
18056
|
-
finish({ outcome: "timeout",
|
|
18433
|
+
finish({ outcome: "timeout", code: null, overflow: false, keep: false });
|
|
18057
18434
|
}, budget);
|
|
18058
18435
|
timer.unref?.();
|
|
18059
18436
|
const onAbort = () => {
|
|
18060
18437
|
kill();
|
|
18061
|
-
finish({ outcome: "cancelled",
|
|
18438
|
+
finish({ outcome: "cancelled", code: null, overflow: false, keep: false });
|
|
18062
18439
|
};
|
|
18063
18440
|
ctx.signal.addEventListener("abort", onAbort, { once: true });
|
|
18064
|
-
child.stdout
|
|
18065
|
-
|
|
18066
|
-
|
|
18441
|
+
const stdout = child.stdout;
|
|
18442
|
+
if (!stdout) {
|
|
18443
|
+
finish({ outcome: "failed", code: null, overflow: false, keep: false });
|
|
18444
|
+
return;
|
|
18445
|
+
}
|
|
18446
|
+
stdout.on("data", (chunk) => {
|
|
18447
|
+
size += chunk.byteLength;
|
|
18067
18448
|
if (size > PROBE_MAX_BYTES) {
|
|
18068
18449
|
kill();
|
|
18069
|
-
finish({ outcome: "failed",
|
|
18450
|
+
finish({ outcome: "failed", code: null, overflow: true, keep: false });
|
|
18070
18451
|
return;
|
|
18071
18452
|
}
|
|
18072
|
-
|
|
18453
|
+
chunks.push(chunk);
|
|
18073
18454
|
});
|
|
18074
|
-
child.
|
|
18455
|
+
if (child.stdin) {
|
|
18456
|
+
child.stdin.on("error", () => {
|
|
18457
|
+
});
|
|
18458
|
+
child.stdin.end(input);
|
|
18459
|
+
}
|
|
18460
|
+
child.on("error", () => finish({ outcome: "failed", code: null, overflow: false, keep: false }));
|
|
18075
18461
|
child.on("close", (code) => {
|
|
18076
18462
|
if (code !== 0) {
|
|
18077
|
-
finish({ outcome: "failed",
|
|
18463
|
+
finish({ outcome: "failed", code, overflow: false, keep: keepStdoutOnFailure });
|
|
18078
18464
|
return;
|
|
18079
18465
|
}
|
|
18080
|
-
finish({ outcome: "ok",
|
|
18466
|
+
finish({ outcome: "ok", code, overflow: false, keep: true });
|
|
18081
18467
|
});
|
|
18082
18468
|
});
|
|
18083
18469
|
}
|
|
@@ -18702,7 +19088,7 @@ function normalizePathValue(value) {
|
|
|
18702
19088
|
}
|
|
18703
19089
|
function conflictGroupId(fieldPath, value) {
|
|
18704
19090
|
const normalized = value.normalize("NFC").trim();
|
|
18705
|
-
return `conflict:${fieldPath}:${
|
|
19091
|
+
return `conflict:${fieldPath}:${createHash9("sha256").update(normalized, "utf8").digest("hex").slice(0, 12)}`;
|
|
18706
19092
|
}
|
|
18707
19093
|
var DIMENSIONS = {
|
|
18708
19094
|
agentId: { key: "agent-id", field: "agents.{agent_id}", kind: "agent" },
|
|
@@ -19169,7 +19555,7 @@ function collectFleetInventory(options = {}) {
|
|
|
19169
19555
|
}
|
|
19170
19556
|
|
|
19171
19557
|
// src/fleet/provenance.ts
|
|
19172
|
-
import { createHash as
|
|
19558
|
+
import { createHash as createHash10 } from "node:crypto";
|
|
19173
19559
|
import { existsSync as existsSync25, lstatSync as lstatSync14, readFileSync as readFileSync24, realpathSync as realpathSync9, statSync as statSync7 } from "node:fs";
|
|
19174
19560
|
import { homedir as homedir13 } from "node:os";
|
|
19175
19561
|
import { dirname as dirname16, isAbsolute as isAbsolute8, join as join33, resolve as resolve19 } from "node:path";
|
|
@@ -19191,7 +19577,6 @@ var SOURCE_TEMPLATE_SUBMODULE = "hermes-agent-template-submodule";
|
|
|
19191
19577
|
var SOURCE_AGENT_REGISTRY = "hermes-agent-registry";
|
|
19192
19578
|
var SOURCE_AGENT_CHECKOUT = "hermes-agent-checkout";
|
|
19193
19579
|
var SOURCE_PROVENANCE_POLICY = "pjangler-fleet-provenance";
|
|
19194
|
-
var SOURCE_ROLE_SCAFFOLD = "hermes-agent-role-scaffold";
|
|
19195
19580
|
var SOURCE_PROFILE_TREE = "hermes-profile-tree";
|
|
19196
19581
|
var SHELL_REFERENCE = /\$\{?[A-Za-z_]/u;
|
|
19197
19582
|
function isRecord7(value) {
|
|
@@ -19338,14 +19723,14 @@ async function probeCheckout(ctx, declared, home) {
|
|
|
19338
19723
|
reason: toplevel.outcome === "timeout" ? "probe-timeout" : toplevel.outcome === "cancelled" ? "probe-cancelled" : "probe-failed"
|
|
19339
19724
|
};
|
|
19340
19725
|
}
|
|
19341
|
-
const
|
|
19726
|
+
const canonical2 = (path) => {
|
|
19342
19727
|
try {
|
|
19343
19728
|
return realpathSync9(path);
|
|
19344
19729
|
} catch {
|
|
19345
19730
|
return resolve19(path);
|
|
19346
19731
|
}
|
|
19347
19732
|
};
|
|
19348
|
-
if (
|
|
19733
|
+
if (canonical2(toplevel.value) !== canonical2(expanded)) {
|
|
19349
19734
|
return { ...base, classification, outcome: "skipped", reason: "not-a-repository-root" };
|
|
19350
19735
|
}
|
|
19351
19736
|
const [remote, head, status] = await Promise.all([
|
|
@@ -19615,23 +20000,7 @@ function addAgentRecordFacts(ctx, subject) {
|
|
|
19615
20000
|
});
|
|
19616
20001
|
}
|
|
19617
20002
|
function addUnsupportedFacts(ctx, subject, profileTemplate) {
|
|
19618
|
-
const { agentId,
|
|
19619
|
-
const scaffoldEvidence = roleDir === null ? absent(SOURCE_ROLE_SCAFFOLD, "missing") : (() => {
|
|
19620
|
-
const dir = expandHome3(roleDir, ctx.home);
|
|
19621
|
-
const view = classifyPath(dir, { directory: true });
|
|
19622
|
-
const manifest = join33(dir, "role.yaml");
|
|
19623
|
-
const state = view.classification === "ok" && existsSync25(manifest) ? "role.yaml present" : `role directory is ${view.classification}`;
|
|
19624
|
-
return present(state, SOURCE_ROLE_SCAFFOLD, { classification: view.classification });
|
|
19625
|
-
})();
|
|
19626
|
-
addFact(ctx, {
|
|
19627
|
-
id: "scaffold.template_ref",
|
|
19628
|
-
scope: "agent",
|
|
19629
|
-
agent_id: agentId,
|
|
19630
|
-
field: "scaffold",
|
|
19631
|
-
desired: absent(SOURCE_ROLE_SCAFFOLD, "unsupported"),
|
|
19632
|
-
observed: scaffoldEvidence,
|
|
19633
|
-
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"
|
|
19634
|
-
});
|
|
20003
|
+
const { agentId, profileName } = subject;
|
|
19635
20004
|
const namedProfile = profileName !== null && /^[A-Za-z0-9._-]+$/u.test(profileName) && profileName !== "." && profileName !== ".." ? profileName : null;
|
|
19636
20005
|
const profilePath = namedProfile && profileTemplate ? profileTemplate.replaceAll("{profile_name}", namedProfile) : null;
|
|
19637
20006
|
const generated = profilePath ? join33(profilePath, "config.yaml") : null;
|
|
@@ -19651,7 +20020,7 @@ function profileDigest(path) {
|
|
|
19651
20020
|
if (stat.isSymbolicLink()) return present("symlink", SOURCE_PROFILE_TREE, { classification: "symlink" });
|
|
19652
20021
|
if (!stat.isFile()) return present(`not-a-file`, SOURCE_PROFILE_TREE, { classification: "not-a-directory" });
|
|
19653
20022
|
if (stat.size > PROFILE_CONFIG_MAX_BYTES) return absent(SOURCE_PROFILE_TREE, "unobserved", { classification: "unreadable" });
|
|
19654
|
-
return present(`sha256:${
|
|
20023
|
+
return present(`sha256:${createHash10("sha256").update(readFileSync24(path)).digest("hex")}`, SOURCE_PROFILE_TREE, { classification: "ok" });
|
|
19655
20024
|
} catch (error) {
|
|
19656
20025
|
const code = error.code;
|
|
19657
20026
|
return absent(SOURCE_PROFILE_TREE, "missing", { classification: code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable" });
|
|
@@ -19809,9 +20178,9 @@ async function collectFleetProvenance(options) {
|
|
|
19809
20178
|
remainingMs(runContext);
|
|
19810
20179
|
const observations = await mapBounded(targets, PROBE_CONCURRENCY, async ([, declared]) => probeCheckout(runContext, declared, home));
|
|
19811
20180
|
const byDeclared = /* @__PURE__ */ new Map();
|
|
19812
|
-
targets.forEach(([
|
|
20181
|
+
targets.forEach(([canonical2, declared], index) => {
|
|
19813
20182
|
const observation3 = observations[index];
|
|
19814
|
-
byDeclared.set(
|
|
20183
|
+
byDeclared.set(canonical2, observation3);
|
|
19815
20184
|
addProbe(ctx, {
|
|
19816
20185
|
id: `checkout:${observation3.target}`,
|
|
19817
20186
|
kind: "checkout",
|
|
@@ -19929,10 +20298,545 @@ async function collectFleetProvenance(options) {
|
|
|
19929
20298
|
}
|
|
19930
20299
|
|
|
19931
20300
|
// src/fleet/status.ts
|
|
19932
|
-
import { createHash as
|
|
19933
|
-
import { existsSync as existsSync26, readFileSync as
|
|
20301
|
+
import { createHash as createHash11 } from "node:crypto";
|
|
20302
|
+
import { existsSync as existsSync26, readFileSync as readFileSync26 } from "node:fs";
|
|
19934
20303
|
import { homedir as homedir14 } from "node:os";
|
|
19935
|
-
import { isAbsolute as
|
|
20304
|
+
import { isAbsolute as isAbsolute10, join as join35, resolve as resolve21 } from "node:path";
|
|
20305
|
+
|
|
20306
|
+
// src/fleet/scaffold.ts
|
|
20307
|
+
import { lstatSync as lstatSync15, readFileSync as readFileSync25, readlinkSync as readlinkSync4, realpathSync as realpathSync10 } from "node:fs";
|
|
20308
|
+
import { dirname as dirname17, isAbsolute as isAbsolute9, join as join34, relative as relative13, resolve as resolve20, sep as sep7 } from "node:path";
|
|
20309
|
+
var SCAFFOLD_MAX_ASSET_BYTES = 4 * 1024 * 1024;
|
|
20310
|
+
var SCAFFOLD_MAX_ROLE_ENTRIES = 5e3;
|
|
20311
|
+
var SCAFFOLD_LINEAGE_DEPTH = 12;
|
|
20312
|
+
var SCAFFOLD_PROBE_KIND = "scaffold";
|
|
20313
|
+
function gitArgv2(path, args) {
|
|
20314
|
+
return ["git", "--no-optional-locks", "-C", path, ...args];
|
|
20315
|
+
}
|
|
20316
|
+
function canonical(path) {
|
|
20317
|
+
try {
|
|
20318
|
+
return realpathSync10(path);
|
|
20319
|
+
} catch {
|
|
20320
|
+
return resolve20(path);
|
|
20321
|
+
}
|
|
20322
|
+
}
|
|
20323
|
+
function within(root, candidate) {
|
|
20324
|
+
return candidate === root || candidate.startsWith(`${root}${sep7}`);
|
|
20325
|
+
}
|
|
20326
|
+
function parseBatch(output, keys) {
|
|
20327
|
+
const out = /* @__PURE__ */ new Map();
|
|
20328
|
+
let offset = 0;
|
|
20329
|
+
for (const key of keys) {
|
|
20330
|
+
const newline = output.indexOf(10, offset);
|
|
20331
|
+
if (newline === -1) {
|
|
20332
|
+
out.set(key, null);
|
|
20333
|
+
continue;
|
|
20334
|
+
}
|
|
20335
|
+
const header = output.subarray(offset, newline).toString("utf8");
|
|
20336
|
+
offset = newline + 1;
|
|
20337
|
+
const parts = header.split(" ");
|
|
20338
|
+
if (parts.length < 3 || parts[1] === "missing" || parts[1] === "ambiguous") {
|
|
20339
|
+
out.set(key, null);
|
|
20340
|
+
continue;
|
|
20341
|
+
}
|
|
20342
|
+
const size = Number(parts[2]);
|
|
20343
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
20344
|
+
out.set(key, null);
|
|
20345
|
+
continue;
|
|
20346
|
+
}
|
|
20347
|
+
out.set(key, output.subarray(offset, offset + size));
|
|
20348
|
+
offset += size + 1;
|
|
20349
|
+
}
|
|
20350
|
+
return out;
|
|
20351
|
+
}
|
|
20352
|
+
async function inspectSource(ctx) {
|
|
20353
|
+
const { manifest } = ctx;
|
|
20354
|
+
const submodule = join34(ctx.pjanglerRoot, manifest.template_submodule);
|
|
20355
|
+
const target = ctx.shown(submodule);
|
|
20356
|
+
const empty = (integrity, detail, gitlink2, outcome = "failed") => ({
|
|
20357
|
+
record: { gitlink: gitlink2, integrity, detail, assets: 0 },
|
|
20358
|
+
submodule,
|
|
20359
|
+
assets: [],
|
|
20360
|
+
renderSources: /* @__PURE__ */ new Map(),
|
|
20361
|
+
probe: { id: `${SCAFFOLD_PROBE_KIND}:${target}`, kind: SCAFFOLD_PROBE_KIND, target, outcome, reason: integrity }
|
|
20362
|
+
});
|
|
20363
|
+
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);
|
|
20364
|
+
const committed = await probe(ctx.run, gitArgv2(ctx.pjanglerRoot, ["ls-tree", "HEAD", "--", manifest.template_submodule]));
|
|
20365
|
+
throwIfCancelled(ctx.run);
|
|
20366
|
+
if (committed.outcome === "timeout") return unobserved("ls-tree HEAD", "timeout", null);
|
|
20367
|
+
const head = /^160000\s+commit\s+([0-9a-f]{40})\t/u.exec(committed.value ?? "");
|
|
20368
|
+
if (!head) {
|
|
20369
|
+
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);
|
|
20370
|
+
}
|
|
20371
|
+
const gitlink = head[1];
|
|
20372
|
+
const staged = await probe(ctx.run, gitArgv2(ctx.pjanglerRoot, ["ls-files", "--stage", "--", manifest.template_submodule]));
|
|
20373
|
+
throwIfCancelled(ctx.run);
|
|
20374
|
+
if (staged.outcome === "timeout") return unobserved("ls-files --stage", "timeout", gitlink);
|
|
20375
|
+
const index = /^160000\s+([0-9a-f]{40})\s/u.exec(staged.value ?? "");
|
|
20376
|
+
if (!index || index[1] !== gitlink) {
|
|
20377
|
+
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);
|
|
20378
|
+
}
|
|
20379
|
+
const toplevel = await probe(ctx.run, gitArgv2(submodule, ["rev-parse", "--show-toplevel"]));
|
|
20380
|
+
throwIfCancelled(ctx.run);
|
|
20381
|
+
if (toplevel.outcome === "timeout") return unobserved("rev-parse --show-toplevel", "timeout", gitlink);
|
|
20382
|
+
if (toplevel.outcome !== "ok" || !toplevel.value || canonical(toplevel.value) !== canonical(submodule)) {
|
|
20383
|
+
return empty("source-uninitialized", `${manifest.template_submodule} is not an initialized submodule checkout; run git submodule update to make the pinned template readable`, gitlink);
|
|
20384
|
+
}
|
|
20385
|
+
const object = await probe(ctx.run, gitArgv2(submodule, ["cat-file", "-e", `${gitlink}^{commit}`]));
|
|
20386
|
+
throwIfCancelled(ctx.run);
|
|
20387
|
+
if (object.outcome === "timeout") return unobserved("cat-file -e", "timeout", gitlink);
|
|
20388
|
+
if (object.outcome !== "ok") {
|
|
20389
|
+
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);
|
|
20390
|
+
}
|
|
20391
|
+
const worktreeHead = await probe(ctx.run, gitArgv2(submodule, ["rev-parse", "HEAD"]));
|
|
20392
|
+
throwIfCancelled(ctx.run);
|
|
20393
|
+
if (worktreeHead.outcome === "timeout") return unobserved("rev-parse HEAD", "timeout", gitlink);
|
|
20394
|
+
if (worktreeHead.outcome !== "ok" || worktreeHead.value !== gitlink) {
|
|
20395
|
+
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);
|
|
20396
|
+
}
|
|
20397
|
+
const dirty = await probe(ctx.run, gitArgv2(submodule, ["status", "--porcelain", "--untracked-files=no"]));
|
|
20398
|
+
throwIfCancelled(ctx.run);
|
|
20399
|
+
if (dirty.outcome === "timeout") return unobserved("status --porcelain", "timeout", gitlink);
|
|
20400
|
+
if (dirty.outcome !== "ok") return unobserved("status --porcelain", "failed", gitlink);
|
|
20401
|
+
if ((dirty.value ?? "") !== "") {
|
|
20402
|
+
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);
|
|
20403
|
+
}
|
|
20404
|
+
const tree = await probeRaw(ctx.run, gitArgv2(submodule, ["ls-tree", "-r", "-z", gitlink, "--", manifest.template_subdirectory]));
|
|
20405
|
+
throwIfCancelled(ctx.run);
|
|
20406
|
+
if (tree.outcome === "timeout") return unobserved("ls-tree -r", "timeout", gitlink);
|
|
20407
|
+
if (tree.outcome !== "ok" || tree.value === null) return unobserved("ls-tree -r", "failed", gitlink);
|
|
20408
|
+
const prefix = `${manifest.template_subdirectory.replace(/\/+$/u, "")}/`;
|
|
20409
|
+
const assets = [];
|
|
20410
|
+
const entries = tree.value.toString("utf8").split("\0").filter((entry) => entry !== "");
|
|
20411
|
+
for (const entry of entries) {
|
|
20412
|
+
const match = /^(\d{6}) (\w+) ([0-9a-f]{40})\t(.+)$/su.exec(entry);
|
|
20413
|
+
if (!match) continue;
|
|
20414
|
+
const [, mode, type, sha, templatePath] = match;
|
|
20415
|
+
if (!templatePath.startsWith(prefix)) continue;
|
|
20416
|
+
const raw = templatePath.slice(prefix.length);
|
|
20417
|
+
if (type !== "blob") {
|
|
20418
|
+
return empty("source-contaminated", `the tree at ${gitlink.slice(0, 12)} carries a ${type} at ${raw}; a template renders blobs only`, gitlink);
|
|
20419
|
+
}
|
|
20420
|
+
const excluded = matchesExcluded(raw, manifest.excluded_patterns);
|
|
20421
|
+
if (excluded !== null) {
|
|
20422
|
+
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);
|
|
20423
|
+
}
|
|
20424
|
+
const rendered = raw.endsWith(manifest.render_suffix);
|
|
20425
|
+
const rolePath = rendered ? raw.slice(0, -manifest.render_suffix.length) : raw;
|
|
20426
|
+
if (rolePath === "" || rolePath.endsWith("/")) {
|
|
20427
|
+
return empty("source-contaminated", `the tree at ${gitlink.slice(0, 12)} renders an unnamed asset from ${raw}`, gitlink);
|
|
20428
|
+
}
|
|
20429
|
+
const group = groupFor(rolePath, manifest.groups);
|
|
20430
|
+
if (group === null) {
|
|
20431
|
+
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);
|
|
20432
|
+
}
|
|
20433
|
+
assets.push({
|
|
20434
|
+
templatePath,
|
|
20435
|
+
rolePath,
|
|
20436
|
+
sha,
|
|
20437
|
+
mode,
|
|
20438
|
+
type: mode === "120000" ? "symlink" : "file",
|
|
20439
|
+
executable: mode === "100755",
|
|
20440
|
+
rendered,
|
|
20441
|
+
group,
|
|
20442
|
+
presenceOnly: manifest.presence_only.some((item) => item.path === rolePath)
|
|
20443
|
+
});
|
|
20444
|
+
}
|
|
20445
|
+
if (assets.length === 0) {
|
|
20446
|
+
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);
|
|
20447
|
+
}
|
|
20448
|
+
assets.sort((a, b) => a.rolePath < b.rolePath ? -1 : a.rolePath > b.rolePath ? 1 : 0);
|
|
20449
|
+
const renderSources = /* @__PURE__ */ new Map();
|
|
20450
|
+
const wanted = assets.filter((asset) => asset.rendered && !asset.presenceOnly && asset.type === "file");
|
|
20451
|
+
if (wanted.length > 0) {
|
|
20452
|
+
const ids = [...new Set(wanted.map((asset) => asset.sha))];
|
|
20453
|
+
const batch = await probeRaw(ctx.run, gitArgv2(submodule, ["cat-file", "--batch"]), void 0, `${ids.join("\n")}
|
|
20454
|
+
`);
|
|
20455
|
+
throwIfCancelled(ctx.run);
|
|
20456
|
+
if (batch.outcome === "timeout") return unobserved("cat-file --batch", "timeout", gitlink);
|
|
20457
|
+
if (batch.outcome !== "ok" || batch.value === null) return unobserved("cat-file --batch", "failed", gitlink);
|
|
20458
|
+
const blobs = parseBatch(batch.value, ids);
|
|
20459
|
+
for (const asset of wanted) {
|
|
20460
|
+
const bytes = blobs.get(asset.sha) ?? null;
|
|
20461
|
+
if (bytes === null) {
|
|
20462
|
+
return empty("source-missing-object", `blob ${asset.sha.slice(0, 12)} for ${asset.rolePath} is absent from the submodule's object database`, gitlink);
|
|
20463
|
+
}
|
|
20464
|
+
const text2 = bytes.toString("utf8");
|
|
20465
|
+
renderSources.set(asset.templatePath, Buffer.from(text2, "utf8").equals(bytes) ? { text: text2 } : { unsupported: "render-unsupported: template source is not UTF-8 text" });
|
|
20466
|
+
}
|
|
20467
|
+
}
|
|
20468
|
+
return {
|
|
20469
|
+
record: { gitlink, integrity: "ok", detail: `${assets.length} asset(s) rendered by the template at the committed gitlink ${gitlink.slice(0, 12)}`, assets: assets.length },
|
|
20470
|
+
submodule,
|
|
20471
|
+
assets,
|
|
20472
|
+
renderSources,
|
|
20473
|
+
probe: { id: `${SCAFFOLD_PROBE_KIND}:${target}`, kind: SCAFFOLD_PROBE_KIND, target, outcome: "ok", reason: null }
|
|
20474
|
+
};
|
|
20475
|
+
}
|
|
20476
|
+
function desiredFor(source, input) {
|
|
20477
|
+
return source.assets.map((asset) => {
|
|
20478
|
+
const base = {
|
|
20479
|
+
path: asset.rolePath,
|
|
20480
|
+
type: asset.type,
|
|
20481
|
+
executable: asset.executable,
|
|
20482
|
+
blobId: asset.sha,
|
|
20483
|
+
presenceOnly: asset.presenceOnly,
|
|
20484
|
+
incomplete: null
|
|
20485
|
+
};
|
|
20486
|
+
if (asset.presenceOnly) return { ...base, blobId: null };
|
|
20487
|
+
if (!asset.rendered || asset.type !== "file") return base;
|
|
20488
|
+
const rendered = source.renderSources.get(asset.templatePath);
|
|
20489
|
+
if (!rendered) return { ...base, blobId: null, incomplete: { reason: "render-unsupported", detail: "render-unsupported: source not read" } };
|
|
20490
|
+
if ("unsupported" in rendered) return { ...base, blobId: null, incomplete: { reason: "render-unsupported", detail: rendered.unsupported } };
|
|
20491
|
+
const result2 = renderTemplate(rendered.text, input.inputs);
|
|
20492
|
+
if (!result2.ok) return { ...base, blobId: null, incomplete: { reason: result2.reason, detail: result2.detail } };
|
|
20493
|
+
return { ...base, blobId: blobId(Buffer.from(result2.text, "utf8")) };
|
|
20494
|
+
});
|
|
20495
|
+
}
|
|
20496
|
+
function absent2() {
|
|
20497
|
+
return { present: false, type: null, executable: false, blobId: null, unsafeSymlink: false, unreadable: null, wip: false };
|
|
20498
|
+
}
|
|
20499
|
+
async function scanAgent(ctx, source, input) {
|
|
20500
|
+
const scan = {
|
|
20501
|
+
input,
|
|
20502
|
+
roleDir: null,
|
|
20503
|
+
roleDirSource: input.roleDir !== null ? "registry" : "default",
|
|
20504
|
+
error: null,
|
|
20505
|
+
top: null,
|
|
20506
|
+
observed: /* @__PURE__ */ new Map(),
|
|
20507
|
+
tracked: [],
|
|
20508
|
+
wipPaths: /* @__PURE__ */ new Set(),
|
|
20509
|
+
wipPreserved: 0,
|
|
20510
|
+
ignoredEntries: 0,
|
|
20511
|
+
probe: { id: "", kind: SCAFFOLD_PROBE_KIND, target: "", outcome: "skipped", reason: null }
|
|
20512
|
+
};
|
|
20513
|
+
const fail = (error, outcome = "skipped") => {
|
|
20514
|
+
scan.error = error;
|
|
20515
|
+
scan.probe = { ...scan.probe, outcome, reason: error };
|
|
20516
|
+
return scan;
|
|
20517
|
+
};
|
|
20518
|
+
const setTarget = (path) => {
|
|
20519
|
+
const shown = ctx.shown(path);
|
|
20520
|
+
scan.probe = { ...scan.probe, id: `${SCAFFOLD_PROBE_KIND}:${shown}`, target: shown };
|
|
20521
|
+
};
|
|
20522
|
+
if (input.projectPath === null) {
|
|
20523
|
+
setTarget(input.agentId);
|
|
20524
|
+
return fail("repository-unreadable:no-project-path");
|
|
20525
|
+
}
|
|
20526
|
+
setTarget(input.projectPath);
|
|
20527
|
+
const toplevel = await probe(ctx.run, gitArgv2(input.projectPath, ["rev-parse", "--show-toplevel"]));
|
|
20528
|
+
throwIfCancelled(ctx.run);
|
|
20529
|
+
if (toplevel.outcome === "timeout") return fail("probe-timeout", "timeout");
|
|
20530
|
+
if (toplevel.outcome !== "ok" || !toplevel.value) return fail("repository-unreadable:not-a-repository", "failed");
|
|
20531
|
+
const top = canonical(toplevel.value);
|
|
20532
|
+
if (top !== canonical(input.projectPath)) return fail("repository-unreadable:not-a-repository-root");
|
|
20533
|
+
scan.top = top;
|
|
20534
|
+
let roleDir;
|
|
20535
|
+
if (input.roleDir !== null) {
|
|
20536
|
+
roleDir = isAbsolute9(input.roleDir) ? resolve20(input.roleDir) : resolve20(input.projectPath, input.roleDir);
|
|
20537
|
+
} else if (input.role !== null && input.role !== "" && input.role !== "." && input.role !== ".." && !input.role.includes("/")) {
|
|
20538
|
+
roleDir = join34(input.projectPath, "agents", "hermes", input.role);
|
|
20539
|
+
} else {
|
|
20540
|
+
return fail("role-unresolved");
|
|
20541
|
+
}
|
|
20542
|
+
scan.roleDir = roleDir;
|
|
20543
|
+
setTarget(roleDir);
|
|
20544
|
+
let roleRel;
|
|
20545
|
+
let roleReal;
|
|
20546
|
+
try {
|
|
20547
|
+
const stat = lstatSync15(roleDir);
|
|
20548
|
+
if (stat.isSymbolicLink()) return fail("role-dir-symlink");
|
|
20549
|
+
if (!stat.isDirectory()) return fail("role-dir-not-a-directory");
|
|
20550
|
+
roleReal = canonical(roleDir);
|
|
20551
|
+
if (roleReal === top) return fail("role-dir-is-repository-root");
|
|
20552
|
+
if (!within(top, roleReal)) return fail("role-dir-outside-project");
|
|
20553
|
+
roleRel = relative13(top, roleReal);
|
|
20554
|
+
} catch {
|
|
20555
|
+
const lexical = resolve20(roleDir);
|
|
20556
|
+
const project = resolve20(input.projectPath);
|
|
20557
|
+
if (lexical === project || lexical === top) return fail("role-dir-is-repository-root");
|
|
20558
|
+
if (within(project, lexical)) roleRel = relative13(project, lexical);
|
|
20559
|
+
else if (within(top, lexical)) roleRel = relative13(top, lexical);
|
|
20560
|
+
else return fail("role-dir-outside-project");
|
|
20561
|
+
roleReal = lexical;
|
|
20562
|
+
}
|
|
20563
|
+
const rolePosix = roleRel.split(sep7).join("/");
|
|
20564
|
+
const pathspec = `:(literal)${rolePosix}`;
|
|
20565
|
+
const tracked = await probeRaw(ctx.run, gitArgv2(top, ["ls-files", "-z", "--", pathspec]));
|
|
20566
|
+
throwIfCancelled(ctx.run);
|
|
20567
|
+
if (tracked.outcome === "timeout") return fail("probe-timeout", "timeout");
|
|
20568
|
+
if (tracked.outcome !== "ok" || tracked.value === null) return fail("repository-unreadable:probe-failed", "failed");
|
|
20569
|
+
const status = await probeRaw(ctx.run, gitArgv2(top, ["status", "--porcelain=v1", "-z", "--ignored=matching", "--untracked-files=all", "--", pathspec]));
|
|
20570
|
+
throwIfCancelled(ctx.run);
|
|
20571
|
+
if (status.outcome === "timeout") return fail("probe-timeout", "timeout");
|
|
20572
|
+
if (status.outcome !== "ok" || status.value === null) return fail("repository-unreadable:probe-failed", "failed");
|
|
20573
|
+
const toRole = (path) => path.startsWith(`${rolePosix}/`) ? path.slice(rolePosix.length + 1) : null;
|
|
20574
|
+
const trackedEntries = tracked.value.toString("utf8").split("\0").filter((entry) => entry !== "");
|
|
20575
|
+
const statusEntries = status.value.toString("utf8").split("\0");
|
|
20576
|
+
if (trackedEntries.length + statusEntries.length > SCAFFOLD_MAX_ROLE_ENTRIES) return fail("role-entries-exceeded");
|
|
20577
|
+
for (const entry of trackedEntries) {
|
|
20578
|
+
const rolePath = toRole(entry);
|
|
20579
|
+
if (rolePath !== null) scan.tracked.push(rolePath);
|
|
20580
|
+
}
|
|
20581
|
+
scan.tracked.sort();
|
|
20582
|
+
for (let index = 0; index < statusEntries.length; index += 1) {
|
|
20583
|
+
const entry = statusEntries[index];
|
|
20584
|
+
if (entry.length < 4) continue;
|
|
20585
|
+
const xy = entry.slice(0, 2);
|
|
20586
|
+
const path = entry.slice(3);
|
|
20587
|
+
if (xy[0] === "R" || xy[0] === "C") index += 1;
|
|
20588
|
+
const rolePath = toRole(path.replace(/\/$/u, ""));
|
|
20589
|
+
if (rolePath === null && !path.startsWith(`${rolePosix}/`)) continue;
|
|
20590
|
+
if (xy === "!!") {
|
|
20591
|
+
scan.ignoredEntries += 1;
|
|
20592
|
+
continue;
|
|
20593
|
+
}
|
|
20594
|
+
scan.wipPreserved += 1;
|
|
20595
|
+
if (xy !== "??" && rolePath !== null) scan.wipPaths.add(rolePath);
|
|
20596
|
+
}
|
|
20597
|
+
for (const asset of source.assets) {
|
|
20598
|
+
const full = join34(roleReal, ...asset.rolePath.split("/"));
|
|
20599
|
+
const seen = absent2();
|
|
20600
|
+
seen.wip = scan.wipPaths.has(asset.rolePath);
|
|
20601
|
+
try {
|
|
20602
|
+
const stat = lstatSync15(full);
|
|
20603
|
+
seen.present = true;
|
|
20604
|
+
if (stat.isSymbolicLink()) {
|
|
20605
|
+
seen.type = "symlink";
|
|
20606
|
+
const link = readlinkSync4(full);
|
|
20607
|
+
let resolvedTarget = null;
|
|
20608
|
+
try {
|
|
20609
|
+
resolvedTarget = realpathSync10(resolve20(dirname17(full), link));
|
|
20610
|
+
} catch {
|
|
20611
|
+
resolvedTarget = null;
|
|
20612
|
+
}
|
|
20613
|
+
seen.unsafeSymlink = isAbsolute9(link) || resolvedTarget !== null && !within(top, resolvedTarget);
|
|
20614
|
+
seen.blobId = blobId(Buffer.from(link, "utf8"));
|
|
20615
|
+
} else if (stat.isDirectory()) {
|
|
20616
|
+
seen.type = "directory";
|
|
20617
|
+
} else if (stat.isFile()) {
|
|
20618
|
+
seen.type = "file";
|
|
20619
|
+
seen.executable = (stat.mode & 73) !== 0;
|
|
20620
|
+
if (stat.size > SCAFFOLD_MAX_ASSET_BYTES) seen.unreadable = "too-large";
|
|
20621
|
+
else {
|
|
20622
|
+
try {
|
|
20623
|
+
seen.blobId = blobId(readFileSync25(full));
|
|
20624
|
+
} catch {
|
|
20625
|
+
seen.unreadable = "unreadable";
|
|
20626
|
+
}
|
|
20627
|
+
}
|
|
20628
|
+
} else {
|
|
20629
|
+
seen.type = "other";
|
|
20630
|
+
}
|
|
20631
|
+
} catch {
|
|
20632
|
+
}
|
|
20633
|
+
scan.observed.set(asset.rolePath, seen);
|
|
20634
|
+
}
|
|
20635
|
+
scan.probe = { ...scan.probe, outcome: "ok", reason: null };
|
|
20636
|
+
return scan;
|
|
20637
|
+
}
|
|
20638
|
+
async function resolveLineage(ctx, source, scans, desiredByAgent) {
|
|
20639
|
+
const lineage = { verbatim: /* @__PURE__ */ new Set(), rendered: /* @__PURE__ */ new Map(), unobserved: false };
|
|
20640
|
+
const verbatimIds = /* @__PURE__ */ new Set();
|
|
20641
|
+
const renderedNeeded = /* @__PURE__ */ new Map();
|
|
20642
|
+
const assetByPath = new Map(source.assets.map((asset) => [asset.rolePath, asset]));
|
|
20643
|
+
scans.forEach((scan, index) => {
|
|
20644
|
+
if (scan.error !== null) return;
|
|
20645
|
+
for (const desired of desiredByAgent[index] ?? []) {
|
|
20646
|
+
if (desired.presenceOnly || desired.blobId === null || desired.type !== "file") continue;
|
|
20647
|
+
const seen = scan.observed.get(desired.path);
|
|
20648
|
+
if (!seen || !seen.present || seen.type !== "file" || seen.blobId === null || seen.blobId === desired.blobId) continue;
|
|
20649
|
+
const asset = assetByPath.get(desired.path);
|
|
20650
|
+
if (!asset) continue;
|
|
20651
|
+
if (asset.rendered) {
|
|
20652
|
+
const set = renderedNeeded.get(asset.templatePath) ?? /* @__PURE__ */ new Set();
|
|
20653
|
+
set.add(index);
|
|
20654
|
+
renderedNeeded.set(asset.templatePath, set);
|
|
20655
|
+
} else {
|
|
20656
|
+
verbatimIds.add(seen.blobId);
|
|
20657
|
+
}
|
|
20658
|
+
}
|
|
20659
|
+
});
|
|
20660
|
+
if (verbatimIds.size > 0) {
|
|
20661
|
+
const ids = [...verbatimIds].sort();
|
|
20662
|
+
const check = await probeRaw(ctx.run, gitArgv2(source.submodule, ["cat-file", "--batch-check"]), void 0, `${ids.join("\n")}
|
|
20663
|
+
`);
|
|
20664
|
+
throwIfCancelled(ctx.run);
|
|
20665
|
+
if (check.outcome !== "ok" || check.value === null) lineage.unobserved = true;
|
|
20666
|
+
else {
|
|
20667
|
+
for (const line of check.value.toString("utf8").split("\n")) {
|
|
20668
|
+
const match = /^([0-9a-f]{40}) (\w+)/u.exec(line);
|
|
20669
|
+
if (match && match[2] !== "missing") lineage.verbatim.add(match[1]);
|
|
20670
|
+
}
|
|
20671
|
+
}
|
|
20672
|
+
}
|
|
20673
|
+
if (renderedNeeded.size > 0) {
|
|
20674
|
+
const gitlink = source.record.gitlink;
|
|
20675
|
+
const keys = [];
|
|
20676
|
+
const versions = /* @__PURE__ */ new Map();
|
|
20677
|
+
for (const templatePath of [...renderedNeeded.keys()].sort()) {
|
|
20678
|
+
const log3 = await probe(ctx.run, gitArgv2(source.submodule, ["log", "--format=%H", "-n", String(SCAFFOLD_LINEAGE_DEPTH), gitlink, "--", templatePath]));
|
|
20679
|
+
throwIfCancelled(ctx.run);
|
|
20680
|
+
if (log3.outcome !== "ok") {
|
|
20681
|
+
lineage.unobserved = true;
|
|
20682
|
+
continue;
|
|
20683
|
+
}
|
|
20684
|
+
const commits = (log3.value ?? "").split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/u.test(line));
|
|
20685
|
+
const perPath = commits.map((commit) => `${commit}:${templatePath}`);
|
|
20686
|
+
versions.set(templatePath, perPath);
|
|
20687
|
+
keys.push(...perPath);
|
|
20688
|
+
}
|
|
20689
|
+
if (keys.length > 0) {
|
|
20690
|
+
const batch = await probeRaw(ctx.run, gitArgv2(source.submodule, ["cat-file", "--batch"]), void 0, `${keys.join("\n")}
|
|
20691
|
+
`);
|
|
20692
|
+
throwIfCancelled(ctx.run);
|
|
20693
|
+
if (batch.outcome !== "ok" || batch.value === null) lineage.unobserved = true;
|
|
20694
|
+
else {
|
|
20695
|
+
const blobs = parseBatch(batch.value, keys);
|
|
20696
|
+
for (const [templatePath, agentIndexes] of renderedNeeded) {
|
|
20697
|
+
const asset = source.assets.find((item) => item.templatePath === templatePath);
|
|
20698
|
+
if (!asset) continue;
|
|
20699
|
+
const sources = (versions.get(templatePath) ?? []).map((key) => blobs.get(key) ?? null).filter((bytes) => bytes !== null).map((bytes) => bytes.toString("utf8"));
|
|
20700
|
+
for (const index of agentIndexes) {
|
|
20701
|
+
const scan = scans[index];
|
|
20702
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
20703
|
+
for (const text2 of sources) {
|
|
20704
|
+
const result2 = renderTemplate(text2, scan.input.inputs);
|
|
20705
|
+
if (result2.ok) rendered.add(blobId(Buffer.from(result2.text, "utf8")));
|
|
20706
|
+
}
|
|
20707
|
+
const perAgent = lineage.rendered.get(index) ?? /* @__PURE__ */ new Map();
|
|
20708
|
+
perAgent.set(asset.rolePath, rendered);
|
|
20709
|
+
lineage.rendered.set(index, perAgent);
|
|
20710
|
+
}
|
|
20711
|
+
}
|
|
20712
|
+
}
|
|
20713
|
+
}
|
|
20714
|
+
}
|
|
20715
|
+
return lineage;
|
|
20716
|
+
}
|
|
20717
|
+
function erroredGroups(manifest, assets, detail) {
|
|
20718
|
+
return Object.keys(manifest.groups).sort().map((group) => {
|
|
20719
|
+
const rolePath = manifest.groups[group];
|
|
20720
|
+
const owned = assets.filter((asset) => asset.group === group).length;
|
|
20721
|
+
return {
|
|
20722
|
+
group,
|
|
20723
|
+
role_path: rolePath,
|
|
20724
|
+
state: "error",
|
|
20725
|
+
owned,
|
|
20726
|
+
matching: 0,
|
|
20727
|
+
drifted: 0,
|
|
20728
|
+
incomplete: owned,
|
|
20729
|
+
unexpected: 0,
|
|
20730
|
+
items: [{ path: rolePath, kind: "incomplete", desired: null, observed: null, detail, wip: false }]
|
|
20731
|
+
};
|
|
20732
|
+
});
|
|
20733
|
+
}
|
|
20734
|
+
function emptyAgentResult(input, groups, error, roleDir, roleDirSource) {
|
|
20735
|
+
const owned = groups.reduce((total, group) => total + group.owned, 0);
|
|
20736
|
+
return {
|
|
20737
|
+
agentId: input.agentId,
|
|
20738
|
+
roleDir,
|
|
20739
|
+
roleDirSource,
|
|
20740
|
+
error,
|
|
20741
|
+
groups,
|
|
20742
|
+
assets: { owned, compared: 0, matching: 0, drifted: 0, incomplete: owned, unexpected_owned: 0 },
|
|
20743
|
+
wipOverlap: [],
|
|
20744
|
+
wipPreserved: 0,
|
|
20745
|
+
foreignTracked: 0,
|
|
20746
|
+
ignoredEntries: 0
|
|
20747
|
+
};
|
|
20748
|
+
}
|
|
20749
|
+
async function collectScaffoldParity(ctx) {
|
|
20750
|
+
throwIfCancelled(ctx.run);
|
|
20751
|
+
const source = await inspectSource(ctx);
|
|
20752
|
+
const probes = [source.probe];
|
|
20753
|
+
const manifest = ctx.manifest;
|
|
20754
|
+
if (source.record.integrity !== "ok") {
|
|
20755
|
+
const detail = `source:${source.record.integrity}`;
|
|
20756
|
+
const agents2 = ctx.agents.map((input) => {
|
|
20757
|
+
const roleDir = input.roleDir !== null ? isAbsolute9(input.roleDir) ? resolve20(input.roleDir) : input.projectPath === null ? null : resolve20(input.projectPath, input.roleDir) : input.projectPath !== null && input.role ? join34(input.projectPath, "agents", "hermes", input.role) : null;
|
|
20758
|
+
const target = ctx.shown(roleDir ?? input.projectPath ?? input.agentId);
|
|
20759
|
+
probes.push({ id: `${SCAFFOLD_PROBE_KIND}:${target}`, kind: SCAFFOLD_PROBE_KIND, target, outcome: "skipped", reason: "source-unreadable" });
|
|
20760
|
+
return emptyAgentResult(input, erroredGroups(manifest, [], detail), detail, roleDir, input.roleDir !== null ? "registry" : "default");
|
|
20761
|
+
});
|
|
20762
|
+
return { source: source.record, agents: agents2, probes };
|
|
20763
|
+
}
|
|
20764
|
+
const scans = await mapBounded(ctx.agents, FLEET_STATUS_SCAFFOLD_CONCURRENCY, (input) => scanAgent(ctx, source, input));
|
|
20765
|
+
const desiredByAgent = ctx.agents.map((input) => desiredFor(source, input));
|
|
20766
|
+
const lineage = await resolveLineage(ctx, source, scans, desiredByAgent);
|
|
20767
|
+
const ownedPaths = new Set(source.assets.map((asset) => asset.rolePath));
|
|
20768
|
+
const agents = scans.map((scan, index) => {
|
|
20769
|
+
probes.push(scan.probe);
|
|
20770
|
+
if (scan.error !== null) {
|
|
20771
|
+
return emptyAgentResult(scan.input, erroredGroups(manifest, source.assets, scan.error), scan.error, scan.roleDir, scan.roleDirSource);
|
|
20772
|
+
}
|
|
20773
|
+
const desired = desiredByAgent[index] ?? [];
|
|
20774
|
+
const renderedLineage = lineage.rendered.get(index) ?? /* @__PURE__ */ new Map();
|
|
20775
|
+
let findings = compareAssets(desired, (asset) => scan.observed.get(asset.path) ?? absent2(), {
|
|
20776
|
+
inLineage: (id, path) => lineage.verbatim.has(id) || (renderedLineage.get(path)?.has(id) ?? false)
|
|
20777
|
+
});
|
|
20778
|
+
if (lineage.unobserved) {
|
|
20779
|
+
findings = findings.map((finding2) => finding2.kind === "stale-content" || finding2.kind === "locally-modified" ? { ...finding2, kind: "incomplete", detail: "lineage-unobserved" } : finding2);
|
|
20780
|
+
}
|
|
20781
|
+
const extras = classifyExtras({
|
|
20782
|
+
trackedPaths: scan.tracked,
|
|
20783
|
+
ownedPaths,
|
|
20784
|
+
groups: manifest.groups,
|
|
20785
|
+
runtimeDir: manifest.runtime_dir,
|
|
20786
|
+
excludedPatterns: manifest.excluded_patterns
|
|
20787
|
+
});
|
|
20788
|
+
const groups = Object.keys(manifest.groups).sort().map((group) => {
|
|
20789
|
+
const rolePath = manifest.groups[group];
|
|
20790
|
+
const ownedHere = source.assets.filter((asset) => asset.group === group);
|
|
20791
|
+
const ownedSet = new Set(ownedHere.map((asset) => asset.rolePath));
|
|
20792
|
+
const items = findings.filter((finding2) => ownedSet.has(finding2.path));
|
|
20793
|
+
for (const extra of extras.unexpected) {
|
|
20794
|
+
if (extra.group !== group) continue;
|
|
20795
|
+
items.push({ path: extra.path, kind: "unexpected-owned", desired: null, observed: "tracked", detail: extra.detail, wip: scan.wipPaths.has(extra.path) });
|
|
20796
|
+
}
|
|
20797
|
+
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);
|
|
20798
|
+
const incompletePaths = new Set(items.filter((item) => item.kind === "incomplete").map((item) => item.path));
|
|
20799
|
+
const driftedPaths = new Set(items.filter((item) => item.kind !== "incomplete" && item.kind !== "unexpected-owned" && !incompletePaths.has(item.path)).map((item) => item.path));
|
|
20800
|
+
const unexpected = items.filter((item) => item.kind === "unexpected-owned").length;
|
|
20801
|
+
const owned = ownedHere.length;
|
|
20802
|
+
return {
|
|
20803
|
+
group,
|
|
20804
|
+
role_path: rolePath,
|
|
20805
|
+
state: incompletePaths.size > 0 ? "error" : driftedPaths.size > 0 || unexpected > 0 ? "fail" : "pass",
|
|
20806
|
+
owned,
|
|
20807
|
+
matching: owned - incompletePaths.size - driftedPaths.size,
|
|
20808
|
+
drifted: driftedPaths.size,
|
|
20809
|
+
incomplete: incompletePaths.size,
|
|
20810
|
+
unexpected,
|
|
20811
|
+
items
|
|
20812
|
+
};
|
|
20813
|
+
});
|
|
20814
|
+
const totals = groups.reduce((sum, group) => ({
|
|
20815
|
+
owned: sum.owned + group.owned,
|
|
20816
|
+
compared: sum.compared + group.owned - group.incomplete,
|
|
20817
|
+
matching: sum.matching + group.matching,
|
|
20818
|
+
drifted: sum.drifted + group.drifted,
|
|
20819
|
+
incomplete: sum.incomplete + group.incomplete,
|
|
20820
|
+
unexpected_owned: sum.unexpected_owned + group.unexpected
|
|
20821
|
+
}), { owned: 0, compared: 0, matching: 0, drifted: 0, incomplete: 0, unexpected_owned: 0 });
|
|
20822
|
+
const wipOverlap = [...new Set(groups.flatMap((group) => group.items).filter((item) => item.wip).map((item) => item.path))].sort();
|
|
20823
|
+
return {
|
|
20824
|
+
agentId: scan.input.agentId,
|
|
20825
|
+
roleDir: scan.roleDir,
|
|
20826
|
+
roleDirSource: scan.roleDirSource,
|
|
20827
|
+
error: null,
|
|
20828
|
+
groups,
|
|
20829
|
+
assets: totals,
|
|
20830
|
+
wipOverlap,
|
|
20831
|
+
wipPreserved: scan.wipPreserved,
|
|
20832
|
+
foreignTracked: extras.foreignTracked,
|
|
20833
|
+
ignoredEntries: scan.ignoredEntries
|
|
20834
|
+
};
|
|
20835
|
+
});
|
|
20836
|
+
return { source: source.record, agents, probes };
|
|
20837
|
+
}
|
|
20838
|
+
|
|
20839
|
+
// src/fleet/status.ts
|
|
19936
20840
|
init_project();
|
|
19937
20841
|
var RULE_DOMAIN = Object.freeze({
|
|
19938
20842
|
// Tracked assets the CommonProject / Hermes template owns in a repository.
|
|
@@ -20025,6 +20929,7 @@ var DOMAIN_AUTHORITY_BLOCK = Object.freeze({
|
|
|
20025
20929
|
var CAPABILITY_SYSTEMD = "unit_topology";
|
|
20026
20930
|
var CAPABILITY_LIVE_PROCESS = "process_attribution";
|
|
20027
20931
|
var CAPABILITY_BLOODBANK_LIVENESS = "routing_liveness";
|
|
20932
|
+
var CAPABILITY_SCAFFOLD_MANIFEST = "scaffold.manifest";
|
|
20028
20933
|
var FINDING_SEVERITY = Object.freeze({
|
|
20029
20934
|
error: "critical",
|
|
20030
20935
|
warn: "medium",
|
|
@@ -20049,6 +20954,16 @@ var SOURCE_REGISTRY = "fleet-inventory";
|
|
|
20049
20954
|
var SOURCE_PROVENANCE = "fleet-provenance";
|
|
20050
20955
|
var SOURCE_AUDIT = "recipe-audit";
|
|
20051
20956
|
var SOURCE_DECLARED_GAP = "declared-gap";
|
|
20957
|
+
var SOURCE_SCAFFOLD = "fleet-scaffold";
|
|
20958
|
+
var SCAFFOLD_SOURCE_RULE_ID = "scaffold.source";
|
|
20959
|
+
var RULE_COVERED_CONTENT_GROUPS = /* @__PURE__ */ new Set([
|
|
20960
|
+
"scaffold.hermes",
|
|
20961
|
+
"scaffold.gitignore",
|
|
20962
|
+
"scaffold.scripts",
|
|
20963
|
+
"scaffold.sentinel.prompt.md"
|
|
20964
|
+
]);
|
|
20965
|
+
var RULE_COVERED_PRESENCE_PATHS = /* @__PURE__ */ new Set(["role.yaml", "SOUL.md", ".runtime-scaffold/README.md"]);
|
|
20966
|
+
var RULE_COVERED_KINDS = /* @__PURE__ */ new Set(["missing", "stale-content", "locally-modified"]);
|
|
20052
20967
|
var FLEET_STATUS_AUDIT_TIMEOUT_MS = 2e4;
|
|
20053
20968
|
var CLI_ENTRY_ENV = "PJ_FLEET_CLI_ENTRY";
|
|
20054
20969
|
var AUDIT_CHILD_ENV_KEYS = [
|
|
@@ -20087,7 +21002,7 @@ function nonEmptyString3(value) {
|
|
|
20087
21002
|
}
|
|
20088
21003
|
function expandHome4(path, home) {
|
|
20089
21004
|
if (path === "~") return home;
|
|
20090
|
-
if (path.startsWith("~/")) return
|
|
21005
|
+
if (path.startsWith("~/")) return join35(home, path.slice(2));
|
|
20091
21006
|
return path;
|
|
20092
21007
|
}
|
|
20093
21008
|
function shownPath3(path) {
|
|
@@ -20095,7 +21010,7 @@ function shownPath3(path) {
|
|
|
20095
21010
|
}
|
|
20096
21011
|
function statusFindingId(scope, agentId, domain, ruleId, field3, source) {
|
|
20097
21012
|
const key = [scope, agentId ?? "", domain, ruleId ?? "", field3, source].join("\0");
|
|
20098
|
-
return
|
|
21013
|
+
return createHash11("sha256").update(key, "utf8").digest("hex").slice(0, 12);
|
|
20099
21014
|
}
|
|
20100
21015
|
function retrievalFor(agentId, domain, live) {
|
|
20101
21016
|
const parts = ["pjangler fleet status"];
|
|
@@ -20264,12 +21179,14 @@ function observation2(ctx, input) {
|
|
|
20264
21179
|
fixable: input.fixable ?? null,
|
|
20265
21180
|
exceptionId: input.exceptionId ?? null,
|
|
20266
21181
|
exceptionReason: input.exceptionReason ?? null,
|
|
21182
|
+
exceptionPolicy: input.exceptionPolicy ?? null,
|
|
20267
21183
|
freshness,
|
|
20268
21184
|
repo: input.agentId === null ? null : ctx.repoFor(input.agentId),
|
|
20269
21185
|
retrieval,
|
|
20270
21186
|
activationField: ctx.activationField,
|
|
20271
21187
|
activationOwner: ctx.activationOwner
|
|
20272
21188
|
}, ctx.policy);
|
|
21189
|
+
const items = input.items && input.items.length > 0 ? { items: [...input.items] } : {};
|
|
20273
21190
|
return {
|
|
20274
21191
|
domain: input.domain,
|
|
20275
21192
|
agent_id: input.agentId,
|
|
@@ -20298,7 +21215,8 @@ function observation2(ctx, input) {
|
|
|
20298
21215
|
desired: bounded2(redactHome(input.desired ?? DOMAIN_DESIRED[input.domain])),
|
|
20299
21216
|
next_action: classification.next_action,
|
|
20300
21217
|
next_action_class: classification.next_action_class,
|
|
20301
|
-
justification: classification.justification
|
|
21218
|
+
justification: classification.justification,
|
|
21219
|
+
...items
|
|
20302
21220
|
};
|
|
20303
21221
|
}
|
|
20304
21222
|
function observeFromInventory(ctx, row, domains) {
|
|
@@ -20538,21 +21456,134 @@ function observeFromProvenance(ctx, facts, agentId, domains) {
|
|
|
20538
21456
|
if (!domains.has(domain)) continue;
|
|
20539
21457
|
out.push(factObservation(ctx, fact, domain, agentId));
|
|
20540
21458
|
}
|
|
20541
|
-
for (const domain of ["
|
|
21459
|
+
for (const domain of ["release_provenance"]) {
|
|
20542
21460
|
if (!domains.has(domain) || (byDomain.get(domain) ?? 0) > 0) continue;
|
|
20543
21461
|
out.push(observation2(ctx, {
|
|
20544
21462
|
domain,
|
|
20545
21463
|
agentId,
|
|
20546
21464
|
state: "unobserved",
|
|
20547
21465
|
field: DOMAIN_FIELD[domain],
|
|
20548
|
-
summary:
|
|
21466
|
+
summary: "the provenance core reported no release fact for this agent",
|
|
20549
21467
|
source: SOURCE_PROVENANCE,
|
|
20550
21468
|
observed: "no fact",
|
|
20551
|
-
desired:
|
|
21469
|
+
desired: "at least one release provenance fact for this agent"
|
|
20552
21470
|
}));
|
|
20553
21471
|
}
|
|
20554
21472
|
return out;
|
|
20555
21473
|
}
|
|
21474
|
+
function observeFromScaffold(ctx, input) {
|
|
21475
|
+
const { agentId, result: result2 } = input;
|
|
21476
|
+
if (!input.manifestDeclared) {
|
|
21477
|
+
return {
|
|
21478
|
+
observations: [observation2(ctx, {
|
|
21479
|
+
domain: "template_scaffold",
|
|
21480
|
+
agentId,
|
|
21481
|
+
state: "unsupported",
|
|
21482
|
+
field: DOMAIN_FIELD.template_scaffold,
|
|
21483
|
+
summary: "the fleet contract declares no scaffold_manifest, so no role directory can be compared against the pinned template",
|
|
21484
|
+
details: ["declare scaffold_manifest in the fleet contract (schema 3) to make this domain observable"],
|
|
21485
|
+
source: SOURCE_SCAFFOLD,
|
|
21486
|
+
capability: CAPABILITY_SCAFFOLD_MANIFEST,
|
|
21487
|
+
observed: "not observed",
|
|
21488
|
+
desired: "every owned asset compared against the template at the committed gitlink"
|
|
21489
|
+
})],
|
|
21490
|
+
summary: null
|
|
21491
|
+
};
|
|
21492
|
+
}
|
|
21493
|
+
if (result2 === void 0) {
|
|
21494
|
+
return {
|
|
21495
|
+
observations: [observation2(ctx, {
|
|
21496
|
+
domain: "template_scaffold",
|
|
21497
|
+
agentId,
|
|
21498
|
+
state: "unobserved",
|
|
21499
|
+
field: DOMAIN_FIELD.template_scaffold,
|
|
21500
|
+
summary: "the scaffold observer produced no result for this agent",
|
|
21501
|
+
source: SOURCE_SCAFFOLD,
|
|
21502
|
+
observed: "no result",
|
|
21503
|
+
desired: "eight group observations for this agent"
|
|
21504
|
+
})],
|
|
21505
|
+
summary: null
|
|
21506
|
+
};
|
|
21507
|
+
}
|
|
21508
|
+
const short = input.gitlink === null ? null : input.gitlink.slice(0, 12);
|
|
21509
|
+
const desiredSuffix = short === null ? "at the committed gitlink" : `at gitlink ${short}`;
|
|
21510
|
+
const observations = [];
|
|
21511
|
+
for (const group of result2.groups) {
|
|
21512
|
+
const items = group.items.map((item) => ({
|
|
21513
|
+
path: bounded2(item.path),
|
|
21514
|
+
kind: item.kind,
|
|
21515
|
+
desired: item.desired === null ? null : bounded2(item.desired, 64),
|
|
21516
|
+
observed: item.observed === null ? null : bounded2(item.observed, 64),
|
|
21517
|
+
detail: item.detail === null ? null : bounded2(item.detail),
|
|
21518
|
+
wip: item.wip
|
|
21519
|
+
}));
|
|
21520
|
+
const kept = items.slice(0, FLEET_STATUS_MAX_ITEMS);
|
|
21521
|
+
if (items.length > kept.length) {
|
|
21522
|
+
input.notes.push(
|
|
21523
|
+
`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`
|
|
21524
|
+
);
|
|
21525
|
+
}
|
|
21526
|
+
const details = kept.map((item) => {
|
|
21527
|
+
const pair = item.desired !== null || item.observed !== null ? ` ${item.desired ?? "-"} -> ${item.observed ?? "-"}` : "";
|
|
21528
|
+
return `${item.kind} ${item.path}${pair}${item.detail ? ` (${item.detail})` : ""}${item.wip ? " [wip]" : ""}`;
|
|
21529
|
+
});
|
|
21530
|
+
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`;
|
|
21531
|
+
const excepted = group.state === "fail" && input.exception !== null;
|
|
21532
|
+
observations.push(observation2(ctx, {
|
|
21533
|
+
domain: "template_scaffold",
|
|
21534
|
+
agentId,
|
|
21535
|
+
state: group.state,
|
|
21536
|
+
field: group.group,
|
|
21537
|
+
ruleId: null,
|
|
21538
|
+
ruleScope: "project",
|
|
21539
|
+
fixable: false,
|
|
21540
|
+
source: SOURCE_SCAFFOLD,
|
|
21541
|
+
summary,
|
|
21542
|
+
details,
|
|
21543
|
+
observed: `${group.matching}/${group.owned} assets match`,
|
|
21544
|
+
desired: `every asset in ${group.group} ${desiredSuffix}`,
|
|
21545
|
+
items: kept,
|
|
21546
|
+
exceptionId: excepted ? agentId : null,
|
|
21547
|
+
exceptionReason: excepted ? input.exception.reason : null,
|
|
21548
|
+
exceptionPolicy: excepted ? input.exception.path : null
|
|
21549
|
+
}));
|
|
21550
|
+
}
|
|
21551
|
+
const overlap = result2.wipOverlap.slice(0, FLEET_STATUS_MAX_WIP_OVERLAP).map((path) => bounded2(path));
|
|
21552
|
+
if (result2.wipOverlap.length > overlap.length) {
|
|
21553
|
+
input.notes.push(`agents.${agentId}.scaffold.wip_overlap: ${result2.wipOverlap.length - overlap.length} of ${result2.wipOverlap.length} paths dropped`);
|
|
21554
|
+
}
|
|
21555
|
+
return {
|
|
21556
|
+
observations,
|
|
21557
|
+
summary: {
|
|
21558
|
+
source_gitlink: input.gitlink,
|
|
21559
|
+
role_dir: result2.roleDir === null ? null : shownPath3(result2.roleDir),
|
|
21560
|
+
role_dir_source: result2.roleDirSource,
|
|
21561
|
+
assets: { ...result2.assets },
|
|
21562
|
+
wip_overlap: overlap,
|
|
21563
|
+
wip_preserved: result2.wipPreserved,
|
|
21564
|
+
foreign_tracked: result2.foreignTracked,
|
|
21565
|
+
ignored_entries: result2.ignoredEntries
|
|
21566
|
+
}
|
|
21567
|
+
};
|
|
21568
|
+
}
|
|
21569
|
+
function scaffoldRuleVerdict(rule) {
|
|
21570
|
+
if (rule === void 0) return null;
|
|
21571
|
+
if (rule.status === "pass") return "pass";
|
|
21572
|
+
if (rule.status !== "fail") return null;
|
|
21573
|
+
const details = Array.isArray(rule.details) ? rule.details.filter((line) => typeof line === "string") : [];
|
|
21574
|
+
return details.some((line) => /: (?:missing|stale) (?!.*\/runtime\/)/u.test(line)) ? "fail" : null;
|
|
21575
|
+
}
|
|
21576
|
+
function scaffoldObserverDrift(result2) {
|
|
21577
|
+
if (result2 === void 0 || result2.error !== null) return null;
|
|
21578
|
+
if (result2.groups.some((group) => group.state === "error" && (RULE_COVERED_CONTENT_GROUPS.has(group.group) || group.owned > 0))) return null;
|
|
21579
|
+
for (const group of result2.groups) {
|
|
21580
|
+
for (const item of group.items) {
|
|
21581
|
+
if (item.kind === "missing" && RULE_COVERED_PRESENCE_PATHS.has(item.path)) return true;
|
|
21582
|
+
if (RULE_COVERED_CONTENT_GROUPS.has(group.group) && RULE_COVERED_KINDS.has(item.kind)) return true;
|
|
21583
|
+
}
|
|
21584
|
+
}
|
|
21585
|
+
return false;
|
|
21586
|
+
}
|
|
20556
21587
|
function factObservation(ctx, fact, domain, agentId) {
|
|
20557
21588
|
return observation2(ctx, {
|
|
20558
21589
|
domain,
|
|
@@ -20614,8 +21645,8 @@ function agentLifecycle(row, own, domains) {
|
|
|
20614
21645
|
}
|
|
20615
21646
|
function resolveAuditCli(env2 = process.env) {
|
|
20616
21647
|
const override = nonEmptyString3(env2[CLI_ENTRY_ENV]);
|
|
20617
|
-
const entry = override ??
|
|
20618
|
-
if (!
|
|
21648
|
+
const entry = override ?? join35(resolvePjanglerRoot(), "dist", "index.js");
|
|
21649
|
+
if (!isAbsolute10(entry)) return null;
|
|
20619
21650
|
return existsSync26(entry) ? entry : null;
|
|
20620
21651
|
}
|
|
20621
21652
|
function auditChildEnv(base = process.env) {
|
|
@@ -20686,10 +21717,10 @@ async function collectFleetStatus(options) {
|
|
|
20686
21717
|
let baselineText = null;
|
|
20687
21718
|
let baselineShown = "";
|
|
20688
21719
|
if (baselinePath !== null) {
|
|
20689
|
-
const resolved =
|
|
21720
|
+
const resolved = resolve21(expandHome4(baselinePath, home));
|
|
20690
21721
|
baselineShown = shownPath3(resolved);
|
|
20691
21722
|
try {
|
|
20692
|
-
baselineText =
|
|
21723
|
+
baselineText = readFileSync26(resolved, "utf8");
|
|
20693
21724
|
} catch {
|
|
20694
21725
|
throw new FleetError(
|
|
20695
21726
|
"INVALID_INPUT",
|
|
@@ -20732,15 +21763,22 @@ async function collectFleetStatus(options) {
|
|
|
20732
21763
|
const agentRawById = /* @__PURE__ */ new Map();
|
|
20733
21764
|
for (const entry of agentRaw.entries) if (!agentRawById.has(entry.key)) agentRawById.set(entry.key, entry.value);
|
|
20734
21765
|
const repoByAgent = /* @__PURE__ */ new Map();
|
|
21766
|
+
const roleDirByAgent = /* @__PURE__ */ new Map();
|
|
21767
|
+
const rawRowByAgent = /* @__PURE__ */ new Map();
|
|
20735
21768
|
for (const entry of agentRaw.entries) {
|
|
20736
|
-
if (!selectedAgents.has(entry.key)) continue;
|
|
21769
|
+
if (!selectedAgents.has(entry.key) || rawRowByAgent.has(entry.key)) continue;
|
|
20737
21770
|
const raw = isRecord8(entry.value) ? entry.value : {};
|
|
20738
21771
|
const declared = nonEmptyString3(raw.project_path);
|
|
20739
|
-
if (declared !== null) repoByAgent.set(entry.key,
|
|
21772
|
+
if (declared !== null) repoByAgent.set(entry.key, resolve21(expandHome4(declared, home)));
|
|
21773
|
+
const declaredRoleDir = nonEmptyString3(raw.role_dir);
|
|
21774
|
+
roleDirByAgent.set(entry.key, declaredRoleDir === null ? null : expandHome4(declaredRoleDir, home));
|
|
21775
|
+
rawRowByAgent.set(entry.key, raw);
|
|
20740
21776
|
}
|
|
20741
21777
|
const policy = readHealthPolicy(contract);
|
|
21778
|
+
const manifest = domainSet.has("template_scaffold") ? contract.scaffold_manifest ?? null : null;
|
|
20742
21779
|
const projectRawBySlug = /* @__PURE__ */ new Map();
|
|
20743
|
-
|
|
21780
|
+
const manifestNeedsProjects = manifest !== null && Object.values(manifest.render_inputs).some((field3) => field3.startsWith("projects."));
|
|
21781
|
+
if (manifestNeedsProjects || policy.freshness.some(({ entry }) => entry.field.startsWith("projects."))) {
|
|
20744
21782
|
const projectRaw = readProjectRegistryRaw(stores.projects.inspectedPath);
|
|
20745
21783
|
for (const entry of projectRaw.entries) {
|
|
20746
21784
|
if (!projectRawBySlug.has(entry.key)) projectRawBySlug.set(entry.key, entry.value);
|
|
@@ -20852,6 +21890,66 @@ async function collectFleetStatus(options) {
|
|
|
20852
21890
|
provenanceFacts = provenance.facts;
|
|
20853
21891
|
ctx.probes.push(...provenance.probes);
|
|
20854
21892
|
}
|
|
21893
|
+
const scalarAt = (root, segments) => {
|
|
21894
|
+
let node = root;
|
|
21895
|
+
for (const segment of segments) {
|
|
21896
|
+
if (!isRecord8(node)) return null;
|
|
21897
|
+
node = node[segment];
|
|
21898
|
+
}
|
|
21899
|
+
if (typeof node === "string") return nonEmptyString3(node);
|
|
21900
|
+
if (typeof node === "number" || typeof node === "boolean") return String(node);
|
|
21901
|
+
return null;
|
|
21902
|
+
};
|
|
21903
|
+
const renderInputsFor = (agentId) => {
|
|
21904
|
+
const inputs = {};
|
|
21905
|
+
if (manifest === null) return inputs;
|
|
21906
|
+
const rawRow = rawRowByAgent.get(agentId);
|
|
21907
|
+
const slug = rowsById.get(agentId)?.project_id.value ?? null;
|
|
21908
|
+
const project = slug === null ? void 0 : projectRawBySlug.get(slug);
|
|
21909
|
+
for (const [name, field3] of Object.entries(manifest.render_inputs)) {
|
|
21910
|
+
const [root, key, ...rest] = field3.split(".");
|
|
21911
|
+
if (field3 === "agents.{agent_id}") inputs[name] = agentId;
|
|
21912
|
+
else if (root === "agents" && key === "{agent_id}") inputs[name] = scalarAt(rawRow, rest);
|
|
21913
|
+
else if (root === "projects" && key === "{slug}") inputs[name] = scalarAt(project, rest);
|
|
21914
|
+
else inputs[name] = null;
|
|
21915
|
+
}
|
|
21916
|
+
return inputs;
|
|
21917
|
+
};
|
|
21918
|
+
const scaffoldExceptionFor = (agentId) => {
|
|
21919
|
+
const ruling = policy.agentExceptions.find(({ entry }) => entry.domain === "template_scaffold" && entry.agent_id === agentId);
|
|
21920
|
+
return ruling ? { path: ruling.path, reason: ruling.entry.reason } : null;
|
|
21921
|
+
};
|
|
21922
|
+
let scaffoldParity = null;
|
|
21923
|
+
const scaffoldByAgent = /* @__PURE__ */ new Map();
|
|
21924
|
+
if (manifest !== null) {
|
|
21925
|
+
throwIfCancelled(runContext);
|
|
21926
|
+
scaffoldParity = await collectScaffoldParity({
|
|
21927
|
+
run: runContext,
|
|
21928
|
+
pjanglerRoot: resolvePjanglerRoot(),
|
|
21929
|
+
manifest,
|
|
21930
|
+
agents: agentIds.map((agentId) => ({
|
|
21931
|
+
agentId,
|
|
21932
|
+
projectPath: repoByAgent.get(agentId) ?? null,
|
|
21933
|
+
roleDir: roleDirByAgent.get(agentId) ?? null,
|
|
21934
|
+
role: nonEmptyString3(rawRowByAgent.get(agentId)?.role),
|
|
21935
|
+
inputs: renderInputsFor(agentId)
|
|
21936
|
+
})),
|
|
21937
|
+
shown: shownPath3
|
|
21938
|
+
});
|
|
21939
|
+
ctx.probes.push(...scaffoldParity.probes);
|
|
21940
|
+
for (const result2 of scaffoldParity.agents) scaffoldByAgent.set(result2.agentId, result2);
|
|
21941
|
+
}
|
|
21942
|
+
const scaffoldCounts = {
|
|
21943
|
+
total_registered: inventory.totals.registered_agents,
|
|
21944
|
+
selected: agentIds.length,
|
|
21945
|
+
applicable: 0,
|
|
21946
|
+
passing: 0,
|
|
21947
|
+
drifted: 0,
|
|
21948
|
+
incomplete: 0,
|
|
21949
|
+
exception_authorized: 0,
|
|
21950
|
+
unobserved: 0
|
|
21951
|
+
};
|
|
21952
|
+
const ruleAgreement = { compared: 0, agree: 0, disagree: 0, not_compared: 0 };
|
|
20855
21953
|
const auditFedSelected = domains.filter((domain) => AUDIT_PER_AGENT_DOMAINS.has(domain));
|
|
20856
21954
|
const wantsAudit = live && auditFedSelected.length > 0;
|
|
20857
21955
|
const auditByAgent = /* @__PURE__ */ new Map();
|
|
@@ -20933,6 +22031,57 @@ async function collectFleetStatus(options) {
|
|
|
20933
22031
|
}
|
|
20934
22032
|
}
|
|
20935
22033
|
const hostByRule = /* @__PURE__ */ new Map();
|
|
22034
|
+
if (scaffoldParity !== null) {
|
|
22035
|
+
const state = scaffoldParity.source.integrity === "ok" ? "pass" : "error";
|
|
22036
|
+
const field3 = DOMAIN_FIELD.template_scaffold;
|
|
22037
|
+
const hostRetrieval = retrievalFor(null, "template_scaffold", live);
|
|
22038
|
+
const sourceClassification = classifyObservation({
|
|
22039
|
+
domain: "template_scaffold",
|
|
22040
|
+
state,
|
|
22041
|
+
field: field3,
|
|
22042
|
+
ruleId: SCAFFOLD_SOURCE_RULE_ID,
|
|
22043
|
+
ruleScope: "host",
|
|
22044
|
+
source: SOURCE_SCAFFOLD,
|
|
22045
|
+
capability: null,
|
|
22046
|
+
evidence: null,
|
|
22047
|
+
fixable: false,
|
|
22048
|
+
exceptionId: null,
|
|
22049
|
+
exceptionReason: null,
|
|
22050
|
+
freshness: "not_applicable",
|
|
22051
|
+
repo: null,
|
|
22052
|
+
retrieval: hostRetrieval,
|
|
22053
|
+
activationField: ctx.activationField,
|
|
22054
|
+
activationOwner: ctx.activationOwner
|
|
22055
|
+
}, ctx.policy);
|
|
22056
|
+
const sourceSummary = bounded2(redactHome(scaffoldParity.source.detail));
|
|
22057
|
+
hostByRule.set(SCAFFOLD_SOURCE_RULE_ID, {
|
|
22058
|
+
finding: {
|
|
22059
|
+
rule_id: SCAFFOLD_SOURCE_RULE_ID,
|
|
22060
|
+
owner: ctx.domainOwner("template_scaffold"),
|
|
22061
|
+
domain: "template_scaffold",
|
|
22062
|
+
state,
|
|
22063
|
+
summary: sourceSummary,
|
|
22064
|
+
details: boundedDetails([
|
|
22065
|
+
`integrity ${scaffoldParity.source.integrity}`,
|
|
22066
|
+
`gitlink ${scaffoldParity.source.gitlink ?? "none recorded"}`,
|
|
22067
|
+
`${scaffoldParity.source.assets} asset(s) in the manifest`
|
|
22068
|
+
]),
|
|
22069
|
+
finding_id: statusFindingId("host", null, "template_scaffold", SCAFFOLD_SOURCE_RULE_ID, field3, SOURCE_SCAFFOLD),
|
|
22070
|
+
retrieval: hostRetrieval,
|
|
22071
|
+
applicability: sourceClassification.applicability,
|
|
22072
|
+
evidence: sourceClassification.evidence,
|
|
22073
|
+
freshness: sourceClassification.freshness,
|
|
22074
|
+
severity: sourceClassification.severity,
|
|
22075
|
+
repair: sourceClassification.repair,
|
|
22076
|
+
observed: bounded2(scaffoldParity.source.integrity),
|
|
22077
|
+
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",
|
|
22078
|
+
next_action: sourceClassification.next_action,
|
|
22079
|
+
next_action_class: sourceClassification.next_action_class,
|
|
22080
|
+
justification: sourceClassification.justification
|
|
22081
|
+
},
|
|
22082
|
+
states: /* @__PURE__ */ new Map([[state, 1]])
|
|
22083
|
+
});
|
|
22084
|
+
}
|
|
20936
22085
|
const agentRecords = [];
|
|
20937
22086
|
let totalObservations = 0;
|
|
20938
22087
|
let emittedObservations = 0;
|
|
@@ -20949,8 +22098,34 @@ async function collectFleetStatus(options) {
|
|
|
20949
22098
|
const own = [];
|
|
20950
22099
|
own.push(...observeFromInventory(ctx, row, domainSet));
|
|
20951
22100
|
if (needsProvenance) own.push(...observeFromProvenance(ctx, provenanceFacts, agentId, domainSet));
|
|
22101
|
+
const agentNotes = [];
|
|
22102
|
+
let scaffoldSummary = null;
|
|
22103
|
+
const scaffoldResult = scaffoldByAgent.get(agentId);
|
|
22104
|
+
const scaffoldObserved = manifest !== null && scaffoldResult !== void 0;
|
|
22105
|
+
if (domainSet.has("template_scaffold")) {
|
|
22106
|
+
const scaffold2 = observeFromScaffold(ctx, {
|
|
22107
|
+
agentId,
|
|
22108
|
+
result: scaffoldResult,
|
|
22109
|
+
manifestDeclared: manifest !== null,
|
|
22110
|
+
gitlink: scaffoldParity?.source.gitlink ?? null,
|
|
22111
|
+
exception: scaffoldExceptionFor(agentId),
|
|
22112
|
+
notes: agentNotes
|
|
22113
|
+
});
|
|
22114
|
+
own.push(...scaffold2.observations);
|
|
22115
|
+
scaffoldSummary = scaffold2.summary;
|
|
22116
|
+
if (!scaffoldObserved) scaffoldCounts.unobserved += 1;
|
|
22117
|
+
else {
|
|
22118
|
+
scaffoldCounts.applicable += 1;
|
|
22119
|
+
if (scaffoldResult.groups.some((group) => group.state === "error")) scaffoldCounts.incomplete += 1;
|
|
22120
|
+
else if (scaffoldResult.groups.some((group) => group.state === "fail")) {
|
|
22121
|
+
if (scaffoldExceptionFor(agentId) !== null) scaffoldCounts.exception_authorized += 1;
|
|
22122
|
+
else scaffoldCounts.drifted += 1;
|
|
22123
|
+
} else scaffoldCounts.passing += 1;
|
|
22124
|
+
}
|
|
22125
|
+
}
|
|
20952
22126
|
const audit = auditByAgent.get(agentId);
|
|
20953
22127
|
if (!live) {
|
|
22128
|
+
if (scaffoldObserved) ruleAgreement.not_compared += 1;
|
|
20954
22129
|
for (const domain of auditFedSelected) {
|
|
20955
22130
|
own.push(observation2(ctx, {
|
|
20956
22131
|
domain,
|
|
@@ -20965,6 +22140,7 @@ async function collectFleetStatus(options) {
|
|
|
20965
22140
|
}));
|
|
20966
22141
|
}
|
|
20967
22142
|
} else if (auditEntry === null || audit === void 0 || audit.outcome !== "ok") {
|
|
22143
|
+
if (scaffoldObserved) ruleAgreement.not_compared += 1;
|
|
20968
22144
|
const failed = audit !== void 0 && audit.outcome === "failed";
|
|
20969
22145
|
const state = failed ? "error" : "unobserved";
|
|
20970
22146
|
const reason = auditEntry === null ? "audit-cli-unavailable" : audit === void 0 ? "no-project-path-recorded" : audit.reason ?? audit.outcome;
|
|
@@ -21117,6 +22293,29 @@ async function collectFleetStatus(options) {
|
|
|
21117
22293
|
fixable: typeof rule.fixable === "boolean" ? rule.fixable : null
|
|
21118
22294
|
}));
|
|
21119
22295
|
}
|
|
22296
|
+
if (scaffoldObserved) {
|
|
22297
|
+
const ruleVerdict = scaffoldRuleVerdict((audit.rules ?? []).find((rule) => nonEmptyString3(rule.id) === "hermes.pm-scaffold"));
|
|
22298
|
+
const observerDrift = scaffoldObserverDrift(scaffoldResult);
|
|
22299
|
+
if (ruleVerdict === null || observerDrift === null) ruleAgreement.not_compared += 1;
|
|
22300
|
+
else {
|
|
22301
|
+
ruleAgreement.compared += 1;
|
|
22302
|
+
if (ruleVerdict === "fail" === observerDrift) ruleAgreement.agree += 1;
|
|
22303
|
+
else {
|
|
22304
|
+
ruleAgreement.disagree += 1;
|
|
22305
|
+
addFinding3(ctx, {
|
|
22306
|
+
code: "scaffold-rule-disagreement",
|
|
22307
|
+
domain: "template_scaffold",
|
|
22308
|
+
field: DOMAIN_FIELD.template_scaffold,
|
|
22309
|
+
agent_id: agentId,
|
|
22310
|
+
source: ctx.domainOwner("template_scaffold"),
|
|
22311
|
+
severity: "error",
|
|
22312
|
+
statusSeverity: "high",
|
|
22313
|
+
gating: true,
|
|
22314
|
+
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`
|
|
22315
|
+
});
|
|
22316
|
+
}
|
|
22317
|
+
}
|
|
22318
|
+
}
|
|
21120
22319
|
}
|
|
21121
22320
|
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);
|
|
21122
22321
|
totalObservations += own.length;
|
|
@@ -21134,6 +22333,7 @@ async function collectFleetStatus(options) {
|
|
|
21134
22333
|
);
|
|
21135
22334
|
members[memberClass] += 1;
|
|
21136
22335
|
if (agentRecords.length >= FLEET_STATUS_MAX_AGENTS) continue;
|
|
22336
|
+
truncated.push(...agentNotes);
|
|
21137
22337
|
let kept = own;
|
|
21138
22338
|
let retrieval = retrievalFor(agentId, domains.length === 1 ? domains[0] : null, live);
|
|
21139
22339
|
if (clipped) {
|
|
@@ -21154,7 +22354,8 @@ async function collectFleetStatus(options) {
|
|
|
21154
22354
|
truncated: clipped,
|
|
21155
22355
|
retrieval,
|
|
21156
22356
|
lifecycle,
|
|
21157
|
-
member_class: memberClass
|
|
22357
|
+
member_class: memberClass,
|
|
22358
|
+
scaffold: scaffoldSummary
|
|
21158
22359
|
});
|
|
21159
22360
|
}
|
|
21160
22361
|
const fleetObservations = [];
|
|
@@ -21243,6 +22444,18 @@ async function collectFleetStatus(options) {
|
|
|
21243
22444
|
policy,
|
|
21244
22445
|
domainStates: new Map(domainRollups.map((rollup) => [rollup.domain, rollup.state]))
|
|
21245
22446
|
});
|
|
22447
|
+
const scaffold = !domainSet.has("template_scaffold") ? null : {
|
|
22448
|
+
source: scaffoldParity === null ? { gitlink: null, integrity: "manifest-undeclared", detail: "the fleet contract declares no scaffold_manifest, so no role directory was compared" } : {
|
|
22449
|
+
gitlink: scaffoldParity.source.gitlink,
|
|
22450
|
+
integrity: bounded2(scaffoldParity.source.integrity),
|
|
22451
|
+
detail: bounded2(redactHome(scaffoldParity.source.detail))
|
|
22452
|
+
},
|
|
22453
|
+
agents: {
|
|
22454
|
+
...scaffoldCounts,
|
|
22455
|
+
unobserved: scaffoldCounts.unobserved + (scaffoldCounts.total_registered - scaffoldCounts.selected)
|
|
22456
|
+
},
|
|
22457
|
+
rule_agreement: { ...ruleAgreement }
|
|
22458
|
+
};
|
|
21246
22459
|
return {
|
|
21247
22460
|
contract_path: shownPath3(contractPath),
|
|
21248
22461
|
contract_version: contract.contract_version,
|
|
@@ -21255,6 +22468,7 @@ async function collectFleetStatus(options) {
|
|
|
21255
22468
|
findings,
|
|
21256
22469
|
probes,
|
|
21257
22470
|
transitions,
|
|
22471
|
+
scaffold,
|
|
21258
22472
|
truncated
|
|
21259
22473
|
};
|
|
21260
22474
|
}
|
|
@@ -21439,7 +22653,7 @@ function validateExternalEffectConsent(input, options) {
|
|
|
21439
22653
|
return selected;
|
|
21440
22654
|
}
|
|
21441
22655
|
function resolveTargetDir(targetDir) {
|
|
21442
|
-
const dir =
|
|
22656
|
+
const dir = resolve22(targetDir ?? process.cwd());
|
|
21443
22657
|
if (!existsSync27(dir)) {
|
|
21444
22658
|
throw new Error(`Target directory does not exist: ${dir}`);
|
|
21445
22659
|
}
|
|
@@ -21449,12 +22663,12 @@ function resolveTargetDir(targetDir) {
|
|
|
21449
22663
|
return dir;
|
|
21450
22664
|
}
|
|
21451
22665
|
function resolvePjanglerRoot3() {
|
|
21452
|
-
let dir =
|
|
21453
|
-
while (dir !==
|
|
21454
|
-
if (existsSync27(
|
|
22666
|
+
let dir = dirname18(fileURLToPath9(import.meta.url));
|
|
22667
|
+
while (dir !== dirname18(dir)) {
|
|
22668
|
+
if (existsSync27(join36(dir, "package.json")) && existsSync27(join36(dir, "templates", "commonproject", "copier.yml"))) {
|
|
21455
22669
|
return dir;
|
|
21456
22670
|
}
|
|
21457
|
-
dir =
|
|
22671
|
+
dir = dirname18(dir);
|
|
21458
22672
|
}
|
|
21459
22673
|
throw new Error("Unable to resolve pjangler root");
|
|
21460
22674
|
}
|
|
@@ -21548,7 +22762,7 @@ function projectPreflightFailure(plan, errors, audit) {
|
|
|
21548
22762
|
};
|
|
21549
22763
|
}
|
|
21550
22764
|
function preflightExistingHermesScaffold(targetDir) {
|
|
21551
|
-
if (!existsSync27(
|
|
22765
|
+
if (!existsSync27(join36(targetDir, "agents", "hermes"))) return void 0;
|
|
21552
22766
|
const owner = recipeRegistry.ownerOf("hermes.pm-scaffold");
|
|
21553
22767
|
if (!owner) return "Hermes lifecycle owner is unavailable";
|
|
21554
22768
|
const finding2 = owner.check.audit(lifecycleContext(targetDir, true));
|
|
@@ -21807,12 +23021,12 @@ server.registerTool(
|
|
|
21807
23021
|
);
|
|
21808
23022
|
const pjanglerRoot = resolvePjanglerRoot3();
|
|
21809
23023
|
const projectSlug = validateSafePathSegment(input.projectSlug ?? slugify(input.projectName), "Project slug");
|
|
21810
|
-
const explicitTargetDir = input.targetDir ?
|
|
21811
|
-
const parentDir =
|
|
23024
|
+
const explicitTargetDir = input.targetDir ? resolve22(input.targetDir) : void 0;
|
|
23025
|
+
const parentDir = resolve22(input.parentDir ?? (explicitTargetDir ? dirname18(explicitTargetDir) : process.cwd()));
|
|
21812
23026
|
if (!existsSync27(parentDir) || !statSync8(parentDir).isDirectory()) throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
21813
23027
|
const targetDir = resolveContainedPath(
|
|
21814
23028
|
parentDir,
|
|
21815
|
-
explicitTargetDir ??
|
|
23029
|
+
explicitTargetDir ?? join36(parentDir, projectSlug),
|
|
21816
23030
|
"Bootstrap target"
|
|
21817
23031
|
);
|
|
21818
23032
|
const overwrite = input.overwrite ?? input.force ?? false;
|
|
@@ -21959,7 +23173,7 @@ server.registerTool(
|
|
|
21959
23173
|
registryPath: input.registryPath,
|
|
21960
23174
|
force: input.force ?? false,
|
|
21961
23175
|
overwrite: input.force ?? false,
|
|
21962
|
-
scaffold: !(input.targetDir && existsSync27(
|
|
23176
|
+
scaffold: !(input.targetDir && existsSync27(join36(resolve22(input.targetDir), ".git")))
|
|
21963
23177
|
});
|
|
21964
23178
|
if (!input.apply) return asText(publicCompositeProjectResponse(publicProjectPlan(plan), plan));
|
|
21965
23179
|
const preflight = preflightProjectApply(plan, resolvePjanglerRoot3());
|