instar 1.3.201 → 1.3.202

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 (78) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +25 -10
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/FeatureRegistry.d.ts.map +1 -1
  5. package/dist/core/FeatureRegistry.js +6 -0
  6. package/dist/core/FeatureRegistry.js.map +1 -1
  7. package/dist/core/LeaseCoordinator.d.ts +40 -0
  8. package/dist/core/LeaseCoordinator.d.ts.map +1 -1
  9. package/dist/core/LeaseCoordinator.js +54 -0
  10. package/dist/core/LeaseCoordinator.js.map +1 -1
  11. package/dist/core/LocalLeaseStore.d.ts +11 -0
  12. package/dist/core/LocalLeaseStore.d.ts.map +1 -1
  13. package/dist/core/LocalLeaseStore.js +18 -0
  14. package/dist/core/LocalLeaseStore.js.map +1 -1
  15. package/dist/core/MultiMachineCoordinator.d.ts +35 -0
  16. package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
  17. package/dist/core/MultiMachineCoordinator.js +94 -0
  18. package/dist/core/MultiMachineCoordinator.js.map +1 -1
  19. package/dist/core/SqliteRegistry.d.ts +59 -0
  20. package/dist/core/SqliteRegistry.d.ts.map +1 -0
  21. package/dist/core/SqliteRegistry.js +97 -0
  22. package/dist/core/SqliteRegistry.js.map +1 -0
  23. package/dist/core/StopGateDb.d.ts +2 -0
  24. package/dist/core/StopGateDb.d.ts.map +1 -1
  25. package/dist/core/StopGateDb.js +22 -0
  26. package/dist/core/StopGateDb.js.map +1 -1
  27. package/dist/memory/SemanticMemory.d.ts.map +1 -1
  28. package/dist/memory/SemanticMemory.js +8 -0
  29. package/dist/memory/SemanticMemory.js.map +1 -1
  30. package/dist/memory/TopicMemory.d.ts.map +1 -1
  31. package/dist/memory/TopicMemory.js +7 -0
  32. package/dist/memory/TopicMemory.js.map +1 -1
  33. package/dist/messaging/MessageProcessingLedger.d.ts.map +1 -1
  34. package/dist/messaging/MessageProcessingLedger.js +7 -0
  35. package/dist/messaging/MessageProcessingLedger.js.map +1 -1
  36. package/dist/messaging/imessage/NativeBackend.d.ts.map +1 -1
  37. package/dist/messaging/imessage/NativeBackend.js +6 -0
  38. package/dist/messaging/imessage/NativeBackend.js.map +1 -1
  39. package/dist/messaging/pending-relay-store.d.ts.map +1 -1
  40. package/dist/messaging/pending-relay-store.js +6 -0
  41. package/dist/messaging/pending-relay-store.js.map +1 -1
  42. package/dist/monitoring/CorrectionLedger.d.ts.map +1 -1
  43. package/dist/monitoring/CorrectionLedger.js +6 -0
  44. package/dist/monitoring/CorrectionLedger.js.map +1 -1
  45. package/dist/monitoring/FailureLedger.d.ts.map +1 -1
  46. package/dist/monitoring/FailureLedger.js +6 -0
  47. package/dist/monitoring/FailureLedger.js.map +1 -1
  48. package/dist/monitoring/FeatureMetricsLedger.d.ts.map +1 -1
  49. package/dist/monitoring/FeatureMetricsLedger.js +6 -0
  50. package/dist/monitoring/FeatureMetricsLedger.js.map +1 -1
  51. package/dist/monitoring/FrameworkIssueLedger.d.ts.map +1 -1
  52. package/dist/monitoring/FrameworkIssueLedger.js +6 -0
  53. package/dist/monitoring/FrameworkIssueLedger.js.map +1 -1
  54. package/dist/monitoring/RevertDetector.d.ts.map +1 -1
  55. package/dist/monitoring/RevertDetector.js +7 -1
  56. package/dist/monitoring/RevertDetector.js.map +1 -1
  57. package/dist/monitoring/TokenLedger.d.ts.map +1 -1
  58. package/dist/monitoring/TokenLedger.js +8 -0
  59. package/dist/monitoring/TokenLedger.js.map +1 -1
  60. package/dist/providers/uxConfirm/PreferenceStore.d.ts +1 -0
  61. package/dist/providers/uxConfirm/PreferenceStore.d.ts.map +1 -1
  62. package/dist/providers/uxConfirm/PreferenceStore.js +13 -0
  63. package/dist/providers/uxConfirm/PreferenceStore.js.map +1 -1
  64. package/dist/threadline/SpawnLedger.d.ts +1 -0
  65. package/dist/threadline/SpawnLedger.d.ts.map +1 -1
  66. package/dist/threadline/SpawnLedger.js +13 -0
  67. package/dist/threadline/SpawnLedger.js.map +1 -1
  68. package/dist/threadline/relay/RegistryStore.d.ts.map +1 -1
  69. package/dist/threadline/relay/RegistryStore.js +6 -0
  70. package/dist/threadline/relay/RegistryStore.js.map +1 -1
  71. package/package.json +1 -1
  72. package/src/data/builtin-manifest.json +5 -5
  73. package/src/templates/scripts/convergence-check.sh +49 -0
  74. package/upgrades/1.3.202.md +65 -0
  75. package/upgrades/side-effects/evo003-revertdetector-git-arg.md +106 -0
  76. package/upgrades/side-effects/git-less-lease-convergence.md +31 -0
  77. package/upgrades/side-effects/sqlite-close-on-exit-registry.md +27 -0
  78. package/upgrades/side-effects/time-awareness-component4-reporting-nudge.md +20 -0
@@ -0,0 +1,106 @@
1
+ # Side-Effects Review — RevertDetector invalid git arg (EVO-003)
2
+
3
+ **Version / slug:** `evo003-revertdetector-git-arg`
4
+ **Date:** `2026-06-02`
5
+ **Author:** `Echo (Instar Agent)`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ `src/monitoring/RevertDetector.ts` builds a `git log` argv to find `Revert "..."` commits. That argv carried `--regexp-ignore-case=false`, which git rejects (`fatal: unrecognized argument` — git's `--grep` case flags take no `=value` form). The call therefore threw on every tick, was swallowed by the existing try/catch, and `scanForReverts` returned 0 — making revert detection a silent no-op on every install. The change deletes that one element (case-sensitive is git's `--grep` default, which is what we want) and adds a NOTE comment so it can't be reintroduced. `tests/unit/RevertDetector.test.ts` gains two regression tests: an argv-shape assertion (injected arg-recording git) and a real-git end-to-end test (real temp repo + real `git revert`). Files touched: `src/monitoring/RevertDetector.ts`, `tests/unit/RevertDetector.test.ts`.
11
+
12
+ ## Decision-point inventory
13
+
14
+ This change touches no block/allow decision point. RevertDetector is an observer that opens forensic FailureLedger records; it never gates a message, job, or session.
15
+
16
+ - `RevertDetector.scanForReverts git argv` — modify — drop the invalid `--regexp-ignore-case=false` element so the scan command is valid git.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ **What legitimate inputs does this change reject that it shouldn't?**
23
+
24
+ No block/allow surface — over-block not applicable. The only behavioral change is that the scan command now executes successfully instead of throwing. Match semantics are unchanged in intent: `--grep=^Revert` already matched case-sensitively (git's default), which is exactly what the broken `=false` flag was *trying* to express. So no commit that should have matched is now excluded, and none that shouldn't is now included.
25
+
26
+ ---
27
+
28
+ ## 2. Under-block
29
+
30
+ **What failure modes does this still miss?**
31
+
32
+ No block/allow surface — under-block not applicable. Scope note: detection keys on the conventional `Revert "<subject>"` message that `git revert` produces by default. A hand-crafted revert with a non-standard subject still won't be detected — but that is pre-existing behavior, unchanged by this fix and out of scope for EVO-003.
33
+
34
+ ---
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ **Is this at the right layer?**
39
+
40
+ Yes. RevertDetector is correctly a low-level detector that emits forensic records into the FailureLedger (a signal), not an authority. This change does not move it across that line — it only repairs the git invocation so the detector can run at all. No higher-level gate needs to consume this differently; the FailureLedger remains the consumer.
41
+
42
+ ---
43
+
44
+ ## 4. Signal vs authority compliance
45
+
46
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
47
+
48
+ **Does this change hold blocking authority with brittle logic?**
49
+
50
+ - [x] No — this change has no block/allow surface.
51
+
52
+ RevertDetector produces forensic FailureLedger entries (a signal). It holds no blocking authority. The fix is a pure correction of a malformed external-command argument; it neither adds nor removes any decision authority.
53
+
54
+ ---
55
+
56
+ ## 5. Interactions
57
+
58
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
59
+
60
+ - **Shadowing:** None. The scan runs on RevertDetector's own tick and writes only to the FailureLedger. Before the fix it always threw → returned 0; after, it returns the real count. Nothing was relying on the always-throws behavior.
61
+ - **Double-fire:** The forensic entry is opened via `resolveByCommit`/ledger logic that already de-dupes by commit. Turning detection on does not change that path; it just lets it receive real input for the first time. A given revert commit yields one record.
62
+ - **Races:** The scan is lease-gated (`isLeaseHolder()` guard, unchanged), so only the awake machine scans. No new shared state is introduced.
63
+ - **Feedback loops:** RevertDetector feeds the FailureLedger, which the failure-learning loop reads. This change makes that input non-empty for revert events for the first time. The volume is bounded by the scan window (recent commits only) and de-duped per commit, so no runaway loop is created.
64
+
65
+ ---
66
+
67
+ ## 6. External surfaces
68
+
69
+ **Does this change anything visible outside the immediate code path?**
70
+
71
+ - Other agents on the same machine? No.
72
+ - Other users of the install base? Only positively: revert detection begins working fleet-wide on update — previously-silent reverts will now produce forensic FailureLedger records. No user-facing message or topic is generated by RevertDetector itself.
73
+ - External systems? No. The change *reduces* the git command to a valid form; it issues no new external calls.
74
+ - Persistent state? FailureLedger will, for the first time, gain `source: 'revert'` forensic entries on installs that have revert commits in their scan window. These are forensic (resolved) records, not open failures — they do not trigger escalation.
75
+ - Timing/runtime: No new timers. Same tick cadence, same lease gate.
76
+
77
+ ---
78
+
79
+ ## 7. Rollback cost
80
+
81
+ **If this turns out wrong in production, what's the back-out?**
82
+
83
+ Pure code change — revert the commit and ship a patch. No data migration. The only persistent residue is new `source: 'revert'` forensic entries in the FailureLedger; these are benign resolved records and need no cleanup on rollback. No agent state repair, no user-visible regression during the rollback window.
84
+
85
+ ---
86
+
87
+ ## Conclusion
88
+
89
+ This review found no block/allow surface and no risky interaction. The change is a one-line deletion of an invalid git argument that has silently disabled revert detection on every install, plus two regression tests (argv-shape + real-git end-to-end) that pin the fix. The net effect is that an existing, already-wired observer starts producing the forensic records it was always meant to produce. Clear to ship as a Tier-1 change.
90
+
91
+ ---
92
+
93
+ ## Second-pass review (if required)
94
+
95
+ **Reviewer:** not required (Tier-1)
96
+ **Independent read of the artifact: concur**
97
+
98
+ ---
99
+
100
+ ## Evidence pointers
101
+
102
+ - `tests/unit/RevertDetector.test.ts` — 11/11 pass, including the two EVO-003 regression tests (argv-shape + real-git end-to-end).
103
+ - `npx tsc --noEmit` — clean.
104
+ - `pnpm lint` — clean (includes lint-no-direct-destructive: the real-git test uses `SafeGitExecutor`/`SafeFsExecutor`, not raw exec/rm).
105
+ - `pnpm build` — clean (dist compiles).
106
+ - Bug confirmation: `JKHeadley/main:src/monitoring/RevertDetector.ts:143` carries `--regexp-ignore-case=false`; git 2.39.5 rejects it with `fatal: unrecognized argument`.
@@ -0,0 +1,31 @@
1
+ # Side-effects review — git-less same-epoch lease convergence (#680 Problem A v3)
2
+
3
+ **Spec:** `docs/specs/MULTI-MACHINE-LEASE-ROBUSTNESS-SPEC.md` §Problem A (converged 3 rounds, approved). The design failed verification twice (R1 zombie-holder, R2 headless-loser) before the v3 mechanism — this is why it shipped spec-first.
4
+
5
+ **Change:** resolve a git-less `LocalLeaseStore` same-epoch leapfrog split-brain to a single holder. When the active pull (#668) observes a peer holding the SAME epoch as our self-issued lease, a deterministic tie-break (lower `machineId` wins) drives a one-shot resolution: the LOSER relinquishes, the WINNER advances once to N+1, the loser adopts N+1 via the existing strict-`>` tunnel fold.
6
+
7
+ ## What it touches (3 files, all on the lease path)
8
+ - **`LocalLeaseStore.forceLocalExpiry()`** (NEW) — expires the persisted self-lease in place, KEEPING the committed epoch as the CAS/replay floor (clearing to 0 would let a replayed stale lease win). No-op if no local lease.
9
+ - **`LeaseCoordinator`** — NEW `relinquish()` (clears `selfIssued` + calls `store.forceLocalExpiry?.()`), NEW `advanceEpochForContestedWin()` (one-time `buildAcquisition`→N+1 via the same casWrite/broadcast/sign path as a normal acquire; bypasses `canAcquire` deliberately — it IS the tie-resolution), + `forceLocalExpiry?()` added as an OPTIONAL method on the `LeaseStore` interface (GitLeaseStore does not implement it → no-op there).
10
+ - **`MultiMachineCoordinator`** — NEW `contestedEpisode` state field + NEW `resolveContestedSplitBrain()` called from `tickLeasePull` after the existing detect-only `surfacePullDiscoveredSplitBrain()`. Tie-break → relinquish (loser) / advance (winner), latched one-shot per episode, K-cycle bounded escalation.
11
+
12
+ ## Side effects & blast radius
13
+ - **Behavior change is confined to a genuine same-epoch contested tie** (two machines both holding epoch N over a git-less store — the post-teardown split-brain). The clean single-holder path, the partition path (checkForUnresolvableSplit), and the git-substrate path (GitLeaseStore) are UNTOUCHED. `forceLocalExpiry` is optional on the interface, so only the git-less store gains the behavior.
14
+ - **The tie-break is deterministic + total-ordered** (lower machineId wins), so all machines compute the SAME winner with no coordination — exactly one relinquishes, exactly one advances. Transitive, so it generalizes to 3+ machines (tested-direction noted in the spec).
15
+ - **One-shot latch per episode** (keyed on the epoch-independent {self,peer} pair) means the relinquish/advance fire ONCE, not every ~5s pull tick — a per-tick action would re-introduce the leapfrog this fixes. The episode clears on the falling edge (tie resolved).
16
+ - **Winner-advance bypasses `canAcquire`** (direct `buildAcquisition`+casWrite). This is intentional: the winner must reach N+1 even while the loser's relinquished-but-floor-intact lease is observed. casWrite still enforces strict epoch advance (N+1 > committed N), so it cannot lower the epoch or forge.
17
+ - **Relinquish → standby → read-only** is the path implicated in the #668 crash incident. #673 (on main) made the standby read-only write non-fatal; this change relies on that and on Problem B (closeAllSqlite) for a clean exit. Per the spec sequencing, B lands first.
18
+ - **Adoption is via the EXISTING strict-`>` fold** — no new adoption write; the loser's `effectiveView()` folds the winner's N+1 (N+1 > N) and `currentHolder()` returns the winner. Verified empirically (the headless-loser guard).
19
+ - **Escalation is observe-only**: a persistently-contested episode emits ONE deduped `splitBrainEscalation` event with a deterministic `demote <loser>` recommendation. It does not auto-act; a consumer routes it to Attention. Distinct from the partition escalation.
20
+ - **No migration** — pure in-process lease logic; no config/route/schema change; reaches existing agents on normal update.
21
+
22
+ ## What could go wrong (and why it won't)
23
+ - **Both advance (leapfrog)?** No — the tie-break gates advance to the winner only; the loser relinquishes. Both latched per-episode.
24
+ - **Loser headless (R2's catch)?** No — the winner advancing to N+1 makes the loser's strict-`>` fold adopt it; `currentHolder()===winner`. Tested.
25
+ - **Stale-lease replay after relinquish?** No — `forceLocalExpiry` keeps the epoch floor; a below-floor lease is rejected by CAS/acceptTunnelLease.
26
+ - **Per-tick churn?** No — the one-shot latch.
27
+
28
+ ## Tests (all green; 9 new + 68 existing lease/MMC regressions)
29
+ - Unit `tests/unit/LeaseCoordinator-convergence.test.ts` (6) — the EMPIRICAL convergence proof: two coordinators both at epoch N converge to a single holder at N+1, with the headless-loser guard (loser's `currentHolder()===winner`), order-independence, winner-advance-alone-adopts, relinquish/forceLocalExpiry primitives + epoch-floor retention.
30
+ - Integration `tests/integration/lease-contested-resolution.test.ts` (3) — the orchestration on the real pull loop: WINNER advances once + latch (epoch stops climbing), LOSER relinquishes (standby), bounded K-cycle escalation (one deduped `demote <loser>`).
31
+ - 68 existing lease/MMC tests unchanged-green; `npm run lint` + `tsc --noEmit` clean.
@@ -0,0 +1,27 @@
1
+ # Side-effects review — SQLite close-on-exit registry (#680 Problem B)
2
+
3
+ **Spec:** `docs/specs/MULTI-MACHINE-LEASE-ROBUSTNESS-SPEC.md` §Problem B (converged 3 rounds, approved).
4
+ **Change:** a central `SqliteRegistry` (`registerSqliteHandle` / `closeAllSqlite` / `__resetSqliteRegistryForTests`); all 15 long-lived better-sqlite3 stores register their handle after open; `server.ts` closes every registered handle LAST on both exit paths (graceful shutdown + uncaughtException), replacing the hand-maintained 2-store close-list.
5
+
6
+ ## What it touches
7
+ - **New module** `src/core/SqliteRegistry.ts` — pure, process-global singleton. No I/O, no deps.
8
+ - **15 store constructors** gain one `registerSqliteHandle(() => this.db?.close())` call after the db is open (StopGateDb, FeatureRegistry, PreferenceStore, SpawnLedger, RegistryStore, TokenLedger, CorrectionLedger, FailureLedger, FeatureMetricsLedger, FrameworkIssueLedger, TopicMemory, SemanticMemory, MessageProcessingLedger, pending-relay-store, iMessage NativeBackend). StopGateDb + PreferenceStore + SpawnLedger also unregister + (StopGateDb) gained a `_closed` idempotency guard.
9
+ - **`src/commands/server.ts`** — `shutdown()` gained a re-entrancy guard and now calls `closeAllSqlite()` after `server.stop()` (was: explicit `topicMemory?.close()` + `semanticMemory?.close()` before `server.stop()`); the `uncaughtException` handler calls `closeAllSqlite()`.
10
+
11
+ ## Side effects & blast radius
12
+ - **Behavior change at exit only.** No runtime/request-path behavior changes — registration is a no-op at steady state; the only new work happens during process teardown.
13
+ - **Ordering moved:** SQLite now closes AFTER `server.stop()` (writers stopped first) instead of before. This is intentional and safer — no route can touch a closed handle because the HTTP server is already stopped. The sidecar flush (`sharedStateLedger.shutdown()`) still runs before the close so no write is lost.
14
+ - **Double-close safety:** better-sqlite3 `.close()` throws on an already-closed handle. The registry guards this three ways — unregister-before-explicit-close, an at-most-once closed-set, and a per-handle try/catch in `closeAllSqlite()` — so a store explicitly closed at runtime and then drained at exit cannot crash teardown.
15
+ - **Reopen safety:** TopicMemory + SemanticMemory reopen their handle on corruption recovery. The registered closeFn reads `this.db` live (not a captured ref), so the current handle is always the one closed.
16
+ - **Registration leak risk:** registration is fire-and-forget for most stores. These 15 are process-lifetime singletons (registered once), so the registry does not grow unboundedly. A store instantiated many times would accumulate entries — not the case for any of the 15.
17
+ - **Test isolation:** `__resetSqliteRegistryForTests()` clears BOTH the handle list and the closed-set (a half-reset would leak an "already closed" verdict between tests).
18
+
19
+ ## What could go wrong (and why it won't break prod)
20
+ - If a store's closeFn threw, the old code would abort teardown — now each is wrapped best-effort, so one bad close cannot block the others (the whole point).
21
+ - The re-entrancy guard on `shutdown()` means a SIGINT+SIGTERM race (or restartDetected racing a signal) runs the resume-UUID save + flush once, not twice.
22
+ - No migration needed — ships in code, reaches existing agents on normal update. No config/route/schema change.
23
+
24
+ ## Tests (3 tiers, all green)
25
+ - Unit `tests/unit/SqliteRegistry.test.ts` (6) — close-all, throwing-close-isolation, unregister, at-most-once, reset-clears-both.
26
+ - Wiring `tests/unit/SqliteRegistry-wiring.test.ts` (5) — static allowlist completeness (every long-lived store registers; every open-callsite accounted) + functional registration-fires.
27
+ - Lifecycle `tests/integration/sqlite-close-on-exit.test.ts` (7) — real better-sqlite3 handles actually close (`.open === false`); StopGateDb closed without throw; double-close safe; server.ts ordering (closeAllSqlite after server.stop; uncaughtException closes all; old 2-store list gone).
@@ -0,0 +1,20 @@
1
+ # Side-effects review — Time Awareness Component 4 (signal-only reporting nudge)
2
+
3
+ **Spec:** `docs/specs/ROBUST-SESSION-TIME-AWARENESS-SPEC.md` §Component 4 (converged + approved, merged as #681; Component 4 was deferred to v1.1, tracked #682 — now built). **Tracks:** #682.
4
+
5
+ **Change:** one new check (signal-only) appended to the pre-messaging `convergence-check.sh` gate (`src/templates/scripts/convergence-check.sh`). If an outbound message asserts the **session/run is done/over** (or "winding down" / "wrapping up the session/run") while a **live autonomous record** (`.instar/autonomous/*.local.md`, `active: true`) still has **>10% of its time-box remaining**, it emits a one-line SIGNAL to `logs/time-awareness-signals.jsonl` + stderr. This guards the exact wind-down-early incident class that started the time-tracking work.
6
+
7
+ ## What it touches
8
+ - `src/templates/scripts/convergence-check.sh` ONLY (the pre-messaging quality gate, run by `grounding-before-messaging.sh`). One appended block + nothing else changed.
9
+ - Reaches existing agents automatically: convergence-check.sh is **always-overwritten on migration** (`PostUpdateMigrator` line ~4122, "Convergence check — always overwrite") + installed by `init`. No new migration code needed. New agents get it via the same install path.
10
+
11
+ ## Side effects & blast radius (deliberately minimal — P2 Signal vs Authority)
12
+ - **SIGNAL-ONLY: it NEVER blocks or rewrites the message.** The block does NOT touch `ISSUE_COUNT` or the exit code — the existing checks 1-8 and the gate's exit logic are untouched. A premature-completion assertion still SENDS; the operator just gets a side-channel signal. (Verified: exit 0 in all test scenarios, including when the signal fires.)
13
+ - **Never quotes the agent's phrase.** The signal carries the computed fact (`≈NN% of the time-box remains`) only — never the "done/over" wording — so it can't be re-read as self-confirming evidence the run is finished (the adversarial-round-2 sink requirement).
14
+ - **Signal sink = operator log + stderr, never the agent's injected context.** On exit 0 the grounding hook does not surface convergence-check output to the agent, so the signal does not pollute context. The JSONL log is the durable operator record.
15
+ - **Targeted regex** (session/run/sprint-level completion + winding-down/wrapping-up-the-session) — chosen to avoid false-positives on subtask reports ("this PR's tasks are complete"). Even a false positive is low-cost (one log line; never blocks). Only fires when an `active: true` record with >10% remaining exists, so non-autonomous messaging is never touched.
16
+ - **Portable date math** via the python3 already used by the script's URL-provenance check (no new dependency). Reads the record directly — no server call (the convergence-check host is pure-bash by design).
17
+ - **Inline fallback NOT modified:** `getConvergenceCheckInline()` is a degraded "template-missing" safety net already divergent from the template (it lacks check 8 too); newer checks live in the template only by established pattern, and there is no template==inline drift test. The template is the canonical always-used source.
18
+
19
+ ## Tests (golden, 5; all green; 66 convergence-check tests total green — no regression)
20
+ `tests/unit/convergence-check-time-awareness.test.ts` — runs the REAL template script: (1) done-assertion + >10% remaining → SIGNAL in log + stderr, computed %, no phrase-quote, exit 0 (not blocked); (2) no assertion → silent; (3) <10% remaining (genuine near-end wrap-up) → silent; (4) record not active → silent; (5) no record at all → silent. `bash -n` + `tsc` clean.