loki-mode 9.22.5 → 9.22.7

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 (86) hide show
  1. package/README.md +11 -4
  2. package/SKILL.md +2 -2
  3. package/VERSION +1 -1
  4. package/autonomy/issue-parser.sh +112 -0
  5. package/autonomy/issue-providers.sh +82 -11
  6. package/autonomy/lib/proof-generator.py +88 -0
  7. package/autonomy/lib/proof-pr.sh +52 -0
  8. package/autonomy/loki +147 -3
  9. package/autonomy/quickstart.sh +250 -5
  10. package/autonomy/run.sh +26 -0
  11. package/dashboard/__init__.py +1 -1
  12. package/docs/EXECUTION-COCKPIT-PLAN.md +289 -0
  13. package/docs/HARNESS-INTELLIGENCE-PLAN.md +194 -0
  14. package/docs/ISSUE-TO-PR-GOLDEN-PATH-PLAN.md +257 -0
  15. package/loki-ts/dist/loki.js +299 -297
  16. package/mcp/__init__.py +1 -1
  17. package/package.json +1 -1
  18. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  19. package/web-app/dist/assets/{AdminPage-DNGNuAge.js → AdminPage-DLhWX6si.js} +1 -1
  20. package/web-app/dist/assets/{Avatar-CqAveJaT.js → Avatar-CqyDcUxl.js} +1 -1
  21. package/web-app/dist/assets/{Badge-BtMejA0Z.js → Badge-BKGSt1n2.js} +1 -1
  22. package/web-app/dist/assets/{Button-CikxmqSs.js → Button-DuuKHXre.js} +1 -1
  23. package/web-app/dist/assets/CockpitPage-DdClYERC.js +47 -0
  24. package/web-app/dist/assets/{ComparePage-BB8DJBWR.js → ComparePage-CUWlqBxv.js} +1 -1
  25. package/web-app/dist/assets/ErrorBoundary-RwfGKyRJ.js +16 -0
  26. package/web-app/dist/assets/{EvidenceReceiptPanel-6oquKEjd.js → EvidenceReceiptPanel-CzIgk9sg.js} +1 -1
  27. package/web-app/dist/assets/{GitHubIssuesPanel-5XLYWzWH.js → GitHubIssuesPanel-CsXVKuyJ.js} +3 -3
  28. package/web-app/dist/assets/{GitHubPRsPanel-DUfmFYeu.js → GitHubPRsPanel-CYRp1eKS.js} +3 -3
  29. package/web-app/dist/assets/{HomePage-DZ4MmOdH.js → HomePage-CVJcI5pQ.js} +3 -3
  30. package/web-app/dist/assets/LoginPage-BBJNV9Qb.js +1 -0
  31. package/web-app/dist/assets/{MagicPage-CFbX5gWg.js → MagicPage-CIi9P8jK.js} +1 -1
  32. package/web-app/dist/assets/{MetricsPage-DbL6xmRQ.js → MetricsPage-Bm34ipiy.js} +2 -2
  33. package/web-app/dist/assets/{NotFoundPage-BUrjXPAR.js → NotFoundPage-DpTbcC2P.js} +1 -1
  34. package/web-app/dist/assets/{ProjectPage-iP6dJihg.js → ProjectPage-CY2SMGx9.js} +65 -90
  35. package/web-app/dist/assets/{ProjectsPage-BibN9bHl.js → ProjectsPage-DSrXUUKJ.js} +2 -2
  36. package/web-app/dist/assets/{SettingsPage-C0WJp1hW.js → SettingsPage-jvDyOqYi.js} +3 -3
  37. package/web-app/dist/assets/{ShowcasePage-DUv0Njp8.js → ShowcasePage-C8tFWKau.js} +1 -1
  38. package/web-app/dist/assets/{SystemSettingsPage-BIaCB0Vn.js → SystemSettingsPage-DH4FTH3K.js} +2 -2
  39. package/web-app/dist/assets/{TeamsPage-BE0szv9r.js → TeamsPage-C1fQYauF.js} +2 -2
  40. package/web-app/dist/assets/{TemplatesPage-BzkA9kat.js → TemplatesPage-CSTFt-4P.js} +1 -1
  41. package/web-app/dist/assets/TerminalOutput-Cu4NsyN6.js +16 -0
  42. package/web-app/dist/assets/{activity-AbfFz38r.js → activity-CtjsDzmh.js} +1 -1
  43. package/web-app/dist/assets/{bell-DF6Z6y5U.js → bell-C5es1xAs.js} +1 -1
  44. package/web-app/dist/assets/{bot-DQ4np_3n.js → bot-DE04U5Lc.js} +1 -1
  45. package/web-app/dist/assets/{check-RWuguCwE.js → check-DSKKA9EU.js} +1 -1
  46. package/web-app/dist/assets/{chevron-left-VpbQPOkC.js → chevron-left-DJ13W5AR.js} +1 -1
  47. package/web-app/dist/assets/{circle-alert-DwT82lJx.js → circle-alert-CS0BfqAd.js} +1 -1
  48. package/web-app/dist/assets/{clock-BBlUa93G.js → clock-D1Y5nruk.js} +1 -1
  49. package/web-app/dist/assets/{cloud-Dgbz3VCA.js → cloud-nU6LBoZL.js} +1 -1
  50. package/web-app/dist/assets/{code-xml-r6qqION5.js → code-xml-BZfvIdFo.js} +1 -1
  51. package/web-app/dist/assets/{copy-DMHj52B8.js → copy-CK_2O1Xv.js} +1 -1
  52. package/web-app/dist/assets/{database-uJqHSHR1.js → database-DJiwznAm.js} +1 -1
  53. package/web-app/dist/assets/{dollar-sign-BZagljek.js → dollar-sign-pO20A3QY.js} +1 -1
  54. package/web-app/dist/assets/{file-code-corner-DMopmhu0.js → file-code-corner-Besy6vek.js} +1 -1
  55. package/web-app/dist/assets/{file-plus-DU_hT_3w.js → file-plus-DeaswxYY.js} +1 -1
  56. package/web-app/dist/assets/{folder-open-BP5TtQAW.js → folder-open-cr1UvQLI.js} +1 -1
  57. package/web-app/dist/assets/{git-commit-horizontal-zFk_IobS.js → git-commit-horizontal-BLxnfbSn.js} +1 -1
  58. package/web-app/dist/assets/{globe-mthCRtfp.js → globe-DrhG1NfX.js} +1 -1
  59. package/web-app/dist/assets/{hammer-BWaLBvSA.js → hammer-CeZtPBfE.js} +1 -1
  60. package/web-app/dist/assets/index-84JUN_aA.css +1 -0
  61. package/web-app/dist/assets/{index-BXA3UT6P.js → index-CLvjO22Z.js} +24 -24
  62. package/web-app/dist/assets/{layers-Q1LXV5fy.js → layers-fldADzyE.js} +1 -1
  63. package/web-app/dist/assets/{lightbulb-vRRNJ3dB.js → lightbulb-CznOfsPb.js} +1 -1
  64. package/web-app/dist/assets/{loader-circle-KgjeHEcV.js → loader-circle---lPB1Fs.js} +1 -1
  65. package/web-app/dist/assets/{lock-BerKqno7.js → lock-D5jVF-tO.js} +1 -1
  66. package/web-app/dist/assets/{mail-XAxTbprX.js → mail-D7HkLnlW.js} +1 -1
  67. package/web-app/dist/assets/minus-C-dT19Ct.js +6 -0
  68. package/web-app/dist/assets/{package-DLPKWAvW.js → package-Cm9B-cKE.js} +1 -1
  69. package/web-app/dist/assets/{plus-NMxxwHMn.js → plus-CQwOffLD.js} +1 -1
  70. package/web-app/dist/assets/{refresh-cw-Dt42kYtM.js → refresh-cw-CvWJZLbN.js} +1 -1
  71. package/web-app/dist/assets/{rotate-ccw-BCR4Tp9u.js → rotate-ccw-C2Q1LYgM.js} +1 -1
  72. package/web-app/dist/assets/{save-sR4MJd42.js → save-BP0MvpoQ.js} +1 -1
  73. package/web-app/dist/assets/{server-SXkchokx.js → server-4i4VZXix.js} +1 -1
  74. package/web-app/dist/assets/{shield-alert-Bvgp6_Dz.js → shield-alert-D2_bgb7j.js} +1 -1
  75. package/web-app/dist/assets/thumbs-up-vACbzeXj.js +26 -0
  76. package/web-app/dist/assets/{trash-2-D0IGudH7.js → trash-2-EYjdESLs.js} +1 -1
  77. package/web-app/dist/assets/trending-down-BWBdcvE3.js +6 -0
  78. package/web-app/dist/assets/{trending-up-DDrJKL5i.js → trending-up-Bix6V_-Y.js} +1 -1
  79. package/web-app/dist/assets/{upload-C54rjN8K.js → upload-B7hh6ERF.js} +1 -1
  80. package/web-app/dist/assets/{usePolling-D7fw4YSM.js → usePolling-b_e-k-IN.js} +1 -1
  81. package/web-app/dist/assets/{user-Cg7fbzdh.js → user-pmjZKWEg.js} +1 -1
  82. package/web-app/dist/index.html +2 -2
  83. package/web-app/dist/assets/LoginPage-VMzI6ROD.js +0 -1
  84. package/web-app/dist/assets/TerminalOutput-DBrdT_Fp.js +0 -31
  85. package/web-app/dist/assets/index-DgpUuqwR.css +0 -1
  86. package/web-app/dist/assets/trending-down-BbFFBq-v.js +0 -11
