loki-mode 7.87.0 → 7.89.0

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/autonomy/run.sh CHANGED
@@ -592,6 +592,84 @@ if [ -f "${SCRIPT_DIR}/app-runner.sh" ]; then
592
592
  source "${SCRIPT_DIR}/app-runner.sh"
593
593
  fi
594
594
 
595
+ # Build-time HOME isolation (F49).
596
+ #
597
+ # When Loki executes/tests the GENERATED app during a build (the app-runner
598
+ # server launch, restarts, and the project's own test suite in
599
+ # enforce_test_coverage), the child process inherits Loki's environment --
600
+ # including the user's REAL $HOME. A generated app that defaults its state file
601
+ # to a HOME-relative path (e.g. a todo CLI writing ~/.todo.json) would then
602
+ # litter the user's home directory with Loki's in-build test data. These two
603
+ # helpers give those in-build executions an isolated HOME/XDG/TMPDIR rooted
604
+ # under .loki/ so the app can run normally but cannot write into the real home.
605
+ #
606
+ # Scope: ONLY Loki's own in-build test executions are sandboxed. The user's
607
+ # later manual `npm start`/run of the app is unaffected -- they invoke it
608
+ # themselves in their own shell with their own HOME.
609
+
610
+ # Lazily create and echo a persistent isolated HOME directory under .loki.
611
+ # Persistent (not a per-call mktemp) so an app launched in one iteration and
612
+ # restarted in a later one keeps a stable home, and so its state survives
613
+ # across the build the same way a real run would.
614
+ _loki_app_sandbox_dir() {
615
+ local _base="${TARGET_DIR:-.}/.loki/app-sandbox"
616
+ if [ ! -d "$_base/home" ]; then
617
+ mkdir -p "$_base/home" "$_base/config" "$_base/data" \
618
+ "$_base/cache" "$_base/state" "$_base/tmp" 2>/dev/null || return 1
619
+ fi
620
+ printf '%s\n' "$_base"
621
+ }
622
+
623
+ # Run a command with the build-time app sandbox env applied, then restore the
624
+ # previous environment. Works for both shell functions (app_runner_start) and
625
+ # external commands because it only mutates env vars around the call. The
626
+ # background app launched by app_runner_start captures the sandbox HOME at fork
627
+ # time and keeps it for its lifetime; restoring here ensures the orchestrator
628
+ # (and the next iteration's provider invocation, which needs the REAL HOME for
629
+ # OAuth/credentials) is never left pointing at the sandbox.
630
+ _loki_with_app_sandbox() {
631
+ local _sb
632
+ _sb=$(_loki_app_sandbox_dir 2>/dev/null) || _sb=""
633
+ # If the sandbox could not be created, run unsandboxed rather than failing
634
+ # the build -- correctness of the app run takes priority over isolation.
635
+ if [ -z "$_sb" ]; then
636
+ "$@"
637
+ return $?
638
+ fi
639
+ # Resolve to an absolute path so child `cd` into the target dir cannot make
640
+ # a relative HOME point somewhere unexpected.
641
+ local _abs
642
+ _abs=$(cd "$_sb" 2>/dev/null && pwd) || _abs="$_sb"
643
+
644
+ # Save current values plus their set/unset state so restore is exact.
645
+ local _had_home="${HOME+x}" _old_home="${HOME:-}"
646
+ local _had_xch="${XDG_CONFIG_HOME+x}" _old_xch="${XDG_CONFIG_HOME:-}"
647
+ local _had_xdh="${XDG_DATA_HOME+x}" _old_xdh="${XDG_DATA_HOME:-}"
648
+ local _had_xcache="${XDG_CACHE_HOME+x}" _old_xcache="${XDG_CACHE_HOME:-}"
649
+ local _had_xsh="${XDG_STATE_HOME+x}" _old_xsh="${XDG_STATE_HOME:-}"
650
+ local _had_tmp="${TMPDIR+x}" _old_tmp="${TMPDIR:-}"
651
+
652
+ export HOME="$_abs/home"
653
+ export XDG_CONFIG_HOME="$_abs/config"
654
+ export XDG_DATA_HOME="$_abs/data"
655
+ export XDG_CACHE_HOME="$_abs/cache"
656
+ export XDG_STATE_HOME="$_abs/state"
657
+ export TMPDIR="$_abs/tmp"
658
+
659
+ local _rc=0
660
+ "$@" || _rc=$?
661
+
662
+ # Restore exactly (re-export prior value, or unset if it was unset before).
663
+ if [ -n "$_had_home" ]; then export HOME="$_old_home"; else unset HOME; fi
664
+ if [ -n "$_had_xch" ]; then export XDG_CONFIG_HOME="$_old_xch"; else unset XDG_CONFIG_HOME; fi
665
+ if [ -n "$_had_xdh" ]; then export XDG_DATA_HOME="$_old_xdh"; else unset XDG_DATA_HOME; fi
666
+ if [ -n "$_had_xcache" ]; then export XDG_CACHE_HOME="$_old_xcache"; else unset XDG_CACHE_HOME; fi
667
+ if [ -n "$_had_xsh" ]; then export XDG_STATE_HOME="$_old_xsh"; else unset XDG_STATE_HOME; fi
668
+ if [ -n "$_had_tmp" ]; then export TMPDIR="$_old_tmp"; else unset TMPDIR; fi
669
+
670
+ return "$_rc"
671
+ }
672
+
595
673
  # Playwright Smoke Test module (v5.46.0)
