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.
- package/dist/core/UnjustifiedStopGate.d.ts +14 -0
- package/dist/core/UnjustifiedStopGate.d.ts.map +1 -1
- package/dist/core/UnjustifiedStopGate.js +52 -2
- package/dist/core/UnjustifiedStopGate.js.map +1 -1
- package/dist/core/WriteDomainRegistry.d.ts.map +1 -1
- package/dist/core/WriteDomainRegistry.js +7 -0
- package/dist/core/WriteDomainRegistry.js.map +1 -1
- package/dist/data/provenanceCoverage.d.ts +2 -0
- package/dist/data/provenanceCoverage.d.ts.map +1 -1
- package/dist/data/provenanceCoverage.js +29 -4
- package/dist/data/provenanceCoverage.js.map +1 -1
- package/dist/monitoring/CheckInReminderReconciler.d.ts +92 -0
- package/dist/monitoring/CheckInReminderReconciler.d.ts.map +1 -0
- package/dist/monitoring/CheckInReminderReconciler.js +167 -0
- package/dist/monitoring/CheckInReminderReconciler.js.map +1 -0
- package/dist/monitoring/CommitmentTracker.d.ts +39 -0
- package/dist/monitoring/CommitmentTracker.d.ts.map +1 -1
- package/dist/monitoring/CommitmentTracker.js.map +1 -1
- package/dist/monitoring/checkInReminderCore.d.ts +77 -0
- package/dist/monitoring/checkInReminderCore.d.ts.map +1 -0
- package/dist/monitoring/checkInReminderCore.js +104 -0
- package/dist/monitoring/checkInReminderCore.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +106 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts.map +1 -1
- package/dist/testing/selfActionRegistry.js +62 -0
- package/dist/testing/selfActionRegistry.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/src/data/provenanceCoverage.ts +30 -3
- package/src/scaffold/templates/jobs/instar/commitment-checkin-reminder.md +38 -0
- package/upgrades/1.3.963.md +135 -0
- package/upgrades/1.3.964.md +123 -0
- package/upgrades/side-effects/dated-commitment-reminder.md +240 -0
- package/upgrades/side-effects/enrol-unjustified-stop-gate.md +126 -0
- 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.
|