instar 1.3.743 → 1.3.745
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/permissions/IntentClassifier.d.ts +10 -0
- package/dist/permissions/IntentClassifier.d.ts.map +1 -1
- package/dist/permissions/IntentClassifier.js +45 -2
- package/dist/permissions/IntentClassifier.js.map +1 -1
- package/dist/permissions/LlmIntentClassifier.d.ts +1 -1
- package/dist/permissions/LlmIntentClassifier.d.ts.map +1 -1
- package/dist/permissions/LlmIntentClassifier.js +12 -0
- package/dist/permissions/LlmIntentClassifier.js.map +1 -1
- package/dist/permissions/types.d.ts +13 -0
- package/dist/permissions/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/scripts/lint-model-registry-freshness.mjs +192 -0
- package/scripts/model-registry-freshness.manifest.json +104 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/{1.3.743.md → 1.3.744.md} +58 -0
- package/upgrades/1.3.745.md +19 -0
- package/upgrades/side-effects/member-seat-gate-fix.eli16.md +62 -0
- package/upgrades/side-effects/member-seat-gate-fix.md +92 -0
- package/upgrades/side-effects/model-registry-freshness-guard.md +53 -0
|
@@ -18,6 +18,16 @@ export interface IntentClassifier {
|
|
|
18
18
|
directed: boolean;
|
|
19
19
|
}): Promise<RequestIntent>;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Is this a harmless conversational self-post (a note / check-in / reminder / status
|
|
23
|
+
* update the bot would post into the CURRENT conversation)? Deterministic and
|
|
24
|
+
* conservative: it requires BOTH a self-post verb AND a conversational-content noun,
|
|
25
|
+
* and it disqualifies on ANY organizational-write / external / operational marker.
|
|
26
|
+
* Floor actions are already handled (and short-circuited) before this runs, so this
|
|
27
|
+
* only ever sees non-floor text. Requiring both a post-verb AND a content-noun keeps
|
|
28
|
+
* plain chatter ("just checking in, how's it going?") at T0 rather than promoting it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isConversationalSelfPost(t: string): boolean;
|
|
21
31
|
/** Does the text CLAIM an authorization from some named party ("X said it's fine")? */
|
|
22
32
|
export declare function mentionsClaimedAuthority(text: string): boolean;
|
|
23
33
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntentClassifier.d.ts","sourceRoot":"","sources":["../../src/permissions/IntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAgC,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5E;
|
|
1
|
+
{"version":3,"file":"IntentClassifier.d.ts","sourceRoot":"","sources":["../../src/permissions/IntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAgC,MAAM,YAAY,CAAC;AAE9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CAC5E;AA2CD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAI3D;AAED,uFAAuF;AACvF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAI9D;AAaD;;;;GAIG;AACH,qBAAa,yBAA0B,YAAW,gBAAgB;IAE1D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC;CAoDjF"}
|
|
@@ -27,14 +27,50 @@ const READ_VERB = /\b(summar\w*|what is|what's|look up|lookup|find out|explain|s
|
|
|
27
27
|
const WRITE_VERB = /\b(post|create|file a ticket|open a ticket|schedule|add a|write up|note)\b/;
|
|
28
28
|
const OP_VERB = /\b(run|trigger|kick off|start|rerun|re-run)\b/;
|
|
29
29
|
const OP_NOUN = /\b(job|task|script|pipeline|staging|test|tests|build)\b/;
|
|
30
|
+
// ── Conversational self-post detection (member-seat false-positive fix) ──────────
|
|
31
|
+
// A "post a check-in note here in 5 minutes" is the bot AUTHORING conversational
|
|
32
|
+
// content into the CURRENT conversation — not an organizational write. It carries
|
|
33
|
+
// no floor signal (floor detection runs FIRST and always wins) and no org-write /
|
|
34
|
+
// external side-effect. Such a request is conversational (T1 read/draft level), so
|
|
35
|
+
// an ordinary member may direct it — the earlier blanket T2 low-write classification
|
|
36
|
+
// wrongly put it above the member ceiling and refused every member.
|
|
37
|
+
//
|
|
38
|
+
// A verb that FRAMES posting/scheduling conversational content...
|
|
39
|
+
const SELF_POST_VERB = /\b(post|drop|leave|put|write|schedule|set|send|remind|share)\b/;
|
|
40
|
+
// ...applied to a conversational-content noun (a note, check-in, reminder, update…).
|
|
41
|
+
const CONVERSATIONAL_NOUN = /\b(note|notes|check[- ]?in|checkin|reminder|reminders|heads[- ]?up|update|updates|standup|stand[- ]?up|message|ping|nudge)\b/;
|
|
42
|
+
// An organizational-write or EXTERNAL side-effect marker — its presence means this is
|
|
43
|
+
// NOT a harmless conversational post but a genuine low-write/external action that must
|
|
44
|
+
// keep its higher tier (a ticket, a record, another/named channel, a doc, a calendar
|
|
45
|
+
// hold, an email, an outside party). Any match disqualifies the conversational path.
|
|
46
|
+
const ORG_WRITE_OR_EXTERNAL_MARKER = /\b(ticket|tickets|jira|linear|asana|notion|confluence|wiki|calendar|invite|record|records|doc|docs|document|documents|announcement|announcements|another channel|other channel|email|e-mail|invoice|spreadsheet|sheet|client|clients|customer|customers|vendor|vendors|external|outside|partner|press|public)\b|#[a-z0-9][\w-]*/i;
|
|
47
|
+
// An operational marker — asking the bot to RUN/DEPLOY/execute something is never a
|
|
48
|
+
// conversational post, even if phrased as "post a note to run …". Disqualifies too.
|
|
49
|
+
const OPERATIONAL_MARKER = /\b(run|trigger|execute|kick off|deploy|rerun|re-run|migrat\w+|job|pipeline|build|script)\b/;
|
|
50
|
+
/**
|
|
51
|
+
* Is this a harmless conversational self-post (a note / check-in / reminder / status
|
|
52
|
+
* update the bot would post into the CURRENT conversation)? Deterministic and
|
|
53
|
+
* conservative: it requires BOTH a self-post verb AND a conversational-content noun,
|
|
54
|
+
* and it disqualifies on ANY organizational-write / external / operational marker.
|
|
55
|
+
* Floor actions are already handled (and short-circuited) before this runs, so this
|
|
56
|
+
* only ever sees non-floor text. Requiring both a post-verb AND a content-noun keeps
|
|
57
|
+
* plain chatter ("just checking in, how's it going?") at T0 rather than promoting it.
|
|
58
|
+
*/
|
|
59
|
+
export function isConversationalSelfPost(t) {
|
|
60
|
+
if (ORG_WRITE_OR_EXTERNAL_MARKER.test(t))
|
|
61
|
+
return false;
|
|
62
|
+
if (OPERATIONAL_MARKER.test(t))
|
|
63
|
+
return false;
|
|
64
|
+
return SELF_POST_VERB.test(t) && CONVERSATIONAL_NOUN.test(t);
|
|
65
|
+
}
|
|
30
66
|
/** Does the text CLAIM an authorization from some named party ("X said it's fine")? */
|
|
31
67
|
export function mentionsClaimedAuthority(text) {
|
|
32
68
|
const t = text.toLowerCase();
|
|
33
69
|
return /\b(\w+\s+)?(said|told me|says|approved|authorized|okayed|gave the ok|signed off)\b/.test(t)
|
|
34
70
|
&& /\b(it'?s fine|approved|ok(ay)?|go ahead|do it|fine|allowed|permission)\b/.test(t);
|
|
35
71
|
}
|
|
36
|
-
function intent(action, tier, confidence, directed, floorAction) {
|
|
37
|
-
return { action, tier, floorAction, confidence, directed };
|
|
72
|
+
function intent(action, tier, confidence, directed, floorAction, conversational) {
|
|
73
|
+
return { action, tier, floorAction, confidence, directed, conversational };
|
|
38
74
|
}
|
|
39
75
|
/**
|
|
40
76
|
* Deterministic, conservative classifier. Floor detection is ordered first and
|
|
@@ -75,6 +111,13 @@ export class HeuristicIntentClassifier {
|
|
|
75
111
|
if (OP_VERB.test(t) && OP_NOUN.test(t)) {
|
|
76
112
|
return intent('operational', 3, 0.78, directed);
|
|
77
113
|
}
|
|
114
|
+
// Harmless conversational self-post (note/check-in/reminder into the current
|
|
115
|
+
// conversation, no org/external/operational marker) → T1, not the T2 low-write
|
|
116
|
+
// that wrongly refused ordinary members. Checked BEFORE the generic WRITE_VERB
|
|
117
|
+
// branch so "post a check-in note here" isn't swept up as a low-write.
|
|
118
|
+
if (isConversationalSelfPost(t)) {
|
|
119
|
+
return intent('conversational-post', 1, 0.78, directed, undefined, true);
|
|
120
|
+
}
|
|
78
121
|
if (WRITE_VERB.test(t)) {
|
|
79
122
|
return intent('low-write', 2, 0.78, directed);
|
|
80
123
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IntentClassifier.js","sourceRoot":"","sources":["../../src/permissions/IntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAQH,MAAM,WAAW,GAAG,sGAAsG,CAAC;AAC3H,MAAM,SAAS,GAAG,6CAA6C,CAAC;AAChE,MAAM,KAAK,GAAG,wEAAwE,CAAC;AACvF,MAAM,SAAS,GAAG,+EAA+E,CAAC;AAClG,MAAM,SAAS,GAAG,wEAAwE,CAAC;AAC3F,MAAM,gBAAgB,GAAG,iEAAiE,CAAC;AAC3F,MAAM,gBAAgB,GAAG,0FAA0F,CAAC;AACpH,MAAM,UAAU,GAAG,2CAA2C,CAAC;AAC/D,MAAM,UAAU,GAAG,2EAA2E,CAAC;AAC/F,MAAM,aAAa,GAAG,+CAA+C,CAAC;AACtE,MAAM,aAAa,GAAG,uGAAuG,CAAC;AAE9H,MAAM,SAAS,GAAG,iHAAiH,CAAC;AACpI,MAAM,UAAU,GAAG,4EAA4E,CAAC;AAChG,MAAM,OAAO,GAAG,+CAA+C,CAAC;AAChE,MAAM,OAAO,GAAG,yDAAyD,CAAC;AAE1E,uFAAuF;AACvF,MAAM,UAAU,wBAAwB,CAAC,IAAY;IACnD,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAC7B,OAAO,oFAAoF,CAAC,IAAI,CAAC,CAAC,CAAC;WAC9F,0EAA0E,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED,SAAS,MAAM,CACb,MAAc,EACd,IAAqB,EACrB,UAAkB,EAClB,QAAiB,EACjB,WAAyB;
|
|
1
|
+
{"version":3,"file":"IntentClassifier.js","sourceRoot":"","sources":["../../src/permissions/IntentClassifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,7 +42,7 @@ import type { IntelligenceProvider } from '../core/types.js';
|
|
|
42
42
|
import type { RequestIntent } from './types.js';
|
|
43
43
|
import { type IntentClassifier } from './IntentClassifier.js';
|
|
44
44
|
/** Observability hook reasons for why a classification fell back to the heuristic. */
|
|
45
|
-
export type LlmIntentDegradeReason = 'no-intelligence' | 'error' | 'unparseable' | 'floor-deterministic';
|
|
45
|
+
export type LlmIntentDegradeReason = 'no-intelligence' | 'error' | 'unparseable' | 'floor-deterministic' | 'conversational-deterministic';
|
|
46
46
|
export interface LlmIntentClassifierDeps {
|
|
47
47
|
/**
|
|
48
48
|
* The internal LLM provider (injected — never a direct framework import). When
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LlmIntentClassifier.d.ts","sourceRoot":"","sources":["../../src/permissions/LlmIntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,YAAY,CAAC;AACjE,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzF,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,OAAO,GACP,aAAa,GACb,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"LlmIntentClassifier.d.ts","sourceRoot":"","sources":["../../src/permissions/LlmIntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,YAAY,CAAC;AACjE,OAAO,EAA6B,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzF,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAC9B,iBAAiB,GACjB,OAAO,GACP,aAAa,GACb,qBAAqB,GACrB,8BAA8B,CAAC;AAEnC,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;IAC7B,uDAAuD;IACvD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,CAAC;CACtD;AAYD,qBAAa,mBAAoB,YAAW,gBAAgB;IAC1D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAuB;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC,CAA2C;gBAE1D,IAAI,GAAE,uBAA4B;IAOxC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,aAAa,CAAC;IAuEhF,OAAO,CAAC,MAAM;CAOf"}
|
|
@@ -64,6 +64,18 @@ export class LlmIntentClassifier {
|
|
|
64
64
|
this.report('floor-deterministic');
|
|
65
65
|
return floorRead;
|
|
66
66
|
}
|
|
67
|
+
// Symmetric to the floor: a recognized-benign CONVERSATIONAL self-post (a
|
|
68
|
+
// note/check-in/reminder into the current conversation — deterministically
|
|
69
|
+
// cleared of any floor/org-write/external/operational marker) is returned AS-IS
|
|
70
|
+
// and the LLM is never consulted. This is the member-seat fix's load-bearing
|
|
71
|
+
// half: reconcile() only ever ESCALATES the tier (Math.max), so without this
|
|
72
|
+
// short-circuit the LLM would re-classify "post a note" up to T2 and re-refuse
|
|
73
|
+
// the member. The deterministic conversational read is the authority here, just
|
|
74
|
+
// as the deterministic floor read is above.
|
|
75
|
+
if (floorRead.conversational) {
|
|
76
|
+
this.report('conversational-deterministic');
|
|
77
|
+
return floorRead;
|
|
78
|
+
}
|
|
67
79
|
// ── 2. No provider → fail closed to the heuristic ───────────────────────────
|
|
68
80
|
if (!this.intelligence) {
|
|
69
81
|
this.report('no-intelligence');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LlmIntentClassifier.js","sourceRoot":"","sources":["../../src/permissions/LlmIntentClassifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,EAAE,yBAAyB,EAAyB,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"LlmIntentClassifier.js","sourceRoot":"","sources":["../../src/permissions/LlmIntentClassifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -63,6 +63,19 @@ export interface RequestIntent {
|
|
|
63
63
|
* request is never authorized.
|
|
64
64
|
*/
|
|
65
65
|
directed: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* True iff this is a recognized HARMLESS CONVERSATIONAL self-post — the bot
|
|
68
|
+
* authoring a note / check-in / reminder / status update into the CURRENT
|
|
69
|
+
* conversation, carrying NO floor signal and NO organizational-write or external
|
|
70
|
+
* side-effect (a ticket, a record, another channel, a calendar hold, an email).
|
|
71
|
+
* Such a request is conversational, not an authority-requiring org action, so it
|
|
72
|
+
* is classified at the read/draft tier (T1) rather than the low-write tier (T2)
|
|
73
|
+
* — an ordinary member may direct it. This is set DETERMINISTICALLY by the
|
|
74
|
+
* heuristic (floor detection still runs first and always wins) and, like the
|
|
75
|
+
* floor, is NOT subject to upward re-classification by the LLM judgment band
|
|
76
|
+
* (see LlmIntentClassifier). Absent/false for everything else.
|
|
77
|
+
*/
|
|
78
|
+
conversational?: boolean;
|
|
66
79
|
}
|
|
67
80
|
/** Relationship/behavioral anomaly assessment for this principal+request. */
|
|
68
81
|
export interface AnomalyAssessment {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/permissions/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1F,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EAOvC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEhD,+EAA+E;AAC/E,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,gBAAgB,GAChB,aAAa,GACb,mBAAmB,GACnB,kBAAkB,GAClB,eAAe,GACf,iBAAiB,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,IAAI,EAAE,OAAO,CAAC;IACd,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,eAAe,CAAC;IACtB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/permissions/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,uDAAuD;AACvD,MAAM,MAAM,OAAO,GAAG,OAAO,GAAG,QAAQ,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,OAAO,CAAC;AAE1F,eAAO,MAAM,SAAS,EAAE,SAAS,OAAO,EAOvC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEhD,+EAA+E;AAC/E,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E;;;;GAIG;AACH,MAAM,MAAM,WAAW,GACnB,gBAAgB,GAChB,aAAa,GACb,mBAAmB,GACnB,kBAAkB,GAClB,eAAe,GACf,iBAAiB,CAAC;AAEtB;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,IAAI,EAAE,OAAO,CAAC;IACd,+DAA+D;IAC/D,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,iEAAiE;AACjE,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,eAAe,CAAC;IACtB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,wDAAwD;IACxD,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;;;OAKG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,mGAAmG;IACnG,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,MAAM,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,SAAS,EAAE,SAAS,CAAC;IACrB,MAAM,EAAE,aAAa,CAAC;IACtB,OAAO,CAAC,EAAE,iBAAiB,CAAC;IAC5B,qBAAqB;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,8FAA8F;AAC9F,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,KAAK,EAAE,WAAW,GAAG,QAAQ,eAAe,EAAE,CAAC;IAC/C,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;CACnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instar",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.745",
|
|
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,9 +28,11 @@
|
|
|
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",
|
|
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",
|
|
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
|
+
"lint:model-freshness": "node scripts/lint-model-registry-freshness.mjs",
|
|
35
|
+
"lint:model-freshness:strict": "INSTAR_MODEL_FRESHNESS_STRICT=1 node scripts/lint-model-registry-freshness.mjs",
|
|
34
36
|
"lint:no-unbounded-llm-spawn": "node scripts/lint-no-unbounded-llm-spawn.js",
|
|
35
37
|
"lint:no-unbounded-llm-spawn:staged": "node scripts/lint-no-unbounded-llm-spawn.js --staged",
|
|
36
38
|
"lint:destructive": "node scripts/lint-no-direct-destructive.js",
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lint-model-registry-freshness.mjs — the model-registry FRESHNESS ratchet.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM (2026-07-03, operator directive): Instar's per-provider "capable/
|
|
6
|
+
* latest/frontier" model pins rot SILENTLY. Nothing forces anyone to re-check
|
|
7
|
+
* that (e.g.) the gemini `capable` tier still points at a current model — so
|
|
8
|
+
* `gemini-2.5-pro` kept routing spec-review/converge work long after
|
|
9
|
+
* Gemini 3-class shipped. A stale pin is invisible until it degrades output.
|
|
10
|
+
*
|
|
11
|
+
* THE GUARD (model-id-AGNOSTIC by construction): a deterministic check with
|
|
12
|
+
* TWO teeth, both driven by scripts/model-registry-freshness.manifest.json —
|
|
13
|
+
* the single human-edit surface:
|
|
14
|
+
*
|
|
15
|
+
* TOOTH 1 — STALENESS. `lastReviewedAt` must be within `stalenessWindowDays`
|
|
16
|
+
* of today. An un-reviewed list ages out and fails LOUDLY, forcing a
|
|
17
|
+
* periodic "is each pin still frontier?" review + a date bump. This is the
|
|
18
|
+
* anti-rot mechanism: it fails even if no id ever changes.
|
|
19
|
+
*
|
|
20
|
+
* TOOTH 2 — DRIFT. Every pinned capable/latest model id (extracted live from
|
|
21
|
+
* the real source files via each pin's regex) must be a member of that
|
|
22
|
+
* door's `frontierAllowlist` (the CURRENT_FRONTIER_MODELS set). A pin that
|
|
23
|
+
* names a model not in the maintained allowlist fails — either the pin is
|
|
24
|
+
* stale, or the allowlist wasn't updated. To go green a human must reconcile
|
|
25
|
+
* the two, which IS the review.
|
|
26
|
+
*
|
|
27
|
+
* Plus: `flaggedStale[]` entries (known-stale-pending-operator-confirmation) are
|
|
28
|
+
* always printed as WARN lines and, under strict enforcement, count as findings.
|
|
29
|
+
*
|
|
30
|
+
* The guard NEVER hard-codes what the "right" model id is — it only asserts the
|
|
31
|
+
* pins and the allowlist agree and that the review is fresh. Swapping to a new
|
|
32
|
+
* frontier id is a manifest edit (allowlist + date), never a code change here.
|
|
33
|
+
*
|
|
34
|
+
* ENFORCEMENT (dark/reversible): manifest `enforcement` field —
|
|
35
|
+
* "report" (default) — prints findings, ALWAYS exits 0 (non-gating). Safe to
|
|
36
|
+
* wire into CI today while the current list is known-stale: it stays VISIBLE
|
|
37
|
+
* in the lint log without breaking the build.
|
|
38
|
+
* "strict" — exits 1 on any finding (staleness, drift, or a flaggedStale row).
|
|
39
|
+
* Flip here once the flagged door swaps are operator-confirmed + applied.
|
|
40
|
+
* Env INSTAR_MODEL_FRESHNESS_STRICT=1 forces strict for a one-off run.
|
|
41
|
+
*
|
|
42
|
+
* Exit codes:
|
|
43
|
+
* 0 — no findings, OR findings under "report" enforcement (non-gating).
|
|
44
|
+
* 1 — findings under "strict" enforcement, or a manifest/parse error.
|
|
45
|
+
*
|
|
46
|
+
* Usage:
|
|
47
|
+
* node scripts/lint-model-registry-freshness.mjs # honor manifest enforcement
|
|
48
|
+
* INSTAR_MODEL_FRESHNESS_STRICT=1 node scripts/lint-model-registry-freshness.mjs
|
|
49
|
+
*
|
|
50
|
+
* Test overrides:
|
|
51
|
+
* INSTAR_MODEL_FRESHNESS_MANIFEST=<path> # point at a fixture manifest
|
|
52
|
+
* INSTAR_MODEL_FRESHNESS_ROOT=<path> # resolve pin files under this root
|
|
53
|
+
* INSTAR_MODEL_FRESHNESS_NOW=<ISO date> # inject the clock (staleness tests)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import fs from 'node:fs';
|
|
57
|
+
import path from 'node:path';
|
|
58
|
+
import { fileURLToPath } from 'node:url';
|
|
59
|
+
|
|
60
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
61
|
+
const REPO_ROOT = process.env.INSTAR_MODEL_FRESHNESS_ROOT
|
|
62
|
+
? path.resolve(process.env.INSTAR_MODEL_FRESHNESS_ROOT)
|
|
63
|
+
: path.resolve(__dirname, '..');
|
|
64
|
+
const MANIFEST_PATH = process.env.INSTAR_MODEL_FRESHNESS_MANIFEST
|
|
65
|
+
? path.resolve(process.env.INSTAR_MODEL_FRESHNESS_MANIFEST)
|
|
66
|
+
: path.join(__dirname, 'model-registry-freshness.manifest.json');
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Run the freshness check. Pure over its inputs (manifest + files under root +
|
|
70
|
+
* the injected clock), so the unit test drives it directly with fixtures.
|
|
71
|
+
* @returns {{ findings: string[], warnings: string[], info: string[], strict: boolean, error: string|null }}
|
|
72
|
+
*/
|
|
73
|
+
export function checkModelRegistryFreshness({
|
|
74
|
+
manifestPath = MANIFEST_PATH,
|
|
75
|
+
repoRoot = REPO_ROOT,
|
|
76
|
+
now = process.env.INSTAR_MODEL_FRESHNESS_NOW
|
|
77
|
+
? new Date(process.env.INSTAR_MODEL_FRESHNESS_NOW)
|
|
78
|
+
: new Date(),
|
|
79
|
+
forceStrict = process.env.INSTAR_MODEL_FRESHNESS_STRICT === '1',
|
|
80
|
+
} = {}) {
|
|
81
|
+
const findings = [];
|
|
82
|
+
const warnings = [];
|
|
83
|
+
const info = [];
|
|
84
|
+
|
|
85
|
+
let manifest;
|
|
86
|
+
try {
|
|
87
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
88
|
+
} catch (e) {
|
|
89
|
+
return { findings: [], warnings: [], info: [], strict: true, error: `cannot read/parse manifest ${manifestPath}: ${e.message}` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const strict = forceStrict || manifest.enforcement === 'strict';
|
|
93
|
+
|
|
94
|
+
// --- TOOTH 1: staleness ---
|
|
95
|
+
const reviewedRaw = manifest.lastReviewedAt;
|
|
96
|
+
const reviewed = reviewedRaw ? new Date(reviewedRaw) : null;
|
|
97
|
+
const windowDays = Number(manifest.stalenessWindowDays);
|
|
98
|
+
if (!reviewed || Number.isNaN(reviewed.getTime())) {
|
|
99
|
+
findings.push(`STALENESS: manifest.lastReviewedAt is missing or unparseable (got ${JSON.stringify(reviewedRaw)}).`);
|
|
100
|
+
} else if (!Number.isFinite(windowDays) || windowDays <= 0) {
|
|
101
|
+
findings.push(`STALENESS: manifest.stalenessWindowDays is missing or invalid (got ${JSON.stringify(manifest.stalenessWindowDays)}).`);
|
|
102
|
+
} else {
|
|
103
|
+
const ageDays = Math.floor((now.getTime() - reviewed.getTime()) / 86_400_000);
|
|
104
|
+
if (ageDays > windowDays) {
|
|
105
|
+
findings.push(
|
|
106
|
+
`STALENESS: model registry last reviewed ${reviewedRaw} (${ageDays}d ago) exceeds the ${windowDays}d window. ` +
|
|
107
|
+
`Re-review each capable/latest pin against current frontier, update frontierAllowlist if a model has moved, then bump lastReviewedAt.`
|
|
108
|
+
);
|
|
109
|
+
} else {
|
|
110
|
+
info.push(`Staleness OK: reviewed ${reviewedRaw} (${ageDays}d ago, window ${windowDays}d).`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- TOOTH 2: drift (pin id must be in its door's frontier allowlist) ---
|
|
115
|
+
const allowlist = manifest.frontierAllowlist || {};
|
|
116
|
+
for (const pin of manifest.pins || []) {
|
|
117
|
+
const abs = path.join(repoRoot, pin.file);
|
|
118
|
+
let src;
|
|
119
|
+
try {
|
|
120
|
+
src = fs.readFileSync(abs, 'utf8');
|
|
121
|
+
} catch {
|
|
122
|
+
findings.push(`DRIFT: pin '${pin.id}' references ${pin.file} which is missing under ${repoRoot} (a pin site moved or was deleted — re-anchor it).`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let re;
|
|
126
|
+
try {
|
|
127
|
+
re = new RegExp(pin.regex);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
findings.push(`DRIFT: pin '${pin.id}' has an invalid regex (${e.message}).`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const m = src.match(re);
|
|
133
|
+
if (!m) {
|
|
134
|
+
findings.push(`DRIFT: pin '${pin.id}' pattern did not match in ${pin.file} (the pinned site changed shape — re-anchor the regex).`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// All capture groups are candidate model ids (e.g. default + escalated).
|
|
138
|
+
const ids = m.slice(1).filter(Boolean);
|
|
139
|
+
const doorAllow = allowlist[pin.door] || [];
|
|
140
|
+
for (const id of ids) {
|
|
141
|
+
if (!doorAllow.includes(id)) {
|
|
142
|
+
findings.push(
|
|
143
|
+
`DRIFT: pin '${pin.id}' (${pin.door}) pins '${id}' in ${pin.file}, which is NOT in frontierAllowlist['${pin.door}'] = [${doorAllow.join(', ')}]. ` +
|
|
144
|
+
`Either the pin is stale or the allowlist wasn't updated — reconcile the two (operator-confirm the frontier id).`
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
info.push(`Drift OK: ${pin.door} '${pin.id}' -> '${id}' (in allowlist).`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// --- flaggedStale (known-stale-pending-confirmation): always WARN; strict counts them ---
|
|
153
|
+
for (const f of manifest.flaggedStale || []) {
|
|
154
|
+
const line =
|
|
155
|
+
`FLAGGED-STALE: ${f.door} pin '${f.pin}' currently '${f.currentId}' -> suspected frontier '${f.suspectedFrontier}'. ` +
|
|
156
|
+
`${f.evidence || ''} ${f.note || ''}`.trim();
|
|
157
|
+
warnings.push(line);
|
|
158
|
+
if (strict) findings.push(line);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { findings, warnings, info, strict, error: null };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// --- CLI entry ---
|
|
165
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
166
|
+
if (isMain) {
|
|
167
|
+
const res = checkModelRegistryFreshness();
|
|
168
|
+
const B = '\x1b[1m', R = '\x1b[0m', Y = '\x1b[33m', RED = '\x1b[31m', G = '\x1b[32m';
|
|
169
|
+
console.log(`${B}[lint-model-registry-freshness]${R} enforcement=${res.strict ? 'strict' : 'report'}`);
|
|
170
|
+
|
|
171
|
+
if (res.error) {
|
|
172
|
+
console.error(`${RED}ERROR:${R} ${res.error}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
for (const i of res.info) console.log(` ${G}ok${R} ${i}`);
|
|
176
|
+
for (const w of res.warnings) console.log(` ${Y}warn${R} ${w}`);
|
|
177
|
+
for (const f of res.findings) console.log(` ${RED}FIND${R} ${f}`);
|
|
178
|
+
|
|
179
|
+
if (res.findings.length === 0) {
|
|
180
|
+
console.log(`${G}PASS${R} — model registry pins fresh and in-allowlist.`);
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
if (res.strict) {
|
|
184
|
+
console.error(`${RED}FAIL${R} — ${res.findings.length} finding(s) under strict enforcement.`);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
console.log(
|
|
188
|
+
`${Y}REPORT-ONLY${R} — ${res.findings.length} finding(s) surfaced but NOT gating (manifest enforcement="report"). ` +
|
|
189
|
+
`Resolve them, then flip enforcement to "strict".`
|
|
190
|
+
);
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Frontier-model allowlist + staleness gate for Instar's per-provider 'capable/latest' model pins. This file is the SINGLE human-edit surface that keeps the model registry from silently rotting. See docs/LLM-ROUTING-REGISTRY.md and scripts/lint-model-registry-freshness.mjs. Two teeth: (1) STALENESS — lastReviewedAt must be within stalenessWindowDays; (2) DRIFT — every pinned capable model id must be a member of frontierAllowlist[door]. To pass the check a human must confirm each capable pin is genuinely current frontier AND bump lastReviewedAt. An un-reviewed list fails loudly.",
|
|
3
|
+
"lastReviewedAt": "2026-07-03",
|
|
4
|
+
"stalenessWindowDays": 45,
|
|
5
|
+
"enforcement": "report",
|
|
6
|
+
"$enforcementNote": "'report' = non-gating (prints findings, always exits 0) — the dark/reversible default while the current list is known-stale. Flip to 'strict' (exits 1 on any finding) ONLY after the flaggedStale door swaps below are operator-confirmed and applied. Env INSTAR_MODEL_FRESHNESS_STRICT=1 forces strict for a one-off run.",
|
|
7
|
+
"doors": {
|
|
8
|
+
"claude-code": {
|
|
9
|
+
"accessMethod": "Claude Code CLI (subscription/OAuth)",
|
|
10
|
+
"status": "alive",
|
|
11
|
+
"note": "Primary door. Tier aliases opus/sonnet/haiku auto-resolve to latest and do NOT rot; only concrete claude-*-N ids below are pinned."
|
|
12
|
+
},
|
|
13
|
+
"anthropic-headless": {
|
|
14
|
+
"accessMethod": "claude -p one-shot (Agent SDK credit pot)",
|
|
15
|
+
"status": "alive",
|
|
16
|
+
"note": "Background/internal calls. Billing shifts to interactive pool after 2026-06-15 per subscriptionPath."
|
|
17
|
+
},
|
|
18
|
+
"codex-cli": {
|
|
19
|
+
"accessMethod": "codex exec (ChatGPT account)",
|
|
20
|
+
"status": "referenced-not-installed",
|
|
21
|
+
"note": "codex CLI is NOT installed on this machine (which codex -> not found), yet componentFrameworks routes job/Usher/TopicIntentExtractor to it -> those fall back at runtime. Door-liveness gap flagged to operator."
|
|
22
|
+
},
|
|
23
|
+
"gemini-cli": {
|
|
24
|
+
"accessMethod": "gemini -m <model> (Homebrew formula, OAuth)",
|
|
25
|
+
"status": "alive-but-deprecated-formula",
|
|
26
|
+
"note": "Homebrew gemini-cli formula DEPRECATED (upstream-unsupported, disabled 2026-12-18). CLI v0.25.2 still runs. Default model still gemini-2.5-pro; CLI already knows gemini-3-pro-preview / gemini-3-flash-preview. Migrate off the deprecated formula (npm @google/gemini-cli, or an OpenRouter/pi door)."
|
|
27
|
+
},
|
|
28
|
+
"pi-cli": {
|
|
29
|
+
"accessMethod": "pi --model <provider>/<id> (multi-provider: openai-codex, openrouter, ...)",
|
|
30
|
+
"status": "alive",
|
|
31
|
+
"note": "Multi-provider consolidation door. Can reach OpenRouter (one door to many frontier models; vault key metered_openrouter_bench present). pi policy DENIES anthropic/claude via openrouter passthrough by default to protect subscription billing."
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"frontierAllowlist": {
|
|
35
|
+
"$comment": "CURRENT_FRONTIER_MODELS — the set of model ids considered acceptable-current for each door. A pinned capable model NOT in its door's list fails the DRIFT tooth. Add a new frontier id here (operator-confirmed) at the same time you update the pin.",
|
|
36
|
+
"claude-code": [
|
|
37
|
+
"claude-opus-4-8",
|
|
38
|
+
"claude-fable-5"
|
|
39
|
+
],
|
|
40
|
+
"anthropic-headless": [
|
|
41
|
+
"claude-opus-4-6",
|
|
42
|
+
"claude-sonnet-4-6",
|
|
43
|
+
"claude-haiku-4-5"
|
|
44
|
+
],
|
|
45
|
+
"codex-cli": [
|
|
46
|
+
"gpt-5.5"
|
|
47
|
+
],
|
|
48
|
+
"gemini-cli": [
|
|
49
|
+
"gemini-2.5-pro"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
"pins": [
|
|
53
|
+
{
|
|
54
|
+
"id": "gemini-capable-tier",
|
|
55
|
+
"door": "gemini-cli",
|
|
56
|
+
"tier": "capable",
|
|
57
|
+
"file": "src/providers/adapters/gemini-cli/models.ts",
|
|
58
|
+
"regex": "capable:\\s*'((?:claude|gpt|gemini)-[^']+)'",
|
|
59
|
+
"note": "TIER_TO_MODEL.capable in the gemini adapter."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"id": "codex-capable-tier",
|
|
63
|
+
"door": "codex-cli",
|
|
64
|
+
"tier": "capable",
|
|
65
|
+
"file": "src/providers/adapters/openai-codex/models.ts",
|
|
66
|
+
"regex": "capable:\\s*'((?:claude|gpt|gemini)-[^']+)'",
|
|
67
|
+
"note": "TIER_TO_MODEL.capable in the codex adapter."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "anthropic-headless-capable-tier",
|
|
71
|
+
"door": "anthropic-headless",
|
|
72
|
+
"tier": "capable",
|
|
73
|
+
"file": "src/providers/adapters/anthropic-headless/models.ts",
|
|
74
|
+
"regex": "capable:\\s*'((?:claude|gpt|gemini)-[^']+)'",
|
|
75
|
+
"note": "TIER_TO_MODEL.capable in the anthropic-headless adapter."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"id": "claude-tier-escalation-default-escalated",
|
|
79
|
+
"door": "claude-code",
|
|
80
|
+
"tier": "capable",
|
|
81
|
+
"file": "src/core/ModelTierEscalation.ts",
|
|
82
|
+
"regex": "'claude-code':\\s*\\{\\s*default:\\s*'([^']+)',\\s*escalated:\\s*'([^']+)'",
|
|
83
|
+
"note": "DEFAULT_TIER_ESCALATION.frameworks['claude-code'] default + escalated ids."
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"flaggedStale": [
|
|
87
|
+
{
|
|
88
|
+
"door": "gemini-cli",
|
|
89
|
+
"pin": "gemini-capable-tier",
|
|
90
|
+
"currentId": "gemini-2.5-pro",
|
|
91
|
+
"suspectedFrontier": "gemini-3-pro-preview",
|
|
92
|
+
"evidence": "gemini CLI v0.25.2 accepts gemini-3-pro-preview / gemini-3-flash-preview (in gemini-cli-core known-models); operator referenced 'Gemini 3-class'. Homebrew formula also deprecated (disabled 2026-12-18).",
|
|
93
|
+
"note": "OPERATOR TO CONFIRM exact frontier id before the pin+allowlist are updated. Also present at src/core/frameworkSessionLaunch.ts, src/commands/route.ts (KNOWN_MODELS), src/server/routes.ts."
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"door": "codex-cli",
|
|
97
|
+
"pin": "codex-capable-tier",
|
|
98
|
+
"currentId": "gpt-5.5",
|
|
99
|
+
"suspectedFrontier": "gpt-5.6 (Sol) — UNVERIFIED",
|
|
100
|
+
"evidence": "codex CLI NOT installed on this machine, so newer accepted ids could NOT be probed. Operator referenced 'GPT-5.6 Sol'.",
|
|
101
|
+
"note": "OPERATOR TO CONFIRM. Cannot verify from a live probe (codex door not installed)."
|
|
102
|
+
}
|
|
103
|
+
]
|
|
104
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-07-04T00:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-07-04T00:24:28.139Z",
|
|
5
|
+
"instarVersion": "1.3.745",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -5,6 +5,33 @@
|
|
|
5
5
|
|
|
6
6
|
## What Changed
|
|
7
7
|
|
|
8
|
+
Fixes the member-seat permission-gate false-positive (fb-e5b8b021-b74). With the
|
|
9
|
+
Slack outbound/permission enforcement gate ON, an ordinary workspace MEMBER's
|
|
10
|
+
harmless conversational asks (e.g. "post a check-in note here in 5 minutes") were
|
|
11
|
+
classified as a tier-2 "low-write" — above the member ceiling — and refused with
|
|
12
|
+
an authority challenge ("above what a member can authorize"). The effect was that
|
|
13
|
+
ordinary members effectively could not talk to the bot at all while enforcement
|
|
14
|
+
was on; admin-seat asks still worked.
|
|
15
|
+
|
|
16
|
+
Root cause: the intent classifier treated any write-verb message (post / note /
|
|
17
|
+
schedule) as a tier-2 organizational write, including the bot simply posting a
|
|
18
|
+
conversational note into the CURRENT conversation — which is not an organizational
|
|
19
|
+
action. On the production LLM path, the tier-reconcile step only ever escalates
|
|
20
|
+
the tier, so it could not correct the over-classification.
|
|
21
|
+
|
|
22
|
+
Fix: recognize a harmless conversational self-post — a note / check-in / reminder
|
|
23
|
+
/ status update the bot would post into the current conversation, when it is
|
|
24
|
+
deterministically cleared of any floor, organizational-write, external, or
|
|
25
|
+
operational marker — and classify it at tier 1 (the read/draft level a member may
|
|
26
|
+
direct). A recognized conversational self-post also short-circuits the LLM
|
|
27
|
+
(symmetric to the existing floor short-circuit) so the judgment band cannot
|
|
28
|
+
re-escalate it. This is a precision fix, not a floor removal: floor detection
|
|
29
|
+
(money, prod-deploy, credentials, destructive, external send, grant authority)
|
|
30
|
+
runs first and always wins; a genuine low-write (file a ticket), an operational
|
|
31
|
+
action (run a job), and every floor action are still refused for a member exactly
|
|
32
|
+
as before; the name-in-content trap is intact; and a guest still cannot direct
|
|
33
|
+
actions.
|
|
34
|
+
|
|
8
35
|
INSTAR-Bench v3 established the "door penalty": the identical Opus 4.8 model scores 99.1% on bounded judging via a clean API but only 81.7% through the Claude Code CLI door (73% on emergency-stop) — the CLI harness's ~20k-token coding-agent framing turns a skeptical judge credulous. Rules R1/R2 forbid routing any bounded/gating verdict through that door. This ships the three low-risk, dark/reversible pieces of applying that finding to routing, without moving any live routing default (the nature-axis router is separately spec-gated):
|
|
9
36
|
|
|
10
37
|
- **S2 — safety guardrail (the load-bearing piece).** The IntelligenceRouter's failure-swap now clamps a bounded/gating swap that lands on `claude-code` from the `capable` tier (Opus) down to `balanced` (Sonnet 4.6 CLI — 99.5%, 28/28 adversarial). It only ever NARROWS a fallback in the safe direction; it never blocks a call and never touches the open-ended-writing quality lane where Opus-via-CLI is the legitimate primary. A new lint (`lint-no-opus-claude-cli-gating.js`) keeps the clamp intact and refuses any committed config that routes a gating call to Opus×claude-CLI.
|
|
@@ -13,10 +40,21 @@ INSTAR-Bench v3 established the "door penalty": the identical Opus 4.8 model sco
|
|
|
13
40
|
|
|
14
41
|
## What to Tell Your User
|
|
15
42
|
|
|
43
|
+
- **Members can talk to the bot again**: "If you turned on Slack permission
|
|
44
|
+
enforcement and found that ordinary members were getting blocked when they
|
|
45
|
+
asked me to post a quick note or check-in, that's fixed. Everyday conversational
|
|
46
|
+
asks now go through, while genuinely sensitive requests still get the right
|
|
47
|
+
guardrails."
|
|
48
|
+
|
|
16
49
|
Under the hood I tightened how I pick which AI model runs my background safety checks. A benchmark found that one strong model becomes unreliable at yes/no judging when it's called through a particular tool door, so I added an automatic safeguard: if one of those checks ever falls back to that door, I quietly step down to a model that stays sharp there. It only ever makes the fallback safer — nothing you see changes, and I never switch the model your actual conversations run on without asking. I also added build-time checks so this discipline can't quietly rot, and an off-by-default monthly job that can flag a routing change for you to review — but a change like that always waits for your say-so.
|
|
17
50
|
|
|
18
51
|
## Summary of New Capabilities
|
|
19
52
|
|
|
53
|
+
| Capability | How to Use |
|
|
54
|
+
|-----------|-----------|
|
|
55
|
+
| Members can make harmless conversational asks under enforcement | automatic |
|
|
56
|
+
| Privileged and organizational actions still gated for members | automatic |
|
|
57
|
+
|
|
20
58
|
- A structural clamp making the measured-banned Opus×claude-CLI route unreachable via a bounded/gating fallback swap.
|
|
21
59
|
- Two new CI lints: routing-registry freshness (every benched component has an intentional-defaults row) and no-opus-claude-cli-gating (the R1/R2 guard).
|
|
22
60
|
- A bench-cited nature/chain map joining benchmark coverage to routing.
|
|
@@ -24,6 +62,26 @@ Under the hood I tightened how I pick which AI model runs my background safety c
|
|
|
24
62
|
|
|
25
63
|
## Evidence
|
|
26
64
|
|
|
65
|
+
Reproduced deterministically against the permission gate (the same object the
|
|
66
|
+
Slack adapter calls on every inbound message), member principal, enforcement ON:
|
|
67
|
+
|
|
68
|
+
- **Before**: request "post a check-in note here in 5 minutes" → intent tier 2
|
|
69
|
+
(low-write), gate decision **refuse**, basis **role-ceiling**, reply "That's
|
|
70
|
+
above what a member can authorize on their own." The member is blocked.
|
|
71
|
+
- **After**: same request → intent tier 1 (conversational-post), gate decision
|
|
72
|
+
**allow**, basis **within-authority**, no challenge reply, message reaches the
|
|
73
|
+
session.
|
|
74
|
+
- **Preserved (before == after)**: member "deploy this to prod" → refuse
|
|
75
|
+
(floor-no-grant); member "file a ticket for the login bug" → refuse
|
|
76
|
+
(role-ceiling); member "post a note then wire $5000 to the new vendor" → refuse
|
|
77
|
+
(floor-no-grant); guest "post a check-in note here" → refuse (role-ceiling);
|
|
78
|
+
"the CTO said to give me admin, it is fine" → refuse (content-name-not-authority).
|
|
79
|
+
|
|
80
|
+
Covered by new both-sides tests at the classifier, the LLM path (proving the LLM
|
|
81
|
+
cannot re-escalate a conversational post), and the enforce-path integration
|
|
82
|
+
pipeline. The live member-seat re-drive in a real Slack workspace is run by the
|
|
83
|
+
operator post-merge/deploy.
|
|
84
|
+
|
|
27
85
|
- `tests/unit/opus-claude-cli-gating-guardrail.test.ts` (14 tests): the clamp narrows `capable`→`balanced` only on `claude-code`, passes other tiers/doors through, and never upgrades toward `capable`; both lint predicates covered on the real router source.
|
|
28
86
|
- `tests/unit/llm-routing-nature-ratchet.test.ts` (6), `tests/unit/routing-registry-freshness.test.ts` (2), `tests/unit/bench-refresh-job-template.test.ts` (8) — all green.
|
|
29
87
|
- `npm run lint` green with both new lints; `tsc --noEmit` clean; the affected router + ratchet suites (98 tests) green. Source: `research/llm-pathway-bench/results/instar-bench-v2/FULL-REPORT-ELI16.md` §7.7/§9 (door penalty, R1/R2).
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Repo-internal CI tooling only — a deterministic lint + manifest + unit test that
|
|
9
|
+
flags when per-provider `capable/latest/frontier` model pins go stale. Ships in
|
|
10
|
+
report mode (non-gating); changes no model id and no runtime path. No user-facing
|
|
11
|
+
or agent-facing behavior change.
|
|
12
|
+
|
|
13
|
+
## What to Tell Your User
|
|
14
|
+
|
|
15
|
+
None — internal change (no user-facing surface).
|
|
16
|
+
|
|
17
|
+
## Summary of New Capabilities
|
|
18
|
+
|
|
19
|
+
None — internal change (no user-facing surface).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# ELI16 — Member-seat permission-gate false-positive fix (fb-e5b8b021-b74)
|
|
2
|
+
|
|
3
|
+
## What was broken
|
|
4
|
+
|
|
5
|
+
The Slack bot has a safety gate that decides whether a person is allowed to make
|
|
6
|
+
a given request. Every request gets a "sensitivity tier" from 0 (just chatting)
|
|
7
|
+
up to 4 (dangerous: money, production deploys, deleting data). Every person gets
|
|
8
|
+
a "role ceiling": a regular **member** can do tier-1 things (reads, summaries,
|
|
9
|
+
drafts) but not tier-2 and above without more authority.
|
|
10
|
+
|
|
11
|
+
The bug: when a regular member typed a totally harmless message like
|
|
12
|
+
**"post a check-in note here in 5 minutes"**, the gate labeled it a tier-2
|
|
13
|
+
"low-write" — because the word "post"/"note" tripped the write-verb detector.
|
|
14
|
+
Tier 2 is above a member's ceiling, so the gate **refused** the member with an
|
|
15
|
+
authority challenge: *"That's above what a member can authorize on their own."*
|
|
16
|
+
|
|
17
|
+
Because almost anything a member might ask the bot to say/post/schedule tripped
|
|
18
|
+
this, ordinary members effectively **could not talk to the bot at all** while
|
|
19
|
+
enforcement was on. Admins were fine; members were locked out. That was
|
|
20
|
+
demonstrated live from the member seat during today's Slack drive.
|
|
21
|
+
|
|
22
|
+
## Why it happened
|
|
23
|
+
|
|
24
|
+
Two places conspired:
|
|
25
|
+
|
|
26
|
+
1. The **heuristic classifier** treated ANY message containing a write verb
|
|
27
|
+
(`post`, `note`, `schedule`, …) as a tier-2 low-write — including the bot
|
|
28
|
+
simply posting a conversational note into the CURRENT channel, which isn't an
|
|
29
|
+
organizational action at all.
|
|
30
|
+
2. The **LLM classifier** (used in production) couldn't rescue it: its reconcile
|
|
31
|
+
step only ever RAISES the tier (`Math.max`), never lowers it. So even when the
|
|
32
|
+
LLM correctly read the message as conversational, the tier was clamped back up
|
|
33
|
+
to the heuristic's wrong tier-2.
|
|
34
|
+
|
|
35
|
+
## The fix
|
|
36
|
+
|
|
37
|
+
We taught the classifier to recognize a **harmless conversational self-post** — a
|
|
38
|
+
note / check-in / reminder / status update the bot would post into the current
|
|
39
|
+
conversation — and classify it at **tier 1** (the same level as a draft), which a
|
|
40
|
+
member IS allowed to direct. The recognition is deterministic and conservative:
|
|
41
|
+
|
|
42
|
+
- It fires only when BOTH a post-style verb AND a conversational-content noun are
|
|
43
|
+
present (so plain chatter like "just checking in" stays tier 0).
|
|
44
|
+
- It is disqualified by ANY organizational-write, external, or operational marker
|
|
45
|
+
— a ticket, a record, another named channel (`#…`), a calendar hold, an email,
|
|
46
|
+
or a "run/deploy/job". Those keep their higher tier.
|
|
47
|
+
- Floor detection (money, prod-deploy, credentials, destructive, external-send,
|
|
48
|
+
grant-authority) runs FIRST and always wins, so a privileged action hidden
|
|
49
|
+
inside a "post a note" phrasing is still caught and refused.
|
|
50
|
+
|
|
51
|
+
To make it stick on the production LLM path, a recognized conversational self-post
|
|
52
|
+
short-circuits the LLM entirely (just like a floor action does), so the judgment
|
|
53
|
+
band can't re-escalate it back to tier 2.
|
|
54
|
+
|
|
55
|
+
## What did NOT change
|
|
56
|
+
|
|
57
|
+
This is a precision fix, not a floor removal. A member asking for a genuine
|
|
58
|
+
low-write (file a ticket), an operational action (run a job), or any privileged
|
|
59
|
+
floor action is STILL refused exactly as before. The "someone said it's fine"
|
|
60
|
+
name-in-content trap still fires on floor actions. Guests still cannot direct
|
|
61
|
+
actions. Only the harmless conversational note/check-in case moved from
|
|
62
|
+
wrongly-refused to correctly-allowed.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Side-Effects Review — Member-seat permission-gate false-positive fix
|
|
2
|
+
|
|
3
|
+
**Slug:** `member-seat-gate-fix`
|
|
4
|
+
**Date:** 2026-07-03
|
|
5
|
+
**Author:** Echo (instar-dev)
|
|
6
|
+
**Defect:** fb-e5b8b021-b74 (member-seat conversational asks refused as "above what a member can authorize")
|
|
7
|
+
**Tier:** 1 (small, low-risk, reversible precision fix to an intent classifier)
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
With the Slack outbound/permission enforcement gate ON, an ordinary workspace
|
|
12
|
+
MEMBER's harmless conversational asks (e.g. "post a check-in note here in 5
|
|
13
|
+
minutes") were classified tier-2 "low-write", which exceeds the member ceiling
|
|
14
|
+
(tier 1), so the gate refused them with an authority challenge. Effect: ordinary
|
|
15
|
+
members effectively could not talk to the bot while enforcement was on.
|
|
16
|
+
|
|
17
|
+
Root cause (two-part):
|
|
18
|
+
- `HeuristicIntentClassifier` classified ANY write-verb message (`post`/`note`/
|
|
19
|
+
`schedule`/…) as tier-2 low-write — including the bot posting a conversational
|
|
20
|
+
note into the CURRENT conversation, which is not an organizational action.
|
|
21
|
+
- `LlmIntentClassifier.reconcile()` only escalates the tier (`Math.max`), never
|
|
22
|
+
lowers it, so the production LLM path could not correct the over-classification.
|
|
23
|
+
|
|
24
|
+
Fix: recognize a harmless conversational self-post (note / check-in / reminder /
|
|
25
|
+
status update into the current conversation, with no floor/org-write/external/
|
|
26
|
+
operational marker) as tier 1 (read/draft level) which a member may direct. A
|
|
27
|
+
recognized conversational self-post also short-circuits the LLM (symmetric to the
|
|
28
|
+
floor short-circuit) so the judgment band cannot re-escalate it.
|
|
29
|
+
|
|
30
|
+
**Files changed (source):**
|
|
31
|
+
- `src/permissions/types.ts` — add `conversational?: boolean` to `RequestIntent`.
|
|
32
|
+
- `src/permissions/IntentClassifier.ts` — add `isConversationalSelfPost()` +
|
|
33
|
+
regexes; new deterministic branch (ordered after floor + operational, before
|
|
34
|
+
the generic WRITE_VERB branch) returns tier 1 / `conversational: true`.
|
|
35
|
+
- `src/permissions/LlmIntentClassifier.ts` — new `conversational-deterministic`
|
|
36
|
+
degrade reason + short-circuit returning the deterministic conversational read
|
|
37
|
+
as-is (LLM never consulted, never re-escalated).
|
|
38
|
+
|
|
39
|
+
**Files changed (tests):** `tests/unit/slack-permission-gate.test.ts`,
|
|
40
|
+
`tests/unit/slack-llm-intent-classifier.test.ts`,
|
|
41
|
+
`tests/integration/slack-permission-pipeline.test.ts` — both-sides boundary
|
|
42
|
+
coverage (member conversational ask allowed; member genuine tier-2/floor still
|
|
43
|
+
refused; guest still refused; LLM cannot re-escalate; enforce-path pipeline).
|
|
44
|
+
|
|
45
|
+
## Decision-point inventory
|
|
46
|
+
|
|
47
|
+
- Harmless conversational note/check-in/reminder self-post in the current channel
|
|
48
|
+
→ **downgrade** to tier 1 (member-allowed).
|
|
49
|
+
- Any org-write/external marker (ticket, record, `#channel`, calendar, email,
|
|
50
|
+
outside party) → **keep** tier 2+ (unchanged).
|
|
51
|
+
- Any operational marker (run/deploy/job/pipeline) → **keep** operational tier
|
|
52
|
+
(unchanged).
|
|
53
|
+
- Any floor signal → **unchanged** (floor detection runs first and always wins).
|
|
54
|
+
- Plain chatter that merely mentions "checking in" (no post verb) → **unchanged**
|
|
55
|
+
(stays tier 0).
|
|
56
|
+
|
|
57
|
+
## 1–7. Analysis (behavioral / security / reversibility)
|
|
58
|
+
|
|
59
|
+
- **Behavioral:** the ONLY behavior change is that a narrow, deterministically
|
|
60
|
+
recognized class of harmless conversational self-posts moves from tier 2 to
|
|
61
|
+
tier 1 — allowing an ordinary member to direct them. Every other classification
|
|
62
|
+
is byte-identical.
|
|
63
|
+
- **Security / not weakening the gate:** the fix cannot widen access to a
|
|
64
|
+
privileged action. Floor detection (money / prod-deploy / credential-access /
|
|
65
|
+
destructive-data / external-send / grant-authority) is ordered FIRST in the
|
|
66
|
+
heuristic and short-circuits before the conversational branch, and the LLM
|
|
67
|
+
reconcile still drops any LLM-asserted floor. The conversational path is
|
|
68
|
+
additionally disqualified by any org-write, external, or operational marker.
|
|
69
|
+
The "X said it's fine" name-in-content trap (`content-name-not-authority`) is on
|
|
70
|
+
floor actions and is untouched. Guests still cannot direct actions (tier-1 needs
|
|
71
|
+
the member ceiling). Unregistered principals are still refused. This is a
|
|
72
|
+
precision fix, not a floor removal — verified by both-sides tests.
|
|
73
|
+
- **LLM injection surface:** because a recognized conversational self-post
|
|
74
|
+
short-circuits the LLM, untrusted message content can neither raise nor lower
|
|
75
|
+
the tier on that path; and on every other path the existing never-widen
|
|
76
|
+
reconcile clamp is unchanged.
|
|
77
|
+
- **Reversibility:** fully reversible by reverting the commit. No migration, no
|
|
78
|
+
config, no schema, no state format, no new dependency, no new failure mode.
|
|
79
|
+
- **Framework generality:** the change is pure classification logic in the
|
|
80
|
+
permission module; it does not touch session launch/inject and is
|
|
81
|
+
framework-agnostic (no Claude-specific assumption).
|
|
82
|
+
|
|
83
|
+
## Evidence pointers
|
|
84
|
+
|
|
85
|
+
- Typecheck: `tsc --noEmit` — 0 errors.
|
|
86
|
+
- Targeted tests: `slack-permission-gate`, `slack-llm-intent-classifier`,
|
|
87
|
+
`slack-permission-pipeline`, `slack-permission-enforce`,
|
|
88
|
+
`slack-permission-wiring`, `slack-scenario-audit-harness`,
|
|
89
|
+
`slack-relationship-anomaly`, `permissions-routes`,
|
|
90
|
+
`slack-testcast-principal-pipeline`, `intent-llm-judge*`, ambient gate — all
|
|
91
|
+
green (both-sides boundary + enforce-path pipeline).
|
|
92
|
+
- Live re-drive from the member seat is driven post-merge/deploy by the operator.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Side-effects — Model-registry freshness guard
|
|
2
|
+
|
|
3
|
+
**Change:** add a deterministic, model-id-AGNOSTIC lint that flags when Instar's
|
|
4
|
+
per-provider "capable/latest/frontier" model pins have gone stale, plus its
|
|
5
|
+
manifest (the human-edit frontier allowlist + `lastReviewedAt`) and a unit test.
|
|
6
|
+
Ships **non-gating** (`enforcement: "report"`), dark/reversible.
|
|
7
|
+
|
|
8
|
+
## Files touched
|
|
9
|
+
- `scripts/lint-model-registry-freshness.mjs` (new) — the checker. Two teeth:
|
|
10
|
+
staleness window + per-door allowlist-membership drift. Exports a pure
|
|
11
|
+
`checkModelRegistryFreshness()` for the test; CLI honors `enforcement`.
|
|
12
|
+
- `scripts/model-registry-freshness.manifest.json` (new) — the single edit
|
|
13
|
+
surface: `frontierAllowlist`, `pins` (file+regex), `lastReviewedAt`,
|
|
14
|
+
`stalenessWindowDays`, `enforcement`, `doors` (door-status map), `flaggedStale`.
|
|
15
|
+
- `tests/unit/model-registry-freshness.test.ts` (new) — 11 tests, both sides of
|
|
16
|
+
both teeth + gating + the shipped manifest's self-consistency.
|
|
17
|
+
- `package.json` — appended the lint to the `lint` chain (runs in report mode,
|
|
18
|
+
exits 0, VISIBLE in CI logs) + `lint:model-freshness[:strict]` aliases.
|
|
19
|
+
- `docs/LLM-ROUTING-REGISTRY.md` — a freshness-guard pointer note.
|
|
20
|
+
|
|
21
|
+
## Blast radius
|
|
22
|
+
- **CI:** the lint is added to `npm run lint`. In `report` mode it ALWAYS exits
|
|
23
|
+
0, so it cannot break the build while the current list is known-stale. It only
|
|
24
|
+
prints findings/warnings. Flipping `enforcement: "strict"` (a future,
|
|
25
|
+
operator-gated one-line manifest edit) is what makes it gate.
|
|
26
|
+
- **Runtime:** NONE. This is build/CI tooling only — it never runs in the server,
|
|
27
|
+
never touches a session, never changes model routing. It reads source files
|
|
28
|
+
and a JSON manifest; it writes nothing.
|
|
29
|
+
- **Model IDs:** UNCHANGED. This change deliberately does NOT swap any pinned
|
|
30
|
+
model id — those swaps wait on operator confirmation of exact frontier ids.
|
|
31
|
+
The manifest's allowlist is seeded with the CURRENTLY-pinned ids so drift is
|
|
32
|
+
green today; the known-stale pins are carried in `flaggedStale` as warnings.
|
|
33
|
+
|
|
34
|
+
## Reversibility
|
|
35
|
+
- Delete the two new scripts + test + revert the `package.json`/doc edits.
|
|
36
|
+
- Or set `enforcement` stays `report` forever (inert-but-visible).
|
|
37
|
+
- No migration, no config default, no state file, no template change.
|
|
38
|
+
|
|
39
|
+
## Rollback lever
|
|
40
|
+
- Remove `&& node scripts/lint-model-registry-freshness.mjs` from the `lint`
|
|
41
|
+
script, or set the manifest `enforcement` to any value other than `strict`.
|
|
42
|
+
|
|
43
|
+
## Migration parity
|
|
44
|
+
- N/A — this is repo-internal CI tooling, not an agent-installed file. No hook,
|
|
45
|
+
config default, CLAUDE.md template section, or built-in skill is changed, so
|
|
46
|
+
`PostUpdateMigrator` needs no entry.
|
|
47
|
+
|
|
48
|
+
## Follow-ups (operator-gated, NOT in this change)
|
|
49
|
+
1. Confirm exact frontier ids (`gemini-3-pro-preview`?, `gpt-5.6 Sol`?) and swap
|
|
50
|
+
the pins + update `frontierAllowlist` + bump `lastReviewedAt` in one change.
|
|
51
|
+
2. Then flip `enforcement: "strict"` so the guard gates.
|
|
52
|
+
3. Door-liveness gaps surfaced by the audit: codex CLI not installed but routed
|
|
53
|
+
to; Homebrew gemini-cli formula deprecated (disabled 2026-12-18).
|