@@ -406,12 +406,21 @@ _qs_selected_provider() {
406
406
  # non-zero if the estimator gave no result (caller falls back to a no-number
407
407
  # confirm, never fabricating a figure).
408
408
  _qs_emit_plan() {
409
- 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
410
415
  local plan_json=""
411
416
  plan_json=$(show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""
412
417
  if [ -z "$plan_json" ]; then
413
418
  return 1
414
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
415
424
  local parsed
416
425
  parsed=$(printf '%s' "$plan_json" | python3 -c "
417
426
  import json, sys
@@ -447,7 +456,19 @@ import sys
447
456
  plan = json.load(sys.stdin)
448
457
  if not isinstance(plan, dict):
449
458
  sys.exit(1)
450
- 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
+ }
451
472
  payload = {
452
473
  "schema_version": 1,
453
474
  "command": "loki quickstart",
@@ -456,9 +477,10 @@ payload = {
456
477
  "selected_template": template_name if input_kind == "idea" else None,
457
478
  "source_name": template_name if input_kind == "prd" else None,
458
479
  "plan": plan,
480
+ "continuation": continuation,
459
481
  }
460
482
  json.dump(payload, sys.stdout, separators=(",", ":"), sort_keys=True)
461
- ' "$template_name" "$input_kind" 2>/dev/null) || json_payload=""
483
+ ' "$template_name" "$input_kind" "$input_value" "$source_digest_after" 2>/dev/null) || json_payload=""
462
484
  [ -n "$json_payload" ] || return 1
463
485
  printf '%s\n' "$json_payload" >&3
464
486
  return 0
@@ -481,6 +503,111 @@ json.dump(payload, sys.stdout, separators=(",", ":"), sort_keys=True)
481
503
  return 0
482
504
  }
483
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
+
484
611
  # _qs_help: concise usage for `loki quickstart --help`.
485
612
  _qs_help() {
486
613
  printf '%sloki quickstart%s - guided first build (setup, idea, template, plan, go)\n' "$_QS_BOLD" "$_QS_NC"
@@ -498,6 +625,7 @@ _qs_help() {
498
625
  printf ' --yes, -y Auto-confirm the final build prompt (still shows the plan)\n'
499
626
  printf ' --dry-run Preview the selected template and plan; write/start nothing\n'
500
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'
501
629
  printf ' --template N Use the exact shipped template N for an IDEA\n'
502
630
  printf ' --list-templates List every shipped template and its purpose\n'
503
631
  printf ' --help, -h Show this help and exit\n'
@@ -515,6 +643,7 @@ _qs_help() {
515
643
  printf ' An IDEA (or readable PRD path) is required. No provider is checked,\n'
516
644
  printf ' no file is written, and no build is started. Do not combine with --yes.\n'
517
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'
518
647
  printf '\n'
519
648
  printf 'Steps:\n'
520
649
  printf ' 1. Setup Check for an AI provider for execution (skipped in preview)\n'
@@ -550,6 +679,9 @@ cmd_quickstart() {
550
679
  local template_flag_seen=false
551
680
  local list_templates=false
552
681
  local list_templates_flag_seen=false
682
+ local from_preview=""
683
+ local from_preview_flag_seen=false
684
+ local preview_prd_digest=""
553
685
  if _qs_assume_yes; then assume_yes=true; fi
554
686
 
555
687
  # yes_flag tracks EXPLICIT --yes/-y on THIS command's argv, and nothing else.
@@ -604,6 +736,19 @@ cmd_quickstart() {
604
736
  list_templates_flag_seen=true
605
737
  shift
606
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
+ ;;
607
752
  --*)
608
753
  printf '%sUnknown option: %s%s\n' "$_QS_RED" "$1" "$_QS_NC" >&2
609
754
  printf "Run 'loki quickstart --help' for usage.\n" >&2
@@ -622,6 +767,52 @@ cmd_quickstart() {
622
767
  esac
623
768
  done
624
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
+
625
816
  # Discovery is a standalone read-only command shape. Refuse input and every
626
817
  # execution/preview selector rather than guessing intent; --json is its only
627
818
  # compatible modifier. This return precedes terminal, provider, estimator,
@@ -883,8 +1074,20 @@ cmd_quickstart() {
883
1074
  # The non-interactive path shows it before execution rather than before a
884
1075
  # prompt: the operator still gets the honest quote in the transcript, and it
885
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
886
1089
  local estimate_ok=true
887
- 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
888
1091
  estimate_ok=false
889
1092
  # No honest number available. Interactively this flips the confirm to
890
1093
  # default-NO below. Non-interactively there is no one to review the
@@ -904,6 +1107,19 @@ cmd_quickstart() {
904
1107
  fi
905
1108
  fi
906
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
+
907
1123
  # The preview terminal boundary is intentionally immediately after the
908
1124
  # shared estimator. Reaching it proves the same template and plan were
909
1125
  # rendered, while returning here makes the confirm, PRD copy, and cmd_start
@@ -968,7 +1184,36 @@ cmd_quickstart() {
968
1184
  done
969
1185
  fi
970
1186
  fi
971
- 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
972
1217
  printf '%sCould not write the PRD to the current directory. Try a writable directory.%s\n' "$_QS_RED" "$_QS_NC" >&2
973
1218
  exit 1
974
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.5"
10
+ __version__ = "9.22.7"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try: