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