instar 1.3.712 → 1.3.714

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.
Files changed (68) hide show
  1. package/.claude/skills/autonomous/SKILL.md +49 -0
  2. package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +103 -9
  3. package/.claude/skills/autonomous/scripts/setup-autonomous.sh +47 -0
  4. package/dist/commands/init.js +9 -0
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  7. package/dist/config/ConfigDefaults.js +19 -0
  8. package/dist/config/ConfigDefaults.js.map +1 -1
  9. package/dist/core/AutonomousRunStore.d.ts +200 -0
  10. package/dist/core/AutonomousRunStore.d.ts.map +1 -0
  11. package/dist/core/AutonomousRunStore.js +402 -0
  12. package/dist/core/AutonomousRunStore.js.map +1 -0
  13. package/dist/core/CompletionEvaluator.d.ts +24 -0
  14. package/dist/core/CompletionEvaluator.d.ts.map +1 -1
  15. package/dist/core/CompletionEvaluator.js +29 -1
  16. package/dist/core/CompletionEvaluator.js.map +1 -1
  17. package/dist/core/ExternalOperationGate.d.ts.map +1 -1
  18. package/dist/core/ExternalOperationGate.js +6 -0
  19. package/dist/core/ExternalOperationGate.js.map +1 -1
  20. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  21. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  22. package/dist/core/PostUpdateMigrator.js +56 -3
  23. package/dist/core/PostUpdateMigrator.js.map +1 -1
  24. package/dist/core/ScopeAccretionCorroboration.d.ts +79 -0
  25. package/dist/core/ScopeAccretionCorroboration.d.ts.map +1 -0
  26. package/dist/core/ScopeAccretionCorroboration.js +159 -0
  27. package/dist/core/ScopeAccretionCorroboration.js.map +1 -0
  28. package/dist/core/ScopeAccretionRatifier.d.ts +93 -0
  29. package/dist/core/ScopeAccretionRatifier.d.ts.map +1 -0
  30. package/dist/core/ScopeAccretionRatifier.js +228 -0
  31. package/dist/core/ScopeAccretionRatifier.js.map +1 -0
  32. package/dist/core/ScopeAccretionSweep.d.ts +83 -0
  33. package/dist/core/ScopeAccretionSweep.d.ts.map +1 -0
  34. package/dist/core/ScopeAccretionSweep.js +353 -0
  35. package/dist/core/ScopeAccretionSweep.js.map +1 -0
  36. package/dist/core/WorkingSetManifest.d.ts.map +1 -1
  37. package/dist/core/WorkingSetManifest.js +26 -1
  38. package/dist/core/WorkingSetManifest.js.map +1 -1
  39. package/dist/core/installCodexHooks.d.ts.map +1 -1
  40. package/dist/core/installCodexHooks.js +9 -0
  41. package/dist/core/installCodexHooks.js.map +1 -1
  42. package/dist/core/types.d.ts +29 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
  46. package/dist/lifeline/TelegramLifeline.js +8 -0
  47. package/dist/lifeline/TelegramLifeline.js.map +1 -1
  48. package/dist/messaging/TelegramAdapter.d.ts +18 -0
  49. package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
  50. package/dist/messaging/TelegramAdapter.js +28 -0
  51. package/dist/messaging/TelegramAdapter.js.map +1 -1
  52. package/dist/scaffold/templates.js +2 -2
  53. package/dist/scaffold/templates.js.map +1 -1
  54. package/dist/server/routes.d.ts.map +1 -1
  55. package/dist/server/routes.js +638 -1
  56. package/dist/server/routes.js.map +1 -1
  57. package/dist/server/specReviewRoutes.d.ts +6 -0
  58. package/dist/server/specReviewRoutes.d.ts.map +1 -1
  59. package/dist/server/specReviewRoutes.js +30 -0
  60. package/dist/server/specReviewRoutes.js.map +1 -1
  61. package/package.json +1 -1
  62. package/scripts/lint-scrape-fixture-realness.js +40 -0
  63. package/src/data/builtin-manifest.json +65 -65
  64. package/src/scaffold/templates.ts +2 -2
  65. package/upgrades/1.3.713.md +65 -0
  66. package/upgrades/1.3.714.md +40 -0
  67. package/upgrades/side-effects/autonomous-scope-accretion-completion.md +77 -0
  68. package/upgrades/side-effects/bench-prompt-fixes-2.md +72 -0
@@ -104,6 +104,11 @@ Write this content:
104
104
  when set, the stop-hook RUNS it on a met:true verdict and only allows the exit if it ALSO
105
105
  passes (fail/timeout → keep working). This sentinel is the PostUpdateMigrator marker that
106
106
  re-deploys this SKILL.md to existing agents. Spec: autonomous-completion-real-checks.md -->
