@holmes-lab/holmes-kit 0.12.2 → 0.13.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 (38) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +12 -3
  3. package/dist/.build-id +1 -1
  4. package/dist/holmes/cli/approve-context.js +10 -10
  5. package/dist/holmes/cli/approve-ref.js +5 -5
  6. package/dist/holmes/cli/approve-watch.d.ts +1 -1
  7. package/dist/holmes/cli/approve-watch.js +6 -6
  8. package/dist/holmes/cli/approve.d.ts +3 -3
  9. package/dist/holmes/cli/approve.js +57 -57
  10. package/dist/holmes/cli/autonomy.d.ts +22 -0
  11. package/dist/holmes/cli/autonomy.js +145 -0
  12. package/dist/holmes/cli/colophon.d.ts +6 -0
  13. package/dist/holmes/cli/colophon.js +24 -0
  14. package/dist/holmes/cli/doctor.d.ts +2 -2
  15. package/dist/holmes/cli/doctor.js +104 -87
  16. package/dist/holmes/cli/index.js +122 -63
  17. package/dist/holmes/cli/init.d.ts +2 -0
  18. package/dist/holmes/cli/init.js +31 -19
  19. package/dist/holmes/cli/interactive-prompt.d.ts +8 -0
  20. package/dist/holmes/cli/interactive-prompt.js +23 -0
  21. package/dist/holmes/cli/semantic-key.js +9 -9
  22. package/dist/holmes/cli/settings-merge.d.ts +2 -1
  23. package/dist/holmes/cli/settings-merge.js +15 -3
  24. package/dist/holmes/cli/upgrade.js +7 -7
  25. package/dist/holmes/cpg/proposed-content.js +2 -2
  26. package/dist/holmes/governance/autonomy.d.ts +9 -2
  27. package/dist/holmes/governance/autonomy.js +166 -5
  28. package/dist/holmes/guardrail/blind-spots.js +15 -15
  29. package/dist/holmes/hooks/pre-tool-use.js +111 -42
  30. package/dist/holmes/hooks/session-start.js +17 -0
  31. package/dist/holmes/hooks/stop.d.ts +1 -1
  32. package/dist/holmes/hooks/stop.js +12 -12
  33. package/dist/holmes/mcp/handlers.js +14 -1
  34. package/dist/holmes/semantic/credentials.js +1 -1
  35. package/dist/holmes/spec/id-collision.js +2 -2
  36. package/package.json +2 -2
  37. package/playbooks/publish/PLAYBOOK.md +47 -35
  38. package/playbooks/remediation/PLAYBOOK.md +1 -1
