From 4e0a71e0a7a0a16507913ec18ddc2efa3ff49784 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:00:41 -0500 Subject: [PATCH 01/15] Add Stage 0 Wave C continuous integration design Records seven brainstorming decisions: checksummed development artifact, a CI secret-scanning job alongside the already-enabled native scanning, validity checks, Windows runners for .NET jobs, a discovered-test-count assertion, C#-only formatting, and one SBOM generator covering both ecosystems. Also records the three repository settings changes this wave needs, so the footprint outside the repository tree is visible up front. Co-Authored-By: Claude Opus 5 Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .../specs/2026-08-16-stage0-wave-c-design.md | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md diff --git a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md new file mode 100644 index 0000000..8bbac67 --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md @@ -0,0 +1,135 @@ +# Stage 0 Wave C — Continuous integration and branch protection (design) + +- **Date:** 2026-08-16 +- **Stage:** 0 (Program foundation) +- **Wave:** C of four +- **Owner:** Root integrator +- **Branch:** `codex/stage0-wave-c` +- **Status:** Awaiting user review + +## 1. Why this wave exists + +Wave A established governance and records. Wave B created the toolchain pins and the solution's dependency graph. Wave C makes both enforceable, covering one Stage 0 bullet: + +> "establish CI for formatting, build, unit tests, dependency review, secret scanning, SBOM generation, and checksummed development artifacts" + +It also discharges two debts Wave A and Wave B deliberately left: + +- [`.github/branch-protection.md`](../../../.github/branch-protection.md) records a ruleset and states that its "Required checks" section is "to be filled in by Wave C with the exact job names from the CI workflow. Wave C is not complete until this section names real, passing checks." +- `D-091` records that branch protection is documented in Wave A but enabled in Wave C. + +Wave D (security contracts and the repository-grounded threat model) remains last, because `F-010` requires the threat model to follow the executable trust boundaries. + +**Scope discipline.** This wave adds automation, not product behavior. No `.cs` or `.ts` file gains logic. The Stage 0 exit gate's "no product capability is claimed" still binds. + +## 2. Current state + +Verified on 2026-08-16 against `main` at `23d4b20`, with Wave B merged. + +**Buildable:** `dotnet build OpenMemory.sln` succeeds with 0 warnings and 0 errors; `dotnet test` passes 4 tests across 3 projects; `pnpm run typecheck` is clean. + +**Repository security settings, read from the API rather than assumed:** + +| Setting | State | +|---|---| +| Secret scanning | Enabled (automatic for public repositories) | +| Push protection | Enabled | +| Non-provider patterns | **Disabled** | +| Validity checks | **Disabled** | +| Dependabot security updates | Disabled | +| Open secret-scanning alerts | 0 | + +**Missing:** `.github/workflows/` does not exist. `main` has no branch protection. No SBOM or artifact has ever been produced. + +## 3. Decisions taken during brainstorming + +| Ref | Decision | Rationale | +|---|---|---| +| C-1 | CI produces a checksummed development artifact: publish `Service`, `Cli`, and `McpBridge`, zip, emit SHA-256, upload with the SBOM. | Stage 8's release depends on this path. Exercising it now, while the payload is three empty `Main` methods, is far cheaper than debugging it when the payload is a real signed installer. Nothing can leak, because nothing is implemented. | +| C-2 | Add a CI secret-scanning job **in addition to** the already-enabled native scanning, and enable non-provider patterns. | GitHub's native secret scanning is not a status check — branch protection cannot require it. Stage 0 asks for secret scanning *in CI*, and `D-091` needs a real check to gate on. Non-provider patterns matter because this project's threat model covers private keys and connection strings in captured transcripts, not only vendor-format tokens. | +| C-3 | Enable validity checks. | A secret leaked into a public repository is already exposed, so testing whether it is still live adds negligible risk and real remediation value: it distinguishes "rotate now" from "already dead". | +| C-4 | .NET jobs run on `windows-latest`; jobs that do not touch the build run on `ubuntu-latest`. | `D-003` fixes Windows 11 x64 as the only supported platform and the Stage 0 gate requires a "clean Windows build". The repository is public, so Actions minutes are free and runner cost is not a factor. | +| C-5 | The test job asserts a minimum discovered-test **count**, not merely a zero exit code. | `dotnet test` exits 0 both when a project has no tests and when test discovery is broken. Exit code alone cannot distinguish them. This was observed directly during Wave B and is the failure mode that would let a Stage 2 misconfiguration report green while nothing ran. | +| C-6 | Formatting is enforced for C# only; no TypeScript formatter is added. | `dotnet format` needs no new dependency. Adding Prettier would introduce one for a single 7-line stub file. TypeScript formatting belongs to Stage 6, when the plugin is real. | +| C-7 | One SBOM tool covering both ecosystems rather than one per ecosystem. | The repository has a .NET and an npm dependency tree. A single generator produces one reconciled document instead of two that must be merged. | + +## 4. Deliverables + +### 4.1 `.github/workflows/ci.yml` + +One workflow, triggered on pull requests targeting `main` and on pushes to `main`. Six jobs, each independently required-able. + +| Job | Runner | Content | +|---|---|---| +| `build-and-test` | `windows-latest` | `dotnet format --verify-no-changes`, `dotnet restore`, `dotnet build`, `dotnet test`, plus the discovered-test-count assertion (C-5) | +| `plugin` | `ubuntu-latest` | `pnpm install --frozen-lockfile`, `pnpm run typecheck` | +| `secret-scan` | `ubuntu-latest` | gitleaks over the repository | +| `dependency-review` | `ubuntu-latest` | `actions/dependency-review-action`; pull-request events only | +| `docs` | `ubuntu-latest` | `bash tools/check-links.sh` | +| `artifact` | `windows-latest` | `dotnet publish` of the three executables, zip, SHA-256, SBOM, upload | + +**On `--frozen-lockfile`:** this is why Wave B committed `pnpm-lock.yaml`. The flag makes the install fail rather than silently resolving different versions when the lockfile and `package.json` disagree, which is what makes the plugin build reproducible. + +**On the test-count assertion:** the job uses `dotnet test --list-tests` and counts discovered tests, failing below an expected floor. Counting discovered tests rather than parsing pass/fail summary text distinguishes the two cases C-5 names: zero discovered tests fails, whereas a passing run of a smaller-than-expected set also fails. + +**On `dotnet format`:** `Directory.Build.props` deliberately does **not** set `EnforceCodeStyleInBuild`, so style is not enforced during compilation. Running `dotnet format --verify-no-changes` as a separate CI step keeps style failures distinguishable from correctness failures, which was the reason that property was removed in Wave B. + +### 4.2 Checksummed development artifact + +The `artifact` job publishes `OpenMemory.Service`, `OpenMemory.Cli`, and `OpenMemory.McpBridge`, bundles them into one archive, writes a SHA-256 file beside it, and uploads both together with the SBOM. + +The archive is **integrity evidence only**. `D-018` states that automatic installation requires "a signature or signed attestation anchored to a pinned trusted project identity; a checksum is integrity evidence only", and `D-071` requires publisher authentication before any automatic update. A checksum published on the same channel as the file it describes proves the bytes were not corrupted; it proves nothing about who produced them. The artifact is therefore explicitly a development artifact, unsigned and manual-install only, exactly as `D-018` describes for this stage. + +### 4.3 SBOM + +A software bill of materials — the inventory of every dependency the build pulls in. One generator covers both the .NET and npm trees (C-7). It is uploaded with the artifact so the two travel together, which is the arrangement Stage 8's release evidence requires. + +### 4.4 Repository settings + +Three changes outside the repository tree, each requiring explicit approval before being applied: + +1. Enable **non-provider patterns** for secret scanning (C-2). +2. Enable **validity checks** (C-3). +3. Enable **branch protection** on `main` with the ruleset already recorded in `.github/branch-protection.md`. + +These are API calls, not file edits. They are listed here so the wave's full footprint is visible in one place rather than discovered during implementation. + +### 4.5 `.github/branch-protection.md` + +The "Required checks" section is replaced with the actual job names from §4.1, and the status blockquote changes from "not yet enabled" to enabled, with the date. Nothing else in the document changes — the ruleset table was settled in Wave A. + +## 5. Verification + +Each check runs before its change to observe it fail first, where that is possible. + +| Check | How | Fails before because | +|---|---|---| +| Workflow is valid YAML and GitHub accepts it | Push the branch; `gh run list` shows a run | No workflow exists | +| Every job passes | `gh run watch` / `gh run view` on the branch's run | — | +| The test-count assertion can fail | Temporarily lower the discovered set or raise the floor, observe failure, revert | — | +| Artifact and SBOM exist and are downloadable | `gh run download` the completed run | No artifact has ever been produced | +| SHA-256 matches the archive | Recompute locally against the downloaded file and compare | — | +| Branch protection is active with the right checks | `gh api repos/June74/openmemory/branches/main/protection` returns the ruleset | Currently returns 404 "Branch not protected" | +| Links | `bash tools/check-links.sh` | New documents are linked before they exist | +| Independent review | `codex exec` over the branch diff | — | + +**A constraint specific to this wave:** a workflow cannot be verified locally. Its first real execution happens on the pushed branch, so unlike Waves A and B, this wave's acceptance evidence requires the push. The wave is not complete on local checks alone. + +## 6. Out of scope + +- Any product behavior. No `.cs` or `.ts` file gains logic. +- A TypeScript formatter (C-6, Stage 6). +- Dependabot version-update configuration. `dependency-review-action` covers pull-request-time review, which is what Stage 0 asks for; scheduled dependency bumps are a separate operational decision. +- Code signing and attestation. `D-018` and `F-007` place these at Stage 8, with explicit approval required before any paid service. +- The threat model and the four frozen security contracts — Wave D, `F-010`. +- Release publishing. This wave uploads workflow artifacts; it does not create GitHub Releases or tags. + +## 7. Risks + +| Risk | Mitigation | +|---|---| +| Enabling branch protection with administrator enforcement means a broken CI blocks merging the fix for that CI. | This is the intended tradeoff — a rule the maintainer can silently bypass is not a control. Recorded here so the consequence is accepted deliberately rather than discovered under pressure. Recovery is to temporarily disable enforcement, which is visible in the audit log. | +| Required checks are named as strings; renaming a job later silently un-enforces it, because a required check that never reports simply never blocks. | `.github/branch-protection.md` records the exact names, and §4.5 makes updating it part of any job rename. | +| A third-party action is a supply-chain dependency in the security-critical path. | Actions are pinned and the set is kept minimal. This is a genuine residual risk, not one this wave eliminates, and belongs in Wave D's threat model. | +| `dotnet format --verify-no-changes` may fail on Wave B's existing files, which were never format-checked. | Expected. If it fails, the correct response is one formatting commit, not relaxing the check. | +| The artifact is downloadable from a public repository. | The binaries are inert stubs with empty entry points. This is the last stage at which that is true, which is part of why the path is being exercised now. | From a6b61cd810276a86bdcba22af953314769228fca Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:11:39 -0500 Subject: [PATCH 02/15] Add Stage 0 Wave C implementation plan Six tasks: the correctness jobs, the security and release jobs, pushing and driving every job green, recording the required checks, the approval-gated repository settings and branch protection, and integration verification with independent Codex review. Co-Authored-By: Claude Opus 5 Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .../plans/2026-08-16-stage0-wave-c.md | 601 ++++++++++++++++++ 1 file changed, 601 insertions(+) create mode 100644 docs/superpowers/plans/2026-08-16-stage0-wave-c.md diff --git a/docs/superpowers/plans/2026-08-16-stage0-wave-c.md b/docs/superpowers/plans/2026-08-16-stage0-wave-c.md new file mode 100644 index 0000000..aae941c --- /dev/null +++ b/docs/superpowers/plans/2026-08-16-stage0-wave-c.md @@ -0,0 +1,601 @@ +# Stage 0 Wave C — Continuous Integration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Establish CI for formatting, build, tests, dependency review, secret scanning, SBOM, and a checksummed development artifact — then enable branch protection against those checks. + +**Architecture:** One workflow, `.github/workflows/ci.yml`, with six independently-required-able jobs. .NET jobs run on `windows-latest` because `D-003` fixes Windows as the only supported platform; jobs that do not touch the build run on `ubuntu-latest`. The repository is public, so Actions minutes are free and runner choice is driven by correctness rather than cost. + +**Tech Stack:** GitHub Actions, .NET SDK 10.0.400, pnpm 11 / Node 24, gitleaks, Syft (SBOM), `actions/dependency-review-action`. + +**Spec:** [docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md](../specs/2026-08-16-stage0-wave-c-design.md) + +## Global Constraints + +Every task's requirements implicitly include this section. + +- **No product behavior.** No `.cs` or `.ts` file gains logic. This wave adds automation only. +- **Repository:** `June74/openmemory`. **Branch:** `codex/stage0-wave-c`. Never commit to `main`. +- **DCO required.** Every commit uses `git commit -s`. +- **Never write a secret value** into any file, including examples. This wave configures secret scanning; a fake-but-realistic token in a workflow would be self-defeating and would trip the very scanners being installed. +- **No repository secrets are needed.** Every action used here works on a public repository with the default `GITHUB_TOKEN`. If a step appears to need a secret, stop and report — do not create one. +- **Pin every third-party action to an explicit major version tag** (`@v4`, not `@main`). An action referenced by a moving ref is an unpinned dependency in the security-critical path. +- **`permissions:` is declared explicitly** and kept least-privilege. Do not grant `write` where `read` suffices. +- **Never commit build output**, `node_modules`, SBOM files, or artifacts. They are produced by CI, not stored in the repository. +- **Use Bash** locally (Git Bash available), never a PowerShell pipeline. Inside the workflow, Windows steps use `shell: pwsh` where a Windows-native cmdlet is the right tool. +- **A workflow cannot be verified locally.** Tasks 1 and 2 validate YAML structure only; Task 3 is where real verification happens, on the pushed branch. + +--- + +### Task 1: Workflow skeleton and the correctness jobs + +**Files:** +- Create: `.github/workflows/ci.yml` + +**Interfaces:** +- Consumes: `OpenMemory.sln`, `global.json`, `tools/check-links.sh`, and `src/OpenMemory.ObsidianPlugin/package.json` — all from `main`. +- Produces: job names `build-and-test`, `plugin`, and `docs`, which Task 4 records as required checks and Task 5 enforces. **These exact strings matter** — branch protection matches required checks by name, and a renamed job silently stops being enforced. + +- [ ] **Step 1: Verify no workflow exists** + +```bash +cd "C:/Users/2006i/projects/openmemory" && ls .github/workflows 2>&1 +``` + +Expected: `No such file or directory`. + +- [ ] **Step 2: Write the workflow with three jobs** + +Create `.github/workflows/ci.yml`: + +```yaml +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: build-and-test + runs-on: windows-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Verify formatting + run: dotnet format OpenMemory.sln --verify-no-changes + + - name: Restore + run: dotnet restore OpenMemory.sln + + - name: Build + run: dotnet build OpenMemory.sln --no-restore --nologo + + - name: Run tests + run: dotnet test OpenMemory.sln --no-build --nologo + + - name: Assert minimum discovered test count + shell: pwsh + run: | + $expected = 4 + $listed = dotnet test OpenMemory.sln --no-build --nologo --list-tests + $count = ($listed | Where-Object { $_ -match '^\s+\S+\.\S+\.\S+$' }).Count + Write-Host "Discovered $count tests (floor $expected)" + if ($count -lt $expected) { + throw "Discovered $count tests, expected at least $expected. Either tests were removed, or test discovery is broken. dotnet test exits 0 in both cases, which is why this assertion exists." + } + + plugin: + name: plugin + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/OpenMemory.ObsidianPlugin + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Enable corepack + run: corepack enable + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Type-check + run: pnpm run typecheck + + docs: + name: docs + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Check repository-internal links + run: bash tools/check-links.sh +``` + +Three things in here are load-bearing and a reasonable-looking edit breaks them: + +1. **`--list-tests` counts discovery, not results.** The point of C-5 is that `dotnet test` exits 0 both when there are no tests and when discovery is broken. Counting *discovered* tests distinguishes those. Do not replace this with a check on the exit code or on "Passed!" text. +2. **`corepack enable` before `pnpm`.** The plugin's `package.json` declares `packageManager: pnpm@11.9.0`; Corepack reads that field and provisions exactly that version. Installing pnpm some other way discards the pin. +3. **`--frozen-lockfile`.** This is why Wave B committed `pnpm-lock.yaml`. It fails rather than silently resolving different versions when lockfile and manifest disagree. + +- [ ] **Step 3: Validate the YAML parses** + +```bash +cd "C:/Users/2006i/projects/openmemory" && node -e ' +const fs=require("fs"); +const t=fs.readFileSync(".github/workflows/ci.yml","utf8"); +if(t.includes("\t")){console.error("FAIL: tab character in YAML");process.exit(1)} +for(const j of ["build-and-test","plugin","docs"]){ + if(!t.includes("name: "+j)){console.error("FAIL: missing job name "+j);process.exit(1)} +} +console.log("PASS: structural check");' +``` + +Expected: `PASS: structural check`. + +This is a structural check only. GitHub validates workflow schema when it runs the file, which is Task 3. + +- [ ] **Step 4: Confirm the expected test count is correct** + +```bash +cd "C:/Users/2006i/projects/openmemory" && dotnet test OpenMemory.sln --nologo --list-tests 2>&1 | grep -cE '^\s+\S+\.\S+\.\S+$' +``` + +Expected: `4`, matching the `$expected` floor in the workflow. + +If this returns a different number, the regex does not match this environment's output format. **Report the actual output rather than loosening the pattern** — a matcher that matches nothing would make the assertion pass vacuously, which is the exact defect it exists to prevent. + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -s -m "Add CI workflow with build, plugin, and docs jobs + +The test step asserts a minimum discovered-test count because dotnet +test exits 0 both when no tests exist and when discovery is broken." +``` + +--- + +### Task 2: Security and release jobs + +**Files:** +- Modify: `.github/workflows/ci.yml` + +**Interfaces:** +- Consumes: the workflow from Task 1. +- Produces: job names `secret-scan`, `dependency-review`, and `artifact`. As in Task 1, **the exact strings matter** for Task 4 and Task 5. + +- [ ] **Step 1: Determine the gitleaks version to pin** + +```bash +gh api repos/gitleaks/gitleaks/releases/latest --jq '.tag_name' +``` + +Record the tag it returns. Pin that exact version in Step 2 — do not use `latest` in the workflow, because an unpinned scanner in the security-critical path is a supply-chain dependency that can change under you. + +The binary is downloaded directly rather than using a marketplace action, because gitleaks' own action has had licensing conditions for some account types and this repository must not depend on that resolving favourably. + +- [ ] **Step 2: Append the three jobs** + +Add to `.github/workflows/ci.yml`, at the same indentation level as the existing jobs. Replace `` with the tag from Step 1 (including its leading `v`) and `` with the same value without the leading `v`. + +```yaml + secret-scan: + name: secret-scan + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install gitleaks + run: | + curl -sSfL -o gitleaks.tar.gz \ + "https://github.com/gitleaks/gitleaks/releases/download//gitleaks__linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + chmod +x gitleaks + + - name: Scan repository history + run: ./gitleaks detect --source . --no-banner --redact --exit-code 1 + + dependency-review: + name: dependency-review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Review dependencies + uses: actions/dependency-review-action@v4 + + artifact: + name: artifact + runs-on: windows-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Publish executables + shell: pwsh + run: | + foreach ($p in "Service", "Cli", "McpBridge") { + dotnet publish "src/OpenMemory.$p/OpenMemory.$p.csproj" ` + --configuration Release ` + --output "artifacts/publish/OpenMemory.$p" + } + + - name: Package and checksum + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path artifacts/out | Out-Null + Compress-Archive -Path artifacts/publish/* -DestinationPath artifacts/out/openmemory-dev.zip + $hash = Get-FileHash -Algorithm SHA256 -Path artifacts/out/openmemory-dev.zip + "$($hash.Hash.ToLower()) openmemory-dev.zip" | + Out-File -FilePath artifacts/out/openmemory-dev.zip.sha256 -Encoding ascii + Get-Content artifacts/out/openmemory-dev.zip.sha256 + + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: cyclonedx-json + output-file: artifacts/out/openmemory-sbom.cyclonedx.json + upload-artifact: false + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: openmemory-dev + path: artifacts/out/ + if-no-files-found: error +``` + +Notes the YAML does not convey: + +- **`fetch-depth: 0` on the secret-scan checkout** gives gitleaks the full history. The default shallow clone would scan only the tip commit, so a secret introduced and later removed would go undetected. +- **`--exit-code 1`** makes a finding fail the job. Without it gitleaks reports and exits 0, and the required check would pass while reporting leaks. +- **`--redact`** keeps any detected value out of the workflow log. Public repository: the log is world-readable, and a scanner that prints the secret it found has published it. +- **`if: github.event_name == 'pull_request'`** on dependency-review because that action only operates on a pull-request diff. +- **`shell: pwsh` on the Windows steps** because `Compress-Archive` and `Get-FileHash` are the native tools. These are single cmdlets, not chained pipelines — the setback that produced `SET-20260816-001` was a PowerShell *pipeline* parse failure. +- **`if-no-files-found: error`** so a silently empty artifact fails rather than uploading nothing. + +Two version-drift traps to check rather than assume, because both fail in confusing ways: + +- **The gitleaks subcommand.** Recent versions moved from `gitleaks detect` to `gitleaks git`, with `detect` deprecated. After pinning the version in Step 1, run `./gitleaks --help` (or check that release's notes) and use whichever subcommand that version actually documents. Record which you used. A deprecated subcommand may still run but emit a warning, and a removed one fails the job with an unhelpful error. +- **`anchore/sbom-action` input names.** The inputs used here (`path`, `format`, `output-file`, `upload-artifact`) are correct for recent v0 releases, but if the step errors on an unrecognised input, read the action's own `action.yml` rather than guessing a substitute — and record what you changed. + +- [ ] **Step 3: Re-run the structural check for all six jobs** + +```bash +cd "C:/Users/2006i/projects/openmemory" && node -e ' +const fs=require("fs"); +const t=fs.readFileSync(".github/workflows/ci.yml","utf8"); +if(t.includes("\t")){console.error("FAIL: tab in YAML");process.exit(1)} +const jobs=["build-and-test","plugin","docs","secret-scan","dependency-review","artifact"]; +for(const j of jobs){ if(!t.includes("name: "+j)){console.error("FAIL: missing "+j);process.exit(1)} } +if(/uses:\s+\S+@main/.test(t)){console.error("FAIL: action pinned to @main");process.exit(1)} +if(/gitleaks\/releases\/download\/latest/.test(t)){console.error("FAIL: gitleaks not version-pinned");process.exit(1)} +console.log("PASS: 6 jobs, all actions pinned");' +``` + +Expected: `PASS: 6 jobs, all actions pinned`. + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/ci.yml +git commit -s -m "Add secret scan, dependency review, and artifact jobs + +gitleaks is version-pinned and downloaded directly rather than through +a marketplace action with licensing conditions. It runs with full +history, redacted output, and a failing exit code on any finding. + +The artifact job publishes the three executables, checksums the +archive, and uploads it with a CycloneDX SBOM. Per D-018 the checksum +is integrity evidence only; it is not publisher authentication." +``` + +--- + +### Task 3: Push and drive every job green + +This is where the wave is actually verified. A workflow's first real execution happens on the pushed branch, so unlike Waves A and B, local checks cannot complete this one. + +**Files:** `.github/workflows/ci.yml` and possibly source files, if the format check fails. + +**Interfaces:** +- Consumes: Tasks 1 and 2. +- Produces: a green run whose job names Task 4 records. + +- [ ] **Step 1: Push the branch** + +```bash +cd "C:/Users/2006i/projects/openmemory" && git push -u origin codex/stage0-wave-c +``` + +- [ ] **Step 2: Watch the run** + +```bash +cd "C:/Users/2006i/projects/openmemory" && gh run list --branch codex/stage0-wave-c --limit 1 +gh run watch $(gh run list --branch codex/stage0-wave-c --limit 1 --json databaseId --jq '.[0].databaseId') +``` + +- [ ] **Step 3: Diagnose any failure at its root** + +For each failing job: + +```bash +gh run view --log-failed +``` + +Fix the cause and push again. Expected failures, and the correct response to each: + +| Failure | Correct response | +|---|---| +| `dotnet format --verify-no-changes` fails | Wave B's files were never format-checked. Run `dotnet format OpenMemory.sln` locally, commit the result as one formatting commit. **Do not** remove or weaken the format step. | +| gitleaks reports a finding | Investigate it. If a real secret exists, stop and report immediately — do not commit over it. If it is a false positive, add a narrowly-scoped `.gitleaks.toml` allowlist entry with a comment explaining why, never a blanket ignore. | +| The test-count assertion fails | The regex does not match this runner's output format. Report the actual `--list-tests` output. **Do not lower `$expected` or loosen the regex** — that would restore the vacuous pass the assertion exists to prevent. | +| `dependency-review` fails | Read what it flagged. A vulnerable or incompatible-licence dependency is a real finding, not a check to disable. | +| An action version does not exist | Bump to the current major tag and record which version you used. | + +Push after each fix and re-watch. Repeat until every job is green. + +- [ ] **Step 4: Confirm the artifact is real** + +```bash +cd "C:/Users/2006i/projects/openmemory" +RUN=$(gh run list --branch codex/stage0-wave-c --limit 1 --json databaseId --jq '.[0].databaseId') +mkdir -p /tmp/wave-c-artifact && gh run download "$RUN" --dir /tmp/wave-c-artifact +ls -R /tmp/wave-c-artifact +``` + +Expected: the zip, its `.sha256`, and the SBOM. + +Then verify the checksum actually matches — a checksum file nobody has checked is decoration: + +```bash +cd /tmp/wave-c-artifact/openmemory-dev && sha256sum -c openmemory-dev.zip.sha256 +``` + +Expected: `openmemory-dev.zip: OK`. + +- [ ] **Step 5: Record the final green run** + +```bash +cd "C:/Users/2006i/projects/openmemory" && gh run list --branch codex/stage0-wave-c --limit 1 --json databaseId,conclusion,url --jq '.[0]' +``` + +Expected: `"conclusion": "success"`. Record the URL — it is this wave's primary acceptance evidence. + +--- + +### Task 4: Record the required checks + +**Files:** +- Modify: `.github/branch-protection.md` + +**Interfaces:** +- Consumes: the green run's job names from Task 3. +- Produces: the documented check list Task 5 applies. + +- [ ] **Step 1: Confirm the current placeholder** + +```bash +cd "C:/Users/2006i/projects/openmemory" && sed -n '30,40p' .github/branch-protection.md +``` + +Expected: the "Required checks" section still says it is to be filled in by Wave C. + +- [ ] **Step 2: Replace the status blockquote and the Required checks section** + +Change the status blockquote at the top from "not yet enabled" to enabled, dated 2026-08-16, and replace the "Required checks" section body with: + +```markdown +## Required checks + +These are the job names from [`ci.yml`](workflows/ci.yml). Branch protection +matches required checks by name, so **renaming a job here or in the workflow +silently un-enforces it** — a required check that never reports simply never +blocks. Any job rename must update both files together. + +| Check | What it verifies | +|---|---| +| `build-and-test` | C# formatting, restore, build, tests, and the minimum discovered-test count | +| `plugin` | The Obsidian plugin installs from the committed lockfile and type-checks | +| `docs` | Every repository-internal Markdown link resolves | +| `secret-scan` | gitleaks finds no secret in the full history | +| `dependency-review` | No vulnerable or incompatible-licence dependency is introduced | +| `artifact` | Publish, checksum, and SBOM generation succeed | +``` + +**Additions and replacements only.** Do not alter the ruleset table above it — that was settled in Wave A and is not this task's business. + +- [ ] **Step 3: Verify** + +```bash +cd "C:/Users/2006i/projects/openmemory" && git add .github/branch-protection.md && bash tools/check-links.sh; echo "exit=$?" +grep -c 'build-and-test\|plugin\|docs\|secret-scan\|dependency-review\|artifact' .github/branch-protection.md +``` + +Expected: `exit=0`, and at least `6` matches. + +- [ ] **Step 4: Commit** + +```bash +git add .github/branch-protection.md +git commit -s -m "Record the required checks from the CI workflow + +Names the six job names branch protection will require, and states +that renaming a job silently un-enforces it unless both files change." +``` + +--- + +### Task 5: Repository settings and branch protection + +**Files:** none. This task makes API calls against repository settings. + +**STOP — this task requires explicit user approval before any call.** These changes take effect outside the repository tree, change how every future merge works, and cannot be reviewed in a diff. Present the exact calls, get a clear yes, then execute. Do not proceed on the plan's authority alone. + +**Interfaces:** +- Consumes: Task 4's documented check list, and a green run from Task 3. +- Produces: enforced branch protection. + +- [ ] **Step 1: Record the "before" state** + +```bash +gh api repos/June74/openmemory --jq '.security_and_analysis' +gh api repos/June74/openmemory/branches/main/protection 2>&1 | head -3 +``` + +Expected: non-provider patterns and validity checks `disabled`; protection returns 404 "Branch not protected". + +- [ ] **Step 2: Enable the two secret-scanning settings** + +```bash +gh api -X PATCH repos/June74/openmemory \ + -f 'security_and_analysis[secret_scanning_non_provider_patterns][status]=enabled' \ + -f 'security_and_analysis[secret_scanning_validity_checks][status]=enabled' +``` + +- [ ] **Step 3: Enable branch protection** + +The ruleset below is transcribed from `.github/branch-protection.md`, which was settled in Wave A. `enforce_admins: true` is deliberate — a rule the maintainer can silently bypass is not a control. + +```bash +cd "C:/Users/2006i/projects/openmemory" && cat > /tmp/protection.json <<'JSON' +{ + "required_status_checks": { + "strict": true, + "contexts": ["build-and-test", "plugin", "docs", "secret-scan", "dependency-review", "artifact"] + }, + "enforce_admins": true, + "required_pull_request_reviews": { + "required_approving_review_count": 0, + "dismiss_stale_reviews": false, + "require_code_owner_reviews": false + }, + "restrictions": null, + "allow_force_pushes": false, + "allow_deletions": false +} +JSON +gh api -X PUT repos/June74/openmemory/branches/main/protection --input /tmp/protection.json +``` + +- [ ] **Step 4: Verify the "after" state** + +```bash +gh api repos/June74/openmemory --jq '.security_and_analysis' +gh api repos/June74/openmemory/branches/main/protection --jq '{ + checks: .required_status_checks.contexts, + strict: .required_status_checks.strict, + admins: .enforce_admins.enabled, + force_push: .allow_force_pushes.enabled, + deletions: .allow_deletions.enabled +}' +``` + +Expected: both secret-scanning settings `enabled`; six contexts listed; `strict: true`; `admins: true`; both `force_push` and `deletions` false. + +--- + +### Task 6: Integration verification and independent review + +**Files:** none created or modified except a possible setback record. + +- [ ] **Step 1: Confirm no product behavior was added** + +```bash +cd "C:/Users/2006i/projects/openmemory" +git diff --name-only main...HEAD +git diff main...HEAD -- '*.cs' '*.ts' | head -20 +``` + +Expected: only `.github/` and `docs/` paths, unless Task 3 required a formatting commit — in which case `.cs` changes must be whitespace only, with no logic added. + +- [ ] **Step 2: DCO and secret scan** + +```bash +cd "C:/Users/2006i/projects/openmemory" +for sha in $(git log main..HEAD --format=%H); do + git log -1 --format=%B "$sha" | grep -qE '^Signed-off-by:' || echo "MISSING SIGN-OFF: $sha" +done; echo "sign-off check complete" +git diff main...HEAD | grep -inE '(sk-[A-Za-z0-9]{8,}|ghp_[A-Za-z0-9]{8,}|-----BEGIN [A-Z ]*PRIVATE KEY-----)' && echo "FAIL" || echo "PASS: no secret-shaped content" +``` + +- [ ] **Step 3: Links** + +```bash +cd "C:/Users/2006i/projects/openmemory" && bash tools/check-links.sh; echo "exit=$?" +``` + +Expected: `exit=0`. + +- [ ] **Step 4: Confirm CI is green on the final commit** + +```bash +cd "C:/Users/2006i/projects/openmemory" && gh run list --branch codex/stage0-wave-c --limit 1 --json headSha,conclusion --jq '.[0]' +git rev-parse HEAD +``` + +Expected: `"conclusion": "success"`, and `headSha` matching local `HEAD`. A green run against an older commit is not evidence about the current tree. + +- [ ] **Step 5: Independent review by Codex** + +```bash +cd "C:/Users/2006i/projects/openmemory" +git diff main...HEAD > "C:/Users/2006i/AppData/Local/Temp/claude/C--Users-2006i-projects-openmemory/321382d3-fcf5-498c-8a6d-843b2f666ac1/scratchpad/wave-c.diff" +codex exec "Review this diff as an independent specification and quality reviewer for the OpenMemory project. Read AGENTS.md, docs/IMPLEMENTATION_PLAN.md, and docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md first. Verify: (1) the diff implements the Wave C spec with no gap and no scope creep; (2) no product behavior was added; (3) every third-party action and downloaded tool is pinned to an explicit version, and permissions are least-privilege; (4) the secret-scan job cannot pass while leaking — check exit code handling, history depth, and log redaction; (5) the test-count assertion cannot pass vacuously; (6) the required-check names in .github/branch-protection.md exactly match the job names in ci.yml. Report findings by severity with concrete evidence. Do not modify files." < "C:/Users/2006i/AppData/Local/Temp/claude/C--Users-2006i-projects-openmemory/321382d3-fcf5-498c-8a6d-843b2f666ac1/scratchpad/wave-c.diff" +``` + +Write the diff to a scratch directory **outside** the repository so it does not appear as an untracked file. Record Codex's findings verbatim; per `receiving-code-review`, verify each technically rather than accepting it performatively. + +- [ ] **Step 6: Record any unexpected failure** + +If any step failed unexpectedly, create a setback record in `docs/operations/setbacks/` following the existing five, add it to `INDEX.md`, and commit. + +--- + +## Wave C completion criteria + +1. Tasks 1–6 complete. +2. All six CI jobs green on a run whose `headSha` matches the branch tip. +3. The downloaded artifact's SHA-256 verifies against the archive. +4. `.github/branch-protection.md` names the six real job names. +5. Branch protection active on `main` with those six contexts, `strict: true`, `enforce_admins: true`, force-pushes and deletions disabled. +6. Non-provider patterns and validity checks enabled. +7. Every commit DCO-signed; no secret-shaped content. +8. Codex independent review findings resolved with evidence. +9. No product behavior added beyond whitespace-only formatting changes, if any. From ca0e6223da521ffa853ea20ababd54fb0a2287ae Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:14:21 -0500 Subject: [PATCH 03/15] Add CI workflow with build, plugin, and docs jobs The test step asserts a minimum discovered-test count because dotnet test exits 0 both when no tests exist and when discovery is broken. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/workflows/ci.yml | 84 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..af4d497 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: build-and-test + runs-on: windows-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Verify formatting + run: dotnet format OpenMemory.sln --verify-no-changes + + - name: Restore + run: dotnet restore OpenMemory.sln + + - name: Build + run: dotnet build OpenMemory.sln --no-restore --nologo + + - name: Run tests + run: dotnet test OpenMemory.sln --no-build --nologo + + - name: Assert minimum discovered test count + shell: pwsh + run: | + $expected = 4 + $listed = dotnet test OpenMemory.sln --no-build --nologo --list-tests + $count = ($listed | Where-Object { $_ -match '^\s+\S+\.\S+\.\S+$' }).Count + Write-Host "Discovered $count tests (floor $expected)" + if ($count -lt $expected) { + throw "Discovered $count tests, expected at least $expected. Either tests were removed, or test discovery is broken. dotnet test exits 0 in both cases, which is why this assertion exists." + } + + plugin: + name: plugin + runs-on: ubuntu-latest + defaults: + run: + working-directory: src/OpenMemory.ObsidianPlugin + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Enable corepack + run: corepack enable + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Type-check + run: pnpm run typecheck + + docs: + name: docs + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Check repository-internal links + run: bash tools/check-links.sh From b522574ff35c6b4bdfb81902254568a5a47075a7 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:19:21 -0500 Subject: [PATCH 04/15] Add secret scan, dependency review, and artifact jobs gitleaks is version-pinned and downloaded directly rather than through a marketplace action with licensing conditions. It runs with full history, redacted output, and a failing exit code on any finding. Pinned to v8.30.1; uses the `git` subcommand rather than the deprecated `detect` subcommand per that release's README. The artifact job publishes the three executables, checksums the archive, and uploads it with a CycloneDX SBOM. Per D-018 the checksum is integrity evidence only; it is not publisher authentication. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/workflows/ci.yml | 76 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af4d497..8b8f555 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -82,3 +82,79 @@ jobs: - name: Check repository-internal links run: bash tools/check-links.sh + + secret-scan: + name: secret-scan + runs-on: ubuntu-latest + steps: + - name: Check out + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install gitleaks + run: | + curl -sSfL -o gitleaks.tar.gz \ + "https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz" + tar -xzf gitleaks.tar.gz gitleaks + chmod +x gitleaks + + - name: Scan repository history + run: ./gitleaks git --no-banner --redact --exit-code 1 . + + dependency-review: + name: dependency-review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Review dependencies + uses: actions/dependency-review-action@v4 + + artifact: + name: artifact + runs-on: windows-latest + steps: + - name: Check out + uses: actions/checkout@v4 + + - name: Set up .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Publish executables + shell: pwsh + run: | + foreach ($p in "Service", "Cli", "McpBridge") { + dotnet publish "src/OpenMemory.$p/OpenMemory.$p.csproj" ` + --configuration Release ` + --output "artifacts/publish/OpenMemory.$p" + } + + - name: Package and checksum + shell: pwsh + run: | + New-Item -ItemType Directory -Force -Path artifacts/out | Out-Null + Compress-Archive -Path artifacts/publish/* -DestinationPath artifacts/out/openmemory-dev.zip + $hash = Get-FileHash -Algorithm SHA256 -Path artifacts/out/openmemory-dev.zip + "$($hash.Hash.ToLower()) openmemory-dev.zip" | + Out-File -FilePath artifacts/out/openmemory-dev.zip.sha256 -Encoding ascii + Get-Content artifacts/out/openmemory-dev.zip.sha256 + + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + path: . + format: cyclonedx-json + output-file: artifacts/out/openmemory-sbom.cyclonedx.json + upload-artifact: false + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: openmemory-dev + path: artifacts/out/ + if-no-files-found: error From 47cc6582a07466fce93774c97899bb95c82f28cf Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:25:26 -0500 Subject: [PATCH 05/15] Fail fast when dotnet publish fails in the artifact job pwsh does not treat a non-zero exit from a native command as a terminating error, and the runner only fails a pwsh step on an uncaught PowerShell exception or explicit exit. Without a check, a failing dotnet publish (e.g. McpBridge fails to compile) would let the loop continue, leaving a partial artifacts/publish/ directory that Compress-Archive and Get-FileHash would happily process into a consistent-looking but incomplete, misleading package. Add an explicit $LASTEXITCODE check after each publish call, so a failure in the first iteration aborts before the second one runs. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b8f555..0defec2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,6 +132,7 @@ jobs: dotnet publish "src/OpenMemory.$p/OpenMemory.$p.csproj" ` --configuration Release ` --output "artifacts/publish/OpenMemory.$p" + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } - name: Package and checksum From 87d43591e6f8e75bc25afc1b1afaa765455dc8c3 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:29:24 -0500 Subject: [PATCH 06/15] Add workflow_dispatch trigger to CI workflow Push/pull_request triggers only fire for main, so pushing a feature branch produces no run and this task is forbidden from opening a PR. workflow_dispatch lets the workflow be run manually against this branch to get real execution evidence, without widening the push/pull_request trigger scope. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0defec2..5f789c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: [main] push: branches: [main] + workflow_dispatch: permissions: contents: read From c7f37fc4b79749894e9b78c7d41e2cf9cd73a7a5 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:30:16 -0500 Subject: [PATCH 07/15] Trigger CI push runs on codex/* branches, not only main workflow_dispatch requires the workflow to already exist on the default branch, so it cannot dispatch a run for this branch before it merges to main. All work in this repo happens on codex/* branches before merging (see codex/stage0-wave-a, -b, -c), so extending the push trigger to that pattern lets CI actually execute and be verified pre-merge, which is the only way to get runtime proof without opening a pull request. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f789c1..89c0f78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [main] push: - branches: [main] + branches: [main, 'codex/**'] workflow_dispatch: permissions: From 4caa133f04d4828d26c3c2fe36ab237626edfad6 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:34:35 -0500 Subject: [PATCH 08/15] Fix dotnet format failure: align .gitattributes eol with .editorconfig Wave B's files were never format-checked by CI. dotnet format --verify-no-changes failed on windows-latest with ENDOFLINE errors across every .cs file, because .gitattributes forced eol=lf for all text files while .editorconfig requires eol=crlf for .cs/.csproj/ .props/.targets. Running dotnet format locally fixed the working tree, but committing it as-is would have git silently renormalize the line endings back to LF on the next checkout (per .gitattributes), reproducing the same CI failure in a loop. Fixed by adding eol=crlf entries to .gitattributes for .cs/.csproj/.props/.targets, matching the extensions .editorconfig already treats as crlf, then renormalizing the tree. The only content diff (not just whitespace) is a stray UTF-8 BOM that dotnet format stripped from three Program.cs files; no logic or member reordering. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .gitattributes | 4 ++++ src/OpenMemory.Cli/Program.cs | 2 +- src/OpenMemory.McpBridge/Program.cs | 2 +- src/OpenMemory.Service/Program.cs | 2 +- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index b0fe015..27b18fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,10 @@ *.cmd text eol=crlf *.bat text eol=crlf *.sln text eol=crlf +*.cs text eol=crlf +*.csproj text eol=crlf +*.props text eol=crlf +*.targets text eol=crlf *.png binary *.jpg binary *.jpeg binary diff --git a/src/OpenMemory.Cli/Program.cs b/src/OpenMemory.Cli/Program.cs index 5042358..d461e6b 100644 --- a/src/OpenMemory.Cli/Program.cs +++ b/src/OpenMemory.Cli/Program.cs @@ -1,4 +1,4 @@ -// Copyright 2026 OpenMemory contributors +// Copyright 2026 OpenMemory contributors // SPDX-License-Identifier: Apache-2.0 namespace OpenMemory.Cli; diff --git a/src/OpenMemory.McpBridge/Program.cs b/src/OpenMemory.McpBridge/Program.cs index 5be2dd1..0cbfb51 100644 --- a/src/OpenMemory.McpBridge/Program.cs +++ b/src/OpenMemory.McpBridge/Program.cs @@ -1,4 +1,4 @@ -// Copyright 2026 OpenMemory contributors +// Copyright 2026 OpenMemory contributors // SPDX-License-Identifier: Apache-2.0 namespace OpenMemory.McpBridge; diff --git a/src/OpenMemory.Service/Program.cs b/src/OpenMemory.Service/Program.cs index 6987aaa..c4a2d20 100644 --- a/src/OpenMemory.Service/Program.cs +++ b/src/OpenMemory.Service/Program.cs @@ -1,4 +1,4 @@ -// Copyright 2026 OpenMemory contributors +// Copyright 2026 OpenMemory contributors // SPDX-License-Identifier: Apache-2.0 namespace OpenMemory.Service; From d0eed67d5a3bac007349baf318144c92612c360d Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:37:14 -0500 Subject: [PATCH 09/15] Write the artifact checksum file with LF, not CRLF Step 4's end-to-end check (download the artifact, run sha256sum -c on it) failed: Out-File on windows-latest wrote openmemory-dev.zip.sha256 with a trailing CRLF, so sha256sum read the filename as 'openmemory-dev.zip\r', which does not match the actual extracted file, and reported FAILED open or read instead of OK. Write the file directly via File.WriteAllText with an explicit LF so the checksum file is consumable by the standard sha256sum -c tool regardless of which OS produced it. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89c0f78..6a50134 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -142,8 +142,14 @@ jobs: New-Item -ItemType Directory -Force -Path artifacts/out | Out-Null Compress-Archive -Path artifacts/publish/* -DestinationPath artifacts/out/openmemory-dev.zip $hash = Get-FileHash -Algorithm SHA256 -Path artifacts/out/openmemory-dev.zip - "$($hash.Hash.ToLower()) openmemory-dev.zip" | - Out-File -FilePath artifacts/out/openmemory-dev.zip.sha256 -Encoding ascii + $line = "$($hash.Hash.ToLower()) openmemory-dev.zip" + # sha256sum expects LF-only line endings; Out-File/Set-Content emit + # CRLF on Windows, which corrupts the filename field (trailing `r`) + # and breaks `sha256sum -c` on Linux/macOS/Git Bash consumers. + [System.IO.File]::WriteAllText( + "artifacts/out/openmemory-dev.zip.sha256", + "$line`n", + [System.Text.Encoding]::ASCII) Get-Content artifacts/out/openmemory-dev.zip.sha256 - name: Generate SBOM From 44ec10fbbb2a1297d5fcaa7e2f3af5e362fc6482 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:45:24 -0500 Subject: [PATCH 10/15] Record the required checks from the CI workflow Names the six job names branch protection will require, and states that renaming a job silently un-enforces it unless both files change. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/branch-protection.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/.github/branch-protection.md b/.github/branch-protection.md index 91d939f..8ca980f 100644 --- a/.github/branch-protection.md +++ b/.github/branch-protection.md @@ -1,8 +1,7 @@ # Branch protection -> **Status:** not yet enabled. This document records the ruleset that Wave C -> will apply to `main` once continuous integration produces checks worth -> requiring. +> **Status:** The required checks below are now real and verified green. +> Enablement to GitHub is pending Task 5. ## Why it is deferred @@ -31,5 +30,16 @@ Applied to `main`: ## Required checks -To be filled in by Wave C with the exact job names from the CI workflow. -Wave C is not complete until this section names real, passing checks. +These are the job names from [`ci.yml`](workflows/ci.yml). Branch protection +matches required checks by name, so **renaming a job here or in the workflow +silently un-enforces it** — a required check that never reports simply never +blocks. Any job rename must update both files together. + +| Check | What it verifies | +|---|---| +| `build-and-test` | C# formatting, restore, build, tests, and the minimum discovered-test count | +| `plugin` | The Obsidian plugin installs from the committed lockfile and type-checks | +| `docs` | Every repository-internal Markdown link resolves | +| `secret-scan` | gitleaks finds no secret in the full history | +| `dependency-review` | No vulnerable or incompatible-licence dependency is introduced | +| `artifact` | Publish, checksum, and SBOM generation succeed | From 0c11b707c878187cdb451ddb431f0d43779c877d Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Sun, 16 Aug 2026 22:49:26 -0500 Subject: [PATCH 11/15] Fix review findings: remove internal task reference and clarify deferral reason Finding 1: Replace internal task number reference with durable condition. Status now says enablement is pending repository owner approval, not Task 5. Finding 2: Rewrite section to resolve contradiction with status. Clarify that the original reason for deferral (no checks) is now resolved. State that the remaining gate is owner approval, not missing prerequisites. Confirm that main is protected by process until enablement, and codex exec review remains the operative control. Rename section from "Why it is deferred" to "Enablement is pending repository owner approval" to reflect current state. Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/branch-protection.md | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/branch-protection.md b/.github/branch-protection.md index 8ca980f..d411485 100644 --- a/.github/branch-protection.md +++ b/.github/branch-protection.md @@ -1,17 +1,22 @@ # Branch protection > **Status:** The required checks below are now real and verified green. -> Enablement to GitHub is pending Task 5. - -## Why it is deferred - -`CONTRIBUTING.md` states that direct implementation commits to `main` are not -allowed. That rule is currently enforced by process, not by GitHub. A -protection rule cannot require status checks that do not exist, so enabling -protection before Wave C would either require nothing or block every merge. - -Until Wave C, independent review is performed locally with `codex exec` after -implementation and before integration. +> Enablement to GitHub is pending explicit approval from the repository owner. + +## Enablement is pending repository owner approval + +Originally, protection was deferred because a protection rule cannot require +status checks that do not exist, and the CI workflow did not exist yet. That +prerequisite has been satisfied: the required checks are now defined, verified +green, and documented in the section below. + +The remaining gate is explicit approval from the repository owner to enable +GitHub branch protection. Until that approval is given, `main` remains +protected by process: `CONTRIBUTING.md` states that direct implementation +commits to `main` are not allowed. That rule is currently enforced by policy +and code review, not by GitHub. Independent review is performed locally with +`codex exec` after implementation and before integration, and this remains +the operative control. ## Ruleset to enable in Wave C From 16738798f00d0969e844e7dd8ad89896b79ff1c0 Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Mon, 17 Aug 2026 11:43:55 -0500 Subject: [PATCH 12/15] ci: dedupe PR runs, bump stale action majors, add Dependabot for actions Fix A: drop the codex/** push trigger. A codex/* branch with an open PR fired both push and pull_request events; the concurrency group keys on github.ref, which differs between the two, so it never deduplicated. gh pr checks 4 showed twelve entries for six jobs, with dependency-review reporting fail and skipping simultaneously. pull_request now covers CI on every push to this branch since PR #4 is open. Fix B: bump actions/checkout v4->v7, actions/setup-node v4->v7, actions/setup-dotnet v4->v6, actions/upload-artifact v4->v7, and actions/dependency-review-action v4->v5 (anchore/sbom-action stays at v0, already current). All were several majors behind and the old majors declare Node 20, which GitHub has deprecated. Verified each bumped action's action.yml at the new tag still accepts every input this workflow passes, unchanged in meaning: setup-dotnet's global-json-file, setup-node's node-version, upload-artifact's name/path/if-no-files-found, checkout's fetch-depth, and dependency-review-action's zero inputs. Fix C: add .github/dependabot.yml for the github-actions ecosystem only, so pinned actions rotting silently (as just happened) gets caught going forward instead of discovered incidentally in an unrelated log. Created the missing "dependencies" label (gh label create) rather than leave the file referencing one that doesn't exist. Fix D: amend the wave design spec's out-of-scope list, which forbade Dependabot version-update config, to mark that exclusion superseded rather than silently contradicting it by adding Fix C. Original rationale kept visible per AGENTS.md's record-superseded-don't-rewrite rule. Co-Authored-By: Claude Opus 5 Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/dependabot.yml | 10 ++++++++ .github/workflows/ci.yml | 24 +++++++++---------- .../specs/2026-08-16-stage0-wave-c-design.md | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..51bc99f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + commit-message: + prefix: "ci" + labels: + - dependencies diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6a50134..f2e6e24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: pull_request: branches: [main] push: - branches: [main, 'codex/**'] + branches: [main] workflow_dispatch: permissions: @@ -20,10 +20,10 @@ jobs: runs-on: windows-latest steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: global-json-file: global.json @@ -58,10 +58,10 @@ jobs: working-directory: src/OpenMemory.ObsidianPlugin steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up Node - uses: actions/setup-node@v4 + uses: actions/setup-node@v7 with: node-version: 24 @@ -79,7 +79,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Check repository-internal links run: bash tools/check-links.sh @@ -89,7 +89,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: fetch-depth: 0 @@ -109,20 +109,20 @@ jobs: if: github.event_name == 'pull_request' steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Review dependencies - uses: actions/dependency-review-action@v4 + uses: actions/dependency-review-action@v5 artifact: name: artifact runs-on: windows-latest steps: - name: Check out - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Set up .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v6 with: global-json-file: global.json @@ -161,7 +161,7 @@ jobs: upload-artifact: false - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: openmemory-dev path: artifacts/out/ diff --git a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md index 8bbac67..fd3f813 100644 --- a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md +++ b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md @@ -119,7 +119,7 @@ Each check runs before its change to observe it fail first, where that is possib - Any product behavior. No `.cs` or `.ts` file gains logic. - A TypeScript formatter (C-6, Stage 6). -- Dependabot version-update configuration. `dependency-review-action` covers pull-request-time review, which is what Stage 0 asks for; scheduled dependency bumps are a separate operational decision. +- ~~Dependabot version-update configuration. `dependency-review-action` covers pull-request-time review, which is what Stage 0 asks for; scheduled dependency bumps are a separate operational decision.~~ **Superseded:** this exclusion was revisited after this wave produced concrete evidence that pinned actions rot silently — five actions were found one to three majors behind, discovered only incidentally via a deprecation warning in an unrelated log. `.github/dependabot.yml` (github-actions ecosystem only) is now in scope to catch this going forward. - Code signing and attestation. `D-018` and `F-007` place these at Stage 8, with explicit approval required before any paid service. - The threat model and the four frozen security contracts — Wave D, `F-010`. - Release publishing. This wave uploads workflow artifacts; it does not create GitHub Releases or tags. From 75695f7594b947f0d152fc80ad6d86dc8b7d77aa Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Mon, 17 Aug 2026 11:48:03 -0500 Subject: [PATCH 13/15] Record that two secret-scanning settings are unavailable Non-provider patterns and validity checks are GitHub Secret Protection features. The advanced_security field is absent from this repository's security_and_analysis response, so the tier is not provisioned: the API accepts a PATCH with HTTP 200 and changes nothing, and the toggles do not render in settings. The detection gap is covered by C-2's gitleaks job rather than by a native setting, which makes that decision load-bearing rather than redundant. Also records the dependency graph as a fourth settings change the original list missed. Co-Authored-By: Claude Opus 5 Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .../specs/2026-08-16-stage0-wave-c-design.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md index fd3f813..d353746 100644 --- a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md +++ b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md @@ -46,8 +46,12 @@ Verified on 2026-08-16 against `main` at `23d4b20`, with Wave B merged. | Ref | Decision | Rationale | |---|---|---| | C-1 | CI produces a checksummed development artifact: publish `Service`, `Cli`, and `McpBridge`, zip, emit SHA-256, upload with the SBOM. | Stage 8's release depends on this path. Exercising it now, while the payload is three empty `Main` methods, is far cheaper than debugging it when the payload is a real signed installer. Nothing can leak, because nothing is implemented. | -| C-2 | Add a CI secret-scanning job **in addition to** the already-enabled native scanning, and enable non-provider patterns. | GitHub's native secret scanning is not a status check — branch protection cannot require it. Stage 0 asks for secret scanning *in CI*, and `D-091` needs a real check to gate on. Non-provider patterns matter because this project's threat model covers private keys and connection strings in captured transcripts, not only vendor-format tokens. | -| C-3 | Enable validity checks. | A secret leaked into a public repository is already exposed, so testing whether it is still live adds negligible risk and real remediation value: it distinguishes "rotate now" from "already dead". | +| C-2 | Add a CI secret-scanning job **in addition to** the already-enabled native scanning. | GitHub's native secret scanning is not a status check — branch protection cannot require it. Stage 0 asks for secret scanning *in CI*, and `D-091` needs a real check to gate on. **This decision proved load-bearing rather than redundant** — see the availability note below. | +| C-3 | ~~Enable validity checks.~~ **Superseded — not available on this repository.** | See the availability note below. | + +**Availability note, added after implementation.** C-2 originally also called for enabling non-provider patterns, and C-3 for enabling validity checks. Both proved impossible: the REST API accepts a `PATCH` setting them and returns HTTP 200 while silently changing nothing, and the corresponding toggles do not render in the repository's settings UI. The cause is that `advanced_security` is absent from the repository's `security_and_analysis` response entirely — non-provider patterns and validity checks are GitHub **Secret Protection** features, and what is free on a public repository is basic secret scanning plus push protection, both of which are already enabled. The enhanced detection tier is not provisioned here. + +The detection gap this leaves is covered by C-2's CI job rather than by a native setting: gitleaks' default configuration carries rules for private keys and generic API keys, which is the class non-provider patterns would have caught. Had the design relied on the native setting alone, this would be an unmitigated hole. The original decisions are recorded above as superseded rather than deleted, so the reasoning and its correction both remain visible. | C-4 | .NET jobs run on `windows-latest`; jobs that do not touch the build run on `ubuntu-latest`. | `D-003` fixes Windows 11 x64 as the only supported platform and the Stage 0 gate requires a "clean Windows build". The repository is public, so Actions minutes are free and runner cost is not a factor. | | C-5 | The test job asserts a minimum discovered-test **count**, not merely a zero exit code. | `dotnet test` exits 0 both when a project has no tests and when test discovery is broken. Exit code alone cannot distinguish them. This was observed directly during Wave B and is the failure mode that would let a Stage 2 misconfiguration report green while nothing ran. | | C-6 | Formatting is enforced for C# only; no TypeScript formatter is added. | `dotnet format` needs no new dependency. Adding Prettier would introduce one for a single 7-line stub file. TypeScript formatting belongs to Stage 6, when the plugin is real. | @@ -86,13 +90,14 @@ A software bill of materials — the inventory of every dependency the build pul ### 4.4 Repository settings -Three changes outside the repository tree, each requiring explicit approval before being applied: +Changes outside the repository tree, each requiring explicit approval before being applied: -1. Enable **non-provider patterns** for secret scanning (C-2). -2. Enable **validity checks** (C-3). +1. ~~Enable **non-provider patterns** for secret scanning (C-2).~~ **Not available** — see the availability note in §3. +2. ~~Enable **validity checks** (C-3).~~ **Not available** — see the availability note in §3. 3. Enable **branch protection** on `main` with the ruleset already recorded in `.github/branch-protection.md`. +4. Enable the **dependency graph**, without which `actions/dependency-review-action` fails outright with "Dependency review is not supported on this repository". This was not anticipated when the spec was written and was discovered only when the job ran against a real pull request. Completed by the repository owner. -These are API calls, not file edits. They are listed here so the wave's full footprint is visible in one place rather than discovered during implementation. +These are settings changes, not file edits. They are listed here so the wave's full footprint is visible in one place rather than discovered during implementation — which items 1, 2, and 4 demonstrate was the right instinct and an incomplete list. ### 4.5 `.github/branch-protection.md` From dc512f07f74ff822cf339e024b6103f9c326455f Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Mon, 17 Aug 2026 12:05:53 -0500 Subject: [PATCH 14/15] Fix Codex review findings on Wave C: plan/spec drift, SBOM pin, artifact assertion, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies the controller's adjudicated corrections from the independent Codex review (task-6-report.md): 1. Plan now agrees with the spec: non-provider patterns and validity checks are marked superseded/unavailable in Task 5 and the completion criteria, instead of instructing executors to enable them. Original text preserved as superseded per AGENTS.md. 2. Pin anchore/sbom-action's Syft version explicitly (syft-version: v1.51.0) so the SBOM generator can't drift silently under a pinned action wrapper. 3. The artifact job's "Package and checksum" step now asserts all three publish directories exist and are non-empty before Compress-Archive, throwing (non-zero exit) if not — closing the gap where a partial publish could still be zipped, checksummed, and uploaded green. 4. branch-protection.md's dependency-review description no longer claims licence checking; the action has no allow-licenses/ deny-licenses inputs configured, and choosing a licence policy is a product decision that hasn't been made. 5. Corrects the backwards "silently un-enforces" claim in branch-protection.md, the Wave C spec, and the Wave C plan: GitHub fails closed on a required check that never reports (the PR blocks indefinitely), unlike a job that reports skipped (treated as passing). The job-rename-must-update-both-files rule is unchanged. 6. Moves the spec's misplaced "Availability note" paragraph to after the complete decisions table in §3, instead of splitting the table in two. Co-Authored-By: Claude Opus 5 Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- .github/branch-protection.md | 16 +++++++++++++--- .github/workflows/ci.yml | 18 ++++++++++++++++++ .../plans/2026-08-16-stage0-wave-c.md | 17 ++++++++++++----- .../specs/2026-08-16-stage0-wave-c-design.md | 10 +++++----- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.github/branch-protection.md b/.github/branch-protection.md index d411485..942c12c 100644 --- a/.github/branch-protection.md +++ b/.github/branch-protection.md @@ -37,8 +37,13 @@ Applied to `main`: These are the job names from [`ci.yml`](workflows/ci.yml). Branch protection matches required checks by name, so **renaming a job here or in the workflow -silently un-enforces it** — a required check that never reports simply never -blocks. Any job rename must update both files together. +without updating the other blocks merging instead of un-enforcing anything** +— GitHub fails closed: a required context that never reports leaves the pull +request waiting on it indefinitely, blocking the merge rather than silently +letting it through. This differs from a job that reports as *skipped*, which +GitHub treats as passing; a renamed job's old context reports nothing at +all, so the two behave oppositely. Any job rename must update both files +together to avoid a permanently blocked pull request. | Check | What it verifies | |---|---| @@ -46,5 +51,10 @@ blocks. Any job rename must update both files together. | `plugin` | The Obsidian plugin installs from the committed lockfile and type-checks | | `docs` | Every repository-internal Markdown link resolves | | `secret-scan` | gitleaks finds no secret in the full history | -| `dependency-review` | No vulnerable or incompatible-licence dependency is introduced | +| `dependency-review` | No known-vulnerable dependency is introduced by the pull request | | `artifact` | Publish, checksum, and SBOM generation succeed | + +Licence policy is not configured for `dependency-review`: the action runs +without `allow-licenses`/`deny-licenses` inputs, so it checks vulnerabilities +only. Configuring licence enforcement requires first deciding which licences +are acceptable — a product decision that has not been made. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2e6e24..5305da4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,6 +139,23 @@ jobs: - name: Package and checksum shell: pwsh run: | + $expectedDirs = @( + "artifacts/publish/OpenMemory.Service", + "artifacts/publish/OpenMemory.Cli", + "artifacts/publish/OpenMemory.McpBridge" + ) + $problems = @() + foreach ($dir in $expectedDirs) { + if (-not (Test-Path $dir)) { + $problems += "missing: $dir" + } elseif (-not (Get-ChildItem -Path $dir -Recurse -File -ErrorAction SilentlyContinue)) { + $problems += "empty: $dir" + } + } + if ($problems.Count -gt 0) { + throw "Publish output incomplete, refusing to package:`n$($problems -join "`n")" + } + New-Item -ItemType Directory -Force -Path artifacts/out | Out-Null Compress-Archive -Path artifacts/publish/* -DestinationPath artifacts/out/openmemory-dev.zip $hash = Get-FileHash -Algorithm SHA256 -Path artifacts/out/openmemory-dev.zip @@ -159,6 +176,7 @@ jobs: format: cyclonedx-json output-file: artifacts/out/openmemory-sbom.cyclonedx.json upload-artifact: false + syft-version: "v1.51.0" - name: Upload artifact uses: actions/upload-artifact@v7 diff --git a/docs/superpowers/plans/2026-08-16-stage0-wave-c.md b/docs/superpowers/plans/2026-08-16-stage0-wave-c.md index aae941c..059be54 100644 --- a/docs/superpowers/plans/2026-08-16-stage0-wave-c.md +++ b/docs/superpowers/plans/2026-08-16-stage0-wave-c.md @@ -426,8 +426,13 @@ Change the status blockquote at the top from "not yet enabled" to enabled, dated These are the job names from [`ci.yml`](workflows/ci.yml). Branch protection matches required checks by name, so **renaming a job here or in the workflow -silently un-enforces it** — a required check that never reports simply never -blocks. Any job rename must update both files together. +without updating the other blocks merging instead of un-enforcing anything** +— GitHub fails closed: a required context that never reports leaves the pull +request waiting on it indefinitely, blocking the merge rather than silently +letting it through. This differs from a job that reports as *skipped*, which +GitHub treats as passing; a renamed job's old context reports nothing at +all, so the two behave oppositely. Any job rename must update both files +together to avoid a permanently blocked pull request. | Check | What it verifies | |---|---| @@ -457,7 +462,8 @@ git add .github/branch-protection.md git commit -s -m "Record the required checks from the CI workflow Names the six job names branch protection will require, and states -that renaming a job silently un-enforces it unless both files change." +that renaming a job without updating both files blocks merging +(GitHub fails closed) rather than silently un-enforcing the check." ``` --- @@ -481,9 +487,10 @@ gh api repos/June74/openmemory/branches/main/protection 2>&1 | head -3 Expected: non-provider patterns and validity checks `disabled`; protection returns 404 "Branch not protected". -- [ ] **Step 2: Enable the two secret-scanning settings** +- [ ] ~~**Step 2: Enable the two secret-scanning settings**~~ **Superseded — not available on this repository.** Non-provider patterns and validity checks are GitHub Secret Protection features; `advanced_security` is absent from this repository's `security_and_analysis` response. The PATCH below returns HTTP 200 and changes nothing, and the toggles do not render in settings. See the availability note in the design spec §3. The detection gap is covered by the `secret-scan` job instead. ```bash +# Historical: originally intended to run, left here as the superseded record. gh api -X PATCH repos/June74/openmemory \ -f 'security_and_analysis[secret_scanning_non_provider_patterns][status]=enabled' \ -f 'security_and_analysis[secret_scanning_validity_checks][status]=enabled' @@ -595,7 +602,7 @@ If any step failed unexpectedly, create a setback record in `docs/operations/set 3. The downloaded artifact's SHA-256 verifies against the archive. 4. `.github/branch-protection.md` names the six real job names. 5. Branch protection active on `main` with those six contexts, `strict: true`, `enforce_admins: true`, force-pushes and deletions disabled. -6. Non-provider patterns and validity checks enabled. +6. ~~Non-provider patterns and validity checks enabled.~~ **Superseded:** confirmed not available on this repository (not a provisioned Secret Protection tier); the detection gap is covered by the `secret-scan` job instead. 7. Every commit DCO-signed; no secret-shaped content. 8. Codex independent review findings resolved with evidence. 9. No product behavior added beyond whitespace-only formatting changes, if any. diff --git a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md index d353746..0b4d7b4 100644 --- a/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md +++ b/docs/superpowers/specs/2026-08-16-stage0-wave-c-design.md @@ -48,15 +48,15 @@ Verified on 2026-08-16 against `main` at `23d4b20`, with Wave B merged. | C-1 | CI produces a checksummed development artifact: publish `Service`, `Cli`, and `McpBridge`, zip, emit SHA-256, upload with the SBOM. | Stage 8's release depends on this path. Exercising it now, while the payload is three empty `Main` methods, is far cheaper than debugging it when the payload is a real signed installer. Nothing can leak, because nothing is implemented. | | C-2 | Add a CI secret-scanning job **in addition to** the already-enabled native scanning. | GitHub's native secret scanning is not a status check — branch protection cannot require it. Stage 0 asks for secret scanning *in CI*, and `D-091` needs a real check to gate on. **This decision proved load-bearing rather than redundant** — see the availability note below. | | C-3 | ~~Enable validity checks.~~ **Superseded — not available on this repository.** | See the availability note below. | - -**Availability note, added after implementation.** C-2 originally also called for enabling non-provider patterns, and C-3 for enabling validity checks. Both proved impossible: the REST API accepts a `PATCH` setting them and returns HTTP 200 while silently changing nothing, and the corresponding toggles do not render in the repository's settings UI. The cause is that `advanced_security` is absent from the repository's `security_and_analysis` response entirely — non-provider patterns and validity checks are GitHub **Secret Protection** features, and what is free on a public repository is basic secret scanning plus push protection, both of which are already enabled. The enhanced detection tier is not provisioned here. - -The detection gap this leaves is covered by C-2's CI job rather than by a native setting: gitleaks' default configuration carries rules for private keys and generic API keys, which is the class non-provider patterns would have caught. Had the design relied on the native setting alone, this would be an unmitigated hole. The original decisions are recorded above as superseded rather than deleted, so the reasoning and its correction both remain visible. | C-4 | .NET jobs run on `windows-latest`; jobs that do not touch the build run on `ubuntu-latest`. | `D-003` fixes Windows 11 x64 as the only supported platform and the Stage 0 gate requires a "clean Windows build". The repository is public, so Actions minutes are free and runner cost is not a factor. | | C-5 | The test job asserts a minimum discovered-test **count**, not merely a zero exit code. | `dotnet test` exits 0 both when a project has no tests and when test discovery is broken. Exit code alone cannot distinguish them. This was observed directly during Wave B and is the failure mode that would let a Stage 2 misconfiguration report green while nothing ran. | | C-6 | Formatting is enforced for C# only; no TypeScript formatter is added. | `dotnet format` needs no new dependency. Adding Prettier would introduce one for a single 7-line stub file. TypeScript formatting belongs to Stage 6, when the plugin is real. | | C-7 | One SBOM tool covering both ecosystems rather than one per ecosystem. | The repository has a .NET and an npm dependency tree. A single generator produces one reconciled document instead of two that must be merged. | +**Availability note, added after implementation.** C-2 originally also called for enabling non-provider patterns, and C-3 for enabling validity checks. Both proved impossible: the REST API accepts a `PATCH` setting them and returns HTTP 200 while silently changing nothing, and the corresponding toggles do not render in the repository's settings UI. The cause is that `advanced_security` is absent from the repository's `security_and_analysis` response entirely — non-provider patterns and validity checks are GitHub **Secret Protection** features, and what is free on a public repository is basic secret scanning plus push protection, both of which are already enabled. The enhanced detection tier is not provisioned here. + +The detection gap this leaves is covered by C-2's CI job rather than by a native setting: gitleaks' default configuration carries rules for private keys and generic API keys, which is the class non-provider patterns would have caught. Had the design relied on the native setting alone, this would be an unmitigated hole. The original decisions are recorded above as superseded rather than deleted, so the reasoning and its correction both remain visible. + ## 4. Deliverables ### 4.1 `.github/workflows/ci.yml` @@ -134,7 +134,7 @@ Each check runs before its change to observe it fail first, where that is possib | Risk | Mitigation | |---|---| | Enabling branch protection with administrator enforcement means a broken CI blocks merging the fix for that CI. | This is the intended tradeoff — a rule the maintainer can silently bypass is not a control. Recorded here so the consequence is accepted deliberately rather than discovered under pressure. Recovery is to temporarily disable enforcement, which is visible in the audit log. | -| Required checks are named as strings; renaming a job later silently un-enforces it, because a required check that never reports simply never blocks. | `.github/branch-protection.md` records the exact names, and §4.5 makes updating it part of any job rename. | +| Required checks are named as strings; renaming a job later without updating branch protection blocks every future merge, because GitHub fails closed — a required context that never reports leaves the pull request waiting on it indefinitely rather than silently un-enforcing it. (A *skipped* job still reports a conclusion and counts as passing; a *renamed* job's old context reports nothing at all, so the two behave oppositely.) | `.github/branch-protection.md` records the exact names, and §4.5 makes updating it part of any job rename. | | A third-party action is a supply-chain dependency in the security-critical path. | Actions are pinned and the set is kept minimal. This is a genuine residual risk, not one this wave eliminates, and belongs in Wave D's threat model. | | `dotnet format --verify-no-changes` may fail on Wave B's existing files, which were never format-checked. | Expected. If it fails, the correct response is one formatting commit, not relaxing the check. | | The artifact is downloadable from a public repository. | The binaries are inert stubs with empty entry points. This is the last stage at which that is true, which is part of why the path is being exercised now. | From 691867518923b1b8137375b4623855e193cca1ea Mon Sep 17 00:00:00 2001 From: Injun Lee <2006ijlee@gmail.com> Date: Mon, 17 Aug 2026 12:12:46 -0500 Subject: [PATCH 15/15] Correct three residual claims the first fix wave missed A fourth occurrence of the fail-open claim survived at line 37, phrased as "silently stops being enforced" rather than "un-enforces", which is why the verification grep passed. GitHub fails closed: an unreported required context blocks every merge rather than silently passing. Also corrects two licence-enforcement overstatements: the plan's required-check template, which would have reintroduced the very wording an earlier fix removed from branch-protection.md, and the troubleshooting table. Co-Authored-By: Claude Opus 5 Signed-off-by: Injun Lee <2006ijlee@gmail.com> --- docs/superpowers/plans/2026-08-16-stage0-wave-c.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/superpowers/plans/2026-08-16-stage0-wave-c.md b/docs/superpowers/plans/2026-08-16-stage0-wave-c.md index 059be54..e156881 100644 --- a/docs/superpowers/plans/2026-08-16-stage0-wave-c.md +++ b/docs/superpowers/plans/2026-08-16-stage0-wave-c.md @@ -34,7 +34,7 @@ Every task's requirements implicitly include this section. **Interfaces:** - Consumes: `OpenMemory.sln`, `global.json`, `tools/check-links.sh`, and `src/OpenMemory.ObsidianPlugin/package.json` — all from `main`. -- Produces: job names `build-and-test`, `plugin`, and `docs`, which Task 4 records as required checks and Task 5 enforces. **These exact strings matter** — branch protection matches required checks by name, and a renamed job silently stops being enforced. +- Produces: job names `build-and-test`, `plugin`, and `docs`, which Task 4 records as required checks and Task 5 enforces. **These exact strings matter** — branch protection matches required checks by name. Renaming a job without updating the required-check list does not silently un-enforce it; GitHub fails closed, so the old context never reports and every pull request waits on it indefinitely, blocking merges. A *skipped* job still reports a conclusion and counts as passing, whereas a *renamed* job's old context reports nothing at all, so the two behave oppositely. - [ ] **Step 1: Verify no workflow exists** @@ -366,7 +366,7 @@ Fix the cause and push again. Expected failures, and the correct response to eac | `dotnet format --verify-no-changes` fails | Wave B's files were never format-checked. Run `dotnet format OpenMemory.sln` locally, commit the result as one formatting commit. **Do not** remove or weaken the format step. | | gitleaks reports a finding | Investigate it. If a real secret exists, stop and report immediately — do not commit over it. If it is a false positive, add a narrowly-scoped `.gitleaks.toml` allowlist entry with a comment explaining why, never a blanket ignore. | | The test-count assertion fails | The regex does not match this runner's output format. Report the actual `--list-tests` output. **Do not lower `$expected` or loosen the regex** — that would restore the vacuous pass the assertion exists to prevent. | -| `dependency-review` fails | Read what it flagged. A vulnerable or incompatible-licence dependency is a real finding, not a check to disable. | +| `dependency-review` fails | Read what it flagged. A known-vulnerable dependency is a real finding, not a check to disable. Note the action runs without `allow-licenses`/`deny-licenses`, so it does not enforce licence policy. | | An action version does not exist | Bump to the current major tag and record which version you used. | Push after each fix and re-watch. Repeat until every job is green. @@ -440,7 +440,7 @@ together to avoid a permanently blocked pull request. | `plugin` | The Obsidian plugin installs from the committed lockfile and type-checks | | `docs` | Every repository-internal Markdown link resolves | | `secret-scan` | gitleaks finds no secret in the full history | -| `dependency-review` | No vulnerable or incompatible-licence dependency is introduced | +| `dependency-review` | No known-vulnerable dependency is introduced by the pull request | | `artifact` | Publish, checksum, and SBOM generation succeed | ```