tldr-experts 0.5.0 → 0.6.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.
Files changed (30) hide show
  1. package/CHANGELOG.md +792 -0
  2. package/README.md +19 -5
  3. package/dist/hooks/answer-capture.js +136 -12
  4. package/dist/hooks/budget-gate.js +9 -9
  5. package/dist/hooks/{chunk-889tybxc.js → chunk-17gv74sr.js} +3 -3
  6. package/dist/hooks/{chunk-3w55tp71.js → chunk-5k4dggq0.js} +51 -17
  7. package/dist/hooks/{chunk-14zn51kh.js → chunk-7eqhfddw.js} +28 -569
  8. package/dist/hooks/{chunk-nadqsr3w.js → chunk-97ncnegd.js} +1 -1
  9. package/dist/hooks/{chunk-a6rpj2cp.js → chunk-9w64mxdh.js} +345 -22
  10. package/dist/hooks/{chunk-a5dq2dcp.js → chunk-dhbxzjfs.js} +2 -4
  11. package/dist/hooks/{chunk-hcrbr430.js → chunk-g4db8rbs.js} +2 -30
  12. package/dist/hooks/chunk-h7151g8w.js +803 -0
  13. package/dist/hooks/{chunk-5556vjt5.js → chunk-ka6bkb64.js} +1 -1
  14. package/dist/hooks/chunk-rrkdfk7s.js +30 -0
  15. package/dist/hooks/{chunk-qw73rdbr.js → chunk-v4ay50nq.js} +1 -1
  16. package/dist/hooks/{chunk-v1c1hpb8.js → chunk-vrhczzxs.js} +1 -1
  17. package/dist/hooks/{chunk-bvm6vjrt.js → chunk-x6n8b5fp.js} +1 -1
  18. package/dist/hooks/claim-sources.js +8 -10
  19. package/dist/hooks/dod-gate.js +7 -7
  20. package/dist/hooks/no-reask.js +8 -9
  21. package/dist/hooks/session-start.js +23 -18
  22. package/dist/hooks/statusline.js +10 -10
  23. package/dist/tldrx.js +2539 -1006
  24. package/env.yml +9 -2
  25. package/package.json +1 -1
  26. package/plugin/.claude-plugin/plugin.json +1 -1
  27. package/plugin/skills/tldrx/SKILL.md +8 -1
  28. package/templates/questions.md +8 -0
  29. package/dist/hooks/chunk-q8d3sff9.js +0 -220
  30. package/dist/hooks/chunk-sae7sqty.js +0 -206
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/tldr-experts?label=npm%20tldr-experts)](https://www.npmjs.com/package/tldr-experts) [![ci](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml/badge.svg)](https://github.com/ederwii/tldr-experts/actions/workflows/ci.yml) ![status](https://img.shields.io/badge/status-beta-blue)
4
4
 
5
- **An evidence-first, file-based AI development framework: five stages, a gate on every one, and every claim cited or refused.** Open source, tool-agnostic in design, piloted on Claude Code. **Beta:** every command is implemented and verified by running it, the `version: 1` file formats only grow from here, and `tldrx --help` is the authoritative command surface.
5
+ **An evidence-first, file-based AI development framework: five stages, a gate on every one, and every claim cited or refused.** Open source. The workflow and the persisted state format are provider-independent; the automated runner currently supports Claude Code. **Beta:** every command is implemented and verified by running it, the `version: 1` file formats only grow from here, and `tldrx --help` is the authoritative command surface.
6
6
 
7
7
  One loop — *Investigate → Handoff → Interview → Gate* — five phases, **what · how · plan · build ·
8
8
  watch**, one stage per command, each stopping at a gate you own; the files ARE the state, the
@@ -41,8 +41,10 @@ tldrx run new payments --scope feature --seed docs/payments/ --budget 25
41
41
  tldrx run auto # `next`, over and over, until something actually needs you
42
42
  ```
43
43
 
44
- **Runtime: Node ≥ 20 or Bun** — the published package is a pre-built bundle with zero runtime
45
- dependencies, so an installed `tldrx` needs only Node; Bun builds it. Full walkthrough:
44
+ **To run it: Node ≥ 20, and nothing else** — the published package is a pre-built bundle with
45
+ zero runtime dependencies, and `dist/` is a Node bundle (Bun runs it too, if you have it).
46
+ **To build or contribute: Bun ≥ 1.3**, which compiles that bundle and runs the test suite.
47
+ `tldrx doctor` is the authority on the rest. Full walkthrough:
46
48
  [`docs/guide/01-quick-start.md`](docs/guide/01-quick-start.md).
47
49
 
48
50
  ## Trying it: three ways to run
@@ -247,14 +249,25 @@ their answers, `run.yml`, `budget.yml`, `events.jsonl`, the handoffs, the plan
247
249
  the repo gets the run. The block `tldrx init` appends to `.gitignore` excludes eight paths and nothing
248
250
  else, because those eight are machine-local, regenerated, or a backup git already holds the history
249
251
  of: `.tldrx/graphify-out/`, `.tldrx/cache/`, `.tldrx/worktrees/`, `tldrx-work/*/.lock`,
250
- `tldrx-work/*/.agent/`, `tldrx-work/*/*.bak`, `.tldrx/memory/*.bak` and
252
+ `tldrx-work/*/.agent/`, `tldrx-work/**/*.bak`, `.tldrx/**/*.bak` and
251
253
  `.claude/settings.json.bak-tldrx-*`.
252
254
 
255
+ **You do not have to commit them by hand.** Closing a run — `tldrx approve` on the last gate,
256
+ `tldrx next`, or `tldrx run cancel` — commits `tldrx-work/<run>/` and `.tldrx/memory/` in the
257
+ workspace checkout, on the branch that checkout is on, and prints one line saying where they went.
258
+ Only those paths: anything else you had staged is still staged afterwards. It is deliberately never
259
+ the epic branch — an epic under review carries feature code, and run state on it collides with the
260
+ same live files in your working tree the moment the PR merges, which is why `tldrx ship` refuses an
261
+ epic that carries any (#102).
262
+
253
263
  ## Documentation
254
264
 
255
265
  **[The documentation site](https://ederwii.github.io/tldr-experts/)** is the place to start if you have
256
266
  never used this: a landing page, a Quickstart and one short page per concept, written for a reader
257
- rather than for an agent. Source in [`docs-site/`](docs-site/).
267
+ rather than for an agent. Source in [`docs-site/`](docs-site/). It carries a
268
+ **[live demo of `tldrx dashboard`](https://ederwii.github.io/tldr-experts/demo)** — a real export,
269
+ rendered from the test suite's synthetic fixtures on every deploy, so you can see what the tool
270
+ draws before installing anything.
258
271
 
259
272
  The reference guide, in `docs/guide/`: [1 Quick start](docs/guide/01-quick-start.md) ·
260
273
  [2 The loop](docs/guide/02-the-loop.md) (the four steps, what a stage file controls, the two execution modes) ·
@@ -285,6 +298,7 @@ back on the registry is 0.3.0.
285
298
 
286
299
  | Version | Date | Status | Contains |
287
300
  |---|---|---|---|
301
+ | 0.6.0 | 2026-09-02 | `beta` | the dashboard suite — a "Now" hero strip, `--serve` live refresh, stage durations and gate notes on page and CLI, and a public demo generated from fixtures; honest dual-economy spend (metered + host, lower-bound named); gate provenance — `executed_by` + `authority` so a delegated signature never reads as a personal one, machine-signed reporting fixed; ONE `absent:` semantic shared by claim-sources and the auto gate; superseded stamps when an owner answer flips an earlier phase doc; run close commits its state and `ship` refuses a state-carrying epic; merge-wave gates `docs:build`, survives interruption and self-rewrite; a public-surface drift guard; env.yml validation (unique ids, tool cap) |
288
302
  | 0.5.0 | 2026-09-02 | `beta` | `tldrx drive` and its own preflight, `watch check` / `watch arm`, `questions cards`, `plan schema`, `retro --all` (with its findings fed back into every reviewer prompt) and `story reopen --for-fix`; `tldrx update` plus a cached newer-version notice; the dashboard reads `budget.yml` and `events.jsonl` — operator notes, reopens and retries, the per-phase budget panel, and a host-attended run metered in tokens against `ceiling_host_tokens`; a rejected review envelope no longer burns a story attempt; `ship` opens one PR per repo; merge-wave lock + ref guard; five golden-transcript evals, one per stage; `CONTRIBUTING.md` and a model-provider contract |
289
303
  | 0.4.0 | 2026-09-01 | `beta` | FIRST BETA — 40-issue hardening burn (DoD pre-flight + `plan sync-dod`, merge-wave lock + gated-HEAD, load-aware tests, claim-sources across all outputs), `tldrx learn` 8-chapter sandbox tutorial (cold-player QA), `tldrx ship` / `tldrx note` / `run gates set`, budget policies + dual-economy wiring, single integration branch for chained epics, epic worktrees live to run close, bilingual docs site |
290
304
  | 0.3.1 | 2026-08-31 | `alpha` | Unattended mode (gates_policy agent, review handshake, fixlist, decision cards, dual economy), 6 contact fixes from the first feature-scope runs, colored init, training repair round |
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FactsStore
4
- } from "./chunk-889tybxc.js";
4
+ } from "./chunk-17gv74sr.js";
5
5
  import {
6
6
  parseHookInput,
7
7
  readStdin
8
- } from "./chunk-v1c1hpb8.js";
8
+ } from "./chunk-vrhczzxs.js";
9
9
  import {
10
- EventLog
11
- } from "./chunk-sae7sqty.js";
12
- import"./chunk-qw73rdbr.js";
10
+ EventLog,
11
+ PHASE_ID_RE
12
+ } from "./chunk-h7151g8w.js";
13
+ import"./chunk-g4db8rbs.js";
14
+ import"./chunk-v4ay50nq.js";
13
15
  import {
14
16
  MAX_FACT_CHARS,
15
17
  detectAnswered,
@@ -17,15 +19,15 @@ import {
17
19
  recordAnswer,
18
20
  replaceBlock,
19
21
  serializeQuestions
20
- } from "./chunk-nadqsr3w.js";
22
+ } from "./chunk-97ncnegd.js";
21
23
  import"./chunk-39zh2e44.js";
22
24
  import {
23
25
  PROJECT_WORK_DIR,
24
26
  factsPath
25
- } from "./chunk-a6rpj2cp.js";
27
+ } from "./chunk-9w64mxdh.js";
26
28
 
27
29
  // src/hooks/answer-capture.ts
28
- import { existsSync as existsSync2 } from "fs";
30
+ import { existsSync as existsSync3 } from "fs";
29
31
 
30
32
  // src/hooks/lib/decide.ts
31
33
  function allow() {
@@ -97,7 +99,95 @@ function nowRfc3339() {
97
99
  }
98
100
 
99
101
  // src/core/answers/captureAnswers.ts
100
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
102
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "node:fs";
103
+
104
+ // src/core/answers/stampSuperseded.ts
105
+ import { appendFileSync, existsSync, readFileSync, statSync } from "node:fs";
106
+ import { isAbsolute as isAbsolute2, join as join2, relative } from "node:path";
107
+ var STAMP_MARKER = "tldrx:superseded";
108
+ var AFFECTS_KEY = "affects";
109
+ function isStamped(text, fact) {
110
+ return text.includes(`${STAMP_MARKER} ${fact} `);
111
+ }
112
+ function stampText(fact, q, questionsRel, at) {
113
+ return `<!-- ${STAMP_MARKER} ${fact} | q: ${q} | at: ${at} | see: ${questionsRel} -->
114
+ ` + `> **Superseded in part by ${fact}** — ${q} was answered after this document was written; ` + `the answer is in \`${questionsRel}\` and \`.tldrx/memory/facts.yml\`. ` + `This document was not reconciled: where the two disagree, the fact is what the workspace believes.
115
+ `;
116
+ }
117
+ function affectedDocs(runDir, questionsPath, block) {
118
+ const self = runRelative(runDir, questionsPath);
119
+ const askedIn = self === null ? null : phaseIdOf(self);
120
+ const found = new Map;
121
+ const consider = (raw, by) => {
122
+ const rel = resolveInRun(runDir, raw);
123
+ if (rel === null || rel === self || !rel.endsWith(".md"))
124
+ return;
125
+ const phase = phaseIdOf(rel);
126
+ if (phase === null)
127
+ return;
128
+ if (by === "cited" && (askedIn === null || !isEarlier(phase, askedIn)))
129
+ return;
130
+ if (!found.has(rel))
131
+ found.set(rel, { rel, abs: join2(runDir, rel), by });
132
+ };
133
+ for (const ref of block.whySrc?.refs ?? []) {
134
+ if (ref.kind === "file")
135
+ consider(ref.path, "cited");
136
+ }
137
+ for (const raw of declaredAffects(block))
138
+ consider(raw, "affects");
139
+ return [...found.values()];
140
+ }
141
+ function stampSuperseded(runDir, questionsPath, block, fact, at) {
142
+ const questionsRel = runRelative(runDir, questionsPath) ?? "questions.md";
143
+ const stamped = [];
144
+ for (const doc of affectedDocs(runDir, questionsPath, block)) {
145
+ const text = readFileSync(doc.abs, "utf8");
146
+ if (isStamped(text, fact))
147
+ continue;
148
+ const lead = text === "" ? "" : text.endsWith(`
149
+ `) ? `
150
+ ` : `
151
+
152
+ `;
153
+ appendFileSync(doc.abs, lead + stampText(fact, block.id, questionsRel, at), "utf8");
154
+ stamped.push(doc);
155
+ }
156
+ return stamped;
157
+ }
158
+ function declaredAffects(block) {
159
+ const raw = block.metadata?.extra.find(([key]) => key === AFFECTS_KEY)?.[1] ?? "";
160
+ return raw.split(/[,\s]+/).map((part) => part.trim()).filter((part) => part !== "");
161
+ }
162
+ function resolveInRun(runDir, raw) {
163
+ if (raw === "" || isAbsolute2(raw))
164
+ return null;
165
+ const root = join2(runDir, "..", "..");
166
+ for (const candidate of [join2(runDir, raw), join2(root, raw)]) {
167
+ const rel = runRelative(runDir, candidate);
168
+ if (rel === null)
169
+ continue;
170
+ if (!existsSync(candidate) || !statSync(candidate).isFile())
171
+ continue;
172
+ return rel;
173
+ }
174
+ return null;
175
+ }
176
+ function runRelative(runDir, path) {
177
+ const rel = relative(runDir, path);
178
+ if (rel === "" || rel.startsWith("..") || isAbsolute2(rel))
179
+ return null;
180
+ return rel.split(/[\\/]/).join("/");
181
+ }
182
+ function phaseIdOf(rel) {
183
+ const first = rel.split("/")[0] ?? "";
184
+ return PHASE_ID_RE.test(first) ? first : null;
185
+ }
186
+ function isEarlier(phase, than) {
187
+ return Number(phase.slice(0, 2)) < Number(than.slice(0, 2));
188
+ }
189
+
190
+ // src/core/answers/captureAnswers.ts
101
191
  var TRUNCATION_MARK = " …";
