Skip to content

feat(credstore): scaffold module + credential-ref grammar (INT-429)#2

Merged
rianjs merged 4 commits into
mainfrom
feat/INT-429-scaffold-ref-handling
May 16, 2026
Merged

feat(credstore): scaffold module + credential-ref grammar (INT-429)#2
rianjs merged 4 commits into
mainfrom
feat/INT-429-scaffold-ref-handling

Conversation

@rianjs
Copy link
Copy Markdown
Contributor

@rianjs rianjs commented May 16, 2026

Closes #1 · Jira INT-429 (child of epic INT-310).

First discrete unit of the cli-common shared credential library, per the Open CLI Collective Secret-Handling Standard working-with-secrets.md §2.1 (ref grammar §1.3).

Scaffold

  • Single Go module github.com/open-cli-collective/cli-common, go 1.24, stdlib-only (no go.sum).
  • CI matrix build-test (ubuntu/macos/windows): tidy-drift check (git diff --exit-code -- go.mod go.sum), go build -v, go test -v -race -coverprofile; lint job via golangci-lint-action@v7 v2.0.2.
  • .golangci.yml mirroring atlassian-cli/shared; Makefile check: tidy lint test gate.
  • Branch protection on main: 4 required checks, strict, 1 review, linear history, squash-only, delete-on-merge. No signed-commit rule (none configured; sibling does not enforce either).

Ref handling (§1.3 / §2.1)

  • ParseRef / FormatRef / DefaultRef / EscapeRefSegment over <service>/<profile>; charset [A-Za-z0-9_-].
  • Typed *RefError (Kind/Segment/Ref) with errors.Is sentinels ErrRefEmpty / ErrRefSegmentCount / ErrRefInvalidChar. Refs are non-secret (§1.2) so messages name the ref; no leak.
  • EscapeRefSegment: deterministic, reversible-by-construction encoding into the charset; Unescape intentionally deferred.

Tests

Table-driven ParseRef/FormatRef/DefaultRef/EscapeRefSegment, round-trip property, FuzzParseRef (no-panic + segment validity + round-trip invariant). 48 cases, -race, local 5s fuzz clean.

Out of scope (later INT-310 units)

Backends, Store/Open, bundle ops + SetBundle atomicity, Linux fail-closed classification, redaction & migration helpers, v0.1.0 tag, nrq/slck Go-1.24 bump.

Architect review

Codex (session reused for this work stream) converged at blockers=0 majors=0 minors=0 nits=0 after two revision rounds (ParseRef empty-vs-segment-count ordering, CI lint checkout/setup-go + go.sum-absent handling, DefaultRef codification, EscapeRefSegment empty-case).

Stand up the cli-common Go module (go 1.24, stdlib-only) with CI matrix
(ubuntu/macos/windows build-test + lint), golangci-lint config mirroring
the atlassian-cli sibling, and a Makefile check gate.

Implement the credential-ref grammar per the Secret-Handling Standard
1.3: ParseRef / FormatRef / EscapeRefSegment over the <service>/<profile>
form, the 2.1 default-ref codification (DefaultProfile / DefaultRef), and
the typed *RefError with errors.Is sentinels. Covered by table-driven,
round-trip, and fuzz tests.

Closes #1
[INT-429]
@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 16, 2026

Blocker

  • .github/workflows/ci.yml:27 — The required Windows leg is currently failing, so the scaffold gate is not yet mergeable/verifiable. In CI run 25961946053, PowerShell/Go argument parsing turns the test command into a bad package load:

    FAIL    .out [setup failed]
    no required module provides package .out
    

    Quote the coverprofile flag or use the space form, e.g. go test -v -race "-coverprofile=coverage.out" ./..., then rerun the required checks.

Major

None.

Minor

  • .github/workflows/ci.yml:20 and Makefile:8git diff --exit-code -- go.mod go.sum tolerates absent go.sum, but it also will not catch a newly generated untracked go.sum once later tickets add dependencies. That is harmless for this stdlib-only unit, but weak as repo scaffold. Add a git status --porcelain -- go.mod go.sum check or equivalent before the backend work lands.

Nit

None.

Code Quality Lens: the implementation stays inside INT-429: no OS-touching code, no Store/Open creep, no speculative Unescape, and the ref grammar matches §1.3. Tests cover the intended parsing/error split and escaping behavior. The only blocking issue is that the cross-platform CI policy is not actually green yet.

@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 16, 2026