package/CHANGELOG.md CHANGED
@@ -5,6 +5,86 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  <!-- @implements A-SPEC-209 -->
8
+ ## [0.13.0] - 2026-09-06
9
+
10
+ Autonomy grows from a single env switch into a **three-layer, always-bounded** posture — so a team can
11
+ let the agent self-drive the SDLC without ever losing the human gate on governance-critical, high-risk,
12
+ or irreversible decisions — and that same risk grade now governs **releases**. Also: a uniform English
13
+ operator surface, and two shell-write gate bypasses closed. Additive; a project that never opts into
14
+ autonomy is byte-identical to before.
15
+
16
+ ### Added
17
+
18
+ - **Three-layer autonomous approval** (REQ-552 / REQ-553 / REQ-554): autonomy is now a posture the agent
19
+ can hold at two scopes above the always-escalate boundary. A **project default** you opt into at
20
+ `holmes-kit init --autonomy` (an interactive prompt defaulting to No; `--autonomy`/`--no-autonomy` skip
21
+ it) persists as the `HOLMES_AUTONOMOUS_APPROVAL` env in `.mcp.json`. A **per-session envelope** you
22
+ grant with `holmes-kit autonomy on [--for 2h]` writes an **expiring marker** under the
23
+ agent-write-protected `.ax/state/`; `holmes-kit autonomy status`/`off` read and end it. The active
24
+ posture is **surfaced at every session start** (SessionStart hook) and the stale marker is pruned
25
+ there. An agent still cannot grant autonomy to itself: the env is env-only (blocked like
26
+ `HOLMES_ROLE`), the session command needs a real TTY or an out-of-band `HOLMES_APPROVAL`, and the
27
+ marker lives where agents can't write. `doctor` reports the project posture.
28
+ - **Release autonomy classifier + docs-currency gate** (REQ-555): `releaseAutonomy(specs, versionBump,
29
+ env, root, now)` decides a release by **what it contains** — reusing the same per-spec risk grade —
30
+ returning `auto` only when autonomy is on, the bump is patch/minor, and every spec is auto-grade; a
31
+ **major** bump, any `gate-behavior`/security/architecture spec, or an upstream `REQ`/`H-SPEC` returns
32
+ `hitl`. `versionBumpKind(from, to)` classifies semver major/minor/patch/none. The `holmes-publish`
33
+ playbook adds a **docs-currency gate** (diff the specs since the last tag; block if a user-facing
34
+ change never reached `README`/`CHANGELOG`) and reworks its step 3 to this classification. `npm publish`
35
+ remains human-approved by default — it is irreversible and outward.
36
+
37
+ - **A maker's colophon** (REQ-548): an undocumented subcommand prints a short detective-themed
38
+ colophon and credit. Deliberately absent from `--help` — a mark for those who go looking.
39
+
40
+ ### Changed
41
+
42
+ - **Autonomy hitl boundary is now enforced through the out-of-band queue** (REQ-551): under autonomy, a
43
+ governance-critical spec used to be reachable through the in-session elicitation prompt (a side-door
44
+ that a test even blessed). It is now **refused and routed to the `holmes-kit approve` queue** instead —
45
+ `gate-behavior`/breaking A-SPECs, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/
46
+ `C-SPEC` never self-approve, whatever the autonomy posture. This supersedes the README's earlier
47
+ "still ask through the in-session TUI" description.
48
+ - **Uniform English operator surface** (REQ-549): every operator-facing message — `doctor` output, the
49
+ CLI usage/errors, the hook `deny` reasons and ART citations, and the interactive `approve`/`init`/
50
+ `upgrade`/`semantic-key` prompts — is now English, guarded by a hangul-absence test over the
51
+ **rendered runtime output** (not just source scan, which misses `\u`-escaped strings). Playbook
52
+ triggers pinned to former Korean deny phrases were re-pinned to the new English wording.
53
+
54
+ ### Fixed
55
+
56
+ - **shell-write gate: two bypasses closed** (REQ-550): the gate now treats a project as **governed when
57
+ any spec exists** — a fresh project holding only *unapproved drafts* was previously ungoverned, so a
58
+ code write slipped through the draft window — and it classifies `cp`/`mv` by their **destination**
59
+ (a copy/move landing on a source path is sealed even when the source file isn't code, which the old
60
+ "≥2 code paths" heuristic missed). Reproduced in the reported draft-only state before the fix.
61
+ - **Release classifier & `autonomy off` fail CLOSED on the edges** (REQ-558): a pre-release adversarial
62
+ audit found the release-autonomy classifier failing OPEN — `releaseAutonomy([], …)` returned `auto`
63
+ (an empty/unknown spec set self-publishing), a same-major **downgrade** (`1.5.0`→`1.4.0`) graded as a
64
+ normal `minor`, and a `none` bump reached `auto` — and `holmes-kit autonomy off` swallowing an
65
+ `rmSync` failure to report "OFF" while the marker (and autonomy) survived. Now: an empty spec set,
66
+ any non-`patch`/`minor` bump, and a downgrade are all `hitl`; and `off` re-checks the marker and
67
+ reports `still ON` (exit 1) if it cannot be removed. A dead `void` stub was removed; the README's
68
+ English-surface claim was narrowed to the CLI/hook surfaces it actually covers.
69
+
70
+ ### Security
71
+
72
+ - **Autonomy grader / enforcer parity** (REQ-556): under autonomy, the spec-approval risk grader
73
+ (`isHighRiskPath`) judged an A-SPEC's `Files to Touch` with a literal `startsWith`, while the write
74
+ authorization that same field confers is decided by the glob-aware, prose-permissive enforcer
75
+ (`matchesFtt`). The gap let an agent **self-approve a spec that writes the gate's own code** by
76
+ spelling `Files to Touch` as a glob (`src/holmes/**/*.ts`), as prose (no path tokens), empty, or in a
77
+ different case (`src/holmes/Governance/…`) — all graded `auto`. The grader now judges by what a token
78
+ can **admit**: zero path tokens ⇒ `hitl` (unconstrained scope), a glob whose prefix overlaps a
79
+ gate/governance/taint root ⇒ high-risk, and paths are case-folded. Verified with two consecutive clean
80
+ adversarial rounds.
81
+ - **shell-write gate: directory-destination and `-t` bypasses closed** (REQ-557): `cp /tmp/evil.ts src/`
82
+ (a directory destination) and `cp -t src /tmp/evil.ts` (GNU `--target-directory`) landed a code file
83
+ inside the governed tree **unjudged**, because the REQ-550 rule only inspected the last token's
84
+ extension. The gate now parses `-t`/`--target-directory` as the destination and, for a directory
85
+ destination, judges each source's landing path `dir/basename(src)`. Read-out backups, out-of-tree and
86
+ non-code landings, and ungoverned repos are unaffected.
87
+
8
88
  ## [0.12.2] - 2026-09-05
