instar 1.3.758 → 1.3.760
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/commands/machine.d.ts.map +1 -1
- package/dist/commands/machine.js +28 -5
- package/dist/commands/machine.js.map +1 -1
- package/dist/commands/server.js +2 -2
- package/dist/commands/server.js.map +1 -1
- package/dist/core/LeaseCoordinator.d.ts +30 -0
- package/dist/core/LeaseCoordinator.d.ts.map +1 -1
- package/dist/core/LeaseCoordinator.js +45 -0
- package/dist/core/LeaseCoordinator.js.map +1 -1
- package/dist/core/MultiMachineCoordinator.d.ts +24 -5
- package/dist/core/MultiMachineCoordinator.d.ts.map +1 -1
- package/dist/core/MultiMachineCoordinator.js +58 -11
- package/dist/core/MultiMachineCoordinator.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +15 -0
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +45 -1
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/messaging/TelegramAdapter.d.ts +2 -0
- package/dist/messaging/TelegramAdapter.d.ts.map +1 -1
- package/dist/messaging/TelegramAdapter.js +65 -25
- package/dist/messaging/TelegramAdapter.js.map +1 -1
- package/dist/scaffold/templates.js +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +1 -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 +64 -64
- package/src/scaffold/templates.ts +1 -1
- package/upgrades/{1.3.758.md → 1.3.759.md} +51 -0
- package/upgrades/1.3.760.md +100 -0
- package/upgrades/side-effects/machine-coherence-guard-5b-awake-count.md +105 -0
- package/upgrades/side-effects/self-action-convergence.md +62 -0
- package/upgrades/side-effects/telegram-conservatism-pass.md +83 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selfActionRegistry.ts — the self-action controller registry (Part D1 of
|
|
3
|
+
* docs/specs/self-action-convergence.md). This is the single list the
|
|
4
|
+
* convergence ratchet (`tests/unit/self-action-convergence.test.ts`) iterates,
|
|
5
|
+
* the direct analog of `lint-no-unbounded-llm-spawn.js`'s one closed set of
|
|
6
|
+
* provider classes: one place to look, one thing the ratchet drives.
|
|
7
|
+
*
|
|
8
|
+
* WHY a modeled registry (not the live controllers): the real controllers
|
|
9
|
+
* (SubscriptionPool proactive swap, SessionReaper age-kill, PromiseBeacon) are
|
|
10
|
+
* bound to tmux, HTTP, config, and a real wall clock — undrivable
|
|
11
|
+
* deterministically. Each entry here is a FAITHFUL, minimal convergence MODEL of
|
|
12
|
+
* a real controller's trigger -> brake -> emit shape under the pinned
|
|
13
|
+
* sustained-pressure fixture (Part D2), with a pointer to the real source. The
|
|
14
|
+
* ratchet proves the BRAKE converges; the forcing lint
|
|
15
|
+
* (`scripts/lint-no-unregistered-self-action.js`) proves COMPLETENESS (a new
|
|
16
|
+
* self-action emit must register here). The two together make the closure
|
|
17
|
+
* genuine rather than opt-in.
|
|
18
|
+
*
|
|
19
|
+
* Constitution: "Capacity Safety — No Unbounded Self-Action" (the temporal twin
|
|
20
|
+
* of "Bounded Blast Radius": BBR bounds instantaneous MASS, this bounds
|
|
21
|
+
* steady-state FREQUENCY under feedback).
|
|
22
|
+
*/
|
|
23
|
+
/** A deterministic virtual clock — no real time, no randomness (a fixed adversary). */
|
|
24
|
+
export interface VirtualClock {
|
|
25
|
+
nowMs(): number;
|
|
26
|
+
advance(ms: number): void;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The sustained worst case that never clears on its own — the exact condition
|
|
30
|
+
* swap-thrash ran under. Deterministic by construction (no fuzzing): a ping-pong
|
|
31
|
+
* cannot hide behind randomness.
|
|
32
|
+
*/
|
|
33
|
+
export interface PressureFixture {
|
|
34
|
+
clock: VirtualClock;
|
|
35
|
+
/** All quota readings >= threshold, forever (the all-hot condition). */
|
|
36
|
+
everyAccountHot(): boolean;
|
|
37
|
+
/** All sessions mid-turn / carrying subagents (no idle window ever opens). */
|
|
38
|
+
everySessionBusy(): boolean;
|
|
39
|
+
/** The peer/CI/flush the loop retries against always rejects (veto never clears). */
|
|
40
|
+
targetAlwaysRejects(): boolean;
|
|
41
|
+
/** A polled quota value that LAGS real state — so a just-vacated account still reads sub-threshold. */
|
|
42
|
+
staleQuotaReading(accountId: string): number;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Where a controller's actions land. `considered` proves the fixture actually
|
|
46
|
+
* pressured the controller (a controller cannot pass by being inertly idle);
|
|
47
|
+
* `count` is the invariant subject; `perTarget` feeds the anti-ping-pong check.
|
|
48
|
+
*/
|
|
49
|
+
export interface ActionSink {
|
|
50
|
+
emit(action: {
|
|
51
|
+
verb: string;
|
|
52
|
+
target: string;
|
|
53
|
+
}): void;
|
|
54
|
+
count: number;
|
|
55
|
+
perTarget: Map<string, number>;
|
|
56
|
+
considered: number;
|
|
57
|
+
/** Virtual-clock timestamps of each emit, for the eternal-sentinel rate-floor assertion. */
|
|
58
|
+
emitTimesMs: number[];
|
|
59
|
+
}
|
|
60
|
+
/** Create a fresh sink. */
|
|
61
|
+
export declare function makeActionSink(): ActionSink;
|
|
62
|
+
export interface SelfActionController {
|
|
63
|
+
/** Stable id (greppable by the forcing lint). */
|
|
64
|
+
id: string;
|
|
65
|
+
/** The action verb — MUST contain a detector token (SELF_ACTION_VERB_TOKENS). */
|
|
66
|
+
actionVerb: string;
|
|
67
|
+
/** Real source this models (documentation + audit trail). */
|
|
68
|
+
models: string;
|
|
69
|
+
/** Build the controller under a fixture + sink; returns something with tick(). */
|
|
70
|
+
makeUnderPressure(f: PressureFixture, sink: ActionSink): {
|
|
71
|
+
tick(): void;
|
|
72
|
+
};
|
|
73
|
+
/** Proven max total actions over `ticks` under the pinned pressure. */
|
|
74
|
+
boundK: number;
|
|
75
|
+
/** Proven max actions against ANY single target (anti-ping-pong). */
|
|
76
|
+
perTargetBoundK: number;
|
|
77
|
+
/** N — the sustained-pressure horizon (ticks driven). */
|
|
78
|
+
ticks: number;
|
|
79
|
+
/** Virtual ms the clock advances each tick. */
|
|
80
|
+
tickMs: number;
|
|
81
|
+
/** A declared Eternal Sentinel (P19 exemption) — replaces the count bound with a rate-floor bound. */
|
|
82
|
+
eternalSentinel?: {
|
|
83
|
+
reason: string;
|
|
84
|
+
rateFloorMs: number;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export declare const SELF_ACTION_CONTROLLERS: SelfActionController[];
|
|
88
|
+
//# sourceMappingURL=selfActionRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selfActionRegistry.d.ts","sourceRoot":"","sources":["../../src/testing/selfActionRegistry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,uFAAuF;AACvF,MAAM,WAAW,YAAY;IAC3B,KAAK,IAAI,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,YAAY,CAAC;IACpB,wEAAwE;IACxE,eAAe,IAAI,OAAO,CAAC;IAC3B,8EAA8E;IAC9E,gBAAgB,IAAI,OAAO,CAAC;IAC5B,qFAAqF;IACrF,mBAAmB,IAAI,OAAO,CAAC;IAC/B,uGAAuG;IACvG,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;CAC9C;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACrD,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,2BAA2B;AAC3B,wBAAgB,cAAc,IAAI,UAAU,CAY3C;AAED,MAAM,WAAW,oBAAoB;IACnC,iDAAiD;IACjD,EAAE,EAAE,MAAM,CAAC;IACX,iFAAiF;IACjF,UAAU,EAAE,MAAM,CAAC;IACnB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,kFAAkF;IAClF,iBAAiB,CAAC,CAAC,EAAE,eAAe,EAAE,IAAI,EAAE,UAAU,GAAG;QAAE,IAAI,IAAI,IAAI,CAAA;KAAE,CAAC;IAC1E,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,eAAe,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAC;IACf,sGAAsG;IACtG,eAAe,CAAC,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3D;AAiKD,eAAO,MAAM,uBAAuB,EAAE,oBAAoB,EAKzD,CAAC"}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* selfActionRegistry.ts — the self-action controller registry (Part D1 of
|
|
3
|
+
* docs/specs/self-action-convergence.md). This is the single list the
|
|
4
|
+
* convergence ratchet (`tests/unit/self-action-convergence.test.ts`) iterates,
|
|
5
|
+
* the direct analog of `lint-no-unbounded-llm-spawn.js`'s one closed set of
|
|
6
|
+
* provider classes: one place to look, one thing the ratchet drives.
|
|
7
|
+
*
|
|
8
|
+
* WHY a modeled registry (not the live controllers): the real controllers
|
|
9
|
+
* (SubscriptionPool proactive swap, SessionReaper age-kill, PromiseBeacon) are
|
|
10
|
+
* bound to tmux, HTTP, config, and a real wall clock — undrivable
|
|
11
|
+
* deterministically. Each entry here is a FAITHFUL, minimal convergence MODEL of
|
|
12
|
+
* a real controller's trigger -> brake -> emit shape under the pinned
|
|
13
|
+
* sustained-pressure fixture (Part D2), with a pointer to the real source. The
|
|
14
|
+
* ratchet proves the BRAKE converges; the forcing lint
|
|
15
|
+
* (`scripts/lint-no-unregistered-self-action.js`) proves COMPLETENESS (a new
|
|
16
|
+
* self-action emit must register here). The two together make the closure
|
|
17
|
+
* genuine rather than opt-in.
|
|
18
|
+
*
|
|
19
|
+
* Constitution: "Capacity Safety — No Unbounded Self-Action" (the temporal twin
|
|
20
|
+
* of "Bounded Blast Radius": BBR bounds instantaneous MASS, this bounds
|
|
21
|
+
* steady-state FREQUENCY under feedback).
|
|
22
|
+
*/
|
|
23
|
+
/** Create a fresh sink. */
|
|
24
|
+
export function makeActionSink() {
|
|
25
|
+
const sink = {
|
|
26
|
+
count: 0,
|
|
27
|
+
considered: 0,
|
|
28
|
+
perTarget: new Map(),
|
|
29
|
+
emitTimesMs: [],
|
|
30
|
+
emit(action) {
|
|
31
|
+
sink.count += 1;
|
|
32
|
+
sink.perTarget.set(action.target, (sink.perTarget.get(action.target) ?? 0) + 1);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
return sink;
|
|
36
|
+
}
|
|
37
|
+
// ── The seeded controllers ─────────────────────────────────────────────────
|
|
38
|
+
/**
|
|
39
|
+
* proactive-swap-monitor — THE presenting case (#1035, ff3083a31). The real
|
|
40
|
+
* proactive pre-limit account swap self-triggered ~72 swaps/day via a
|
|
41
|
+
* quota-POSITIVE kill+respawn. Models the shipped "three brakes" fix
|
|
42
|
+
* (SubscriptionPool proactiveSwap antiThrash): under the pinned all-hot
|
|
43
|
+
* fixture, the ALL-HOT brake refuses every candidate swap (STAY PUT) and the
|
|
44
|
+
* projected-post-swap-load gate refuses a target that a stale reading makes look
|
|
45
|
+
* cool-but-actually-hot — so the count settles to 0, horizon-independent, not
|
|
46
|
+
* 72/day.
|
|
47
|
+
*/
|
|
48
|
+
const proactiveSwapMonitor = {
|
|
49
|
+
id: 'proactive-swap-monitor',
|
|
50
|
+
actionVerb: 'account-swap',
|
|
51
|
+
models: 'src/monitoring/SubscriptionPool.ts (proactive pre-limit swap, antiThrash all-hot + projected-load brake)',
|
|
52
|
+
boundK: 1,
|
|
53
|
+
perTargetBoundK: 1,
|
|
54
|
+
ticks: 20,
|
|
55
|
+
tickMs: 5 * 60_000, // 5 min/tick — crosses the ~45-min dwell window several times
|
|
56
|
+
makeUnderPressure(f, sink) {
|
|
57
|
+
const CANDIDATES = ['acct-A', 'acct-B'];
|
|
58
|
+
const REHYDRATION_BURST_PCT = 15; // a swap's own kill+re-hydrate lands on the destination
|
|
59
|
+
return {
|
|
60
|
+
tick() {
|
|
61
|
+
sink.considered += 1;
|
|
62
|
+
// Brake 1 — ALL-HOT: when every account reads hot, staying put beats a
|
|
63
|
+
// pointless kill+re-hydrate. Under the pinned fixture this is true
|
|
64
|
+
// forever, so the loop converges to 0 (the swap-thrash fix).
|
|
65
|
+
if (f.everyAccountHot())
|
|
66
|
+
return;
|
|
67
|
+
// Brake 2 — PROJECTED-POST-SWAP-LOAD: gate on (target current + this
|
|
68
|
+
// swap's re-hydration burst), NOT the stale current reading. A target
|
|
69
|
+
// that a lagging poll makes look cool but that this swap would itself
|
|
70
|
+
// push hot is refused — the amplifying edge that caused the ping-pong.
|
|
71
|
+
const target = CANDIDATES.find((id) => {
|
|
72
|
+
const projected = f.staleQuotaReading(id) + REHYDRATION_BURST_PCT;
|
|
73
|
+
return projected < 80;
|
|
74
|
+
});
|
|
75
|
+
if (!target)
|
|
76
|
+
return; // no MATERIALLY-cooler destination -> stay put
|
|
77
|
+
sink.emit({ verb: 'account-swap', target });
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* age-kill-backoff — the 2026-06-05 reaper age-gate that fired 17,503 identical
|
|
84
|
+
* kill requests/day (request -> veto -> re-request every 5s). Models the P19
|
|
85
|
+
* fix: exponential backoff between attempts + a breaker cap that gives up
|
|
86
|
+
* LOUDLY. Under the pinned targetAlwaysRejects fixture (the veto never clears —
|
|
87
|
+
* a protected/in-flight session), the count settles to `maxAttempts` and the
|
|
88
|
+
* breaker opens, horizon-independent.
|
|
89
|
+
*/
|
|
90
|
+
const ageKillBackoff = {
|
|
91
|
+
id: 'age-kill-backoff',
|
|
92
|
+
actionVerb: 'age-kill',
|
|
93
|
+
models: 'src/monitoring/SessionReaper.ts (age-limit kill-request; AgeKillBackoff — P19 backoff + breaker)',
|
|
94
|
+
boundK: 5,
|
|
95
|
+
perTargetBoundK: 5, // legitimately the SAME session, bounded by the breaker (not a ping-pong)
|
|
96
|
+
// Horizon chosen so N ticks FULLY reaches the breaker cap (cumulative
|
|
97
|
+
// exponential backoff to the 5th attempt is ~150s = 30 ticks); N=50 and
|
|
98
|
+
// 2N=100 both settle at 5, so the horizon-independence check is exact.
|
|
99
|
+
ticks: 50,
|
|
100
|
+
tickMs: 5_000, // 5s/tick — the original re-request cadence
|
|
101
|
+
eternalSentinel: undefined,
|
|
102
|
+
makeUnderPressure(f, sink) {
|
|
103
|
+
const MAX_ATTEMPTS = 5;
|
|
104
|
+
const BACKOFF_BASE_MS = 5_000;
|
|
105
|
+
const TARGET = 'over-age-session';
|
|
106
|
+
let attempts = 0;
|
|
107
|
+
let nextAllowedAtMs = 0;
|
|
108
|
+
let breakerOpen = false;
|
|
109
|
+
return {
|
|
110
|
+
tick() {
|
|
111
|
+
sink.considered += 1;
|
|
112
|
+
if (breakerOpen)
|
|
113
|
+
return; // P19 breaker — gave up LOUDLY (in real code: one aggregated attention item)
|
|
114
|
+
if (f.clock.nowMs() < nextAllowedAtMs)
|
|
115
|
+
return; // dwell inside the backoff window
|
|
116
|
+
sink.emit({ verb: 'requestKill', target: TARGET });
|
|
117
|
+
attempts += 1;
|
|
118
|
+
if (f.targetAlwaysRejects()) {
|
|
119
|
+
// Exponential backoff, then trip the breaker at the cap.
|
|
120
|
+
nextAllowedAtMs = f.clock.nowMs() + BACKOFF_BASE_MS * 2 ** attempts;
|
|
121
|
+
if (attempts >= MAX_ATTEMPTS)
|
|
122
|
+
breakerOpen = true;
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* promise-beacon-notify — the ⏳ "still on it, no new output" heartbeat that
|
|
130
|
+
* used to fire every tick (zero-information spam). Models the honest-progress
|
|
131
|
+
* fix (PromiseBeacon suppressUnchangedHeartbeats): under the worst case
|
|
132
|
+
* (everySessionBusy with NO genuine new progress ever), the progress heartbeat
|
|
133
|
+
* is SUPPRESSED every tick -> count 0, horizon-independent. (The sparse
|
|
134
|
+
* once-per-interval liveness line is the SEPARATE eternal-sentinel controller
|
|
135
|
+
* below — a rate-floored constant-cost heartbeat, not a bounded action.)
|
|
136
|
+
*/
|
|
137
|
+
const promiseBeaconNotify = {
|
|
138
|
+
id: 'promise-beacon-notify',
|
|
139
|
+
actionVerb: 'beacon-notify',
|
|
140
|
+
models: 'src/monitoring/PromiseBeacon.ts (suppressUnchangedHeartbeats — progress-only heartbeat)',
|
|
141
|
+
boundK: 1,
|
|
142
|
+
perTargetBoundK: 1,
|
|
143
|
+
ticks: 20,
|
|
144
|
+
tickMs: 20 * 60_000, // 20 min/tick (the relaxed base cadence)
|
|
145
|
+
makeUnderPressure(f, sink) {
|
|
146
|
+
return {
|
|
147
|
+
tick() {
|
|
148
|
+
sink.considered += 1;
|
|
149
|
+
// The worst case: work is happening (everySessionBusy) but the terminal
|
|
150
|
+
// frame never CHANGES — the exact "busy long task looks identical to a
|
|
151
|
+
// frozen one" the fix addresses. No new progress -> suppress. Emitting a
|
|
152
|
+
// filler line here is precisely the zero-info spam the fix removed.
|
|
153
|
+
const hasGenuineNewProgress = false;
|
|
154
|
+
if (hasGenuineNewProgress) {
|
|
155
|
+
sink.emit({ verb: 'notify', target: 'topic-progress' });
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* liveness-heartbeat — a DECLARED Eternal Sentinel (P19 exemption). A sparse,
|
|
163
|
+
* constant-cost "I'm alive" line on a rate FLOOR (the once-per-60m liveness the
|
|
164
|
+
* PromiseBeacon still emits). It is EXEMPT from a total-count bound by design;
|
|
165
|
+
* the ratchet asserts the P19 four conditions instead — a constant per-attempt
|
|
166
|
+
* cost + a rate floor that prevents accumulation (emits never exceed
|
|
167
|
+
* elapsed/rateFloorMs). This exercises the ratchet's eternal-sentinel path so
|
|
168
|
+
* the exemption is a tested contract, not prose.
|
|
169
|
+
*/
|
|
170
|
+
const livenessHeartbeat = {
|
|
171
|
+
id: 'liveness-heartbeat',
|
|
172
|
+
actionVerb: 'liveness-notify',
|
|
173
|
+
models: 'src/monitoring/PromiseBeacon.ts (beaconLivenessIntervalMs — sparse once-per-interval liveness line)',
|
|
174
|
+
boundK: Number.POSITIVE_INFINITY, // exempt — bounded by the rate floor, not a total count
|
|
175
|
+
perTargetBoundK: Number.POSITIVE_INFINITY,
|
|
176
|
+
ticks: 24,
|
|
177
|
+
tickMs: 20 * 60_000, // 20 min/tick
|
|
178
|
+
eternalSentinel: { reason: 'sparse constant-cost liveness heartbeat', rateFloorMs: 60 * 60_000 },
|
|
179
|
+
makeUnderPressure(f, sink) {
|
|
180
|
+
const RATE_FLOOR_MS = 60 * 60_000;
|
|
181
|
+
let lastEmitAtMs = Number.NEGATIVE_INFINITY;
|
|
182
|
+
return {
|
|
183
|
+
tick() {
|
|
184
|
+
sink.considered += 1;
|
|
185
|
+
// Constant per-attempt cost (one fixed-size line) + a hard rate floor:
|
|
186
|
+
// emit ONLY if at least RATE_FLOOR_MS has elapsed since the last line.
|
|
187
|
+
if (f.clock.nowMs() - lastEmitAtMs >= RATE_FLOOR_MS) {
|
|
188
|
+
sink.emit({ verb: 'notify', target: 'topic-liveness' });
|
|
189
|
+
sink.emitTimesMs.push(f.clock.nowMs());
|
|
190
|
+
lastEmitAtMs = f.clock.nowMs();
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
export const SELF_ACTION_CONTROLLERS = [
|
|
197
|
+
proactiveSwapMonitor,
|
|
198
|
+
ageKillBackoff,
|
|
199
|
+
promiseBeaconNotify,
|
|
200
|
+
livenessHeartbeat,
|
|
201
|
+
];
|
|
202
|
+
//# sourceMappingURL=selfActionRegistry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"selfActionRegistry.js","sourceRoot":"","sources":["../../src/testing/selfActionRegistry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instar",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.760",
|
|
4
4
|
"description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test:contract": "node scripts/run-contract-tests.js",
|
|
29
29
|
"test:contract:raw": "vitest run --config vitest.contract.config.ts",
|
|
30
30
|
"test:all": "vitest run && vitest run --config vitest.integration.config.ts && vitest run --config vitest.e2e.config.ts",
|
|
31
|
-
"lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unbounded-llm-spawn.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-sync-subprocess-chokepoint.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js && node scripts/lint-routing-registry-freshness.js && node scripts/lint-no-opus-claude-cli-gating.js && node scripts/lint-model-registry-freshness.mjs",
|
|
31
|
+
"lint": "tsc --noEmit && node scripts/lint-no-direct-destructive.js && node scripts/lint-no-direct-llm-http.js && node scripts/lint-no-direct-url-log.js && node scripts/lint-no-unfunneled-topic-creation.js && node scripts/lint-no-unfunneled-headless-launch.js && node scripts/lint-no-unbounded-llm-spawn.js && node scripts/lint-no-unregistered-self-action.js && node scripts/lint-no-unfunneled-credential-write.js && node scripts/lint-state-registry.js && node scripts/lint-store-retention-declared.js && node scripts/lint-no-wholefile-sync-read.js && node scripts/lint-cas-emit-placement.js && node scripts/lint-journal-actuation-ban.js && node scripts/lint-no-blocking-process-scans.js && node scripts/lint-sync-subprocess-chokepoint.js && node scripts/lint-dev-agent-dark-gate.js && node scripts/lint-guard-manifest.js && node scripts/lint-llm-attribution.js && node scripts/lint-no-mainthread-cartographer-walk.js && node scripts/lint-scrape-fixture-realness.js && node scripts/check-codex-rule1-drift.js && node scripts/lint-routing-registry-freshness.js && node scripts/lint-no-opus-claude-cli-gating.js && node scripts/lint-model-registry-freshness.mjs",
|
|
32
32
|
"lint:routing-registry": "node scripts/lint-routing-registry-freshness.js",
|
|
33
33
|
"lint:no-opus-claude-cli-gating": "node scripts/lint-no-opus-claude-cli-gating.js",
|
|
34
34
|
"lint:model-freshness": "node scripts/lint-model-registry-freshness.mjs",
|
|
@@ -27,6 +27,7 @@ import fs from 'node:fs';
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
|
|
29
29
|
const DECISIONS_REL = path.join('.instar', 'instar-dev-decisions');
|
|
30
|
+
const TRACES_REL = path.join('.instar', 'instar-dev-traces');
|
|
30
31
|
|
|
31
32
|
/** Parse `--key value` / `--flag` pairs into a plain object. */
|
|
32
33
|
export function parseArgs(argv) {
|
|
@@ -67,10 +68,20 @@ export function buildClassClosureBlock(args) {
|
|
|
67
68
|
if (!args.class || typeof args.class !== 'string') throw new Error('--class <id|novel> is required');
|
|
68
69
|
block.defectClass = args.class;
|
|
69
70
|
const closure = args.closure;
|
|
70
|
-
|
|
71
|
+
// 'n/a' is the NEGATIVE declaration (docs/specs/self-action-convergence.md →
|
|
72
|
+
// E3): a genuine one-shot / user-driven action, not a self-triggered loop —
|
|
73
|
+
// the trace-level analog of the D4 lint's allowlist entry. Requires a reason.
|
|
74
|
+
if (closure !== 'guard' && closure !== 'gap' && closure !== 'n/a') {
|
|
75
|
+
throw new Error("--closure must be 'guard', 'gap', or 'n/a' (negative declaration)");
|
|
76
|
+
}
|
|
71
77
|
block.closure = closure;
|
|
72
78
|
|
|
73
|
-
if (closure === '
|
|
79
|
+
if (closure === 'n/a') {
|
|
80
|
+
if (!args.reason || typeof args.reason !== 'string') {
|
|
81
|
+
throw new Error("closure 'n/a' (negative declaration) requires --reason \"<why this is a one-shot / user-driven action, not a loop>\"");
|
|
82
|
+
}
|
|
83
|
+
block.reason = args.reason;
|
|
84
|
+
} else if (closure === 'guard') {
|
|
74
85
|
if (!args.citation || typeof args.citation !== 'string') throw new Error("closure 'guard' requires --citation <path|route|symbol>");
|
|
75
86
|
block.guardEvidence = {
|
|
76
87
|
enforcementType: typeof args.enforcement === 'string' ? args.enforcement : undefined,
|
|
@@ -166,11 +177,65 @@ export function applyDeclaration(repoRoot, block) {
|
|
|
166
177
|
return { changed: true, file: target.file, block };
|
|
167
178
|
}
|
|
168
179
|
|
|
180
|
+
/** Find the most-recent instar-dev TRACE file (by mtime). */
|
|
181
|
+
export function findMostRecentTrace(repoRoot) {
|
|
182
|
+
const dir = path.join(repoRoot, TRACES_REL);
|
|
183
|
+
let files;
|
|
184
|
+
try {
|
|
185
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith('.json'));
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
if (files.length === 0) return null;
|
|
190
|
+
let best = null;
|
|
191
|
+
for (const f of files) {
|
|
192
|
+
const full = path.join(dir, f);
|
|
193
|
+
let mtime;
|
|
194
|
+
try {
|
|
195
|
+
mtime = fs.statSync(full).mtimeMs;
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (best === null || mtime > best.mtime) best = { file: full, mtime };
|
|
200
|
+
}
|
|
201
|
+
if (!best) return null;
|
|
202
|
+
let entry;
|
|
203
|
+
try {
|
|
204
|
+
entry = JSON.parse(fs.readFileSync(best.file, 'utf-8'));
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
return { file: best.file, entry };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Merge the block into the most-recent instar-dev TRACE (the E3 declaration
|
|
213
|
+
* host — the same place `specPath`/`tier`/`causalAutopsy` live, authored BEFORE
|
|
214
|
+
* the commit). Idempotent. The precommit's writeDecisionAudit then persists it
|
|
215
|
+
* into the machine-readable decision-audit entry.
|
|
216
|
+
* @returns {{ changed: boolean, file: string, block: object }}
|
|
217
|
+
*/
|
|
218
|
+
export function applyDeclarationToTrace(repoRoot, block) {
|
|
219
|
+
const target = findMostRecentTrace(repoRoot);
|
|
220
|
+
if (!target) {
|
|
221
|
+
throw new Error(`no instar-dev trace found under ${TRACES_REL} to attach the declaration to (run the /instar-dev skill first)`);
|
|
222
|
+
}
|
|
223
|
+
const before = JSON.stringify(target.entry.classClosure ?? null);
|
|
224
|
+
const after = JSON.stringify(block);
|
|
225
|
+
if (before === after) {
|
|
226
|
+
return { changed: false, file: target.file, block };
|
|
227
|
+
}
|
|
228
|
+
target.entry.classClosure = block;
|
|
229
|
+
fs.writeFileSync(target.file, `${JSON.stringify(target.entry, null, 2)}\n`, 'utf-8');
|
|
230
|
+
return { changed: true, file: target.file, block };
|
|
231
|
+
}
|
|
232
|
+
|
|
169
233
|
// ── CLI entrypoint ──────────────────────────────────────────────────────────
|
|
170
234
|
const invokedDirectly = process.argv[1] && import.meta.url.endsWith(process.argv[1].split('/').pop());
|
|
171
235
|
if (invokedDirectly) {
|
|
172
236
|
const repoRoot = process.env.CLASS_CLOSURE_REPO_ROOT || process.cwd();
|
|
173
237
|
const args = parseArgs(process.argv.slice(2));
|
|
238
|
+
const toTrace = args['to-trace'] === true;
|
|
174
239
|
let block;
|
|
175
240
|
try {
|
|
176
241
|
block = buildClassClosureBlock(args);
|
|
@@ -180,12 +245,12 @@ if (invokedDirectly) {
|
|
|
180
245
|
}
|
|
181
246
|
let result;
|
|
182
247
|
try {
|
|
183
|
-
result = applyDeclaration(repoRoot, block);
|
|
248
|
+
result = toTrace ? applyDeclarationToTrace(repoRoot, block) : applyDeclaration(repoRoot, block);
|
|
184
249
|
} catch (err) {
|
|
185
250
|
console.error(`class-closure-declare: ${err instanceof Error ? err.message : String(err)}`);
|
|
186
251
|
process.exit(2);
|
|
187
252
|
}
|
|
188
|
-
console.log(`class-closure-declare: ${result.changed ? 'wrote' : 'no change (idempotent)'} classClosure into ${path.relative(repoRoot, result.file)}`);
|
|
253
|
+
console.log(`class-closure-declare: ${result.changed ? 'wrote' : 'no change (idempotent)'} classClosure into ${path.relative(repoRoot, result.file)}${toTrace ? ' (trace host)' : ''}`);
|
|
189
254
|
console.log(JSON.stringify(result.block, null, 2));
|
|
190
255
|
process.exit(0);
|
|
191
256
|
}
|
|
@@ -54,9 +54,27 @@ import {
|
|
|
54
54
|
REGISTRY_REL_PATH,
|
|
55
55
|
} from './lib/defect-class-registry.mjs';
|
|
56
56
|
import { evaluateGuardClosure } from './lib/class-closure-grader.mjs';
|
|
57
|
+
import { isSelfActionControllerFile } from './lib/self-action-detect.mjs';
|
|
57
58
|
|
|
58
59
|
const DEFAULT_CONFIG = { enabled: false, dryRun: true, escalatorDrafting: false };
|
|
59
60
|
|
|
61
|
+
// The self-action class id + the convergence-argument regex (Part E2). A
|
|
62
|
+
// `defectClass: unbounded-self-action` + `closure: guard` declaration's
|
|
63
|
+
// guardEvidence.howCaught must ADDRESS the convergence argument (control-loop
|
|
64
|
+
// edge + steady-state bound + settling brake) — a per-tick-cap-only
|
|
65
|
+
// justification does NOT address it.
|
|
66
|
+
const SELF_ACTION_CLASS_ID = 'unbounded-self-action';
|
|
67
|
+
const CONVERGENCE_ADDRESSED = /converge|steady[- ]?state|bound|dwell|hysteresis|all[- ]?hot|projected.*load|breaker/i;
|
|
68
|
+
|
|
69
|
+
/** Read a repo file, or null (a deleted/binary path in a diff). */
|
|
70
|
+
function readFileMaybe(repoRoot, rel) {
|
|
71
|
+
try {
|
|
72
|
+
return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
60
78
|
/**
|
|
61
79
|
* The agent-authored-artifact predicate — a fix touching one of these is
|
|
62
80
|
* IN SCOPE for the class declaration (docs/specs/class-closure-gate.md →
|
|
@@ -92,7 +110,14 @@ export function isGateSourceFile(file) {
|
|
|
92
110
|
f === 'docs/defect-classes.json' ||
|
|
93
111
|
f === '.github/workflows/class-closure-gate.yml' ||
|
|
94
112
|
f === 'docs/specs/class-closure-gate.md' ||
|
|
95
|
-
/^tests\/(unit|integration)\/class-closure-/.test(f)
|
|
113
|
+
/^tests\/(unit|integration)\/class-closure-/.test(f) ||
|
|
114
|
+
// The self-action gate's own source (docs/specs/self-action-convergence.md
|
|
115
|
+
// → Part E5): a broken self-action gate can never block its own repair.
|
|
116
|
+
f === 'scripts/lint-no-unregistered-self-action.js' ||
|
|
117
|
+
f === 'scripts/lib/self-action-detect.mjs' ||
|
|
118
|
+
f === 'src/testing/selfActionRegistry.ts' ||
|
|
119
|
+
f === 'docs/specs/self-action-convergence.md' ||
|
|
120
|
+
/^tests\/(unit|integration)\/self-action-/.test(f)
|
|
96
121
|
);
|
|
97
122
|
}
|
|
98
123
|
|
|
@@ -169,6 +194,17 @@ function validateDeclaration(repoRoot, decl, knownClassIds) {
|
|
|
169
194
|
findings.push(`${where}: guard citation resolves (${verdict.gradedKind}) — closure:'guard' upheld`);
|
|
170
195
|
}
|
|
171
196
|
}
|
|
197
|
+
// Class-specific arm (Part E2): for the unbounded-self-action class, the
|
|
198
|
+
// convergence ARGUMENT must be addressed in howCaught — a per-tick-cap-only
|
|
199
|
+
// justification bounds one pass, never the loop. A hard violation when
|
|
200
|
+
// enforcing (returned to the caller so it can gate).
|
|
201
|
+
if (decl.defectClass === SELF_ACTION_CLASS_ID) {
|
|
202
|
+
const howCaught = decl.guardEvidence && decl.guardEvidence.howCaught;
|
|
203
|
+
if (typeof howCaught !== 'string' || !CONVERGENCE_ADDRESSED.test(howCaught)) {
|
|
204
|
+
findings.push(`${where}: unbounded-self-action closure:'guard' howCaught does NOT address convergence (steady-state bound / settling brake / dwell / all-hot / breaker) — a per-tick-cap-only justification is insufficient`);
|
|
205
|
+
hardViolations.push(`${where}: unbounded-self-action guard howCaught fails the convergence-addressed check (E2)`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
172
208
|
} else if (decl.closure === 'gap') {
|
|
173
209
|
if (!decl.gapItem || typeof decl.gapItem !== 'string') {
|
|
174
210
|
findings.push(`${where}: closure:'gap' should cite a tracked gap item (gapItem) — none provided`);
|
|
@@ -247,12 +283,19 @@ export function runClassClosureLint(input) {
|
|
|
247
283
|
findings.push('mirror-consistency: counted the decision-audit host only (side-effects mirror is display-only) — trivially satisfied');
|
|
248
284
|
}
|
|
249
285
|
|
|
250
|
-
// Diff-scope: does this PR touch an agent-authored artifact
|
|
286
|
+
// Diff-scope: does this PR touch an agent-authored artifact OR a self-action
|
|
287
|
+
// controller (Part E2 — the concrete realization of #1347 Frontloaded
|
|
288
|
+
// Decision 1) needing a declaration?
|
|
251
289
|
let inScope = false;
|
|
252
290
|
let exempt;
|
|
253
291
|
if (changedFiles && changedFiles.length > 0) {
|
|
254
292
|
const agentFiles = changedFiles.filter(isAgentAuthoredArtifact);
|
|
255
|
-
|
|
293
|
+
// A diff touching a self-action controller file (name-shape or marker) is
|
|
294
|
+
// in scope for the unbounded-self-action declaration.
|
|
295
|
+
const selfActionFiles = changedFiles.filter((f) =>
|
|
296
|
+
isSelfActionControllerFile(f, readFileMaybe(repoRoot, f)),
|
|
297
|
+
);
|
|
298
|
+
inScope = agentFiles.length > 0 || selfActionFiles.length > 0;
|
|
256
299
|
if (inScope) {
|
|
257
300
|
// Gate-source-ONLY self-wedge exemption: applies ONLY when the diff
|
|
258
301
|
// touches EXCLUSIVELY the gate's own source (a mixed PR cannot ride it).
|
|
@@ -260,8 +303,21 @@ export function runClassClosureLint(input) {
|
|
|
260
303
|
if (allGateSource) {
|
|
261
304
|
exempt = 'gate-source-only';
|
|
262
305
|
findings.push('exempt: diff touches EXCLUSIVELY the gate\'s own source (gate-source-only self-wedge exemption, logged)');
|
|
263
|
-
} else
|
|
264
|
-
|
|
306
|
+
} else {
|
|
307
|
+
if (declarations.length === 0) {
|
|
308
|
+
findings.push('missing class declaration (report-only) — this PR touches agent-authored artifacts or self-action controllers but carries no classClosure declaration');
|
|
309
|
+
}
|
|
310
|
+
// Enforcement condition (i) (Part E2): an in-scope SELF-ACTION diff with
|
|
311
|
+
// no unbounded-self-action declaration is a hard violation when
|
|
312
|
+
// enforcing (report-only until prGate.classClosure.dryRun:false).
|
|
313
|
+
if (selfActionFiles.length > 0) {
|
|
314
|
+
const hasSelfActionDecl = declarations.some((d) => d.defectClass === SELF_ACTION_CLASS_ID);
|
|
315
|
+
if (!hasSelfActionDecl) {
|
|
316
|
+
const msg = `self-action controller(s) touched (${selfActionFiles.join(', ')}) but no unbounded-self-action classClosure declaration is present`;
|
|
317
|
+
findings.push(`missing unbounded-self-action declaration — ${msg}`);
|
|
318
|
+
hardViolations.push(msg);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
265
321
|
}
|
|
266
322
|
}
|
|
267
323
|
}
|