TDD Coverage Assessment

Verdict: Coverage is strong. No blocking gaps. Two minor points worth noting.

What's well-covered

  • ParseRef: all three error kinds, both empty-segment variants, multi-slash and zero-slash counts, invalid char cases (space, dot, @, multi-byte UTF-8), and three valid-input flavors.
  • FormatRef: all error kinds, slash-in-segment (correctly hits ErrRefInvalidChar since / is not in charset), round-trip test, a single valid-output check.
  • DefaultRef: happy path, invalid-char service, empty service — adequate for a thin wrapper over FormatRef.
  • EscapeRefSegment: empty passthrough, identity, underscore doubling, e-mail, space, slash, multi-byte unicode → charset property checked via regex, integration with FormatRef.
  • *RefError.Error(): message non-emptiness checked; errors.Is sentinel matching exercised throughout.
  • Fuzz: FuzzParseRef covers the no-panic + round-trip contract under arbitrary input. Seeds include the important boundary cases.

Minor

FormatRef validation order not explicitly pinned (credstore/ref.go:148-161).
FormatRef validates service first (empty → invalid-char), then profile (empty → invalid-char). The tests don't include a case where both segments are bad (e.g. FormatRef("", "") or FormatRef("bad.", "")). If the order were accidentally swapped the existing tests would still pass. A two-bad-segment case would lock in the defined precedence and make future refactors safer.

RefError.Error() default branch is unreachable but untested.
ref.go:67-68 (default: "credstore: invalid credential ref %q") can never be reached through the exported API — the zero value of RefErrorKind is RefErrorEmpty. It's dead code; the pragmatic fix is to either delete it or add a //nolint:exhaustive note rather than write a test for it.


Nit

TestRefErrorMessageHasNoSecretButNamesRef only checks that Error() is non-empty and that Segment == "profile". It could also assert the message contains the offending ref value (demonstrating the "names the ref, not the secret" property the test name promises), but this is cosmetic.


No missing coverage for the stated scope (ref grammar + escape). OS/keyring backends are correctly marked out of scope.

…ests

- CI: run all steps under bash (Git Bash on windows-latest) so PowerShell
  no longer mangles -coverprofile=coverage.out into a bad package path
  (Windows build-test leg was failing).
- Tidy gate (CI + Makefile): git status --porcelain so a future untracked
  go.sum is caught, not just modified tracked files.
- RefError.Error: drop the unreachable default branch; exhaustive switch
  plus a documented trailing fallback for out-of-range Kind.
- Tests: pin FormatRef empty-before-invalid and service-before-profile
  ordering; assert the error message actually names the ref.

[INT-429]
@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 16, 2026

Addressed review feedback in 55bbd03:

Codex

  • Blocker — Windows CI arg parsing: set defaults.run.shell: bash workflow-wide so PowerShell no longer splits -coverprofile=coverage.out. All three OS legs now run under bash.
  • Minor — tidy gate: replaced git diff --exit-code with a git status --porcelain -- go.mod go.sum check (CI + Makefile) so a newly-generated untracked go.sum is caught once later tickets add deps.

TDD assessment

  • Minor — FormatRef order untested: added cases pinning empty-before-invalid-char and service-before-profile.
  • Minor — unreachable default in Error(): removed the default branch (exhaustive switch over all 3 kinds) with a documented trailing fallback for an out-of-range Kind; no tested-impossible branch.
  • Nit — message-naming test: TestRefErrorMessageHasNoSecretButNamesRef now asserts the message contains the ref value, matching its name.

Local: build, go test -race, golangci-lint, and the tidy gate all clean.

windows-latest checks out CRLF (Git autocrlf) while go mod tidy and
gofmt emit LF, so the tidy gate saw go.mod as modified. Pin LF in the
working tree on all platforms.

[INT-429]
@rianjs
Copy link
Copy Markdown
Contributor Author

rianjs commented May 16, 2026

Blocker

None. Confirmed cleared: latest head 1139434da048a553d7cbf798fb42056b12481142, CI run 25962092822 is green across build-test (ubuntu-latest), build-test (macos-latest), build-test (windows-latest), and lint.

Major

None.

Minor

None.

Nit

None.

Code Quality Lens: the .gitattributes addition is justified by the cross-platform scaffold goal, not a drive-by edit. Workflow-wide bash is pragmatic and now verified on Windows. The tidy gate now catches both tracked drift and future untracked go.sum. The ref API remains tightly scoped to §1.3/§2.1, with no OS/backend creep and no speculative unescape path. Tests now pin the important validation-order semantics and the actionable error-message behavior.