102
192
  function factTextFor(title, answer) {
103
193
  const whole = `${title} — ${answer}`;
@@ -109,14 +199,15 @@ function factWasTruncated(title, answer) {
109
199
  return `${title} — ${answer}`.length > MAX_FACT_CHARS;
110
200
  }
111
201
  function captureAnswers(questionsPath, ctx) {
112
- if (!existsSync(questionsPath))
202
+ if (!existsSync2(questionsPath))
113
203
  return [];
114
- let doc = parseQuestions(readFileSync(questionsPath, "utf8"));
204
+ let doc = parseQuestions(readFileSync2(questionsPath, "utf8"));
115
205
  const answered = detectAnswered(doc.blocks);
116
206
  if (answered.length === 0)
117
207
  return [];
118
208
  const log = EventLog.forRun(ctx.runDir);
119
209
  const captured = [];
210
+ const recorded = [];
120
211
  FactsStore.update(factsPath(ctx.root), (store) => {
121
212
  for (const block of answered) {
122
213
  const area = block.metadata?.area ?? "unscoped";
@@ -150,11 +241,44 @@ function captureAnswers(questionsPath, ctx) {
150
241
  payload: { fact: fact.id, area: fact.area, kind: fact.kind, q: block.id }
151
242
  });
152
243
  captured.push({ q: block.id, fact: fact.id, answer: block.answer, area });
244
+ recorded.push({ block, fact: fact.id });
153
245
  }
154
246
  });
155
247
  writeFileSync(questionsPath, serializeQuestions(doc), "utf8");
248
+ for (const item of recorded) {
249
+ markSuperseded(log, ctx, questionsPath, item.block, item.fact);
250
+ }
156
251
  return captured;
157
252
  }
