@intentius/behold 0.13.1 → 0.15.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/AGENTS.md CHANGED
@@ -54,6 +54,13 @@ preview-locked, and one load runs at a time (409 otherwise).
54
54
  per-node observed state, drift and field ownership; `/api/reconcile?env=`
55
55
  summarizes the pending change set; `/api/substrates` reports substrate
56
56
  readiness; `/api/events` (SSE) pushes `changed`/`op`/`apply`/`run`/`pr`.
57
+ With `?components=1&env=`, each component node carries `_liveStatus` (the
58
+ `chant components status` verdict) and, when the release ledger recorded
59
+ one, `_release` — `{runId, forge, originSource, url?, gitSha, digest,
60
+ timestamp, actor, approver?}` (#165). `url` is present only when the record
61
+ itself carries an address (chant#2045); `originSource: "inferred"` means
62
+ behold read the id's spelling and nothing more, and a `forge: "unknown"` id
63
+ is never resolved to a link on behold's guess.
57
64
  `?ops=1` is the ops lens — the project's declared Ops as a phase track, read
58
65
  from each emitted `dist/ops/<name>/op.json`, with the current run painted
59
66
  over it (`meta.run`, `meta.gate`); `/api/ops/<name>/status` reads that Op's
@@ -164,13 +171,35 @@ and nothing else about the run is behold's to decide: a stream that dies leaves
164
171
  the playhead at the last settled step and says so, and a gate paints as pending
165
172
  only when chant's `gateState` query named it.
166
173
 
167
- ## The operating loop (a strip, and a gate that is not the run gate)
174
+ ## The operating loop (a strip, a timeline, and a gate that is not the run gate)
168
175
 
169
176
  A project that declares a `ConvergeOp` gets an operator strip on the ops lens:
170
177
  per loop, the last tick's own log line (verbatim, with its instant), the lease,
171
178
  and how many gates are pending. **One** tick — `chant operator status` keeps only
172
- `records.at(-1)` and no CLI exposes the history (chant#2029), so this is a strip
173
- and never a timeline; the statusbar says so rather than leaving you to assume.
179
+ `records.at(-1)` — so the strip is never a timeline, and it says so.
180
+
181
+ The timeline is its own read: `GET /api/operator/log` → `chant operator log
182
+ --json` (chant#2029), the tick records and the gate resolutions against them
183
+ merged oldest-first. It lives in a panel that grows from the strip, and two rules
184
+ hold it there. It is **pulled**, on the click that opens it and never on a timer.
185
+ And it is **bounded** — `--limit` on every invocation, 50 by default and 200 at
186
+ most, `--since` when asked — because the ledger grows one line per tick forever;
187
+ the answer reports the window it used, so a full one can't read as the end of
188
+ history. The route checks the project's chant version *before* it spawns: below
189
+ 0.53.1, `chant operator log` resolves to `chant operator`, the tick daemon, and
190
+ behold reading a history must never become behold running an operator.
191
+
192
+ Since chant 0.53.1 (chant#2027) a tick record carries an `id` and the
193
+ per-component verdicts it derived, and both ride through to
194
+ `[].lastTick.{id,components}`. The strip line names the tick (truncated), and
195
+ the verdicts join onto the component DAG by component name — the same key the
196
+ live `chant components status` read is joined by. They join **under** that read:
197
+ a tick only ever feeds the last tier of `componentStatusColor` (the
198
+ reconciliation verdict), only on a node the live read left unpainted, and only
199
+ while the tick is younger than fifteen of chant's own operator rounds. Past
200
+ that it is named on the node, dated, and painted nothing — a graph fill has
201
+ nowhere to put "as of an hour ago". A chant older than 0.53.1 sends neither
202
+ field and every one of these paths is a no-op.
174
203
 
175
204
  Two gate cards exist and they are **different acts**:
176
205
 
@@ -182,10 +211,16 @@ Two gate cards exist and they are **different acts**:
182
211
  resolution or not. The next tick reads the fact. The card says this in its
183
212
  body, and the toast after the click says "recorded; the next tick acts on it".
184
213
 
185
- A pending gate fact carries no URL (chant#2028), so the card renders no link and
186
- no placeholder for one. A gate someone already resolved simply leaves
187
- `pendingGates` — the status read carries no `resolvedBy`/`when` — so behold shows
188
- no resolved-by line rather than attributing an approval to nobody.
214
+ A gate fact carries its address since chant#2028, on both halves, and the card
215
+ links it under chant's own words — `approve at: <url>`. The field is optional by
216
+ design (a local tick with no PR behind it has none) and never synthesized, so a
217
+ fact without one renders no link and no placeholder for one, and a url behold
218
+ would not put behind an `href` reads as no address at all.
219
+
220
+ A gate someone already resolved simply leaves `pendingGates` — the status read
221
+ carries no `resolvedBy`/`when` — so the strip shows no resolved-by line rather
222
+ than attributing an approval to nobody. The timeline is where resolutions are
223
+ named, because there they are records rather than an inference.
189
224
 
190
225
  An `OperatorStack` (chant#1940) declares the loop as a k8s estate, so it already
191
226
  appears in the entity graph. behold names it there — the Namespace as the loop's
@@ -229,3 +264,39 @@ own Terraform is not edited either. The only request-derived value is `select`,
229
264
  and it must be an address the served report already ranks — the value that
230
265
  reaches the spawn's argv comes from a closed set read off disk. No cloud write,
231
266
  no Terraform mutation, no edit to anyone's chant source.
267
+
268
+ ## Changing behold (for agents working on this repo)
269
+
270
+ The sections above are about driving a running behold. This one is about
271
+ changing it — the working rules that used to live in a handoff note and now
272
+ live here, with the enforceable ones enforced.
273
+
274
+ - **Branches and merges.** Work on a branch off `main` (an isolated worktree
275
+ when more than one agent is active). `main` is protected by a ruleset: every
276
+ change arrives as a PR, and the `check` job in `ci.yml` must be green before
277
+ it merges. A check that has merely settled is not a pass — read the lines.
278
+ No `gh pr merge --auto`; merge one PR at a time, in order, and rebase the
279
+ next onto what landed.
280
+ - **The local gate** is `just check` (tsc, tests, build). `just test` keeps
281
+ the reporter's full output in `vitest.log`, because #334's one-shot
282
+ first-run failures kept losing the failing file's name — if the suite fails
283
+ once and passes on rerun, the log from the first run is the evidence; attach
284
+ it to #334.
285
+ - **Releasing** is `just release`, after the version-bump PR merged: it tags
286
+ `behold-v<version>`, pushes the tag (which is what runs `release.yml`), and
287
+ waits for npm to show the version. It refuses in every state where a tag
288
+ push would be wrong. Never re-push a `behold-v*` tag by hand — a tag push
289
+ re-fires publish. Tags absent from origin (behold-v0.10.0, 0.10.1) stay
290
+ absent for that reason.
291
+ - **Scratch infrastructure** is governed by `src/scratch.ts`, asserted by
292
+ `scratch.test.ts` across every boot site: anything behold boots is named
293
+ `behold-*`, refuses if the name is taken, tears down only what it created,
294
+ and never binds the shared emulator ports. The protected names — `floci*`,
295
+ `chant-floci*`, the kubemicrovm and fountain k3d clusters, :4566 — are
296
+ listed there, not in prose. A new boot site calls `assertScratch` before
297
+ its spawn and gets a row in the test.
298
+ - **The write boundary** is the Invariant section above. Apply is never a
299
+ button; `/api/carve/apply` does not exist and the route test asserts it
300
+ stays that way.
301
+ - **chant's working rules** (its single-lane checkout, its release path) are
302
+ chant's and live in chant's repo, not here.