instar 1.3.29 → 1.3.31

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.
@@ -147,6 +147,32 @@ STARTED_AT=$(fm_get started_at)
147
147
  COMPLETION_PROMISE=$(fm_get completion_promise)
148
148
  COMPLETION_CONDITION=$(fm_get completion_condition)
149
149
  GOAL_MODE=$(fm_get goal_mode) # "native" = the framework's own /goal loop drives completion
150
+ RUN_GOAL=$(fm_get goal)
151
+
152
+ # ── Layer A: notify-on-stop (2026-05-27 silent-stalls postmortem, Task 2) ──────
153
+ # When an autonomous run reaches a TERMINAL exit (completion / duration / emergency),
154
+ # send ONE plain-English Telegram to the run's report topic explaining why it
155
+ # stopped. Closes Justin's "a session either keeps going OR tells me why it
156
+ # stopped" requirement structurally: previously every terminal exit only echoed
157
+ # to stderr (the terminal the user can't see), so an autonomous run could end in
158
+ # silence. Best-effort + NON-BLOCKING — a delivery failure never blocks the exit.
159
+ # Fires at most once per run (the state file is removed right after each terminal
160
+ # exit, so the hook won't re-enter). Reuses telegram-reply.sh (the same transport
161
+ # the restart-resume recovery note uses), and only for the telegram channel.
162
+ goal_snippet() {
163
+ printf '%s' "${RUN_GOAL:-the autonomous run}" | tr '\n\t' ' ' | cut -c1-80
164
+ }
165
+ notify_terminal_stop() {
166
+ local msg="$1"
167
+ [[ -z "$REPORT_TOPIC" ]] && return 0
168
+ [[ "${REPORT_CHANNEL:-telegram}" != "telegram" ]] && return 0
169
+ local script=""
170
+ if [[ -x ".instar/scripts/telegram-reply.sh" ]]; then script=".instar/scripts/telegram-reply.sh"
171
+ elif [[ -x ".claude/scripts/telegram-reply.sh" ]]; then script=".claude/scripts/telegram-reply.sh"
172
+ fi
173
+ [[ -z "$script" ]] && return 0
174
+ printf '%s\n' "$msg" | "$script" "$REPORT_TOPIC" >/dev/null 2>&1 || true
175
+ }
150
176
 
151
177
  # Validate recorded session_id is a real UUID. Claude sometimes writes a custom
152
178
  # string instead of $CLAUDE_CODE_SESSION_ID; non-UUID values are treated as
@@ -245,12 +271,14 @@ if [[ "$GOAL_MODE" == "native" ]]; then
245
271
  --data-binary @- "http://localhost:${port}/autonomous/native-goal/clear" >/dev/null 2>&1 || true
246
272
  }
247
273
  if [[ -f ".instar/autonomous-emergency-stop" ]]; then
274
+ notify_terminal_stop "🛑 My autonomous run on \"$(goal_snippet)\" was stopped (emergency stop)."
248
275
  native_goal_clear; rm -f "$STATE_FILE"
249
276
  echo "[autonomous] emergency stop — native /goal cleared" >&2; exit 0
250
277
  fi
251
278
  if [[ "$DURATION_SECONDS" =~ ^[0-9]+$ ]] && [[ $DURATION_SECONDS -gt 0 ]]; then
252
279
  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)
253
280
  if [[ "$NG_START" =~ ^[0-9]+$ ]] && [[ $NG_START -gt 0 ]] && [[ $(( $(date +%s) - NG_START )) -ge $DURATION_SECONDS ]]; then
281
+ 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."
254
282
  native_goal_clear; rm -f "$STATE_FILE"
255
283
  echo "[autonomous] duration expired — native /goal cleared" >&2; exit 0
256
284
  fi
@@ -280,6 +308,7 @@ if [[ "$DURATION_SECONDS" =~ ^[0-9]+$ ]] && [[ $DURATION_SECONDS -gt 0 ]]; then
280
308
  if [[ $ELAPSED -ge $DURATION_SECONDS ]]; then
281
309
  echo "⏰ Autonomous mode: Duration expired ($ELAPSED seconds elapsed)."
282
310
  echo " Session is free to exit."
311
+ 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."
283
312
  rm -f "$STATE_FILE"
284
313
  exit 0
285
314
  fi
@@ -293,6 +322,7 @@ fi
293
322
  # Emergency stop (global — halts every autonomous job on its next fire)
294
323
  if [[ -f ".instar/autonomous-emergency-stop" ]]; then
295
324
  echo "🛑 Autonomous mode: Emergency stop detected."
325
+ notify_terminal_stop "🛑 My autonomous run on \"$(goal_snippet)\" was stopped (emergency stop)."
296
326
  rm -f "$STATE_FILE"
297
327
  # NOTE: the emergency flag is left in place so OTHER topics' hooks also see it
298
328
  # and clear their own per-topic files; stop-all clears the flag when complete.