253
+ function markSuperseded(log, ctx, questionsPath, block, fact) {
254
+ try {
255
+ for (const doc of stampSuperseded(ctx.runDir, questionsPath, block, fact, ctx.at)) {
256
+ log.tryAppend({
257
+ ts: ctx.at,
258
+ run: ctx.run,
259
+ stage: null,
260
+ type: "doc.superseded",
261
+ actor: ctx.actor,
262
+ cost_usd: 0,
263
+ payload: { doc: doc.rel, fact, q: block.id, by: doc.by }
264
+ });
265
+ }
266
+ } catch (error) {
267
+ log.tryAppend({
268
+ ts: ctx.at,
269
+ run: ctx.run,
270
+ stage: null,
271
+ type: "error",
272
+ actor: ctx.actor,
273
+ cost_usd: 0,
274
+ payload: {
275
+ message: `could not stamp the documents ${block.id} supersedes: ` + (error instanceof Error ? error.message : String(error)),
276
+ q: block.id,
277
+ fact
278
+ }
279
+ });
280
+ }
281
+ }
158
282
 
159
283
  // src/hooks/answer-capture.ts
160
284
  await runHook("answer-capture", async () => {
@@ -163,7 +287,7 @@ await runHook("answer-capture", async () => {
163
287
  if (event === "PostToolUse" && payload.tool_name !== "Write" && payload.tool_name !== "Edit")
164
288
  return;
165
289
  const filePath = filePathOf(payload);
166
- if (!filePath.endsWith("questions.md") || !existsSync2(filePath))
290
+ if (!filePath.endsWith("questions.md") || !existsSync3(filePath))
167
291
  return;
168
292
  const location = locateWork(filePath);
169
293
  if (location === null)
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  budgetGateDeny
4
- } from "./chunk-a5dq2dcp.js";
4
+ } from "./chunk-dhbxzjfs.js";
5
5
  import {
6
6
  allow,
7
7
  deny,
8
8
  readPayload,
9
9
  runHook,
10
10
  toolInput
11
- } from "./chunk-bvm6vjrt.js";
12
- import"./chunk-v1c1hpb8.js";
11
+ } from "./chunk-x6n8b5fp.js";
12
+ import"./chunk-vrhczzxs.js";
13
13
  import {
14
14
  asRunBudget,
15
15
  currentActor,
@@ -29,23 +29,23 @@ import {
29
29
  validateRunBudget,
30
30
  wouldExceed,
31
31
  wouldExceedHostTokens
32
- } from "./chunk-14zn51kh.js";
32
+ } from "./chunk-7eqhfddw.js";
33
33
  import {
34
34
  EventLog
35
- } from "./chunk-sae7sqty.js";
36
- import"./chunk-hcrbr430.js";
35
+ } from "./chunk-h7151g8w.js";
36
+ import"./chunk-rrkdfk7s.js";
37
+ import"./chunk-g4db8rbs.js";
37
38
  import {
38
39
  noteDeprecations
39
- } from "./chunk-nadqsr3w.js";
40
+ } from "./chunk-97ncnegd.js";
40
41
  import"./chunk-39zh2e44.js";
41
- import"./chunk-q8d3sff9.js";
42
42
  import {
43
43
  PROJECT_WORK_DIR,
44
44
  findWorkspaceRoot,
45
45
  locateWork,
46
46
  parseYaml,
47
47
  stageYamlPath
48
- } from "./chunk-a6rpj2cp.js";
48
+ } from "./chunk-9w64mxdh.js";
49
49
 
50
50
  // src/hooks/budget-gate.ts
51
51
  import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -3,7 +3,7 @@ import {
3
3
  withWorkspaceLock,
4
4
  workspaceRootOfFactsPath,
5
5
  writeAtomic
6
- } from "./chunk-qw73rdbr.js";
6
+ } from "./chunk-v4ay50nq.js";
7
7
  import {
8
8
  FACT_CONFIDENCES,
9
9
  FACT_KINDS,
@@ -14,7 +14,7 @@ import {
14
14
  isLive,
15
15
  isRetired,
16
16
  noteDeprecations
17
- } from "./chunk-nadqsr3w.js";
17
+ } from "./chunk-97ncnegd.js";
18
18
  import {
19
19
  asDocument,
20
20
  isRecord,
@@ -29,7 +29,7 @@ import {
29
29
  SRC_PATTERNS,
30
30
  parseYaml,
31
31
  readableSource
32
- } from "./chunk-a6rpj2cp.js";
32
+ } from "./chunk-9w64mxdh.js";
33
33
 