Copy link
Copy Markdown

@monit-reviewer monit-reviewer left a comment

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: 1139434

Approved with 8 non-blocking suggestions below. Address at your discretion.

Summary

Reviewer Findings
harness-engineering:harness-architecture-reviewer 3
harness-engineering:harness-enforcement-reviewer 2
harness-engineering:harness-knowledge-reviewer 1
harness-engineering:harness-self-documenting-code-reviewer 2
harness-engineering:harness-architecture-reviewer (3 findings)

💡 Suggestion - credstore/ref.go:148

FormatRef leaves RefError.Ref as the zero value ("") for empty-segment and invalid-char errors, while ParseRef always populates Ref with the full input string. Error() then produces output like 'empty service segment in credential ref ""' where the trailing quoted empty string is redundant and misleading — FormatRef takes two separate args, not a single ref string. Callers using errors.As to inspect Ref for diagnostics get an empty string. Consider setting Ref to the offending segment or documenting the intentional semantic difference from ParseRef.

💡 Suggestion - credstore/ref.go:175

EscapeRefSegment pre-allocates cap=len(raw) bytes, but each non-passthrough byte expands to 2 bytes ('_' → '__') or 4 bytes (others → '_xNN'). Inputs with many non-ASCII or special characters (e.g. email-derived or path-derived identifiers) will always reallocate. For a shared library hot path, sizing at 2*len(raw) or using strings.Builder with a reasonable growth factor would avoid the predictable reallocation.

💡 Suggestion - .golangci.yml:17

The revive linter is enabled but restricted to only the 'unused-parameter' rule, disabling the full default rule set (exported-functions-have-comments, error-return, var-naming, receiver-naming, etc.). For a shared library consumed by multiple CLIs, these structural rules carry higher value than for application code. The stability rationale in the comment is valid, but the trade-off disproportionately reduces coverage of the public API surface.

harness-engineering:harness-enforcement-reviewer (2 findings)

💡 Suggestion - .github/workflows/ci.yml:43

actions/checkout@v4, actions/setup-go@v5, and golangci/golangci-lint-action@v7 are pinned to mutable major-version tags rather than immutable commit SHAs. A compromised or force-pushed tag can silently replace the action binary. Standard hardening pins to the full SHA (e.g. actions/checkout@11bd719) with the tag as a comment.

💡 Suggestion - .golangci.yml:18

The linters.exclusions.rules block follows a golangci-lint v2 config shape, but in golangci-lint v2.0.x the canonical location for per-path linter exclusions may be issues.exclude-rules (unchanged from v1). If linters.exclusions is not recognized, the errcheck exclusion on test files is silently dropped, causing spurious lint failures on unhandled error returns in *_test.go files. Verify against golangci-lint v2.0.2 docs.

harness-engineering:harness-knowledge-reviewer (1 findings)

💡 Suggestion - credstore/ref.go:120

ParseRef and FormatRef have asymmetric validation ordering: ParseRef checks all structural concerns (slash count, both-segment emptiness) before any charset check; FormatRef interleaves per-segment (service empty → service charset → profile empty → profile charset). Result: ParseRef("bad.svc/") returns ErrRefEmpty (profile) while FormatRef("bad.svc", "") returns ErrRefInvalidChar (service). The asymmetry is only partially covered by tests and is undocumented, which could confuse maintainers extending either function. A comment on FormatRef explaining the deliberate ordering would reduce the risk of a future reorder silently breaking the pinned test cases.

harness-engineering:harness-self-documenting-code-reviewer (2 findings)

💡 Suggestion - credstore/ref.go:163

EscapeRefSegment("") silently returns "", which is not a valid ref segment and will be rejected downstream by FormatRef/ParseRef with ErrRefEmpty. EscapeRefSegment is the only API entry point that accepts invalid input without returning an error, giving callers no signal at the escape site. Consider adding a test that chains EscapeRefSegment("") directly into FormatRef to make the failure mode explicit in the test suite, or add a doc note on the required follow-up validation.

💡 Suggestion - credstore/ref.go:75

RefError.Is compares only Kind, so errors.Is(err, ErrRefEmpty) matches any RefErrorEmpty regardless of which segment (service vs profile) was empty. Callers who need to distinguish sub-cases must use errors.As and inspect Segment, but there is no test demonstrating or documenting that pattern. For a shared library, a single test showing the errors.As/Segment path would prevent callers from incorrectly relying solely on errors.Is for disambiguation.

