Skip to content

fix: pre-3.0.0 hardening of the single-scope install and per-repo config layer - #407

Merged
dean0x merged 59 commits into
mainfrom
feat/406-pre-3-0-0-hardening-local-scope-retireme
Sep 30, 2026
Merged

dean0x merged 59 commits into
mainfrom
feat/406-pre-3-0-0-hardening-local-scope-retireme

Conversation

@dean0x

@dean0x dean0x commented Sep 30, 2026 •

Copy link
Copy Markdown
Owner

Summary

Hardens the single-scope install and the per-repository settings layer (epic #387) before the 3.0.0 release: the confirmed bugs and audit leftovers in issue #406, the regression net ADR-031 required, and every doc, CHANGELOG, knowledge-base and ledger update that goes with them. Nothing is deferred.

Release: not included — 3.0.0 is dispatched separately after merge.

User decisions (2026-09-30)

  • D-A — compliance lens is a union (D-LENS-UNION). COMPLIANCE = machine ∪ the default branch's project.json (read from the local tracking copy refs/remotes/origin/<default>, never the network) ∪ the worktree's. A branch can add a framework and never remove one; an unreadable file never lowers the lens. The evidence floor is unchanged.
  • D-B — a git-tracked .devflow/config.json is ignored (D-PERSONAL-UNTRACKED). Checked locally with git ls-files --error-unmatch; a tracked copy reads as absent and the resolver warns to git rm --cached it. Closes the publication-ceiling bypass of a committed {"reviewPublication":"full"}. If git does not answer, the personal keys fail closed.
  • D-C — offline evidence reads the local origin/HEAD (D-OFFLINE-ORIGIN-HEAD). When gh and ls-remote both fail, git symbolic-ref --quiet refs/remotes/origin/HEAD names the default branch and its tracking copy is folded in. With no origin/HEAD, the worktree governs with WARN=remote-unavailable as before; an unanswered read raises to required.
  • D-D — docs approved into this PR, for review in the diff.

Changes

Settings and evidence resolvers

  • resolve-settings.cjs: D-A, D-B. An unreadable personal config.json fails only its own keys and keeps every readable layer's frameworks.
  • resolve-evidence-policy.cjs: D-C. SAFE_REF_RE and the origin/HEAD parser moved to the shared lib/project-config.cjs.

Hooks

  • D-HOOKS-TOPLEVEL-ONLY: the .git entry must be at the root, so a failed git rev-parse in a subdirectory no longer scaffolds .devflow/ there.
  • D-GITIGNORE-IN-BLOCK: v6 top-up lines are inserted inside the devflow block, never appended at end of file. The shell and TS twins stay byte-identical, and the block comment calls policy.json retired (v6 is unreleased, so the marker is not bumped).
  • Session start honours a personal tracker narrowing, and prints a one-line notice for a legacy repo-local install (D-LEGACY-LOCAL-NOTICE).
  • D-QUEUE-NO-ORPHAN-DELETE: Stop hooks run in parallel, so a user-only memory queue is kept for the next run instead of deleted. The comments that claimed array order sequenced them now state the parallel contract.
  • D-LOG-DIR-CAP (hook side): devflow_log_dir prunes to 200 folders, at most 50 per new folder. The debug trace creates its folder through the same path.

Prompts

  • D-CLAUDE-DIR-PROMPTS: /code-review's language gate and /dynamic-profile resolve CLAUDE_CONFIG_DIR the way the installer does.
  • D-DOCS-ROOT: every .devflow/docs write is rooted at the checkout toplevel, through _partials/_docs_root.mds.
  • The Git agent's tracker-mismatch remedy names the personal config.json key; git.md stays under its ceiling and the frozen status-lines golden is untouched.
  • docs-framework no longer sources a nonexistent docs-helpers.sh.

CLI

  • D-INIT-NOT-HOME: init writes no repository files when the git root is HOME.
  • D-CONFIG-STRICT-PARSE / D-CONFIG-NO-REPAIR: a malformed or duplicate-key config.json is never rewritten.
  • D-SETTINGS-ATOMIC: every settings.json write is atomic and preserves a symlink.
  • D-DEBUG-ENV-OBJECT: a non-object env is rejected.
  • D-INIT-DOWNGRADE-WARN: init warns before an older CLI downgrades the install.
  • D-LOG-DIR-CAP (init side): at most 200 log folders kept, at most 10k pruned per run.
  • Help text: --no-compliance removes only the rule; the --enable switches say a repository can opt out.
  • compliance --enable suggests adding keys to project.json, never replacing it, and the migration hint says to keep policy.json until every teammate runs 3.0.
  • compliance --status shows Default branch: and Effective here: lines. tracker|memory|learning|knowledge --status warn on a tracked config.json.
  • Residue removed: the dead uninstall scope guard, init's "Resolving paths" spinner, and tombstone comments.

Tests

  • Write-set fence around every CLI toggle (51 rows / 15 commands). Init location invariance across 7 locations. Both have ratchet floors.
  • New guards: claude-dir, docs-root, settings-atomic-write. no-config-read now also covers the ambient charter, hook directives and dist staleness.
  • Behavioural proofs replace the spy and source-count tests. A test covers uninstall --scope local outside a repository.
  • skill-references Format 3 now holds compiled commands to directory-neutral install paths; the language-gate-focuses floor replaces the removed install-path-refs floor.
  • The heredoc guard's frozen exclusions are re-pinned after the memory-worker comment shift.

Docs

  • CHANGELOG [Unreleased] (3.0.0): corrected migration notes (keep policy.json until everyone runs 3.0; removing a stale ~/.claude install; CLAUDE_CODE_DIR), a "how a team adopts project.json" guide, and entries for every change above.
  • README, CLAUDE.md, cli-reference, commands, file-organization (170 deny entries; the tracker files uninstall keeps), hooks, working-memory.
  • Six feature KBs refreshed. ADR-030 and ADR-031 amended through refresh-anchor; the ledger is local and gitignored, so it is not in this diff.

Breaking Changes

Part of 3.0.0, already breaking. The new break here: a .devflow/config.json committed to git is now ignored. Teams that shared a tracker or feature switch that way move the keys into .devflow/project.json and run git rm --cached .devflow/config.json.

Testing

  • npm run build: passes.
  • Full unit suite (excluding eager-memory-refresh, which spawns a real claude): 211 of 217 files passed. The 6 that failed (compliance-e2e, decisions/learning-curation, decisions/ledger-ops, queue-append, redact-secrets, shell-hooks) are on the known load-flaky list, and each passes in isolation.
  • Targeted run over the touched areas (install-snapshot, hook-log-paths, init-machine-switch-e2e, write-set-fence, init-location-invariance, tests/guards, tests/docs, skill-references, shell-hooks, capture-hooks, memory, cli-seam, feature-status-narrow): 937 passed, 4 skipped. The one failure, in shell-hooks, passes in isolation (255/255).
  • The README gitignore block is checked byte-for-byte against DEVFLOW_GITIGNORE_BLOCK.
  • Not run: eager-memory-refresh and the integration suite, both of which spawn a real claude.

Related Issues

Closes #406

Test Plan

  • TP-1 (AC-1) A committed personal config.json asking for full publication is ignored and the resolver warns to untrack it — method:ci [files: src/assets/scripts/resolve-settings.cjs, tests/**/settings-mode.test.ts]
  • TP-2 (AC-2) With CLAUDE_CONFIG_DIR set, the code-review language gate and dynamic-profile use the configured Claude directory, and a guard bans bare home Claude paths — method:ci [files: src/assets/commands/code-review.mds, src/assets/commands/dynamic-profile.mds, tests/guards/**]
  • TP-3 (AC-3) A malformed personal config.json keeps the team compliance frameworks in the lens, and the lens is the union of machine, default branch and worktree — method:ci [files: src/assets/scripts/resolve-settings.cjs]
  • TP-4 (AC-4) A personal tracker narrowing to github stops the session-start jira setup directive — method:ci [files: src/assets/scripts/hooks/session-start-context]
  • TP-5 (AC-5) Offline with origin HEAD recorded locally, a branch setting standard against a required default branch resolves required — method:ci [files: src/assets/scripts/resolve-evidence-policy.cjs]
  • TP-6 (AC-6) devflow init in a git repo rooted at HOME writes no config.json, claudeignore or gitignore block there and prints a notice — method:ci [files: src/cli/commands/init.ts]
  • TP-7 (AC-7) Every docs write site in compiled commands is rooted at the checkout toplevel, held by a guard test — method:ci [files: src/assets/commands/**]
  • TP-8 (AC-8) devflow init leaves a malformed or duplicate-key personal config.json byte-identical and warns — method:ci [files: src/core/feature-config.ts]
  • TP-9 (AC-9) Hooks never scaffold .devflow or a gitignore in a subdirectory when git rev-parse fails inside a checkout — method:ci [files: src/assets/scripts/hooks/**]
  • TP-10 (AC-10) The v6 gitignore upgrade inserts the project.json carve-out inside the devflow block, byte-identical between shell and TypeScript — method:ci [files: src/assets/scripts/hooks/ensure-root-gitignore, src/targets/claude-code/post-install.ts]
  • TP-11 (AC-11) The generated gitignore block and init log text no longer call policy.json the evidence policy — method:ci [files: src/targets/claude-code/post-install.ts]
  • TP-12 (AC-12) The Git agent mismatch remedy names the personal tracker key, git.md stays under its byte ceiling and the frozen status-lines fixture is unchanged — method:ci [files: src/assets/agents/git.mds]
  • TP-13 (AC-13) init help for no-compliance says only the rule is removed — method:ci [files: src/cli/commands/init.ts]
  • TP-14 (AC-14) Orphan cleanup in capture never drops a prompt row — method:ci
  • TP-15 (AC-15) devflow debug rejects a non-object env and every settings write is atomic — method:ci
  • TP-16 (AC-16) init warns when the installed version is newer than the running one — method:ci [files: src/cli/commands/init.ts]
  • TP-17 (AC-17) Log directories are capped at a bounded count and the existing backlog is pruned once — method:ci
  • TP-18 (AC-18) Session start prints a one-line notice when a legacy repo-local devflow install is present — method:ci [files: src/assets/scripts/hooks/session-start-context]
  • TP-19 (AC-19) Every CLI toggle writes only inside its allowlisted write set and init is location-invariant across six start locations — method:ci [files: tests/install-snapshot*.ts]
  • TP-20 (AC-20) uninstall with local scope refuses outside a git repository — method:ci [files: src/cli/commands/uninstall.ts]
  • TP-21 (AC-21) The policy.json never-opened and resolver git-only properties are proven behaviourally, without source-count or spy assertions — method:ci
  • TP-22 (AC-22) The no-config-read guard also scans the ambient charter and hook directive text and refuses a stale dist — method:ci [files: tests/guards/no-config-read.test.ts]
  • TP-23 (AC-23) uninstall directory cleanup has no unreachable scope guard — method:ci [files: src/cli/commands/uninstall.ts]
  • TP-24 (AC-24) init has no path-resolution spinner or catch for removed throws — method:ci [files: src/cli/commands/init.ts]
  • TP-25 (AC-25) claude-paths and manifest comments describe the end state with no history narration — method:manual [files: src/targets/claude-code/claude-paths.ts, src/core/manifest.ts]
  • TP-26 (AC-26) No test fixture carries the retired DEVFLOW_DIR instruction string — method:ci
  • TP-27 (AC-27) The docs-framework skill no longer sources a repo-local helper script — method:ci [files: src/assets/skills/docs-framework/**]
  • TP-28 (AC-28) CHANGELOG unreleased section carries corrected migration notes and a project.json adoption guide — method:manual [files: CHANGELOG.md]
  • TP-29 (AC-29) README states the keep-policy.json advice, offline caveat, fail-open cases, compliance default and the two project.json sources — method:manual [files: README.md]
  • TP-30 (AC-30) CLAUDE.md evidence, compliance and runtime-data sections match the shipped behaviour — method:manual [files: CLAUDE.md]
  • TP-31 (AC-31) file-organization reference lists the kept tracker files and the verified deny count — method:manual [files: docs/reference/file-organization.md]
  • TP-32 (AC-32) cli-reference and CLI help text match the resolver status variants and never suggest overwriting project.json — method:manual [files: docs/cli-reference.md, src/core/evidence-policy.ts]
  • TP-33 (AC-33) Docs describing the language gate reflect the configured Claude directory — method:manual [files: docs/reference/**]
  • TP-34 (AC-34) Feature knowledge bases touched by this change match the code — method:manual [files: .devflow/features/**]
  • TP-35 (AC-35) ADR-031 and the ledger record D-A, D-B, D-C and the regression net, with D-series comments at the code sites — method:manual
  • TP-36 (AC-36) Issue housekeeping for 382, 349, 390 and the epic is done — method:manual

When gh and ls-remote both fail to name the default branch, the
resolver now reads `git symbolic-ref --quiet refs/remotes/origin/HEAD`
(local, no network) and folds that branch's tracking copy as the
existing offline path does, so a branch that sets evidence standard can
no longer resolve below main's required just because origin is
unreachable (D-OFFLINE-ORIGIN-HEAD). With no origin/HEAD recorded the
worktree still governs with WARN=remote-unavailable; an unanswered read
raises. SAFE_REF_RE and the origin/HEAD parser move to the shared lib
so the settings resolver reads branch names the same way.

Replaces the openSync/readSync spy test of the retired policy.json with
the behavioural FIFO test that already proves it is never opened.

Refs #406
D-PERSONAL-UNTRACKED: a .devflow/config.json that git tracks (checked
locally with `git ls-files --error-unmatch`, only when the file exists)
is not personal, so it is ignored entirely and the resolver warns on
stderr to untrack it. A branch that commits `reviewPublication: "full"`
with `git add -f` now resolves the ceiling default, auto. The hooks'
readRepoLayers fold ignores it the same way.

D-LENS-UNION: the compliance lens is machine ∪ default branch ∪
worktree. The default branch's project.json is read from the local
tracking copy (origin/HEAD, then cat-file of refs/remotes/origin/<D>,
bounded, strict parser), so a PR that deletes compliance:["hipaa"] is
still reviewed under hipaa; a branch can only add frameworks. Any
unreadable default-branch state is a malformed declaration (generic),
never a lower lens.

An unreadable config.json now fails only the keys it owns: the
readable project.json's (and default branch's) frameworks stay in
COMPLIANCE.

The settings tests prove the git-only, never-gh property from the
scripted shim's call log instead of counting spawnSync/stdout/require
sites in the source, and add a behavioural writes-nothing check.

Refs #406
The v6 upgrade appended `!.devflow/project.json` (and any other missing
completion line) at the END of .gitignore. gitignore is last-match-wins,
so that line overrode a user's later `.devflow/project.json` re-ignore,
and it contradicted the documented placement just before the block's
.claudeignore line.

D-GITIGNORE-IN-BLOCK: both twins (ensure-root-gitignore and
computeDevflowGitignore) now insert the missing lines as one run, in
block order, right after the block sentinel and the block lines that
precede the first missing one in a fresh block (bounded at three).
Every other byte is kept; a v5 block upgrades to the current block
byte for byte. v6 is unreleased, so the marker stays v6.

The block comment no longer calls policy.json the evidence policy: it
is retired and only its presence matters. The init log line says so
too.

Refs #406 (items 10, 11)
When `git rev-parse` failed from a subdirectory of a checkout (dubious
ownership, GIT_CEILING_DIRECTORIES), df_resolve_roots fell back to the
raw cwd while df_is_project_root's ancestor marker walk still found the
checkout's .git. ensure-devflow-init and session-start-context then
wrote .devflow/ and a .gitignore block into the subdirectory.

D-HOOKS-TOPLEVEL-ONLY: df_is_project_root now requires the .git entry
AT the root (a directory, or a linked worktree's/submodule's file), so
the gate and the resolver agree. Still zero forks; the HOME refusal and
the non-git refusal are unchanged, and a directory holding its own .git
marker still counts when git cannot read it.

tests/shell-hooks-tracker.test.ts seeds project roots as subdirectories
of a marker-only tmpDir; those fixtures need a .git of their own (patch
handed to the orchestrator, file outside this package).

Refs #406 (item 9)
/code-review's language-focus presence gate probed
~/.claude/skills/devflow:{focus}/SKILL.md and /dynamic-profile mined
~/.claude/projects, history.jsonl and rules directly, so on a machine that
installs into CLAUDE_CONFIG_DIR every language focus was silently dropped
and the profile read the wrong tree. Both now resolve the directory once
with the installer's rule (CLAUDE_CONFIG_DIR when absolute, else
$HOME/.claude — D-CLAUDE-DIR-PROMPTS); the worktree-support pointers are
directory-neutral.

tests/guards/claude-dir.test.ts bans a path under the home Claude
directory, or the bare directory without the rule, across compiled
commands, agents, references, skills and rules, with red probes and a
behavioural run of the sanctioned line.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
About thirty relative .devflow/docs/ read and write sites in plan,
research, bug-analysis, implement (handoff and evidence files) and the
dynamic plan/tickets/build commands resolved against the session's cwd,
so a run started in a subdirectory scattered a second .devflow/docs/ tree
there that no later run found.

A new one-define partial, _docs_root.mds (D-DOCS-ROOT, extending
D-PROMPT-ROOT), resolves {worktree} with git -C "{start}" rev-parse
--show-toplevel, falling back to the start directory outside git, and every
docs path is written {worktree}/.devflow/docs/... File names are unchanged.
Paths that reach a PR or issue comment (EVIDENCE_FILE, REVIEW_SUMMARY_PATH,
WAVE_REPORT_PATH, PLAN_ARTIFACT_PATH) stay repo-relative and now always
travel with WORKTREE_PATH; authored workflow scripts take the root as
args.root.

tests/guards/docs-root.test.ts holds every compiled command to rooted
paths, relative agent fields beside WORKTREE_PATH, or a named live
exemption; partials-root.test.ts runs the resolution command on real git.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
… remedy

TRACKER_WARN=mismatch means a personal tracker override tried to widen the
resolved provider. The remedy said `devflow tracker --set {id}`, which only
moves the machine default and changes nothing when the repository's
project.json selects the provider. It now says to correct or drop the
personal config.json tracker key. The rewording is length-neutral, so the
compiled git.md stays at 43,832 chars against its 43,912 ceiling.

The seam test pins the new remedy and reports the machine-default command
as a missing remedy.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
Fixture-only: npm run test:golden:update -- git-agent after the remedy
rewording in the previous commit. One line changes; the fixture stays
44,149 bytes.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
no-config-read now reads the orchestrator charter and the text hooks hand
a model — *_SECTION/*_NOTE/*_HEADER/CONTEXT literals, json_prompt_output
arguments and $(cat <<EOF) prompt bodies — extracted from the hook source,
whose own shell stays out of scope, with a marker per emitter and a red
probe.

It first fails by name on any compiled command, agent or reference older
than a source it is compiled from, so a clean scan can no longer be a scan
of stale dist/ text. The staleness collector has a hermetic mtime probe.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
The skill told agents to source .devflow/scripts/docs-helpers.sh, a
repo-local install path that no longer exists. The helpers are now defined
inline, and ensure_docs_dir roots .devflow/docs at the checkout toplevel
(D-DOCS-ROOT).

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
The forbidden-I/O negative control's harmless neighbour named the retired
DEVFLOW_DIR. It now names DEVFLOW_BODY; the control's intent is unchanged.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme-wp4
Refs #406
Section 3 of session-start-context now also forks the settings resolver
when a bounded builtin read of .devflow/config.json shows a "tracker"
key and the machine sentinel names a provider (a personal override can
only narrow, so a github machine never reads it). A jira machine whose
repository has config.json {"tracker":"github"} no longer gets a jira
Tracker directive there; a config.json git tracks stays ignored, as the
resolver ignores it.

New Section 4 (D-LEGACY-LOCAL-NOTICE): when <root>/.claude/settings.json
registers a devflow hook, matched by the exact ownership shape (a
command ending in /scripts/hooks/run-hook <marker> for a marker devflow
registered, or a v1 .sh hook), a fresh session gets one line pointing
at `devflow uninstall --scope local`. Project work only, never for the
machine-wide .claude, and a settings.json with no devflow hook costs a
builtin read and no subprocess.

Also gives the tracker suite's fixture roots their own .git, as the
toplevel-only project gate (5e8b656) requires.

Refs #406
The shim-recorded fork check runs three hook sessions through freshly
written wrapper executables; like the tracker suite's fork tests it now
allows NODE_EXEC_STALL_MS for the first exec, so it does not time out
under a loaded run.

Refs #406
init resolved its install paths four times, and guarded one of them
with a 'Resolving paths' spinner and a "Path configuration error" catch
for throws that could no longer happen. The paths now resolve once, at
the top of the action, through resolveInstallationPaths(), whose only
failure — a process with no home directory — is a Result the command
reports and exits on. readHomeDirectory() never throws, even where
os.homedir() does.

The path and manifest comments describe the end state in the present
tense, without the retired mechanisms they replaced.

Refs #406 (items 24, 25)
In a dotfiles repository rooted at HOME, init treated HOME as a
project: it wrote ~/.devflow/config.json into the machine root, a
~/.claudeignore, and devflow's block into ~/.gitignore.

D-INIT-NOT-HOME extends the hooks' D-HOOKS-GIT-ONLY rule to the CLI:
a git root whose realpath is HOME's is treated as no repository, so
init writes no per-repository file, runs no per-project migration or
queue drain there, drops HOME from the discovered .claudeignore
targets, and prints a one-line notice. isSameLocation moves from
uninstall.ts to src/core/same-location.ts, shared by both commands.

Refs #406 (item 6)
init's managed write read .devflow/config.json with a bare JSON.parse:
a syntax error read as an empty file, and the rewrite then deleted the
user's hand-written keys (the tracker override among them); a duplicate
key read as its last value, where the resolvers read the file as
malformed.

D-CONFIG-STRICT-PARSE: the file is now judged by the shared strict
parser the resolvers use (lib/project-config.cjs, loaded through the
CJS seam as loadProjectConfigLib). D-CONFIG-NO-REPAIR: a malformed or
unreadable file is left byte-for-byte as it is; writeManagedConfig
returns a Result naming why, and init prints it as a warning.

Refs #406 (item 8)
- init --no-compliance said "artifacts removed"; it removes only the
  rule, and the skill and framework references stay installed.
- memory/learning/knowledge --enable said "in every project"; a
  repository can narrow the machine switch, so the text says so.
- compliance --enable told a team to "commit this as project.json",
  which overwrites a committed file; it now says to add the keys to
  it, never replacing it.
- the compliance --status migration hint told a team to delete
  policy.json at once; it now says to keep it until every teammate
  runs devflow 3.0 or later.

Refs #406 (items 13, 32)
`devflow debug --enable` with `"env": []` set the key on the array,
which JSON.stringify drops, and reported success having written
nothing. D-DEBUG-ENV-OBJECT: a present env that is not an object is
rejected — exit 1, file untouched — and the pure edits return a
Result instead of throwing on malformed JSON.

Settings writes were a mix: init, debug and uninstall wrote in place
with fs.writeFile, the rest renamed a temp file over the target, which
also replaced a dotfiles-managed symlink with a plain file.
D-SETTINGS-ATOMIC: every Claude settings.json write now goes through
writeSettingsFileAtomic, which renames a sibling temp file into place
and, for a symlinked settings.json, writes the file the link resolves
to so the link survives. A guard holds src/ to the one helper.

Refs #406 (item 15)
detectUpgrade computed isDowngrade and init ignored it, so an older
`npx devflow-kit@x init` silently swept every newer skill, agent and
command as an orphan.

D-INIT-DOWNGRADE-WARN: when the manifest names a newer version than
the running CLI, init warns before installing, naming both versions,
what will be removed and how to keep it. It does not block.

Refs #406 (item 16)
Hooks log under ~/.devflow/logs/<cwd-slug>/, one folder per working
directory, and nothing removed any: one machine held 31k of them, most
left by test runs in temp directories.

D-LOG-DIR-CAP: init keeps the MAX_HOOK_LOG_DIRS (200) most recently
written folders and removes the rest, oldest first. Recency is the
newest mtime among a folder and its logs, since an appended log never
moves the folder's own mtime. Each pass reads at most 100,000 folders
and removes at most MAX_LOG_DIRS_PRUNED_PER_RUN (10,000), so a backlog
of any size is worked off over successive inits at a bounded cost per
run. Files at the logs root (proxy.log) and symlinks are never
touched.

Refs #406 (item 17)
`uninstall --scope local` refuses when there is no repo-local install
to act on — outside a git repository as well as in one rooted at HOME
(uninstall.ts). Only the HOME case was tested. The new arm runs the
built CLI from a non-git temp directory holding .claude/.devflow decoys
and proves exit 1, the refusal message, and both HOME and the cwd
byte-identical.

Refs #406 (item 20)
resolveDevflowDirCleanup took a `scope` and returned artifacts-only
for anything but 'user', but its one caller is the user-scope branch
and always passed 'user'. The parameter, the unreachable guard and
the two tests pinning it are gone.

Refs #406 (item 23)
compliance --status still said an unreadable personal config.json left
the repository at "generic controls only". Since D-LENS-UNION a broken
file affects only the keys it owns: config.json owns no compliance, so
the lines are now those of a readable file, and an unreadable
project.json reads as a malformed (generic) declaration. The status
also shows the default branch's declared ids and the effective lens
(machine + default branch + this checkout).

The resolver ignores a git-tracked .devflow/config.json and says so on
stderr (D-PERSONAL-UNTRACKED), but prompts discard its stderr. tracker,
memory, learning and knowledge --status now print the warning with
the `git rm --cached` fix.

The switch-table fixture header describes the union lens.

Refs #406 (resolver follow-ups)
Claude Code runs a Stop event's hooks in parallel, so the memory worker
can read the queue after capture-prompt appended the user row and before
capture-turn appends the assistant row. The orphan-only auto-clean then
deleted the queue and lost that turn's prompt (audit REPORT §4 #13).

The worker now logs and exits without the LLM run, leaving the queue in
place (D-QUEUE-NO-ORPHAN-DELETE); the next run takes the whole turn, and
queue-append already caps the file. capture.ts's comment claimed array
order sequenced the Stop hooks; it now states the real contract.

Refs #406
The init-only prune (D-LOG-DIR-CAP) never runs for a user who does not
re-init, so ~/.devflow/logs kept growing one folder per working
directory. devflow_log_dir now prunes whenever it creates a new folder:
one `ls -t`, oldest first, at most 50 removed per call, never a root
file or a symlink, and a folder whose log is newer than the oldest kept
folder is spared. The common path, an existing folder, costs nothing.
The shell cap is pinned equal to MAX_HOOK_LOG_DIRS by a test.

Refs #406
_docs_root.mds brought ALL_MDS_PARTIALS to 16; the ratchet follows it
up so the roster cannot shrink back to 15 unnoticed.

Refs #406
The regression net ADR-031 names (audit appendix 09-tests.md, items 3,
4 and 9) now exists.

write-set-fence: after one sandboxed init, 51 rows run every toggle
action (and the clear/status actions beside them) through the built
CLI, walk the whole sandbox before and after, and fail naming any path
outside the row's allowlist; a mustWrite row must change something.
Coverage is held to the CLI itself: every command in --help, and every
--enable/--disable/--set a command defines, has a row or a recorded
reason (security --disable reaches the managed-settings path; proxy
starts a relay; safe-delete depends on the trash tool).

init-location-invariance: the same init from the repo root, a
subdirectory, a linked worktree, a non-git dir, a repo path with a
space, a symlinked HOME and a repo rooted at HOME installs an
identical machine side, and writes per-repo files only at the checkout
toplevel (none in subdirs, outside git, or at HOME).

numeric-floors.json registers the row, command and location counts as
floors and the allowlist size (11) as a ceiling.

Refs #406
Claude Code runs a Stop event's hooks in parallel, so settings.json array
position sequences nothing at run time. The memory.ts, init.ts,
memory-worker and capture-turn comments claimed an append-before-spawn
order enforced by array position; they now state the real contract: the
worker tolerates a not-yet-appended turn (D-QUEUE-NO-ORPHAN-DELETE), and
the array position only keeps init and the memory toggle byte-alike.

Refs #406
devflow_debug_set_cwd created the per-cwd log folder itself when hook
debugging was on, so a later devflow_log_dir saw an existing folder and
skipped the D-LOG-DIR-CAP prune. The debug trace now takes its folder from
devflow_log_dir (sourcing log-paths beside it when the hook has not), so
every hook-created log folder goes through the same bounded prune.

Refs #406
The D-SPAWN-BUDGET note claimed a 12-spawn allowance across the unit
suite that nothing enforces and many later files exceed. It now states
this file's own spend (9 CLI spawns, 32 hook spawns) and that the file's
structure, not a counter, holds it.

Refs #406
- feature-knowledge-system: 47 reference files (11 tracker ops, 9 PR-host
  ops, 4 named docs), 16 partials with _docs_root.mds; the retired
  D-INSTALL-SET text now states install-all.
- compliance-feature: the lens is machine ∪ default branch ∪ worktree
  (D-LENS-UNION); unreadable files never lower it; the offline
  origin/HEAD fallback; the language gate resolves CLAUDE_CONFIG_DIR.
- installer-shadowing: the v6 block with top-ups inserted inside it
  (D-GITIGNORE-IN-BLOCK); init-not-home, no config repair, atomic
  settings writes, the downgrade warning.
- learning-capture-system: parallel Stop hooks and the kept user-only
  queue; the hook log cap; top-level-only scaffolding; the session-start
  personal tracker narrowing and legacy-install notice.
- test-harness: the write-set fence, the init location matrix, and the
  new guards.
- tracker-feature: the personal narrowing in Section 3, the corrected
  mismatch remedy, and the tracked-config.json rule.

Refs #406
session-start-context: extract the bounded-read + tracker-key case
logic shared by TRACKER_PROJECT_FILE and TRACKER_PERSONAL_FILE into
one _sc_tracker_file_asks() shell function (no new forks). Same D-series
commentary, same behavior.

resolve-settings.cjs: rewrap one overlong JSDoc line in foldPublication
to match the file's ~80-char comment width. No logic change.
git ls-files reads the index, and reading the index runs a configured
core.fsmonitor hook (verified on git 2.50.1). The settings resolver runs
from session hooks and documented the check as a pure read, so pass
-c core.fsmonitor=false on that one call. Adds a real-git test with a
marker-writing hook plus a known-bad probe.
The resolver.test.ts full-suite failure (exit 4 at the first e2e row) was
the first exec of the freshly written bash fakes: macOS scans a new
executable on its first run, and under a loaded syspolicyd that exceeds
the resolvers' 5 s per-git-call bound, so rev-parse read as unanswered
and the row failed closed. warmFakeBin pays that cost in beforeAll.
Rewrites transition phrasing ("can no longer", "exactly as before",
"now keeps") in the D-OFFLINE-ORIGIN-HEAD and D-LOG-DIR-CAP comments
and a test header, and unescapes the literal \` sequences a heredoc
copy left in session-start-context's Section 4 comments.
…clone

The D-OFFLINE-ORIGIN-HEAD rows were scripted only. A real clone records
refs/remotes/origin/HEAD; with origin gone the lowered branch resolves
required from main's tracking copy, and deleting the symref (the control)
leaves the residual worktree-governed standard.
An origin/HEAD that exists but names no safe branch (another remote, a
hostile or unparseable name) fell back to the worktree's own file, the
residual case. It is a failure, and every failure resolves required: it
now reads as an invalid base, as the settings resolver already fails the
same answer closed to generic. A genuinely unrecorded origin/HEAD keeps
the residual behaviour (worktree governs, WARN=remote-unavailable).

Refs #406
feature-config read config.json through a symlink while the resolvers
refuse one. It now reads through the resolvers' own bounded no-follow
read (readBoundedRegularFile), so a symlink, dangling or not, reads as
unreadable: readers configure nothing from it, and the managed write
leaves it in place with a warning (D-CONFIG-NO-REPAIR) rather than
writing through it or renaming a file over it (D-CONFIG-NO-FOLLOW).

Refs #406
The only writing security row re-enabled a deny list init had already
installed, so it was marked mustWrite: false and proved nothing. A new
row seeds the state `init --security none` leaves (no deny list in the
user settings, `none` in the manifest) and requires the enable to write
inside [settings, manifest]. It is seeded rather than installed because
`init --security none` also strips the platform managed-settings file,
an absolute system path the sandbox cannot redirect.

`security --disable` stays exempt, with the reason stated precisely: it
removes Devflow entries from that same managed file. Row floor 51 -> 52.

Refs #406
MAX_LOG_DIRS_PRUNED_PER_RUN was 10,000, so the 31k backlog the cap was
written for took about four inits to clear. The removal bound now equals
the scan bound (100,000): one init removes every scanned folder beyond
the cap, about 2.2 s per 10,000 removals, and only a backlog larger than
the scan bound waits for the next init.

Refs #406
- README: compliance --enable/--set print the keys to add to
  .devflow/project.json, never a file to replace it; the lens reads the
  default branch's copy from the local tracking ref.
- CHANGELOG: an unsafe origin/HEAD resolves required; a symlinked
  config.json reads as unreadable and init leaves it; the lens reads the
  default branch from the last fetch; the log prune clears in one run.
- file-organization: 170 deny entries = 145 Bash + 25 Read patterns.

Refs #406
MDS_PARTIALS is 15 (ALL_MDS_PARTIALS 16 with _common.mds) and
ALL_DISCOVERED_HOSTS is 20 (13 command + 1 generator + 6 reference
modules), as the build prints. Three KBs still said 12/13 partials and
16 hosts with two reference modules.

Refs #406
resolveDevflowDirCleanup no longer takes a scope, so its test titles no
longer say "for user scope".

Refs #406
init --security none reaches the managed-settings removal, which
rewrites or unlinks the real platform file before any TTY or sudo
step. The init-guards and hook-ownership e2e suites passed `none`
with no guard; neither needs that mode, so both now install with
--security user. The one test that does need it keeps its skip,
now through the shared systemManagedSettingsAtRisk() helper
(D-TESTS-NO-SYSTEM-MANAGED), and a guard fails any test file that
passes --security none or security --disable without it.

Refs #406
The dynamic-workflow-engine description and its count-rule pointer
named 12 partials and 16 hosts; the tree holds 15 and 20.

Refs #406
The prune-call comment still said a large backlog is worked off over
successive inits. One pass clears every folder it scans beyond the
cap; only a backlog beyond the 100,000-folder scan bound waits.

Refs #406
An unreadable personal config.json resolves to the fail-closed
settings, not to no configuration; only the memory and learning
hooks read it as narrowing nothing.

Refs #406
The CLI prints the keys to add to .devflow/project.json, not the
file itself.

Refs #406
The from-no-deny-list row shared its test name with the re-enable
row, so a failure could not say which one broke. Rows take an
optional label that rowName appends.

Refs #406
The notice must reach piped stdout exactly once, ahead of the first-install
migrations and the install spinner. It is printed before any spinner starts,
so nothing drawn later can overwrite it. QA saw it missing in 1 of 5 runs.
That run had no cd into the sandbox HOME, so init ran in an ordinary
checkout, where the notice is rightly absent.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme
Refs #406
Registers the literal, scoped to the skill's own tree, so the sourced
repo-local helper script cannot come back. CHANGELOG.md keeps naming it.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme
Refs #406
Labels the second compliance --disable row 'after --enable'. Adds a check,
with a red probe, that no two rows register the same test name.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme
Refs #406
The doc comment now matches the printed text: the keys to add to the
repository's .devflow/project.json, merged into it, never replacing it.

Task: feat/406-pre-3-0-0-hardening-local-scope-retireme
Refs #406
`compliance --status` runs the evidence-policy resolver, whose one
reachability probe is `gh api repos/{owner}/{repo}`. The sandbox PATH
reached the machine's gh, and gh 2.10x seeds
$HOME/.local/state/gh/device-id on its first run, so on CI the
write-set fence saw four writes outside the row's allowlist. Older gh
releases (2.88 locally) write nothing, which is why it passed here.

createSandbox now ships a fake gh beside the fake claude: exit 4 with
gh's unauthenticated message and nothing on stdout, writing nothing, so
the resolver takes its offline path whatever gh the machine carries.
Every sandbox consumer (the fence, init location invariance, the
install snapshot and hook matrix, the golden generator) inherits it.
A harness test pins that claude and gh resolve to the sandbox fakes
and that the resolver's probe writes nothing; the fence asserts the
same resolution beside its HOME check.

Refs #406
@dean0x

dean0x commented Sep 30, 2026

Copy link
Copy Markdown
Owner Author

Test Plan Evidence — 495d84c

Verified 10/36: VERIFIED-CI 1, ATTESTED-LOCAL 9, UNVERIFIED 0, STALE 7, FAILED 19, INDETERMINATE 0

  • TP-1 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:4d8a287eb459
  • TP-2 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS exit:0 h:2a6ada83c11b
  • TP-3 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:c9e7bd524dde
  • TP-4 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:5587199534b2
  • TP-5 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:14043fa4565d
  • TP-6 FAILED sha:12da9004fb45ea70fc64584a44192431e1b47667 out:PASS run:36756313323/1 exit:0 h:14781d4a6b72
  • TP-7 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:b3fe2376455b
  • TP-8 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:8ea88c81fe8c
  • TP-9 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:145998cfa338
  • TP-10 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:d444591918e0
  • TP-11 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:f8335173a928
  • TP-12 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:878aa6ef5df9
  • TP-13 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:8159ee4e9c9b
  • TP-14 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS exit:0 h:36ebe944f0a7
  • TP-15 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS exit:0 h:b999ef4d2b57
  • TP-16 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:42337c06e6dc
  • TP-17 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS exit:0 h:0df9e2b13dff
  • TP-18 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:b17c3f65da4a
  • TP-19 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:0eb705c47a2d
  • TP-20 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:c86b2b839908
  • TP-21 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS exit:0 h:4a299837bb65
  • TP-22 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:3c02d51dadac
  • TP-23 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:75297ec43bd3
  • TP-24 FAILED sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS run:36754124089/1 exit:0 h:96b78b0db747
  • TP-25 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:6707eab35a53
  • TP-26 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS exit:0 h:330884d8ddda
  • TP-27 FAILED sha:12da9004fb45ea70fc64584a44192431e1b47667 out:PASS run:36756313323/1 exit:0 h:4cc38eb972fc
  • TP-28 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:f591985755be
  • TP-29 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:f4f6ddb63486
  • TP-30 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:8b24d6b45b78
  • TP-31 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:16d9f72d8066
  • TP-32 ATTESTED-LOCAL sha:12da9004fb45ea70fc64584a44192431e1b47667 out:PASS exit:0 h:a58c4f145ed1
  • TP-33 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:6b57ce4248af
  • TP-34 ATTESTED-LOCAL sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:0a56096b3914
  • TP-35 STALE sha:80b97fd3da33614b6e7bc8494e9bfeaf33fcc68b out:PASS h:5217eb5fde80
  • TP-36 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:b177e7256c26

@dean0x

dean0x commented Sep 30, 2026

Copy link
Copy Markdown
Owner Author

Test Plan Evidence — 495d84c

Verified 36/36: VERIFIED-CI 26, ATTESTED-LOCAL 10, UNVERIFIED 0, STALE 0, FAILED 0, INDETERMINATE 0

  • TP-1 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:4d8a287eb459
  • TP-2 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:2a6ada83c11b
  • TP-3 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:c9e7bd524dde
  • TP-4 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:5587199534b2
  • TP-5 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:14043fa4565d
  • TP-6 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:14781d4a6b72
  • TP-7 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:b3fe2376455b
  • TP-8 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:8ea88c81fe8c
  • TP-9 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:145998cfa338
  • TP-10 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:d444591918e0
  • TP-11 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:f8335173a928
  • TP-12 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:878aa6ef5df9
  • TP-13 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:8159ee4e9c9b
  • TP-14 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:36ebe944f0a7
  • TP-15 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:b999ef4d2b57
  • TP-16 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:42337c06e6dc
  • TP-17 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:0df9e2b13dff
  • TP-18 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:b17c3f65da4a
  • TP-19 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:0eb705c47a2d
  • TP-20 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:c86b2b839908
  • TP-21 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:4a299837bb65
  • TP-22 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:3c02d51dadac
  • TP-23 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:75297ec43bd3
  • TP-24 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:96b78b0db747
  • TP-25 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:6707eab35a53
  • TP-26 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:330884d8ddda
  • TP-27 VERIFIED-CI sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS run:36757296073/1 exit:0 h:4cc38eb972fc
  • TP-28 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:f591985755be
  • TP-29 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:f4f6ddb63486
  • TP-30 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:8b24d6b45b78
  • TP-31 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:16d9f72d8066
  • TP-32 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:a58c4f145ed1
  • TP-33 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:6b57ce4248af
  • TP-34 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:0a56096b3914
  • TP-35 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:5217eb5fde80
  • TP-36 ATTESTED-LOCAL sha:495d84c5789516ed7966e001e86ed28befe92315 out:PASS h:b177e7256c26

@dean0x
dean0x merged commit e68c9be into main Sep 30, 2026
3 checks passed
@dean0x
dean0x deleted the feat/406-pre-3-0-0-hardening-local-scope-retireme branch September 30, 2026 19:44
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.

Pre-3.0.0 hardening: local-scope retirement + per-repo config layer (epic #387 follow-through)

1 participant