instar 1.3.962 → 1.3.964

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 (37) hide show
  1. package/dist/core/UnjustifiedStopGate.d.ts +14 -0
  2. package/dist/core/UnjustifiedStopGate.d.ts.map +1 -1
  3. package/dist/core/UnjustifiedStopGate.js +52 -2
  4. package/dist/core/UnjustifiedStopGate.js.map +1 -1
  5. package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
  6. package/dist/core/WriteDomainRegistry.js +7 -0
  7. package/dist/core/WriteDomainRegistry.js.map +1 -1
  8. package/dist/data/provenanceCoverage.d.ts +2 -0
  9. package/dist/data/provenanceCoverage.d.ts.map +1 -1
  10. package/dist/data/provenanceCoverage.js +29 -4
  11. package/dist/data/provenanceCoverage.js.map +1 -1
  12. package/dist/monitoring/CheckInReminderReconciler.d.ts +92 -0
  13. package/dist/monitoring/CheckInReminderReconciler.d.ts.map +1 -0
  14. package/dist/monitoring/CheckInReminderReconciler.js +167 -0
  15. package/dist/monitoring/CheckInReminderReconciler.js.map +1 -0
  16. package/dist/monitoring/CommitmentTracker.d.ts +39 -0
  17. package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
  18. package/dist/monitoring/CommitmentTracker.js.map +1 -1
  19. package/dist/monitoring/checkInReminderCore.d.ts +77 -0
  20. package/dist/monitoring/checkInReminderCore.d.ts.map +1 -0
  21. package/dist/monitoring/checkInReminderCore.js +104 -0
  22. package/dist/monitoring/checkInReminderCore.js.map +1 -0
  23. package/dist/server/routes.d.ts.map +1 -1
  24. package/dist/server/routes.js +106 -0
  25. package/dist/server/routes.js.map +1 -1
  26. package/dist/testing/selfActionRegistry.d.ts.map +1 -1
  27. package/dist/testing/selfActionRegistry.js +62 -0
  28. package/dist/testing/selfActionRegistry.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/data/builtin-manifest.json +46 -46
  31. package/src/data/provenanceCoverage.ts +30 -3
  32. package/src/scaffold/templates/jobs/instar/commitment-checkin-reminder.md +38 -0
  33. package/upgrades/1.3.963.md +135 -0
  34. package/upgrades/1.3.964.md +123 -0
  35. package/upgrades/side-effects/dated-commitment-reminder.md +240 -0
  36. package/upgrades/side-effects/enrol-unjustified-stop-gate.md +126 -0
  37. package/upgrades/side-effects/spec-round10-manifest-rows.md +13 -0
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: Commitment Check-In Reminder
3
+ description: "Drives ONE check-in reminder pass (ACT-724; docs/specs/dated-commitment-reminder.md). Calls POST /commitments/check-in-reminder/pass: the server scans open commitments whose `checkInAt` has arrived and posts exactly one fixed-template reminder each to the commitment's own topic. Idempotent — the `checkInReminderSentAt` stamp is written only AFTER a successful send, so a re-run, a retried trigger, or two passes racing cannot double-send, and a failed send is retried (bounded) rather than silently marked delivered. Tier-0 supervision is JUSTIFIED: the pass is a deterministic predicate over durable state plus a fixed-template send — there is no LLM step for a supervisor to validate, and the endpoint's own idempotency and per-pass cap already bound it. Ships enabled:false (dark; the feature itself is dev-agent gated and dryRun-first). This job NEVER authors prose — the reminder text is code-generated and the job's own output is mechanical."
4
+ schedule: "*/5 * * * *"
5
+ priority: medium
6
+ expectedDurationMinutes: 1
7
+ supervision: tier0
8
+ enabled: false
9
+ tags:
10
+ - cat:commitments
11
+ - check-in-reminder
12
+ - role:worker
13
+ gate: curl -sf http://localhost:${INSTAR_PORT:-4042}/health >/dev/null 2>&1
14
+ toolAllowlist: "*"
15
+ unrestrictedTools: true
16
+ mcpAccess: none
17
+ ---
18
+ Trigger one commitment check-in reminder pass. This is a mechanical cadence job — do NOT message the user yourself. The reminder message is composed and delivered server-side from a fixed template; your job is only to fire the pass and report the counts.
19
+
20
+ AUTH="${INSTAR_AUTH_TOKEN:-$(python3 -c "import json; v=json.load(open('.instar/config.json')).get('authToken',''); print(v if isinstance(v, str) else '')" 2>/dev/null)}"
21
+ AGENT_ID="${INSTAR_AGENT_ID:-$(python3 -c "import json; print(json.load(open('.instar/config.json')).get('projectName',''))" 2>/dev/null)}"
22
+ PORT="${INSTAR_PORT:-4042}"
23
+
24
+ curl -sS -m 60 -X POST \
25
+ -H "Authorization: Bearer $AUTH" \
26
+ -H "X-Instar-AgentId: $AGENT_ID" \
27
+ -H 'Content-Type: application/json' \
28
+ -d '{}' \
29
+ "http://localhost:${PORT}/commitments/check-in-reminder/pass"
30
+
31
+ Interpreting the response:
32
+
33
+ - `503 check-in-reminder-not-enabled` — the feature is dark on this agent. Expected on the fleet. Nothing to do, nothing to report.
34
+ - `{"ran":true,"dryRun":true,"wouldSend":N}` — soak mode. The pass decided what it WOULD send and sent nothing. Normal during the dark window.
35
+ - `{"ran":true,"sent":N}` — N reminders delivered.
36
+ - `"gaveUp":N` greater than zero — N reminders are genuinely UNDELIVERED after exhausting retries. This is the one outcome worth surfacing: read `GET /commitments/check-in-reminder` for the `undelivered` list. Those are promises the user did not receive.
37
+
38
+ Do not retry the pass yourself on a non-zero `failed` count — the next scheduled pass retries automatically, bounded, and the endpoint is idempotent.
@@ -0,0 +1,135 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ A commitment can now carry a **check-in date**, and a background reconciler
9
+ posts one reminder into that commitment's own topic when the date arrives.
10
+
11
+ Before this, "I'll report back by Friday" lived in the session that said it. If
12
+ that session ended, restarted, or compacted, Friday arrived and nothing
13
+ happened. The PromiseBeacon nudges on *rhythm*, not on dates — it has no concept
14
+ of a specific promised moment.
15
+
16
+ **What ships (ACT-724 step 1 of 2):**
17
+
18
+ - `Commitment.checkInAt` — an absolute instant, deliberately separate from the
19
+ beacon's `nextUpdateDueAt` / `softDeadlineAt` so a one-time dated reminder and
20
+ a rolling nudge cadence are not side effects of each other.
21
+ - `CheckInReminderReconciler` — a scan over open commitments, driven by the new
22
+ `commitment-checkin-reminder` built-in job (every 5 minutes, `enabled: false`).
23
+ - `POST /commitments/check-in-reminder/pass` and `GET /commitments/check-in-reminder`.
24
+
25
+ **One recurring scan, not one alarm per commitment.** ACT-724 sketched a
26
+ per-commitment scheduler entry; taken literally that reproduces two of the three
27
+ defects the action itself lists — the two-file job dance (defect b) and
28
+ self-disable by file edit (defect c). A scan has neither: coverage is a property
29
+ of the scan, so there is no registration step that can be skipped, and teardown
30
+ is just the commitment reaching a terminal status.
31
+
32
+ **The ordering was wrong first, and the fix's own justification was unwired.**
33
+ The first design stamped `checkInReminderSentAt` *before* sending — which made
34
+ zero delivery a designed outcome, since a failed send left the commitment marked
35
+ delivered and permanently ineligible. Inverted to send-then-stamp with bounded
36
+ retry (5 attempts, then a loud `checkInReminderFailedAt`). The second review
37
+ round then caught that the duplicate mitigation justifying that inversion —
38
+ "the relay dedups" — was not connected: `sendToTopic` does not dedup, that lives
39
+ in the `/telegram/reply` route. The send is now explicitly routed through the
40
+ same durably-backed `OutboundContentDedup`.
41
+
42
+ **Honest guarantee: at-least-once, deduped at the delivery layer** — not
43
+ exactly-once. Both remaining windows are stated in the spec rather than designed
44
+ away.
45
+
46
+ A newly created scheduled job fired immediately on the next server restart —
47
+ including one scheduled months ahead.
48
+
49
+ At startup the scheduler sweeps for work that was missed while the machine was
50
+ off. To decide, it compares now against the job's last run. For a job that has
51
+ **never** run there is no last run, and the code resolved that case by treating
52
+ it as overdue and triggering it. That is right for a job added while the server
53
+ was down that slept through its window, and wrong for a job created minutes ago
54
+ whose first window is in December — "has never run" describes both identically.
55
+
56
+ The comment directly above that branch stated the correct rule —
57
+
58
+ > *"Jobs that have never run: trigger on startup if their first expected run
59
+ > time has already passed"*
60
+
61
+ — and the code never checked the condition. It couldn't: nothing recorded when a
62
+ job started existing, so there was no fact to reason from.
63
+
64
+ `JobState.firstSeenAt` is now stamped once at registration, and the sweep applies
65
+ the intended rule: a never-run job is missed only if it has existed longer than
66
+ one of its own intervals. A December reminder created today waits; a job that has
67
+ sat unrun through six four-hour windows still catches up.
68
+
69
+ Every unknown resolves to **not firing** — missing or unparseable `firstSeenAt`,
70
+ uncomputable interval. That asymmetry is deliberate: a skipped catch-up costs one
71
+ delayed run, while a future-dated job that fires early marks itself delivered and
72
+ never fires again, so nobody goes looking for it.
73
+
74
+ **Why now:** this is the prerequisite for dated commitments materializing real
75
+ scheduled reminders (ACT-724). A reminder built on a scheduler that fires
76
+ future-dated jobs at boot would discharge itself before its date — worse than
77
+ never having built it. The standard itself is not in this change.
78
+
79
+ ## What to Tell Your User
80
+
81
+ Nothing changes until it's switched on. When it is: on the day the agent said it
82
+ would check in on something, you get one short message in that conversation
83
+ saying the date has arrived.
84
+
85
+ It will not claim the work is done — a reminder that implies completion closes
86
+ the question instead of reopening it.
87
+
88
+ **Not yet true:** ACT-724 asks that a dated commitment without a reminder be
89
+ structurally impossible. This ships dark, and a disabled watcher guarantees
90
+ nothing. What is true today: if the reconciler is running, no dated commitment
91
+ can slip past it individually. The creation-time gate that closes the rest is
92
+ step 2, and it cannot itself ship dark.
93
+
94
+ If you've ever seen a scheduled job run for no obvious reason shortly after a
95
+ restart, this was probably it.
96
+
97
+ The practical effect: reminders and scheduled work now happen when they're
98
+ scheduled, not when the server next happens to reboot. A job that's been waiting
99
+ less than one of its own cycles will now wait for its proper turn rather than
100
+ firing at startup — that delay is the point.
101
+
102
+ ## Summary of New Capabilities
103
+
104
+ - Commitments carry a first-class check-in date that produces a real reminder.
105
+ - A reminder is never recorded as delivered unless it actually was; failures
106
+ retry and then give up visibly.
107
+ - Two read/drive endpoints and a built-in job, all dark by default.
108
+
109
+ - Scheduled jobs with a future first window are no longer discharged at server
110
+ startup.
111
+ - Job state records when a job was first registered, so "brand new" and "long
112
+ overdue" are distinguishable facts rather than a guess.
113
+ - The startup sweep fails toward not-running on every uncertainty.
114
+
115
+ ## Evidence
116
+
117
+ - Tier 1 (41): the full eligibility predicate on both sides of every clause;
118
+ idempotency across repeated passes; **a failed send never reads as sent** (the
119
+ regression test for the round-1 finding); bounded retry to a loud terminal
120
+ state; per-pass cap deferring rather than dropping; the reminder text never
121
+ asserting completion.
122
+ - Tier 2 (10): real Express routes + auth — delivery, HTTP-boundary idempotency,
123
+ dark 503, missing-transport 503, and `dryRun` defaulting ON when config omits it.
124
+ - Tier 3 (4): real `AgentServer` boot with a real injected `CommitmentTracker`.
125
+
126
+ - `tests/unit/job-scheduler-never-run-future-job.test.ts` (3 cases): a never-run
127
+ annual job and a freshly-registered daily job both stay at zero triggers; a job
128
+ registered 6h ago on a 5-minute cron still catches up. Written failing first —
129
+ the two "must not fire" cases failed against the old code, the catch-up case
130
+ already passed.
131
+ - `tests/unit/job-scheduler-edge.test.ts`: the existing test that asserted the
132
+ buggy behaviour was **updated, not deleted** — it now seeds `firstSeenAt` in the
133
+ past so it exercises the scenario it was written for (a job that genuinely
134
+ missed windows), plus a new sibling pinning the other side of the boundary.
135
+ - Full scheduler suite: 76 tests across 8 files, green.
@@ -0,0 +1,123 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ `UnjustifiedStopGate` — the authority that decides whether an agent may stop
9
+ working — is now enrolled in the LLM-Decision Quality Meter.
10
+
11
+ At ~1343 calls / 7 days it was the **highest-volume unenrolled decision point in
12
+ the census** and the second-busiest gate overall, yet nothing recorded what it
13
+ decided or on what basis. The meter can only answer "is this judgment any good?"
14
+ for points that record; this one didn't, so the question was unanswerable in
15
+ principle.
16
+
17
+ **What the row carries — identity only.** The gate reads untrusted session
18
+ content (the stop rationale and up to ten recent conversation turns). None of it
19
+ is recorded. The context is an explicit allowlist of derived values:
20
+
21
+ - `stopReasonSha256` + `stopReasonChars` — distinguishes two rationales without
22
+ reproducing either
23
+ - `artifactCount` / `artifactKinds` — the evidence the authority could cite
24
+ - `signals` — code-derived booleans (not user text)
25
+ - `recentTurnCount` / `recentTurnChars` — conversation shape only
26
+
27
+ An allowlist rather than a filtered copy, so a future input field cannot appear
28
+ in the row by default. Tests plant a fake API key and password in the input and
29
+ assert neither reaches the serialized context.
30
+
31
+ **Declared `measurement-only`, on purpose.** Recording a decision is not grading
32
+ it. Grading this point needs a downstream fact — did work resume after an allowed
33
+ stop; did the agent genuinely continue after a refused one — and joining those
34
+ signals to a decision row is real plumbing. Rather than let `wired` imply more
35
+ than exists, the entry declares `gradingPosture: 'measurement-only'` with an
36
+ argued reason, so `findWiredWithoutGraders` reports it as an explicit posture
37
+ instead of a silent contradiction.
38
+
39
+ That distinction is the point: the easy version of this change moves a census
40
+ number and leaves a reader believing the busiest judgment call is being
41
+ evaluated. It isn't yet. Rows accumulate now so the grader has history when it
42
+ lands, rather than starting cold.
43
+
44
+ Volume is valved at `budget:300`/day (an always-on gate must not grow the archive
45
+ unbounded); the `decision_quality` row is written for every settlement regardless,
46
+ so counts stay complete.
47
+
48
+ Spec clarification for the (unshipped, dark) cross-machine capability registry: `scanState` and row
49
+ presence are INDEPENDENT. A machine with a model manifest and no doorway scan-state now returns
50
+ `scanState: "never-observed"` **together with** its manifest-derived rows at status `unknown`,
51
+ instead of zero rows.
52
+
53
+ The earlier wording ("`200` with `scanState: never-observed` and empty entries when no observation
54
+ exists") was ambiguous, and the ambiguity produced a real defect: the Increment-2 route derived ten
55
+ rows from the real manifest and served none of them, because "no observation" was read as "no scan".
56
+ Manifest rows ARE observations — the record admits them with `sourceDetail: doorway-manifest`, and
57
+ the status matrix gives `manifest-only` evidence the status `unknown` precisely so documented
58
+ existence is reportable without ever implying reachability.
59
+
60
+ The rule now carries its failing input: on a machine with a manifest and no scan-state the route
61
+ MUST return rows with status `unknown`; a change that makes it return zero rows must fail a test.
62
+
63
+ ## What to Tell Your User
64
+
65
+ Nothing changes in how the agent behaves. This is instrumentation on an internal
66
+ judgment — no new messages, no new settings, nothing you interact with.
67
+
68
+ What it buys: the check that decides whether the agent may stop working is the
69
+ busiest judgment call in the system, and until now nothing recorded what it
70
+ decided. Now it does. So the question "is that check actually any good — too
71
+ strict, too lenient, wrong half the time?" stops being unanswerable and becomes
72
+ answerable once enough history accumulates.
73
+
74
+ Worth being precise about what is NOT true yet: the decisions are recorded, not
75
+ graded. Knowing whether a given call was *right* needs a later fact (did work
76
+ resume after a stop was allowed?) that isn't connected up yet. The record says so
77
+ explicitly rather than implying the check is being evaluated when only half of
78
+ that is in place.
79
+
80
+ None of the conversation is stored — only a fingerprint of the stop reason and
81
+ counts describing the decision's shape.
82
+
83
+ Nothing. This is a design-document correction for a feature that is switched off everywhere, so
84
+ there is no user-visible change to announce. If a user asks why a capability list looked empty on a
85
+ machine that had never run a scan, the honest answer is that it was a specification ambiguity: the
86
+ catalog rows existed and were being discarded, and the corrected rule serves them marked as
87
+ unverified rather than hiding them.
88
+
89
+ ## Summary of New Capabilities
90
+
91
+ - The stop-justification authority now records every decision it makes, so its
92
+ quality can be evaluated rather than assumed.
93
+ - Recording is identity-only by construction: an explicit allowlist of derived
94
+ values, tested against planted secrets, so session content cannot leak into the
95
+ provenance store.
96
+ - The census now distinguishes "recorded and gradeable" from "recorded but not
97
+ yet gradeable", so the coverage number reflects what actually exists.
98
+
99
+ None. This amendment adds no capability — it removes an ambiguity that was silently costing one
100
+ (the ability to see what a machine's catalog claims before anything has probed it).
101
+
102
+ ## Evidence
103
+
104
+ - `tests/unit/unjustified-stop-gate-provenance-enrollment.test.ts` (11): the
105
+ census declares it wired with a volume valve and content class; the
106
+ measurement-only posture carries a real argument; the provenance block names
107
+ the typed constant with the prompt hash; **planted secrets never reach the
108
+ context**; the hash distinguishes different rationales (so it isn't a constant);
109
+ empty input doesn't throw; the verdict is unchanged and `attribution` survives
110
+ alongside the new block.
111
+ - Census ratchet: pending baseline shrinks by exactly one; typed-registration
112
+ check independently verifies the source imports `DP_UNJUSTIFIED_STOP_GATE`.
113
+ - 72 green across the affected suites.
114
+
115
+ Found by running the Increment-2 route's local-projection path against a real agent state dir rather
116
+ than reading the diff: `durable projection ABSENT / fallback readDoorwaySources entries = 10 /
117
+ scanState = never-observed`, and then observing that `classifyProjection` early-returned a single
118
+ `no-data-yet` row, discarding all ten.
119
+
120
+ ## Audience
121
+
122
+ Agent-only, maturity experimental. No user-visible behavior: the feature is dark everywhere, no
123
+ route ships enabled by this change, and the amendment is spec text only.
@@ -0,0 +1,240 @@
1
+ # Side-effects review — dated-commitment-reminder
2
+
3
+ **Change:** commitments gain a first-class `checkInAt`; a recurring reconciler
4
+ posts exactly one fixed-template reminder to the commitment's topic when that
5
+ instant arrives; a built-in job drives it. ACT-724 (critical, pinned).
6
+
7
+ **Spec:** `docs/specs/dated-commitment-reminder.md` (2 review rounds; round 1
8
+ returned SERIOUS ISSUES and inverted the send/stamp ordering).
9
+
10
+ **Ships dark:** dev-agent gated + `dryRun` defaulting true + job manifest
11
+ `enabled: false`.
12
+
13
+ ---
14
+
15
+ ## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
16
+
17
+ - **A dated commitment with no `topicId` is never reminded** (`no-topic`). A
18
+ reminder with nowhere to go is not a reminder; it is reported as a skip reason
19
+ rather than silently dropped, and appears in the `pending` list.
20
+ - **An unparseable `checkInAt` fails closed.** Deliberate: coercing it would make
21
+ the commitment look overdue since epoch and fire immediately — the exact
22
+ boot-fire shape fixed in `scheduler-never-run-window` days earlier.
23
+ - **A pass is capped at 25.** An overdue backlog drains across passes rather than
24
+ flooding a topic. The cap logs when it drops work, so a truncated pass is never
25
+ reported as a complete one.
26
+ - **Only `pending` counts as open.** A status added later is treated as not-open
27
+ (no reminder) until classified deliberately. Safe direction, but it does mean a
28
+ future status would silently stop reminders until someone adds it to the set.
29
+
30
+ ## 2. Under-block — what failure modes does this still miss?
31
+
32
+ - **"Structurally impossible" is not yet true.** ACT-724 asks that a dated
33
+ commitment without a reminder be impossible. Shipping dark means the watcher
34
+ can be off, so a dated commitment CAN exist with nothing watching. What IS
35
+ structural today: given the reconciler runs, no individual commitment can be
36
+ missed (coverage is a property of the scan, not of a per-commitment
37
+ registration). The creation-time gate that closes the rest is the declared
38
+ graduation step, and it cannot itself ship dark. Called out because the spec's
39
+ own framing over-claimed until review pushed back.
40
+ - **A duplicate is possible in one narrow window** — crash between send and
41
+ stamp. Accepted, and mitigated by the relay's existing content dedup rather
42
+ than by design novelty. Stated plainly instead of claimed away.
43
+ - **Clock jumps** shift a reminder in time (late on a backwards jump, early on a
44
+ forwards one) but cannot double-send or permanently suppress.
45
+ - **No escalation if a reminder is ignored.** Out of scope: this delivers the
46
+ date, it does not chase. The beacon owns chasing.
47
+
48
+ ## 3. Level-of-abstraction fit
49
+
50
+ The decision (`isCheckInReminderDue`) is pure and lives in
51
+ `checkInReminderCore.ts`; the effects (CAS, send, retry) live in the reconciler;
52
+ the cadence lives in a built-in job. That split is what let both sides of every
53
+ clause be tested without a store, a clock, or a transport.
54
+
55
+ **The alternative I rejected is the more standard one.** A transactional outbox
56
+ (`reminder_deliveries` with pending/sent/failed) is the textbook answer and is
57
+ genuinely better at volume. Rejected here because the commitment record already
58
+ IS durable single-writer state: a second store would add a consistency boundary
59
+ — a new class of divergence bug — to serve tens of commitments. The fields on the
60
+ commitment are a degenerate outbox with exactly one row per commitment. If volume
61
+ ever justifies it, the migration is mechanical.
62
+
63
+ ## 4. Signal vs authority compliance
64
+
65
+ `docs/signal-vs-authority.md`. Both decision points are `invariant`:
66
+
67
+ - **The due predicate** is arithmetic over durable state (open ∧ date arrived ∧
68
+ unstamped ∧ routed ∧ retries remain). No competing signals, nothing to weigh.
69
+ The failure being fixed was an ABSENT mechanism, not a bad judgment.
70
+ - **The reminder text** is a fixed template, deliberately not model-authored.
71
+ Generating it would add a judgment where none is wanted and a failure mode
72
+ (provider down) on a path whose entire purpose is to not fail.
73
+
74
+ **Deliberate gate bypass, argued:** the reminder rides the deterministic delivery
75
+ path, not the LLM tone gate. The tone gate fails closed, and a reminder that can
76
+ be held by a failing gate is the defect this feature exists to fix. This is not
77
+ weakening a safety control — the control is inapplicable, because the message
78
+ carries no agent prose for it to judge. Nothing model-generated reaches the user
79
+ through this path.
80
+
81
+ ## 5. Interactions
82
+
83
+ - **PromiseBeacon** — untouched. `checkInAt` is a NEW field precisely so beacon
84
+ cadence (`nextUpdateDueAt`, `softDeadlineAt`) is not overloaded; a one-time
85
+ dated reminder and a rolling nudge cadence stay independent.
86
+ - **`CommitmentTracker.mutate`** — the existing single-writer CAS is reused, not
87
+ replaced. Two passes racing cannot double-stamp.
88
+ - **The relay's content dedup** (`outboundContentDedup`) — now load-bearing for
89
+ this feature's duplicate story. Documented in both the spec and the reconciler
90
+ so a future change to that window is understood to affect this.
91
+ - **The scheduler** — this is why `scheduler-never-run-window` shipped first. A
92
+ reminder job on the old scheduler would have fired at boot.
93
+ - **`installBuiltinJobs`** — writes both `jobs/instar` and `jobs/schedule` for
94
+ built-ins, which is exactly why ACT-724's defect (b) (the two-file dance) does
95
+ not apply: that dance is only manual for USER jobs.
96
+
97
+ ## 6. External surfaces
98
+
99
+ - **New:** `POST /commitments/check-in-reminder/pass`, `GET
100
+ /commitments/check-in-reminder`. Both Bearer-authed, both 503 when dark.
101
+ - **New built-in job** `commitment-checkin-reminder`, `enabled: false`.
102
+ - **New commitment fields** — additive and optional; existing records load
103
+ unchanged, no migration.
104
+ - **User-visible:** one message per dated commitment, at its date, in its own
105
+ topic. Fixed template. The only user-facing text this change introduces.
106
+ - **A transport is required only to SEND.** A dry run needs none — surfaced by
107
+ the Tier-3 test, which would otherwise have reported the feature dead on an
108
+ agent with no messaging configured.
109
+
110
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
111
+
112
+ **Unified, with a single-executor rule.**
113
+
114
+ - `checkInAt` / `checkInReminderSentAt` / `checkInReminderAttempts` /
115
+ `checkInReminderFailedAt` live on the commitment record and inherit the
116
+ commitment store's existing posture. No new state surface.
117
+ - **The pass must run on ONE machine.** The built-in job is per-machine (each
118
+ machine runs its own scheduler), so on a pool the pass must be lease-gated the
119
+ way the benchmark-divergence analysis pass is. Belt-and-braces: even if two
120
+ ran, the CAS stamp makes the second a no-op — the lease prevents wasted work,
121
+ the CAS prevents the duplicate.
122
+ - **HONEST GAP:** the lease gate is specified in §Multi-machine posture but the
123
+ job ships `enabled: false`, so it is not yet wired in this change. Enabling the
124
+ job on a multi-machine pool BEFORE adding the lease check would risk two
125
+ machines both passing — the CAS still prevents a duplicate reminder, but the
126
+ work would be duplicated. Recorded as a precondition of enabling, not as done.
127
+ - No notice routing, no generated URL, nothing that strands on topic transfer.
128
+
129
+ ## 8. Rollback cost
130
+
131
+ Disable `commitments.checkInReminder` or the job manifest. The reconciler is the
132
+ only reader of these fields; commitments keep them inertly. No migration, no
133
+ durable cleanup, no agent-state repair. Reverting the commit is equally safe.
134
+
135
+ ## Second-pass review
136
+
137
+ **Required** — this sends messages to users (outbound messaging is on the Phase-5
138
+ trigger list).
139
+
140
+ Two rounds of external cross-model review, the first of which returned **SERIOUS
141
+ ISSUES** and changed the design:
142
+
143
+ 1. **Stamp-before-send made zero delivery a designed outcome.** A failed send
144
+ left the commitment marked `checkInReminderSentAt` and permanently
145
+ ineligible. Inverted to send-then-stamp with bounded retry; the duplicate risk
146
+ is covered by the relay's existing dedup. A regression test asserts a failed
147
+ send never reads as sent.
148
+ 2. **"Structurally impossible" over-claimed** for a feature behind a disableable
149
+ flag. Split into what is true now versus at graduation.
150
+ 3. **Time semantics under-specified** — `checkInAt` is now defined as an absolute
151
+ instant with normalization at the creation boundary, and DST/clock-jump
152
+ behaviour is stated.
153
+ 4. **Cadence and scale omitted** — 5-minute cadence, worst-case lateness, the
154
+ in-memory store's actual size, per-pass cap and backlog drain are now stated.
155
+ 5. **Alternatives not compared** — outbox, durable queue, one-shot entries, and
156
+ per-commitment scheduler entries are now compared with reasons.
157
+
158
+ Self-review found one thing the reviewer did not: the Tier-3 test initially
159
+ asserted that `AgentServer` self-constructs the `CommitmentTracker`. It does
160
+ not — `server.ts` injects it — so the routes 503'd on the real boot path while
161
+ Tier 2 passed happily against a hand-assembled context. That is exactly the
162
+ wiring assumption Tier 3 exists to break, and the test now mirrors the
163
+ production entrypoint. It also surfaced the dry-run/transport coupling in §6.
164
+
165
+ Tests: 41 unit + 10 integration + 4 e2e = 55.
166
+
167
+
168
+ ## Class-Closure Declaration
169
+
170
+ **Class:** `unbounded-self-action` · **Closure:** `guard` · **Enforcement:** ratchet
171
+ **Citation:** `tests/unit/self-action-convergence.test.ts`
172
+
173
+ This change adds a self-triggered controller: a cadenced pass that SENDS
174
+ messages to users without a human in the loop. Under the "Capacity Safety — No
175
+ Unbounded Self-Action" standard that has to be proven to CONVERGE under
176
+ sustained pressure, not merely be individually correct.
177
+
178
+ Registered as `check-in-reminder-pass` in `src/testing/selfActionRegistry.ts`.
179
+
180
+ **Steady-state bound.** The emit is gated by a durable per-commitment attempt
181
+ counter (`CHECK_IN_MAX_ATTEMPTS = 5`), persisted through the CAS `mutate()`
182
+ BEFORE the send — so a crash-loop cannot buy fresh attempts, and the registry's
183
+ restart model (which reconstructs from durable state) settles at the same bound.
184
+
185
+ **Settling brake.** Two terminal states, both durable: a successful send writes
186
+ `checkInReminderSentAt`, making the commitment permanently ineligible; repeated
187
+ failure exhausts the counter and writes a loud `checkInReminderFailedAt`.
188
+
189
+ **Modeled at the worst case.** The registry model has every send FAIL, so the
190
+ success stamp never lands and only the attempt counter can stop it. Without that
191
+ counter the model emits once per tick forever and the ratchet fails — which is
192
+ the point: the bound is load-bearing, not decorative.
193
+
194
+ **Instantaneous mass** is separately bounded by the per-pass cap (25), which logs
195
+ when it defers rather than dropping silently.
196
+
197
+
198
+ ---
199
+
200
+ ## Addendum — three CI gates, and a coverage gap they led me to
201
+
202
+ **1. Write-domain classification.** `POST /commitments/check-in-reminder/pass`
203
+ mutates the commitment store and was undeclared. Classified `cluster-shared` in
204
+ `WriteDomainRegistry`, matching the requirement that the pass run on the
205
+ serving-lease holder only — the same single-writer shape as the feedback-factory
206
+ routes. This does NOT weaken the honest gap in section 7: the lease gate is still
207
+ not wired, and the classification records the intended posture rather than
208
+ asserting the wiring exists.
209
+
210
+ **2. Agent-id header.** The job's curl shipped Bearer-only; I dropped the
211
+ AGENT_ID line when adapting the template I copied from. Restored. Worth noting
212
+ as a pattern: adapting a template is where declared requirements silently fall
213
+ out.
214
+
215
+ **3. Docs coverage.** Job coverage fell 85% to 83% because the new built-in job
216
+ was undocumented. Added to the default-jobs reference. That floor caught a real
217
+ Agent-Awareness omission, not a bureaucratic one — an undocumented job is one no
218
+ operator can discover.
219
+
220
+ **And the thing worth more than all three: a coverage gap in the tone-gate suite.**
221
+
222
+ While dogfooding I hit a live advisory I could not override — four attempts,
223
+ byte-identical text, matching rule, sufficient reason, refused every time. That
224
+ is the RELEASING half of the advisory migration failing in production.
225
+
226
+ Investigating: every existing test in the advisory-migration integration file
227
+ pins ONE verdict for the whole run via a fixed mock, so the rule is constant by
228
+ construction and an ack always matches. The live gate RE-REVIEWS on every attempt
229
+ and can return a different rule — mine went B20_INTERNAL_ID_LEAK then
230
+ B11_STYLE_MISMATCH. That variability was not covered at all.
231
+
232
+ Two tests added: (a) re-review returns the SAME acked rule, ack honored, message
233
+ sent; (b) re-review returns a DIFFERENT rule, the stale ack is refused rather
234
+ than silently passing an objection nobody reviewed.
235
+
236
+ BOTH PASS, which exonerates the ack mechanism and kills my working hypothesis. I
237
+ therefore have a reproducible symptom and NO diagnosis, and I am shipping the
238
+ closed coverage gap rather than a guess dressed as a fix. The evidence and the
239
+ candidates I have not ruled out are in the run log; the four tone-gate tracking
240
+ actions stay OPEN because the releasing half is unproven in production.
@@ -0,0 +1,126 @@
1
+ # Side-effects review — enrol-unjustified-stop-gate
2
+
3
+ **Change:** enrol `UnjustifiedStopGate` into the LLM-Decision Quality Meter
4
+ (`pending:backlog:decision-quality-enrolment` → `wired`), with an identity-only
5
+ provenance context and an explicitly declared `measurement-only` grading posture.
6
+
7
+ **Why this point:** at ~1343 calls / 7 days (`GET /metrics/features`) it is the
8
+ highest-volume UNENROLLED decision point in the census, and the second-busiest
9
+ gate overall. This is the second half of the run's census task — the half where
10
+ the backlog actually shrinks rather than merely becoming legible.
11
+
12
+ **Tier:** 1 — one census entry, one context builder, one provenance block. No new
13
+ route, config key, flag, or durable store.
14
+
15
+ ---
16
+
17
+ ## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
18
+
19
+ **Nothing.** Enrollment adds an `options.provenance` block to an existing
20
+ provider call. It introduces no predicate, no branch, and no rejection path. The
21
+ gate's continue/allow/escalate verdict is computed exactly as before — asserted
22
+ directly by a test rather than argued.
23
+
24
+ The one build-time narrowing: the census ratchet now requires this point to stay
25
+ declared. Removing the enrollment without updating the census fails CI, which is
26
+ the intent.
27
+
28
+ ## 2. Under-block — what failure modes does this still miss?
29
+
30
+ - **It records decisions; it does not grade them.** This is the honest half and
31
+ is declared as `gradingPosture: 'measurement-only'` with a ≥40-char argument,
32
+ so `findWiredWithoutGraders` reports it as an explicit posture rather than a
33
+ silent contradiction. **The census number improves and the point is still not
34
+ measured end-to-end** — anyone reading "wired" without reading the posture will
35
+ overestimate what exists. That is why the posture is declared, tested, and
36
+ stated in the release note.
37
+ - **Volume is capped at `budget:300`/day.** A day with more than 300 stop
38
+ decisions archives the first 300; the `decision_quality` row is still written
39
+ for every settlement, so counts stay complete while the archive is valved. A
40
+ loud `droppedByBudget` counter surfaces the truncation.
41
+ - **The context is a fixed projection.** If a future reader needs a field I did
42
+ not include, the history will not have it retroactively. I chose the minimum
43
+ that reconstructs decision shape; widening later is additive but not backfillable.
44
+
45
+ ## 3. Level-of-abstraction fit
46
+
47
+ The context builder is a private method on the gate, because only the gate knows
48
+ which of its inputs are trusted evidence and which are untrusted session text.
49
+ Building it anywhere else would require exporting that distinction — and a
50
+ downstream builder that guesses wrong is precisely how a transcript ends up in
51
+ a provenance store.
52
+
53
+ The provenance block rides the existing `options` argument alongside
54
+ `attribution`, so enrollment adds a signal rather than displacing one (tested).
55
+
56
+ ## 4. Signal vs authority compliance
57
+
58
+ `docs/signal-vs-authority.md`. Enrollment holds **no authority at all** — it is
59
+ pure observability. The settlement seam consumes the block and records on its own
60
+ path; it never reaches the model and never alters the verdict.
61
+
62
+ The gate itself remains what it was: an LLM authority with a deterministic
63
+ fail-open contract. This change deliberately does not touch that.
64
+
65
+ **The content-bearing contract is the safety property here**, and it is enforced
66
+ by test rather than by care: the fixtures plant a fake API key and a fake
67
+ password in the untrusted input, and the tests assert neither appears anywhere in
68
+ the serialized context. A hash-distinctness test prevents a constant or empty
69
+ hash from making the identity field useless while still passing the leak tests.
70
+
71
+ ## 5. Interactions
72
+
73
+ - **`/metrics/features`** — unchanged; `attribution.component` is preserved
74
+ (explicitly tested, because losing it would silently drop this component from
75
+ the cost surface).
76
+ - **The census ratchet** — the pending baseline shrinks by exactly one line, which
77
+ is the direction it is allowed to move. The typed-registration check
78
+ independently verifies the enrolling source imports `DP_UNJUSTIFIED_STOP_GATE`
79
+ rather than restating the string.
80
+ - **The breaker / fail-open path** — untouched. A provenance write failure is
81
+ contained by the recorder's own fail-open contract, so it cannot convert into a
82
+ stop-decision failure.
83
+ - **The prompt hash** — reused as `promptId`, so a row records WHICH prompt
84
+ decided. The gate already computed it; nothing new is derived.
85
+
86
+ ## 6. External surfaces
87
+
88
+ - No route, config key, flag, env var, CLI, message, or notification.
89
+ - `GET /decision-quality` will show one more `wired` point and one fewer
90
+ `pending`. Field shapes unchanged.
91
+ - No user-visible behaviour whatsoever.
92
+
93
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
94
+
95
+ **Unified.** The census is a shipped source constant, byte-identical on every
96
+ install, so every machine enrolls the same point with the same volume valve and
97
+ content class. The provenance rows themselves inherit the meter's existing
98
+ posture — this change adds no new state surface, no notice, and no generated URL.
99
+
100
+ ## 8. Rollback cost
101
+
102
+ Revert. The census entry returns to `pending:` (restoring the baseline line), the
103
+ provenance block disappears, and previously-recorded rows remain inertly in the
104
+ store — harmless, and still readable if the enrollment returns.
105
+
106
+ ## Second-pass review
107
+
108
+ **Not required** by the Phase-5 trigger list: enrollment makes no block/allow
109
+ decision, touches no session lifecycle, and adds no gate. The gate it observes is
110
+ unchanged.
111
+
112
+ Self-review, recorded rather than skipped:
113
+
114
+ 1. **Is this the honest version or the number-improving version?** The
115
+ temptation was real: flipping `pending` → `wired` shrinks census debt whether
116
+ or not anything can be graded. I took the declared `measurement-only` path
117
+ because the census distinguishes it and `findWiredWithoutGraders` surfaces it
118
+ — the gap stays visible instead of being absorbed into a better-looking
119
+ number. If that posture did not exist I would not have shipped this as-is.
120
+ 2. **Could the untrusted text leak by a route I did not consider?** The context is
121
+ an explicit allowlist of derived values, not a filtered copy of the input, so
122
+ a new input field cannot appear in the row by default. The leak tests assert
123
+ absence of planted secrets rather than presence of expected keys, which is the
124
+ direction that catches accidental widening.
125
+
126
+ Tests: 11 new unit tests; 72 green across the affected suites.