5 info-level observations excluded. Run with --verbose to include.

4 PR discussion threads considered.


Completed in 3m 59s | $0.92 | sonnet | daemon 0.2.116 | Glorfindel
Field Value
Model sonnet
Reviewers hybrid-synthesis, documentation:docs-reviewer, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 3m 59s wall · 11m 14s compute (Reviewers: 2m 10s · Synthesis: 1m 46s)
Cost $0.92
Tokens 112.2k in / 44.8k out
Turns 7

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 29.1k 5.9k 0 29.1k (1h) $0.21
documentation:docs-reviewer haiku 3.8k 9.8k 0 0 $0.05
harness-engineering:harness-architecture-reviewer sonnet 16.6k 6.3k 2.1k 14.5k (1h) $0.16
harness-engineering:harness-enforcement-reviewer sonnet 16.6k 7.2k 2.1k 14.5k (1h) $0.17
harness-engineering:harness-knowledge-reviewer sonnet 16.6k 5.3k 2.1k 14.5k (1h) $0.14
harness-engineering:harness-self-documenting-code-reviewer sonnet 14.6k 5.2k 2.1k 12.4k (1h) $0.13
security:security-code-auditor haiku 14.8k 5.0k 0 14.8k (1h) $0.05

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

Comment thread .github/workflows/ci.yml
- name: Test
run: go test -v -race -coverprofile=coverage.out ./...

lint:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-enforcement-reviewer): actions/checkout@v4, actions/setup-go@v5, and golangci/golangci-lint-action@v7 are pinned to mutable major-version tags rather than immutable commit SHAs. A compromised or force-pushed tag can silently replace the action binary. Standard hardening pins to the full SHA (e.g. actions/checkout@11bd719) with the tag as a comment.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged, deferring. Sibling Collective repos (e.g. atlassian-cli) pin actions to major-version tags by convention; diverging here alone is inconsistent. SHA-pinning is a worthwhile repo-wide hardening pass but out of scope for INT-429 (scaffold + ref grammar).

Comment thread .golangci.yml
settings:
revive:
# Explicit rule list so behavior is stable across golangci-lint versions.
rules:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-enforcement-reviewer): The linters.exclusions.rules block follows a golangci-lint v2 config shape, but in golangci-lint v2.0.x the canonical location for per-path linter exclusions may be issues.exclude-rules (unchanged from v1). If linters.exclusions is not recognized, the errcheck exclusion on test files is silently dropped, causing spurious lint failures on unhandled error returns in *_test.go files. Verify against golangci-lint v2.0.2 docs.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Verified — no change needed. This .golangci.yml is a verbatim copy of atlassian-cli/shared/.golangci.yml, which CI runs successfully on golangci-lint v2.0.2, and this PR's own lint job is green on v2.0.2 with the test-file errcheck exclusion in effect. The linters.exclusions shape is recognized in v2.

Comment thread credstore/ref.go
return service, profile, nil
}

// FormatRef is the inverse of ParseRef: it validates both segments and
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-architecture-reviewer): FormatRef leaves RefError.Ref as the zero value ("") for empty-segment and invalid-char errors, while ParseRef always populates Ref with the full input string. Error() then produces output like 'empty service segment in credential ref ""' where the trailing quoted empty string is redundant and misleading — FormatRef takes two separate args, not a single ref string. Callers using errors.As to inspect Ref for diagnostics get an empty string. Consider setting Ref to the offending segment or documenting the intentional semantic difference from ParseRef.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dfa1a1d. Error() no longer appends the misleading 'in credential ref ""' when Ref is empty (the FormatRef path); it now reads 'empty segment'. ParseRef always populates Ref so its messages are unchanged.

Comment thread credstore/ref.go
}
}
return true
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-knowledge-reviewer): ParseRef and FormatRef have asymmetric validation ordering: ParseRef checks all structural concerns (slash count, both-segment emptiness) before any charset check; FormatRef interleaves per-segment (service empty → service charset → profile empty → profile charset). Result: ParseRef("bad.svc/") returns ErrRefEmpty (profile) while FormatRef("bad.svc", "") returns ErrRefInvalidChar (service). The asymmetry is only partially covered by tests and is undocumented, which could confuse maintainers extending either function. A comment on FormatRef explaining the deliberate ordering would reduce the risk of a future reorder silently breaking the pinned test cases.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in dfa1a1d. Added a doc comment on FormatRef explaining the deliberate per-segment order (service before profile, empty before charset) and why it differs from ParseRef's structural-first order, with a note that the order is test-pinned.

