The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, and YAML; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.

The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, YAML, shell, and PowerShell; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


M6-D1: spt-releases repo + public face (README, licenses)

Create SaberMage/spt-releases private via gh; author README.md per grill decision 7; LICENSE-MIT + LICENSE-BINARY; Pages config deferred to D7 flip. Activate REQ-REL-1 doc stage.

Creating spt-releases repo + public face

M6-D2: installer scripts (install.sh / install.ps1)

Non-interactive: latest-release lookup → platform asset fetch → sha256 verify → per-OS install root → user PATH → print absolute path. Idempotent. CI rung against staged artifact + spt --version. Activate REQ-INSTALL-1/2/3/5.

M6-D3: manifest JSON Schema (schemars) + drift gate

schemars derives on spt-runtime manifest types; emit manifest.schema.json at stable site-root path + release asset; CI regenerate+diff gate. Activate REQ-DOCS-4 (partial), REQ-DOCS-5.

M6-D4: docs-site Tier 1 content (mdBook)

mdBook skeleton + Starlight-inspired theme; mental model; human quickstart; adapter quickstart (mock-adapter); harness-contract vertical complete; shell getting-started (spt-shell-notify); stubs for remaining verticals. Activate REQ-DOCS-1/2/3.

M6-D5: agent-export + generation plumbing

llms.txt + llms-full.txt; raw .md alongside HTML; CLI ref from clap; rustdoc → Pages; source-repo CI publish job → spt-releases Pages branch; drift gates. REQ-REL-1 impl, REQ-DOCS-4/5 complete.

M6-D6: release pipeline + signing ceremony

Workspace → 0.1.0; tag-triggered build matrix both runners; cross-repo release via fine-grained token; key ceremony per ADR-0015 (primary + recovery, embed both pubkeys in VerifyPolicy trusted set, seed backups); manual SignedRelease signing. Activate REQ-REL-2/3.

M6-D7: v0.1.0 ship + flip public + acceptance run

Tag v0.1.0; ceremony; publish release; flip spt-releases public; Pages live; acceptance proof per M6-PLAN (clean-machine install, quickstarts, llms.txt + schema fetch, staged v0.1.1 verify, recovery-key verify).


74	## Scope
75
76	The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, and YAML; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.
77

## Scope

The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, and YAML; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.


1	[package]
2	name = "traceable-reqs"
3	version = "0.1.1"
4	edition = "2021"
5	description = "Requirements tracing CLI driven by a TOML manifest and [stage->REQ-ID] tags"
6	license = "Apache-2.0 OR MIT"
7
8	[dependencies]
9	clap = { version = "4", features = ["derive"] }
10	serde = { version = "1", features = ["derive"] }

[package]
name = "traceable-reqs"
version = "0.1.1"
edition = "2021"
description = "Requirements tracing CLI driven by a TOML manifest and [stage->REQ-ID] tags"
license = "Apache-2.0 OR MIT"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }


Edit

comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, and YAML;

comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, YAML, shell, and PowerShell;


The file C:\Users\decid\Documents\projects\traceable-reqs\README.md has been updated successfully. (file state is current in your context — no need to Read it back)

# Traceable Reqs

