loki-mode 7.13.0 → 7.15.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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-to-product system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product via the RARV-C closure loop, with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v7.13.0
6
+ # Loki Mode v7.15.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -160,6 +160,8 @@ GROWTH ──[continuous improvement loop]──> GROWTH
160
160
  | `.loki/signals/HUMAN_REVIEW_NEEDED` | Never | When human decision required |
161
161
  | `.loki/state/checkpoints/` | After task completion | Automatic + manual via `loki checkpoint` |
162
162
 
163
+ One-command rollback (v7.5.2+): `loki rollback latest` or `loki rollback to <id>` restores `.loki/` state from a checkpoint. It first captures a forced pre-rollback snapshot of the current state and prints its id, so a rollback is itself undoable (`loki rollback to <that-id>`). Use `loki rollback list` to see checkpoints.
164
+
163
165
  ---
164
166
 
165
167
  ## Module Loading Protocol (Skills)
@@ -381,4 +383,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
381
383
 
382
384
  ---
383
385
 
384
- **v7.13.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
386
+ **v7.15.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 7.13.0
1
+ 7.15.0
package/autonomy/loki CHANGED
@@ -13111,6 +13111,9 @@ main() {
13111
13111
  checkpoint|cp)
13112
13112
  cmd_checkpoint "$@"
13113
13113
  ;;
13114
+ rollback)
13115
+ cmd_rollback "$@"
13116
+ ;;
13114
13117
  council)
13115
13118
  cmd_council "$@"
13116
13119
  ;;
@@ -16438,12 +16441,19 @@ SHOW_EOF
16438
16441
  local restored=0
16439
16442
  for item in "$cp_dir"/*; do
16440
16443
  [ -e "$item" ] || continue
16441
- local name=$(basename "$item")
16444
+ local name
16445
+ name=$(basename "$item")
16442
16446
  [ "$name" = "metadata.json" ] && continue
16447
+ # R6: do not restore the worktree-snapshot sidecar as state.
16448
+ [ "$name" = "worktree-snapshot.txt" ] && continue
16443
16449
 
16444
16450
  if [ -d "$item" ]; then
16445
- rm -rf ".loki/$name"
16446
- cp -r "$item" ".loki/$name" 2>/dev/null && restored=$((restored + 1))
16451
+ # R6 data-loss fix: NEVER `rm -rf ".loki/$name"` -- the
16452
+ # checkpoint store lives under .loki/state/checkpoints/, so
16453
+ # blowing away .loki/state/ would destroy every checkpoint
16454
+ # (including this one). Merge-copy directory contents instead.
16455
+ mkdir -p ".loki/$name"
16456
+ cp -r "$item"/. ".loki/$name"/ 2>/dev/null && restored=$((restored + 1))
16447
16457
  else
16448
16458
  cp "$item" ".loki/$name" 2>/dev/null && restored=$((restored + 1))
16449
16459
  fi
@@ -16451,17 +16461,28 @@ SHOW_EOF
16451
16461
 
16452
16462
  echo -e " Restored: ${GREEN}$restored${NC} state items from $cp_id"
16453
16463
 
16454
- # Show git info for manual code rollback
16455
- local cp_sha=$(_CP_METADATA="$metadata" python3 -c "import json, os; d=json.load(open(os.environ['_CP_METADATA'])); print(d.get('git_sha','unknown'))" 2>/dev/null)
16456
- if [ -n "$cp_sha" ] && [ "$cp_sha" != "unknown" ] && [ "$cp_sha" != "not-a-git-repo" ]; then
16464
+ # Show how to also restore code. R6: prefer the anchored working-tree
16465
+ # snapshot over `git reset --hard <git_sha>`. git_sha is HEAD (the last
16466
+ # commit), and Loki does not commit per iteration, so a hard reset
16467
+ # discards the iteration's work instead of reconstructing it -- the old
16468
+ # hint was misleading.
16469
+ if git rev-parse --verify "refs/loki/cp/${cp_id}" >/dev/null 2>&1; then
16457
16470
  echo ""
16458
- echo -e " ${YELLOW}Note:${NC} Session state has been restored, but code is unchanged."
16459
- echo " To also roll back code to the checkpoint's git state:"
16471
+ echo -e " ${YELLOW}Note:${NC} Session state restored. Code is unchanged."
16472
+ echo " To also restore the working tree to this checkpoint's snapshot:"
16460
16473
  echo ""
16461
- echo -e " ${DIM}git reset --hard ${cp_sha:0:8}${NC}"
16474
+ echo -e " ${DIM}git stash apply refs/loki/cp/${cp_id}${NC}"
16462
16475
  echo ""
16463
- echo -e " ${RED}Warning:${NC} git reset --hard will discard uncommitted changes."
16464
- echo " Consider 'git stash' first to preserve current work."
16476
+ echo -e " ${DIM}(restores tracked files; newly-added files are not removed)${NC}"
16477
+ else
16478
+ local cp_sha
16479
+ cp_sha=$(_CP_METADATA="$metadata" python3 -c "import json, os; d=json.load(open(os.environ['_CP_METADATA'])); print(d.get('git_sha','unknown'))" 2>/dev/null)
16480
+ if [ -n "$cp_sha" ] && [ "$cp_sha" != "unknown" ] && [ "$cp_sha" != "not-a-git-repo" ]; then
16481
+ echo ""
16482
+ echo -e " ${YELLOW}Note:${NC} Session state restored, but no working-tree snapshot"
16483
+ echo " was captured for this checkpoint, so code changes since the last"
16484
+ echo -e " commit (${cp_sha:0:8}) are not restorable from here."
16485
+ fi
16465
16486
  fi
16466
16487
  ;;
16467
16488
 
@@ -16497,6 +16518,157 @@ SHOW_EOF
16497
16518
  esac
16498
16519
  }
16499
16520
 
16521
+ # R6: one-command rollback. Top-level, obvious entry point that mirrors the Bun
16522
+ # `loki rollback` subcommands (list/show/to/latest) so both routes are at parity.
16523
+ # Restore is destructive on .loki/ state, so it ALWAYS captures a forced
16524
+ # pre-rollback snapshot first (re-undoability invariant) and then glob-restores
16525
+ # whatever the checkpoint dir contains (works across all three checkpoint writers).
16526
+ cmd_rollback() {
16527
+ local subcommand="${1:-help}"
16528
+ shift 2>/dev/null || true
16529
+
16530
+ local checkpoints_dir=".loki/state/checkpoints"
16531
+ local index_file="$checkpoints_dir/index.jsonl"
16532
+
16533
+ # Resolve the most recent checkpoint id from on-disk cp-*/chk-* dirs, sorted
16534
+ # by mtime (newest last). Mirrors the Bun "latest = last entry" semantics but
16535
+ # is robust to the three differing id formats.
16536
+ _rollback_latest_id() {
16537
+ [ -d "$checkpoints_dir" ] || return 1
16538
+ ls -1dt "$checkpoints_dir"/*/ 2>/dev/null | head -1 | sed 's#/$##' | xargs -I{} basename {} 2>/dev/null
16539
+ }
16540
+
16541
+ # Force a pre-rollback snapshot of current state, then glob-restore the target.
16542
+ # Delegates the actual restore to `cmd_checkpoint rollback`, which already
16543
+ # glob-restores and is shared with the manual path (no duplication).
16544
+ _rollback_restore() {
16545
+ local target_id="$1"
16546
+ local want_code="$2"
16547
+
16548
+ # Validate id (defense in depth; cmd_checkpoint validates again).
16549
+ if [[ ! "$target_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
16550
+ echo -e "${RED}Error: Invalid checkpoint ID${NC}"
16551
+ return 1
16552
+ fi
16553
+ if [ ! -d "$checkpoints_dir/$target_id" ]; then
16554
+ echo -e "${RED}Error: Checkpoint not found: $target_id${NC}"
16555
+ echo "Run 'loki rollback list' to see available checkpoints."
16556
+ return 1
16557
+ fi
16558
+
16559
+ # Re-undoability: snapshot current state before overwriting it.
16560
+ local pre_id
16561
+ pre_id="rb-pre-$(date -u '+%Y%m%d-%H%M%S')"
16562
+ local pre_dir="$checkpoints_dir/$pre_id"
16563
+ mkdir -p "$pre_dir"
16564
+ local saved=0
16565
+ for item in .loki/session.json .loki/dashboard-state.json .loki/CONTINUITY.md .loki/autonomy-state.json .loki/state .loki/queue; do
16566
+ if [ -e "$item" ]; then
16567
+ cp -r "$item" "$pre_dir/" 2>/dev/null && saved=$((saved + 1))
16568
+ fi
16569
+ done
16570
+ _RB_PID="$pre_id" _RB_TS="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" _RB_DIR="$pre_dir" \
16571
+ _RB_INDEX="$index_file" _RB_CHKDIR="$checkpoints_dir" python3 << 'RB_PRE_EOF' 2>/dev/null || true
16572
+ import json, os
16573
+ meta = {"id": os.environ["_RB_PID"], "timestamp": os.environ["_RB_TS"],
16574
+ "message": "pre-rollback snapshot", "created_by": "loki rollback"}
16575
+ d = os.environ["_RB_DIR"]
16576
+ os.makedirs(d, exist_ok=True)
16577
+ with open(os.path.join(d, "metadata.json"), "w") as f:
16578
+ json.dump(meta, f, indent=2)
16579
+ os.makedirs(os.environ["_RB_CHKDIR"], exist_ok=True)
16580
+ with open(os.environ["_RB_INDEX"], "a") as f:
16581
+ f.write(json.dumps({"id": meta["id"], "timestamp": meta["timestamp"], "message": meta["message"]}) + "\n")
16582
+ RB_PRE_EOF
16583
+ echo -e " ${DIM}Saved prior state as ${pre_id} (undo this rollback with: loki rollback to ${pre_id})${NC}"
16584
+
16585
+ # Perform the actual state restore via the shared glob-restore path.
16586
+ cmd_checkpoint rollback "$target_id"
16587
+
16588
+ # Optional code restore from the anchored working-tree snapshot.
16589
+ if [ "$want_code" = "1" ]; then
16590
+ if git rev-parse --verify "refs/loki/cp/${target_id}" >/dev/null 2>&1; then
16591
+ echo ""
16592
+ echo -e " ${YELLOW}Restoring working tree from snapshot refs/loki/cp/${target_id}...${NC}"
16593
+ if git stash apply "refs/loki/cp/${target_id}" 2>/dev/null; then
16594
+ echo -e " ${GREEN}Working tree restored.${NC} (tracked files only; newly-added files were not removed)"
16595
+ else
16596
+ echo -e " ${RED}Could not apply snapshot cleanly.${NC} Resolve conflicts, or run: git stash apply refs/loki/cp/${target_id}"
16597
+ fi
16598
+ else
16599
+ echo ""
16600
+ echo -e " ${YELLOW}No working-tree snapshot anchored for ${target_id}; code not restored.${NC}"
16601
+ fi
16602
+ fi
16603
+ }
16604
+
16605
+ case "$subcommand" in
16606
+ list|ls)
16607
+ cmd_checkpoint list
16608
+ ;;
16609
+ show)
16610
+ cmd_checkpoint show "$@"
16611
+ ;;
16612
+ to)
16613
+ local target="${1:-}"
16614
+ local want_code=0
16615
+ shift 2>/dev/null || true
16616
+ for a in "$@"; do [ "$a" = "--code" ] && want_code=1; done
16617
+ if [ -z "$target" ]; then
16618
+ echo -e "${RED}Error: Specify a checkpoint ID${NC}"
16619
+ echo "Usage: loki rollback to <id> [--code]"
16620
+ echo "Run 'loki rollback list' to see available checkpoints."
16621
+ return 1
16622
+ fi
16623
+ echo -e "${BOLD}Rolling back to checkpoint: $target${NC}"
16624
+ echo ""
16625
+ _rollback_restore "$target" "$want_code"
16626
+ ;;
16627
+ latest)
16628
+ local want_code=0
16629
+ for a in "$@"; do [ "$a" = "--code" ] && want_code=1; done
16630
+ local latest_id
16631
+ latest_id=$(_rollback_latest_id)
16632
+ if [ -z "$latest_id" ]; then
16633
+ echo -e "${RED}No checkpoints found to roll back to.${NC}"
16634
+ return 1
16635
+ fi
16636
+ echo -e "${BOLD}Rolling back to latest checkpoint: $latest_id${NC}"
16637
+ echo ""
16638
+ _rollback_restore "$latest_id" "$want_code"
16639
+ ;;
16640
+ help|--help|-h)
16641
+ echo -e "${BOLD}loki rollback${NC} - One-command rollback to a checkpoint"
16642
+ echo ""
16643
+ echo "Restore .loki/ state and iteration/conversation context to a"
16644
+ echo "previous checkpoint. Every rollback first saves your current state"
16645
+ echo "as a pre-rollback snapshot, so you can always undo the undo."
16646
+ echo ""
16647
+ echo "Usage: loki rollback <command> [args]"
16648
+ echo ""
16649
+ echo "Commands:"
16650
+ echo " list List recent checkpoints"
16651
+ echo " show <id> Show checkpoint details"
16652
+ echo " to <id> [--code] Restore to checkpoint <id>"
16653
+ echo " latest [--code] Restore to the most recent checkpoint"
16654
+ echo ""
16655
+ echo " --code Also restore the working tree from the anchored"
16656
+ echo " git snapshot (tracked files only; overwrites them)."
16657
+ echo ""
16658
+ echo "Examples:"
16659
+ echo " loki rollback list"
16660
+ echo " loki rollback latest"
16661
+ echo " loki rollback to cp-3-1717000000"
16662
+ echo " loki rollback to cp-3-1717000000 --code"
16663
+ ;;
16664
+ *)
16665
+ echo -e "${RED}Unknown rollback command: $subcommand${NC}"
16666
+ echo "Run 'loki rollback help' for usage."
16667
+ return 1
16668
+ ;;
16669
+ esac
16670
+ }
16671
+
16500
16672
  # Completion Council management
16501
16673
  cmd_council() {
16502
16674
  local subcommand="${1:-status}"
@@ -25103,6 +25275,173 @@ _loki_gist_upload() {
25103
25275
  echo -e "${GREEN}Shared: ${gist_url}${NC}"
25104
25276
  }
25105
25277
 
25278
+ # loki_tier_gate - R9 open-core tier/license seam.
25279
+ #
25280
+ # OSS-FIRST CONTRACT: this is a no-op ALLOW for OSS users. LOKI_TIER defaults
25281
+ # to "oss" and every existing free feature stays fully free. This function is
25282
+ # the single place where a future hosted/enterprise build would gate a
25283
+ # hosted-only capability. It is NEVER called from any existing free command
25284
+ # path; its only caller is the opt-in --hosted publish seam below. For OSS
25285
+ # (the default), it always returns 0 (allow).
25286
+ #
25287
+ # Args: $1 = capability name (informational; e.g. "hosted_publish").
25288
+ # Returns: 0 = allowed, 1 = gated (non-OSS tier without entitlement).
25289
+ # Env: LOKI_TIER (default "oss"), LOKI_LICENSE_KEY (optional, non-OSS only).
25290
+ loki_tier_gate() {
25291
+ local capability="${1:-}"
25292
+ local tier="${LOKI_TIER:-oss}"
25293
+
25294
+ # OSS tier: everything is allowed, always. No license, no network, no gate.
25295
+ if [ "$tier" = "oss" ]; then
25296
+ return 0
25297
+ fi
25298
+
25299
+ # Non-OSS tiers (hosted/enterprise) are a SEAM only. The hosted backend
25300
+ # and license-verification service do not exist yet, so we cannot validate
25301
+ # an entitlement. Be honest: do not pretend to grant a paid capability.
25302
+ # A real hosted build replaces this branch with a verified license check.
25303
+ if [ -z "${LOKI_LICENSE_KEY:-}" ]; then
25304
+ echo -e "${YELLOW}LOKI_TIER='${tier}' requested but no LOKI_LICENSE_KEY set.${NC}" >&2
25305
+ echo "Hosted/enterprise license verification is not available yet." >&2
25306
+ echo "OSS users: leave LOKI_TIER unset (or 'oss') -- everything stays free." >&2
25307
+ return 1
25308
+ fi
25309
+
25310
+ # A license key is present but there is no verification backend yet. We do
25311
+ # NOT fabricate a successful verification. The capability stays ungated for
25312
+ # OSS-equivalent use; the seam is documented in docs/OPEN-CORE-BOUNDARY.md.
25313
+ echo -e "${YELLOW}LOKI_LICENSE_KEY set but the verification backend is not available yet (R9 seam).${NC}" >&2
25314
+ return 0
25315
+ }
25316
+
25317
+ # _loki_hosted_publish_proof - R9 hosted proof-publish client stub.
25318
+ #
25319
+ # Posts an ALREADY-REDACTED proof page to a self-hosted/SaaS endpoint given by
25320
+ # LOKI_HOSTED_ENDPOINT. There is NO official Loki hosted backend yet; this is a
25321
+ # clean client seam an operator can point at their own endpoint. We never
25322
+ # fabricate a hosted URL: on success we print the URL the endpoint returned (or
25323
+ # the endpoint itself); on any failure we print an honest error and exit non-0.
25324
+ #
25325
+ # Args: $1 = proof id, $2 = redacted index.html path, $3 = proof.json path.
25326
+ # Returns: 0 on success, non-zero on missing endpoint / transport / non-2xx.
25327
+ _loki_hosted_publish_proof() {
25328
+ local id="$1"
25329
+ local html="$2"
25330
+ local pj="$3"
25331
+
25332
+ # Tier seam (no-op allow for OSS). Hosted publish is opt-in regardless.
25333
+ loki_tier_gate "hosted_publish" || true
25334
+
25335
+ local endpoint="${LOKI_HOSTED_ENDPOINT:-}"
25336
+ if [ -z "$endpoint" ]; then
25337
+ echo -e "${YELLOW}Hosted publishing backend not available.${NC}" >&2
25338
+ echo "There is no official Loki hosted service yet (R9 ships the seam, not a live backend)." >&2
25339
+ echo "To publish to your own hosted endpoint, set LOKI_HOSTED_ENDPOINT to its URL." >&2
25340
+ echo "Or publish to a GitHub Gist instead: loki proof share ${id}" >&2
25341
+ return 1
25342
+ fi
25343
+
25344
+ if ! command -v curl &>/dev/null; then
25345
+ echo -e "${RED}curl not found${NC}" >&2
25346
+ echo "Hosted publishing requires curl. Install curl or use: loki proof share ${id}" >&2
25347
+ return 1
25348
+ fi
25349
+
25350
+ # CREDIBILITY: we upload the file the generator already redacted (the same
25351
+ # bytes 'loki proof share' would put on a gist). We do not build a fresh
25352
+ # body that could bypass redaction. If proof.json reports redaction was not
25353
+ # applied, refuse -- never publish an unredacted artifact.
25354
+ if [ -f "$pj" ]; then
25355
+ local redaction_ok
25356
+ redaction_ok=$(LOKI_PROOF_JSON="$pj" python3 - <<'PYEOF' 2>/dev/null || echo "unknown"
25357
+ import json, os
25358
+ try:
25359
+ d = json.load(open(os.environ["LOKI_PROOF_JSON"]))
25360
+ except Exception:
25361
+ print("unknown")
25362
+ else:
25363
+ print("yes" if (d.get("redaction") or {}).get("applied") else "no")
25364
+ PYEOF
25365
+ )
25366
+ if [ "$redaction_ok" = "no" ]; then
25367
+ echo -e "${RED}Refusing to publish: proof redaction was not applied.${NC}" >&2
25368
+ echo "Regenerate the proof (LOKI_PROOF=1) so the redactor runs, then retry." >&2
25369
+ return 1
25370
+ fi
25371
+ fi
25372
+
25373
+ echo -e "${BOLD}Publishing proof '${id}' to hosted endpoint${NC}"
25374
+ echo " endpoint: ${endpoint}"
25375
+ echo " payload: ${html} (already redacted by the generator)"
25376
+ echo ""
25377
+
25378
+ # POST the redacted HTML. Auth header is sent only if a license key exists;
25379
+ # OSS users with their own endpoint need no key.
25380
+ local tmp_body tmp_code
25381
+ tmp_body=$(mktemp "/tmp/loki-hosted-XXXXXX.out")
25382
+ local -a curl_args=(-sS -o "$tmp_body" -w '%{http_code}' -X POST
25383
+ -H "Content-Type: text/html"
25384
+ -H "X-Loki-Proof-Id: ${id}"
25385
+ --data-binary "@${html}")
25386
+ if [ -n "${LOKI_LICENSE_KEY:-}" ]; then
25387
+ curl_args+=(-H "Authorization: Bearer ${LOKI_LICENSE_KEY}")
25388
+ fi
25389
+ tmp_code=$(curl "${curl_args[@]}" "$endpoint" 2>/dev/null)
25390
+ local curl_exit=$?
25391
+
25392
+ if [ "$curl_exit" -ne 0 ]; then
25393
+ echo -e "${RED}Failed to reach hosted endpoint (curl exit ${curl_exit}).${NC}" >&2
25394
+ echo "Check LOKI_HOSTED_ENDPOINT or publish to a gist: loki proof share ${id}" >&2
25395
+ rm -f "$tmp_body"
25396
+ return 1
25397
+ fi
25398
+
25399
+ # Accept any 2xx. The published URL comes from the endpoint response if it
25400
+ # returns one (we look for a "url" field), else we report the endpoint. We
25401
+ # NEVER print a fabricated URL.
25402
+ case "$tmp_code" in
25403
+ 2*)
25404
+ local published_url
25405
+ published_url=$(LOKI_HOSTED_BODY="$tmp_body" LOKI_HOSTED_EP="$endpoint" python3 - <<'PYEOF' 2>/dev/null || true
25406
+ import json, os
25407
+ body_path = os.environ["LOKI_HOSTED_BODY"]
25408
+ try:
25409
+ txt = open(body_path).read().strip()
25410
+ except Exception:
25411
+ txt = ""
25412
+ url = ""
25413
+ try:
25414
+ d = json.loads(txt)
25415
+ if isinstance(d, dict):
25416
+ url = d.get("url") or d.get("public_url") or ""
25417
+ except Exception:
25418
+ url = ""
25419
+ print(url)
25420
+ PYEOF
25421
+ )
25422
+ rm -f "$tmp_body"
25423
+ if [ -n "$published_url" ]; then
25424
+ echo -e "${GREEN}Published: ${published_url}${NC}"
25425
+ else
25426
+ echo -e "${GREEN}Published to ${endpoint} (HTTP ${tmp_code}).${NC}"
25427
+ echo "The endpoint did not return a 'url' field; check your endpoint's response."
25428
+ fi
25429
+ return 0
25430
+ ;;
25431
+ *)
25432
+ echo -e "${RED}Hosted endpoint returned HTTP ${tmp_code}.${NC}" >&2
25433
+ if [ -s "$tmp_body" ]; then
25434
+ echo "Response:" >&2
25435
+ head -c 500 "$tmp_body" >&2
25436
+ echo "" >&2
25437
+ fi
25438
+ echo "Nothing was published. Or publish to a gist: loki proof share ${id}" >&2
25439
+ rm -f "$tmp_body"
25440
+ return 1
25441
+ ;;
25442
+ esac
25443
+ }
25444
+
25106
25445
  # loki bench - head-to-head benchmark harness (R2).
25107
25446
  # Subcommands: run <task> | vs <task> | list | verify <result.json>.
25108
25447
  # Thin pass-through to benchmarks/bench/run.sh (shared python core runner.py).
@@ -25170,7 +25509,7 @@ cmd_proof() {
25170
25509
  echo "Options for 'share':"
25171
25510
  echo " --yes Skip the redaction-preview confirmation prompt"
25172
25511
  echo " --private Create a secret gist (default: public)"
25173
- echo " --hosted Reserved for hosted publishing (coming in R9)"
25512
+ echo " --hosted Publish to LOKI_HOSTED_ENDPOINT (open-core seam; no official backend yet)"
25174
25513
  echo ""
25175
25514
  echo "Proofs are generated automatically at run completion (LOKI_PROOF=0 to opt out)."
25176
25515
  [ "$sub" = "" ] && exit 1
@@ -25269,15 +25608,13 @@ PYEOF
25269
25608
  local id=""
25270
25609
  local skip_confirm=0
25271
25610
  local visibility="--public"
25611
+ local hosted=0
25272
25612
  while [[ $# -gt 0 ]]; do
25273
25613
  case "$1" in
25274
25614
  --yes|-y) skip_confirm=1; shift ;;
25275
25615
  --private) visibility=""; shift ;;
25276
25616
  --public) visibility="--public"; shift ;;
25277
- --hosted)
25278
- echo -e "${RED}Hosted publishing is not available yet (coming in R9).${NC}"
25279
- exit 1
25280
- ;;
25617
+ --hosted) hosted=1; shift ;;
25281
25618
  -*) echo -e "${RED}Unknown option: $1${NC}"; exit 1 ;;
25282
25619
  *) id="$1"; shift ;;
25283
25620
  esac
@@ -25292,6 +25629,17 @@ PYEOF
25292
25629
  echo "Use 'loki proof list' to see available proofs."
25293
25630
  exit 1
25294
25631
  fi
25632
+ # R9 open-core hosted-publish seam. Only taken when the user
25633
+ # explicitly passes --hosted. The default gist path below stays
25634
+ # byte-for-byte unchanged for OSS users (zero hosted backend
25635
+ # required). We never silent-fall-back to gist here: the user asked
25636
+ # for hosted, so we POST to a configured LOKI_HOSTED_ENDPOINT or
25637
+ # print an honest "no endpoint configured" message and exit non-zero.
25638
+ # We never fabricate a hosted URL.
25639
+ if [ "$hosted" -eq 1 ]; then
25640
+ _loki_hosted_publish_proof "$id" "$html" "${proofs_dir}/${id}/proof.json"
25641
+ exit $?
25642
+ fi
25295
25643
  if ! command -v gh &>/dev/null; then
25296
25644
  echo -e "${RED}gh CLI not found${NC}"
25297
25645
  echo "Install the GitHub CLI to publish a proof:"
package/autonomy/run.sh CHANGED
@@ -7443,10 +7443,18 @@ create_checkpoint() {
7443
7443
 
7444
7444
  mkdir -p "$checkpoint_dir"
7445
7445
 
7446
- # Only checkpoint if there are uncommitted changes
7447
- if git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null; then
7448
- log_info "No uncommitted changes to checkpoint"
7449
- return 0
7446
+ # Only checkpoint if there are uncommitted changes.
7447
+ # R6: _LOKI_CP_FORCE=1 bypasses this guard. Used by rollback to guarantee a
7448
+ # pre-rollback snapshot of .loki/ state even when the git tree is clean (the
7449
+ # .loki/ state files about to be overwritten are not git-tracked, so the
7450
+ # clean-tree guard would otherwise skip the safety snapshot). Mirrors the
7451
+ # Bun `forceCreate` seam in checkpoint.ts.
7452
+ if [ "${_LOKI_CP_FORCE:-0}" != "1" ]; then
7453
+ if git diff --quiet 2>/dev/null && git diff --cached --quiet 2>/dev/null; then
7454
+ log_info "No uncommitted changes to checkpoint"
7455
+ _LAST_CHECKPOINT_ID=""
7456
+ return 0
7457
+ fi
7450
7458
  fi
7451
7459
 
7452
7460
  # Capture git state
@@ -7465,7 +7473,9 @@ create_checkpoint() {
7465
7473
 
7466
7474
  # Copy critical state files (lightweight -- not full .loki/)
7467
7475
  # BUG-ST-009: Include autonomy-state.json in checkpoint backup
7468
- for f in state/orchestrator.json autonomy-state.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json; do
7476
+ # R6: Include CONTINUITY.md so a rollback also restores iteration/conversation
7477
+ # handoff context, not just machine state. Mirrors Bun COPIED_FILES.
7478
+ for f in state/orchestrator.json autonomy-state.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json CONTINUITY.md; do
7469
7479
  if [ -f ".loki/$f" ]; then
7470
7480
  local target_dir="$cp_dir/$(dirname "$f")"
7471
7481
  mkdir -p "$target_dir"
@@ -7473,6 +7483,23 @@ create_checkpoint() {
7473
7483
  fi
7474
7484
  done
7475
7485
 
7486
+ # R6: capture a real working-tree snapshot so code can be truly undone later.
7487
+ # Loki does not commit per iteration, so git_sha (HEAD) cannot reconstruct
7488
+ # this iteration's working tree. `git stash create` builds a commit object
7489
+ # capturing tracked changes WITHOUT disturbing the tree; we then anchor it
7490
+ # under refs/loki/cp/<id> so `git gc` cannot prune the dangling commit. The
7491
+ # snapshot sha goes in a sidecar (worktree-snapshot.txt), NOT metadata.json,
7492
+ # to preserve byte-for-byte parity with the Bun port.
7493
+ # Honest limit: captures tracked changes only (not untracked/ignored files).
7494
+ if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
7495
+ local snap_sha
7496
+ snap_sha=$(git stash create "loki checkpoint ${checkpoint_id}" 2>/dev/null || echo "")
7497
+ if [ -n "$snap_sha" ]; then
7498
+ git update-ref "refs/loki/cp/${checkpoint_id}" "$snap_sha" 2>/dev/null \
7499
+ && printf '%s\n' "$snap_sha" > "$cp_dir/worktree-snapshot.txt" 2>/dev/null || true
7500
+ fi
7501
+ fi
7502
+
7476
7503
  # Write checkpoint metadata (use python3 json.dumps for safe serialization)
7477
7504
  local phase_val
7478
7505
  phase_val=$(cat .loki/state/orchestrator.json 2>/dev/null | python3 -c 'import sys,json; print(json.load(sys.stdin).get("currentPhase","unknown"))' 2>/dev/null || echo 'unknown')
@@ -7531,6 +7558,10 @@ print(json.dumps({'id':m['id'],'ts':m['timestamp'],'iter':m['iteration'],'task':
7531
7558
  fi
7532
7559
 
7533
7560
  log_info "Checkpoint created: ${checkpoint_id} (git: ${git_sha:0:8})"
7561
+ # R6: expose the id via a global so callers (rollback, run loop) can reference
7562
+ # it without parsing stdout (log_info writes to stdout, so command-substitution
7563
+ # capture would include log lines).
7564
+ _LAST_CHECKPOINT_ID="$checkpoint_id"
7534
7565
  }
7535
7566
 
7536
7567
  rollback_to_checkpoint() {
@@ -7558,11 +7589,18 @@ rollback_to_checkpoint() {
7558
7589
 
7559
7590
  log_warn "Rolling back to checkpoint: ${checkpoint_id}"
7560
7591
 
7561
- # Create a pre-rollback checkpoint first
7562
- create_checkpoint "pre-rollback snapshot" "rollback"
7592
+ # R6 re-undoability invariant: force a pre-rollback snapshot of CURRENT state
7593
+ # before overwriting, even if the git tree is clean (the .loki/ state we are
7594
+ # about to clobber is not git-tracked). _LOKI_CP_FORCE bypasses the clean-tree
7595
+ # guard. Capture the snapshot id so we can tell the user how to undo the undo.
7596
+ _LOKI_CP_FORCE=1 create_checkpoint "pre-rollback snapshot (before restoring ${checkpoint_id})" "rollback"
7597
+ local pre_rollback_id="${_LAST_CHECKPOINT_ID:-}"
7598
+ if [ -n "$pre_rollback_id" ]; then
7599
+ log_info "Saved prior state as ${pre_rollback_id} (undo this rollback with: loki rollback to ${pre_rollback_id})"
7600
+ fi
7563
7601
 
7564
- # Restore state files
7565
- for f in state/orchestrator.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json; do
7602
+ # Restore state files (R6: CONTINUITY.md restores iteration/conversation context)
7603
+ for f in state/orchestrator.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json CONTINUITY.md; do
7566
7604
  if [ -f "${cp_dir}/${f}" ]; then
7567
7605
  local target_dir=".loki/$(dirname "$f")"
7568
7606
  mkdir -p "$target_dir"
@@ -7599,9 +7637,17 @@ print(json.dumps({'event':'rollback','checkpoint':os.environ['_RB_CPID'],'git_sh
7599
7637
 
7600
7638
  log_info "State files restored from checkpoint: ${checkpoint_id}"
7601
7639
 
7602
- if [ -n "$git_sha" ] && [ "$git_sha" != "no-git" ]; then
7603
- log_info "Git SHA at checkpoint: ${git_sha}"
7604
- log_info "To rollback code: git reset --hard ${git_sha}"
7640
+ # R6: the prior hint `git reset --hard ${git_sha}` was MISLEADING. git_sha is
7641
+ # HEAD (the last commit), and Loki does not commit per iteration, so a hard
7642
+ # reset would discard the iteration's work rather than reconstruct it. The
7643
+ # correct, durable recovery is the anchored working-tree snapshot, if present.
7644
+ if [ -f "${cp_dir}/worktree-snapshot.txt" ]; then
7645
+ log_info "To also restore the working tree to this checkpoint:"
7646
+ log_info " git stash apply refs/loki/cp/${checkpoint_id}"
7647
+ elif [ -n "$git_sha" ] && [ "$git_sha" != "no-git" ]; then
7648
+ log_info "Git SHA at checkpoint (last commit): ${git_sha}"
7649
+ log_info "Note: no working-tree snapshot was captured for this checkpoint;"
7650
+ log_info "code changes since the last commit are not restorable from here."
7605
7651
  fi
7606
7652
  }
7607
7653
 
@@ -12001,6 +12047,10 @@ if __name__ == "__main__":
12001
12047
 
12002
12048
  # Checkpoint after each iteration (v5.57.0)
12003
12049
  create_checkpoint "iteration-${ITERATION_COUNT} complete" "iteration-${ITERATION_COUNT}"
12050
+ # R6: prominent "you can safely undo this" signal so users run boldly.
12051
+ if [ -n "${_LAST_CHECKPOINT_ID:-}" ]; then
12052
+ log_info "Safety net: checkpoint ${_LAST_CHECKPOINT_ID} saved. Undo this iteration with: loki rollback to ${_LAST_CHECKPOINT_ID}"
12053
+ fi
12004
12054
 
12005
12055
  # Quality gates (v6.10.0 - escalation ladder)
12006
12056
  log_step "Post-iteration: running quality gates..."
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.13.0"
10
+ __version__ = "7.15.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: