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
package/README.md CHANGED
@@ -70,9 +70,16 @@ Choose an exact shipped starter when the top-ranked match is not the one you
70
70
  want:
71
71
 
72
72
  ```bash
73
+ loki quickstart --list-templates
74
+ loki quickstart --list-templates --json # schema-v1 automation output
73
75
  loki quickstart "an internal reporting workspace" --template dashboard --yes
74
76
  ```
75
77
 
78
+ Template discovery works without a terminal or provider and lists every shipped
79
+ starter's stable name and purpose in catalog order. It returns before estimation,
80
+ consent, PRD writes, or build execution. Positional input and execution/preview
81
+ flags are intentionally incompatible; `--json` is the only optional modifier.
82
+
76
83
  `--template` accepts an exact template name for idea inputs and works the same
77
84
  way with interactive use or `--dry-run` (including JSON preview). Unknown
78
85
  templates, duplicate flags, and combinations with a PRD path refuse before
@@ -93,13 +100,20 @@ For scripts and local dashboards, add `--json` to receive one versioned JSON
93
100
  object instead of terminal text:
94
101
 
95
102
  ```bash
96
- loki quickstart "a todo app with user accounts" --dry-run --json
103
+ loki quickstart "a todo app with user accounts" --dry-run --json > preview.json
104
+ loki quickstart --from-preview preview.json --yes
97
105
  ```
98
106
 
99
107
  The object contains the input kind, deterministic selected template (or `null`
100
- for an existing PRD), and the exact estimator response under `plan`. `--json`
101
- requires `--dry-run`; invalid input or estimator failure writes no JSON, and the
102
- command still exits before provider discovery, file writes, or build execution.
108
+ for an existing PRD), the exact estimator response under `plan`, and a bounded
109
+ continuation containing the exact idea/template or the PRD path and SHA-256.
110
+ `--from-preview` requires explicit argv `--yes`, rejects malformed, conflicting,
111
+ symlinked, or changed inputs before provider and build boundaries, then uses the
112
+ existing no-clobber quickstart path. The saved plan is evidence rather than
113
+ execution authority: Loki recomputes and displays the current estimate before
114
+ starting. `--json` requires `--dry-run`; invalid input or estimator failure
115
+ writes no JSON, and preview still exits before provider discovery, file writes,
116
+ or build execution.
103
117
 
104
118
  Or go straight at it:
