loki-mode 7.25.0 → 7.27.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/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ---
20
20
 
21
- > **How it works:** Drop a spec -- a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. Loki Mode classifies complexity (`run.sh:detect_complexity()`), assembles an agent team from 41 specialized types across 8 swarms, and runs autonomous RARV cycles (Reason - Act - Reflect - Verify, see `run.sh:run_autonomous()`) with 11 quality gates (see `skills/quality-gates.md`). Code is not "done" until it passes automated verification. Output is a Git repo with source, tests, configs, and audit logs.
21
+ > **How it works:** Drop a spec -- a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. Loki Mode classifies complexity (`run.sh:detect_complexity()`), assembles an agent team from 41 specialized agent roles across 8 domains - prompt-defined specifications the orchestrator adopts per phase, with parallel review (blind council) and optional worktree streams on Claude Code, sequential on other providers - and runs autonomous RARV cycles (Reason - Act - Reflect - Verify, see `run.sh:run_autonomous()`) with 11 quality gates (see `skills/quality-gates.md`). Code is not "done" until it passes automated verification. Output is a Git repo with source, tests, configs, and audit logs.
22
22
 
23
23
  ---
24
24
 
@@ -26,13 +26,15 @@
26
26
 
27
27
  - **Spec-driven, autonomous, with a built-in trust layer** -- Hand Loki a spec, walk away, come back to working code with tests. The full RARV-C closure loop (Reason - Act - Reflect - Verify - Close) runs until the work is actually done, not just attempted. The verified-completion evidence gate (`skills/quality-gates.md`) refuses any "done" claim on an empty git diff against the run-start commit, and blocks completion when tests run red, so "complete" means proven, not promised.
28
28
  - **Production quality built in** -- 11 quality gates (`skills/quality-gates.md`), blind 3-reviewer code review (`run.sh:run_code_review()`), anti-sycophancy checks
29
+ - **Standalone verification: `loki verify`** -- Run Loki's deterministic gates (build, tests, static analysis, secret scan, dependency audit) against any branch or PR diff, including code written by other agents or humans. CI-ready exit codes (0 VERIFIED, 1 CONCERNS, 2 BLOCKED), machine-readable evidence at `.loki/verify/evidence.json`. Inconclusive evidence is never reported as VERIFIED (v7.27.0).
29
30
  - **Live App Preview** -- The dashboard embeds the locally-running app in an iframe so you can interact with it immediately during a build. Use `loki preview` (alias `loki open`) to print the URL and open it in your browser. Local-first: no hosted service, no vendor lock (v7.24.0).
31
+ - **Compose-first fullstack** -- When a spec needs more than one service (web + database + cache) Loki generates a 12-factor `docker-compose.yml` with healthchecks, `depends_on` wiring, env-var config, and a `.env.example`. The Live App Preview surfaces the web service URL (not a database port), and health reflects the web service's Docker healthcheck so a crashed app shows as crashed even when the database stays up. Single-service apps stay on a plain run command. All local-first, no hosted service (v7.26.0).
30
32
  - **Intelligent `loki start`** -- For interactive foreground runs the dashboard auto-opens in the browser (cross-platform; skipped in CI, SSH-without-TTY, and piped runs; opt out with `LOKI_NO_AUTO_OPEN=1`). The completion summary shows "Your app is live at <url>" so you know exactly where to try what Loki just built. The autonomous loop passes Claude Code's `--effort`, `--max-budget-usd`, and `--fallback-model` on every iteration (each gated on CLI support and individual opt-out env vars) for better long-run unattended execution (v7.25.0).
31
33
  - **Cross-project memory** -- Episodic/semantic/procedural memory with vector search; knowledge learned on one project surfaces on the next (v5.15.0+, see `memory/engine.py`)
32
34
  - **Self-hosted and private** -- Your keys, your infrastructure, no data leaves your network
33
35
  - **Legacy system healing** -- `loki heal` archaeology/stabilize/isolate/modernize/validate phases (v6.67.0, see `skills/healing.md`)
34
36
  - **MCP server** -- 34 tools (including ChromaDB code search) plus 3 resources and 2 prompts (`mcp/server.py`, with managed-memory and magic tools registered from `mcp/managed_tools.py` and `mcp/magic_tools.py`)
