* fix(windows): preserve a file's existing line endings on rewrite The parsers normalize CRLF to LF on read, but nothing restored it on write. On a Windows checkout (core.autocrlf=true) that turned every rewrite into a whole-file change: applying a delta that added one requirement produced a diff of 21 insertions and 14 deletions, burying the real change. Archiving the same spec now writes 7 insertions and 0 deletions. - specs-apply: write an updated spec back with the convention the file already used; a spec that does not exist yet stays LF. - file-system: same fix for updateFileWithMarkers, so installing shell completions into a CRLF .bashrc/.zshrc no longer leaves mixed endings, which bash reports as "$'\r': command not found". - pack-version-check: spawn npm through cross-spawn, since execFile cannot resolve npm.cmd on Windows. Adds src/utils/line-endings.ts for the detect/restore pair, plus tests pinning the CRLF round trip through the real write paths. Also adds regression tests for path containment under Windows case variance: path.win32.relative already folds case, and those tests pin both halves of the contract so a future "case-insensitive" change cannot quietly loosen the traversal guard. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(windows): keep removeMarkerBlock on the file's own newline Addresses the two review points and one more instance of the same bug. `removeMarkerBlock` collapses a run of blank lines, and rebuilt the separator as a bare '\n' regardless of the file it came from. Removing a managed block from a CRLF CLAUDE.md or rc file therefore left a lone LF behind - the mixed ending this PR exists to prevent. It now uses the newline it already detects for the trailing ending. Test fixes: - `marker-updates.test.ts`: close `describe('line endings')` so `removeMarkerBlock` is no longer nested inside `updateFileWithMarkers`. - `path-containment.test.ts`: exercise `FileSystemUtils.assertPathWithin` and `resolveProjectArtifactPath` instead of a private copy of the containment logic, which passed whatever the production guard did. The guard had no coverage at all; a prefix-comparison regression now fails the sibling case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(windows): read the file's convention consistently, and only ENOENT as absent Three follow-ups from CodeRabbit's pass on the superseding PR. `writeUpdatedSpec` turned every read error into "no previous file", so an existing but unreadable spec was treated as absent and rewritten as LF. Only ENOENT means absent now; everything else propagates. `removeMarkerBlock` chose CRLF whenever the content held one anywhere, so a single stray CRLF in an otherwise-LF file pulled the whole rewrite to CRLF. It now uses detectLineEnding, the same dominant-ending reading matchLineEnding uses, so both write paths agree. Added the alias-path case the containment suite was missing: a directory link inside the root that resolves outside it. That exercises the canonicalization half of the guard, which a lexical check cannot do - the link's own path looks contained. Skipped where creating a directory link needs a privilege the runner lacks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Travis James <travis@tribehealthsolutions.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
OpenSpec Scripts
Utility scripts for OpenSpec maintenance and development.
update-flake.sh
Updates flake.nix pnpm dependency hash automatically.
When to use: After updating dependencies (pnpm install, pnpm update).
Usage:
./scripts/update-flake.sh
What it does:
- Reads version from
package.json(dynamically used byflake.nix) - Automatically determines the correct pnpm dependency hash
- Updates the hash in
flake.nix - Verifies the build succeeds
Example workflow:
# After dependency updates
pnpm install
./scripts/update-flake.sh
git add flake.nix
git commit -m "chore: update flake.nix dependency hash"
regen-parity-hashes.mjs
Recomputes the golden hashes pinned in
test/core/templates/skill-templates-parity.test.ts.
When to use: After any intended workflow-template change, and after rebasing a branch that edits templates — two branches touching different templates collide on the same hash map, and hand-editing 64-character hashes during a conflict is where transcription mistakes happen.
Usage:
pnpm build && pnpm regen:parity-hashes
pnpm vitest run test/core/templates/skill-templates-parity.test.ts
What it does:
- Refuses to run if
dist/is missing or older thansrc/— hashes come from the build, while the parity test readssrc/, so regenerating against a stale build writes hashes the test then rejects - Recomputes every pinned hash from the built
dist/ - Rewrites the map in place and prints which entries moved
- Exits non-zero, writing nothing, if it cannot account for every pinned hash:
a label with no matching export (a renamed or deleted template), or a hash
line these patterns do not recognise. Both would otherwise be left stale
while the run reported success, so
nothing to updatealways means it.
Line endings round-trip unchanged, so a CRLF checkout is safe — test/** has no
text eol=lf attribute, so the file arrives with CRLF on Windows.
The parity test recomputes the same hashes independently, so this script cannot silently produce a wrong value. Always run the test afterwards; it, not this script, is the authority.
The rewriting lives in parity-hash-shared.mjs so its guards can be exercised
against fabricated input — see test/core/templates/parity-hash-shared.test.ts.
A test that ran this script for real would rewrite the repository's own parity
test file mid-suite.
pack-version-check.mjs
Validates package version consistency before publishing.