loki-mode 11.17.1 → 11.17.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +3 -3
- package/VERSION +1 -1
- package/autonomy/grill.sh +24 -2
- package/autonomy/hooks/precompact-flush.sh +51 -0
- package/autonomy/hooks/validate-bash.sh +895 -5
- package/autonomy/lib/chat.sh +8 -3
- package/autonomy/lib/claude-flags.sh +143 -6
- package/autonomy/lib/claude_permissions.py +24 -4
- package/autonomy/lib/decision_record.py +92 -3
- package/autonomy/lib/efficiency_cost.py +102 -0
- package/autonomy/lib/modernize/py_capture.py +62 -3
- package/autonomy/lib/prd-enrich.sh +28 -2
- package/autonomy/lib/voter-agents.sh +61 -6
- package/autonomy/lib/wiki-generator.py +58 -10
- package/autonomy/lib/wiki_llm.py +41 -2
- package/autonomy/loki +391 -28
- package/autonomy/quickstart.sh +1 -1
- package/autonomy/run.sh +201 -10
- package/autonomy/trigger-server.py +70 -2
- package/completions/_loki +3 -0
- package/completions/loki.bash +2 -1
- package/dashboard/__init__.py +1 -1
- package/dashboard/audit.py +204 -17
- package/docs/CLI-REFERENCE.md +4 -1
- package/docs/EVALUATING.md +2 -2
- package/docs/INSTALLATION.md +1 -1
- package/docs/environment-variables.md +1 -0
- package/docs/trackers.md +6 -0
- package/docs/v10/BOARD.md +7 -4
- package/docs/v10/CLI-MODERN.md +8 -5
- package/docs/v10/DECISIONS.md +31 -1
- package/docs/v10/ENGINE.md +1 -0
- package/docs/v10/FAILURE-CLASSES.md +60 -1
- package/docs/v10/FOUNDER-QUEUE.md +1 -0
- package/docs/v10/GUIDE-MODERNIZE.md +9 -4
- package/docs/v10/PLAN-V12.md +896 -0
- package/docs/v10/PROGRESS.md +10 -0
- package/docs/v11/COST-HALF.md +3 -1
- package/docs/visual-evidence.md +2 -0
- package/loki-ts/dist/loki.js +1104 -1057
- package/mcp/__init__.py +1 -1
- package/memory/retrieval.py +15 -3
- package/memory/schemas.py +43 -1
- package/memory/storage.py +11 -0
- package/package.json +2 -2
- package/packages/control-plane/dist/server.js +989 -693
- package/packages/control-plane/src/server/legacy/routes-control.ts +5 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/providers/claude.sh +1 -0
- package/providers/loader.sh +26 -0
- package/providers/model_routes.sh +31 -0
- package/src/policies/claude_permissions.js +20 -2
- package/web-app/dist/assets/{AdminPage-OVzN6eLm.js → AdminPage-CCedYmn3.js} +1 -1
- package/web-app/dist/assets/{Avatar-hCCrxrCZ.js → Avatar-DMrDsuGe.js} +1 -1
- package/web-app/dist/assets/{Badge-CDcpkhbJ.js → Badge-K4Ui2tJz.js} +1 -1
- package/web-app/dist/assets/{Button-8mwWlhEu.js → Button-Bhjts5QZ.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-D8cf3u22.js → CockpitPage-Be7sEVqh.js} +1 -1
- package/web-app/dist/assets/{ComparePage-CIgkbDRY.js → ComparePage-D4ty3jiF.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-99kJH9g2.js → ErrorBoundary-Cmt2x_mL.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-C9fj32eS.js → EvidenceReceiptPanel-D2IXLnp9.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-De43YQ_C.js → GitHubIssuesPanel-CyXitr6h.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-7g7ZTnkx.js → GitHubPRsPanel-RRM3H-xd.js} +1 -1
- package/web-app/dist/assets/{HomePage-CcNA24ny.js → HomePage-CMtrrgfs.js} +2 -2
- package/web-app/dist/assets/{LoginPage-BYjlb-IK.js → LoginPage-BrzNhnMJ.js} +1 -1
- package/web-app/dist/assets/{MagicPage-C0Dwt9Na.js → MagicPage-D7VynOKM.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-jDGmdcgg.js → MetricsPage-DJ-gDbfP.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-C_ZO8tLE.js → NotFoundPage-BJh0XVTj.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-B5wnurfQ.js → ProjectPage-CAodVUpE.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage--EyKt5-D.js → ProjectsPage-m3siS0Rf.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-cB0lnfti.js → SettingsPage-CFGgjOvy.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-Dl16FDkK.js → ShowcasePage-Dd9tpSKJ.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-BmAXd4V2.js → SystemSettingsPage-DXd0PtTI.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-BFkWVOZe.js → TeamsPage-C5KaLIGW.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-Bf2kmEUM.js → TemplatesPage-CDbzE0ZZ.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-BItv9Wa6.js → TerminalOutput-CKG79r_-.js} +1 -1
- package/web-app/dist/assets/{activity-Df2Awg6I.js → activity-BaQQRhDp.js} +1 -1
- package/web-app/dist/assets/{bell-D-1ztTsJ.js → bell-OeFqvU4e.js} +1 -1
- package/web-app/dist/assets/{bot-A4qgZsKi.js → bot-CSxaA0KU.js} +1 -1
- package/web-app/dist/assets/{check-BPfFpF0c.js → check-DJml2Xdn.js} +1 -1
- package/web-app/dist/assets/{chevron-left-CfSJvmDi.js → chevron-left-DGnt-DtK.js} +1 -1
- package/web-app/dist/assets/{circle-alert-wx2d1dzb.js → circle-alert-BV5rVL7K.js} +1 -1
- package/web-app/dist/assets/{clock-pQMai6l-.js → clock-CZ7SvULz.js} +1 -1
- package/web-app/dist/assets/{cloud-DWL470r6.js → cloud-lkIzrZps.js} +1 -1
- package/web-app/dist/assets/{code-xml-BaEQpQLZ.js → code-xml-DzA2wL8F.js} +1 -1
- package/web-app/dist/assets/{database-h-N7WkQN.js → database-CLFG5upC.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-B5oYiD27.js → dollar-sign-DgWePGsv.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-fbTtTy0o.js → file-code-corner-DGQX2bmv.js} +1 -1
- package/web-app/dist/assets/{file-plus-PaF6HMo8.js → file-plus-LkaXeYd6.js} +1 -1
- package/web-app/dist/assets/{globe-Ct-GlWID.js → globe-rSsEzPYv.js} +1 -1
- package/web-app/dist/assets/{hammer-C1oZFX51.js → hammer-CPogONt9.js} +1 -1
- package/web-app/dist/assets/{index-C2egrRhz.js → index-DpBLuMuz.js} +3 -3
- package/web-app/dist/assets/{layers-Dt5ZK4v2.js → layers-CwFaqidn.js} +1 -1
- package/web-app/dist/assets/{loader-circle-DrpMziic.js → loader-circle-C43izEm5.js} +1 -1
- package/web-app/dist/assets/{lock-DOez8MmK.js → lock-Bc_Uqxpp.js} +1 -1
- package/web-app/dist/assets/{package-ClGIkerr.js → package-BM25lcR-.js} +1 -1
- package/web-app/dist/assets/{plus-C004i9ua.js → plus-D0xoiY6N.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-Ddfk-7Sy.js → refresh-cw-COI1R5Oo.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-Bu1KgDjS.js → rotate-ccw-BoNZx4xe.js} +1 -1
- package/web-app/dist/assets/{scroll-text-3q1lW5pm.js → scroll-text-Cakp8XHd.js} +1 -1
- package/web-app/dist/assets/{server-pd6x4HW1.js → server-CjfIlPmE.js} +1 -1
- package/web-app/dist/assets/{shield-alert-B9jGJepT.js → shield-alert-BilhziJF.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-Cdcc9nQf.js → thumbs-up-CglMrtrt.js} +1 -1
- package/web-app/dist/assets/{trash-2-foSojXHK.js → trash-2-B6zVvnoG.js} +1 -1
- package/web-app/dist/assets/{trending-down-CTTiqlJ1.js → trending-down-BVhwD9bt.js} +1 -1
- package/web-app/dist/assets/{trending-up-B_MgmRAx.js → trending-up-CVJ1ox6c.js} +1 -1
- package/web-app/dist/assets/{upload-g6BfALzG.js → upload-DmuR09WL.js} +1 -1
- package/web-app/dist/assets/{usePolling-DrHwvzuh.js → usePolling-BTALfO91.js} +1 -1
- package/web-app/dist/assets/{user-COp75m7l.js → user-CRdHMZNH.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/autonomy/loki
CHANGED
|
@@ -134,15 +134,15 @@ _loki_caveman_narration_level() {
|
|
|
134
134
|
|
|
135
135
|
# P12-26b: permission flags for the migrate/doc/heal claude launches. Lazily sources
|
|
136
136
|
# claude-flags.sh (like the caveman wrapper above) and fills _LOKI_PERM_ARGV; unset
|
|
137
|
-
# LOKI_PERMISSION_POLICY keeps --dangerously-skip-permissions byte-identically.
|
|
137
|
+
# LOKI_PERMISSION_POLICY=legacy keeps --dangerously-skip-permissions byte-identically.
|
|
138
138
|
_loki_perm_argv_for() {
|
|
139
139
|
_LOKI_PERM_ARGV=(--dangerously-skip-permissions)
|
|
140
|
-
#
|
|
141
|
-
[ "${LOKI_PERMISSION_POLICY:-}" = "
|
|
140
|
+
# Legacy policy: return before sourcing anything (claude-flags.sh exports caveman vars).
|
|
141
|
+
[ "${LOKI_PERMISSION_POLICY:-}" = "legacy" ] && return 0
|
|
142
142
|
_LOKI_PERM_ARGV=(--permission-mode plan)
|
|
143
143
|
local _pa_lib="${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" _pa_out _pa_a
|
|
144
144
|
if [ ! -f "$_pa_lib" ]; then
|
|
145
|
-
echo "[loki] WARNING:
|
|
145
|
+
echo "[loki] WARNING: compiled permission policy: helper missing; using most restrictive mode (plan)" >&2
|
|
146
146
|
return 0
|
|
147
147
|
fi
|
|
148
148
|
# Subshell so the helper's top-level exports never leak into this shell.
|
|
@@ -1825,6 +1825,7 @@ show_help_all() {
|
|
|
1825
1825
|
echo " stop Stop execution immediately"
|
|
1826
1826
|
echo " pause Pause after current session"
|
|
1827
1827
|
echo " resume Resume a paused or interrupted run (after Ctrl-C or a crash)"
|
|
1828
|
+
echo " cancel Cancel the active Loki 10 run at its next stage boundary"
|
|
1828
1829
|
echo " steer \"<note>\" Nudge a running build (Loki 10 engine; legacy loop needs LOKI_PROMPT_INJECTION=true)"
|
|
1829
1830
|
echo ""
|
|
1830
1831
|
echo "Verify / trust:"
|
|
@@ -1957,7 +1958,7 @@ show_help_all() {
|
|
|
1957
1958
|
echo ""
|
|
1958
1959
|
echo "All commands:"
|
|
1959
1960
|
echo " agents chat diff edit models route"
|
|
1960
|
-
echo " acp agent analyze api assets audit backlog bench checkpoint (cp) ci cleanup"
|
|
1961
|
+
echo " acp agent analyze api assets audit backlog backup bench cancel checkpoint (cp) ci cleanup"
|
|
1961
1962
|
echo " cluster cockpit code compliance completions compound config context (ctx)"
|
|
1962
1963
|
echo " cost council crash dashboard demo deploy docker docs doctor dogfood"
|
|
1963
1964
|
echo " enterprise estimate explain export failover github grill heal help"
|
|
@@ -1965,7 +1966,7 @@ show_help_all() {
|
|
|
1965
1966
|
echo " issue keys kpis logs magic mcp memory metrics migrate modernize monitor"
|
|
1966
1967
|
echo " next notify onboard open optimize otel outcomes own (handoff) pause plan preview"
|
|
1967
1968
|
echo " projects proof (receipt) provider quick quickstart rc remote report reset"
|
|
1968
|
-
echo " merge resume review rollback run sandbox secrets secure self-update sentrux"
|
|
1969
|
+
echo " merge restore resume review rollback run sandbox secrets secure self-update sentrux"
|
|
1969
1970
|
echo " serve setup setup-skill share ship spec start state stats status steer stop"
|
|
1970
1971
|
echo " syslog telemetry template test tour trigger trust trust-metrics"
|
|
1971
1972
|
echo " ultracode undo update verify version voice watch watchdog web welcome why"
|
|
@@ -2547,23 +2548,33 @@ _loki_start_handoff() {
|
|
|
2547
2548
|
}
|
|
2548
2549
|
|
|
2549
2550
|
# Start Loki Mode
|
|
2550
|
-
#
|
|
2551
|
-
#
|
|
2552
|
-
#
|
|
2553
|
-
#
|
|
2554
|
-
#
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2551
|
+
# The ONE allowlist of cmd_start flags. _loki_start_flag_kind sets
|
|
2552
|
+
# _LOKI_START_FLAG_KIND to value | optional | bool, or returns 1 for an unknown
|
|
2553
|
+
# flag. The --workspace brief scan and the --resume scan both read it, so a value
|
|
2554
|
+
# is never mistaken for a brief or run id and an unknown flag exits 64 like the
|
|
2555
|
+
# parser's own catch-all. tests/test-cli-allow-haiku-flag.sh checks it against the
|
|
2556
|
+
# cmd_start case arms, so the parser and this list cannot drift apart. --model and
|
|
2557
|
+
# the other Bun/engine10 flags are handled by bin/loki before cmd_start runs, so
|
|
2558
|
+
# they are unknown to cmd_start's pre-scans by design.
|
|
2559
|
+
_loki_start_flag_kind() {
|
|
2560
|
+
_LOKI_START_FLAG_KIND=""
|
|
2561
|
+
case "${1%%=*}" in
|
|
2562
|
+
--provider|--brief|--budget|--prd|--issue|--isolation|--aider-model|--aider-flags|--cline-model|--compliance|--max-duration|--log-level|--bmad-project|--openspec|--mirofish-rounds|--mirofish-timeout|--output|--config|--vars|--env-file|--remote|--attempts|--mirofish-docker) _LOKI_START_FLAG_KIND=value ;;
|
|
2563
|
+
--mirofish) _LOKI_START_FLAG_KIND=optional ;;
|
|
2564
|
+
--parallel|--allow-haiku|--regen-prd|--regenerate-prd|--regen|--fresh-prd|--bg|--background|--simple|--complex|--github|--no-dashboard|--api|--sandbox|--skip-memory|--yes|-y|--quiet|--mirofish-bg|--no-mirofish|--no-plan|--fast|--repo-fast|--full|--repo-full|--dry-run|--no-start|--worktree|-w|--pr|--prepare-pr|--ship|--detach|-d|--help|-h|--resume|--) _LOKI_START_FLAG_KIND=bool ;;
|
|
2565
|
+
*) return 1 ;;
|
|
2558
2566
|
esac
|
|
2559
|
-
|
|
2567
|
+
# A "--flag=value" token carries its own value, so it never consumes the next one.
|
|
2568
|
+
if [[ "$1" == --*=* ]]; then _LOKI_START_FLAG_KIND=bool; fi
|
|
2569
|
+
return 0
|
|
2560
2570
|
}
|
|
2561
2571
|
|
|
2562
2572
|
# Optional-value cmd_start flags (--mirofish [URL]). The one decision on whether
|
|
2563
2573
|
# the NEXT token is the flag's value: non-empty and not another --flag. The
|
|
2564
2574
|
# cmd_start --mirofish arm and both scans use it, so they cannot disagree.
|
|
2565
|
-
_loki_start_flag_optional_value() { [ "$1" = "--mirofish" ]; }
|
|
2566
2575
|
_loki_start_optval_consumes() { [[ -n "${1:-}" && "${1:-}" != --* ]]; }
|
|
2576
|
+
# Same message and exit code as the cmd_start parser's catch-all arm.
|
|
2577
|
+
_loki_start_unknown_flag() { echo -e "${RED}Unknown option: $1${NC}"; exit 64; }
|
|
2567
2578
|
|
|
2568
2579
|
# One exit-code mapping for an engine10 resume, shared by cmd_resume and
|
|
2569
2580
|
# `loki start --resume`. 0 and 4 (phase reported) -> 0, 5 (publish failed) -> 1.
|
|
@@ -2648,8 +2659,13 @@ cmd_start() {
|
|
|
2648
2659
|
case "$_ps_a" in
|
|
2649
2660
|
--brief=*) _ps_brief+=("${_ps_a#*=}") ;;
|
|
2650
2661
|
--brief) _ps_skip=2 ;;
|
|
2662
|
+
# bin/loki turns --model into --session-model and forwards --max-cost on
|
|
2663
|
+
# the legacy route; a workspace start ignores both. Skip them and their value.
|
|
2664
|
+
--session-model|--max-cost) _ps_skip=1 ;;
|
|
2665
|
+
--session-model=*|--max-cost=*) ;;
|
|
2651
2666
|
# Every value-taking cmd_start flag: its value is never brief text.
|
|
2652
|
-
-*)
|
|
2667
|
+
-*) _loki_start_flag_kind "$_ps_a" || _loki_start_unknown_flag "$_ps_a"
|
|
2668
|
+
if [ "$_LOKI_START_FLAG_KIND" = value ]; then _ps_skip=1; elif [ "$_LOKI_START_FLAG_KIND" = optional ]; then _ps_skip=3; fi ;;
|
|
2653
2669
|
*) _ps_brief+=("$_ps_a") ;;
|
|
2654
2670
|
esac
|
|
2655
2671
|
done
|
|
@@ -2677,17 +2693,22 @@ cmd_start() {
|
|
|
2677
2693
|
# P12-07b: a bare `loki start --resume [run-id]` goes to the same engine10
|
|
2678
2694
|
# resume path as `loki resume` (util/run_outputs.ts resumeMain), never into a
|
|
2679
2695
|
# new build. Exit codes go through _loki_e10_resume_map (0 and 4 -> 0, 5 -> 1); any other code, such as 3 for nothing to resume, is returned as is.
|
|
2680
|
-
local _ps_resume=0 _ps_resume_args=() _ps_rskip=0
|
|
2696
|
+
local _ps_resume=0 _ps_resume_args=() _ps_rskip=0 _ps_unknown=""
|
|
2681
2697
|
for _ps_a in ${_ps_rest[@]+"${_ps_rest[@]}"}; do
|
|
2682
2698
|
if [ "$_ps_rskip" = 1 ]; then _ps_rskip=0; continue; fi
|
|
2683
2699
|
if [ "$_ps_rskip" = 3 ]; then _ps_rskip=0; if _loki_start_optval_consumes "$_ps_a"; then continue; fi; fi
|
|
2684
2700
|
case "$_ps_a" in
|
|
2685
2701
|
--resume) _ps_resume=1 ;;
|
|
2686
|
-
|
|
2687
|
-
|
|
2702
|
+
-*) if ! _loki_start_flag_kind "$_ps_a"; then
|
|
2703
|
+
[ -n "$_ps_unknown" ] || _ps_unknown="$_ps_a"
|
|
2704
|
+
elif [ "$_LOKI_START_FLAG_KIND" = value ]; then _ps_rskip=1
|
|
2705
|
+
elif [ "$_LOKI_START_FLAG_KIND" = optional ]; then _ps_rskip=3
|
|
2706
|
+
fi ;;
|
|
2688
2707
|
*) _ps_resume_args+=("$_ps_a") ;;
|
|
2689
2708
|
esac
|
|
2690
2709
|
done
|
|
2710
|
+
# Without --resume the cmd_start parser reports the same error itself.
|
|
2711
|
+
if [ "$_ps_resume" = 1 ] && [ -n "$_ps_unknown" ]; then _loki_start_unknown_flag "$_ps_unknown"; fi
|
|
2691
2712
|
if [ "$_ps_resume" = 1 ]; then
|
|
2692
2713
|
local _ps_rrc=0
|
|
2693
2714
|
for _ps_a in ${_ps_resume_args[@]+"${_ps_resume_args[@]}"}; do
|
|
@@ -5098,6 +5119,39 @@ cmd_cleanup() {
|
|
|
5098
5119
|
fi
|
|
5099
5120
|
}
|
|
5100
5121
|
|
|
5122
|
+
# P12-10b: Loki 10 run control. True when the newest engine10 run has no run.completed yet. A pending request
|
|
5123
|
+
# ("$1" = pause) additionally requires control-request.json to hold that action.
|
|
5124
|
+
_loki_e10_active_run() {
|
|
5125
|
+
local last="" d
|
|
5126
|
+
for d in "$LOKI_DIR"/runs/e10-*; do [ -d "$d" ] && last="$d"; done
|
|
5127
|
+
[ -n "$last" ] && [ -f "$last/events.jsonl" ] || return 1
|
|
5128
|
+
! grep -q '"type":"run.completed"' "$last/events.jsonl" 2>/dev/null || return 1
|
|
5129
|
+
[ -z "${1:-}" ] || grep -q "\"action\":\"$1\"" "$last/control-request.json" 2>/dev/null
|
|
5130
|
+
}
|
|
5131
|
+
|
|
5132
|
+
# loki cancel: asks the active Loki 10 run to stop at its next stage boundary; the run ends CANCELLED with no PR.
|
|
5133
|
+
cmd_cancel() {
|
|
5134
|
+
case "${1:-}" in
|
|
5135
|
+
--help|-h)
|
|
5136
|
+
echo -e "${BOLD}Loki Mode -- cancel the active Loki 10 run${NC}"
|
|
5137
|
+
echo ""
|
|
5138
|
+
echo "Usage: loki cancel"
|
|
5139
|
+
echo ""
|
|
5140
|
+
echo "Writes a cancel request that the engine reads at the next stage boundary."
|
|
5141
|
+
echo "The run then completes as CANCELLED (run.completed with cancelled:true)."
|
|
5142
|
+
echo ""
|
|
5143
|
+
echo "See also: loki pause, loki resume, loki stop"
|
|
5144
|
+
return 0
|
|
5145
|
+
;;
|
|
5146
|
+
esac
|
|
5147
|
+
if _loki_e10_active_run; then
|
|
5148
|
+
( LOKI_DIR="$LOKI_DIR" _loki_bun_delegate cancel )
|
|
5149
|
+
return $?
|
|
5150
|
+
fi
|
|
5151
|
+
echo -e "${YELLOW}No active Loki 10 run to cancel.${NC}"
|
|
5152
|
+
return 1
|
|
5153
|
+
}
|
|
5154
|
+
|
|
5101
5155
|
# Pause after current session
|
|
5102
5156
|
cmd_pause() {
|
|
5103
5157
|
# v7.6.2 B-13 fix: --help must print help, not act on session state.
|
|
@@ -5123,6 +5177,13 @@ cmd_pause() {
|
|
|
5123
5177
|
exit 0
|
|
5124
5178
|
fi
|
|
5125
5179
|
|
|
5180
|
+
# P12-10b: an active Loki 10 run takes a stage-boundary request; anything else keeps the legacy PAUSE file.
|
|
5181
|
+
if _loki_e10_active_run; then
|
|
5182
|
+
local _pz_rc=0
|
|
5183
|
+
( LOKI_DIR="$LOKI_DIR" _loki_bun_delegate pause ) || _pz_rc=$?
|
|
5184
|
+
[ "$_pz_rc" -ne 0 ] || return 0
|
|
5185
|
+
fi
|
|
5186
|
+
|
|
5126
5187
|
if is_session_running; then
|
|
5127
5188
|
# Warn if running in perpetual mode where PAUSE is auto-cleared (#84)
|
|
5128
5189
|
local current_mode="${LOKI_AUTONOMY_MODE:-perpetual}"
|
|
@@ -5379,6 +5440,13 @@ cmd_resume() {
|
|
|
5379
5440
|
exit 0
|
|
5380
5441
|
fi
|
|
5381
5442
|
|
|
5443
|
+
# P12-10b: a Loki 10 run paused by `loki pause` is released by clearing its request.
|
|
5444
|
+
if _loki_e10_active_run pause; then
|
|
5445
|
+
local _rz_rc=0
|
|
5446
|
+
( LOKI_DIR="$LOKI_DIR" _loki_bun_delegate resume ) || _rz_rc=$?
|
|
5447
|
+
[ "$_rz_rc" -ne 0 ] || return 0
|
|
5448
|
+
fi
|
|
5449
|
+
|
|
5382
5450
|
# RECOVERY: an interrupted engine10 run is resumed at its phase by loki-ts/src/util/run_outputs.ts
|
|
5383
5451
|
# (exit 0 resumed, 4 phase reported, 5 publish failed, anything else: nothing for it, fall through).
|
|
5384
5452
|
if ls "$LOKI_DIR"/runs/e10-*/events.jsonl >/dev/null 2>&1; then
|
|
@@ -6646,6 +6714,22 @@ cmd_status() {
|
|
|
6646
6714
|
fi
|
|
6647
6715
|
fi
|
|
6648
6716
|
|
|
6717
|
+
# P12-19 (D04): freshness of the newest Loki 10 run, same wording as the Bun route
|
|
6718
|
+
# ("last event Ns ago (seq N)"). Display-only; silent when there is no run or no parseable event.
|
|
6719
|
+
local _e10_last _e10_ev _e10_seq _e10_ts _e10_epoch
|
|
6720
|
+
_e10_last=$(ls -d "$LOKI_DIR"/runs/e10-*/ 2>/dev/null | sort | tail -1) || _e10_last=""
|
|
6721
|
+
_e10_ev="${_e10_last%/}/events.jsonl"
|
|
6722
|
+
if [ -n "$_e10_last" ] && [ -s "$_e10_ev" ]; then
|
|
6723
|
+
_e10_seq=$(tail -n 1 "$_e10_ev" | jq -r '.seq // empty' 2>/dev/null) || _e10_seq=""
|
|
6724
|
+
_e10_ts=$(tail -n 1 "$_e10_ev" | jq -r '.ts // empty' 2>/dev/null) || _e10_ts=""
|
|
6725
|
+
_e10_epoch=$(python3 -c "import sys,datetime; print(int(datetime.datetime.fromisoformat(sys.argv[1].replace('Z','+00:00')).timestamp()))" "$_e10_ts" 2>/dev/null) || _e10_epoch=""
|
|
6726
|
+
if [ -n "$_e10_seq" ] && [ -n "$_e10_epoch" ]; then
|
|
6727
|
+
local _e10_age=$(( $(date +%s) - _e10_epoch ))
|
|
6728
|
+
[ "$_e10_age" -lt 0 ] && _e10_age=0
|
|
6729
|
+
echo -e "${CYAN}Freshness:${NC} last event ${_e10_age}s ago (seq ${_e10_seq})"
|
|
6730
|
+
fi
|
|
6731
|
+
fi
|
|
6732
|
+
|
|
6649
6733
|
# v7.82: surface the single most valuable, most shareable fact -- where the
|
|
6650
6734
|
# built app is running -- when the app runner reports a live app. Display-only
|
|
6651
6735
|
# read of .loki/app-runner/state.json (same shape build_completion_summary
|
|
@@ -9889,7 +9973,7 @@ _deploy_latest_receipt() {
|
|
|
9889
9973
|
return 0
|
|
9890
9974
|
}
|
|
9891
9975
|
|
|
9892
|
-
# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha]
|
|
9976
|
+
# _deploy_record <dir> <command> <exit_code> <run_id> <anchor_sha> [state] [health_status] [health_url] [rb_attempted] [rb_exit] [receipt_sha256] [started_at] [finished_at] [target] [canary] [post_rollback_health] [health_http_code] [health_match] [tree_sha] [destination]
|
|
9893
9977
|
# Writes the executed deploy's own record, then signs it (loki.deploy/1, Ed25519 via the
|
|
9894
9978
|
# receipt signer in loki-ts/src/util/receipt_signer.ts) so it is bound to the receipt that
|
|
9895
9979
|
# authorized it; `loki verify <record>` re-checks it offline. An unsigned record is reported
|
|
@@ -9912,7 +9996,7 @@ _deploy_record() {
|
|
|
9912
9996
|
local health_status="${7:-unchecked}" health_url="${8:-}"
|
|
9913
9997
|
local rb_attempted="${9:-false}" rb_exit="${10:-}"
|
|
9914
9998
|
local receipt_sha="${11:-}" started_at="${12:-}" finished_at="${13:-}" target="${14:-}" canary="${15:-false}"
|
|
9915
|
-
local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}"
|
|
9999
|
+
local post_rb="${16:-unchecked}" hcode_rec="${17:-}" hmatch="${18:-}" tree_sha="${19:-}" dest_rec="${20:-}"
|
|
9916
10000
|
local ddir="$dir/.loki/deploys"
|
|
9917
10001
|
[ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
|
|
9918
10002
|
mkdir -p "$ddir" 2>/dev/null || return 0
|
|
@@ -9920,7 +10004,7 @@ _deploy_record() {
|
|
|
9920
10004
|
DR_CMD="$command" DR_RC="$rc" DR_RUN="$run_id" DR_ANCHOR="$anchor" DR_OUT="$f" \
|
|
9921
10005
|
DR_STATE="$anchor_state" DR_HS="$health_status" DR_HU="$health_url" \
|
|
9922
10006
|
DR_RBA="$rb_attempted" DR_RBX="$rb_exit" DR_RSHA="$receipt_sha" DR_T0="$started_at" \
|
|
9923
|
-
DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" \
|
|
10007
|
+
DR_T1="$finished_at" DR_TGT="$target" DR_CAN="$canary" DR_PRB="$post_rb" DR_HC="$hcode_rec" DR_HM="$hmatch" DR_TREE="$tree_sha" DR_DEST="$dest_rec" \
|
|
9924
10008
|
python3 -c '
|
|
9925
10009
|
import json, os, datetime
|
|
9926
10010
|
state = os.environ.get("DR_STATE") or "anchored"
|
|
@@ -9949,6 +10033,7 @@ json.dump({
|
|
|
9949
10033
|
"post_rollback_health": os.environ.get("DR_PRB") or "unchecked",
|
|
9950
10034
|
"health_match": os.environ.get("DR_HM", ""),
|
|
9951
10035
|
"tree_sha": os.environ.get("DR_TREE", ""),
|
|
10036
|
+
"destination": os.environ.get("DR_DEST", ""),
|
|
9952
10037
|
"health_http_code": int(os.environ["DR_HC"]) if os.environ.get("DR_HC", "").isdigit() else None,
|
|
9953
10038
|
}, open(os.environ["DR_OUT"], "w"), indent=2)
|
|
9954
10039
|
' 2>/dev/null || return 0
|
|
@@ -10097,6 +10182,26 @@ _deploy_policy_refusal() {
|
|
|
10097
10182
|
return 0
|
|
10098
10183
|
}
|
|
10099
10184
|
|
|
10185
|
+
# _deploy_staging_receipt_ok <dir> <sha>
|
|
10186
|
+
# Returns 0 only when a staging deploy record for exactly <sha> exists in
|
|
10187
|
+
# .loki/deploys/, exited 0, did not fail its health probe, and `loki verify`
|
|
10188
|
+
# reports VERIFIED. Anything that cannot be evaluated is "no" (fail closed).
|
|
10189
|
+
_deploy_staging_receipt_ok() {
|
|
10190
|
+
local dir="${1:-.}" sha="${2:-}" ddir="$1/.loki/deploys" f
|
|
10191
|
+
[ -n "$sha" ] || return 1
|
|
10192
|
+
[ -d "${LOKI_DIR:-}" ] && ddir="${LOKI_DIR}/deploys"
|
|
10193
|
+
[ -d "$ddir" ] || return 1
|
|
10194
|
+
for f in "$ddir"/*.json; do
|
|
10195
|
+
[ -f "$f" ] || continue
|
|
10196
|
+
python3 -c 'import json,sys
|
|
10197
|
+
r=json.load(open(sys.argv[1]))
|
|
10198
|
+
ok=r.get("schema")=="loki.deploy/1" and r.get("destination")=="staging" and r.get("anchor_sha")==sys.argv[2] and r.get("exit_code")==0 and r.get("health_status")!="FAILED_HEALTH"
|
|
10199
|
+
sys.exit(0 if ok else 1)' "$f" "$sha" 2>/dev/null || continue
|
|
10200
|
+
( _loki_bun_delegate engine10 verify "$f" 2>&1 ) | grep -q "^verdict: VERIFIED" && return 0
|
|
10201
|
+
done
|
|
10202
|
+
return 1
|
|
10203
|
+
}
|
|
10204
|
+
|
|
10100
10205
|
# _deploy_try_execute <dir> <type>
|
|
10101
10206
|
# The ONLY place a deploy command is ever run. Evaluates the gate, and either
|
|
10102
10207
|
# executes + records, or REFUSES naming every failed check. Returns the deploy
|
|
@@ -10133,6 +10238,19 @@ _deploy_try_execute() {
|
|
|
10133
10238
|
esac
|
|
10134
10239
|
fi
|
|
10135
10240
|
|
|
10241
|
+
# preview/staging use a non-production verb; only Vercel documents one here.
|
|
10242
|
+
local dest="${_LOKI_DEPLOY_DESTINATION:-}"
|
|
10243
|
+
if [ "$dest" = preview ] || [ "$dest" = staging ]; then
|
|
10244
|
+
case "$command" in
|
|
10245
|
+
"vercel --prod") exec_cmd="vercel" ;;
|
|
10246
|
+
*)
|
|
10247
|
+
echo -e "${RED}REFUSED: --destination ${dest} is not supported for: ${command}${NC}" >&2
|
|
10248
|
+
echo " No non-production verb is documented for this provider (Vercel: 'vercel' without --prod). Nothing was run." >&2
|
|
10249
|
+
return 1
|
|
10250
|
+
;;
|
|
10251
|
+
esac
|
|
10252
|
+
fi
|
|
10253
|
+
|
|
10136
10254
|
local reasons
|
|
10137
10255
|
reasons="$(_deploy_gate "$dir" "$command")"
|
|
10138
10256
|
|
|
@@ -10292,7 +10410,7 @@ _deploy_try_execute() {
|
|
|
10292
10410
|
rsha="$(python3 -c 'import json,sys;print((json.load(open(sys.argv[1])).get("verification") or {}).get("hash") or "")' "$pj" 2>/dev/null || printf '')"
|
|
10293
10411
|
rec="$(_deploy_record "$dir" "$exec_cmd" "$rc" "${run_id:-unknown}" "${anchor:-unknown}" \
|
|
10294
10412
|
"$anchor_state" "$health_status" "$health_url" "$rb_attempted" "$rb_exit" \
|
|
10295
|
-
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)")"
|
|
10413
|
+
"$rsha" "$started_at" "$finished_at" "${command%% *}" "$canary" "$post_rb" "$hcode" "$health_match" "$(git -C "$dir" rev-parse 'HEAD^{tree}' 2>/dev/null || true)" "$dest")"
|
|
10296
10414
|
echo ""
|
|
10297
10415
|
if [ "$rc" -eq 0 ]; then
|
|
10298
10416
|
echo -e "${GREEN}Deploy command exited 0.${NC}"
|
|
@@ -10403,6 +10521,12 @@ _deploy_gate() {
|
|
|
10403
10521
|
esac
|
|
10404
10522
|
fi
|
|
10405
10523
|
|
|
10524
|
+
if [ "${_LOKI_DEPLOY_DESTINATION:-}" = production ]; then
|
|
10525
|
+
_deploy_staging_receipt_ok "$dir" "$(git -C "$dir" rev-parse HEAD 2>/dev/null || true)" \
|
|
10526
|
+
|| reasons="${reasons}no VERIFIED staging deploy receipt for this SHA (deploy with --destination staging first)
|
|
10527
|
+
"
|
|
10528
|
+
fi
|
|
10529
|
+
|
|
10406
10530
|
local dirty
|
|
10407
10531
|
dirty="$(_deploy_tree_clean "$dir")"
|
|
10408
10532
|
[ -n "$dirty" ] && reasons="${reasons}${dirty}
|
|
@@ -10434,7 +10558,7 @@ cmd_deploy() {
|
|
|
10434
10558
|
# file alone weakens this gate.
|
|
10435
10559
|
local LOKI_DEPLOY_ALLOW_UNANCHORED=false
|
|
10436
10560
|
# Per-invocation like --execute; local so an ambient env var cannot set them.
|
|
10437
|
-
local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_HEALTH_MATCH="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false
|
|
10561
|
+
local _LOKI_DEPLOY_HEALTH_URL="" _LOKI_DEPLOY_HEALTH_MATCH="" _LOKI_DEPLOY_AUTO_ROLLBACK=false _LOKI_DEPLOY_CANARY=false _LOKI_DEPLOY_DESTINATION=""
|
|
10438
10562
|
|
|
10439
10563
|
# Arg parse (mirror cmd_preview: lenient on unknown args -> no behavior drift).
|
|
10440
10564
|
while [ $# -gt 0 ]; do
|
|
@@ -10442,7 +10566,7 @@ cmd_deploy() {
|
|
|
10442
10566
|
--help|-h|help)
|
|
10443
10567
|
echo -e "${BOLD}Loki Mode -- deploy command advisory (print-only by default)${NC}"
|
|
10444
10568
|
echo ""
|
|
10445
|
-
echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--health-url URL [--health-match STR] [--auto-rollback]]"
|
|
10569
|
+
echo "Usage: loki deploy [--dir <path>] [--no-clip] [--execute [--canary]] [--destination preview|staging|production] [--health-url URL [--health-match STR] [--auto-rollback]]"
|
|
10446
10570
|
echo ""
|
|
10447
10571
|
echo "Detects your project type and your installed cloud CLI (and any"
|
|
10448
10572
|
echo "CI/CD pipeline), then PRINTS the exact deploy command for YOU to run."
|
|
@@ -10528,6 +10652,10 @@ cmd_deploy() {
|
|
|
10528
10652
|
echo " --dir <path> Project directory to scan (default: current dir)"
|
|
10529
10653
|
echo " --no-clip Do not copy the idiomatic command to the clipboard"
|
|
10530
10654
|
echo " --execute Run the deploy command if the receipt gate passes"
|
|
10655
|
+
echo " --destination preview|staging|production"
|
|
10656
|
+
echo " With --execute: preview/staging run a non-production deploy"
|
|
10657
|
+
echo " (Vercel only); production is refused unless a VERIFIED staging"
|
|
10658
|
+
echo " deploy record exists for the same SHA. Unset keeps today's behavior."
|
|
10531
10659
|
echo " --allow-unanchored"
|
|
10532
10660
|
echo " Proceed when the receipt records NO baseline (nothing"
|
|
10533
10661
|
echo " about this tree is proven). Never bypasses a recorded"
|
|
@@ -10559,6 +10687,12 @@ cmd_deploy() {
|
|
|
10559
10687
|
--canary)
|
|
10560
10688
|
_LOKI_DEPLOY_CANARY=true
|
|
10561
10689
|
;;
|
|
10690
|
+
--destination)
|
|
10691
|
+
case "${2:-}" in
|
|
10692
|
+
preview|staging|production) _LOKI_DEPLOY_DESTINATION="$2"; shift ;;
|
|
10693
|
+
*) echo "loki deploy --destination must be one of: preview, staging, production" >&2; return 1 ;;
|
|
10694
|
+
esac
|
|
10695
|
+
;;
|
|
10562
10696
|
--dir)
|
|
10563
10697
|
# Guard against a missing OR flag-shaped value: an unguarded
|
|
10564
10698
|
# shift when --dir is the last arg underflows under set -e, and a
|
|
@@ -10588,6 +10722,10 @@ cmd_deploy() {
|
|
|
10588
10722
|
echo "loki deploy --canary requires --execute" >&2
|
|
10589
10723
|
return 1
|
|
10590
10724
|
fi
|
|
10725
|
+
if [ "$_LOKI_DEPLOY_CANARY" = true ] && [ -n "$_LOKI_DEPLOY_DESTINATION" ] && [ "$_LOKI_DEPLOY_DESTINATION" != production ]; then
|
|
10726
|
+
echo "loki deploy --canary only applies to --destination production" >&2
|
|
10727
|
+
return 1
|
|
10728
|
+
fi
|
|
10591
10729
|
if [ "$do_execute" != true ] && [ -n "$_LOKI_DEPLOY_HEALTH_URL" ]; then
|
|
10592
10730
|
echo "Note: --health-url and --auto-rollback only apply with --execute; ignored on this advisory run." >&2
|
|
10593
10731
|
fi
|
|
@@ -14630,6 +14768,7 @@ cmd_doctor() {
|
|
|
14630
14768
|
echo "loki doctor: the Bun doctor is unavailable, so only this minimal report can run."
|
|
14631
14769
|
echo "Fix: ${_LOKI_DELEGATE_FIX}"
|
|
14632
14770
|
echo "${_LOKI_DELEGATE_WHY}"
|
|
14771
|
+
( source "$SKILL_DIR/providers/model_routes.sh" 2>/dev/null && loki_model_route_lane_report ) || true
|
|
14633
14772
|
fi
|
|
14634
14773
|
return 1
|
|
14635
14774
|
}
|
|
@@ -18632,6 +18771,7 @@ cmd_modernize() {
|
|
|
18632
18771
|
echo " heal <path> Legacy system healing in phases (was: loki heal)"
|
|
18633
18772
|
echo " migrate <path> Codebase migration in phases (was: loki migrate)"
|
|
18634
18773
|
echo " unit --repo <owner/name> --ref <ref> --unit <id> Run one unit (remote unit job worker)"
|
|
18774
|
+
echo " <repo> --to <lang> Full modernization: oracle capture, waves, verify, ship"
|
|
18635
18775
|
echo ""
|
|
18636
18776
|
echo "Run 'loki modernize <subcommand> --help' for each. The old"
|
|
18637
18777
|
echo "top-level names still work; see 'loki help aliases'."
|
|
@@ -21646,6 +21786,9 @@ main() {
|
|
|
21646
21786
|
steer)
|
|
21647
21787
|
cmd_steer "$@"
|
|
21648
21788
|
;;
|
|
21789
|
+
cancel)
|
|
21790
|
+
cmd_cancel "$@"
|
|
21791
|
+
;;
|
|
21649
21792
|
resume)
|
|
21650
21793
|
cmd_resume "$@"
|
|
21651
21794
|
;;
|
|
@@ -21787,6 +21930,12 @@ main() {
|
|
|
21787
21930
|
rollback)
|
|
21788
21931
|
cmd_rollback "$@"
|
|
21789
21932
|
;;
|
|
21933
|
+
backup)
|
|
21934
|
+
cmd_backup "$@"
|
|
21935
|
+
;;
|
|
21936
|
+
restore)
|
|
21937
|
+
cmd_restore "$@"
|
|
21938
|
+
;;
|
|
21790
21939
|
merge)
|
|
21791
21940
|
cmd_merge "$@"
|
|
21792
21941
|
;;
|
|
@@ -26327,6 +26476,178 @@ cmd_merge() {
|
|
|
26327
26476
|
# Restore is destructive on .loki/ state, so it ALWAYS captures a forced
|
|
26328
26477
|
# pre-rollback snapshot first (re-undoability invariant) and then glob-restores
|
|
26329
26478
|
# whatever the checkpoint dir contains (works across all three checkpoint writers).
|
|
26479
|
+
# P12-48: back up and restore the machine-global dashboard state directory
|
|
26480
|
+
# (audit log, retention checkpoint, config). Archive = tar.gz holding
|
|
26481
|
+
# LOKI-BACKUP-MANIFEST (sha256 per file) plus the state tree, with a sidecar
|
|
26482
|
+
# <archive>.sha256 over the whole archive. Restore verifies both before
|
|
26483
|
+
# touching anything and refuses to overwrite existing state without --force.
|
|
26484
|
+
_backup_state_dir() {
|
|
26485
|
+
printf '%s' "${LOKI_DASHBOARD_STATE_DIR:-${HOME}/.loki/dashboard}"
|
|
26486
|
+
}
|
|
26487
|
+
|
|
26488
|
+
_backup_sha256() {
|
|
26489
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
26490
|
+
sha256sum "$1" | awk '{print $1}'
|
|
26491
|
+
else
|
|
26492
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
26493
|
+
fi
|
|
26494
|
+
}
|
|
26495
|
+
|
|
26496
|
+
cmd_backup() {
|
|
26497
|
+
local dir out="" arg
|
|
26498
|
+
dir="$(_backup_state_dir)"
|
|
26499
|
+
while [ $# -gt 0 ]; do
|
|
26500
|
+
arg="$1"; shift
|
|
26501
|
+
case "$arg" in
|
|
26502
|
+
--dir) dir="${1:-}"; shift || true ;;
|
|
26503
|
+
--out) out="${1:-}"; shift || true ;;
|
|
26504
|
+
-h|--help)
|
|
26505
|
+
echo "Usage: loki backup [--dir STATE_DIR] [--out ARCHIVE.tar.gz]"
|
|
26506
|
+
echo " Archives the dashboard state directory (default ~/.loki/dashboard) with a"
|
|
26507
|
+
echo " sha256 manifest and writes ARCHIVE.sha256 next to it."
|
|
26508
|
+
return 0 ;;
|
|
26509
|
+
*) echo -e "${RED}Unknown option: $arg${NC}" >&2; return 2 ;;
|
|
26510
|
+
esac
|
|
26511
|
+
done
|
|
26512
|
+
if [ ! -d "$dir" ]; then
|
|
26513
|
+
echo -e "${RED}Error: state directory not found: $dir${NC}" >&2
|
|
26514
|
+
return 1
|
|
26515
|
+
fi
|
|
26516
|
+
[ -n "$out" ] || out="./loki-backup-$(date -u +%Y%m%d-%H%M%S).tar.gz"
|
|
26517
|
+
if [ -e "$out" ]; then
|
|
26518
|
+
echo -e "${RED}Error: refusing to overwrite existing archive: $out${NC}" >&2
|
|
26519
|
+
return 1
|
|
26520
|
+
fi
|
|
26521
|
+
dir="$(cd "$dir" && pwd -P)" || return 1
|
|
26522
|
+
|
|
26523
|
+
local mdir manifest rel digest count=0
|
|
26524
|
+
mdir="$(mktemp -d "${TMPDIR:-/tmp}/loki-backup.XXXXXXXX")" || return 1
|
|
26525
|
+
manifest="$mdir/LOKI-BACKUP-MANIFEST"
|
|
26526
|
+
: > "$manifest"
|
|
26527
|
+
while IFS= read -r -d '' rel; do
|
|
26528
|
+
digest="$(_backup_sha256 "$dir/$rel")" || { rm -rf -- "$mdir"; return 1; }
|
|
26529
|
+
printf '%s %s\n' "$digest" "$rel" >> "$manifest"
|
|
26530
|
+
count=$((count + 1))
|
|
26531
|
+
done < <(cd "$dir" && find . -type f ! -name '*.pid' -print0 | LC_ALL=C sort -z)
|
|
26532
|
+
|
|
26533
|
+
# Symlinks and special files cannot be verified by the manifest; refuse them.
|
|
26534
|
+
if [ -n "$(cd "$dir" && find . ! -type f ! -type d ! -name '*.pid' -print -quit)" ]; then
|
|
26535
|
+
rm -rf -- "$mdir"
|
|
26536
|
+
echo -e "${RED}Error: $dir holds symlinks or special files; cannot back up safely${NC}" >&2
|
|
26537
|
+
return 1
|
|
26538
|
+
fi
|
|
26539
|
+
if ! tar -czf "$out" --exclude='*.pid' -C "$mdir" LOKI-BACKUP-MANIFEST -C "$dir" .; then
|
|
26540
|
+
rm -f -- "$out"
|
|
26541
|
+
rm -rf -- "$mdir"
|
|
26542
|
+
echo -e "${RED}Error: tar failed${NC}" >&2
|
|
26543
|
+
return 1
|
|
26544
|
+
fi
|
|
26545
|
+
rm -rf -- "$mdir"
|
|
26546
|
+
printf '%s %s\n' "$(_backup_sha256 "$out")" "$(basename "$out")" > "${out}.sha256"
|
|
26547
|
+
echo "Backed up $count file(s) from $dir"
|
|
26548
|
+
echo " archive: $out"
|
|
26549
|
+
echo " sha256: ${out}.sha256"
|
|
26550
|
+
}
|
|
26551
|
+
|
|
26552
|
+
cmd_restore() {
|
|
26553
|
+
local archive="" dir force=0 expect="" arg
|
|
26554
|
+
dir="$(_backup_state_dir)"
|
|
26555
|
+
while [ $# -gt 0 ]; do
|
|
26556
|
+
arg="$1"; shift
|
|
26557
|
+
case "$arg" in
|
|
26558
|
+
--dir) dir="${1:-}"; shift || true ;;
|
|
26559
|
+
--force) force=1 ;;
|
|
26560
|
+
--sha256) expect="${1:-}"; shift || true ;;
|
|
26561
|
+
-h|--help)
|
|
26562
|
+
echo "Usage: loki restore ARCHIVE.tar.gz [--dir STATE_DIR] [--force] [--sha256 HEX]"
|
|
26563
|
+
echo " Verifies the archive (sidecar or --sha256) and its manifest, then restores."
|
|
26564
|
+
echo " Refuses to replace a non-empty STATE_DIR without --force; with --force the"
|
|
26565
|
+
echo " existing directory is moved to STATE_DIR.pre-restore.<timestamp>."
|
|
26566
|
+
return 0 ;;
|
|
26567
|
+
-*) echo -e "${RED}Unknown option: $arg${NC}" >&2; return 2 ;;
|
|
26568
|
+
*) archive="$arg" ;;
|
|
26569
|
+
esac
|
|
26570
|
+
done
|
|
26571
|
+
if [ -z "$archive" ] || [ ! -f "$archive" ]; then
|
|
26572
|
+
echo -e "${RED}Error: archive not found. Usage: loki restore ARCHIVE.tar.gz${NC}" >&2
|
|
26573
|
+
return 2
|
|
26574
|
+
fi
|
|
26575
|
+
|
|
26576
|
+
# 1. Whole-archive digest: explicit --sha256, else the sidecar. Required.
|
|
26577
|
+
if [ -z "$expect" ]; then
|
|
26578
|
+
if [ -f "${archive}.sha256" ]; then
|
|
26579
|
+
expect="$(awk 'NR==1 {print $1}' "${archive}.sha256")"
|
|
26580
|
+
else
|
|
26581
|
+
echo -e "${RED}Error: no ${archive}.sha256 sidecar and no --sha256; refusing unverified restore${NC}" >&2
|
|
26582
|
+
return 1
|
|
26583
|
+
fi
|
|
26584
|
+
fi
|
|
26585
|
+
if [ "$(_backup_sha256 "$archive")" != "$expect" ]; then
|
|
26586
|
+
echo -e "${RED}Error: archive sha256 mismatch (tampered or corrupt); nothing restored${NC}" >&2
|
|
26587
|
+
return 1
|
|
26588
|
+
fi
|
|
26589
|
+
|
|
26590
|
+
# 2. Entry names must be plain relative paths with no symlinks or specials.
|
|
26591
|
+
local listing
|
|
26592
|
+
listing="$(tar -tzf "$archive" 2>/dev/null)" || { echo -e "${RED}Error: unreadable archive${NC}" >&2; return 1; }
|
|
26593
|
+
if printf '%s\n' "$listing" | grep -E -q '(^/|(^|/)\.\.(/|$))'; then
|
|
26594
|
+
echo -e "${RED}Error: archive contains unsafe paths; nothing restored${NC}" >&2
|
|
26595
|
+
return 1
|
|
26596
|
+
fi
|
|
26597
|
+
|
|
26598
|
+
# 3. Extract to a staging dir beside the target (same filesystem), verify the manifest.
|
|
26599
|
+
mkdir -p "$(dirname "$dir")" || return 1
|
|
26600
|
+
local stage
|
|
26601
|
+
stage="$(mktemp -d "${dir}.restore.XXXXXXXX")" || return 1
|
|
26602
|
+
if ! tar -xzf "$archive" -C "$stage" 2>/dev/null; then
|
|
26603
|
+
rm -rf -- "$stage"
|
|
26604
|
+
echo -e "${RED}Error: extraction failed; nothing restored${NC}" >&2
|
|
26605
|
+
return 1
|
|
26606
|
+
fi
|
|
26607
|
+
local bad=""
|
|
26608
|
+
if [ ! -f "$stage/LOKI-BACKUP-MANIFEST" ]; then
|
|
26609
|
+
bad="missing manifest"
|
|
26610
|
+
elif [ -n "$(find "$stage" ! -type f ! -type d -print -quit)" ]; then
|
|
26611
|
+
bad="non-regular entry in archive"
|
|
26612
|
+
else
|
|
26613
|
+
local digest rel n_files n_listed
|
|
26614
|
+
while IFS= read -r line; do
|
|
26615
|
+
digest="${line%% *}"; rel="${line#* }"
|
|
26616
|
+
if [ ! -f "$stage/$rel" ] || [ "$(_backup_sha256 "$stage/$rel")" != "$digest" ]; then
|
|
26617
|
+
bad="manifest mismatch: $rel"; break
|
|
26618
|
+
fi
|
|
26619
|
+
done < "$stage/LOKI-BACKUP-MANIFEST"
|
|
26620
|
+
if [ -z "$bad" ]; then
|
|
26621
|
+
n_files="$(find "$stage" -type f ! -name LOKI-BACKUP-MANIFEST | wc -l | tr -d ' ')"
|
|
26622
|
+
n_listed="$(wc -l < "$stage/LOKI-BACKUP-MANIFEST" | tr -d ' ')"
|
|
26623
|
+
[ "$n_files" = "$n_listed" ] || bad="archive holds files the manifest does not list"
|
|
26624
|
+
fi
|
|
26625
|
+
fi
|
|
26626
|
+
if [ -n "$bad" ]; then
|
|
26627
|
+
rm -rf -- "$stage"
|
|
26628
|
+
echo -e "${RED}Error: archive failed verification ($bad); nothing restored${NC}" >&2
|
|
26629
|
+
return 1
|
|
26630
|
+
fi
|
|
26631
|
+
rm -f -- "$stage/LOKI-BACKUP-MANIFEST"
|
|
26632
|
+
|
|
26633
|
+
# 4. Never overwrite existing state silently.
|
|
26634
|
+
if [ -e "$dir" ] && [ -n "$(ls -A "$dir" 2>/dev/null)" ]; then
|
|
26635
|
+
if [ "$force" -ne 1 ]; then
|
|
26636
|
+
rm -rf -- "$stage"
|
|
26637
|
+
echo -e "${RED}Error: $dir is not empty; re-run with --force to replace it${NC}" >&2
|
|
26638
|
+
return 1
|
|
26639
|
+
fi
|
|
26640
|
+
local aside
|
|
26641
|
+
aside="${dir}.pre-restore.$(date -u +%Y%m%d-%H%M%S)"
|
|
26642
|
+
mv "$dir" "$aside" || { rm -rf -- "$stage"; return 1; }
|
|
26643
|
+
echo " previous state kept at: $aside"
|
|
26644
|
+
elif [ -d "$dir" ]; then
|
|
26645
|
+
rmdir "$dir" 2>/dev/null || true
|
|
26646
|
+
fi
|
|
26647
|
+
mv "$stage" "$dir" || return 1
|
|
26648
|
+
echo "Restored $dir from $archive (verified)"
|
|
26649
|
+
}
|
|
26650
|
+
|
|
26330
26651
|
cmd_rollback() {
|
|
26331
26652
|
local subcommand="${1:-help}"
|
|
26332
26653
|
shift 2>/dev/null || true
|
|
@@ -32374,7 +32695,32 @@ _docs_invoke_provider() {
|
|
|
32374
32695
|
# not the workload that needs the top tier. Pinned to the same
|
|
32375
32696
|
# default the review subcall uses (run.sh:13762), and overridable
|
|
32376
32697
|
# for anyone who wants a different trade.
|
|
32377
|
-
|
|
32698
|
+
# S6 lean child: text-only role, so the lean flag set (from
|
|
32699
|
+
# claude-flags.sh, lazily sourced) drops user hooks, MCP and tools.
|
|
32700
|
+
# When the child ledger helper exists the call uses
|
|
32701
|
+
# --output-format json so usage is recorded, then unwraps .result.
|
|
32702
|
+
local _lean=() _lf _ledger=false
|
|
32703
|
+
if ! declare -F loki_lean_child_flags >/dev/null 2>&1 \
|
|
32704
|
+
&& [ -f "${_LOKI_SCRIPT_DIR:-.}/lib/claude-flags.sh" ]; then
|
|
32705
|
+
# shellcheck source=autonomy/lib/claude-flags.sh
|
|
32706
|
+
. "${_LOKI_SCRIPT_DIR}/lib/claude-flags.sh" 2>/dev/null || true
|
|
32707
|
+
fi
|
|
32708
|
+
if declare -F loki_lean_child_flags >/dev/null 2>&1; then
|
|
32709
|
+
while IFS= read -r _lf; do _lean+=("$_lf"); done < <(loki_lean_child_flags docs 2>/dev/null)
|
|
32710
|
+
fi
|
|
32711
|
+
declare -F loki_child_ledger_append >/dev/null 2>&1 && _ledger=true
|
|
32712
|
+
local _fmt=()
|
|
32713
|
+
[ "$_ledger" = true ] && _fmt=(--output-format json)
|
|
32714
|
+
result=$($t_prefix env CAVEMAN_DEFAULT_MODE=off claude -p ${_lean[@]+"${_lean[@]}"} ${_fmt[@]+"${_fmt[@]}"} --model "${LOKI_DOCS_MODEL:-sonnet}" "$prompt" 2>/dev/null) || exit_code=$?
|
|
32715
|
+
if [ "$_ledger" = true ] && [ -n "$result" ]; then
|
|
32716
|
+
loki_child_ledger_append docs "$result" >/dev/null 2>&1 || true
|
|
32717
|
+
result=$(printf '%s' "$result" | python3 -c 'import json,sys
|
|
32718
|
+
try:
|
|
32719
|
+
d = json.load(sys.stdin)
|
|
32720
|
+
print(d.get("result", ""))
|
|
32721
|
+
except Exception:
|
|
32722
|
+
pass' 2>/dev/null)
|
|
32723
|
+
fi
|
|
32378
32724
|
;;
|
|
32379
32725
|
codex)
|
|
32380
32726
|
result=$($t_prefix codex exec --sandbox workspace-write "$prompt" 2>/dev/null) || exit_code=$?
|
|
@@ -32442,14 +32788,20 @@ MANIFEST_EOF
|
|
|
32442
32788
|
# --- loki docs generate ---
|
|
32443
32789
|
_docs_generate() {
|
|
32444
32790
|
local target_path="."
|
|
32791
|
+
local docs_requested=false
|
|
32445
32792
|
|
|
32446
32793
|
while [[ $# -gt 0 ]]; do
|
|
32447
32794
|
case "$1" in
|
|
32448
32795
|
--help|-h)
|
|
32449
|
-
echo "Usage: loki docs generate [path]"
|
|
32796
|
+
echo "Usage: loki docs generate [--docs] [path]"
|
|
32450
32797
|
echo "Generate full documentation suite in .loki/docs/"
|
|
32798
|
+
echo " --docs Generate even when LOKI_DOCS_TIER is small (explicit request)"
|
|
32451
32799
|
return 0
|
|
32452
32800
|
;;
|
|
32801
|
+
--docs)
|
|
32802
|
+
docs_requested=true
|
|
32803
|
+
shift
|
|
32804
|
+
;;
|
|
32453
32805
|
-*)
|
|
32454
32806
|
log_error "Unknown option: $1"
|
|
32455
32807
|
return 1
|
|
@@ -32467,6 +32819,17 @@ _docs_generate() {
|
|
|
32467
32819
|
fi
|
|
32468
32820
|
target_path="$(cd "$target_path" && pwd)"
|
|
32469
32821
|
|
|
32822
|
+
# S6: a small-tier run (caller exports LOKI_DOCS_TIER) does not pay for the
|
|
32823
|
+
# eight-doc suite unless docs were asked for explicitly.
|
|
32824
|
+
case "${LOKI_DOCS_TIER:-}" in
|
|
32825
|
+
small|simple)
|
|
32826
|
+
if [ "$docs_requested" != true ]; then
|
|
32827
|
+
log_info "Small tier: skipping the 8-doc suite (pass --docs to force)"
|
|
32828
|
+
return 0
|
|
32829
|
+
fi
|
|
32830
|
+
;;
|
|
32831
|
+
esac
|
|
32832
|
+
|
|
32470
32833
|
local docs_dir="$target_path/.loki/docs"
|
|
32471
32834
|
mkdir -p "$docs_dir"
|
|
32472
32835
|
|