instar 1.3.1627 → 1.3.1628

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
5
- "packageVersion": "1.3.1627",
5
+ "packageVersion": "1.3.1628",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "24434c017122276e23e09b821e5f9e7f900d878c9a7cdaa143f3c6b0debdf62a",
2
+ "sha256": "b7bdec77909966bae0c48ed812dc177a28d214980fa5e6f5d415831cca3395b2",
3
3
  "registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
4
- "packageVersion": "1.3.1627"
4
+ "packageVersion": "1.3.1628"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
3
3
  "articleCount": 93,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1627"
5
+ "packageVersion": "1.3.1628"
6
6
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1627",
3
+ "version": "1.3.1628",
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",
@@ -21,11 +21,14 @@
21
21
  * for direct destructive `git <verb>` invocations (closed verb list).
22
22
  *
23
23
  * Exit codes:
24
- * 0 — no violations.
24
+ * 0 — every enumerated target file was inspected and none has a violation.
25
25
  * 1 — at least one violation.
26
- * 2 — could not inspect: files were scanned and NONE parsed (an environment
27
- * problem, e.g. a missing `typescript`). Distinct from 1 on purpose —
28
- * "I could not look" is not "I looked and found nothing".
26
+ * 2 — could not inspect: NONE of the scanned files parsed (an environment
27
+ * problem, e.g. a missing `typescript`), or SOME target file could not be
28
+ * read or parsed (or a directory could not be listed) and no violation was
29
+ * found elsewhere. Each uninspected path is printed with its reason.
30
+ * Distinct from 1 on purpose — "I could not look" is not "I looked and
31
+ * found nothing".
29
32
  *
30
33
  * Usage:
31
34
  * node scripts/lint-no-direct-destructive.js # full repo
@@ -237,6 +240,54 @@ function hasAllowComment(text) {
237
240
 
238
241
  const violations = [];
239
242
 
243
+ /**
244
+ * Files (or directories) this run was meant to inspect but could not: unreadable,
245
+ * unparseable, or an unlistable directory. Each one is a hole in the scan, and a
246
+ * hole must not look like a clean file (#2190 / #2189) — any entry here refuses
247
+ * the run with the path and the reason.
248
+ */
249
+ const uninspected = [];
250
+
251
+ function recordUninspected(file, reason) {
252
+ uninspected.push({ file, reason });
253
+ }
254
+
255
+ /** Errors worth one more attempt: the file may be mid-write or the fd table momentarily full. */
256
+ const TRANSIENT_READ_CODES = new Set(['EAGAIN', 'EBUSY', 'EINTR', 'EMFILE', 'ENFILE']);
257
+ const READ_RETRY_DELAYS_MS = [50, 200];
258
+
259
+ function sleepSync(ms) {
260
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
261
+ }
262
+
263
+ /** Read a file, briefly retrying transient errors. Throws the last error if every attempt fails. */
264
+ function readFileWithRetry(file) {
265
+ for (let attempt = 0; ; attempt++) {
266
+ try {
267
+ // Test hook, fail-closed only (same contract as INSTAR_LINT_FORCE_PARSE_FAILURE):
268
+ // it can only make a read fail, never make a file pass. `<basename>` fails
269
+ // every attempt; `<basename>:<n>` fails only the first n (exercises the retry).
270
+ const forced = process.env.INSTAR_LINT_FORCE_READ_FAILURE;
271
+ const [forcedName, forcedTimes] = (forced || '').split(':');
272
+ if (forcedName && path.basename(file) === forcedName
273
+ && (forcedTimes === undefined || attempt < Number(forcedTimes))) {
274
+ const err = new Error(`forced read failure (INSTAR_LINT_FORCE_READ_FAILURE=${forced})`);
275
+ err.code = 'EBUSY';
276
+ throw err;
277
+ }
278
+ return fs.readFileSync(file, 'utf8');
279
+ } catch (err) {
280
+ if (attempt >= READ_RETRY_DELAYS_MS.length || !TRANSIENT_READ_CODES.has(err.code)) throw err;
281
+ sleepSync(READ_RETRY_DELAYS_MS[attempt]);
282
+ }
283
+ }
284
+ }
285
+
286
+ function describeError(err) {
287
+ const message = String(err && err.message ? err.message : err);
288
+ return err && err.code && !message.startsWith(err.code) ? `${err.code}: ${message}` : message;
289
+ }
290
+
240
291
  /**
241
292
  * Per-callsite marker: `// safe-git-allow: incremental-migration` placed on
242
293
  * the line immediately above (or at end of) a flagged callsite suppresses
@@ -336,6 +387,12 @@ const FS_MODULE_NAMES = new Set([
336
387
  * 7. Dynamic property access used to evade the rule (`fs['rm' + 'Sync']`) — flag.
337
388
  */
338
389
  function lintTsFile(file, text) {
390
+ // Per-file variant of the INSTAR_LINT_FORCE_PARSE_FAILURE hook (a basename
391
+ // instead of '1'), for the partial-failure refusal. Fail-closed only.
392
+ const forcedParse = process.env.INSTAR_LINT_FORCE_PARSE_FAILURE;
393
+ if (forcedParse && forcedParse !== '1' && path.basename(file) === forcedParse) {
394
+ throw new Error(`forced parse failure (INSTAR_LINT_FORCE_PARSE_FAILURE=${forcedParse})`);
395
+ }
339
396
  const T = ts();
340
397
  const sf = T.createSourceFile(file, text, T.ScriptTarget.Latest, true);
341
398
  const ctx = { text };
@@ -728,7 +785,9 @@ function lintPackageJsonScripts(file, text) {
728
785
  let pkg;
729
786
  try {
730
787
  pkg = JSON.parse(text);
731
- } catch {
788
+ } catch (err) {
789
+ // An unparseable package.json hides every npm script from the git-verb check.
790
+ recordUninspected(file, `could not parse as JSON: ${describeError(err)}`);
732
791
  return;
733
792
  }
734
793
  const scripts = pkg.scripts || {};
@@ -751,7 +810,10 @@ function walkDir(dir, out) {
751
810
  let entries;
752
811
  try {
753
812
  entries = fs.readdirSync(dir, { withFileTypes: true });
754
- } catch {
813
+ } catch (err) {
814
+ // A target dir that does not exist has nothing to inspect; one that exists
815
+ // but cannot be listed hides every file beneath it.
816
+ if (err.code !== 'ENOENT') recordUninspected(dir, `could not list directory: ${describeError(err)}`);
755
817
  return;
756
818
  }
757
819
  for (const e of entries) {
@@ -809,6 +871,18 @@ function gatherAll() {
809
871
  return out;
810
872
  }
811
873
 
874
+ function writeUninspected() {
875
+ if (uninspected.length === 0) return;
876
+ process.stderr.write('\n');
877
+ process.stderr.write(
878
+ `[lint-no-direct-destructive] COULD NOT INSPECT ${uninspected.length} path(s) — `
879
+ + 'these were NOT checked, so this run cannot report them clean:\n'
880
+ );
881
+ for (const u of uninspected) {
882
+ process.stderr.write(` ${path.relative(ROOT, u.file) || u.file}\n ${u.reason}\n`);
883
+ }
884
+ }
885
+
812
886
  // ── Main ───────────────────────────────────────────────────────────
813
887
 
814
888
  function main() {
@@ -828,8 +902,9 @@ function main() {
828
902
  for (const file of files) {
829
903
  let text;
830
904
  try {
831
- text = fs.readFileSync(file, 'utf8');
832
- } catch {
905
+ text = readFileWithRetry(file);
906
+ } catch (err) {
907
+ recordUninspected(file, `could not read: ${describeError(err)}`);
833
908
  continue;
834
909
  }
835
910
 
@@ -854,10 +929,12 @@ function main() {
854
929
  lintTsFile(file, text);
855
930
  astParsed += 1;
856
931
  } catch (err) {
857
- // Parse failure → emit a soft warning, not a violation. Deliberate: one
858
- // unparseable file should not fail a build over syntax this parser does
859
- // not accept. The total-failure case below is a different situation.
860
- process.stderr.write(`[lint-no-direct-destructive] failed to parse ${rel}: ${err.message}\n`);
932
+ // A thrown parse is a file this run did not inspect (#2190). It used to be a
933
+ // stderr warning with exit 0, so an unchecked file looked exactly like a
934
+ // clean one. Syntax the parser dislikes does NOT land here —
935
+ // createSourceFile recovers and returns a tree — so this cannot fail a
936
+ // build over one odd-but-valid file; it fires when inspection itself broke.
937
+ recordUninspected(file, `failed to parse: ${describeError(err)}`);
861
938
  }
862
939
  }
863
940
 
@@ -870,6 +947,7 @@ function main() {
870
947
  // This is deliberately the TOTAL-failure case only, so it cannot fail a build
871
948
  // over one file the parser dislikes: in any working checkout, files parse.
872
949
  if (astAttempted > 0 && astParsed === 0) {
950
+ writeUninspected();
873
951
  process.stderr.write('\n');
874
952
  process.stderr.write(
875
953
  `[lint-no-direct-destructive] COULD NOT INSPECT — ${astAttempted} file(s) were scanned `
@@ -886,6 +964,17 @@ function main() {
886
964
  return 2;
887
965
  }
888
966
 
967
+ // Partial holes (#2190): some files could not be inspected. Name each one and
968
+ // its reason. Exit 2 ("could not inspect") unless violations were also found,
969
+ // which stay exit 1 — a definite finding outranks an incomplete scan.
970
+ if (uninspected.length > 0) {
971
+ writeUninspected();
972
+ if (violations.length === 0) {
973
+ process.stderr.write('Fix the cause (permissions, encoding, a broken install) and re-run.\n');
974
+ return 2;
975
+ }
976
+ }
977
+
889
978
  if (violations.length === 0) {
890
979
  return 0;
891
980
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-11-03T14:15:29.861Z",
5
- "instarVersion": "1.3.1627",
4
+ "generatedAt": "2026-11-03T18:02:09.861Z",
5
+ "instarVersion": "1.3.1628",
6
6
  "entryCount": 206,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
5
- "packageVersion": "1.3.1627",
5
+ "packageVersion": "1.3.1628",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "24434c017122276e23e09b821e5f9e7f900d878c9a7cdaa143f3c6b0debdf62a",
2
+ "sha256": "b7bdec77909966bae0c48ed812dc177a28d214980fa5e6f5d415831cca3395b2",
3
3
  "registrySha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
4
- "packageVersion": "1.3.1627"
4
+ "packageVersion": "1.3.1628"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "db01d9f92c6aa26ca903702896b41be53722982ed07d27633237bb254c214290",
3
3
  "articleCount": 93,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1627"
5
+ "packageVersion": "1.3.1628"
6
6
  }
@@ -0,0 +1,21 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The direct destructive-operation lint now reports files and directories it could not inspect. Temporary read errors are retried briefly; an unreadable or unparseable path produces a nonzero result instead of being silently skipped.
9
+
10
+ ## What to Tell Your User
11
+
12
+ The source safety check now tells you when it could not inspect an enumerated source file. A clean result means it read the paths it selected to inspect.
13
+
14
+ ## Summary of New Capabilities
15
+
16
+ - Names unreadable or unparseable selected files and unlistable existing directories, with the reason inspection failed.
17
+ - Retries short-lived file read errors before reporting that the scan was incomplete.
18
+
19
+ ## Evidence
20
+
21
+ The focused lint tests cover read and parse failures, transient retries, unreadable directories, and a clean tree. The exact PR head also passed the local unit, integration, and end-to-end verification before this release-note-only follow-up; the new head still requires its own verification.
@@ -0,0 +1,72 @@
1
+ # Side-Effects Review — destructive-operation lint scan completeness
2
+
3
+ **Version / slug:** `lint-destructive-fail-closed`
4
+ **Date:** 2026-11-03
5
+ **Author:** Echo desk, integrating Justin's 2.0 agent PR #2641
6
+ **Second-pass reviewer:** independent reviewer subagent, 2026-11-03
7
+
8
+ ## Summary of the change
9
+
10
+ `scripts/lint-no-direct-destructive.js` now records source paths it could not read, parse, or list, and returns exit 2 when inspection was incomplete and no definite violation was found. It retries transient read failures twice. Definite violations retain exit 1 and the incomplete paths are still reported. `tests/unit/lint-no-direct-destructive.test.ts` covers these boundaries. The release fragment and ELI16 overview explain the change. The decision point is whether a lint scan may certify a target set as clean; the existing policy still decides which destructive operations violate the rule.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - Lint scan completion and exit code — **modified**: an uninspected requested path makes a clean verdict unavailable.
15
+ - Destructive-operation detection — **passed through**: existing patterns and allow comments are unchanged.
16
+ - Release and pre-commit callers — **passed through**: they consume the command's result; this patch does not change those callers.
17
+
18
+ ## 1. Over-block
19
+
20
+ A legitimate source tree can now fail lint when a file is temporarily unreadable or the parser throws. Two short retries cover transient read errors, but a longer file replacement may still require a rerun. A directory that exists but cannot be listed also fails, even if it ultimately contains no relevant source file, because the scanner cannot establish that fact. A missing optional directory remains acceptable. The cost is a failed development gate with a named path, which is repairable without changing product state.
21
+
22
+ ## 2. Under-block
23
+
24
+ The scanner still only inspects paths selected by its existing traversal and file-extension rules. A destructive call outside those targets or hidden behind dynamic code can evade the preexisting detector. Explicit paths that do not exist, and staged paths whose files cannot be stat'ed, are filtered before the read loop; those remain a separate missing-target gap. A target file that is modified after it was read can change before a later build step. The normal `npm run lint` chain stops on exit 2, but `scripts/pre-push-gate.js` currently turns exit 2 into a warning. That caller can still let an incomplete pre-push scan through. This patch changes the scanner's verdict for selected paths; it does not make every caller reject that verdict or claim a transactionally frozen repository snapshot.
25
+
26
+ ## 3. Level-of-abstraction fit
27
+
28
+ Scan completeness belongs inside the scanner, where read, parse, and directory-list errors are visible. The existing development and release gates consume its exit code; a separate downstream checker could not recover which path was skipped. This is a structural validator for an attempted inspection, while the existing destructive-operation detector retains its policy.
29
+
30
+ ## 4. Signal vs authority compliance
31
+
32
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
33
+
34
+ - [x] This is a hard-invariant validation: a scan cannot certify a selected path it did not inspect.
35
+ - [ ] This adds a semantic judgment based on a brittle signal.
36
+
37
+ The principle explicitly permits structural validation and safety guards for destructive actions. The rule here is enumerable: an attempted target path was inspected or it was not. Exit 2 says “could not inspect” and prints evidence; it does not claim a destructive operation was found or infer author intent. The existing gate owns whether to block the commit on that result. A failure may need a retry, but a clean verdict must not be fabricated.
38
+
39
+ ## 4b. Judgment-point check
40
+
41
+ No new static heuristic at a competing-signals decision point. The only new choice is whether every selected path was inspected. That is an invariant of the scan, not a ranking among liveness, urgency, ownership, or conversational signals.
42
+
43
+ ## 5. Interactions
44
+
45
+ - **Shadowing:** The existing total-parser-failure branch still runs first; the new partial-hole branch follows it. A definite destructive violation still exits 1. The package-script JSON parser records malformed input rather than silently skipping it.
46
+ - **Double-fire:** A read failure records one path once; no separate worker is started. The diagnostic lists incomplete paths when a definite violation is present, avoiding a silent second failure.
47
+ - **Races:** A concurrent write may produce a transient read error. Two short retries precede exit 2; rerunning remains available. There is no shared mutable state across lint processes.
48
+ - **Feedback loops:** The lint is invoked by development gates and may stop a commit or push, but it does not mutate the source being inspected or schedule a new run.
49
+
50
+ ## 6. External surfaces
51
+
52
+ Other contributors and agents see path-specific stderr and exit 2 for an incomplete scan that previously looked clean. Existing scripts that distinguish exit 1 from exit 2 retain that distinction, including the pre-push gate's warning behavior. No Telegram, GitHub API, database, or user message is emitted by the lint. No operator-facing action is added; resolving a named unreadable file remains a developer task. Runtime product behavior is unchanged until a developer invokes the lint gate.
53
+
54
+ ## 6b. Operator-surface quality
55
+
56
+ No operator surface — not applicable. No dashboard, approval, or grant form is changed.
57
+
58
+ ## 7. Multi-machine posture
59
+
60
+ **Machine-local BY DESIGN.** Each checkout's files, permissions, and parser installation are machine-specific facts; each machine runs its own lint against its own source tree. The result has no agent conversation, durable user state, notice, or URL to replicate. A PR merges only after its relevant gate evidence is evaluated for the code being shipped.
61
+
62
+ ## 8. Rollback cost
63
+
64
+ Revert the source and test commit, then rerun lint. No state migration or user repair is needed. A revert restores the old incomplete-scan behavior, so it should be used only if the new diagnostic itself is faulty; the named path gives a narrower repair route.
65
+
66
+ ## Conclusion and class closure
67
+
68
+ The change handles unreadable files, thrown parses, malformed package scripts, and unlistable existing directories with one incomplete-scan record path. A selected file that reaches the read loop is no longer silently skipped. Remaining limits include missing paths filtered before that loop, the pre-push caller's warning treatment of exit 2, and the existing pattern policy. The focused tests and exact-head local gate are the verification surfaces; the new commit needs its own exact-head gate before merge.
69
+
70
+ ## Independent second pass
71
+
72
+ Confirmed the pre-push warning and pre-enumeration missing-path gaps are disclosed under Under-block; the selected-path scan-completeness change is reversible and consistent with the structural safety-guard exception in `docs/signal-vs-authority.md`. **Concur with the review.**