instar 1.3.478 → 1.3.480

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 (31) hide show
  1. package/.claude/skills/autonomous/SKILL.md +10 -0
  2. package/.claude/skills/autonomous/hooks/autonomous-stop-hook.sh +418 -23
  3. package/.claude/skills/autonomous/scripts/setup-autonomous.sh +29 -0
  4. package/dist/commands/server.d.ts.map +1 -1
  5. package/dist/commands/server.js +10 -7
  6. package/dist/commands/server.js.map +1 -1
  7. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  8. package/dist/config/ConfigDefaults.js +39 -8
  9. package/dist/config/ConfigDefaults.js.map +1 -1
  10. package/dist/core/PostUpdateMigrator.d.ts +1 -0
  11. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  12. package/dist/core/PostUpdateMigrator.js +121 -3
  13. package/dist/core/PostUpdateMigrator.js.map +1 -1
  14. package/dist/core/devGatedFeatures.d.ts +31 -0
  15. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  16. package/dist/core/devGatedFeatures.js +121 -0
  17. package/dist/core/devGatedFeatures.js.map +1 -1
  18. package/dist/scaffold/templates.d.ts.map +1 -1
  19. package/dist/scaffold/templates.js +1 -0
  20. package/dist/scaffold/templates.js.map +1 -1
  21. package/dist/server/routes.js +1 -1
  22. package/dist/server/routes.js.map +1 -1
  23. package/package.json +1 -1
  24. package/scripts/lib/dark-gate-attribution.js +148 -0
  25. package/scripts/lint-dev-agent-dark-gate.js +159 -14
  26. package/src/data/builtin-manifest.json +64 -64
  27. package/src/scaffold/templates.ts +1 -0
  28. package/upgrades/1.3.479.md +53 -0
  29. package/upgrades/1.3.480.md +35 -0
  30. package/upgrades/side-effects/autonomous-completion-real-checks.md +88 -0
  31. package/upgrades/side-effects/dev-agent-dark-gate-enforcement.md +95 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.478",
3
+ "version": "1.3.480",
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",
@@ -0,0 +1,148 @@
1
+ /**
2
+ * dark-gate-attribution.js — the path-attribution + registry-extraction core
3
+ * shared by scripts/lint-dev-agent-dark-gate.js (assertion C) and its golden-path
4
+ * drift-canary test. Extracted so there is ONE attributor implementation: the
5
+ * lint and the test agree by construction (the test asserts THIS resolver
6
+ * reproduces a hand-authored map — never the resolver's own output).
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+
11
+ /** Strip a `//` line comment; return null if the line is a pure comment line. */
12
+ export function codeOnly(line) {
13
+ const trimmed = line.trim();
14
+ if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
15
+ return null; // comment line — no code
16
+ }
17
+ const idx = line.indexOf('//');
18
+ return idx >= 0 ? line.slice(0, idx) : line;
19
+ }
20
+
21
+ /**
22
+ * Does a code-stripped line contain a `{` or `}` inside a string or template
23
+ * literal? codeOnly() does not strip string contents, so such a brace would
24
+ * desync the depth counter.
25
+ */
26
+ export function braceInString(code) {
27
+ let inStr = false;
28
+ let quote = '';
29
+ for (let i = 0; i < code.length; i++) {
30
+ const ch = code[i];
31
+ if (inStr) {
32
+ if (ch === '\\') { i++; continue; } // skip escaped char
33
+ if (ch === quote) { inStr = false; quote = ''; }
34
+ } else if (ch === '"' || ch === "'" || ch === '`') {
35
+ inStr = true;
36
+ quote = ch;
37
+ }
38
+ if (inStr && (ch === '{' || ch === '}')) return true;
39
+ }
40
+ return false;
41
+ }
42
+
43
+ /**
44
+ * Attribute every `enabled: false` line in a ConfigDefaults.ts to a dotted config
45
+ * path by brace-tracking from the top of the SHARED_DEFAULTS object literal.
46
+ * Returns { paths: [{ line, dottedPath }], error }.
47
+ */
48
+ export function attributeEnabledFalsePaths(absPath) {
49
+ const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
50
+ const startIdx = lines.findIndex((l) => /const\s+SHARED_DEFAULTS\b[^=]*=\s*{/.test(l));
51
+ if (startIdx < 0) {
52
+ return { paths: [], error: 'could not locate `const SHARED_DEFAULTS = {` in ConfigDefaults.ts' };
53
+ }
54
+
55
+ // Stack of { key } — key is null for an anonymous `{` (array element object
56
+ // etc.). depth starts at 0; the SHARED_DEFAULTS `{` takes it to 1 (the root
57
+ // object body lives at depth 1).
58
+ const stack = [];
59
+ let depth = 0;
60
+ const results = [];
61
+ const ENABLED_FALSE = /(["']?)enabled\1\s*:\s*false\b/;
62
+ const KEY_LINE = /^\s*(["']?)([A-Za-z_$][\w$-]*)\1\s*:/;
63
+
64
+ for (let i = startIdx; i < lines.length; i++) {
65
+ const code = codeOnly(lines[i]);
66
+ if (code === null) continue; // pure comment line
67
+
68
+ if (braceInString(code)) {
69
+ return {
70
+ paths: [],
71
+ error: `brace-in-string in defaults block can desync path attribution (line ${i + 1}) — split the value or extend the parser`,
72
+ };
73
+ }
74
+
75
+ const isEnabledFalse = ENABLED_FALSE.test(code);
76
+ const keyMatch = code.match(KEY_LINE);
77
+ const keyCandidate = keyMatch ? keyMatch[2] : null;
78
+
79
+ if (isEnabledFalse) {
80
+ const dottedPath = stack.map((s) => s.key).filter(Boolean).join('.') + '.enabled';
81
+ results.push({ line: i + 1, dottedPath });
82
+ }
83
+
84
+ let firstOpenOnLine = true;
85
+ for (const ch of code) {
86
+ if (ch === '{') {
87
+ if (firstOpenOnLine && keyCandidate && depth >= 1) {
88
+ stack.push({ key: keyCandidate });
89
+ } else {
90
+ stack.push({ key: null });
91
+ }
92
+ firstOpenOnLine = false;
93
+ depth++;
94
+ } else if (ch === '}') {
95
+ depth--;
96
+ stack.pop();
97
+ if (depth <= 0) {
98
+ return { paths: results, error: null };
99
+ }
100
+ }
101
+ }
102
+ }
103
+ return { paths: results, error: null };
104
+ }
105
+
106
+ const VALID_CATEGORIES = new Set([
107
+ 'destructive',
108
+ 'optional-integration',
109
+ 'cost-bearing',
110
+ 'structural-stub',
111
+ 'deliberate-fleet-default',
112
+ ]);
113
+
114
+ export { VALID_CATEGORIES };
115
+
116
+ /**
117
+ * Extract configPath literals + exclusion entries from a devGatedFeatures.ts.
118
+ * The file is hand-authored with a stable shape; the lint runs as plain JS
119
+ * (importing the TS registry directly won't work), so we regex the source.
120
+ */
121
+ export function extractRegistry(absPath) {
122
+ const src = fs.readFileSync(absPath, 'utf-8');
123
+ const configPathOf = (arrName) => {
124
+ const m = src.match(new RegExp(`export const ${arrName}[^=]*=\\s*\\[([\\s\\S]*?)\\n\\];`));
125
+ if (!m) return [];
126
+ const body = m[1];
127
+ const paths = [];
128
+ const re = /configPath:\s*(['"])([^'"]+)\1/g;
129
+ let mm;
130
+ while ((mm = re.exec(body)) !== null) paths.push(mm[2]);
131
+ return paths;
132
+ };
133
+ const exclBody = (() => {
134
+ const m = src.match(/export const DARK_GATE_EXCLUSIONS[^=]*=\s*\[([\s\S]*?)\n\];/);
135
+ return m ? m[1] : '';
136
+ })();
137
+ const exclusionEntries = [];
138
+ const entryRe = /\{\s*configPath:\s*(['"])([^'"]+)\1\s*,\s*category:\s*(['"])([^'"]+)\3\s*,\s*reason:\s*(['"])((?:[^'"\\]|\\.)*)\5\s*,?\s*\}/g;
139
+ let em;
140
+ while ((em = entryRe.exec(exclBody)) !== null) {
141
+ exclusionEntries.push({ configPath: em[2], category: em[4], reason: em[6] });
142
+ }
143
+ return {
144
+ gatedPaths: configPathOf('DEV_GATED_FEATURES'),
145
+ exclusionPaths: configPathOf('DARK_GATE_EXCLUSIONS'),
146
+ exclusionEntries,
147
+ };
148
+ }
@@ -8,7 +8,7 @@
8
8
  * (dev agents included), silently contradicting the standard. Caught only by
9
9
  * operator review — there was no structural guard. This is that guard.
10
10
  *
11
- * Two assertions over `src/`:
11
+ * Three assertions over `src/`:
12
12
  *
13
13
  * A. FUNNEL — every dev-agent gate resolution must go through
14
14
  * `resolveDevAgentGate` (src/core/devAgentGate.ts). A hand-rolled
@@ -25,6 +25,15 @@
25
25
  * is NOT flagged — that is an allowed deliberate fleet-flip. Comment prose is
26
26
  * skipped so the convention's own documentation never trips the check.
27
27
  *
28
+ * C. NO UNCLASSIFIED DARK DEFAULT (DEV-AGENT-DARK-GATE-ENFORCEMENT B2) — every
29
+ * literal `enabled: false` in src/config/ConfigDefaults.ts must be a DECLARED
30
+ * choice: its brace-attributed config path is EITHER in DEV_GATED_FEATURES
31
+ * (and then must NOT also hardcode false — a dev-gated feature OMITS enabled)
32
+ * OR in DARK_GATE_EXCLUSIONS with a valid category + a ≥12-char reason. This
33
+ * closes the hole assertion B left: a marker-less hardcoded `enabled: false`
34
+ * (exactly the cartographer specs) shipped dark for everyone, invisibly.
35
+ * LIMITATION: C matches the literal `enabled: false` spelling only.
36
+ *
28
37
  * Exit codes:
29
38
  * 0 — no violations.
30
39
  * 1 — at least one violation.
@@ -39,6 +48,12 @@ import fs from 'node:fs';
39
48
  import path from 'node:path';
40
49
  import { execSync } from 'node:child_process';
41
50
  import { fileURLToPath } from 'node:url';
51
+ import {
52
+ codeOnly,
53
+ attributeEnabledFalsePaths,
54
+ extractRegistry,
55
+ VALID_CATEGORIES,
56
+ } from './lib/dark-gate-attribution.js';
42
57
 
43
58
  const __filename = fileURLToPath(import.meta.url);
44
59
  const __dirname = path.dirname(__filename);
@@ -69,16 +84,6 @@ const HARDCODED_ENABLED = /(["']?)enabled\1\s*:\s*false\b/;
69
84
  const BLOCK_OPEN_SEARCH = 15; // max non-code lines between marker and the block's `{`
70
85
  const BLOCK_MAX_LINES = 120; // safety bound on block-body scan
71
86
 
72
- /** Strip a `//` line comment; return null if the line is a pure comment line. */
73
- function codeOnly(line) {
74
- const trimmed = line.trim();
75
- if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
76
- return null; // comment line — no code
77
- }
78
- const idx = line.indexOf('//');
79
- return idx >= 0 ? line.slice(0, idx) : line;
80
- }
81
-
82
87
  /** Is this line (trimmed) a comment line? */
83
88
  function isCommentLine(line) {
84
89
  const t = line.trim();
@@ -118,6 +123,47 @@ function resolveTargets() {
118
123
 
119
124
  const violations = [];
120
125
 
126
+ // Paths used by assertions B (skip-classified) and C (classification check).
127
+ // Tests inject fixtures via env overrides (the C-assertion reads the registry +
128
+ // ConfigDefaults directly, independent of the file args, so without an override a
129
+ // test could not exercise C's failure modes). Absolute paths only; default to the
130
+ // real repo files.
131
+ const CONFIG_DEFAULTS_REL = 'src/config/ConfigDefaults.ts';
132
+ const DEV_GATED_FEATURES_REL = 'src/core/devGatedFeatures.ts';
133
+ const CONFIG_DEFAULTS_ABS = process.env.INSTAR_DARKGATE_CONFIG_DEFAULTS
134
+ ? path.resolve(process.env.INSTAR_DARKGATE_CONFIG_DEFAULTS)
135
+ : path.join(ROOT, CONFIG_DEFAULTS_REL);
136
+ const REGISTRY_ABS = process.env.INSTAR_DARKGATE_REGISTRY
137
+ ? path.resolve(process.env.INSTAR_DARKGATE_REGISTRY)
138
+ : path.join(ROOT, DEV_GATED_FEATURES_REL);
139
+
140
+ // Precompute the registry + path attribution so assertion B can SKIP a hardcoded
141
+ // `enabled: false` whose attributed path is a DELIBERATE classification (a nested
142
+ // DARK_GATE_EXCLUSIONS entry under a gate-marker comment, e.g. the cost-bearing
143
+ // cartographer sweep). Without this, B false-flags a declared dark default that a
144
+ // parent block's gate-marker comment happens to enclose. The full classification
145
+ // check is assertion C below; B only needs the exclusion line-set to stay quiet on
146
+ // already-classified lines.
147
+ const _configDefaultsAbs = CONFIG_DEFAULTS_ABS;
148
+ const _registryAbs = REGISTRY_ABS;
149
+ let _exclusionClassifiedLines = new Set(); // 1-based ConfigDefaults.ts line numbers
150
+ if (fs.existsSync(_configDefaultsAbs) && fs.existsSync(_registryAbs)) {
151
+ try {
152
+ const { exclusionPaths } = extractRegistry(_registryAbs);
153
+ const exclSet = new Set(exclusionPaths);
154
+ const { paths: attributed, error } = attributeEnabledFalsePaths(_configDefaultsAbs);
155
+ if (!error) {
156
+ for (const { line, dottedPath } of attributed) {
157
+ if (exclSet.has(dottedPath)) _exclusionClassifiedLines.add(line);
158
+ }
159
+ }
160
+ } catch {
161
+ // If precompute fails, B falls back to its original behavior (flag all);
162
+ // assertion C reports the real desync/error loudly.
163
+ _exclusionClassifiedLines = new Set();
164
+ }
165
+ }
166
+
121
167
  for (const file of resolveTargets()) {
122
168
  if (!fs.existsSync(file)) continue;
123
169
  const rel = path.relative(ROOT, file);
@@ -163,7 +209,15 @@ for (const file of resolveTargets()) {
163
209
  for (let j = openIdx; j <= Math.min(openIdx + BLOCK_MAX_LINES, lines.length - 1); j++) {
164
210
  const codeJ = codeOnly(lines[j]);
165
211
  if (codeJ === null) continue;
166
- if (HARDCODED_ENABLED.test(codeJ) && !reportedB.has(j)) {
212
+ // Skip a line that is a DELIBERATE DARK_GATE_EXCLUSIONS classification
213
+ // (a nested dark default that a parent block's gate-marker comment merely
214
+ // encloses — e.g. the cost-bearing cartographer sweep). Assertion C still
215
+ // requires it to be classified; B should not double-flag it.
216
+ if (
217
+ HARDCODED_ENABLED.test(codeJ) &&
218
+ !reportedB.has(j) &&
219
+ !(rel === CONFIG_DEFAULTS_REL && _exclusionClassifiedLines.has(j + 1))
220
+ ) {
167
221
  reportedB.add(j);
168
222
  violations.push({
169
223
  file: rel, line: j + 1, kind: 'B: hardcoded enabled under gate marker',
@@ -181,16 +235,107 @@ for (const file of resolveTargets()) {
181
235
  }
182
236
  }
183
237
 
238
+ // ════════════════════════════════════════════════════════════════════════════
239
+ // Assertion C — NO UNCLASSIFIED DARK DEFAULT (DEV-AGENT-DARK-GATE-ENFORCEMENT B2)
240
+ //
241
+ // Every literal `enabled: false` in src/config/ConfigDefaults.ts must be a
242
+ // DECLARED choice: its attributed config path is EITHER registered in
243
+ // DEV_GATED_FEATURES (→ but then it must NOT also hardcode `enabled: false`; a
244
+ // dev-gated feature OMITS enabled) OR classified in DARK_GATE_EXCLUSIONS with a
245
+ // category + reason. Neither → violation. This closes the cartographer hole:
246
+ // assertion B only fires under a comment marker; a marker-less hardcoded
247
+ // `enabled: false` (exactly the cartographer specs) was invisible.
248
+ //
249
+ // LIMITATION (P2 Signal-vs-Authority — do NOT claim full closure): assertion C
250
+ // matches the LITERAL `enabled: false` spelling ONLY. A non-literal default
251
+ // (`enabled: someFlag ?? false`) evades it — the same miss named in the prior
252
+ // conformance spec's Layer-2 row. C closes the literal-false hole (cartographer +
253
+ // #1001), not the non-literal-expression hole.
254
+ //
255
+ // Path attribution reuses codeOnly() for depth (shared with the golden-path test
256
+ // via scripts/lib/dark-gate-attribution.js — ONE attributor implementation).
257
+ // codeOnly() strips `//` comments but does NOT skip braces inside string/template
258
+ // literals — so a loud-fail guard in attributeEnabledFalsePaths errors if any line
259
+ // in the defaults-block region carries a `{`/`}` inside a string, rather than
260
+ // silently desyncing.
261
+ // ════════════════════════════════════════════════════════════════════════════
262
+
263
+ // Run assertion C only on a full-tree / explicit run that includes ConfigDefaults
264
+ // (the path attribution needs the whole file; a --staged run that doesn't touch
265
+ // it is a no-op for C, which is fine — CI runs the full tree).
266
+ (() => {
267
+ const configDefaultsAbs = CONFIG_DEFAULTS_ABS;
268
+ const registryAbs = REGISTRY_ABS;
269
+ if (!fs.existsSync(configDefaultsAbs) || !fs.existsSync(registryAbs)) return;
270
+
271
+ const { gatedPaths, exclusionPaths, exclusionEntries } = extractRegistry(registryAbs);
272
+ const gatedSet = new Set(gatedPaths);
273
+ const exclusionSet = new Set(exclusionPaths);
274
+
275
+ // Validate exclusion-entry quality (closed enum + reason length).
276
+ for (const e of exclusionEntries) {
277
+ if (!VALID_CATEGORIES.has(e.category)) {
278
+ violations.push({
279
+ file: DEV_GATED_FEATURES_REL, line: 0, kind: 'C: invalid exclusion category',
280
+ text: `${e.configPath} → category '${e.category}'`,
281
+ fix: `category must be one of: ${[...VALID_CATEGORIES].join(', ')}`,
282
+ });
283
+ }
284
+ const reasonLen = (e.reason || '').replace(/\s/g, '').length;
285
+ if (reasonLen < 12) {
286
+ violations.push({
287
+ file: DEV_GATED_FEATURES_REL, line: 0, kind: 'C: exclusion reason too short',
288
+ text: `${e.configPath} → reason '${e.reason}' (${reasonLen} non-ws chars)`,
289
+ fix: 'a DARK_GATE_EXCLUSIONS reason must be ≥12 non-whitespace chars (defeats placeholder reasons)',
290
+ });
291
+ }
292
+ }
293
+
294
+ const { paths: attributed, error } = attributeEnabledFalsePaths(configDefaultsAbs);
295
+ if (error) {
296
+ violations.push({
297
+ file: CONFIG_DEFAULTS_REL, line: 0, kind: 'C: path-attribution error',
298
+ text: error,
299
+ fix: 'resolve the desync condition before the lint can attribute dark defaults',
300
+ });
301
+ return;
302
+ }
303
+
304
+ for (const { line, dottedPath } of attributed) {
305
+ const inGated = gatedSet.has(dottedPath);
306
+ const inExcluded = exclusionSet.has(dottedPath);
307
+ if (inGated) {
308
+ // Registered as dev-gated but still hardcodes `enabled: false` — the #1001
309
+ // shape. A dev-gated feature OMITS enabled.
310
+ violations.push({
311
+ file: CONFIG_DEFAULTS_REL, line, kind: 'C: registered but hardcodes false',
312
+ text: `${dottedPath} is in DEV_GATED_FEATURES but still hardcodes \`enabled: false\``,
313
+ fix: 'OMIT `enabled` from the default so the gate decides (resolved as enabled ?? !!developmentAgent)',
314
+ });
315
+ } else if (!inExcluded) {
316
+ violations.push({
317
+ file: CONFIG_DEFAULTS_REL, line, kind: 'C: unclassified dark default',
318
+ text: `${dottedPath} has \`enabled: false\` but is in NEITHER DEV_GATED_FEATURES NOR DARK_GATE_EXCLUSIONS`,
319
+ fix: 'dev-gate it (omit `enabled` + register in DEV_GATED_FEATURES) OR add it to DARK_GATE_EXCLUSIONS with a category+reason',
320
+ });
321
+ }
322
+ }
323
+ })();
324
+
184
325
  if (violations.length === 0) {
185
326
  console.log('lint-dev-agent-dark-gate: clean');
186
327
  process.exit(0);
187
328
  }
188
329
 
189
330
  console.error('\n❌ lint-dev-agent-dark-gate found violations of the developmentAgent dark-feature gate standard:\n');
331
+ console.error('NOTE: assertion C matches the literal `enabled: false` spelling only — a non-literal');
332
+ console.error('default (`enabled: someFlag ?? false`) evades it. C closes the literal-false hole');
333
+ console.error('(cartographer + #1001), not the non-literal-expression hole.\n');
190
334
  for (const v of violations) {
191
- console.error(` ${v.file}:${v.line} [${v.kind}]`);
335
+ const loc = v.line ? `${v.file}:${v.line}` : v.file;
336
+ console.error(` ${loc} [${v.kind}]`);
192
337
  console.error(` ${v.text}`);
193
338
  console.error(` fix: ${v.fix}\n`);
194
339
  }
195
- console.error('Standard: a dev-gated feature OMITS `enabled` and resolves it through resolveDevAgentGate so it runs LIVE on dev agents and DARK on the fleet. Spec: docs/specs/DEV-AGENT-DARK-GATE-CONFORMANCE-SPEC.md\n');
340
+ console.error('Standard: a dev-gated feature OMITS `enabled` and resolves it through resolveDevAgentGate so it runs LIVE on dev agents and DARK on the fleet; every other `enabled: false` default must be classified in DARK_GATE_EXCLUSIONS. Spec: docs/specs/DEV-AGENT-DARK-GATE-ENFORCEMENT-SPEC.md\n');
196
341
  process.exit(1);