loki-mode 9.22.4 → 9.22.6

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.
Files changed (85) hide show
  1. package/README.md +18 -4
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/issue-parser.sh +112 -0
  5. package/autonomy/lib/proof-generator.py +88 -0
  6. package/autonomy/lib/proof-pr.sh +52 -0
  7. package/autonomy/loki +147 -3
  8. package/autonomy/quickstart.sh +342 -5
  9. package/autonomy/run.sh +26 -0
  10. package/dashboard/__init__.py +1 -1
  11. package/docs/EXECUTION-COCKPIT-PLAN.md +289 -0
  12. package/docs/HARNESS-INTELLIGENCE-PLAN.md +194 -0
  13. package/docs/ISSUE-TO-PR-GOLDEN-PATH-PLAN.md +257 -0
  14. package/loki-ts/dist/loki.js +299 -297
  15. package/mcp/__init__.py +1 -1
  16. package/package.json +1 -1
  17. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  18. package/web-app/dist/assets/{AdminPage-DNGNuAge.js → AdminPage-DLhWX6si.js} +1 -1
  19. package/web-app/dist/assets/{Avatar-CqAveJaT.js → Avatar-CqyDcUxl.js} +1 -1
  20. package/web-app/dist/assets/{Badge-BtMejA0Z.js → Badge-BKGSt1n2.js} +1 -1
  21. package/web-app/dist/assets/{Button-CikxmqSs.js → Button-DuuKHXre.js} +1 -1
  22. package/web-app/dist/assets/CockpitPage-DdClYERC.js +47 -0
  23. package/web-app/dist/assets/{ComparePage-BB8DJBWR.js → ComparePage-CUWlqBxv.js} +1 -1
  24. package/web-app/dist/assets/ErrorBoundary-RwfGKyRJ.js +16 -0
  25. package/web-app/dist/assets/{EvidenceReceiptPanel-6oquKEjd.js → EvidenceReceiptPanel-CzIgk9sg.js} +1 -1
  26. package/web-app/dist/assets/{GitHubIssuesPanel-5XLYWzWH.js → GitHubIssuesPanel-CsXVKuyJ.js} +3 -3
  27. package/web-app/dist/assets/{GitHubPRsPanel-DUfmFYeu.js → GitHubPRsPanel-CYRp1eKS.js} +3 -3
  28. package/web-app/dist/assets/{HomePage-DZ4MmOdH.js → HomePage-CVJcI5pQ.js} +3 -3
  29. package/web-app/dist/assets/LoginPage-BBJNV9Qb.js +1 -0
  30. package/web-app/dist/assets/{MagicPage-CFbX5gWg.js → MagicPage-CIi9P8jK.js} +1 -1
  31. package/web-app/dist/assets/{MetricsPage-DbL6xmRQ.js → MetricsPage-Bm34ipiy.js} +2 -2
  32. package/web-app/dist/assets/{NotFoundPage-BUrjXPAR.js → NotFoundPage-DpTbcC2P.js} +1 -1
  33. package/web-app/dist/assets/{ProjectPage-iP6dJihg.js → ProjectPage-CY2SMGx9.js} +65 -90
  34. package/web-app/dist/assets/{ProjectsPage-BibN9bHl.js → ProjectsPage-DSrXUUKJ.js} +2 -2
  35. package/web-app/dist/assets/{SettingsPage-C0WJp1hW.js → SettingsPage-jvDyOqYi.js} +3 -3
  36. package/web-app/dist/assets/{ShowcasePage-DUv0Njp8.js → ShowcasePage-C8tFWKau.js} +1 -1
  37. package/web-app/dist/assets/{SystemSettingsPage-BIaCB0Vn.js → SystemSettingsPage-DH4FTH3K.js} +2 -2
  38. package/web-app/dist/assets/{TeamsPage-BE0szv9r.js → TeamsPage-C1fQYauF.js} +2 -2
  39. package/web-app/dist/assets/{TemplatesPage-BzkA9kat.js → TemplatesPage-CSTFt-4P.js} +1 -1
  40. package/web-app/dist/assets/TerminalOutput-Cu4NsyN6.js +16 -0
  41. package/web-app/dist/assets/{activity-AbfFz38r.js → activity-CtjsDzmh.js} +1 -1
  42. package/web-app/dist/assets/{bell-DF6Z6y5U.js → bell-C5es1xAs.js} +1 -1
  43. package/web-app/dist/assets/{bot-DQ4np_3n.js → bot-DE04U5Lc.js} +1 -1
  44. package/web-app/dist/assets/{check-RWuguCwE.js → check-DSKKA9EU.js} +1 -1
  45. package/web-app/dist/assets/{chevron-left-VpbQPOkC.js → chevron-left-DJ13W5AR.js} +1 -1
  46. package/web-app/dist/assets/{circle-alert-DwT82lJx.js → circle-alert-CS0BfqAd.js} +1 -1
  47. package/web-app/dist/assets/{clock-BBlUa93G.js → clock-D1Y5nruk.js} +1 -1
  48. package/web-app/dist/assets/{cloud-Dgbz3VCA.js → cloud-nU6LBoZL.js} +1 -1
  49. package/web-app/dist/assets/{code-xml-r6qqION5.js → code-xml-BZfvIdFo.js} +1 -1
  50. package/web-app/dist/assets/{copy-DMHj52B8.js → copy-CK_2O1Xv.js} +1 -1
  51. package/web-app/dist/assets/{database-uJqHSHR1.js → database-DJiwznAm.js} +1 -1
  52. package/web-app/dist/assets/{dollar-sign-BZagljek.js → dollar-sign-pO20A3QY.js} +1 -1
  53. package/web-app/dist/assets/{file-code-corner-DMopmhu0.js → file-code-corner-Besy6vek.js} +1 -1
  54. package/web-app/dist/assets/{file-plus-DU_hT_3w.js → file-plus-DeaswxYY.js} +1 -1
  55. package/web-app/dist/assets/{folder-open-BP5TtQAW.js → folder-open-cr1UvQLI.js} +1 -1
  56. package/web-app/dist/assets/{git-commit-horizontal-zFk_IobS.js → git-commit-horizontal-BLxnfbSn.js} +1 -1
  57. package/web-app/dist/assets/{globe-mthCRtfp.js → globe-DrhG1NfX.js} +1 -1
  58. package/web-app/dist/assets/{hammer-BWaLBvSA.js → hammer-CeZtPBfE.js} +1 -1
  59. package/web-app/dist/assets/index-84JUN_aA.css +1 -0
  60. package/web-app/dist/assets/{index-BXA3UT6P.js → index-CLvjO22Z.js} +24 -24
  61. package/web-app/dist/assets/{layers-Q1LXV5fy.js → layers-fldADzyE.js} +1 -1
  62. package/web-app/dist/assets/{lightbulb-vRRNJ3dB.js → lightbulb-CznOfsPb.js} +1 -1
  63. package/web-app/dist/assets/{loader-circle-KgjeHEcV.js → loader-circle---lPB1Fs.js} +1 -1
  64. package/web-app/dist/assets/{lock-BerKqno7.js → lock-D5jVF-tO.js} +1 -1
  65. package/web-app/dist/assets/{mail-XAxTbprX.js → mail-D7HkLnlW.js} +1 -1
  66. package/web-app/dist/assets/minus-C-dT19Ct.js +6 -0
  67. package/web-app/dist/assets/{package-DLPKWAvW.js → package-Cm9B-cKE.js} +1 -1
  68. package/web-app/dist/assets/{plus-NMxxwHMn.js → plus-CQwOffLD.js} +1 -1
  69. package/web-app/dist/assets/{refresh-cw-Dt42kYtM.js → refresh-cw-CvWJZLbN.js} +1 -1
  70. package/web-app/dist/assets/{rotate-ccw-BCR4Tp9u.js → rotate-ccw-C2Q1LYgM.js} +1 -1
  71. package/web-app/dist/assets/{save-sR4MJd42.js → save-BP0MvpoQ.js} +1 -1
  72. package/web-app/dist/assets/{server-SXkchokx.js → server-4i4VZXix.js} +1 -1
  73. package/web-app/dist/assets/{shield-alert-Bvgp6_Dz.js → shield-alert-D2_bgb7j.js} +1 -1
  74. package/web-app/dist/assets/thumbs-up-vACbzeXj.js +26 -0
  75. package/web-app/dist/assets/{trash-2-D0IGudH7.js → trash-2-EYjdESLs.js} +1 -1
  76. package/web-app/dist/assets/trending-down-BWBdcvE3.js +6 -0
  77. package/web-app/dist/assets/{trending-up-DDrJKL5i.js → trending-up-Bix6V_-Y.js} +1 -1
  78. package/web-app/dist/assets/{upload-C54rjN8K.js → upload-B7hh6ERF.js} +1 -1
  79. package/web-app/dist/assets/{usePolling-D7fw4YSM.js → usePolling-b_e-k-IN.js} +1 -1
  80. package/web-app/dist/assets/{user-Cg7fbzdh.js → user-pmjZKWEg.js} +1 -1
  81. package/web-app/dist/index.html +2 -2
  82. package/web-app/dist/assets/LoginPage-VMzI6ROD.js +0 -1
  83. package/web-app/dist/assets/TerminalOutput-DBrdT_Fp.js +0 -31
  84. package/web-app/dist/assets/index-DgpUuqwR.css +0 -1
  85. package/web-app/dist/assets/trending-down-BbFFBq-v.js +0 -11