34
34
  // src/core/facts/FactsStore.ts
35
35
  import { existsSync, readFileSync } from "node:fs";
@@ -2,24 +2,24 @@ import {
2
2
  DEFAULT_ECONOMY,
3
3
  DEFAULT_ON_HOST_TOKENS_EXCEED,
4
4
  asRunBudget,
5
- asRunFile,
6
5
  cursorStage,
6
+ isAttendedByHostView,
7
+ nowRfc3339,
8
+ openRunViews,
9
+ validateRunBudget
10
+ } from "./chunk-7eqhfddw.js";
11
+ import {
12
+ EventLog,
13
+ asRunFile,
7
14
  derivePhaseStatus,
8
15
  deriveRunStatus,
9
16
  flatten,
10
17
  isAttendedByHost,
11
- isAttendedByHostView,
12
18
  isFinished,
13
19
  isTerminal,
14
- nowRfc3339,
15
- openRunViews,
16
20
  stageAt,
17
- validateRunBudget,
18
21
  validateRunFile
19
- } from "./chunk-14zn51kh.js";
20
- import {
21
- EventLog
22
- } from "./chunk-sae7sqty.js";
22
+ } from "./chunk-h7151g8w.js";
23
23
  import {
24
24
  backupPathFor,
25
25
  isAlive,
@@ -28,17 +28,17 @@ import {
28
28
  workspaceRootOfRunDir,
29
29
  writeAtomic,
30
30
  yamlScalar
31
- } from "./chunk-qw73rdbr.js";
31
+ } from "./chunk-v4ay50nq.js";
32
32
  import {
33
33
  noteDeprecations,
34
34
  openBlocks,
35
35
  parseQuestions
36
- } from "./chunk-nadqsr3w.js";
36
+ } from "./chunk-97ncnegd.js";
37
37
  import {
38
38
  listRunDirs,
39
39
  parseYaml,
40
40
  parseYamlRepairing
41
- } from "./chunk-a6rpj2cp.js";
41
+ } from "./chunk-9w64mxdh.js";
42
42
 
