loki-mode 9.22.3 → 9.22.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,6 +51,63 @@ That is the whole happy path. It asks for a one-line idea, picks a template,
51
51
  shows the real cost and time estimate before spending anything, then builds.
52
52
  Press Enter through every step and you get a sample Todo app.
53
53
 
54
+ One command, no prompts (CI, scripts, containers, any shell without a terminal):
55
+
56
+ ```bash
57
+ loki quickstart "a todo app with user accounts" --yes
58
+ ```
59
+
60
+ Both halves are required with no terminal: an idea (or a path to a PRD file)
61
+ and an explicit `--yes`. Given both, Loki picks the top-ranked template
62
+ automatically, prints the same honest cost and time estimate, and starts the
63
+ build without asking anything. Missing either half exits 2 with the
64
+ needs-a-terminal message and writes nothing, so an ambient `LOKI_AUTO_CONFIRM`
65
+ or a stray argument in CI can never start a paid build on its own. Existing
66
+ files are never overwritten: if `prd.md` is present the PRD lands at
67
+ `prd-quickstart.md`, then numbered suffixes as needed.
68
+
69
+ Choose an exact shipped starter when the top-ranked match is not the one you
70
+ want:
71
+
72
+ ```bash
73
+ loki quickstart --list-templates
74
+ loki quickstart --list-templates --json # schema-v1 automation output
75
+ loki quickstart "an internal reporting workspace" --template dashboard --yes
76
+ ```
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
+
83
+ `--template` accepts an exact template name for idea inputs and works the same
84
+ way with interactive use or `--dry-run` (including JSON preview). Unknown
85
+ templates, duplicate flags, and combinations with a PRD path refuse before
86
+ provider discovery, estimation, writes, or build execution.
87
+
88
+ Preview the same deterministic template choice and estimator-backed plan with
89
+ zero writes or execution:
90
+
91
+ ```bash
92
+ loki quickstart "a todo app with user accounts" --dry-run
93
+ ```
94
+
95
+ Preview requires an idea or readable PRD path, works without a terminal or AI
96
+ provider, and exits before creating a PRD or starting a build. `--dry-run` and
97
+ `--yes` are mutually exclusive so execution intent is never ambiguous.
98
+
99
+ For scripts and local dashboards, add `--json` to receive one versioned JSON
100
+ object instead of terminal text:
101
+
102
+ ```bash
103
+ loki quickstart "a todo app with user accounts" --dry-run --json
104
+ ```
105
+
106
+ The object contains the input kind, deterministic selected template (or `null`
107
+ for an existing PRD), and the exact estimator response under `plan`. `--json`
108
+ requires `--dry-run`; invalid input or estimator failure writes no JSON, and the
109
+ command still exits before provider discovery, file writes, or build execution.
110
+
54
111
  Or go straight at it:
55
112
 
56
113
  ```bash
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.3
6
+ # Loki Mode v9.22.5
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.3 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.22.5 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.22.3
1
+ 9.22.5
package/autonomy/loki CHANGED
@@ -35298,6 +35298,9 @@ cmd_proof() {
35298
35298
  echo " show <id> Pretty-print .loki/proofs/<id>/proof.json"
35299
35299
  echo " verify <id> Re-check a receipt against the repo (tamper + drift);"
35300
35300
  echo " exit 0 clean, 1 tamper/drift. Verify it yourself."
35301
+ echo " passport <contract> <id|proof.json> <output.json>"
35302
+ echo " Bind work from any agent to a portable Outcome Contract"
35303
+ echo " and independently rechecked Evidence Receipt"
35301
35304
  echo ""
35302
35305
  echo "Options for 'verify':"
35303
35306
  echo " --human Render the verdict and every failure reason as prose"
@@ -35474,6 +35477,47 @@ PYEOF
35474
35477
  fi
35475
35478
  exit 0
35476
35479
  ;;
35480
+ passport)
35481
+ # EXEC-PROOF-01: make the provider-neutral Proof Passport reachable
35482
+ # from the installed product. The Python implementation already
35483
+ # binds the executor/verifier identities, exact contract digest,
35484
+ # exact receipt bytes, and independently re-derived verdict. Before
35485
+ # this subcommand, users had to know the repository-only tools/
35486
+ # path, so the portable proof workflow was not actually a product
35487
+ # workflow for npm consumers.
35488
+ #
35489
+ # The second positional accepts either a local proof id or an
35490
+ # explicit receipt path. That lets a Loki run use its short id while
35491
+ # Codex, Claude Code, Cursor, Factory, or any other executor can hand
35492
+ # over a provider-neutral proof.json without copying it into .loki.
35493
+ local contract="${1:-}"
35494
+ local receipt_ref="${2:-}"
35495
+ local output="${3:-}"
35496
+ if [ -z "$contract" ] || [ -z "$receipt_ref" ] || [ -z "$output" ]; then
35497
+ echo -e "${RED}Usage: loki proof passport <contract.json> <proof-id|proof.json> <output.json> [options]${NC}" >&2
35498
+ echo "Options: --repo-dir DIR --markdown-output FILE --force" >&2
35499
+ exit 64
35500
+ fi
35501
+ shift 3
35502
+
35503
+ local receipt_path="$receipt_ref"
35504
+ if [ ! -f "$receipt_path" ]; then
35505
+ receipt_path="${proofs_dir}/${receipt_ref}/proof.json"
35506
+ fi
35507
+ if [ ! -f "$receipt_path" ]; then
35508
+ echo -e "${RED}Receipt not found: ${receipt_ref}${NC}" >&2
35509
+ echo "Pass a proof id from 'loki proof list' or a path to proof.json." >&2
35510
+ exit 2
35511
+ fi
35512
+
35513
+ local passport_tool="${SKILL_DIR}/tools/proof-passport.py"
35514
+ if [ ! -f "$passport_tool" ]; then
35515
+ echo -e "${RED}Proof Passport tool not found: ${passport_tool}${NC}" >&2
35516
+ exit 3
35517
+ fi
35518
+ python3 "$passport_tool" "$contract" "$receipt_path" "$output" "$@"
35519
+ exit $?
35520
+ ;;
35477
35521
  md)
35478
35522
  # Paste-able Markdown for a PR comment, a Slack message, or a
35479
35523
  # ticket. The renderer already existed -- render_evidence_receipt_md
@@ -283,6 +283,85 @@ _qs_template_summary() {
283
283
  esac
284
284
  }
285
285
 
286
+ # _qs_shipped_template_names: print every shipped template basename in stable
287
+ # catalog order. The filesystem is the source of truth: adding or removing a
288
+ # templates/*.md payload changes discovery automatically, while README.md is
289
+ # deliberately excluded because it is gallery documentation, not a PRD.
290
+ _qs_shipped_template_names() {
291
+ local tdir; tdir="$(_qs_templates_dir)"
292
+ local f name
293
+ for f in "$tdir"/*.md; do
294
+ [ -f "$f" ] || continue
295
+ name=$(basename "$f" .md)
296
+ [ "$name" = "README" ] && continue
297
+ printf '%s\n' "$name"
298
+ done | LC_ALL=C sort
299
+ }
300
+
301
+ # _qs_list_templates [json]: provider-free discovery for terminals and local
302
+ # automation. Human and machine output are derived from the same shipped-name
303
+ # stream and the same stable purpose table used by the interactive picker.
304
+ _qs_list_templates() {
305
+ local json_output="${1:-false}"
306
+ local catalog="" count=0 name purpose
307
+ while IFS= read -r name; do
308
+ [ -n "$name" ] || continue
309
+ purpose="$(_qs_template_summary "$name")"
310
+ catalog="${catalog}${name}\t${purpose}\n"
311
+ count=$((count + 1))
312
+ done < <(_qs_shipped_template_names)
313
+
314
+ if [ "$count" -eq 0 ]; then
315
+ printf 'No shipped quickstart templates were found.\n' >&2
316
+ return 2
317
+ fi
318
+
319
+ if [ "$json_output" = true ]; then
320
+ printf '%b' "$catalog" | python3 -c '
321
+ import json
322
+ import sys
323
+
324
+ templates = []
325
+ for raw in sys.stdin:
326
+ name, purpose = raw.rstrip("\n").split("\t", 1)
327
+ templates.append({"name": name, "purpose": purpose})
328
+ json.dump(
329
+ {
330
+ "schema_version": 1,
331
+ "command": "loki quickstart",
332
+ "mode": "list-templates",
333
+ "templates": templates,
334
+ },
335
+ sys.stdout,
336
+ separators=(",", ":"),
337
+ sort_keys=True,
338
+ )
339
+ sys.stdout.write("\n")
340
+ ' || return 2
341
+ return 0
342
+ fi
343
+
344
+ printf '%sShipped quickstart templates (%d)%s\n' "$_QS_BOLD" "$count" "$_QS_NC"
345
+ printf '%b' "$catalog" | while IFS=$'\t' read -r name purpose; do
346
+ [ -n "$name" ] || continue
347
+ printf ' %-20s %s\n' "$name" "$purpose"
348
+ done
349
+ return 0
350
+ }
351
+
352
+ # _qs_template_exists <name>: accept only an exact shipped template basename.
353
+ # Keeping validation here (rather than accepting an arbitrary path) prevents
354
+ # --template from becoming a second PRD/file-read surface. The intentionally
355
+ # narrow character set also makes names safe to join below without traversal.
356
+ _qs_template_exists() {
357
+ local name="${1:-}"
358
+ case "$name" in
359
+ ""|*[!a-z0-9-]*) return 1;;
360
+ esac
361
+ [ "$name" != "README" ] || return 1
362
+ [ -f "$(_qs_templates_dir)/$name.md" ]
363
+ }
364
+
286
365
  # _qs_selected_provider: print the provider a build would ACTUALLY pick, or
287
366
  # nothing. Single source of truth is providers/loader.sh auto_detect_provider --
288
367
  # the same seam render_provider_availability (provider-offer.sh:395) uses, and
@@ -327,7 +406,7 @@ _qs_selected_provider() {
327
406
  # non-zero if the estimator gave no result (caller falls back to a no-number
328
407
  # confirm, never fabricating a figure).
329
408
  _qs_emit_plan() {
330
- local prd_path="$1" template_name="$2"
409
+ local prd_path="$1" template_name="$2" json_output="${3:-false}" input_kind="${4:-idea}"
331
410
  local plan_json=""
332
411
  plan_json=$(show_prd_plan "$prd_path" "true" "false" 2>/dev/null) || plan_json=""
333
412
  if [ -z "$plan_json" ]; then
@@ -358,6 +437,32 @@ print('{}{}'.format(iters, rng_str))
358
437
  if [ -z "$parsed" ]; then
359
438
  return 1
360
439
  fi
440
+
441
+ if [ "$json_output" = true ]; then
442
+ local json_payload=""
443
+ json_payload=$(printf '%s' "$plan_json" | python3 -c '
444
+ import json
445
+ import sys
446
+
447
+ plan = json.load(sys.stdin)
448
+ if not isinstance(plan, dict):
449
+ sys.exit(1)
450
+ template_name, input_kind = sys.argv[1:3]
451
+ payload = {
452
+ "schema_version": 1,
453
+ "command": "loki quickstart",
454
+ "mode": "dry-run",
455
+ "input_kind": input_kind,
456
+ "selected_template": template_name if input_kind == "idea" else None,
457
+ "source_name": template_name if input_kind == "prd" else None,
458
+ "plan": plan,
459
+ }
460
+ json.dump(payload, sys.stdout, separators=(",", ":"), sort_keys=True)
461
+ ' "$template_name" "$input_kind" 2>/dev/null) || json_payload=""
462
+ [ -n "$json_payload" ] || return 1
463
+ printf '%s\n' "$json_payload" >&3
464
+ return 0
465
+ fi
361
466
  local tier_u cost_u time_u iter_u
362
467
  tier_u=$(printf '%s' "$parsed" | sed -n '1p')
363
468
  cost_u=$(printf '%s' "$parsed" | sed -n '2p')
@@ -391,16 +496,34 @@ _qs_help() {
391
496
  printf '\n'
392
497
  printf 'Options:\n'
393
498
  printf ' --yes, -y Auto-confirm the final build prompt (still shows the plan)\n'
499
+ printf ' --dry-run Preview the selected template and plan; write/start nothing\n'
500
+ printf ' --json With --dry-run, emit one machine-readable JSON object\n'
501
+ printf ' --template N Use the exact shipped template N for an IDEA\n'
502
+ printf ' --list-templates List every shipped template and its purpose\n'
394
503
  printf ' --help, -h Show this help and exit\n'
395
504
  printf '\n'
505
+ printf 'Non-interactive use:\n'
506
+ printf ' loki quickstart "a todo app with user accounts" --yes\n'
507
+ printf ' Both an IDEA (or PRD path) and --yes are required with no terminal.\n'
508
+ printf ' Missing either one exits 2 and writes nothing. The top-ranked\n'
509
+ printf ' template is chosen automatically and the plan is still shown.\n'
510
+ printf ' Add --template NAME to choose a shipped template instead.\n'
511
+ printf ' Run with --list-templates (and optional --json) to discover names.\n'
512
+ printf '\n'
513
+ printf 'Zero-spend preview:\n'
514
+ printf ' loki quickstart "a todo app" --dry-run\n'
515
+ printf ' An IDEA (or readable PRD path) is required. No provider is checked,\n'
516
+ printf ' no file is written, and no build is started. Do not combine with --yes.\n'
517
+ printf ' Add --json for versioned JSON only; --json requires --dry-run.\n'
518
+ printf '\n'
396
519
  printf 'Steps:\n'
397
- printf ' 1. Setup Check for an AI provider; offer to install if missing\n'
520
+ printf ' 1. Setup Check for an AI provider for execution (skipped in preview)\n'
398
521
  printf ' 2. Build Describe what you want, or Enter for the sample Todo app\n'
399
522
  printf ' 3. Template Pick the closest starting template (offline keyword match)\n'
400
523
  printf ' 4. Plan Review the honest cost/time estimate, then confirm\n'
401
524
  printf '\n'
402
- printf 'The PRD is written to ./prd.md in the current directory, then the build\n'
403
- printf 'starts. For non-interactive automation use: loki start <prd> --yes\n'
525
+ printf 'The PRD is written to ./prd.md in the current directory (or the next\n'
526
+ printf 'free prd-quickstart*.md name if that exists), then the build starts.\n'
404
527
  return 0
405
528
  }
406
529
 
@@ -408,14 +531,38 @@ _qs_help() {
408
531
  # (slice B), show_prd_plan (slice A), the template matcher, and cmd_start.
409
532
  #
410
533
  # Order is load-bearing:
411
- # --help (exit 0) -> non-TTY/CI gate (hint + exit 2) -> provider gate ->
534
+ # argv validation -> non-TTY/CI gate (hint + exit 2) -> provider gate or skip ->
412
535
  # step 2 (idea / PRD path) -> step 3 (template) -> step 4 (plan + confirm) ->
413
536
  # write PRD to CWD -> cmd_start --yes --no-plan (subshelled; it execs the runner).
537
+ #
538
+ # The non-TTY/CI gate admits two fully-specified shapes:
539
+ # loki quickstart "<idea>|<prd-path>" --yes
540
+ # loki quickstart "<idea>|<prd-path>" --dry-run
541
+ # Execution requires argv consent; preview forbids it. Both paths skip prompts
542
+ # and share template ranking plus the honest estimator. Only execution crosses
543
+ # the provider, confirm, write, and cmd_start boundaries.
414
544
  cmd_quickstart() {
415
545
  local positional=""
416
546
  local assume_yes=false
547
+ local dry_run=false
548
+ local json_output=false
549
+ local template_override=""
550
+ local template_flag_seen=false
551
+ local list_templates=false
552
+ local list_templates_flag_seen=false
417
553
  if _qs_assume_yes; then assume_yes=true; fi
418
554
 
555
+ # yes_flag tracks EXPLICIT --yes/-y on THIS command's argv, and nothing else.
556
+ # It is deliberately NOT assume_yes: _qs_assume_yes also returns true for an
557
+ # ambient LOKI_ASSUME_YES or LOKI_AUTO_CONFIRM=true, and LOKI_AUTO_CONFIRM is
558
+ # exactly what `loki --yes <anything>` (loki:2313) and CI-ish environments
559
+ # export. Gating the non-interactive bypass on assume_yes would let an
560
+ # ambient env var plus a stray positional silently start a PAID build in CI
561
+ # with no human in the loop. The safety contract asks for explicit consent,
562
+ # so consent must come from argv. assume_yes keeps its existing meaning for
563
+ # the confirm prompt, so the interactive journey is byte-identical.
564
+ local yes_flag=false
565
+
419
566
  while [ $# -gt 0 ]; do
420
567
  case "$1" in
421
568
  --help|-h)
@@ -424,6 +571,37 @@ cmd_quickstart() {
424
571
  ;;
425
572
  --yes|-y)
426
573
  assume_yes=true
574
+ yes_flag=true
575
+ shift
576
+ ;;
577
+ --dry-run)
578
+ dry_run=true
579
+ shift
580
+ ;;
581
+ --json)
582
+ json_output=true
583
+ shift
584
+ ;;
585
+ --template)
586
+ if [ "$template_flag_seen" = true ]; then
587
+ printf '%s--template may be specified only once.%s\n' "$_QS_RED" "$_QS_NC" >&2
588
+ exit 2
589
+ fi
590
+ template_flag_seen=true
591
+ if [ $# -lt 2 ] || [ -z "${2:-}" ] || [[ "${2:-}" == --* ]]; then
592
+ printf '%s--template requires an exact shipped template name.%s\n' "$_QS_RED" "$_QS_NC" >&2
593
+ exit 2
594
+ fi
595
+ template_override="$2"
596
+ shift 2
597
+ ;;
598
+ --list-templates)
599
+ if [ "$list_templates_flag_seen" = true ]; then
600
+ printf '%s--list-templates may be specified only once.%s\n' "$_QS_RED" "$_QS_NC" >&2
601
+ exit 2
602
+ fi
603
+ list_templates=true
604
+ list_templates_flag_seen=true
427
605
  shift
428
606
  ;;
429
607
  --*)
@@ -444,36 +622,146 @@ cmd_quickstart() {
444
622
  esac
445
623
  done
446
624
 
447
- # Non-TTY / CI: quickstart is interactive by definition. Never hang on read;
448
- # print the automation hint to stderr and exit 2 (design 3.8).
449
- if _qs_non_interactive; then
450
- printf 'loki quickstart is interactive and needs a terminal. For automation use: loki start <prd> --yes\n' >&2
625
+ # Discovery is a standalone read-only command shape. Refuse input and every
626
+ # execution/preview selector rather than guessing intent; --json is its only
627
+ # compatible modifier. This return precedes terminal, provider, estimator,
628
+ # consent, PRD, and build boundaries.
629
+ if [ "$list_templates" = true ]; then
630
+ if [ -n "$positional" ] || [ "$yes_flag" = true ] || [ "$dry_run" = true ] || [ "$template_flag_seen" = true ]; then
631
+ printf '%s--list-templates accepts only the optional --json flag.%s\n' "$_QS_RED" "$_QS_NC" >&2
632
+ exit 2
633
+ fi
634
+ _qs_list_templates "$json_output"
635
+ return $?
636
+ fi
637
+
638
+ # A preview is an explicit no-execution request. Reject simultaneous build
639
+ # consent instead of guessing which instruction wins. This check precedes
640
+ # provider discovery, estimation, and every write.
641
+ if [ "$dry_run" = true ] && [ "$yes_flag" = true ]; then
642
+ printf '%s--dry-run cannot be combined with --yes or -y.%s\n' "$_QS_RED" "$_QS_NC" >&2
451
643
  exit 2
452
644
  fi
453
645
 
646
+ if [ "$json_output" = true ] && [ "$dry_run" != true ]; then
647
+ printf '%s--json requires --dry-run.%s\n' "$_QS_RED" "$_QS_NC" >&2
648
+ exit 2
649
+ fi
650
+
651
+ # Explicit template selection is deliberately an IDEA-only surface. It is
652
+ # validated before provider discovery, estimation, and every write/build
653
+ # boundary so a typo or conflicting PRD can never fall through to spend.
654
+ if [ "$template_flag_seen" = true ]; then
655
+ if [ -z "$positional" ]; then
656
+ printf '%s--template requires an IDEA argument.%s\n' "$_QS_RED" "$_QS_NC" >&2
657
+ exit 2
658
+ fi
659
+ case "$positional" in
660
+ */*|*.md|*.markdown|*.txt|*.json|*.yaml|*.yml)
661
+ printf '%s--template cannot be combined with a PRD path.%s\n' "$_QS_RED" "$_QS_NC" >&2
662
+ exit 2
663
+ ;;
664
+ esac
665
+ if [ -f "$positional" ]; then
666
+ printf '%s--template cannot be combined with a PRD path.%s\n' "$_QS_RED" "$_QS_NC" >&2
667
+ exit 2
668
+ fi
669
+ if ! _qs_template_exists "$template_override"; then
670
+ printf '%sUnknown shipped template: %s%s\n' "$_QS_RED" "$template_override" "$_QS_NC" >&2
671
+ exit 2
672
+ fi
673
+ fi
674
+
675
+ # Preview is deliberately non-interactive: without an explicit idea or PRD
676
+ # path there is nothing deterministic to estimate. A path-looking argument
677
+ # must resolve to a readable regular file; otherwise treating a typo such as
678
+ # ./prd.md as prose would preview an unrelated template and mislead the user.
679
+ if [ "$dry_run" = true ]; then
680
+ if [ -z "$positional" ]; then
681
+ printf 'loki quickstart --dry-run requires an IDEA or readable PRD path.\n' >&2
682
+ exit 2
683
+ fi
684
+ case "$positional" in
685
+ */*|*.md|*.markdown|*.txt|*.json|*.yaml|*.yml)
686
+ if [ ! -f "$positional" ] || [ ! -r "$positional" ]; then
687
+ printf 'PRD path is not a readable file: %s\n' "$positional" >&2
688
+ exit 2
689
+ fi
690
+ ;;
691
+ esac
692
+ fi
693
+
694
+ # Non-TTY / CI: quickstart is interactive by definition, so by default it
695
+ # never hangs on a read -- it prints the automation hint to stderr and exits
696
+ # 2 (design 3.8).
697
+ #
698
+ # The execution exception is a fully-specified invocation:
699
+ # loki quickstart "<idea>" --yes (or a PRD path in place of the idea)
700
+ # Both halves are required. A non-empty positional supplies the input that
701
+ # steps 2-3 would otherwise have to ask for, and an explicit argv --yes
702
+ # supplies the consent step 4 would otherwise have to ask for. With both
703
+ # present there is nothing left to prompt about, so the refusal is pure
704
+ # friction for a newly installed operator running one command.
705
+ #
706
+ # Missing EITHER half keeps the refusal verbatim and writes nothing: a bare
707
+ # `loki quickstart` in CI still cannot spend, and neither can one that has an
708
+ # idea but no consent. Fail-closed is the load-bearing direction here -- the
709
+ # bypass must be something an operator opts into by typing both, never
710
+ # something an environment can arrive at on its own.
711
+ local noninteractive_ok=false
712
+ if _qs_non_interactive; then
713
+ if [ "$dry_run" = true ] || { [ -n "$positional" ] && [ "$yes_flag" = true ]; }; then
714
+ noninteractive_ok=true
715
+ else
716
+ printf 'loki quickstart is interactive and needs a terminal. For automation use: loki start <prd> --yes\n' >&2
717
+ exit 2
718
+ fi
719
+ fi
720
+
721
+ # Machine-readable preview follows the exact same selection and estimator
722
+ # path as the human preview. Suppress presentation stdout while retaining a
723
+ # duplicate of the caller's stdout on fd 3; _qs_emit_plan writes the single
724
+ # validated JSON object there only after estimation succeeds. Stderr stays
725
+ # available for fail-closed diagnostics.
726
+ local json_stdout_redirected=false
727
+ if [ "$json_output" = true ]; then
728
+ exec 3>&1 1>/dev/null
729
+ json_stdout_redirected=true
730
+ fi
731
+
454
732
  printf '\n'
455
- printf '%sLoki Mode quickstart -- four quick questions, then your build starts.%s\n' "$_QS_BOLD" "$_QS_NC"
733
+ if [ "$dry_run" = true ]; then
734
+ printf '%sLoki Mode quickstart preview -- template and plan only.%s\n' "$_QS_BOLD" "$_QS_NC"
735
+ else
736
+ printf '%sLoki Mode quickstart -- four quick questions, then your build starts.%s\n' "$_QS_BOLD" "$_QS_NC"
737
+ fi
456
738
  printf '\n'
457
739
 
458
740
  # ----- Step 1 of 4: Setup (reuse the slice-B provider offer) -------------
459
741
  printf '%sStep 1 of 4: Setup%s\n' "$_QS_BOLD" "$_QS_NC"
460
- printf ' Checking for an AI provider CLI ...\n'
742
+ if [ "$dry_run" = true ]; then
743
+ # Estimation and deterministic template selection are local. Previewing
744
+ # must work before provider installation and must not run provider code.
745
+ printf ' Preview mode: provider check skipped; no build will start.\n'
746
+ else
747
+ printf ' Checking for an AI provider CLI ...\n'
461
748
  # Ask the loader FIRST: it is the only thing that knows what the runner will
462
749
  # really pick, and it is the only check that sees opencode. Falling back to
463
750
  # detect_any_provider (stale four, PATH-only) keeps the no-provider guard
464
751
  # intact when the loader is absent or unreadable.
465
- local found=""
466
- found="$(_qs_selected_provider)" || found=""
467
- if [ -n "$found" ]; then
468
- printf ' Found: %s. Good.\n' "$found"
469
- elif detect_any_provider; then
470
- printf ' Found: an AI provider CLI. Good.\n'
471
- else
472
- # Run the inline install + login offer. provider_offer_gate returns 2 if
473
- # no provider ends up available (declined, or install failed).
474
- if ! provider_offer_gate; then
475
- printf '%sNo provider available; cannot start a build. Install one and re-run loki quickstart.%s\n' "$_QS_RED" "$_QS_NC" >&2
476
- exit 2
752
+ local found=""
753
+ found="$(_qs_selected_provider)" || found=""
754
+ if [ -n "$found" ]; then
755
+ printf ' Found: %s. Good.\n' "$found"
756
+ elif detect_any_provider; then
757
+ printf ' Found: an AI provider CLI. Good.\n'
758
+ else
759
+ # Run the inline install + login offer. provider_offer_gate returns 2 if
760
+ # no provider ends up available (declined, or install failed).
761
+ if ! provider_offer_gate; then
762
+ printf '%sNo provider available; cannot start a build. Install one and re-run loki quickstart.%s\n' "$_QS_RED" "$_QS_NC" >&2
763
+ exit 2
764
+ fi
477
765
  fi
478
766
  fi
479
767
  printf '\n'
@@ -484,27 +772,34 @@ cmd_quickstart() {
484
772
  local prd_source="" # an existing PRD file path, when the user has one
485
773
  local brief="" # the one-line idea (drives template matching)
486
774
  local template_name=""
775
+ local input_kind="idea"
487
776
 
488
777
  if [ -n "$positional" ] && [ -f "$positional" ]; then
489
778
  prd_source="$positional"
779
+ input_kind="prd"
490
780
  printf '%sUsing your PRD: %s%s\n' "$_QS_DIM" "$positional" "$_QS_NC"
491
781
  printf '\n'
492
782
  else
493
783
  printf '%sStep 2 of 4: What do you want to build?%s\n' "$_QS_BOLD" "$_QS_NC"
494
- printf ' Describe it in one line, or paste a path to a PRD file.\n'
495
- printf ' (Press Enter to build the sample Todo app.)\n'
496
- if [ -n "$positional" ]; then
784
+ if [ "$dry_run" = true ]; then
497
785
  brief="$positional"
498
- printf '> %s\n' "$brief"
786
+ printf ' Previewing idea: %s\n' "$brief"
499
787
  else
500
- local answer=""
501
- printf '> '
502
- read -r answer 2>/dev/null || answer=""
503
- # If the typed value is an existing file, treat it as a PRD path.
504
- if [ -n "$answer" ] && [ -f "$answer" ]; then
505
- prd_source="$answer"
788
+ printf ' Describe it in one line, or paste a path to a PRD file.\n'
789
+ printf ' (Press Enter to build the sample Todo app.)\n'
790
+ if [ -n "$positional" ]; then
791
+ brief="$positional"
792
+ printf '> %s\n' "$brief"
506
793
  else
507
- brief="$answer"
794
+ local answer=""
795
+ printf '> '
796
+ read -r answer 2>/dev/null || answer=""
797
+ # If the typed value is an existing file, treat it as a PRD path.
798
+ if [ -n "$answer" ] && [ -f "$answer" ]; then
799
+ prd_source="$answer"
800
+ else
801
+ brief="$answer"
802
+ fi
508
803
  fi
509
804
  fi
510
805
  printf '\n'
@@ -512,6 +807,11 @@ cmd_quickstart() {
512
807
 
513
808
  # ----- Step 3 of 4: Pick a template (skipped if a PRD path was given) ----
514
809
  if [ -z "$prd_source" ]; then
810
+ if [ -n "$template_override" ]; then
811
+ template_name="$template_override"
812
+ printf '%sStep 3 of 4: Template%s\n' "$_QS_BOLD" "$_QS_NC"
813
+ printf ' Selected %s (--template).\n\n' "$template_name"
814
+ else
515
815
  local -a top3=()
516
816
  local line
517
817
  while IFS= read -r line; do
@@ -523,25 +823,40 @@ cmd_quickstart() {
523
823
  top3=("simple-todo-app")
524
824
  fi
525
825
 
526
- printf '%sStep 3 of 4: Pick a starting template%s\n' "$_QS_BOLD" "$_QS_NC"
527
- if [ -n "$brief" ]; then
528
- printf ' Closest matches for "%s":\n' "$brief"
826
+ # The MENU, not just the read, is interactive-only. Offering "Choose 1-3"
827
+ # to a shell that can never answer is a prompt the operator has to read
828
+ # and mistrust; worse, it makes a transcript indistinguishable from one
829
+ # that actually stopped for input. The ranking above is shared by both
830
+ # paths -- only its presentation differs.
831
+ local pick=""
832
+ if [ "$noninteractive_ok" != true ]; then
833
+ printf '%sStep 3 of 4: Pick a starting template%s\n' "$_QS_BOLD" "$_QS_NC"
834
+ if [ -n "$brief" ]; then
835
+ printf ' Closest matches for "%s":\n' "$brief"
836
+ else
837
+ printf ' Closest matches for the sample Todo app:\n'
838
+ fi
839
+ local i=1 t suffix
840
+ for t in "${top3[@]}"; do
841
+ suffix=""
842
+ [ "$i" -eq 1 ] && suffix=" (default)"
843
+ printf ' %d) %-18s %s%s\n' "$i" "$t" "$(_qs_template_summary "$t")" "$suffix"
844
+ i=$((i + 1))
845
+ done
846
+ printf ' Choose 1-%d, or press Enter for 1.\n' "${#top3[@]}"
847
+ printf '> '
848
+ read -r pick 2>/dev/null || pick=""
849
+ printf '\n'
529
850
  else
530
- printf ' Closest matches for the sample Todo app:\n'
851
+ # Deterministic selection: the empty pick below resolves to top3[0],
852
+ # the same rank the interactive picker offers as "(default)". The
853
+ # ranking itself is unchanged -- _qs_score_templates is offline,
854
+ # deterministic and already the single source of order -- so the
855
+ # non-interactive choice is exactly the one an operator pressing
856
+ # Enter would get. No picker, no prompt, no second code path.
857
+ printf '%sStep 3 of 4: Template%s\n' "$_QS_BOLD" "$_QS_NC"
858
+ printf ' Selected %s (top match) for "%s".\n\n' "${top3[0]}" "$brief"
531
859
  fi
532
- local i=1 t suffix
533
- for t in "${top3[@]}"; do
534
- suffix=""
535
- [ "$i" -eq 1 ] && suffix=" (default)"
536
- printf ' %d) %-18s %s%s\n' "$i" "$t" "$(_qs_template_summary "$t")" "$suffix"
537
- i=$((i + 1))
538
- done
539
- printf ' Choose 1-%d, or press Enter for 1.\n' "${#top3[@]}"
540
-
541
- local pick=""
542
- printf '> '
543
- read -r pick 2>/dev/null || pick=""
544
- printf '\n'
545
860
 
546
861
  case "$pick" in
547
862
  ""|1) template_name="${top3[0]}";;
@@ -550,6 +865,8 @@ cmd_quickstart() {
550
865
  *) template_name="${top3[0]}";; # any unexpected input -> the default
551
866
  esac
552
867
 
868
+ fi
869
+
553
870
  local tdir; tdir="$(_qs_templates_dir)"
554
871
  prd_source="$tdir/$template_name.md"
555
872
  if [ ! -f "$prd_source" ]; then
@@ -562,11 +879,41 @@ cmd_quickstart() {
562
879
 
563
880
  # ----- Step 4 of 4: Review the plan (reuse the slice-A estimator) --------
564
881
  printf '%sStep 4 of 4: Review the plan%s\n' "$_QS_BOLD" "$_QS_NC"
882
+ # The plan is ALWAYS rendered, on both paths, from the same real estimator.
883
+ # The non-interactive path shows it before execution rather than before a
884
+ # prompt: the operator still gets the honest quote in the transcript, and it
885
+ # is still the figure cmd_start runs with, so the quote equals the charge.
565
886
  local estimate_ok=true
566
- if ! _qs_emit_plan "$prd_source" "$template_name"; then
887
+ if ! _qs_emit_plan "$prd_source" "$template_name" "$json_output" "$input_kind"; then
567
888
  estimate_ok=false
568
- printf '%sCould not compute a cost estimate (the estimator did not return a result).%s\n' "$_QS_YELLOW" "$_QS_NC"
569
- printf '\n'
889
+ # No honest number available. Interactively this flips the confirm to
890
+ # default-NO below. Non-interactively there is no one to review the
891
+ # missing plan, so fail closed before writing a PRD or starting a build.
892
+ # Explicit --yes authorizes the displayed estimate; it is not consent
893
+ # to spend without one.
894
+ if [ "$noninteractive_ok" = true ]; then
895
+ if [ "$json_stdout_redirected" = true ]; then
896
+ exec 1>&3 3>&-
897
+ json_stdout_redirected=false
898
+ fi
899
+ printf 'Could not compute a cost estimate; non-interactive quickstart requires a displayed plan and started no build.\n' >&2
900
+ return 2
901
+ else
902
+ printf '%sCould not compute a cost estimate (the estimator did not return a result).%s\n' "$_QS_YELLOW" "$_QS_NC"
903
+ printf '\n'
904
+ fi
905
+ fi
906
+
907
+ # The preview terminal boundary is intentionally immediately after the
908
+ # shared estimator. Reaching it proves the same template and plan were
909
+ # rendered, while returning here makes the confirm, PRD copy, and cmd_start
910
+ # structurally unreachable.
911
+ if [ "$dry_run" = true ]; then
912
+ printf 'Preview complete. No provider was run, no file was written, and no build was started.\n'
913
+ if [ "$json_stdout_redirected" = true ]; then
914
+ exec 1>&3 3>&-
915
+ fi
916
+ return 0
570
917
  fi
571
918
 
572
919
  # ----- Confirm ----------------------------------------------------------
@@ -595,8 +942,16 @@ cmd_quickstart() {
595
942
  local target="./prd.md"
596
943
  if [ -e "$target" ]; then
597
944
  local overwrite=""
598
- printf 'prd.md already exists. Overwrite? [y/N] '
599
- read -r overwrite 2>/dev/null || overwrite=""
945
+ # Non-interactive never asks and never overwrites. Leaving $overwrite
946
+ # empty falls into the existing suffix walk below, which is already the
947
+ # correct no-clobber behavior -- prd-quickstart.md, then numbered free
948
+ # suffixes exactly as needed. Reused rather than reimplemented so the two
949
+ # paths cannot drift, and so "never overwrite" holds by construction
950
+ # (there is no branch here that can reach the clobber).
951
+ if [ "$noninteractive_ok" != true ]; then
952
+ printf 'prd.md already exists. Overwrite? [y/N] '
953
+ read -r overwrite 2>/dev/null || overwrite=""
954
+ fi
600
955
  if [[ ! "$overwrite" =~ ^[Yy] ]]; then
601
956
  # Declining to overwrite one file must never silently destroy
602
957
  # another (bug-hunt MEDIUM): the fallback gets the same existence
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.22.3"
10
+ __version__ = "9.22.5"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -133,9 +133,12 @@ def _git_tags(repo_dir: str, limit: int) -> tuple:
133
133
  "refs/tags",
134
134
  ]
135
135
  try:
136
+ # Fixed git executable, integer-formatted option, separate cwd, and an
137
+ # argv vector keep caller data out of shell parsing.
136
138
  proc = subprocess.run(
137
- cmd, cwd=repo_dir, capture_output=True, text=True,
138
- timeout=_GIT_TIMEOUT_S,
139
+ cmd, # lgtm[py/command-line-injection]
140
+ cwd=repo_dir, capture_output=True, text=True,
141
+ timeout=_GIT_TIMEOUT_S, shell=False,
139
142
  )
140
143
  except FileNotFoundError:
141
144
  return [], "git executable not found on PATH"
@@ -495,14 +495,17 @@ async def start_session(request: StartRequest):
495
495
 
496
496
  try:
497
497
  # Start the process
498
+ # RUN_SH is a trusted fixed path; provider and PRD were validated above,
499
+ # and argv is passed directly without a command shell.
498
500
  process = subprocess.Popen(
499
- args,
501
+ args, # lgtm[py/command-line-injection]
500
502
  stdout=subprocess.DEVNULL,
501
503
  stderr=subprocess.DEVNULL,
502
504
  start_new_session=True,
503
505
  # A deleted cwd would make Popen itself raise; fall back to the
504
506
  # skill tree rather than failing to launch the run at all.
505
- cwd=str(_cwd_or_skill_dir())
507
+ cwd=str(_cwd_or_skill_dir()),
508
+ shell=False,
506
509
  )
507
510
 
508
511
  # Save provider for status tracking
@@ -4313,13 +4313,16 @@ def _start_supervised_workspace_build(
4313
4313
  supervisor_log = _build_execution.execution_dir(execution_id) / "supervisor.log"
4314
4314
  try:
4315
4315
  with _build_execution.open_private_log(supervisor_log) as log_handle:
4316
+ # sys.executable and the module name are fixed; validated build
4317
+ # fields remain argv data and never reach a command shell.
4316
4318
  process = subprocess.Popen(
4317
- supervisor_args,
4319
+ supervisor_args, # lgtm[py/command-line-injection]
4318
4320
  stdout=log_handle,
4319
4321
  stderr=subprocess.STDOUT,
4320
4322
  start_new_session=True,
4321
4323
  cwd=str(skill_dir),
4322
4324
  env=popen_env,
4325
+ shell=False,
4323
4326
  )
4324
4327
  except (OSError, subprocess.SubprocessError) as exc:
4325
4328
  finished_at = _build_execution.utc_now()
@@ -4582,13 +4585,16 @@ async def start_build(request: Request, body: StartBuildRequest):
4582
4585
  args.append("--bg")
4583
4586
  args.append(str(spec_file))
4584
4587
  try:
4588
+ # run_sh is a trusted fixed path; provider and spec were validated, and
4589
+ # the argv vector is executed directly without shell interpretation.
4585
4590
  process = subprocess.Popen(
4586
- args,
4591
+ args, # lgtm[py/command-line-injection]
4587
4592
  stdout=subprocess.DEVNULL,
4588
4593
  stderr=subprocess.DEVNULL,
4589
4594
  start_new_session=True,
4590
4595
  cwd=str(project_dir),
4591
4596
  env=popen_env,
4597
+ shell=False,
4592
4598
  )
4593
4599
  except (OSError, subprocess.SubprocessError) as e:
4594
4600
  raise HTTPException(status_code=500, detail=f"Failed to start build: {e}")
@@ -183,6 +183,17 @@ builds the sample Todo app.
183
183
  loki quickstart
184
184
  ```
