loki-mode 7.72.0 → 7.74.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,354 @@
1
+ # BRANCH-LIFECYCLE-PLAN.md -- feature-branch-by-default lifecycle + CI/CD-aware deploy
2
+
3
+ **Features:** (A) `loki start` always works out of a feature branch off the branch it was
4
+ run from, commits the work at session end, and ADVISES the user to PR (does not auto-PR by
5
+ default). (B) `loki deploy` (see `docs/DEPLOY-PLAN.md`) becomes CI/CD-aware: when a pipeline
6
+ config is present, the primary advised "deploy" path is commit + push + PR, because the
7
+ enterprise pipeline deploys on merge/push.
8
+
9
+ **Unifying insight:** for most enterprises "git commit + PR" IS the deploy -- their existing
10
+ CI/CD pipeline runs on push/PR. So Loki should leave the user on a committed feature branch
11
+ ready to PR, and `loki deploy` should recognise pipeline projects and advise the git path.
12
+
13
+ **Status:** Architecture plan. No implementation code here. All `run.sh` line anchors below
14
+ were verified by reading the file at authoring time; line numbers drift, so anchors are given
15
+ WITH the function/marker name to re-locate.
16
+
17
+ **Hard environment fact (verified):** `autonomy/run.sh` runs under `set -uo pipefail` (line
18
+ 172) -- there is NO `-e`. The brief's "set -euo pipefail safe" phrasing is loose; this plan is
19
+ written to the actual `set -uo pipefail`. Consequence: bare non-zero returns will NOT abort the
20
+ script, but `set -u` punishes unbound vars and `pipefail` still propagates pipe failures. All
21
+ new code: quote everything, `x=$((x+1))` never `((x++))`, guard every optional tool with
22
+ `command -v`, default every var (`${VAR:-}`).
23
+
24
+ ---
25
+
26
+ ## Product Owner scope locks (RECOMMENDED -- integrator to confirm)
27
+
28
+ These are the decisions the integrator should confirm before build. Each has a clear recommended
29
+ default. Enumerated: default-flip mechanism + opt-out name (A1), base-branch capture (A2), commit
30
+ granularity (A3) + commit-on-failure (A4), resume-reuse (A5), PR advisory-vs-auto + opt-in env name
31
+ (A6), LOKI_DELEGATE_BRANCH interaction (A7), parallel-mode guard (A8), CI/CD detection globs (B1),
32
+ shared push+PR helper (B2), deploy precedence (B3), non-execution invariant (B4).
33
+
34
+ ### Change A locks
35
+
36
+ ### LOCK A1 -- Default-flip mechanism + opt-out name
37
+ - Recommendation: **flip the default of the existing `LOKI_BRANCH_PROTECTION` to `true`** in
38
+ `setup_agent_branch` (currently `local branch_protection="${LOKI_BRANCH_PROTECTION:-false}"`
39
+ at run.sh:6236). `LOKI_BRANCH_PROTECTION=false` stays a fully working opt-out (back-compat
40
+ preserved for anyone who already sets it false; their behavior is unchanged).
41
+ - Justification: zero new surface, the var name already means exactly this, and the opt-out
42
+ contract is unchanged. Introducing a new name (e.g. `LOKI_FEATURE_BRANCH`) would force a
43
+ two-var back-compat matrix (new-true vs old-false precedence) for no behavioral gain.
44
+ - Also update the header doc at run.sh:159 (`# LOKI_BRANCH_PROTECTION ... (default: false)`)
45
+ to read `(default: true)` and the inline comment at run.sh:6235.
46
+
47
+ ### LOCK A2 -- Base-branch capture: PERSIST it, fresh-run-only
48
+ - Capture the branch Loki was run FROM and PERSIST it to `.loki/state/base-branch.txt`
49
+ **before** the `git checkout -b` in `setup_agent_branch`, and **only on a fresh run**.
50
+ - Why persisted, not recomputed: `create_session_pr` runs at session END (run.sh:17464, far
51
+ from the :17298 capture), and by then HEAD is the `loki/*` branch -- recomputing
52
+ `git rev-parse --abbrev-ref HEAD` there would capture the loki branch, not the base. The
53
+ hardcoded `main` at run.sh:6291 AND the missing `--base` on `gh pr create` (run.sh:6307-6314,
54
+ it currently relies on the host default = main) are BOTH the same requirement violation; both
55
+ are fixed by reading this file.
56
+ - Why fresh-run-only: on resume we are already on `loki/*`; re-capturing would poison the base.
57
+ Mirror the proven `_start_sha_file` idiom at run.sh:14276 (`ITERATION_COUNT==0 || ! -s file`).
58
+ - Detached HEAD = `git rev-parse --abbrev-ref HEAD` returns the literal string `HEAD`. Detect
59
+ that exact value; on detached HEAD, do NOT branch and do NOT fabricate a base -- honest
60
+ message, proceed on current ref (LOCK A6).
61
+
62
+ ### LOCK A3 -- Commit granularity: ONE squashed session-end commit
63
+ - Recommendation: a single session-end commit in the main flow, placed immediately BEFORE
64
+ `create_session_pr` at run.sh:17463 (after the loop has fully finished, in the cleanup region).
65
+ - Justification: simpler, `set -uo pipefail`-safe, one honest message, no per-iteration churn,
66
+ and it does not interleave with the live RARV loop. Per-iteration commits would multiply
67
+ failure surface inside the loop and fight the evidence-gate diff window
68
+ (`_LOKI_RUN_START_SHA`, run.sh:14279).
69
+ - Reuse the EXACT safe exclusions already used by the worktree path at run.sh:3401-3402:
70
+ `git add -A ':!.env' ':!*.key' ':!*.pem' ':!credentials*'`.
71
+ - Commit ONLY if something is staged (`git diff --cached --quiet` -> if non-zero, commit).
72
+ Nothing-to-commit is a clean no-op, never an error.
73
+ - Honest commit message, ABSOLUTE project rules: no emoji, no em-dash, no "Co-Authored-By:
74
+ Claude" / no Claude attribution. Recommended message form:
75
+ `Loki Mode session changes (<N> iterations, result=<code>)`.
76
+
77
+ ### LOCK A4 -- Commit-on-failure: COMMIT ALWAYS (result-reflecting message)
78
+ - run.sh:17463 is reached for `result != 0` too. Recommendation: commit regardless of result,
79
+ with the result encoded in the message (LOCK A3). The whole point is to leave committed work
80
+ the user can inspect/PR even on a partial/failed run. Flagged as a scope lock to confirm.
81
+
82
+ ### LOCK A5 -- Resume reuses the branch (signal = state file, not ITERATION_COUNT)
83
+ - Primary reuse gate: `.loki/state/agent-branch.txt` exists AND that branch is the current
84
+ branch OR is checkout-able. If so, reuse it; do NOT mint a new `loki/session-*` (the current
85
+ code mints one every invocation at run.sh:6249-6251, which under default-on would orphan prior
86
+ work on every resume).
87
+ - Do NOT rely solely on `ITERATION_COUNT` at the :17298 call site -- it is populated/used deep
88
+ in the run loop (run.sh:14263) and is NOT confirmed meaningful at session-start; the state
89
+ file is the order-independent, robust signal.
90
+ - Idempotency: if HEAD is ALREADY on any `loki/*` branch (session-* OR delegate-*, see LOCK A7),
91
+ treat as reuse -- do not branch off a loki branch recursively.
92
+
93
+ ### LOCK A6 -- PR is ADVISORY, not auto (opt-in env = LOKI_AUTO_PR=1)
94
+ - Default: `create_session_pr` does NOT `git push` and does NOT `gh pr create`. Instead it
95
+ PRINTS (and best-effort clipboard-copies) the exact commands the user runs:
96
+ `git push -u origin <branch>` then
97
+ `gh pr create --base <base-branch> --head <branch> --title "..." --body "..."`
98
+ (or, if gh absent, the plain GitHub compare URL derived from origin, or a branch-name + "open
99
+ a PR on your host" message if the URL cannot be parsed).
100
+ - Opt-in: `LOKI_AUTO_PR=1` restores the current auto-push + auto-PR behavior (back-compat for
101
+ anyone relying on today's behavior), and that path MUST also emit `--base <base-branch>`.
102
+ - Justification: matches the deploy-advisory ethos (advise, never act outward), reversible,
103
+ outward-safe, and is literally what the founder asked ("so the user can do a PR then").
104
+
105
+ ### LOCK A7 -- Interaction with LOKI_DELEGATE_BRANCH (do NOT refactor; do NOT enable both)
106
+ - A third branch mechanism exists: `LOKI_DELEGATE_BRANCH=1` creates `loki/delegate-<ts>` at
107
+ run.sh:14263 inside `run_autonomous()` (default OFF). Do NOT unify it with `setup_agent_branch`
108
+ -- that is scope creep on an untouched feature with high blast radius.
109
+ - VERIFIED call order: `setup_agent_branch` is called from `main()` at run.sh:17298, BEFORE
110
+ `main()` calls `run_autonomous()` (run.sh:14105, which contains the :14263 delegate code). So
111
+ with branch-default ON, setup would create `loki/session-*` first, then a later
112
+ `LOKI_DELEGATE_BRANCH=1` would nest `loki/delegate-*` on top of it -- the two do NOT compose
113
+ order-independently. Do NOT claim they do.
114
+ - Resolution: branch-default being ON makes `LOKI_DELEGATE_BRANCH` REDUNDANT (both isolate work on
115
+ a `loki/*` branch). Recommendation: document "do not enable both; if you want the delegate
116
+ naming, also set `LOKI_BRANCH_PROTECTION=false` so only one mechanism branches." The LOCK A5
117
+ idempotency rule (no-op if HEAD already on any `loki/*` branch) is still the safety net that
118
+ prevents `setup_agent_branch` from nesting when delegate happened to run first (e.g. on a
119
+ resume), but it is NOT a general order-independence guarantee.
120
+
121
+ ### LOCK A8 -- Parallel-mode guard for the session-end commit
122
+ - The worktree path already commits at run.sh:3403 and merges back; the conflict-merge path
123
+ commits at run.sh:3646. The session-end commit at :17463 is in the SHARED main flow.
124
+ - Recommendation: gate the session-end commit on `[ "${PARALLEL_MODE:-false}" != "true" ]`
125
+ (PARALLEL_MODE is set at run.sh:778 from LOKI_PARALLEL_MODE). Belt-and-suspenders: the
126
+ "commit only if staged" check (LOCK A3) makes it a no-op anyway after a worktree merge, but
127
+ the explicit guard documents intent and avoids a redundant/confusing squash commit on top of
128
+ the merge commits.
129
+
130
+ ---
131
+
132
+ ### Change B locks
133
+
134
+ ### LOCK B1 -- CI/CD detection globs (exact)
135
+ Detect (read-only file existence, in `${TARGET_DIR:-.}`):
136
+ - GitHub Actions: any file matching `.github/workflows/*.yml` OR `.github/workflows/*.yaml`
137
+ - GitLab CI: `.gitlab-ci.yml`
138
+ - Jenkins: `Jenkinsfile`
139
+ - CircleCI: `.circleci/config.yml`
140
+ - Azure Pipelines: `azure-pipelines.yml`
141
+ - Bitbucket: `bitbucket-pipelines.yml`
142
+ Presence of ANY one = "pipeline project".
143
+
144
+ ### LOCK B2 -- Shared push+PR helper: NEW sourced lib (literal shared function IS buildable)
145
+ - VERIFIED architecture fact: `autonomy/loki` does NOT source `autonomy/run.sh` (it shells out
146
+ to `RUN_SH` as a subprocess, run.sh:199 in loki). They are separate entry points. BUT `loki`
147
+ already sources sibling libs (tui.sh, crash.sh, quickstart.sh, provider-offer.sh, telemetry.sh
148
+ -- loki:59-252) and `run.sh` sources libs too. So a literal shared function IS buildable via a
149
+ NEW small lib that BOTH source.
150
+ - Recommendation: create `autonomy/lib/git-pr-advisory.sh` exposing pure, print-only helpers:
151
+ - `_git_pr_advisory_origin_url <dir>` -- echoes origin URL or empty (best-effort).
152
+ - `_git_pr_advisory_compare_url <origin_url> <base> <head>` -- echoes a GitHub compare URL, or
153
+ empty if not parseable. Reuse the ssh/https GitHub parse idiom already at run.sh:2123-2133.
154
+ - `print_pr_advice <base_branch> <head_branch> [<dir>]` -- prints the `git push -u origin
155
+ <head>` line, then the `gh pr create --base <base> --head <head> ...` line if `command -v
156
+ gh`, else the compare URL, else branch-name + "open a PR on your host." Best-effort
157
+ clipboard-copy of the push line (TTY-gated, `command -v` guarded, always `|| true`).
158
+ Both `run.sh` (Change A `create_session_pr`) and `loki` (`cmd_deploy`) source this lib and call
159
+ `print_pr_advice` so the two surfaces print BYTE-IDENTICAL, correct commands.
160
+ - This is the single source of truth that prevents the two surfaces from drifting.
161
+
162
+ ### LOCK B3 -- Deploy precedence when a pipeline IS detected
163
+ - Print the git/PR path FIRST (most idiomatic for pipeline projects), via the SAME
164
+ `print_pr_advice` helper (commit-if-needed advice + push + PR). THEN the cloud-CLI options
165
+ from DEPLOY-PLAN.md as secondary.
166
+ - When NO pipeline config present: fall back to the cloud-CLI advisory exactly as DEPLOY-PLAN.md
167
+ specifies (no behavior change to that path).
168
+
169
+ ### LOCK B4 -- Same hard invariant as DEPLOY-PLAN.md
170
+ - `loki deploy` is PRINT-ONLY. It NEVER runs `git push`, NEVER runs any cloud CLI, not even
171
+ `--version`. Tool detection is `command -v` ONLY. It ADVISES; the user runs the command. This
172
+ extends DEPLOY-PLAN LOCK 5's non-execution invariant to git push as well.
173
+
174
+ ---
175
+
176
+ ## Change A: full design (with verified run.sh anchors)
177
+
178
+ ### A.0 Verified current state
179
+ - `setup_agent_branch()` at run.sh:6232; gated `${LOKI_BRANCH_PROTECTION:-false}` at :6236;
180
+ early-returns when off (:6238-6241); else mints `loki/session-<ts>-$$` (:6249-6251) via
181
+ `git checkout -b` (:6256), writes `.loki/state/agent-branch.txt` (:6263). Called once at
182
+ run.sh:17298.
183
+ - `create_session_pr()` at run.sh:6270; reads agent-branch.txt (:6273-6285); counts commits with
184
+ hardcoded `main`: `git rev-list --count HEAD ^"$(git merge-base HEAD main ...)"` at :6291; if
185
+ >0 AUTO `git push -u origin` (:6299) and AUTO `gh pr create` with NO `--base` (:6307-6314).
186
+ Called at run.sh:17464.
187
+ - The MAIN single-stream RARV loop does NOT commit. Only the worktree path commits (:3403) and
188
+ the conflict-merge path (:3646). So a normal `loki start ./prd.md` leaves edits UNCOMMITTED ->
189
+ `create_session_pr`'s commit-count is 0 -> it skips. "Complete on a feature branch ready to
190
+ PR" is hollow today. THIS CHANGE ADDS THE COMMIT STEP (LOCK A3).
191
+ - `set -uo pipefail` at run.sh:172 (no -e). Origin-URL ssh/https parse precedent at
192
+ run.sh:2123-2133. PARALLEL_MODE at run.sh:778.
193
+
194
+ ### A.1 `setup_agent_branch` rewrite (run.sh:6232-6268)
195
+ 1. Flip default: `local branch_protection="${LOKI_BRANCH_PROTECTION:-true}"` (LOCK A1). Keep the
196
+ `!= "true"` early-return so `=false` opt-out still works.
197
+ 2. Guard non-git: keep `git rev-parse --is-inside-work-tree` check (:6244) -> honest no-op.
198
+ 3. Capture current ref: `cur="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo HEAD)"`.
199
+ - If `cur == "HEAD"` (detached): honest message ("detached HEAD; staying on current commit,
200
+ no feature branch created"), return 0, write NO base file (LOCK A2/A6).
201
+ - If `cur == loki/*` (session-* or delegate-*): idempotent reuse -- log "already on loki
202
+ branch <cur>", ensure agent-branch.txt records it, return 0 (LOCK A5/A7). Do NOT branch.
203
+ 4. Resume reuse: if `.loki/state/agent-branch.txt` exists and names a branch that is checkout-
204
+ able, `git checkout <that>` and return (LOCK A5). Do NOT mint a new name.
205
+ 5. Fresh branch: mint `loki/session-<ts>-$$` as today, BUT first persist base fresh-run-only:
206
+ `mkdir -p .loki/state; [ ! -s .loki/state/base-branch.txt ] && printf '%s\n' "$cur" >
207
+ .loki/state/base-branch.txt` (LOCK A2). Then `git checkout -b`, write agent-branch.txt.
208
+ 6. All git calls `command -v git` guarded and `2>/dev/null`; no `((..))`; quote everything.
209
+
210
+ ### A.2 Session-end commit (NEW, before run.sh:17463)
211
+ Insert a `commit_session_changes` call in the cleanup region immediately before
212
+ `create_session_pr` (:17463). Function:
213
+ 1. `command -v git` + inside-work-tree guard; else return 0.
214
+ 2. Guard: `[ "${PARALLEL_MODE:-false}" = "true" ]` -> return 0 (LOCK A8).
215
+ 3. `git add -A ':!.env' ':!*.key' ':!*.pem' ':!credentials*'` (exact exclusions from :3401-3402).
216
+ 4. `if git diff --cached --quiet; then return 0; fi` (nothing staged = clean no-op, LOCK A3).
217
+ 5. `git commit -m "Loki Mode session changes (${ITERATION_COUNT:-0} iterations, result=${result})"`
218
+ (honest, no emoji/em-dash/Claude attribution; commit-always incl. failure, LOCK A3/A4).
219
+ 6. `audit_agent_action "git_commit" ...` for the audit chain (mirrors :3647).
220
+
221
+ ### A.3 `create_session_pr` rewrite (run.sh:6270-6326)
222
+ 1. Read agent-branch.txt (keep early-returns). Read base from `.loki/state/base-branch.txt`;
223
+ if empty/missing, do not fabricate -- skip with honest message (detached/no-base case).
224
+ 2. Commit count uses the captured base, NOT main:
225
+ `git rev-list --count HEAD ^"$(git merge-base HEAD "$base" 2>/dev/null || echo HEAD)"`.
226
+ Fixes the run.sh:6291 violation. If 0 commits -> honest "no commits to PR" no-op.
227
+ 3. Default (advisory): call `print_pr_advice "$base" "$branch_name"` from the shared lib
228
+ (LOCK B2) -- prints `git push -u origin <branch>` + `gh pr create --base <base> --head
229
+ <branch> ...` (or compare URL / fallback), best-effort clipboard. Does NOT push, does NOT
230
+ create a PR.
231
+ 4. `LOKI_AUTO_PR=1` -> restore current auto behavior: `git push -u origin` then `gh pr create`
232
+ but now WITH `--base "$base"` (LOCK A6). Keep the existing title/body block (:6308-6313).
233
+ 5. Source the shared lib near the top of run.sh where sibling libs are sourced.
234
+
235
+ ### A.4 No-op matrix (all honest, all proceed, set -uo pipefail safe)
236
+ | Situation | Behavior |
237
+ |---|---|
238
+ | Non-git dir | skip branch + skip commit + skip PR advice; honest log; run proceeds |
239
+ | Detached HEAD (`abbrev-ref`==`HEAD`) | no branch, no base file, no fabricated base; honest msg |
240
+ | Already on `loki/*` (session/delegate) | reuse, do not nest-branch (LOCK A5/A7) |
241
+ | Resume w/ existing agent-branch.txt | checkout + reuse that branch, do not mint new |
242
+ | CI without a remote | commit happens; push/PR advice prints (push will be user's problem); no crash |
243
+ | Nothing staged at session end | commit no-op (clean), advice notes "no commits to PR" |
244
+ | Parallel mode | session-end commit skipped (worktree already committed) |
245
+
246
+ ---
247
+
248
+ ## Change B delta (folds into docs/DEPLOY-PLAN.md)
249
+
250
+ Add to `cmd_deploy` (DEPLOY-PLAN section 2.3), as a new FIRST step before the cloud-CLI cascade:
251
+ 1. `_deploy_detect_cicd "$dir"` -- returns 0 if any LOCK B1 glob matches, echoes the detected
252
+ system name(s). Pure, read-only file existence (`compgen -G` or a `for f in .github/
253
+ workflows/*.yml; do [ -e "$f" ] && ...; done` guarded with nullglob-safe iteration).
254
+ 2. If a pipeline IS detected: print a "CI/CD pipeline detected (<system>)" header, then call
255
+ the SHARED `print_pr_advice "$base" "$head" "$dir"` (LOCK B2) FIRST -- commit-if-needed +
256
+ push + PR -- because the pipeline deploys on merge/push. For `loki deploy` the base/head are
257
+ derived from the current repo state (`git rev-parse --abbrev-ref HEAD`); if on a `loki/*`
258
+ branch, base = `.loki/state/base-branch.txt` when present, else origin's default branch
259
+ best-effort, else honest "set your PR base manually." THEN print the cloud-CLI options from
260
+ DEPLOY-PLAN.md as secondary.
261
+ 3. If NO pipeline detected: unchanged DEPLOY-PLAN.md cloud-CLI advisory.
262
+ 4. Invariant (LOCK B4): print-only. `loki deploy` NEVER runs `git push` or any cloud CLI;
263
+ `command -v` only.
264
+
265
+ ---
266
+
267
+ ## SDET test plan
268
+
269
+ ### Change A: tests/test-branch-lifecycle.sh (NEW)
270
+ Harness mirrors tests/test-preview-public.sh: `set -uo pipefail`; SCRIPT_DIR/PROJECT_DIR;
271
+ PASS/FAIL/TOTAL counters with `pass`/`fail` helpers; `mktemp -d` WORKROOT; `trap cleanup EXIT
272
+ INT TERM`; color globals exported empty. Extract the contiguous branch block (setup_agent_branch,
273
+ commit_session_changes, create_session_pr) from run.sh by NAME ANCHOR (awk from
274
+ `setup_agent_branch() {` to the close of `create_session_pr`) into a temp lib and source THAT
275
+ (do not source run.sh -- it runs main). Non-vacuity gate: assert all three function defs are
276
+ present in the extracted lib, else fail loudly and abort. Every un-runnable case emits visible
277
+ FAIL/SKIP, never a silent pass.
278
+
279
+ Each test builds a throwaway git repo fixture under WORKROOT (`git init`, set user.email/name
280
+ to test values, an initial commit on a NAMED non-main branch e.g. `develop` to prove base != main).
281
+
282
+ 1. **Branch created off CURRENT branch, not main.** Fixture HEAD on `develop`. Run
283
+ setup_agent_branch. Assert: HEAD now on `loki/session-*` AND `.loki/state/base-branch.txt`
284
+ contents == `develop` (NOT `main`). Tripwire for the run.sh:6291 / missing-`--base` violation.
285
+ 2. **Resume reuses the branch.** Run setup_agent_branch twice (simulate resume: agent-branch.txt
286
+ persists). Assert exactly ONE `loki/session-*` branch exists (`git branch --list 'loki/*'` ==
287
+ 1) and HEAD is on it -- no second branch minted.
288
+ 3. **Commit happens / uncommitted -> committed.** Make an uncommitted file change. Run
289
+ commit_session_changes. Assert: `git status --porcelain` is clean AND `git log -1 --format=%s`
290
+ matches the honest message AND the message contains NO emoji/em-dash and NO "Claude".
291
+ 4. **Nothing-to-commit is a clean no-op.** Clean tree. Run commit_session_changes inside a
292
+ `set -u -o pipefail` subshell; assert it returns 0, creates NO new commit (commit count
293
+ unchanged), and reaches an "ALIVE" sentinel after the call (no abort).
294
+ 5. **Non-git dir no-op.** WORKROOT subdir that is NOT a git repo. Run all three functions; assert
295
+ no crash, return 0, honest message, no files created under a non-existent `.git`.
296
+ 6. **Detached-HEAD no-op.** `git checkout <sha>` to detach. Run setup_agent_branch. Assert: no
297
+ `loki/*` branch created, NO base-branch.txt written, honest "detached" message, HEAD still
298
+ detached at that sha.
299
+ 7. **Already-on-loki-branch no-op (idempotency).** Manually `git checkout -b loki/session-x`.
300
+ Run setup_agent_branch. Assert: no NEW branch, still on `loki/session-x`, not nested.
301
+ 8. **HEADLINE -- advisory prints push+PR commands and does NOT push.** Build a bare remote
302
+ (`git init --bare remote.git`, `git remote add origin <bare>`) in the fixture, with at least
303
+ one commit on the working branch. Run create_session_pr in DEFAULT mode (no LOKI_AUTO_PR).
304
+ Assert BOTH halves: (a) NO push occurred -- the bare remote has ZERO refs afterward
305
+ (`git --git-dir=remote.git show-ref` is empty / returns non-zero); and (b) the exact strings
306
+ `git push -u origin loki/session-` AND `gh pr create --base develop` were PRINTED to stdout
307
+ (non-vacuity -- an empty-remote check alone passes falsely if nothing was printed). This is the
308
+ highest-stakes test: it proves the advisory advises without acting outward.
309
+ 9. **LOKI_AUTO_PR=1 opt-in pushes (and uses --base).** With the fake-git push sentinel, run
310
+ create_session_pr under `LOKI_AUTO_PR=1`; assert the push sentinel IS present and the gh
311
+ invocation (stub gh) received `--base develop`. Proves back-compat opt-in.
312
+ 10. **set -u / no-abort.** Run a representative setup_agent_branch + commit + advisory inside a
313
+ `set -u -o pipefail` subshell with minimal env; assert it reaches an "ALIVE" echo (no unbound
314
+ var / pipefail abort).
315
+
316
+ ### Change B: extend tests/test-deploy.sh (per DEPLOY-PLAN.md section 5)
317
+ - **CI/CD detected -> git path printed FIRST.** Fixture with `.github/workflows/ci.yml` + a
318
+ Next.js package.json + all four cloud-CLI stubs on PATH. Assert the `git push`/`gh pr create`
319
+ advisory block is printed BEFORE the `vercel --prod` block (ordering), AND the non-execution
320
+ invariant still holds (no cloud-CLI sentinel, no git push sentinel).
321
+ - **No CI/CD -> cloud-CLI fallback unchanged.** Fixture with NO pipeline config; assert behavior
322
+ identical to DEPLOY-PLAN.md's existing cloud-CLI cascade (no git advisory printed).
323
+ - **Detection per glob.** One fixture per LOCK B1 file; assert `_deploy_detect_cicd` returns 0
324
+ and names the right system for each.
325
+ - **Shared-helper identical output.** Assert the push+PR lines printed by `loki deploy` are
326
+ byte-identical to those from run.sh's create_session_pr for the same base/head (proves the
327
+ shared lib is the single source of truth).
328
+
329
+ ### Wiring into tests/run-all-tests.sh
330
+ Add near the preview registration (run-all-tests.sh:~204, after the FEAT-PREVIEW line):
331
+ ```
332
+ run_test "Branch Lifecycle (default-on, base!=main, commit, advisory no-push)" "$SCRIPT_DIR/test-branch-lifecycle.sh"
333
+ ```
334
+ (test-deploy.sh registration is already covered by DEPLOY-PLAN.md.) Both files are also covered
335
+ transitively by tests/run-shellcheck.sh and the mock/mutation detectors (gates #8/#9). The new
336
+ lib `autonomy/lib/git-pr-advisory.sh` must pass tests/run-shellcheck.sh clean.
337
+
338
+ ---
339
+
340
+ ## Docs to update on release
341
+ - README.md -- note `loki start` leaves work on a committed feature branch and PRINTS the PR
342
+ command (advisory; user runs the PR). Keep "human runs deploy" claims TRUE.
343
+ - CHANGELOG.md -- FEAT-BRANCH-DEFAULT (default-on feature branch + session-end commit + advisory
344
+ PR; `LOKI_BRANCH_PROTECTION=false` opt-out preserved; `LOKI_AUTO_PR=1` opt-in for old auto
345
+ behavior) and the Change B CI/CD-aware deploy delta.
346
+ - run.sh:159 header -- `LOKI_BRANCH_PROTECTION ... (default: true)`; document `LOKI_AUTO_PR`.
347
+ - wiki/CLI-Reference.md + skills/production.md -- document the branch + advisory-PR workflow.
348
+
349
+ ## Critical files for implementation
350
+ - /Users/lokesh/git/loki-mode/autonomy/run.sh (setup_agent_branch :6232, create_session_pr :6270 incl. hardcoded main :6291 + no-base gh :6307, call sites :17298 / :17464, new session-end commit before :17463, default flip :6236, set line :172)
351
+ - /Users/lokesh/git/loki-mode/autonomy/lib/git-pr-advisory.sh (NEW shared print_pr_advice + origin/compare-URL helpers, sourced by BOTH run.sh and autonomy/loki)
352
+ - /Users/lokesh/git/loki-mode/autonomy/loki (cmd_deploy CI/CD branch + sourcing the shared lib; per docs/DEPLOY-PLAN.md)
353
+ - /Users/lokesh/git/loki-mode/tests/test-branch-lifecycle.sh (NEW -- mirror tests/test-preview-public.sh harness)
354
+ - /Users/lokesh/git/loki-mode/tests/run-all-tests.sh (register the new test near :204)