**NLSpec** (natural-language specification) for a requirements-tracing CLI—organized like [strongdm/attractor](https://github.com/strongdm/attractor): point an agent at the spec to implement the tool or verify behavior.

The tool follows the spirit of [OpenFastTrace](https://github.com/itsallcode/openfasttrace): tag-based traces across languages, with a focus on **coding agents** and **CI**.

> **Status:** the spec is the primary artifact at the repo root. A reference Rust implementation lives in [`rust/`](rust/) — `cd rust && cargo build` produces `rust/target/debug/traceable-reqs`. Run the binary from a directory that holds `traceable-reqs.toml` (e.g. `cd example && ../rust/target/debug/traceable-reqs check --json`). Issues and PRs welcome.

## Install

### If you are human

Tell your coding agent (Claude Code, etc.):

```
Read README.md from BigscreenVR/traceable-reqs and follow the install instructions there.
```

### If you've been sent here by your human

Clone this repo into the user's Claude skills directory, then follow [`SKILL.md`](SKILL.md):

- **Claude Code:** `gh repo clone BigscreenVR/traceable-reqs ~/.claude/skills/traceable-reqs`

Then read `~/.claude/skills/traceable-reqs/SKILL.md` and execute the install procedure: resolve the latest release, `gh release download` the matching platform asset (`linux-x86_64`, `macos-arm64`, or `windows-x86_64.exe`), drop it in `~/.local/bin` (Linux/macOS) or `%LOCALAPPDATA%\Programs\traceable-reqs` (Windows), `chmod +x` if Unix, verify with `--version`, and surface the PATH line if needed.

Requires `gh` ([install](https://cli.github.com/)) authenticated via `gh auth login`. The skill stays in `~/.claude/skills/traceable-reqs` — re-invoke any time with "install traceable-reqs" to update. Without Claude: follow [`SKILL.md`](SKILL.md) by hand, or build from source with `cd rust && cargo build --release`.

## What the tool does

The specified CLI is **written in Rust**. It reads the required **`traceable-reqs.toml`** manifest as the authoritative list of requirement IDs, scans configured files for **`[stage->requirement-id]`** tags, reports missing required documentation, implementation, unit-test, and integration-test evidence, and exposes the same trace facts through pyramid-friendly human and agent output plus structured JSON.

---

## Spec

Read [**SPEC.md**](SPEC.md). It is the source of truth for the core behavior and optional capabilities: manifest shape, tag grammar, trace completeness, commands, findings, examples, and extension details.

---

## Terminology

- **NLSpec:** a human-readable specification meant for coding agents (and humans) to implement or check behavior—following the usage in [Attractor](https://github.com/strongdm/attractor).

---

## Non-goals

- Replacing GitHub Issues (or your planner).
- Full ReqIF or OpenFastTrace interchange.
- Shipping a deep IDE plugin for every editor on day one—**CLI first**.

---

## Prior art

- [OpenFastTrace](https://github.com/itsallcode/openfasttrace) — tag-based tracing across languages.
- [Sphinx-Needs](https://www.sphinx-needs.com/), [Doorstop](https://github.com/doorstop-dev/doorstop), [ReqIF](https://www.omg.org/spec/ReqIF/) — other requirement workflows.

---

## Implementing

Example prompt for a coding agent:

```
Implement the traceable-reqs tool from SPEC.md in this repository.
```

Point the agent at the **root of this repo** (or a published URL). Implement against [**SPEC.md**](SPEC.md).

---

## Scope

The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, and YAML; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.

Additional capabilities such as include/exclude globs, priority gates, custom stage definitions, and stability promises are defined in [**SPEC.md**](SPEC.md). Implementation order is up to the implementor.

Optional later: a small Rust **library** for deeper integration—not required to use the CLI.

---

## License

Default intent: **Apache-2.0 OR MIT** dual license once `LICENSE` files are added. Submitting PRs implies you are ok with that choice unless you state otherwise.

-The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, and YAML; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.

+The core behavior is intentionally small: required **`traceable-reqs.toml` manifest** with authoritative IDs; comment scanning for Rust, Python, C/C++, JavaScript/TypeScript, Markdown, CMake, YAML, shell, and PowerShell; stages `doc`, `impl`, `unit`, `int`; manifest-driven required stages; pyramid reporting; CLI commands `list`, `trace`, **`check`**, with `--json` for structured output.


The file C:\Users\decid\Documents\projects\traceable-reqs\rust\Cargo.toml has been updated successfully. (file state is current in your context — no need to Read it back)

[package]
name = "traceable-reqs"
version = "0.1.1"
edition = "2021"
description = "Requirements tracing CLI driven by a TOML manifest and [stage->REQ-ID] tags"
license = "Apache-2.0 OR MIT"

[dependencies]
clap = { version = "4", features = ["derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
toml = "0.8"
walkdir = "2"


PowerShell

cd C:\Users\decid\Documents\projects\traceable-reqs\rust; cargo test 2>&1 | Select-Object -Last 15

Build + test traceable-reqs


     Running tests\review.rs (target\debug\deps\review-626741bf4a4d3279.exe)

running 2 tests
test review_prints_prompt_and_exits_zero ... ok
test review_json_carries_schema_version_findings_and_agent_prompt ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s

   Doc-tests traceable_reqs

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


cd C:\Users\decid\Documents\projects\traceable-reqs\rust; cargo test 2>&1 | Select-String "test result|FAILED|failed" | Select-Object -First 12

Confirm all test suites green


test result: ok. 85 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.34s
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


cd C:\Users\decid\Documents\projects\traceable-reqs\rust; .\target\debug\traceable-reqs.exe check 2>&1 | Select-Object -First 3; "exit=$LASTEXITCODE"

Dogfood check on own manifest


28 requirements: 28 complete, 0 incomplete, 0 findings
  [OK] REQ-BLOCK-COMMENT  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Scan Rust/C/C++ block comments by physical line, with /* ... */ inclusive of both ends
exit=
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


Read release workflow


name: Release

on:
  push:
    tags:
      - 'v*'

permissions:
  contents: write

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
            artifact: traceable-reqs
            asset_suffix: linux-x86_64
          - os: windows-latest
            target: x86_64-pc-windows-msvc
            artifact: traceable-reqs.exe
            asset_suffix: windows-x86_64.exe
          - os: macos-latest
            target: aarch64-apple-darwin
            artifact: traceable-reqs
            asset_suffix: macos-arm64
    runs-on: ${{ matrix.os }}
    defaults:
      run:
        working-directory: rust
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@v2
        with:
          workspaces: rust
      - run: cargo build --release --target ${{ matrix.target }}
      - name: Stage asset
        shell: bash
        run: |
          mkdir -p ../dist
          cp target/${{ matrix.target }}/release/${{ matrix.artifact }} \
             ../dist/traceable-reqs-${{ github.ref_name }}-${{ matrix.asset_suffix }}
      - uses: softprops/action-gh-release@v2
        with:
          files: dist/traceable-reqs-${{ github.ref_name }}-${{ matrix.asset_suffix }}
          fail_on_unmatched_files: true


cd C:\Users\decid\Documents\projects\traceable-reqs; git add -A ':!.claude'; git status --short; git commit -m @'
Support shell and PowerShell scanning

Scan `#` line comments in .sh/.bash and .ps1/.psm1/.psd1 files (text from
the first `#` to end of line), reusing the hash-comment region rule shared
with Python/CMake/YAML. PowerShell `<# ... #>` block comments are not
scanned - tags go on `#` lines (documented in SPEC.md).

New REQ-SCAN-SHELL-PS traced doc/impl/unit/int; REQ-SCAN-LANGS title,
SPEC.md core-behavior list, and README scope updated. v0.1.2.

Motivation: spt-core M6 installer scripts (install.sh/install.ps1) carry
[impl->REQ-INSTALL-*] evidence the scanner previously could not see -- the
TRACEABILITY contract there names scripts and workflows as first-class
evidence locations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@

Commit traceable-reqs shell/ps1 scanning


warning: in the working copy of 'rust/Cargo.lock', LF will be replaced by CRLF the next time Git touches it
M  README.md
M  SPEC.md
M  rust/Cargo.lock
M  rust/Cargo.toml
M  rust/src/scan.rs
M  rust/tests/issues.rs
M  rust/traceable-reqs.toml
?? .claude/
[main aaf234d] Support shell and PowerShell scanning
 7 files changed, 115 insertions(+), 7 deletions(-)
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


cd C:\Users\decid\Documents\projects\traceable-reqs; git push origin main 2>&1 | Select-Object -Last 2; git tag v0.1.2; git push origin v0.1.2 2>&1 | Select-Object -Last 2

Push main + tag v0.1.2 (triggers release build)


To https://github.com/BigscreenVR/traceable-reqs.git
   6420384..aaf234d  main -> main
To https://github.com/BigscreenVR/traceable-reqs.git
 * [new tag]         v0.1.2 -> v0.1.2
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


Release pipeline triggered. Installing v0.1.2 locally + wiring gravity's self-healing install into ci.yml.


cd C:\Users\decid\Documents\projects\traceable-reqs\rust; cargo build --release 2>&1 | Select-Object -Last 2; Copy-Item target\release\traceable-reqs.exe C:\Users\decid\AppData\Local\Programs\traceable-reqs\traceable-reqs.exe -Force; traceable-reqs --version

Build release + install locally


   Compiling traceable-reqs v0.1.2 (C:\Users\decid\Documents\projects\traceable-reqs\rust)
    Finished `release` profile [optimized] target(s) in 4.41s
traceable-reqs 0.1.2
Shell cwd was reset to C:\Users\decid\Documents\projects\spt-core


Edit

      - name: Install traceable-reqs
        # The self-hosted gravity runner carries a source-built binary in
        # /usr/local/bin (the release asset needs GLIBC 2.39; gravity's
        # Ubuntu 22.04 has 2.35). The download is the portability fallback
        # for any other runner.
        run: |
          if command -v traceable-reqs >/dev/null; then
            ln -sf "$(command -v traceable-reqs)" ./traceable-reqs
          else
            gh release download --repo BigscreenVR/traceable-reqs \
              --pattern '*linux-x86_64' --output traceable-reqs
            chmod +x traceable-reqs
          fi

      - name: Install traceable-reqs
        # Source-pinned, self-healing (M6-D2): gravity cannot run the release
        # asset (it needs GLIBC 2.39; gravity's Ubuntu 22.04 has 2.35), and we
        # have no SSH admin path to the box — so the job itself builds the
        # pinned tag once and caches it in ~/.local/bin, rebuilding only when
        # the pin moves. v0.1.2 = shell/PowerShell tag scanning (the M6
        # installer scripts carry [impl->REQ-INSTALL-*] evidence).
        run: |
          WANT=0.1.2
          BIN="$HOME/.local/bin/traceable-reqs"
          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then
            ln -sf "$BIN" ./traceable-reqs
          else
            rm -rf .tr-src
            git clone --depth 1 --branch "v$WANT" \
              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" .tr-src
            cargo build --release --manifest-path .tr-src/rust/Cargo.toml
            mkdir -p "$HOME/.local/bin"
            cp .tr-src/rust/target/release/traceable-reqs "$BIN"
            ln -sf "$BIN" ./traceable-reqs
          fi
          ./traceable-reqs --version


The file C:\Users\decid\Documents\projects\spt-core\.github\workflows\ci.yml has been updated successfully. (file state is current in your context — no need to Read it back)

      - name: Install traceable-reqs
        # The self-hosted gravity runner carries a source-built binary in
        # /usr/local/bin (the release asset needs GLIBC 2.39; gravity's
        # Ubuntu 22.04 has 2.35). The download is the portability fallback
        # for any other runner.
        run: |
          if command -v traceable-reqs >/dev/null; then
            ln -sf "$(command -v traceable-reqs)" ./traceable-reqs
          else
            gh release download --repo BigscreenVR/traceable-reqs \
              --pattern '*linux-x86_64' --output traceable-reqs
            chmod +x traceable-reqs
          fi

      - name: Install traceable-reqs
        # Source-pinned, self-healing (M6-D2): gravity cannot run the release
        # asset (it needs GLIBC 2.39; gravity's Ubuntu 22.04 has 2.35), and we
        # have no SSH admin path to the box — so the job itself builds the
        # pinned tag once and caches it in ~/.local/bin, rebuilding only when
        # the pin moves. v0.1.2 = shell/PowerShell tag scanning (the M6
        # installer scripts carry [impl->REQ-INSTALL-*] evidence).
        run: |
          WANT=0.1.2
          BIN="$HOME/.local/bin/traceable-reqs"
          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then
            ln -sf "$BIN" ./traceable-reqs
          else
            rm -rf .tr-src
            git clone --depth 1 --branch "v$WANT" \
              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" .tr-src
            cargo build --release --manifest-path .tr-src/rust/Cargo.toml
            mkdir -p "$HOME/.local/bin"
            cp .tr-src/rust/target/release/traceable-reqs "$BIN"
            ln -sf "$BIN" ./traceable-reqs
          fi
          ./traceable-reqs --version

-        # The self-hosted gravity runner carries a source-built binary in

-        # /usr/local/bin (the release asset needs GLIBC 2.39; gravity's

-        # Ubuntu 22.04 has 2.35). The download is the portability fallback

+        # Source-pinned, self-healing (M6-D2): gravity cannot run the release

+        # asset (it needs GLIBC 2.39; gravity's Ubuntu 22.04 has 2.35), and we

+        # have no SSH admin path to the box — so the job itself builds the

+        # pinned tag once and caches it in ~/.local/bin, rebuilding only when

+        # the pin moves. v0.1.2 = shell/PowerShell tag scanning (the M6

+        # installer scripts carry [impl->REQ-INSTALL-*] evidence).

-          if command -v traceable-reqs >/dev/null; then

-            ln -sf "$(command -v traceable-reqs)" ./traceable-reqs

+          BIN="$HOME/.local/bin/traceable-reqs"

+          if [ -x "$BIN" ] && "$BIN" --version | grep -q " $WANT\$"; then

+            ln -sf "$BIN" ./traceable-reqs

-            gh release download --repo BigscreenVR/traceable-reqs \

-              --pattern '*linux-x86_64' --output traceable-reqs

+            git clone --depth 1 --branch "v$WANT" \

+              "https://x-access-token:${GH_TOKEN}@github.com/BigscreenVR/traceable-reqs" .tr-src

+            cargo build --release --manifest-path .tr-src/rust/Cargo.toml

+            mkdir -p "$HOME/.local/bin"

+            cp .tr-src/rust/target/release/traceable-reqs "$BIN"

# Cross-org private repo: the default repo-scoped GITHUB_TOKEN cannot

# read BigscreenVR/traceable-reqs. Supply a PAT with read access to it


[scan]
roots = ["src", "tests", "docs", "crates", "installer"]

[scan]
# CONTEXT.md is a file root: the glossary is authoritative for meaning and
# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.
roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md"]


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

[scan]
roots = ["src", "tests", "docs", "crates", "installer"]

[scan]
# CONTEXT.md is a file root: the glossary is authoritative for meaning and
# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.
roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md"]

# installed CLI at M0 (the binary isn't installed yet).

-roots = ["src", "tests", "docs", "crates", "installer"]

+# CONTEXT.md is a file root: the glossary is authoritative for meaning and

+# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.

+roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md"]

# Default policy applied at activation: doc + impl + unit. Networking/lifecycle


PowerShell

traceable-reqs check > $null 2>&1; "exit=$LASTEXITCODE"; traceable-reqs check 2>&1 | Select-String "requirements:|REQ-INSTALL"

Re-run check with v0.1.2


exit=0

139 requirements: 139 complete, 0 incomplete, 0 findings
  [OK] REQ-INSTALL-1  required: [doc, impl, int]  stages: +doc +impl -unit +int
  [OK] REQ-INSTALL-2  required: [doc]  stages: +doc -impl -unit -int
  [OK] REQ-INSTALL-3  required: [impl, int]  stages: -doc +impl -unit +int
  [OK] REQ-INSTALL-4  required: [impl, unit]  stages: -doc +impl +unit -int
  [OK] REQ-INSTALL-5  required: [impl, int]  stages: -doc +impl -unit +int


Trace green. Now prove the rung locally — build + run the gated E2E:


cd C:\Users\decid\Documents\projects\spt-core; $env:SPT_INSTALL_E2E="1"; cargo test -p spt --test install_e2e -- --nocapture 2>&1 | Select-Object -Last 15

Run installer E2E on Windows


Error: Exit code 101
   Compiling spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 19.70s
     Running tests\install_e2e.rs (target\debug\deps\install_e2e-6b19db2722aa3dc6.exe)
error: test failed, to rerun pass `-p spt --test install_e2e`

Caused by:
  could not execute process `C:\Users\decid\Documents\projects\spt-core\target\debug\deps\install_e2e-6b19db2722aa3dc6.exe --nocapture` (never executed)

Caused by:
  The requested operation requires elevation. (os error 740)


cd C:\Users\decid\Documents\projects\spt-core; git mv 2>$null; Move-Item crates\spt\tests\install_e2e.rs crates\spt\tests\oneliner_e2e.rs

Rename E2E test file


Exit code 129


cd C:\Users\decid\Documents\projects\spt-core; Test-Path crates\spt\tests\oneliner_e2e.rs

Confirm rename landed


True


Edit

//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a

//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC
//! installer-detection heuristic demands elevation for any executable whose
//! name contains "install" — the test binary inherits the file name and
//! os error 740s before running.)
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>


    if !gated() {
        eprintln!("install_e2e: SPT_INSTALL_E2E unset - skipping");
        return;
    }

    if !gated() {
        eprintln!("oneliner_e2e: SPT_INSTALL_E2E unset - skipping");
        return;
    }


      # binary's `spt --version`. Env-gated: the normal sweep silent-skips
      # the same test when SPT_INSTALL_E2E is unset. Hermetic — temp HOME on
      # Linux, SPT_INSTALL_NO_PATH on Windows (never writes the runner's
      # real user PATH).
      - name: Installer E2E (staged-release rung)
        run: cargo test -p spt --test install_e2e -- --nocapture

      # binary's `spt --version`. Env-gated: the normal sweep silent-skips
      # the same test when SPT_INSTALL_E2E is unset. Hermetic — temp HOME on
      # Linux, SPT_INSTALL_NO_PATH on Windows (never writes the runner's
      # real user PATH). Named oneliner_e2e: an exe named *install* trips
      # Windows' UAC installer heuristic (os error 740).
      - name: Installer E2E (staged-release rung)
        run: cargo test -p spt --test oneliner_e2e -- --nocapture


      # binary's `spt --version`. Env-gated: the normal sweep silent-skips
      # the same test when SPT_INSTALL_E2E is unset. Hermetic — temp HOME on
      # Linux, SPT_INSTALL_NO_PATH on Windows (never writes the runner's
      # real user PATH).
      - name: Installer E2E (staged-release rung)
        run: cargo test -p spt --test install_e2e -- --nocapture

      # binary's `spt --version`. Env-gated: the normal sweep silent-skips
      # the same test when SPT_INSTALL_E2E is unset. Hermetic — temp HOME on
      # Linux, SPT_INSTALL_NO_PATH on Windows (never writes the runner's
      # real user PATH). Named oneliner_e2e: an exe named *install* trips
      # Windows' UAC installer heuristic (os error 740).
      - name: Installer E2E (staged-release rung)
        run: cargo test -p spt --test oneliner_e2e -- --nocapture

+      # real user PATH). Named oneliner_e2e: an exe named *install* trips

+      # Windows' UAC installer heuristic (os error 740).

-        run: cargo test -p spt --test install_e2e -- --nocapture

+        run: cargo test -p spt --test oneliner_e2e -- --nocapture


1	//! M6-D2 installer-script E2E — the real `install.sh` / `install.ps1` (the
2	//! canonical one-liners served at the Pages URL, ADR-0014) run against a
3	//! STAGED release artifact: the freshly built workspace `spt` binary is laid
4	//! out exactly like a release asset (platform asset name + SHA256SUMS), the
5	//! matching per-OS script installs it into an isolated root, and the rung
6	//! finishes with the installed binary answering `spt --version` — the
7	//! M6-PLAN D2 CI rung.
8	//!
9	//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
10	//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a
11	//! temp dir, so PATH registration lands in a throwaway `.profile`; Windows
12	//! runs with `SPT_INSTALL_NO_PATH=1` (the user-PATH leg writes the real HKCU
13	//! registry — never from a test). The asset fetch uses
14	//! `SPT_INSTALL_ASSET_BASE=<staged dir>`, the scripts' CI/air-gap knob.
15	//
16	// [int->REQ-INSTALL-1]
17	// [int->REQ-INSTALL-5]
18
19	use std::path::{Path, PathBuf};
20	use std::process::Command;
21
22	use sha2::{Digest, Sha256};
23
24	fn gated() -> bool {
25	    std::env::var("SPT_INSTALL_E2E").map(|v| v == "1").unwrap_or(false)
26	}
27
28	fn installer_dir() -> PathBuf {
29	    // crates/spt -> repo root -> installer/
30	    Path::new(env!("CARGO_MANIFEST_DIR"))

//! M6-D2 installer-script E2E — the real `install.sh` / `install.ps1` (the
//! canonical one-liners served at the Pages URL, ADR-0014) run against a
//! STAGED release artifact: the freshly built workspace `spt` binary is laid
//! out exactly like a release asset (platform asset name + SHA256SUMS), the
//! matching per-OS script installs it into an isolated root, and the rung
//! finishes with the installed binary answering `spt --version` — the
//! M6-PLAN D2 CI rung.
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a
//! temp dir, so PATH registration lands in a throwaway `.profile`; Windows
//! runs with `SPT_INSTALL_NO_PATH=1` (the user-PATH leg writes the real HKCU
//! registry — never from a test). The asset fetch uses
//! `SPT_INSTALL_ASSET_BASE=<staged dir>`, the scripts' CI/air-gap knob.
//
// [int->REQ-INSTALL-1]
// [int->REQ-INSTALL-5]

use std::path::{Path, PathBuf};
use std::process::Command;

use sha2::{Digest, Sha256};

fn gated() -> bool {
    std::env::var("SPT_INSTALL_E2E").map(|v| v == "1").unwrap_or(false)
}

fn installer_dir() -> PathBuf {
    // crates/spt -> repo root -> installer/
    Path::new(env!("CARGO_MANIFEST_DIR"))


//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a

//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC
//! installer-detection heuristic demands elevation for any executable whose
//! name contains "install" — the test binary inherits the file name and
//! os error 740s before it can run.)
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\tests\oneliner_e2e.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC
//! installer-detection heuristic demands elevation for any executable whose
//! name contains "install" — the test binary inherits the file name and
//! os error 740s before it can run.)
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a

//! M6-D2 installer-script E2E — the real `install.sh` / `install.ps1` (the
//! canonical one-liners served at the Pages URL, ADR-0014) run against a
//! STAGED release artifact: the freshly built workspace `spt` binary is laid
//! out exactly like a release asset (platform asset name + SHA256SUMS), the
//! matching per-OS script installs it into an isolated root, and the rung
//! finishes with the installed binary answering `spt --version` — the
//! M6-PLAN D2 CI rung.
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a
//! temp dir, so PATH registration lands in a throwaway `.profile`; Windows
//! runs with `SPT_INSTALL_NO_PATH=1` (the user-PATH leg writes the real HKCU
//! registry — never from a test). The asset fetch uses
//! `SPT_INSTALL_ASSET_BASE=<staged dir>`, the scripts' CI/air-gap knob.
//
// [int->REQ-INSTALL-1]
// [int->REQ-INSTALL-5]

use std::path::{Path, PathBuf};
use std::process::Command;

use sha2::{Digest, Sha256};

fn gated() -> bool {
    std::env::var("SPT_INSTALL_E2E").map(|v| v == "1").unwrap_or(false)
}

fn installer_dir() -> PathBuf {
    // crates/spt -> repo root -> installer/
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../installer")
        .canonicalize()
        .expect("installer/ dir exists at the repo root")
}

#[cfg(windows)]
const ASSET: &str = "spt-x86_64-windows.exe";
#[cfg(not(windows))]
const ASSET: &str = "spt-x86_64-linux";

/// Lay the built `spt` binary out as a staged release: asset-named copy +
/// a SHA256SUMS in the exact `<hex><2 spaces><name>` shape the release
/// pipeline emits (D6).
fn stage_release(dir: &Path) {
    let bin = env!("CARGO_BIN_EXE_spt");
    let asset_path = dir.join(ASSET);
    std::fs::copy(bin, &asset_path).expect("stage the built spt binary");
    let bytes = std::fs::read(&asset_path).expect("read staged asset");
    let digest = Sha256::digest(&bytes);
    let line = format!("{:x}  {}\n", digest, ASSET);
    std::fs::write(dir.join("SHA256SUMS"), line).expect("write SHA256SUMS");
}

/// Run the per-OS install script non-interactively against the staged dir.
fn run_script(staged: &Path, install_dir: &Path, fake_home: &Path) -> std::process::Output {
    let mut cmd;
    #[cfg(windows)]
    {
        // `powershell` (5.1), not pwsh: the `irm | iex` one-liner reality.
        cmd = Command::new("powershell");
        cmd.args([
            "-NoProfile",
            "-ExecutionPolicy",
            "Bypass",
            "-File",
            installer_dir().join("install.ps1").to_str().unwrap(),
        ]);
        // Never write the real HKCU user PATH from a test.
        cmd.env("SPT_INSTALL_NO_PATH", "1");
        let _ = fake_home; // PATH isolation is the NO_PATH knob on Windows
    }
    #[cfg(not(windows))]
    {
        // `sh`, not bash: the curl-pipe target is POSIX sh (dash on Ubuntu).
        cmd = Command::new("sh");
        cmd.arg(installer_dir().join("install.sh"));
        // PATH registration lands in this throwaway $HOME/.profile.
        cmd.env("HOME", fake_home);
    }
    cmd.env("SPT_INSTALL_ASSET_BASE", staged)
        .env("SPT_INSTALL_DIR", install_dir)
        .output()
        .expect("run install script")
}

fn installed_bin(install_dir: &Path) -> PathBuf {
    if cfg!(windows) {
        install_dir.join("spt.exe")
    } else {
        install_dir.join("spt")
    }
}

/// The whole D2 rung: staged release -> script install -> `spt --version`,
/// then the idempotent re-run (REQ-INSTALL-3) and the sha-mismatch refusal
/// (the REQ-INSTALL-5 verify gate actually gating).
#[test]
fn install_script_against_staged_release() {
    if !gated() {
        eprintln!("install_e2e: SPT_INSTALL_E2E unset - skipping");
        return;
    }

    let tmp = tempfile::tempdir().expect("tempdir");
    let staged = tmp.path().join("staged");
    let root = tmp.path().join("bin");
    let home = tmp.path().join("home");
    std::fs::create_dir_all(&staged).unwrap();
    std::fs::create_dir_all(&home).unwrap();
    stage_release(&staged);

    // ---- first install ----
    let out = run_script(&staged, &root, &home);
    assert!(
        out.status.success(),
        "install script failed:\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let bin = installed_bin(&root);
    assert!(bin.is_file(), "installed binary missing at {}", bin.display());

    // The acceptance shape: the *installed* binary answers --version.
    let ver = Command::new(&bin).arg("--version").output().expect("run installed spt");
    assert!(ver.status.success(), "installed spt --version failed");
    let ver_out = String::from_utf8_lossy(&ver.stdout);
    assert!(
        ver_out.contains("spt"),
        "--version output unexpected: {ver_out}"
    );

    // Linux: the PATH leg registered the install dir in the throwaway profile.
    #[cfg(not(windows))]
    {
        let profile = std::fs::read_to_string(home.join(".profile")).expect("profile written");
        assert!(
            profile.contains(root.to_str().unwrap()),
            "install dir not registered in .profile:\n{profile}"
        );
    }

    // ---- idempotent re-run (REQ-INSTALL-3) ----
    // [int->REQ-INSTALL-3]
    let out2 = run_script(&staged, &root, &home);
    assert!(
        out2.status.success(),
        "idempotent re-run failed:\nstderr: {}",
        String::from_utf8_lossy(&out2.stderr)
    );
    assert!(installed_bin(&root).is_file(), "binary gone after re-run");
    #[cfg(not(windows))]
    {
        // PATH registration is added at most once.
        let profile = std::fs::read_to_string(home.join(".profile")).unwrap();
        let hits = profile.matches("# added by spt installer").count();
        assert_eq!(hits, 1, "PATH block duplicated on re-run:\n{profile}");
    }

    // ---- sha mismatch refuses (the verify gate is real) ----
    let evil = tmp.path().join("evil");
    std::fs::create_dir_all(&evil).unwrap();
    stage_release(&evil);
    std::fs::write(
        evil.join("SHA256SUMS"),
        format!("{}  {}\n", "0".repeat(64), ASSET),
    )
    .unwrap();
    let root2 = tmp.path().join("bin2");
    let out3 = run_script(&evil, &root2, &home);
    assert!(
        !out3.status.success(),
        "script must refuse a sha256 mismatch"
    );
    assert!(
        !installed_bin(&root2).exists(),
        "tampered binary must never be placed"
    );
}

+//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC

+//! installer-detection heuristic demands elevation for any executable whose

+//! name contains "install" — the test binary inherits the file name and


eprintln!("install_e2e: SPT_INSTALL_E2E unset - skipping");

eprintln!("oneliner_e2e: SPT_INSTALL_E2E unset - skipping");


//! M6-D2 installer-script E2E — the real `install.sh` / `install.ps1` (the
//! canonical one-liners served at the Pages URL, ADR-0014) run against a
//! STAGED release artifact: the freshly built workspace `spt` binary is laid
//! out exactly like a release asset (platform asset name + SHA256SUMS), the
//! matching per-OS script installs it into an isolated root, and the rung
//! finishes with the installed binary answering `spt --version` — the
//! M6-PLAN D2 CI rung.
//!
//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC
//! installer-detection heuristic demands elevation for any executable whose
//! name contains "install" — the test binary inherits the file name and
//! os error 740s before it can run.)
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a
//! temp dir, so PATH registration lands in a throwaway `.profile`; Windows
//! runs with `SPT_INSTALL_NO_PATH=1` (the user-PATH leg writes the real HKCU
//! registry — never from a test). The asset fetch uses
//! `SPT_INSTALL_ASSET_BASE=<staged dir>`, the scripts' CI/air-gap knob.
//
// [int->REQ-INSTALL-1]
// [int->REQ-INSTALL-5]

use std::path::{Path, PathBuf};
use std::process::Command;

use sha2::{Digest, Sha256};

fn gated() -> bool {
    std::env::var("SPT_INSTALL_E2E").map(|v| v == "1").unwrap_or(false)
}

fn installer_dir() -> PathBuf {
    // crates/spt -> repo root -> installer/
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../installer")
        .canonicalize()
        .expect("installer/ dir exists at the repo root")
}

#[cfg(windows)]
const ASSET: &str = "spt-x86_64-windows.exe";
#[cfg(not(windows))]
const ASSET: &str = "spt-x86_64-linux";

/// Lay the built `spt` binary out as a staged release: asset-named copy +
/// a SHA256SUMS in the exact `<hex><2 spaces><name>` shape the release
/// pipeline emits (D6).
fn stage_release(dir: &Path) {
    let bin = env!("CARGO_BIN_EXE_spt");
    let asset_path = dir.join(ASSET);
    std::fs::copy(bin, &asset_path).expect("stage the built spt binary");
    let bytes = std::fs::read(&asset_path).expect("read staged asset");
    let digest = Sha256::digest(&bytes);
    let line = format!("{:x}  {}\n", digest, ASSET);
    std::fs::write(dir.join("SHA256SUMS"), line).expect("write SHA256SUMS");
}

/// Run the per-OS install script non-interactively against the staged dir.
fn run_script(staged: &Path, install_dir: &Path, fake_home: &Path) -> std::process::Output {
    let mut cmd;
    #[cfg(windows)]
    {
        // `powershell` (5.1), not pwsh: the `irm | iex` one-liner reality.
        cmd = Command::new("powershell");
        cmd.args([
            "-NoProfile",
            "-ExecutionPolicy",
            "Bypass",
            "-File",
            installer_dir().join("install.ps1").to_str().unwrap(),
        ]);
        // Never write the real HKCU user PATH from a test.
        cmd.env("SPT_INSTALL_NO_PATH", "1");
        let _ = fake_home; // PATH isolation is the NO_PATH knob on Windows
    }
    #[cfg(not(windows))]
    {
        // `sh`, not bash: the curl-pipe target is POSIX sh (dash on Ubuntu).
        cmd = Command::new("sh");
        cmd.arg(installer_dir().join("install.sh"));
        // PATH registration lands in this throwaway $HOME/.profile.
        cmd.env("HOME", fake_home);
    }
    cmd.env("SPT_INSTALL_ASSET_BASE", staged)
        .env("SPT_INSTALL_DIR", install_dir)
        .output()
        .expect("run install script")
}

fn installed_bin(install_dir: &Path) -> PathBuf {
    if cfg!(windows) {
        install_dir.join("spt.exe")
    } else {
        install_dir.join("spt")
    }
}

/// The whole D2 rung: staged release -> script install -> `spt --version`,
/// then the idempotent re-run (REQ-INSTALL-3) and the sha-mismatch refusal
/// (the REQ-INSTALL-5 verify gate actually gating).
#[test]
fn install_script_against_staged_release() {
    if !gated() {
        eprintln!("install_e2e: SPT_INSTALL_E2E unset - skipping");
        return;
    }

    let tmp = tempfile::tempdir().expect("tempdir");
    let staged = tmp.path().join("staged");
    let root = tmp.path().join("bin");
    let home = tmp.path().join("home");
    std::fs::create_dir_all(&staged).unwrap();
    std::fs::create_dir_all(&home).unwrap();
    stage_release(&staged);

    // ---- first install ----
    let out = run_script(&staged, &root, &home);
    assert!(
        out.status.success(),
        "install script failed:\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let bin = installed_bin(&root);
    assert!(bin.is_file(), "installed binary missing at {}", bin.display());

    // The acceptance shape: the *installed* binary answers --version.
    let ver = Command::new(&bin).arg("--version").output().expect("run installed spt");
    assert!(ver.status.success(), "installed spt --version failed");
    let ver_out = String::from_utf8_lossy(&ver.stdout);
    assert!(
        ver_out.contains("spt"),
        "--version output unexpected: {ver_out}"
    );

    // Linux: the PATH leg registered the install dir in the throwaway profile.
    #[cfg(not(windows))]
    {
        let profile = std::fs::read_to_string(home.join(".profile")).expect("profile written");
        assert!(
            profile.contains(root.to_str().unwrap()),
            "install dir not registered in .profile:\n{profile}"
        );
    }

    // ---- idempotent re-run (REQ-INSTALL-3) ----
    // [int->REQ-INSTALL-3]
    let out2 = run_script(&staged, &root, &home);
    assert!(
        out2.status.success(),
        "idempotent re-run failed:\nstderr: {}",
        String::from_utf8_lossy(&out2.stderr)
    );
    assert!(installed_bin(&root).is_file(), "binary gone after re-run");
    #[cfg(not(windows))]
    {
        // PATH registration is added at most once.
        let profile = std::fs::read_to_string(home.join(".profile")).unwrap();
        let hits = profile.matches("# added by spt installer").count();
        assert_eq!(hits, 1, "PATH block duplicated on re-run:\n{profile}");
    }

    // ---- sha mismatch refuses (the verify gate is real) ----
    let evil = tmp.path().join("evil");
    std::fs::create_dir_all(&evil).unwrap();
    stage_release(&evil);
    std::fs::write(
        evil.join("SHA256SUMS"),
        format!("{}  {}\n", "0".repeat(64), ASSET),
    )
    .unwrap();
    let root2 = tmp.path().join("bin2");
    let out3 = run_script(&evil, &root2, &home);
    assert!(
        !out3.status.success(),
        "script must refuse a sha256 mismatch"
    );
    assert!(
        !installed_bin(&root2).exists(),
        "tampered binary must never be placed"
    );
}

-        eprintln!("install_e2e: SPT_INSTALL_E2E unset - skipping");

+        eprintln!("oneliner_e2e: SPT_INSTALL_E2E unset - skipping");


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; (Get-Content traceable-reqs.toml -Raw) -replace 'install_e2e', 'oneliner_e2e' | Set-Content traceable-reqs.toml -NoNewline; git diff --stat; $env:SPT_INSTALL_E2E="1"; cargo test -p spt --test oneliner_e2e -- --nocapture 2>&1 | Select-Object -Last 12

Fix toml refs + run renamed E2E


Error: Exit code 101
warning: in the working copy of '.github/workflows/ci.yml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'CONTEXT.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DEFERRED.md', LF will be replaced by CRLF the next time Git touches it
 .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++---------
 CONTEXT.md               |  2 ++
 Cargo.lock               |  1 +
 crates/spt/Cargo.toml    |  3 +++
 docs/DEFERRED.md         |  1 +
 traceable-reqs.toml      | 12 +++++++-----
 6 files changed, 46 insertions(+), 14 deletions(-)

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
test install_script_against_staged_release ... FAILED

failures:

failures:
    install_script_against_staged_release

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.05s

error: test failed, to rerun pass `-p spt --test oneliner_e2e`


cd C:\Users\decid\Documents\projects\spt-core; $env:SPT_INSTALL_E2E="1"; cargo test -p spt --test oneliner_e2e -- --nocapture 2>&1 | Select-String -Pattern "panicked|assert|stdout:|stderr:|error" -Context 1 | Select-Object -First 20

Get failure detail


Error: Exit code 101
> thread 'install_script_against_staged_release' (57536) panicked at crates\spt\tests\oneliner_e2e.rs:119:5:
  install script failed:
> stdout: spt-install: fetching spt-x86_64-windows.exe ...

> stderr: Get-FileHash : The term 'Get-FileHash' is not recognized as the name of a cmdlet, function, script file, or operable program.
Check the
  spelling of the name, or if a path was included, verify that the path is correct and try again.
  +             ~~~~~~~~~~~~
>     + CategoryInfo          : ObjectNotFound: (Get-FileHash:String) [], ParentContainsErrorRecordException
>     + FullyQualifiedErrorId : CommandNotFoundException

> error: test failed, to rerun pass `-p spt --test oneliner_e2e`


Edit

    if (-not $want) { Fail "SHA256SUMS has no entry for $asset" }
    $got = (Get-FileHash -Path $binPath -Algorithm SHA256).Hash.ToLowerInvariant()

    if (-not $want) { Fail "SHA256SUMS has no entry for $asset" }
    # .NET directly, not Get-FileHash: cmdlet autoload breaks when a PS7
    # parent's PSModulePath leaks into a 5.1 child (and on hardened boxes).
    $sha = [Security.Cryptography.SHA256]::Create()
    $stream = [IO.File]::OpenRead($binPath)
    try { $hashBytes = $sha.ComputeHash($stream) } finally { $stream.Dispose() }
    $got = ([BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()


The file C:\Users\decid\Documents\projects\spt-core\installer\install.ps1 has been updated successfully. (file state is current in your context — no need to Read it back)

    if (-not $want) { Fail "SHA256SUMS has no entry for $asset" }
    # .NET directly, not Get-FileHash: cmdlet autoload breaks when a PS7
    # parent's PSModulePath leaks into a 5.1 child (and on hardened boxes).
    $sha = [Security.Cryptography.SHA256]::Create()
    $stream = [IO.File]::OpenRead($binPath)
    try { $hashBytes = $sha.ComputeHash($stream) } finally { $stream.Dispose() }
    $got = ([BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()

# spt installer (Windows) - served at the canonical Pages URL (ADR-0014):
#
#   irm https://sabermage.github.io/spt-releases/install.ps1 | iex
#
# [impl->REQ-INSTALL-5] Non-interactive BY CONSTRUCTION: this same script is
# every adapter's pack-in on-demand install (no second mechanism), so it must
# never prompt. It downloads the latest release binary, sha256-checks it
# against the release's SHA256SUMS (full ed25519 verification is `spt update`'s
# job after first install; first-fetch trust = HTTPS + GitHub), places it under
# the per-OS install root, and registers the *user* PATH.
#
# Compatible with stock Windows PowerShell 5.1 (the `irm | iex` default) and
# PowerShell 7+. ASCII only: PS 5.1 reads BOM-less scripts as ANSI.
#
# Environment knobs (all optional):
#   SPT_INSTALL_DIR        install dir          (default: %LOCALAPPDATA%\spt\bin)
#   SPT_INSTALL_REPO       owner/repo           (default: SaberMage/spt-releases)
#   SPT_INSTALL_VERSION    release tag to pin   (default: latest)
#   SPT_INSTALL_ASSET_BASE URL or local dir holding the assets + SHA256SUMS
#                          directly (bypasses the release lookup; CI / air-gap)
#   SPT_INSTALL_NO_PATH    set to 1 to skip PATH registration

$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# PS 5.1 defaults to TLS 1.0; GitHub requires >= 1.2.
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12

# throw, never `exit`: under `irm | iex` the script body runs in the user's
# session, where `exit` would close their terminal. A throw aborts the install
# either way, and `powershell -File` still maps it to a nonzero exit code.
function Fail([string]$msg) {
    throw "spt-install: error: $msg"
}

$repo = if ($env:SPT_INSTALL_REPO) { $env:SPT_INSTALL_REPO } else { 'SaberMage/spt-releases' }
$installDir = if ($env:SPT_INSTALL_DIR) { $env:SPT_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'spt\bin' }

# ---- platform / arch -> release asset name -------------------------------
$arch = $env:PROCESSOR_ARCHITECTURE
if ($arch -ne 'AMD64') {
    Fail "no published build for architecture '$arch' yet (x86_64 only today)"
}
$asset = 'spt-x86_64-windows.exe'

$work = Join-Path ([IO.Path]::GetTempPath()) ("spt-install-" + [IO.Path]::GetRandomFileName())
New-Item -ItemType Directory -Path $work -Force | Out-Null

try {
    # ---- fetch: binary + SHA256SUMS ----------------------------------------
    # [impl->REQ-INSTALL-1] the standalone one-line script half: latest-release
    # lookup -> platform asset fetch -> sha256 verify -> place -> user PATH.
    function Fetch([string]$name, [string]$dest) {
        $base = $env:SPT_INSTALL_ASSET_BASE
        if ($base) {
            if ($base -match '^https?://') {
                Invoke-WebRequest -Uri "$base/$name" -OutFile $dest -UseBasicParsing
            } else {
                Copy-Item -Path (Join-Path $base $name) -Destination $dest
            }
        } elseif ($env:SPT_INSTALL_VERSION) {
            Invoke-WebRequest -Uri "https://github.com/$repo/releases/download/$($env:SPT_INSTALL_VERSION)/$name" -OutFile $dest -UseBasicParsing
        } else {
            Invoke-WebRequest -Uri "https://github.com/$repo/releases/latest/download/$name" -OutFile $dest -UseBasicParsing
        }
    }

    Write-Output "spt-install: fetching $asset ..."
    $binPath = Join-Path $work $asset
    $sumsPath = Join-Path $work 'SHA256SUMS'
    Fetch $asset $binPath
    Fetch 'SHA256SUMS' $sumsPath

    # ---- verify -------------------------------------------------------------
    $want = $null
    foreach ($line in (Get-Content $sumsPath)) {
        if ($line -match '^([0-9a-fA-F]{64})\s+\*?(.+)$' -and $Matches[2].Trim() -eq $asset) {
            $want = $Matches[1].ToLowerInvariant()
        }
    }
    if (-not $want) { Fail "SHA256SUMS has no entry for $asset" }
    $got = (Get-FileHash -Path $binPath -Algorithm SHA256).Hash.ToLowerInvariant()
    if ($got -ne $want) {
        Fail "sha256 mismatch for $asset (expected $want, got $got) - refusing to install"
    }
    Write-Output "spt-install: sha256 verified."

    # ---- place --------------------------------------------------------------
    # [impl->REQ-INSTALL-3] idempotent re-run: the binary is replaced in place;
    # the PATH registration below is added at most once.
    New-Item -ItemType Directory -Path $installDir -Force | Out-Null
    $target = Join-Path $installDir 'spt.exe'
    try {
        Move-Item -Path $binPath -Destination $target -Force
    } catch {
        Fail "could not replace $target (is spt running? stop it and re-run): $_"
    }

    # ---- user PATH registration ----------------------------------------------
    $pathNote = $null
    if ($env:SPT_INSTALL_NO_PATH -ne '1') {
        $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
        if (-not $userPath) { $userPath = '' }
        $entries = $userPath -split ';' | ForEach-Object { $_.Trim().TrimEnd('\') } | Where-Object { $_ }
        if ($entries -notcontains $installDir.TrimEnd('\')) {
            $newPath = if ($userPath -and -not $userPath.EndsWith(';')) { "$userPath;$installDir" } else { "$userPath$installDir" }
            [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
            $pathNote = "Registered $installDir on your user PATH (takes effect in NEW terminals only)."
        }
    }

    # ---- report ---------------------------------------------------------------
    # Print the absolute path: the PATH change does not reach this terminal, so
    # the first invocation here must use it absolutely.
    Write-Output "spt-install: installed $target"
    & $target --version
    if ($pathNote) {
        Write-Output "spt-install: $pathNote"
        Write-Output "spt-install: in THIS terminal, invoke it as: $target"
    }
} finally {
    Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
}

-    $got = (Get-FileHash -Path $binPath -Algorithm SHA256).Hash.ToLowerInvariant()

+    # .NET directly, not Get-FileHash: cmdlet autoload breaks when a PS7

+    # parent's PSModulePath leaks into a 5.1 child (and on hardened boxes).

+    $sha = [Security.Cryptography.SHA256]::Create()

+    $stream = [IO.File]::OpenRead($binPath)

+    try { $hashBytes = $sha.ComputeHash($stream) } finally { $stream.Dispose() }

+    $got = ([BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()


        // Never write the real HKCU user PATH from a test.
        cmd.env("SPT_INSTALL_NO_PATH", "1");

        // Never write the real HKCU user PATH from a test.
        cmd.env("SPT_INSTALL_NO_PATH", "1");
        // A PS7 parent's PSModulePath poisons 5.1 cmdlet autoload (modules
        // resolve into PS7's dirs and fail to load); a real user shell never
        // inherits it — drop it so the child rebuilds the 5.1 default.
        cmd.env_remove("PSModulePath");


        // Never write the real HKCU user PATH from a test.
        cmd.env("SPT_INSTALL_NO_PATH", "1");
        // A PS7 parent's PSModulePath poisons 5.1 cmdlet autoload (modules
        // resolve into PS7's dirs and fail to load); a real user shell never
        // inherits it — drop it so the child rebuilds the 5.1 default.
        cmd.env_remove("PSModulePath");

//! M6-D2 installer-script E2E — the real `install.sh` / `install.ps1` (the
//! canonical one-liners served at the Pages URL, ADR-0014) run against a
//! STAGED release artifact: the freshly built workspace `spt` binary is laid
//! out exactly like a release asset (platform asset name + SHA256SUMS), the
//! matching per-OS script installs it into an isolated root, and the rung
//! finishes with the installed binary answering `spt --version` — the
//! M6-PLAN D2 CI rung.
//!
//! (File is named `oneliner_e2e`, not `install_e2e`: Windows' UAC
//! installer-detection heuristic demands elevation for any executable whose
//! name contains "install" — the test binary inherits the file name and
//! os error 740s before it can run.)
//!
//! **Env-gated** (the twohost pattern): `SPT_INSTALL_E2E=1` runs it; absent
//! ⇒ silent skip inside the normal sweep. Hermetic: `HOME` (Linux) is a
//! temp dir, so PATH registration lands in a throwaway `.profile`; Windows
//! runs with `SPT_INSTALL_NO_PATH=1` (the user-PATH leg writes the real HKCU
//! registry — never from a test). The asset fetch uses
//! `SPT_INSTALL_ASSET_BASE=<staged dir>`, the scripts' CI/air-gap knob.
//
// [int->REQ-INSTALL-1]
// [int->REQ-INSTALL-5]

use std::path::{Path, PathBuf};
use std::process::Command;

use sha2::{Digest, Sha256};

fn gated() -> bool {
    std::env::var("SPT_INSTALL_E2E").map(|v| v == "1").unwrap_or(false)
}

fn installer_dir() -> PathBuf {
    // crates/spt -> repo root -> installer/
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../installer")
        .canonicalize()
        .expect("installer/ dir exists at the repo root")
}

#[cfg(windows)]
const ASSET: &str = "spt-x86_64-windows.exe";
#[cfg(not(windows))]
const ASSET: &str = "spt-x86_64-linux";

/// Lay the built `spt` binary out as a staged release: asset-named copy +
/// a SHA256SUMS in the exact `<hex><2 spaces><name>` shape the release
/// pipeline emits (D6).
fn stage_release(dir: &Path) {
    let bin = env!("CARGO_BIN_EXE_spt");
    let asset_path = dir.join(ASSET);
    std::fs::copy(bin, &asset_path).expect("stage the built spt binary");
    let bytes = std::fs::read(&asset_path).expect("read staged asset");
    let digest = Sha256::digest(&bytes);
    let line = format!("{:x}  {}\n", digest, ASSET);
    std::fs::write(dir.join("SHA256SUMS"), line).expect("write SHA256SUMS");
}

/// Run the per-OS install script non-interactively against the staged dir.
fn run_script(staged: &Path, install_dir: &Path, fake_home: &Path) -> std::process::Output {
    let mut cmd;
    #[cfg(windows)]
    {
        // `powershell` (5.1), not pwsh: the `irm | iex` one-liner reality.
        cmd = Command::new("powershell");
        cmd.args([
            "-NoProfile",
            "-ExecutionPolicy",
            "Bypass",
            "-File",
            installer_dir().join("install.ps1").to_str().unwrap(),
        ]);
        // Never write the real HKCU user PATH from a test.
        cmd.env("SPT_INSTALL_NO_PATH", "1");
        let _ = fake_home; // PATH isolation is the NO_PATH knob on Windows
    }
    #[cfg(not(windows))]
    {
        // `sh`, not bash: the curl-pipe target is POSIX sh (dash on Ubuntu).
        cmd = Command::new("sh");
        cmd.arg(installer_dir().join("install.sh"));
        // PATH registration lands in this throwaway $HOME/.profile.
        cmd.env("HOME", fake_home);
    }
    cmd.env("SPT_INSTALL_ASSET_BASE", staged)
        .env("SPT_INSTALL_DIR", install_dir)
        .output()
        .expect("run install script")
}

fn installed_bin(install_dir: &Path) -> PathBuf {
    if cfg!(windows) {
        install_dir.join("spt.exe")
    } else {
        install_dir.join("spt")
    }
}

/// The whole D2 rung: staged release -> script install -> `spt --version`,
/// then the idempotent re-run (REQ-INSTALL-3) and the sha-mismatch refusal
/// (the REQ-INSTALL-5 verify gate actually gating).
#[test]
fn install_script_against_staged_release() {
    if !gated() {
        eprintln!("oneliner_e2e: SPT_INSTALL_E2E unset - skipping");
        return;
    }

    let tmp = tempfile::tempdir().expect("tempdir");
    let staged = tmp.path().join("staged");
    let root = tmp.path().join("bin");
    let home = tmp.path().join("home");
    std::fs::create_dir_all(&staged).unwrap();
    std::fs::create_dir_all(&home).unwrap();
    stage_release(&staged);

    // ---- first install ----
    let out = run_script(&staged, &root, &home);
    assert!(
        out.status.success(),
        "install script failed:\nstdout: {}\nstderr: {}",
        String::from_utf8_lossy(&out.stdout),
        String::from_utf8_lossy(&out.stderr)
    );
    let bin = installed_bin(&root);
    assert!(bin.is_file(), "installed binary missing at {}", bin.display());

    // The acceptance shape: the *installed* binary answers --version.
    let ver = Command::new(&bin).arg("--version").output().expect("run installed spt");
    assert!(ver.status.success(), "installed spt --version failed");
    let ver_out = String::from_utf8_lossy(&ver.stdout);
    assert!(
        ver_out.contains("spt"),
        "--version output unexpected: {ver_out}"
    );

    // Linux: the PATH leg registered the install dir in the throwaway profile.
    #[cfg(not(windows))]
    {
        let profile = std::fs::read_to_string(home.join(".profile")).expect("profile written");
        assert!(
            profile.contains(root.to_str().unwrap()),
            "install dir not registered in .profile:\n{profile}"
        );
    }

    // ---- idempotent re-run (REQ-INSTALL-3) ----
    // [int->REQ-INSTALL-3]
    let out2 = run_script(&staged, &root, &home);
    assert!(
        out2.status.success(),
        "idempotent re-run failed:\nstderr: {}",
        String::from_utf8_lossy(&out2.stderr)
    );
    assert!(installed_bin(&root).is_file(), "binary gone after re-run");
    #[cfg(not(windows))]
    {
        // PATH registration is added at most once.
        let profile = std::fs::read_to_string(home.join(".profile")).unwrap();
        let hits = profile.matches("# added by spt installer").count();
        assert_eq!(hits, 1, "PATH block duplicated on re-run:\n{profile}");
    }

    // ---- sha mismatch refuses (the verify gate is real) ----
    let evil = tmp.path().join("evil");
    std::fs::create_dir_all(&evil).unwrap();
    stage_release(&evil);
    std::fs::write(
        evil.join("SHA256SUMS"),
        format!("{}  {}\n", "0".repeat(64), ASSET),
    )
    .unwrap();
    let root2 = tmp.path().join("bin2");
    let out3 = run_script(&evil, &root2, &home);
    assert!(
        !out3.status.success(),
        "script must refuse a sha256 mismatch"
    );
    assert!(
        !installed_bin(&root2).exists(),
        "tampered binary must never be placed"
    );
}

+        // A PS7 parent's PSModulePath poisons 5.1 cmdlet autoload (modules

+        // resolve into PS7's dirs and fail to load); a real user shell never

+        // inherits it — drop it so the child rebuilds the 5.1 default.

+        cmd.env_remove("PSModulePath");


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; $env:SPT_INSTALL_E2E="1"; cargo test -p spt --test oneliner_e2e -- --nocapture 2>&1 | Select-Object -Last 8

Re-run renamed E2E


    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.86s
     Running tests\oneliner_e2e.rs (target\debug\deps\oneliner_e2e-bb573ea4b8052087.exe)

running 1 test
test install_script_against_staged_release ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.43s


Bash

bash -n installer/install.sh && echo SYNTAX_OK && shellcheck installer/install.sh 2>/dev/null || echo "shellcheck unavailable (syntax check passed above)"

Syntax-check install.sh


SYNTAX_OK
shellcheck unavailable (syntax check passed above)


cd /tmp && rm -rf sptsh && mkdir -p sptsh/staged sptsh/home && cp /c/Users/decid/Documents/projects/spt-core/target/debug/spt.exe sptsh/staged/spt-x86_64-linux && (cd sptsh/staged && sha256sum spt-x86_64-linux > SHA256SUMS) && HOME=/tmp/sptsh/home SPT_INSTALL_ASSET_BASE=/tmp/sptsh/staged SPT_INSTALL_DIR=/tmp/sptsh/bin sh -c 'uname_override() { echo Linux; }; sh /c/Users/decid/Documents/projects/spt-core/installer/install.sh' 2>&1 | tail -5; cat /tmp/sptsh/home/.profile 2>/dev/null

Smoke-run install.sh under git-bash with staged asset


Error: Exit code 1
spt-install: error: unsupported OS 'MINGW64_NT-10.0-26200' (this script installs the Linux binary; Windows: install.ps1; macOS builds are not published yet)


OS gate refuses MINGW correctly — the real Linux leg proves on gravity. Final check + commit D2:


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; traceable-reqs check > $null 2>&1; "check exit=$LASTEXITCODE"; cargo clippy -p spt --all-targets -- -D warnings 2>&1 | Select-Object -Last 3

Final traceability + clippy gate


check exit=0
    Checking spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.63s


cd C:\Users\decid\Documents\projects\spt-core; git add installer crates/spt/tests/oneliner_e2e.rs crates/spt/Cargo.toml Cargo.lock .github/workflows/ci.yml traceable-reqs.toml CONTEXT.md docs/DEFERRED.md; git commit -m @'
feat(install): D2 — non-interactive installer scripts + staged-release CI rung

installer/install.sh + install.ps1, the canonical one-liners served at the
Pages URL (ADR-0014): latest-release lookup (GitHub releases/latest/download,
no API parse) -> platform asset fetch -> sha256 verify against SHA256SUMS
(mismatch refuses before placement) -> per-OS install root (~/.local/bin /
%LOCALAPPDATA%\spt\bin) -> user-PATH registration (marker-guarded .profile
block / HKCU entry-set check) -> absolute-path print (the PATH-refresh
gotcha). Non-interactive by construction (REQ-INSTALL-5: env knobs only,
zero prompts; SPT_INSTALL_ASSET_BASE = the CI/air-gap override); idempotent
re-run (REQ-INSTALL-3). PS 5.1-safe: throw-not-exit under irm|iex, TLS 1.2,
ASCII-only, .NET SHA256 (cmdlet autoload breaks under a PS7 parent''s leaked
PSModulePath).

CI rung: crates/spt/tests/oneliner_e2e.rs (env-gated SPT_INSTALL_E2E, the
twohost pattern) stages the built binary as a release asset and runs the real
script: install -> installed-binary --version -> idempotent re-run (single
PATH block) -> tamper-refusal negative. Named oneliner_e2e because an exe
named *install* trips Windows'' UAC installer heuristic (os error 740).
Hermetic: temp HOME on Linux; SPT_INSTALL_NO_PATH + PSModulePath scrub on
Windows. Green on hfenduleam.

Traceability: REQ-INSTALL-1 (doc/impl/int), -2 (doc), -3 (impl/int),
-5 (impl/int) activated; OS-service leg -> DEFERRED.md row. Scan roots gain
installer/ + the CONTEXT.md file root; the traceability job now source-pins
traceable-reqs v0.1.2 self-healing on gravity (v0.1.2 = the shell/PowerShell
tag scanning shipped today in BigscreenVR/traceable-reqs@aaf234d for exactly
this evidence). 139/139 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@

Commit D2


warning: in the working copy of '.github/workflows/ci.yml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'CONTEXT.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'Cargo.lock', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/Cargo.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs/DEFERRED.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/tests/oneliner_e2e.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'installer/install.ps1', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'installer/install.sh', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 2ab8bad] feat(install): D2 — non-interactive installer scripts + staged-release CI rung
 9 files changed, 467 insertions(+), 14 deletions(-)
 create mode 100644 crates/spt/tests/oneliner_e2e.rs
 create mode 100644 installer/install.ps1
 create mode 100644 installer/install.sh


CRLF hazard: `install.sh` must stay LF everywhere (CRLF breaks `sh`, and the served file must be byte-clean). Pinning with `.gitattributes`:


cd C:\Users\decid\Documents\projects\spt-core; Test-Path .gitattributes

Check for existing .gitattributes


False


Write

# install.sh is piped straight into `sh` from the Pages URL — a CRLF checkout
# (or publish) would inject \r into every command. Pin LF end to end.
installer/install.sh text eol=lf
# Keep the PowerShell half deterministic too (PS tolerates either; the
# published artifact should not depend on the publisher's autocrlf).
installer/install.ps1 text eol=lf


File created successfully at: C:\Users\decid\Documents\projects\spt-core\.gitattributes (file state is current in your context — no need to Read it back)

# install.sh is piped straight into `sh` from the Pages URL — a CRLF checkout
# (or publish) would inject \r into every command. Pin LF end to end.
installer/install.sh text eol=lf
# Keep the PowerShell half deterministic too (PS tolerates either; the
# published artifact should not depend on the publisher's autocrlf).
installer/install.ps1 text eol=lf


PowerShell

cd C:\Users\decid\Documents\projects\spt-core; git add .gitattributes; git add --renormalize installer; git status --short; git commit -m @'
chore: pin installer scripts to LF (.gitattributes)

install.sh is piped straight into sh from the Pages URL; a CRLF checkout or
publish injects \r into every command. Renormalized.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@; git log --oneline -4

Commit .gitattributes + renormalize


warning: in the working copy of '.gitattributes', LF will be replaced by CRLF the next time Git touches it
A  .gitattributes
[dev-freeform f8e0f8a] chore: pin installer scripts to LF (.gitattributes)
 1 file changed, 6 insertions(+)
 create mode 100644 .gitattributes
f8e0f8a chore: pin installer scripts to LF (.gitattributes)
2ab8bad feat(install): D2 — non-interactive installer scripts + staged-release CI rung
e430327 docs(rel): D1 — spt-releases public face shipped; REQ-REL-1 doc stage live
dbb5188 docs(plan): M6 stage-setting — JIT plan, ROADMAP entry, REQ registry rows


cd C:\Users\decid\Documents\projects\spt-core; git push 2>&1 | Select-Object -Last 2; gh run list --branch dev-freeform --limit 1

Push D1+D2 and confirm CI run starts


To https://github.com/SaberMage/spt-core.git
   dbb5188..f8e0f8a  dev-freeform -> dev-freeform
completed	cancelled	docs(plan): M6 stage-setting — JIT plan, ROADMAP entry, REQ registry …	ci	dev-freeform	push	27011578821	25m34s	2026-06-05T11:13:18Z


cd C:\Users\decid\Documents\projects\spt-core; Start-Sleep 10; gh run list --branch dev-freeform --limit 1

Confirm new CI run queued


in_progress		chore: pin installer scripts to LF (.gitattributes)	ci	dev-freeform	push	27033872389	16s	2026-06-05T18:50:47Z


Updated task #3 status


Get-ChildItem C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src | Select-Object Name; Get-Content C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\Cargo.toml

spt-runtime layout + deps


Name
----
lib.rs
manifest.rs
registry.rs
runtime.rs
[package]
name = "spt-runtime"
version = "0.0.0"
edition = "2021"
publish = false
description = "spt-core harness contract: runtime-manifest schema + AgentRuntime/ManifestRuntime (opaque-template substitution + spawn-session seam). The outbound half of the harness-agnostic contract (M2a). Public SDK (R-ARCH-2)."

[dependencies]
spt-proto = { path = "../spt-proto" }
spt-store = { path = "../spt-store" }
spt-msg = { path = "../spt-msg" }
serde = { version = "1", features = ["derive"] }
toml = "0.8"

[dev-dependencies]
tempfile = "3"