Comment thread credstore/ref.go
}
if !validSegment(profile) {
return "", &RefError{Kind: RefErrorInvalidChar, Segment: "profile", Ref: profile}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-self-documenting-code-reviewer): EscapeRefSegment("") silently returns "", which is not a valid ref segment and will be rejected downstream by FormatRef/ParseRef with ErrRefEmpty. EscapeRefSegment is the only API entry point that accepts invalid input without returning an error, giving callers no signal at the escape site. Consider adding a test that chains EscapeRefSegment("") directly into FormatRef to make the failure mode explicit in the test suite, or add a doc note on the required follow-up validation.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in dfa1a1d. The godoc already documents EscapeRefSegment("")=="" and the deferred validation; added TestEscapeRefSegmentEmptyRejectedDownstream chaining EscapeRefSegment("") into FormatRef to make the ErrRefEmpty failure mode explicit in the suite.

Comment thread credstore/ref.go
return fmt.Sprintf("credstore: invalid credential ref %q", e.Ref)
}

// Is reports whether target is a *RefError of the same Kind, so callers
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-self-documenting-code-reviewer): RefError.Is compares only Kind, so errors.Is(err, ErrRefEmpty) matches any RefErrorEmpty regardless of which segment (service vs profile) was empty. Callers who need to distinguish sub-cases must use errors.As and inspect Segment, but there is no test demonstrating or documenting that pattern. For a shared library, a single test showing the errors.As/Segment path would prevent callers from incorrectly relying solely on errors.Is for disambiguation.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already covered. TestRefErrorMessageHasNoSecretButNamesRef uses errors.As(&RefError) and asserts re.Segment=="profile", demonstrating the errors.As/Segment disambiguation pattern for callers who need to distinguish sub-cases.

Comment thread credstore/ref.go

// EscapeRefSegment deterministically encodes an arbitrary string into the
// segment charset [A-Za-z0-9_-], for CLIs that derive a profile from a
// richer identifier such as an email address (standard §1.3).
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-architecture-reviewer): EscapeRefSegment pre-allocates cap=len(raw) bytes, but each non-passthrough byte expands to 2 bytes ('_' → '__') or 4 bytes (others → '_xNN'). Inputs with many non-ASCII or special characters (e.g. email-derived or path-derived identifiers) will always reallocate. For a shared library hot path, sizing at 2*len(raw) or using strings.Builder with a reasonable growth factor would avoid the predictable reallocation.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't change, by rationale. EscapeRefSegment runs once per profile derivation at setup/init time, not on any request hot path; a 2*len pre-size trades a deviation for unmeasurable gain. Keeping the change set surgical and traceable to INT-429.

Comment thread .golangci.yml
- exhaustive
settings:
revive:
# Explicit rule list so behavior is stable across golangci-lint versions.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Low (harness-engineering:harness-architecture-reviewer): The revive linter is enabled but restricted to only the 'unused-parameter' rule, disabling the full default rule set (exported-functions-have-comments, error-return, var-naming, receiver-naming, etc.). For a shared library consumed by multiple CLIs, these structural rules carry higher value than for application code. The stability rationale in the comment is valid, but the trade-off disproportionately reduces coverage of the public API surface.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Won't change here. The revive-restricted-to-unused-parameter config is an established cross-repo convention copied verbatim from atlassian-cli/shared (stability across golangci-lint versions). Expanding the revive rule set for shared libraries is a reasonable idea but is a separate cross-repo decision, not INT-429 scope.

Address PR review (all low-severity):
- Error(): drop misleading 'in credential ref ""' for FormatRef empty-
  segment errors where no full ref string exists.
- Document FormatRef's deliberate, test-pinned validation order and how
  it differs from ParseRef.
- Add a test making the EscapeRefSegment("") -> FormatRef rejection
  path explicit.

[INT-429]
@rianjs rianjs merged commit 78fa717 into main May 16, 2026
4 checks passed
@rianjs rianjs deleted the feat/INT-429-scaffold-ref-handling branch May 16, 2026 12:46
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.

Repo scaffold + ref handling (credstore §2.1, PR0–1)

2 participants