@@ -323,6 +353,7 @@ if [[ -n "$COMPLETION_CONDITION" ]] && [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TR
323
353
  fi
324
354
  if [[ "$EVAL_MET" == "true" ]]; then
325
355
  echo "✅ Autonomous mode: completion condition met (independent evaluator): ${EVAL_REASON}"
356
+ notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — the goal was met."
326
357
  rm -f "$STATE_FILE"
327
358
  exit 0
328
359
  fi
@@ -341,6 +372,7 @@ if [[ -n "$TRANSCRIPT_PATH" ]] && [[ -f "$TRANSCRIPT_PATH" ]]; then
341
372
  if [[ -n "$PROMISE_TEXT" ]] && [[ "$PROMISE_TEXT" = "$COMPLETION_PROMISE" ]]; then
342
373
  echo "✅ Autonomous mode: Completion promise detected — <promise>$COMPLETION_PROMISE</promise>"
343
374
  echo " Session is free to exit. Good work!"
375
+ notify_terminal_stop "✅ My autonomous run on \"$(goal_snippet)\" finished — all the work is done."
344
376
  rm -f "$STATE_FILE"
345
377
  exit 0
346
378
  fi
@@ -777,6 +777,50 @@
777
777
  display: none !important;
778
778
  }
779
779
 