107
+ <!-- SCOPE_ACCRETION — scope-accretion completion discipline: register the run server-side
108
+ (POST /autonomous/register) and record the returned run_id in the frontmatter; the server
109
+ then holds completion while deliverables THIS run creates sit unbuilt, uncorroborated, and
110
+ unratified. This sentinel is the PostUpdateMigrator marker that re-deploys this SKILL.md to
111
+ existing agents. Spec: autonomous-scope-accretion-completion.md -->
107
112
 
108
113
  ```markdown
109
114
  ---
@@ -124,6 +129,7 @@ completion_mode: condition # "condition" (default) | "promise-fallback"
124
129
  promise_fallback_reason: "" # one line, REQUIRED iff completion_mode == promise-fallback
125
130
  completion_promise: "ALL_TASKS_COMPLETE" # retained as the fallback token
126
131
  hard_blocker_nonce: "{a random per-run token — get via: openssl rand -hex 8}"
132
+ run_id: "{the runId returned by POST /autonomous/register — see step 2b-2 below}"
127
133
  # OPTIONAL real-check (ACT-152): a command that must exit 0 before the run may stop. When set, a
128
134
  # met:true verdict RUNS it and gates the exit on it (fail/timeout → keep working). Omit if the
129
135
  # goal isn't checkable by a single command. work_dir is captured by setup so a relative command
@@ -171,6 +177,27 @@ echo $CLAUDE_CODE_SESSION_ID
171
177
  ```
172
178
  Then include the output in the `session_id:` field. This ensures session isolation works.
173
179
 
180
+ **2b-2. Register the run server-side (SCOPE_ACCRETION — MANDATORY when a report topic exists).**
181
+ The server takes the start snapshot (config, condition, declared deliverables, git base-root
182
+ SHAs), mints the `runId`, and from then on the completion judge structurally HOLDS "done" while
183
+ deliverables this run itself creates (specs, audits, runbooks, scripts) sit unbuilt without
184
+ corroboration (a merged PR / a converged report) or explicit operator ratification:
185
+
186
+ ```bash
187
+ PORT=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))")
188
+ AUTH=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))")
189
+ jq -nc --arg t "TOPIC_ID" --arg c "YOUR completion_condition text" --arg w "$(pwd)" \
190
+ --arg s "STARTED_AT_ISO" --arg e "END_AT_ISO" --arg sid "$CLAUDE_CODE_SESSION_ID" \
191
+ '{topicId:$t,condition:$c,workDir:$w,startedAt:$s,endAt:$e,sessionId:$sid,declaredDeliverables:[]}' \
192
+ | curl -s -H "Authorization: Bearer $AUTH" -H 'Content-Type: application/json' \
193
+ --data-binary @- "http://localhost:${PORT}/autonomous/register"
194
+ ```
195
+
196
+ Write the returned `runId` into the state file's `run_id:` frontmatter field. If the mission is
197
+ GENUINELY draft-only (the operator asked for drafts, not builds), list those exact repo-relative
198
+ paths in `declaredDeliverables` — that list is operator-visible at setup and immutable mid-run.
199
+ If the server is unreachable, leave `run_id` empty and continue — the gate degrades honestly.
200
+
174
201
  **WHY NOT bash script?** Running `bash setup-autonomous.sh` creates a subprocess that does NOT inherit `CLAUDE_CODE_SESSION_ID`. The state file ends up with an empty session_id, which causes the hook to leak into all sessions. Always write the state file from within Claude Code's context.
175
202
 
176
203
  **SESSION ISOLATION**: The stop hook checks `session_id` — it only blocks the session that activated autonomous mode. Other sessions on the same machine pass through unaffected.
@@ -207,6 +234,28 @@ The stop hook will catch every attempt to exit and feed your task list back. Eac
207
234
  5. Move to next task
208
235
  6. Send progress reports at the configured interval
209
236
 
237
+ ### Scope Accretion — work YOU create joins YOUR completion bar (Layer A duty)
238
+
239
+ The completion bar is NOT frozen at session start. When you discover new in-scope work and
240
+ draft it (a spec, an audit, a runbook, a script), that artifact joins your bar: the server's
241
+ git-truth sweep sees it regardless of HOW it was written (Write tool, Bash heredoc, `tee`, a
242
+ subagent), and the judge will HOLD "done" until it is built+corroborated (merged PR / converged
243
+ report), was declared at setup, or the operator ratifies deferring it. "The documented stretch
244
+ (out of completion condition)" is the exact anti-pattern this closes — Deferral = Deletion.
245
+
246
+ **Your recording duty (Layer A):** the moment you create a deliverable, append to the state
247
+ file's task list:
248
+
249
+ ```
250
+ - [ ] ACCRETED(<ISO date>): <repo-relative path> — build/converge/deliver, or obtain operator ratification
251
+ ```
252
+
253
+ This is a willpower-assist for the honest case and carries zero safety weight — the server-side
254
+ sweep is the enforcement. If deferral is genuinely right, ASK the operator in the topic (say
255
+ "ratify deferral of <paths>?"); a server-authored enumeration message will list the exact set,
256
+ and only the verified operator's reply binds it. The operator can also ratify from the dashboard
257
+ (PIN-gated).
258
+
210
259
  ### The Defer-to-Future-Self Trap