9
89
 
10
90
  Makes the update-notification actually work. Measured this release: the "new version available" notice
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
 
15
15
  ---
16
16
 
17
- ### 🛡️ Currently Supported Features (v0.9.x Production Features)
17
+ ### 🛡️ Currently Supported Features (v0.13.x Production Features)
18
18
 
19
19
  - 📋 **Requirements & Specification Governance**: Strict **"No Spec, No Code"** enforcement with 4-tier spec chain traceability (`REQ ➔ H-SPEC ➔ A-SPEC ➔ T-SPEC`) and `// @implements A-SPEC-XXX` code anchors (comma-lists and every anchor in a file participate in the gate).
20
20
  - 🔴 **Inbuilt TDD — RED-first, enforced not asked** *(new in 0.9.0)*: the test-first discipline is a holmes-installed `holmes-tdd-slice` skill **and** a new constitution article **ART-8**. A changed A-SPEC must show a recorded `red-assertion → green` sequence in the ledger; a `red-error` (a test that could not run) is not a valid RED, so "the covering test failed *correctly*" is judged mechanically, not on trust. `test_run` classifies each covered file (`red-assertion`/`red-error`/`green`) and records per-A-SPEC outcomes the Stop hook reads. Ships at `redFirstEvidence: track` (observe-first, non-blocking; `strict`/`off` per repo), evidence-gated and jest-only for now. A T-SPEC may also declare `kills:` mutations and `test_run --mutate` reports which SURVIVED (a coverage gap). Where superpowers *asks* for RED-first and discriminating power, holmes-kit *proves* them.
@@ -22,9 +22,11 @@
22
22
  - ⬆️ **Zero-config upgrades** *(new in 0.11.0)*: `holmes-kit upgrade` moves **every** wired workspace to the latest in one command — plan → confirm → install → re-pin all recorded workspaces (`--dry-run`/`--yes` supported). Preparation is automatic (each `init` records the workspace; a session whose pin is behind nudges you to upgrade); the re-pin **write** stays your explicit choice, never a silent auto-install. Opt out of the nudge with `HOLMES_NO_AUTO_REPIN`.
