loki-mode 6.9.0 → 6.11.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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/autonomy/completion-council.sh +19 -2
- package/autonomy/loki +82 -0
- package/autonomy/openspec-adapter.py +827 -0
- package/autonomy/prd-analyzer.py +14 -1
- package/autonomy/run.sh +294 -18
- package/dashboard/__init__.py +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/skills/00-index.md +9 -0
- package/skills/openspec-integration.md +147 -0
package/autonomy/prd-analyzer.py
CHANGED
|
@@ -252,7 +252,7 @@ class PrdAnalyzer:
|
|
|
252
252
|
count = 0
|
|
253
253
|
in_feature_section = False
|
|
254
254
|
for line in self.lines:
|
|
255
|
-
if re.search(r"(?i)#+\s.*(?:feature|requirement|scope|functional)", line):
|
|
255
|
+
if re.search(r"(?i)#+\s.*(?:feature|requirement|scope|functional|module|component|service|endpoint|api|milestone|deliverable|workstream|epic|story|task|phase|capability|objective)", line):
|
|
256
256
|
in_feature_section = True
|
|
257
257
|
continue
|
|
258
258
|
if in_feature_section and re.match(r"^\s*#+\s", line):
|
|
@@ -262,12 +262,25 @@ class PrdAnalyzer:
|
|
|
262
262
|
if re.match(r"^\s*[-*]\s+\S", line) or re.match(r"^\s*\d+\.\s+\S", line):
|
|
263
263
|
count += 1
|
|
264
264
|
|
|
265
|
+
# Fallback: count ## headings as feature indicators when bullet items are few
|
|
266
|
+
if count == 0:
|
|
267
|
+
for line in self.lines:
|
|
268
|
+
if re.match(r"^##\s+\S", line):
|
|
269
|
+
count += 1
|
|
270
|
+
|
|
265
271
|
self.feature_count = count
|
|
266
272
|
for threshold, label in SCOPE_THRESHOLDS:
|
|
267
273
|
if count <= threshold:
|
|
268
274
|
self.scope = label
|
|
269
275
|
break
|
|
270
276
|
|
|
277
|
+
# Word-count fallback: large PRDs should never be classified as small
|
|
278
|
+
word_count = len(self.content.split())
|
|
279
|
+
if word_count > 2000 and self.scope in ("small", "medium"):
|
|
280
|
+
self.scope = "large"
|
|
281
|
+
elif word_count > 500 and self.scope == "small":
|
|
282
|
+
self.scope = "medium"
|
|
283
|
+
|
|
271
284
|
def generate_observations(self):
|
|
272
285
|
"""Generate the observations markdown content."""
|
|
273
286
|
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
package/autonomy/run.sh
CHANGED
|
@@ -622,6 +622,11 @@ MAX_WORKTREES=${LOKI_MAX_WORKTREES:-5}
|
|
|
622
622
|
MAX_PARALLEL_SESSIONS=${LOKI_MAX_PARALLEL_SESSIONS:-3}
|
|
623
623
|
PARALLEL_TESTING=${LOKI_PARALLEL_TESTING:-true}
|
|
624
624
|
PARALLEL_DOCS=${LOKI_PARALLEL_DOCS:-true}
|
|
625
|
+
|
|
626
|
+
# Gate Escalation Ladder (v6.10.0)
|
|
627
|
+
GATE_CLEAR_LIMIT=${LOKI_GATE_CLEAR_LIMIT:-3}
|
|
628
|
+
GATE_ESCALATE_LIMIT=${LOKI_GATE_ESCALATE_LIMIT:-5}
|
|
629
|
+
GATE_PAUSE_LIMIT=${LOKI_GATE_PAUSE_LIMIT:-10}
|
|
625
630
|
TARGET_DIR="${LOKI_TARGET_DIR:-$(pwd)}"
|
|
626
631
|
PARALLEL_BLOG=${LOKI_PARALLEL_BLOG:-false}
|
|
627
632
|
AUTO_MERGE=${LOKI_AUTO_MERGE:-true}
|
|
@@ -5250,6 +5255,69 @@ SAFEOF
|
|
|
5250
5255
|
fi
|
|
5251
5256
|
}
|
|
5252
5257
|
|
|
5258
|
+
#===============================================================================
|
|
5259
|
+
# Gate Failure Tracking (v6.10.0)
|
|
5260
|
+
#===============================================================================
|
|
5261
|
+
|
|
5262
|
+
track_gate_failure() {
|
|
5263
|
+
local gate_name="$1"
|
|
5264
|
+
local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
|
|
5265
|
+
mkdir -p "$(dirname "$gate_file")"
|
|
5266
|
+
|
|
5267
|
+
_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
|
|
5268
|
+
import json, os
|
|
5269
|
+
gate_file = os.environ['_GATE_FILE']
|
|
5270
|
+
gate_name = os.environ['_GATE_NAME']
|
|
5271
|
+
try:
|
|
5272
|
+
with open(gate_file) as f:
|
|
5273
|
+
counts = json.load(f)
|
|
5274
|
+
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
5275
|
+
counts = {}
|
|
5276
|
+
counts[gate_name] = counts.get(gate_name, 0) + 1
|
|
5277
|
+
with open(gate_file, 'w') as f:
|
|
5278
|
+
json.dump(counts, f, indent=2)
|
|
5279
|
+
print(counts[gate_name])
|
|
5280
|
+
" 2>/dev/null || echo "1"
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
clear_gate_failure() {
|
|
5284
|
+
local gate_name="$1"
|
|
5285
|
+
local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
|
|
5286
|
+
[ -f "$gate_file" ] || return 0
|
|
5287
|
+
|
|
5288
|
+
_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
|
|
5289
|
+
import json, os
|
|
5290
|
+
gate_file = os.environ['_GATE_FILE']
|
|
5291
|
+
gate_name = os.environ['_GATE_NAME']
|
|
5292
|
+
try:
|
|
5293
|
+
with open(gate_file) as f:
|
|
5294
|
+
counts = json.load(f)
|
|
5295
|
+
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
5296
|
+
counts = {}
|
|
5297
|
+
counts[gate_name] = 0
|
|
5298
|
+
with open(gate_file, 'w') as f:
|
|
5299
|
+
json.dump(counts, f, indent=2)
|
|
5300
|
+
" 2>/dev/null || true
|
|
5301
|
+
}
|
|
5302
|
+
|
|
5303
|
+
get_gate_failure_count() {
|
|
5304
|
+
local gate_name="$1"
|
|
5305
|
+
local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
|
|
5306
|
+
[ -f "$gate_file" ] || { echo "0"; return; }
|
|
5307
|
+
|
|
5308
|
+
_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
|
|
5309
|
+
import json, os
|
|
5310
|
+
gate_file = os.environ['_GATE_FILE']
|
|
5311
|
+
gate_name = os.environ['_GATE_NAME']
|
|
5312
|
+
try:
|
|
5313
|
+
with open(gate_file) as f:
|
|
5314
|
+
counts = json.load(f)
|
|
5315
|
+
print(counts.get(gate_name, 0))
|
|
5316
|
+
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
5317
|
+
print(0)
|
|
5318
|
+
" 2>/dev/null || echo "0"
|
|
5319
|
+
}
|
|
5320
|
+
|
|
5253
5321
|
# ============================================================================
|
|
5254
5322
|
# Hard Quality Gate: Test Coverage (v6.7.0)
|
|
5255
5323
|
# Detects test runner and runs tests with coverage reporting
|
|
@@ -5287,6 +5355,58 @@ enforce_test_coverage() {
|
|
|
5287
5355
|
fi
|
|
5288
5356
|
fi
|
|
5289
5357
|
|
|
5358
|
+
# Monorepo: scan workspace packages for test runners (v6.10.0)
|
|
5359
|
+
if [ "$test_runner" = "none" ] && [ -f "${TARGET_DIR:-.}/package.json" ]; then
|
|
5360
|
+
local is_monorepo=false
|
|
5361
|
+
# Detect monorepo indicators
|
|
5362
|
+
if [ -f "${TARGET_DIR:-.}/pnpm-workspace.yaml" ] || \
|
|
5363
|
+
[ -f "${TARGET_DIR:-.}/turbo.json" ] || \
|
|
5364
|
+
[ -f "${TARGET_DIR:-.}/lerna.json" ] || \
|
|
5365
|
+
grep -q '"workspaces"' "${TARGET_DIR:-.}/package.json" 2>/dev/null; then
|
|
5366
|
+
is_monorepo=true
|
|
5367
|
+
fi
|
|
5368
|
+
|
|
5369
|
+
if [ "$is_monorepo" = "true" ]; then
|
|
5370
|
+
# Allow env override
|
|
5371
|
+
if [ -n "${LOKI_MONOREPO_TEST_CMD:-}" ]; then
|
|
5372
|
+
test_runner="monorepo-custom"
|
|
5373
|
+
local output
|
|
5374
|
+
output=$(cd "${TARGET_DIR:-.}" && eval "$LOKI_MONOREPO_TEST_CMD" 2>&1) || test_passed=false
|
|
5375
|
+
details="monorepo-custom: $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
5376
|
+
else
|
|
5377
|
+
# Scan workspace packages for test runners
|
|
5378
|
+
local workspace_runner=""
|
|
5379
|
+
for pkg_json in "${TARGET_DIR:-.}"/packages/*/package.json \
|
|
5380
|
+
"${TARGET_DIR:-.}"/apps/*/package.json \
|
|
5381
|
+
"${TARGET_DIR:-.}"/services/*/package.json; do
|
|
5382
|
+
[ -f "$pkg_json" ] || continue
|
|
5383
|
+
if grep -q '"vitest"' "$pkg_json" 2>/dev/null; then
|
|
5384
|
+
workspace_runner="vitest"
|
|
5385
|
+
break
|
|
5386
|
+
elif grep -q '"jest"' "$pkg_json" 2>/dev/null; then
|
|
5387
|
+
workspace_runner="jest"
|
|
5388
|
+
break
|
|
5389
|
+
fi
|
|
5390
|
+
done
|
|
5391
|
+
|
|
5392
|
+
if [ -n "$workspace_runner" ]; then
|
|
5393
|
+
test_runner="monorepo-$workspace_runner"
|
|
5394
|
+
local output
|
|
5395
|
+
if [ -f "${TARGET_DIR:-.}/turbo.json" ] && command -v turbo &>/dev/null; then
|
|
5396
|
+
output=$(cd "${TARGET_DIR:-.}" && npx turbo test 2>&1) || test_passed=false
|
|
5397
|
+
details="turbo test ($workspace_runner): $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
5398
|
+
elif [ -f "${TARGET_DIR:-.}/pnpm-workspace.yaml" ] && command -v pnpm &>/dev/null; then
|
|
5399
|
+
output=$(cd "${TARGET_DIR:-.}" && pnpm test --recursive 2>&1) || test_passed=false
|
|
5400
|
+
details="pnpm test --recursive ($workspace_runner): $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
5401
|
+
else
|
|
5402
|
+
output=$(cd "${TARGET_DIR:-.}" && npm test 2>&1) || test_passed=false
|
|
5403
|
+
details="npm test ($workspace_runner): $(echo "$output" | tail -3 | tr '\n' ' ')"
|
|
5404
|
+
fi
|
|
5405
|
+
fi
|
|
5406
|
+
fi
|
|
5407
|
+
fi
|
|
5408
|
+
fi
|
|
5409
|
+
|
|
5290
5410
|
# Python
|
|
5291
5411
|
if [ "$test_runner" = "none" ]; then
|
|
5292
5412
|
if [ -f "${TARGET_DIR:-.}/setup.py" ] || [ -f "${TARGET_DIR:-.}/pyproject.toml" ] || \
|
|
@@ -7398,11 +7518,16 @@ build_prompt() {
|
|
|
7398
7518
|
if [ -n "$COMPLETION_PROMISE" ]; then
|
|
7399
7519
|
completion_instruction="COMPLETION_PROMISE: [$COMPLETION_PROMISE]. ONLY output 'COMPLETION PROMISE FULFILLED: $COMPLETION_PROMISE' when this EXACT condition is met."
|
|
7400
7520
|
else
|
|
7401
|
-
completion_instruction="NO COMPLETION PROMISE SET.
|
|
7521
|
+
completion_instruction="NO COMPLETION PROMISE SET. Continue finding improvements. The Completion Council will evaluate your progress periodically. Iteration $iteration of max $MAX_ITERATIONS."
|
|
7402
7522
|
fi
|
|
7403
7523
|
|
|
7404
7524
|
# Core autonomous instructions - NO questions, NO waiting, NEVER say done
|
|
7405
|
-
local autonomous_suffix="
|
|
7525
|
+
local autonomous_suffix=""
|
|
7526
|
+
if [ "$AUTONOMY_MODE" = "perpetual" ] || [ "$PERPETUAL_MODE" = "true" ]; then
|
|
7527
|
+
autonomous_suffix="CRITICAL AUTONOMY RULES: 1) NEVER ask questions - just decide. 2) NEVER wait for confirmation - just act. 3) NEVER say 'done' or 'complete' - there's always more to improve. 4) NEVER stop voluntarily - if out of tasks, create new ones (add tests, optimize, refactor, add features). 5) Work continues PERPETUALLY. Even if PRD is implemented, find bugs, add tests, improve UX, optimize performance."
|
|
7528
|
+
else
|
|
7529
|
+
autonomous_suffix="CRITICAL AUTONOMY RULES: 1) NEVER ask questions - just decide. 2) NEVER wait for confirmation - just act. 3) When all PRD requirements are implemented and tests pass, output the completion promise text EXACTLY: '$COMPLETION_PROMISE'. 4) If out of tasks but PRD is not fully implemented, continue working on remaining requirements. 5) Focus on completing PRD scope, not endless improvements."
|
|
7530
|
+
fi
|
|
7406
7531
|
|
|
7407
7532
|
# Skill files are always copied to .loki/skills/ for all providers
|
|
7408
7533
|
local sdlc_instruction="SDLC_PHASES_ENABLED: [$phases]. Execute ALL enabled phases. Log results to .loki/logs/. See .loki/SKILL.md for phase details. Skill modules at .loki/skills/."
|
|
@@ -7585,6 +7710,29 @@ except: pass
|
|
|
7585
7710
|
fi
|
|
7586
7711
|
fi
|
|
7587
7712
|
|
|
7713
|
+
# OpenSpec delta context injection (if available)
|
|
7714
|
+
local openspec_context=""
|
|
7715
|
+
if [[ -f ".loki/openspec/delta-context.json" ]]; then
|
|
7716
|
+
openspec_context=$(_DELTA_FILE=".loki/openspec/delta-context.json" python3 -c "
|
|
7717
|
+
import json, os
|
|
7718
|
+
try:
|
|
7719
|
+
with open(os.environ['_DELTA_FILE']) as f:
|
|
7720
|
+
data = json.load(f)
|
|
7721
|
+
parts = ['OPENSPEC DELTA CONTEXT:']
|
|
7722
|
+
for domain, deltas in data.get('deltas', {}).items():
|
|
7723
|
+
for req in deltas.get('added', []):
|
|
7724
|
+
parts.append(f' ADDED [{domain}]: {req[\"name\"]} - Create new code following existing patterns')
|
|
7725
|
+
for req in deltas.get('modified', []):
|
|
7726
|
+
parts.append(f' MODIFIED [{domain}]: {req[\"name\"]} - Find and update existing code, do NOT create new files. Previously: {req.get(\"previously\", \"N/A\")}')
|
|
7727
|
+
for req in deltas.get('removed', []):
|
|
7728
|
+
parts.append(f' REMOVED [{domain}]: {req[\"name\"]} - Deprecate or remove. Reason: {req.get(\"reason\", \"N/A\")}')
|
|
7729
|
+
parts.append(f'Complexity: {data.get(\"complexity\", \"unknown\")}')
|
|
7730
|
+
print(' '.join(parts))
|
|
7731
|
+
except Exception:
|
|
7732
|
+
pass
|
|
7733
|
+
" 2>/dev/null || true)
|
|
7734
|
+
fi
|
|
7735
|
+
|
|
7588
7736
|
# Degraded providers with small models need simplified prompts
|
|
7589
7737
|
# Full RARV/SDLC instructions overwhelm models < 30B parameters
|
|
7590
7738
|
if [ "${PROVIDER_DEGRADED:-false}" = "true" ]; then
|
|
@@ -7609,15 +7757,15 @@ except: pass
|
|
|
7609
7757
|
else
|
|
7610
7758
|
if [ $retry -eq 0 ]; then
|
|
7611
7759
|
if [ -n "$prd" ]; then
|
|
7612
|
-
echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7760
|
+
echo "Loki Mode with PRD at $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7613
7761
|
else
|
|
7614
|
-
echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7762
|
+
echo "Loki Mode. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $checklist_status $app_runner_info $playwright_info $memory_context_section $analysis_instruction $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7615
7763
|
fi
|
|
7616
7764
|
else
|
|
7617
7765
|
if [ -n "$prd" ]; then
|
|
7618
|
-
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7766
|
+
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). PRD: $prd. $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $checklist_status $app_runner_info $playwright_info $memory_context_section $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7619
7767
|
else
|
|
7620
|
-
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7768
|
+
echo "Loki Mode - Resume iteration #$iteration (retry #$retry). $human_directive $gate_failure_context $queue_tasks $bmad_context $openspec_context $checklist_status $app_runner_info $playwright_info $memory_context_section Use .loki/generated-prd.md if exists. $rarv_instruction $memory_instruction $compaction_reminder $completion_instruction $sdlc_instruction $autonomous_suffix"
|
|
7621
7769
|
fi
|
|
7622
7770
|
fi
|
|
7623
7771
|
fi
|
|
@@ -7742,6 +7890,91 @@ BMAD_QUEUE_EOF
|
|
|
7742
7890
|
log_info "BMAD queue population complete"
|
|
7743
7891
|
}
|
|
7744
7892
|
|
|
7893
|
+
#===============================================================================
|
|
7894
|
+
# OpenSpec Task Queue Population
|
|
7895
|
+
#===============================================================================
|
|
7896
|
+
|
|
7897
|
+
# Populate the task queue from OpenSpec task artifacts
|
|
7898
|
+
# Only runs once -- skips if queue was already populated from OpenSpec
|
|
7899
|
+
populate_openspec_queue() {
|
|
7900
|
+
# Skip if no OpenSpec tasks file
|
|
7901
|
+
if [[ ! -f ".loki/openspec-tasks.json" ]]; then
|
|
7902
|
+
return 0
|
|
7903
|
+
fi
|
|
7904
|
+
|
|
7905
|
+
# Skip if already populated (marker file)
|
|
7906
|
+
if [[ -f ".loki/queue/.openspec-populated" ]]; then
|
|
7907
|
+
log_info "OpenSpec queue already populated, skipping"
|
|
7908
|
+
return 0
|
|
7909
|
+
fi
|
|
7910
|
+
|
|
7911
|
+
log_step "Populating task queue from OpenSpec tasks..."
|
|
7912
|
+
|
|
7913
|
+
# Ensure queue directory exists
|
|
7914
|
+
mkdir -p ".loki/queue"
|
|
7915
|
+
|
|
7916
|
+
# Read OpenSpec tasks and create queue entries
|
|
7917
|
+
python3 << 'OPENSPEC_QUEUE_EOF'
|
|
7918
|
+
import json
|
|
7919
|
+
import sys
|
|
7920
|
+
|
|
7921
|
+
openspec_tasks_path = ".loki/openspec-tasks.json"
|
|
7922
|
+
pending_path = ".loki/queue/pending.json"
|
|
7923
|
+
|
|
7924
|
+
try:
|
|
7925
|
+
with open(openspec_tasks_path, "r") as f:
|
|
7926
|
+
openspec_tasks = json.load(f)
|
|
7927
|
+
except (json.JSONDecodeError, FileNotFoundError) as e:
|
|
7928
|
+
print(f"Warning: Could not read OpenSpec tasks: {e}", file=sys.stderr)
|
|
7929
|
+
sys.exit(0)
|
|
7930
|
+
|
|
7931
|
+
# Load existing queue
|
|
7932
|
+
existing = []
|
|
7933
|
+
try:
|
|
7934
|
+
with open(pending_path, "r") as f:
|
|
7935
|
+
existing = json.load(f)
|
|
7936
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
7937
|
+
pass
|
|
7938
|
+
|
|
7939
|
+
# Convert OpenSpec tasks to queue format (skip completed tasks)
|
|
7940
|
+
for task in openspec_tasks:
|
|
7941
|
+
if task.get("status") == "completed":
|
|
7942
|
+
continue
|
|
7943
|
+
queue_entry = {
|
|
7944
|
+
"id": task.get("id", "openspec-unknown"),
|
|
7945
|
+
"title": task.get("title", "Untitled"),
|
|
7946
|
+
"description": f"[OpenSpec] {task.get('group', 'General')}: {task.get('title', '')}",
|
|
7947
|
+
"priority": task.get("priority", "medium"),
|
|
7948
|
+
"status": "pending",
|
|
7949
|
+
"source": "openspec",
|
|
7950
|
+
"metadata": {
|
|
7951
|
+
"openspec_source": task.get("source", "tasks.md"),
|
|
7952
|
+
"openspec_group": task.get("group", ""),
|
|
7953
|
+
}
|
|
7954
|
+
}
|
|
7955
|
+
existing.append(queue_entry)
|
|
7956
|
+
|
|
7957
|
+
with open(pending_path, "w") as f:
|
|
7958
|
+
json.dump(existing, f, indent=2)
|
|
7959
|
+
|
|
7960
|
+
pending_count = sum(1 for t in openspec_tasks if t.get('status') != 'completed')
|
|
7961
|
+
if pending_count == 0:
|
|
7962
|
+
print("WARNING: All OpenSpec tasks are already marked as completed. No tasks added to queue.", file=sys.stderr)
|
|
7963
|
+
print("Check your tasks.md file -- all checkboxes are checked.", file=sys.stderr)
|
|
7964
|
+
else:
|
|
7965
|
+
print(f"Added {pending_count} OpenSpec tasks to queue")
|
|
7966
|
+
OPENSPEC_QUEUE_EOF
|
|
7967
|
+
|
|
7968
|
+
if [[ $? -ne 0 ]]; then
|
|
7969
|
+
log_warn "Failed to populate OpenSpec queue (python3 error)"
|
|
7970
|
+
return 0
|
|
7971
|
+
fi
|
|
7972
|
+
|
|
7973
|
+
# Mark as populated so we don't re-add on restart
|
|
7974
|
+
touch ".loki/queue/.openspec-populated"
|
|
7975
|
+
log_info "OpenSpec queue population complete"
|
|
7976
|
+
}
|
|
7977
|
+
|
|
7745
7978
|
#===============================================================================
|
|
7746
7979
|
# Main Autonomous Loop
|
|
7747
7980
|
#===============================================================================
|
|
@@ -7823,9 +8056,27 @@ run_autonomous() {
|
|
|
7823
8056
|
fi
|
|
7824
8057
|
fi
|
|
7825
8058
|
|
|
8059
|
+
# Auto-derive completion promise from PRD (v6.10.0)
|
|
8060
|
+
# When PRD exists but no explicit promise, auto-derive one and switch to checkpoint mode
|
|
8061
|
+
if [ -n "$prd_path" ] && [ -f "$prd_path" ] && [ -z "$COMPLETION_PROMISE" ]; then
|
|
8062
|
+
if [ "${LOKI_AUTO_COMPLETION_PROMISE:-true}" = "true" ]; then
|
|
8063
|
+
COMPLETION_PROMISE="All PRD requirements implemented and tests passing"
|
|
8064
|
+
log_info "Auto-derived completion promise: $COMPLETION_PROMISE"
|
|
8065
|
+
# PRD-driven work is finite; switch from perpetual to checkpoint
|
|
8066
|
+
if [ "${LOKI_FORCE_PERPETUAL:-false}" != "true" ] && [ "$AUTONOMY_MODE" = "perpetual" ]; then
|
|
8067
|
+
AUTONOMY_MODE="checkpoint"
|
|
8068
|
+
PERPETUAL_MODE="false"
|
|
8069
|
+
log_info "Switched autonomy mode: perpetual -> checkpoint (PRD-driven work is finite)"
|
|
8070
|
+
fi
|
|
8071
|
+
fi
|
|
8072
|
+
fi
|
|
8073
|
+
|
|
7826
8074
|
# Populate task queue from BMAD artifacts (if present, runs once)
|
|
7827
8075
|
populate_bmad_queue
|
|
7828
8076
|
|
|
8077
|
+
# Populate task queue from OpenSpec artifacts (if present, runs once)
|
|
8078
|
+
populate_openspec_queue
|
|
8079
|
+
|
|
7829
8080
|
# Check max iterations before starting
|
|
7830
8081
|
if check_max_iterations; then
|
|
7831
8082
|
log_error "Max iterations already reached. Reset with: rm .loki/autonomy-state.json"
|
|
@@ -8302,29 +8553,54 @@ if __name__ == "__main__":
|
|
|
8302
8553
|
# Checkpoint after each iteration (v5.57.0)
|
|
8303
8554
|
create_checkpoint "iteration-${ITERATION_COUNT} complete" "iteration-${ITERATION_COUNT}"
|
|
8304
8555
|
|
|
8305
|
-
# Quality gates (v6.
|
|
8556
|
+
# Quality gates (v6.10.0 - escalation ladder)
|
|
8306
8557
|
local gate_failures=""
|
|
8307
8558
|
if [ "${LOKI_HARD_GATES:-true}" = "true" ]; then
|
|
8308
8559
|
# Static analysis gate
|
|
8309
8560
|
if [ "${PHASE_STATIC_ANALYSIS:-true}" = "true" ]; then
|
|
8310
|
-
enforce_static_analysis
|
|
8561
|
+
if enforce_static_analysis; then
|
|
8562
|
+
clear_gate_failure "static_analysis"
|
|
8563
|
+
else
|
|
8564
|
+
local sa_count
|
|
8565
|
+
sa_count=$(track_gate_failure "static_analysis")
|
|
8311
8566
|
gate_failures="${gate_failures}static_analysis,"
|
|
8312
|
-
log_warn "Static analysis FAILED - findings injected into next iteration"
|
|
8313
|
-
|
|
8567
|
+
log_warn "Static analysis FAILED ($sa_count consecutive) - findings injected into next iteration"
|
|
8568
|
+
fi
|
|
8314
8569
|
fi
|
|
8315
8570
|
# Test coverage gate
|
|
8316
8571
|
if [ "${PHASE_UNIT_TESTS:-true}" = "true" ]; then
|
|
8317
|
-
enforce_test_coverage
|
|
8572
|
+
if enforce_test_coverage; then
|
|
8573
|
+
clear_gate_failure "test_coverage"
|
|
8574
|
+
else
|
|
8575
|
+
local tc_count
|
|
8576
|
+
tc_count=$(track_gate_failure "test_coverage")
|
|
8318
8577
|
gate_failures="${gate_failures}test_coverage,"
|
|
8319
|
-
log_warn "Test coverage gate FAILED - must pass next iteration"
|
|
8320
|
-
|
|
8578
|
+
log_warn "Test coverage gate FAILED ($tc_count consecutive) - must pass next iteration"
|
|
8579
|
+
fi
|
|
8321
8580
|
fi
|
|
8322
|
-
# Code review gate (upgraded from advisory)
|
|
8581
|
+
# Code review gate (upgraded from advisory, with escalation)
|
|
8323
8582
|
if [ "$PHASE_CODE_REVIEW" = "true" ] && [ "$ITERATION_COUNT" -gt 0 ]; then
|
|
8324
|
-
run_code_review
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
|
|
8583
|
+
if run_code_review; then
|
|
8584
|
+
clear_gate_failure "code_review"
|
|
8585
|
+
else
|
|
8586
|
+
local cr_count
|
|
8587
|
+
cr_count=$(track_gate_failure "code_review")
|
|
8588
|
+
if [ "$cr_count" -ge "$GATE_PAUSE_LIMIT" ]; then
|
|
8589
|
+
log_error "Gate escalation: code_review failed $cr_count times (>= $GATE_PAUSE_LIMIT) - forcing PAUSE for human intervention"
|
|
8590
|
+
echo "PAUSE" > "${TARGET_DIR:-.}/.loki/signals/GATE_ESCALATION"
|
|
8591
|
+
echo "code_review gate failed $cr_count consecutive times" >> "${TARGET_DIR:-.}/.loki/signals/GATE_ESCALATION"
|
|
8592
|
+
touch "${TARGET_DIR:-.}/.loki/signals/PAUSE"
|
|
8593
|
+
elif [ "$cr_count" -ge "$GATE_ESCALATE_LIMIT" ]; then
|
|
8594
|
+
log_warn "Gate escalation: code_review failed $cr_count times (>= $GATE_ESCALATE_LIMIT) - escalating"
|
|
8595
|
+
echo "ESCALATE" > "${TARGET_DIR:-.}/.loki/signals/GATE_ESCALATION"
|
|
8596
|
+
gate_failures="${gate_failures}code_review_ESCALATED,"
|
|
8597
|
+
elif [ "$cr_count" -ge "$GATE_CLEAR_LIMIT" ]; then
|
|
8598
|
+
log_warn "Gate cleared: code_review failed $cr_count times (>= $GATE_CLEAR_LIMIT) - passing gate this iteration, counter continues"
|
|
8599
|
+
else
|
|
8600
|
+
gate_failures="${gate_failures}code_review,"
|
|
8601
|
+
log_warn "Code review BLOCKED ($cr_count consecutive) - Critical/High findings"
|
|
8602
|
+
fi
|
|
8603
|
+
fi
|
|
8328
8604
|
fi
|
|
8329
8605
|
# Store gate failures for prompt injection
|
|
8330
8606
|
if [ -n "$gate_failures" ]; then
|
package/dashboard/__init__.py
CHANGED
package/docs/INSTALLATION.md
CHANGED
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
package/skills/00-index.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| Scale patterns (50+ agents) | `parallel-workflows.md` + `references/cursor-learnings.md` |
|
|
28
28
|
| GitHub issues, PRs, syncing | `github-integration.md` |
|
|
29
29
|
| Multi-provider (Codex, Gemini) | `providers.md` |
|
|
30
|
+
| OpenSpec delta context, brownfield modifications | `openspec-integration.md` |
|
|
30
31
|
| Plan deepening, knowledge extraction | `compound-learning.md` |
|
|
31
32
|
|
|
32
33
|
## Module Descriptions
|
|
@@ -109,6 +110,14 @@
|
|
|
109
110
|
- Filter by labels, milestone, assignee
|
|
110
111
|
- Requires `gh` CLI authenticated
|
|
111
112
|
|
|
113
|
+
### openspec-integration.md
|
|
114
|
+
**When:** Working with OpenSpec delta context, `--openspec` flag, brownfield modifications
|
|
115
|
+
- Delta-aware development rules (ADDED/MODIFIED/REMOVED)
|
|
116
|
+
- Task execution by group order
|
|
117
|
+
- Scenario-to-test mapping (GIVEN/WHEN/THEN)
|
|
118
|
+
- Source mapping and verification tracking
|
|
119
|
+
- Complexity-based agent strategy
|
|
120
|
+
|
|
112
121
|
### compound-learning.md (v5.30.0)
|
|
113
122
|
**When:** After architecture phase (deepen plan), after verification (extract learnings)
|
|
114
123
|
- Deepen-plan: 4 parallel research agents enhance plans before implementation
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# OpenSpec Integration
|
|
2
|
+
|
|
3
|
+
> **Reference:** OpenSpec delta specs use ADDED/MODIFIED/REMOVED sections to describe changes to existing system behavior. See `.loki/openspec/delta-context.json` for the parsed delta context injected into your prompt.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## When This Module Applies
|
|
8
|
+
|
|
9
|
+
- Your prompt contains an `OPENSPEC DELTA CONTEXT` section
|
|
10
|
+
- The project has `.loki/openspec/delta-context.json`
|
|
11
|
+
- The session was started with `--openspec` flag
|
|
12
|
+
- Tasks in `.loki/queue/pending.json` have `openspec_group` metadata
|
|
13
|
+
|
|
14
|
+
**If none of the above are true, do not load this module.**
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Delta-Aware Development Rules
|
|
19
|
+
|
|
20
|
+
### ADDED Requirements
|
|
21
|
+
|
|
22
|
+
New behavior that does not exist in the codebase yet.
|
|
23
|
+
|
|
24
|
+
1. Create NEW files and functions following existing codebase patterns
|
|
25
|
+
2. Do NOT modify existing code unless the new feature integrates with it
|
|
26
|
+
3. Write tests for every scenario (GIVEN/WHEN/THEN from the delta spec)
|
|
27
|
+
4. Reference: `delta-context.json` entries with `"type": "added"`
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
# Mental model for ADDED
|
|
31
|
+
Read scenario -> Write test -> Implement -> Verify test passes
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### MODIFIED Requirements
|
|
35
|
+
|
|
36
|
+
Existing behavior that is changing. This is the most common delta type in brownfield work.
|
|
37
|
+
|
|
38
|
+
1. Find the EXISTING code that implements this requirement
|
|
39
|
+
2. Modify IN PLACE -- do NOT create new files for modified behavior
|
|
40
|
+
3. Check the `(Previously: ...)` annotation to understand what changed
|
|
41
|
+
4. Update existing tests to match the new behavior
|
|
42
|
+
5. Reference: `delta-context.json` entries with `"type": "modified"`
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
# Mental model for MODIFIED
|
|
46
|
+
Read "(Previously: ...)" -> Find existing code -> Update code -> Update tests -> Verify
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Common mistake:** Treating MODIFIED as ADDED and creating new files. Always search the codebase first for the existing implementation.
|
|
50
|
+
|
|
51
|
+
### REMOVED Requirements
|
|
52
|
+
|
|
53
|
+
Behavior that is being deprecated or deleted.
|
|
54
|
+
|
|
55
|
+
1. Find and remove or deprecate the code implementing this requirement
|
|
56
|
+
2. Check the `(Deprecated: ...)` annotation for the reason
|
|
57
|
+
3. Remove associated tests
|
|
58
|
+
4. Ensure no orphaned imports or dead code remains
|
|
59
|
+
5. Reference: `delta-context.json` entries with `"type": "removed"`
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
# Mental model for REMOVED
|
|
63
|
+
Read "(Deprecated: ...)" -> Find existing code -> Remove code -> Remove tests -> Verify no dead refs
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Task Execution
|
|
69
|
+
|
|
70
|
+
Tasks are generated from OpenSpec `tasks.md` and loaded into `.loki/queue/pending.json`.
|
|
71
|
+
|
|
72
|
+
- Each task has `openspec_group` metadata indicating its task group number
|
|
73
|
+
- Execute tasks in group order (group 1 before group 2, etc.)
|
|
74
|
+
- Within a group, tasks can run in parallel if they touch different files
|
|
75
|
+
- Mark tasks complete in the queue when done
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"id": "task-3",
|
|
80
|
+
"title": "Implement session timeout change",
|
|
81
|
+
"openspec_group": 1,
|
|
82
|
+
"delta_type": "modified",
|
|
83
|
+
"spec_ref": "auth/spec.md#session-expiration"
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Scenario Verification
|
|
90
|
+
|
|
91
|
+
After implementing a requirement, verify its scenarios.
|
|
92
|
+
|
|
93
|
+
1. Each scenario has GIVEN (precondition), WHEN (action), THEN (expected outcome)
|
|
94
|
+
2. Write test cases that map 1:1 to scenarios
|
|
95
|
+
3. Use the scenario name as the test name for traceability
|
|
96
|
+
4. Verification results are tracked in `.loki/openspec/verification-results.json`
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
# Scenario: "Idle timeout" -> test name matches scenario
|
|
100
|
+
def test_idle_timeout():
|
|
101
|
+
# GIVEN an authenticated session
|
|
102
|
+
session = create_authenticated_session()
|
|
103
|
+
# WHEN 15 minutes pass without activity
|
|
104
|
+
advance_time(minutes=15)
|
|
105
|
+
# THEN the session is invalidated
|
|
106
|
+
assert session.is_expired()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Source Mapping
|
|
112
|
+
|
|
113
|
+
`.loki/openspec/source-map.json` maps each task ID to its origin in the spec files.
|
|
114
|
+
|
|
115
|
+
| Field | Purpose |
|
|
116
|
+
|-------|---------|
|
|
117
|
+
| `task_id` | Queue task identifier |
|
|
118
|
+
| `spec_file` | Source spec file path |
|
|
119
|
+
| `requirement` | Requirement name |
|
|
120
|
+
| `scenario` | Scenario name (if applicable) |
|
|
121
|
+
| `line` | Line number in spec file |
|
|
122
|
+
|
|
123
|
+
Use this to trace implementation decisions back to the specification.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Complexity Levels
|
|
128
|
+
|
|
129
|
+
| Level | Tasks | Spec Files | Design | Agent Strategy |
|
|
130
|
+
|-------|-------|------------|--------|----------------|
|
|
131
|
+
| simple | 1-3 | 1 | none | Single agent, sequential |
|
|
132
|
+
| standard | 4-10 | 2-5 | present | Parallel where possible |
|
|
133
|
+
| complex | 11-20 | 5-10 | present | Task tool parallelization |
|
|
134
|
+
| enterprise | 20+ | 10+ | present | Full agent team |
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Common Mistakes
|
|
139
|
+
|
|
140
|
+
| Mistake | Correction |
|
|
141
|
+
|---------|------------|
|
|
142
|
+
| Creating new files for MODIFIED requirements | Search codebase first, update existing code in place |
|
|
143
|
+
| Ignoring `(Previously: ...)` annotations | These tell you exactly what changed -- read them |
|
|
144
|
+
| Not writing tests for GIVEN/WHEN/THEN scenarios | Every scenario must have a corresponding test |
|
|
145
|
+
| Treating all deltas as ADDED | Most brownfield work is MODIFIED -- check the delta type |
|
|
146
|
+
| Skipping REMOVED cleanup | Dead code and orphaned imports cause maintenance burden |
|
|
147
|
+
| Implementing groups out of order | Group 1 must complete before group 2 starts |
|