105
119
 
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v9.22.4
6
+ # Loki Mode v9.22.6
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.22.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.22.6 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.22.4
1
+ 9.22.6
@@ -247,6 +247,110 @@ extract_type_from_labels() {
247
247
  #===============================================================================
248
248
 
249
249
  # Parse a GitHub issue and output structured data
250
+ #===============================================================================
251
+ # Golden path: issue context + early plan
252
+ #===============================================================================
253
+
254
+ # Split the extracted acceptance-criteria blob into one normalized item per
255
+ # line. Keeps ONLY genuine list items (checkbox / bullet / numbered) so a
256
+ # section heading or a stray prose line never becomes a fake "criterion".
257
+ # Prints nothing when there is nothing to print -- an empty criteria list is a
258
+ # truthful answer, and inventing one would poison every downstream coverage
259
+ # claim.
260
+ _gp_criteria_lines() {
261
+ printf '%s\n' "${1:-}" \
262
+ | sed -E 's/^[[:space:]]*[-*][[:space:]]*\[[ xX]\][[:space:]]*//; s/^[[:space:]]*[-*][[:space:]]+//; s/^[[:space:]]*[0-9]+[.)][[:space:]]+//' \
263
+ | grep -vE '^[[:space:]]*$' \
264
+ | grep -vE '^[[:space:]]*-{3,}[[:space:]]*$'
265
+ }
266
+
267
+ # write_journey_context <owner> <repo> <number> <title> <url> <acceptance>
268
+ # <files> <type> <priority>
269
+ #
270
+ # Writes .loki/state/issue-context.json (feature 2: the exact acceptance context)
271
+ # and .loki/state/journey-plan.json (feature 3: the early machine-readable
272
+ # status/plan). Both are written from `gh` output BEFORE the first provider
273
+ # call, so the early artifact is deterministic rather than latency-dependent.
274
+ #
275
+ # Never fails the caller: every write is best-effort and the function always
276
+ # returns 0. Honors LOKI_DIR so a test can point it at a scratch directory.
277
+ write_journey_context() {
278
+ local owner="$1" repo="$2" number="$3" title="$4" url="$5"
279
+ local acceptance="$6" files="$7" issue_type="$8" priority="$9"
280
+
281
+ local state_dir="${LOKI_DIR:-.loki}/state"
282
+ mkdir -p "$state_dir" 2>/dev/null || return 0
283
+
284
+ local criteria
285
+ criteria=$(_gp_criteria_lines "$acceptance")
286
+
287
+ # jq builds both documents so quoting/escaping is handled once, correctly.
288
+ command -v jq >/dev/null 2>&1 || return 0
289
+
290
+ local ctx_tmp="$state_dir/.issue-context.$$.json"
291
+ if jq -n \
292
+ --arg owner "$owner" --arg repo "$repo" --arg number "$number" \
293
+ --arg title "$title" --arg url "$url" --arg criteria "$criteria" \
294
+ --arg files "$files" --arg type "$issue_type" --arg priority "$priority" \
295
+ --arg captured_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
296
+ '{
297
+ schema_version: "1.0",
298
+ captured_at: $captured_at,
299
+ issue: {
300
+ owner: $owner, repo: $repo,
301
+ number: (try ($number | tonumber) catch null),
302
+ ref: ($owner + "/" + $repo + "#" + $number),
303
+ url: $url, title: $title,
304
+ type: $type, priority: $priority
305
+ },
306
+ acceptance_criteria: (
307
+ if ($criteria | length) == 0 then []
308
+ else ($criteria | split("\n") | map(select(length > 0)))
309
+ end
310
+ ),
311
+ file_references: (
312
+ if ($files | length) == 0 then []
313
+ else ($files | split("\n") | map(select(length > 0)))
314
+ end
315
+ )
316
+ }' > "$ctx_tmp" 2>/dev/null; then
317
+ mv -f "$ctx_tmp" "$state_dir/issue-context.json" 2>/dev/null || rm -f "$ctx_tmp"
318
+ else
319
+ rm -f "$ctx_tmp" 2>/dev/null || true
320
+ fi
321
+
322
+ # The early plan is a PLAN, not a result: it states what was understood and
323
+ # what will be attempted. It deliberately carries no outcome fields -- a
324
+ # status file that guessed at outcomes before any work happened would be the
325
+ # exact fake-green this repo's trust core forbids.
326
+ local plan_tmp="$state_dir/.journey-plan.$$.json"
327
+ if jq -n \
328
+ --arg ref "$owner/$repo#$number" --arg title "$title" --arg url "$url" \
329
+ --arg criteria "$criteria" \
330
+ --arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
331
+ '{
332
+ schema_version: "1.0",
333
+ command: "loki start",
334
+ mode: "issue",
335
+ stage: "planned",
336
+ generated_at: $generated_at,
337
+ issue: { ref: $ref, title: $title, url: $url },
338
+ acceptance_criteria: (
339
+ if ($criteria | length) == 0 then []
340
+ else ($criteria | split("\n") | map(select(length > 0)))
341
+ end
342
+ ),
343
+ provider_invoked: false,
344
+ next: "implement, verify, then review-ready result with evidence receipt"
345
+ }' > "$plan_tmp" 2>/dev/null; then
346
+ mv -f "$plan_tmp" "$state_dir/journey-plan.json" 2>/dev/null || rm -f "$plan_tmp"
347
+ else
348
+ rm -f "$plan_tmp" 2>/dev/null || true
349
+ fi
350
+
351
+ return 0
352
+ }
353
+
250
354
  parse_github_issue() {
251
355
  local issue_ref="$1"
252
356
  local output_format="${2:-yaml}" # yaml or json
@@ -303,6 +407,14 @@ parse_github_issue() {
303
407
  priority=$(extract_priority_from_labels "$labels_json")
304
408
  issue_type=$(extract_type_from_labels "$labels_json")
305
409
 
410
+ # Golden path (feature 2): persist the EXACT criteria we just extracted, plus
411
+ # the early machine-readable plan (feature 3), before any provider call. Both
412
+ # are derived from `gh` output only, so the early artifact does not depend on
413
+ # provider latency at all. Best-effort: a failure here must never break issue
414
+ # parsing, which is why it is guarded and ignores its own exit status.
415
+ write_journey_context "$owner" "$repo" "$number" "$title" "$url" \
416
+ "$acceptance_criteria" "$file_references" "$issue_type" "$priority" || true
417
+
306
418
  # Output based on format
307
419
  if [ "$output_format" = "json" ]; then
308
420
  output_json "$owner" "$repo" "$number" "$title" "$body" "$problem_statement" \
@@ -990,6 +990,78 @@ def _collect_iterations(loki_dir):
990
990
  return {"count": count, "succeeded": n_completed, "failed": n_failed}
991
991
 
992
992
 
993
+ def _collect_journey(loki_dir):
994
+ """Issue-to-PR journey facts (golden path features 4 and 10).
995
+
996
+ Descriptive record half ONLY. Like `functional` and `healthcheck`, this is
997
+ NOT read by _compute_headline / _compute_degraded: nothing here can turn a
998
+ receipt green or make a blocked run look finished.
999
+
1000
+ Every field is absent-not-zero. A measurement that did not happen renders
1001
+ nothing rather than a fabricated 0, matching run.sh:4258 ("a wrong timing
1002
+ table is worse than silence"). Returns {} when this run was not an
1003
+ issue-mode run, so a PRD/brief run's proof bytes are unchanged.
1004
+ """
1005
+ state = os.path.join(loki_dir, "state")
1006
+ ctx = _read_json(os.path.join(state, "issue-context.json"), default=None)
1007
+ if not isinstance(ctx, dict):
1008
+ return {}
1009
+
1010
+ issue = ctx.get("issue") if isinstance(ctx.get("issue"), dict) else {}
1011
+ stated = ctx.get("acceptance_criteria")
1012
+ stated = [str(c) for c in stated] if isinstance(stated, list) else []
1013
+
1014
+ out = {
1015
+ "issue": {
1016
+ "ref": str(issue.get("ref") or ""),
1017
+ "url": str(issue.get("url") or ""),
1018
+ "title": str(issue.get("title") or ""),
1019
+ },
1020
+ # Coverage is a CORRESPONDENCE, not a verdict. There is no deterministic
1021
+ # checker for free-text criteria, so we report what the issue stated and
1022
+ # decline to claim any of it was satisfied. `addressed` is deliberately
1023
+ # null rather than 0: 0 would read as "none met", which we do not know.
1024
+ "acceptance": {
1025
+ "stated": stated,
1026
+ "stated_count": len(stated),
1027
+ "addressed_count": None,
1028
+ "basis": "criteria imported verbatim from the issue; no "
1029
+ "deterministic checker exists for free-text criteria, so "
1030
+ "this receipt reports what was ASKED, never what was met",
1031
+ },
1032
+ }
1033
+
1034
+ # Time to first useful result. Reuses the number run.sh already writes; we
1035
+ # do not add a second writer and we never synthesize the value.
1036
+ fa = _read_json(os.path.join(state, "first-artifact.json"), default=None)
1037
+ if isinstance(fa, dict):
1038
+ v = fa.get("seconds_to_first_artifact")
1039
+ if isinstance(v, (int, float)) and v >= 0:
1040
+ out["time_to_first_result_sec"] = int(v)
1041
+
1042
+ # Human interventions. Fills the socket trust_trajectory.py:145 already
1043
+ # reads and documents as "no per-run counter persisted today". Absent file
1044
+ # means unmeasured, NOT zero -- claiming a confident 0 would overstate
1045
+ # autonomy, which is the one direction this number must never err in.
1046
+ iv = _read_json(os.path.join(state, "interventions.json"), default=None)
1047
+ if isinstance(iv, dict):
1048
+ n = iv.get("count")
1049
+ if isinstance(n, int) and n >= 0:
1050
+ out["interventions"] = n
1051
+
1052
+ # PR state/URL, written by the consent-gated PR step. "prepared" means a PR
1053
+ # body exists locally and GitHub was NOT mutated.
1054
+ pr = _read_json(os.path.join(state, "pr.json"), default=None)
1055
+ if isinstance(pr, dict) and pr.get("state"):
1056
+ out["pull_request"] = {
1057
+ "state": str(pr.get("state")),
1058
+ "url": str(pr.get("url") or ""),
1059
+ "branch": str(pr.get("branch") or ""),
1060
+ }
1061
+
1062
+ return out
1063
+
1064
+
993
1065
  def _collect_spec(loki_dir, target_dir):
994
1066
  """Return spec dict {source, brief}. brief truncated to 600 chars."""
995
1067
  prd_path = os.environ.get("PRD_PATH", "").strip()
@@ -1158,6 +1230,7 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1158
1230
  healthcheck = _collect_healthcheck(loki_dir) # Evidence Receipt record-half
1159
1231
  evidence_gate = _collect_evidence_gate(loki_dir)
1160
1232
  functionality = _collect_functionality(loki_dir) # func axes as HONEST facts
1233
+ journey = _collect_journey(loki_dir) # issue-to-PR record half; {} when N/A
1161
1234
 
1162
1235
  deployed_url = os.environ.get("LOKI_DEPLOYED_URL") or None
1163
1236
 
@@ -1238,6 +1311,12 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1238
1311
  },
1239
1312
  }