211
260
 
212
261
  **This is the #1 failure mode.** It looks like:
@@ -44,6 +44,13 @@ for _arg in "$@"; do [[ "$_arg" == "--codex" ]] && IS_CODEX=1; done
44
44
  # verification_command on a met:true verdict and gates the exit on it; fail/timeout/breaker-open →
45
45
  # keep working, the safe direction). This sentinel is the PostUpdateMigrator marker that re-deploys
46
46
  # this hook to existing agents that carry COMPLETION_DISCIPLINE but not REALCHECK_VERIFY.
47
+ # hook-capability: SCOPE_ACCRETION — scope-accretion completion discipline (spec:
48
+ # autonomous-scope-accretion-completion.md). Layer B evasion-vocabulary scan (advisory
49
+ # scopeAccretionSuspected signal, fenced/quoted-region excluded), topicId/runId/sessionId echoed
50
+ # to the evaluate-completion chokepoint (the server computes the LOAD-BEARING git-truth sweep +
51
+ # ratification state there), and run_end_call fired on EVERY terminal exit surface (R40/R44) so
52
+ # no exit is silent. This sentinel is the PostUpdateMigrator marker that re-deploys this hook to
53
+ # existing agents that carry REALCHECK_VERIFY but not SCOPE_ACCRETION.
47
54
  # emit — human-facing approve/status text. In codex mode the Stop hook's STDOUT must be
48
55
  # ONLY valid decision-JSON (the `{"decision":"block",...}` case far below) or empty:
49
56
  # codex rejects ANY other stdout as "invalid stop hook JSON output" and reports the stop
@@ -239,6 +246,11 @@ WORK_DIR=$(fm_get_raw work_dir)
239
246
  # marker (mirrors the completion_promise exact-match guard). Absent on older
240
247
  # state files → the marker branch self-disables for that run (no false exit).
241
248
  HARD_BLOCKER_NONCE=$(fm_get hard_blocker_nonce)
249
+ # SCOPE_ACCRETION — the server-minted run id written by setup at registration
250
+ # (POST /autonomous/register). Echoed on evaluate-completion + run-end calls so
251
+ # the server can verify the (topicId, runId) pair against its OWN registration
252
+ # record. Absent on older/unregistered runs → the server degrades honestly.
253
+ RUN_ID=$(fm_get run_id)
242
254
 
243
255
  # ── COMPLETION_DISCIPLINE — off-switch + judge curl budget (read at the chokepoint) ──
244
256
  # Autonomous Completion Discipline (spec: AUTONOMOUS-COMPLETION-DISCIPLINE.md).
@@ -281,7 +293,13 @@ try:
281
293
  rcbt = rc.get('failBreakerThreshold', 3)
282
294
  rcbw = rc.get('failWindowMs', 600000)
283
295
  rcbc = rc.get('failCooldownMs', 600000)