23
23
  - 📊 **World-top-tier RTM dashboard** *(new in 0.12.0–0.12.1)*: `holmes-kit serve` — ask to *see* the RTM heatmap and the `rtm_dashboard` MCP tool launches the server idempotently and hands back the URL plus an honesty **census** (requirement/pipeline counts, coverage %, what's excluded). The heatmap is a real **2D coverage matrix** (requirements × pipeline stages, rows seriated by completeness, sequential-ramp cells with the percent printed in each). Drilling into a symbol renders that function's **CFG as a layered DAG** with **PDG (data/control-dependence) colour overlays**, served by `/api/cfg?file=&symbol=` from the same engine the taint lane uses — a non-CFG language is named, never faked. Tokenised palette (sequential ramp, status colours, UI/mono pairing) with light/dark.
24
24
  - 🔢 **Sensible spec numbering** *(new in 0.12.1)*: a brand-new project's first slice is now **REQ-100**, not REQ-201 — `spec_slice_init` shares the same id allocator as the reverse-draft path (`nextIdBase`, floor 100). Existing projects are untouched: the next id is always `max(existing)+1`, so a repo already numbering from 201 keeps the exact same sequence. Numbering past 999 yields 4-digit ids cleanly, and ADR references now recognise 4-digit ADRs (`ADR-1000+`).
25
- - 🤖 **Autonomous Approval** *(new in 0.8.0)*: with the out-of-band `HOLMES_AUTONOMOUS_APPROVAL` switch on, an agent seals **low/mid-risk** specs itself (ledgered under an `autonomous:<client>` actor) — while `gate-behavior` changes, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` still ask a human through the in-session TUI. The switch is env-only; a session cannot set it (blocked like `HOLMES_ROLE`). Off = byte-identical to before. *(new in 0.10.0)* Set **`HOLMES_ELICIT=off`** to skip the in-session prompt entirely and route every decision to the out-of-band `holmes-kit approve` queue instead — one decision surface that behaves identically across Claude Code / Antigravity / Codex.
25
+ - 🤖 **Autonomous Approval — three layers, always bounded** *(reworked in 0.13.0; foundation 0.8.0)*: for teams that want the agent to self-drive the SDLC, autonomy is a posture the agent holds at two scopes — a **project default** you opt into at `holmes-kit init --autonomy` (persisted as the `HOLMES_AUTONOMOUS_APPROVAL` env in `.mcp.json`), and a **per-session envelope** you grant on the spot with `holmes-kit autonomy on --for 2h` (an expiring marker under the agent-write-protected `.ax/state/`). Under either, the agent seals **low-risk** specs itself (ledgered under an `autonomous:<client>` actor); every **governance-critical, high-risk, or irreversible** decision — `gate-behavior`/breaking A-SPECs, architecture/gate/taint files, and every upstream `REQ`/`H-SPEC`/`C-SPEC` is instead **refused and routed to the out-of-band `holmes-kit approve` queue** for a human, never silently self-approved. The active posture is **surfaced at every session start** so it can't be forgotten, and an agent can never grant it to itself: the env is env-only (blocked like `HOLMES_ROLE`), the session command needs a real TTY or an out-of-band `HOLMES_APPROVAL`, and the marker lives where agents can't write. Off = byte-identical to a fully human-gated project. *(new in 0.10.0)* `HOLMES_ELICIT=off` routes every decision straight to the same queue.
26
+ - 🚢 **Release Autonomy + Docs-Currency Gate** *(new in 0.13.0)*: publishing is irreversible and outward, so `npm publish` stays **human-approved by default** — but a deterministic classifier (`releaseAutonomy`, reusing the same per-spec risk grade) lets a **low-risk** release (patch/minor, every spec auto-grade, autonomy on) self-publish under the ledger, while a **major** bump, any `gate-behavior`/security/architecture spec, or an upstream `REQ`/`H-SPEC` forces HITL. The `holmes-publish` playbook also gains a **docs-currency gate**: before any release it diffs the specs since the last tag and blocks if a user-facing change never reached `README`/`CHANGELOG` — a stale doc is a false claim.
27
+ - 🌐 **English CLI & hook surface** *(new in 0.13.0)*: the operator-facing CLI and hook messages — `doctor` output, the CLI usage/errors, the hook `deny` reasons and ART citations, and the interactive `approve`/`init`/`upgrade`/`semantic-key` prompts — are now English, guarded by a hangul-absence test over the **rendered runtime output** (not just a source scan, which misses `\u`-escaped strings). The MCP tool responses (`spec_create`/`spec_approve`/ledger/review) are still being migrated and are next.
26
28
  - 🪧 **Session Banner + Update Notice** *(new in 0.8.0; refresh implemented + made uniform in 0.12.2)*: every session start emits an English intro (version + governance rule + npm URL) to both the human transcript and the agent context (SessionStart hook + MCP `instructions`); when a newer published version is on npm, an install-mode-aware `holmes-kit upgrade` command is appended. The registry refresh (dist-tags query → cached in `~/.holmes/update-check.json`) is detached, TTL-gated, and fail-silent, and now fires from **every harness's MCP-server startup** — not just Claude's SessionStart hook — so Claude / Antigravity / Codex are notified alike. Opts out via `HOLMES_NO_UPDATE_CHECK`/`CI`. Upgrade execution stays your explicit choice (`holmes-kit upgrade`), never a silent auto-install.
27
- - 🧱 **Deterministic Gate, Hardened** *(new in 0.8.0)*: shell writes are judged at the segment's **effective working directory** (`cd sub && cat > ../src/x.ts` is sealed, legitimate out-of-tree scratch writes are freed); the governing anchor is the **whole set**, not the first match. Every gate change ships with two consecutive clean adversarial rounds.
29
+ - 🧱 **Deterministic Gate, Hardened** *(new in 0.8.0; further hardened in 0.13.0)*: shell writes are judged at the segment's **effective working directory** (`cd sub && cat > ../src/x.ts` is sealed, legitimate out-of-tree scratch writes are freed); the governing anchor is the **whole set**, not the first match. *(0.13.0)* Two more bypasses are closed: the gate treats a project as **governed when any spec exists** (a fresh project holding only unapproved drafts is no longer an ungoverned free-for-all), and it classifies `cp`/`mv` by their **destination** (a copy/move landing on a source path is sealed even when the source file isn't code). Every gate change ships with two consecutive clean adversarial rounds.
28
30
  - 🧠 **3-Tier Semantic Layer** *(new in 0.3.0)*: knowledge-graph semantic search with an explicit consent ladder — `none` (default, **zero egress**), `local` (bge-m3, no egress, optional module), `cloud` (gemini-embedding-001, opt-in via `GEMINI_API_KEY`). Measured on 305 traceability cases: recall 0.486 (lexical) → 0.667 (local) → **0.887 (cloud)**; on lexical-zero requests: 0% → 52% → **92%**. Surfaced only additively — rerank, evidence (`semCos`), and `semanticAlternates` — never as a hard filter.
29
31
  - 🎯 **Graded Impact Surface** *(new in 0.3.0)*: `rankedImpact` (personalized-PageRank over the spec/code graph) beat its pre-registered naive baseline on **both recall and precision across 3 corpora (×1.6–×17)** — the necessary condition for any better-than-a-person phrasing, measured before claimed.
30
32
  - 🐞 **Causal Defect Localization & CPG** *(equalized in 0.5–0.7)*: AST Code Property Graph (CFG/DDG/CDG) & Dataflow Taint reachability across 7 languages (TS/JS, Python, Go, Rust, Java, C/C++, C#) — **42 language×layer cells graded on measured evidence** (11 corpora, 39,344 functions, zero invariant violations; C++ conditional on 67.9% parse coverage, disclosed in the matrix).
@@ -118,6 +120,9 @@ npx holmes-kit init # drop the npx prefix if you installed with -g
118
120
  [X] 🚀 Antigravity CLI (AGY) (.agents/mcp_config.json, hooks.json, skills)
119
121
  [ ] 💻 Codex CLI (.codex/config.toml)
120
122
  ```
123
+ *It then asks — defaulting to **No** — whether to grant **autonomous spec approval** (low-risk specs
124
+ self-seal; governance-critical, high-risk and irreversible always stay human). Skip the prompt with
125
+ `--autonomy` / `--no-autonomy`, or turn it on later per-session with `holmes-kit autonomy on`.*
121
126
 
122
127
  ### 3. Verify Health
123
128
  ```bash
@@ -191,7 +196,11 @@ flowchart LR
191
196
  | :--- | :--- |
192
197
  | `holmes-kit init` | Interactive agent harness setup (Claude, Antigravity, Codex) |
193
198
  | `holmes-kit init --agent all` | Non-interactive instant setup for all supported agents |
199
+ | `holmes-kit init --autonomy` | Opt the project into autonomous spec approval (low-risk only; governance-critical still HITL) |
194
200
  | `holmes-kit init --dry-run` | Preview files and configuration changes without writing |
201
+ | `holmes-kit autonomy status` | Show the current autonomous-approval posture (project default + session envelope) |
202
+ | `holmes-kit autonomy on --for 2h` | Self-drive low-risk specs for this session only (expiring; needs a TTY or `HOLMES_APPROVAL`) |
203
+ | `holmes-kit autonomy off` | End session autonomy immediately |
195
204
  | `holmes-kit doctor` | Comprehensive health check of specs, hooks, MCP, and anchors |
196
205
  | `holmes-kit doctor --fix` | Automatically self-heal and repair broken hooks or missing skills |
197
206
  | `holmes-kit ci` | Run non-interactive headless governance gate for GitHub Actions / GitLab CI |
package/dist/.build-id CHANGED
@@ -1 +1 @@
1
- 7bd28fc-mtnyp765
1
+ 55b0a37-mtp8gdtd
@@ -58,7 +58,7 @@ const MAX_BODY = 2400;
58
58
  // Round-6: and folded UNDER A MARGIN, so no body byte can start a line and pose as a template row.
59
59
  const BODY_INDENT = ' \u2502 ';
60
60
  // Round-10: the clip ran AFTER escaping, so it counted escape-expanded units — 500 non-breaking
61
- // spaces (503 source characters) reported "603 잘림", a statement false on its face on the screen
61
+ // spaces (503 source characters) reported "clipped at 603 chars", a statement false on its face on the screen
62
62
  // whose job is saying how much is not shown, and it cut MAX_BODY's real budget by six. It also let
63
63
  // the cut land INSIDE an escape, leaving a bare `00` that was the tail of `\u3000` (round-1's
64
64
  // severed-surrogate lesson, one representation later). The source is clipped first; the notice then
@@ -108,20 +108,20 @@ function specContext(root, id) {
108
108
  walk(specsDir);
109
109
  // The absolute specs path alone can exceed a row on a real machine, so it gets one of its own.
110
110
  if (found.length === 0) {
111
- return `맥락을 읽을 없습니다: 스펙 ${(0, screen_safe_1.rowField)(id, 24)} 찾지 못했습니다\n ${(0, screen_safe_1.rowField)(specsDir, 76)}`;
111
+ return `cannot read context: spec ${(0, screen_safe_1.rowField)(id, 24)} was not found\n ${(0, screen_safe_1.rowField)(specsDir, 76)}`;
112
112
  }
113
113
  if (found.length > 1) {
114
114
  return [
115
- `맥락이 모호합니다: ${(0, screen_safe_1.rowField)(id, 40)} 를 주장하는 파일이 ${found.length}건입니다:`,
115
+ `context is ambiguous: ${found.length} file(s) claim ${(0, screen_safe_1.rowField)(id, 40)}:`,
116
116
  ...found.map((f) => ` ${(0, screen_safe_1.rowField)(f.file, 72)}`).sort(),
117
- '어느 것이 승인 대상인지 확정되기 전에는 본문을 보여 주지 않습니다',
117
+ 'the body is not shown until which one is the approval target is settled',
118
118
  ].join('\n')
119
- + `\n (holmes-kit doctor 의 'spec id 선점 충돌' 확인하십시오)`;
119
+ + `\n (check 'spec id preemption collision' in holmes-kit doctor)`;
120
120
  }
121
121
  const { spec, file } = found[0];
122
122
  const body = [spec.preamble ?? '', ...Object.entries(spec.sections).map(([h, t]) => `## ${h}\n${t}`)]
123
123
  .filter((s2) => s2.trim() !== '').join('\n\n');
124
- return clip(`■ ${(0, screen_safe_1.flattenField)(spec.id, 40)} — ${(0, screen_safe_1.flattenField)(spec.title, 120)}\n [현재 상태: ${(0, screen_safe_1.flattenField)(spec.status, 20)}] (${(0, screen_safe_1.flattenField)(file, 60)})\n\n${body}`);
124
+ return clip(`■ ${(0, screen_safe_1.flattenField)(spec.id, 40)} — ${(0, screen_safe_1.flattenField)(spec.title, 120)}\n [current status: ${(0, screen_safe_1.flattenField)(spec.status, 20)}] (${(0, screen_safe_1.flattenField)(file, 60)})\n\n${body}`);
125
125
  }