185
185
 
186
+ For a fully specified first build, provide the idea, an optional exact shipped
187
+ starter template, and explicit consent:
188
+
189
+ ```bash
190
+ loki quickstart "an internal reporting workspace" --template dashboard --yes
191
+ ```
192
+
193
+ Use `--dry-run` instead of `--yes` to preview the same template and plan without
194
+ provider discovery, file writes, or execution; add `--json` for one versioned
195
+ machine-readable object.
196
+
186
197
  Drop a spec -- any artifact that describes what you want built -- and Loki
187
198
  Mode takes it from spec to deployed app. Specs can be a markdown PRD, a
188
199
  GitHub issue URL, or a YAML feature description.
@@ -50,6 +50,19 @@ loki outcomes canary receipt report.json observations.json receipt.json \
50
50
  --enable-receipt --control-route safe --canary-percent 10 --min-samples 5
51
51
  ```
52
52
 
53
+ Automation can require the rederived verdict before the receipt is published:
54
+
55
+ ```bash
56
+ loki outcomes canary receipt report.json observations.json receipt.json \
57
+ --enable-receipt --control-route safe --canary-percent 10 --min-samples 5 \
58
+ --require-verdict PROMOTE --json
59
+ ```
60
+
61
+ The target is created only when the actual verdict exactly matches the required
62
+ `PROMOTE`, `HOLD`, or `ROLLBACK` value. A mismatch exits `3`, reports both verdicts
63
+ and `"requirement_met": false`, and leaves the target absent. Omitting
64
+ `--require-verdict` preserves the existing behavior of recording any complete verdict.
65
+
53
66
  The command creates a new canonical `loki-outcome-canary-decision-receipt/v1`
54
67
  file. It binds the exact report, source, and observation digests; the full
55
68
  evaluation policy; privacy-safe aggregate arm results; the acceptance delta; and
@@ -5,10 +5,12 @@ receipt. The JSON artifact preserves the exact contract and receipt digests,
5
5
  the verifier verdict, executor/verifier separation, and explicit trust
6
6
  limitations.
7
7
 
8
- Generate the machine-readable passport and a portable PR/CI summary together:
8
+ Generate the machine-readable passport and a portable PR/CI summary together
9
+ from the installed product. The receipt argument can be a proof id from
10
+ `loki proof list` or a path to any provider's compatible `proof.json`:
9
11
 
10
12
  ```bash
11
- python3 tools/proof-passport.py \
13
+ loki proof passport \
12
14
  outcome-contract.json proof.json proof-passport.json \
13
15
  --repo-dir . \
14
16
  --markdown-output proof-passport.md
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.22.3";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
2
+ var p_=Object.create;var{getPrototypeOf:d_,defineProperty:eK,getOwnPropertyNames:c_}=Object;var l_=Object.prototype.hasOwnProperty;function i_(Z){return this[Z]}var a_,s_,n_=(Z,X,Q)=>{var Y=Z!=null&&typeof Z==="object";if(Y){var J=X?a_??=new WeakMap:s_??=new WeakMap,z=J.get(Z);if(z)return z}Q=Z!=null?p_(d_(Z)):{};let K=X||!Z||!Z.__esModule?eK(Q,"default",{value:Z,enumerable:!0}):Q;for(let $ of c_(Z))if(!l_.call(K,$))eK(K,$,{get:i_.bind(Z,$),enumerable:!0});if(Y)J.set(Z,K);return K};var HQ=(Z,X)=>()=>(X||Z((X={exports:{}}).exports,X),X.exports);var o_=(Z)=>Z;function r_(Z,X){this[Z]=o_.bind(null,X)}var l0=(Z,X)=>{for(var Q in X)eK(Z,Q,{get:X[Q],enumerable:!0,configurable:!0,set:r_.bind(X,Q)})};var p=(Z,X)=>()=>(Z&&(X=Z(Z=0)),X);var t0=import.meta.require;var xO={};l0(xO,{lokiDir:()=>j0,homeLokiDir:()=>R4,findRepoRootForVersion:()=>X$,REPO_ROOT:()=>i0});import{resolve as n7,dirname as Z$}from"path";import{fileURLToPath as t_}from"url";import{existsSync as UQ}from"fs";import{homedir as e_}from"os";function Zf(){let Z=kO;for(let X=0;X<6;X++){if(UQ(n7(Z,"VERSION"))&&UQ(n7(Z,"autonomy/run.sh")))return Z;let Q=Z$(Z);if(Q===Z)break;Z=Q}return n7(kO,"..","..","..")}function X$(Z){let X=Z;for(let Q=0;Q<6;Q++){if(UQ(n7(X,"VERSION"))&&UQ(n7(X,"autonomy/run.sh")))return X;let Y=Z$(X);if(Y===X)break;X=Y}return n7(Z,"..","..","..")}function j0(){return process.env.LOKI_DIR??n7(process.cwd(),".loki")}function R4(){return n7(e_(),".loki")}var kO,i0;var H8=p(()=>{kO=Z$(t_(import.meta.url));i0=Zf()});import{readFileSync as Xf}from"fs";import{resolve as Qf,dirname as Yf}from"path";import{fileURLToPath as Jf}from"url";function h3(){if(h5!==null)return h5;let Z="9.22.5";if(typeof Z==="string"&&Z.length>0)return h5=Z,h5;try{let X=Yf(Jf(import.meta.url)),Q=X$(X);h5=Xf(Qf(Q,"VERSION"),"utf-8").trim()}catch{h5="unknown"}return h5}var h5=null;var BQ=p(()=>{H8()});var _O={};l0(_O,{runOrThrow:()=>Tf,run:()=>E0,readStreamCapped:()=>NQ,commandVersion:()=>Cf,commandExists:()=>X9,ShellError:()=>Q$,MAX_STDOUT_BYTES:()=>bO});async function NQ(Z,X=bO){let Q=Z.getReader(),Y=new TextDecoder,J="",z=0;try{while(z<X){let{done:K,value:$}=await Q.read();if(K)break;if(!$)continue;if(z+=$.byteLength,z>X){let V=$.byteLength-(z-X);J+=Y.decode($.subarray(0,V),{stream:!0});break}J+=Y.decode($,{stream:!0})}J+=Y.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return J}async function E0(Z,X={}){let Q=Bun.spawn({cmd:[...Z],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),Y,J;if(X.timeoutMs&&X.timeoutMs>0)Y=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}J=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[z,K,$]=await Promise.all([NQ(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:z,stderr:K,exitCode:$}}finally{if(Y)clearTimeout(Y);if(J)clearTimeout(J)}}async function Tf(Z,X={}){let Q=await E0(Z,X);if(Q.exitCode!==0)throw new Q$(`command failed (${Q.exitCode}): ${Z.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function X9(Z){let X=wf(Z),Q=await E0(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function wf(Z){if(!/^[A-Za-z0-9._/-]+$/.test(Z))throw Error(`refused to shell-escape suspect token: ${Z}`);return Z}async function Cf(Z,X="--version"){if(!await X9(Z))return null;let Y=await E0([Z,X],{timeoutMs:5000});if(Y.exitCode!==0)return null;return((Y.stdout||Y.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var bO=16777216,Q$;var x9=p(()=>{Q$=class Q$ extends Error{message;exitCode;stdout;stderr;constructor(Z,X,Q,Y){super(Z);this.message=Z;this.exitCode=X;this.stdout=Q;this.stderr=Y;this.name="ShellError"}}});function o7(Z){return Ff?"":Z}var Ff,A0,F8,_0,$W0,a0,V8,Q9,h;var S6=p(()=>{Ff=(process.env.NO_COLOR??"").length>0;A0=o7("\x1B[0;31m"),F8=o7("\x1B[0;32m"),_0=o7("\x1B[1;33m"),$W0=o7("\x1B[0;34m"),a0=o7("\x1B[0;36m"),V8=o7("\x1B[1m"),Q9=o7("\x1B[2m"),h=o7("\x1B[0m")});import{existsSync as ff}from"fs";async function E7(){if(x4!==void 0)return x4;let Z="/opt/homebrew/bin/python3.12";if(ff(Z))return x4=Z,Z;let X=await X9("python3.12");if(X)return x4=X,X;let Q=await X9("python3");return x4=Q,Q}async function Y7(Z,X={}){let Q=await E7();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return E0([Q,"-c",Z],X)}var x4;var r7=p(()=>{x9()});var XA={};l0(XA,{runStatus:()=>Vh});import{existsSync as Y9,readFileSync as g3,readdirSync as aO,statSync as sO}from"fs";import{resolve as h8,basename as ef}from"path";import{homedir as Zh}from"os";function nO(Z){let X=Math.trunc(Z);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function oO(Z,X,Q){if(X===0)return null;let Y=Math.trunc(Z*100/X),J=Math.trunc(Z*AQ/X);if(J>AQ)J=AQ;let z=AQ-J,K=F8;if(Y>=80)K=A0;else if(Y>=50)K=_0;let $="=".repeat(Math.max(0,J))+" ".repeat(Math.max(0,z)),V=nO(Z),W=nO(X);return` ${V8}${Q}${h} ${K}[${$}]${h} ${Y}% (${V} / ${W})`}async function Qh(){if(await X9("jq"))return!0;return process.stdout.write(`${A0}Error: jq is required but not installed.${h}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -1236,4 +1236,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1236
1236
  `),2}case"start":{let{runStart:Y}=await Promise.resolve().then(() => (g_(),v_));return Y(Q)}default:return process.stderr.write(`Unknown command: ${X}
1237
1237
  `),process.stderr.write(m_),2}}lO();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var dV0=await pV0(Bun.argv.slice(2));process.exit(dV0);
1238
1238
 
1239
- //# debugId=45400DDB79710D0464756E2164756E21
1239
+ //# debugId=2BABAC8DCF0D367564756E2164756E21
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.22.3'
78
+ __version__ = '9.22.5'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "9.22.3",
4
+ "version": "9.22.5",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "9.22.3",
5
+ "version": "9.22.5",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -13,6 +13,7 @@ import tempfile
13
13
  OK, REFUSED, USAGE, NO_INPUT = 0, 3, 64, 66
14
14
  DOMAIN = "loki-outcome-canary-decision-receipt/v1"
15
15
  MAX_BYTES = 5 * 1024 * 1024
16
+ VERDICTS = {"PROMOTE", "HOLD", "ROLLBACK"}
16
17
 
17
18
 
18
19
  class Parser(argparse.ArgumentParser):
@@ -106,7 +107,8 @@ def _publish_create_only(path, payload):
106
107
 
107
108
  def create_receipt(report_path, observations_path, receipt_path, control_route,
108
109
  canary_percent=10.0, max_risk=.25, min_samples=5,
109
- min_lift_bps=1, enable_receipt=False):
110
+ min_lift_bps=1, enable_receipt=False,
111
+ require_verdict=None):
110
112
  """Reverify a decision and publish one immutable portable receipt."""
111
113
  result = {
112
114
  "receipt": DOMAIN,
@@ -118,6 +120,14 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
118
120
  if not enable_receipt:
119
121
  result["refusal_reason"] = "receipt_not_enabled"
120
122
  return result
123
+ if require_verdict is not None:
124
+ result.update({
125
+ "required_verdict": require_verdict,
126
+ "requirement_met": False,
127
+ })
128
+ if require_verdict not in VERDICTS:
129
+ result["refusal_reason"] = "required_verdict_invalid"
130
+ return result
121
131
  try:
122
132
  _target_is_absent(receipt_path)
123
133
  evaluator = _load_tool(
@@ -127,11 +137,11 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
127
137
  report_path, observations_path, control_route, canary_percent,
128
138
  max_risk, min_samples, min_lift_bps, True,
129
139
  )
130
- if decision.get("refusal_reasons") or decision.get("verdict") not in {
131
- "PROMOTE", "HOLD", "ROLLBACK"
132
- }:
140
+ if decision.get("refusal_reasons") or decision.get("verdict") not in VERDICTS:
133
141
  result["refusal_reason"] = "evaluation_refused"
134
142
  return result
143
+ if require_verdict is not None:
144
+ result["verdict"] = decision["verdict"]
135
145
 
136
146
  report_bytes = _read_named_regular(report_path)
137
147
  observations_bytes = _read_named_regular(observations_path)
@@ -149,6 +159,9 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
149
159
  if _sha256(source_bytes) != decision["source_sha256"]:
150
160
  result["refusal_reason"] = "source_unsafe_or_drifted"
151
161
  return result
162
+ if require_verdict is not None and decision["verdict"] != require_verdict:
163
+ result["refusal_reason"] = "verdict_mismatch"
164
+ return result
152
165
 
153
166
  receipt = {
154
167
  "receipt": DOMAIN,
@@ -184,6 +197,8 @@ def create_receipt(report_path, observations_path, receipt_path, control_route,
184
197
  "verdict": decision["verdict"],
185
198
  "receipt_sha256": _sha256(payload),
186
199
  })
200
+ if require_verdict is not None:
201
+ result["requirement_met"] = True
187
202
  except FileNotFoundError:
188
203
  result["refusal_reason"] = "input_missing"
189
204
  except (OSError, ValueError, json.JSONDecodeError, UnicodeDecodeError) as exc:
@@ -210,6 +225,7 @@ def main(argv=None):
210
225
  parser.add_argument("--max-risk", type=float, default=.25)
211
226
  parser.add_argument("--min-samples", type=int, default=5)
212
227
  parser.add_argument("--min-lift-bps", type=int, default=1)
228
+ parser.add_argument("--require-verdict", choices=("PROMOTE", "HOLD", "ROLLBACK"))
213
229
  parser.add_argument("--json", action="store_true")
214
230
  args = parser.parse_args(argv)
215
231
  for path in (args.report, args.observations):
@@ -219,15 +235,20 @@ def main(argv=None):
219
235
  result = create_receipt(
220
236
  args.report, args.observations, args.receipt, args.control_route,
221
237
  args.canary_percent, args.max_risk, args.min_samples,
222
- args.min_lift_bps, args.enable_receipt,
238
+ args.min_lift_bps, args.enable_receipt, args.require_verdict,
223
239
  )
224
240
  if args.json:
225
241
  print(json.dumps(result, sort_keys=True))
226
242
  elif result["status"] == "RECORDED":
227
243
  print(f"Canary decision receipt: RECORDED ({result['verdict']})")
228
244
  print(f" sha256={result['receipt_sha256']}")
245
+ if args.require_verdict is not None:
246
+ print(f" required verdict: {args.require_verdict} (MET)")
229
247
  else:
230
248
  print(f"Canary decision receipt: REFUSED ({result['refusal_reason']})")
249
+ if args.require_verdict is not None and result["verdict"] is not None:
250
+ print(f" actual verdict: {result['verdict']}")
251
+ print(f" required verdict: {args.require_verdict} (NOT MET)")
231
252
  return OK if result["status"] == "RECORDED" else REFUSED
232
253
 
233
254
 
package/web-app/server.py CHANGED
@@ -2668,14 +2668,17 @@ async def start_session(req: StartRequest) -> JSONResponse:
2668
2668
  if req.provider:
2669
2669
  build_env["LOKI_PROVIDER"] = req.provider
2670
2670
 
2671
+ # LOKI_CLI is a trusted fixed path; request data stays in an argv
2672
+ # vector and is never parsed as shell command text.
2671
2673
  proc = subprocess.Popen(
2672
- cmd,
2674
+ cmd, # lgtm[py/command-line-injection]
2673
2675
  stdout=subprocess.PIPE,
2674
2676
  stderr=subprocess.STDOUT,
2675
2677
  stdin=subprocess.DEVNULL,
2676
2678
  text=True,
2677
2679
  cwd=project_dir,
2678
2680
  env=build_env,
2681
+ shell=False,
2679
2682
  **({"start_new_session": True} if sys.platform != "win32"
2680
2683
  else {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}),
2681
2684
  )
@@ -4253,8 +4256,10 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
4253
4256
  chat_env.update(_load_secrets())
4254
4257
  # Pass provider via env for quick mode (loki quick uses LOKI_PROVIDER env)
4255
4258
  chat_env["LOKI_PROVIDER"] = chat_provider
4259
+ # loki is resolved from the trusted application install; message,
4260
+ # provider, and PRD path remain argv data with no command shell.
4256
4261
  proc = subprocess.Popen(
4257
- cmd_args,
4262
+ cmd_args, # lgtm[py/command-line-injection]
4258
4263
  stdout=subprocess.PIPE,
4259
4264
  stderr=subprocess.STDOUT,
4260
4265
  stdin=subprocess.DEVNULL,
@@ -4262,6 +4267,7 @@ async def chat_session(session_id: str, req: ChatRequest) -> JSONResponse:
4262
4267
  cwd=str(target),
4263
4268
  env=chat_env,
4264
4269
  start_new_session=True,
4270
+ shell=False,
4265
4271
  )
4266
4272
  task.process = proc
4267
4273
  _track_child_pid(proc.pid)