596
674
  if [ -f "${SCRIPT_DIR}/playwright-verify.sh" ]; then
597
675
  # shellcheck source=playwright-verify.sh
@@ -4641,6 +4719,49 @@ check_notification_triggers() {
4641
4719
  # Task Queue Auto-Tracking (for degraded mode providers)
4642
4720
  #===============================================================================
4643
4721
 
4722
+ # Derive a plain-language, honest summary of the spec this run is building from,
4723
+ # for the dashboard iteration card (Task G). Mirrors the "Building:" start
4724
+ # headline (run.sh:17061) and proof-generator's source resolution so the card,
4725
+ # the banner, and the proof artifact all describe the same real intent.
4726
+ #
4727
+ # Echoes two tab-separated fields: <human spec label>\t<spec kind>, where kind is
4728
+ # one of: brief | prd | codebase-analysis. The label is plain English with no
4729
+ # RARV jargon. Best-effort and never fails (used only to enrich a card).
4730
+ _loki_iteration_spec_summary() {
4731
+ local prd="${1:-}"
4732
+ local label="" kind=""
4733
+
4734
+ # 1. A recorded one-line brief (`loki start "<brief>"`) is the user's own
4735
+ # words; show them verbatim, truncated to stay on one tidy line.
4736
+ if [ -f ".loki/state/brief.txt" ]; then
4737
+ local _brief
4738
+ _brief="$(tr '\n' ' ' < .loki/state/brief.txt 2>/dev/null | sed 's/ */ /g; s/^ //; s/ $//')"
4739
+ if [ -n "$_brief" ]; then
4740
+ if [ "${#_brief}" -gt 80 ]; then
4741
+ _brief="${_brief:0:77}..."
4742
+ fi
4743
+ printf '%s\t%s' "$_brief" "brief"
4744
+ return 0
4745
+ fi
4746
+ fi
4747
+
4748
+ # 2. A generated PRD means there was no user spec; we are reverse-engineering
4749
+ # one from the code. Say exactly that rather than naming the internal file.
4750
+ case "$prd" in
4751
+ ""|*.loki/generated-prd.md|*.loki/generated-prd.json)
4752
+ label="the codebase (no spec provided)"
4753
+ kind="codebase-analysis"
4754
+ ;;
4755
+ *)
4756
+ # 3. A real user PRD / spec file: name it by basename, no path noise.
4757
+ label="$(basename "$prd" 2>/dev/null || printf '%s' "$prd")"
4758
+ kind="prd"
4759
+ ;;
4760
+ esac
4761
+
4762
+ printf '%s\t%s' "$label" "$kind"
4763
+ }
4764
+
4644
4765
  # Track iteration start - create task in in-progress queue
