instar 1.3.490 → 1.3.492

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/dashboard/index.html +30 -0
  2. package/dist/commands/init.d.ts.map +1 -1
  3. package/dist/commands/init.js +3 -0
  4. package/dist/commands/init.js.map +1 -1
  5. package/dist/commands/server.d.ts.map +1 -1
  6. package/dist/commands/server.js +199 -1
  7. package/dist/commands/server.js.map +1 -1
  8. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  9. package/dist/config/ConfigDefaults.js +11 -0
  10. package/dist/config/ConfigDefaults.js.map +1 -1
  11. package/dist/core/CartographerSweepEngine.d.ts +55 -11
  12. package/dist/core/CartographerSweepEngine.d.ts.map +1 -1
  13. package/dist/core/CartographerSweepEngine.js +276 -95
  14. package/dist/core/CartographerSweepEngine.js.map +1 -1
  15. package/dist/core/CartographerTree.d.ts +77 -1
  16. package/dist/core/CartographerTree.d.ts.map +1 -1
  17. package/dist/core/CartographerTree.js +257 -5
  18. package/dist/core/CartographerTree.js.map +1 -1
  19. package/dist/core/GuardPostureStore.d.ts +38 -0
  20. package/dist/core/GuardPostureStore.d.ts.map +1 -0
  21. package/dist/core/GuardPostureStore.js +87 -0
  22. package/dist/core/GuardPostureStore.js.map +1 -0
  23. package/dist/core/MachinePoolRegistry.d.ts +16 -0
  24. package/dist/core/MachinePoolRegistry.d.ts.map +1 -1
  25. package/dist/core/MachinePoolRegistry.js +24 -1
  26. package/dist/core/MachinePoolRegistry.js.map +1 -1
  27. package/dist/core/PeerPresencePuller.d.ts +9 -0
  28. package/dist/core/PeerPresencePuller.d.ts.map +1 -1
  29. package/dist/core/PeerPresencePuller.js +1 -1
  30. package/dist/core/PeerPresencePuller.js.map +1 -1
  31. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  32. package/dist/core/PostUpdateMigrator.js +43 -2
  33. package/dist/core/PostUpdateMigrator.js.map +1 -1
  34. package/dist/core/cartographerDetect.d.ts +133 -0
  35. package/dist/core/cartographerDetect.d.ts.map +1 -0
  36. package/dist/core/cartographerDetect.js +431 -0
  37. package/dist/core/cartographerDetect.js.map +1 -0
  38. package/dist/core/cartographerDetect.worker.d.ts +2 -0
  39. package/dist/core/cartographerDetect.worker.d.ts.map +1 -0
  40. package/dist/core/cartographerDetect.worker.js +37 -0
  41. package/dist/core/cartographerDetect.worker.js.map +1 -0
  42. package/dist/core/types.d.ts +29 -0
  43. package/dist/core/types.d.ts.map +1 -1
  44. package/dist/core/types.js.map +1 -1
  45. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts +8 -0
  46. package/dist/monitoring/ActiveWorkSilenceSentinel.d.ts.map +1 -1
  47. package/dist/monitoring/ActiveWorkSilenceSentinel.js +8 -0
  48. package/dist/monitoring/ActiveWorkSilenceSentinel.js.map +1 -1
  49. package/dist/monitoring/ContextWedgeSentinel.d.ts +8 -0
  50. package/dist/monitoring/ContextWedgeSentinel.d.ts.map +1 -1
  51. package/dist/monitoring/ContextWedgeSentinel.js +8 -0
  52. package/dist/monitoring/ContextWedgeSentinel.js.map +1 -1
  53. package/dist/monitoring/GuardPostureTripwire.d.ts +3 -27
  54. package/dist/monitoring/GuardPostureTripwire.d.ts.map +1 -1
  55. package/dist/monitoring/GuardPostureTripwire.js +8 -76
  56. package/dist/monitoring/GuardPostureTripwire.js.map +1 -1
  57. package/dist/monitoring/GuardRegistry.d.ts +48 -0
  58. package/dist/monitoring/GuardRegistry.d.ts.map +1 -0
  59. package/dist/monitoring/GuardRegistry.js +49 -0
  60. package/dist/monitoring/GuardRegistry.js.map +1 -0
  61. package/dist/monitoring/SessionReaper.d.ts +7 -0
  62. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  63. package/dist/monitoring/SessionReaper.js +9 -0
  64. package/dist/monitoring/SessionReaper.js.map +1 -1
  65. package/dist/monitoring/SessionWatchdog.d.ts +8 -0
  66. package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
  67. package/dist/monitoring/SessionWatchdog.js +8 -0
  68. package/dist/monitoring/SessionWatchdog.js.map +1 -1
  69. package/dist/monitoring/SocketDisconnectSentinel.d.ts +8 -0
  70. package/dist/monitoring/SocketDisconnectSentinel.d.ts.map +1 -1
  71. package/dist/monitoring/SocketDisconnectSentinel.js +8 -0
  72. package/dist/monitoring/SocketDisconnectSentinel.js.map +1 -1
  73. package/dist/monitoring/guardManifest.d.ts +67 -0
  74. package/dist/monitoring/guardManifest.d.ts.map +1 -0
  75. package/dist/monitoring/guardManifest.js +536 -0
  76. package/dist/monitoring/guardManifest.js.map +1 -0
  77. package/dist/monitoring/guardPosture.d.ts +77 -0
  78. package/dist/monitoring/guardPosture.d.ts.map +1 -0
  79. package/dist/monitoring/guardPosture.js +198 -0
  80. package/dist/monitoring/guardPosture.js.map +1 -0
  81. package/dist/monitoring/guardPostureView.d.ts +100 -0
  82. package/dist/monitoring/guardPostureView.d.ts.map +1 -0
  83. package/dist/monitoring/guardPostureView.js +294 -0
  84. package/dist/monitoring/guardPostureView.js.map +1 -0
  85. package/dist/monitoring/probes/GuardPostureProbe.d.ts +82 -0
  86. package/dist/monitoring/probes/GuardPostureProbe.d.ts.map +1 -0
  87. package/dist/monitoring/probes/GuardPostureProbe.js +384 -0
  88. package/dist/monitoring/probes/GuardPostureProbe.js.map +1 -0
  89. package/dist/scaffold/templates.d.ts.map +1 -1
  90. package/dist/scaffold/templates.js +9 -0
  91. package/dist/scaffold/templates.js.map +1 -1
  92. package/dist/scheduler/JobScheduler.d.ts +10 -0
  93. package/dist/scheduler/JobScheduler.d.ts.map +1 -1
  94. package/dist/scheduler/JobScheduler.js +12 -0
  95. package/dist/scheduler/JobScheduler.js.map +1 -1
  96. package/dist/server/AgentServer.d.ts +8 -0
  97. package/dist/server/AgentServer.d.ts.map +1 -1
  98. package/dist/server/AgentServer.js +2 -0
  99. package/dist/server/AgentServer.js.map +1 -1
  100. package/dist/server/CapabilityIndex.d.ts.map +1 -1
  101. package/dist/server/CapabilityIndex.js +11 -0
  102. package/dist/server/CapabilityIndex.js.map +1 -1
  103. package/dist/server/peerUrlGuard.d.ts +35 -0
  104. package/dist/server/peerUrlGuard.d.ts.map +1 -0
  105. package/dist/server/peerUrlGuard.js +93 -0
  106. package/dist/server/peerUrlGuard.js.map +1 -0
  107. package/dist/server/routes.d.ts +14 -0
  108. package/dist/server/routes.d.ts.map +1 -1
  109. package/dist/server/routes.js +239 -25
  110. package/dist/server/routes.js.map +1 -1
  111. package/package.json +3 -2
  112. package/scripts/lint-guard-manifest.js +264 -0
  113. package/scripts/lint-no-direct-destructive.js +4 -0
  114. package/scripts/lint-no-mainthread-cartographer-walk.js +101 -0
  115. package/src/data/builtin-manifest.json +65 -65
  116. package/src/data/state-coherence-registry.json +54 -18
  117. package/src/scaffold/templates.ts +9 -0
  118. package/upgrades/1.3.491.md +37 -0
  119. package/upgrades/1.3.492.md +31 -0
  120. package/upgrades/side-effects/cartographer-sweep-eventloop-safety.md +110 -0
  121. package/upgrades/side-effects/guard-posture-endpoint.md +101 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.490",