126
126
  function findingContext(root, id) {
127
127
  const file = path.join(root, '.ax', 'ledger', 'findings.jsonl');
@@ -136,7 +136,7 @@ function findingContext(root, id) {
136
136
  // were justified as well.
137
137
  catch (e) {
138
138
  return [
139
- '맥락을 읽을 없습니다: 발견 원장을 읽지 못했습니다',
139
+ 'cannot read context: could not read the findings ledger',
140
140
  ` ${(0, screen_safe_1.rowField)(file, 76)}`,
141
141
  ` ${(0, screen_safe_1.rowField)(String(e instanceof Error ? e.message : e).split('\n')[0], 76)}`,
142
142
  ].join('\n');
@@ -155,8 +155,8 @@ function findingContext(root, id) {
155
155
  // Round-3: this was the one sibling site that skipped flattenField, and it is outside clip() —
156
156
  // a review-resolve target that is not in the ledger repainted the whole interactive screen.
157
157
  if (!latest)
158
- return `맥락을 읽을 없습니다: 발견 ${(0, screen_safe_1.rowField)(id, 24)} 기록이 원장에 없습니다`;
159
- return clip(`■ 발견 ${(0, screen_safe_1.flattenField)(id, 60)}\n [${(0, screen_safe_1.flattenField)(String(latest.severity ?? '?'), 20)} · ${(0, screen_safe_1.flattenField)(String(latest.status ?? '?'), 20)}${latest.category ? ' · ' + (0, screen_safe_1.flattenField)(String(latest.category), 40) : ''}]\n\n${(0, screen_safe_1.flattenField)(String(latest.summary ?? '(요약 없음)'), 2000)}`);
158
+ return `cannot read context: no record of finding ${(0, screen_safe_1.rowField)(id, 24)} in the ledger`;
159
+ return clip(`■ finding ${(0, screen_safe_1.flattenField)(id, 60)}\n [${(0, screen_safe_1.flattenField)(String(latest.severity ?? '?'), 20)} · ${(0, screen_safe_1.flattenField)(String(latest.status ?? '?'), 20)}${latest.category ? ' · ' + (0, screen_safe_1.flattenField)(String(latest.category), 40) : ''}]\n\n${(0, screen_safe_1.flattenField)(String(latest.summary ?? '(no summary)'), 2000)}`);
160
160
  }
161
161
  function renderDecisionContext(root, entry) {
162
162
  try {
@@ -175,6 +175,6 @@ function renderDecisionContext(root, entry) {
175
175
  }
176
176
  catch (e) {
177
177
  // Belt and braces: no context failure may kill the decision surface.
178
- return `맥락을 읽을 수 없습니다:\n ${(0, screen_safe_1.rowField)(String(e instanceof Error ? e.message : e).split('\n')[0], 76)}`;
178
+ return `cannot read context:\n ${(0, screen_safe_1.rowField)(String(e instanceof Error ? e.message : e).split('\n')[0], 76)}`;
179
179
  }
180
180
  }
@@ -5,13 +5,13 @@ const screen_safe_1 = require("./screen-safe");
5
5
  function resolveRequestRef(pending, ref) {
6
6
  const r = ref.trim();
7
7
  if (r === '')
8
- return { ok: false, reason: '참조가 비어 있습니다목록의 번호나 id 접두를 주십시오' };
8
+ return { ok: false, reason: 'the reference is empty give a list number or an id prefix' };
9
9
  if (pending.length === 0)
10
- return { ok: false, reason: '승인 대기 중인 요청이 없습니다' };
10
+ return { ok: false, reason: 'no requests awaiting approval' };
11
11
  if (/^\d+$/.test(r)) {
12
12
  const n = Number(r);
13
13
  if (n < 1 || n > pending.length) {
14
- return { ok: false, reason: `번호 ${(0, screen_safe_1.rowField)(r, 20)} 목록 밖입니다유효 범위는 1..${pending.length} 입니다` };
14
+ return { ok: false, reason: `number ${(0, screen_safe_1.rowField)(r, 20)} is out of the list the valid range is 1..${pending.length}` };
15
15
  }
16
16
  return { ok: true, entry: pending[n - 1] };
17
17
  }
@@ -24,7 +24,7 @@ function resolveRequestRef(pending, ref) {
24
24
  if (matches.length === 1)
25
25
  return { ok: true, entry: matches[0] };
26
26
  if (matches.length === 0) {
27
- return { ok: false, reason: `참조 '${(0, screen_safe_1.rowField)(r, 60)}' 맞는 대기 요청이 없습니다 목록의 번호나 id 접두를 확인하십시오` };
27
+ return { ok: false, reason: `no pending request matches reference '${(0, screen_safe_1.rowField)(r, 60)}' check a list number or an id prefix` };
28
28
  }
29
29
  return {
30
30
  ok: false,
@@ -33,7 +33,7 @@ function resolveRequestRef(pending, ref) {
33
33
  // id) wrapped it past a row and pushed the question off the top of the screen. The refusal keeps
34
34
  // one row; each candidate gets its own, which is also how the operator copies one.
35
35
  reason: [
36
- `참조 '${(0, screen_safe_1.rowField)(r, 30)}' ${matches.length}건에 맞습니다 길게 주십시오:`,
36
+ `reference '${(0, screen_safe_1.rowField)(r, 30)}' matches ${matches.length} — give a longer one:`,
37
37
  ...matches.map((m) => ` ${(0, screen_safe_1.rowField)(m.id, 60)}`),
38
38
  ].join('\n'),
39
39
  };
@@ -24,7 +24,7 @@ export declare function decideByKey(stream: KeyStream, _item: Pick<PendingReques
24
24
  */
25
25
  export declare function presentByKey(root: string, io: ApproveIO, actor: string, stream: KeyStream, state: ReturnType<typeof readQueue>): Promise<void>;
26
26
  /** The idle line, printed once on entering idle (not on every poll — flood is the template's enemy). */
27
- export declare const WATCH_IDLE = "\u25C6 \uC2B9\uC778 \uB300\uAE30 \uC911\u2026 (Ctrl-C \uB85C \uC885\uB8CC)";
27
+ export declare const WATCH_IDLE = "\u25C6 awaiting approval\u2026 (Ctrl-C to exit)";
28
28
  /** Default idle poll cadence. The reaction DEADLINE is a tested judgment (T-SPEC), not this number. */
29
29
  export declare const DEFAULT_POLL_MS = 500;
30
30
  export interface WatchOpts {
@@ -85,25 +85,25 @@ function decideByKey(stream, _item) {
85
85
  async function presentByKey(root, io, actor, stream, state) {
86
86
  for (const p of state.pending) {
87
87
  io.print(`\n─ ${(0, approve_1.subjectCells)('─ ', p)}`);
88
- io.print('[a]승인 [d]거부 [q]질문 [s]건너뜀 > ');
88
+ io.print('[a]approve [d]deny [q]question [s]skip > ');
89
89
  const d = await decideByKey(stream, p);
90
90
  if (d === 'granted') {
91
91
  const r = (0, approve_1.grantRequest)(root, p.id, { actor });
92
- io.print(r.ok ? `✓ 승인 — ${(0, approve_1.subjectCells)('✓ 승인 — ', p)}` : `✗ ${r.reason}`);
92
+ io.print(r.ok ? `✓ approved — ${(0, approve_1.subjectCells)('✓ approved — ', p)}` : `✗ ${r.reason}`);
93
93
  }
94
94
  else if (d === 'denied') {
95
- const r = (0, approve_1.denyRequest)(root, p.id, '운영자가 watch에서 단일 키로 거부(사유 즉답 생략)', actor);
96
- io.print(r.ok ? `✗ 거부 — ${(0, approve_1.subjectCells)('✗ 거부 — ', p)}` : `✗ ${r.reason}`);
95
+ const r = (0, approve_1.denyRequest)(root, p.id, 'denied by the operator with a single key in watch (no immediate reason)', actor);
96
+ io.print(r.ok ? `✗ denied — ${(0, approve_1.subjectCells)('✗ denied — ', p)}` : `✗ ${r.reason}`);
97
97
  }
98
98
  else if (typeof d === 'object') {
99
99
  const r = (0, approve_1.holdRequest)(root, p.id, d.question, actor);
100
- io.print(r.ok ? `? 보류질문이 다음 거부 문면에 실립니다` : `✗ ${r.reason}`);
100
+ io.print(r.ok ? `? heldthe question rides on the next denial message` : `✗ ${r.reason}`);
101
101
  }
102
102
  // skipped: leave it pending, silently
103
103
  }
104
104
  }
105
105
  /** The idle line, printed once on entering idle (not on every poll — flood is the template's enemy). */
106
- exports.WATCH_IDLE = '◆ 승인 대기 중… (Ctrl-C 종료)';
106
+ exports.WATCH_IDLE = '◆ awaiting approval… (Ctrl-C to exit)';
107
107
  /** Default idle poll cadence. The reaction DEADLINE is a tested judgment (T-SPEC), not this number. */
108
108
  exports.DEFAULT_POLL_MS = 500;
109
109
  /** Exported for test: the shipped idle wait must resolve at once on abort, not after the timer. */
@@ -12,7 +12,7 @@ export declare function grantRequest(root: string, id: string, opts: {
12
12
  rationale?: string;
13
13
  }): DecisionResult;
14
14
  export declare function denyRequest(root: string, id: string, reason: string, actor: string): DecisionResult;
15
- /** Hold with a question — 추후 승인/거부. The agent carries the question to the user. */
15
+ /** Hold with a question — approve/deny later. The agent carries the question to the user. */
16
16
  export declare function holdRequest(root: string, id: string, question: string, actor: string): DecisionResult;
17
17
  /**
18
18
  * The review screen. Pure.
@@ -36,7 +36,7 @@ export declare function renderPending(state: QueueState, view?: {
36
36
  }): string;
37
37
  /**
38
38
  * @implements A-SPEC-262.1
39
- * What a decision line must say. Round-1: `✓ 승인 — <expires> 까지 유효` named NOTHING, so an index
39
+ * What a decision line must say. Round-1: `✓ approvedvalid until <expires>` named NOTHING, so an index
40
40
  * that resolved against a shifted queue granted a different request than the operator read, with no
41
41
  * way to see it from the screen. Every decision now echoes the request it acted on.
42
42
  */
@@ -44,7 +44,7 @@ export declare function renderPending(state: QueueState, view?: {
44
44
  * @implements A-SPEC-262.1
45
45
  * The kind cell and the subject, fitted to what is LEFT OF THE ROW after the caller's own prefix.
46
46
  *
47
- * Round-8: the budgets were global constants derived once, from `✓ 승인 — ` — and `✓ 거부 기록됨 — `
47
+ * Round-8: the budgets were global constants derived once, from `✓ approved — ` — and `✓ denied — `
48
48
  * is seven columns wider, so every denial echo ran to 88 columns and wrapped. Round-7 had widened
49
49
  * the kind cell without re-measuring any line that carries it. A constant cannot know which prefix
50
50
  * it will be printed behind, so the line computes its own room and the prefix is passed in. The