780
+ /* ── process-health tab (PROCESS-HEALTH-DASHBOARD-TAB-SPEC §4.1) ──
781
+ Large, readable, calm. No monospace, no tables, no alarm colors.
782
+ Measurable bars: headline ≥24px, section headers ≥20px, body ≥17px,
783
+ line-height 1.5–1.6. One neutral accent token, used ONLY for the
784
+ maturation "you're here" marker. */
785
+ .ph-root {
786
+ --ph-accent: #4ade80;
787
+ max-width: 760px;
788
+ font-size: 18px;
789
+ line-height: 1.55;
790
+ color: var(--text-bright);
791
+ }
792
+ .ph-title { font-size: 22px; font-weight: 600; color: var(--text-bright); line-height: 1.5; }
793
+ .ph-stamp { font-size: 14px; color: var(--text-dim); }
794
+ .ph-headline-wrap { margin: 4px 0 8px; }
795
+ .ph-headline { font-size: 26px; font-weight: 600; line-height: 1.5; color: var(--text-bright); }
796
+ .ph-subline { font-size: 18px; color: var(--text-dim); margin-top: 8px; line-height: 1.55; }
797
+ .ph-section { display: flex; flex-direction: column; gap: 10px; }
798
+ .ph-h { font-size: 20px; font-weight: 600; color: var(--text-bright); line-height: 1.5; }
799
+ .ph-empty { font-size: 18px; color: var(--text-dim); line-height: 1.6; }
800
+ .ph-card {
801
+ padding: 14px 16px; border: 1px solid var(--border); border-radius: 10px;
802
+ display: flex; flex-direction: column; gap: 6px;
803
+ }
804
+ .ph-card-summary { font-size: 18px; color: var(--text-bright); line-height: 1.5; }
805
+ .ph-card-frame { font-size: 15px; color: var(--text-dim); font-style: italic; }
806
+ .ph-card-rec { font-size: 17px; color: var(--text); line-height: 1.55; }
807
+ .ph-card-evidence { font-size: 15px; color: var(--text-dim); }
808
+ .ph-record { font-size: 18px; color: var(--text); line-height: 1.6; padding: 5px 0; }
809
+ .ph-record-inert { color: var(--text-dim); }
810
+ .ph-stage { display: flex; align-items: baseline; gap: 10px; font-size: 18px; padding: 3px 0; color: var(--text-dim); line-height: 1.5; }
811
+ .ph-stage-here { color: var(--text-bright); }
812
+ .ph-stage-glyph { color: var(--text-dim); }
813
+ .ph-stage-here .ph-stage-glyph { color: var(--ph-accent); }
814
+ .ph-stage-marker { color: var(--ph-accent); font-size: 15px; }
815
+ .ph-stage-note { color: var(--text-dim); font-size: 15px; }
816
+ .ph-detail { margin-top: 4px; }
817
+ .ph-detail-summary { font-size: 17px; color: var(--text-dim); cursor: pointer; }
818
+ .ph-detail-body { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
819
+ .ph-detail-row { font-size: 17px; color: var(--text); line-height: 1.55; }
820
+ .ph-operator { margin-top: 14px; }
821
+ .ph-operator-summary { font-size: 15px; color: var(--text-dim); cursor: pointer; }
822
+ .ph-operator-hint { font-size: 16px; color: var(--text-dim); margin-top: 8px; line-height: 1.55; }
823
+
780
824
  /* ── File viewer ─────────────────────────────────────── */
781
825
  .files-container {
782
826
  display: flex;
@@ -2529,6 +2573,7 @@
2529
2573
  <button class="tab" data-tab="tokens" onclick="switchTab('tokens')">Tokens</button>
2530
2574
  <button class="tab" data-tab="threadline" onclick="switchTab('threadline')">Threadline</button>
2531
2575
  <button class="tab" data-tab="evidence" onclick="switchTab('evidence')">Evidence</button>
2576
+ <button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
2532
2577
  </nav>
2533
2578
  </div>
2534
2579
  <div class="vital-signs" id="vitalSigns">
@@ -3012,6 +3057,31 @@
3012
3057
 
3013
3058
  <!-- Evidence Tab (WikiClaim Phase 4) — per-entity evidence + reverse "what cites this?" view.
3014
3059
  Spec: docs/specs/OPENCLAW-IMPORT-WIKICLAIM-EVIDENCE-SPEC.md § Phase 4 (line 343). -->
3060
+ <!-- process-health tab -->
3061
+ <div id="processHealthPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3062
+ <div style="display:flex;justify-content:space-between;align-items:baseline">
3063
+ <h2 class="ph-title">Process Health</h2>
3064
+ <div id="phStamp" class="ph-stamp" aria-live="polite"></div>
3065
+ </div>
3066
+ <div id="phHeadline" class="ph-headline-wrap" aria-live="polite"></div>
3067
+ <section class="ph-section">
3068
+ <h3 class="ph-h">Patterns to know about</h3>
3069
+ <div id="phPatterns"></div>
3070
+ </section>
3071
+ <section class="ph-section">
3072
+ <h3 class="ph-h">What’s been captured</h3>
3073
+ <div id="phCaptured"></div>
3074
+ </section>
3075
+ <section class="ph-section">
3076
+ <h3 class="ph-h">Maturation</h3>
3077
+ <div id="phMaturation"></div>
3078
+ </section>
3079
+ <details class="ph-detail">
3080
+ <summary class="ph-detail-summary">Detail</summary>
3081
+ <div id="phDetail" class="ph-detail-body"></div>
3082
+ </details>
3083
+ </div>
3084
+
3015
3085
  <div id="evidenceTab" style="display:none;flex-direction:column;padding:20px;gap:16px;overflow-y:auto">
3016
3086
  <div style="display:flex;justify-content:space-between;align-items:center">
3017
3087
  <h2 style="margin:0">Evidence</h2>
@@ -4244,6 +4314,13 @@
4244
4314
  display: ['flex'],
4245
4315
  onActivate: null,
4246
4316
  },
4317
+ {
4318
+ id: 'process-health',
4319
+ panels: ['processHealthPanel'],
4320
+ display: ['flex'],
4321
+ onActivate: () => { if (typeof startProcessHealth === 'function') startProcessHealth(); },
4322
+ onDeactivate: () => { if (typeof stopProcessHealth === 'function') stopProcessHealth(); },
4323
+ },
4247
4324
  {
4248
4325
  id: 'secrets',
4249
4326
  panels: ['secretsPanel'],
@@ -7973,13 +8050,48 @@
7973
8050
  return m + 'm';
7974
8051
  }
7975
8052
 
8053
+ // ── Process Health tab (PROCESS-HEALTH-DASHBOARD-TAB-SPEC) ──
8054
+ // Lazy dynamic-import keeps the controller in this (classic) scope so it can
8055
+ // read `token`; the module ships the pure renderers + the polling controller.
8056
+ let __phController = null;
8057
+ let __phModule = null;
8058
+ async function startProcessHealth() {
8059
+ if (!__phModule) {
8060
+ try { __phModule = await import('/dashboard/process-health.js'); }
8061
+ catch (e) { console.error('[process-health] module load failed', e); return; }
8062
+ }
8063
+ if (!__phController) {
8064
+ const els = {
8065
+ headline: document.getElementById('phHeadline'),
8066
+ patterns: document.getElementById('phPatterns'),
8067
+ captured: document.getElementById('phCaptured'),
8068
+ maturation: document.getElementById('phMaturation'),
8069
+ detail: document.getElementById('phDetail'),
8070
+ stamp: document.getElementById('phStamp'),
8071
+ };
8072
+ // Raw fetch (NOT apiFetch) so the controller sees 304/503/ETag itself.
8073
+ const fetchImpl = (url, opts = {}) => fetch(url, {
8074
+ headers: { 'Authorization': `Bearer ${token}`, ...(opts.headers || {}) },
8075
+ signal: opts.signal,
8076
+ });
8077
+ __phController = __phModule.createController({ doc: document, els, fetchImpl });
8078
+ }
8079
+ __phController.start();
8080
+ }
8081
+ function stopProcessHealth() {
8082
+ if (__phController) __phController.stop();
8083
+ }
8084
+
7976
8085
  // Handle tab visibility — close SSE when tab is backgrounded
7977
8086
  document.addEventListener('visibilitychange', () => {
7978
8087
  if (document.hidden) {
7979
8088
  disconnectJobsSSE();
8089
+ if (currentTab === 'process-health') stopProcessHealth();
7980
8090
  } else if (currentTab === 'jobs') {
7981
8091
  connectJobsSSE();
7982
8092
  loadJobs(); // Resync on return
8093
+ } else if (currentTab === 'process-health') {
8094
+ startProcessHealth(); // resume on return: one immediate refresh + re-arm
7983
8095
  }
7984
8096
  // Always refresh vital signs on tab focus
7985
8097
  if (!document.hidden) pollVitalSigns();