1240
1313
 
1314
+ # Issue-to-PR journey facts, attached ONLY on an issue-mode run so a PRD or
1315
+ # brief run's proof bytes stay exactly as they were. Record half: descriptive
1316
+ # only, never read by _compute_headline / _compute_degraded.
1317
+ if journey:
1318
+ facts["journey"] = journey
1319
+
1241
1320
  # ASSESSMENTS: LLM opinions. Explicitly labeled as judgment, NOT proof. A
1242
1321
  # green council verdict is an opinion that can be wrong or gamed; it never
1243
1322
  # contributes to the deterministic headline.
@@ -1351,6 +1430,15 @@ def _build_proof(args, loki_dir, target_dir, repo_root):
1351
1430
  "assessments": assessments,
1352
1431
  "honesty": honesty,
1353
1432
  }
1433
+
1434
+ # Top-level mirror for the intervention axis. trust_trajectory.py:145 already
1435
+ # reads proof["interventions"] and documents that no writer exists yet; this
1436
+ # is that writer. Mirrored (not moved) for the same back-compat reason the
1437
+ # other flat keys are mirrored. Only ever set when actually measured, so the
1438
+ # axis stays honestly "unavailable" rather than showing a fabricated zero.
1439
+ if isinstance(journey, dict) and isinstance(journey.get("interventions"), int):
1440
+ proof["interventions"] = journey["interventions"]
1441
+
1354
1442
  return proof, run_id
