loki-mode 10.5.11 → 10.5.13

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 (92) hide show
  1. package/README.md +30 -962
  2. package/SKILL.md +3 -3
  3. package/VERSION +1 -1
  4. package/autonomy/app-runner.sh +2 -1
  5. package/autonomy/completion-council.sh +40 -3
  6. package/autonomy/lib/doctor-fix.py +1 -1
  7. package/autonomy/lib/proof-generator.py +40 -84
  8. package/autonomy/lib/proof-template.html +7 -11
  9. package/autonomy/loki +118 -69
  10. package/autonomy/receipt_jwt.py +59 -14
  11. package/autonomy/run.sh +91 -124
  12. package/autonomy/trigger-server.py +2 -2
  13. package/autonomy/verify.sh +37 -2
  14. package/dashboard/__init__.py +1 -1
  15. package/dashboard/server.py +2 -2
  16. package/docs/COMPETITIVE-NEXT-10.md +3 -4
  17. package/docs/INSTALLATION.md +1 -1
  18. package/docs/README-FULL.md +977 -0
  19. package/docs/SIGNED-RECEIPTS.md +40 -82
  20. package/docs/TOOLS.md +0 -23
  21. package/docs/VERIFICATION-COST.md +2 -2
  22. package/docs/v10/ADOPTION-PLAN.md +1 -1
  23. package/docs/v10/BOARD.md +42 -22
  24. package/docs/v10/DECISIONS.md +11 -0
  25. package/docs/v10/ENGINE.md +4 -4
  26. package/docs/v10/FOUNDER-QUEUE.md +1 -0
  27. package/docs/v10/GUIDE.md +0 -4
  28. package/docs/v10/PROGRESS.md +43 -0
  29. package/loki-ts/dist/loki.js +587 -563
  30. package/mcp/__init__.py +1 -1
  31. package/package.json +2 -2
  32. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  33. package/tools/gate-status.py +3 -46
  34. package/tools/receipt-attest.py +1 -1
  35. package/web-app/dist/assets/{AdminPage-bi-XcWhD.js → AdminPage-C8zraQ0u.js} +1 -1
  36. package/web-app/dist/assets/{Avatar-CmeIKHo6.js → Avatar-DnjIA4Ap.js} +1 -1
  37. package/web-app/dist/assets/{Badge-Bc3RMd7Y.js → Badge-C487hf0b.js} +1 -1
  38. package/web-app/dist/assets/{Button-Dc7yzWmJ.js → Button-5AWAEksl.js} +1 -1
  39. package/web-app/dist/assets/{CockpitPage-C_aRTgNm.js → CockpitPage-B6_1gTFO.js} +1 -1
  40. package/web-app/dist/assets/{ComparePage-B8eqE3GD.js → ComparePage-DDbC8-yi.js} +1 -1
  41. package/web-app/dist/assets/{ErrorBoundary-BRVSyrME.js → ErrorBoundary-CWHKwjEp.js} +1 -1
  42. package/web-app/dist/assets/{EvidenceReceiptPanel-DBkXGSAF.js → EvidenceReceiptPanel-MfJq92Ty.js} +1 -1
  43. package/web-app/dist/assets/{GitHubIssuesPanel-rRZ4rP-a.js → GitHubIssuesPanel-BBdijzdg.js} +1 -1
  44. package/web-app/dist/assets/{GitHubPRsPanel-NMj2a_-3.js → GitHubPRsPanel-DjBHyymT.js} +1 -1
  45. package/web-app/dist/assets/{HomePage-YGPKiV9F.js → HomePage-B_6m7ITK.js} +2 -2
  46. package/web-app/dist/assets/{LoginPage-CVx7_jtD.js → LoginPage-OGgMvfjT.js} +1 -1
  47. package/web-app/dist/assets/{MagicPage-G9BuqW4T.js → MagicPage-m4MSfxWf.js} +1 -1
  48. package/web-app/dist/assets/{MetricsPage-CrPo66D6.js → MetricsPage-D8Ea1ALe.js} +1 -1
  49. package/web-app/dist/assets/{NotFoundPage-CbjchFXV.js → NotFoundPage-ttvXL-7K.js} +1 -1
  50. package/web-app/dist/assets/{ProjectPage-CmPoA170.js → ProjectPage-CDX8tSPV.js} +3 -3
  51. package/web-app/dist/assets/{ProjectsPage-CfVMc4nw.js → ProjectsPage-BQ73nYWf.js} +1 -1
  52. package/web-app/dist/assets/{SettingsPage-6nZ-vONY.js → SettingsPage-B6k71q2g.js} +1 -1
  53. package/web-app/dist/assets/{ShowcasePage-CxdZjDzY.js → ShowcasePage-CNbwtr7D.js} +1 -1
  54. package/web-app/dist/assets/{SystemSettingsPage-Bu0kwwMe.js → SystemSettingsPage-CD2qys43.js} +1 -1
  55. package/web-app/dist/assets/{TeamsPage-Ck6xWgOM.js → TeamsPage-DAmsRIVD.js} +1 -1
  56. package/web-app/dist/assets/{TemplatesPage-BduvcCsH.js → TemplatesPage-CcRyyUhL.js} +1 -1
  57. package/web-app/dist/assets/{TerminalOutput-BmleKz7Q.js → TerminalOutput-DRquxtQ1.js} +1 -1
  58. package/web-app/dist/assets/{activity-xUOENXj1.js → activity-DUDUi3Qd.js} +1 -1
  59. package/web-app/dist/assets/{bell-Do1DnngS.js → bell-tDpCOcHx.js} +1 -1
  60. package/web-app/dist/assets/{bot-Csus-lhk.js → bot-87IR4aTF.js} +1 -1
  61. package/web-app/dist/assets/{check-CDMuF2fW.js → check-C2Y2z-Ge.js} +1 -1
  62. package/web-app/dist/assets/{chevron-left-1pxhei7a.js → chevron-left-CsPFT88x.js} +1 -1
  63. package/web-app/dist/assets/{circle-alert-HufOSfwc.js → circle-alert-Cp2UJikd.js} +1 -1
  64. package/web-app/dist/assets/{clock-BoCMViPQ.js → clock-DZ1cwNyf.js} +1 -1
  65. package/web-app/dist/assets/{cloud-B7JfKA_r.js → cloud-q5TrIOV-.js} +1 -1
  66. package/web-app/dist/assets/{code-xml-BduQ3MqM.js → code-xml-0TNGOS3c.js} +1 -1
  67. package/web-app/dist/assets/{database-CQ9s31cU.js → database-g73vgDVx.js} +1 -1
  68. package/web-app/dist/assets/{dollar-sign-CVJu5-A0.js → dollar-sign-DAKkR_MO.js} +1 -1
  69. package/web-app/dist/assets/{file-code-corner-BPu1BX6Z.js → file-code-corner-DTAvdxjl.js} +1 -1
  70. package/web-app/dist/assets/{file-plus-CGa7y-QG.js → file-plus-D5SB-1Uk.js} +1 -1
  71. package/web-app/dist/assets/{globe-ClS0c8Cw.js → globe-j5YaNmx9.js} +1 -1
  72. package/web-app/dist/assets/{hammer-CeljzUTd.js → hammer-C9yoXOC6.js} +1 -1
  73. package/web-app/dist/assets/{index-Dn35XhiL.js → index--EayJsJ-.js} +3 -3
  74. package/web-app/dist/assets/{layers-CzGZ-NNL.js → layers-B4mxkWdH.js} +1 -1
  75. package/web-app/dist/assets/{loader-circle-Dy1j6q2e.js → loader-circle-CP-qD1W2.js} +1 -1
  76. package/web-app/dist/assets/{lock-pmDyRsWL.js → lock-DwHNUSTU.js} +1 -1
  77. package/web-app/dist/assets/{package-C1dXDph_.js → package-BxLWioQ8.js} +1 -1
  78. package/web-app/dist/assets/{plus-CKPSHefh.js → plus-B-43Byzl.js} +1 -1
  79. package/web-app/dist/assets/{refresh-cw-CRmwRgDw.js → refresh-cw-5s47zAFV.js} +1 -1
  80. package/web-app/dist/assets/{rotate-ccw-Br04gZR3.js → rotate-ccw-BewZ8_Rx.js} +1 -1
  81. package/web-app/dist/assets/{scroll-text-BHjpthNc.js → scroll-text-DPNpHL-Z.js} +1 -1
  82. package/web-app/dist/assets/{server-Db3IfTpl.js → server-hL8_-vRq.js} +1 -1
  83. package/web-app/dist/assets/{shield-alert-jJXWKLDZ.js → shield-alert-DAGc2aLO.js} +1 -1
  84. package/web-app/dist/assets/{thumbs-up-BksBTkqT.js → thumbs-up-DRGzcWEH.js} +1 -1
  85. package/web-app/dist/assets/{trash-2-CzXuGa-4.js → trash-2-Bo_YPIqS.js} +1 -1
  86. package/web-app/dist/assets/{trending-down-c_JXJq0s.js → trending-down-BQ1cK9F3.js} +1 -1
  87. package/web-app/dist/assets/{trending-up-DtvURYKn.js → trending-up-BpnkTO1T.js} +1 -1
  88. package/web-app/dist/assets/{upload-BSsOPrmR.js → upload-DDdQNxmW.js} +1 -1
  89. package/web-app/dist/assets/{usePolling-CFWdgkmg.js → usePolling-z_FXsGz7.js} +1 -1
  90. package/web-app/dist/assets/{user-CL-HjV4R.js → user-obPH42HX.js} +1 -1
  91. package/web-app/dist/index.html +1 -1
  92. package/tools/signing-status.py +0 -292
package/autonomy/run.sh CHANGED
@@ -238,6 +238,31 @@ fi
238
238
  SCRIPT_DIR="${LOKI_ORIGINAL_SCRIPT_DIR:-$SCRIPT_DIR}"
239
239
  PROJECT_DIR="${LOKI_ORIGINAL_PROJECT_DIR:-$PROJECT_DIR}"
240
240
 
241
+ # A-134: `loki quick` is quiet unless LOKI_VERBOSE=1. The run re-executes itself with
242
+ # stdout in .loki/quick-run.log; this shell prints the inner run's compact receipt and
243
+ # a warning count. Errors go to stderr.
244
+ if [ -z "${LOKI_QUICK_INNER:-}" ] && [ "${LOKI_VERBOSE:-0}" != "1" ] && [ "${BASH_SOURCE[0]}" = "${0}" ]; then
245
+ case "${1:-}" in */quick-prd-*.md | quick-prd-*.md)
246
+ _qd="$(cd "$(dirname "$1")" && pwd -P)"; rm -f "$_qd/quick-receipt.txt"
247
+ # Async with default INT/QUIT (a bare `&` child ignores them), signals forwarded.
248
+ ( trap - INT QUIT; LOKI_QUICK_DIR="$_qd" LOKI_QUICK_OUTER_PID=$$ LOKI_QUICK_INNER=1 exec "$BASH" "$0" "$@" >"$_qd/quick-run.log" ) &
249
+ _qpid=$!
250
+ trap 'kill -INT "$_qpid" 2>/dev/null' INT
251
+ trap 'kill -TERM "$_qpid" 2>/dev/null' TERM HUP
252
+ wait "$_qpid"; _qrc=$?
253
+ kill -0 "$_qpid" 2>/dev/null && { wait "$_qpid"; _qrc=$?; }
254
+ trap - INT TERM HUP
255
+ _qw=$(grep -cF '[WARN]' "$_qd/quick-run.log" 2>/dev/null); _qw="${_qw:-0}"
256
+ if [ "$_qrc" -ne 0 ] || [ ! -s "$_qd/quick-receipt.txt" ]; then
257
+ _qr=$(grep -E '\[(ERROR|WARN)\]|[Ss]topped|[Ff]ailed' "$_qd/quick-run.log" 2>/dev/null | tail -n 3)
258
+ echo "${_qr:-$(tail -n 3 "$_qd/quick-run.log" 2>/dev/null)}"
259
+ fi
260
+ [ "$_qw" -gt 0 ] && echo "Warnings: $_qw (see $_qd/quick-run.log)"
261
+ cat "$_qd/quick-receipt.txt" 2>/dev/null
262
+ exit "$_qrc" ;;
263
+ esac
264
+ fi
265
+
241
266
  # Clean up ONLY the recorded temp copy, and ONLY if it is a real temp file.