3
+ "version": "1.3.492",
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,10 @@
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-state-registry.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-dev-agent-dark-gate.js && node scripts/lint-llm-attribution.js && node scripts/check-codex-rule1-drift.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-state-registry.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-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/check-codex-rule1-drift.js",
32
32
  "lint:destructive": "node scripts/lint-no-direct-destructive.js",
33
33
  "lint:dev-agent-dark-gate": "node scripts/lint-dev-agent-dark-gate.js",
34
+ "lint:guard-manifest": "node scripts/lint-guard-manifest.js",
34
35
  "lint:dev-agent-dark-gate:staged": "node scripts/lint-dev-agent-dark-gate.js --staged",
35
36
  "lint:destructive:staged": "node scripts/lint-no-direct-destructive.js --staged",
36
37
  "lint:llm-http": "node scripts/lint-no-direct-llm-http.js",
@@ -0,0 +1,264 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-guard-manifest.js — the guard-inventory CI ratchet
4
+ * (GUARD-POSTURE-ENDPOINT-SPEC §2.1, "CI ratchet (manifest-driven, repo-wide)").
5
+ *
6
+ * THE RULE: every guard-shaped component in src/monitoring, src/messaging,
7
+ * src/lifeline, src/core must be CLASSIFIED — it appears either as a
8
+ * `component:` in GUARD_MANIFEST (it IS an inventory guard) or in NOT_A_GUARD
9
+ * (it deliberately is not, with a real reason). A future guard cannot be
10
+ * forgotten from the /guards inventory: the moment its file lands, this lint
11
+ * fails until someone makes the classification decision explicitly
12
+ * (Structure > Willpower — same contract as lint-dev-agent-dark-gate.js +
13
+ * DARK_GATE_EXCLUSIONS).
14
+ *
15
+ * Three assertions:
16
+ *
17
+ * A. EVERY CANDIDATE CLASSIFIED — a candidate component is (1) a .ts file in
18
+ * one of the four dirs whose basename matches the guard-shape suffix
19
+ * pattern (Sentinel|Reaper|Watchdog|Guard|Tripwire|Monitor|Beacon|
20
+ * Detector|Promoter|Pauser|Backstop), component name = basename, OR
21
+ * (2) a name in ADDITIONAL_CANDIDATES (enabled-gated boot constructs
22
+ * whose filenames don't match the pattern, e.g. QuotaTracker, PromptGate).
23
+ * Each candidate must appear in GUARD_MANIFEST's `component:` fields or
24
+ * in NOT_A_GUARD.
25
+ *
26
+ * B. REAL REASONS — every NOT_A_GUARD entry's reason must be ≥12
27
+ * non-whitespace chars (defeats placeholder reasons; same bar as
28
+ * DARK_GATE_EXCLUSIONS).
29
+ *
30
+ * C. EXACTLY ONE LIST — a component must not appear in BOTH GUARD_MANIFEST
31
+ * and NOT_A_GUARD (a dual classification is a contradiction the
32
+ * inventory cannot resolve).
33
+ *
34
+ * LIMITATIONS (P2 Signal-vs-Authority — do NOT claim full closure):
35
+ * - Detection is the file-basename suffix pattern + the explicit
36
+ * ADDITIONAL_CANDIDATES list. A guard-shaped component in a file whose
37
+ * name matches neither (e.g. a guard class defined inside server.ts)
38
+ * evades detection until someone names it in ADDITIONAL_CANDIDATES.
39
+ * Deliberate: cheap, deterministic, explainable — the precedent's
40
+ * pragmatism over an AST-grade analyzer.
41
+ * - The manifest is parsed STATICALLY (regex over the source — this lint
42
+ * must not import TS). `component:` and `reason:` must be single-line
43
+ * 'single-' or "double-quoted" string literals, which is the file's
44
+ * enforced house style; a template-literal or multi-line reason would be
45
+ * invisible to the parse (and the entry would then fail assertion A,
46
+ * loudly, not silently pass).
47
+ *
48
+ * Exit codes:
49
+ * 0 — no violations.
50
+ * 1 — at least one violation.
51
+ *
52
+ * Usage:
53
+ * node scripts/lint-guard-manifest.js # full scan of the four dirs
54
+ * node scripts/lint-guard-manifest.js path1 path2 # specific candidate files
55
+ *
56
+ * Test override:
57
+ * INSTAR_GUARDLINT_MANIFEST=<path> # point the parse at a fixture manifest
58
+ */
59
+
60
+ import fs from 'node:fs';
61
+ import path from 'node:path';
62
+ import { fileURLToPath } from 'node:url';
63
+
64
+ const __filename = fileURLToPath(import.meta.url);
65
+ const __dirname = path.dirname(__filename);
66
+ const ROOT = path.resolve(__dirname, '..');
67
+
68
+ const MANIFEST_REL = 'src/monitoring/guardManifest.ts';
69
+ const MANIFEST_ABS = process.env.INSTAR_GUARDLINT_MANIFEST
70
+ ? path.resolve(process.env.INSTAR_GUARDLINT_MANIFEST)
71
+ : path.join(ROOT, MANIFEST_REL);
72
+
73
+ // The repo-wide scan boundary (spec §2.1: "in ANY of src/monitoring,
74
+ // src/messaging, src/lifeline, src/core" — path-pattern linting scoped to one
75
+ // directory would miss the spec's own canonical examples).
76
+ const GUARD_DIRS = ['src/monitoring', 'src/messaging', 'src/lifeline', 'src/core'];
77
+
78
+ // Guard-shape detection 1: the basename suffix pattern.
79
+ const GUARD_SHAPE_BASENAME =
80
+ /(Sentinel|Reaper|Watchdog|Guard|Tripwire|Monitor|Beacon|Detector|Promoter|Pauser|Backstop)\.ts$/;
81
+
82
+ // Guard-shape detection 2: explicit additional candidates — enabled-gated
83
+ // boot-constructed components whose FILENAMES do not match the suffix pattern.
84
+ // Append here when a new guard-shaped component ships under a non-matching
85
+ // name (the lint then forces its classification like any other candidate).
86
+ const ADDITIONAL_CANDIDATES = [
87
+ 'QuotaTracker', // monitoring.quotaTracking
88
+ 'ResourceLedger', // monitoring.resourceLedger.enabled
89
+ 'TelemetryCollector', // monitoring.telemetry.enabled
90
+ 'StallTriageNurse', // monitoring.triage.enabled
91
+ 'TriageOrchestrator', // monitoring.triageOrchestrator.enabled
92
+ 'GrowthMilestoneAnalyst',// monitoring.growthAnalyst.enabled (dev-gated)
93
+ 'BlockerLedger', // monitoring.blockerLedger.enabled (dev-gated)
94
+ 'SecretSync', // multiMachine.secretSync.enabled
95
+ 'CoherenceJournal', // multiMachine.coherenceJournal.enabled
96
+ 'PromptGate', // monitoring.promptGate.enabled
97
+ ];
98
+
99
+ const MIN_REASON_NON_WS = 12;
100
+
101
+ /** Strip line + block comments so commented-out entries can't satisfy the rule. */
102
+ function stripComments(src) {
103
+ return src
104
+ .replace(/\/\*[\s\S]*?\*\//g, '')
105
+ .replace(/^\s*\/\/.*$/gm, '');
106
+ }
107
+
108
+ /** Extract the source slice of `export const NAME ... = [ ... ] as const;`. */
109
+ function extractArrayBlock(src, name) {
110
+ const declRe = new RegExp(`export\\s+const\\s+${name}\\b[\\s\\S]*?=\\s*\\[`);
111
+ const m = declRe.exec(src);
112
+ if (!m) return null;
113
+ const start = m.index + m[0].length;
114
+ const end = src.indexOf('] as const', start);
115
+ if (end < 0) return null;
116
+ return src.slice(start, end);
117
+ }
118
+
119
+ /**
120
+ * Statically parse the manifest file (regex over source — never imports TS).
121
+ * Returns { manifestComponents: Set, notAGuard: [{component, reason, line}] }.
122
+ */
123
+ function parseManifest(manifestAbs) {
124
+ const raw = fs.readFileSync(manifestAbs, 'utf-8');
125
+ const src = stripComments(raw);
126
+
127
+ const manifestBlock = extractArrayBlock(src, 'GUARD_MANIFEST');
128
+ const notAGuardBlock = extractArrayBlock(src, 'NOT_A_GUARD');
129
+ if (manifestBlock === null || notAGuardBlock === null) {
130
+ return {
131
+ error: `could not locate ${manifestBlock === null ? 'GUARD_MANIFEST' : 'NOT_A_GUARD'} array in ${manifestAbs}`,
132
+ };
133
+ }
134
+
135
+ const manifestComponents = new Set();
136
+ const componentRe = /component:\s*(['"])([^'"]+)\1/g;
137
+ for (const m of manifestBlock.matchAll(componentRe)) manifestComponents.add(m[2]);
138
+
139
+ // NOT_A_GUARD entries: `{ component: '...', reason: '...' }` (single-line
140
+ // string literals — the file's house style; see header LIMITATIONS).
141
+ const notAGuard = [];
142
+ const entryRe = /component:\s*(['"])([^'"]+)\1\s*,\s*reason:\s*(['"])((?:[^\\]|\\.)*?)\3/g;
143
+ for (const m of notAGuardBlock.matchAll(entryRe)) {
144
+ notAGuard.push({ component: m[2], reason: m[4] });
145
+ }
146
+
147
+ return { manifestComponents, notAGuard };
148
+ }
149
+
150
+ /** Walk the four guard dirs for candidate .ts files (skip tests/declarations). */
151
+ function listCandidateFiles() {
152
+ const out = [];
153
+ for (const rel of GUARD_DIRS) {
154
+ const dir = path.join(ROOT, rel);
155
+ if (!fs.existsSync(dir)) continue;
156
+ const walk = (d) => {
157
+ for (const e of fs.readdirSync(d, { withFileTypes: true })) {
158
+ const full = path.join(d, e.name);
159
+ if (e.isDirectory()) {
160
+ if (e.name === 'node_modules') continue;
161
+ walk(full);
162
+ } else if (
163
+ e.name.endsWith('.ts') &&
164
+ !e.name.endsWith('.test.ts') &&
165
+ !e.name.endsWith('.d.ts')
166
+ ) {
167
+ out.push(full);
168
+ }
169
+ }
170
+ };
171
+ walk(dir);
172
+ }
173
+ return out;
174
+ }
175
+
176
+ function resolveCandidateFiles() {
177
+ const explicit = process.argv.slice(2).filter((a) => !a.startsWith('--'));
178
+ if (explicit.length) return explicit.map((f) => path.resolve(ROOT, f));
179
+ return listCandidateFiles();
180
+ }
181
+
182
+ // ── Run ──────────────────────────────────────────────────────────────────────
183
+
184
+ const violations = [];
185
+
186
+ if (!fs.existsSync(MANIFEST_ABS)) {
187
+ console.error(`lint-guard-manifest: manifest not found at ${MANIFEST_ABS}`);
188
+ process.exit(1);
189
+ }
190
+
191
+ const parsed = parseManifest(MANIFEST_ABS);
192
+ if (parsed.error) {
193
+ console.error(`lint-guard-manifest: ${parsed.error}`);
194
+ process.exit(1);
195
+ }
196
+ const { manifestComponents, notAGuard } = parsed;
197
+ const notAGuardComponents = new Set(notAGuard.map((e) => e.component));
198
+
199
+ // ── Assertion B: every NOT_A_GUARD reason is real (≥12 non-ws chars) ──
200
+ for (const e of notAGuard) {
201
+ const reasonLen = (e.reason || '').replace(/\s/g, '').length;
202
+ if (reasonLen < MIN_REASON_NON_WS) {
203
+ violations.push({
204
+ file: MANIFEST_REL, kind: 'B: NOT_A_GUARD reason too short',
205
+ text: `${e.component} → reason '${e.reason}' (${reasonLen} non-ws chars)`,
206
+ fix: `a NOT_A_GUARD reason must be ≥${MIN_REASON_NON_WS} non-whitespace chars (defeats placeholder reasons)`,
207
+ });
208
+ }
209
+ }
210
+
211
+ // ── Assertion C: exactly one list ──
212
+ for (const c of notAGuardComponents) {
213
+ if (manifestComponents.has(c)) {
214
+ violations.push({
215
+ file: MANIFEST_REL, kind: 'C: dual classification',
216
+ text: `${c} appears in BOTH GUARD_MANIFEST and NOT_A_GUARD`,
217
+ fix: 'a component is a guard or it is not — remove it from one list',
218
+ });
219
+ }
220
+ }
221
+
222
+ // ── Assertion A: every candidate component is classified ──
223
+ const explicitArgs = process.argv.slice(2).filter((a) => !a.startsWith('--'));
224
+ const candidates = new Map(); // component name → where it was detected
225
+ for (const file of resolveCandidateFiles()) {
226
+ if (!fs.existsSync(file)) continue;
227
+ const base = path.basename(file);
228
+ if (!GUARD_SHAPE_BASENAME.test(base)) continue;
229
+ candidates.set(base.replace(/\.ts$/, ''), path.relative(ROOT, file));
230
+ }
231
+ // ADDITIONAL_CANDIDATES are repo-global names — only meaningful on a full
232
+ // scan (an explicit-args run, e.g. a test fixture, scopes to those files).
233
+ if (explicitArgs.length === 0) {
234
+ for (const name of ADDITIONAL_CANDIDATES) {
235
+ if (!candidates.has(name)) candidates.set(name, '(ADDITIONAL_CANDIDATES)');
236
+ }
237
+ }
238
+
239
+ for (const [component, where] of [...candidates.entries()].sort()) {
240
+ if (manifestComponents.has(component)) continue;
241
+ if (notAGuardComponents.has(component)) continue;
242
+ violations.push({
243
+ file: where, kind: 'A: unclassified guard-shaped component',
244
+ text: `${component} is in NEITHER GUARD_MANIFEST (component:) NOR NOT_A_GUARD`,
245
+ fix: `declare it in GUARD_MANIFEST (it joins the /guards inventory) OR classify it in NOT_A_GUARD with a ≥${MIN_REASON_NON_WS}-char reason (${MANIFEST_REL})`,
246
+ });
247
+ }
248
+
249
+ if (violations.length === 0) {
250
+ console.log('lint-guard-manifest: clean');
251
+ process.exit(0);
252
+ }
253
+
254
+ console.error('\n❌ lint-guard-manifest found violations of the guard-inventory classification standard:\n');
255
+ console.error('NOTE: detection is the file-basename suffix pattern + ADDITIONAL_CANDIDATES only —');
256
+ console.error('a guard-shaped component under a non-matching filename evades it until named in');
257
+ console.error('ADDITIONAL_CANDIDATES. This closes the forgotten-file hole, not every hole.\n');
258
+ for (const v of violations) {
259
+ console.error(` ${v.file} [${v.kind}]`);
260
+ console.error(` ${v.text}`);
261
+ console.error(` fix: ${v.fix}\n`);
262
+ }
263
+ console.error('Standard: every guard-shaped component is CLASSIFIED — declared in GUARD_MANIFEST (it appears in the /guards inventory) or in NOT_A_GUARD with a real reason. A future guard cannot be silently forgotten. Spec: docs/specs/GUARD-POSTURE-ENDPOINT-SPEC.md §2.1\n');
264
+ process.exit(1);
@@ -88,6 +88,10 @@ const ALLOWLIST = new Set([
88
88
  // completeness) — read-only `git diff --cached --name-only` for --staged.
89
89
  // Cannot depend on the TS funnel because TS is not compiled at pre-push.
90
90
  'scripts/lint-llm-attribution.js',
91
+ // Same bootstrap-escape pattern (cartographer main-thread-walk lint, fix
92
+ // instar#1069) — read-only `git diff --cached --name-only` for --staged.
93
+ // Cannot depend on the TS funnel because TS is not compiled at pre-push.
94
+ 'scripts/lint-no-mainthread-cartographer-walk.js',
91
95
  // Postinstall bootstrap script — runs before TypeScript is compiled and
92
96
  // before SafeFsExecutor is available. CommonJS, can't use ESM imports.
93
97
  'scripts/fix-better-sqlite3.cjs',
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * lint-no-mainthread-cartographer-walk.js — refuses the O(nodeCount)/67MB
4
+ * synchronous cartographer operations on the server's main-thread surfaces.
5
+ *
6
+ * Earned by instar#1069: enabling the doc-freshness sweep on a real tree
7
+ * (366,757 nodes / 67MB index) put the AgentServer into a supervisor kill-loop.
8
+ * The "what's stale?" detect read + walked the whole index ON THE MAIN EVENT
9
+ * LOOP, starving /health for ~35s at a stretch, so the supervisor declared the
10
+ * (alive) server dead and force-restarted it — every ~10-15 min.
11
+ *
12
+ * The fix moved the detect + index writes off the event loop (a worker thread)
13
+ * and made every /cartographer/* route serve a cheap snapshot. This lint stops
14
+ * the regression from returning: "Structure > Willpower", not "enforced by review".
15
+ *
16
+ * Rule (PATH-ALLOWLIST — a callsite lint matches FILES, not runtime threads):
17
+ * FORBIDDEN in the main-thread surfaces:
18
+ * - src/server/routes.ts (the request thread)
19
+ * - src/core/CartographerSweepEngine.ts (the sweep poller thread = main loop)
20
+ * the heavy calls: .staleNodes( .loadIndex( .freshnessHealth( .scaffold(
21
+ * PERMITTED everywhere else, specifically:
22
+ * - src/core/cartographerDetect.ts (the pure module — legitimately parses, OFF-thread)
23
+ * - src/core/CartographerTree.ts (the definitions of these methods)
24
+ * - tests/ (tests scaffold/load directly)
25
+ * The bounded request loader (`.loadIndexBounded(`) is NOT matched — it stats
26
+ * the file and refuses before parsing, so it is safe on the request thread.
27
+ *
28
+ * Escape hatch (closed, reviewed): a genuinely off-thread / boot-only call may
29
+ * carry an inline justification on the same line or directly above:
30
+ * // lint-allow-carto-heavy: <why this can't starve the event loop>
31
+ *
32
+ * Exit codes: 0 — clean; 1 — at least one violation.
33
+ * Usage:
34
+ * node scripts/lint-no-mainthread-cartographer-walk.js # full repo
35
+ * node scripts/lint-no-mainthread-cartographer-walk.js --staged # staged files
36
+ */
37
+
38
+ import fs from 'node:fs';
39
+ import path from 'node:path';
40
+ import { execSync } from 'node:child_process';
41
+ import { fileURLToPath } from 'node:url';
42
+
43
+ const __filename = fileURLToPath(import.meta.url);
44
+ const ROOT = path.resolve(path.dirname(__filename), '..');
45
+
46
+ // The main-thread surfaces where these heavy calls re-introduce #1069.
47
+ const FORBIDDEN_FILES = new Set([
48
+ 'src/server/routes.ts',
49
+ 'src/core/CartographerSweepEngine.ts',
50
+ ]);
51
+
52
+ // .staleNodes( / .loadIndex( / .freshnessHealth( / .scaffold( — note the trailing
53
+ // `\(` means `.loadIndexBounded(` (the safe byte-bounded loader) is NOT matched.
54
+ const VIOLATION = /\.(staleNodes|loadIndex|freshnessHealth|scaffold)\(/;
55
+ const ALLOW = /lint-allow-carto-heavy:/;
56
+
57
+ function listFiles() {
58
+ if (process.argv.includes('--staged')) {
59
+ const out = execSync('git diff --cached --name-only', { cwd: ROOT, encoding: 'utf-8' });
60
+ return out.split('\n').filter(Boolean);
61
+ }
62
+ const explicit = process.argv.slice(2).filter((a) => !a.startsWith('--'));
63
+ if (explicit.length) return explicit;
64
+ return [...FORBIDDEN_FILES];
65
+ }
66
+
67
+ let violations = 0;
68
+ for (const rel of listFiles()) {
69
+ const normalized = rel.split(path.sep).join('/');
70
+ if (!FORBIDDEN_FILES.has(normalized)) continue; // only the main-thread surfaces are enforced
71
+ const full = path.isAbsolute(normalized) ? normalized : path.join(ROOT, normalized);
72
+ let content;
73
+ try { content = fs.readFileSync(full, 'utf-8'); } catch { continue; }
74
+ const lines = content.split('\n');
75
+ for (let i = 0; i < lines.length; i++) {
76
+ const trimmed = lines[i].trimStart();
77
+ if (/^(\/\/|\*|\/\*)/.test(trimmed)) continue; // comment-only mention
78
+ if (!VIOLATION.test(lines[i])) continue;
79
+ let allowed = false;
80
+ for (let j = i; j >= Math.max(0, i - 6); j--) {
81
+ if (ALLOW.test(lines[j])) { allowed = true; break; }
82
+ }
83
+ if (allowed) continue;
84
+ const m = lines[i].match(VIOLATION);
85
+ console.error(
86
+ `${normalized}:${i + 1} — main-thread cartographer ${m ? m[1] + '()' : 'walk'} ` +
87
+ `(O(nodeCount)/67MB synchronous) on a server hot path. This is the instar#1069 ` +
88
+ `kill-loop regression. Use the off-event-loop detect (cartographerDetect.runDetect) ` +
89
+ `or serve the snapshot / use loadIndexBounded(). If genuinely off-thread/boot-only, ` +
90
+ `add "// lint-allow-carto-heavy: <reason>".`,
91
+ );
92
+ violations++;
93
+ }
94
+ }
95
+
96
+ if (violations > 0) {
97
+ console.error(`\nlint-no-mainthread-cartographer-walk: ${violations} violation(s). ` +
98
+ `See docs/specs/CARTOGRAPHER-SWEEP-EVENTLOOP-SAFETY.md (fix instar#1069).`);
99
+ process.exit(1);
100
+ }
101
+ console.log('lint-no-mainthread-cartographer-walk: clean');