loki-mode 7.71.0 → 7.73.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.
@@ -0,0 +1,302 @@
1
+ # DEPLOY-PLAN.md -- `loki deploy` (ADVISORY / PRINT-ONLY)
2
+
3
+ **Feature:** `loki deploy` -- detect project type + the user's installed cloud CLI,
4
+ PRINT (and best-effort copy) the canonical deploy command(s), and **never run them**.
5
+
6
+ **Status:** Architecture plan. No implementation code here. Pattern family: the just-shipped
7
+ `loki preview --public` (`_preview_public` at `autonomy/loki:5306`, helpers from `_read_app_state`
8
+ at `:5216`, arg parsing in `cmd_preview` at `:5546`-`:5609`, dispatch arm `preview)` at `:14966`).
9
+
10
+ **Why this exists / why it is safe.** README states (line 447) *"Does not deploy -- human runs
11
+ deploy commands"* and (line 452) *"It does NOT access your cloud accounts ... Human oversight is
12
+ expected for deployment."* This feature keeps that literally true: it is an **advisory printer**.
13
+ It runs `command -v <cli>` ONLY (never the CLI itself, not even `--version`), prints the exact
14
+ command, and the **human runs it**. Fully reversible; touches no cloud account.
15
+
16
+ ---
17
+
18
+ ## Product Owner scope locks (RECOMMENDED -- integrator to confirm)
19
+
20
+ These are the decisions the Product Owner / integrator should lock before build. Each has a
21
+ clear recommended default.
22
+
23
+ ### LOCK 1 -- Command surface: NEW top-level command `loki deploy` (not a flag)
24
+ - Recommendation: **new top-level `deploy)` dispatch arm + `cmd_deploy()`**, NOT `loki preview --deploy`.
25
+ - Justification: `preview` is "show me the local app I already built/started" and is gated on a
26
+ **running app** (`state.json` status=running, live port). `deploy` is conceptually distinct:
27
+ it is a **static, filesystem-only advisory about project type** that must work with nothing
28
+ running and no build started. Folding it into `preview` would force the running-app
29
+ preconditions onto a feature that must not have them (see LOCK 6). Per the CLI-consolidation
30
+ mandate (`autonomy/loki:675`, lean ~17-entry front page), `deploy` does **not** go on the
31
+ front-page command list; it lives in the "More commands" footer (`:728`) and is fully
32
+ documented via `loki deploy --help`. It earns a command (verb users already expect) but not
33
+ front-page real estate.
34
+
35
+ ### LOCK 2 -- Disambiguation: print ALL viable options, most-idiomatic first
36
+ - Recommendation: when several `(project-type x installed-CLI)` pairs are viable, **print every
37
+ viable option, clearly labeled, idiomatic one first.** Never silently pick one.
38
+ - Precedence (the "idiomatic first" order), recommended default:
39
+ | Detected project type | Idiomatic 1st | Then (if that CLI also installed) |
40
+ |---|---|---|
41
+ | Next.js | Vercel | Netlify, Fly (if Dockerfile present) |
42
+ | Static / SPA (dist/ or build/ + index.html; Vite/CRA) | Netlify | Cloudflare Pages, Vercel |
43
+ | Dockerfile / containerized server | Fly | Cloudflare (wrangler), (Render: doc link only) |
44
+ | Generic Node server (package.json, no static build) | Fly | Vercel |
45
+ | Python (requirements.txt / pyproject.toml) | Fly | (doc links: Render/Railway) |
46
+ - Within each row, only print options whose **CLI is actually installed** (`command -v`).
47
+ - "Idiomatic first" = ordering only; it never suppresses a viable installed alternative.
48
+
49
+ ### LOCK 3 -- Per-provider canonical commands (DOCUMENTED, not invented)
50
+ Confidence column: "task" = exactly the form specified in the approved task brief (authoritative
51
+ for this build); placeholders are `<...>` the user fills. No flag is invented.
52
+
53
+ | Provider | CLI binary (`command -v`) | Command printed | Notes / placeholders | Confidence |
54
+ |---|---|---|---|---|
55
+ | Vercel | `vercel` | `vercel --prod` | none | task / high |
56
+ | Netlify | `netlify` | `netlify deploy --prod` | for **static**, append `--dir=<build-output>` (e.g. `dist` or `build`); print the placeholder, do not guess | task / high |
57
+ | Fly.io | `flyctl` | `fly deploy` | **Detect `flyctl`, print `fly deploy`** -- the binary is `flyctl` but the canonical subcommand verb is `fly deploy`; this is intentional, not a typo. If no `fly.toml`, append a one-line note: "run `fly launch` once first to create fly.toml" (note text only, never executed) | task / high |
58
+ | Cloudflare Pages (static) | `wrangler` | `wrangler pages deploy <build-output>` | placeholder dir | task / high |
59
+ | Cloudflare Workers/containers | `wrangler` | `wrangler deploy` | none | task / high |
60
+
61
+ - Where a flag depends on the project (build dir), print a `<placeholder>` and reference official
62
+ docs rather than inventing. Each printed block ends with the provider's official docs URL.
63
+ - **A wrong flag is worse than no feature** -- conservatism over completeness.
64
+
65
+ ### LOCK 4 -- Clipboard: best-effort, cross-platform, never fatal, copy the idiomatic one
66
+ - Recommendation: **always copy the idiomatic (first) command**, and print a note:
67
+ `copied to clipboard: <cmd> (other options shown above)`. Cleaner than "copy only when exactly one".
68
+ - TTY gate: skip clipboard entirely when `[ ! -t 1 ]` (meaningless over SSH / pipes / non-TTY).
69
+ - Tools, in order, guarded by `command -v`: `pbcopy` (macOS); `wl-copy`, `xclip -selection clipboard`,
70
+ `xsel --clipboard --input` (Linux); `clip.exe`/`clip` (Windows/WSL).
71
+ - **Never fatal:** `printf '%s' "$cmd" | <tool> 2>/dev/null || true`. If no tool exists, print is the
72
+ primary output and the command still exits 0. Clipboard failure never changes the exit code.
73
+ - These clipboard tools ARE allowed to run. Only the FOUR cloud CLIs are forbidden (LOCK 5).
74
+
75
+ ### LOCK 5 -- NON-EXECUTION is a tested invariant
76
+ - The command must **NEVER** invoke vercel / netlify / flyctl / wrangler -- not even `--version`.
77
+ - Detection is **`command -v <cli>` ONLY**. No `$cli ...` call anywhere in the code path.
78
+ - Proven by SDET test (see Section 8, headline test): PATH stubs for all four that write a sentinel
79
+ IF invoked; after `loki deploy` runs, assert all four sentinels ABSENT **and** the expected
80
+ command string was printed (both halves -- sentinel-absent alone passes vacuously).
81
+
82
+ ### LOCK 6 -- Filesystem-only; NOT gated on a running app
83
+ - `loki deploy` is a static advisory. It must work with **nothing running, no build started.**
84
+ - It does **NOT** read `app-runner/state.json` and has **no** status=running / live-port
85
+ precondition (this is the key divergence from `_preview_public`). Detection is from project
86
+ files in the target dir, using the same `${TARGET_DIR:-.}` resolution app-runner uses.
87
+ - Only two real preconditions: (a) a project type is detected, else honest non-zero exit;
88
+ (b) at least one matching CLI is installed, else honest install hint + non-zero exit.
89
+
90
+ ### LOCK 7 -- Bun parity: bash-only is acceptable
91
+ - bash-only, no Bun runner change. Precedent: HUD (`FEAT-HUD`) and preview (`FEAT-PREVIEW-LINK`)
92
+ are bash-only; the Bun runner is dormant for the live/advisory path. State explicitly in CHANGELOG.
93
+
94
+ ---
95
+
96
+ ## 1. Project-type detection (filesystem signals)
97
+
98
+ Reuse the signals already proven in `autonomy/app-runner.sh`. All checks are read-only file/dir
99
+ existence + `grep` on `package.json`. Resolve the directory as `local dir="${TARGET_DIR:-.}"`
100
+ (same idiom as `app_runner_init` at `app-runner.sh:739` and `_detect_nextjs_standalone` at `:594`).
101
+
102
+ Detection cascade (first match wins for the *primary* type label; multiple provider options can
103
+ still be offered per LOCK 2):
104
+
105
+ | # | Type | Signal (exact checks) | Source in repo |
106
+ |---|---|---|---|
107
+ | 1 | Next.js | `grep -q '"next"' "$dir/package.json"` OR `[ -f "$dir/next.config.js" ]` OR `next.config.mjs`/`next.config.ts` present | mirrors next detection near `app-runner.sh:800-813` |
108
+ | 2 | Static / SPA | (`[ -d "$dir/dist" ]` OR `[ -d "$dir/build" ]`) AND a built `index.html` in that dir; OR `grep -q '"vite"' package.json` / CRA (`react-scripts`) | vite signal at `app-runner.sh:697` |
109
+ | 3 | Dockerfile / container | `[ -f "$dir/Dockerfile" ]` (and/or `docker-compose.yml`/`compose.yml`) | `app-runner.sh:759`, `:774` |
110
+ | 4 | Generic Node server | `[ -f "$dir/package.json" ]` with a `"start"`/`"dev"` script and no static build dir | `app-runner.sh:798`, `:814`, `:821` |
111
+ | 5 | Python | `[ -f "$dir/requirements.txt" ]` OR `[ -f "$dir/pyproject.toml" ]` | `app-runner.sh:953` |
112
+
113
+ - Do **not** call `_detect_nextjs_standalone`'s artifact-only path as the sole Next signal; for an
114
+ advisory we want the *source* signal (`"next"` in package.json / `next.config.*`) since the build
115
+ may not have run yet. (The artifact path keys on `.next/standalone/server.js` -- too narrow here.)
116
+ - Type -> candidate providers per the LOCK 2 precedence table.
117
+ - If **no** type matches: honest "no deployable project detected here" message naming the signals
118
+ it looked for (package.json / Dockerfile / dist|build / requirements.txt|pyproject.toml), non-zero exit.
119
+
120
+ ---
121
+
122
+ ## 2. Command surface + dispatch wiring
123
+
124
+ ### 2.1 Dispatch arm (new, contiguous block placement)
125
+ Add a `deploy)` arm to the dispatch `case` next to `preview)` (`autonomy/loki:14966`):
126
+ ```
127
+ deploy)
128
+ cmd_deploy "$@"
129
+ ;;
130
+ ```
131
+ No deprecated alias (brand-new verb; nothing to alias).
132
+
133
+ ### 2.2 Function placement -- CONTIGUOUS, for test extractability
134
+ Place ALL deploy code as **one contiguous block** so the SDET test can extract it by name anchor
135
+ the same way `test-preview-public.sh` extracts the preview block (awk from the first helper def to
136
+ the closing `}` of `cmd_deploy`). Recommended location: immediately AFTER `cmd_preview()` ends
137
+ (after `autonomy/loki:5649`-area close), so the two advisory features sit together. The block:
138
+ - `_deploy_detect_type()` -- echoes the primary type label (Section 1); always returns 0.
139
+ - `_deploy_options_for_type()`-- given type, echoes ordered `provider|cli|command` rows (pure, testable).
140
+ - `_deploy_copy_clipboard()` -- best-effort copy (LOCK 4); always returns 0; TTY/`command -v` guarded.
141
+ - `cmd_deploy()` -- arg parse + orchestration + printing.
142
+
143
+ Keep helpers pure where possible (take dir / type as args, echo to stdout, `return 0`) so unit
144
+ tests can call them directly without a process -- mirrors the pure extractors `_extract_tunnel_url_*`.
145
+
146
+ ### 2.3 `cmd_deploy()` flow
147
+ 1. Parse args in a `while [ $# -gt 0 ]` loop (mirror `cmd_preview` `:5555`):
148
+ - `--help|-h|help` -> print help block (Section 2.4), `return 0`.
149
+ - `--dir <path>` -> override the project dir to scan (default `${TARGET_DIR:-.}` then `.`).
150
+ - `--no-clip` -> disable clipboard copy.
151
+ - `--json` -> machine-readable output (optional; see 2.5). Lenient on unknown args (no hard
152
+ error -> no behavior drift), matching `cmd_preview`. Guard value-consuming shifts exactly as
153
+ `--provider` does at `:5595`-`:5599` (avoid set -e underflow when a flag is the last arg).
154
+ 2. `type=$(_deploy_detect_type "$dir")`.
155
+ 3. If empty -> no-project path: honest message + `return 1`.
156
+ 4. `options=$(_deploy_options_for_type "$type")`; filter to rows whose CLI is installed
157
+ (`command -v "$cli" >/dev/null 2>&1`).
158
+ 5. If zero installed CLIs -> no-CLI path: honest install hints for each candidate provider
159
+ (brew + official URL), `return 1` (Section 3).
160
+ 6. Else: print a header (detected type), then each installed option block (label, command,
161
+ docs URL), idiomatic first. Best-effort copy the first command (LOCK 4). `return 0`.
162
+ 7. **Never** call any cloud CLI. Print only.
163
+
164
+ ### 2.4 `--help`
165
+ Mirror the `cmd_preview` help block (`:5557`-`:5585`). Must state: advisory/print-only; it does NOT
166
+ deploy and does NOT run any cloud CLI; it detects project type + your installed CLI and prints the
167
+ command for YOU to run; clipboard is best-effort. List options `--dir`, `--no-clip`, `--json`, `--help`.
168
+
169
+ ### 2.5 `--json` (optional, recommended)
170
+ Emit `{"type": "...", "options": [{"provider","cli","command","docs"}], "copied": "<cmd|>"}`.
171
+ Honest: empty `options` when no CLI installed. Suppress clipboard note under `--json`.
172
+
173
+ ---
174
+
175
+ ## 3. No-CLI-installed path (honest install hints)
176
+
177
+ Mirror the gh-missing / tunnel-missing block at `autonomy/loki:5413`-`:5433`. For each candidate
178
+ provider for the detected type, print to **stderr**, then `return 1`:
179
+ ```
180
+ No deploy CLI found for this <type> project.
181
+ Loki never accesses your cloud account or runs deploy for you -- you run the printed command.
182
+ Install one of the following, then re-run 'loki deploy':
183
+
184
+ Vercel: brew install vercel | https://vercel.com/docs/cli
185
+ Netlify: brew install netlify-cli | https://docs.netlify.com/cli/get-started/
186
+ Fly.io: brew install flyctl | https://fly.io/docs/flyctl/install/
187
+ Cloudflare: npm i -g wrangler | https://developers.cloudflare.com/workers/wrangler/install-and-update/
188
+ ```
189
+ - NEVER fabricate success; NEVER download a binary. Only print candidates relevant to the type.
190
+ - Non-zero exit so scripts/CI see the failure.
191
+
192
+ ## 3b. No-detected-project path
193
+ Honest message to stderr naming the signals checked (Section 1), `return 1`.
194
+
195
+ ---
196
+
197
+ ## 4. set -e / shellcheck safety (file runs under `set -euo pipefail`)
198
+
199
+ - Increment counters as `i=$((i + 1))` (never `((i++))`, which returns 1 at zero and aborts).
200
+ - All `grep`/`command -v` that may "fail" guarded: `grep -q ... || true` or `if command -v ...`.
201
+ - Quote every path: `"$dir"`, `"$dir/package.json"`. Pass paths as argv to any python helper
202
+ (never interpolate into a heredoc) -- mirror `_read_app_state` (`:5219`-`:5227`).
203
+ - If a python heredoc is used (e.g. for `--json`), escape `$` that bash must not expand
204
+ (`\$`); follow `tests/check-heredoc-dollar-digit.sh` (the repo gate for `$1`/`$2` in heredocs).
205
+ - Clipboard pipeline always `|| true`; never let a missing tool or non-TTY abort.
206
+ - Value-consuming flag shifts guarded (`[ $# -ge 2 ] && shift`) -- see `:5595`.
207
+ - Capture sub-function exit into a local then `return $rc` (don't bare-return a non-zero call line
208
+ under set -e) -- mirror `:5614`-`:5617`.
209
+ - Must pass `tests/run-shellcheck.sh` clean.
210
+
211
+ ---
212
+
213
+ ## 5. SDET test plan -- `tests/test-deploy.sh` (NEW)
214
+
215
+ Structure mirrors `tests/test-preview-public.sh`: extract the contiguous deploy block by name anchor
216
+ into a temp lib, source it, drive `cmd_deploy` and the pure helpers. `pass/fail` counters, `mktemp -d`
217
+ WORKROOT, `trap cleanup EXIT INT TERM`. Color globals exported empty. Every un-runnable case emits a
218
+ visible FAIL/SKIP -- never a silent pass. Fixtures live under temp dirs (fake package.json,
219
+ Dockerfile, dist/index.html, requirements.txt).
220
+
221
+ Extraction sanity (non-vacuity gate, like preview lines 113-126): assert all 4 deploy function
222
+ defs are present in the extracted lib, else fail loudly and abort.
223
+
224
+ ### Test 1 (HEADLINE) -- NON-EXECUTION invariant
225
+ - Build a `fake-bin` with stubs `vercel`, `netlify`, `flyctl`, `wrangler`, each:
226
+ `#!/usr/bin/env bash` -> `echo ran > "$SENTINEL_<name>"` -> `exit 0`.
227
+ - Prepend `fake-bin` to PATH so `command -v` RESOLVES all four (CLIs DETECTED + printed).
228
+ - Fixture: a Next.js project dir (package.json with `"next"`).
229
+ - Run `cmd_deploy --dir <fixture>`; capture stdout.
230
+ - Assert BOTH halves:
231
+ 1. **NONE** of the four sentinels exists (no cloud CLI was invoked) -- the core invariant.
232
+ 2. The expected command string (`vercel --prod`) **was printed** (proves non-vacuity -- a
233
+ deploy that prints nothing would pass half 1 falsely).
234
+ - This is the highest-stakes test (echoes the real tunnel-CLI-launched-during-dev incident).
235
+
236
+ ### Test 2 -- Project-type detection (pure helper, per type)
237
+ For each fixture (Next.js / static dist+index.html / Dockerfile / generic Node / Python), assert
238
+ `_deploy_detect_type "$dir"` returns the expected label. Include a "none" fixture (empty dir) ->
239
+ empty label.
240
+
241
+ ### Test 3 -- Option ordering / idiomatic-first (pure helper)
242
+ With all four stubs on PATH, assert `_deploy_options_for_type next` lists Vercel before Netlify
243
+ before Fly; static lists Netlify/Cloudflare before Vercel; Dockerfile lists Fly first. Assert the
244
+ printed command strings are EXACTLY the LOCK 3 canonical forms (mutation-proof: a wrong flag fails).
245
+
246
+ ### Test 4 -- No-CLI-installed path
247
+ Curated clean PATH with NONE of the four CLIs (build_clean_bin idiom, preview lines 191-201;
248
+ symlink only coreutils). Next.js fixture. Assert: honest install hint printed (contains
249
+ "Install one of the following" and at least the Vercel brew + URL) AND non-zero exit AND no sentinel.
250
+
251
+ ### Test 5 -- No-detected-project path
252
+ Empty dir fixture. Assert honest "no deployable project" message naming signals + non-zero exit.
253
+
254
+ ### Test 6 -- Clipboard best-effort, never fatal
255
+ - 6a: PATH with NO clipboard tool (clean bin) + Next.js fixture + on a non-TTY (`</dev/null` /
256
+ piped stdout). Assert exit 0 and the command still PRINTED (clipboard absence is non-fatal).
257
+ - 6b (if a clipboard tool exists on host or via a fake `pbcopy` stub that writes a file): assert the
258
+ idiomatic command was copied AND the "copied to clipboard" note printed. Fake pbcopy is allowed to
259
+ run (it is not a cloud CLI). SKIP-safe if no TTY can be simulated.
260
+
261
+ ### Test 7 -- `--json` honesty (if implemented)
262
+ Assert valid JSON; `options` empty when no CLI installed; non-empty with correct commands when stubs
263
+ present. Suppresses the clipboard note.
264
+
265
+ ### Test 8 -- set -e safety / no-abort
266
+ Run a representative `cmd_deploy` invocation inside a `set -e -o pipefail` subshell and assert it
267
+ reaches a sentinel "ALIVE" echo after the call (proves no spurious abort), mirroring preview 1b
268
+ (lines 304-314).
269
+
270
+ ### Wiring into `tests/run-all-tests.sh`
271
+ Add next to the preview registration (`tests/run-all-tests.sh:204`):
272
+ ```
273
+ run_test "Deploy Advisory (print-only, non-execution invariant)" "$SCRIPT_DIR/test-deploy.sh"
274
+ ```
275
+ Also covered transitively by `tests/run-shellcheck.sh` and the mutation/mock detectors (gates #8/#9).
276
+
277
+ ---
278
+
279
+ ## 6. Docs to update on release
280
+
281
+ - `README.md:447` -- change the Deploy "What Works" cell to note advisory-print: e.g.
282
+ "Generates configs/Dockerfiles/CI-CD; `loki deploy` prints the exact deploy command (advisory)".
283
+ Keep "What Doesn't (Yet)": "Does not run deploy -- human runs the printed command." Line 452's
284
+ "Human oversight is expected for deployment" stays TRUE and unchanged.
285
+ - `CHANGELOG.md` -- new feature entry (FEAT-DEPLOY): advisory print-only `loki deploy`; non-execution
286
+ invariant; filesystem-only detection; best-effort clipboard; bash-only (Bun parity note).
287
+ - `docs/INSTALLATION.md` -- optional "Deploying your build" note pointing at `loki deploy`.
288
+ - `wiki/CLI-Reference.md` -- add the `deploy` command entry + options.
289
+ - `skills/production.md` (and `skills/00-index.md` if it indexes commands) -- document the advisory
290
+ deploy step in the production workflow.
291
+ - Create this plan's sibling precedent set is `docs/PREVIEW-LINK-PLAN.md` / `docs/BUILD-HUD-PLAN.md`
292
+ -- this file (`docs/DEPLOY-PLAN.md`) is the matching plan artifact.
293
+
294
+ ---
295
+
296
+ ## 7. Critical files for implementation
297
+
298
+ - /Users/lokesh/git/loki-mode/autonomy/loki (cmd_deploy + helpers as a contiguous block after cmd_preview ~:5649; dispatch arm next to preview ~:14966; "More commands" footer ~:728)
299
+ - /Users/lokesh/git/loki-mode/autonomy/app-runner.sh (detection signal source: next ~:800-813, vite ~:697, Dockerfile ~:759/:774, python ~:953, ${TARGET_DIR:-.} idiom ~:739)
300
+ - /Users/lokesh/git/loki-mode/tests/test-deploy.sh (NEW -- mirror tests/test-preview-public.sh)
301
+ - /Users/lokesh/git/loki-mode/tests/run-all-tests.sh (register the new test near :204)
302
+ - /Users/lokesh/git/loki-mode/README.md (line 447 Deploy row -> advisory-print)
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.71.0
5
+ **Version:** v7.73.0
6
6
 
7
7
  ---
8
8
 
@@ -395,7 +395,7 @@ provider works inside the container. Provide auth with your Anthropic API key:
395
395
  # Run Loki Mode in Docker (Claude provider, API-key auth)
396
396
  docker run --rm -e ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" \
397
397
  -v $(pwd):/workspace -w /workspace \
398
- asklokesh/loki-mode:7.71.0 start ./my-spec.md
398
+ asklokesh/loki-mode:7.73.0 start ./my-spec.md
399
399
  ```
400
400
 
401
401
  ##### docker compose + .env (no host install)
@@ -0,0 +1,77 @@
1
+ # PREVIEW-LINK-PLAN.md -- Public Preview Link (BYO-tunnel)
2
+
3
+ ## Product Owner scope locks (decided 2026-06-18)
4
+ 1. Command surface: `loki preview --public` (a flag on the existing command, NOT a new top-level command and NOT `loki share preview` which is a deprecated alias to report-gist). Respects the CLI-consolidation mandate.
5
+ 2. Lifecycle: FOREGROUND-blocking with a trap teardown + "Press Ctrl+C to stop sharing." (Safer than background+pidfile: no orphaned public tunnel a user forgets about.)
6
+ 3. Default provider / detection order: cloudflared first (quick tunnels need NO account), ngrok second (needs authtoken). `--provider cloudflared|ngrok` override.
7
+ 4. Host-header rewrite: default-ON (cloudflared `--http-host-header localhost`, ngrok `--host-header=rewrite`) with a `--no-host-rewrite` escape. Fixes the #1 dev-server "Invalid Host header" failure.
8
+ 5. Bun parity: bash-only is acceptable for v7.72.0 (HUD precedent; the Bun runner is dormant for the live path). No loki-ts mirror required now.
9
+ 6. Consent: explicit, default-NO. Interactive `[y/N]` on a TTY (only ^[Yy] proceeds); `--yes` skips the prompt but still prints the warning; non-TTY without `--yes` REFUSES.
10
+
11
+ ## 1. Goal
12
+ Loki builds + runs the app locally and `loki preview` (cmd_preview, autonomy/loki:5212) opens it at http://localhost:PORT. There is no way to share the running app. Add a consent-gated `--public` path that creates a PUBLIC URL for the already-running local app by wrapping the USER'S OWN tunnel CLI (cloudflared or ngrok). The app + the user's creds stay on their machine; Loki never proxies traffic and never bundles/downloads a binary. Delivers the "share what was built" wow (Replit/Lovable/Bolt have it) without breaking "your keys, nothing leaves your network."
13
+
14
+ ## 2. Command surface + dispatch
15
+ `loki preview --public` (+ `--provider`, `--yes`, `--no-host-rewrite`). The `preview)` dispatch arm (autonomy/loki:14596 -> cmd_preview "$@") already forwards args; no dispatch-table change. Add the flags to cmd_preview's arg parser (~:5214); `--public` branches into a new `_preview_public` helper BEFORE the existing browser-open logic. Update cmd_preview --help (~:5216-5229).
16
+
17
+ ## 3. Precondition checks (REUSE cmd_preview state.json read)
18
+ Refactor the inline parse at autonomy/loki:5246-5263 into a shared `_read_app_state <state_file>` echoing url/status/port/primary_service; both the existing browser-open path and `--public` call it (no drift). Then, in order, each with honest degrade + non-zero exit:
19
+ 1. state.json exists (${LOKI_DIR}/app-runner/state.json) -> else "No app running. loki start / loki status".
20
+ 2. status == running (mirror :5265) -> else "App is not running (status: X)".
21
+ 3. URL/port resolved (fallback http://localhost:${port:-3000} per :5271-5273).
22
+ 4. PORT reachability (NEW): poll with the curl-readiness pattern at autonomy/loki:4979 (curl -s http://localhost:PORT >/dev/null, few retries, sleep 0.5, retries=$((retries+1))). Dead port -> "not exposing a dead port", non-zero. Never tunnel a dead port.
23
+
24
+ ## 4. Consent (load-bearing, default-OFF)
25
+ - Interactive TTY ([ -t 0 ]): print the full warning (SS9), prompt `Expose this app publicly? [y/N] ` via `read -r` (idiom at :1897-1902) but DEFAULT-N (only ^[Yy] proceeds; deliberately NOT the default-Y at :1894 -- public exposure is unsafe).
26
+ - --yes: skips the prompt; warning still PRINTED.
27
+ - Non-TTY without --yes: REFUSE ("Refusing to expose a public tunnel non-interactively without --yes"), non-zero. Never silently expose.
28
+
29
+ ## 5. BYO-CLI detection + install hint (command -v based, so a PATH stub works in CI)
30
+ Order (override with --provider): cloudflared, then ngrok, else honest install hint + non-zero. NEVER pretend success, NEVER download a binary. Hint mirrors the gh-missing block at :28304-28311; names brew + official URLs for both; states Loki wraps YOUR OWN client.
31
+
32
+ ## 6. URL extraction (pure, testable: read from file/string, not a live process)
33
+ - cloudflared (`cloudflared tunnel --url http://localhost:PORT [--http-host-header localhost]`): quick-tunnel URL prints to stderr/log. Redirect stdout+stderr to ${LOKI_DIR}/preview/cloudflared.log; poll (bounded ~20 x sleep 0.5, tries=$((tries+1))) `grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com'` first match. Timeout no-match -> teardown + non-zero.
34
+ - ngrok (`ngrok http PORT [--host-header=rewrite]`): scrape the local API `curl -s http://127.0.0.1:4040/api/tunnels` -> .tunnels[].public_url (prefer https); python3 json parse, grep fallback; bounded poll. No authtoken -> 4040 never comes up -> honest "ngrok config add-authtoken" hint, non-zero.
35
+ Factor `_extract_tunnel_url_cloudflared <logfile>` and `_extract_tunnel_url_ngrok <json-file-or-string>` as PURE functions for the stub test.
36
+
37
+ ## 7. Lifecycle (foreground + trap, per lock #2)
38
+ - `tunnel_cmd ... > "$log" 2>&1 & tunnel_pid=$!`
39
+ - `trap '_preview_public_teardown "$tunnel_pid"' EXIT INT TERM` immediately. Teardown: `kill -TERM "$tunnel_pid" 2>/dev/null || true; sleep 1; kill -KILL "$tunnel_pid" 2>/dev/null || true` + remove log/pidfile (all || true, set -e safe).
40
+ - After URL capture: print public URL + live warning + "Press Ctrl+C to stop sharing." Then `wait "$tunnel_pid"`. Ctrl+C -> trap -> clean teardown.
41
+ - State dir ${LOKI_DIR}/preview/ (mkdir -p), parallel to app-runner/.
42
+
43
+ ## 8. Host-header (default-ON, lock #4; escape --no-host-rewrite)
44
+ Dev servers (Vite/Next dev/webpack/Django ALLOWED_HOSTS) reject a tunneled Host: <random>.trycloudflare.com with "Invalid Host header". cloudflared `--http-host-header localhost`; ngrok `--host-header=rewrite`. Verify the exact flag against the installed CLI version at runtime; do not hardcode blindly. Document that production-style servers may still need the tunnel host added to their allowlist.
45
+
46
+ ## 9. Help + warning copy (honest, no fabricated safety claims)
47
+ Help appended to cmd_preview --help: --public, --provider, --yes, --no-host-rewrite (per SS2). Warning printed before the prompt every time:
48
+ ```
49
+ WARNING: This makes the app running on THIS machine reachable by ANYONE who has
50
+ the URL, over the public internet, using YOUR tunnel account.
51
+ - The app may have NO authentication. Anyone with the link can use it.
52
+ - Traffic flows through your own cloudflared/ngrok account, not through Loki.
53
+ - This stays up until you stop it. Stop it when you are done.
54
+ ```
55
+ No "secure"/"encrypted" claims beyond what the tunnel CLI itself provides.
56
+
57
+ ## 10. Degrade / error table (all set -e safe)
58
+ state.json absent / status!=running / dead port / no CLI / non-TTY-no-yes / URL-capture-timeout / ngrok-no-authtoken -> honest message + non-zero. Consent declined -> "Aborted. App was not exposed." exit 0. Ctrl+C -> trap teardown + "Tunnel stopped." exit 0.
59
+
60
+ ## 11. Test plan (no real tunnel in CI; FAKE binary on PATH + pure extractors)
61
+ 1. Consent: pipe `n` -> Aborted, no spawn; `y` (fake bin) -> proceeds; --yes skips prompt but prints warning; non-TTY no --yes -> refuse + non-zero.
62
+ 2. CLI-absent: PATH without cloudflared/ngrok -> install hint + non-zero, no download.
63
+ 3. URL extraction: cloudflared stub script prints a fixed trycloudflare URL to its log -> assert extractor returns it (+ a real-format log fixture); ngrok extractor against a fixture 4040 JSON -> assert public_url; empty log -> timeout path tears down + non-zero.
64
+ 4. Preconditions: missing state.json; status=building; unreachable port -> each right message + non-zero.
65
+ 5. Teardown: SIGINT to the fake-bin run -> child pid gone, log/pidfile cleaned, no orphan.
66
+ 6. set -e / lint: bash parity + shellcheck/local-ci over the new code (x=$((x+1)), escaped $ in python heredocs, path as argv).
67
+ Mirror the existing CLI test harness that covers cmd_preview/gist-share.
68
+
69
+ ## 12. Task list
70
+ Agent A (surface, consent, preconditions): extract _read_app_state from :5246-5263 + repoint the existing browser path (regression-test plain `loki preview`); add flags to arg parse; branch --public into _preview_public; preconditions incl port poll (:4979 pattern); consent (warning + default-N + non-TTY/--yes); update --help.
71
+ Agent B (detection, extraction, lifecycle): command -v detection + order + hint; pure _extract_tunnel_url_cloudflared / _extract_tunnel_url_ngrok; foreground launch + trap teardown; host-header flags + --no-host-rewrite.
72
+ Both: tests per SS11; local-ci/parity gate; v7.72.0 bump + changelog (integrator); no commit/push unless asked.
73
+
74
+ ## Critical files
75
+ - autonomy/loki (cmd_preview :5212; arg parse :5214; help :5216-5229; state read to extract :5246-5263; dispatch :14596; consent prompt :1897; nohup/pidfile :4964-4968; curl poll :4979; pgid teardown :2221)
76
+ - autonomy/app-runner.sh (state.json writer :104-135 -- url/status/port/primary_service source of truth)
77
+ - The bash test harness covering cmd_preview (mirror its PATH-stub + fixture style)
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var t6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)t6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};h(D1,{lokiDir:()=>j,homeLokiDir:()=>r$,findRepoRootForVersion:()=>s$,REPO_ROOT:()=>g});import{resolve as a,dirname as a$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=S1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=a$($);if(Z===$)break;$=Z}return a(S1,"..","..","..")}function s$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=a$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function j(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function r$(){return a(QQ(),".loki")}var S1,g;var b=P(()=>{S1=a$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.71.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=KQ(qQ(import.meta.url)),Z=s$(Q);Q$=zQ(XQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var t$=P(()=>{b()});var b1={};h(b1,{runOrThrow:()=>VQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>i$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function VQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new i$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=JQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function JQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var i$;var d=P(()=>{i$=class i$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return UQ?"":$}var UQ,T,S,_,_Z,I,x,y,V;var c=P(()=>{UQ=(process.env.NO_COLOR??"").length>0;T=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),x=s("\x1B[1m"),y=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as _Q}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(_Q($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var Y$;var V$=P(()=>{d()});var e1={};h(e1,{runStatus:()=>cQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as D,basename as CQ}from"path";import{homedir as bQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*x$/Q);if(X>x$)X=x$;let q=x$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${x}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function yQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
2
+ var t6=Object.defineProperty;var i6=($)=>$;function e6($,Q){this[$]=i6.bind(null,Q)}var h=($,Q)=>{for(var Z in Q)t6($,Z,{get:Q[Z],enumerable:!0,configurable:!0,set:e6.bind(Q,Z)})};var P=($,Q)=>()=>($&&(Q=$($=0)),Q);var q$=import.meta.require;var D1={};h(D1,{lokiDir:()=>j,homeLokiDir:()=>r$,findRepoRootForVersion:()=>s$,REPO_ROOT:()=>g});import{resolve as a,dirname as a$}from"path";import{fileURLToPath as $Q}from"url";import{existsSync as F$}from"fs";import{homedir as QQ}from"os";function ZQ(){let $=S1;for(let Q=0;Q<6;Q++){if(F$(a($,"VERSION"))&&F$(a($,"autonomy/run.sh")))return $;let Z=a$($);if(Z===$)break;$=Z}return a(S1,"..","..","..")}function s$($){let Q=$;for(let Z=0;Z<6;Z++){if(F$(a(Q,"VERSION"))&&F$(a(Q,"autonomy/run.sh")))return Q;let z=a$(Q);if(z===Q)break;Q=z}return a($,"..","..","..")}function j(){return process.env.LOKI_DIR??a(process.cwd(),".loki")}function r$(){return a(QQ(),".loki")}var S1,g;var b=P(()=>{S1=a$($Q(import.meta.url));g=ZQ()});import{readFileSync as zQ}from"fs";import{resolve as XQ,dirname as KQ}from"path";import{fileURLToPath as qQ}from"url";function R$(){if(Q$!==null)return Q$;let $="7.73.0";if(typeof $==="string"&&$.length>0)return Q$=$,Q$;try{let Q=KQ(qQ(import.meta.url)),Z=s$(Q);Q$=zQ(XQ(Z,"VERSION"),"utf-8").trim()}catch{Q$="unknown"}return Q$}var Q$=null;var t$=P(()=>{b()});var b1={};h(b1,{runOrThrow:()=>VQ,run:()=>k,commandVersion:()=>WQ,commandExists:()=>f,ShellError:()=>i$});async function k($,Q={}){let Z=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:Q.env?{...process.env,...Q.env}:process.env,cwd:Q.cwd}),z,X;if(Q.timeoutMs&&Q.timeoutMs>0)z=setTimeout(()=>{try{Z.kill("SIGTERM")}catch{}X=setTimeout(()=>{try{Z.kill("SIGKILL")}catch{}},2000)},Q.timeoutMs);try{let[q,K,W]=await Promise.all([new Response(Z.stdout).text(),new Response(Z.stderr).text(),Z.exited]);return{stdout:q,stderr:K,exitCode:W}}finally{if(z)clearTimeout(z);if(X)clearTimeout(X)}}async function VQ($,Q={}){let Z=await k($,Q);if(Z.exitCode!==0)throw new i$(`command failed (${Z.exitCode}): ${$.join(" ")}`,Z.exitCode,Z.stdout,Z.stderr);return Z}async function f($){let Q=JQ($),Z=await k(["sh","-c",`command -v ${Q}`],{timeoutMs:5000});if(Z.exitCode===0)return Z.stdout.trim()||null;return null}function JQ($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function WQ($,Q="--version"){if(!await f($))return null;let z=await k([$,Q],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var i$;var d=P(()=>{i$=class i$ extends Error{message;exitCode;stdout;stderr;constructor($,Q,Z,z){super($);this.message=$;this.exitCode=Q;this.stdout=Z;this.stderr=z;this.name="ShellError"}}});function s($){return UQ?"":$}var UQ,T,S,_,_Z,I,x,y,V;var c=P(()=>{UQ=(process.env.NO_COLOR??"").length>0;T=s("\x1B[0;31m"),S=s("\x1B[0;32m"),_=s("\x1B[1;33m"),_Z=s("\x1B[0;34m"),I=s("\x1B[0;36m"),x=s("\x1B[1m"),y=s("\x1B[2m"),V=s("\x1B[0m")});import{existsSync as _Q}from"fs";async function Z$(){if(Y$!==void 0)return Y$;let $="/opt/homebrew/bin/python3.12";if(_Q($))return Y$=$,$;let Q=await f("python3.12");if(Q)return Y$=Q,Q;let Z=await f("python3");return Y$=Z,Z}async function z$($,Q={}){let Z=await Z$();if(!Z)return{stdout:"",stderr:"python3 not found",exitCode:127};return k([Z,"-c",$],Q)}var Y$;var V$=P(()=>{d()});var e1={};h(e1,{runStatus:()=>cQ});import{existsSync as v,readFileSync as W$,readdirSync as d1,statSync as o1}from"fs";import{resolve as D,basename as CQ}from"path";import{homedir as bQ}from"os";function n1($){let Q=Math.trunc($);if(Q>=1e6)return`${(Math.trunc(Q/1e6*10)/10).toFixed(1)}M`;if(Q>=1000)return`${(Math.trunc(Q/1000*10)/10).toFixed(1)}K`;return String(Q)}function a1($,Q,Z){if(Q===0)return null;let z=Math.trunc($*100/Q),X=Math.trunc($*x$/Q);if(X>x$)X=x$;let q=x$-X,K=S;if(z>=80)K=T;else if(z>=50)K=_;let W="=".repeat(Math.max(0,X))+" ".repeat(Math.max(0,q)),J=n1($),U=n1(Q);return` ${x}${Z}${V} ${K}[${W}]${V} ${z}% (${J} / ${U})`}async function yQ(){if(await f("jq"))return!0;return process.stdout.write(`${T}Error: jq is required but not installed.${V}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -793,4 +793,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
793
793
  `),2}default:return process.stderr.write(`Unknown command: ${Q}
794
794
  `),process.stderr.write(r6),2}}l1();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var qZ=await KZ(Bun.argv.slice(2));process.exit(qZ);
795
795
 
796
- //# debugId=BBA47536FE0B2AB264756E2164756E21
796
+ //# debugId=8A68E6EB2762797464756E2164756E21
package/mcp/__init__.py CHANGED
@@ -57,4 +57,4 @@ try:
57
57
  except ImportError:
58
58
  __all__ = ['mcp']
59
59
 
60
- __version__ = '7.71.0'
60
+ __version__ = '7.73.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "7.71.0",
4
+ "version": "7.73.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "7.71.0",
5
+ "version": "7.73.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",