43
43
  // src/core/statusline/runSnapshot.ts
44
44
  import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
@@ -62,9 +62,18 @@ function gatesPolicy(policy) {
62
62
  function gateEvidence(e) {
63
63
  return `{path: ${yamlScalar(e.path)}, role: ${yamlScalar(e.role)}, verdict: ${yamlScalar(e.verdict)}, ` + `sampled: ${String(e.sampled)}, of: ${String(e.of)}, resolved: ${String(e.resolved)}, ` + `refuted: ${String(e.refuted)}, outside_surface: ${String(e.outside_surface)}}`;
64
64
  }
65
+ function gateExecutor(e) {
66
+ const id = e.id === undefined ? "" : `, id: ${yamlScalar(e.id)}`;
67
+ return `{type: ${yamlScalar(e.type)}${id}}`;
68
+ }
69
+ function gateAuthority(a) {
70
+ return `{type: ${yamlScalar(a.type)}, policy: ${yamlScalar(a.policy)}, ` + `authorized_by: ${yamlScalar(a.authorized_by)}, source: ${yamlScalar(a.source)}}`;
71
+ }
65
72
  function gate(g) {
66
73
  const evidence = g.evidence === undefined ? "" : `, evidence: ${gateEvidence(g.evidence)}`;
67
- return `{type: ${yamlScalar(g.type)}, status: ${yamlScalar(g.status)}, by: ${yamlScalar(g.by)}, ` + `at: ${yamlScalar(g.at)}, note: ${yamlScalar(g.note)}${evidence}}`;
74
+ const executor = g.executed_by === undefined ? "" : `, executed_by: ${gateExecutor(g.executed_by)}`;
75
+ const authority = g.authority === undefined ? "" : `, authority: ${gateAuthority(g.authority)}`;
76
+ return `{type: ${yamlScalar(g.type)}, status: ${yamlScalar(g.status)}, by: ${yamlScalar(g.by)}, ` + `at: ${yamlScalar(g.at)}, note: ${yamlScalar(g.note)}${evidence}${executor}${authority}}`;
68
77
  }
69
78
  function task(t, indent) {
70
79
  const inner = `${indent} `;
@@ -407,6 +416,32 @@ function round(n) {
407
416
  return Math.round(n * 100) / 100;
408
417
  }
409
418
 
419
+ // src/core/run/autoGate.ts
420
+ var AUTO_GATE_ACTOR = "auto";
421
+
422
+ // src/core/run/gateAuthority.ts
423
+ var UNRECORDED_AUTHORIZER = "unrecorded";
424
+ function closedByMachine(sig) {
425
+ const executed = sig.executed_by ?? null;
426
+ if (executed !== null)
427
+ return executed.type !== "human";
428
+ return sig.by === AUTO_GATE_ACTOR;
429
+ }
430
+ function describeGateSignature(sig) {
431
+ const by = sig.by ?? "?";
432
+ const executed = sig.executed_by ?? null;
433
+ if (executed === null)
434
+ return by;
435
+ const authority = sig.authority ?? null;
436
+ if (executed.type === "human" && (authority === null || authority.type === "direct"))
437
+ return by;
438
+ const who = executed.type === "auto" ? "auto" : `${executed.type} ${executed.id ?? by}`;
439
+ if (authority === null)
440
+ return who;
441
+ const granter = authority.authorized_by ?? UNRECORDED_AUTHORIZER;
442
+ return `${who} (${authority.type} by ${granter}, policy: ${authority.policy})`;
443
+ }
444
+
410
445
  // src/core/facilitator/skipIf.ts
411
446
  import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "node:fs";
412
447
  function phaseDirs(runDir) {
@@ -427,7 +462,6 @@ function openQuestionIds(path) {
427
462
  }
428
463
 
429
464
  // src/core/statusline/runSnapshot.ts
430
- var AUTO = "auto";
431
465
  function runSnapshot(root) {
432
466
  return fromStore(root) ?? fromTolerantRead(root);
433
467
  }
@@ -466,7 +500,7 @@ function fromStore(root) {
466
500
  ceilingUsd: run.budget.ceiling_usd,
467
501
  spentUsd: run.budget.spent_usd,
468
502
  openCount: open.length,
469
- autoGates: stages.filter((s) => s.gate.status === "approved" && s.gate.by === AUTO).length,
503
+ machineGates: stages.filter((s) => s.gate.status === "approved" && closedByMachine(s.gate)).length,
470
504
  staleStages: stages.filter((s) => s.stale === true).length,
471
505
  attendedByHost: isAttendedByHost(run),
472
506
  source: "run-store"
@@ -495,7 +529,7 @@ function fromTolerantRead(root) {
495
529
  ceilingUsd: budget.ceiling_usd,
496
530
  spentUsd: budget.spent_usd,
497
531
  openCount: open.length,
498
- autoGates: 0,
532
+ machineGates: 0,
499
533
  staleStages: 0,
500
534
  attendedByHost: isAttendedByHostView(view),
501
535
  source: "tolerant"
@@ -685,4 +719,4 @@ function whatIsWaiting(run, runDir) {
685
719
  return waitingFor(run, runDir);
686
720
  }
687
721
 
688
- export { RunStore, runSnapshot, openQuestions, isMovable, bar, whatIsWaiting };
722
+ export { RunStore, closedByMachine, describeGateSignature, runSnapshot, openQuestions, isMovable, bar, whatIsWaiting };