242
267
  # Deleting BASH_SOURCE[0] here was the bug: an inherited LOKI_RUNNING_FROM_TEMP
243
268
  # made BASH_SOURCE[0] the canonical source, so run.sh deleted itself on spawned
@@ -1722,7 +1747,7 @@ if [ -f "$PROVIDERS_DIR/loader.sh" ]; then
1722
1747
  _detected="$(auto_detect_provider 2>/dev/null || true)"
1723
1748
  if [ -n "$_detected" ]; then
1724
1749
  LOKI_PROVIDER="$_detected"
1725
- echo "[loki] provider: $LOKI_PROVIDER (auto-detected)" >&2
1750
+ [ -n "${LOKI_QUICK_INNER:-}" ] || echo "[loki] provider: $LOKI_PROVIDER (auto-detected)" >&2
1726
1751
  else
1727
1752
  # Nothing installed. Keep the historical default so the existing
1728
1753
  # "not installed" error path reports claude, which is the actionable
@@ -1833,7 +1858,7 @@ _loki_log_enabled() { [ "$1" -ge "$(_loki_log_threshold)" ]; }
1833
1858
  log_info() { _loki_log_enabled 1 && echo -e "${GREEN}[INFO]${NC} $*" || true; }
1834
1859
  log_warn() { _loki_log_enabled 2 && echo -e "${YELLOW}[WARN]${NC} $*" || true; }
1835
1860
  log_warning() { log_warn "$@"; } # Alias for backwards compatibility
1836
- log_error() { echo -e "${RED}[ERROR]${NC} $*"; }
1861
+ log_error() { local _fd=1; [ -z "${LOKI_QUICK_INNER:-}" ] || _fd=2; echo -e "${RED}[ERROR]${NC} $*" >&"$_fd"; }
1837
1862
  log_step() { _loki_log_enabled 1 && echo -e "${CYAN}[STEP]${NC} $*" || true; }
1838
1863
  log_debug() { [[ "${LOKI_DEBUG:-}" == "true" ]] && echo -e "${CYAN}[DEBUG]${NC} $*" >&2 || true; }
1839
1864
 
@@ -7716,68 +7741,6 @@ EOF
7716
7741
  # Track last known phase to detect changes
7717
7742
  LAST_KNOWN_PHASE=""
7718
7743
 
7719
- # Set the current phase and emit event if changed
7720
- # v7.5.12: append a log entry to the iteration-N task in in-progress.json.
7721
- # Args: iteration, phase, level, message. All silent on failure -- this
7722
- # must NEVER kill the run.
7723
- append_iteration_task_log() {
7724
- local iteration="${1:-0}"
7725
- local phase="${2:-}"
7726
- local level="${3:-info}"
7727
- local message="${4:-}"
7728
- local in_progress_file=".loki/queue/in-progress.json"
7729
-
7730
- [ -z "$iteration" ] && return 0
7731
- [ "$iteration" = "0" ] && return 0
7732
- [ ! -f "$in_progress_file" ] && return 0
7733
-
7734
- local timestamp
7735
- timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
7736
-
7737
- ITER="$iteration" PHASE="$phase" LEVEL="$level" \
7738
- MESSAGE="$message" TIMESTAMP="$timestamp" \
7739
- python3 - "$in_progress_file" <<'PY' 2>/dev/null || true
7740
- import json, os, sys, tempfile
7741
- path = sys.argv[1]
7742
- target_id = f"iteration-{os.environ['ITER']}"
7743
- entry = {
7744
- "timestamp": os.environ["TIMESTAMP"],
7745
- "iteration": int(os.environ["ITER"]),
7746
- "level": os.environ.get("LEVEL", "info"),
7747
- "phase": os.environ.get("PHASE", ""),
7748
- "message": os.environ.get("MESSAGE", ""),
7749
- }
7750
- try:
7751
- with open(path) as f:
7752
- data = json.load(f)
7753
- except Exception:
7754
- sys.exit(0)
7755
- # Support both [...] and {tasks: [...]} shapes (matches load_queue_tasks).
7756
- tasks = data["tasks"] if isinstance(data, dict) and isinstance(data.get("tasks"), list) else (data if isinstance(data, list) else None)
7757
- if tasks is None:
7758
- sys.exit(0)
7759
- mutated = False
7760
- for t in tasks:
7761
- if not isinstance(t, dict):
7762
- continue
7763
- if t.get("id") == target_id:
7764
- logs = t.get("logs")
7765
- if not isinstance(logs, list):
7766
- logs = []
7767
- logs.append(entry)
7768
- t["logs"] = logs
7769
- mutated = True
7770
- break
7771
- if not mutated:
7772
- sys.exit(0)
7773
- out_dir = os.path.dirname(path) or "."
7774
- fd, tmp = tempfile.mkstemp(dir=out_dir, suffix=".json")
7775
- with os.fdopen(fd, "w") as f:
7776
- json.dump(data, f, indent=2)
7777
- os.replace(tmp, path)
7778
- PY
7779
- }
7780
-
7781
7744
  #===============================================================================
7782
7745
  # Dashboard State Writer (Real-time sync with web dashboard)
7783
7746
  #===============================================================================
@@ -11062,6 +11025,12 @@ _loki_untrack_agent_committed_user_files() {
11062
11025
  return 0
11063
11026
  }
11064
11027
 
