loki-mode 7.5.10 → 7.5.12
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 +29 -4
- package/SKILL.md +17 -14
- package/VERSION +1 -1
- package/autonomy/app-runner.sh +81 -6
- package/autonomy/lib/lock.sh +147 -0
- package/autonomy/loki +22 -0
- package/autonomy/run.sh +332 -69
- package/dashboard/__init__.py +1 -1
- package/dashboard/database.py +26 -0
- package/dashboard/models.py +8 -0
- package/dashboard/server.py +125 -4
- package/dashboard/static/index.html +361 -172
- package/docs/COMPARISON.md +6 -6
- package/docs/INSTALLATION.md +32 -22
- package/docs/cursor-comparison.md +6 -6
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
-
> **How it works:**
|
|
21
|
+
> **How it works:** Drop a spec -- a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief. Loki Mode classifies complexity (`run.sh:detect_complexity()`), assembles an agent team from 41 specialized types across 8 swarms, and runs autonomous RARV cycles (Reason - Act - Reflect - Verify, see `run.sh:run_autonomous()`) with 11 quality gates (see `skills/quality-gates.md`). Code is not "done" until it passes automated verification. Output is a Git repo with source, tests, configs, and audit logs.
|
|
22
22
|
|
|
23
23
|
---
|
|
24
24
|
|
|
@@ -49,7 +49,9 @@ bun install -g loki-mode
|
|
|
49
49
|
loki doctor # verify environment
|
|
50
50
|
loki init my-app --template simple-todo-app
|
|
51
51
|
cd my-app
|
|
52
|
-
loki start prd.md # autonomous build
|
|
52
|
+
loki start prd.md # autonomous build from a Markdown PRD
|
|
53
|
+
loki start owner/repo#123 # ...or a GitHub issue
|
|
54
|
+
loki start ./openapi.yaml # ...or an OpenAPI/YAML spec
|
|
53
55
|
```
|
|
54
56
|
|
|
55
57
|
Or skip scaffolding and go straight to a quick task:
|
|
@@ -64,7 +66,7 @@ loki quick "build a landing page with a signup form"
|
|
|
64
66
|
|--------|---------|-------|
|
|
65
67
|
| **Bun (recommended)** | `bun install -g loki-mode` | Fastest. v8 will be Bun-only. |
|
|
66
68
|
| **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` | Auto-installs Bun as a dep |
|
|
67
|
-
| **Docker** | `docker pull asklokesh/loki-mode:7.5.
|
|
69
|
+
| **Docker** | `docker pull asklokesh/loki-mode:7.5.11 && docker run --rm asklokesh/loki-mode:7.5.11 start prd.md` | Bun pre-installed in image |
|
|
68
70
|
| **npm (compat)** | `npm install -g loki-mode` | Works without Bun (bash fallback). Migrate any time with `loki self-update --to bun`. |
|
|
69
71
|
|
|
70
72
|
**Upgrading:**
|
|
@@ -124,7 +126,7 @@ The next major release sunsets the Bash runtime entirely. There is no firm calen
|
|
|
124
126
|
| Method | Command |
|
|
125
127
|
|--------|---------|
|
|
126
128
|
| **Homebrew** | `brew tap asklokesh/tap && brew install loki-mode` |
|
|
127
|
-
| **Docker** | `docker pull asklokesh/loki-mode:7.5.
|
|
129
|
+
| **Docker** | `docker pull asklokesh/loki-mode:7.5.11` |
|
|
128
130
|
| **Inside Claude Code** | `claude --dangerously-skip-permissions` then type "Loki Mode" |
|
|
129
131
|
| **Git clone** | `git clone https://github.com/asklokesh/loki-mode.git` |
|
|
130
132
|
|
|
@@ -132,6 +134,29 @@ See the full [Installation Guide](docs/INSTALLATION.md).
|
|
|
132
134
|
|
|
133
135
|
</details>
|
|
134
136
|
|
|
137
|
+
<details>
|
|
138
|
+
<summary><strong>Supported spec formats</strong></summary>
|
|
139
|
+
|
|
140
|
+
A "spec" is whatever you hand `loki start`. Loki auto-detects the format and normalises it before the RARV loop. A Markdown PRD is one form of spec; the table below lists every input the v7.5.11 CLI accepts.
|
|
141
|
+
|
|
142
|
+
| Format | Example | Notes |
|
|
143
|
+
|--------|---------|-------|
|
|
144
|
+
| Markdown PRD | `loki start ./prd.md` | Canonical form. Headings become section anchors. |
|
|
145
|
+
| JSON spec | `loki start ./spec.json` | Free-form JSON; keys surfaced to agents. |
|
|
146
|
+
| YAML spec | `loki start ./openapi.yaml` | OpenAPI / AsyncAPI / plain YAML all accepted. |
|
|
147
|
+
| Plain text brief | `loki start ./brief.txt` | One-paragraph briefs work; complexity auto-detects to "simple". |
|
|
148
|
+
| GitHub issue URL | `loki start https://github.com/owner/repo/issues/42` | Title + body + labels become the spec. |
|
|
149
|
+
| GitHub shorthand | `loki start owner/repo#42` | Same as above, shorter. |
|
|
150
|
+
| Jira ticket key | `loki start PROJ-456` | Requires `JIRA_BASE_URL` + `JIRA_TOKEN` env vars. |
|
|
151
|
+
| GitLab / Azure DevOps URL | `loki start https://gitlab.com/group/proj/-/issues/7` | GitLab and Azure DevOps issue URLs both supported. |
|
|
152
|
+
| Bare issue number | `loki start #123` or `loki start 123` | Resolved against the current repo's `origin` remote. |
|
|
153
|
+
| OpenSpec change directory | `loki start --openspec ./openspec/change-001` | Reads OpenSpec change manifest + delta files. |
|
|
154
|
+
| Auto-detect (no input) | `loki start` | Picks up `./prd.md`, `./spec.{json,yaml,yml}`, or `./SPEC.md` from cwd. |
|
|
155
|
+
|
|
156
|
+
All formats land in the same RARV pipeline and pass the same 11 quality gates (`skills/quality-gates.md`).
|
|
157
|
+
|
|
158
|
+
</details>
|
|
159
|
+
|
|
135
160
|
---
|
|
136
161
|
|
|
137
162
|
## What You Can Build
|
package/SKILL.md
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: loki-mode
|
|
3
|
-
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes PRD to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
3
|
+
description: Multi-agent autonomous startup system. Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Requires --dangerously-skip-permissions flag.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Loki Mode v7.5.
|
|
6
|
+
# Loki Mode v7.5.12
|
|
7
7
|
|
|
8
8
|
**You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
|
|
9
9
|
|
|
10
|
+
**Spec in, product out.** A "spec" is whatever describes the work: a Markdown PRD, a GitHub issue, an OpenAPI doc, a Jira ticket -- a PRD is one form of spec.
|
|
11
|
+
|
|
10
12
|
**Multi-provider (stable since v5.0.0):** Claude/Codex/Gemini/Cline/Aider with abstract model tiers and degraded mode for non-Claude providers. See `skills/providers.md`. **Current track (v7.5.x):** Phase 1 RARV-C closure -- real provider judges, gate-failure flock, synthetic PRD e2e, status `--json`, dead code cleanup.
|
|
11
13
|
|
|
12
14
|
**Runtime migration in progress:** A bash-to-Bun migration is underway on the `feat/bun-migration` branch. The first phase (shipped in v7.3.0) routes a small set of read-only commands -- `version`, `status`, `stats`, `doctor`, `provider show/list`, `memory list/index` -- through a Bun runtime via `bin/loki`. Every other command remains on the Bash runtime (`autonomy/loki`). Rollback is available with `LOKI_LEGACY_BASH=1`. See `UPGRADING.md` and `docs/architecture/ADR-001-runtime-migration.md` for the full plan.
|
|
@@ -89,11 +91,11 @@ These rules guide autonomous operation. Test results and code quality always tak
|
|
|
89
91
|
|
|
90
92
|
## Model Selection
|
|
91
93
|
|
|
92
|
-
**Default since v5.3.0 (reaffirmed in v7.5.
|
|
94
|
+
**Default since v5.3.0 (reaffirmed in v7.5.12):** Haiku disabled for quality. Use `--allow-haiku` or `LOKI_ALLOW_HAIKU=true` to enable.
|
|
93
95
|
|
|
94
96
|
| Task Type | Tier | Claude (default) | Claude (--allow-haiku) | Codex (GPT-5.3) | Gemini |
|
|
95
97
|
|-----------|------|------------------|------------------------|------------------|--------|
|
|
96
|
-
|
|
|
98
|
+
| Spec analysis, architecture, system design | **planning** | opus | opus | effort=xhigh | thinking=high |
|
|
97
99
|
| Feature implementation, complex bugs | **development** | opus | sonnet | effort=high | thinking=medium |
|
|
98
100
|
| Code review (planned: 3 parallel reviewers) | **development** | opus | sonnet | effort=high | thinking=medium |
|
|
99
101
|
| Integration tests, E2E, deployment | **development** | opus | sonnet | effort=high | thinking=medium |
|
|
@@ -113,7 +115,7 @@ These rules guide autonomous operation. Test results and code quality always tak
|
|
|
113
115
|
|
|
114
116
|
```
|
|
115
117
|
BOOTSTRAP ──[project initialized]──> DISCOVERY
|
|
116
|
-
DISCOVERY ──[
|
|
118
|
+
DISCOVERY ──[spec analyzed, requirements clear]──> ARCHITECTURE
|
|
117
119
|
ARCHITECTURE ──[design approved, specs written]──> DEEPEN_PLAN (standard/complex only)
|
|
118
120
|
DEEPEN_PLAN ──[plan enhanced by 4 research agents]──> INFRASTRUCTURE
|
|
119
121
|
INFRASTRUCTURE ──[cloud/DB ready]──> DEVELOPMENT
|
|
@@ -187,20 +189,21 @@ This protocol governs **skill module** loading -- task-scoped instruction files
|
|
|
187
189
|
|
|
188
190
|
## Invocation
|
|
189
191
|
|
|
190
|
-
**Unified entry point (v6.84.0):** `loki start` auto-detects whether the input is a PRD file, an issue URL,
|
|
192
|
+
**Unified entry point (v6.84.0):** `loki start [SPEC|ISSUE-REF]` auto-detects whether the input is a PRD file, an issue URL, an issue number, or another spec format (e.g. OpenAPI). No need to pick between `loki start` and `loki run` -- the single command handles all cases.
|
|
191
193
|
|
|
192
194
|
```bash
|
|
193
195
|
# Standard mode (Claude - full features)
|
|
194
196
|
claude --dangerously-skip-permissions
|
|
195
|
-
# Then say: "Loki Mode" or "Loki Mode with
|
|
197
|
+
# Then say: "Loki Mode" or "Loki Mode with spec at path/to/spec" (PRD .md/.json, OpenAPI .yaml, etc.)
|
|
196
198
|
|
|
197
199
|
# Unified `loki start` -- one command, auto-detected mode
|
|
198
|
-
loki start # no arg: analyze current dir, auto-generate
|
|
199
|
-
loki start ./prd.md # PRD mode (.md/.json/.txt/.yaml)
|
|
200
|
+
loki start # no arg: analyze current dir, auto-generate spec
|
|
201
|
+
loki start ./prd.md # PRD mode (.md/.json/.txt/.yaml) -- a PRD is one form of spec
|
|
202
|
+
loki start ./openapi.yaml # SPEC mode (OpenAPI doc treated as the spec)
|
|
203
|
+
loki start owner/repo#123 # ISSUE mode (GitHub specific repo)
|
|
200
204
|
loki start https://github.com/o/r/issues/42 # ISSUE mode (GitHub URL)
|
|
201
205
|
loki start 123 # ISSUE mode (GitHub issue in current repo)
|
|
202
206
|
loki start PROJ-456 # ISSUE mode (Jira)
|
|
203
|
-
loki start owner/repo#789 # ISSUE mode (GitHub specific repo)
|
|
204
207
|
loki start --prd ./prd.md # Explicit PRD mode (overrides detection)
|
|
205
208
|
loki start --issue 123 # Explicit issue mode (overrides detection)
|
|
206
209
|
|
|
@@ -330,7 +333,7 @@ See `references/core-workflow.md` for the full RARV-C contract.
|
|
|
330
333
|
|
|
331
334
|
---
|
|
332
335
|
|
|
333
|
-
## Concurrency and Security Hardening (v7.5.7 - v7.5.
|
|
336
|
+
## Concurrency and Security Hardening (v7.5.7 - v7.5.12)
|
|
334
337
|
|
|
335
338
|
Three back-to-back patches closed cross-process and security gaps. No user-facing behavior change on the default flow; verify via the cited paths.
|
|
336
339
|
|
|
@@ -339,7 +342,7 @@ Three back-to-back patches closed cross-process and security gaps. No user-facin
|
|
|
339
342
|
- **Dashboard auth** now required on `/api/memory/*`, `/api/learning/*`, and `/api/status` in `dashboard/server.py` (previously unauthenticated read paths).
|
|
340
343
|
- **Bash quoting hardening** across `autonomy/run.sh` and `autonomy/loki` -- variable expansions inside command substitution and `[ ]` tests quoted to prevent word-splitting on paths with spaces.
|
|
341
344
|
|
|
342
|
-
See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.
|
|
345
|
+
See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.12] for the per-fix list and reviewer sign-off.
|
|
343
346
|
|
|
344
347
|
---
|
|
345
348
|
|
|
@@ -355,7 +358,7 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.10] for the per-fix list and r
|
|
|
355
358
|
| Notification Triggers | v5.40.0 | `GET/PUT /api/notifications/triggers` |
|
|
356
359
|
| GitHub integration | v5.42.2 | Import, sync-back, PR creation, export. CLI: `loki github`, API: `/api/github/*` |
|
|
357
360
|
| Legacy System Healing | v6.67.0 | `loki heal <path>` -- friction-as-semantics, characterization tests |
|
|
358
|
-
| Unified `loki start` | v6.84.0 | Auto-detects PRD vs issue input |
|
|
361
|
+
| Unified `loki start` | v6.84.0 | Auto-detects spec (PRD, OpenAPI, etc.) vs issue input |
|
|
359
362
|
| Managed Agents (memory mirror) | v7.2.0 | Opt-in via `LOKI_MANAGED_AGENTS` -- see Managed Agents section |
|
|
360
363
|
| Bun runtime (Phase 1) | v7.3.0 | Read-only commands routed through `bin/loki`; `LOKI_LEGACY_BASH=1` to revert |
|
|
361
364
|
| Phase 1 RARV-C closure | v7.5.x | Findings injection, real judges, auto-learnings, handoff.md |
|
|
@@ -378,4 +381,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.10] for the per-fix list and r
|
|
|
378
381
|
|
|
379
382
|
---
|
|
380
383
|
|
|
381
|
-
**v7.5.
|
|
384
|
+
**v7.5.12 | [Autonomi](https://www.autonomi.dev/) flagship product | ~260 lines core**
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
7.5.
|
|
1
|
+
7.5.12
|
package/autonomy/app-runner.sh
CHANGED
|
@@ -431,6 +431,66 @@ _install_python_deps() {
|
|
|
431
431
|
fi
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
+
# Resolve the directory containing the compose file. Falls back to the passed
|
|
435
|
+
# directory when no compose file is found (callers should already have verified
|
|
436
|
+
# detection). Honors LOKI_COMPOSE_FILE override.
|
|
437
|
+
_app_runner_compose_dir() {
|
|
438
|
+
local base="${1:-${TARGET_DIR:-.}}"
|
|
439
|
+
if [ -n "${LOKI_COMPOSE_FILE:-}" ] && [ -f "${LOKI_COMPOSE_FILE}" ]; then
|
|
440
|
+
dirname "${LOKI_COMPOSE_FILE}"
|
|
441
|
+
return
|
|
442
|
+
fi
|
|
443
|
+
for candidate in \
|
|
444
|
+
"$base/docker-compose.yml" \
|
|
445
|
+
"$base/docker-compose.yaml" \
|
|
446
|
+
"$base/compose.yml" \
|
|
447
|
+
"$base/compose.yaml"; do
|
|
448
|
+
if [ -f "$candidate" ]; then
|
|
449
|
+
dirname "$candidate"
|
|
450
|
+
return
|
|
451
|
+
fi
|
|
452
|
+
done
|
|
453
|
+
printf '%s\n' "$base"
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# Count containers currently in the "running" state for the compose project.
|
|
457
|
+
# Polls up to LOKI_COMPOSE_HEALTH_TIMEOUT seconds (default 30) at 1s intervals
|
|
458
|
+
# so containers transitioning from Created -> Running are not falsely reported
|
|
459
|
+
# as failed. Echoes the final running-container count (0 on failure).
|
|
460
|
+
_app_runner_compose_running_count() {
|
|
461
|
+
local base="${1:-${TARGET_DIR:-.}}"
|
|
462
|
+
local compose_dir
|
|
463
|
+
compose_dir=$(_app_runner_compose_dir "$base")
|
|
464
|
+
local timeout="${LOKI_COMPOSE_HEALTH_TIMEOUT:-30}"
|
|
465
|
+
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
|
|
466
|
+
timeout=30
|
|
467
|
+
fi
|
|
468
|
+
local elapsed=0
|
|
469
|
+
local count=0
|
|
470
|
+
while [ "$elapsed" -lt "$timeout" ]; do
|
|
471
|
+
# Prefer the structured --format '{{.State}}' which lists one state per
|
|
472
|
+
# container (one per line) and is stable across docker-compose v2.x.
|
|
473
|
+
local states
|
|
474
|
+
states=$(cd "$compose_dir" && docker compose ps --format '{{.State}}' 2>/dev/null || true)
|
|
475
|
+
if [ -n "$states" ]; then
|
|
476
|
+
# Match exact "running" lines only (case-insensitive). Avoid grep -c
|
|
477
|
+
# on empty input which can return 0 with success even when nothing
|
|
478
|
+
# ran. Also strip CR for safety on weird terminals.
|
|
479
|
+
count=$(printf '%s\n' "$states" | tr -d '\r' | grep -ciE '^running$' || true)
|
|
480
|
+
else
|
|
481
|
+
count=0
|
|
482
|
+
fi
|
|
483
|
+
if [ "${count:-0}" -gt 0 ]; then
|
|
484
|
+
printf '%s\n' "$count"
|
|
485
|
+
return 0
|
|
486
|
+
fi
|
|
487
|
+
sleep 1
|
|
488
|
+
elapsed=$(( elapsed + 1 ))
|
|
489
|
+
done
|
|
490
|
+
printf '%s\n' "${count:-0}"
|
|
491
|
+
return 0
|
|
492
|
+
}
|
|
493
|
+
|
|
434
494
|
#===============================================================================
|
|
435
495
|
# Lifecycle
|
|
436
496
|
#===============================================================================
|
|
@@ -501,15 +561,24 @@ app_runner_start() {
|
|
|
501
561
|
|
|
502
562
|
# Verify process started
|
|
503
563
|
if [ "$_APP_RUNNER_IS_DOCKER" = true ] && echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
|
|
504
|
-
# Docker compose -d exits immediately;
|
|
564
|
+
# Docker compose -d exits immediately; poll for containers in "running"
|
|
565
|
+
# state. Containers may report "Created" briefly before transitioning to
|
|
566
|
+
# "Running", so retry up to ~30 seconds before declaring failure.
|
|
505
567
|
local running_containers
|
|
506
|
-
running_containers=$(
|
|
568
|
+
running_containers=$(_app_runner_compose_running_count "$dir")
|
|
507
569
|
if [ "${running_containers:-0}" -gt 0 ]; then
|
|
508
570
|
_write_app_state "running"
|
|
509
571
|
log_info "App Runner: docker compose started ($running_containers container(s) running)"
|
|
510
572
|
return 0
|
|
511
573
|
else
|
|
512
|
-
|
|
574
|
+
# Capture diagnostic output for postmortem
|
|
575
|
+
local compose_dir
|
|
576
|
+
compose_dir=$(_app_runner_compose_dir "$dir")
|
|
577
|
+
local diag
|
|
578
|
+
diag=$(cd "$compose_dir" && docker compose ps 2>&1 || true)
|
|
579
|
+
log_error "App Runner: docker compose containers failed to start (no containers in running state after retries)"
|
|
580
|
+
log_error "App Runner: docker compose ps output:"
|
|
581
|
+
printf '%s\n' "$diag" | while IFS= read -r line; do log_error " $line"; done
|
|
513
582
|
_APP_RUNNER_CRASH_COUNT=$(( _APP_RUNNER_CRASH_COUNT + 1 ))
|
|
514
583
|
_write_app_state "failed"
|
|
515
584
|
return 1
|
|
@@ -547,7 +616,9 @@ app_runner_stop() {
|
|
|
547
616
|
docker rm "$_APP_RUNNER_DOCKER_CONTAINER" 2>/dev/null || true
|
|
548
617
|
fi
|
|
549
618
|
if echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
|
|
550
|
-
|
|
619
|
+
local _stop_compose_dir
|
|
620
|
+
_stop_compose_dir=$(_app_runner_compose_dir "${TARGET_DIR:-.}")
|
|
621
|
+
(cd "$_stop_compose_dir" && docker compose down 2>/dev/null) || true
|
|
551
622
|
fi
|
|
552
623
|
fi
|
|
553
624
|
|
|
@@ -619,8 +690,10 @@ app_runner_health_check() {
|
|
|
619
690
|
|
|
620
691
|
# Docker compose: check containers instead of PID (docker compose up -d exits immediately)
|
|
621
692
|
if [ "$_APP_RUNNER_IS_DOCKER" = true ] && echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
|
|
693
|
+
# Use a 1-second timeout for health checks (no long retry); start-time
|
|
694
|
+
# retries are handled in app_runner_start.
|
|
622
695
|
local running_containers
|
|
623
|
-
running_containers=$(
|
|
696
|
+
running_containers=$(LOKI_COMPOSE_HEALTH_TIMEOUT=1 _app_runner_compose_running_count "${TARGET_DIR:-.}")
|
|
624
697
|
if [ "${running_containers:-0}" -gt 0 ]; then
|
|
625
698
|
_write_health "true"
|
|
626
699
|
_write_app_state "running"
|
|
@@ -759,7 +832,9 @@ app_runner_cleanup() {
|
|
|
759
832
|
docker rm "$_APP_RUNNER_DOCKER_CONTAINER" 2>/dev/null || true
|
|
760
833
|
fi
|
|
761
834
|
if echo "$_APP_RUNNER_METHOD" | grep -q "docker compose"; then
|
|
762
|
-
|
|
835
|
+
local _stop_compose_dir
|
|
836
|
+
_stop_compose_dir=$(_app_runner_compose_dir "${TARGET_DIR:-.}")
|
|
837
|
+
(cd "$_stop_compose_dir" && docker compose down 2>/dev/null) || true
|
|
763
838
|
fi
|
|
764
839
|
fi
|
|
765
840
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Loki Mode -- portable file locking helper.
|
|
3
|
+
#
|
|
4
|
+
# Why this exists:
|
|
5
|
+
# flock(1) is a Linux util-linux binary not shipped on macOS or BSDs.
|
|
6
|
+
# Bash callers that depend on it either degrade to non-atomic PID checks
|
|
7
|
+
# (race condition) or print a "flock not available" warning. This helper
|
|
8
|
+
# gives every bash caller one cross-platform primitive.
|
|
9
|
+
#
|
|
10
|
+
# Strategy:
|
|
11
|
+
# mkdir() is atomic on all POSIX filesystems -- exactly one concurrent
|
|
12
|
+
# caller wins the create. We use <target>.lockdir as the mutex, write a
|
|
13
|
+
# PID-stamped sentinel inside it for stale detection, and clean up via
|
|
14
|
+
# trap so a killed holder cannot wedge later callers.
|
|
15
|
+
#
|
|
16
|
+
# Public API:
|
|
17
|
+
# safe_acquire_lock <target> [timeout_seconds] -> 0 on acquire, 1 on timeout
|
|
18
|
+
# safe_release_lock <target> -> always 0
|
|
19
|
+
# safe_with_lock <target> <command...> -> runs command under lock,
|
|
20
|
+
# returns command's exit code
|
|
21
|
+
#
|
|
22
|
+
# Stale-lock policy: a lockdir whose sentinel PID is no longer alive AND
|
|
23
|
+
# whose mtime is >30s old is reaped automatically.
|
|
24
|
+
#
|
|
25
|
+
# Acquire timing: poll every 50ms, default ceiling 5s.
|
|
26
|
+
|
|
27
|
+
# Guard against double-source.
|
|
28
|
+
if [ "${__LOKI_LOCK_SH_LOADED:-0}" = "1" ]; then
|
|
29
|
+
return 0 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
__LOKI_LOCK_SH_LOADED=1
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Internal helpers
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# _loki_lock_sleep_50ms: portable 50ms sleep.
|
|
38
|
+
# perl is preinstalled on macOS + most Linux; bash builtin `read -t 0.05` is
|
|
39
|
+
# the fallback; final fallback is a 1s sleep (still correct, just slower).
|
|
40
|
+
_loki_lock_sleep_50ms() {
|
|
41
|
+
perl -e 'select(undef,undef,undef,0.05)' 2>/dev/null \
|
|
42
|
+
|| read -r -t 0.05 _ < /dev/null 2>/dev/null \
|
|
43
|
+
|| sleep 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# _loki_lock_mtime <path>: portable mtime in epoch seconds, "0" on failure.
|
|
47
|
+
_loki_lock_mtime() {
|
|
48
|
+
stat -f%m "$1" 2>/dev/null \
|
|
49
|
+
|| stat -c%Y "$1" 2>/dev/null \
|
|
50
|
+
|| echo 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# _loki_lock_is_stale <lockdir>: 0 if reapable, 1 otherwise.
|
|
54
|
+
# Stale = sentinel PID dead AND mtime >30s old. A bare lockdir with no
|
|
55
|
+
# sentinel (legacy / partial create) is treated as stale after 30s as well.
|
|
56
|
+
_loki_lock_is_stale() {
|
|
57
|
+
local lockdir="$1"
|
|
58
|
+
local sentinel="$lockdir/owner.pid"
|
|
59
|
+
local now age pid
|
|
60
|
+
now=$(date +%s 2>/dev/null || echo 0)
|
|
61
|
+
age=$(( now - $(_loki_lock_mtime "$lockdir") ))
|
|
62
|
+
if [ "$age" -le 30 ]; then
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
if [ -f "$sentinel" ]; then
|
|
66
|
+
pid=$(cat "$sentinel" 2>/dev/null)
|
|
67
|
+
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
|
|
68
|
+
return 1
|
|
69
|
+
fi
|
|
70
|
+
fi
|
|
71
|
+
return 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Public API
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
# safe_acquire_lock <target> [timeout_seconds=5]
|
|
79
|
+
# Acquires a mutex on <target>.lockdir. Returns 0 on acquire, 1 on timeout.
|
|
80
|
+
safe_acquire_lock() {
|
|
81
|
+
local target="$1"
|
|
82
|
+
local timeout_s="${2:-5}"
|
|
83
|
+
local lockdir="${target}.lockdir"
|
|
84
|
+
local target_dir
|
|
85
|
+
target_dir=$(dirname "$target")
|
|
86
|
+
[ -d "$target_dir" ] || mkdir -p "$target_dir" 2>/dev/null || true
|
|
87
|
+
|
|
88
|
+
# 50ms poll interval -> 20 attempts/sec.
|
|
89
|
+
local max_attempts=$(( timeout_s * 20 ))
|
|
90
|
+
[ "$max_attempts" -lt 1 ] && max_attempts=1
|
|
91
|
+
local attempts=0
|
|
92
|
+
|
|
93
|
+
while ! mkdir "$lockdir" 2>/dev/null; do
|
|
94
|
+
if _loki_lock_is_stale "$lockdir"; then
|
|
95
|
+
rm -rf "$lockdir" 2>/dev/null || true
|
|
96
|
+
continue
|
|
97
|
+
fi
|
|
98
|
+
attempts=$((attempts + 1))
|
|
99
|
+
if [ "$attempts" -ge "$max_attempts" ]; then
|
|
100
|
+
return 1
|
|
101
|
+
fi
|
|
102
|
+
_loki_lock_sleep_50ms
|
|
103
|
+
done
|
|
104
|
+
|
|
105
|
+
# Stamp sentinel for stale detection.
|
|
106
|
+
echo "$$" > "$lockdir/owner.pid" 2>/dev/null || true
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# safe_release_lock <target>
|
|
111
|
+
# Releases the mutex on <target>.lockdir. Idempotent.
|
|
112
|
+
safe_release_lock() {
|
|
113
|
+
local target="$1"
|
|
114
|
+
local lockdir="${target}.lockdir"
|
|
115
|
+
rm -rf "$lockdir" 2>/dev/null || true
|
|
116
|
+
return 0
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# safe_with_lock <target> <command> [args...]
|
|
120
|
+
# Runs <command args...> under an exclusive lock on <target>. Releases the
|
|
121
|
+
# lock automatically (trap-based) even on signal. Returns the command's
|
|
122
|
+
# exit code. If the lock cannot be acquired within 5s, returns 1 without
|
|
123
|
+
# running the command (caller can detect via $?).
|
|
124
|
+
safe_with_lock() {
|
|
125
|
+
local target="$1"; shift
|
|
126
|
+
if ! safe_acquire_lock "$target" 5; then
|
|
127
|
+
return 1
|
|
128
|
+
fi
|
|
129
|
+
# Trap at caller scope so signal-driven termination still releases.
|
|
130
|
+
# We keep this in the current shell (not a subshell) so the trap can
|
|
131
|
+
# see the local $target. We carefully restore any prior EXIT trap.
|
|
132
|
+
local rc=0
|
|
133
|
+
local _prev_exit_trap
|
|
134
|
+
_prev_exit_trap=$(trap -p EXIT 2>/dev/null)
|
|
135
|
+
# shellcheck disable=SC2064
|
|
136
|
+
trap "safe_release_lock '$target'" EXIT INT TERM HUP
|
|
137
|
+
"$@"
|
|
138
|
+
rc=$?
|
|
139
|
+
safe_release_lock "$target"
|
|
140
|
+
# Restore prior EXIT trap (or clear if none).
|
|
141
|
+
if [ -n "$_prev_exit_trap" ]; then
|
|
142
|
+
eval "$_prev_exit_trap"
|
|
143
|
+
else
|
|
144
|
+
trap - EXIT INT TERM HUP
|
|
145
|
+
fi
|
|
146
|
+
return $rc
|
|
147
|
+
}
|
package/autonomy/loki
CHANGED
|
@@ -1379,6 +1379,28 @@ cmd_start() {
|
|
|
1379
1379
|
fi
|
|
1380
1380
|
fi
|
|
1381
1381
|
|
|
1382
|
+
# v7.5.12 Gap B: Stale-PID detection. Hard-kill (Ctrl+C followed by SIGKILL or
|
|
1383
|
+
# `loki stop`) can leave .loki/loki.pid + .loki/session.lock orphaned. The
|
|
1384
|
+
# next `loki start` then refuses to launch -- or worse, run.sh's downstream
|
|
1385
|
+
# cleanup may treat the stale pid as live. Detect-and-clean here, BEFORE
|
|
1386
|
+
# exec, so the user gets one clear log line instead of mysterious silent
|
|
1387
|
+
# behavior.
|
|
1388
|
+
local _start_loki_dir="${LOKI_DIR:-.loki}"
|
|
1389
|
+
local _start_pid_file="$_start_loki_dir/loki.pid"
|
|
1390
|
+
if [ -f "$_start_pid_file" ]; then
|
|
1391
|
+
local _existing_pid
|
|
1392
|
+
_existing_pid=$(cat "$_start_pid_file" 2>/dev/null | tr -dc '0-9')
|
|
1393
|
+
if [ -n "$_existing_pid" ] && kill -0 "$_existing_pid" 2>/dev/null; then
|
|
1394
|
+
echo -e "${RED}Error: another loki instance is running (pid $_existing_pid).${NC}" >&2
|
|
1395
|
+
echo -e "${YELLOW}Run 'loki stop' first, then retry 'loki start'.${NC}" >&2
|
|
1396
|
+
exit 1
|
|
1397
|
+
fi
|
|
1398
|
+
# PID is stale (file present but process gone). Log + remove + continue.
|
|
1399
|
+
echo -e "${YELLOW}Removing stale pid file ($_start_pid_file, pid=${_existing_pid:-empty} not alive)${NC}" >&2
|
|
1400
|
+
rm -f "$_start_pid_file" 2>/dev/null || true
|
|
1401
|
+
rm -f "$_start_loki_dir/session.lock" 2>/dev/null || true
|
|
1402
|
+
fi
|
|
1403
|
+
|
|
1382
1404
|
# Determine effective provider for display
|
|
1383
1405
|
local effective_provider="${provider:-${LOKI_PROVIDER:-claude}}"
|
|
1384
1406
|
|