@@ -283,6 +283,72 @@ _qs_template_summary() {
283
283
  esac
284
284
  }
285
285
 
286
+ # _qs_shipped_template_names: print every shipped template basename in stable
287
+ # catalog order. The filesystem is the source of truth: adding or removing a
288
+ # templates/*.md payload changes discovery automatically, while README.md is
289
+ # deliberately excluded because it is gallery documentation, not a PRD.
290
+ _qs_shipped_template_names() {
291
+ local tdir; tdir="$(_qs_templates_dir)"
292
+ local f name
293
+ for f in "$tdir"/*.md; do
294
+ [ -f "$f" ] || continue
295
+ name=$(basename "$f" .md)
296
+ [ "$name" = "README" ] && continue
297
+ printf '%s\n' "$name"
298
+ done | LC_ALL=C sort
299
+ }
300
+
301
+ # _qs_list_templates [json]: provider-free discovery for terminals and local
302
+ # automation. Human and machine output are derived from the same shipped-name
303
+ # stream and the same stable purpose table used by the interactive picker.
304
+ _qs_list_templates() {
305
+ local json_output="${1:-false}"
306
+ local catalog="" count=0 name purpose
307
+ while IFS= read -r name; do
308
+ [ -n "$name" ] || continue
309
+ purpose="$(_qs_template_summary "$name")"
310
+ catalog="${catalog}${name}\t${purpose}\n"
311
+ count=$((count + 1))
312
+ done < <(_qs_shipped_template_names)
313
+
314
+ if [ "$count" -eq 0 ]; then
315
+ printf 'No shipped quickstart templates were found.\n' >&2
316
+ return 2
317
+ fi
318
+
319
+ if [ "$json_output" = true ]; then
320
+ printf '%b' "$catalog" | python3 -c '
321
+ import json
322
+ import sys
323
+
324
+ templates = []
325
+ for raw in sys.stdin:
326
+ name, purpose = raw.rstrip("\n").split("\t", 1)
327
+ templates.append({"name": name, "purpose": purpose})
328
+ json.dump(
329
+ {
330
+ "schema_version": 1,
331
+ "command": "loki quickstart",
332
+ "mode": "list-templates",
333
+ "templates": templates,
334
+ },
335
+ sys.stdout,
336
+ separators=(",", ":"),
337
+ sort_keys=True,
338
+ )
339
+ sys.stdout.write("\n")
340
+ ' || return 2
341
+ return 0
342
+ fi
343
+
344
+ printf '%sShipped quickstart templates (%d)%s\n' "$_QS_BOLD" "$count" "$_QS_NC"
345
+ printf '%b' "$catalog" | while IFS=$'\t' read -r name purpose; do
346
+ [ -n "$name" ] || continue
347
+ printf ' %-20s %s\n' "$name" "$purpose"
348
+ done
349
+ return 0
350
+ }
351
+
286
352
  # _qs_template_exists <name>: accept only an exact shipped template basename.
287
353
  # Keeping validation here (rather than accepting an arbitrary path) prevents
288
354
  # --template from becoming a second PRD/file-read surface. The intentionally
@@ -340,12 +406,21 @@ _qs_selected_provider() {
340
406
  # non-zero if the estimator gave no result (caller falls back to a no-number
341
407
  # confirm, never fabricating a figure).
342
408
  _qs_emit_plan() {
343
- local prd_path="$1" template_name="$2" json_output="${3:-false}" input_kind="${4:-idea}"
409
+ local prd_path="$1" template_name="$2" json_output="${3:-false}" input_kind="${4:-idea}" input_value="${5:-}"
410
+ local source_digest_before="" source_digest_after=""
411
+ if [ "$json_output" = true ] && [ "$input_kind" = "prd" ]; then
412
+ source_digest_before=$(shasum -a 256 "$prd_path" 2>/dev/null | awk '{print $1}') || return 1
413
+ [ -n "$source_digest_before" ] || return 1
414
+ fi
344
415
  local plan_json=""
345
416
  plan_json=$(show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""
346
417
  if [ -z "$plan_json" ]; then
347
418
  return 1
348
419
  fi
420
+ if [ "$json_output" = true ] && [ "$input_kind" = "prd" ]; then
421
+ source_digest_after=$(shasum -a 256 "$prd_path" 2>/dev/null | awk '{print $1}') || return 1
422
+ [ "$source_digest_before" = "$source_digest_after" ] || return 1
423
+ fi
349
424
  local parsed
350
425
  parsed=$(printf '%s' "$plan_json" | python3 -c "
351
426
  import json, sys
@@ -381,7 +456,19 @@ import sys
381
456
  plan = json.load(sys.stdin)
382
457
  if not isinstance(plan, dict):
383
458
  sys.exit(1)
384
- template_name, input_kind = sys.argv[1:3]
459
+ template_name, input_kind, input_value, source_digest = sys.argv[1:5]
460
+ if input_kind == "idea":
461
+ continuation = {
462
+ "kind": "idea",
463
+ "template": template_name,
464
+ "value": input_value,
465
+ }
466
+ else:
467
+ continuation = {
468
+ "kind": "prd",
469
+ "path": input_value,
470
+ "sha256": source_digest,
471
+ }
385
472
  payload = {
386
473
  "schema_version": 1,
387
474
  "command": "loki quickstart",
@@ -390,9 +477,10 @@ payload = {
390
477
  "selected_template": template_name if input_kind == "idea" else None,
391
478
  "source_name": template_name if input_kind == "prd" else None,
392
479
  "plan": plan,
480
+ "continuation": continuation,
393
481
  }
394
482
  json.dump(payload, sys.stdout, separators=(",", ":"), sort_keys=True)
395
- ' "$template_name" "$input_kind" 2>/dev/null) || json_payload=""
483
+ ' "$template_name" "$input_kind" "$input_value" "$source_digest_after" 2>/dev/null) || json_payload=""
396
484
  [ -n "$json_payload" ] || return 1
397
485
  printf '%s\n' "$json_payload" >&3
398
486
  return 0
@@ -415,6 +503,111 @@ json.dump(payload, sys.stdout, separators=(",", ":"), sort_keys=True)
415
503
  return 0
416
504
  }
417
505
 
506
+ # _qs_load_preview <path>: validate one bounded schema-v1 preview before any
507
+ # provider, estimator, PRD, or build boundary. Only the continuation fields are
508
+ # handed to the shell. The saved plan is evidence, not execution authority;
509
+ # cmd_quickstart always recomputes and displays the current estimator result.
510
+ _qs_load_preview() {
511
+ local preview_path="$1"
512
+ if [ ! -f "$preview_path" ] || [ ! -r "$preview_path" ] || [ -L "$preview_path" ]; then
513
+ printf 'Preview path is not a readable regular non-symlink file: %s\n' "$preview_path" >&2
514
+ return 2
515
+ fi
516
+ local preview_size
517
+ preview_size=$(wc -c < "$preview_path" 2>/dev/null | tr -d '[:space:]') || return 2
518
+ case "$preview_size" in
519
+ ""|*[!0-9]*) return 2;;
520
+ esac
521
+ if [ "$preview_size" -eq 0 ] || [ "$preview_size" -gt 1048576 ]; then
522
+ printf 'Preview JSON must be between 1 byte and 1 MiB.\n' >&2
523
+ return 2
524
+ fi
525
+
526
+ python3 - "$preview_path" <<'PY'
527
+ import base64
528
+ import json
529
+ import os
530
+ import re
531
+ import stat
532
+ import sys
533
+
534
+ path = sys.argv[1]
535
+ try:
536
+ flags = os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)
537
+ descriptor = os.open(path, flags)
538
+ try:
539
+ metadata = os.fstat(descriptor)
540
+ if not stat.S_ISREG(metadata.st_mode) or metadata.st_size < 1 or metadata.st_size > 1048576:
541
+ raise ValueError("unsafe preview")
542
+ raw = os.read(descriptor, 1048577)
543
+ finally:
544
+ os.close(descriptor)
545
+ if len(raw) > 1048576:
546
+ raise ValueError("oversized")
547
+ def reject_duplicate_keys(pairs):
548
+ result = {}
549
+ for key, value in pairs:
550
+ if key in result:
551
+ raise ValueError("duplicate key")
552
+ result[key] = value
553
+ return result
554
+
555
+ payload = json.loads(raw, object_pairs_hook=reject_duplicate_keys)
556
+ except Exception:
557
+ sys.exit(2)
558
+
559
+ if not isinstance(payload, dict):
560
+ sys.exit(2)
561
+ if payload.get("schema_version") != 1:
562
+ sys.exit(2)
563
+ if payload.get("command") != "loki quickstart" or payload.get("mode") != "dry-run":
564
+ sys.exit(2)
565
+ if not isinstance(payload.get("plan"), dict) or not payload["plan"]:
566
+ sys.exit(2)
567
+
568
+ continuation = payload.get("continuation")
569
+ if not isinstance(continuation, dict):
570
+ sys.exit(2)
571
+ kind = continuation.get("kind")
572
+
573
+ def valid_text(value):
574
+ return (
575
+ isinstance(value, str)
576
+ and 0 < len(value) <= 4096
577
+ and not any(ord(char) < 32 or ord(char) == 127 for char in value)
578
+ )
579
+
580
+ if kind == "idea":
581
+ value = continuation.get("value")
582
+ template = continuation.get("template")
583
+ if not valid_text(value):
584
+ sys.exit(2)
585
+ if not isinstance(template, str) or re.fullmatch(r"[a-z0-9-]+", template) is None:
586
+ sys.exit(2)
587
+ if payload.get("input_kind") != "idea":
588
+ sys.exit(2)
589
+ if payload.get("selected_template") != template or payload.get("source_name") is not None:
590
+ sys.exit(2)
591
+ encoded = base64.b64encode(value.encode("utf-8")).decode("ascii")
592
+ print(f"idea|{template}|{encoded}|")
593
+ elif kind == "prd":
594
+ value = continuation.get("path")
595
+ digest = continuation.get("sha256")
596
+ if not valid_text(value):
597
+ sys.exit(2)
598
+ if not isinstance(digest, str) or re.fullmatch(r"[0-9a-f]{64}", digest) is None:
599
+ sys.exit(2)
600
+ if payload.get("input_kind") != "prd":
601
+ sys.exit(2)
602
+ if payload.get("selected_template") is not None or payload.get("source_name") != os.path.basename(value):
603
+ sys.exit(2)
604
+ encoded = base64.b64encode(value.encode("utf-8")).decode("ascii")
605
+ print(f"prd||{encoded}|{digest}")
606
+ else:
607
+ sys.exit(2)
608
+ PY
609
+ }
610
+
418
611
  # _qs_help: concise usage for `loki quickstart --help`.
419
612
  _qs_help() {
420
613
  printf '%sloki quickstart%s - guided first build (setup, idea, template, plan, go)\n' "$_QS_BOLD" "$_QS_NC"
@@ -432,7 +625,9 @@ _qs_help() {
432
625
  printf ' --yes, -y Auto-confirm the final build prompt (still shows the plan)\n'
433
626
  printf ' --dry-run Preview the selected template and plan; write/start nothing\n'
434
627
  printf ' --json With --dry-run, emit one machine-readable JSON object\n'
628
+ printf ' --from-preview F Continue a saved JSON preview; requires explicit --yes\n'
435
629
  printf ' --template N Use the exact shipped template N for an IDEA\n'
630
+ printf ' --list-templates List every shipped template and its purpose\n'
436
631
  printf ' --help, -h Show this help and exit\n'
437
632
  printf '\n'
438
633
  printf 'Non-interactive use:\n'
@@ -441,12 +636,14 @@ _qs_help() {
441
636
  printf ' Missing either one exits 2 and writes nothing. The top-ranked\n'
442
637
  printf ' template is chosen automatically and the plan is still shown.\n'
443
638
  printf ' Add --template NAME to choose a shipped template instead.\n'
639
+ printf ' Run with --list-templates (and optional --json) to discover names.\n'
444
640
  printf '\n'
445
641
  printf 'Zero-spend preview:\n'
446
642
  printf ' loki quickstart "a todo app" --dry-run\n'
447
643
  printf ' An IDEA (or readable PRD path) is required. No provider is checked,\n'
448
644
  printf ' no file is written, and no build is started. Do not combine with --yes.\n'
449
645
  printf ' Add --json for versioned JSON only; --json requires --dry-run.\n'
646
+ printf ' Save that JSON, then continue it with --from-preview FILE --yes.\n'
450
647
  printf '\n'
451
648
  printf 'Steps:\n'
452
649
  printf ' 1. Setup Check for an AI provider for execution (skipped in preview)\n'
@@ -480,6 +677,11 @@ cmd_quickstart() {
480
677
  local json_output=false
481
678
  local template_override=""
482
679
  local template_flag_seen=false
680
+ local list_templates=false
681
+ local list_templates_flag_seen=false
682
+ local from_preview=""
683
+ local from_preview_flag_seen=false
684
+ local preview_prd_digest=""
483
685
  if _qs_assume_yes; then assume_yes=true; fi
484
686
 
485
687
  # yes_flag tracks EXPLICIT --yes/-y on THIS command's argv, and nothing else.
@@ -525,6 +727,28 @@ cmd_quickstart() {
525
727
  template_override="$2"
526
728
  shift 2
527
729
  ;;
730
+ --list-templates)
731
+ if [ "$list_templates_flag_seen" = true ]; then
732
+ printf '%s--list-templates may be specified only once.%s\n' "$_QS_RED" "$_QS_NC" >&2
733
+ exit 2
734
+ fi
735
+ list_templates=true
736
+ list_templates_flag_seen=true
737
+ shift
738
+ ;;
739
+ --from-preview)
740
+ if [ "$from_preview_flag_seen" = true ]; then
741
+ printf '%s--from-preview may be specified only once.%s\n' "$_QS_RED" "$_QS_NC" >&2
742
+ exit 2
743
+ fi
744
+ from_preview_flag_seen=true
745
+ if [ $# -lt 2 ] || [ -z "${2:-}" ] || [[ "${2:-}" == --* ]]; then
746
+ printf '%s--from-preview requires a preview JSON path.%s\n' "$_QS_RED" "$_QS_NC" >&2
747
+ exit 2
748
+ fi
749
+ from_preview="$2"
750
+ shift 2
751
+ ;;
528
752
  --*)
529
753
  printf '%sUnknown option: %s%s\n' "$_QS_RED" "$1" "$_QS_NC" >&2
530
754
  printf "Run 'loki quickstart --help' for usage.\n" >&2
@@ -543,6 +767,65 @@ cmd_quickstart() {
543
767
  esac
544
768
  done
545
769
 
770
+ # Continuation is a standalone execution shape. Explicit argv consent is
771
+ # mandatory, and no caller-supplied input or selector may compete with the
772
+ # reviewed preview. Validation happens before every provider, estimator,
773
+ # PRD, and build boundary. Accepted values then rejoin the existing path.
774
+ if [ "$from_preview_flag_seen" = true ]; then
775
+ if [ -n "$positional" ] || [ "$yes_flag" != true ] || [ "$dry_run" = true ] || [ "$json_output" = true ] || [ "$template_flag_seen" = true ] || [ "$list_templates" = true ]; then
776
+ printf '%s--from-preview accepts only a preview JSON path and explicit --yes.%s\n' "$_QS_RED" "$_QS_NC" >&2
777
+ exit 2
778
+ fi
779
+ local preview_fields="" preview_kind="" preview_template="" preview_encoded="" preview_digest="" preview_value=""
780
+ preview_fields=$(_qs_load_preview "$from_preview") || {
781
+ printf 'Preview JSON is malformed or incompatible.\n' >&2
782
+ exit 2
783
+ }
784
+ IFS='|' read -r preview_kind preview_template preview_encoded preview_digest <<< "$preview_fields"
785
+ preview_value=$(python3 -c 'import base64,sys; sys.stdout.buffer.write(base64.b64decode(sys.argv[1], validate=True))' "$preview_encoded" 2>/dev/null) || {
786
+ printf 'Preview JSON continuation is invalid.\n' >&2
787
+ exit 2
788
+ }
789
+ [ -n "$preview_value" ] || {
790
+ printf 'Preview JSON continuation is empty.\n' >&2
791
+ exit 2
792
+ }
793
+ if [ "$preview_kind" = "idea" ]; then
794
+ positional="$preview_value"
795
+ template_override="$preview_template"
796
+ template_flag_seen=true
797
+ elif [ "$preview_kind" = "prd" ]; then
798
+ positional="$preview_value"
799
+ if [ ! -f "$positional" ] || [ ! -r "$positional" ] || [ -L "$positional" ]; then
800
+ printf 'Preview PRD is not a readable regular non-symlink file: %s\n' "$positional" >&2
801
+ exit 2
802
+ fi
803
+ local current_digest=""
804
+ current_digest=$(shasum -a 256 "$positional" 2>/dev/null | awk '{print $1}') || current_digest=""
805
+ if [ -z "$current_digest" ] || [ "$current_digest" != "$preview_digest" ]; then
806
+ printf 'Preview PRD has changed since the saved preview; run --dry-run --json again.\n' >&2
807
+ exit 2
808
+ fi
809
+ preview_prd_digest="$preview_digest"
810
+ else
811
+ printf 'Preview JSON continuation kind is unsupported.\n' >&2
812
+ exit 2
813
+ fi
814
+ fi
815
+
816
+ # Discovery is a standalone read-only command shape. Refuse input and every
817
+ # execution/preview selector rather than guessing intent; --json is its only
818
+ # compatible modifier. This return precedes terminal, provider, estimator,
819
+ # consent, PRD, and build boundaries.
820
+ if [ "$list_templates" = true ]; then
821
+ if [ -n "$positional" ] || [ "$yes_flag" = true ] || [ "$dry_run" = true ] || [ "$template_flag_seen" = true ]; then
822
+ printf '%s--list-templates accepts only the optional --json flag.%s\n' "$_QS_RED" "$_QS_NC" >&2
823
+ exit 2
824
+ fi
825
+ _qs_list_templates "$json_output"
826
+ return $?
827
+ fi
828
+
546
829
  # A preview is an explicit no-execution request. Reject simultaneous build
547
830
  # consent instead of guessing which instruction wins. This check precedes
548
831
  # provider discovery, estimation, and every write.
@@ -791,8 +1074,20 @@ cmd_quickstart() {
791
1074
  # The non-interactive path shows it before execution rather than before a
792
1075
  # prompt: the operator still gets the honest quote in the transcript, and it
793
1076
  # is still the figure cmd_start runs with, so the quote equals the charge.
1077
+ if [ -n "$preview_prd_digest" ]; then
1078
+ local pre_estimate_digest=""
1079
+ if [ ! -f "$prd_source" ] || [ ! -r "$prd_source" ] || [ -L "$prd_source" ]; then
1080
+ printf 'Preview PRD changed before estimation; run --dry-run --json again.\n' >&2
1081
+ return 2
1082
+ fi
1083
+ pre_estimate_digest=$(shasum -a 256 "$prd_source" 2>/dev/null | awk '{print $1}') || pre_estimate_digest=""
1084
+ if [ "$pre_estimate_digest" != "$preview_prd_digest" ]; then
1085
+ printf 'Preview PRD changed before estimation; run --dry-run --json again.\n' >&2
1086
+ return 2
1087
+ fi
1088
+ fi
794
1089
  local estimate_ok=true
795
- if ! _qs_emit_plan "$prd_source" "$template_name" "$json_output" "$input_kind"; then
1090
+ if ! _qs_emit_plan "$prd_source" "$template_name" "$json_output" "$input_kind" "$positional"; then
796
1091
  estimate_ok=false
797
1092
  # No honest number available. Interactively this flips the confirm to
798
1093
  # default-NO below. Non-interactively there is no one to review the
@@ -812,6 +1107,19 @@ cmd_quickstart() {
812
1107
  fi
813
1108
  fi
814
1109
 
1110
+ if [ -n "$preview_prd_digest" ]; then
1111
+ local post_estimate_digest=""
1112
+ if [ ! -f "$prd_source" ] || [ ! -r "$prd_source" ] || [ -L "$prd_source" ]; then
1113
+ printf 'Preview PRD changed during estimation; started no build.\n' >&2
1114
+ return 2
1115
+ fi
1116
+ post_estimate_digest=$(shasum -a 256 "$prd_source" 2>/dev/null | awk '{print $1}') || post_estimate_digest=""
1117
+ if [ "$post_estimate_digest" != "$preview_prd_digest" ]; then
1118
+ printf 'Preview PRD changed during estimation; started no build.\n' >&2
1119
+ return 2
1120
+ fi
1121
+ fi
1122
+
815
1123
  # The preview terminal boundary is intentionally immediately after the
816
1124
  # shared estimator. Reaching it proves the same template and plan were
817
1125
  # rendered, while returning here makes the confirm, PRD copy, and cmd_start
@@ -876,7 +1184,36 @@ cmd_quickstart() {
876
1184
  done
877
1185
  fi
878
1186
  fi
879
- if ! cp "$prd_source" "$target" 2>/dev/null; then
1187
+ if [ "$from_preview_flag_seen" = true ]; then
1188
+ # A reviewed continuation must publish exactly the bytes that were
1189
+ # previewed, and must never win a race by overwriting another file.
1190
+ # Stage in the destination directory, verify the staged bytes, then
1191
+ # claim the final name with an atomic hard link (which fails if another
1192
+ # process created it after the suffix walk). Removing the private stage
1193
+ # leaves the linked final file intact.
1194
+ local preview_stage="" preview_stage_digest=""
1195
+ preview_stage=$(mktemp "./.loki-quickstart-prd.XXXXXX") || {
1196
+ printf '%sCould not stage the reviewed PRD in the current directory.%s\n' "$_QS_RED" "$_QS_NC" >&2
1197
+ return 2
1198
+ }
1199
+ if ! cp "$prd_source" "$preview_stage" 2>/dev/null; then
1200
+ rm -f "$preview_stage"
1201
+ printf '%sCould not stage the reviewed PRD in the current directory.%s\n' "$_QS_RED" "$_QS_NC" >&2
1202
+ return 2
1203
+ fi
1204
+ preview_stage_digest=$(shasum -a 256 "$preview_stage" 2>/dev/null | awk '{print $1}') || preview_stage_digest=""
1205
+ if [ -z "$preview_stage_digest" ] || { [ -n "$preview_prd_digest" ] && [ "$preview_stage_digest" != "$preview_prd_digest" ]; }; then
1206
+ rm -f "$preview_stage"
1207
+ printf '%sReviewed PRD changed before publish; started no build.%s\n' "$_QS_RED" "$_QS_NC" >&2
1208
+ return 2
1209
+ fi
1210
+ if ! ln "$preview_stage" "$target" 2>/dev/null; then
1211
+ rm -f "$preview_stage"
1212
+ printf '%sPRD destination changed before publish; started no build.%s\n' "$_QS_RED" "$_QS_NC" >&2
1213
+ return 2
1214
+ fi
1215
+ rm -f "$preview_stage"
1216
+ elif ! cp "$prd_source" "$target" 2>/dev/null; then
880
1217
  printf '%sCould not write the PRD to the current directory. Try a writable directory.%s\n' "$_QS_RED" "$_QS_NC" >&2
881
1218
  exit 1
882
1219
  fi
package/autonomy/run.sh CHANGED
@@ -25125,6 +25125,32 @@ handle_pause() {
25125
25125
  PAUSED=true
25126
25126
  local loki_dir="${TARGET_DIR:-.}/.loki"
25127
25127
 
25128
+ # Human-intervention counter for the Evidence Receipt.
25129
+ #
25130
+ # Counted HERE because this is the single point where a run actually blocks
25131
+ # waiting on a person -- every pause path funnels through handle_pause, and
25132
+ # the _PAUSE_IN_PROGRESS guard above means one blocking pause counts once.
25133
+ # Counting at the PAUSE-file write sites instead would over-count
25134
+ # auto-cleared perpetual-mode pauses, which no human ever saw.
25135
+ #
25136
+ # proof-generator reads this; when the file is absent the receipt renders NO
25137
+ # interventions row rather than claiming zero. Best-effort: never fail a run.
25138
+ if [ -n "${loki_dir}" ]; then
25139
+ mkdir -p "$loki_dir/state" 2>/dev/null || true
25140
+ local _iv_file="$loki_dir/state/interventions.json"
25141
+ local _iv_prev=0
25142
+ if [ -f "$_iv_file" ]; then
25143
+ _iv_prev=$(python3 -c "import json,sys
25144
+ try:
25145
+ v=json.load(open(sys.argv[1])).get('count')
25146
+ print(int(v) if isinstance(v,int) and v>=0 else 0)
25147
+ except Exception:
25148
+ print(0)" "$_iv_file" 2>/dev/null || echo 0)
25149
+ fi
25150
+ printf '{"count":%s,"basis":"blocking pauses that waited on a human"}\n' \
25151
+ "$(( _iv_prev + 1 ))" > "$_iv_file" 2>/dev/null || true
25152
+ fi
25153
+
25128
25154
  # Save state before pausing so it persists across potential crashes
25129
25155
  save_state ${RETRY_COUNT:-0} "paused" 0
25130
25156
 
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.22.4"
10
+ __version__ = "9.22.6"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: