instar 1.3.759 → 1.3.761
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/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +22 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/DoorwayRegistryReader.d.ts +66 -0
- package/dist/core/DoorwayRegistryReader.d.ts.map +1 -0
- package/dist/core/DoorwayRegistryReader.js +243 -0
- package/dist/core/DoorwayRegistryReader.js.map +1 -0
- package/dist/core/PostUpdateMigrator.d.ts +23 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +69 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/types.d.ts +24 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +2 -1
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +9 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +30 -0
- package/dist/server/routes.js.map +1 -1
- package/dist/testing/selfActionRegistry.d.ts +88 -0
- package/dist/testing/selfActionRegistry.d.ts.map +1 -0
- package/dist/testing/selfActionRegistry.js +202 -0
- package/dist/testing/selfActionRegistry.js.map +1 -0
- package/package.json +2 -2
- package/scripts/class-closure-declare.mjs +69 -4
- package/scripts/class-closure-lint.mjs +61 -5
- package/scripts/instar-dev-precommit.js +100 -2
- package/scripts/lib/self-action-detect.mjs +162 -0
- package/scripts/lint-no-unregistered-self-action.js +204 -0
- package/skills/instar-dev/templates/side-effects-artifact.md +8 -1
- package/src/data/builtin-manifest.json +63 -63
- package/src/scaffold/templates.ts +2 -1
- package/upgrades/1.3.760.md +100 -0
- package/upgrades/1.3.761.md +34 -0
- package/upgrades/side-effects/doorway-model-registry-inc3.md +82 -0
- package/upgrades/side-effects/self-action-convergence.md +62 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Doorway/Model Knowledge Registry — rollout increment 3 (the `GET /doorways` read surface + CLAUDE.md awareness + the `maintenance.doorwayScan` config knob, per `docs/specs/DOORWAY-MODEL-KNOWLEDGE-REGISTRY-SPEC.md` §Rollout step 3). Increments 1 (enriched manifest + lint) and 2 (deterministic prober + scan-state + dark job + §2.7 guard) are already merged. **The scan job stays DARK** — this increment adds a READ route + awareness only; nothing probes, spends, or changes routing.
|
|
9
|
+
|
|
10
|
+
- **`GET /doorways`** (`src/core/DoorwayRegistryReader.ts` + `src/server/routes.ts`, spec §D5) — a read-only, Bearer-authed, always-mounted route returning the merged doorway→model map: the git-tracked canonical `topModels` (authoritative for routing, with pricing) overlaid with THIS machine's read-validated live reachability, keyed by `doorId`. It honors the **D5 two-state contract**: `200` with `scanState:"never-run"` when the registry is present but no scan has run yet (each door honestly `reachable:null`/`probeStatus:"never-scanned"`), `200` merged once a scan has run, and `503` with a stable `code` when the registry is genuinely unavailable — `registry-unavailable-no-instar-source` (a pure end-user install carries no manifest) or `registry-corrupt` (present but unparseable). It never fabricates an empty map. The machine-local scan-state is UNTRUSTED and re-validated field-by-field on read (door id charset + known-candidate cross-check, `probeStatus` fixed enum, ISO timestamps; `reachable` DERIVED from the clamped `probeStatus`, never the stored boolean), so a poisoned local file is contained.
|
|
11
|
+
- **CLAUDE.md awareness block** (`generateClaudeMd()` for new agents + a content-sniffed `migrateClaudeMd()` block for existing agents, via the shared `DOORWAY_REGISTRY_CLAUDEMD_SECTION()`, spec §Agent Awareness + §Migration Parity) — a pointer (not an inlined door/model table): what the registry is, the `GET /doorways` read, the OFF-by-default scan job + how to enable it, and the proactive trigger ("what models can I reach? / is my model map current?" → read `GET /doorways`, don't guess). `/doorways` is also registered in `CAPABILITY_INDEX` so it surfaces in `/capabilities`.
|
|
12
|
+
- **`maintenance.doorwayScan` config knob** (`migrateConfig()` via `ConfigDefaults.SHARED_DEFAULTS`, seeded on both init AND update, spec §D6 + §Migration Parity) — the D6 fail-closed defaults `scope:"free-probes"`, `cadence:"0 4 * * 1"` (weekly), `digestTopicId:null`, `budgetCapUsd:0`. It seeds **every field EXCEPT `enabled`** (which stays absent): a seeded `false` would trip the D6 deny-wins predicate (`config.enabled !== false`) and permanently block the scan even after the job manifest is enabled — the round-2/round-5 bug the spec calls out. Whether the scan RUNS is governed by the (dark) job manifest; `maintenance.doorwayScan.enabled:false` is a master kill-switch.
|
|
13
|
+
|
|
14
|
+
Increment 5 (reconcile `flaggedStale` → flip the freshness lint to `strict` → ratify the "Keep the Doorway/Model Map Current" standard) is companion-gated + operator-gated and is NOT in this increment.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
You can now ask me "what models can I reach right now?" or "is my model map current?" and I'll read a real, live answer instead of guessing — a map from each of your doorways (Claude Code, Codex, Gemini, a paid API key, …) to the top models each one reaches. Before the (still-off-by-default) weekly scan ever runs, that map is honest: it shows the reviewed model list and says "not scanned yet" for live reachability, rather than pretending to be empty. Nothing here turns on any probing or spends any money — the scan that keeps the map fresh remains switched off until you deliberately enable it later.
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
- **`GET /doorways`** — a read-only "what models can I reach?" map: canonical top-models per doorway (with pricing) overlaid with this machine's live reachability. Honest two-state contract (200 never-run / 200 merged / 503 with a precise `code` when there's no registry to serve), never a fabricated empty payload, with the untrusted machine-local scan-state re-validated on every read.
|
|
23
|
+
- **Agent awareness** of the doorway registry — every agent (new via init, existing via update) now knows the registry + `GET /doorways` + the dark scan job exist, and the proactive trigger to read it.
|
|
24
|
+
- **A `maintenance.doorwayScan` config knob** with fail-closed defaults (free-probes, weekly, `budgetCapUsd:0`) seeded on init and update — every field except `enabled`, so the scan can never be permanently blocked by a seeded `false`.
|
|
25
|
+
|
|
26
|
+
## Evidence
|
|
27
|
+
|
|
28
|
+
- `tests/unit/doorway-registry-reader.test.ts` (10) — the D5 two-state contract at the reader layer (never-run / scanned / no-manifest / corrupt), the merged view, `reachable` derived from the clamped `probeStatus` (P20 tri-state: ok→true, timeout→null, not-installed/http-4xx→false, malformed→true), the EXHAUSTIVE §1.3 read-clamp (out-of-enum probeStatus coerced, injection/metachar door-ids dropped, unknown door dropped, bad timestamp → null, machineId control-chars stripped), corrupt-scan-state → degrades to never-run (never crashes), and a MIRROR-PIN asserting the TS clamp constants (`PROBE_STATUS_ENUM`, `probeStatusToReachable`) match the prober's exported source of truth so they can't drift.
|
|
29
|
+
- `tests/unit/doorway-config-migration.test.ts` (6) — `getInitDefaults`/`getMigrationDefaults` seed every field EXCEPT `enabled`; `applyDefaults` seeds `0`/`null` correctly, never clobbers an operator override, never adds an `enabled` the operator omitted, preserves an operator's explicit `enabled:false`, and is idempotent; a full `PostUpdateMigrator.migrate()` seeds `maintenance.doorwayScan` on an existing agent with `enabled` absent.
|
|
30
|
+
- `tests/unit/doorway-claudemd-migration.test.ts` (4) — `generateClaudeMd` includes the awareness section + the `GET /doorways` read; the section function honors the injected port (never hardcoded); `migrateClaudeMd` appends the section once, is a no-op on a second run (content-sniffed idempotency), and does not re-append when already present.
|
|
31
|
+
- `tests/integration/doorway-routes.test.ts` (4) — `GET /doorways` through the real `createRoutes` HTTP pipeline: never-run → 200 `scanState:"never-run"` + canonical topModels (asserted NOT 503); scan-state present → 200 merged; no manifest → 503 `registry-unavailable-no-instar-source`; corrupt manifest → 503 `registry-corrupt`.
|
|
32
|
+
- `tests/e2e/doorway-registry-alive.test.ts` (4) — the "feature is alive" test on the REAL `AgentServer` init path: a source-carrying agent returns **200-not-503 with `scanState:"never-run"` before any scan** and 200 merged after; a non-instar-source agent returns 503 `registry-unavailable-no-instar-source`; the route requires Bearer auth (401 without a token).
|
|
33
|
+
- Ratchets satisfied: `capabilities-discoverability` (145) — `/doorways` classified in `CAPABILITY_INDEX`; `feature-delivery-completeness` (115) — the CLAUDE.md section tracked in `legacyMigratorSections`; `CapabilityIndex` (10), `ConfigDefaults` (36), `scaffold-templates` (26) all green. `npx tsc --noEmit` clean; the full `npm run lint` chain PASS (incl. `lint-model-registry-freshness` report-mode PASS, `lint-dev-agent-dark-gate` PASS); `npm run build` OK.
|
|
34
|
+
- Deferred items (spec §Deferred DF1-DF6, incl. the pool-scope `/doorways` merge) remain tracked in `docs/specs/reports/DOORWAY-MODEL-KNOWLEDGE-REGISTRY-followups.md`.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Side-Effects Review — Doorway/Model Knowledge Registry, increment 3 (GET /doorways route + CLAUDE.md awareness + maintenance.doorwayScan config-knob migration)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `doorway-model-registry-inc3`
|
|
4
|
+
**Date:** `2026-07-04`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Spec:** `docs/specs/DOORWAY-MODEL-KNOWLEDGE-REGISTRY-SPEC.md` (approved:true, review-convergence tagged) — §Rollout step 3, §D5, §D6, §Agent Awareness, §Migration Parity.
|
|
7
|
+
**Second-pass reviewer:** `not-required` — this increment adds NO block/allow authority. The one new HTTP surface (`GET /doorways`) is READ-ONLY (it never gates a message, dispatch, session, or action); the other two pieces are a data-model seed (`migrateConfig`) and a documentation append (`generateClaudeMd`/`migrateClaudeMd`). Nothing here touches messaging/dispatch block-allow, session lifecycle, compaction/respawn, coherence/idempotency/trust, or anything named sentinel/guard/gate/watchdog. See §4.
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
Third rollout increment of `DOORWAY-MODEL-KNOWLEDGE-REGISTRY-SPEC.md` (§Rollout step 3). Increments 1 (enriched manifest + lint, #1364) and 2 (deterministic prober + scan-state + dark job + §2.7 guard, #1373) are merged. This increment ships the READ + AWARENESS layer:
|
|
12
|
+
|
|
13
|
+
1. **`GET /doorways` route** (`src/server/routes.ts` + new `src/core/DoorwayRegistryReader.ts`) — a read-only, Bearer-authed (global middleware), always-mounted route returning the merged doorway→model view: canonical `topModels` (git-tracked, authoritative — §1.1) overlaid with this machine's read-validated live reachability. Honors the **D5 two-state contract**: `200` with `scanState:"never-run"` when the canonical manifest is present but no scan has run (each door `reachable:null`/`probeStatus:"never-scanned"`/`lastScannedAt:null`), `200` merged once a scan-state exists, and `503` with a stable machine-readable `code` (`registry-unavailable-no-instar-source` when the manifest is absent — a non-instar-source install; `registry-corrupt` when present-but-unparseable). It never fabricates an empty map.
|
|
14
|
+
2. **CLAUDE.md awareness block** — a new shared section function `DOORWAY_REGISTRY_CLAUDEMD_SECTION(port)` in `PostUpdateMigrator.ts`, interpolated into `generateClaudeMd()` (`src/scaffold/templates.ts`, new agents via init) AND appended by a content-sniffed `migrateClaudeMd()` block (existing agents via update — Migration Parity). Pointer-style (not an inlined door/model table) per §Agent Awareness.
|
|
15
|
+
3. **`maintenance.doorwayScan` config-knob migration** — seeded via the canonical `SHARED_DEFAULTS`/`applyDefaults` path in `ConfigDefaults.ts` (runs inside `migrateConfig()` on both init AND migration), with the D6 fail-closed defaults `scope:"free-probes"`, `cadence:"0 4 * * 1"`, `digestTopicId:null`, `budgetCapUsd:0` — and seeding **every field EXCEPT `enabled`** (the round-2/round-5 deny-wins bug: a seeded `false` would make `config.enabled !== false` false and permanently block the scan). Typed on `InstarConfig` (`src/core/types.ts`). Plus classification wiring: the `/doorways` prefix added to `CAPABILITY_INDEX` (discoverability lint) and the CLAUDE.md section added to `feature-delivery-completeness`'s tracked `legacyMigratorSections`.
|
|
16
|
+
|
|
17
|
+
The scan job itself remains DARK (inc2's `enabled:false` job manifest); this increment adds no cadence, no probing, no spend. Increment 5 (reconcile `flaggedStale` → flip lint to strict → ratify the standard) is companion-gated and explicitly NOT in this PR.
|
|
18
|
+
|
|
19
|
+
## Decision-point inventory
|
|
20
|
+
|
|
21
|
+
- **`GET /doorways` (NEW read route)** — `add` — read-only observability. Returns 200/503 by an OBJECTIVE, decidable file-presence/parse check (manifest present+parseable? scan-state present+scanned?). No behavioural gate, no message/dispatch/session decision. Bearer-auth is applied globally by `authMiddleware` (like every other `/`-mounted route); an un-authed request 401s (proven in the Tier-3 e2e).
|
|
22
|
+
- **`maintenance.doorwayScan` config seed** — `add` — a pure additive data-model default (add-missing-only; never clobbers an operator value; seeds `0`/`null` correctly). NOT a decision point.
|
|
23
|
+
- **CLAUDE.md awareness block** — `add` — documentation. NOT a decision point.
|
|
24
|
+
- No message/dispatch/tone-gate/session-lifecycle/coherence/trust decision point is touched.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 1. Over-block
|
|
29
|
+
|
|
30
|
+
Not applicable in the blocking sense — this increment adds no block authority. The nearest analogue is the route's 503 branch: it "withholds" a payload only when there is genuinely no registry to serve (manifest absent or unparseable). That is the honest D5 answer, not an over-block — the `code` field distinguishes "this feature isn't present on this install" (`registry-unavailable-no-instar-source`) from "the manifest is corrupt" (`registry-corrupt`) so a client never mistakes either for a transient server failure. A present-but-empty `doors{}` still returns 200 with an empty `doorways[]` (honest), not 503.
|
|
31
|
+
|
|
32
|
+
## 2. Under-block
|
|
33
|
+
|
|
34
|
+
Also not applicable. Two correctness risks were considered and closed:
|
|
35
|
+
- **Trusting poisoned machine-local scan-state.** The scan-state file is plaintext + machine-local; a local writer can plant well-formed poison after a clamped write (§1.3). The reader re-validates EVERY scan-state field it uses on read: door ids are charset/length-clamped AND cross-checked against the manifest's known-candidate set (unknown/metachar ids dropped), `probeStatus` is coerced to the fixed enum (out-of-enum → `malformed-response`), timestamps are ISO-or-null, and `reachable` is DERIVED from the clamped `probeStatus` (never the stored boolean — so a poisoned `reachable:"yes"` is never trusted, and P20 unknown≠down holds). Nothing raw-passes into the response. Honest scope: containment, not tamper-proofing a plaintext file (the accepted at-rest posture of every machine-local registry).
|
|
36
|
+
- **A crash on a corrupt scan-state.** The reader degrades a corrupt/unparseable scan-state to never-run and never throws — a bad machine-local file can only make the live half honest-empty, never break the read.
|
|
37
|
+
|
|
38
|
+
## 3. Level-of-abstraction fit
|
|
39
|
+
|
|
40
|
+
Correct layers. The merged-view composition lives in a dedicated `src/core/DoorwayRegistryReader.ts` (a pure reader — no I/O beyond two small file reads, no runtime authority), the route is a thin handler that maps the reader's discriminated result to the D5 status codes, the config default lives in the single-source-of-truth `ConfigDefaults.SHARED_DEFAULTS` (so init AND migration seed it identically), and the awareness text lives in the shared `*_CLAUDEMD_SECTION` function used by BOTH `generateClaudeMd` and `migrateClaudeMd` (so new-install and update text can never drift). The route reads the canonical manifest via `ctx.config.projectDir` (the source-tree idiom already used for `STANDARDS-REGISTRY.md`), matching the prober's dark posture (§2.9): a non-instar-source install honestly 503s.
|
|
41
|
+
|
|
42
|
+
## 4. Signal vs authority compliance
|
|
43
|
+
|
|
44
|
+
**Required reference:** `docs/signal-vs-authority.md`
|
|
45
|
+
|
|
46
|
+
Fully compliant — this increment adds **only SIGNAL / read surfaces**, no authority:
|
|
47
|
+
- `GET /doorways` is a read. It surfaces the merged map; it never blocks, gates, delays, or rewrites anything. Routing continues to read the canonical layer only (§1.1) — the route is observability over the same data, never a routing input.
|
|
48
|
+
- The config seed and CLAUDE.md block are data/documentation, not decisions.
|
|
49
|
+
- No brittle logic carries blocking authority (there is no blocking authority here at all). The only binary in the change — the 200-vs-503 branch — rests on an objective, decidable check (file present + parseable), the same shape as the sibling read routes (`/cartographer/health`, `/codex/usage`, `/guards`).
|
|
50
|
+
|
|
51
|
+
## 5. Interactions
|
|
52
|
+
|
|
53
|
+
- **Shadowing:** none. `GET /doorways` is a brand-new top-level prefix (no collision — verified by the discoverability lint, which now classifies `/doorways` in `CAPABILITY_INDEX`). The `migrateClaudeMd` block is content-sniffed on a unique heading substring (`Doorway/Model Knowledge Registry`) → idempotent, never double-appends, never shadows another section's sniff. The `SHARED_DEFAULTS` seed is add-missing-only → never overwrites another feature's config or an operator value.
|
|
54
|
+
- **Double-fire / races:** none. The route is a pure read (two `readFileSync` of small files per request, no shared mutable state). It reads the same machine-local scan-state the inc2 prober writes atomically (temp+rename) — a concurrent write can never produce a torn read that crashes the route (corrupt-parse → degrades to never-run).
|
|
55
|
+
- **Feedback loops:** none. Reading `/doorways` has no side effect on the scan-state or manifest.
|
|
56
|
+
|
|
57
|
+
## 6. External surfaces
|
|
58
|
+
|
|
59
|
+
- **Other agents / users:** the CLAUDE.md awareness block teaches every agent (new via init, existing via update) that `GET /doorways` exists and when to use it — the intended Agent-Awareness surface. The route is new but read-only + Bearer-gated; a pure end-user install (no manifest) gets an honest 503 rather than a fabricated payload.
|
|
60
|
+
- **Install base:** the config seed reaches existing agents via `migrateConfig`→`applyDefaults` on update (Migration Parity), new agents via `init`; the CLAUDE.md block via `migrateClaudeMd`/`generateClaudeMd`; the route via the compiled `dist` (always-mounted). No new job, no new hook — the scan job + guard already shipped in inc2.
|
|
61
|
+
- **External systems (Telegram/Slack/GitHub/Cloudflare):** none. This increment performs no network I/O and no messaging.
|
|
62
|
+
- **Persistent state:** none written. The route only READS the (inc2) machine-local scan-state + the git-tracked manifest.
|
|
63
|
+
- **Timing / runtime conditions:** none — the route's answer is a pure function of two files on disk at request time.
|
|
64
|
+
|
|
65
|
+
## 6b. Operator-surface quality
|
|
66
|
+
|
|
67
|
+
`GET /doorways` is an agent/API read surface, not an operator-tap surface (Mobile-Complete Operator Actions is N/A — there is no operator DECISION or credential collection here). The read is exposed to the agent (via the CLAUDE.md awareness block + `CAPABILITY_INDEX`) so the agent answers "what models can I reach?" conversationally, without the operator running a CLI. No config edit is asked of the operator in this increment.
|
|
68
|
+
|
|
69
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
70
|
+
|
|
71
|
+
- **`GET /doorways` is machine-local-by-design on its live half, and that is the correct posture.** The canonical `topModels` half is git-tracked source → byte-identical on every machine. The live `reachable`/`probeStatus`/`lastScannedAt` half is read from THIS machine's scan-state (`.instar/state/doorway-scan.json`) — machine-local because a door's reachability is a physical fact of that machine's disk (installed CLIs, per-disk logins), the same justification inc2 recorded (`machine-local-justification: physical-credential-locality`). A machine correctly answers `/doorways` about ITS OWN reachability. A pool-scope read (merge each machine's live half, tagged by machine) is the DECLINED/tracked follow-up the spec already names (§Multi-machine) — deliberately not built here (more conservative than adding a half-baked merge).
|
|
72
|
+
- **`maintenance.doorwayScan` config** is a normal config field; it is not a replicated store and carries no cross-machine coherence hazard (each machine reads its own config, as with every other `maintenance.*`/`monitoring.*` knob).
|
|
73
|
+
- **CLAUDE.md awareness** is per-agent local documentation. No cross-machine action.
|
|
74
|
+
- No feature here silently assumes a single machine; the one live surface is machine-local on purpose with the reason stated, and the pool-scope merge is explicitly deferred, not assumed-away.
|
|
75
|
+
|
|
76
|
+
## 8. Rollback cost
|
|
77
|
+
|
|
78
|
+
Low and clean. The route is read-only and additive — reverting the PR removes the `/doorways` route, the reader module, the config seed, the type, the CLAUDE.md block, and the two classification entries with no data migration and no state repair (nothing durable was written). An operator's already-seeded `maintenance.doorwayScan` block would remain in their config as inert (unread) keys — harmless, and re-added identically on the next update. No hot-fix urgency: the scan job stays dark regardless, so a route bug can only affect a read response, never behaviour, spend, or routing.
|
|
79
|
+
|
|
80
|
+
## Second-pass review
|
|
81
|
+
|
|
82
|
+
Not required (see the header rationale): no block/allow authority on messaging/inbound/dispatch, no session-lifecycle/compaction/respawn change, no coherence/idempotency/trust gate, and nothing named sentinel/guard/gate/watchdog is touched. The change is a read-only route + a data-model seed + a documentation block. The signal-vs-authority answer (§4) is unambiguous: signal only.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Side-Effects Review — Self-Action Convergence (the class-closure guard for self-inflicted loops)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `self-action-convergence`
|
|
4
|
+
**Date:** `2026-07-04`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — additive dev-tooling (a defect-class entry, a test ratchet, two lints, a shared detector, a precommit arm, a template clause, a migration, a constitution standard). No runtime decision surface, no routes, no user-facing behavior, no data migration. Everything ships report-only under the pre-existing prGate.classClosure dark default.`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Registers `unbounded-self-action` as a formal defect class in the class-closure registry (#1347) — the FIRST product-code member of that program — and builds the live guard that ends it: an N-tick sustained-pressure convergence ratchet (`tests/unit/self-action-convergence.test.ts` + the `src/testing/selfActionRegistry.ts` controller registry), a fail-closed forcing lint (`scripts/lint-no-unregistered-self-action.js`), a shared detector (`scripts/lib/self-action-detect.mjs`), a scope+convergence arm added to the class-closure CI lint (`scripts/class-closure-lint.mjs`), an early-detection arm at instar-dev commit time (`assertSelfActionDeclared` in `scripts/instar-dev-precommit.js`), a `--to-trace`/negative-declaration mode in `scripts/class-closure-declare.mjs`, a one-line side-effects-template clause, its migration (`migrateClassClosureTemplateSelfActionClause`), and the ratified constitution standard "Capacity Safety — No Unbounded Self-Action."
|
|
11
|
+
|
|
12
|
+
This is the structural fix for the entire self-inflicted-loop class (swap-thrash being the presenting case). It is BBR's temporal twin: Bounded Blast Radius bounds instantaneous MASS; this bounds steady-state FREQUENCY under feedback.
|
|
13
|
+
|
|
14
|
+
Files touched: `docs/defect-classes.json`, `docs/STANDARDS-REGISTRY.md`, `docs/specs/self-action-convergence.md` (+ `.eli16.md`), `scripts/lib/self-action-detect.mjs` (new), `src/testing/selfActionRegistry.ts` (new), `tests/unit/self-action-convergence.test.ts` (new), `scripts/lint-no-unregistered-self-action.js` (new), `scripts/class-closure-lint.mjs`, `scripts/class-closure-declare.mjs`, `scripts/instar-dev-precommit.js`, `skills/instar-dev/templates/side-effects-artifact.md`, `src/core/PostUpdateMigrator.ts`, `package.json`, plus tests (`tests/unit/self-action-detect.test.ts`, `tests/unit/lint-no-unregistered-self-action.test.ts`, `tests/unit/migrate-class-closure-self-action-clause.test.ts`, `tests/integration/class-closure-lint-self-action.test.ts`).
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
- **`unbounded-self-action` defect class** — add (registry semantics) — a protected-path registry entry. Governs fix-time declarations; does not gate runtime.
|
|
19
|
+
- **Convergence ratchet + controller registry** — add (CI test) — drives modeled controllers under a pinned worst-case fixture; fails the build (once green) if a registered controller does not settle. Zero runtime authority.
|
|
20
|
+
- **Forcing lint** — add (CI lint) — refuses an unregistered self-action controller emit. **Report-only by default** (exits 0 unless `prGate.classClosure.enabled && !dryRun`).
|
|
21
|
+
- **Class-closure CI lint scope arm + convergence check** — modify (CI lint) — a self-action diff now requires a declaration; a per-tick-cap-only `howCaught` is flagged. Hard-violations recorded; **fail the build only when enforcing** (dark default).
|
|
22
|
+
- **`assertSelfActionDeclared` precommit arm** — add (commit gate) — blocks a self-action src/ change with no declaration/negative. FAIL-OPEN on tooling failure; a genuine one-shot costs one attested negative line.
|
|
23
|
+
- **Shared detector** — add (pure library) — the single source for the emit set + scope predicate + the gate decision. Deterministic; never LLM-guesses.
|
|
24
|
+
- **Side-effects template clause + migration** — modify (skill template) — the Class-Closure Declaration trigger now names the self-action case; the migration reaches existing agents (Migration Parity).
|
|
25
|
+
- **Constitution standard** — add (standards text) — "Capacity Safety — No Unbounded Self-Action," ratified by the operator 2026-07-04.
|
|
26
|
+
- **All existing gates** — pass-through — the tone gate, coherence gate, external-operation gate, class-closure gate's existing behavior are unchanged; this composes with #1347, it does not fork it.
|
|
27
|
+
|
|
28
|
+
## 1. Over-block
|
|
29
|
+
|
|
30
|
+
**No runtime block/allow surface — over-block N/A.** The only new blocking paths are DEV-TIME gates (the precommit arm + the CI lints), and both ship report-only under the pre-existing `prGate.classClosure` dark default. The precommit arm fails OPEN on any tooling failure (empty diff, no src/ file, unreadable artifact) and offers an explicit one-line negative-declaration escape, so a genuine one-shot / user-driven action is never unescapably blocked. The forcing lint is scoped to controller-SHAPE files to avoid flagging every `retry(` in src/.
|
|
31
|
+
|
|
32
|
+
## 2. Under-block
|
|
33
|
+
|
|
34
|
+
**No runtime under-block surface.** The completeness guarantee (every controller registered) is report-only until the operator flips enforcing on measured declaration population, per #1347's own criterion. Until then a NEW self-action can still ship un-registered — the honest, deliberate soak posture; the E2 CI lint + E3 precommit arm are the earlier backstops, and the string-based scope is named as an accident-deterrent (the obfuscation-resistant closure is the follow-on funnel, Part B).
|
|
35
|
+
|
|
36
|
+
## 3. Ordering / idempotency
|
|
37
|
+
|
|
38
|
+
The migration is idempotent (marker short-circuit). The declare `--to-trace` mode is idempotent (identical block → no change). The ratchet + lints are pure re-runnable evaluations. No persistent runtime state, no ordering dependency.
|
|
39
|
+
|
|
40
|
+
## 4. Signal vs. Authority
|
|
41
|
+
|
|
42
|
+
Compliant. The detector is DETERMINISTIC — it forces a declaration, it never LLM-guesses (Signal vs. Authority). The ratchet + lints are structure, not willpower. The convergence proof is a CI ratchet, not discretionary prose. No new blocking authority is granted to any LLM path.
|
|
43
|
+
|
|
44
|
+
## 5. Feedback loops
|
|
45
|
+
|
|
46
|
+
**This spec IS the answer to the feedback-loop question** the swap-thrash review never answered. The change adds NO new runtime control loop — it adds a CI/commit-time invariant that PROVES other control loops converge. It cannot itself spiral (it is a stateless test/lint run per CI invocation).
|
|
47
|
+
|
|
48
|
+
## 6. External surfaces
|
|
49
|
+
|
|
50
|
+
None. No HTTP routes, no external API calls, no network egress. All new surfaces are local dev-tooling (a test, two lints, a shared library, a commit-gate arm, a registry entry, a template clause, a migration, a standard).
|
|
51
|
+
|
|
52
|
+
## 7. Multi-machine posture
|
|
53
|
+
|
|
54
|
+
`unified` (repo-resident). The registry entry, the lints, the detector, the ratchet, and the standard all ride the git repo itself (the replication medium), reaching every checkout on `git pull` — matching #1347's declared posture. The template clause reaches existing deployed agents through the PostUpdateMigrator migration. No machine-local state, no per-machine divergence.
|
|
55
|
+
|
|
56
|
+
## Class-Closure Declaration (display-only mirror)
|
|
57
|
+
|
|
58
|
+
- **`defectClass`** — `unbounded-self-action` (this change REGISTERS the class AND builds its closing guard; the self-referential canonical closure).
|
|
59
|
+
- **`closure`** — `guard`.
|
|
60
|
+
- **`guardEvidence`** — `{ enforcementType: ratchet, citation: tests/unit/self-action-convergence.test.ts, howCaught: the ratchet drives every registered self-action controller N ticks under a pinned sustained-pressure fixture (all accounts hot / veto never clears / no progress) and asserts the action count SETTLES to a small bound that does NOT scale with the horizon — steady-state convergence via each controller's settling brake (all-hot / projected-load / exponential-backoff+breaker / suppress-unchanged), with the declared Eternal-Sentinel exemption asserting a P19 rate floor instead. A per-tick cap would NOT satisfy this — it bounds one pass, never the loop. }`
|
|
61
|
+
|
|
62
|
+
The machine-readable counterpart lives in the commit's decision-audit entry (persisted from the instar-dev trace by `writeDecisionAudit`), which the CI class-closure lint reads. This mirror is display-only and is never summed with it (C1).
|