@intentius/behold 0.10.0 → 0.11.0

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/cli.js CHANGED
@@ -1121,7 +1121,7 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
1121
1121
  import { execFile as execFile3 } from "node:child_process";
1122
1122
  import { promisify as promisify3 } from "node:util";
1123
1123
  import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
1124
- import { dirname as dirname4, join as join16, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
1124
+ import { basename, dirname as dirname4, join as join16, relative as relative3, resolve as resolve5, sep as sep4 } from "node:path";
1125
1125
 
1126
1126
  // src/recents.ts
1127
1127
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
@@ -1552,8 +1552,15 @@ function parseYAMLArray(lines, startIndex, baseIndent) {
1552
1552
  result.push(obj);
1553
1553
  i = j;
1554
1554
  } else {
1555
- result.push(parseScalar(itemValue));
1556
- i++;
1555
+ const header = blockScalarHeader(itemValue);
1556
+ if (header) {
1557
+ const block = parseBlockScalar(lines, i + 1, indent, header);
1558
+ result.push(block.value);
1559
+ i = block.endIndex;
1560
+ } else {
1561
+ result.push(parseScalar(itemValue));
1562
+ i++;
1563
+ }
1557
1564
  }
1558
1565
  } else {
1559
1566
  break;
@@ -4187,6 +4194,7 @@ function renderGraph(ir, opts = {}) {
4187
4194
  const layout = layoutIr(ir, { fit: true, ...boxes ? { groups: boxes } : {} });
4188
4195
  if (opts.radial && !boxes) radializeLayout(layout, groupKeyByNode(ir), footprints(ir));
4189
4196
  else if (!boxes) packComponents(layout, ir);
4197
+ else if (boxKey === "byStack" && boxes) packMemberBoxes(layout, ir, boxes);
4190
4198
  const svg = renderSvg(ir, layout, {
4191
4199
  fit: true,
4192
4200
  hideTitle: true,
@@ -4409,6 +4417,92 @@ function packComponents(layout, ir) {
4409
4417
  layout.width = Math.max(...nodes.map((n) => n.x + halfW(n))) + pad;
4410
4418
  layout.height = Math.max(...nodes.map((n) => n.y + halfH(n))) + pad;
4411
4419
  }
4420
+ function packMemberBoxes(layout, ir, members) {
4421
+ const boxes = layout.groups ?? [];
4422
+ if (boxes.length < 4) return;
4423
+ const nodes = layout.nodes;
4424
+ const size = footprints(ir);
4425
+ const halfW = (n) => (size.get(n.id)?.w ?? NODE_W) / 2;
4426
+ const halfH = (n) => (size.get(n.id)?.h ?? NODE_H) / 2;
4427
+ for (const n of nodes) n.y = layout.height - n.y;
4428
+ for (const b of boxes) b.y = layout.height - b.y;
4429
+ const byId = new Map(nodes.map((n) => [n.id, n]));
4430
+ const claimed = /* @__PURE__ */ new Set();
4431
+ const blocks = boxes.map((box) => {
4432
+ const ns = (members[box.id ?? box.title] ?? members[box.title] ?? []).flatMap((id) => byId.get(id) ?? []);
4433
+ for (const n of ns) claimed.add(n.id);
4434
+ let minX = box.x - box.w / 2;
4435
+ let maxX2 = box.x + box.w / 2;
4436
+ let minY = box.y - box.h / 2;
4437
+ let maxY2 = box.y + box.h / 2;
4438
+ for (const n of ns) {
4439
+ minX = Math.min(minX, n.x - halfW(n));
4440
+ maxX2 = Math.max(maxX2, n.x + halfW(n));
4441
+ minY = Math.min(minY, n.y - halfH(n));
4442
+ maxY2 = Math.max(maxY2, n.y + halfH(n));
4443
+ }
4444
+ return { ns, box, minX, minY, w: maxX2 - minX, h: maxY2 - minY };
4445
+ });
4446
+ const rest = nodes.filter((n) => !claimed.has(n.id));
4447
+ if (rest.length) {
4448
+ const idxOf = new Map(rest.map((n, i) => [n.id, i]));
4449
+ const parent = rest.map((_, i) => i);
4450
+ const find = (x) => {
4451
+ while (parent[x] !== x) x = parent[x] = parent[parent[x]];
4452
+ return x;
4453
+ };
4454
+ for (const e of ir.edges) {
4455
+ const a = idxOf.get(e.from);
4456
+ const b = idxOf.get(e.to);
4457
+ if (a != null && b != null) parent[find(a)] = find(b);
4458
+ }
4459
+ const comps = /* @__PURE__ */ new Map();
4460
+ rest.forEach((n, i) => (comps.get(find(i)) ?? comps.set(find(i), []).get(find(i))).push(n));
4461
+ for (const ns of comps.values()) {
4462
+ const minX = Math.min(...ns.map((n) => n.x - halfW(n)));
4463
+ const minY = Math.min(...ns.map((n) => n.y - halfH(n)));
4464
+ blocks.push({
4465
+ ns,
4466
+ minX,
4467
+ minY,
4468
+ w: Math.max(...ns.map((n) => n.x + halfW(n))) - minX,
4469
+ h: Math.max(...ns.map((n) => n.y + halfH(n))) - minY
4470
+ });
4471
+ }
4472
+ }
4473
+ const totalArea = blocks.reduce((s, b) => s + (b.w + MEMBER_GAP) * (b.h + MEMBER_GAP), 0);
4474
+ const targetW = Math.max(Math.max(...blocks.map((b) => b.w)), Math.sqrt(totalArea) * 1.3);
4475
+ let shelfX = 0;
4476
+ let shelfY = 0;
4477
+ let shelfH = 0;
4478
+ let maxX = 0;
4479
+ let maxY = 0;
4480
+ for (const b of blocks) {
4481
+ if (shelfX > 0 && shelfX + b.w > targetW) {
4482
+ shelfX = 0;
4483
+ shelfY += shelfH + MEMBER_GAP;
4484
+ shelfH = 0;
4485
+ }
4486
+ const dx = shelfX - b.minX;
4487
+ const dy = shelfY - b.minY;
4488
+ for (const n of b.ns) {
4489
+ n.x += dx;
4490
+ n.y += dy;
4491
+ }
4492
+ if (b.box) {
4493
+ b.box.x += dx;
4494
+ b.box.y += dy;
4495
+ }
4496
+ maxX = Math.max(maxX, shelfX + b.w);
4497
+ maxY = Math.max(maxY, shelfY + b.h);
4498
+ shelfX += b.w + MEMBER_GAP;
4499
+ shelfH = Math.max(shelfH, b.h);
4500
+ }
4501
+ layout.width = maxX;
4502
+ layout.height = maxY;
4503
+ for (const n of nodes) n.y = maxY - n.y;
4504
+ for (const b of boxes) b.y = maxY - b.y;
4505
+ }
4412
4506
  function groupKeyByNode(ir) {
4413
4507
  const out = /* @__PURE__ */ new Map();
4414
4508
  for (const n of ir.nodes) {
@@ -4796,6 +4890,67 @@ async function runCarvePlan(demo) {
4796
4890
  noDestroy: r.noDestroy
4797
4891
  };
4798
4892
  }
4893
+ async function runCarveObserve(demo, select) {
4894
+ if (!demo.live) {
4895
+ return refuse2(
4896
+ "carve-action",
4897
+ "the observe beat reads the scratch Floci, and this server isn't serving one",
4898
+ "run `behold demo carve --live` (needs docker + terraform)"
4899
+ );
4900
+ }
4901
+ if (!existsSync8(join10(demo.out, "src"))) {
4902
+ return refuse2("carve-action", "nothing has been emitted yet", "run Emit first \u2014 observe reads the carveout emit writes");
4903
+ }
4904
+ const args = ["lifecycle", "diff", "prod", "--live", "--json"];
4905
+ const run3 = await runChantRaw(args, demo.out, { AWS_ENDPOINT_URL: demo.live.endpoint }).catch((err) => ({
4906
+ code: 127,
4907
+ stdout: "",
4908
+ stderr: err instanceof Error ? err.message : String(err)
4909
+ }));
4910
+ if (run3.code !== 0) {
4911
+ return refuse2(
4912
+ "carve-action",
4913
+ `chant lifecycle diff exited ${run3.code}: ${merge(run3, demo.root) || "(no output)"}`,
4914
+ "Is the scratch Floci still up? `docker ps` should list " + demo.live.container + "."
4915
+ );
4916
+ }
4917
+ let aws;
4918
+ try {
4919
+ aws = JSON.parse(run3.stdout).lexicons?.aws ?? {};
4920
+ } catch {
4921
+ return refuse2("carve-action", "chant lifecycle diff answered something that isn't JSON", "re-run \u2014 a partial read is not a verdict");
4922
+ }
4923
+ const entity = select.split(".").pop() ?? select;
4924
+ const command = `AWS_ENDPOINT_URL=${demo.live.endpoint} chant ${args.join(" ")} # in ${shortenIn(demo.out, demo.root)}`;
4925
+ const queried = aws.resources?.queried?.[entity];
4926
+ const meta = aws.observed?.[entity];
4927
+ if (meta) {
4928
+ return {
4929
+ ok: true,
4930
+ select,
4931
+ command,
4932
+ entity,
4933
+ verdict: "observed",
4934
+ ...meta.status ? { status: meta.status } : {},
4935
+ ...meta.ownership ? { ownership: meta.ownership } : {},
4936
+ ...meta.physicalId ? { physicalId: meta.physicalId } : {},
4937
+ ...queried ? { queried } : {}
4938
+ };
4939
+ }
4940
+ const hole = aws.resources?.unobserved?.find((u) => u.name === entity);
4941
+ if (hole) {
4942
+ return {
4943
+ ok: true,
4944
+ select,
4945
+ command,
4946
+ entity,
4947
+ verdict: "unobserved",
4948
+ ...hole.detail || hole.reason ? { detail: [hole.reason, hole.detail].filter(Boolean).join(": ") } : {},
4949
+ ...queried ? { queried } : {}
4950
+ };
4951
+ }
4952
+ return { ok: true, select, command, entity, verdict: "missing", ...queried ? { queried } : {} };
4953
+ }
4799
4954
  var MAX_ARTIFACT_BYTES = 64 * 1024;
4800
4955
  var MAX_ARTIFACTS = 24;
4801
4956
  var MAX_OUTPUT_BYTES = 32 * 1024;
@@ -5126,6 +5281,11 @@ function statusTree(o) {
5126
5281
  if (!attrs) return void 0;
5127
5282
  return rec2(attrs.status) ?? attrs;
5128
5283
  }
5284
+ function specTree(o) {
5285
+ const attrs = rec2(o.attributes);
5286
+ if (!attrs) return void 0;
5287
+ return rec2(attrs.spec) ?? attrs;
5288
+ }
5129
5289
  var ARGO_HEALTH = {
5130
5290
  Healthy: "healthy",
5131
5291
  Degraded: "degraded",
@@ -5140,10 +5300,16 @@ var ARGO_SYNC = {
5140
5300
  Unknown: "unknown"
5141
5301
  };
5142
5302
  var ARGO_ERROR_CONDITION = /Error$/;
5303
+ var ARGO_ERROR_STRING = /^([A-Za-z]*Error)(?:=[^:]*)?: (.+)$/;
5143
5304
  function argoErrorMessage(tree) {
5144
5305
  const conditions = tree?.conditions;
5145
5306
  if (!Array.isArray(conditions)) return void 0;
5146
5307
  for (const c of conditions) {
5308
+ if (typeof c === "string") {
5309
+ const m = ARGO_ERROR_STRING.exec(c);
5310
+ if (m) return m[2];
5311
+ continue;
5312
+ }
5147
5313
  const type = str2(rec2(c)?.type);
5148
5314
  if (!type || !ARGO_ERROR_CONDITION.test(type)) continue;
5149
5315
  const message = str2(rec2(c)?.message);
@@ -5220,6 +5386,7 @@ function revisionVerdict(tree) {
5220
5386
  return { health: "progressing", detail: `applied ${applied}, attempted ${attempted}` };
5221
5387
  }
5222
5388
  var FLUX_TYPE_PREFIX = "K8s::Flux::";
5389
+ var FLUX_HELMREPOSITORY_TYPE = "K8s::Flux::HelmRepository";
5223
5390
  function fluxVerdict(o) {
5224
5391
  if (!o.type?.startsWith(FLUX_TYPE_PREFIX)) return void 0;
5225
5392
  const tree = statusTree(o);
@@ -5227,6 +5394,9 @@ function fluxVerdict(o) {
5227
5394
  if (ready?.status === "False") return { health: "degraded", detail: describeReady(ready) };
5228
5395
  if (ready?.status === "Unknown") return { health: "progressing", detail: describeReady(ready) };
5229
5396
  if (ready?.status === "True") return revisionVerdict(tree) ?? { health: "healthy", detail: describeReady(ready) };
5397
+ if (o.type === FLUX_HELMREPOSITORY_TYPE && str2(specTree(o)?.type) === "oci") {
5398
+ return { health: "healthy", detail: "spec.type=oci (nothing to reconcile)" };
5399
+ }
5230
5400
  const conditions = tree?.conditions;
5231
5401
  const reconciling = Array.isArray(conditions) && conditions.some((c) => {
5232
5402
  const cond = rec2(c);
@@ -5238,9 +5408,41 @@ function fluxVerdict(o) {
5238
5408
  }
5239
5409
  return void 0;
5240
5410
  }
5411
+ var DEPLOYMENT_TYPE = "K8s::Apps::Deployment";
5412
+ function unhappyCondition(o, type) {
5413
+ const fromStrings = rec2(o.attributes)?.conditions;
5414
+ if (Array.isArray(fromStrings)) {
5415
+ for (const c of fromStrings) {
5416
+ if (typeof c === "string" && c.startsWith(type)) return c;
5417
+ }
5418
+ }
5419
+ const tree = statusTree(o);
5420
+ const raw = tree?.conditions;
5421
+ if (Array.isArray(raw)) {
5422
+ for (const c of raw) {
5423
+ const cond = rec2(c);
5424
+ if (!cond || cond.type !== type) continue;
5425
+ const status = str2(cond.status);
5426
+ const unhappy = type === "ReplicaFailure" ? status === "True" : status === "False";
5427
+ if (!unhappy) continue;
5428
+ const reason = str2(cond.reason);
5429
+ const message = str2(cond.message);
5430
+ return `${type}${reason ? `=${reason}` : ""}${message ? `: ${message}` : ""}`;
5431
+ }
5432
+ }
5433
+ return void 0;
5434
+ }
5435
+ function deploymentVerdict(o) {
5436
+ if (o.type !== DEPLOYMENT_TYPE) return void 0;
5437
+ const damage = unhappyCondition(o, "ReplicaFailure") ?? unhappyCondition(o, "Progressing");
5438
+ if (damage) return { health: "degraded", detail: damage };
5439
+ const available = unhappyCondition(o, "Available");
5440
+ if (available) return { health: "progressing", detail: available };
5441
+ return void 0;
5442
+ }
5241
5443
  function classifyObservedHealth(observed) {
5242
5444
  if (!observed) return { health: "unknown" };
5243
- return argoVerdict(observed) ?? fluxVerdict(observed) ?? { health: classifyHealth(observed.status) };
5445
+ return argoVerdict(observed) ?? fluxVerdict(observed) ?? deploymentVerdict(observed) ?? { health: classifyHealth(observed.status) };
5244
5446
  }
5245
5447
 
5246
5448
  // src/apply.ts
@@ -5371,9 +5573,14 @@ var OpRunner = class {
5371
5573
  * Start `chant run <name>` unless one is already running (the Sync/Adopt/auto-
5372
5574
  * sync path). `cwd` is the Op's own project dir (#31 multi-estate); defaults to
5373
5575
  * the primary. Returns true if it started, false if busy.
5576
+ *
5577
+ * `temporal` — pass `--temporal` so the run gets the durable runtime. Callers
5578
+ * set it from the Op's declared gate: chant refuses a gated Op outright in
5579
+ * local mode ("gates and schedules need a durable runtime"), so without the
5580
+ * flag a gated Op could never run from behold at all.
5374
5581
  */
5375
- trigger(name, opEnv, cwd) {
5376
- return this.start(["run", name], name, opEnv, cwd);
5582
+ trigger(name, opEnv, cwd, temporal) {
5583
+ return this.start(temporal ? ["run", name, "--temporal"] : ["run", name], name, opEnv, cwd);
5377
5584
  }
5378
5585
  /**
5379
5586
  * Run an arbitrary `chant` invocation through the same guard/stream/PR/capture
@@ -5766,13 +5973,31 @@ async function openRollbackBranches(projectDir, env) {
5766
5973
 
5767
5974
  // src/estate.ts
5768
5975
  import { statSync as statSync3 } from "node:fs";
5976
+ import { availableParallelism } from "node:os";
5769
5977
  import { join as join13, resolve as resolve3, sep as sep2 } from "node:path";
5770
5978
  import { composeStacks, shortStackNames } from "@intentius/pinhole";
5979
+ function estateReadPool(members, env = process.env) {
5980
+ const override = Number.parseInt(env.BEHOLD_ESTATE_CONCURRENCY ?? "", 10);
5981
+ const cap = Number.isInteger(override) && override >= 1 ? override : Math.min(4, availableParallelism());
5982
+ return Math.max(1, Math.min(cap, members));
5983
+ }
5984
+ async function mapPool(items, width, fn) {
5985
+ const results = new Array(items.length);
5986
+ let next = 0;
5987
+ const worker = async () => {
5988
+ for (let i = next++; i < items.length; i = next++) {
5989
+ results[i] = await fn(items[i], i);
5990
+ }
5991
+ };
5992
+ await Promise.all(Array.from({ length: Math.max(1, Math.min(width, items.length)) }, () => worker()));
5993
+ return results;
5994
+ }
5771
5995
  async function composeEstate(projectDirs, opts = {}) {
5772
5996
  const names = shortStackNames(projectDirs);
5773
- const stacks = await Promise.all(
5774
- projectDirs.map(async (dir, i) => ({ name: names[i], ir: await graphIr(dir, opts) }))
5775
- );
5997
+ const stacks = await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => ({
5998
+ name: names[i],
5999
+ ir: await graphIr(dir, opts)
6000
+ }));
5776
6001
  return composeStacks(stacks);
5777
6002
  }
5778
6003
  var firstLine2 = (e) => {
@@ -5866,7 +6091,11 @@ function joinNamespaceBindings(members, isDir = realIsDir) {
5866
6091
  async function estateNamespaceScopes(projectDirs, opts, isDir = realIsDir) {
5867
6092
  if (projectDirs.length < 2) return /* @__PURE__ */ new Map();
5868
6093
  const { live: _live, overlay: _overlay, namespace: _namespace, ...src } = opts;
5869
- const irs = await Promise.all(projectDirs.map((dir) => graphIr(dir, { ...src, detail: 3 }).catch(() => void 0)));
6094
+ const irs = await mapPool(
6095
+ projectDirs,
6096
+ estateReadPool(projectDirs.length),
6097
+ (dir) => graphIr(dir, { ...src, detail: 3 }).catch(() => void 0)
6098
+ );
5870
6099
  const scopes = /* @__PURE__ */ new Map();
5871
6100
  for (const j of joinNamespaceBindings(projectDirs.map((dir, i) => ({ dir, ir: irs[i] })), isDir)) {
5872
6101
  if (!meetsFloor(resolveChant(j.dir).version, NAMESPACE_JOIN_FLOOR)) continue;
@@ -5885,28 +6114,26 @@ async function composeEstateOverlay(projectDirs, opts, classify) {
5885
6114
  const joined = [];
5886
6115
  const stacks = new Array(projectDirs.length);
5887
6116
  const scopes = await estateNamespaceScopes(projectDirs, opts);
5888
- await Promise.all(
5889
- projectDirs.map(async (dir, i) => {
5890
- const name = names[i];
5891
- const namespace = scopes.get(dir);
6117
+ await mapPool(projectDirs, estateReadPool(projectDirs.length), async (dir, i) => {
6118
+ const name = names[i];
6119
+ const namespace = scopes.get(dir);
6120
+ try {
6121
+ const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
6122
+ stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
6123
+ if (namespace) joined.push({ name, namespace });
6124
+ } catch (err) {
6125
+ const reason = firstLine2(err);
5892
6126
  try {
5893
- const live = { ...opts, live: true, overlay: true, ...namespace ? { namespace } : {} };
5894
- stacks[i] = { name, ir: namespaceRuntimeOwners(name, classify(await graphIr(dir, live))) };
5895
- if (namespace) joined.push({ name, namespace });
5896
- } catch (err) {
5897
- const reason = firstLine2(err);
5898
- try {
5899
- const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
5900
- const src = await graphIr(dir, srcOpts);
5901
- for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
5902
- stacks[i] = { name, ir: src };
5903
- unobserved.push({ name, reason });
5904
- } catch (err2) {
5905
- dropped.push({ name, reason: firstLine2(err2) });
5906
- }
6127
+ const { env: _env, live: _live, overlay: _overlay, ...srcOpts } = opts;
6128
+ const src = await graphIr(dir, srcOpts);
6129
+ for (const n of src.nodes) n.attrs = { ...n.attrs, _status: "neutral", _unobserved: reason };
6130
+ stacks[i] = { name, ir: src };
6131
+ unobserved.push({ name, reason });
6132
+ } catch (err2) {
6133
+ dropped.push({ name, reason: firstLine2(err2) });
5907
6134
  }
5908
- })
5909
- );
6135
+ }
6136
+ });
5910
6137
  const present = stacks.filter((s) => !!s);
5911
6138
  return {
5912
6139
  ir: composeStacks(present),
@@ -5953,6 +6180,12 @@ function watchSource(projectDir, onChange, debounceMs = 200) {
5953
6180
  watcher.close();
5954
6181
  };
5955
6182
  }
6183
+ function watchSources(projectDirs, onChange, debounceMs = 200) {
6184
+ const stops = projectDirs.map((dir) => watchSource(dir, () => onChange(dir), debounceMs));
6185
+ return () => {
6186
+ for (const stop of stops) stop();
6187
+ };
6188
+ }
5956
6189
 
5957
6190
  // src/poll.ts
5958
6191
  function digestOf(nodes) {
@@ -5980,18 +6213,22 @@ function changedLexicons(prev, next) {
5980
6213
  }
5981
6214
  function startDriftPoll(opts) {
5982
6215
  let stopped = false;
5983
- let last;
6216
+ const last = /* @__PURE__ */ new Map();
5984
6217
  let timer;
5985
6218
  const tick = async () => {
5986
- try {
5987
- const digests = driftDigestsByLexicon(await opts.query());
5988
- if (last !== void 0) {
5989
- const moved = changedLexicons(last, digests);
5990
- if (moved.length) opts.onChange(moved);
6219
+ for (const member of opts.members) {
6220
+ if (stopped) return;
6221
+ try {
6222
+ const digests = driftDigestsByLexicon(await member.query());
6223
+ const prev = last.get(member.dir);
6224
+ if (prev !== void 0) {
6225
+ const moved = changedLexicons(prev, digests);
6226
+ if (moved.length) opts.onChange(member.dir, moved);
6227
+ }
6228
+ last.set(member.dir, digests);
6229
+ } catch (err) {
6230
+ opts.onError?.(member.dir, err);
5991
6231
  }
5992
- last = digests;
5993
- } catch (err) {
5994
- opts.onError?.(err);
5995
6232
  }
5996
6233
  if (!stopped) timer = setTimeout(tick, opts.intervalMs);
5997
6234
  };
@@ -6565,6 +6802,7 @@ function carveRoutes(app, reportPath, demo) {
6565
6802
  };
6566
6803
  app.post("/api/carve/emit", (c) => runStep2(c, runCarveEmit));
6567
6804
  app.post("/api/carve/bridge", (c) => runStep2(c, runCarveBridge));
6805
+ app.post("/api/carve/observe", (c) => runStep2(c, runCarveObserve));
6568
6806
  app.post("/api/carve/plan", async (c) => {
6569
6807
  const block = demoBlock();
6570
6808
  if (block) {
@@ -6641,7 +6879,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6641
6879
  if (!info) {
6642
6880
  return c.json({ error: `no Op named "${name}" in the estate` }, 404);
6643
6881
  }
6644
- if (!runner.trigger(name, info.env, info.dir)) {
6882
+ if (!runner.trigger(name, info.env, info.dir, Boolean(info.gate))) {
6645
6883
  return c.json({ error: `an Op is already running (${runner.running})` }, 409);
6646
6884
  }
6647
6885
  return c.json({ started: true, name });
@@ -6686,7 +6924,7 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
6686
6924
  const { name, gate } = c.req.param();
6687
6925
  const info = estateOps().find((o) => o.name === name);
6688
6926
  broadcaster.emit("op", `\u270E signal ${name} ${gate}`);
6689
- const { code, stderr } = await runChantRaw(["run", "signal", name, gate], info?.dir ?? cfg.projectDir);
6927
+ const { code, stderr } = await runChantRaw(["run", "signal", name, gate, "--temporal"], info?.dir ?? cfg.projectDir);
6690
6928
  if (code !== 0) return c.json({ error: stderr.trim() || `signal exited ${code}` }, 500);
6691
6929
  return c.json({ signalled: true });
6692
6930
  });
@@ -7428,17 +7666,18 @@ async function startServer(cfg) {
7428
7666
  const app = createApp(cfg, broadcaster, frames, runner);
7429
7667
  const autoSync = cfg.autoSync ?? "off";
7430
7668
  const capture2 = () => captureFrame(cfg.projectDir, cfg.env, frames, broadcaster);
7431
- const onEstateChange = () => {
7432
- broadcaster.emit("changed");
7669
+ const onEstateChange = (memberDir) => {
7670
+ broadcaster.emit("changed", memberDir ?? "");
7433
7671
  void capture2();
7434
7672
  };
7435
- const onPollDrift = (movedLexicons) => {
7436
- onEstateChange();
7673
+ const memberTag = (dir) => (cfg.projectDirs?.length ?? 0) > 1 ? ` [${basename(dir)}]` : "";
7674
+ const onPollDrift = (dir, movedLexicons) => {
7675
+ onEstateChange(dir);
7437
7676
  if (autoSync === "off") return;
7438
- void routeAutoSync(movedLexicons);
7677
+ void routeAutoSync(dir, movedLexicons);
7439
7678
  };
7440
- const routeAutoSync = async (movedLexicons) => {
7441
- const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(cfg.projectDir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
7679
+ const routeAutoSync = async (dir, movedLexicons) => {
7680
+ const suspended = autoSync === "pull-request" ? suspendedByRollback(await openRollbackBranches(dir, cfg.env), movedLexicons) : /* @__PURE__ */ new Set();
7442
7681
  const { picks, declined } = pickAutoSyncOps(
7443
7682
  autoSync,
7444
7683
  discoverEstateOps(cfg.projectDirs ?? [cfg.projectDir]),
@@ -7447,31 +7686,37 @@ async function startServer(cfg) {
7447
7686
  suspended
7448
7687
  );
7449
7688
  for (const d of declined) {
7450
- broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) declined ${d.lexicon}: ${d.reason}`);
7689
+ broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} declined ${d.lexicon}: ${d.reason}`);
7451
7690
  }
7452
7691
  for (const { op, lexicons } of picks) {
7453
7692
  const scope = lexicons.join("+");
7454
- if (runner.trigger(op.name, op.env, op.dir)) {
7455
- broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) ${scope} \u2192 ${op.name}`);
7693
+ if (runner.trigger(op.name, op.env, op.dir, Boolean(op.gate))) {
7694
+ broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} ${scope} \u2192 ${op.name}`);
7456
7695
  } else {
7457
- broadcaster.emit("op", `\u27F3 auto-sync (${autoSync}) ${scope} \u2192 ${op.name} waiting \u2014 ${runner.running} is running`);
7696
+ broadcaster.emit("op", `\u27F3 auto-sync (${autoSync})${memberTag(dir)} ${scope} \u2192 ${op.name} waiting \u2014 ${runner.running} is running`);
7458
7697
  }
7459
7698
  }
7460
7699
  };
7461
7700
  const carve = !!cfg.carveReport;
7462
7701
  let stopWatch = carve ? () => {
7463
- } : watchSource(cfg.projectDir, onEstateChange);
7702
+ } : watchSources(cfg.projectDirs ?? [cfg.projectDir], onEstateChange);
7464
7703
  let stopPoll = !carve && cfg.env && cfg.pollSecs ? startDriftPoll({
7465
7704
  intervalMs: cfg.pollSecs * 1e3,
7466
- query: () => graphIr(cfg.projectDir, { live: true, overlay: true, env: cfg.env }),
7705
+ // Per-member queries, swept sequentially inside the poll (#297): a
7706
+ // member read can take seconds (#295), so an estate-wide tick must
7707
+ // stretch, not stampede N describes at once.
7708
+ members: (cfg.projectDirs ?? [cfg.projectDir]).map((dir) => ({
7709
+ dir,
7710
+ query: () => graphIr(dir, { live: true, overlay: true, env: cfg.env })
7711
+ })),
7467
7712
  onChange: onPollDrift,
7468
- onError: (err) => process.stderr.write(`poll: ${err instanceof Error ? err.message : String(err)}
7713
+ onError: (dir, err) => process.stderr.write(`poll${memberTag(dir)}: ${err instanceof Error ? err.message : String(err)}
7469
7714
  `)
7470
7715
  }) : () => {
7471
7716
  };
7472
7717
  cfg.onProjectSwitch = (dir) => {
7473
7718
  stopWatch();
7474
- stopWatch = watchSource(dir, onEstateChange);
7719
+ stopWatch = watchSources(cfg.projectDirs ?? [dir], onEstateChange);
7475
7720
  stopPoll();
7476
7721
  stopPoll = () => {
7477
7722
  };
@@ -7545,7 +7790,7 @@ async function startServer(cfg) {
7545
7790
 
7546
7791
  // src/export.ts
7547
7792
  import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, copyFileSync, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "node:fs";
7548
- import { join as join17, dirname as dirname5, basename } from "node:path";
7793
+ import { join as join17, dirname as dirname5, basename as basename2 } from "node:path";
7549
7794
  import { fileURLToPath as fileURLToPath4 } from "node:url";
7550
7795
  var LENS_PARAMS = ["components", "detail", "env", "logical", "radial", "tier"];
7551
7796
  function canonicalKey(path, params) {
@@ -7596,7 +7841,7 @@ function webDir() {
7596
7841
  return join17(dirname5(fileURLToPath4(import.meta.url)), "..", "web");
7597
7842
  }
7598
7843
  function workerName(project, override) {
7599
- const raw = override ?? `behold-${basename(project)}`;
7844
+ const raw = override ?? `behold-${basename2(project)}`;
7600
7845
  const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
7601
7846
  return name || "behold-export";
7602
7847
  }
@@ -7,7 +7,7 @@
7
7
  "build": "chant build src -o manifests/app.yaml --format yaml"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.44.7",
11
- "@intentius/chant-lexicon-k8s": "^0.44.7"
10
+ "@intentius/chant": "^0.44.8",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.8"
12
12
  }
13
13
  }
@@ -7,7 +7,7 @@
7
7
  "build": "chant build src -o manifests/app.yaml --format yaml"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.44.7",
11
- "@intentius/chant-lexicon-k8s": "^0.44.7"
10
+ "@intentius/chant": "^0.44.8",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.8"
12
12
  }
13
13
  }
@@ -7,7 +7,7 @@
7
7
  "build": "chant build src -o dist/control-plane.yaml --format yaml"
8
8
  },
9
9
  "dependencies": {
10
- "@intentius/chant": "^0.44.7",
11
- "@intentius/chant-lexicon-k8s": "^0.44.7"
10
+ "@intentius/chant": "^0.44.8",
11
+ "@intentius/chant-lexicon-k8s": "^0.44.8"
12
12
  }
13
13
  }