fix(codex): rewrite memory write via PreToolUse allow to stop double exec #424
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [main, dev] | |
| # Run on every PR regardless of base branch. The `branches` filter on | |
| # pull_request only matches base, so stacked / long-lived branches | |
| # (e.g. `optimizations`) would otherwise skip the whole CI job. | |
| pull_request: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| jobs: | |
| duplication: | |
| # Code-duplication regression guard. Pulled out of the `test` job so | |
| # the PR checks table shows a dedicated pass/fail row — reviewers see | |
| # at a glance whether the change introduced duplicated code without | |
| # having to open the combined "Typecheck and Test" log. | |
| name: Duplication check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| with: | |
| # Read-only job: don't leave GITHUB_TOKEN in .git/config. | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Run jscpd | |
| # Threshold 7% is the current baseline (see .jscpd.json). The job | |
| # fails if a future change pushes duplication above it, so the | |
| # number is a regression guard — reviewers can see the exact | |
| # clones in the markdown report uploaded below. | |
| run: npm run dup | |
| - name: Upload jscpd report | |
| if: always() | |
| uses: actions/upload-artifact@v7.0.1 | |
| with: | |
| name: jscpd-report | |
| path: jscpd-report/ | |
| if-no-files-found: ignore | |
| windows-smoke: | |
| # Windows leg for the win32-specific bugs fixed in the Codex/PowerShell | |
| # report (2026-05-29): | |
| # 1. capture hook crashed with exit 1 because `spawn("nohup", ...)` | |
| # ENOENTs on Windows (no nohup) and the async 'error' event had no | |
| # listener — fixed in src/utils/spawn-detached.ts. | |
| # 2. re-install duplicated the PostToolUse hook because hook-dedup | |
| # matched forward-slash paths while Windows writes backslash paths — | |
| # fixed in src/cli/install-codex.ts + install-cursor.ts. | |
| # 3. wiki-worker summary generation failed on Windows because the | |
| # prompt was passed as a CLI arg (crashes with E2BIG / quoting | |
| # issues) — fixed by writing to a temp file and passing the path | |
| # instead (codex/cursor/pi/hermes workers, PR #250). | |
| # | |
| # Scoped to suites that cover those fixes rather than the full vitest run: | |
| # the bulk of the suite assumes POSIX paths / chmod / symlinks and would | |
| # fail on a Windows runner for reasons unrelated to these fixes. | |
| # The wiki-worker suites mock execFileSync entirely (no real spawning, | |
| # no POSIX-only paths), so they run cleanly on windows-latest. | |
| name: Windows smoke (spawn + hook dedup + wiki-worker) | |
| runs-on: windows-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| with: | |
| # Don't persist the checkout token into .git/config — it would be | |
| # readable by later steps / artifacts. This job only reads + tests. | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Install dependencies | |
| # tree-sitter is an optionalDependency; if its native build fails on | |
| # Windows, npm continues — the scoped suites below don't import it. | |
| run: npm install | |
| - name: Run Windows-relevant suites | |
| run: npx vitest run tests/shared/spawn-detached.test.ts tests/cli/install-helpers.test.ts tests/codex/codex-wiki-worker.test.ts tests/cursor/cursor-wiki-worker.test.ts tests/pi/pi-wiki-worker.test.ts tests/hermes/hermes-wiki-worker.test.ts | |
| test: | |
| name: Typecheck and Test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| with: | |
| # Full history so the "Build PR coverage comment" step can do | |
| # `git diff origin/<base>...HEAD` to detect touched src/ files. | |
| # Default shallow checkout (depth=1) produces "no merge base". | |
| fetch-depth: 0 | |
| # Read-only job (diff is local once history is fetched): don't leave | |
| # GITHUB_TOKEN in .git/config. | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build (typecheck + emit bundle artefacts) | |
| # `build` runs `tsc && esbuild`, which is a strict superset of | |
| # `typecheck` (the bare `tsc --noEmit` we used to run here). It | |
| # ALSO produces the per-agent bundles AND `harnesses/openclaw/dist/`. The | |
| # latter is gitignored, so it doesn't exist after a fresh | |
| # `actions/checkout` — and several bundle-scan tests under | |
| # `harnesses/claude-code/tests/skillify-session-start-injection.test.ts` read | |
| # `harnesses/openclaw/dist/index.js` and `harnesses/openclaw/dist/skillify-worker.js` | |
| # directly. Without this rebuild they fail with ENOENT (see PR #98 | |
| # — first CI run after the openclaw skillify wiring landed). | |
| run: npm run build | |
| - name: Audit openclaw bundle against ClawHub static-scan rules | |
| # PR-time gate mirroring release.yml's audit step. Catches a | |
| # flagged bundle BEFORE the PR merges instead of only at release | |
| # time (where it'd correctly abort the publish, but the bad code | |
| # would already be in main). `--criticals-only` blocks on | |
| # critical findings only; warns are advisory (the openclaw | |
| # skillify-worker's `readFileSync` + `fetch` warn is irreducible | |
| # without splitting the worker into multiple shipped files). | |
| # If this step trips, run `npm run audit:openclaw` locally and | |
| # see scripts/audit-openclaw-bundle.mjs for the rule details. | |
| run: npm run audit:openclaw -- --criticals-only | |
| - name: Run tests with coverage | |
| # Per-file 80% thresholds for PR #60 files are declared in | |
| # vitest.config.ts under `coverage.thresholds`. Vitest exits non-zero | |
| # if any of them regress below 80%, which fails the job. | |
| run: npx vitest run --coverage | |
| - name: Write coverage summary to job page | |
| if: always() | |
| run: | | |
| if [ -f coverage/coverage-summary.json ]; then | |
| echo "### Test Coverage (overall)" >> $GITHUB_STEP_SUMMARY | |
| node -e " | |
| const c = require('./coverage/coverage-summary.json').total; | |
| const fmt = (v) => v.pct.toFixed(1) + '%'; | |
| console.log('| Metric | Coverage |'); | |
| console.log('|--------|----------|'); | |
| console.log('| Statements | ' + fmt(c.statements) + ' |'); | |
| console.log('| Branches | ' + fmt(c.branches) + ' |'); | |
| console.log('| Functions | ' + fmt(c.functions) + ' |'); | |
| console.log('| Lines | ' + fmt(c.lines) + ' |'); | |
| " >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### PR-tracked files (must stay ≥ 80 %)" >> $GITHUB_STEP_SUMMARY | |
| node -e " | |
| const summary = require('./coverage/coverage-summary.json'); | |
| const tracked = [ | |
| 'src/shell/grep-core.ts', | |
| 'src/shell/grep-interceptor.ts', | |
| 'src/hooks/grep-direct.ts', | |
| ]; | |
| console.log('| File | Stmts | Branch | Funcs | Lines |'); | |
| console.log('|------|------:|-------:|------:|------:|'); | |
| const fmt = v => v == null ? '—' : v.toFixed(1) + '%'; | |
| for (const rel of tracked) { | |
| const key = Object.keys(summary).find(k => k.endsWith(rel)); | |
| const c = key ? summary[key] : null; | |
| if (!c) { console.log('| \`' + rel + '\` | — | — | — | — |'); continue; } | |
| console.log('| \`' + rel + '\` | ' + fmt(c.statements.pct) + ' | ' + fmt(c.branches.pct) + ' | ' + fmt(c.functions.pct) + ' | ' + fmt(c.lines.pct) + ' |'); | |
| } | |
| " >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Build PR coverage comment | |
| if: github.event_name == 'pull_request' && always() | |
| id: pr-coverage | |
| continue-on-error: true | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| if [ ! -f coverage/coverage-summary.json ]; then | |
| echo "no coverage summary — skipping PR comment" | |
| echo "body-file=" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| node <<'NODE' > /tmp/pr-coverage.md | |
| const { execSync } = require('node:child_process'); | |
| const fs = require('node:fs'); | |
| const summary = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8')); | |
| const baseRef = process.env.BASE_REF || 'main'; | |
| const diff = execSync(`git diff --name-only origin/${baseRef}...HEAD`, { encoding: 'utf8' }); | |
| const changed = diff.split('\n') | |
| .filter(f => f.startsWith('src/') && f.endsWith('.ts') && !f.endsWith('.test.ts') && !f.endsWith('.d.ts')) | |
| .sort(); | |
| // Aggregate totals across ONLY the PR-touched files. Same shape | |
| // as the davelosert "Coverage Report" box, but scoped to this PR | |
| // instead of the whole src/ tree. | |
| const agg = { | |
| statements: { total: 0, covered: 0 }, | |
| branches: { total: 0, covered: 0 }, | |
| functions: { total: 0, covered: 0 }, | |
| lines: { total: 0, covered: 0 }, | |
| }; | |
| const perFile = []; | |
| for (const f of changed) { | |
| const key = Object.keys(summary).find(k => k.endsWith(f)); | |
| if (!key) { perFile.push({ f, c: null }); continue; } | |
| const c = summary[key]; | |
| for (const k of Object.keys(agg)) { | |
| agg[k].total += c[k].total; | |
| agg[k].covered += c[k].covered; | |
| } | |
| perFile.push({ f, c }); | |
| } | |
| const THRESHOLD = 90; | |
| const pct = (m) => m.total === 0 ? null : (m.covered / m.total) * 100; | |
| const icon = (p) => p == null ? '⚪' : (p >= THRESHOLD ? '🟢' : '🔴'); | |
| const fmtPct = (p) => p == null ? '—' : p.toFixed(2) + '%'; | |
| const fmtCell = (c, k) => c ? ((c[k].pct >= THRESHOLD ? '🟢 ' : '🔴 ') + c[k].pct.toFixed(1) + '%') : '—'; | |
| const out = []; | |
| out.push('## Coverage Report'); | |
| out.push(''); | |
| if (changed.length === 0) { | |
| out.push('_No `src/*.ts` files changed in this PR._'); | |
| } else { | |
| out.push('Scope: files changed in this PR. Enforced threshold: **' + THRESHOLD + '%** per metric (per file via `vitest.config.ts`).'); | |
| out.push(''); | |
| out.push('| Status | Category | Percentage | Covered / Total |'); | |
| out.push('|--------|----------|-----------:|----------------:|'); | |
| for (const [label, key] of [["Lines","lines"],["Statements","statements"],["Functions","functions"],["Branches","branches"]]) { | |
| const p = pct(agg[key]); | |
| out.push(`| ${icon(p)} | ${label} | ${fmtPct(p)} (🎯 ${THRESHOLD}%) | ${agg[key].covered} / ${agg[key].total} |`); | |
| } | |
| out.push(''); | |
| // File-level breakdown inside a <details> dropdown so a PR that | |
| // touches dozens/hundreds of files does not produce an endless | |
| // comment. Summary text shows the file count so you can see at | |
| // a glance how much is inside before expanding. | |
| out.push('<details>'); | |
| out.push(`<summary><strong>File Coverage</strong> — ${perFile.length} file${perFile.length === 1 ? '' : 's'} changed</summary>`); | |
| out.push(''); | |
| out.push('| File | Stmts | Branches | Functions | Lines |'); | |
| out.push('|------|------:|---------:|----------:|------:|'); | |
| for (const { f, c } of perFile) { | |
| out.push(`| \`${f}\` | ${fmtCell(c, 'statements')} | ${fmtCell(c, 'branches')} | ${fmtCell(c, 'functions')} | ${fmtCell(c, 'lines')} |`); | |
| } | |
| out.push(''); | |
| out.push('</details>'); | |
| } | |
| out.push(''); | |
| out.push(`<sub>Generated for commit ${(process.env.GITHUB_SHA || '?').slice(0,7)}.</sub>`); | |
| console.log(out.join('\n')); | |
| NODE | |
| echo "body-file=/tmp/pr-coverage.md" >> "$GITHUB_OUTPUT" | |
| - name: Post coverage comment on PR | |
| # Fork PRs get a read-only GITHUB_TOKEN: the comment API returns | |
| # "Resource not accessible by integration", failing the job and | |
| # skipping the bundle build/smoke steps below. Skip the comment | |
| # there — the coverage gate itself already ran above. | |
| if: github.event_name == 'pull_request' && always() && steps.pr-coverage.outputs.body-file != '' && github.event.pull_request.head.repo.full_name == github.repository | |
| uses: marocchino/sticky-pull-request-comment@v3.0.4 | |
| with: | |
| header: pr-coverage-report | |
| path: /tmp/pr-coverage.md | |
| - name: Build bundles | |
| run: npm run build | |
| - name: Smoke-test built bundles parse cleanly | |
| # Replaces the prior "Verify bundle/ directories are up to date" | |
| # diff guard, which only made sense when bundles were tracked in | |
| # git. Now bundles are gitignored on main and live only at release | |
| # commits referenced by `marketplace.json`'s sha-pinned source. | |
| # Without the diff guard, a build that emits syntactically broken | |
| # JS would ship silently — so this step `node --check`s every | |
| # bundle entrypoint after `npm run build` runs. --check is a | |
| # parse-only validation: catches syntax/parse errors without | |
| # executing module top-level code (which would spawn workers, | |
| # open file handles, try to write to disk, etc.). | |
| run: | | |
| set -e | |
| for f in \ | |
| bundle/cli.js \ | |
| harnesses/claude-code/bundle/capture.js \ | |
| harnesses/claude-code/bundle/session-start.js \ | |
| harnesses/claude-code/bundle/session-end.js \ | |
| harnesses/claude-code/bundle/plugin-cache-gc.js \ | |
| harnesses/claude-code/bundle/skillify-worker.js \ | |
| harnesses/codex/bundle/capture.js \ | |
| harnesses/codex/bundle/session-start.js \ | |
| harnesses/cursor/bundle/capture.js \ | |
| harnesses/cursor/bundle/session-start.js \ | |
| harnesses/hermes/bundle/capture.js \ | |
| harnesses/hermes/bundle/session-start.js \ | |
| mcp/bundle/server.js \ | |
| harnesses/pi/bundle/skillify-worker.js; do | |
| if [ ! -f "$f" ]; then | |
| echo "::error::expected bundle entrypoint missing: $f" | |
| exit 1 | |
| fi | |
| node --check "$f" || { | |
| echo "::error::bundle failed to parse: $f" | |
| exit 1 | |
| } | |
| done | |
| echo "All bundle entrypoints parse cleanly." | |
| - name: Pack-check (refuse forbidden filenames in tarball) | |
| # Regression guard: hard-fails CI if a PR widens package.json's | |
| # `files` array (or adds a permissive .npmignore) such that | |
| # credentials, CI workflows, or git internals would ship to npm. | |
| # Mirrors the same gate inside release.yml's publish job, so a | |
| # broken `files` array trips on the PR — never on a release run | |
| # where tokens are reachable. | |
| run: npm run pack:check | |
| windows-test: | |
| # Full typecheck + test suite on a real Windows runner, gated to push | |
| # events only (not PRs) to keep PR feedback fast. Catches the class of | |
| # Windows-only failures that mocked unit tests and Linux CI miss: | |
| # path-separator bugs, CRLF handling, missing POSIX binaries, and | |
| # node:fs behaviour differences (no chmod/symlinks). Mirrors the `test` | |
| # job exactly except: | |
| # - `defaults: run: shell: bash` — forces all `run:` steps through | |
| # Git Bash (pre-installed on windows-latest) so the existing bash | |
| # scripts (coverage summary, bundle smoke-test) work unchanged. | |
| # - `npm install` instead of `npm ci` — if tree-sitter's native | |
| # prebuild doesn't match the runner's ABI, npm continues rather | |
| # than aborting (same pattern as the windows-smoke job). | |
| # - PR-only steps omitted (coverage comment posting requires a PR | |
| # context and a writable GITHUB_TOKEN the push job doesn't need). | |
| name: Typecheck and Test (Windows) | |
| runs-on: windows-latest | |
| if: github.event_name == 'push' | |
| defaults: | |
| run: | |
| shell: bash | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: 22 | |
| - name: Install dependencies | |
| run: npm install | |
| - name: Build (typecheck + emit bundle artefacts) | |
| run: npm run build | |
| - name: Audit openclaw bundle against ClawHub static-scan rules | |
| run: npm run audit:openclaw -- --criticals-only | |
| - name: Run tests | |
| # No --coverage here: this job exists to catch Windows-only test | |
| # FAILURES, and per-file coverage thresholds are enforced on the Linux | |
| # `test` job. Several suites are platform-skipped on Windows | |
| # (skipIf(win32) for the Unix-socket embed daemon, /bin/sh safe-echo, | |
| # Unix file-mode bits, etc.), so their src files can't meet the | |
| # thresholds here by design — enforcing coverage on Windows would fail | |
| # on those skips, not on any real regression. | |
| run: npx vitest run | |
| - name: Write coverage summary to job page | |
| if: always() | |
| run: | | |
| if [ -f coverage/coverage-summary.json ]; then | |
| echo "### Test Coverage (overall)" >> $GITHUB_STEP_SUMMARY | |
| node -e " | |
| const c = require('./coverage/coverage-summary.json').total; | |
| const fmt = (v) => v.pct.toFixed(1) + '%'; | |
| console.log('| Metric | Coverage |'); | |
| console.log('|--------|----------|'); | |
| console.log('| Statements | ' + fmt(c.statements) + ' |'); | |
| console.log('| Branches | ' + fmt(c.branches) + ' |'); | |
| console.log('| Functions | ' + fmt(c.functions) + ' |'); | |
| console.log('| Lines | ' + fmt(c.lines) + ' |'); | |
| " >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### PR-tracked files (must stay ≥ 80 %)" >> $GITHUB_STEP_SUMMARY | |
| node -e " | |
| const summary = require('./coverage/coverage-summary.json'); | |
| const tracked = [ | |
| 'src/shell/grep-core.ts', | |
| 'src/shell/grep-interceptor.ts', | |
| 'src/hooks/grep-direct.ts', | |
| ]; | |
| console.log('| File | Stmts | Branch | Funcs | Lines |'); | |
| console.log('|------|------:|-------:|------:|------:|'); | |
| const fmt = v => v == null ? '—' : v.toFixed(1) + '%'; | |
| for (const rel of tracked) { | |
| const key = Object.keys(summary).find(k => k.endsWith(rel)); | |
| const c = key ? summary[key] : null; | |
| if (!c) { console.log('| \`' + rel + '\` | — | — | — | — |'); continue; } | |
| console.log('| \`' + rel + '\` | ' + fmt(c.statements.pct) + ' | ' + fmt(c.branches.pct) + ' | ' + fmt(c.functions.pct) + ' | ' + fmt(c.lines.pct) + ' |'); | |
| } | |
| " >> $GITHUB_STEP_SUMMARY | |
| fi | |
| - name: Build bundles | |
| run: npm run build | |
| - name: Smoke-test built bundles parse cleanly | |
| run: | | |
| set -e | |
| for f in \ | |
| bundle/cli.js \ | |
| harnesses/claude-code/bundle/capture.js \ | |
| harnesses/claude-code/bundle/session-start.js \ | |
| harnesses/claude-code/bundle/session-end.js \ | |
| harnesses/claude-code/bundle/plugin-cache-gc.js \ | |
| harnesses/claude-code/bundle/skillify-worker.js \ | |
| harnesses/codex/bundle/capture.js \ | |
| harnesses/codex/bundle/session-start.js \ | |
| harnesses/cursor/bundle/capture.js \ | |
| harnesses/cursor/bundle/session-start.js \ | |
| harnesses/hermes/bundle/capture.js \ | |
| harnesses/hermes/bundle/session-start.js \ | |
| mcp/bundle/server.js \ | |
| harnesses/pi/bundle/skillify-worker.js; do | |
| if [ ! -f "$f" ]; then | |
| echo "::error::expected bundle entrypoint missing: $f" | |
| exit 1 | |
| fi | |
| node --check "$f" || { | |
| echo "::error::bundle failed to parse: $f" | |
| exit 1 | |
| } | |
| done | |
| echo "All bundle entrypoints parse cleanly." | |
| - name: Pack-check (refuse forbidden filenames in tarball) | |
| run: npm run pack:check | |
| cross-node-install: | |
| # Cross-Node-major install + build canary. Catches the class of bug | |
| # where a native dep's prebuild ABI no longer matches a newer Node | |
| # major and the fall-back compile fails — silently, because the | |
| # offending package is now an optionalDependency. That exact pattern | |
| # broke the post-merge Release run on PR #206 (tree-sitter@0.21 vs | |
| # Node 24): the main `test` job above runs only on Node 22 where | |
| # the prebuild matches, so the failure was invisible at PR time and | |
| # only surfaced on `main` when release.yaml booted Node 24.12. | |
| # | |
| # This job runs the same install + build on every Node major the | |
| # `engines` field admits, with HIVEMIND_STRICT_POSTINSTALL=1 so the | |
| # ensure-tree-sitter.mjs heal turns "WARNING: bindings still | |
| # unavailable" into a hard `exit 1` instead of swallowing it. The | |
| # result: a Node-24-only install regression now fails this canary | |
| # job at PR time with a clear error, not on the post-merge Release. | |
| # | |
| # `continue-on-error: true` keeps PRs unblocked while a known | |
| # upstream Node-major break (e.g. the current tree-sitter@0.21 + | |
| # Node 24 incompatibility) is being resolved. The signal is visible | |
| # in the PR checks table as a yellow/red row. Once the underlying | |
| # incompatibility is fixed (or matrix entries adjusted), flip this | |
| # to `continue-on-error: false` to make the canary blocking. | |
| name: Cross-Node install canary (Node ${{ matrix.node-version }}) | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| strategy: | |
| # fail-fast: false so a failure on one Node major doesn't cancel | |
| # the other — both signals are useful in the PR checks table. | |
| fail-fast: false | |
| matrix: | |
| # Each major the `engines` field (">=22.0.0") admits. Bump this | |
| # list when a new LTS lands (28, 30, …) or when 22 falls out of | |
| # the support window. setup-node picks the latest available | |
| # minor/patch for each major. | |
| node-version: [22, 24] | |
| env: | |
| # Opt the heal into strict mode for our own CI runs only. | |
| # Downstream consumers of @deeplake/hivemind never see this flag, | |
| # so their install stays non-fatal. | |
| HIVEMIND_STRICT_POSTINSTALL: "1" | |
| steps: | |
| - uses: actions/checkout@v6.0.2 | |
| with: | |
| # Read-only job: don't leave GITHUB_TOKEN in .git/config. | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6.4.0 | |
| with: | |
| node-version: ${{ matrix.node-version }} | |
| - name: Install dependencies | |
| # If the postinstall heal cannot make tree-sitter loadable under | |
| # this Node major, HIVEMIND_STRICT_POSTINSTALL=1 makes ensure- | |
| # tree-sitter.mjs exit 1 here, failing the job at the earliest | |
| # possible step (instead of swallowing the warning and failing | |
| # later in tsc with a confusing "Cannot find module" error). | |
| run: npm ci | |
| - name: Build (typecheck + emit bundle artefacts) | |
| # Second backstop: if the strict-postinstall path somehow doesn't | |
| # catch a broken native dep, the tsc step downstream of it will, | |
| # because `import Parser from 'tree-sitter'` (src/graph/extract/ | |
| # typescript.ts) needs the package resolvable at type-check time. | |
| run: npm run build |