35
- - **Full-stack output** -- Source code, tests, Docker configs, CI/CD pipelines, audit logs
37
+ - **Full-stack output** -- Source code, tests, Docker Compose stacks (multi-service with healthchecks), CI/CD pipelines, audit logs
36
38
  - **Provider-agnostic** -- runs on Claude, Codex, Cline, or Aider with automatic failover (`loki-ts/src/runner/providers.ts`); no vendor lock-in. Gemini CLI deprecated v7.5.18; Antigravity CLI coming soon.
37
39
  - **Open source** -- Free for personal, internal, and academic use.
38
40
 
@@ -46,7 +48,7 @@ Loki drives a coding agent CLI and orchestrates real builds, so it needs a few t
46
48
 
47
49
  Required:
48
50
 
49
- - An agent provider CLI (at least one): [Claude Code](https://docs.claude.com/en/docs/claude-code) (`claude`, Tier 1, recommended), or OpenAI Codex CLI (`codex`), Cline, or Aider.
51
+ - An agent provider CLI: [Claude Code](https://docs.claude.com/en/docs/claude-code) (`claude`, Tier 1, recommended and E2E-verified - the provider Loki Mode is built for). Codex, Cline, and Aider are supported as experimental providers (wiring in place; not yet E2E-verified by us).
50
52
  - Python 3.10+ (`python3`) for the dashboard, memory system, and orchestration helpers.
51
53
  - Git 2.x (`git`) for checkpoints and worktrees.
52
54
  - `curl` for installation and network calls.
@@ -86,7 +88,7 @@ loki quick "build a landing page with a signup form"
86
88
 
87
89
  | Method | Command | Notes |
88
90
  |--------|---------|-------|
89
- | **Bun (recommended)** | `bun install -g loki-mode` | Fastest. v8 will be Bun-only. |
91
+ | **Bun (recommended)** | `bun install -g loki-mode` | Fastest startup for CLI commands. |
90
92
  | **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
91
93
  | **Docker** | `docker pull asklokesh/loki-mode:7.7.31 && docker run --rm asklokesh/loki-mode:7.7.31 start prd.md` | Bun pre-installed in image |
92
94
  | **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
@@ -107,7 +109,7 @@ See the [Installation Guide](docs/INSTALLATION.md) for the long form.
107
109
 
108
110
  ## Runtime Architecture
109
111
 
110
- Loki Mode is in a phased migration from a Bash-based runtime to a TypeScript/Bun runtime. The migration has merged to `main` and ships incrementally with each release.
112
+ Loki Mode runs a dual runtime by deliberate design: the battle-tested Bash engine is the stable core (the autonomous loop, quality gates, and completion council stay on it; it receives bug fixes and hardening), and new product surfaces are built TypeScript/Bun-first as modules that wrap the engine rather than reimplement it. An earlier plan to make v8 Bun-only has been superseded by this stable-engine approach: rewriting the verified trust layer would risk the exact guarantees this product exists to provide, for no capability gain. Bash support is not going away.
111
113
 
112
114
  **What ships today:**
113
115
 
@@ -226,8 +228,8 @@ Every iteration: **Reason** (read state) - **Act** (execute, commit) - **Reflect
226
228
  </td>
227
229
  <td width="33%" valign="top">
228
230
 
229
- ### 41 Agent Types
230
- 8 swarms: engineering, operations, business, data, product, growth, review, orchestration. Auto-composed by PRD complexity.
231
+ ### 41 Agent Roles
232
+ 8 domains: engineering, operations, business, data, product, growth, review, orchestration. These are prompt-defined role specifications the orchestrator adopts per phase, auto-composed by PRD complexity; parallelism comes from the blind review council, the adversarial reviewer, and optional git-worktree streams on Claude Code, sequential on other providers.
231
233
 
232
234
  [Agent Types](references/agent-types.md)
233
235
 
@@ -330,14 +332,14 @@ Loki's autonomy and quality loop are the product; the underlying coding CLI is s
330
332
 
331
333
  | Provider | Status | Autonomous Flag | Parallel Agents | Install |
332
334
  |----------|--------|:-:|:-:|---------|
333
- | **Claude Code** | Active (Tier 1) | `--dangerously-skip-permissions` | Yes (10+) | `npm i -g @anthropic-ai/claude-code` |
334
- | **Codex CLI** | Active (Tier 3) | `--full-auto` | Sequential | `npm i -g @openai/codex` |
335
- | **Cline CLI** | Active (Tier 2) | `--auto-approve` | Sequential | `npm i -g @anthropic-ai/cline` |
336
- | **Aider** | Active (Tier 3) | `--yes-always` | Sequential | `pip install aider-chat` |
335
+ | **Claude Code** | Active (Tier 1, E2E-verified) | `--dangerously-skip-permissions` | Yes (10+) | `npm i -g @anthropic-ai/claude-code` |
336
+ | **Codex CLI** | Experimental (Tier 3) | `--full-auto --skip-git-repo-check` | Sequential | `npm i -g @openai/codex` |
337
+ | **Cline CLI** | Experimental (Tier 2) | `-y` | Sequential | `npm i -g @anthropic-ai/cline` |
338
+ | **Aider** | Experimental (Tier 3) | `--yes-always` | Sequential | `pip install aider-chat` |
337
339
  | **Google Gemini CLI** | DEPRECATED v7.5.18 | -- | -- | Upstream deprecated; runtime removed. `LOKI_PROVIDER=gemini` exits with migration message. |
338
340
  | **Anthropic Antigravity CLI** | Coming soon | -- | -- | Integration planned. |
339
341
 
340
- Claude gets full features (subagents, parallelization, MCP, Task tool). Other active providers run sequentially. Auto-failover switches providers when rate-limited. See [Provider Guide](skills/providers.md).
342
+ Status legend: "E2E-verified" means we run real spec-to-code builds on it ourselves. Claude Code is the primary, fully supported provider and the one Loki Mode is built for; it gets full features (subagents, parallelization, MCP, Task tool). "Experimental" means the wiring is in place but we have not produced an end-to-end verified build ourselves; treat as community-tested. Experimental providers run sequentially. Auto-failover switches providers when rate-limited. See [Provider Guide](skills/providers.md).
341
343
 
342
344
  ---
343
345
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 11 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.25.0
6
+ # Loki Mode v7.27.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -383,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
383
383
 
384
384
  ---
385
385
 
386
- **v7.25.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
386
+ **v7.27.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.25.0
1
+ 7.27.0
@@ -43,6 +43,10 @@ _APP_RUNNER_PID=""
43
43
  _APP_RUNNER_URL=""
44
44
  _APP_RUNNER_IS_DOCKER=false
45
45
  _APP_RUNNER_DOCKER_CONTAINER=""
46
+ # v7.26.0 (Phase 4): the identified primary web service of a compose project,
47
+ # used for service-aware health checks and the preview URL. Empty for
48
+ # non-compose runs or when identification falls back to legacy port parsing.
49
+ _APP_RUNNER_WEB_SERVICE=""
46
50
  _APP_RUNNER_HAS_SETSID=false
47
51
  _APP_RUNNER_CRASH_COUNT=0
48
52
  _APP_RUNNER_RESTART_COUNT=0
@@ -146,6 +150,77 @@ _rotate_app_log() {
146
150
  fi
147
151
  }
148
152
 
153
+ # Identify the primary web service of a docker compose project and its
154
+ # published host port. Uses `docker compose config --format json` (fully
155
+ # resolved: env-interpolated, overrides merged) parsed with python3, so we do
156
+ # NOT hand-parse YAML. Precedence MATCHES the contract in COMPOSE_INSTRUCTION
157
+ # (run.sh build_prompt): (1) label loki.primary=true, (2) service named
158
+ # web/app, (3) service publishing a common web port, (4) first service with any
159
+ # published port. Echoes "service_name|published_port" on success, nothing on
160
+ # failure (caller falls back to legacy behavior). Never hard-fails.
161
+ # v7.26.0 (Phase 4): fixes the multi-service URL/health gaps (GAP #1-4).
162
+ _identify_compose_web_service() {
163
+ local base="${1:-${TARGET_DIR:-.}}"
164
+ local compose_dir
165
+ compose_dir=$(_app_runner_compose_dir "$base")
166
+ command -v docker >/dev/null 2>&1 || return 0
167
+ command -v python3 >/dev/null 2>&1 || return 0
168
+ local cfg
169
+ cfg=$(cd "$compose_dir" && docker compose config --format json 2>/dev/null) || return 0
170
+ [ -n "$cfg" ] || return 0
171
+ printf '%s' "$cfg" | python3 -c '
172
+ import json, sys
173
+ COMMON = ["3000", "8000", "8080", "5000", "4200", "5173", "80"]
174
+ try:
175
+ d = json.load(sys.stdin)
176
+ except Exception:
177
+ sys.exit(0)
178
+ services = d.get("services", {})
179
+ if not isinstance(services, dict) or not services:
180
+ sys.exit(0)
181
+
182
+ def published_ports(svc):
183
+ out = []
184
+ for p in (svc.get("ports") or []):
185
+ if isinstance(p, dict):
186
+ pub = p.get("published")
187
+ else:
188
+ pub = None
189
+ if pub is not None and str(pub).strip():
190
+ out.append(str(pub).strip())
191
+ return out
192
+
193
+ # (1) label loki.primary=true
194
+ for name, svc in services.items():
195
+ labels = svc.get("labels") or {}
196
+ if isinstance(labels, list):
197
+ labels = dict(x.split("=", 1) for x in labels if "=" in x)
198
+ if str(labels.get("loki.primary", "")).lower() == "true":
199
+ pp = published_ports(svc)
200
+ if pp:
201
+ print(name + "|" + pp[0]); sys.exit(0)
202
+ # (2) service named web/app
203
+ for cand in ("web", "app"):
204
+ svc = services.get(cand)
205
+ if svc:
206
+ pp = published_ports(svc)
207
+ if pp:
208
+ print(cand + "|" + pp[0]); sys.exit(0)
209
+ # (3) service publishing a common web port
210
+ for name, svc in services.items():
211
+ pp = published_ports(svc)
212
+ for cp in COMMON:
213
+ if cp in pp:
214
+ print(name + "|" + cp); sys.exit(0)
215
+ # (4) first service with any published port
216
+ for name, svc in services.items():
217
+ pp = published_ports(svc)
218
+ if pp:
219
+ print(name + "|" + pp[0]); sys.exit(0)
220
+ sys.exit(0)
221
+ ' 2>/dev/null || return 0
222
+ }
223
+
149
224
  # Detect port from project files
150
225
  _detect_port() {
151
226
  local method="$1"
@@ -158,18 +233,34 @@ _detect_port() {
158
233
 
159
234
  case "$method" in
160
235
  *docker\ compose*)
161
- # Parse port from compose file
162
- local compose_file
163
- if [ -f "${TARGET_DIR:-.}/docker-compose.yml" ]; then
164
- compose_file="${TARGET_DIR:-.}/docker-compose.yml"
236
+ # v7.26.0: identify the PRIMARY WEB service and ITS published port
237
+ # via docker compose config (resolved JSON), so the preview URL and
238
+ # health check target the web service, not whichever port (e.g. a
239
+ # db/cache) appears first in the file. Falls back to the legacy
240
+ # first-port grep when docker/python is unavailable or no web
241
+ # service is found.
242
+ local web_info web_port
243
+ web_info=$(_identify_compose_web_service "${TARGET_DIR:-.}")
244
+ if [ -n "$web_info" ]; then
245
+ _APP_RUNNER_WEB_SERVICE="${web_info%%|*}"
246
+ web_port="${web_info##*|}"
247
+ fi
248
+ if [ -n "${web_port:-}" ] && [[ "$web_port" =~ ^[0-9]+$ ]]; then
249
+ _APP_RUNNER_PORT="$web_port"
165
250
  else
166
- compose_file="${TARGET_DIR:-.}/compose.yml"
251
+ # Legacy fallback: first published port from the compose file.
252
+ local compose_file
253
+ if [ -f "${TARGET_DIR:-.}/docker-compose.yml" ]; then
254
+ compose_file="${TARGET_DIR:-.}/docker-compose.yml"
255
+ else
256
+ compose_file="${TARGET_DIR:-.}/compose.yml"
257
+ fi
258
+ local port
259
+ # Handle both simple (HOST:CONTAINER) and IP-bound (IP:HOST:CONTAINER) port formats
260
+ # Also handle port ranges like "8080-8090:8080-8090" by taking the first port
261
+ port=$(grep -E '^\s*-\s*"?[0-9]' "$compose_file" 2>/dev/null | head -1 | sed 's/.*- *"*//;s/".*//;' | awk -F: '{print $(NF-1)}' | awk -F- '{print $1}')
262
+ _APP_RUNNER_PORT="${port:-8080}"
167
263
  fi
168
- local port
169
- # Handle both simple (HOST:CONTAINER) and IP-bound (IP:HOST:CONTAINER) port formats
170
- # Also handle port ranges like "8080-8090:8080-8090" by taking the first port
171
- port=$(grep -E '^\s*-\s*"?[0-9]' "$compose_file" 2>/dev/null | head -1 | sed 's/.*- *"*//;s/".*//;' | awk -F: '{print $(NF-1)}' | awk -F- '{print $1}')
172
- _APP_RUNNER_PORT="${port:-8080}"
173
264
  ;;
174
265
  *docker\ build*)
175
266
  local port
@@ -694,14 +785,64 @@ app_runner_health_check() {
694
785
  # retries are handled in app_runner_start.
695
786
  local running_containers
696
787
  running_containers=$(LOKI_COMPOSE_HEALTH_TIMEOUT=1 _app_runner_compose_running_count "${TARGET_DIR:-.}")
697
- if [ "${running_containers:-0}" -gt 0 ]; then
788
+ if [ "${running_containers:-0}" -le 0 ]; then
789
+ # Nothing running at all.
790
+ _write_health "false"
791
+ _write_app_state "crashed"
792
+ return 1
793
+ fi
794
+ # v7.26.0 (Phase 4) GAP #4 fix: "some container is up" is NOT health for
795
+ # a multi-service stack. If we identified a primary web service, health
796
+ # keys on THAT service, not on whether any container (e.g. a db/cache) is
797
+ # up. Two signals, in order: (1) the web service's docker HEALTHCHECK
798
+ # result when one is declared (COMPOSE_INSTRUCTION mandates an HTTP
799
+ # healthcheck on the web service) -- "healthy" means actually serving,
800
+ # "unhealthy"/"starting" do not; (2) when no healthcheck is declared,
801
+ # fall back to the container lifecycle State (running), matching the
802
+ # codebase convention. Reads both fields from one `docker compose ps`.
803
+ if [ -n "${_APP_RUNNER_WEB_SERVICE:-}" ]; then
804
+ local _web_line _web_state _web_health
805
+ _web_line=$(cd "$(_app_runner_compose_dir "${TARGET_DIR:-.}")" \
806
+ && docker compose ps --format '{{.Service}}|{{.State}}|{{.Health}}' 2>/dev/null \
807
+ | tr -d '\r' | awk -F'|' -v s="$_APP_RUNNER_WEB_SERVICE" '$1==s {print; exit}')
808
+ _web_state=$(printf '%s' "$_web_line" | awk -F'|' '{print $2}')
809
+ _web_health=$(printf '%s' "$_web_line" | awk -F'|' '{print $3}')
810
+ if [ "$_web_state" != "running" ]; then
811
+ # Container not running -> definitively down.
812
+ _write_health "false"
813
+ _write_app_state "crashed"
814
+ return 1
815
+ fi
816
+ if [ -n "$_web_health" ]; then
817
+ # A healthcheck is declared: it is authoritative.
818
+ if [ "$_web_health" = "healthy" ]; then
819
+ _write_health "true"
820
+ _write_app_state "running"
821
+ return 0
822
+ fi
823
+ if [ "$_web_health" = "unhealthy" ]; then
824
+ # Container up but failing its own healthcheck (not serving).
825
+ _write_health "false"
826
+ _write_app_state "crashed"
827
+ return 1
828
+ fi
829
+ # "starting" (within start_period): up, not yet healthy. Report
830
+ # running so the watchdog gives it time instead of restarting,
831
+ # but do not yet claim a passing health.
832
+ _write_health "false"
833
+ _write_app_state "running"
834
+ return 0
835
+ fi
836
+ # No healthcheck declared: container running is the signal.
698
837
  _write_health "true"
699
838
  _write_app_state "running"
700
839
  return 0
701
- else
702
- _write_health "false"
703
- return 1
704
840
  fi
841
+ # No web service identified (legacy/degraded): fall back to the
842
+ # original "any container running" signal.
843
+ _write_health "true"
844
+ _write_app_state "running"
845
+ return 0
705
846
  fi
706
847
 
707
848
  # Check PID is alive (non-docker-compose methods)
@@ -769,6 +910,36 @@ app_runner_should_restart() {
769
910
  app_runner_watchdog() {
770
911
  _app_runner_dir
771
912
 
913
+ # v7.26.0 (Phase 4): docker compose runs detached (`up -d` exits immediately),
914
+ # so the captured PID is a short-lived subshell and `kill -0` is the wrong
915
+ # liveness signal for a compose stack. For compose, delegate to
916
+ # app_runner_health_check, whose compose branch keys on the primary web
917
+ # SERVICE container running (GAP #4) and writes health.json + state.json.
918
+ # This is what makes the service-aware health logic actually fire in the
919
+ # live monitoring loop (not just in isolation). On an unhealthy web service
920
+ # it restarts the stack under the same crash-count circuit breaker.
921
+ if [ "$_APP_RUNNER_IS_DOCKER" = true ] && echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
922
+ if app_runner_health_check; then
923
+ return 0
924
+ fi
925
+ _APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
926
+ log_warn "App Runner: compose web service unhealthy (crash #$_APP_RUNNER_CRASH_COUNT)"
927
+ if [ "$_APP_RUNNER_CRASH_COUNT" -ge 5 ]; then
928
+ log_error "App Runner: crash limit reached (5), marking as crashed"
929
+ tail -20 "$_APP_RUNNER_DIR/app.log" 2>/dev/null | while IFS= read -r line; do
930
+ log_error " $line"
931
+ done
932
+ _write_app_state "crashed"
933
+ return 1
934
+ fi
935
+ local _c_backoff=$(( 1 << _APP_RUNNER_CRASH_COUNT ))
936
+ [ "$_c_backoff" -gt 30 ] && _c_backoff=30
937
+ log_info "App Runner: restarting compose stack in ${_c_backoff}s..."
938
+ sleep "$_c_backoff"
939
+ app_runner_start || log_warn "App Runner: compose auto-restart failed"
940
+ return 0
941
+ fi
942
+
772
943
  if [ -z "$_APP_RUNNER_PID" ] && [ -f "$_APP_RUNNER_DIR/app.pid" ]; then
773
944
  _APP_RUNNER_PID=$(cat "$_APP_RUNNER_DIR/app.pid" 2>/dev/null)
774
945
  fi
@@ -752,6 +752,18 @@ with open(state_file, 'w') as f:
752
752
  "threshold=$effective_threshold" \
753
753
  "result=$([ $approve_count -ge $effective_threshold ] && echo 'APPROVED' || echo 'REJECTED')" 2>/dev/null || true
754
754
 
755
+ # Trust-metrics: durable per-vote record for the council rejection / split
756
+ # rate. The council state.json verdicts[] array is per-run only; this log is
757
+ # the cross-run corpus. Additive, best-effort, stdout-silent.
758
+ if type record_trust_event_bash &>/dev/null; then
759
+ record_trust_event_bash "council_vote" \
760
+ "approve=$approve_count" \
761
+ "reject=$reject_count" \
762
+ "threshold=$effective_threshold" \
763
+ "result=$([ $approve_count -ge $effective_threshold ] && echo 'APPROVED' || echo 'REJECTED')" \
764
+ >/dev/null 2>&1 || true
765
+ fi
766
+
755
767
  # Write transcript for this council round (Path A: council_vote path)
756
768
  local _ct_outcome
757
769
  _ct_outcome=$([ $approve_count -ge $effective_threshold ] && echo "APPROVED" || echo "REJECTED")
@@ -1366,6 +1378,19 @@ print(json.dumps(items[:5]))
1366
1378
  }
1367
1379
  EVIDENCE_EOF
1368
1380
  mv "$ev_tmp" "$ev_file"
1381
+
1382
+ # Trust-metrics: durable per-block record. evidence-block.json is a single
1383
+ # state file that is DELETED the moment the gate next passes, so it cannot
1384
+ # be the cross-run corpus for the block rate. Append an event here, where a
1385
+ # block is definitely happening. Additive, best-effort, stdout-silent.
1386
+ if type record_trust_event_bash &>/dev/null; then
1387
+ record_trust_event_bash "evidence_block" \
1388
+ "reason=$reason" \
1389
+ "diff_ok=$diff_ok" \
1390
+ "tests_ok=$tests_ok" \
1391
+ >/dev/null 2>&1 || true
1392
+ fi
1393
+
1369
1394
  return 1
1370
1395
  }
1371
1396