4645
4766
  track_iteration_start() {
4646
4767
  local iteration="$1"
@@ -4694,6 +4815,19 @@ except: pass
4694
4815
  local prd_escaped
4695
4816
  prd_escaped=$(printf '%s' "${prd:-Codebase Analysis}" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
4696
4817
 
4818
+ # Task G: derive a plain-language, honest summary of what this run is building
4819
+ # from, so the card title/description describe the real work instead of the
4820
+ # generic "Iteration N" / "RARV iteration N" placeholder. Best-effort.
4821
+ local _spec_label="" _spec_kind=""
4822
+ local _spec_summary
4823
+ _spec_summary=$(_loki_iteration_spec_summary "$prd")
4824
+ _spec_label="${_spec_summary%%$'\t'*}"
4825
+ _spec_kind="${_spec_summary##*$'\t'}"
4826
+ # Escape for safe embedding in the python -c literals below.
4827
+ local _spec_label_esc _spec_kind_esc
4828
+ _spec_label_esc=$(printf '%s' "$_spec_label" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
4829
+ _spec_kind_esc=$(printf '%s' "$_spec_kind" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g')
4830
+
4697
4831
  # Build enriched task JSON with pending task context.
4698
4832
  # Must initialize to empty: this script runs under `set -u` (line 152),
4699
4833
  # so `local task_json` without a value leaves it unset. When the pending
@@ -4704,25 +4838,33 @@ except: pass
4704
4838
  task_json=$(python3 -c "
4705
4839
  import json, sys
4706
4840
  ctx = json.loads('''$next_task_context''')
4707
- # v7.5.12: always emit acceptance_criteria, notes, logs so the dashboard
4708
- # task model has consistent shape. default_ac covers the RARV gate-pass
4709
- # requirements when no PRD-provided list exists.
4710
- default_ac = [
4711
- 'REASON phase identifies next task without errors',
4712
- 'ACT phase produces verifiable artifacts (code/docs/tests)',
4713
- 'REFLECT phase records progress in CONTINUITY.md',
4714
- 'VERIFY phase passes automated tests / quality gates'
4715
- ]
4841
+ # Task G: the card must describe the REAL work in plain language, not the
4842
+ # generic 'Iteration N' / 'RARV iteration N' placeholder. Prefer the pending
4843
+ # PRD task's own title/description/criteria. When those are absent, fall back to
4844
+ # a plain-language summary derived from the spec this run is building from, and
4845
+ # OMIT acceptance criteria rather than show RARV phase-name boilerplate (only
4846
+ # state what is really known).
4847
+ spec_label = '${_spec_label_esc}'
4848
+ spec_kind = '${_spec_kind_esc}'
4849
+ if spec_kind == 'codebase-analysis':
4850
+ fallback_title = 'Analyzing the codebase and generating a spec'
4851
+ fallback_desc = 'Reading the existing code to reverse-engineer a spec, then building against it.'
4852
+ elif spec_kind == 'brief':
4853
+ fallback_title = 'Building: ' + spec_label
4854
+ fallback_desc = 'Building from your brief: ' + spec_label
4855
+ else:
4856
+ fallback_title = 'Building from ' + spec_label
4857
+ fallback_desc = 'Implementing the spec in ' + spec_label + ' and verifying it.'
4716
4858
  task = {
4717
4859
  'id': 'iteration-$iteration',
4718
4860
  'type': 'iteration',
4719
- 'title': ctx.get('current_task') or 'Iteration $iteration',
4720
- 'description': ctx.get('description') or 'RARV iteration $iteration. PRD: ${prd_escaped}',
4861
+ 'title': ctx.get('current_task') or fallback_title,
4862
+ 'description': ctx.get('description') or fallback_desc,
4721
4863
  'status': 'in_progress',
4722
4864
  'priority': 'medium',
4723
4865
  'startedAt': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
4724
4866
  'provider': '${PROVIDER_NAME:-claude}',
4725
- 'acceptance_criteria': ctx.get('acceptance_criteria') or default_ac,
4867
+ 'acceptance_criteria': ctx.get('acceptance_criteria') or [],
4726
4868
  'notes': [],
4727
4869
  'logs': [{
4728
4870
  'timestamp': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
@@ -4742,29 +4884,75 @@ print(json.dumps(task, indent=2))
4742
4884
  " 2>/dev/null) || task_json=""
4743
4885
  fi
4744
4886
 
4745
- # Fallback to basic task JSON if enrichment failed
4887
+ # Fallback when there is no pending PRD task (codebase-analysis run or empty
4888
+ # queue). Task G: this was the worst placeholder card -- 'Iteration N' /
4889
+ # 'RARV iteration N' with RARV-phase-name acceptance criteria, which tells a
4890
+ # watching user nothing about the real work. Build an honest, plain-language
4891
+ # card from the spec summary instead, and omit acceptance criteria (we have no
4892
+ # real per-iteration criteria here -- do not invent RARV jargon).
4746
4893
  if [[ -z "${task_json:-}" ]]; then
4747
4894
  local _start_ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
4895
+ task_json=$(python3 -c "
4896
+ import json
4897
+ spec_label = '${_spec_label_esc}'
4898
+ spec_kind = '${_spec_kind_esc}'
4899
+ if spec_kind == 'codebase-analysis':
4900
+ title = 'Analyzing the codebase and generating a spec'
4901
+ desc = 'Reading the existing code to reverse-engineer a spec, then building against it.'
4902
+ elif spec_kind == 'brief':
4903
+ title = 'Building: ' + spec_label
4904
+ desc = 'Building from your brief: ' + spec_label
4905
+ else:
4906
+ title = 'Building from ' + spec_label
4907
+ desc = 'Implementing the spec in ' + spec_label + ' and verifying it.'
4908
+ task = {
4909
+ 'id': '$task_id',
4910
+ 'type': 'iteration',
4911
+ 'title': title,
4912
+ 'description': desc,
4913
+ 'status': 'in_progress',
4914
+ 'priority': 'medium',
4915
+ 'startedAt': '$_start_ts',
4916
+ 'provider': '${PROVIDER_NAME:-claude}',
4917
+ 'acceptance_criteria': [],
4918
+ 'notes': [],
4919
+ 'logs': [{
4920
+ 'timestamp': '$_start_ts',
4921
+ 'iteration': $iteration,
4922
+ 'level': 'info',
4923
+ 'phase': 'BOOTSTRAP',
4924
+ 'message': 'Iteration $iteration started'
4925
+ }]
4926
+ }
4927
+ print(json.dumps(task, indent=2))
4928
+ " 2>/dev/null) || task_json=""
4929
+ fi
4930
+
4931
+ # Last-resort safety net if python is unavailable. Still avoids the generic
4932
+ # 'RARV iteration' wording and omits boilerplate acceptance criteria.
4933
+ if [[ -z "${task_json:-}" ]]; then
4934
+ local _start_ts2="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
4935
+ local _safe_title
4936
+ case "$_spec_kind" in
4937
+ codebase-analysis) _safe_title="Analyzing the codebase and generating a spec" ;;
4938
+ brief) _safe_title="Building from your brief" ;;
4939
+ *) _safe_title="Building from the spec" ;;
4940
+ esac
4748
4941
  task_json=$(cat <<EOF
4749
4942
  {
4750
4943
  "id": "$task_id",
4751
4944
  "type": "iteration",
4752
- "title": "Iteration $iteration",
4753
- "description": "RARV iteration $iteration. PRD: ${prd_escaped}",
4945
+ "title": "$_safe_title",
4946
+ "description": "$_safe_title (iteration $iteration).",
4754
4947
  "status": "in_progress",
4755
4948
  "priority": "medium",
4756
- "startedAt": "$_start_ts",
4949
+ "startedAt": "$_start_ts2",
4757
4950
  "provider": "${PROVIDER_NAME:-claude}",
4758
- "acceptance_criteria": [
4759
- "REASON phase identifies next task without errors",
4760
- "ACT phase produces verifiable artifacts (code/docs/tests)",
4761
- "REFLECT phase records progress in CONTINUITY.md",
4762
- "VERIFY phase passes automated tests / quality gates"
4763
- ],
4951
+ "acceptance_criteria": [],
4764
4952
  "notes": [],
4765
4953
  "logs": [
4766
4954
  {
4767
- "timestamp": "$_start_ts",
4955
+ "timestamp": "$_start_ts2",
4768
4956
  "iteration": $iteration,
4769
4957
  "level": "info",
4770
4958
  "phase": "BOOTSTRAP",
@@ -7907,9 +8095,57 @@ sys.stdout.write(t.strip())
7907
8095
  # unit-tests.pass is only read for the status-line display (run.sh ~2183,
7908
8096
  # PASS vs PENDING); keeping the touch preserves the historical
7909
8097
  # non-blocking behavior for legitimate no-test projects.
8098
+ #
8099
+ # F56/F53 (verification-gap honesty): a generated project that shipped
8100
+ # source but no runnable tests previously recorded a bare "not_run" and
8101
+ # the gate passed through silently -- so a real logic bug (F53: todo-id
8102
+ # numbering) reached the receipt unverified, and a TESTING.md that
8103
+ # DESCRIBES tests shipped with NONE executed (F56: test docs without test
8104
+ # execution). We do not scaffold+run tests here (generating correct test
8105
+ # code for an arbitrary project is the agent's job, not the gate's, and
8106
+ # would overreach a pass/fail gate). Instead we make the no-runner record
8107
+ # HONEST about the gap: if the project has generated source, the gap is
8108
+ # "source present, no runnable tests"; if a TESTING.md also claims tests,
8109
+ # the gap is the stronger "test docs without execution" mismatch. The
8110
+ # record stays non-blocking (runner=="none" short-circuits the evidence
8111
+ # gate as before; no infinite hang), but the receipt and the operator now
8112
+ # SEE the gap instead of a silent pass, and "no tests" never reads as
8113
+ # "tests verified".
8114
+ local _vgap="none" _vgap_summary="No test runner detected"
8115
+ local _has_src=false _has_testdoc=false
8116
+ # Generated source present? Look only at top-of-tree, skipping vendored /
8117
+ # tooling dirs, so this is cheap and never walks node_modules.
8118
+ if find "${TARGET_DIR:-.}" -maxdepth 3 -type f \
8119
+ \( -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.jsx' \
8120
+ -o -name '*.tsx' -o -name '*.go' -o -name '*.rs' -o -name '*.java' \
8121
+ -o -name '*.rb' -o -name '*.php' \) \
8122
+ -not -path '*/node_modules/*' -not -path '*/.loki/*' \
8123
+ -not -path '*/.git/*' -not -path '*/vendor/*' -not -path '*/dist/*' \
8124
+ -not -path '*/build/*' -not -path '*/.venv/*' -not -path '*/venv/*' \
8125
+ -print -quit 2>/dev/null | grep -q .; then
8126
+ _has_src=true
8127
+ fi
8128
+ # TESTING.md (project root or .loki/docs) that documents tests despite no
8129
+ # runner -- the docs-without-execution mismatch.
8130
+ local _td
8131
+ for _td in "${TARGET_DIR:-.}/TESTING.md" "${TARGET_DIR:-.}/.loki/docs/TESTING.md"; do
8132
+ if [ -f "$_td" ] && grep -qiE 'test|coverage' "$_td" 2>/dev/null; then
8133
+ _has_testdoc=true
8134
+ break
8135
+ fi
8136
+ done
8137
+ if [ "$_has_testdoc" = "true" ]; then
8138
+ _vgap="test_docs_without_execution"
8139
+ _vgap_summary="TESTING.md documents tests but no runner ran any (unverified)"
8140
+ log_warn "Verification gap: TESTING.md describes tests but no test runner executed -- shipping test docs without test execution"
8141
+ elif [ "$_has_src" = "true" ]; then
8142
+ _vgap="source_without_tests"
8143
+ _vgap_summary="Source present but no runnable tests detected (unverified logic)"
8144
+ log_warn "Verification gap: generated source present but no runnable tests -- logic is unverified by execution"
8145
+ fi
7910
8146
  touch "$quality_dir/unit-tests.pass"
7911
8147
  cat > "$quality_dir/test-results.json" << TREOF
7912
- {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":"inconclusive","summary":"No test runner detected","command":null,"exit_code":null,"status":"not_run","passed_count":null,"failed_count":null}
8148
+ {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"none","pass":"inconclusive","summary":"$_vgap_summary","command":null,"exit_code":null,"status":"not_run","passed_count":null,"failed_count":null,"verification_gap":"$_vgap"}
7913
8149
  TREOF
7914
8150
  # Finding #598: stamp the per-iteration freshness marker so a later
7915
8151
  # completion-route capture (ensure_completion_test_evidence) reuses this
@@ -7946,8 +8182,11 @@ TREOF
7946
8182
  [ -n "$_tr_passed_n" ] || _tr_passed_n=null
7947
8183
  [ -n "$_tr_failed_n" ] || _tr_failed_n=null
7948
8184
 
8185
+ # verification_gap is "none" whenever a real runner executed: the suite ran,
8186
+ # so there is no docs-without-execution / source-without-tests gap. Keeping
8187
+ # the key on BOTH writers gives consumers a single stable schema shape.
7949
8188
  cat > "$quality_dir/test-results.json" << TREOF
7950
- {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"$test_runner","pass":$test_passed,"min_coverage":$min_coverage,"summary":"$details","command":"$_tr_cmd","exit_code":$_tr_exit,"status":"$_tr_status","passed_count":$_tr_passed_n,"failed_count":$_tr_failed_n}
8189
+ {"timestamp":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","runner":"$test_runner","pass":$test_passed,"min_coverage":$min_coverage,"summary":"$details","command":"$_tr_cmd","exit_code":$_tr_exit,"status":"$_tr_status","passed_count":$_tr_passed_n,"failed_count":$_tr_failed_n,"verification_gap":"none"}
7951
8190
  TREOF
7952
8191
  # Finding #598: stamp the per-iteration freshness marker (see above).
7953
8192
  printf '%s\n' "${ITERATION_COUNT:-0}" > "$quality_dir/.test-results.iter" 2>/dev/null || true
@@ -8177,7 +8416,9 @@ ensure_completion_test_evidence() {
8177
8416
  [ -f "$_results_file" ] && _mtime_before=$(stat -f %m "$_results_file" 2>/dev/null || stat -c %Y "$_results_file" 2>/dev/null || echo "")
8178
8417
  # The gate decides on the persisted file; a red suite (nonzero rc) is expected
8179
8418
  # and must not abort the completion path here.
8180
- enforce_test_coverage || true
8419
+ # F49: the project's test suite may exec the app, which can write into HOME;
8420
+ # run it under the isolated build-time HOME.
8421
+ _loki_with_app_sandbox enforce_test_coverage || true
8181
8422
  mkdir -p "$quality_dir" 2>/dev/null || true
8182
8423
  local _mtime_after=""
8183
8424
  [ -f "$_results_file" ] && _mtime_after=$(stat -f %m "$_results_file" 2>/dev/null || stat -c %Y "$_results_file" 2>/dev/null || echo "")
@@ -11757,6 +11998,94 @@ ${_commits}"
11757
11998
  }
11758
11999
 
11759
12000
 
12001
+ # Auto-wiki regeneration after an iteration (v7.88.2). Mirrors the
12002
+ # _intelligent_usage_regen contract: best-effort, NON-blocking, never fails the
12003
+ # iteration (every path returns 0; the caller also guards with `|| true`).
12004
+ #
12005
+ # Default-ON; opt out with LOKI_WIKI_AUTO=0. Off-TTY / CI safe: no prompts, no
12006
+ # stream-fighting, byte-identical behavior (no TTY checks, no interactive paths;
12007
+ # the generator is deterministic given the same codebase index).
12008
+ #
12009
+ # INCREMENTAL change-detection (E3): regenerating the wiki walks + hashes the
12010
+ # source set, which is wasted work on an iteration that did not change the
12011
+ # codebase structure. We avoid even spawning python on an unchanged repo with a
12012
+ # cheap bash-level signal: a hash over the source file list and their mtimes,
12013
+ # cached at .loki/wiki/.auto-hash. We only invoke the generator when that hash
12014
+ # differs from the cached value. The generator ALSO has its own content-hash
12015
+ # signature gate (wiki-manifest.json), so this is a fast pre-filter, not the
12016
+ # only guard -- if the cheap signal ever misfires, the generator still skips a
12017
+ # truly-unchanged codebase. The cheap signal uses find (file list + mtimes),
12018
+ # which is far cheaper than the generator's full byte-hash of every file.
12019
+ _auto_wiki_regen() {
12020
+ # Opt-out honored first so a disabled run does zero work.
12021
+ if [ "${LOKI_WIKI_AUTO:-1}" = "0" ]; then
12022
+ return 0
12023
+ fi
12024
+ local target_dir="${TARGET_DIR:-.}"
12025
+ # python3 is required for the generator; bail silently if absent.
12026
+ if ! command -v python3 >/dev/null 2>&1; then
12027
+ return 0
12028
+ fi
12029
+ local gen="$SCRIPT_DIR/lib/wiki-generator.py"
12030
+ if [ ! -f "$gen" ]; then
12031
+ return 0
12032
+ fi
12033
+
12034
+ # Cheap structure signal: a hash over (path, mtime, size) of every source
12035
+ # file, excluding the usual noise dirs (and .loki itself, so writing the
12036
+ # wiki never changes the signal). Computed with a small stat-only python
12037
+ # walk -- portable across macOS/BSD and Linux (GNU vs BSD `find -printf`
12038
+ # diverge), and far cheaper than the generator's full byte-hash of every
12039
+ # file because it reads no file contents. python3 is already required above.
12040
+ local hash_file="$target_dir/.loki/wiki/.auto-hash"
12041
+ local cur_hash
12042
+ cur_hash=$(python3 - "$target_dir" <<'PY' 2>/dev/null || true
12043
+ import hashlib, os, sys
12044
+ root = sys.argv[1]
12045
+ SKIP = {"node_modules", ".git", ".loki", "dist", "build", "venv", ".venv",
12046
+ "__pycache__", "target", "vendor", ".next", ".cache", "out"}
12047
+ h = hashlib.sha256()
12048
+ for dirpath, dirnames, filenames in os.walk(root):
12049
+ dirnames[:] = sorted(d for d in dirnames if d not in SKIP)
12050
+ for fn in sorted(filenames):
12051
+ p = os.path.join(dirpath, fn)
12052
+ try:
12053
+ st = os.stat(p)
12054
+ except OSError:
12055
+ continue
12056
+ rel = os.path.relpath(p, root)
12057
+ h.update(("%s|%d|%d\n" % (rel, int(st.st_mtime), st.st_size)).encode("utf-8", "replace"))
12058
+ sys.stdout.write(h.hexdigest())
12059
+ PY
12060
+ )
12061
+ if [ -n "$cur_hash" ] && [ -f "$hash_file" ]; then
12062
+ local prev_hash
12063
+ prev_hash=$(cat "$hash_file" 2>/dev/null || echo "")
12064
+ if [ "$cur_hash" = "$prev_hash" ]; then
12065
+ # Structure unchanged since the last wiki -- skip without spawning
12066
+ # the generator. No token burn on an unchanged repo.
12067
+ return 0
12068
+ fi
12069
+ fi
12070
+
12071
+ log_info "Auto-regenerating project wiki (codebase structure changed)..."
12072
+ # The generator is deterministic and self-incremental; --quiet keeps the
12073
+ # iteration log clean. Never let a wiki failure surface to the iteration.
12074
+ if python3 "$gen" --root "$target_dir" --quiet >/dev/null 2>&1; then
12075
+ # Record the cheap signal only on a successful generation so a failed
12076
+ # run is retried next iteration rather than silently skipped.
12077
+ if [ -n "$cur_hash" ]; then
12078
+ mkdir -p "$target_dir/.loki/wiki" 2>/dev/null || true
12079
+ printf '%s\n' "$cur_hash" > "$hash_file" 2>/dev/null || true
12080
+ fi
12081
+ log_info "Project wiki refreshed -> $target_dir/.loki/wiki/"
12082
+ else
12083
+ log_info "Auto-wiki regen skipped (generator returned non-zero); keeping existing wiki."
12084
+ fi
12085
+ return 0
12086
+ }
12087
+
12088
+
11760
12089
  # Magic Modules COMPOUND: record successful component patterns (v6.77.0)
11761
12090
  # Called at end of each iteration to capture generated/updated components
11762
12091
  # as semantic memory patterns via magic.core.memory_bridge.
@@ -12465,6 +12794,24 @@ build_prompt() {
12465
12794
  # and to the dashboard/Purple Lab UI.
12466
12795
  local usage_doc_instruction="USAGE_DOC_REQUIRED: Before invoking loki_complete_task (or touching .loki/signals/COMPLETION_REQUESTED), write USAGE.md at the project root. Detect the stack from package.json/requirements.txt/Cargo.toml/go.mod/etc. and include these sections: (1) Prerequisites (runtimes, ports, env vars), (2) Install (exact command, e.g. 'npm install' or 'pip install -r requirements.txt'), (3) Start (exact command, e.g. 'npm start' or 'python server.py'), (4) Verify -- 2 to 3 copy-paste commands the user can run to confirm it works (curl examples for APIs with expected output, browser URL for web UIs, command invocation for CLIs), (5) Stop (Ctrl+C or 'lsof -ti:PORT | xargs kill -9' for backgrounded servers). Keep it under 100 lines, plain Markdown, no emojis. If USAGE.md already exists and is accurate, leave it; otherwise create or update it."
12467
12796
 
12797
+ # DOC_SCOPE instruction (F52): scale generated documentation to the detected
12798
+ # project complexity. A trivial one-file app does not warrant a nine-file
12799
+ # architecture suite (ARCHITECTURE/COMPONENTS/DECISIONS/API/SETUP/TESTING) --
12800
+ # that is token and iteration burn with no reader. USAGE.md (above) and
12801
+ # HANDOFF.md (rendered post-completion) are written regardless of tier, so this
12802
+ # string only governs the OPTIONAL architecture suite. simple -> minimal set;
12803
+ # standard/complex -> full suite only where it genuinely helps a reader.
12804
+ # MUST stay byte-identical to DOC_SCOPE_INSTRUCTION_{SIMPLE,FULL} in
12805
+ # loki-ts/src/runner/build_prompt.ts (parity-locked, same precedent as
12806
+ # COMPOSE_INSTRUCTION). Tier is read from DETECTED_COMPLEXITY (set once per run
12807
+ # by run_autonomous before the first build_prompt); cache-stable within a run.
12808
+ local doc_scope_instruction
12809
+ if [ "${DETECTED_COMPLEXITY:-standard}" = "simple" ]; then
12810
+ doc_scope_instruction="DOC_SCOPE: This is a small, simple project. Keep documentation minimal and proportional: a short README.md (what it is, install, run) plus the required USAGE.md is sufficient. Do NOT generate a multi-file architecture documentation suite (ARCHITECTURE.md, COMPONENTS.md, DECISIONS.md, API.md, SETUP.md, TESTING.md) for a project this small -- it is overhead with no reader and wastes iterations. Only add one of those files if the code genuinely warrants it (e.g. write API.md only when there is a real public API surface). Never claim a doc exists that you did not write."
12811
+ else
12812
+ doc_scope_instruction="DOC_SCOPE: Scale documentation to what a reader of THIS project actually needs -- do not generate empty boilerplate. Beyond the required USAGE.md, write a README.md and add architecture-suite docs (ARCHITECTURE.md, COMPONENTS.md, DECISIONS.md, API.md, SETUP.md, TESTING.md) only where each one carries real, project-specific content (e.g. API.md only with a real public API, COMPONENTS.md only with multiple distinct components, DECISIONS.md only when there are non-obvious design decisions). Prefer fewer accurate docs over a complete-looking suite of stubs. Never claim a doc exists that you did not write."
12813
+ fi
12814
+
12468
12815
  # v7.7.8: LSP grounding instruction. The lsp-proxy MCP server (auto-mounted
12469
12816
  # when a language server is on PATH) exposes four tools that ground the
12470
12817
  # agent in real workspace symbols instead of hallucinated names. Before
@@ -12883,15 +13230,15 @@ except Exception:
12883
13230
  else
12884
13231
  if [ $retry -eq 0 ]; then
12885
13232
  if [ -n "$prd" ]; then
12886
- echo "Loki Mode with PRD at $prd. $update_instruction $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
13233
+ echo "Loki Mode with PRD at $prd. $update_instruction $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $doc_scope_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12887
13234
  else
12888
- echo "Loki Mode. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
13235
+ echo "Loki Mode. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $usage_doc_instruction $doc_scope_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12889
13236
  fi
12890
13237
  else
12891
13238
  if [ -n "$prd" ]; then
12892
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
13239
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $usage_doc_instruction $doc_scope_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12893
13240
  else
12894
- echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
13241
+ echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $assumption_context $queue_tasks $bmad_context $openspec_context $mirofish_context $magic_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $usage_doc_instruction $doc_scope_instruction $compose_instruction $lsp_grounding_instruction $agents_md_instruction $completion_instruction $sdlc_instruction $autonomous_suffix"
12895
13242
  fi
12896
13243
  fi
12897
13244
  fi
@@ -12933,6 +13280,7 @@ except Exception:
12933
13280
  printf 'You are a coding assistant. Analyze this codebase and suggest improvements. Write working code and commit changes.\n'
12934
13281
  fi
12935
13282
  printf '%s\n' "$usage_doc_instruction"
13283
+ printf '%s\n' "$doc_scope_instruction"
12936
13284
  printf '%s\n' "$compose_instruction"
12937
13285
  printf '%s\n' "$lsp_grounding_instruction"
12938
13286
  printf '%s\n' "$agents_md_instruction"
@@ -12967,6 +13315,7 @@ except Exception:
12967
13315
  printf '%s\n' "$autonomous_suffix"
12968
13316
  printf '%s\n' "$memory_instruction"
12969
13317
  printf '%s\n' "$usage_doc_instruction"
13318
+ printf '%s\n' "$doc_scope_instruction"
12970
13319
  printf '%s\n' "$compose_instruction"
12971
13320
  printf '%s\n' "$lsp_grounding_instruction"
12972
13321
  printf '%s\n' "$agents_md_instruction"
@@ -15343,7 +15692,9 @@ if __name__ == "__main__":
15343
15692
  if [ "${APP_RUNNER_INITIALIZED:-}" != "true" ] && [ $exit_code -eq 0 ] && \
15344
15693
  [ "${LOKI_APP_RUNNER:-true}" = "true" ] && type app_runner_init &>/dev/null; then
15345
15694
  if app_runner_init; then
15346
- app_runner_start || log_warn "App runner: failed to start application"
15695
+ # F49: sandbox the generated app's HOME so its in-build run
15696
+ # cannot write into the user's real home directory.
15697
+ _loki_with_app_sandbox app_runner_start || log_warn "App runner: failed to start application"
15347
15698
  APP_RUNNER_INITIALIZED=true
15348
15699
  fi
15349
15700
  fi
@@ -15353,7 +15704,8 @@ if __name__ == "__main__":
15353
15704
  # App Runner: restart on code changes (v5.45.0)
15354
15705
  if [ "${APP_RUNNER_INITIALIZED:-}" = "true" ] && type app_runner_should_restart &>/dev/null; then
15355
15706
  if app_runner_should_restart; then
15356
- app_runner_restart || log_warn "App runner: failed to restart application"
15707
+ # F49: re-launch under the same isolated HOME as the initial start.
15708
+ _loki_with_app_sandbox app_runner_restart || log_warn "App runner: failed to restart application"
15357
15709
  fi
15358
15710
  fi
15359
15711
 
@@ -15378,7 +15730,8 @@ if __name__ == "__main__":
15378
15730
  if [ -f ".loki/app-runner/restart-signal" ]; then
15379
15731
  rm -f ".loki/app-runner/restart-signal"
15380
15732
  log_info "App runner: restart signal received from dashboard"
15381
- app_runner_restart || true
15733
+ # F49: dashboard-triggered restart uses the isolated HOME too.
15734
+ _loki_with_app_sandbox app_runner_restart || true
15382
15735
  fi
15383
15736
  if [ -f ".loki/app-runner/stop-signal" ]; then
15384
15737
  rm -f ".loki/app-runner/stop-signal"
@@ -15438,7 +15791,9 @@ if __name__ == "__main__":
15438
15791
  # Test coverage gate
15439
15792
  if [ "${PHASE_UNIT_TESTS:-true}" = "true" ]; then
15440
15793
  log_info "Quality gate: test suite (pass/fail)..."
15441
- if enforce_test_coverage; then
15794
+ # F49: isolate HOME so the project's suite cannot pollute the
15795
+ # user's real home when it execs the generated app.
15796
+ if _loki_with_app_sandbox enforce_test_coverage; then
15442
15797
  clear_gate_failure "test_coverage"
15443
15798
  else
15444
15799
  local tc_count
@@ -15746,6 +16101,12 @@ else:
15746
16101
  # Magic Modules COMPOUND capture (v6.77.0): record component patterns
15747
16102
  _magic_compound_capture
15748
16103
 
16104
+ # Auto-wiki regeneration after every iteration (v7.88.2). Best-effort,
16105
+ # non-blocking, incremental (only regenerates when the codebase
16106
+ # structure changed). Default-on; opt out LOKI_WIKI_AUTO=0. The `|| true`
16107
+ # is belt-and-suspenders -- the function already returns 0 on every path.
16108
+ _auto_wiki_regen 2>/dev/null || true
16109
+
15749
16110
  # BUG-QG-008: Track iteration for convergence regardless of exit code
15750
16111
  if type council_track_iteration &>/dev/null; then
15751
16112
  council_track_iteration "$log_file"
@@ -17367,6 +17728,30 @@ main() {
17367
17728
  generate_proof_of_run "$result" || true
17368
17729
  fi
17369
17730
 
17731
+ # Finish-and-own (v7.88.0): write a plain-English ownership handoff
17732
+ # (HANDOFF.md) for a non-technical owner. Runs AFTER the proof so the
17733
+ # "is it working?" verdict reads the receipt's honest headline. Default-on,
17734
+ # opt out with LOKI_HANDOFF=0. Fire-and-forget: best-effort, never blocks
17735
+ # completion (same contract as the proof + usage-regen). A pure render over
17736
+ # the proof + completion + USAGE.md, so it cannot fabricate.
17737
+ if [ "${LOKI_HANDOFF:-1}" != "0" ]; then
17738
+ local _own_render="$SCRIPT_DIR/lib/own-render.py"
17739
+ if [ -f "$_own_render" ] && command -v python3 >/dev/null 2>&1; then
17740
+ # The renderer prints the plain-English doc on stdout (--md); the hook
17741
+ # places it at the project root as HANDOFF.md. Write to a temp then
17742
+ # move, so a partial write never leaves a truncated HANDOFF.md.
17743
+ local _handoff_dir _handoff_md _handoff_tmp
17744
+ _handoff_dir="${TARGET_DIR:-.}"
17745
+ _handoff_md="$_handoff_dir/HANDOFF.md"
17746
+ _handoff_tmp="$_handoff_dir/.HANDOFF.md.tmp"
17747
+ if python3 "$_own_render" --loki-dir "$LOKI_DIR" --md > "$_handoff_tmp" 2>/dev/null; then
17748
+ mv -f "$_handoff_tmp" "$_handoff_md" 2>/dev/null || rm -f "$_handoff_tmp" 2>/dev/null || true
17749
+ else
17750
+ rm -f "$_handoff_tmp" 2>/dev/null || true
17751
+ fi
17752
+ fi
17753
+ fi
17754
+
17370
17755
  # R7 (zero-config first run): "what next / go deeper" framing. Only when the
17371
17756
  # CLI flagged this as a TTFV first run and stdout is a TTY, so it stays
17372
17757
  # silent in CI / pipes and never fires for normal PRD runs. The wording