Skip to content

feat: implement scc through validate — scaffolding, artifacts, and eight validators - #1

Merged
protonspy merged 1 commit into
mainfrom
feat/scc-v0.3.0
Jul 30, 2026
Merged

protonspy merged 1 commit into
mainfrom
feat/scc-v0.3.0

Conversation

@protonspy

@protonspy protonspy commented Jul 30, 2026 •

Copy link
Copy Markdown
Owner

Lands the spec-driven development binary described in design/plan.md phases 1–10. scc is a single stdlib-only Go binary with one surface — a headless CLI — so an agent or a CI job drives it exactly as well as a human does.

What works

Command What it does
init Scaffolds .claude/rules/, the review agents, and the layout. Idempotent; never overwrites your edits.
spec new|list|show|delete|validate The three-artifact vehicle (requirements/design/tasks) for work whose what and how need settling first.
plan new|list|delete|validate One file, for everything else.
skill validate Conformance to the published Agent Skills specification.
validate Every applicable validator, one exit code, one JSON document.

The eight validators cover EARS grammar across all five patterns, requirement numbering, one methodology annotation per task, traceability in both directions, plan one-source-of-truth, skill conformance, the wiki link/orphan graph, ADR numbering and superseding, glossary vocabulary drift, dependencies missing from docs/stack.md, and codewiki citations that no longer resolve.

Source code is deliberately not checked — scc never parses it, so it cannot tell you the code honors what the artifact says. That stays the orchestrator's accountability; a checker that was confidently incomplete would be worse than none.

Decisions worth reviewing

  • The marker is the file .claude/scc-manifest.json, never the .claude/ directory. ~/.claude is Claude Code's own global config directory and exists on every machine that runs it — an upward walk accepting the directory would resolve the root to $HOME for any command run outside a workspace, and every command would then read and write the user's global config. .claude/ also exists in every repo that merely uses Claude Code. workspace.Find therefore stats a regular file.
  • Exit codes are the contract: 0 ok, 1 usage/runtime error, 2 findings. A finding is a legitimate answer to a lint question, not a failure of the tool, so 2 is never collapsed into 1.
  • One file, no config file. The manifest is it. scc runs no tests and no linters, so it has nothing to configure — a project's test and lint commands are a rule under .claude/rules/, which is Markdown the orchestrator already reads.
  • Workspace templates are data-free; artifact templates take data. That split is what keeps a future three-way merge possible while letting the user own the rendered artifact.
  • Hostile input: any positional name that becomes a path segment passes workspace.SafeName before filepath.Join — without it scc spec delete .. --force resolves to the workspace root.

Not in scope, deliberately

scc update (phase 11) is absent. A three-way merge is untestable until a second template version exists, so until then the upgrade story is re-running init, which is additive and never overwrites a file you edited.

Verification

make check is green — gofmt -l clean, go vet, go test -race across all packages. Coverage where it matters: mdscan 97.9%, finding 95.2%, ears 87.0%, manifest 87.3%, validate 85.0%, cli 79.8%.

TestFreshArtifactsPassTheirOwnValidators in internal/cli is the gate that matters most here: a validator that fires on scc's own scaffolded output would be the worst bug in the product, since one wrong finding teaches the user to disbelieve all eight.

golangci-lint and govulncheck were not run locally (not installed) — CI runs both, along with the suite on Linux/macOS/Windows.

🤖 Generated with Claude Code

https://claude.ai/code/session_013XH8WYThtTSGJxS7tXRvfp

Summary by CodeRabbit

  • New Features

    • Added the scc command-line tool with help, version, JSON output, and stable exit codes.
    • Added workspace initialization with safe, repeatable scaffolding and optional force replacement.
    • Added commands for creating, listing, viewing, validating, and deleting specs and plans.
    • Added comprehensive validation for requirements, tasks, skills, documentation, architecture records, glossaries, and technology stack references.
    • Added npm distribution with platform-specific native binaries for supported systems.
  • Documentation

    • Added usage, installation, workflow, development, and release documentation.
  • Chores

    • Added automated cross-platform testing, linting, security checks, and versioned release packaging.

…ght validators

Lands the spec-driven development binary described in design/plan.md phases 1
through 10: a single stdlib-only Go binary with one surface, a headless CLI.

- init      scaffolds .claude/rules/, the review agents, and the layout, recording
            a content hash per managed file in .claude/scc-manifest.json — which
            doubles as the workspace marker, so the upward walk stats a regular
            file and never resolves to ~/.claude
- spec/plan create the two artifact vehicles; artifact templates take data and the
            user owns the result, workspace templates stay data-free so a
            three-way merge remains possible
- validate  the eight validators over EARS grammar, requirement numbering and
            traceability, plan one-source-of-truth, skill conformance, and the
            wiki/ADR/glossary/stack/codewiki knowledge base