1355
1443
 
1356
1444
 
@@ -206,6 +206,58 @@ def main():
206
206
  _line("| Gates disabled | " + ", ".join(sorted(_off)) + " |")
207
207
  _line("| Base sha | `" + (base_sha or "(none)") + "` |")
208
208
  _line("| Head sha | `" + (head_sha or "(none)") + "` |")
209
+
210
+ # Issue-to-PR journey rows. Emitted ONLY on an issue-mode run, and each row
211
+ # ONLY when that fact was actually measured -- an unmeasured number renders
212
+ # no row rather than a zero, the same rule the rest of this receipt follows.
213
+ journey = facts.get("journey") if isinstance(facts.get("journey"), dict) else {}
214
+ if journey:
215
+ jissue = journey.get("issue") if isinstance(journey.get("issue"), dict) else {}
216
+ jref = str(jissue.get("ref") or "").strip()
217
+ jurl = str(jissue.get("url") or "").strip()
218
+ if jref:
219
+ _line("| Issue | " + (("[" + jref + "](" + jurl + ")") if jurl else jref) + " |")
220
+
221
+ ttfr = journey.get("time_to_first_result_sec")
222
+ if isinstance(ttfr, int):
223
+ _line("| Time to first result | " + str(ttfr) + "s |")
224
+
225
+ ivs = journey.get("interventions")
226
+ if isinstance(ivs, int):
227
+ _line("| Human interventions | " + str(ivs) + " |")
228
+
229
+ acc = journey.get("acceptance") if isinstance(journey.get("acceptance"), dict) else {}
230
+ n_stated = acc.get("stated_count")
231
+ if isinstance(n_stated, int):
232
+ # Reported as ASKED, never as met. addressed_count is null by design
233
+ # (no deterministic checker for free-text criteria), so this row must
234
+ # never be phrased as coverage or a pass -- that would be the exact
235
+ # fake-green the headline rules forbid.
236
+ _line("| Acceptance criteria | " + str(n_stated)
237
+ + " stated in the issue (not machine-verified) |")
238
+
239
+ pr = journey.get("pull_request") if isinstance(journey.get("pull_request"), dict) else {}
240
+ pr_state = str(pr.get("state") or "").strip()
241
+ if pr_state:
242
+ pr_url = str(pr.get("url") or "").strip()
243
+ _line("| Pull request | " + pr_state + ((" -- " + pr_url) if pr_url else "") + " |")
244
+
245
+ # Rollback: derived from the base sha this run started at, so the reader
246
+ # gets a runnable command rather than a promise. Only emitted when the
247
+ # base is actually known.
248
+ if base_sha:
249
+ _line("| Rollback | `git reset --hard " + base_sha + "` |")
250
+
251
+ # Uncertainty: the honest count of checks that did NOT conclusively pass,
252
+ # read from the SAME honesty.degraded[] the headline is computed from, so
253
+ # it can never disagree with the verdict above it.
254
+ _degraded = honesty.get("degraded")
255
+ _degraded = _degraded if isinstance(_degraded, list) else []
256
+ if effective_headline == "VERIFIED" and not _degraded:
257
+ _line("| Uncertainty | none recorded; every check that ran concluded |")
258
+ elif _degraded:
259
+ _line("| Uncertainty | " + str(len(_degraded))
260
+ + " check(s) not conclusive -- see the list below |")
209
261
  _line()
210
262
 
211
263
  # Gaps: when headline != VERIFIED, list honesty.degraded[] verbatim. By
package/autonomy/loki CHANGED
@@ -2583,6 +2583,14 @@ cmd_start() {
2583
2583
  issue_mode_args+=("--pr")
2584
2584
  shift
2585
2585
  ;;
2586
+ --prepare-pr)
2587
+ # Feature 1: the SAME `loki start <issue>` entrypoint carries the
2588
+ # prepare-only flag through to cmd_run. Does not set
2589
+ # issue_create_pr -- preparing is not publishing.
2590
+ issue_use_worktree=true
2591
+ issue_mode_args+=("--prepare-pr")
2592
+ shift
2593
+ ;;
2586
2594
  --ship)
2587
2595
  issue_use_worktree=true
2588
2596
  issue_create_pr=true
@@ -9734,6 +9742,7 @@ cmd_run() {
9734
9742
  local provider_override=""
9735
9743
  local use_worktree=false
9736
9744
  local create_pr=false
9745
+ local prepare_pr=false
9737
9746
  local auto_merge=false
9738
9747
  local run_detached=false
9739
9748
 
@@ -9772,6 +9781,8 @@ cmd_run() {
9772
9781
  echo "Progressive Isolation:"
9773
9782
  echo " --worktree, -w Git worktree isolation (separate branch)"
9774
9783
  echo " --pr Worktree + auto-create PR (implies --worktree)"
9784
+ echo " --prepare-pr Worktree + write a review-ready PR title/body"
9785
+ echo " locally; never contacts GitHub (implies --worktree)"
9775
9786
  echo " --ship Worktree + PR + auto-merge (implies --pr)"
9776
9787
  echo " --detach, -d Run in background (implies --worktree)"
9777
9788
  echo ""
@@ -9888,6 +9899,15 @@ cmd_run() {
9888
9899
  create_pr=true
9889
9900
  shift
9890
9901
  ;;
9902
+ --prepare-pr)
9903
+ # Golden path (feature 9): produce a review-ready PR WITHOUT any
9904
+ # GitHub mutation. Writes the title/body to .loki/state/ and
9905
+ # records state="prepared" for the receipt. Deliberately does not
9906
+ # set create_pr: consent to prepare is not consent to publish.
9907
+ use_worktree=true
9908
+ prepare_pr=true
9909
+ shift
9910
+ ;;
9891
9911
  --ship)
9892
9912
  use_worktree=true
9893
9913
  create_pr=true
@@ -9971,6 +9991,21 @@ cmd_run() {
9971
9991
  fi
9972
9992
  source "$issue_providers_script"
9973
9993
 
9994
+ # Golden path: also source issue-parser.sh for write_journey_context and
9995
+ # extract_acceptance_criteria. Optional -- a missing file must never break a
9996
+ # run, so this is guarded rather than fatal like the block above.
9997
+ #
9998
+ # issue-parser.sh sets `set -euo pipefail` at the top, which would otherwise
9999
+ # LEAK into cmd_run and abort the CLI on the first benign non-zero. We
10000
+ # snapshot the current options and restore them immediately after sourcing.
10001
+ local _gp_parser_script="$SKILL_DIR/autonomy/issue-parser.sh"
10002
+ if [[ -f "$_gp_parser_script" ]]; then
10003
+ local _gp_saved_opts
10004
+ _gp_saved_opts=$(set +o)
10005
+ source "$_gp_parser_script" 2>/dev/null || true
10006
+ eval "$_gp_saved_opts" 2>/dev/null || true
10007
+ fi
10008
+
9974
10009
  # Detect provider
9975
10010
  local issue_provider
9976
10011
  issue_provider=$(detect_issue_provider "$issue_ref")
@@ -9996,6 +10031,38 @@ cmd_run() {
9996
10031
  fi
9997
10032
  echo ""
9998
10033
 
10034
+ # Golden path (features 2 + 3): capture the EXACT acceptance context and the
10035
+ # early machine-readable plan here, at the real `loki start <issue>`
10036
+ # chokepoint, BEFORE any provider call.
10037
+ #
10038
+ # This is deliberately hooked to fetch_issue rather than to
10039
+ # issue-parser.sh's parse_github_issue: `loki start` routes through
10040
+ # fetch_issue (issue-providers.sh:333), while parse_github_issue serves only
10041
+ # the DEPRECATED `loki issue parse|view` commands. Hooking the parser alone
10042
+ # would leave the required path with no context written at all. fetch_issue
10043
+ # is also provider-agnostic, so gitlab/jira/azure get the same artifacts.
10044
+ #
10045
+ # Best-effort: never allowed to break the run.
10046
+ if declare -f write_journey_context >/dev/null 2>&1; then
10047
+ # owner/repo come from the NORMALIZED json's `repo` field ("owner/name"),
10048
+ # not from ISSUE_OWNER/ISSUE_REPO: those globals are re-cleared by any
10049
+ # later parse_issue_reference call, which yielded a "/#42" ref here.
10050
+ # Every provider populates `repo`, so this stays provider-agnostic.
10051
+ local _gp_owner="" _gp_repo="" _gp_acc="" _gp_body="" _gp_repo_ref=""
10052
+ _gp_repo_ref=$(printf '%s' "$issue_json" | python3 -c \
10053
+ "import json,sys; print(json.loads(sys.stdin.read()).get('repo','') or '')" 2>/dev/null || true)
10054
+ _gp_owner="${_gp_repo_ref%%/*}"
10055
+ _gp_repo="${_gp_repo_ref#*/}"
10056
+ [ "$_gp_owner" = "$_gp_repo_ref" ] && _gp_repo=""
10057
+ _gp_body=$(printf '%s' "$issue_json" | python3 -c \
10058
+ "import json,sys; print(json.loads(sys.stdin.read()).get('body','') or '')" 2>/dev/null || true)
10059
+ if declare -f extract_acceptance_criteria >/dev/null 2>&1; then
10060
+ _gp_acc=$(extract_acceptance_criteria "$_gp_body" 2>/dev/null || true)
10061
+ fi
10062
+ write_journey_context "$_gp_owner" "$_gp_repo" "${number:-}" "$title" \
10063
+ "$url" "$_gp_acc" "" "${issue_provider:-github}" "" 2>/dev/null || true
10064
+ fi
10065
+
9999
10066
  # Per-session locking (v6.4.0): export session ID so run.sh uses
10000
10067
  # per-session PID/lock files instead of the global lock. This allows
10001
10068
  # multiple concurrent `loki run` sessions (e.g., loki run 52 -d && loki run 54 -d).
@@ -10190,7 +10257,71 @@ INNER_SCRIPT_EOF
10190
10257
  else
10191
10258
  echo ""
10192
10259
  echo -e "${GREEN}Starting Loki Mode with generated PRD...${NC}"
10193
- cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"}
10260
+ # cmd_start EXEC-replaces this process (loki:3490
10261
+ # _loki_new_session_exec), so ANY code after a bare call is unreachable
10262
+ # -- which is why the --pr block below has never run on this path and
10263
+ # why the detached route at ~:10088 reimplements PR creation instead of
10264
+ # sharing it. Verified empirically, not inferred.
10265
+ #
10266
+ # Only --prepare-pr subshells the runner, so the exec replaces the CHILD
10267
+ # and control returns here. The default path keeps the bare call
10268
+ # untouched: subshelling everything would change signal delivery and
10269
+ # Ctrl+C semantics for every existing user, which this slice must not do.
10270
+ if $prepare_pr && ! $create_pr; then
10271
+ ( cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"} )
10272
+ else
10273
+ cmd_start "$output_file" ${start_args[@]+"${start_args[@]}"}
10274
+ fi
10275
+
10276
+ # Golden path (feature 9): prepare-only. Builds the SAME review-ready
10277
+ # title/body the --pr path would publish, writes it to .loki/state/, and
10278
+ # records state="prepared" for the receipt -- without ever invoking gh.
10279
+ # Consent to prepare is not consent to publish, so this branch is
10280
+ # deliberately separate from create_pr and never falls through to it.
10281
+ if $prepare_pr && ! $create_pr; then
10282
+ echo ""
10283
+ echo -e "${GREEN}Preparing pull request (no GitHub changes)...${NC}"
10284
+ local _pp_title="${title:-Implementation for issue ${issue_ref}}"
10285
+ local _pp_branch
10286
+ _pp_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
10287
+ local _pp_body="Implemented by Loki Mode (autonomous agent)
10288
+
10289
+ Issue: ${issue_ref}
10290
+ Provider: ${issue_provider}
10291
+
10292
+ ## Changes
10293
+ $(git log --oneline "main..HEAD" 2>/dev/null || echo "See diff")"
10294
+
10295
+ # Same receipt the published path appends, so a prepared PR body is
10296
+ # byte-for-byte what would be posted. Print-only and honesty-gated.
10297
+ if [ "${LOKI_PROVEN_PR:-1}" != "0" ] && declare -f render_evidence_receipt_md >/dev/null 2>&1; then
10298
+ local _pp_idfile=".loki/state/last-proof-id.txt"
10299
+ if [ -s "$_pp_idfile" ]; then
10300
+ local _pp_rid=""
10301
+ _pp_rid="$(cat "$_pp_idfile" 2>/dev/null || true)"
10302
+ if [ -n "$_pp_rid" ] && [ -f ".loki/proofs/$_pp_rid/proof.json" ]; then
10303
+ local _pp_receipt=""
10304
+ _pp_receipt="$(render_evidence_receipt_md ".loki/proofs/$_pp_rid/proof.json" "" "" 2>/dev/null || true)"
10305
+ [ -n "$_pp_receipt" ] && _pp_body="${_pp_body}
10306
+
10307
+ ${_pp_receipt}"
10308
+ fi
10309
+ fi
10310
+ fi
10311
+
10312
+ mkdir -p .loki/state 2>/dev/null || true
10313
+ printf '%s\n' "$_pp_title" > .loki/state/pr-title.txt 2>/dev/null || true
10314
+ printf '%s\n' "$_pp_body" > .loki/state/pr-body.md 2>/dev/null || true
10315
+ if command -v jq >/dev/null 2>&1; then
10316
+ jq -n --arg branch "$_pp_branch" --arg title "$_pp_title" \
10317
+ '{state:"prepared", url:"", branch:$branch, title:$title,
10318
+ body_path:".loki/state/pr-body.md",
10319
+ note:"written locally; GitHub was not contacted"}' \
10320
+ > .loki/state/pr.json 2>/dev/null || true
10321
+ fi
10322
+ echo -e "${GREEN}PR prepared${NC} -- review .loki/state/pr-body.md"
10323
+ echo -e "${DIM}Publish it yourself: gh pr create --title \"\$(cat .loki/state/pr-title.txt)\" --body-file .loki/state/pr-body.md${NC}"
10324
+ fi
10194
10325
 
10195
10326
  # Progressive isolation: create PR
10196
10327
  if $create_pr; then
@@ -10234,9 +10365,22 @@ ${_gh_receipt}"
10234
10365
  branch_current=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
10235
10366
  if [[ -n "$branch_current" && "$branch_current" != "main" && "$branch_current" != "master" ]]; then
10236
10367
  git push origin "$branch_current" 2>/dev/null || true
10237
- gh pr create --title "$pr_title" --body "$pr_body" --head "$branch_current" 2>/dev/null && \
10238
- echo -e "${GREEN}PR created${NC}" || \
10368
+ local _pr_out=""
10369
+ if _pr_out=$(gh pr create --title "$pr_title" --body "$pr_body" --head "$branch_current" 2>/dev/null); then
10370
+ echo -e "${GREEN}PR created${NC}"
10371
+ # Record the real outcome for the receipt. Only
10372
+ # written on ACTUAL success, so a failed create
10373
+ # can never leave a "created" claim behind.
10374
+ mkdir -p .loki/state 2>/dev/null || true
10375
+ if command -v jq >/dev/null 2>&1; then
10376
+ jq -n --arg url "$(printf '%s' "$_pr_out" | grep -Eo 'https://[^[:space:]]+' | head -1)" \
10377
+ --arg branch "$branch_current" \
10378
+ '{state:"created", url:$url, branch:$branch}' \
10379
+ > .loki/state/pr.json 2>/dev/null || true
10380
+ fi
10381
+ else
10239
10382
  echo -e "${YELLOW}PR creation failed${NC}"
10383
+ fi
10240
10384
  fi
10241
10385
  fi
10242
10386
  ;;