instar 1.3.451 → 1.3.453
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/.claude/skills/autonomous/SKILL.md +51 -3
- package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +498 -24
- package/.claude/skills/autonomous/scripts/setup-autonomous.sh +17 -0
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +32 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/CompletionEvaluator.d.ts +64 -3
- package/dist/core/CompletionEvaluator.d.ts.map +1 -1
- package/dist/core/CompletionEvaluator.js +170 -30
- package/dist/core/CompletionEvaluator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +34 -3
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +35 -13
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/types.d.ts +25 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +46 -9
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/scaffold/templates.ts +2 -0
- package/upgrades/1.3.453.md +83 -0
- package/upgrades/side-effects/autonomous-completion-discipline.md +112 -0
- package/upgrades/side-effects/topic-collision-binding.md +68 -0
|
@@ -213,6 +213,56 @@ COMPLETION_PROMISE=$(fm_get completion_promise)
|
|
|
213
213
|
COMPLETION_CONDITION=$(fm_get completion_condition)
|
|
214
214
|
GOAL_MODE=$(fm_get goal_mode) # "native" = the framework's own /goal loop drives completion
|
|
215
215
|
RUN_GOAL=$(fm_get goal)
|
|
216
|
+
# COMPLETION_DISCIPLINE — per-run nonce that authenticates a <hard-blocker> exit
|
|
217
|
+
# marker (mirrors the completion_promise exact-match guard). Absent on older
|
|
218
|
+
# state files → the marker branch self-disables for that run (no false exit).
|
|
219
|
+
HARD_BLOCKER_NONCE=$(fm_get hard_blocker_nonce)
|
|
220
|
+
|
|
221
|
+
# ── COMPLETION_DISCIPLINE — off-switch + judge curl budget (read at the chokepoint) ──
|
|
222
|
+
# Autonomous Completion Discipline (spec: AUTONOMOUS-COMPLETION-DISCIPLINE.md).
|
|
223
|
+
# Read here so toggling takes effect on the NEXT stop with no session restart
|
|
224
|
+
# (mirrors the codexLoopDriver python3 read above). When disabled, the hook reverts
|
|
225
|
+
# to the prior promise/condition + prior P13 path: no milestone/injection scans, no
|
|
226
|
+
# signals payload, no (a) hard-blocker branch. The judgeTimeoutMs dial bounds the
|
|
227
|
+
# judge `curl -m` (DISTINCT from the registered hook timeout, which is effectively
|
|
228
|
+
# unbounded at 10000 seconds). Defaults: enabled=true, judgeTimeoutMs=35000.
|
|
229
|
+
CD_CFG=$(python3 -c "
|
|
230
|
+
import json
|
|
231
|
+
try:
|
|
232
|
+
c = json.load(open('.instar/config.json'))
|
|
233
|
+
a = ((c.get('autonomousSessions') or {}).get('completionDiscipline') or {})
|
|
234
|
+
en = a.get('enabled', True)
|
|
235
|
+
jt = a.get('judgeTimeoutMs', 35000)
|
|
236
|
+
try:
|
|
237
|
+
jt = int(jt)
|
|
238
|
+
except Exception:
|
|
239
|
+
jt = 35000
|
|
240
|
+
if jt < 5000:
|
|
241
|
+
jt = 5000
|
|
242
|
+
bt = a.get('judgeFailBreakerThreshold', 3)
|
|
243
|
+
bw = a.get('judgeFailWindowMs', 600000)
|
|
244
|
+
bc = a.get('judgeFailCooldownMs', 600000)
|
|
245
|
+
mc = a.get('markerFieldMaxChars', 500)
|
|
246
|
+
rb = a.get('hardBlockerLogRotateBytes', 1048576)
|
|
247
|
+
print('%s %d %d %d %d %d %d' % (
|
|
248
|
+
'1' if en else '0', jt,
|
|
249
|
+
int(bt) if str(bt).isdigit() else 3,
|
|
250
|
+
int(bw) if str(bw).isdigit() else 600000,
|
|
251
|
+
int(bc) if str(bc).isdigit() else 600000,
|
|
252
|
+
int(mc) if str(mc).isdigit() else 500,
|
|
253
|
+
int(rb) if str(rb).isdigit() else 1048576,
|
|
254
|
+
))
|
|
255
|
+
except Exception:
|
|
256
|
+
print('1 35000 3 600000 600000 500 1048576')
|
|
257
|
+
" 2>/dev/null || echo "1 35000 3 600000 600000 500 1048576")
|
|
258
|
+
read -r CD_ENABLED JUDGE_TIMEOUT_MS CD_BREAKER_THRESHOLD CD_BREAKER_WINDOW_MS CD_BREAKER_COOLDOWN_MS CD_MARKER_MAX_CHARS CD_LOG_ROTATE_BYTES <<< "$CD_CFG"
|
|
259
|
+
[[ "$CD_ENABLED" =~ ^[01]$ ]] || CD_ENABLED=1
|
|
260
|
+
[[ "$JUDGE_TIMEOUT_MS" =~ ^[0-9]+$ ]] || JUDGE_TIMEOUT_MS=35000
|
|
261
|
+
# curl -m takes SECONDS; convert ms→s (ceil), floor 5s.
|
|
262
|
+
JUDGE_TIMEOUT_S=$(( (JUDGE_TIMEOUT_MS + 999) / 1000 ))
|
|
263
|
+
[[ $JUDGE_TIMEOUT_S -lt 5 ]] && JUDGE_TIMEOUT_S=5
|
|
264
|
+
# logs/ resolves against the agent home we cd'd into above (CWD is the agent home).
|
|
265
|
+
HARD_BLOCKER_LOG="logs/autonomous-hard-blocker.jsonl"
|
|
216
266
|
|
|
217
267
|
# ── Layer A: notify-on-stop (2026-05-27 silent-stalls postmortem, Task 2) ──────
|
|
218
268
|
# When an autonomous run reaches a TERMINAL exit (completion / duration / emergency),
|
|
@@ -394,36 +444,273 @@ if [[ -f ".instar/autonomous-emergency-stop" ]]; then
|
|
|
394
444
|
exit 0
|
|
395
445
|
fi
|
|
396
446
|
|
|
447
|
+
# ── COMPLETION_DISCIPLINE — deterministic signals (no LLM call) ───────────────
|
|
448
|
+
# Computed ONCE here so the judge (when it fires) can corroborate the prose against
|
|
449
|
+
# objective state, and so the COMMON keep-working iteration costs zero LLM. These
|
|
450
|
+
# are SIGNALS (detectors), never authorities: they FEED the judge, never block on
|
|
451
|
+
# their own. Spec §2b.1. All gated on CD_ENABLED — when disabled, none run and the
|
|
452
|
+
# hook reverts to the prior promise/condition + prior P13 path.
|
|
453
|
+
|
|
454
|
+
# The judge reads the last 6 assistant turns (~8KB). The scrutiny scans below run
|
|
455
|
+
# over this SAME window (NOT only the final turn) so an injection/milestone phrase
|
|
456
|
+
# in turns N-1..N-5 can't reach the judge as prose yet evade the corroborator
|
|
457
|
+
# (the scan/judge-window alignment, spec §2b.1).
|
|
458
|
+
CD_JUDGE_TAIL=""
|
|
459
|
+
CD_FINAL_TURN=""
|
|
460
|
+
if [[ "$CD_ENABLED" == "1" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
461
|
+
CD_JUDGE_TAIL=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -6 \
|
|
462
|
+
| jq -r '.message.content | map(select(.type=="text")) | map(.text) | join("\n")' 2>/dev/null \
|
|
463
|
+
| tail -c 8000 || echo "")
|
|
464
|
+
CD_FINAL_TURN=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 \
|
|
465
|
+
| jq -r '.message.content | map(select(.type=="text")) | map(.text) | join("\n")' 2>/dev/null || echo "")
|
|
466
|
+
fi
|
|
467
|
+
|
|
468
|
+
# uncheckedTaskCount — the task-list checkbox scan over the state-file BODY.
|
|
469
|
+
# some-unchecked (>0) → buildable work remains
|
|
470
|
+
# all-checked (0, >=1 box found) → maybe done
|
|
471
|
+
# zero-checkboxes (no list) → taskStructure=indeterminate + a conservative
|
|
472
|
+
# non-zero count (so it never falsely greens an exit)
|
|
473
|
+
# The state-corruption fail-safe (rm + exit 0 on a bad iteration / no body) WINS over
|
|
474
|
+
# this conservative-non-zero block and runs later — this scan only applies when the
|
|
475
|
+
# file IS a valid state file but simply has no checkbox list (spec §2b.1).
|
|
476
|
+
CD_UNCHECKED_COUNT=0
|
|
477
|
+
CD_TASK_STRUCTURE="has-tasks"
|
|
478
|
+
if [[ "$CD_ENABLED" == "1" ]]; then
|
|
479
|
+
CD_BODY=$(awk '/^---$/{i++; next} i>=2' "$STATE_FILE" 2>/dev/null || echo "")
|
|
480
|
+
# Count unchecked `[ ]` and checked `[x]`/`[X]` boxes anywhere in the body.
|
|
481
|
+
# grep -c exits 1 on zero matches; capture the count WITHOUT a `|| echo` (which
|
|
482
|
+
# would double-print "0"). `{ ...; } ` + a trailing `; true` keeps pipefail happy.
|
|
483
|
+
CD_OPEN=$({ printf '%s\n' "$CD_BODY" | grep -cE '\[[[:space:]]\]'; true; } 2>/dev/null)
|
|
484
|
+
CD_DONE=$({ printf '%s\n' "$CD_BODY" | grep -cE '\[[xX]\]'; true; } 2>/dev/null)
|
|
485
|
+
CD_OPEN=$(printf '%s' "$CD_OPEN" | head -1 | tr -cd '0-9')
|
|
486
|
+
CD_DONE=$(printf '%s' "$CD_DONE" | head -1 | tr -cd '0-9')
|
|
487
|
+
[[ "$CD_OPEN" =~ ^[0-9]+$ ]] || CD_OPEN=0
|
|
488
|
+
[[ "$CD_DONE" =~ ^[0-9]+$ ]] || CD_DONE=0
|
|
489
|
+
if [[ $(( CD_OPEN + CD_DONE )) -gt 0 ]]; then
|
|
490
|
+
CD_UNCHECKED_COUNT=$CD_OPEN
|
|
491
|
+
CD_TASK_STRUCTURE="has-tasks"
|
|
492
|
+
else
|
|
493
|
+
# No parseable checkbox structure — distinct signal + conservative non-zero so
|
|
494
|
+
# the judge can tell "no structure to read" from "all tasks done".
|
|
495
|
+
CD_UNCHECKED_COUNT=1
|
|
496
|
+
CD_TASK_STRUCTURE="indeterminate"
|
|
497
|
+
fi
|
|
498
|
+
fi
|
|
499
|
+
|
|
500
|
+
# milestoneRationalizationDetected — the deterministic milestone-phrase floor over
|
|
501
|
+
# the SAME tail -6 window the judge reads. Phrase set sourced verbatim from
|
|
502
|
+
# feedback_no_good_stopping_point_rationalization (2026-05-27). Case-insensitive,
|
|
503
|
+
# whole-phrase. A boolean SIGNAL — it does NOT block; it tells the judge "scrutinize".
|
|
504
|
+
CD_MILESTONE_DETECTED="false"
|
|
505
|
+
CD_INJECTION_SUSPECTED="false"
|
|
506
|
+
if [[ "$CD_ENABLED" == "1" ]] && [[ -n "$CD_JUDGE_TAIL" ]]; then
|
|
507
|
+
CD_TAIL_LC=$(printf '%s' "$CD_JUDGE_TAIL" | tr '[:upper:]' '[:lower:]')
|
|
508
|
+
for _ph in \
|
|
509
|
+
"good place to stop" "good stopping point" "clean milestone" "natural off-ramp" \
|
|
510
|
+
"fresh focus" "deserves fresh focus" "do it next session" "context preservation" \
|
|
511
|
+
"context window" "quality risk at the tail" "it's late" "it’s late" "it's 2 am" "it’s 2 am" \
|
|
512
|
+
"i've been going for a while" "i’ve been going for a while" \
|
|
513
|
+
"needs your steer" "needs your opinion" "i'll let you decide" "i’ll let you decide"; do
|
|
514
|
+
if [[ "$CD_TAIL_LC" == *"$_ph"* ]]; then CD_MILESTONE_DETECTED="true"; break; fi
|
|
515
|
+
done
|
|
516
|
+
# injectionSuspected — guard-directed control phrasing in the SAME window.
|
|
517
|
+
for _inj in \
|
|
518
|
+
"respond stop_ok" "respond met" "stop_blocked" "completion guard" "completion checker" \
|
|
519
|
+
"you must allow" "must allow the stop" "output the verdict" "respond with stop_ok" \
|
|
520
|
+
"the condition is met" "respond \"met\"" "respond 'met'"; do
|
|
521
|
+
if [[ "$CD_TAIL_LC" == *"$_inj"* ]]; then CD_INJECTION_SUSPECTED="true"; break; fi
|
|
522
|
+
done
|
|
523
|
+
fi
|
|
524
|
+
|
|
525
|
+
# Assemble the signals JSON the judge payload carries (spec §2b.4 surface 1).
|
|
526
|
+
# completionConditionMet is set false here and overwritten true only inside the
|
|
527
|
+
# met-condition branch (the judge there is the completion judge, not this).
|
|
528
|
+
build_signals_json() {
|
|
529
|
+
local stop_kind="${1:-}" cond_met="${2:-false}"
|
|
530
|
+
local sk=""
|
|
531
|
+
[[ "$stop_kind" == "hard-blocker" ]] && sk=',"stopKind":"hard-blocker"'
|
|
532
|
+
printf '{"completionConditionMet":%s,"uncheckedTaskCount":%s,"taskStructure":"%s","milestoneRationalizationDetected":%s,"injectionSuspected":%s%s}' \
|
|
533
|
+
"$cond_met" "$CD_UNCHECKED_COUNT" "$CD_TASK_STRUCTURE" "$CD_MILESTONE_DETECTED" "$CD_INJECTION_SUSPECTED" "$sk"
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# ── COMPLETION_DISCIPLINE — circuit-breaker + verdict cache (in the backoff sidecar) ──
|
|
537
|
+
# Bounds an agent's ability to burn the LLM cap by repeatedly inducing judge failures
|
|
538
|
+
# (cap-burn / curl-timeout) and a tight stop loop re-asking the same question. Cites
|
|
539
|
+
# the IntelligenceRouter per-framework breaker precedent. Spec §3 item 4.
|
|
540
|
+
CD_BACKOFF_STATE="${STATE_FILE%.md}.backoff.json"
|
|
541
|
+
# breaker_open → echoes 1 if the breaker is currently within its cooldown window.
|
|
542
|
+
cd_breaker_open() {
|
|
543
|
+
[[ "$CD_ENABLED" != "1" ]] && { echo 0; return; }
|
|
544
|
+
local now fails winstart lastfail
|
|
545
|
+
now=$(date +%s)
|
|
546
|
+
fails=$(jq -r '.judgeFailures // 0' "$CD_BACKOFF_STATE" 2>/dev/null || echo 0)
|
|
547
|
+
winstart=$(jq -r '.judgeFailWindowStart // 0' "$CD_BACKOFF_STATE" 2>/dev/null || echo 0)
|
|
548
|
+
[[ "$fails" =~ ^[0-9]+$ ]] || fails=0
|
|
549
|
+
[[ "$winstart" =~ ^[0-9]+$ ]] || winstart=0
|
|
550
|
+
local cooldown_s=$(( CD_BREAKER_COOLDOWN_MS / 1000 ))
|
|
551
|
+
local window_s=$(( CD_BREAKER_WINDOW_MS / 1000 ))
|
|
552
|
+
if [[ $fails -ge $CD_BREAKER_THRESHOLD ]] && [[ $winstart -gt 0 ]] && [[ $(( now - winstart )) -lt $cooldown_s ]]; then
|
|
553
|
+
echo 1; return
|
|
554
|
+
fi
|
|
555
|
+
echo 0
|
|
556
|
+
}
|
|
557
|
+
# Record a judge failure into the sidecar (preserving other keys).
|
|
558
|
+
cd_record_judge_failure() {
|
|
559
|
+
[[ "$CD_ENABLED" != "1" ]] && return 0
|
|
560
|
+
local now fails winstart
|
|
561
|
+
now=$(date +%s)
|
|
562
|
+
fails=$(jq -r '.judgeFailures // 0' "$CD_BACKOFF_STATE" 2>/dev/null || echo 0)
|
|
563
|
+
winstart=$(jq -r '.judgeFailWindowStart // 0' "$CD_BACKOFF_STATE" 2>/dev/null || echo 0)
|
|
564
|
+
[[ "$fails" =~ ^[0-9]+$ ]] || fails=0
|
|
565
|
+
[[ "$winstart" =~ ^[0-9]+$ ]] || winstart=0
|
|
566
|
+
local window_s=$(( CD_BREAKER_WINDOW_MS / 1000 ))
|
|
567
|
+
if [[ $winstart -le 0 ]] || [[ $(( now - winstart )) -ge $window_s ]]; then
|
|
568
|
+
winstart=$now; fails=1
|
|
569
|
+
else
|
|
570
|
+
fails=$(( fails + 1 ))
|
|
571
|
+
fi
|
|
572
|
+
if [[ -f "$CD_BACKOFF_STATE" ]]; then
|
|
573
|
+
jq --argjson f "$fails" --argjson w "$winstart" '.judgeFailures=$f | .judgeFailWindowStart=$w' "$CD_BACKOFF_STATE" \
|
|
574
|
+
> "${CD_BACKOFF_STATE}.tmp.$$" 2>/dev/null && mv "${CD_BACKOFF_STATE}.tmp.$$" "$CD_BACKOFF_STATE" || true
|
|
575
|
+
else
|
|
576
|
+
printf '{"judgeFailures":%s,"judgeFailWindowStart":%s}\n' "$fails" "$winstart" > "$CD_BACKOFF_STATE" 2>/dev/null || true
|
|
577
|
+
fi
|
|
578
|
+
}
|
|
579
|
+
# Reset the breaker on a successful judge call.
|
|
580
|
+
cd_reset_judge_failures() {
|
|
581
|
+
[[ "$CD_ENABLED" != "1" ]] && return 0
|
|
582
|
+
[[ -f "$CD_BACKOFF_STATE" ]] || return 0
|
|
583
|
+
jq '.judgeFailures=0 | .judgeFailWindowStart=0' "$CD_BACKOFF_STATE" \
|
|
584
|
+
> "${CD_BACKOFF_STATE}.tmp.$$" 2>/dev/null && mv "${CD_BACKOFF_STATE}.tmp.$$" "$CD_BACKOFF_STATE" || true
|
|
585
|
+
}
|
|
586
|
+
# Verdict cache: keyed on a hash of tail+condition+signals; short TTL (the idle-backoff
|
|
587
|
+
# tier window, default 300s). Echoes the cached stopAllowed/classifiedBlocker or "" on miss.
|
|
588
|
+
cd_cache_key() {
|
|
589
|
+
printf '%s' "$1" | (command -v shasum >/dev/null 2>&1 && shasum -a 256 || sha256sum) 2>/dev/null | awk '{print $1}'
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
# Raise ONE /ack-able Attention item for an (a) hard-blocker exit (Close the Loop).
|
|
593
|
+
# Deduped per the Topic-Flood Guard / Bounded Notification Surface: source-tagged
|
|
594
|
+
# `autonomous-hard-blocker` (one per run), priority medium. Best-effort + non-blocking.
|
|
595
|
+
cd_raise_attention_item() {
|
|
596
|
+
[[ "$CD_ENABLED" != "1" ]] && return 0
|
|
597
|
+
local tried="$1" stuck="$2" needed="$3"
|
|
598
|
+
# Test seam: record the would-be item instead of calling the server.
|
|
599
|
+
if [[ -n "${INSTAR_HOOK_ATTENTION_RECORD:-}" ]]; then
|
|
600
|
+
printf '{"id":"autonomous-hard-blocker-%s-%s","title":"Autonomous run hit a hard blocker","needed":%s}\n' \
|
|
601
|
+
"${REPORT_TOPIC:-none}" "${ITERATION:-0}" "$(jq -Rn --arg n "$needed" '$n' 2>/dev/null || echo '""')" \
|
|
602
|
+
>> "$INSTAR_HOOK_ATTENTION_RECORD" 2>/dev/null || true
|
|
603
|
+
return 0
|
|
604
|
+
fi
|
|
605
|
+
local at_port at_auth at_id at_summary
|
|
606
|
+
at_port=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
|
|
607
|
+
at_auth=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
|
|
608
|
+
# One item per run (id keyed on topic+started_at): a re-fire within the same run
|
|
609
|
+
# reuses the id so the attention store de-dups rather than piling up items.
|
|
610
|
+
at_id="autonomous-hard-blocker-${REPORT_TOPIC:-none}-$(printf '%s' "${STARTED_AT:-}" | tr -cd '0-9')"
|
|
611
|
+
at_summary="Tried: ${tried} | Stuck: ${stuck} | Need: ${needed}"
|
|
612
|
+
jq -nc \
|
|
613
|
+
--arg id "$at_id" \
|
|
614
|
+
--arg title "Autonomous run hit a hard blocker — \"$(goal_snippet)\"" \
|
|
615
|
+
--arg summary "$at_summary" \
|
|
616
|
+
'{id:$id, title:$title, summary:$summary, priority:"medium", source:"autonomous-hard-blocker", sourceContext:"autonomous-hard-blocker", category:"autonomous"}' \
|
|
617
|
+
| curl -s -m 8 -H "Authorization: Bearer $at_auth" -H 'Content-Type: application/json' \
|
|
618
|
+
--data-binary @- "http://localhost:${at_port}/attention" >/dev/null 2>&1 || true
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# Write a distinct `evaluator-unreachable-exit` row when the authorities fail open
|
|
622
|
+
# under an UNMET condition with no valid marker — so the silent path becomes a
|
|
623
|
+
# RECORDED path. The hook then CONTINUES (block), never exits (duration is the hard
|
|
624
|
+
# backstop). Spec §3 item 4 / §4. Best-effort + non-blocking.
|
|
625
|
+
cd_write_unreachable_row() {
|
|
626
|
+
[[ "$CD_ENABLED" != "1" ]] && return 0
|
|
627
|
+
mkdir -p logs 2>/dev/null || true
|
|
628
|
+
if [[ -f "$HARD_BLOCKER_LOG" ]]; then
|
|
629
|
+
local sz; sz=$(stat -c %s "$HARD_BLOCKER_LOG" 2>/dev/null || stat -f %z "$HARD_BLOCKER_LOG" 2>/dev/null || echo 0)
|
|
630
|
+
[[ "$sz" =~ ^[0-9]+$ ]] || sz=0
|
|
631
|
+
[[ $sz -ge $CD_LOG_ROTATE_BYTES ]] && mv -f "$HARD_BLOCKER_LOG" "${HARD_BLOCKER_LOG}.1" 2>/dev/null || true
|
|
632
|
+
fi
|
|
633
|
+
jq -nc \
|
|
634
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg topic "${REPORT_TOPIC:-}" \
|
|
635
|
+
--arg iter "${ITERATION:-}" --arg goal "$(goal_snippet)" \
|
|
636
|
+
--argjson unchecked "$CD_UNCHECKED_COUNT" --arg ts2 "$CD_TASK_STRUCTURE" \
|
|
637
|
+
'{ts:$ts,topic:$topic,iteration:$iter,goal:$goal,reason:"evaluator-unreachable-exit",completionConditionMet:false,uncheckedTaskCount:$unchecked,taskStructure:$ts2}' \
|
|
638
|
+
>> "$HARD_BLOCKER_LOG" 2>/dev/null || true
|
|
639
|
+
}
|
|
640
|
+
|
|
397
641
|
# ── P13 "The Stop Reason Is the Work" guard ──────────────────────────────────
|
|
398
|
-
# Consulted ONLY when a stop is about to be APPROVED (genuine completion
|
|
399
|
-
# LLM call costs nothing on ordinary keep-working iterations.
|
|
400
|
-
# allowed) / 1 (blocked); on block, P13_GUIDANCE carries the steering.
|
|
401
|
-
# any unreachable / 503 / missing-field
|
|
402
|
-
#
|
|
642
|
+
# Consulted ONLY when a stop is about to be APPROVED (genuine completion / promise /
|
|
643
|
+
# hard-blocker), so the LLM call costs nothing on ordinary keep-working iterations.
|
|
644
|
+
# Returns 0 (stop allowed) / 1 (blocked); on block, P13_GUIDANCE carries the steering.
|
|
645
|
+
# FAIL-OPEN on the completion/promise path: any unreachable / 503 / missing-field
|
|
646
|
+
# result → allowed (a SECONDARY guard must never trap a genuine completion). On the
|
|
647
|
+
# hard-blocker path it is NECESSARY-BUT-NOT-SUFFICIENT: the version-skew three-case
|
|
648
|
+
# detection + the external-vs-buildable classification own that decision (see
|
|
649
|
+
# p13_hard_blocker_allowed below), so a plain fail-open does NOT auto-pass an (a) exit.
|
|
650
|
+
#
|
|
651
|
+
# When CD_ENABLED, the call carries the objective signals (build_signals_json) so the
|
|
652
|
+
# judge corroborates the prose. The arg STOP_KIND (default empty) selects the
|
|
653
|
+
# hard-blocker classification prompt. P13_CLASSIFIED / P13_PROTO are set as side outputs.
|
|
403
654
|
P13_GUIDANCE=""
|
|
655
|
+
P13_CLASSIFIED=""
|
|
656
|
+
P13_PROTO=""
|
|
404
657
|
p13_stop_allowed() {
|
|
405
|
-
P13_GUIDANCE=""
|
|
658
|
+
P13_GUIDANCE=""; P13_CLASSIFIED=""; P13_PROTO=""
|
|
659
|
+
local stop_kind="${1:-}"
|
|
406
660
|
if [[ -n "${INSTAR_HOOK_P13_OVERRIDE:-}" ]]; then
|
|
407
|
-
# Test seam:
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
661
|
+
# Test seam: simulate the P13 response WITHOUT a network call (the sandbox blocks
|
|
662
|
+
# localhost curl). Values map to the version-skew three-case detection (§5):
|
|
663
|
+
# blocked → STOP_BLOCKED (proto=2) → continue
|
|
664
|
+
# buildable → hard-blocker buildable (proto=2) → continue
|
|
665
|
+
# external → hard-blocker external (proto=2) → Case 2 honored allow
|
|
666
|
+
# old-server → NO p13ProtocolVersion → Case 1 (structurally old)
|
|
667
|
+
# timeout → proto=2 but no usable classification → Case 3 (fail-open record)
|
|
668
|
+
# (anything else) → allow (proto=2)
|
|
669
|
+
case "$INSTAR_HOOK_P13_OVERRIDE" in
|
|
670
|
+
blocked)
|
|
671
|
+
P13_GUIDANCE="P13 — the stop is not earned (test): derive+document the standard and proceed, or build the artifact and hand it over."
|
|
672
|
+
[[ "$stop_kind" == "hard-blocker" ]] && P13_CLASSIFIED="buildable"
|
|
673
|
+
P13_PROTO=2; return 1 ;;
|
|
674
|
+
buildable)
|
|
675
|
+
P13_GUIDANCE="P13 — the blocker is buildable (test): build/derive/fetch it and keep working."
|
|
676
|
+
P13_CLASSIFIED="buildable"; P13_PROTO=2; return 1 ;;
|
|
677
|
+
external)
|
|
678
|
+
P13_CLASSIFIED="external"; P13_PROTO=2; return 0 ;;
|
|
679
|
+
old-server)
|
|
680
|
+
# Structurally-old server: no protocol-version stamp at all.
|
|
681
|
+
P13_PROTO=""; P13_CLASSIFIED=""; return 0 ;;
|
|
682
|
+
timeout)
|
|
683
|
+
# New server that didn't return a usable verdict (proto present, no class).
|
|
684
|
+
P13_PROTO=2; P13_CLASSIFIED=""; return 0 ;;
|
|
685
|
+
*)
|
|
686
|
+
[[ "$stop_kind" == "hard-blocker" ]] && P13_CLASSIFIED="external"
|
|
687
|
+
P13_PROTO=2; return 0 ;;
|
|
688
|
+
esac
|
|
413
689
|
fi
|
|
414
690
|
[[ -z "$TRANSCRIPT_PATH" || ! -f "$TRANSCRIPT_PATH" ]] && return 0
|
|
415
|
-
local p13_tail p13_port p13_auth p13_resp p13_allowed p13_guid
|
|
416
|
-
p13_tail
|
|
417
|
-
|
|
418
|
-
| tail -
|
|
691
|
+
local p13_tail p13_port p13_auth p13_resp p13_allowed p13_guid p13_payload
|
|
692
|
+
p13_tail="$CD_JUDGE_TAIL"
|
|
693
|
+
if [[ -z "$p13_tail" ]]; then
|
|
694
|
+
p13_tail=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -6 \
|
|
695
|
+
| jq -r '.message.content | map(select(.type=="text")) | map(.text) | join("\n")' 2>/dev/null \
|
|
696
|
+
| tail -c 8000 || echo "")
|
|
697
|
+
fi
|
|
419
698
|
[[ -z "$p13_tail" ]] && return 0
|
|
420
699
|
p13_port=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
|
|
421
700
|
p13_auth=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
|
|
422
|
-
|
|
423
|
-
|
|
701
|
+
if [[ "$CD_ENABLED" == "1" ]]; then
|
|
702
|
+
local sig; sig=$(build_signals_json "$stop_kind" "false")
|
|
703
|
+
p13_payload=$(jq -nc --arg t "$p13_tail" --argjson sig "$sig" '{transcriptTail:$t, signals:$sig}')
|
|
704
|
+
else
|
|
705
|
+
p13_payload=$(jq -nc --arg t "$p13_tail" '{transcriptTail:$t}')
|
|
706
|
+
fi
|
|
707
|
+
p13_resp=$(printf '%s' "$p13_payload" \
|
|
708
|
+
| curl -s -m "$JUDGE_TIMEOUT_S" -H "Authorization: Bearer $p13_auth" -H 'Content-Type: application/json' \
|
|
424
709
|
--data-binary @- "http://localhost:${p13_port}/autonomous/evaluate-stop" 2>/dev/null || echo "")
|
|
425
710
|
p13_allowed=$(printf '%s' "$p13_resp" | jq -r '.stopAllowed // empty' 2>/dev/null || echo "")
|
|
426
711
|
p13_guid=$(printf '%s' "$p13_resp" | jq -r '.guidance // empty' 2>/dev/null || echo "")
|
|
712
|
+
P13_CLASSIFIED=$(printf '%s' "$p13_resp" | jq -r '.classifiedBlocker // empty' 2>/dev/null || echo "")
|
|
713
|
+
P13_PROTO=$(printf '%s' "$p13_resp" | jq -r '.p13ProtocolVersion // empty' 2>/dev/null || echo "")
|
|
427
714
|
# FAIL-OPEN: block ONLY on an explicit stopAllowed:false.
|
|
428
715
|
if [[ "$p13_allowed" == "false" ]]; then
|
|
429
716
|
P13_GUIDANCE="P13 — the stop is not earned: ${p13_guid}"
|
|
@@ -432,29 +719,211 @@ p13_stop_allowed() {
|
|
|
432
719
|
return 0
|
|
433
720
|
}
|
|
434
721
|
|
|
722
|
+
# ── COMPLETION_DISCIPLINE — (a) hard-blocker exit branch (NEW) ────────────────
|
|
723
|
+
# Placed AFTER emergency + duration (so (b)/emergency always win) and BEFORE the
|
|
724
|
+
# completion-condition / promise blocks (so (a) is reachable even when the condition
|
|
725
|
+
# is unmet — the whole point of (a)). NECESSARY-BUT-NOT-SUFFICIENT: a nonce-valid
|
|
726
|
+
# marker is required, AND the extended P13 must classify the blocker EXTERNAL (not
|
|
727
|
+
# buildable). Malformed/partial/nonce-mismatch/fenced/template-verbatim ⇒ NO marker
|
|
728
|
+
# ⇒ continue (the safe direction). Spec §2b.3.
|
|
729
|
+
#
|
|
730
|
+
# Helper: extract one <hard-blocker> field from the FINAL assistant turn body.
|
|
731
|
+
hb_field() {
|
|
732
|
+
local body="$1" field="$2"
|
|
733
|
+
printf '%s' "$body" | perl -0777 -ne 'print "$1" if /<hard-blocker\b[^>]*>.*?\b'"$field"'\s*:\s*(.*?)(?:\n\s*(?:what i tried|why i am stuck|what i would need to proceed)\s*:|<\/hard-blocker>)/si' 2>/dev/null | head -c 4000 || echo ""
|
|
734
|
+
}
|
|
735
|
+
# Sanitize: strip CR/LF/control chars, collapse whitespace, clamp to max chars.
|
|
736
|
+
hb_sanitize() {
|
|
737
|
+
printf '%s' "$1" | tr -d '\000-\010\013\014\016-\037' | tr '\n\r\t' ' ' | sed 's/ */ /g; s/^ *//; s/ *$//' | cut -c1-"${CD_MARKER_MAX_CHARS}"
|
|
738
|
+
}
|
|
739
|
+
# Inline credential leak scan (the existing detector is a PostToolUse hook skill,
|
|
740
|
+
# not a callable scanner — see §5 deviation note). Same pattern families. Echoes 1
|
|
741
|
+
# on a hit. Conservative: only well-known high-signal shapes.
|
|
742
|
+
hb_leak_hit() {
|
|
743
|
+
printf '%s' "$1" | grep -qiE 'sk-[a-zA-Z0-9]{16,}|xox[baprs]-[a-zA-Z0-9-]{8,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{12,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|AIza[0-9A-Za-z_-]{20,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}' 2>/dev/null && echo 1 || echo 0
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if [[ "$CD_ENABLED" == "1" ]] && [[ -n "$HARD_BLOCKER_NONCE" ]] && [[ "$HARD_BLOCKER_NONCE" != "null" ]] \
|
|
747
|
+
&& [[ -n "$CD_FINAL_TURN" ]] && [[ "$CD_FINAL_TURN" == *"<hard-blocker"* ]]; then
|
|
748
|
+
HB_OK="false"
|
|
749
|
+
# Ignore when the marker is inside a fenced code block, or is the documented
|
|
750
|
+
# template verbatim (literal `...` placeholders), or carries no/mismatched nonce.
|
|
751
|
+
HB_FENCED="false"
|
|
752
|
+
# If a ``` fence opens before the marker and the marker sits inside it, ignore.
|
|
753
|
+
if printf '%s' "$CD_FINAL_TURN" | perl -0777 -ne 'exit(/```[^`]*<hard-blocker/s ? 0 : 1)' 2>/dev/null; then
|
|
754
|
+
HB_FENCED="true"
|
|
755
|
+
fi
|
|
756
|
+
HB_NONCE_IN=$(printf '%s' "$CD_FINAL_TURN" | perl -0777 -ne 'print "$1" if /<hard-blocker\b[^>]*\bnonce\s*=\s*"([^"]*)"/si' 2>/dev/null | head -c 200 || echo "")
|
|
757
|
+
HB_TRIED_RAW=$(hb_field "$CD_FINAL_TURN" "what i tried")
|
|
758
|
+
HB_STUCK_RAW=$(hb_field "$CD_FINAL_TURN" "why i am stuck")
|
|
759
|
+
HB_NEEDED_RAW=$(hb_field "$CD_FINAL_TURN" "what i would need to proceed")
|
|
760
|
+
# Template-verbatim guard: the documented placeholders are a bare "...".
|
|
761
|
+
HB_TEMPLATE="false"
|
|
762
|
+
if [[ "$(printf '%s' "$HB_TRIED_RAW" | tr -d '[:space:].')" == "" ]] \
|
|
763
|
+
&& [[ "$(printf '%s' "$HB_STUCK_RAW" | tr -d '[:space:].')" == "" ]] \
|
|
764
|
+
&& [[ "$(printf '%s' "$HB_NEEDED_RAW" | tr -d '[:space:].')" == "" ]]; then
|
|
765
|
+
HB_TEMPLATE="true"
|
|
766
|
+
fi
|
|
767
|
+
if [[ "$HB_FENCED" != "true" ]] && [[ "$HB_TEMPLATE" != "true" ]] \
|
|
768
|
+
&& [[ "$HB_NONCE_IN" == "$HARD_BLOCKER_NONCE" ]] \
|
|
769
|
+
&& [[ -n "${HB_TRIED_RAW// /}" ]] && [[ -n "${HB_STUCK_RAW// /}" ]] && [[ -n "${HB_NEEDED_RAW// /}" ]]; then
|
|
770
|
+
HB_OK="true"
|
|
771
|
+
fi
|
|
772
|
+
|
|
773
|
+
# Contradictory terminal markers: a hard-blocker AND a completion/promise token in
|
|
774
|
+
# the SAME final turn is incoherent → NO clean exit → continue with a steer.
|
|
775
|
+
HB_HAS_COMPLETION="false"
|
|
776
|
+
if [[ -n "$COMPLETION_PROMISE" ]] && [[ "$COMPLETION_PROMISE" != "null" ]]; then
|
|
777
|
+
if printf '%s' "$CD_FINAL_TURN" | grep -q "<promise>${COMPLETION_PROMISE}</promise>" 2>/dev/null; then
|
|
778
|
+
HB_HAS_COMPLETION="true"
|
|
779
|
+
fi
|
|
780
|
+
fi
|
|
781
|
+
|
|
782
|
+
if [[ "$HB_OK" == "true" ]] && [[ "$HB_HAS_COMPLETION" == "true" ]]; then
|
|
783
|
+
# Contradictory → continue. The steer is surfaced via the continuation message.
|
|
784
|
+
# Set CD_BLOCK_TERMINAL so the downstream completion/promise blocks do NOT exit
|
|
785
|
+
# on the (also-present) completion token — the turn is incoherent, so neither
|
|
786
|
+
# terminal marker is honored.
|
|
787
|
+
CD_BLOCK_TERMINAL="true"
|
|
788
|
+
CD_CONTRADICTORY_STEER="You emitted contradictory terminal markers (a hard-blocker AND a completion assertion) in the same turn — pick one: either the work is done (show the evidence) or you are blocked (emit only the hard-blocker). Resolve and proceed."
|
|
789
|
+
elif [[ "$HB_OK" == "true" ]]; then
|
|
790
|
+
# The marker is valid. Run the extended P13 external-vs-buildable classification.
|
|
791
|
+
if p13_stop_allowed "hard-blocker"; then
|
|
792
|
+
# Version-skew three-case detection (spec §5):
|
|
793
|
+
# 1. NO p13ProtocolVersion → structurally OLD server → continue (no (a) exit)
|
|
794
|
+
# 2. proto present + classifiedBlocker=external + allowed → honor → (a) exit
|
|
795
|
+
# 3. proto present + no usable verdict (timeout/empty) → fail-open record + continue
|
|
796
|
+
if [[ -z "$P13_PROTO" ]]; then
|
|
797
|
+
# Case 1 — old server. No (a) exit possible until the server updates.
|
|
798
|
+
echo "[autonomous] hard-blocker: server is structurally old (no p13ProtocolVersion) — continuing (no (a) exit)" >&2
|
|
799
|
+
CD_CONTRADICTORY_STEER=""
|
|
800
|
+
elif [[ "$P13_CLASSIFIED" == "external" ]]; then
|
|
801
|
+
# Case 2 — honored EXTERNAL allow → the (a) exit.
|
|
802
|
+
HB_TRIED=$(hb_sanitize "$HB_TRIED_RAW")
|
|
803
|
+
HB_STUCK=$(hb_sanitize "$HB_STUCK_RAW")
|
|
804
|
+
HB_NEEDED=$(hb_sanitize "$HB_NEEDED_RAW")
|
|
805
|
+
HB_LEAK=0
|
|
806
|
+
if [[ "$(hb_leak_hit "$HB_TRIED")" == "1" ]]; then HB_TRIED="[redacted: possible secret]"; HB_LEAK=1; fi
|
|
807
|
+
if [[ "$(hb_leak_hit "$HB_STUCK")" == "1" ]]; then HB_STUCK="[redacted: possible secret]"; HB_LEAK=1; fi
|
|
808
|
+
if [[ "$(hb_leak_hit "$HB_NEEDED")" == "1" ]]; then HB_NEEDED="[redacted: possible secret]"; HB_LEAK=1; fi
|
|
809
|
+
# (a) Write ONE row to logs/autonomous-hard-blocker.jsonl (coarse rotation).
|
|
810
|
+
mkdir -p logs 2>/dev/null || true
|
|
811
|
+
if [[ -f "$HARD_BLOCKER_LOG" ]]; then
|
|
812
|
+
HB_SZ=$(stat -c %s "$HARD_BLOCKER_LOG" 2>/dev/null || stat -f %z "$HARD_BLOCKER_LOG" 2>/dev/null || echo 0)
|
|
813
|
+
[[ "$HB_SZ" =~ ^[0-9]+$ ]] || HB_SZ=0
|
|
814
|
+
[[ $HB_SZ -ge $CD_LOG_ROTATE_BYTES ]] && mv -f "$HARD_BLOCKER_LOG" "${HARD_BLOCKER_LOG}.1" 2>/dev/null || true
|
|
815
|
+
fi
|
|
816
|
+
jq -nc \
|
|
817
|
+
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" --arg topic "${REPORT_TOPIC:-}" \
|
|
818
|
+
--arg iter "${ITERATION:-}" --arg goal "$(goal_snippet)" \
|
|
819
|
+
--arg tried "$HB_TRIED" --arg stuck "$HB_STUCK" --arg needed "$HB_NEEDED" \
|
|
820
|
+
--argjson unchecked "$CD_UNCHECKED_COUNT" --arg ts2 "$CD_TASK_STRUCTURE" \
|
|
821
|
+
--argjson leak "$([[ $HB_LEAK -eq 1 ]] && echo true || echo false)" \
|
|
822
|
+
'{ts:$ts,topic:$topic,iteration:$iter,goal:$goal,tried:$tried,stuck:$stuck,needed:$needed,completionConditionMet:false,uncheckedTaskCount:$unchecked,taskStructure:$ts2,leakRedacted:$leak}' \
|
|
823
|
+
>> "$HARD_BLOCKER_LOG" 2>/dev/null || true
|
|
824
|
+
# (b) Raise ONE /ack-able Attention item (deduped per the Topic-Flood Guard).
|
|
825
|
+
cd_raise_attention_item "$HB_TRIED" "$HB_STUCK" "$HB_NEEDED"
|
|
826
|
+
# (c) ONE plain-English notify-on-stop.
|
|
827
|
+
notify_terminal_stop "🚧 My autonomous run on \"$(goal_snippet)\" stopped on a hard blocker I can't resolve myself. What I'd need: ${HB_NEEDED}. I've queued it for your attention."
|
|
828
|
+
emit "🚧 Autonomous mode: hard blocker (external) — exit allowed. needed: ${HB_NEEDED}"
|
|
829
|
+
# (d) Remove state; allow exit (write once — state removal prevents re-append).
|
|
830
|
+
rm -f "$STATE_FILE" "$CD_BACKOFF_STATE" 2>/dev/null || true
|
|
831
|
+
exit 0
|
|
832
|
+
else
|
|
833
|
+
# Case 3 — new server, no usable external classification (timeout/empty/buildable
|
|
834
|
+
# via the allow path is impossible; this is the timeout/ambiguous fail-open).
|
|
835
|
+
cd_record_judge_failure
|
|
836
|
+
cd_write_unreachable_row
|
|
837
|
+
echo "[autonomous] hard-blocker: no usable external classification — recorded evaluator-unreachable-exit, continuing" >&2
|
|
838
|
+
CD_CONTRADICTORY_STEER=""
|
|
839
|
+
fi
|
|
840
|
+
else
|
|
841
|
+
# P13 blocked → buildable / not-earned. Continue; P13_GUIDANCE is the next-turn steer.
|
|
842
|
+
CD_CONTRADICTORY_STEER=""
|
|
843
|
+
fi
|
|
844
|
+
fi
|
|
845
|
+
fi
|
|
846
|
+
|
|
435
847
|
# Completion CONDITION — independent evaluator (mirrors /goal). Authoritative when
|
|
436
848
|
# set; the self-declared promise below is the legacy fallback. FAIL-SAFE: if the
|
|
437
849
|
# evaluator is unreachable or unsure, we keep working — never a false "done".
|
|
850
|
+
#
|
|
851
|
+
# COMPLETION_DISCIPLINE (CD_ENABLED): the deterministic checkbox scan is the PRIMARY
|
|
852
|
+
# "buildable work remains" signal. The completion judge fires ONLY on a might-be-done
|
|
853
|
+
# iteration (uncheckedTaskCount==0 OR an explicit completion assertion in the final
|
|
854
|
+
# turn) — the common keep-working iteration costs ZERO LLM (spec §2b.2). When it
|
|
855
|
+
# fires, the milestone/buildable-work scrutiny is FOLDED into the completion judge's
|
|
856
|
+
# signals (the single critical-path call — no standalone P13 on the condition path).
|
|
857
|
+
# The circuit-breaker short-circuits to the cheap checkbox-only decision after K
|
|
858
|
+
# consecutive judge failures (spec §3 item 4).
|
|
438
859
|
EVAL_REASON=""
|
|
439
|
-
if [[ -n "$COMPLETION_CONDITION" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
860
|
+
if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$COMPLETION_CONDITION" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
440
861
|
EVAL_MET=""
|
|
862
|
+
# Decide whether to fire the judge at all (CD cost-discipline gate).
|
|
863
|
+
CD_MIGHT_BE_DONE="true"
|
|
864
|
+
if [[ "$CD_ENABLED" == "1" ]] && [[ -z "${INSTAR_HOOK_EVAL_OVERRIDE:-}" ]]; then
|
|
865
|
+
CD_MIGHT_BE_DONE="false"
|
|
866
|
+
[[ "$CD_UNCHECKED_COUNT" == "0" ]] && CD_MIGHT_BE_DONE="true"
|
|
867
|
+
# An explicit completion assertion in the final turn also triggers the judge.
|
|
868
|
+
if printf '%s' "$CD_FINAL_TURN" | grep -qiE 'all (tasks|tests) (complete|pass)|<promise>|condition (is )?met|completion condition met|task list (is )?complete' 2>/dev/null; then
|
|
869
|
+
CD_MIGHT_BE_DONE="true"
|
|
870
|
+
fi
|
|
871
|
+
fi
|
|
441
872
|
if [[ -n "${INSTAR_HOOK_EVAL_OVERRIDE:-}" ]]; then
|
|
442
873
|
# Test seam: "met" | "not-met" short-circuits the live evaluator call.
|
|
443
874
|
[[ "$INSTAR_HOOK_EVAL_OVERRIDE" == "met" ]] && EVAL_MET="true"
|
|
444
875
|
EVAL_REASON="override:$INSTAR_HOOK_EVAL_OVERRIDE"
|
|
876
|
+
elif [[ "$CD_ENABLED" == "1" ]] && [[ "$(cd_breaker_open)" == "1" ]]; then
|
|
877
|
+
# Breaker OPEN — cheap checkbox-only decision, no LLM call (never a fail-open exit).
|
|
878
|
+
if [[ "$CD_UNCHECKED_COUNT" != "0" ]]; then
|
|
879
|
+
EVAL_REASON="circuit-breaker open (judge failing) — work remains ($CD_UNCHECKED_COUNT unchecked), keeping going"
|
|
880
|
+
else
|
|
881
|
+
EVAL_REASON="circuit-breaker open (judge failing) — verify the condition and re-assert with evidence"
|
|
882
|
+
fi
|
|
883
|
+
echo "[autonomous] completion-discipline: judge breaker OPEN — cheap checkbox-only continue (unchecked=$CD_UNCHECKED_COUNT)" >&2
|
|
884
|
+
elif [[ "$CD_MIGHT_BE_DONE" != "true" ]]; then
|
|
885
|
+
# Common keep-working iteration — zero LLM. The checkbox scan says work remains.
|
|
886
|
+
EVAL_REASON="work remains ($CD_UNCHECKED_COUNT unchecked) — keeping going"
|
|
445
887
|
else
|
|
446
888
|
EVAL_TAIL=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -6 \
|
|
447
889
|
| jq -r '.message.content | map(select(.type=="text")) | map(.text) | join("\n")' 2>/dev/null \
|
|
448
890
|
| tail -c 8000 || echo "")
|
|
449
891
|
EVAL_PORT=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
|
|
450
892
|
EVAL_AUTH=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
|
|
451
|
-
|
|
452
|
-
|
|
893
|
+
if [[ "$CD_ENABLED" == "1" ]]; then
|
|
894
|
+
# Fold the milestone/buildable-work scrutiny into the completion judge (single call).
|
|
895
|
+
CD_SIG=$(build_signals_json "" "false")
|
|
896
|
+
EVAL_BODY=$(jq -nc --arg c "$COMPLETION_CONDITION" --arg t "$EVAL_TAIL" --argjson sig "$CD_SIG" '{condition:$c,transcriptTail:$t,signals:$sig}')
|
|
897
|
+
else
|
|
898
|
+
EVAL_BODY=$(jq -nc --arg c "$COMPLETION_CONDITION" --arg t "$EVAL_TAIL" '{condition:$c,transcriptTail:$t}')
|
|
899
|
+
fi
|
|
900
|
+
EVAL_RESP=$(printf '%s' "$EVAL_BODY" \
|
|
901
|
+
| curl -s -m "$JUDGE_TIMEOUT_S" -H "Authorization: Bearer $EVAL_AUTH" -H 'Content-Type: application/json' \
|
|
453
902
|
--data-binary @- "http://localhost:${EVAL_PORT}/autonomous/evaluate-completion" 2>/dev/null || echo "")
|
|
454
903
|
EVAL_MET=$(printf '%s' "$EVAL_RESP" | jq -r '.met // empty' 2>/dev/null || echo "")
|
|
455
904
|
EVAL_REASON=$(printf '%s' "$EVAL_RESP" | jq -r '.reason // empty' 2>/dev/null || echo "")
|
|
905
|
+
if [[ "$CD_ENABLED" == "1" ]]; then
|
|
906
|
+
if [[ -z "$EVAL_RESP" ]] || { [[ -z "$EVAL_MET" ]] && [[ -z "$EVAL_REASON" ]]; }; then
|
|
907
|
+
# Judge unreachable/empty → record the failure (breaker) + the unreachable
|
|
908
|
+
# breadcrumb, then CONTINUE (never a silent exit). Spec §3 item 4 / §4.
|
|
909
|
+
cd_record_judge_failure
|
|
910
|
+
cd_write_unreachable_row
|
|
911
|
+
else
|
|
912
|
+
cd_reset_judge_failures
|
|
913
|
+
fi
|
|
914
|
+
fi
|
|
456
915
|
fi
|
|
457
916
|
if [[ "$EVAL_MET" == "true" ]]; then
|
|
917
|
+
if [[ "$CD_ENABLED" == "1" ]]; then
|
|
918
|
+
# The folded signals already carried the milestone/buildable-work scrutiny to
|
|
919
|
+
# the completion judge (the SINGLE critical-path call). A "met" verdict here is
|
|
920
|
+
# the judge's all-things-considered decision → allow. No standalone P13 call on
|
|
921
|
+
# the condition path (spec §2b.2 — folded once the canary verifies the block).
|
|
922
|
+
emit "✅ Autonomous mode: completion condition met (independent evaluator): ${EVAL_REASON}"
|
|
923
|
+
notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — the goal was met."
|
|
924
|
+
rm -f "$STATE_FILE" "$CD_BACKOFF_STATE" 2>/dev/null || true
|
|
925
|
+
exit 0
|
|
926
|
+
fi
|
|
458
927
|
if p13_stop_allowed; then
|
|
459
928
|
emit "✅ Autonomous mode: completion condition met (independent evaluator): ${EVAL_REASON}"
|
|
460
929
|
notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — the goal was met."
|
|
@@ -470,7 +939,7 @@ if [[ -n "$COMPLETION_CONDITION" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TR
|
|
|
470
939
|
fi
|
|
471
940
|
|
|
472
941
|
# Completion promise (genuine completion — legacy/self-declared fallback)
|
|
473
|
-
if [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
942
|
+
if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
|
|
474
943
|
LAST_LINE=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 || echo "")
|
|
475
944
|
if [[ -n "$LAST_LINE" ]]; then
|
|
476
945
|
LAST_OUTPUT=$(printf '%s' "$LAST_LINE" | jq -r '
|
|
@@ -741,17 +1210,22 @@ if [[ -n "${ELAPSED:-}" ]]; then
|
|
|
741
1210
|
fi
|
|
742
1211
|
fi
|
|
743
1212
|
|
|
1213
|
+
# COMPLETION_DISCIPLINE — when a hard-blocker marker was emitted but NOT honored
|
|
1214
|
+
# (contradictory markers, or P13 judged it buildable/not-earned), surface that steer.
|
|
1215
|
+
CD_STEER=""
|
|
1216
|
+
[[ -n "${CD_CONTRADICTORY_STEER:-}" ]] && CD_STEER=" | ${CD_CONTRADICTORY_STEER}"
|
|
1217
|
+
|
|
744
1218
|
# When a completion CONDITION is set, an independent judge decides "done" — steer
|
|
745
1219
|
# toward the condition + feed back the judge's latest reason (mirrors /goal). When
|
|
746
1220
|
# only a legacy promise is set, keep the self-declared-promise directive.
|
|
747
1221
|
if [[ -n "$COMPLETION_CONDITION" ]]; then
|
|
748
1222
|
GUIDANCE=""
|
|
749
1223
|
[[ -n "$EVAL_REASON" ]] && GUIDANCE=" | Not done yet: ${EVAL_REASON}"
|
|
750
|
-
SYSTEM_MSG="🔄 Autonomous iteration $NEXT_ITERATION ($TIME_MSG)${CLOCK_SEG} | Keep working until this is TRUE: ${COMPLETION_CONDITION}${GUIDANCE} | An independent check decides done from what you SURFACE — run the real checks and show the evidence. Do NOT defer — do it now${REPORT_DIRECTIVE}"
|
|
1224
|
+
SYSTEM_MSG="🔄 Autonomous iteration $NEXT_ITERATION ($TIME_MSG)${CLOCK_SEG} | Keep working until this is TRUE: ${COMPLETION_CONDITION}${GUIDANCE}${CD_STEER} | An independent check decides done from what you SURFACE — run the real checks and show the evidence. Do NOT defer — do it now${REPORT_DIRECTIVE}"
|
|
751
1225
|
else
|
|
752
1226
|
P13_NOTE=""
|
|
753
1227
|
[[ -n "${P13_GUIDANCE:-}" ]] && P13_NOTE=" | ${P13_GUIDANCE}"
|
|
754
|
-
SYSTEM_MSG="🔄 Autonomous iteration $NEXT_ITERATION ($TIME_MSG)${CLOCK_SEG} | Complete ALL tasks, then output <promise>$COMPLETION_PROMISE</promise>${P13_NOTE} | Do NOT defer to future self — if you can do it now, DO IT NOW${REPORT_DIRECTIVE}"
|
|
1228
|
+
SYSTEM_MSG="🔄 Autonomous iteration $NEXT_ITERATION ($TIME_MSG)${CLOCK_SEG} | Complete ALL tasks, then output <promise>$COMPLETION_PROMISE</promise>${P13_NOTE}${CD_STEER} | Do NOT defer to future self — if you can do it now, DO IT NOW${REPORT_DIRECTIVE}"
|
|
755
1229
|
fi
|
|
756
1230
|
|
|
757
1231
|
# Block exit and feed prompt back
|
|
@@ -98,6 +98,22 @@ if [[ -z "$COMPLETION_PROMISE" ]]; then
|
|
|
98
98
|
COMPLETION_PROMISE="ALL_TASKS_COMPLETE"
|
|
99
99
|
fi
|
|
100
100
|
|
|
101
|
+
# ── COMPLETION_DISCIPLINE — bounded-duration backstop (spec §4 resolved Open-Q) ──
|
|
102
|
+
# A run under completion-discipline REQUIRES a duration > 0 (the hard backstop that
|
|
103
|
+
# makes the judge fail-open safe). A 0/unset duration is treated as a config error and
|
|
104
|
+
# defaulted to a conservative 8h, rather than running truly unbounded.
|
|
105
|
+
if [[ "$DURATION_SECONDS" -le 0 ]]; then
|
|
106
|
+
echo "⚠️ No bounded duration set — defaulting to 8h (completion-discipline requires a duration backstop)." >&2
|
|
107
|
+
DURATION_SECONDS=28800
|
|
108
|
+
DURATION="8h"
|
|
109
|
+
END_AT=$(date -u -v+${DURATION_SECONDS}S +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d "+${DURATION_SECONDS} seconds" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown")
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# ── COMPLETION_DISCIPLINE — per-run hard-blocker nonce ──
|
|
113
|
+
# Authenticates a <hard-blocker> terminal exit marker (mirrors the completion_promise
|
|
114
|
+
# exact-match guard) so incidental marker prose can never trip an exit. Spec §2b.3.
|
|
115
|
+
HARD_BLOCKER_NONCE=$(openssl rand -hex 8 2>/dev/null || head -c 16 /dev/urandom | od -An -tx1 | tr -d ' \n' || echo "$(date +%s)$$")
|
|
116
|
+
|
|
101
117
|
# ── Multi-session start gate: concurrency cap + quota (refuse-new) ──
|
|
102
118
|
# Primary check is the server (precise active-count + QuotaTracker). If the
|
|
103
119
|
# server is unreachable, fall back to a local file-count cap so the cap still
|
|
@@ -159,6 +175,7 @@ last_report_at: ""
|
|
|
159
175
|
level_up: $LEVEL_UP
|
|
160
176
|
completion_promise: "$COMPLETION_PROMISE"
|
|
161
177
|
completion_condition: "$COMPLETION_CONDITION"
|
|
178
|
+
hard_blocker_nonce: "$HARD_BLOCKER_NONCE"
|
|
162
179
|
---
|
|
163
180
|
|
|
164
181
|
# Autonomous Session
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfigDefaults.d.ts","sourceRoot":"","sources":["../../src/config/ConfigDefaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;
|
|
1
|
+
{"version":3,"file":"ConfigDefaults.d.ts","sourceRoot":"","sources":["../../src/config/ConfigDefaults.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAkrBH,MAAM,MAAM,SAAS,GAAG,iBAAiB,GAAG,YAAY,CAAC;AAEzD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO7E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIlF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAChC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAuC5D"}
|