Exit codes are the contract: 0 ok, 1 usage or runtime error, 2 findings — so CI
and agents can branch on a lint result without conflating it with a failure of
the tool. Every command emits --json to stdout with diagnostics on stderr.

scc update (phase 11) is deliberately absent: a three-way merge is untestable
until a second template version exists, so the upgrade path is re-running init,
which is additive and never overwrites an edited file.

Ships with the CI gate (gofmt, vet, test -race on Linux/macOS/Windows,
golangci-lint, govulncheck), the manual release workflow, and the npm packages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013XH8WYThtTSGJxS7tXRvfp
@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds the scc Go CLI, workspace scaffolding and manifest management, Markdown/EARS parsing, artifact and knowledge-base validators, npm packaging, local automation, and GitHub Actions CI and release workflows.

Changes

SCC platform

Layer / File(s) Summary
Repository foundation and workspace scaffolding
.gitattributes, go.mod, internal/assets/*, internal/manifest/*, internal/scaffold/*, internal/workspace/*, internal/paths/*, internal/render/*
Defines normalized text handling, embedded templates, workspace layout, manifest persistence, atomic writes, scaffolding behavior, and terminal rendering.
CLI command surface
cmd/scc/*, internal/cli/*
Adds dispatch, help/version output, init, spec, plan, skill, validation commands, shared flags, JSON output, and exit-code handling.
Markdown and requirement parsing
internal/mdscan/*, internal/ears/*, internal/finding/*, internal/textutil/*
Adds frontmatter parsing, fence/comment-aware Markdown scanning, EARS requirement parsing, deterministic findings, and newline/BOM normalization.
Artifact and knowledge validators
internal/validate/*
Adds validation for specs, plans, skills, wiki content, ADRs, glossary terms, documented dependencies, codewiki citations, and aggregate results.
Packaging and delivery automation
.github/workflows/*, Makefile, npm/*, .gitignore, README.md, CLAUDE.md, design/*
Adds local build/release targets, cross-platform npm package assembly, CI and manual release workflows, repository guidance, and design documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant SCCCLI
  participant Workspace
  participant Validators
  User->>SCCCLI: Run scc validate
  SCCCLI->>Workspace: Resolve root and check manifest
  SCCCLI->>Validators: Run selected validators
  Validators-->>SCCCLI: Return findings
  SCCCLI-->>User: Print report and exit code
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the PR’s main scope: implementing scc, its scaffolding/artifacts, and validation pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/scc-v0.3.0

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (4)
npm/scripts/build-packages.mjs (1)

135-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sort generated optional dependencies before serialization.

Serialize a lexically sorted map so generated package metadata remains stable independently of TARGETS declaration order.

Proposed fix
-rootPkg.optionalDependencies = optionalDependencies;
+rootPkg.optionalDependencies = Object.fromEntries(
+  Object.entries(optionalDependencies).sort(([a], [b]) =>
+    a < b ? -1 : a > b ? 1 : 0
+  )
+);

As per coding guidelines, **/*.{yml,mjs,go} requires sorting before serialization.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@npm/scripts/build-packages.mjs` around lines 135 - 140, Update the package
metadata generation around rootPkg.optionalDependencies in the build script to
assign a lexically sorted dependency map before JSON serialization. Preserve the
existing dependency values while ensuring output order is independent of TARGETS
declaration order and satisfies the sorting guideline.

Source: Coding guidelines

internal/validate/spec_test.go (1)

239-244: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test checks less than its comment claims.

R1.3 (REMOVED) carries no requirement text, so it is precisely the input that would regress into ears.malformed, but only the orphan rule is asserted against.

♻️ Proposed tightening
-	for _, unwanted := range []string{"spec.orphan-requirement"} {
+	for _, unwanted := range []string{"spec.orphan-requirement", "ears.malformed"} {
 		if contains(got, unwanted) {
 			t.Errorf("rules = %v, want no %s: a REMOVED requirement needs no task", got, unwanted)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/validate/spec_test.go` around lines 239 - 244, Strengthen the
assertions in the REMOVED requirement test around the unwanted rules list so it
verifies that R1.3 does not produce either spec.orphan-requirement or
ears.malformed. Keep the existing no-task expectation and ensure the test input
still specifically exercises the REMOVED requirement with no requirement text.
internal/mdscan/mdscan_test.go (1)

151-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning the unterminated-fence behavior.

An unclosed fence makes every remaining line invisible to all eight validators, which is a silent whole-file pass. Worth one test so the behavior is deliberate rather than incidental.

