loki-mode 7.42.0 → 7.44.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
@@ -13,7 +13,7 @@ _The free, source-available autonomous coding agent by [Autonomi](https://www.au
13
13
  [![Docker Pulls](https://img.shields.io/docker/pulls/asklokesh/loki-mode?style=for-the-badge&logo=docker&logoColor=white&color=2F71E3)](https://hub.docker.com/r/asklokesh/loki-mode)
14
14
  [![License](https://img.shields.io/badge/License-BUSL--1.1-36342E?style=for-the-badge)](LICENSE)
15
15
 
16
- [Website](https://www.autonomi.dev/) | [Documentation](wiki/Home.md) | [Installation](docs/INSTALLATION.md) | [Changelog](CHANGELOG.md) | [Purple Lab Web UI](#purple-lab)
16
+ [Website](https://www.autonomi.dev/) | [Documentation](wiki/Home.md) | [Installation](docs/INSTALLATION.md) | [Changelog](CHANGELOG.md) | [Purple Lab -- deprecated v7.44.0](#purple-lab)
17
17
 
18
18
  </div>
19
19
 
@@ -290,36 +290,9 @@ TLS, OIDC/SSO, RBAC, OTEL tracing, policy engine, audit trails. Activated via en
290
290
 
291
291
  ## Purple Lab
292
292
 
293
- The hosted development platform. A Replit-like web UI for visual PRD-to-code workflow, with the Loki agent for iterative development. The same software is free and source-available as the local Loki Mode dashboard; offered managed to teams and enterprises under the **Autonomi** brand (Autonomi Cloud).
293
+ **[DEPRECATED in v7.44.0]** Purple Lab (`loki web`, port 57375) is deprecated. The local build monitor and project dashboard are now the dashboard (auto-launched by `loki start`, http://localhost:57374). For the hosted/commercial platform, see [Autonomi Cloud](https://www.autonomi.dev/).
294
294
 
295
- ```bash
296
- loki web # launches at http://localhost:57375
297
- ```
298
-
299
- <table>
300
- <tr>
301
- <td width="50%" valign="top">
302
-
303
- **Platform Pages**
304
- - Home -- One-line prompt to start building instantly
305
- - Projects -- Browse, search, filter past builds
306
- - Templates -- 20+ starter PRDs by category
307
- - Showcase -- Gallery of example projects to build
308
- - Compare -- Feature comparison vs competitors
309
-
310
- </td>
311
- <td width="50%" valign="top">
312
-
313
- **IDE Workspace**
314
- - Monaco editor with tabs, Cmd+P quick open
315
- - AI chat panel for iterative development
316
- - Activity panel: build log, agents, quality gates
317
- - Live preview with URL bar navigation
318
- - Right-click context menu: Review, Test, Explain
319
-
320
- </td>
321
- </tr>
322
- </table>
295
+ The historical feature set (platform pages, Monaco IDE workspace, AI chat panel) lives on in the dashboard and in Autonomi Cloud. `loki web` still invokes the old binary for backward compatibility but will be removed in a future major version.
323
296
 
324
297
  ---
325
298
 
@@ -372,7 +345,7 @@ Status legend: "E2E-verified" means we run real spec-to-code builds on it oursel
372
345
  | `loki status` | Show current status |
373
346
  | `loki dashboard` | Open web dashboard |
374
347
  | `loki preview` | Print running app URL and open in browser (Live App Preview, v7.24.0; was: `loki open`) |
375
- | `loki web` | Launch Purple Lab web UI |
348
+ | `loki web` | Launch Purple Lab web UI [DEPRECATED in v7.44.0 -- use `loki start` which auto-opens the dashboard at http://localhost:57374; for the hosted platform see Autonomi Cloud] |
376
349
  | `loki doctor` | Check environment and dependencies |
377
350
  | `loki plan [PRD]` | Pre-execution analysis: complexity, cost, iterations |
378
351
  | `loki review [--staged\|--diff]` | AI-powered code review with severity filtering |
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.42.0
6
+ # Loki Mode v7.44.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -398,4 +398,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
398
398
 
399
399
  ---
400
400
 
401
- **v7.42.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
401
+ **v7.44.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.42.0
1
+ 7.44.0
@@ -156,6 +156,112 @@ _rewrite_detection_port() {
156
156
  _write_detection "$d_type" "$d_command"
157
157
  }
158
158
 
159
+ # Collect the transitive descendant tree of a PID (children, grandchildren, ...).
160
+ #
161
+ # Echoes one PID per line, deepest-LAST is NOT guaranteed; order is breadth-first
162
+ # from the root. The root PID itself is NOT included. Used by the non-setsid stop
163
+ # fallback (BUG 1): the app is started as `( ... ) &` WITHOUT setsid, so on stock
164
+ # macOS the whole tree (subshell -> bash -lc -> npm -> sh -> node -> workers)
165
+ # inherits the ORCHESTRATOR's process group. A `kill -- -PGID` would therefore
166
+ # signal run.sh and the Claude agent driving it (self-termination), so we MUST
167
+ # walk parent->child links from OUR pid only. This guarantees we never signal a
168
+ # process outside our own subtree: every returned pid has our root as an ancestor.
169
+ #
170
+ # Snapshot semantics: the caller MUST collect the full tree BEFORE sending any
171
+ # signal. If we TERM top-down while walking, grandchildren reparent to init and
172
+ # `pgrep -P <dead-parent>` returns nothing, re-creating the orphaned-worker bug
173
+ # this fix exists to close.
174
+ _app_runner_collect_descendants() {
175
+ local root="$1"
176
+ # Guard against empty / init / kernel pids: walking from 0/1 would sweep
177
+ # unrelated processes. A valid app pid is always > 1.
178
+ case "$root" in
179
+ ''|0|1) return 0 ;;
180
+ esac
181
+ if ! [[ "$root" =~ ^[0-9]+$ ]]; then
182
+ return 0
183
+ fi
184
+
185
+ local -a frontier=("$root")
186
+ local -a found=()
187
+ local pid child
188
+ local -a kids
189
+ # Bound iterations defensively against a pathological/looping tree.
190
+ local guard=0
191
+ while [ "${#frontier[@]}" -gt 0 ] && [ "$guard" -lt 10000 ]; do
192
+ guard=$(( guard + 1 ))
193
+ pid="${frontier[0]}"
194
+ frontier=("${frontier[@]:1}")
195
+ # Direct children of pid.
196
+ kids=()
197
+ while IFS= read -r child; do
198
+ [ -n "$child" ] && kids+=("$child")
199
+ done < <(pgrep -P "$pid" 2>/dev/null)
200
+ local k
201
+ for k in "${kids[@]:-}"; do
202
+ [ -n "$k" ] || continue
203
+ found+=("$k")
204
+ frontier+=("$k")
205
+ done
206
+ done
207
+
208
+ local f
209
+ for f in "${found[@]:-}"; do
210
+ [ -n "$f" ] && printf '%s\n' "$f"
211
+ done
212
+ }
213
+
214
+ # Signal an EXPLICIT, pre-captured set of PIDs with a given signal.
215
+ #
216
+ # Usage: _app_runner_signal_pids <SIGNAL> <pid> [pid ...]
217
+ #
218
+ # Why an explicit list and not "(re-)walk from root": a worker that traps
219
+ # SIGTERM (a Node server doing graceful shutdown is the textbook case) survives
220
+ # the TERM phase while its intermediate ancestors (npm, sh) die. Once the
221
+ # ancestors die, the surviving worker reparents to init, so re-deriving the tree
222
+ # from the now-dead root via `pgrep -P` would return NOTHING -- the KILL phase
223
+ # would be skipped and the orphaned, port-holding worker would live on. That is
224
+ # exactly the orphaned-worker bug (BUG 1) resurfacing at the force-kill phase.
225
+ # The fix: the caller snapshots root + all descendants ONCE before any signal,
226
+ # and every phase (TERM, aliveness, KILL) operates over that frozen list.
227
+ #
228
+ # Safety: the caller builds the list from _app_runner_collect_descendants, which
229
+ # only ever follows parent->child links from OUR pid, so the list can never
230
+ # contain a process outside our own subtree. We signal pids individually (never
231
+ # a process group) because in the non-setsid path the app inherits the
232
+ # orchestrator's process group; a group signal would kill run.sh and the agent.
233
+ # Pids are signaled in REVERSE capture order so descendants (captured after the
234
+ # root) are signaled before the root.
235
+ _app_runner_signal_pids() {
236
+ local sig="$1"; shift
237
+ local -a pids=("$@")
238
+ local i p
239
+ for (( i=${#pids[@]}-1; i>=0; i-- )); do
240
+ p="${pids[$i]}"
241
+ case "$p" in
242
+ ''|0|1) continue ;;
243
+ esac
244
+ kill "-${sig}" "$p" 2>/dev/null || true
245
+ done
246
+ }
247
+
248
+ # True (0) if ANY pid in the EXPLICIT pre-captured list is still alive.
249
+ # Used by the non-setsid stop grace-wait so a deep worker that outlived the main
250
+ # subshell does not let us fall through to "stopped" prematurely. Operates over
251
+ # the frozen snapshot for the same reason _app_runner_signal_pids does.
252
+ _app_runner_any_alive() {
253
+ local p
254
+ for p in "$@"; do
255
+ case "$p" in
256
+ ''|0|1) continue ;;
257
+ esac
258
+ if kill -0 "$p" 2>/dev/null; then
259
+ return 0
260
+ fi
261
+ done
262
+ return 1
263
+ }
264
+
159
265
  # Fix #2 (finding #597): reconcile the recorded port with the port the app
160
266
  # ACTUALLY bound, using the listen line in app.log as the source of truth. This
161
267
  # corrects the dashboard Live Preview even when the app ignores PORT and picks
@@ -887,32 +993,82 @@ app_runner_stop() {
887
993
  fi
888
994
  fi
889
995
 
996
+ # BUG 1 fix: on the non-setsid fallback (the DEFAULT path on stock macOS,
997
+ # which has no setsid) capture the FULL process subtree -- root + every
998
+ # transitive descendant -- ONCE, BEFORE sending any signal. The old
999
+ # `pkill -TERM -P <pid>` reached only ONE level of children, so deep workers
1000
+ # (npm -> sh -> node -> workers) holding the listening socket survived as
1001
+ # orphans and kept the port bound, blocking the next start.
1002
+ #
1003
+ # Capturing once is load-bearing: a worker that traps SIGTERM survives the
1004
+ # TERM phase while its intermediate ancestors die, then reparents to init.
1005
+ # Re-deriving the tree from the now-dead root would return nothing and skip
1006
+ # the KILL phase, leaving the port-holder alive. Every phase below (TERM,
1007
+ # grace-wait, KILL) operates over this one frozen snapshot instead.
1008
+ local -a _stop_snapshot=()
1009
+ if [ "$_APP_RUNNER_HAS_SETSID" != true ]; then
1010
+ _stop_snapshot=("$_APP_RUNNER_PID")
1011
+ local _snap_d
1012
+ while IFS= read -r _snap_d; do
1013
+ [ -n "$_snap_d" ] && _stop_snapshot+=("$_snap_d")
1014
+ done < <(_app_runner_collect_descendants "$_APP_RUNNER_PID")
1015
+ fi
1016
+
890
1017
  # Send SIGTERM to process and children
891
1018
  if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
1019
+ # setsid path: the app is its own process group leader, so a group
1020
+ # signal reaches the whole tree safely. Unchanged.
892
1021
  kill -TERM "-$_APP_RUNNER_PID" 2>/dev/null || kill -TERM "$_APP_RUNNER_PID" 2>/dev/null || true
893
1022
  else
894
- pkill -TERM -P "$_APP_RUNNER_PID" 2>/dev/null || true
895
- kill -TERM "$_APP_RUNNER_PID" 2>/dev/null || true
1023
+ # Group-kill is NOT used here: in this path the app inherits the
1024
+ # orchestrator's process group, so a group signal would kill run.sh and
1025
+ # the agent driving it. Signal the frozen snapshot, descendants first.
1026
+ _app_runner_signal_pids TERM "${_stop_snapshot[@]}"
896
1027
  fi
897
1028
 
898
- # Wait up to 5 seconds for graceful shutdown
1029
+ # Wait up to 5 seconds for graceful shutdown. Key the wait on the WHOLE
1030
+ # snapshot being alive (not just the main pid): a deep worker can outlive the
1031
+ # main subshell, and treating the main pid's exit as "done" is exactly what
1032
+ # let workers leak before. setsid path keeps the simpler main-pid check.
899
1033
  local waited=0
900
1034
  while [ "$waited" -lt 5 ]; do
901
- if ! kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
902
- break
1035
+ if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
1036
+ kill -0 "$_APP_RUNNER_PID" 2>/dev/null || break
1037
+ else
1038
+ _app_runner_any_alive "${_stop_snapshot[@]}" || break
903
1039
  fi
904
1040
  sleep 1
905
1041
  waited=$(( waited + 1 ))
906
1042
  done
907
1043
 
908
1044
  # Force kill if still running
909
- if kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
1045
+ local _still_alive=false
1046
+ if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
1047
+ kill -0 "$_APP_RUNNER_PID" 2>/dev/null && _still_alive=true
1048
+ else
1049
+ _app_runner_any_alive "${_stop_snapshot[@]}" && _still_alive=true
1050
+ fi
1051
+ if [ "$_still_alive" = true ]; then
910
1052
  log_warn "App Runner: process did not stop gracefully, sending SIGKILL"
911
1053
  if [ "$_APP_RUNNER_HAS_SETSID" = true ]; then
912
1054
  kill -KILL "-$_APP_RUNNER_PID" 2>/dev/null || kill -KILL "$_APP_RUNNER_PID" 2>/dev/null || true
913
1055
  else
914
- pkill -KILL -P "$_APP_RUNNER_PID" 2>/dev/null || true
915
- kill -KILL "$_APP_RUNNER_PID" 2>/dev/null || true
1056
+ # BUG 1 fix (KILL phase): SIGKILL the SAME frozen snapshot (root +
1057
+ # all descendants captured pre-signal), so a TERM-trapping worker
1058
+ # that reparented to init is still force-killed. SIGKILL cannot be
1059
+ # trapped, so this is the terminal guarantee that no port-holder
1060
+ # survives. The snapshot does the real work. The fresh walk below only
1061
+ # adds anything while the root is still alive (a worker spawned during
1062
+ # shutdown); once the root is dead it is empty and the snapshot covers.
1063
+ _app_runner_signal_pids KILL "${_stop_snapshot[@]}"
1064
+ local -a _kill_fresh=()
1065
+ local _kf
1066
+ while IFS= read -r _kf; do
1067
+ [ -n "$_kf" ] && _kill_fresh+=("$_kf")
1068
+ done < <(_app_runner_collect_descendants "$_APP_RUNNER_PID")
1069
+ if [ "${#_kill_fresh[@]}" -gt 0 ]; then
1070
+ _app_runner_signal_pids KILL "${_kill_fresh[@]}"
1071
+ fi
916
1072
  fi
917
1073
  fi
918
1074
 
@@ -1094,6 +1250,11 @@ app_runner_watchdog() {
1094
1250
  # it restarts the stack under the same crash-count circuit breaker.
1095
1251
  if [ "$_APP_RUNNER_IS_DOCKER" = true ] && echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
1096
1252
  if app_runner_health_check; then
1253
+ # BUG 3 fix: the breaker is meant to fire on 5 CONSECUTIVE failures.
1254
+ # A confirmed-healthy observation clears any accumulated count so a
1255
+ # long-lived stack that recovered from a few transient blips is not
1256
+ # tripped permanently on cumulative (non-consecutive) crashes.
1257
+ _APP_RUNNER_CRASH_COUNT=0
1097
1258
  return 0
1098
1259
  fi
1099
1260
  _APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
@@ -1125,6 +1286,11 @@ app_runner_watchdog() {
1125
1286
 
1126
1287
  # Process alive, nothing to do
1127
1288
  if kill -0 "$_APP_RUNNER_PID" 2>/dev/null; then
1289
+ # BUG 3 fix: a confirmed-alive observation clears the accumulated crash
1290
+ # count so the breaker fires only on 5 CONSECUTIVE deaths, not on 5
1291
+ # cumulative crashes that were each successfully recovered over a long
1292
+ # session (which would trip the breaker on a HEALTHY app).
1293
+ _APP_RUNNER_CRASH_COUNT=0
1128
1294
  return 0
1129
1295
  fi
1130
1296
 
@@ -1519,8 +1519,17 @@ council_evidence_gate() {
1519
1519
  if committed_files=$(git diff --name-only "$base_sha" HEAD 2>/dev/null); then
1520
1520
  :
1521
1521
  else
1522
- # Base present but unreachable (e.g. shallow clone): fall back to
1523
- # working-tree diff vs HEAD (mirrors proof-generator.py fallback).
1522
+ # Base present but UNREACHABLE (e.g. shallow clone, history rewrite,
1523
+ # or `git reset --hard` -- a documented live hazard). The diff vs the
1524
+ # run-start SHA cannot be computed, so we can no longer prove that the
1525
+ # committed-union diff is empty. Treat this as INCONCLUSIVE, not as
1526
+ # positive empty-diff fabrication evidence: an agent that committed
1527
+ # all its work leaves a clean working tree, and `git diff HEAD` would
1528
+ # read empty -> a false BLOCK. We still fall back to the working-tree
1529
+ # diff vs HEAD to capture any uncommitted work, but the empty-diff
1530
+ # block is suppressed below via the diff_inconclusive guard.
1531
+ diff_inconclusive="true"
1532
+ diff_inconclusive_reason="base_unreachable"
1524
1533
  committed_files=$(git diff --name-only HEAD 2>/dev/null || echo "")
1525
1534
  fi
1526
1535
  unstaged_files=$(git diff --name-only HEAD 2>/dev/null || echo "")
@@ -1543,7 +1552,11 @@ council_evidence_gate() {
1543
1552
  else
1544
1553
  diff_files=0
1545
1554
  fi
1546
- if [ "$diff_files" -eq 0 ]; then
1555
+ # Only treat an empty union as positive fabrication evidence when the
1556
+ # baseline was CONCLUSIVE. If the base SHA was unreachable (history
1557
+ # rewrite / reset --hard), a clean committed tree yields an empty
1558
+ # working-tree diff that must NOT read as empty-diff fabrication.
1559
+ if [ "$diff_files" -eq 0 ] && [ "$diff_inconclusive" != "true" ]; then
1547
1560
  diff_fails="true"
1548
1561
  fi
1549
1562
  fi
package/autonomy/loki CHANGED
@@ -891,7 +891,6 @@ show_landing() {
891
891
  echo "Get started:"
892
892
  echo -e " ${CYAN}loki start ./prd.md${NC} Build from a spec (PRD file, GitHub issue, or no arg)"
893
893
  echo -e " ${CYAN}loki demo${NC} Build a sample todo app end to end (real run)"
894
- echo -e " ${CYAN}loki web${NC} Open the visual builder to input a spec and watch agents build"
895
894
  echo -e " ${CYAN}loki dashboard start${NC} Start the live run monitor (then: loki dashboard open)"
896
895
  echo ""
897
896
  echo -e "Need help? ${CYAN}loki help${NC} lists every command."
@@ -1057,6 +1056,7 @@ cmd_start() {
1057
1056
  echo "Options:"
1058
1057
  echo " --provider NAME AI provider: claude (default), codex, cline, aider"
1059
1058
  echo " --parallel Enable parallel mode with git worktrees"
1059
+ echo " --allow-haiku Enable Haiku model for the fast tier (default: disabled)"
1060
1060
  echo " --bg, --background Run in background mode"
1061
1061
  echo " --simple Force simple complexity tier (3 phases)"
1062
1062
  echo " --complex Force complex complexity tier (8 phases)"
@@ -1180,6 +1180,17 @@ cmd_start() {
1180
1180
  args+=("--parallel")
1181
1181
  shift
1182
1182
  ;;
1183
+ --allow-haiku)
1184
+ # Enable Haiku for the fast tier. Mirrors the LOKI_ALLOW_HAIKU=true
1185
+ # env var (consumed by providers/claude.sh and run.sh). Documented in
1186
+ # loki --help and run.sh; previously only the env var worked here, so
1187
+ # `loki start ./prd.md --allow-haiku` aborted with "Unknown option".
1188
+ # Export reaches the runner; also forward as an arg so the run.sh
1189
+ # parser (run.sh:15015) sees it on every route.
1190
+ export LOKI_ALLOW_HAIKU=true
1191
+ args+=("--allow-haiku")
1192
+ shift
1193
+ ;;
1183
1194
  --regen-prd|--regenerate-prd|--regen|--fresh-prd)
1184
1195
  # v7.8.1: force a fresh generated PRD on a no-PRD run, overriding
1185
1196
  # the staleness-aware reuse (decide_generated_prd_action in
@@ -3958,9 +3969,9 @@ cmd_dashboard_help() {
3958
3969
  echo "Usage: loki dashboard <command> [options]"
3959
3970
  echo ""
3960
3971
  echo "Note: 'loki dashboard' is the operations/observability UI (port ${DASHBOARD_DEFAULT_PORT})."
3961
- echo " It is NOT the same as 'loki web' (Purple Lab, port ${PURPLE_LAB_DEFAULT_PORT}, where you input PRDs)."
3962
- echo " Use 'loki dashboard' to monitor agents, tasks, costs, council, escalations."
3963
- echo " Use 'loki web' to submit a PRD and watch agents build."
3972
+ echo " It is NOT the same as 'loki web' (Purple Lab, port ${PURPLE_LAB_DEFAULT_PORT}, deprecated v7.44.0)."
3973
+ echo " Use 'loki dashboard' to monitor agents, tasks, costs, council, escalations,"
3974
+ echo " and to submit a PRD via the embedded 'Lab' tab (replaces the deprecated 'loki web')."
3964
3975
  echo ""
3965
3976
  echo "Commands:"
3966
3977
  echo " start Start the dashboard server"
@@ -4193,9 +4204,11 @@ cmd_dashboard_start() {
4193
4204
  echo " URL: $url"
4194
4205
  echo " Logs: $log_file"
4195
4206
  echo ""
4196
- # v7.28.x UX #5: distinguish the two browser UIs up front so a newcomer
4197
- # who wants to submit a spec does not land on the ops monitor by mistake.
4198
- echo -e "${DIM}Looking for the project web UI to submit a spec? loki web (:${PURPLE_LAB_DEFAULT_PORT:-57375})${NC}"
4207
+ # v7.28.x UX #5: point a newcomer who wants to submit a spec at the right
4208
+ # surface. v7.44.0: the standalone Purple Lab (loki web) is deprecated and
4209
+ # consolidated here, so steer to the embedded Lab tab instead of spawning
4210
+ # a second port.
4211
+ echo -e "${DIM}Want to submit a spec in the browser? Open the 'Lab' tab in this dashboard.${NC}"
4199
4212
  echo ""
4200
4213
  echo -e "Open in browser: ${CYAN}loki dashboard open${NC}"
4201
4214
  echo -e "Check status: ${CYAN}loki dashboard status${NC}"
@@ -4566,6 +4579,32 @@ PURPLE_LAB_PID_FILE="${PURPLE_LAB_STATE_DIR}/purple-lab.pid"
4566
4579
  cmd_web() {
4567
4580
  local subcommand="${1:-start}"
4568
4581
 
4582
+ # Deprecation (v7.44.0): Purple Lab (loki web, port 57375) is deprecated and
4583
+ # consolidated into the dashboard. The hosted/commercial role moved to the
4584
+ # separate Autonomi Cloud repo. This is a VALUE-PRESERVING deprecation: the
4585
+ # command STILL WORKS (it launches after the banner) so no one is stranded.
4586
+ # The banner + telemetry mirror the run->start deprecation contract: print
4587
+ # to stderr, suppress under machine-output flags (--json/-q/--quiet/json...),
4588
+ # and only emit telemetry when .loki already exists (events/emit.sh creates
4589
+ # "$LOKI_DIR/events/pending" as a side effect that would flip downstream
4590
+ # guards in a clean directory). The --help/-h/help path shows the deprecation
4591
+ # in its own help text instead, so we skip the banner there.
4592
+ case "$subcommand" in
4593
+ --help|-h|help) ;;
4594
+ *)
4595
+ if ! _deprecated_alias_should_suppress "$@"; then
4596
+ echo "loki web (Purple Lab) is deprecated as of v7.44.0. For local build + monitoring, use the dashboard (auto-launches at 'loki start', http://localhost:${DASHBOARD_DEFAULT_PORT}; or 'loki dashboard'). For the hosted platform, see Autonomi Cloud." >&2
4597
+ if [ -d "${LOKI_DIR:-.loki}" ]; then
4598
+ emit_event cli_command_deprecated cli web \
4599
+ "from=web" \
4600
+ "to=dashboard" \
4601
+ "version=7.44.0" \
4602
+ "argv=${*:-}"
4603
+ fi
4604
+ fi
4605
+ ;;
4606
+ esac
4607
+
4569
4608
  case "$subcommand" in
4570
4609
  start)
4571
4610
  shift || true
@@ -4605,13 +4644,19 @@ cmd_web() {
4605
4644
  }
4606
4645
 
4607
4646
  cmd_web_help() {
4608
- echo -e "${BOLD}Purple Lab -- Loki Mode Web UI${NC}"
4647
+ echo -e "${BOLD}Purple Lab -- Loki Mode Web UI (deprecated -- use the dashboard)${NC}"
4609
4648
  echo ""
4610
4649
  echo "Usage: loki web [command] [options]"
4611
4650
  echo ""
4651
+ echo "DEPRECATED as of v7.44.0: Purple Lab is consolidated into the dashboard."
4652
+ echo " For local build + monitoring, use the dashboard (auto-launches at"
4653
+ echo " 'loki start', http://localhost:${DASHBOARD_DEFAULT_PORT}; or 'loki dashboard')."
4654
+ echo " For the hosted platform, see Autonomi Cloud."
4655
+ echo " 'loki web' still works for now (it launches after a deprecation notice),"
4656
+ echo " but it will be removed in a future release. Migrate to the dashboard."
4657
+ echo ""
4612
4658
  echo "Note: 'loki web' is Purple Lab (the PRD-input/build-watch UI, port ${PURPLE_LAB_DEFAULT_PORT})."
4613
4659
  echo " It is NOT the same as 'loki dashboard' (operations UI, port ${DASHBOARD_DEFAULT_PORT})."
4614
- echo " Use 'loki web' to submit a PRD and watch agents build it."
4615
4660
  echo " Use 'loki dashboard' to monitor running agents, tasks, costs, council, escalations."
4616
4661
  echo ""
4617
4662
  echo "Commands:"
package/autonomy/run.sh CHANGED
@@ -7041,6 +7041,48 @@ enforce_test_coverage() {
7041
7041
  local output
7042
7042
  output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" npx mocha 2>&1) || test_passed=false
7043
7043
  details="mocha: $(echo "$output" | tail -3 | tr '\n' ' ')"
7044
+ else
7045
+ # v7.41.x (test-coverage fail-open fix): a real "scripts.test" was
7046
+ # previously missed entirely. A greenfield project whose package.json
7047
+ # has {"scripts":{"test":"node --test"}} (or any non-placeholder test
7048
+ # script) actually runs a working suite via `npm test`, yet the gate
7049
+ # reported runner:none + pass:true -- so a project whose tests FAIL
7050
+ # green-lit identically. Detect a real test script (excluding the npm
7051
+ # placeholder "no test specified") with a JSON parser, not grep (grep
7052
+ # would false-positive on devDeps / unrelated keys), then run the
7053
+ # configured command. This MUST sit before the monorepo/python/go/rust
7054
+ # checks, all of which gate on test_runner=="none".
7055
+ local _pkg_test_script
7056
+ _pkg_test_script=$(_LOKI_PKG="${TARGET_DIR:-.}/package.json" python3 -c "
7057
+ import json, os, sys
7058
+ try:
7059
+ with open(os.environ['_LOKI_PKG']) as f:
7060
+ d = json.load(f)
7061
+ except Exception:
7062
+ sys.exit(0)
7063
+ t = (d.get('scripts') or {}).get('test') or ''
7064
+ # npm's default placeholder; treat as 'no test'.
7065
+ if 'no test specified' in t.lower():
7066
+ sys.exit(0)
7067
+ sys.stdout.write(t.strip())
7068
+ " 2>/dev/null || echo "")
7069
+ if [ -n "$_pkg_test_script" ]; then
7070
+ # LOKI_TEST_COMMAND lets an operator override the invocation; the
7071
+ # default is the project's own `npm test`.
7072
+ local _test_cmd="${LOKI_TEST_COMMAND:-npm test}"
7073
+ # Label the runner by what the script invokes so evidence is
7074
+ # honest (node --test, vitest, jest, etc. all surface here).
7075
+ case "$_pkg_test_script" in
7076
+ *"node --test"*|*"node:test"*) test_runner="node-test" ;;
7077
+ *vitest*) test_runner="vitest" ;;
7078
+ *jest*) test_runner="jest" ;;
7079
+ *mocha*) test_runner="mocha" ;;
7080
+ *) test_runner="npm-test" ;;
7081
+ esac
7082
+ local output
7083
+ output=$(cd "${TARGET_DIR:-.}" && timeout "$gate_timeout" sh -c "$_test_cmd" 2>&1) || test_passed=false
7084
+ details="$test_runner ($_test_cmd): $(echo "$output" | tail -5 | tr '\n' ' ')"
7085
+ fi
7044
7086
  fi
7045
7087
  fi
7046
7088
 
@@ -7165,10 +7207,23 @@ enforce_test_coverage() {
7165
7207
  fi
7166
7208
 
7167
7209
  if [ "$test_runner" = "none" ]; then
7168
- log_info "Test coverage: no test runner detected, skipping"
7210
+ log_info "Test coverage: no test runner detected, recording inconclusive (not pass)"
7211
+ # v7.41.x fail-open fix: previously this wrote pass:true, so a project
7212
+ # whose tests truly do not run was indistinguishable from one whose tests
7213
+ # passed. Record pass:"inconclusive" instead. The completion-council
7214
+ # evidence gate already treats runner=="none" as pass-through regardless
7215
+ # of the pass value (completion-council.sh: runner=='none' short-circuits
7216
+ # BEFORE the `passed is False` block), so genuinely-no-tests stays
7217
+ # non-blocking (no infinite hang), while the JSON record is now honest:
7218
+ # "no tests" never reads as "tests passed". A DETECTED runner that fails
7219
+ # still writes pass:false below and BLOCKS.
7220
+ #
7221
+ # unit-tests.pass is only read for the status-line display (run.sh ~2183,
7222
+ # PASS vs PENDING); keeping the touch preserves the historical
7223
+ # non-blocking behavior for legitimate no-test projects.
7169
7224
  touch "$quality_dir/unit-tests.pass"
7170
7225
  cat > "$quality_dir/test-results.json" << TREOF
7171
- {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":true,"summary":"No test runner detected"}
7226
+ {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":"inconclusive","summary":"No test runner detected"}
7172
7227
  TREOF
7173
7228
  # Finding #598: stamp the per-iteration freshness marker so a later
7174
7229
  # completion-route capture (ensure_completion_test_evidence) reuses this
@@ -14161,6 +14216,22 @@ if __name__ == "__main__":
14161
14216
  log_warn " Review details under .loki/quality/reviews/ ; gate_failures=${gate_failures}"
14162
14217
  _gate_block_for_completion=""
14163
14218
  # Fall through; the gate-failed loop continues normally
14219
+ # HIGH (trust-gate): the checklist hard gate must also guard the
14220
+ # DEFAULT completion-promise / loki_complete_task route, not only the
14221
+ # interval-gated council path (council_evaluate) and the dashboard
14222
+ # force-review path -- both of which already call this gate. Without
14223
+ # it, an agent that leaves a `priority: critical` checklist item
14224
+ # `failing` and claims done on a non-council-interval iteration would
14225
+ # ship, bypassing the checklist gate entirely. council_reverify_checklist
14226
+ # ran above (when a claim is present) so statuses are fresh here.
14227
+ # Mirrors the evidence/held-out gate arms below. No-op safe:
14228
+ # council_checklist_gate returns 0 (pass) when there is no checklist
14229
+ # results file or when no critical items are failing, so this branch
14230
+ # never fires on those projects. Gate output is written by the gate.
14231
+ elif [ "$_completion_claimed" = 1 ] && type council_checklist_gate &>/dev/null && ! council_checklist_gate; then
14232
+ log_warn "Completion claim rejected: critical checklist item(s) failing (hard gate)."
14233
+ log_warn " Details under .loki/council/gate-block.json"
14234
+ # Fall through; keep iterating until critical checklist items pass.
14164
14235
  # v7.19.1: the verified-completion evidence gate must also guard the
14165
14236
  # DEFAULT completion route (a completion claim via loki_complete_task
14166
14237
  # / the completion-promise text), not only the interval-gated council
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.42.0"
10
+ __version__ = "7.44.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: