loki-mode 9.49.4 → 9.50.0

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v9.49.4
6
+ # Loki Mode v9.50.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.49.4 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.50.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.49.4
1
+ 9.50.0
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.49.4"
10
+ __version__ = "9.50.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v9.49.4
5
+ **Version:** v9.50.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var ht=Object.create;var{getPrototypeOf:gt,defineProperty:jG,getOwnPropertyNames:mt}=Object;var ut=Object.prototype.hasOwnProperty;function dt($){return this[$]}var pt,ct,lt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?pt??=new WeakMap:ct??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?ht(gt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of mt($))if(!ut.call(J,q))jG(J,q,{get:dt.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var it=($)=>$;function at($,X){this[$]=it.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:at.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var HR={};B1(HR,{lokiDir:()=>h0,homeLokiDir:()=>GQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as ot}from"url";import{existsSync as Zq}from"fs";import{homedir as st}from"os";function nt(){let $=UR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(UR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function GQ(){return h2(st(),".loki")}var UR,L1;var k1=s(()=>{UR=LG(ot(import.meta.url));L1=nt()});import{readFileSync as rt}from"fs";import{resolve as tt,dirname as et}from"path";import{fileURLToPath as $e}from"url";function j9(){if(n3!==null)return n3;let $="9.49.4";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=et($e(import.meta.url)),Q=AG(X);n3=rt(tt(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var jR={};B1(jR,{runOrThrow:()=>Me,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>je,commandExists:()=>g5,ShellError:()=>TG,MAX_STDOUT_BYTES:()=>OR});async function Jq($,X=OR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Me($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new TG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=Oe($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Oe($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function je($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var OR=16777216,TG;var y8=s(()=>{TG=class TG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Le?"":$}var Le,p0,$5,q1,F61,A1,f1,m5,r;var t7=s(()=>{Le=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),F61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as Ee}from"fs";async function Z2(){if(BQ!==void 0)return BQ;let $="/opt/homebrew/bin/python3.12";if(Ee($))return BQ=$,$;let X=await g5("python3.12");if(X)return BQ=X,X;let Q=await g5("python3");return BQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var BQ;var m2=s(()=>{y8()});var hR={};B1(hR,{runStatus:()=>$00});import{existsSync as u5,readFileSync as C9,readdirSync as xR,statSync as kR}from"fs";import{resolve as A5,basename as le}from"path";import{homedir as ie}from"os";function SR($){let X=Math.trunc($);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function yR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=SR($),Y=SR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function oe(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
2
+ var ht=Object.create;var{getPrototypeOf:gt,defineProperty:jG,getOwnPropertyNames:mt}=Object;var ut=Object.prototype.hasOwnProperty;function dt($){return this[$]}var pt,ct,lt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?pt??=new WeakMap:ct??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?ht(gt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of mt($))if(!ut.call(J,q))jG(J,q,{get:dt.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var it=($)=>$;function at($,X){this[$]=it.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:at.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var HR={};B1(HR,{lokiDir:()=>h0,homeLokiDir:()=>GQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as ot}from"url";import{existsSync as Zq}from"fs";import{homedir as st}from"os";function nt(){let $=UR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(UR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function GQ(){return h2(st(),".loki")}var UR,L1;var k1=s(()=>{UR=LG(ot(import.meta.url));L1=nt()});import{readFileSync as rt}from"fs";import{resolve as tt,dirname as et}from"path";import{fileURLToPath as $e}from"url";function j9(){if(n3!==null)return n3;let $="9.50.0";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=et($e(import.meta.url)),Q=AG(X);n3=rt(tt(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var jR={};B1(jR,{runOrThrow:()=>Me,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>je,commandExists:()=>g5,ShellError:()=>TG,MAX_STDOUT_BYTES:()=>OR});async function Jq($,X=OR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Me($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new TG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=Oe($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Oe($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function je($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var OR=16777216,TG;var y8=s(()=>{TG=class TG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Le?"":$}var Le,p0,$5,q1,F61,A1,f1,m5,r;var t7=s(()=>{Le=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),F61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as Ee}from"fs";async function Z2(){if(BQ!==void 0)return BQ;let $="/opt/homebrew/bin/python3.12";if(Ee($))return BQ=$,$;let X=await g5("python3.12");if(X)return BQ=X,X;let Q=await g5("python3");return BQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var BQ;var m2=s(()=>{y8()});var hR={};B1(hR,{runStatus:()=>$00});import{existsSync as u5,readFileSync as C9,readdirSync as xR,statSync as kR}from"fs";import{resolve as A5,basename as le}from"path";import{homedir as ie}from"os";function SR($){let X=Math.trunc($);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function yR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=SR($),Y=SR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function oe(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -1337,4 +1337,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1337
1337
  `),2}case"start":{let{runStart:z}=await Promise.resolve().then(() => (ft(),bt));return z(Q)}default:return process.stderr.write(`Unknown command: ${X}
1338
1338
  `),process.stderr.write(_t),2}}PR();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var J61=await K61(Bun.argv.slice(2));process.exit(J61);
1339
1339
 
1340
- //# debugId=7E564B4B25CD72DC1835C1160D48FD27
1340
+ //# debugId=DA77814E40E3A050A0095A6FF7E6A0A4
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.49.4'
78
+ __version__ = '9.50.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "9.49.4",
4
+ "version": "9.50.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider, opencode).",
6
6
  "keywords": [
7
7
  "agent",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "9.49.4",
5
+ "version": "9.50.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -1255,7 +1255,12 @@ MemEvolve analyzes how memories are located and retrieved at inference time.
1255
1255
  ```
1256
1256
 
1257
1257
  2. **Temporal Index:** Date-based episodic retrieval
1258
- - `.loki/memory/episodic/index.json` provides temporal navigation
1258
+ - Episodes are stored as `.loki/memory/episodic/<YYYY-MM-DD>/task-<id>.json`
1259
+ (`memory/storage.py:477,521`), so the DATE DIRECTORY is what provides
1260
+ temporal navigation
1261
+ - The index layer itself is `index.json` at the memory ROOT, not under
1262
+ `episodic/` (`memory/engine.py:151`). `memory/cross_project.py:67` walks
1263
+ the store recursively and explicitly SKIPS `index.json` sidecars
1259
1264
  - Enables queries like "what happened last week"
1260
1265
 
1261
1266
  3. **Keyword Search:** Skill matching by keywords
@@ -601,7 +601,12 @@ VERIFY:
601
601
 
602
602
  ## Key Metrics Dashboard
603
603
 
604
- Track these metrics in `.loki/metrics/dashboard.json`:
604
+ Track these metrics using the shape below. NOTE: this is the reference schema
605
+ for the pattern, NOT a file Loki writes -- nothing in this repository produces
606
+ `.loki/metrics/dashboard.json`. The metrics that ARE written per run are
607
+ `.loki/metrics/efficiency/iteration-<N>.json` (`autonomy/run.sh:8303`),
608
+ `.loki/metrics/budget.json`, and `.loki/metrics/trust-events.jsonl`
609
+ (`autonomy/run.sh:2620`).
605
610
 
606
611
  ```json
607
612
  {
@@ -150,14 +150,27 @@ rate_limit_handling:
150
150
  circuit_breaker:
151
151
  threshold: 3 failures in 60s
152
152
  cooldown: 300s
153
- state_file: ".loki/state/circuit-breakers.json"
154
153
  ```
155
154
 
156
155
  ### Circuit Breaker System
157
156
 
158
157
  The circuit breaker prevents cascading failures by temporarily disabling operations that are repeatedly failing.
159
158
 
160
- #### JSON Schema: `.loki/state/circuit-breakers.json`
159
+ Loki ships three circuit breakers, and all of them hold state IN MEMORY for the
160
+ life of the run. None persists to a state file:
161
+
162
+ - `council_circuit_breaker_triggered()` (`autonomy/completion-council.sh:397`) --
163
+ stagnation and repeated done-signals
164
+ - the repeated-signature breaker (`loki-ts/src/runner/recovery_policy.ts:91`)
165
+ - the budget breaker (`loki-ts/src/runner/budget.ts:296`,
166
+ `check_budget_limit()` in `autonomy/run.sh`), which writes
167
+ `.loki/metrics/budget.json` and `.loki/signals/BUDGET_EXCEEDED`
168
+
169
+ #### Reference schema (DESIGN PATTERN -- not an artifact Loki writes)
170
+
171
+ The shape below describes the pattern for an implementer. No code in this
172
+ repository reads or writes `.loki/state/circuit-breakers.json`; do not expect
173
+ the file to exist at runtime.
161
174
 
162
175
  ```json
163
176
  {
@@ -310,8 +323,12 @@ The circuit breaker prevents cascading failures by temporarily disabling operati
310
323
  When a circuit breaker is OPEN:
311
324
 
312
325
  1. **Check State**
326
+
327
+ Breaker state is in memory, so read it from the run's own output rather than
328
+ a file. The budget breaker is the one with durable artifacts:
313
329
  ```bash
314
- cat .loki/state/circuit-breakers.json | jq '.["api/claude"]'
330
+ cat .loki/metrics/budget.json # budget_used vs the cap
331
+ ls .loki/signals/BUDGET_EXCEEDED # present once the cap is breached
315
332
  ```
316
333
 
317
334
  2. **Calculate Wait Time**
@@ -461,7 +478,9 @@ A task should be permanently abandoned when:
461
478
  | Scope no longer relevant | Project direction changed |
462
479
  | Human explicitly abandons | Documented decision to deprioritize |
463
480
 
464
- When abandoning, move task to `.loki/queue/abandoned.json` with reason documented.
481
+ When abandoning, move the task to `.loki/queue/dead-letter.json` with the reason
482
+ documented. That is the real terminal queue (`autonomy/run.sh:6536`,
483
+ `dashboard/server.py:2348`); there is no `abandoned.json`.
465
484
 
466
485
  ### Recovery Strategies
467
486
 
@@ -470,7 +489,7 @@ When abandoning, move task to `.loki/queue/abandoned.json` with reason documente
470
489
  | `retry_with_simpler_approach` | Complex implementation failed multiple times | Break into smaller subtasks, reduce scope, use simpler patterns |
471
490
  | `dependency_blocked` | Task needs output from another task that failed | Wait for dependency resolution, check daily |
472
491
  | `requires_human_review` | Security decision, unclear spec, or irreversible action | Log to `.loki/escalations/` and notify, do not retry |
473
- | `permanent_abandon` | Met abandon criteria above | Move to `abandoned.json`, document reason, move on |
492
+ | `permanent_abandon` | Met abandon criteria above | Move to `dead-letter.json`, document reason, move on |
474
493
 
475
494
  ### Human Escalation Triggers
476
495
 
@@ -523,7 +542,7 @@ Human decision on recovery path.
523
542
  - Document recovery reason
524
543
  - Reset failure_count if new approach
525
544
  d. ELSE IF meets abandon criteria:
526
- - Move to abandoned.json
545
+ - Move to dead-letter.json
527
546
  - Document reason
528
547
  e. ELSE IF needs human:
529
548
  - Create escalation file
@@ -87,7 +87,16 @@ local_import_is_source() {
87
87
  import_base="$test_dir/$spec"
88
88
 
89
89
  case "$leaf" in
90
- *.js|*.jsx|*.ts|*.tsx|*.mjs|*.cjs|*.mts|*.cts|*.vue|*.svelte)
90
+ # TS NodeNext resolution: a spec written "./foo.js" (or .jsx/.mjs/.cjs)
91
+ # commonly names a real ".ts"/".tsx"/".mts"/".cts" source file -- the
92
+ # extension in the specifier is the post-build one, not the on-disk one.
93
+ # Try the literal path first, then its TS sibling, before falling
94
+ # through to "not a source import".
95
+ *.js) [ -f "$import_base" ] && return 0; [ -f "${import_base%.js}.ts" ] && return 0 ;;
96
+ *.jsx) [ -f "$import_base" ] && return 0; [ -f "${import_base%.jsx}.tsx" ] && return 0 ;;
97
+ *.mjs) [ -f "$import_base" ] && return 0; [ -f "${import_base%.mjs}.mts" ] && return 0 ;;
98
+ *.cjs) [ -f "$import_base" ] && return 0; [ -f "${import_base%.cjs}.cts" ] && return 0 ;;
99
+ *.ts|*.tsx|*.mts|*.cts|*.vue|*.svelte)
91
100
  [ -f "$import_base" ] && return 0
92
101
  ;;
93
102
  *.*) ;;
@@ -119,6 +128,11 @@ test_has_source_import() {
119
128
  # does -- it is how a subprocess/E2E test points at the source it runs.
120
129
  # Separate pattern (not an alternation) so the path stays BASH_REMATCH[1].
121
130
  local cjs_resolve_re="require\.resolve[[:space:]]*\([[:space:]]*['\"](\.{1,2}/[^'\"]+)['\"]"
131
+ # `await import('./x.ts')` / `import('./x.ts')` -- the dynamic-import form,
132
+ # used to load the module under test after mock.module() has been set up
133
+ # (bun:test's documented pattern for stubbing an import before it resolves).
134
+ # Same relative-path capture as the static forms above.
135
+ local dynamic_import_re="import[[:space:]]*\([[:space:]]*['\"](\.{1,2}/[^'\"]+)['\"]"
122
136
  # A test that hands a literal source path to a child process exercises the
123
137
  # real code end-to-end without ever naming it in an import. Gated on the
124
138
  # file importing child_process, because this flag unlocks a deliberately
@@ -212,6 +226,8 @@ test_has_source_import() {
212
226
  spec="${BASH_REMATCH[1]}"
213
227
  elif [[ "$line" =~ $cjs_re ]]; then
214
228
  spec="${BASH_REMATCH[1]}"
229
+ elif [[ "$line" =~ $dynamic_import_re ]]; then
230
+ spec="${BASH_REMATCH[1]}"
215
231
  elif [ "$has_subprocess" = true ] && [[ "$line" =~ $subproc_re ]]; then
216
232
  spec="${BASH_REMATCH[1]}"
217
233
  elif [ "$has_spawn_call" = true ] && [[ "$line" =~ $subproc_bare_re ]]; then