♻️ Suggested test
// An unclosed fence hides the rest of the document from every validator.
func TestUnterminatedFenceSwallowsTheRest(t *testing.T) {
	doc := parse(t, "```\n- [ ] inside\n\n# not seen\n")
	if len(doc.Checkboxes) != 0 || len(doc.Headings) != 0 {
		t.Errorf("content after an unclosed fence was scanned: %+v %+v", doc.Checkboxes, doc.Headings)
	}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/mdscan/mdscan_test.go` around lines 151 - 172, Extend the fence
behavior tests near TestTildeFences with a case for an unclosed backtick fence,
such as TestUnterminatedFenceSwallowsTheRest. Parse content containing a fence,
checkbox, and heading without a closing fence, then assert that both Checkboxes
and Headings remain empty, preserving the intended behavior that the
unterminated fence hides the remainder of the document.
internal/validate/skill.go (1)

160-174: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

.. in the middle of a target still escapes the skill directory.

isLocalReference only rejects a .. prefix, so refs/../../other/x.md reaches filepath.Join(dir, …) and the existence probe lands outside the skill — a finding (or a silent pass) about a file that is not the skill's. Check the cleaned path instead.

♻️ Suggested tightening
 		target = strings.SplitN(target, "#", 2)[0]
 		if target == "" {
 			continue
 		}
-		if strings.Count(strings.Trim(path.Clean(target), "/"), "/") > 1 {
+		clean := path.Clean(target)
+		if clean == ".." || strings.HasPrefix(clean, "../") {
+			continue // escapes the skill; not scc's to judge
+		}
+		if strings.Count(strings.Trim(clean, "/"), "/") > 1 {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/validate/skill.go` around lines 160 - 174, Update the
local-reference validation around isLocalReference and the target path handling
to clean the target before resolving it, then reject any cleaned path that
escapes the skill directory via parent traversal. Use the cleaned target
consistently for the shallow-reference check and filepath.Join existence probe,
while preserving fragment removal and empty-target handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 31: Update both actions/checkout steps in the CI workflow, including the
step pinned to v5.0.1 and the checkout step near line 72, to set
persist-credentials to false. Keep the existing checkout references and other
configuration unchanged.

In @.github/workflows/release.yml:
- Around line 36-39: Replace the shell glob validation in the release workflow
at .github/workflows/release.yml lines 36-39 with an anchored numeric regex that
accepts only the exact vX.Y.Z format; update the corresponding release-version
check in Makefile lines 64-66 to use the identical rule so CI and local release
targets behave consistently.
- Around line 112-122: Update the release archive and checksum steps around the
tar/zip commands and checksum generation to produce deterministic artifacts.
Derive and export SOURCE_DATE_EPOCH from the target commit, normalize archive
file timestamps and metadata, use zip metadata-free mode, and explicitly sort
checksum input/output. Normalize generated checksum content to LF before
serialization while preserving the existing archive naming and packaging
behavior.
- Around line 152-155: Update the “Upgrade npm” workflow step to install the
exact npm version 11.5.1 instead of the floating npm@11 major-line reference,
preserving the existing release toolchain setup.

In `@CLAUDE.md`:
- Around line 47-59: Update the architecture diagram in CLAUDE.md by changing
its fenced code block language identifier to text, preserving the diagram
content unchanged.

In `@internal/assets/templates/claude/rules/delivery.md`:
- Around line 11-13: Add the bash language specifier to the fenced code block
containing the git worktree add command, changing the opening fence to ```bash
while leaving the command and closing fence unchanged.

In `@internal/assets/templates/claude/rules/knowledge-base.md`:
- Around line 7-15: Label the directory-tree fenced code block in the
knowledge-base documentation with the text language identifier by changing the
opening fence to ```text, while preserving the tree contents and closing fence.

In `@internal/cli/cli.go`:
- Around line 111-114: Update the CLI help text near the shared-flag description
to scope --root to workspace commands and state that only commands supporting
--json emit machine-readable output on stdout. Ensure JSON-capable commands bind
the flag through addJSON and produce output via emitJSON, without implying these
flags apply to version or top-level help.

In `@internal/cli/validate_all.go`:
- Around line 43-50: Update the jsonOut branch in the validation command to pass
set.Document() directly to emitJSON, removing the anonymous wrapper and its
validators field. Preserve per-validator counts only in the human-readable
output and keep the frozen {findings, count} schema consistent with the other
validation commands.

In `@internal/render/render.go`:
- Line 14: Update the color-selection logic in the rendering functions,
including Warn and Err, to evaluate TTY support for the specific output stream
being written rather than the global useColor derived from os.Stdout. Ensure
stderr output disables ANSI color when redirected while preserving
stream-appropriate color for terminal destinations.

In `@internal/validate/codewiki.go`:
- Around line 91-125: Validate the cleaned citation path before resolving or
reading it in checkCitation: reject targets whose cleaned path is absolute or
begins with “..”, report them as unresolved, and return. Only allow paths
contained under root to proceed to isFile and os.ReadFile.

In `@internal/validate/skill.go`:
- Around line 98-101: Update the length checks in the validation logic for both
description and compatibility to count Unicode characters rather than UTF-8
bytes. Import and use unicode/utf8’s rune-counting helper in the checks that
emit skill.description-too-long and skill.compatibility-too-long, while leaving
the existing ASCII-only name validation unchanged.

In `@internal/validate/stack.go`:
- Around line 181-182: Update the majorVersionRe pattern used by stack
validation to recognize every valid Go major-version suffix from v2 onward,
including multi-digit versions such as v10 and v11, while continuing to reject
v0 and v1.

In `@internal/validate/wiki.go`:
- Around line 65-73: Normalize wikilink targets before slug resolution by
trimming whitespace and removing any “#anchor” suffix, so links such as
order-total#totals resolve against the page slug while preserving the original
target in diagnostics. Add a shared normalization helper and use it in both the
wikilink validation loop and checkChangelog, ensuring both paths apply identical
matching behavior.
- Around line 117-120: The changelog read failure path in the validation flow
silently returns without reporting the unreadable file or performing desync
checks. Update the logic around read(root, path) to distinguish a nil or errored
document from a successful read, emit the existing wiki.frontmatter-unreadable
finding for the changelog, and preserve normal validation for successfully read
documents.

In `@internal/workspace/workspace.go`:
- Around line 140-158: The no-clobber paths in SafeWrite and WriteFile must
prevent concurrent creators from replacing a file after the existence check.
Replace their stat-then-AtomicWrite behavior with an atomic create-if-absent
operation while preserving the existing return and refusal-error semantics, and
add a regression test that exercises concurrent creation attempts.
- Around line 54-75: Update the contract for AtomicWrite to avoid promising
atomic replacement across platforms, particularly on Windows where os.Rename may
expose intermediate states. Either document the guarantee as Unix-specific or
implement Windows-specific replacement handling while preserving the existing
temporary-file workflow.

In `@npm/scripts/build-packages.mjs`:
- Around line 21-28: Replace the cpSync import and its uses in the
build-packages script with fs.copyFileSync for these simple file copies,
preserving the existing copy destinations and behavior while keeping
compatibility with the declared Node >=16 engine.
- Around line 95-105: Replace the external "unzip" invocation in the Windows
branch of the package extraction flow with a Node-managed extractor or an
existing bundled repository helper, preserving extraction of the archive at
artifactsDir/base into binDir. Keep the non-Windows tar extraction and chmodSync
behavior unchanged.

In `@README.md`:
- Around line 67-70: Update the managed-artifact list in the README to include
plans/ alongside .claude/, specs/, and docs/. Keep the existing description
unchanged and ensure it reflects that scc plan creates and validates artifacts
there.

---

Nitpick comments:
In `@internal/mdscan/mdscan_test.go`:
- Around line 151-172: Extend the fence behavior tests near TestTildeFences with
a case for an unclosed backtick fence, such as
TestUnterminatedFenceSwallowsTheRest. Parse content containing a fence,
checkbox, and heading without a closing fence, then assert that both Checkboxes
and Headings remain empty, preserving the intended behavior that the
unterminated fence hides the remainder of the document.

In `@internal/validate/skill.go`:
- Around line 160-174: Update the local-reference validation around
isLocalReference and the target path handling to clean the target before
resolving it, then reject any cleaned path that escapes the skill directory via
parent traversal. Use the cleaned target consistently for the shallow-reference
check and filepath.Join existence probe, while preserving fragment removal and
empty-target handling.

In `@internal/validate/spec_test.go`:
- Around line 239-244: Strengthen the assertions in the REMOVED requirement test
around the unwanted rules list so it verifies that R1.3 does not produce either
spec.orphan-requirement or ears.malformed. Keep the existing no-task expectation
and ensure the test input still specifically exercises the REMOVED requirement
with no requirement text.

In `@npm/scripts/build-packages.mjs`:
- Around line 135-140: Update the package metadata generation around
rootPkg.optionalDependencies in the build script to assign a lexically sorted
dependency map before JSON serialization. Preserve the existing dependency
values while ensuring output order is independent of TARGETS declaration order
and satisfies the sorting guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a69e7f7e-8c97-4c31-9ebd-7b918631f973

📥 Commits

Reviewing files that changed from the base of the PR and between 421435a and b094c31.

📒 Files selected for processing (84)
  • .gitattributes
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .gitignore
  • .golangci.yml
  • CLAUDE.md
  • Makefile
  • README.md
  • cmd/scc/main.go
  • design/orchestration.md
  • design/plan.md
  • design/research.md
  • go.mod
  • internal/assets/assets.go
  • internal/assets/assets_test.go
  • internal/assets/templates/CLAUDE.md
  • internal/assets/templates/artifacts/design.md
  • internal/assets/templates/artifacts/plan.md
  • internal/assets/templates/artifacts/requirements.md
  • internal/assets/templates/artifacts/tasks.md
  • internal/assets/templates/claude/agents/code-review.md
  • internal/assets/templates/claude/agents/security-review.md
  • internal/assets/templates/claude/rules/autonomy.md
  • internal/assets/templates/claude/rules/delivery.md
  • internal/assets/templates/claude/rules/knowledge-base.md
  • internal/assets/templates/claude/rules/methodology.md
  • internal/assets/templates/claude/rules/project.md
  • internal/assets/templates/claude/rules/routing.md
  • internal/assets/templates/claude/rules/specs.md
  • internal/assets/templates/claude/rules/tasks.md
  • internal/assets/templates/claude/rules/verification.md
  • internal/cli/cli.go
  • internal/cli/cli_test.go
  • internal/cli/flags.go
  • internal/cli/init.go
  • internal/cli/init_test.go
  • internal/cli/jsonout.go
  • internal/cli/plan.go
  • internal/cli/plan_test.go
  • internal/cli/skill.go
  • internal/cli/spec.go
  • internal/cli/spec_test.go
  • internal/cli/validate.go
  • internal/cli/validate_all.go
  • internal/cli/validate_test.go
  • internal/ears/ears.go
  • internal/ears/ears_test.go
  • internal/finding/finding.go
  • internal/finding/finding_test.go
  • internal/manifest/manifest.go
  • internal/manifest/manifest_test.go
  • internal/mdscan/frontmatter.go
  • internal/mdscan/frontmatter_test.go
  • internal/mdscan/mdscan.go
  • internal/mdscan/mdscan_test.go
  • internal/paths/paths.go
  • internal/render/render.go
  • internal/scaffold/scaffold.go
  • internal/scaffold/scaffold_test.go
  • internal/textutil/textutil.go
  • internal/textutil/textutil_test.go
  • internal/validate/adr.go
  • internal/validate/all.go
  • internal/validate/codewiki.go
  • internal/validate/glossary.go
  • internal/validate/knowledge_test.go
  • internal/validate/plan.go
  • internal/validate/plan_test.go
  • internal/validate/skill.go
  • internal/validate/skill_test.go
  • internal/validate/spec.go
  • internal/validate/spec_test.go
  • internal/validate/stack.go
  • internal/validate/tasks.go
  • internal/validate/validate.go
  • internal/validate/wiki.go
  • internal/workspace/workspace.go
  • internal/workspace/workspace_test.go
  • npm/.gitignore
  • npm/README.md
  • npm/scc/README.md
  • npm/scc/bin/scc.js
  • npm/scc/package.json
  • npm/scripts/build-packages.mjs

Comment thread .github/workflows/ci.yml
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

wc -l .github/workflows/ci.yml
cat -n .github/workflows/ci.yml | sed -n '1,220p'

Repository: protonspy/spec-claude-code

Length of output: 4730


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

p = Path('.github/workflows/ci.yml')
text = p.read_text()
print("checkout occurrences:", text.count('actions/checkout'))
for m in re.finditer(r'actions/checkout@[^\s#]+(?:\s*#\s*([^\n]+))?', text):
    start = text.rfind('\n', 0, m.start()) + 1
    end = text.find('\n', m.start())
    print(text[start:end])
PY

Repository: protonspy/spec-claude-code

Length of output: 350


🌐 Web query:

actions/checkout v5 persist-credentials default token .git/config

💡 Result:

In actions/checkout v5, the persist-credentials input defaults to true [1][2]. When persist-credentials is set to true (the default), actions/checkout configures the GitHub Actions runner to authenticate Git commands by writing credential information to a file [3][4]. Crucially, in v5.0.1 and later versions of the v5 release, this behavior was updated: rather than writing credentials directly into the local .git/config file, the action stores them in a separate configuration file located within the $RUNNER_TEMP directory [4]. The main .git/config is then updated with an include.path directive to reference this temporary file [4]. This change was implemented to improve security by isolating the credentials from the repository's main configuration and preventing accidental leakage [4]. The credentials are removed during the post-job cleanup process [3]. If you wish to prevent this behavior, you can explicitly set persist-credentials: false in your workflow step [3].

Citations:


Disable persisted checkout credentials on both checkout steps. The default checkout config leaves a repo-scoped credential available to later steps; set persist-credentials: false here and at line 72 so PR code can’t reuse it.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 31-31: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 31, Update both actions/checkout steps in
the CI workflow, including the step pinned to v5.0.1 and the checkout step near
line 72, to set persist-credentials to false. Keep the existing checkout
references and other configuration unchanged.

Source: Linters/SAST tools

Comment on lines +36 to +39
case "$VERSION" in
v[0-9]*.[0-9]*.[0-9]*) echo "releasing $VERSION" ;;
*) echo "::error::version must look like vX.Y.Z (got '$VERSION')"; exit 1 ;;
esac

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one strict release-version rule. Both checks accept malformed values such as v1.2.3-extra; npm packaging may then fail only after CI/build work has completed. Require the documented exact vX.Y.Z format with an anchored numeric regex.

  • .github/workflows/release.yml#L36-L39: replace the shell glob with an anchored numeric validation.
  • Makefile#L64-L66: enforce the same anchored numeric validation for local release targets.
📍 Affects 2 files
  • .github/workflows/release.yml#L36-L39 (this comment)
  • Makefile#L64-L66
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 36 - 39, Replace the shell glob
validation in the release workflow at .github/workflows/release.yml lines 36-39
with an anchored numeric regex that accepts only the exact vX.Y.Z format; update
the corresponding release-version check in Makefile lines 64-66 to use the
identical rule so CI and local release targets behave consistently.

Comment on lines +112 to +122
mkdir -p dist
go build -trimpath \
-ldflags "-s -w -X github.com/protonspy/spec-claude-code/internal/cli.version=${VERSION}" \
-o "dist/${bin}" ./cmd/scc
name="scc_${VERSION}_${GOOS}_${GOARCH}"
cd dist
if [ "$GOOS" = "windows" ]; then
zip -q "${name}.zip" "${bin}"
else
tar -czf "${name}.tar.gz" "${bin}"
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Make release archives and checksums reproducible.

The tar/zip metadata captures build-time timestamps, so rebuilding the same commit/version changes release assets and checksums.txt. Derive SOURCE_DATE_EPOCH from the target commit, normalize file/archive metadata, use zip -X, and explicitly sort checksum output.

As per coding guidelines, “Maintain deterministic generated artifacts by normalizing newlines to LF and sorting before serialization.”

Also applies to: 212-213

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 112 - 122, Update the release
archive and checksum steps around the tar/zip commands and checksum generation
to produce deterministic artifacts. Derive and export SOURCE_DATE_EPOCH from the
target commit, normalize archive file timestamps and metadata, use zip
metadata-free mode, and explicitly sort checksum input/output. Normalize
generated checksum content to LF before serialization while preserving the
existing archive naming and packaging behavior.

Source: Coding guidelines

Comment on lines +152 to +155
# Trusted publishing requires npm >= 11.5.1 (newer than what ships with
# Node). Pin the major line so the release toolchain is reproducible.
- name: Upgrade npm
run: npm install -g npm@11

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
npm view npm@11.5.1 version engines dist.integrity --json

Repository: protonspy/spec-claude-code

Length of output: 676


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

sed -n '140,170p' .github/workflows/release.yml

Repository: protonspy/spec-claude-code

Length of output: 1347


Pin npm to 11.5.1. npm@11 will keep moving to newer 11.x patches, which breaks release reproducibility and can change the publishing toolchain without review.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 155-155: ad-hoc installation of packages (adhoc-packages): installs a package outside of a lockfile

(adhoc-packages)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 152 - 155, Update the “Upgrade
npm” workflow step to install the exact npm version 11.5.1 instead of the
floating npm@11 major-line reference, preserving the existing release toolchain
setup.

