loki-mode 7.77.0 → 7.79.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/NOTIFY_INTEGRATION.md +9 -0
- package/autonomy/lib/checkpoint_sync.py +189 -0
- package/autonomy/lib/config-map.sh +955 -0
- package/autonomy/loki +143 -8
- package/autonomy/run.sh +222 -1156
- package/autonomy/sandbox.sh +98 -0
- package/autonomy/trigger-server.py +419 -43
- package/dashboard/__init__.py +1 -1
- package/dashboard/server.py +63 -7
- package/docs/CONFIG-FILE-PLAN.md +459 -0
- package/docs/INSTALLATION.md +2 -2
- package/loki-ts/dist/loki.js +2 -2
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
package/autonomy/run.sh
CHANGED
|
@@ -40,7 +40,18 @@
|
|
|
40
40
|
# LOKI_AUDIT_DISABLED - Disable audit logging (default: false)
|
|
41
41
|
# LOKI_MAX_PARALLEL_AGENTS - Limit concurrent agent spawning (default: 10)
|
|
42
42
|
# LOKI_SANDBOX_MODE - Run in sandboxed container (default: false, requires Docker)
|
|
43
|
-
# LOKI_ALLOWED_PATHS - Comma-separated
|
|
43
|
+
# LOKI_ALLOWED_PATHS - Comma-separated path allowlist. PARTIAL enforcement
|
|
44
|
+
# (honest scope): when set, run.sh rejects a sandbox
|
|
45
|
+
# custom --mount whose host path is outside the
|
|
46
|
+
# allowlist (a write surface run.sh itself controls).
|
|
47
|
+
# It does NOT and CANNOT restrict provider-driven
|
|
48
|
+
# agent file writes: claude/codex/cline/aider write
|
|
49
|
+
# files directly, not through run.sh, so run.sh never
|
|
50
|
+
# sees those writes. The containment for agent writes
|
|
51
|
+
# is the OS user + the LOKI_SANDBOX_MODE container
|
|
52
|
+
# (cap-drop, seccomp, read-only mounts), not this var.
|
|
53
|
+
# Treat ALLOWED_PATHS as defense-in-depth on the
|
|
54
|
+
# mount surface, not a complete write sandbox.
|
|
44
55
|
# LOKI_BLOCKED_COMMANDS - Comma-separated blocked shell commands (default: rm -rf /)
|
|
45
56
|
#
|
|
46
57
|
# OIDC / SSO Authentication (optional, works alongside token auth):
|
|
@@ -225,6 +236,17 @@ fi
|
|
|
225
236
|
# Configuration File Support (v4.1.0)
|
|
226
237
|
# Loads settings from config file, environment variables take precedence
|
|
227
238
|
#===============================================================================
|
|
239
|
+
# #691: source the canonical config mapping + shared per-key export helper. This
|
|
240
|
+
# is the SAME lib the loki CLI pre-pass uses, so the YAML/config-file mapping
|
|
241
|
+
# cannot drift between the CLI and the runner. Side-effect-free on source. The
|
|
242
|
+
# parsers below delegate to loki_config_export_key with override=0, preserving
|
|
243
|
+
# the shipped env-wins auto-discovery contract byte-for-byte.
|
|
244
|
+
_LOKI_CONFIG_MAP_LIB="$SCRIPT_DIR/lib/config-map.sh"
|
|
245
|
+
if [ -f "$_LOKI_CONFIG_MAP_LIB" ]; then
|
|
246
|
+
# shellcheck source=lib/config-map.sh
|
|
247
|
+
source "$_LOKI_CONFIG_MAP_LIB"
|
|
248
|
+
fi
|
|
249
|
+
|
|
228
250
|
load_config_file() {
|
|
229
251
|
local config_file=""
|
|
230
252
|
|
|
@@ -258,95 +280,22 @@ load_config_file() {
|
|
|
258
280
|
parse_yaml_with_yq "$config_file"
|
|
259
281
|
}
|
|
260
282
|
|
|
261
|
-
# Fallback YAML parser for simple key: value format
|
|
283
|
+
# Fallback YAML parser for simple key: value format.
|
|
284
|
+
# #691: iterates the canonical LOKI_CONFIG_MAP (single source of truth) instead
|
|
285
|
+
# of 64 hand-maintained set_from_yaml calls. override=0 -> ambient env still
|
|
286
|
+
# wins (auto-discovery contract unchanged). Falls back to nothing if the lib is
|
|
287
|
+
# absent (defensive; the lib ships alongside run.sh).
|
|
262
288
|
parse_simple_yaml() {
|
|
263
289
|
local file="$1"
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
set_from_yaml "$file" "dashboard.port" "LOKI_DASHBOARD_PORT"
|
|
274
|
-
|
|
275
|
-
# Resources
|
|
276
|
-
set_from_yaml "$file" "resources.check_interval" "LOKI_RESOURCE_CHECK_INTERVAL"
|
|
277
|
-
set_from_yaml "$file" "resources.cpu_threshold" "LOKI_RESOURCE_CPU_THRESHOLD"
|
|
278
|
-
set_from_yaml "$file" "resources.mem_threshold" "LOKI_RESOURCE_MEM_THRESHOLD"
|
|
279
|
-
|
|
280
|
-
# Security
|
|
281
|
-
set_from_yaml "$file" "security.staged_autonomy" "LOKI_STAGED_AUTONOMY"
|
|
282
|
-
set_from_yaml "$file" "security.audit_log" "LOKI_AUDIT_LOG"
|
|
283
|
-
set_from_yaml "$file" "security.max_parallel_agents" "LOKI_MAX_PARALLEL_AGENTS"
|
|
284
|
-
set_from_yaml "$file" "security.sandbox_mode" "LOKI_SANDBOX_MODE"
|
|
285
|
-
set_from_yaml "$file" "security.allowed_paths" "LOKI_ALLOWED_PATHS"
|
|
286
|
-
set_from_yaml "$file" "security.blocked_commands" "LOKI_BLOCKED_COMMANDS"
|
|
287
|
-
|
|
288
|
-
# Phases
|
|
289
|
-
set_from_yaml "$file" "phases.unit_tests" "LOKI_PHASE_UNIT_TESTS"
|
|
290
|
-
set_from_yaml "$file" "phases.api_tests" "LOKI_PHASE_API_TESTS"
|
|
291
|
-
set_from_yaml "$file" "phases.e2e_tests" "LOKI_PHASE_E2E_TESTS"
|
|
292
|
-
set_from_yaml "$file" "phases.security" "LOKI_PHASE_SECURITY"
|
|
293
|
-
set_from_yaml "$file" "phases.integration" "LOKI_PHASE_INTEGRATION"
|
|
294
|
-
set_from_yaml "$file" "phases.code_review" "LOKI_PHASE_CODE_REVIEW"
|
|
295
|
-
set_from_yaml "$file" "phases.web_research" "LOKI_PHASE_WEB_RESEARCH"
|
|
296
|
-
set_from_yaml "$file" "phases.performance" "LOKI_PHASE_PERFORMANCE"
|
|
297
|
-
set_from_yaml "$file" "phases.accessibility" "LOKI_PHASE_ACCESSIBILITY"
|
|
298
|
-
set_from_yaml "$file" "phases.regression" "LOKI_PHASE_REGRESSION"
|
|
299
|
-
set_from_yaml "$file" "phases.uat" "LOKI_PHASE_UAT"
|
|
300
|
-
|
|
301
|
-
# Completion
|
|
302
|
-
set_from_yaml "$file" "completion.promise" "LOKI_COMPLETION_PROMISE"
|
|
303
|
-
set_from_yaml "$file" "completion.max_iterations" "LOKI_MAX_ITERATIONS"
|
|
304
|
-
set_from_yaml "$file" "completion.perpetual_mode" "LOKI_PERPETUAL_MODE"
|
|
305
|
-
set_from_yaml "$file" "completion.council.enabled" "LOKI_COUNCIL_ENABLED"
|
|
306
|
-
set_from_yaml "$file" "completion.council.size" "LOKI_COUNCIL_SIZE"
|
|
307
|
-
set_from_yaml "$file" "completion.council.threshold" "LOKI_COUNCIL_THRESHOLD"
|
|
308
|
-
set_from_yaml "$file" "completion.council.check_interval" "LOKI_COUNCIL_CHECK_INTERVAL"
|
|
309
|
-
set_from_yaml "$file" "completion.council.min_iterations" "LOKI_COUNCIL_MIN_ITERATIONS"
|
|
310
|
-
set_from_yaml "$file" "completion.council.stagnation_limit" "LOKI_COUNCIL_STAGNATION_LIMIT"
|
|
311
|
-
set_from_yaml "$file" "completion.uncertainty.escalation" "LOKI_UNCERTAINTY_ESCALATION"
|
|
312
|
-
set_from_yaml "$file" "completion.uncertainty.rounds" "LOKI_UNCERTAINTY_ROUNDS"
|
|
313
|
-
set_from_yaml "$file" "completion.uncertainty.nochange_min" "LOKI_UNCERTAINTY_NOCHANGE_MIN"
|
|
314
|
-
set_from_yaml "$file" "completion.uncertainty.split_rounds" "LOKI_UNCERTAINTY_SPLIT_ROUNDS"
|
|
315
|
-
|
|
316
|
-
# Model
|
|
317
|
-
set_from_yaml "$file" "model.prompt_repetition" "LOKI_PROMPT_REPETITION"
|
|
318
|
-
set_from_yaml "$file" "model.confidence_routing" "LOKI_CONFIDENCE_ROUTING"
|
|
319
|
-
set_from_yaml "$file" "model.autonomy_mode" "LOKI_AUTONOMY_MODE"
|
|
320
|
-
set_from_yaml "$file" "model.planning" "LOKI_MODEL_PLANNING"
|
|
321
|
-
set_from_yaml "$file" "model.development" "LOKI_MODEL_DEVELOPMENT"
|
|
322
|
-
set_from_yaml "$file" "model.fast" "LOKI_MODEL_FAST"
|
|
323
|
-
|
|
324
|
-
# Parallel
|
|
325
|
-
set_from_yaml "$file" "parallel.enabled" "LOKI_PARALLEL_MODE"
|
|
326
|
-
set_from_yaml "$file" "parallel.max_worktrees" "LOKI_MAX_WORKTREES"
|
|
327
|
-
set_from_yaml "$file" "parallel.max_sessions" "LOKI_MAX_PARALLEL_SESSIONS"
|
|
328
|
-
set_from_yaml "$file" "parallel.testing" "LOKI_PARALLEL_TESTING"
|
|
329
|
-
set_from_yaml "$file" "parallel.docs" "LOKI_PARALLEL_DOCS"
|
|
330
|
-
set_from_yaml "$file" "parallel.blog" "LOKI_PARALLEL_BLOG"
|
|
331
|
-
set_from_yaml "$file" "parallel.auto_merge" "LOKI_AUTO_MERGE"
|
|
332
|
-
|
|
333
|
-
# Complexity
|
|
334
|
-
set_from_yaml "$file" "complexity.tier" "LOKI_COMPLEXITY"
|
|
335
|
-
|
|
336
|
-
# GitHub
|
|
337
|
-
set_from_yaml "$file" "github.import" "LOKI_GITHUB_IMPORT"
|
|
338
|
-
set_from_yaml "$file" "github.pr" "LOKI_GITHUB_PR"
|
|
339
|
-
set_from_yaml "$file" "github.sync" "LOKI_GITHUB_SYNC"
|
|
340
|
-
set_from_yaml "$file" "github.repo" "LOKI_GITHUB_REPO"
|
|
341
|
-
set_from_yaml "$file" "github.labels" "LOKI_GITHUB_LABELS"
|
|
342
|
-
set_from_yaml "$file" "github.milestone" "LOKI_GITHUB_MILESTONE"
|
|
343
|
-
set_from_yaml "$file" "github.assignee" "LOKI_GITHUB_ASSIGNEE"
|
|
344
|
-
set_from_yaml "$file" "github.limit" "LOKI_GITHUB_LIMIT"
|
|
345
|
-
set_from_yaml "$file" "github.pr_label" "LOKI_GITHUB_PR_LABEL"
|
|
346
|
-
|
|
347
|
-
# Notifications
|
|
348
|
-
set_from_yaml "$file" "notifications.enabled" "LOKI_NOTIFICATIONS"
|
|
349
|
-
set_from_yaml "$file" "notifications.sound" "LOKI_NOTIFICATION_SOUND"
|
|
290
|
+
if ! declare -p LOKI_CONFIG_MAP >/dev/null 2>&1; then
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
local mapping yaml_path env_var
|
|
294
|
+
for mapping in "${LOKI_CONFIG_MAP[@]}"; do
|
|
295
|
+
yaml_path="${mapping%%:*}"
|
|
296
|
+
env_var="${mapping##*:}"
|
|
297
|
+
set_from_yaml "$file" "$yaml_path" "$env_var"
|
|
298
|
+
done
|
|
350
299
|
}
|
|
351
300
|
|
|
352
301
|
# Validate YAML value to prevent injection attacks
|
|
@@ -359,11 +308,19 @@ validate_yaml_value() {
|
|
|
359
308
|
return 1
|
|
360
309
|
fi
|
|
361
310
|
|
|
362
|
-
# Reject values with dangerous shell metacharacters
|
|
363
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
311
|
+
# Reject values with dangerous shell metacharacters.
|
|
312
|
+
# SECURITY FIX (#691 wave): the previous [[ "$value" =~ [\$\`...] ]]
|
|
313
|
+
# bracket-regex matched NOTHING -- backslash escapes inside a bash regex
|
|
314
|
+
# bracket class are taken literally, so the guard ACCEPTED $(...), backticks,
|
|
315
|
+
# pipes, etc. (verified behaviorally: it returned 0 for "$(touch /tmp/x)").
|
|
316
|
+
# A `case` glob class has no such ambiguity and actually rejects the metachars.
|
|
317
|
+
# Allow alphanumeric, spaces, dots, dashes, underscores, slashes, colons,
|
|
318
|
+
# commas, @.
|
|
319
|
+
case "$value" in
|
|
320
|
+
*'$'* | *'`'* | *'|'* | *';'* | *'&'* | *'>'* | *'<'* | *'('* | *')'* \
|
|
321
|
+
| *'{'* | *'}'* | *'['* | *']'* | *'\'* )
|
|
322
|
+
return 1 ;;
|
|
323
|
+
esac
|
|
367
324
|
|
|
368
325
|
# Reject values that are too long (DoS protection)
|
|
369
326
|
if [ "${#value}" -gt "$max_length" ]; then
|
|
@@ -391,7 +348,8 @@ set_from_yaml() {
|
|
|
391
348
|
local yaml_path="$2"
|
|
392
349
|
local env_var="$3"
|
|
393
350
|
|
|
394
|
-
# Skip if env var is already set
|
|
351
|
+
# Skip if env var is already set (env-wins guard; the shared helper also
|
|
352
|
+
# enforces this with override=0, but checking here avoids the extraction).
|
|
395
353
|
if [ -n "${!env_var:-}" ]; then
|
|
396
354
|
return 0
|
|
397
355
|
fi
|
|
@@ -409,95 +367,40 @@ set_from_yaml() {
|
|
|
409
367
|
# Use read to avoid xargs command execution risks
|
|
410
368
|
value=$(grep -E "^\s*${escaped_key}:" "$file" 2>/dev/null | head -1 | sed -E 's/.*:\s*//' | sed 's/#.*//' | sed 's/^["\x27]//;s/["\x27]$//' | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
411
369
|
|
|
412
|
-
#
|
|
413
|
-
|
|
370
|
+
# #691: delegate the env-wins guard + ${VAR}-expand + validate + export to the
|
|
371
|
+
# single shared helper (override=0 -> ambient env wins). Falls back to the
|
|
372
|
+
# original inline export if the lib is unavailable, so behavior is identical
|
|
373
|
+
# either way.
|
|
374
|
+
if declare -f loki_config_export_key >/dev/null 2>&1; then
|
|
375
|
+
loki_config_export_key "$env_var" "$value" 0 || true
|
|
376
|
+
elif [ -n "$value" ] && [ "$value" != "null" ] && validate_yaml_value "$value"; then
|
|
414
377
|
export "$env_var=$value"
|
|
415
378
|
fi
|
|
416
379
|
}
|
|
417
380
|
|
|
418
|
-
# Parse YAML using yq (proper parser)
|
|
381
|
+
# Parse YAML using yq (proper parser).
|
|
382
|
+
# #691: iterates the canonical LOKI_CONFIG_MAP and delegates the env-wins guard +
|
|
383
|
+
# expand + validate + export to the single shared helper (override=0 -> ambient
|
|
384
|
+
# env wins; auto-discovery contract unchanged). The inline 61-entry table that
|
|
385
|
+
# used to drift from parse_simple_yaml's 64 is gone.
|
|
419
386
|
parse_yaml_with_yq() {
|
|
420
387
|
local file="$1"
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
"core.max_wait:LOKI_MAX_WAIT"
|
|
425
|
-
"core.skip_prereqs:LOKI_SKIP_PREREQS"
|
|
426
|
-
"dashboard.enabled:LOKI_DASHBOARD"
|
|
427
|
-
"dashboard.port:LOKI_DASHBOARD_PORT"
|
|
428
|
-
"resources.check_interval:LOKI_RESOURCE_CHECK_INTERVAL"
|
|
429
|
-
"resources.cpu_threshold:LOKI_RESOURCE_CPU_THRESHOLD"
|
|
430
|
-
"resources.mem_threshold:LOKI_RESOURCE_MEM_THRESHOLD"
|
|
431
|
-
"security.staged_autonomy:LOKI_STAGED_AUTONOMY"
|
|
432
|
-
"security.audit_log:LOKI_AUDIT_LOG"
|
|
433
|
-
"security.max_parallel_agents:LOKI_MAX_PARALLEL_AGENTS"
|
|
434
|
-
"security.sandbox_mode:LOKI_SANDBOX_MODE"
|
|
435
|
-
"security.allowed_paths:LOKI_ALLOWED_PATHS"
|
|
436
|
-
"security.blocked_commands:LOKI_BLOCKED_COMMANDS"
|
|
437
|
-
"phases.unit_tests:LOKI_PHASE_UNIT_TESTS"
|
|
438
|
-
"phases.api_tests:LOKI_PHASE_API_TESTS"
|
|
439
|
-
"phases.e2e_tests:LOKI_PHASE_E2E_TESTS"
|
|
440
|
-
"phases.security:LOKI_PHASE_SECURITY"
|
|
441
|
-
"phases.integration:LOKI_PHASE_INTEGRATION"
|
|
442
|
-
"phases.code_review:LOKI_PHASE_CODE_REVIEW"
|
|
443
|
-
"phases.web_research:LOKI_PHASE_WEB_RESEARCH"
|
|
444
|
-
"phases.performance:LOKI_PHASE_PERFORMANCE"
|
|
445
|
-
"phases.accessibility:LOKI_PHASE_ACCESSIBILITY"
|
|
446
|
-
"phases.regression:LOKI_PHASE_REGRESSION"
|
|
447
|
-
"phases.uat:LOKI_PHASE_UAT"
|
|
448
|
-
"completion.promise:LOKI_COMPLETION_PROMISE"
|
|
449
|
-
"completion.max_iterations:LOKI_MAX_ITERATIONS"
|
|
450
|
-
"completion.perpetual_mode:LOKI_PERPETUAL_MODE"
|
|
451
|
-
"completion.council.enabled:LOKI_COUNCIL_ENABLED"
|
|
452
|
-
"completion.council.size:LOKI_COUNCIL_SIZE"
|
|
453
|
-
"completion.council.threshold:LOKI_COUNCIL_THRESHOLD"
|
|
454
|
-
"completion.council.check_interval:LOKI_COUNCIL_CHECK_INTERVAL"
|
|
455
|
-
"completion.council.min_iterations:LOKI_COUNCIL_MIN_ITERATIONS"
|
|
456
|
-
"completion.council.stagnation_limit:LOKI_COUNCIL_STAGNATION_LIMIT"
|
|
457
|
-
"completion.uncertainty.escalation:LOKI_UNCERTAINTY_ESCALATION"
|
|
458
|
-
"completion.uncertainty.rounds:LOKI_UNCERTAINTY_ROUNDS"
|
|
459
|
-
"completion.uncertainty.nochange_min:LOKI_UNCERTAINTY_NOCHANGE_MIN"
|
|
460
|
-
"completion.uncertainty.split_rounds:LOKI_UNCERTAINTY_SPLIT_ROUNDS"
|
|
461
|
-
"model.prompt_repetition:LOKI_PROMPT_REPETITION"
|
|
462
|
-
"model.confidence_routing:LOKI_CONFIDENCE_ROUTING"
|
|
463
|
-
"model.autonomy_mode:LOKI_AUTONOMY_MODE"
|
|
464
|
-
"parallel.enabled:LOKI_PARALLEL_MODE"
|
|
465
|
-
"parallel.max_worktrees:LOKI_MAX_WORKTREES"
|
|
466
|
-
"parallel.max_sessions:LOKI_MAX_PARALLEL_SESSIONS"
|
|
467
|
-
"parallel.testing:LOKI_PARALLEL_TESTING"
|
|
468
|
-
"parallel.docs:LOKI_PARALLEL_DOCS"
|
|
469
|
-
"parallel.blog:LOKI_PARALLEL_BLOG"
|
|
470
|
-
"parallel.auto_merge:LOKI_AUTO_MERGE"
|
|
471
|
-
"complexity.tier:LOKI_COMPLEXITY"
|
|
472
|
-
"github.import:LOKI_GITHUB_IMPORT"
|
|
473
|
-
"github.pr:LOKI_GITHUB_PR"
|
|
474
|
-
"github.sync:LOKI_GITHUB_SYNC"
|
|
475
|
-
"github.repo:LOKI_GITHUB_REPO"
|
|
476
|
-
"github.labels:LOKI_GITHUB_LABELS"
|
|
477
|
-
"github.milestone:LOKI_GITHUB_MILESTONE"
|
|
478
|
-
"github.assignee:LOKI_GITHUB_ASSIGNEE"
|
|
479
|
-
"github.limit:LOKI_GITHUB_LIMIT"
|
|
480
|
-
"github.pr_label:LOKI_GITHUB_PR_LABEL"
|
|
481
|
-
"notifications.enabled:LOKI_NOTIFICATIONS"
|
|
482
|
-
"notifications.sound:LOKI_NOTIFICATION_SOUND"
|
|
483
|
-
)
|
|
484
|
-
|
|
485
|
-
for mapping in "${mappings[@]}"; do
|
|
486
|
-
local yaml_path="${mapping%%:*}"
|
|
487
|
-
local env_var="${mapping##*:}"
|
|
388
|
+
if ! declare -p LOKI_CONFIG_MAP >/dev/null 2>&1; then
|
|
389
|
+
return 0
|
|
390
|
+
fi
|
|
488
391
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
392
|
+
local mapping yaml_path env_var value
|
|
393
|
+
for mapping in "${LOKI_CONFIG_MAP[@]}"; do
|
|
394
|
+
yaml_path="${mapping%%:*}"
|
|
395
|
+
env_var="${mapping##*:}"
|
|
493
396
|
|
|
494
|
-
# Extract value using yq
|
|
495
|
-
local value
|
|
397
|
+
# Extract value using yq (env-wins guard is enforced by the shared helper).
|
|
496
398
|
value=$(yq eval ".$yaml_path // \"\"" "$file" 2>/dev/null)
|
|
497
399
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
400
|
+
if declare -f loki_config_export_key >/dev/null 2>&1; then
|
|
401
|
+
loki_config_export_key "$env_var" "$value" 0 || true
|
|
402
|
+
elif [ -n "$value" ] && [ "$value" != "null" ] && [ "$value" != "" ] \
|
|
403
|
+
&& [ -z "${!env_var:-}" ] && validate_yaml_value "$value"; then
|
|
501
404
|
export "$env_var=$value"
|
|
502
405
|
fi
|
|
503
406
|
done
|
|
@@ -588,9 +491,29 @@ STAGED_AUTONOMY=${LOKI_STAGED_AUTONOMY:-false} # Require plan approval
|
|
|
588
491
|
AUDIT_LOG_ENABLED=${LOKI_AUDIT_LOG:-true} # Enable audit logging (on by default)
|
|
589
492
|
MAX_PARALLEL_AGENTS=${LOKI_MAX_PARALLEL_AGENTS:-10} # Limit concurrent agents
|
|
590
493
|
SANDBOX_MODE=${LOKI_SANDBOX_MODE:-false} # Docker sandbox mode (informational; the real dispatch reads LOKI_SANDBOX_MODE at autonomy/loki:1965 and execs sandbox.sh -- this var is not consumed in run.sh)
|
|
591
|
-
ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} #
|
|
494
|
+
ALLOWED_PATHS=${LOKI_ALLOWED_PATHS:-""} # PARTIAL enforcement. When set, a sandbox custom --mount host path outside the allowlist is rejected in sandbox.sh (_sandbox_path_within_allowed), NOT in run.sh. Does NOT restrict provider-driven agent writes (run.sh never sees them); the sandbox container is the containment for those. Default empty = no restriction.
|
|
592
495
|
BLOCKED_COMMANDS=${LOKI_BLOCKED_COMMANDS:-"rm -rf /,dd if=,mkfs,:(){ :|:& };:"}
|
|
593
496
|
|
|
497
|
+
# LOKI_SESSION_ID is woven into many filesystem paths (.loki/sessions/<id>/...,
|
|
498
|
+
# session locks, PID files, per-session state). It is operator/issue-derived
|
|
499
|
+
# (trusted-ish), but a value like "../../tmp/x" would escape the .loki/ tree.
|
|
500
|
+
# Sanitize ONCE here at intake: any char outside [A-Za-z0-9._-] is replaced with
|
|
501
|
+
# "_" and a leading run of dots/dashes is neutralized, so every downstream
|
|
502
|
+
# session path is contained. Unset stays unset (default behavior unchanged).
|
|
503
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
504
|
+
_loki_sid_raw="$LOKI_SESSION_ID"
|
|
505
|
+
_loki_sid_safe="${_loki_sid_raw//[^A-Za-z0-9._-]/_}"
|
|
506
|
+
# Strip leading dots/dashes so "../x" -> "x" cannot reference a parent dir
|
|
507
|
+
# or look like an option; collapse a now-empty result to a safe literal.
|
|
508
|
+
_loki_sid_safe="${_loki_sid_safe#"${_loki_sid_safe%%[!.-]*}"}"
|
|
509
|
+
[ -n "$_loki_sid_safe" ] || _loki_sid_safe="session"
|
|
510
|
+
if [ "$_loki_sid_safe" != "$_loki_sid_raw" ]; then
|
|
511
|
+
printf 'loki: WARNING sanitized unsafe LOKI_SESSION_ID %s -> %s\n' "$_loki_sid_raw" "$_loki_sid_safe" >&2
|
|
512
|
+
fi
|
|
513
|
+
export LOKI_SESSION_ID="$_loki_sid_safe"
|
|
514
|
+
unset _loki_sid_raw _loki_sid_safe
|
|
515
|
+
fi
|
|
516
|
+
|
|
594
517
|
# Process Supervision (opt-in)
|
|
595
518
|
WATCHDOG_ENABLED=${LOKI_WATCHDOG:-"false"} # Enable process health monitoring
|
|
596
519
|
WATCHDOG_INTERVAL=${LOKI_WATCHDOG_INTERVAL:-30} # Check interval in seconds
|
|
@@ -1902,41 +1825,7 @@ detect_complexity() {
|
|
|
1902
1825
|
}
|
|
1903
1826
|
|
|
1904
1827
|
# Get phases based on complexity tier
|
|
1905
|
-
get_complexity_phases() {
|
|
1906
|
-
case "$DETECTED_COMPLEXITY" in
|
|
1907
|
-
simple)
|
|
1908
|
-
echo "3"
|
|
1909
|
-
;;
|
|
1910
|
-
standard)
|
|
1911
|
-
echo "6"
|
|
1912
|
-
;;
|
|
1913
|
-
complex)
|
|
1914
|
-
echo "8"
|
|
1915
|
-
;;
|
|
1916
|
-
*)
|
|
1917
|
-
echo "6" # Default to standard
|
|
1918
|
-
;;
|
|
1919
|
-
esac
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
1828
|
# Get phase names based on complexity tier
|
|
1923
|
-
get_phase_names() {
|
|
1924
|
-
case "$DETECTED_COMPLEXITY" in
|
|
1925
|
-
simple)
|
|
1926
|
-
echo "IMPLEMENT TEST DEPLOY"
|
|
1927
|
-
;;
|
|
1928
|
-
standard)
|
|
1929
|
-
echo "RESEARCH DESIGN IMPLEMENT TEST REVIEW DEPLOY"
|
|
1930
|
-
;;
|
|
1931
|
-
complex)
|
|
1932
|
-
echo "RESEARCH ARCHITECTURE DESIGN IMPLEMENT TEST REVIEW SECURITY DEPLOY"
|
|
1933
|
-
;;
|
|
1934
|
-
*)
|
|
1935
|
-
echo "RESEARCH DESIGN IMPLEMENT TEST REVIEW DEPLOY"
|
|
1936
|
-
;;
|
|
1937
|
-
esac
|
|
1938
|
-
}
|
|
1939
|
-
|
|
1940
1829
|
#===============================================================================
|
|
1941
1830
|
# Dynamic Tier Selection (RARV-aware model routing)
|
|
1942
1831
|
#===============================================================================
|
|
@@ -2629,32 +2518,6 @@ send_notification() {
|
|
|
2629
2518
|
return 0
|
|
2630
2519
|
}
|
|
2631
2520
|
|
|
2632
|
-
# Convenience notification functions
|
|
2633
|
-
notify_task_started() {
|
|
2634
|
-
local task_name="$1"
|
|
2635
|
-
send_notification "Task Started" "$task_name" "low"
|
|
2636
|
-
}
|
|
2637
|
-
|
|
2638
|
-
notify_task_completed() {
|
|
2639
|
-
local task_name="$1"
|
|
2640
|
-
send_notification "Task Completed" "$task_name" "normal"
|
|
2641
|
-
}
|
|
2642
|
-
|
|
2643
|
-
notify_task_failed() {
|
|
2644
|
-
local task_name="$1"
|
|
2645
|
-
local error="${2:-Unknown error}"
|
|
2646
|
-
send_notification "Task Failed" "$task_name: $error" "critical"
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
notify_phase_complete() {
|
|
2650
|
-
local phase_name="$1"
|
|
2651
|
-
send_notification "Phase Complete" "$phase_name" "normal"
|
|
2652
|
-
}
|
|
2653
|
-
|
|
2654
|
-
notify_all_complete() {
|
|
2655
|
-
send_notification "All Tasks Complete" "Loki Mode has finished all tasks" "normal"
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
2521
|
notify_intervention_needed() {
|
|
2659
2522
|
local reason="$1"
|
|
2660
2523
|
# Delegate-then-notify: this helper ONLY fires the (gated) desktop ping. It
|
|
@@ -3722,23 +3585,6 @@ init_parallel_streams() {
|
|
|
3722
3585
|
}
|
|
3723
3586
|
|
|
3724
3587
|
# Spawn feature worktree from task
|
|
3725
|
-
spawn_feature_stream() {
|
|
3726
|
-
local feature_name="$1"
|
|
3727
|
-
local task_description="$2"
|
|
3728
|
-
|
|
3729
|
-
# Check worktree limit
|
|
3730
|
-
# BUG-PAR-012: Worktree count subtracts 1 for main (git worktree list includes main)
|
|
3731
|
-
local worktree_count_raw=$(git -C "$TARGET_DIR" worktree list 2>/dev/null | wc -l)
|
|
3732
|
-
local worktree_count=$((worktree_count_raw > 0 ? worktree_count_raw - 1 : 0))
|
|
3733
|
-
if [ "$worktree_count" -ge "$MAX_WORKTREES" ]; then
|
|
3734
|
-
log_warn "Max worktrees reached ($MAX_WORKTREES). Queuing feature: $feature_name"
|
|
3735
|
-
return 1
|
|
3736
|
-
fi
|
|
3737
|
-
|
|
3738
|
-
create_worktree "feature-$feature_name" "feature/$feature_name"
|
|
3739
|
-
spawn_worktree_session "feature-$feature_name" "$task_description"
|
|
3740
|
-
}
|
|
3741
|
-
|
|
3742
3588
|
# Cleanup all worktrees on exit
|
|
3743
3589
|
cleanup_parallel_streams() {
|
|
3744
3590
|
log_header "Cleaning Up Parallel Streams"
|
|
@@ -4352,55 +4198,6 @@ EOF
|
|
|
4352
4198
|
LAST_KNOWN_PHASE=""
|
|
4353
4199
|
|
|
4354
4200
|
# Set the current phase and emit event if changed
|
|
4355
|
-
set_phase() {
|
|
4356
|
-
local new_phase="$1"
|
|
4357
|
-
local orch_file=".loki/state/orchestrator.json"
|
|
4358
|
-
|
|
4359
|
-
mkdir -p .loki/state
|
|
4360
|
-
|
|
4361
|
-
# Get current phase
|
|
4362
|
-
local current_phase=""
|
|
4363
|
-
if [ -f "$orch_file" ]; then
|
|
4364
|
-
current_phase=$(python3 -c "import json; print(json.load(open('$orch_file')).get('currentPhase', ''))" 2>/dev/null || echo "")
|
|
4365
|
-
fi
|
|
4366
|
-
|
|
4367
|
-
# Only emit event if phase changed
|
|
4368
|
-
if [ "$new_phase" != "$current_phase" ]; then
|
|
4369
|
-
emit_event_json "phase_change" \
|
|
4370
|
-
"from=$current_phase" \
|
|
4371
|
-
"to=$new_phase" \
|
|
4372
|
-
"iteration=$ITERATION_COUNT"
|
|
4373
|
-
|
|
4374
|
-
log_info "Phase changed: $current_phase -> $new_phase"
|
|
4375
|
-
|
|
4376
|
-
# Update orchestrator state (atomic via temp file + mv)
|
|
4377
|
-
# BUG ARCH-001 fix: prevent state corruption if process is killed mid-write
|
|
4378
|
-
if [ -f "$orch_file" ]; then
|
|
4379
|
-
python3 -c "
|
|
4380
|
-
import json, sys, os, tempfile
|
|
4381
|
-
orch_file = sys.argv[1]
|
|
4382
|
-
new_phase = sys.argv[2]
|
|
4383
|
-
with open(orch_file, 'r') as f:
|
|
4384
|
-
data = json.load(f)
|
|
4385
|
-
data['currentPhase'] = new_phase
|
|
4386
|
-
orch_dir = os.path.dirname(orch_file)
|
|
4387
|
-
fd, tmp = tempfile.mkstemp(dir=orch_dir, suffix='.json')
|
|
4388
|
-
with os.fdopen(fd, 'w') as f:
|
|
4389
|
-
json.dump(data, f, indent=2)
|
|
4390
|
-
os.replace(tmp, orch_file)
|
|
4391
|
-
" "$orch_file" "$new_phase" 2>/dev/null || true
|
|
4392
|
-
fi
|
|
4393
|
-
fi
|
|
4394
|
-
|
|
4395
|
-
LAST_KNOWN_PHASE="$new_phase"
|
|
4396
|
-
|
|
4397
|
-
# v7.5.12: Append a structured log entry to the active iteration task so
|
|
4398
|
-
# the dashboard shows per-phase progress (REASON / ACT / REFLECT / VERIFY).
|
|
4399
|
-
# No-op if no iteration is active or queue file is missing/corrupt.
|
|
4400
|
-
append_iteration_task_log "${ITERATION_COUNT:-0}" "$new_phase" "info" \
|
|
4401
|
-
"Phase entered: $new_phase" 2>/dev/null || true
|
|
4402
|
-
}
|
|
4403
|
-
|
|
4404
4201
|
# v7.5.12: append a log entry to the iteration-N task in in-progress.json.
|
|
4405
4202
|
# Args: iteration, phase, level, message. All silent on failure -- this
|
|
4406
4203
|
# must NEVER kill the run.
|
|
@@ -5729,438 +5526,6 @@ stop_status_monitor() {
|
|
|
5729
5526
|
# Web Dashboard
|
|
5730
5527
|
#===============================================================================
|
|
5731
5528
|
|
|
5732
|
-
generate_dashboard() {
|
|
5733
|
-
# Copy dashboard from skill installation (v4.0.0 with Anthropic design language)
|
|
5734
|
-
local skill_dashboard="$SCRIPT_DIR/.loki/dashboard/index.html"
|
|
5735
|
-
local project_name=$(basename "$(pwd)")
|
|
5736
|
-
local project_path=$(pwd)
|
|
5737
|
-
|
|
5738
|
-
if [ -f "$skill_dashboard" ]; then
|
|
5739
|
-
# v7.5.8: Escape sed-special chars in project_name/project_path before
|
|
5740
|
-
# interpolating into the substitution RHS. Without this, a project
|
|
5741
|
-
# whose path contains '|' (the chosen sed delimiter), '&' (RHS
|
|
5742
|
-
# backref), '\' or '/' would either break the substitution or allow
|
|
5743
|
-
# attacker-controlled text to be smuggled into the served HTML.
|
|
5744
|
-
local project_name_sed project_path_sed
|
|
5745
|
-
project_name_sed=$(printf '%s' "$project_name" | sed -e 's/[\\&|/]/\\&/g')
|
|
5746
|
-
project_path_sed=$(printf '%s' "$project_path" | sed -e 's/[\\&|/]/\\&/g')
|
|
5747
|
-
|
|
5748
|
-
# Copy and inject project info
|
|
5749
|
-
sed -e "s|Loki Mode</title>|Loki Mode - ${project_name_sed}</title>|g" \
|
|
5750
|
-
-e "s|<div class=\"project-name\" id=\"project-name\">--|<div class=\"project-name\" id=\"project-name\">${project_name_sed}|g" \
|
|
5751
|
-
-e "s|<div class=\"project-path\" id=\"project-path\" title=\"\">--|<div class=\"project-path\" id=\"project-path\" title=\"${project_path_sed}\">${project_path_sed}|g" \
|
|
5752
|
-
"$skill_dashboard" > .loki/dashboard/index.html
|
|
5753
|
-
log_info "Dashboard copied from skill installation"
|
|
5754
|
-
log_info "Project: $project_name ($project_path)"
|
|
5755
|
-
return
|
|
5756
|
-
fi
|
|
5757
|
-
|
|
5758
|
-
# Fallback: Generate basic dashboard if external file not found
|
|
5759
|
-
cat > .loki/dashboard/index.html << 'DASHBOARD_HTML'
|
|
5760
|
-
<!DOCTYPE html>
|
|
5761
|
-
<html lang="en">
|
|
5762
|
-
<head>
|
|
5763
|
-
<meta charset="UTF-8">
|
|
5764
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5765
|
-
<title>Loki Mode Dashboard</title>
|
|
5766
|
-
<style>
|
|
5767
|
-
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
5768
|
-
body {
|
|
5769
|
-
font-family: 'Söhne', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
5770
|
-
background: #FAF9F6;
|
|
5771
|
-
color: #1A1A1A;
|
|
5772
|
-
padding: 24px;
|
|
5773
|
-
min-height: 100vh;
|
|
5774
|
-
}
|
|
5775
|
-
.header {
|
|
5776
|
-
text-align: center;
|
|
5777
|
-
padding: 32px 20px;
|
|
5778
|
-
margin-bottom: 32px;
|
|
5779
|
-
}
|
|
5780
|
-
.header h1 {
|
|
5781
|
-
color: #D97757;
|
|
5782
|
-
font-size: 28px;
|
|
5783
|
-
font-weight: 600;
|
|
5784
|
-
letter-spacing: -0.5px;
|
|
5785
|
-
margin-bottom: 8px;
|
|
5786
|
-
}
|
|
5787
|
-
.header .subtitle {
|
|
5788
|
-
color: #666;
|
|
5789
|
-
font-size: 14px;
|
|
5790
|
-
font-weight: 400;
|
|
5791
|
-
}
|
|
5792
|
-
.header .phase {
|
|
5793
|
-
display: inline-block;
|
|
5794
|
-
margin-top: 16px;
|
|
5795
|
-
padding: 8px 16px;
|
|
5796
|
-
background: #FFF;
|
|
5797
|
-
border: 1px solid #E5E3DE;
|
|
5798
|
-
border-radius: 20px;
|
|
5799
|
-
font-size: 13px;
|
|
5800
|
-
color: #1A1A1A;
|
|
5801
|
-
font-weight: 500;
|
|
5802
|
-
}
|
|
5803
|
-
.stats {
|
|
5804
|
-
display: flex;
|
|
5805
|
-
justify-content: center;
|
|
5806
|
-
gap: 16px;
|
|
5807
|
-
margin-bottom: 40px;
|
|
5808
|
-
flex-wrap: wrap;
|
|
5809
|
-
}
|
|
5810
|
-
.stat {
|
|
5811
|
-
background: #FFF;
|
|
5812
|
-
border: 1px solid #E5E3DE;
|
|
5813
|
-
border-radius: 12px;
|
|
5814
|
-
padding: 20px 32px;
|
|
5815
|
-
text-align: center;
|
|
5816
|
-
min-width: 140px;
|
|
5817
|
-
transition: box-shadow 0.2s ease;
|
|
5818
|
-
}
|
|
5819
|
-
.stat:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.06); }
|
|
5820
|
-
.stat .number { font-size: 36px; font-weight: 600; margin-bottom: 4px; }
|
|
5821
|
-
.stat .label { font-size: 12px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
5822
|
-
.stat.pending .number { color: #D97757; }
|
|
5823
|
-
.stat.progress .number { color: #5B8DEF; }
|
|
5824
|
-
.stat.completed .number { color: #2E9E6E; }
|
|
5825
|
-
.stat.failed .number { color: #D44F4F; }
|
|
5826
|
-
.stat.agents .number { color: #9B6DD6; }
|
|
5827
|
-
.section-header {
|
|
5828
|
-
text-align: center;
|
|
5829
|
-
font-size: 16px;
|
|
5830
|
-
font-weight: 600;
|
|
5831
|
-
color: #666;
|
|
5832
|
-
margin: 40px 0 20px 0;
|
|
5833
|
-
text-transform: uppercase;
|
|
5834
|
-
letter-spacing: 1px;
|
|
5835
|
-
}
|
|
5836
|
-
.agents-grid {
|
|
5837
|
-
display: grid;
|
|
5838
|
-
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
5839
|
-
gap: 16px;
|
|
5840
|
-
max-width: 1400px;
|
|
5841
|
-
margin: 0 auto 40px auto;
|
|
5842
|
-
}
|
|
5843
|
-
.agent-card {
|
|
5844
|
-
background: #FFF;
|
|
5845
|
-
border: 1px solid #E5E3DE;
|
|
5846
|
-
border-radius: 12px;
|
|
5847
|
-
padding: 16px;
|
|
5848
|
-
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
|
5849
|
-
}
|
|
5850
|
-
.agent-card:hover {
|
|
5851
|
-
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
|
5852
|
-
border-color: #9B6DD6;
|
|
5853
|
-
}
|
|
5854
|
-
.agent-card .agent-header {
|
|
5855
|
-
display: flex;
|
|
5856
|
-
justify-content: space-between;
|
|
5857
|
-
align-items: flex-start;
|
|
5858
|
-
margin-bottom: 12px;
|
|
5859
|
-
}
|
|
5860
|
-
.agent-card .agent-id {
|
|
5861
|
-
font-size: 11px;
|
|
5862
|
-
color: #999;
|
|
5863
|
-
font-family: monospace;
|
|
5864
|
-
}
|
|
5865
|
-
.agent-card .model-badge {
|
|
5866
|
-
padding: 4px 10px;
|
|
5867
|
-
border-radius: 6px;
|
|
5868
|
-
font-size: 10px;
|
|
5869
|
-
font-weight: 600;
|
|
5870
|
-
text-transform: uppercase;
|
|
5871
|
-
letter-spacing: 0.5px;
|
|
5872
|
-
}
|
|
5873
|
-
.agent-card .model-badge.sonnet {
|
|
5874
|
-
background: #E8F0FD;
|
|
5875
|
-
color: #5B8DEF;
|
|
5876
|
-
}
|
|
5877
|
-
.agent-card .model-badge.haiku {
|
|
5878
|
-
background: #FFF4E6;
|
|
5879
|
-
color: #F59E0B;
|
|
5880
|
-
}
|
|
5881
|
-
.agent-card .model-badge.opus {
|
|
5882
|
-
background: #F3E8FF;
|
|
5883
|
-
color: #9B6DD6;
|
|
5884
|
-
}
|
|
5885
|
-
.agent-card .agent-type {
|
|
5886
|
-
font-size: 14px;
|
|
5887
|
-
font-weight: 600;
|
|
5888
|
-
color: #1A1A1A;
|
|
5889
|
-
margin-bottom: 8px;
|
|
5890
|
-
}
|
|
5891
|
-
.agent-card .agent-status {
|
|
5892
|
-
display: inline-block;
|
|
5893
|
-
padding: 3px 8px;
|
|
5894
|
-
border-radius: 4px;
|
|
5895
|
-
font-size: 10px;
|
|
5896
|
-
font-weight: 500;
|
|
5897
|
-
margin-bottom: 12px;
|
|
5898
|
-
}
|
|
5899
|
-
.agent-card .agent-status.active {
|
|
5900
|
-
background: #E6F5EE;
|
|
5901
|
-
color: #2E9E6E;
|
|
5902
|
-
}
|
|
5903
|
-
.agent-card .agent-status.completed {
|
|
5904
|
-
background: #F0EFEA;
|
|
5905
|
-
color: #666;
|
|
5906
|
-
}
|
|
5907
|
-
.agent-card .agent-work {
|
|
5908
|
-
font-size: 12px;
|
|
5909
|
-
color: #666;
|
|
5910
|
-
line-height: 1.5;
|
|
5911
|
-
margin-bottom: 8px;
|
|
5912
|
-
}
|
|
5913
|
-
.agent-card .agent-meta {
|
|
5914
|
-
display: flex;
|
|
5915
|
-
gap: 12px;
|
|
5916
|
-
font-size: 11px;
|
|
5917
|
-
color: #999;
|
|
5918
|
-
margin-top: 8px;
|
|
5919
|
-
padding-top: 8px;
|
|
5920
|
-
border-top: 1px solid #F0EFEA;
|
|
5921
|
-
}
|
|
5922
|
-
.agent-card .agent-meta span {
|
|
5923
|
-
display: flex;
|
|
5924
|
-
align-items: center;
|
|
5925
|
-
gap: 4px;
|
|
5926
|
-
}
|
|
5927
|
-
.columns {
|
|
5928
|
-
display: flex;
|
|
5929
|
-
gap: 20px;
|
|
5930
|
-
overflow-x: auto;
|
|
5931
|
-
padding-bottom: 24px;
|
|
5932
|
-
max-width: 1400px;
|
|
5933
|
-
margin: 0 auto;
|
|
5934
|
-
}
|
|
5935
|
-
.column {
|
|
5936
|
-
flex: 1;
|
|
5937
|
-
min-width: 300px;
|
|
5938
|
-
max-width: 350px;
|
|
5939
|
-
background: #FFF;
|
|
5940
|
-
border: 1px solid #E5E3DE;
|
|
5941
|
-
border-radius: 12px;
|
|
5942
|
-
padding: 20px;
|
|
5943
|
-
}
|
|
5944
|
-
.column h2 {
|
|
5945
|
-
font-size: 13px;
|
|
5946
|
-
font-weight: 600;
|
|
5947
|
-
color: #666;
|
|
5948
|
-
margin-bottom: 16px;
|
|
5949
|
-
display: flex;
|
|
5950
|
-
align-items: center;
|
|
5951
|
-
gap: 10px;
|
|
5952
|
-
text-transform: uppercase;
|
|
5953
|
-
letter-spacing: 0.5px;
|
|
5954
|
-
}
|
|
5955
|
-
.column h2 .count {
|
|
5956
|
-
background: #F0EFEA;
|
|
5957
|
-
padding: 3px 10px;
|
|
5958
|
-
border-radius: 12px;
|
|
5959
|
-
font-size: 11px;
|
|
5960
|
-
color: #1A1A1A;
|
|
5961
|
-
}
|
|
5962
|
-
.column.pending h2 .count { background: #FCEEE8; color: #D97757; }
|
|
5963
|
-
.column.progress h2 .count { background: #E8F0FD; color: #5B8DEF; }
|
|
5964
|
-
.column.completed h2 .count { background: #E6F5EE; color: #2E9E6E; }
|
|
5965
|
-
.column.failed h2 .count { background: #FCE8E8; color: #D44F4F; }
|
|
5966
|
-
.task {
|
|
5967
|
-
background: #FAF9F6;
|
|
5968
|
-
border: 1px solid #E5E3DE;
|
|
5969
|
-
border-radius: 8px;
|
|
5970
|
-
padding: 14px;
|
|
5971
|
-
margin-bottom: 12px;
|
|
5972
|
-
transition: border-color 0.2s ease;
|
|
5973
|
-
}
|
|
5974
|
-
.task:hover { border-color: #D97757; }
|
|
5975
|
-
.task .id { font-size: 10px; color: #999; margin-bottom: 6px; font-family: monospace; }
|
|
5976
|
-
.task .type {
|
|
5977
|
-
display: inline-block;
|
|
5978
|
-
background: #FCEEE8;
|
|
5979
|
-
color: #D97757;
|
|
5980
|
-
padding: 3px 10px;
|
|
5981
|
-
border-radius: 4px;
|
|
5982
|
-
font-size: 11px;
|
|
5983
|
-
font-weight: 500;
|
|
5984
|
-
margin-bottom: 8px;
|
|
5985
|
-
}
|
|
5986
|
-
.task .title { font-size: 13px; color: #1A1A1A; line-height: 1.5; }
|
|
5987
|
-
.task .error {
|
|
5988
|
-
font-size: 11px;
|
|
5989
|
-
color: #D44F4F;
|
|
5990
|
-
margin-top: 10px;
|
|
5991
|
-
padding: 10px;
|
|
5992
|
-
background: #FCE8E8;
|
|
5993
|
-
border-radius: 6px;
|
|
5994
|
-
font-family: monospace;
|
|
5995
|
-
}
|
|
5996
|
-
.refresh {
|
|
5997
|
-
position: fixed;
|
|
5998
|
-
bottom: 24px;
|
|
5999
|
-
right: 24px;
|
|
6000
|
-
background: #D97757;
|
|
6001
|
-
color: white;
|
|
6002
|
-
border: none;
|
|
6003
|
-
padding: 12px 24px;
|
|
6004
|
-
border-radius: 8px;
|
|
6005
|
-
cursor: pointer;
|
|
6006
|
-
font-size: 14px;
|
|
6007
|
-
font-weight: 500;
|
|
6008
|
-
transition: background 0.2s ease;
|
|
6009
|
-
box-shadow: 0 4px 12px rgba(217, 119, 87, 0.3);
|
|
6010
|
-
}
|
|
6011
|
-
.refresh:hover { background: #C56747; }
|
|
6012
|
-
.updated {
|
|
6013
|
-
text-align: center;
|
|
6014
|
-
color: #999;
|
|
6015
|
-
font-size: 12px;
|
|
6016
|
-
margin-top: 24px;
|
|
6017
|
-
}
|
|
6018
|
-
.empty {
|
|
6019
|
-
color: #999;
|
|
6020
|
-
font-size: 13px;
|
|
6021
|
-
text-align: center;
|
|
6022
|
-
padding: 24px;
|
|
6023
|
-
font-style: italic;
|
|
6024
|
-
}
|
|
6025
|
-
.powered-by {
|
|
6026
|
-
text-align: center;
|
|
6027
|
-
margin-top: 40px;
|
|
6028
|
-
padding-top: 24px;
|
|
6029
|
-
border-top: 1px solid #E5E3DE;
|
|
6030
|
-
color: #999;
|
|
6031
|
-
font-size: 12px;
|
|
6032
|
-
}
|
|
6033
|
-
.powered-by span { color: #D97757; font-weight: 500; }
|
|
6034
|
-
</style>
|
|
6035
|
-
</head>
|
|
6036
|
-
<body>
|
|
6037
|
-
<div class="header">
|
|
6038
|
-
<h1>LOKI MODE</h1>
|
|
6039
|
-
<div class="subtitle">Autonomous Spec-to-Product System</div>
|
|
6040
|
-
<div class="phase" id="phase">Loading...</div>
|
|
6041
|
-
</div>
|
|
6042
|
-
<div class="stats">
|
|
6043
|
-
<div class="stat agents"><div class="number" id="agents-count">-</div><div class="label">Active Agents</div></div>
|
|
6044
|
-
<div class="stat pending"><div class="number" id="pending-count">-</div><div class="label">Pending</div></div>
|
|
6045
|
-
<div class="stat progress"><div class="number" id="progress-count">-</div><div class="label">In Progress</div></div>
|
|
6046
|
-
<div class="stat completed"><div class="number" id="completed-count">-</div><div class="label">Completed</div></div>
|
|
6047
|
-
<div class="stat failed"><div class="number" id="failed-count">-</div><div class="label">Failed</div></div>
|
|
6048
|
-
</div>
|
|
6049
|
-
<div class="section-header">Active Agents</div>
|
|
6050
|
-
<div class="agents-grid" id="agents-grid"></div>
|
|
6051
|
-
<div class="section-header">Task Queue</div>
|
|
6052
|
-
<div class="columns">
|
|
6053
|
-
<div class="column pending"><h2>Pending <span class="count" id="pending-badge">0</span></h2><div id="pending-tasks"></div></div>
|
|
6054
|
-
<div class="column progress"><h2>In Progress <span class="count" id="progress-badge">0</span></h2><div id="progress-tasks"></div></div>
|
|
6055
|
-
<div class="column completed"><h2>Completed <span class="count" id="completed-badge">0</span></h2><div id="completed-tasks"></div></div>
|
|
6056
|
-
<div class="column failed"><h2>Failed <span class="count" id="failed-badge">0</span></h2><div id="failed-tasks"></div></div>
|
|
6057
|
-
</div>
|
|
6058
|
-
<div class="updated" id="updated">Last updated: -</div>
|
|
6059
|
-
<div class="powered-by">Powered by <span>${PROVIDER_DISPLAY_NAME:-Claude}</span></div>
|
|
6060
|
-
<button class="refresh" onclick="loadData()">Refresh</button>
|
|
6061
|
-
<script>
|
|
6062
|
-
async function loadJSON(path) {
|
|
6063
|
-
try {
|
|
6064
|
-
const res = await fetch(path + '?t=' + Date.now());
|
|
6065
|
-
if (!res.ok) return [];
|
|
6066
|
-
const text = await res.text();
|
|
6067
|
-
if (!text.trim()) return [];
|
|
6068
|
-
const data = JSON.parse(text);
|
|
6069
|
-
return Array.isArray(data) ? data : (data.tasks || data.agents || []);
|
|
6070
|
-
} catch { return []; }
|
|
6071
|
-
}
|
|
6072
|
-
function getModelClass(model) {
|
|
6073
|
-
if (!model) return 'sonnet';
|
|
6074
|
-
const m = model.toLowerCase();
|
|
6075
|
-
if (m.includes('haiku')) return 'haiku';
|
|
6076
|
-
if (m.includes('opus')) return 'opus';
|
|
6077
|
-
return 'sonnet';
|
|
6078
|
-
}
|
|
6079
|
-
function formatDuration(isoDate) {
|
|
6080
|
-
if (!isoDate) return 'Unknown';
|
|
6081
|
-
const start = new Date(isoDate);
|
|
6082
|
-
const now = new Date();
|
|
6083
|
-
const seconds = Math.floor((now - start) / 1000);
|
|
6084
|
-
if (seconds < 60) return seconds + 's';
|
|
6085
|
-
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
|
6086
|
-
return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm';
|
|
6087
|
-
}
|
|
6088
|
-
function renderAgent(agent) {
|
|
6089
|
-
const modelClass = getModelClass(agent.model);
|
|
6090
|
-
const modelName = agent.model || 'Sonnet 4.5';
|
|
6091
|
-
const agentType = agent.agent_type || 'general-purpose';
|
|
6092
|
-
const status = agent.status === 'completed' ? 'completed' : 'active';
|
|
6093
|
-
const currentTask = agent.current_task || (agent.tasks_completed && agent.tasks_completed.length > 0
|
|
6094
|
-
? 'Completed: ' + agent.tasks_completed.join(', ')
|
|
6095
|
-
: 'Initializing...');
|
|
6096
|
-
const duration = formatDuration(agent.spawned_at);
|
|
6097
|
-
const tasksCount = agent.tasks_completed ? agent.tasks_completed.length : 0;
|
|
6098
|
-
|
|
6099
|
-
return `
|
|
6100
|
-
<div class="agent-card">
|
|
6101
|
-
<div class="agent-header">
|
|
6102
|
-
<div class="agent-id">${agent.agent_id || 'Unknown'}</div>
|
|
6103
|
-
<div class="model-badge ${modelClass}">${modelName}</div>
|
|
6104
|
-
</div>
|
|
6105
|
-
<div class="agent-type">${agentType}</div>
|
|
6106
|
-
<div class="agent-status ${status}">${status}</div>
|
|
6107
|
-
<div class="agent-work">${currentTask}</div>
|
|
6108
|
-
<div class="agent-meta">
|
|
6109
|
-
<span>${duration}</span>
|
|
6110
|
-
<span>${tasksCount} tasks</span>
|
|
6111
|
-
</div>
|
|
6112
|
-
</div>
|
|
6113
|
-
`;
|
|
6114
|
-
}
|
|
6115
|
-
function renderTask(task) {
|
|
6116
|
-
const payload = task.payload || {};
|
|
6117
|
-
const title = payload.description || payload.action || task.type || 'Task';
|
|
6118
|
-
const error = task.lastError ? `<div class="error">${task.lastError}</div>` : '';
|
|
6119
|
-
return `<div class="task"><div class="id">${task.id}</div><span class="type">${task.type || 'general'}</span><div class="title">${title}</div>${error}</div>`;
|
|
6120
|
-
}
|
|
6121
|
-
async function loadData() {
|
|
6122
|
-
const [pending, progress, completed, failed, agents] = await Promise.all([
|
|
6123
|
-
loadJSON('../queue/pending.json'),
|
|
6124
|
-
loadJSON('../queue/in-progress.json'),
|
|
6125
|
-
loadJSON('../queue/completed.json'),
|
|
6126
|
-
loadJSON('../queue/failed.json'),
|
|
6127
|
-
loadJSON('../state/agents.json')
|
|
6128
|
-
]);
|
|
6129
|
-
|
|
6130
|
-
// Agent stats
|
|
6131
|
-
document.getElementById('agents-count').textContent = agents.length;
|
|
6132
|
-
document.getElementById('agents-grid').innerHTML = agents.length
|
|
6133
|
-
? agents.map(renderAgent).join('')
|
|
6134
|
-
: '<div class="empty">No active agents</div>';
|
|
6135
|
-
|
|
6136
|
-
// Task stats
|
|
6137
|
-
document.getElementById('pending-count').textContent = pending.length;
|
|
6138
|
-
document.getElementById('progress-count').textContent = progress.length;
|
|
6139
|
-
document.getElementById('completed-count').textContent = completed.length;
|
|
6140
|
-
document.getElementById('failed-count').textContent = failed.length;
|
|
6141
|
-
document.getElementById('pending-badge').textContent = pending.length;
|
|
6142
|
-
document.getElementById('progress-badge').textContent = progress.length;
|
|
6143
|
-
document.getElementById('completed-badge').textContent = completed.length;
|
|
6144
|
-
document.getElementById('failed-badge').textContent = failed.length;
|
|
6145
|
-
document.getElementById('pending-tasks').innerHTML = pending.length ? pending.map(renderTask).join('') : '<div class="empty">No pending tasks</div>';
|
|
6146
|
-
document.getElementById('progress-tasks').innerHTML = progress.length ? progress.map(renderTask).join('') : '<div class="empty">No tasks in progress</div>';
|
|
6147
|
-
document.getElementById('completed-tasks').innerHTML = completed.length ? completed.slice(-10).reverse().map(renderTask).join('') : '<div class="empty">No completed tasks</div>';
|
|
6148
|
-
document.getElementById('failed-tasks').innerHTML = failed.length ? failed.map(renderTask).join('') : '<div class="empty">No failed tasks</div>';
|
|
6149
|
-
|
|
6150
|
-
try {
|
|
6151
|
-
const state = await fetch('../state/orchestrator.json?t=' + Date.now()).then(r => r.json());
|
|
6152
|
-
document.getElementById('phase').textContent = 'Phase: ' + (state.currentPhase || 'UNKNOWN');
|
|
6153
|
-
} catch { document.getElementById('phase').textContent = 'Phase: UNKNOWN'; }
|
|
6154
|
-
document.getElementById('updated').textContent = 'Last updated: ' + new Date().toLocaleTimeString();
|
|
6155
|
-
}
|
|
6156
|
-
loadData();
|
|
6157
|
-
setInterval(loadData, 3000);
|
|
6158
|
-
</script>
|
|
6159
|
-
</body>
|
|
6160
|
-
</html>
|
|
6161
|
-
DASHBOARD_HTML
|
|
6162
|
-
}
|
|
6163
|
-
|
|
6164
5529
|
update_agents_state() {
|
|
6165
5530
|
# Aggregate agent information from .agent/sub-agents/*.json into .loki/state/agents.json
|
|
6166
5531
|
local agents_dir=".agent/sub-agents"
|
|
@@ -6828,6 +6193,28 @@ check_command_allowed() {
|
|
|
6828
6193
|
return 0
|
|
6829
6194
|
}
|
|
6830
6195
|
|
|
6196
|
+
# A5 (ALLOWED_PATHS enforcement) -- HONEST THREAT MODEL
|
|
6197
|
+
# --------------------------------------------------------------------------
|
|
6198
|
+
# The DOMINANT file-write path in an autonomous build is the PROVIDER CLI
|
|
6199
|
+
# (claude / codex / cline / aider) writing files directly. run.sh hands the
|
|
6200
|
+
# work to that CLI and never sees those writes as shell redirections, so run.sh
|
|
6201
|
+
# CANNOT intercept provider-driven writes. ALLOWED_PATHS therefore cannot be a
|
|
6202
|
+
# complete sandbox; the real containment for provider writes is the OS user +
|
|
6203
|
+
# the LOKI_SANDBOX_MODE container (sandbox.sh, cap-drop/seccomp/read-only
|
|
6204
|
+
# mounts), not this shell.
|
|
6205
|
+
#
|
|
6206
|
+
# Where ALLOWED_PATHS is actually enforced: the sandbox custom --mount surface.
|
|
6207
|
+
# That enforcement lives in sandbox.sh, NOT here. When ALLOWED_PATHS is set, a
|
|
6208
|
+
# host path that loki is about to bind-mount into the container is checked by
|
|
6209
|
+
# _sandbox_path_within_allowed (autonomy/sandbox.sh), and an operator-supplied
|
|
6210
|
+
# `loki sandbox run` argv is checked by _sandbox_command_allowed there too.
|
|
6211
|
+
# run.sh itself does not consume ALLOWED_PATHS for any write decision -- it has
|
|
6212
|
+
# no run.sh-controlled host-write surface to gate -- so there is no enforcement
|
|
6213
|
+
# helper here. (A run.sh-local helper existed previously but had zero call
|
|
6214
|
+
# sites; it was removed so the only enforcement path is the one that genuinely
|
|
6215
|
+
# runs. See tests/test-allowed-paths-a5.sh, which exercises the sandbox.sh
|
|
6216
|
+
# functions.)
|
|
6217
|
+
|
|
6831
6218
|
#===============================================================================
|
|
6832
6219
|
# Cross-Project Learnings Database
|
|
6833
6220
|
#===============================================================================
|
|
@@ -6853,34 +6240,6 @@ init_learnings_db() {
|
|
|
6853
6240
|
log_info "Learnings database initialized at: $learnings_dir"
|
|
6854
6241
|
}
|
|
6855
6242
|
|
|
6856
|
-
save_learning() {
|
|
6857
|
-
# Save a learning to the cross-project database
|
|
6858
|
-
local learning_type="$1" # pattern, mistake, success
|
|
6859
|
-
local category="$2"
|
|
6860
|
-
local description="$3"
|
|
6861
|
-
local project="${4:-$(basename "$(pwd)")}"
|
|
6862
|
-
|
|
6863
|
-
local learnings_dir="${HOME}/.loki/learnings"
|
|
6864
|
-
local target_file="$learnings_dir/${learning_type}s.jsonl"
|
|
6865
|
-
|
|
6866
|
-
if [ ! -d "$learnings_dir" ]; then
|
|
6867
|
-
init_learnings_db
|
|
6868
|
-
fi
|
|
6869
|
-
|
|
6870
|
-
local learning_entry
|
|
6871
|
-
if command -v jq >/dev/null 2>&1; then
|
|
6872
|
-
learning_entry=$(jq -n --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg proj "$project" --arg cat "$category" --arg desc "$description" '{timestamp:$ts,project:$proj,category:$cat,description:$desc}')
|
|
6873
|
-
else
|
|
6874
|
-
local safe_proj safe_cat safe_desc
|
|
6875
|
-
safe_proj=$(printf '%s' "$project" | sed 's/["\\]/\\&/g; s/\n/\\n/g')
|
|
6876
|
-
safe_cat=$(printf '%s' "$category" | sed 's/["\\]/\\&/g; s/\n/\\n/g')
|
|
6877
|
-
safe_desc=$(printf '%s' "$description" | sed 's/["\\]/\\&/g; s/\n/\\n/g')
|
|
6878
|
-
learning_entry="{\"timestamp\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\",\"project\":\"$safe_proj\",\"category\":\"$safe_cat\",\"description\":\"$safe_desc\"}"
|
|
6879
|
-
fi
|
|
6880
|
-
echo "$learning_entry" >> "$target_file"
|
|
6881
|
-
log_info "Saved $learning_type: $category"
|
|
6882
|
-
}
|
|
6883
|
-
|
|
6884
6243
|
get_relevant_learnings() {
|
|
6885
6244
|
# Get learnings relevant to the current context
|
|
6886
6245
|
local context="$1"
|
|
@@ -7882,24 +7241,6 @@ with open(gate_file, 'w') as f:
|
|
|
7882
7241
|
" 2>/dev/null || true
|
|
7883
7242
|
}
|
|
7884
7243
|
|
|
7885
|
-
get_gate_failure_count() {
|
|
7886
|
-
local gate_name="$1"
|
|
7887
|
-
local gate_file="${TARGET_DIR:-.}/.loki/quality/gate-failure-count.json"
|
|
7888
|
-
[ -f "$gate_file" ] || { echo "0"; return; }
|
|
7889
|
-
|
|
7890
|
-
_GATE_FILE="$gate_file" _GATE_NAME="$gate_name" python3 -c "
|
|
7891
|
-
import json, os
|
|
7892
|
-
gate_file = os.environ['_GATE_FILE']
|
|
7893
|
-
gate_name = os.environ['_GATE_NAME']
|
|
7894
|
-
try:
|
|
7895
|
-
with open(gate_file) as f:
|
|
7896
|
-
counts = json.load(f)
|
|
7897
|
-
print(counts.get(gate_name, 0))
|
|
7898
|
-
except (json.JSONDecodeError, FileNotFoundError, OSError):
|
|
7899
|
-
print(0)
|
|
7900
|
-
" 2>/dev/null || echo "0"
|
|
7901
|
-
}
|
|
7902
|
-
|
|
7903
7244
|
# ============================================================================
|
|
7904
7245
|
# Hard Quality Gate: Test Coverage (v6.7.0)
|
|
7905
7246
|
# Detects test runner and runs tests with coverage reporting
|
|
@@ -10102,182 +9443,6 @@ DA_AGG_PATCH
|
|
|
10102
9443
|
# Only runs when complexity >= standard (6+ agents).
|
|
10103
9444
|
#===============================================================================
|
|
10104
9445
|
|
|
10105
|
-
run_adversarial_testing() {
|
|
10106
|
-
local loki_dir="${TARGET_DIR:-.}/.loki"
|
|
10107
|
-
local adversarial_dir="$loki_dir/quality/adversarial"
|
|
10108
|
-
local test_id
|
|
10109
|
-
test_id="adversarial-$(date -u +%Y%m%dT%H%M%SZ)-${ITERATION_COUNT:-0}"
|
|
10110
|
-
mkdir -p "$adversarial_dir/$test_id"
|
|
10111
|
-
|
|
10112
|
-
# Only run for Standard+ complexity
|
|
10113
|
-
local complexity="${LOKI_COMPLEXITY:-auto}"
|
|
10114
|
-
if [ "$complexity" = "simple" ]; then
|
|
10115
|
-
log_debug "Adversarial testing skipped: simple complexity tier"
|
|
10116
|
-
return 0
|
|
10117
|
-
fi
|
|
10118
|
-
|
|
10119
|
-
# Check if adversarial testing is disabled
|
|
10120
|
-
if [ "${LOKI_ADVERSARIAL_TESTING:-true}" = "false" ]; then
|
|
10121
|
-
log_debug "Adversarial testing disabled via LOKI_ADVERSARIAL_TESTING=false"
|
|
10122
|
-
return 0
|
|
10123
|
-
fi
|
|
10124
|
-
|
|
10125
|
-
log_header "ADVERSARIAL TESTING: $test_id"
|
|
10126
|
-
|
|
10127
|
-
# Get diff for adversarial analysis
|
|
10128
|
-
local diff_content
|
|
10129
|
-
diff_content=$(git -C "${TARGET_DIR:-.}" diff HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --cached 2>/dev/null || echo "")
|
|
10130
|
-
if [ -z "$diff_content" ]; then
|
|
10131
|
-
log_info "Adversarial testing: No diff to test, skipping"
|
|
10132
|
-
return 0
|
|
10133
|
-
fi
|
|
10134
|
-
|
|
10135
|
-
local changed_files
|
|
10136
|
-
changed_files=$(git -C "${TARGET_DIR:-.}" diff --name-only HEAD~1 2>/dev/null || git -C "${TARGET_DIR:-.}" diff --name-only --cached 2>/dev/null || echo "")
|
|
10137
|
-
|
|
10138
|
-
# Write analysis files -- use printf to prevent shell variable expansion (#78)
|
|
10139
|
-
local diff_file="$adversarial_dir/$test_id/diff.txt"
|
|
10140
|
-
local files_file="$adversarial_dir/$test_id/files.txt"
|
|
10141
|
-
printf '%s\n' "$diff_content" > "$diff_file"
|
|
10142
|
-
printf '%s\n' "$changed_files" > "$files_file"
|
|
10143
|
-
|
|
10144
|
-
# Build adversarial prompt -- use heredoc with quoted delimiter to prevent
|
|
10145
|
-
# shell variable expansion in diff content (fixes #78)
|
|
10146
|
-
local files_content changed_content
|
|
10147
|
-
files_content=$(cat "$files_file")
|
|
10148
|
-
changed_content=$(head -500 "$diff_file")
|
|
10149
|
-
local adversarial_prompt
|
|
10150
|
-
read -r -d '' adversarial_prompt <<'ADVERSARIAL_EOF' || true
|
|
10151
|
-
You are an ADVERSARIAL TESTER. Your goal is to BREAK the implementation.
|
|
10152
|
-
|
|
10153
|
-
CHANGED FILES:
|
|
10154
|
-
__FILES_PLACEHOLDER__
|
|
10155
|
-
|
|
10156
|
-
DIFF:
|
|
10157
|
-
__DIFF_PLACEHOLDER__
|
|
10158
|
-
|
|
10159
|
-
YOUR MISSION:
|
|
10160
|
-
1. Find edge cases that will cause crashes or incorrect behavior
|
|
10161
|
-
2. Identify inputs that bypass validation
|
|
10162
|
-
3. Find race conditions or concurrency issues
|
|
10163
|
-
4. Discover security vulnerabilities (injection, auth bypass, SSRF)
|
|
10164
|
-
5. Find resource exhaustion vectors (unbounded loops, memory leaks)
|
|
10165
|
-
6. Identify error handling gaps (missing try/catch, unchecked returns)
|
|
10166
|
-
|
|
10167
|
-
OUTPUT FORMAT (STRICT):
|
|
10168
|
-
ATTACK_VECTORS:
|
|
10169
|
-
- [severity] [category] description | reproduction steps
|
|
10170
|
-
Severity: Critical, High, Medium, Low
|
|
10171
|
-
Category: crash, security, correctness, performance, resource
|
|
10172
|
-
|
|
10173
|
-
SUGGESTED_TESTS:
|
|
10174
|
-
- Test description that would catch this issue
|
|
10175
|
-
|
|
10176
|
-
OVERALL_RISK: HIGH or MEDIUM or LOW
|
|
10177
|
-
ADVERSARIAL_EOF
|
|
10178
|
-
# Substitute placeholders with actual content (safe from shell expansion)
|
|
10179
|
-
adversarial_prompt="${adversarial_prompt/__FILES_PLACEHOLDER__/$files_content}"
|
|
10180
|
-
adversarial_prompt="${adversarial_prompt/__DIFF_PLACEHOLDER__/$changed_content}"
|
|
10181
|
-
|
|
10182
|
-
local result_file="$adversarial_dir/$test_id/result.txt"
|
|
10183
|
-
|
|
10184
|
-
# Run adversarial agent
|
|
10185
|
-
log_info "Spawning adversarial agent..."
|
|
10186
|
-
case "${PROVIDER_NAME:-claude}" in
|
|
10187
|
-
claude)
|
|
10188
|
-
if command -v claude &>/dev/null; then
|
|
10189
|
-
# EMBED 2 + 3 (v7.33.0). Adversarial probe subcall.
|
|
10190
|
-
# $adversarial_prompt is fully self-contained (instructions +
|
|
10191
|
-
# changed files + diff inlined via the heredoc above) and output
|
|
10192
|
-
# is captured to $result_file. So:
|
|
10193
|
-
# EMBED 2 (--bare): no hooks/LSP/CLAUDE.md/MCP needed; cheaper.
|
|
10194
|
-
# Opt out LOKI_BARE_SUBCALLS=0.
|
|
10195
|
-
# EMBED 3 (--disallowedTools): keep an adversarial agent from
|
|
10196
|
-
# casually mutating the tree. Deny Edit/Write/NotebookEdit +
|
|
10197
|
-
# git mutation forms (incl. git -C / --git-dir evasions);
|
|
10198
|
-
# read-only git stays allowed. Guardrail, not a sandbox.
|
|
10199
|
-
# Opt out LOKI_REVIEW_TOOL_GUARD=0.
|
|
10200
|
-
local _adv_argv=("--dangerously-skip-permissions")
|
|
10201
|
-
if type loki_subcall_bare_enabled >/dev/null 2>&1 && loki_subcall_bare_enabled; then
|
|
10202
|
-
_adv_argv+=("--bare")
|
|
10203
|
-
fi
|
|
10204
|
-
if type loki_review_guard_enabled >/dev/null 2>&1 && loki_review_guard_enabled; then
|
|
10205
|
-
_adv_argv+=("--disallowedTools" "$(loki_review_guard_denylist)")
|
|
10206
|
-
fi
|
|
10207
|
-
# EMBED 3b (--allowedTools, #167): positive least-privilege
|
|
10208
|
-
# allowlist. DEFAULT OFF (opt-in LOKI_REVIEW_ALLOWLIST=1).
|
|
10209
|
-
# Emitted ALONGSIDE the denylist (deny precedence verified
|
|
10210
|
-
# live, holds under --dangerously-skip-permissions). See
|
|
10211
|
-
# loki_review_allowlist.
|
|
10212
|
-
if type loki_review_allowlist_enabled >/dev/null 2>&1 && loki_review_allowlist_enabled; then
|
|
10213
|
-
_adv_argv+=("--allowedTools" "$(loki_review_allowlist)")
|
|
10214
|
-
fi
|
|
10215
|
-
# caveman HARD-SUPPRESS (parsed output): the adversarial probe's
|
|
10216
|
-
# output is parsed for findings/severity. Disable caveman
|
|
10217
|
-
# unconditionally (CAVEMAN_DEFAULT_MODE=off) so compression cannot
|
|
10218
|
-
# drop or reword a finding. No-op when caveman is absent.
|
|
10219
|
-
CAVEMAN_DEFAULT_MODE=off \
|
|
10220
|
-
claude "${_adv_argv[@]}" -p "$adversarial_prompt" \
|
|
10221
|
-
--output-format text > "$result_file" 2>/dev/null || true
|
|
10222
|
-
fi
|
|
10223
|
-
;;
|
|
10224
|
-
codex)
|
|
10225
|
-
if command -v codex &>/dev/null; then
|
|
10226
|
-
codex exec --sandbox workspace-write --skip-git-repo-check "$adversarial_prompt" \
|
|
10227
|
-
> "$result_file" 2>/dev/null || true
|
|
10228
|
-
fi
|
|
10229
|
-
;;
|
|
10230
|
-
cline)
|
|
10231
|
-
if command -v cline &>/dev/null; then
|
|
10232
|
-
invoke_cline_capture "$adversarial_prompt" \
|
|
10233
|
-
> "$result_file" 2>/dev/null || true
|
|
10234
|
-
fi
|
|
10235
|
-
;;
|
|
10236
|
-
aider)
|
|
10237
|
-
if command -v aider &>/dev/null; then
|
|
10238
|
-
invoke_aider_capture "$adversarial_prompt" \
|
|
10239
|
-
> "$result_file" 2>/dev/null || true
|
|
10240
|
-
fi
|
|
10241
|
-
;;
|
|
10242
|
-
*)
|
|
10243
|
-
echo "ATTACK_VECTORS: None (unknown provider)" > "$result_file"
|
|
10244
|
-
echo "OVERALL_RISK: LOW" >> "$result_file"
|
|
10245
|
-
;;
|
|
10246
|
-
esac
|
|
10247
|
-
|
|
10248
|
-
if [ ! -s "$result_file" ]; then
|
|
10249
|
-
log_warn "Adversarial agent produced no output"
|
|
10250
|
-
return 0
|
|
10251
|
-
fi
|
|
10252
|
-
|
|
10253
|
-
# Parse risk level
|
|
10254
|
-
local risk_level
|
|
10255
|
-
risk_level=$(grep -i "OVERALL_RISK:" "$result_file" | head -1 | sed 's/.*OVERALL_RISK:[[:space:]]*//' | awk '{print toupper($1)}')
|
|
10256
|
-
|
|
10257
|
-
# Count critical/high attack vectors
|
|
10258
|
-
local critical_count high_count
|
|
10259
|
-
critical_count=$(grep -ci "\[critical\]" "$result_file" 2>/dev/null || echo "0")
|
|
10260
|
-
high_count=$(grep -ci "\[high\]" "$result_file" 2>/dev/null || echo "0")
|
|
10261
|
-
|
|
10262
|
-
log_info "Adversarial testing complete: risk=$risk_level, critical=$critical_count, high=$high_count"
|
|
10263
|
-
|
|
10264
|
-
emit_event_json "adversarial_test_complete" \
|
|
10265
|
-
"test_id=$test_id" \
|
|
10266
|
-
"risk_level=${risk_level:-UNKNOWN}" \
|
|
10267
|
-
"critical_count=$critical_count" \
|
|
10268
|
-
"high_count=$high_count" \
|
|
10269
|
-
"iteration=$ITERATION_COUNT"
|
|
10270
|
-
|
|
10271
|
-
# Block on critical findings
|
|
10272
|
-
if [ "$critical_count" -gt 0 ]; then
|
|
10273
|
-
log_error "ADVERSARIAL TEST BLOCKED: $critical_count critical attack vectors found"
|
|
10274
|
-
log_error "Details: $adversarial_dir/$test_id/result.txt"
|
|
10275
|
-
return 1
|
|
10276
|
-
fi
|
|
10277
|
-
|
|
10278
|
-
return 0
|
|
10279
|
-
}
|
|
10280
|
-
|
|
10281
9446
|
load_solutions_context() {
|
|
10282
9447
|
# Load relevant structured solutions for the current task context
|
|
10283
9448
|
local context="$1"
|
|
@@ -10468,6 +9633,18 @@ create_checkpoint() {
|
|
|
10468
9633
|
cp ".loki/$f" "$cp_dir/$f" 2>/dev/null || true
|
|
10469
9634
|
fi
|
|
10470
9635
|
done
|
|
9636
|
+
# A6: under a namespaced session the live state file is at
|
|
9637
|
+
# .loki/sessions/<id>/autonomy-state.json, which the legacy loop above misses.
|
|
9638
|
+
# Snapshot it into the checkpoint as autonomy-state.json so a rollback still
|
|
9639
|
+
# captures the current run's machine state. (Default `loki start` has no
|
|
9640
|
+
# LOKI_SESSION_ID, so this block is skipped and the loop above is unchanged.)
|
|
9641
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
9642
|
+
local _ns_state_file
|
|
9643
|
+
_ns_state_file="$(_loki_state_file)"
|
|
9644
|
+
if [ -f "$_ns_state_file" ]; then
|
|
9645
|
+
cp "$_ns_state_file" "$cp_dir/autonomy-state.json" 2>/dev/null || true
|
|
9646
|
+
fi
|
|
9647
|
+
fi
|
|
10471
9648
|
|
|
10472
9649
|
# R6: capture a real working-tree snapshot so code can be truly undone later.
|
|
10473
9650
|
# Loki does not commit per iteration, so git_sha (HEAD) cannot reconstruct
|
|
@@ -10565,118 +9742,47 @@ print(json.dumps({'id':m['id'],'ts':m['timestamp'],'iter':m['iteration'],'task':
|
|
|
10565
9742
|
# it without parsing stdout (log_info writes to stdout, so command-substitution
|
|
10566
9743
|
# capture would include log lines).
|
|
10567
9744
|
_LAST_CHECKPOINT_ID="$checkpoint_id"
|
|
10568
|
-
}
|
|
10569
|
-
|
|
10570
|
-
rollback_to_checkpoint() {
|
|
10571
|
-
# Rollback state files to a specific checkpoint
|
|
10572
|
-
# Args: $1 = checkpoint_id
|
|
10573
|
-
local checkpoint_id="$1"
|
|
10574
|
-
local checkpoint_dir=".loki/state/checkpoints"
|
|
10575
|
-
|
|
10576
|
-
# Validate checkpoint ID (prevent path traversal)
|
|
10577
|
-
if [[ ! "$checkpoint_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then
|
|
10578
|
-
log_error "Invalid checkpoint ID: must be alphanumeric, hyphens, underscores only"
|
|
10579
|
-
return 1
|
|
10580
|
-
fi
|
|
10581
|
-
|
|
10582
|
-
local cp_dir="${checkpoint_dir}/${checkpoint_id}"
|
|
10583
|
-
|
|
10584
|
-
if [ ! -d "$cp_dir" ]; then
|
|
10585
|
-
log_error "Checkpoint not found: ${checkpoint_id}"
|
|
10586
|
-
return 1
|
|
10587
|
-
fi
|
|
10588
|
-
|
|
10589
|
-
# Read checkpoint metadata
|
|
10590
|
-
local git_sha
|
|
10591
|
-
git_sha=$(_CP_META="${cp_dir}/metadata.json" python3 -c "import json, os; print(json.load(open(os.environ['_CP_META']))['git_sha'])" 2>/dev/null || echo "")
|
|
10592
|
-
|
|
10593
|
-
log_warn "Rolling back to checkpoint: ${checkpoint_id}"
|
|
10594
|
-
|
|
10595
|
-
# R6 re-undoability invariant: force a pre-rollback snapshot of CURRENT state
|
|
10596
|
-
# before overwriting, even if the git tree is clean (the .loki/ state we are
|
|
10597
|
-
# about to clobber is not git-tracked). _LOKI_CP_FORCE bypasses the clean-tree
|
|
10598
|
-
# guard. Capture the snapshot id so we can tell the user how to undo the undo.
|
|
10599
|
-
_LOKI_CP_FORCE=1 create_checkpoint "pre-rollback snapshot (before restoring ${checkpoint_id})" "rollback"
|
|
10600
|
-
local pre_rollback_id="${_LAST_CHECKPOINT_ID:-}"
|
|
10601
|
-
if [ -n "$pre_rollback_id" ]; then
|
|
10602
|
-
log_info "Saved prior state as ${pre_rollback_id} (undo this rollback with: loki rollback to ${pre_rollback_id})"
|
|
10603
|
-
fi
|
|
10604
|
-
|
|
10605
|
-
# Restore state files (R6: CONTINUITY.md restores iteration/conversation context)
|
|
10606
|
-
for f in state/orchestrator.json queue/pending.json queue/completed.json queue/in-progress.json queue/current-task.json CONTINUITY.md; do
|
|
10607
|
-
if [ -f "${cp_dir}/${f}" ]; then
|
|
10608
|
-
local target_dir=".loki/$(dirname "$f")"
|
|
10609
|
-
mkdir -p "$target_dir"
|
|
10610
|
-
cp "${cp_dir}/${f}" ".loki/${f}" 2>/dev/null || true
|
|
10611
|
-
fi
|
|
10612
|
-
done
|
|
10613
|
-
|
|
10614
|
-
# Log the rollback (use python3 for safe JSON serialization)
|
|
10615
|
-
# v7.5.10: route through safe_append_event_jsonl() so parallel-worktree
|
|
10616
|
-
# rollbacks cannot interleave partial JSONL lines (POSIX append is
|
|
10617
|
-
# only atomic for <PIPE_BUF and not all platforms honor it).
|
|
10618
|
-
local timestamp rb_event
|
|
10619
|
-
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
10620
|
-
rb_event=$(_RB_CPID="$checkpoint_id" _RB_SHA="$git_sha" _RB_TS="$timestamp" \
|
|
10621
|
-
python3 -c "
|
|
10622
|
-
import json,os
|
|
10623
|
-
print(json.dumps({'event':'rollback','checkpoint':os.environ['_RB_CPID'],'git_sha':os.environ['_RB_SHA'],'timestamp':os.environ['_RB_TS']}))
|
|
10624
|
-
" 2>/dev/null) || rb_event=""
|
|
10625
|
-
if [ -n "$rb_event" ]; then
|
|
10626
|
-
# Source the emit lib once per call to get safe_append_event_jsonl.
|
|
10627
|
-
# Lib-only mode skips the emit script's normal CLI execution.
|
|
10628
|
-
# shellcheck disable=SC1091
|
|
10629
|
-
if [ -z "${_LOKI_EMIT_LIB_LOADED:-}" ]; then
|
|
10630
|
-
LOKI_EMIT_LIB_ONLY=1 . "$(dirname "${BASH_SOURCE[0]}")/../events/emit.sh" 2>/dev/null \
|
|
10631
|
-
&& _LOKI_EMIT_LIB_LOADED=1
|
|
10632
|
-
fi
|
|
10633
|
-
if declare -f safe_append_event_jsonl >/dev/null 2>&1; then
|
|
10634
|
-
safe_append_event_jsonl ".loki/events.jsonl" "$rb_event" 2>/dev/null || true
|
|
10635
|
-
else
|
|
10636
|
-
# Last-resort fallback: bare append (preserves prior behavior).
|
|
10637
|
-
printf '%s\n' "$rb_event" >> ".loki/events.jsonl" 2>/dev/null || true
|
|
10638
|
-
fi
|
|
10639
|
-
fi
|
|
10640
|
-
|
|
10641
|
-
log_info "State files restored from checkpoint: ${checkpoint_id}"
|
|
10642
9745
|
|
|
10643
|
-
#
|
|
10644
|
-
#
|
|
10645
|
-
#
|
|
10646
|
-
|
|
10647
|
-
if [ -f "${cp_dir}/worktree-snapshot.txt" ]; then
|
|
10648
|
-
log_info "To also restore the working tree to this checkpoint:"
|
|
10649
|
-
log_info " git stash apply refs/loki/cp/${checkpoint_id}"
|
|
10650
|
-
elif [ -n "$git_sha" ] && [ "$git_sha" != "no-git" ]; then
|
|
10651
|
-
log_info "Git SHA at checkpoint (last commit): ${git_sha}"
|
|
10652
|
-
log_info "Note: no working-tree snapshot was captured for this checkpoint;"
|
|
10653
|
-
log_info "code changes since the last commit are not restorable from here."
|
|
10654
|
-
fi
|
|
9746
|
+
# A3: best-effort object-store sync of checkpoint state. No-op unless
|
|
9747
|
+
# LOKI_STORAGE_BACKEND is set to a non-local backend. Never blocks/fails the
|
|
9748
|
+
# build on a sync error (the shim swallows errors; we also `|| true`).
|
|
9749
|
+
_loki_object_store_sync_checkpoints || true
|
|
10655
9750
|
}
|
|
10656
9751
|
|
|
10657
|
-
|
|
10658
|
-
|
|
10659
|
-
|
|
10660
|
-
|
|
10661
|
-
local
|
|
9752
|
+
# A3: push .loki/state/checkpoints/** to the configured object store (best-effort).
|
|
9753
|
+
# Local/unset backend => no-op (zero behavior change for local users). Provider
|
|
9754
|
+
# writes are out of scope; this syncs only the checkpoint state run.sh owns.
|
|
9755
|
+
_loki_object_store_sync_checkpoints() {
|
|
9756
|
+
local backend="${LOKI_STORAGE_BACKEND:-local}"
|
|
9757
|
+
case "$backend" in
|
|
9758
|
+
local|""|file|filesystem) return 0 ;;
|
|
9759
|
+
esac
|
|
9760
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
9761
|
+
local shim="${SCRIPT_DIR}/lib/checkpoint_sync.py"
|
|
9762
|
+
[ -f "$shim" ] || return 0
|
|
9763
|
+
python3 "$shim" sync >/dev/null 2>&1 || log_warn "Object-store checkpoint sync skipped (backend=$backend); build continues."
|
|
9764
|
+
return 0
|
|
9765
|
+
}
|
|
10662
9766
|
|
|
10663
|
-
|
|
10664
|
-
|
|
10665
|
-
|
|
9767
|
+
# A3: hydrate .loki/state/checkpoints/** from the object store on a durable
|
|
9768
|
+
# resume when the local volume is empty (best-effort). No-op unless
|
|
9769
|
+
# LOKI_DURABLE_STATE=1 AND a non-local LOKI_STORAGE_BACKEND is set. The shim
|
|
9770
|
+
# itself guards against overwriting a non-empty local volume.
|
|
9771
|
+
_loki_object_store_hydrate_checkpoints() {
|
|
9772
|
+
[ "${LOKI_DURABLE_STATE:-0}" = "1" ] || return 0
|
|
9773
|
+
local backend="${LOKI_STORAGE_BACKEND:-local}"
|
|
9774
|
+
case "$backend" in
|
|
9775
|
+
local|""|file|filesystem) return 0 ;;
|
|
9776
|
+
esac
|
|
9777
|
+
command -v python3 >/dev/null 2>&1 || return 0
|
|
9778
|
+
local shim="${SCRIPT_DIR}/lib/checkpoint_sync.py"
|
|
9779
|
+
[ -f "$shim" ] || return 0
|
|
9780
|
+
if python3 "$shim" hydrate >/dev/null 2>&1; then
|
|
9781
|
+
log_info "Durable resume: checked object store ($backend) for prior checkpoints."
|
|
9782
|
+
else
|
|
9783
|
+
log_warn "Object-store checkpoint hydrate skipped (backend=$backend); build continues with local state."
|
|
10666
9784
|
fi
|
|
10667
|
-
|
|
10668
|
-
tail -n "$limit" "$index_file" | python3 -c "
|
|
10669
|
-
import sys, json
|
|
10670
|
-
lines = sys.stdin.readlines()
|
|
10671
|
-
for line in reversed(lines):
|
|
10672
|
-
try:
|
|
10673
|
-
cp = json.loads(line)
|
|
10674
|
-
sha = cp.get('sha','')[:8]
|
|
10675
|
-
task = cp.get('task','')[:60]
|
|
10676
|
-
print(f\" {cp['id']} {cp['ts']} [{sha}] {task}\")
|
|
10677
|
-
except:
|
|
10678
|
-
continue
|
|
10679
|
-
"
|
|
9785
|
+
return 0
|
|
10680
9786
|
}
|
|
10681
9787
|
|
|
10682
9788
|
start_dashboard() {
|
|
@@ -11928,17 +11034,6 @@ except Exception as e:
|
|
|
11928
11034
|
}
|
|
11929
11035
|
|
|
11930
11036
|
# Load relevant learnings
|
|
11931
|
-
load_learnings_context() {
|
|
11932
|
-
local learnings=""
|
|
11933
|
-
|
|
11934
|
-
# Get recent learnings (last 7 days)
|
|
11935
|
-
for learning in $(find .loki/memory/learnings -name "*.md" -mtime -7 2>/dev/null | head -5); do
|
|
11936
|
-
learnings+="$(head -30 "$learning")\n---\n"
|
|
11937
|
-
done
|
|
11938
|
-
|
|
11939
|
-
echo -e "$learnings"
|
|
11940
|
-
}
|
|
11941
|
-
|
|
11942
11037
|
# Load pre-computed relevant learnings from CLI startup (SYN-008)
|
|
11943
11038
|
# Reads .loki/state/memory-context.json written by load_memory_context() in CLI
|
|
11944
11039
|
# Note: Different from get_relevant_learnings() which writes to relevant-learnings.json
|
|
@@ -12700,89 +11795,44 @@ PYEOF
|
|
|
12700
11795
|
#===============================================================================
|
|
12701
11796
|
|
|
12702
11797
|
# Enrich prompt context with relevant cross-project patterns
|
|
12703
|
-
enrich_from_knowledge_graph() {
|
|
12704
|
-
local context="$1"
|
|
12705
|
-
local max_patterns="${2:-5}"
|
|
12706
|
-
|
|
12707
|
-
_LOKI_KG_CONTEXT="$context" _LOKI_KG_MAX="$max_patterns" \
|
|
12708
|
-
_LOKI_PROJECT_DIR="$PROJECT_DIR" \
|
|
12709
|
-
python3 << 'PYEOF' 2>/dev/null || echo ""
|
|
12710
|
-
import sys
|
|
12711
|
-
import os
|
|
12712
|
-
import json
|
|
12713
|
-
|
|
12714
|
-
project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
|
|
12715
|
-
context = os.environ.get('_LOKI_KG_CONTEXT', '')
|
|
12716
|
-
max_results = int(os.environ.get('_LOKI_KG_MAX', '5'))
|
|
12717
|
-
|
|
12718
|
-
if not project_dir:
|
|
12719
|
-
sys.exit(0)
|
|
12720
|
-
sys.path.insert(0, project_dir)
|
|
12721
|
-
try:
|
|
12722
|
-
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
12723
|
-
kg = OrganizationKnowledgeGraph()
|
|
12724
|
-
patterns = kg.query_patterns(context, max_results=max_results)
|
|
12725
|
-
if patterns:
|
|
12726
|
-
output = "\n## Cross-Project Knowledge (from knowledge graph)\n"
|
|
12727
|
-
for p in patterns:
|
|
12728
|
-
name = p.get('name', p.get('pattern', 'unnamed'))
|
|
12729
|
-
category = p.get('category', '')
|
|
12730
|
-
desc = p.get('description', '')
|
|
12731
|
-
output += f"- **{name}** ({category}): {desc}\n"
|
|
12732
|
-
print(output)
|
|
12733
|
-
except Exception:
|
|
12734
|
-
pass
|
|
12735
|
-
PYEOF
|
|
12736
|
-
}
|
|
12737
|
-
|
|
12738
11798
|
# Store new patterns to the knowledge graph after successful iterations
|
|
12739
|
-
store_to_knowledge_graph() {
|
|
12740
|
-
local target_dir="${TARGET_DIR:-.}"
|
|
12741
|
-
|
|
12742
|
-
_LOKI_PROJECT_DIR="$PROJECT_DIR" _LOKI_TARGET_DIR="$target_dir" \
|
|
12743
|
-
python3 << 'PYEOF' 2>/dev/null || true
|
|
12744
|
-
import sys
|
|
12745
|
-
import os
|
|
12746
|
-
|
|
12747
|
-
project_dir = os.environ.get('_LOKI_PROJECT_DIR', '')
|
|
12748
|
-
target_dir = os.environ.get('_LOKI_TARGET_DIR', '.')
|
|
12749
|
-
|
|
12750
|
-
sys.path.insert(0, project_dir)
|
|
12751
|
-
try:
|
|
12752
|
-
from memory.knowledge_graph import OrganizationKnowledgeGraph
|
|
12753
|
-
from pathlib import Path
|
|
12754
|
-
|
|
12755
|
-
kg = OrganizationKnowledgeGraph()
|
|
12756
|
-
project_dirs = [Path(target_dir)]
|
|
12757
|
-
|
|
12758
|
-
# Extract and store patterns
|
|
12759
|
-
patterns = kg.extract_patterns(project_dirs)
|
|
12760
|
-
if patterns:
|
|
12761
|
-
patterns = kg.deduplicate_patterns(patterns)
|
|
12762
|
-
kg.save_patterns(patterns)
|
|
12763
|
-
|
|
12764
|
-
# Rebuild graph
|
|
12765
|
-
kg.build_graph(project_dirs)
|
|
12766
|
-
kg.save_graph()
|
|
12767
|
-
except Exception:
|
|
12768
|
-
pass
|
|
12769
|
-
PYEOF
|
|
12770
|
-
}
|
|
12771
|
-
|
|
12772
11799
|
#===============================================================================
|
|
12773
11800
|
# Save/Load Wrapper State
|
|
12774
11801
|
#===============================================================================
|
|
12775
11802
|
|
|
11803
|
+
# A6 (multi-build-per-pod state isolation): resolve the autonomy-state.json path.
|
|
11804
|
+
# The existing .loki/sessions/<id>/ scoping (v6.4.0) namespaces PID + lock files
|
|
11805
|
+
# so concurrent sessions in the SAME TARGET_DIR do not collide. autonomy-state.json
|
|
11806
|
+
# was NOT covered by that scoping, so two concurrent builds with distinct
|
|
11807
|
+
# LOKI_SESSION_IDs still raced on the one global .loki/autonomy-state.json
|
|
11808
|
+
# (lost-update on retry/iteration counts). When LOKI_SESSION_ID is set we mirror
|
|
11809
|
+
# the session scoping and put the file under .loki/sessions/<id>/. When unset
|
|
11810
|
+
# (the normal `loki start ./prd.md` case) the legacy .loki/autonomy-state.json
|
|
11811
|
+
# path is returned UNCHANGED, so default single-session behavior is byte-identical.
|
|
11812
|
+
_loki_state_file() {
|
|
11813
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
11814
|
+
printf '%s' ".loki/sessions/${LOKI_SESSION_ID}/autonomy-state.json"
|
|
11815
|
+
else
|
|
11816
|
+
printf '%s' ".loki/autonomy-state.json"
|
|
11817
|
+
fi
|
|
11818
|
+
}
|
|
11819
|
+
|
|
12776
11820
|
save_state() {
|
|
12777
11821
|
local retry_count="$1"
|
|
12778
11822
|
local status="$2"
|
|
12779
11823
|
local exit_code="$3"
|
|
12780
11824
|
|
|
12781
|
-
|
|
12782
|
-
|
|
11825
|
+
local state_file
|
|
11826
|
+
state_file="$(_loki_state_file)"
|
|
11827
|
+
|
|
11828
|
+
# BUG-ST-013: Ensure target directory exists (defensive -- may be called from
|
|
11829
|
+
# signal handler). For a namespaced session path this also creates
|
|
11830
|
+
# .loki/sessions/<id>/; for the default path it is .loki/ as before.
|
|
11831
|
+
mkdir -p "$(dirname "$state_file")" 2>/dev/null || true
|
|
12783
11832
|
|
|
12784
|
-
# BUG-XC-004: Atomic write via temp file + mv
|
|
12785
|
-
|
|
11833
|
+
# BUG-XC-004: Atomic write via temp file + mv (temp lives beside the target so
|
|
11834
|
+
# the rename stays on the same filesystem)
|
|
11835
|
+
local state_tmp="${state_file}.tmp.$$"
|
|
12786
11836
|
cat > "$state_tmp" << EOF
|
|
12787
11837
|
{
|
|
12788
11838
|
"retryCount": $retry_count,
|
|
@@ -12796,23 +11846,30 @@ save_state() {
|
|
|
12796
11846
|
"baseWait": $BASE_WAIT
|
|
12797
11847
|
}
|
|
12798
11848
|
EOF
|
|
12799
|
-
mv -f "$state_tmp" "
|
|
11849
|
+
mv -f "$state_tmp" "$state_file"
|
|
12800
11850
|
}
|
|
12801
11851
|
|
|
12802
11852
|
load_state() {
|
|
11853
|
+
local state_file
|
|
11854
|
+
state_file="$(_loki_state_file)"
|
|
11855
|
+
|
|
12803
11856
|
# BUG-EP-015: Clean up orphaned temp files from kill -9 crashes
|
|
12804
11857
|
# These are left behind when the process is killed during atomic writes
|
|
12805
11858
|
find .loki/ -maxdepth 1 -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
12806
11859
|
find .loki/state/ -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11860
|
+
# A6: also sweep namespaced session state temps when running under a session id
|
|
11861
|
+
if [ -n "${LOKI_SESSION_ID:-}" ]; then
|
|
11862
|
+
find ".loki/sessions/${LOKI_SESSION_ID}/" -maxdepth 1 -name "*.tmp.*" -mmin +5 -delete 2>/dev/null || true
|
|
11863
|
+
fi
|
|
12807
11864
|
|
|
12808
|
-
if [ -f "
|
|
11865
|
+
if [ -f "$state_file" ]; then
|
|
12809
11866
|
if command -v python3 &> /dev/null; then
|
|
12810
11867
|
# BUG-ST-006: Validate checkpoint integrity before loading state
|
|
12811
11868
|
local state_valid
|
|
12812
|
-
state_valid=$(python3 -c "
|
|
12813
|
-
import json, sys
|
|
11869
|
+
state_valid=$(LOKI_STATE_FILE="$state_file" python3 -c "
|
|
11870
|
+
import json, os, sys
|
|
12814
11871
|
try:
|
|
12815
|
-
with open(
|
|
11872
|
+
with open(os.environ['LOKI_STATE_FILE']) as f:
|
|
12816
11873
|
d = json.load(f)
|
|
12817
11874
|
# Validate required fields exist and have sane types
|
|
12818
11875
|
rc = d.get('retryCount', 0)
|
|
@@ -12834,16 +11891,16 @@ except (json.JSONDecodeError, KeyError, TypeError, OSError):
|
|
|
12834
11891
|
RETRY_COUNT=0
|
|
12835
11892
|
ITERATION_COUNT=0
|
|
12836
11893
|
# Back up corrupted state file for diagnosis
|
|
12837
|
-
mv "
|
|
11894
|
+
mv "$state_file" "${state_file}.corrupt.$(date +%s)" 2>/dev/null || true
|
|
12838
11895
|
return
|
|
12839
11896
|
fi
|
|
12840
11897
|
|
|
12841
11898
|
# Load retry count, iteration count, and status from previous session
|
|
12842
11899
|
local prev_status
|
|
12843
|
-
prev_status=$(python3 -c "import json; print(json.load(open(
|
|
12844
|
-
RETRY_COUNT=$(python3 -c "import json; print(json.load(open(
|
|
11900
|
+
prev_status=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status', 'unknown'))" 2>/dev/null || echo "unknown")
|
|
11901
|
+
RETRY_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('retryCount', 0))" 2>/dev/null || echo "0")
|
|
12845
11902
|
# BUG-RUN-003: Restore ITERATION_COUNT from persisted state
|
|
12846
|
-
ITERATION_COUNT=$(python3 -c "import json; print(json.load(open(
|
|
11903
|
+
ITERATION_COUNT=$(LOKI_STATE_FILE="$state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('iterationCount', 0))" 2>/dev/null || echo "0")
|
|
12847
11904
|
|
|
12848
11905
|
# Reset retry count + iteration count if previous session ended in a
|
|
12849
11906
|
# terminal state. A fresh `loki start` after a terminal run is a NEW
|
|
@@ -14945,7 +14002,7 @@ except Exception as exc:
|
|
|
14945
14002
|
|
|
14946
14003
|
# Check max iterations before starting
|
|
14947
14004
|
if check_max_iterations; then
|
|
14948
|
-
log_error "Max iterations already reached. Reset with: rm
|
|
14005
|
+
log_error "Max iterations already reached. Reset with: rm $(_loki_state_file)"
|
|
14949
14006
|
# Delegate-then-notify: terminal state. Mirror the in-loop max-iterations
|
|
14950
14007
|
# site so a detached (--bg) run still writes COMPLETION.txt + fires the
|
|
14951
14008
|
# ping on this pre-loop exit. _LOKI_RUN_START_SHA is already exported
|
|
@@ -17784,6 +16841,14 @@ main() {
|
|
|
17784
16841
|
# durable mount, so a misconfigured volume never silently loses build state.
|
|
17785
16842
|
assert_durable_state_mount
|
|
17786
16843
|
|
|
16844
|
+
# A3: on a durable resume with an object-store backend, hydrate this run's
|
|
16845
|
+
# checkpoints from the store IF the local volume came up empty (a fresh pod /
|
|
16846
|
+
# non-durable volume). Best-effort + idempotent: the shim no-ops when the
|
|
16847
|
+
# backend is local, when the local volume already has checkpoints, or when
|
|
16848
|
+
# the store has nothing for this run. Runs before the main loop so any
|
|
16849
|
+
# resume/rollback sees the restored state. Zero behavior change for local.
|
|
16850
|
+
_loki_object_store_hydrate_checkpoints || true
|
|
16851
|
+
|
|
17787
16852
|
# Setup agent branch protection (isolates agent changes to a feature branch)
|
|
17788
16853
|
setup_agent_branch
|
|
17789
16854
|
|
|
@@ -18001,8 +17066,9 @@ main() {
|
|
|
18001
17066
|
# because the process was SIGKILLed before reaching here). Retryable;
|
|
18002
17067
|
# the restarted Job resumes via the ENT-2 durable-resume path.
|
|
18003
17068
|
if [ "${LOKI_DURABLE_STATE:-0}" = "1" ]; then
|
|
18004
|
-
local _final_status
|
|
18005
|
-
|
|
17069
|
+
local _final_status _final_state_file
|
|
17070
|
+
_final_state_file="$(_loki_state_file)"
|
|
17071
|
+
_final_status=$(LOKI_STATE_FILE="$_final_state_file" python3 -c "import json, os; print(json.load(open(os.environ['LOKI_STATE_FILE'])).get('status','unknown'))" 2>/dev/null || echo "unknown")
|
|
18006
17072
|
case "$_final_status" in
|
|
18007
17073
|
council_approved|council_force_approved|completion_promise_fulfilled|force_stopped|paused|interrupted|budget_exceeded|stopped)
|
|
18008
17074
|
result=0 ;;
|