284
- print('%s %d %d %d %d %d %d %s %d %d %d %d %d %d' % (
296
+ # SCOPE_ACCRETION Layer B advisory scan gate. NOTE: the LOAD-BEARING gate
297
+ # is snapshotted SERVER-SIDE at registration (this local read only gates the
298
+ # hook's ADVISORY scopeAccretionSuspected field, keeping the judge prompt
299
+ # byte-identical when the feature is off).
300
+ sa = a.get('scopeAccretion') or {}
301
+ sae = sa.get('enabled', True)
302
+ print('%s %d %d %d %d %d %d %s %d %d %d %d %d %d %s' % (
285
303
  '1' if en else '0', jt,
286
304
  int(bt) if str(bt).isdigit() else 3,
287
305
  int(bw) if str(bw).isdigit() else 600000,
@@ -294,12 +312,14 @@ try:
294
312
  int(rcbt) if str(rcbt).isdigit() else 3,
295
313
  int(rcbw) if str(rcbw).isdigit() else 600000,
296
314
  int(rcbc) if str(rcbc).isdigit() else 600000,
315
+ '1' if sae else '0',
297
316
  ))
298
317
  except Exception:
299
- print('1 35000 3 600000 600000 500 1048576 1 120000 2000 65536 3 600000 600000')
300
- " 2>/dev/null || echo "1 35000 3 600000 600000 500 1048576 1 120000 2000 65536 3 600000 600000")
301
- 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 RC_ENABLED RC_TIMEOUT_MS RC_MAX_CHARS RC_CAPTURE_BYTES RC_BREAKER_THRESHOLD RC_BREAKER_WINDOW_MS RC_BREAKER_COOLDOWN_MS <<< "$CD_CFG"
318
+ print('1 35000 3 600000 600000 500 1048576 1 120000 2000 65536 3 600000 600000 1')
319
+ " 2>/dev/null || echo "1 35000 3 600000 600000 500 1048576 1 120000 2000 65536 3 600000 600000 1")
320
+ 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 RC_ENABLED RC_TIMEOUT_MS RC_MAX_CHARS RC_CAPTURE_BYTES RC_BREAKER_THRESHOLD RC_BREAKER_WINDOW_MS RC_BREAKER_COOLDOWN_MS SA_ENABLED <<< "$CD_CFG"
302
321
  [[ "$CD_ENABLED" =~ ^[01]$ ]] || CD_ENABLED=1
322
+ [[ "${SA_ENABLED:-}" =~ ^[01]$ ]] || SA_ENABLED=1
303
323
  [[ "$JUDGE_TIMEOUT_MS" =~ ^[0-9]+$ ]] || JUDGE_TIMEOUT_MS=35000
304
324
  # curl -m takes SECONDS; convert ms→s (ceil), floor 5s.
305
325
  JUDGE_TIMEOUT_S=$(( (JUDGE_TIMEOUT_MS + 999) / 1000 ))
@@ -343,6 +363,29 @@ notify_terminal_stop() {
343
363
  printf '%s\n' "$msg" | "$script" "$REPORT_TOPIC" >/dev/null 2>&1 || true
344
364
  }
345
365
 
366
+ # SCOPE_ACCRETION — run-end reporting (R44): EVERY terminal exit surface calls
367
+ # POST /autonomous/:topic/run-end so the server runs the non-blocking advisory
368
+ # sweep and, when unbuilt accreted work remains, enumerates it LOUDLY (R40 —
369
+ # the silent clock-out is structurally closed; the emergency-stop lever buys a
370
+ # faster exit, never a quieter one). Best-effort + `-m`-bounded: a delivery
371
+ # failure NEVER blocks or delays the exit (the R28b daily sweep is the backstop).
372
+ run_end_call() {
373
+ local reason="$1"
374
+ [[ -z "$REPORT_TOPIC" ]] && return 0
375
+ # Test seam: record the would-be call instead of hitting the server.
376
+ if [[ -n "${INSTAR_HOOK_RUNEND_RECORD:-}" ]]; then
377
+ printf '{"topic":"%s","reason":"%s","runId":"%s"}\n' "$REPORT_TOPIC" "$reason" "${RUN_ID:-}" \
378
+ >> "$INSTAR_HOOK_RUNEND_RECORD" 2>/dev/null || true
379
+ return 0
380
+ fi
381
+ local re_port re_auth
382
+ re_port=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
383
+ re_auth=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
384
+ jq -nc --arg r "$reason" --arg id "${RUN_ID:-}" '{reason:$r} + (if $id != "" then {runId:$id} else {} end)' 2>/dev/null \
385
+ | curl -s -m 8 -H "Authorization: Bearer $re_auth" -H 'Content-Type: application/json' \
386
+ --data-binary @- "http://localhost:${re_port}/autonomous/${REPORT_TOPIC}/run-end" >/dev/null 2>&1 || true
387
+ }
388
+
346
389
  # Validate recorded session_id is a real UUID. Claude sometimes writes a custom
347
390
  # string instead of $CLAUDE_CODE_SESSION_ID; non-UUID values are treated as
348
391
  # empty so the session-match backstop self-bootstraps from the real UUID.
@@ -441,6 +484,7 @@ if [[ "$GOAL_MODE" == "native" ]]; then
441
484
  }
442
485
  if [[ -f ".instar/autonomous-emergency-stop" ]]; then
443
486
  notify_terminal_stop "🛑 My autonomous run on \"$(goal_snippet)\" was stopped (emergency stop)."
487
+ run_end_call "emergency-stop (native-goal)"
444
488
  native_goal_clear; rm -f "$STATE_FILE"
445
489
  echo "[autonomous] emergency stop — native /goal cleared" >&2; exit 0
446
490
  fi
@@ -448,6 +492,7 @@ if [[ "$GOAL_MODE" == "native" ]]; then
448
492
  NG_START=$(date -u -j -f "%Y-%m-%dT%H:%M:%SZ" "$STARTED_AT" +%s 2>/dev/null || date -d "$STARTED_AT" +%s 2>/dev/null || echo 0)
449
493
  if [[ "$NG_START" =~ ^[0-9]+$ ]] && [[ $NG_START -gt 0 ]] && [[ $(( $(date +%s) - NG_START )) -ge $DURATION_SECONDS ]]; then
450
494
  notify_terminal_stop "⏰ My autonomous run on \"$(goal_snippet)\" hit its time limit and stopped. Ask me and I'll pick up where I left off."
495
+ run_end_call "duration-expiry (native-goal)"
451
496
  native_goal_clear; rm -f "$STATE_FILE"
452
497
  echo "[autonomous] duration expired — native /goal cleared" >&2; exit 0
453
498
  fi
@@ -461,6 +506,7 @@ fi
461
506
  # Validate iteration
462
507
  if [[ ! "$ITERATION" =~ ^[0-9]+$ ]]; then
463
508
  echo "⚠️ Autonomous mode: State file corrupted (bad iteration)" >&2
509
+ run_end_call "state-corrupt (bad iteration)"
464
510
  rm -f "$STATE_FILE"
465
511
  exit 0
466
512
  fi
@@ -478,6 +524,7 @@ if [[ "$DURATION_SECONDS" =~ ^[0-9]+$ ]] && [[ $DURATION_SECONDS -gt 0 ]]; then
478
524
  emit "⏰ Autonomous mode: Duration expired ($ELAPSED seconds elapsed)."
479
525
  emit " Session is free to exit."
480
526
  notify_terminal_stop "⏰ My autonomous run on \"$(goal_snippet)\" just hit its time limit and stopped. Ask me and I'll pick up where I left off."
527
+ run_end_call "duration-expiry"
481
528
  rm -f "$STATE_FILE"
482
529
  exit 0
483
530
  fi
@@ -492,6 +539,10 @@ fi
492
539
  if [[ -f ".instar/autonomous-emergency-stop" ]]; then
493
540
  emit "🛑 Autonomous mode: Emergency stop detected."
494
541
  notify_terminal_stop "🛑 My autonomous run on \"$(goal_snippet)\" was stopped (emergency stop)."
542
+ # SCOPE_ACCRETION: the emergency-stop file is a run-end surface a session could
543
+ # `touch` itself — so it fires run-end IDENTICALLY (R44): the one-step bypass
544
+ # is thereby exactly as LOUD as the two-step (spec §6).
545
+ run_end_call "emergency-stop"
495
546
  rm -f "$STATE_FILE"
496
547
  # NOTE: the emergency flag is left in place so OTHER topics' hooks also see it
497
548
  # and clear their own per-topic files; stop-all clears the flag when complete.
@@ -576,15 +627,38 @@ if [[ "$CD_ENABLED" == "1" ]] && [[ -n "$CD_JUDGE_TAIL" ]]; then
576
627
  done
577
628
  fi
578
629
 
630
+ # SCOPE_ACCRETION — Layer B evasion-vocabulary scan (advisory, spec §2.7).
631
+ # Scans the SAME already-extracted CD_JUDGE_TAIL window (no second transcript
632
+ # read) for accretion-evasion vocabulary, with a fenced/quoted-region exclusion
633
+ # that is NEW bash logic (the milestone/injection scans above are plain
634
+ # substring matches): text inside ``` fences and `>`-quoted lines is stripped
635
+ # BEFORE matching, so quoting this rule or a doc that names the anti-pattern
636
+ # can never trip the signal. Advisory by construction — false negatives are
637
+ # harmless (the server-side git-truth sweep is the defense, Layer 0).
638
+ CD_SCOPE_SUSPECTED="false"
639
+ if [[ "$CD_ENABLED" == "1" ]] && [[ "$SA_ENABLED" == "1" ]] && [[ -n "$CD_JUDGE_TAIL" ]]; then
640
+ CD_TAIL_SCOPE=$(printf '%s\n' "$CD_JUDGE_TAIL" \
641
+ | awk 'BEGIN{f=0} /^[[:space:]]*```/{f=1-f; next} f==0 && $0 !~ /^[[:space:]]*>/{print}' 2>/dev/null \
642
+ | tr '[:upper:]' '[:lower:]' || echo "")
643
+ for _sa in \
644
+ "documented stretch" "filed for a future session" "out of completion condition" \
645
+ "out of the completion condition" "drafts for later" "deferred as a follow-up spec"; do
646
+ if [[ "$CD_TAIL_SCOPE" == *"$_sa"* ]]; then CD_SCOPE_SUSPECTED="true"; break; fi
647
+ done
648
+ fi
649
+
579
650
  # Assemble the signals JSON the judge payload carries (spec §2b.4 surface 1).
580
651
  # completionConditionMet is set false here and overwritten true only inside the
581
652
  # met-condition branch (the judge there is the completion judge, not this).
582
653
  build_signals_json() {
583
654
  local stop_kind="${1:-}" cond_met="${2:-false}"
584
- local sk=""
655
+ local sk="" sa=""
585
656
  [[ "$stop_kind" == "hard-blocker" ]] && sk=',"stopKind":"hard-blocker"'
586
- printf '{"completionConditionMet":%s,"uncheckedTaskCount":%s,"taskStructure":"%s","milestoneRationalizationDetected":%s,"injectionSuspected":%s%s}' \
587
- "$cond_met" "$CD_UNCHECKED_COUNT" "$CD_TASK_STRUCTURE" "$CD_MILESTONE_DETECTED" "$CD_INJECTION_SUSPECTED" "$sk"
657
+ # SCOPE_ACCRETION — the advisory Layer-B boolean is included ONLY when the
658
+ # feature is on, so a disabled agent's judge prompt stays byte-identical.
659
+ [[ "$SA_ENABLED" == "1" ]] && sa=",\"scopeAccretionSuspected\":${CD_SCOPE_SUSPECTED:-false}"
660
+ printf '{"completionConditionMet":%s,"uncheckedTaskCount":%s,"taskStructure":"%s","milestoneRationalizationDetected":%s,"injectionSuspected":%s%s%s}' \
661
+ "$cond_met" "$CD_UNCHECKED_COUNT" "$CD_TASK_STRUCTURE" "$CD_MILESTONE_DETECTED" "$CD_INJECTION_SUSPECTED" "$sa" "$sk"
588
662
  }
589
663
 
590
664
  # ── COMPLETION_DISCIPLINE — circuit-breaker + verdict cache (in the backoff sidecar) ──
@@ -1203,6 +1277,8 @@ if [[ "$CD_ENABLED" == "1" ]] && [[ -n "$HARD_BLOCKER_NONCE" ]] && [[ "$HARD_BLO
1203
1277
  # (c) ONE plain-English notify-on-stop.
1204
1278
  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."
1205
1279
  emit "🚧 Autonomous mode: hard blocker (external) — exit allowed. needed: ${HB_NEEDED}"
1280
+ # SCOPE_ACCRETION (R40/R44): the hard-blocker exit is a run-end surface.
1281
+ run_end_call "hard-blocker"
1206
1282
  # (d) Remove state; allow exit (write once — state removal prevents re-append).
1207
1283
  rm -f "$STATE_FILE" "$CD_BACKOFF_STATE" 2>/dev/null || true
1208
1284
  exit 0
@@ -1236,6 +1312,13 @@ fi
1236
1312
  EVAL_REASON=""
1237
1313
  if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$COMPLETION_CONDITION" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
1238
1314
  EVAL_MET=""
1315
+ # Test seam (SCOPE_ACCRETION): record the signals + identity fields the judge
1316
+ # payload would carry, WITHOUT a network call (mirrors INSTAR_HOOK_ATTENTION_RECORD).
1317
+ if [[ -n "${INSTAR_HOOK_SIGNALS_RECORD:-}" ]] && [[ "$CD_ENABLED" == "1" ]]; then
1318
+ printf '{"signals":%s,"topicId":"%s","runId":"%s"}\n' \
1319
+ "$(build_signals_json "" "false")" "${REPORT_TOPIC:-}" "${RUN_ID:-}" \
1320
+ >> "$INSTAR_HOOK_SIGNALS_RECORD" 2>/dev/null || true
1321
+ fi
1239
1322
  # Decide whether to fire the judge at all (CD cost-discipline gate).
1240
1323
  CD_MIGHT_BE_DONE="true"
1241
1324
  if [[ "$CD_ENABLED" == "1" ]] && [[ -z "${INSTAR_HOOK_EVAL_OVERRIDE:-}" ]]; then
@@ -1267,12 +1350,17 @@ if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$COMPLETION_CONDITION" ]]
1267
1350
  | tail -c 8000 || echo "")
1268
1351
  EVAL_PORT=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
1269
1352
  EVAL_AUTH=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
1353
+ # SCOPE_ACCRETION — echo topicId/runId/sessionId so the server resolves the
1354
+ # run against its OWN registration record (R35 arming; R36 registered-
1355
+ # condition authority; §6 runId pair check). Empty fields are omitted.
1356
+ CD_IDS=$(jq -nc --arg topic "${REPORT_TOPIC:-}" --arg rid "${RUN_ID:-}" --arg sid "${HOOK_SESSION:-}" \
1357
+ '(if $topic != "" then {topicId:$topic} else {} end) + (if $rid != "" then {runId:$rid} else {} end) + (if $sid != "" then {sessionId:$sid} else {} end)' 2>/dev/null || echo '{}')
1270
1358
  if [[ "$CD_ENABLED" == "1" ]]; then
1271
1359
  # Fold the milestone/buildable-work scrutiny into the completion judge (single call).
1272
1360
  CD_SIG=$(build_signals_json "" "false")
1273
- EVAL_BODY=$(jq -nc --arg c "$COMPLETION_CONDITION" --arg t "$EVAL_TAIL" --argjson sig "$CD_SIG" '{condition:$c,transcriptTail:$t,signals:$sig}')
1361
+ EVAL_BODY=$(jq -nc --arg c "$COMPLETION_CONDITION" --arg t "$EVAL_TAIL" --argjson sig "$CD_SIG" --argjson ids "$CD_IDS" '{condition:$c,transcriptTail:$t,signals:$sig} + $ids')
1274
1362
  else
1275
- EVAL_BODY=$(jq -nc --arg c "$COMPLETION_CONDITION" --arg t "$EVAL_TAIL" '{condition:$c,transcriptTail:$t}')
1363
+ EVAL_BODY=$(jq -nc --arg c "$COMPLETION_CONDITION" --arg t "$EVAL_TAIL" --argjson ids "$CD_IDS" '{condition:$c,transcriptTail:$t} + $ids')
1276
1364
  fi
1277
1365
  EVAL_RESP=$(printf '%s' "$EVAL_BODY" \
1278
1366
  | curl -s -m "$JUDGE_TIMEOUT_S" -H "Authorization: Bearer $EVAL_AUTH" -H 'Content-Type: application/json' \
@@ -1302,6 +1390,7 @@ if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$COMPLETION_CONDITION" ]]
1302
1390
  if realcheck_gate; then
1303
1391
  emit "✅ Autonomous mode: completion condition met (independent evaluator): ${EVAL_REASON}"
1304
1392
  notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — the goal was met."
1393
+ run_end_call "met"
1305
1394
  rm -f "$STATE_FILE" "$CD_BACKOFF_STATE" 2>/dev/null || true
1306
1395
  exit 0
1307
1396
  fi
@@ -1311,6 +1400,7 @@ if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$COMPLETION_CONDITION" ]]
1311
1400
  if realcheck_gate; then
1312
1401
  emit "✅ Autonomous mode: completion condition met (independent evaluator): ${EVAL_REASON}"
1313
1402
  notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — the goal was met."
1403
+ run_end_call "met"
1314
1404
  rm -f "$STATE_FILE"
1315
1405
  exit 0
1316
1406
  fi
@@ -1343,6 +1433,7 @@ if [[ "${CD_BLOCK_TERMINAL:-}" != "true" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[
1343
1433
  emit "✅ Autonomous mode: Completion promise detected — <promise>$COMPLETION_PROMISE</promise>"
1344
1434
  emit " Session is free to exit. Good work!"
1345
1435
  notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — all the work is done."
1436
+ run_end_call "met (promise)"
1346
1437
  rm -f "$STATE_FILE"
1347
1438
  exit 0
1348
1439
  fi
@@ -1507,11 +1598,13 @@ except Exception:
1507
1598
  if [[ ! -f "$STATE_FILE" ]]; then
1508
1599
  rm -f "$BACKOFF_STATE" 2>/dev/null || true
1509
1600
  echo "[autonomous] state file removed during idle backoff — allowing exit" >&2
1601
+ run_end_call "stopped (state file removed during backoff)"
1510
1602
  exit 0
1511
1603
  fi
1512
1604
  if [[ -f ".instar/autonomous-emergency-stop" ]]; then
1513
1605
  emit "🛑 Autonomous mode: Emergency stop detected (during idle backoff)."
1514
1606
  notify_terminal_stop "🛑 My autonomous run on \"$(goal_snippet)\" was stopped (emergency stop)."
1607
+ run_end_call "emergency-stop (during backoff)"
1515
1608
  rm -f "$STATE_FILE" "$BACKOFF_STATE" 2>/dev/null || true
1516
1609
  exit 0
1517
1610
  fi
@@ -1528,6 +1621,7 @@ NEXT_ITERATION=$((ITERATION + 1))
1528
1621
  PROMPT_TEXT=$(awk '/^---$/{i++; next} i>=2' "$STATE_FILE")
1529
1622
  if [[ -z "$PROMPT_TEXT" ]]; then
1530
1623
  echo "⚠️ Autonomous mode: State file has no task content" >&2
1624
+ run_end_call "state-corrupt (no task content)"
1531
1625
  rm -f "$STATE_FILE"
1532
1626
  exit 0
1533
1627
  fi
@@ -18,6 +18,9 @@ COMPLETION_CONDITION="" # verifiable end-state; an independent judge decides "
18
18
  REPORT_INTERVAL="30m"
19
19
  VERIFICATION_COMMAND="" # opt-in real check (ACT-152): the stop-hook RUNS this on a met:true verdict and gates the exit on exit-0.
20
20
  VERIFICATION_CWD="" # directory the verification command runs in (resolves verification_cwd → work_dir → agent home).
21
+ DECLARED_DELIVERABLES="" # SCOPE_ACCRETION: comma-separated repo-relative paths the run is EXPECTED to
22
+ # produce as drafts (the escape for genuinely draft-only missions — operator-
23
+ # confirmed at setup). Recorded server-side at registration; immutable mid-run.
21
24
 
22
25
  while [[ $# -gt 0 ]]; do
23
26
  case $1 in
@@ -61,6 +64,10 @@ while [[ $# -gt 0 ]]; do
61
64
  VERIFICATION_CWD="$2"
62
65
  shift 2
63
66
  ;;
67
+ --declared-deliverables)
68
+ DECLARED_DELIVERABLES="$2"
69
+ shift 2
70
+ ;;
64
71
  --report-interval)
65
72
  REPORT_INTERVAL="$2"
66
73
  shift 2
@@ -186,6 +193,46 @@ if [[ -n "$VERIFICATION_CWD" ]]; then
186
193
  $VERIFICATION_FIELDS"
187
194
  fi
188
195
 
196
+ # ── SCOPE_ACCRETION — server-side run registration (spec: autonomous-scope-
197
+ # accretion-completion.md R30). The SERVER mints the runId, snapshots the
198
+ # scopeAccretion config + the sweep base roots with their start-SHAs, and clamps
199
+ # endAt — so mid-run edits to config/state-file change nothing the completion
200
+ # chokepoint reads. The runId is written into the frontmatter so the stop hook
201
+ # can echo it on every evaluate-completion / run-end call. Best-effort: an
202
+ # unreachable server leaves run_id empty (the gate degrades honestly server-side;
203
+ # the run still starts — registration is a discipline layer, not a start gate).
204
+ RUN_ID=""
205
+ if [[ -n "$REPORT_TOPIC" ]]; then
206
+ REG_PORT=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('port',4040))" 2>/dev/null || echo 4040)
207
+ REG_AUTH=$(python3 -c "import json;print(json.load(open('.instar/config.json')).get('authToken',''))" 2>/dev/null || echo "")
208
+ # Comma-separated → JSON array (empty string → []).
209
+ REG_DECLARED=$(printf '%s' "$DECLARED_DELIVERABLES" | python3 -c "import sys,json
210
+ print(json.dumps([p.strip() for p in sys.stdin.read().split(',') if p.strip()]))" 2>/dev/null || echo '[]')
211
+ REG_END_AT="$END_AT"
212
+ [[ "$REG_END_AT" == "unknown" || "$REG_END_AT" == "unlimited" ]] && REG_END_AT=""
213
+ REG_RESP=$(jq -nc \
214
+ --arg t "$REPORT_TOPIC" --arg c "$COMPLETION_CONDITION" --arg w "$WORK_DIR" \
215
+ --arg s "$STARTED_AT" --arg e "$REG_END_AT" --arg sid "${CLAUDE_CODE_SESSION_ID:-}" \
216
+ --argjson d "$REG_DECLARED" \
217
+ '{topicId:$t,condition:$c,workDir:$w,declaredDeliverables:$d,startedAt:$s}
218
+ + (if $e != "" then {endAt:$e} else {} end)
219
+ + (if $sid != "" then {sessionId:$sid} else {} end)' 2>/dev/null \
220
+ | curl -s -m 8 -H "Authorization: Bearer $REG_AUTH" -H 'Content-Type: application/json' \
221
+ --data-binary @- "http://localhost:${REG_PORT}/autonomous/register" 2>/dev/null || echo "")
222
+ RUN_ID=$(printf '%s' "$REG_RESP" | python3 -c "import sys,json
223
+ try: print(json.load(sys.stdin).get('runId',''))
224
+ except Exception: print('')" 2>/dev/null || echo "")
225
+ if [[ -n "$RUN_ID" ]]; then
226
+ echo " Scope-accretion: run registered server-side (runId $RUN_ID)"
227
+ else
228
+ echo " Scope-accretion: server registration unavailable — accretion gate degrades honestly (run still bounded by duration)"
229
+ fi
230
+ fi
231
+ if [[ -n "$RUN_ID" ]]; then
232
+ VERIFICATION_FIELDS="run_id: \"$RUN_ID\"
233
+ $VERIFICATION_FIELDS"
234
+ fi
235
+
189
236
  cat > "$STATE_PATH" <<EOF
190
237
  ---
191
238
  active: true
@@ -4170,6 +4170,15 @@ process.stdin.on('end', async () => {
4170
4170
  event: input.hook_event || (input.tool_name ? 'PostToolUse' : 'Unknown'),
4171
4171
  session_id: input.session_id || '',
4172
4172
  tool_name: input.tool_name || '',
4173
+ // green-pr-automerge Layer 2: forward the session cwd so the server can
4174
+ // resolve the ending session's branch (without it, Layer 2 ships inert).
4175
+ // (Reconciled with the PostUpdateMigrator copy — keep BOTH in sync.)
4176
+ cwd: input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd() || '',
4177
+ // scope-accretion ADVISORY ledger (spec autonomous-scope-accretion-
4178
+ // completion.md R18): forward the Write/Edit file path for attribution
4179
+ // detail. Optional + designed-benign: the receiver stores extra fields
4180
+ // as-is and a payload without it remains valid.
4181
+ file_path: (input.tool_input && (input.tool_input.file_path || input.tool_input.path)) || '',
4173
4182
  });
4174
4183
 
4175
4184
  const url = new URL(serverUrl + '/hooks/events?instar_sid=' + instarSid);