11028
+ # A-132: true for a `loki quick` run. Only cmd_quick writes .loki/quick-prd-<pid>.md.
11029
+ _loki_is_quick_prd() {
11030
+ case "${1:-}" in */quick-prd-*.md | quick-prd-*.md) return 0 ;; esac
11031
+ return 1
11032
+ }
11033
+
11065
11034
  commit_session_changes() {
11066
11035
  # Squash the session's work into one honest session-end commit on the agent
11067
11036
  # branch (LOCK A3/A4/A8). Commit-always (incl. failed runs) so the user is
@@ -11256,6 +11225,29 @@ _loki_proof_json_for_pr() {
11256
11225
  return 0
11257
11226
  }
11258
11227
 
11228
+ # A-134: compact quiet-mode receipt for the outer shell (see top of file) to print.
11229
+ _loki_quick_receipt_write() {
11230
+ local ld="${TARGET_DIR:-.}/.loki" rid pj
11231
+ rid="$(cat "$ld/state/last-proof-id.txt" 2>/dev/null || true)"
11232
+ case "$rid" in '' | *[!A-Za-z0-9._-]*) return 0 ;; esac
11233
+ pj="$ld/proofs/$rid/proof.json"
11234
+ [ -f "$pj" ] || return 0
11235
+ python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
11236
+ import json
11237
+ d = json.load(open(sys.argv[1]))
11238
+ g = (d.get('facts') or {}).get('git') or {}
11239
+ v = d.get('verification') or {}
11240
+ if sys.argv[3] and g.get('head_sha') != sys.argv[3]:
11241
+ print('Evidence Receipt: unavailable (final proof generation failed; newest proof predates the session commit)')
11242
+ sys.exit(0)
11243
+ deg = [str(x.get('item') or '').split(':')[-1] for x in ((d.get('honesty') or {}).get('degraded') or []) if isinstance(x, dict)]
11244
+ notes = ([] if (v.get('gpg_signature') or v.get('attestation')) else ['unsigned']) + (['%d not proven: %s' % (len(deg), ', '.join(deg))] if deg else [])
11245
+ print('Evidence Receipt: ' + str((d.get('honesty') or {}).get('headline') or 'unavailable') + (' (' + '; '.join(notes) + ')' if notes else ''))
11246
+ print('receipt_sha256: ' + str(v.get('hash') or ''))
11247
+ print('Head sha: ' + str(g.get('head_sha') or '') + ' Diff sha256: ' + str(g.get('diff_sha256') or ''))
11248
+ print('Check it: loki verify (or loki proof verify ' + sys.argv[2] + ')')" "$pj" "$rid" "$(git -C "${TARGET_DIR:-.}" rev-parse HEAD 2>/dev/null || true)" > "$LOKI_QUICK_DIR/quick-receipt.txt" 2>/dev/null || rm -f "$LOKI_QUICK_DIR/quick-receipt.txt"
11249
+ }
11250
+
11259
11251
  create_session_pr() {
11260
11252
  # Advise the user how to open a PR for the agent branch. PRINT-ONLY by
11261
11253
  # default (no push, no PR). LOKI_AUTO_PR=1 restores the legacy auto behavior.
@@ -11321,11 +11313,9 @@ create_session_pr() {
11321
11313
  # Proven PR (Loop 6): print the Evidence Receipt block AFTER the push/PR
11322
11314
  # advice so a user opening a manual PR can paste it into the body. This is
11323
11315
  # the print-PR-body fallback. Default-on; LOKI_PROVEN_PR=0 -> not invoked
11324
- # (advisory output byte-identical to before). Production callers pass an
11325
- # empty expected_head_sha: the session commit lands between proof-gen and
11326
- # this point, so the branch head is structurally offset from the proof's
11327
- # head and feeding it would false-degrade every legitimate receipt; the
11328
- # anti-stale guarantee is the run_id pointer (R-DET-1), not a head match.
11316
+ # (advisory output byte-identical to before). The final proof now precedes this
11317
+ # call, so its head is the branch head; expected_head_sha stays empty and the
11318
+ # anti-stale guarantee is the run_id pointer (R-DET-1).
11329
11319
  if [ "${LOKI_PROVEN_PR:-1}" != "0" ] && declare -f render_evidence_receipt_md >/dev/null 2>&1; then
11330
11320
  local _pr_proof=""
11331
11321
  _pr_proof="$(_loki_proof_json_for_pr 2>/dev/null || true)"
@@ -20989,6 +20979,7 @@ PYEOF
20989
20979
  _intelligent_usage_regen() {
20990
20980
  local target_dir="${TARGET_DIR:-.}"
20991
20981
  local usage_path="$target_dir/USAGE.md"
20982
+ _loki_is_quick_prd "${PRD_PATH:-}" && return 0 # A-132: no USAGE regen on a quick fix
20992
20983
  # Only the provider the operator chose may see a prompt (V10 P5): on any
20993
20984
  # other provider keep the agent-written USAGE.md. Policy (and the
20994
20985
  # LOKI_ALLOW_CLAUDE_SIDECALLS=1 opt-in) lives in providers/loader.sh,
@@ -22426,6 +22417,9 @@ build_prompt() {
22426
22417
  # and to the dashboard/Purple Lab UI.
22427
22418
  local usage_doc_instruction="USAGE_DOC_REQUIRED: Before invoking loki_complete_task (or touching .loki/signals/COMPLETION_REQUESTED), write USAGE.md at the project root. Detect the stack from package.json/requirements.txt/Cargo.toml/go.mod/etc. and include these sections: (1) Prerequisites (runtimes, ports, env vars), (2) Install (exact command, e.g. 'npm install' or 'pip install -r requirements.txt'), (3) Start (exact command, e.g. 'npm start' or 'python server.py'), (4) Verify -- 2 to 3 copy-paste commands the user can run to confirm it works (curl examples for APIs with expected output, browser URL for web UIs, command invocation for CLIs), (5) Stop (Ctrl+C or 'lsof -ti:PORT | xargs kill -9' for backgrounded servers). Keep it under 100 lines, plain Markdown, no emojis. If USAGE.md already exists and is accurate, leave it; otherwise create or update it."
22428
22419
 
22420
+ # A-132: a quick fix does not ask for USAGE.md (persist_user_prd repoints $prd, so key on PRD_PATH).
22421
+ _loki_is_quick_prd "${PRD_PATH:-}" && usage_doc_instruction=""
22422
+
22429
22423
  # DOC_SCOPE instruction (F52): scale generated documentation to the detected
22430
22424
  # project complexity. A trivial one-file app does not warrant a nine-file
22431
22425
  # architecture suite (ARCHITECTURE/COMPONENTS/DECISIONS/API/SETUP/TESTING) --
@@ -23625,7 +23619,7 @@ populate_prd_queue() {
23625
23619
  # Prefer the original project PRD over generated quick-prd.md
23626
23620
  # quick-prd.md contains boilerplate that produces garbage tasks
23627
23621
  local effective_prd="$prd_file"
23628
- if [[ "$prd_file" == *"quick-prd.md" ]] || [[ "$prd_file" == *"chat-prd.md" ]]; then
23622
+ if [[ "$prd_file" == *"quick-prd.md" ]] || _loki_is_quick_prd "$prd_file" || [[ "$prd_file" == *"chat-prd.md" ]]; then
23629
23623
  # Look for the real PRD in the project root
23630
23624
  for candidate in "PRD.md" "prd.md" "requirements.md" "REQUIREMENTS.md" "spec.md" "SPEC.md"; do
23631
23625
  if [[ -f "$candidate" ]]; then
@@ -23999,7 +23993,8 @@ else:
23999
23993
  with open(pending_path, "w") as f:
24000
23994
  json.dump(output, f, indent=2)
24001
23995
 
24002
- print(f"Extracted {added} tasks from PRD ({len(features)} features found)", file=sys.stderr)
23996
+ if not os.environ.get("LOKI_QUICK_INNER"):
23997
+ print(f"Extracted {added} tasks from PRD ({len(features)} features found)", file=sys.stderr)
24003
23998
  PRD_PARSE_EOF
24004
23999
 
24005
24000
  if [[ $? -ne 0 ]]; then
@@ -27753,7 +27748,7 @@ reap_own_process_group() {
27753
27748
  # Collect protected pids (dashboard, app-runner, registered children) so the
27754
27749
  # reap never takes down the shared dashboard if it happens to share our
27755
27750
  # group. Mirrors the `loki stop` / dashboard reaper protection set.
27756
- local _protected=" $$ "
27751
+ local _protected=" $$ ${LOKI_QUICK_OUTER_PID:-} " # A-134: the quiet-mode outer shell shares our group
27757
27752
  local _pf _p
27758
27753
  if [ -d "$loki_dir/pids" ]; then
27759
27754
  for _pf in "$loki_dir/pids"/*.json; do
@@ -29200,27 +29195,13 @@ main() {
29200
29195
  # learnings writers, proof generation, metrics aggregation -- runs here
29201
29196
  # and was never timed. Opened immediately so nothing below is missed.
29202
29197
  _teardown_t0=$(date +%s 2>/dev/null)
29203
- # PRE-EDIT SNAPSHOT: freeze the agent's raw diff HERE, the first
29204
- # instruction after the loop returns, because everything below this line
29205
- # can change the tree -- commit_session_changes commits the work (after
29206
- # which `git diff HEAD` is empty), and HANDOFF.md/learnings writers touch
29207
- # files before that. The snapshot is write-once, so capturing it late
29208
- # would permanently record someone else's edits as the agent's. Runs in
29209
- # the FOREGROUND on purpose: the entire value of this position is that
29210
- # the capture COMPLETES before any mutation, and backgrounding it would
29211
- # reintroduce exactly the race the placement exists to remove (the
29212
- # module bounds each git call at 60s, so the cost is bounded).
29198
+ # PRE-EDIT SNAPSHOT: freeze the agent's raw diff HERE, the first instruction
29199
+ # after the loop: commit, HANDOFF and learnings writers below all change the
29200
+ # tree, and the snapshot is write-once. Foreground on purpose (each git call
29201
+ # is bounded at 60s) so the capture completes before any mutation.
29213
29202
  capture_preedit_snapshot || true
29214
- # ZOMBIE-RECEIPT GUARD: proof generation + the COMPLETED marker live in the
29215
- # teardown far below. If the process is killed (Docker restart, OOM, worker
29216
- # reap) between here and there, a genuinely finished build (real code, exit
29217
- # 0) leaves NO proof.json and STATUS stuck "BUILDING" -- the build "worked"
29218
- # but produced no Evidence Receipt (observed on run-20260716194328). Emit
29219
- # the proof HERE too, right after the loop returns, so the receipt survives
29220
- # a late teardown death. Idempotent + fire-and-forget: the teardown's own
29221
- # generate_proof_of_run re-runs harmlessly (same run_id -> same proof dir),
29222
- # and LOKI_PROOF=0 still opts out. This closes the "nothing gets verified"
29223
- # gap at its highest-value point without touching run_autonomous itself.
29203
+ # ZOMBIE-RECEIPT GUARD: emit the proof here too, so a teardown killed below
29204
+ # (Docker restart, OOM) still leaves a receipt. Idempotent; LOKI_PROOF=0 opts out.
29224
29205
  if [ "${LOKI_PROOF:-1}" != "0" ] && type generate_proof_of_run &>/dev/null; then
29225
29206
  generate_proof_of_run "$result" || true
29226
29207
  fi
@@ -29344,6 +29325,11 @@ main() {
29344
29325
  # move, so a partial write never leaves a truncated HANDOFF.md.
29345
29326
  local _handoff_dir _handoff_md _handoff_tmp
29346
29327
  _handoff_dir="${TARGET_DIR:-.}"
29328
+ # A-132: a quick fix keeps HANDOFF.md under .loki/, out of the repo root.
29329
+ if _loki_is_quick_prd "${PRD_PATH:-}"; then
29330
+ _handoff_dir="${LOKI_DIR:-${TARGET_DIR:-.}/.loki}"
29331
+ mkdir -p "$_handoff_dir" 2>/dev/null || true
29332
+ fi
29347
29333
  _handoff_md="$_handoff_dir/HANDOFF.md"
29348
29334
  _handoff_tmp="$_handoff_dir/.HANDOFF.md.tmp"
29349
29335
  if python3 "$_own_render" --loki-dir "${LOKI_DIR:-${TARGET_DIR:-.}/.loki}" --md > "$_handoff_tmp" 2>/dev/null; then
@@ -29367,13 +29353,6 @@ main() {
29367
29353
  # then advise the user how to open a PR. Both are no-ops when no agent branch
29368
29354
  # was set up (LOKI_BRANCH_PROTECTION=false) or nothing changed.
29369
29355
  commit_session_changes
29370
- # Trusted post-session step. NOT wrapped in _loki_with_github_tokens as a
29371
- # whole (round 5): that ran every git call inside it (rev-list, merge-base,
29372
- # push) with the real credentials AND the agent's repo config loaded. Its
29373
- # push (_loki_trusted_push) and gh calls (gh wrapper) each re-grant the
29374
- # credentials themselves, outside the agent's repo.
29375
- create_session_pr
29376
- audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
29377
29356
 
29378
29357
  # The first terminal summary is written before session changes are committed.
29379
29358
  # Refresh its durable files against the final HEAD without notifying twice.
@@ -29391,35 +29370,23 @@ except Exception:
29391
29370
  build_completion_summary "$_completion_outcome" || true
29392
29371
  fi
29393
29372
 
29394
- # Final source-tree binding for server-owned terminal state. HANDOFF.md and
29395
- # commit_session_changes can change the worktree after the earlier receipt.
29396
- # Regenerate idempotently only after those writers finish, before cleanup,
29397
- # so proof.tree_sha256 describes the exact tree the runner returns.
29373
+ # Final source-tree binding, AFTER HANDOFF.md and commit_session_changes (A-134),
29374
+ # so Head and diff sha256 describe the tree the runner returns. Every receipt
29375
+ # print follows; create_session_pr never changes HEAD or the tree.
29398
29376
  if [ "${LOKI_PROOF:-1}" != "0" ]; then
29399
29377
  generate_proof_of_run "$result" || true
29400
29378
  fi
29379
+ [ -z "${LOKI_QUICK_INNER:-}" ] || _loki_quick_receipt_write
29380
+ # Trusted post-session step. NOT wrapped in _loki_with_github_tokens as a
29381
+ # whole (round 5): its push (_loki_trusted_push) and gh calls each re-grant
29382
+ # the credentials themselves, outside the agent's repo.
29383
+ create_session_pr
29384
+ audit_agent_action "session_stop" "Session ended" "result=$result,iterations=$ITERATION_COUNT"
29401
29385
 
29402
- # Evidence Receipt (#209): tell the user, on screen, that a checkable
29403
- # receipt exists and how to re-check it.
29404
- #
29405
- # POSITION IS LOAD-BEARING. This sits immediately after the FINAL
29406
- # generate_proof_of_run, which is the only point where the receipt for THIS
29407
- # run is guaranteed written and .loki/state/last-proof-id.txt is guaranteed
29408
- # to point at it. Every earlier surface is either too early (the completion
29409
- # card renders from inside run_autonomous, long before any proof exists) or
29410
- # unreachable to a foreground user (COMPLETION.txt self-heals here, but only
29411
- # a --bg launch is ever told to read it). Before #211 the pointer was never
29412
- # cleared at run start, so announcing from an earlier site printed the
29413
- # PREVIOUS run's receipt and verdict on a second run in the same directory.
29414
- # Run init now clears it (search "Same reasoning for the proof pointer"), so
29415
- # a stale pointer no longer survives into a new run; the position still
29416
- # matters because an earlier site is simply too early for THIS run's proof.
29417
- #
29418
- # TTY-gated the same way print_ttfv_next_steps is above: machine output and
29419
- # --bg stay byte-identical, and those readers already get the same facts
29420
- # from COMPLETION.txt. Fail-silent and best-effort: prints nothing at all
29421
- # when no receipt was written (LOKI_PROOF=0, or generation failed), and
29422
- # never fails the run.
29386
+ # Evidence Receipt (#209): tell the user a checkable receipt exists. POSITION IS
29387
+ # LOAD-BEARING: only after the final proof above does last-proof-id.txt point at
29388
+ # THIS run's receipt (earlier sites print the previous run's). TTY-only and
29389
+ # fail-silent; --bg and machine output read the same facts from COMPLETION.txt.
29423
29390
  if [ -t 1 ] && [ "${BACKGROUND_MODE:-false}" != "true" ]; then
29424
29391
  _rcpt="$(_loki_receipt_facts "${TARGET_DIR:-.}/.loki" 2>/dev/null || true)"
29425
29392
  if [ -n "${_rcpt:-}" ]; then
@@ -71,7 +71,7 @@ except ImportError: # pragma: no cover - only on a stripped install
71
71
  def build_jwks(private_key=None, retired_public_keys=None):
72
72
  return {"keys": []}
73
73
 
74
- def load_signing_key():
74
+ def load_signing_key(auto_generate=True):
75
75
  return None, ""
76
76
 
77
77
  def load_retired_public_keys():
@@ -1360,7 +1360,7 @@ def main():
1360
1360
  WebhookHandler.api_token = api_token
1361
1361
  WebhookHandler.dispatcher = dispatcher
1362
1362
 
1363
- signing_key, signing_kid = load_signing_key()
1363
+ signing_key, signing_kid = load_signing_key(auto_generate=False)
1364
1364
  WebhookHandler.signing_key = signing_key
1365
1365
  WebhookHandler.signing_kid = signing_kid
1366
1366
  WebhookHandler.retired_pubkeys = tuple(load_retired_public_keys())
@@ -3048,6 +3048,36 @@ PYEOF
3048
3048
  # ---------------------------------------------------------------------------
3049
3049
  # Entry point
3050
3050
  # ---------------------------------------------------------------------------
3051
+ # A-134: when this tree holds a legacy Evidence Receipt, report its integrity digest
3052
+ # (proof-verify.py's own canonical re-hash, no second canonicalizer). Silent otherwise.
3053
+ _verify_receipt_digest() {
3054
+ local rid pj lib rc=0
3055
+ rid="$(cat .loki/state/last-proof-id.txt 2>/dev/null || true)"
3056
+ case "$rid" in '' | *[!A-Za-z0-9._-]*) return 0 ;; esac
3057
+ pj=".loki/proofs/$rid/proof.json"
3058
+ [ -f "$pj" ] || return 0
3059
+ lib="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib"
3060
+ python3 -E -c "import sys; sys.path[:] = [p for p in sys.path if p not in ('', '.')]
3061
+ import importlib.util, json
3062
+ sys.path.insert(0, sys.argv[1])
3063
+ sp = importlib.util.spec_from_file_location('pv', sys.argv[1] + '/proof-verify.py')
3064
+ m = importlib.util.module_from_spec(sp); sp.loader.exec_module(m)
3065
+ p = json.load(open(sys.argv[2]))
3066
+ ok = m.verify_integrity(p)['hash_ok']
3067
+ h = (p.get('verification') or {}).get('hash') or ''
3068
+ print('receipt_sha256: ' + h if ok else 'receipt: TAMPERED (integrity hash does not match proof.json)' if h else 'receipt: NOT CHECKABLE (no hash recorded)')
3069
+ sys.exit(0 if ok else 3)" "$lib" "$pj" 2>/dev/null || rc=$?
3070
+ [ "$rc" -ne 3 ] || return 1
3071
+ # Provenance: the deploy gate's own verdict (Ed25519 attestation against the local
3072
+ # key plus LOKI_RECEIPT_RETIRED_PUBKEYS; unknown kid or no key is UNCHECKED).
3073
+ # Defined in autonomy/loki, so it is absent when verify.sh runs standalone.
3074
+ declare -f _deploy_receipt_verdict >/dev/null 2>&1 || return 0
3075
+ local att
3076
+ att="$(_deploy_receipt_verdict "$pj")"
3077
+ printf 'attestation: %s\n' "$att"
3078
+ [ "$att" != "TAMPERED" ]
3079
+ }
3080
+
3051
3081
  verify_main() {
3052
3082
  local base_ref=""
3053
3083
  local out_dir=".loki/verify"
@@ -3247,6 +3277,13 @@ verify_main() {
3247
3277
 
3248
3278
  verify_compute_verdict "$block_on"
3249
3279
 
3280
+ # A-134: a tampered receipt turns the verdict BLOCKED BEFORE evidence.json is
3281
+ # written, so no artifact ever records VERIFIED for it. Under --json the human
3282
+ # lines go to stderr.
3283
+ _v_banner_fd=1
3284
+ [ "${VERIFY_JSON:-0}" = "1" ] && _v_banner_fd=2
3285
+ _verify_receipt_digest >&$_v_banner_fd || { VERIFY_VERDICT=BLOCKED; VERIFY_EXIT=$VERIFY_EXIT_BLOCKED; }
3286
+
3250
3287
  completed_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
3251
3288
 
3252
3289
  # stdout is discarded (the emitter's own chatter is not wanted on the human
@@ -3284,8 +3321,6 @@ verify_main() {
3284
3321
  # Under --json stdout carries the evidence document alone, so the human
3285
3322
  # banner is redirected to stderr rather than dropped: an operator watching a
3286
3323
  # terminal still sees the verdict, and `| jq` still parses.
3287
- _v_banner_fd=1
3288
- [ "${VERIFY_JSON:-0}" = "1" ] && _v_banner_fd=2
3289
3324
  printf 'VERDICT: %s\n' "$VERIFY_VERDICT" >&$_v_banner_fd
3290
3325
  printf 'Evidence: %s/evidence.json\n' "$out_dir" >&$_v_banner_fd
3291
3326
  printf 'Report: %s/report.md\n' "$out_dir" >&$_v_banner_fd
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "10.5.11"
10
+ __version__ = "10.5.13"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -12574,8 +12574,8 @@ async def proofs_summary():
12574
12574
  But on the UNSIGNED path the generator is TRUSTED -- a forger who rewrites
12575
12575
  both the facts and the headline to a mutually consistent lie and recomputes
12576
12576
  the hash still buckets as verified here. Neutral, adversarial non-forgeability
12577
- (the generator is not trusted) requires the SIGNED record (a gpg signature
12578
- proof-verify.py checks). This endpoint reports what the generator recorded;
12577
+ (the generator is not trusted) requires the SIGNED record (an Ed25519
12578
+ attestation checked against a key set). This endpoint reports what the generator recorded;
12579
12579
  it does not itself re-verify. Buckets:
12580
12580
 
12581
12581
  verified -> honesty.headline == "VERIFIED"
@@ -264,10 +264,9 @@ fixed at the same time.
264
264
  already states signature status explicitly in both directions
265
265
  (`proof-generator.py:1808-1820`): SIGNED, or UNSIGNED with the honest line that
266
266
  the integrity hash "does NOT prove who produced them, so this receipt trusts
267
- its generator." That is the shippable version. Do **not** flip
268
- `LOKI_PROOF_GPG_KEY` to default-on: with no key present it would either fail
269
- the run or silently emit no signature, and the second is the false-green this
270
- project exists to prevent. Key distribution is a founder decision.
267
+ its generator." That is the shippable version. Signing is now default-on through an
268
+ auto-generated local Ed25519 key (A-120); the old gpg switch was deleted.
269
+ Key distribution remains a founder decision.
271
270
  - **OTEL to a customer-owned collector.** Already implemented -- `otel_endpoint`
272
271
  is persisted and read (`autonomy/loki:26755`, `:26828-26837`).
273
272
  - **A headless exec contract with documented exit codes.** Already exists via
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v10.5.11
5
+ **Version:** v10.5.13
6
6
 
7
7
  ---
8
8