Source: Linters/SAST tools

Comment thread CLAUDE.md
Comment on lines +47 to +59
```
cmd/scc/main.go os.Exit(cli.Run(os.Args[1:]))
|
internal/cli the whole command surface
|
scaffold · validate write / check
| \
assets · manifest ears · mdscan templates, hashes, grammars
|
paths · workspace · render · textutil · finding
|
plain files on disk: .claude/ · specs/ · plans/ · docs/ · CLAUDE.md
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language to the architecture fence.

Use ````text` for this diagram so Markdown linting does not report MD040.

🧰 Tools
🪛 markdownlint-cli2 (0.23.1)

[warning] 47-47: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CLAUDE.md` around lines 47 - 59, Update the architecture diagram in CLAUDE.md
by changing its fenced code block language identifier to text, preserving the
diagram content unchanged.

Source: Linters/SAST tools

Comment on lines +54 to +75
func AtomicWrite(path string, data []byte, perm os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(path), "."+filepath.Base(path)+".tmp-*")
if err != nil {
return err
}
tmpName := tmp.Name()
defer func() { _ = os.Remove(tmpName) }() // no-op once the rename succeeds
if _, err := tmp.Write(data); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Chmod(perm); err != nil {
_ = tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmpName, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workspace.go ==\n'
sed -n '1,140p' internal/workspace/workspace.go

printf '\n== AtomicWrite usages ==\n'
rg -n "AtomicWrite\\(" -S .

printf '\n== workspace tests ==\n'
fd -a 'workspace*test.go' internal/workspace

Repository: protonspy/spec-claude-code

Length of output: 7746


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Go os.Rename docs excerpt ==\n'
go doc -all os.Rename | sed -n '1,120p'

Repository: protonspy/spec-claude-code

Length of output: 708


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== workspace package outline ==\n'
ast-grep outline internal/workspace/workspace.go --view expanded

printf '\n== adjacent files ==\n'
fd -a . internal/workspace

Repository: protonspy/spec-claude-code

Length of output: 1022


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' internal/workspace/workspace.go

Repository: protonspy/spec-claude-code

Length of output: 5125


🌐 Web query:

Go os.Rename non-Unix platforms atomicity docs

💡 Result:

The Go standard library documentation for the os.Rename function explicitly states that it is not an atomic operation on non-Unix platforms, even when the operation occurs within the same directory [1][2][3][4]. On Unix-like systems, os.Rename generally provides atomic behavior, meaning that if the destination path already exists, it is replaced atomically, and other processes will always see either the old file or the new file [5][6]. However, because the underlying operating system behavior for renaming files varies significantly, the Go standard library does not guarantee atomic semantics across all platforms [6]. For developers who require atomic file replacement (e.g., to ensure that applications never see a partially written or empty file), the standard library's os.Rename is insufficient on non-Unix platforms [7]. Third-party solutions, such as the google/renameio package, are often used to handle the complexities of atomic file operations, though they may explicitly note that reliable atomic file writing is not possible on Windows due to platform limitations [7].

Citations:


AtomicWrite overstates its cross-platform guarantee. os.Rename is not atomic on non-Unix platforms, so this helper shouldn’t promise that concurrent readers never observe an intermediate state on Windows; either narrow the contract or add Windows-specific replace handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/workspace/workspace.go` around lines 54 - 75, Update the contract
for AtomicWrite to avoid promising atomic replacement across platforms,
particularly on Windows where os.Rename may expose intermediate states. Either
document the guarantee as Unix-specific or implement Windows-specific
replacement handling while preserving the existing temporary-file workflow.

Source: Coding guidelines

Comment on lines +140 to +158
func SafeWrite(path, content string) (bool, error) {
if _, err := os.Stat(path); err == nil {
return false, nil
}
if err := AtomicWrite(path, []byte(content), 0o644); err != nil {
return false, err
}
return true, nil
}

// WriteFile writes content unconditionally unless the path exists and overwrite
// is false.
func WriteFile(path, content string, overwrite bool) error {
if !overwrite {
if _, err := os.Stat(path); err == nil {
return fmt.Errorf("refusing to overwrite existing file: %s (pass --force)", path)
}
}
return AtomicWrite(path, []byte(content), 0o644)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files internal/workspace
printf '\n--- workspace.go (outline) ---\n'
ast-grep outline internal/workspace/workspace.go --view expanded || true

printf '\n--- workspace.go relevant lines ---\n'
sed -n '1,240p' internal/workspace/workspace.go | cat -n

printf '\n--- tests mentioning SafeWrite/WriteFile/AtomicWrite ---\n'
rg -n "SafeWrite|WriteFile|AtomicWrite|overwrite|no-clobber|concurrent|race" internal/workspace -g '*.go' -A 3 -B 3 || true

Repository: protonspy/spec-claude-code

Length of output: 15553


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "O_EXCL|CreateTemp|Create\(|SafeWrite|WriteFile|concurrent|race" internal/workspace -g '*.go' -A 2 -B 2 || true

Repository: protonspy/spec-claude-code

Length of output: 4029


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "\bSafeWrite\b|\bWriteFile\b" -g '*.go' .
printf '\n--- workspace_test.go excerpt ---\n'
sed -n '170,230p' internal/workspace/workspace_test.go | cat -n

Repository: protonspy/spec-claude-code

Length of output: 6108


Make the no-clobber path atomic. SafeWrite and WriteFile(..., false) still do a Stat/Rename sequence, so a file created after the existence check can be replaced. Use an atomic create-if-absent path for the no-force branch and add a concurrent-creator regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/workspace/workspace.go` around lines 140 - 158, The no-clobber paths
in SafeWrite and WriteFile must prevent concurrent creators from replacing a
file after the existence check. Replace their stat-then-AtomicWrite behavior
with an atomic create-if-absent operation while preserving the existing return
and refusal-error semantics, and add a regression test that exercises concurrent
creation attempts.

Source: Coding guidelines

Comment on lines +21 to +28
chmodSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

node -e 'const fs = require("node:fs"); if (typeof fs.cpSync !== "function") process.exit(1)'
rg -n -C2 'cpSync|copyFileSync|"node": ">=16"' npm

Repository: protonspy/spec-claude-code

Length of output: 1108


🏁 Script executed:

#!/bin/bash
set -eu

sed -n '120,140p' npm/scripts/build-packages.mjs

Repository: protonspy/spec-claude-code

Length of output: 1038


🌐 Web query:

Node.js fs.cpSync introduced version 16.7.0 14.18.0 official docs

💡 Result:

The fs.cpSync method was introduced in Node.js version 16.7.0 [1][2][3]. It provides a synchronous way to copy the entire directory structure from a source path to a destination path, including files and subdirectories [2][4]. Originally introduced as an experimental feature, the method was later marked as stable in Node.js 22.3.0 [5][1][2].

Citations:


Avoid fs.cpSync with the current Node floor.

cpSync requires Node 16.7.0, but npm/scc/package.json still allows >=16, so Node 16.0–16.6 will fail to load npm/scripts/build-packages.mjs. These are simple file copies, so copyFileSync fits here unless the engine requirement is raised.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@npm/scripts/build-packages.mjs` around lines 21 - 28, Replace the cpSync
import and its uses in the build-packages script with fs.copyFileSync for these
simple file copies, preserving the existing copy destinations and behavior while
keeping compatibility with the declared Node >=16 engine.

Comment on lines +95 to +105
const base = `${BIN}_${tag}_${t.goos}_${t.goarch}`;
if (t.goos === "windows") {
execFileSync("unzip", ["-o", join(artifactsDir, `${base}.zip`), "-d", binDir], {
stdio: "inherit",
});
} else {
execFileSync("tar", ["-xzf", join(artifactsDir, `${base}.tar.gz`), "-C", binDir], {
stdio: "inherit",
});
chmodSync(join(binDir, binName), 0o755);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

rg -n -C2 'build-packages|npm-build|execFileSync\("unzip"|windows' \
  npm Makefile .github/workflows

Repository: protonspy/spec-claude-code

Length of output: 8745


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n## npm/scripts/build-packages.mjs\n'
cat -n npm/scripts/build-packages.mjs | sed -n '1,180p'

printf '\n## npm/README.md\n'
cat -n npm/README.md | sed -n '1,120p'

printf '\n## .github/workflows/release.yml (relevant excerpt)\n'
cat -n .github/workflows/release.yml | sed -n '145,190p'

Repository: protonspy/spec-claude-code

Length of output: 10826


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n## package files mentioning unzip/zip\n'
rg -n 'unzip|zip' package.json npm package-lock.json .github/workflows Makefile

Repository: protonspy/spec-claude-code

Length of output: 921


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n## npm/scripts/build-packages.mjs\n'
cat -n npm/scripts/build-packages.mjs | sed -n '1,180p'

printf '\n## npm/README.md\n'
cat -n npm/README.md | sed -n '1,120p'

printf '\n## .github/workflows/release.yml (relevant excerpt)\n'
cat -n .github/workflows/release.yml | sed -n '145,190p'

printf '\n## package files mentioning unzip/zip\n'
rg -n 'unzip|zip' package.json npm package-lock.json .github/workflows Makefile

Repository: protonspy/spec-claude-code

Length of output: 11583


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n## workflow references to npm-build / build-packages\n'
rg -n -C2 'npm-build|build-packages\.mjs|make dist|make npm-build' .github/workflows Makefile npm

printf '\n## release workflow header and job runners\n'
cat -n .github/workflows/release.yml | sed -n '1,140p'

printf '\n## ci workflow header and job runners\n'
cat -n .github/workflows/ci.yml | sed -n '1,140p'

Repository: protonspy/spec-claude-code

Length of output: 16359


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n## npm workspace files\n'
git ls-files npm | sed -n '1,200p'

printf '\n## package manifests\n'
for f in $(git ls-files 'npm/**/package.json'); do
  echo
  echo "### $f"
  cat -n "$f"
done

printf '\n## archive-related dependencies in npm manifests\n'
rg -n 'yauzl|adm-zip|unzip|tar|extract|zip' npm/**/package.json

Repository: protonspy/spec-claude-code

Length of output: 1781


Avoid the undeclared unzip dependency on Windows npm/scripts/build-packages.mjs:95-100 will fail on Windows hosts that don’t already have unzip installed, and the repo doesn’t bundle or declare one. Use a Node-managed extractor or another bundled helper for the Windows archive path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@npm/scripts/build-packages.mjs` around lines 95 - 105, Replace the external
"unzip" invocation in the Windows branch of the package extraction flow with a
Node-managed extractor or an existing bundled repository helper, preserving
extraction of the archive at artifactsDir/base into binDir. Keep the non-Windows
tar extraction and chmodSync behavior unchanged.

Source: Coding guidelines

Comment thread README.md
Comment on lines +67 to +70
The artifacts it governs are plain Markdown and JSON in your repo — `.claude/`,
`specs/`, `docs/`. No server, no database, no conversion layer: the files *are* the
API, and they are the ones Claude Code already reads. `scc` adds no directory of its
own and no config file — it owns a single manifest inside `.claude/`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Include plans/ in the managed-artifact list.

This list omits plans/, although scc plan creates and validates plans. Add it so the storage contract matches the command surface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 67 - 70, Update the managed-artifact list in the
README to include plans/ alongside .claude/, specs/, and docs/. Keep the
existing description unchanged and ensure it reflects that scc plan creates and
validates artifacts there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant