For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.
Goal: Resolve the four design-bearing issue clusters from the batch-1 code review (#30, #31, #32, #35, #36, #39, #43) on top of the already-merged mechanical fixes.
Architecture: Each cluster is an independent commit on integration/review-batch-1. Land in order C → A → D → B so the smallest, lowest-risk change goes first and the largest (team.py rewrite) lands on a known-good base.
Tech Stack: Python 3.11+, stdlib only. Tests via pytest. Lint via ruff. Run gates uv run ruff check and uv run pytest clean after each task.
Replace the SECRET_ENV_NAMES block at the top of src/coding_scaffold/credentials.py with:
SECRET_KEY_ENV_NAMES = ( "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "AZURE_OPENAI_API_KEY", "AZURE_AI_API_KEY", "AZURE_AI_SERVICES_KEY", "AZURE_COGNITIVE_SERVICES_KEY", "OPENROUTER_API_KEY", "GROQ_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "GITHUB_TOKEN", "GH_TOKEN",)AZURE_NONKEY_ENV_NAMES = ( "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOYMENT", "AZURE_AI_ENDPOINT", "AZURE_AI_MODEL", "AZURE_AI_MODEL_FAMILY", "AZURE_AI_SERVICES_ENDPOINT", "AZURE_COGNITIVE_SERVICES_ENDPOINT",)# Union: everything that belongs in .env.local rather than committed config.# Azure endpoints/deployments are treated as sensitive because the subdomain# typically encodes tenant identity.SECRET_ENV_NAMES = SECRET_KEY_ENV_NAMES + AZURE_NONKEY_ENV_NAMES
The SECRET_ENV_NAMES name is preserved so existing callers in write_local_credential_file don't break.
Step 2: Write the failing test for credentials split
Add to tests/test_credentials.py:
def test_secret_env_names_separates_keys_from_azure_config(): from coding_scaffold.credentials import ( AZURE_NONKEY_ENV_NAMES, SECRET_ENV_NAMES, SECRET_KEY_ENV_NAMES, ) assert "OPENAI_API_KEY" in SECRET_KEY_ENV_NAMES assert "AZURE_OPENAI_ENDPOINT" in AZURE_NONKEY_ENV_NAMES assert "AZURE_OPENAI_ENDPOINT" not in SECRET_KEY_ENV_NAMES # Backwards-compat: the union is still importable under the old name. assert set(SECRET_KEY_ENV_NAMES) <= set(SECRET_ENV_NAMES) assert set(AZURE_NONKEY_ENV_NAMES) <= set(SECRET_ENV_NAMES)
Step 3: Add redact_fields to Provider dataclass
In src/coding_scaffold/providers.py, change the Provider dataclass to:
REDACTED_PLACEHOLDER = "<configured locally; see .env.local>"@dataclass(frozen=True)class Provider: name: str kind: str available: bool status: str endpoint: str | None = None model_family: str | None = None deployment: str | None = None redact_fields: tuple[str, ...] = () def to_dict(self) -> dict[str, object]: data = asdict(self) for field in self.redact_fields: if data.get(field): data[field] = REDACTED_PLACEHOLDER data.pop("redact_fields", None) return data
Step 4: Tag Azure providers
In src/coding_scaffold/providers.py, update both Azure constructors to set redact_fields:
_azure_ai_provider — same pattern, set redact_fields=("endpoint", "deployment", "model_family") (model_family for Azure AI can come from AZURE_AI_MODEL_FAMILY which is in the redact list).
Step 5: Write the failing test for redaction
Add to tests/test_providers.py:
def test_azure_openai_provider_redacts_endpoint_and_deployment(): from coding_scaffold.providers import REDACTED_PLACEHOLDER, detect_providers env = { "AZURE_OPENAI_API_KEY": "key-xyz", "AZURE_OPENAI_ENDPOINT": "https://contoso-prod.openai.azure.com/", "AZURE_OPENAI_DEPLOYMENT": "gpt-4-internal", } providers = detect_providers(env=env) azure = next(p for p in providers if p.name == "azure-openai") # In-memory values stay intact for routing. assert azure.endpoint == "https://contoso-prod.openai.azure.com/" assert azure.deployment == "gpt-4-internal" # Serialized form redacts. serialized = azure.to_dict() assert serialized["endpoint"] == REDACTED_PLACEHOLDER assert serialized["deployment"] == REDACTED_PLACEHOLDER assert "redact_fields" not in serialized # Non-redacted providers serialize unchanged. openai = next(p for p in providers if p.name == "openai") assert "redact_fields" not in openai.to_dict()
Find the CREDENTIALS.md generator (search: grep -rn "CREDENTIALS\|credentials.md" src/coding_scaffold/writers.py). Append a paragraph near the Azure-related section:
Azure endpoint and deployment values are treated as sensitive. Thegenerated `providers.json` and adapter configs contain a placeholder; thereal values live only in `.env.local` and are resolved at agent start.
In tests/test_writers.py, add a regression test:
def test_providers_json_redacts_azure_endpoint(tmp_path, monkeypatch): from coding_scaffold.providers import detect_providers from coding_scaffold.writers import write_scaffold monkeypatch.setenv("AZURE_OPENAI_API_KEY", "k") monkeypatch.setenv("AZURE_OPENAI_ENDPOINT", "https://contoso.openai.azure.com/") monkeypatch.setenv("AZURE_OPENAI_DEPLOYMENT", "internal-gpt") # Use whatever the existing write_scaffold signature requires; mirror an # existing test in test_writers.py. write_scaffold(tmp_path, providers=detect_providers()) providers_json = (tmp_path / ".coding-scaffold" / "providers.json").read_text(encoding="utf-8") assert "contoso.openai.azure.com" not in providers_json assert "internal-gpt" not in providers_json
If write_scaffold's signature is different, mirror the call shape used by the nearest existing test in that file.
Step 7: Run tests
uv run ruff checkuv run pytest
All must pass.
Step 8: Commit
git add src/coding_scaffold/credentials.py src/coding_scaffold/providers.py \ src/coding_scaffold/writers.py \ tests/test_providers.py tests/test_credentials.py tests/test_writers.pygit commit -m "Treat Azure endpoint/deployment as secret in serialized configProvider gains a redact_fields tag; Azure constructors set it so to_dict()emits a placeholder instead of the tenant-identifying URL/deployment.In-memory values still drive routing.Closes #31Closes #35"
#Task 3 — Cluster D: Deep merge + .new staging for policy
def deep_merge_mapping( base: dict[str, object], overlay: dict[str, object], deep_keys: tuple[str, ...] = (),) -> dict[str, object]: """Merge ``overlay`` into ``base``. For top-level keys in ``deep_keys``, if both sides are mappings the merge recurses one level; otherwise overlay wins. Non-listed keys use shallow overlay-wins. """ result: dict[str, object] = dict(base) for key, value in overlay.items(): if ( key in deep_keys and isinstance(value, dict) and isinstance(result.get(key), dict) ): result[key] = {**result[key], **value} # type: ignore[dict-item] else: result[key] = value return result
If the write_policy_pack signature differs, adjust the kwargs but keep the assertions.
Step 3: Rewrite _merge_opencode_config to stage .new and deep-merge
In src/coding_scaffold/policy.py, replace the existing _merge_opencode_config body:
def _merge_opencode_config(path: Path, policy: dict[str, object]) -> tuple[Path, str | None]: from .file_ops import deep_merge_mapping current: dict[str, object] = {} warning = None target_path = path file_existed = path.exists() if file_existed: try: loaded = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError: warning = f"Could not parse {path}; wrote policy overlay to {path}.new instead." target_path = path.with_suffix(path.suffix + ".new") target_path.write_text(json.dumps(policy, indent=2, sort_keys=True) + "\n", encoding="utf-8") return target_path, warning if isinstance(loaded, dict): current = loaded merged = deep_merge_mapping(current, policy, deep_keys=("mcp", "permission")) if "instructions" in current or "instructions" in policy: merged["instructions"] = _merge_list(current.get("instructions"), policy.get("instructions")) if file_existed: target_path = path.with_suffix(path.suffix + ".new") warning = ( f"Staged {target_path.name}; review and `mv {target_path.name} {path.name}` to apply." ) target_path.write_text(json.dumps(merged, indent=2, sort_keys=True) + "\n", encoding="utf-8") return target_path, warning
Drop the now-unused _merge_mapping if no other call site uses it (grep first; keep if used).
Step 4: Run tests
uv run ruff checkuv run pytest
Existing policy tests may break if they assert that opencode.json is rewritten in place. Update those tests to look at opencode.json.new when an existing file is present; the no-existing-file path still writes opencode.json directly.
Step 5: Commit
git add src/coding_scaffold/file_ops.py src/coding_scaffold/policy.py tests/test_policy.pygit commit -m "Stage opencode.json.new and deep-merge nested keys on policyWhen opencode.json already exists, the policy pack now writes the mergedresult to opencode.json.new so the user reviews and applies the changeexplicitly. mcp.<server> and permission.<scope> entries from the user arepreserved by deep-merging those top-level keys.Closes #39"
Step 1: Write the four new failing regression tests
Add to tests/test_team.py:
def test_team_sync_never_touches_knowledge_dir(tmp_path): """User-owned .coding-scaffold/knowledge/ files survive team sync.""" import json from coding_scaffold.team import sync_team knowledge = tmp_path / ".coding-scaffold" / "knowledge" knowledge.mkdir(parents=True) (knowledge / "secret-note.md").write_text("private", encoding="utf-8") # Manifest with a benign local-path remote (allow_local enabled). remote = tmp_path / "team-remote" remote.mkdir() (remote / "shared.md").write_text("shared", encoding="utf-8") manifest = tmp_path / ".coding-scaffold" / "team-onboarding.json" manifest.write_text(json.dumps({ "team": "t", "knowledge": {"backend": "markdown", "path": ".coding-scaffold/knowledge", "remote": str(remote)}, }), encoding="utf-8") sync_team(tmp_path, allow_local=True) assert (knowledge / "secret-note.md").exists() assert (knowledge / "secret-note.md").read_text(encoding="utf-8") == "private"def test_team_sync_rejects_local_remote_without_allow_local(tmp_path): import json from coding_scaffold.team import sync_team remote = tmp_path / "team-remote" remote.mkdir() manifest = tmp_path / ".coding-scaffold" / "team-onboarding.json" manifest.parent.mkdir(parents=True, exist_ok=True) manifest.write_text(json.dumps({ "team": "t", "knowledge": {"backend": "markdown", "path": ".coding-scaffold/knowledge", "remote": str(remote)}, }), encoding="utf-8") result = sync_team(tmp_path) assert any("allow-local" in w.lower() or "local path" in w.lower() for w in result.warnings)def test_team_sync_keeps_git_directory_inside_repo_subdir(tmp_path, monkeypatch): """Cloned repo retains .git inside _repo so subsequent syncs can ff-pull.""" # Will require monkeypatching subprocess.run for git clone/pull and # asserting that team/sources/<kind>/<slug>/_repo/.git is created. # Detailed implementation deferred to the agent — keep this stub and # implement once the team.py rewrite lands so the helpers exist. pytest.skip("Implement after team.py rewrite — see plan task 4 step 3.")def test_team_sync_strips_nested_git_dirs_from_agent_path(tmp_path): """Submodule .git directories don't leak into the agent-readable copy.""" pytest.skip("Implement after team.py rewrite — see plan task 4 step 3.")
Import pytest at the top of test_team.py if not already there.
Step 2: Run failing tests
uv run pytest tests/test_team.py::test_team_sync_never_touches_knowledge_dir \ tests/test_team.py::test_team_sync_rejects_local_remote_without_allow_local -v
Both must FAIL (the protection doesn't exist yet).
Step 3: Rewrite team.py sync internals
Replace _sync_team_payload, _resolve_knowledge_destination (introduce if missing), _sync_source, _clone_or_pull, and _remove_nested_git with the layered design:
ALLOWED_REMOTE_SCHEMES = ("https", "http", "ssh")SOURCES_SUBDIR = Path(".coding-scaffold") / "team" / "sources"KNOWLEDGE_FORBIDDEN_PREFIX = Path(".coding-scaffold") / "knowledge"def _classify_remote(remote: str) -> str: """Return 'url', 'local', or raise ValueError for empty/unknown.""" if not remote: raise ValueError("Remote is empty.") if "://" in remote: scheme = remote.split("://", 1)[0].lower() if scheme in ALLOWED_REMOTE_SCHEMES: return "url" if scheme == "file": return "local" raise ValueError(f"Unsupported remote scheme: {scheme}") if remote.startswith("git@"): return "url" return "local" # bare pathdef _team_destination(root: Path, kind: str, remote: str) -> Path: """Compute the team-sources destination for a (kind, remote) pair. Always lives under SOURCES_SUBDIR — never under knowledge/.""" slug = _slug(remote) return root / SOURCES_SUBDIR / kind / slugdef _sync_source( remote: str, destination: Path, *, dry_run: bool, allow_local: bool,) -> tuple[str, str | None]: """Copy or clone `remote` into `destination`. Never touches paths outside SOURCES_SUBDIR.""" classification = _classify_remote(remote) if classification == "local" and not allow_local: return ( f"refused {remote}", "Local-path remote requires --allow-local. Skipped.", ) destination.parent.mkdir(parents=True, exist_ok=True) if classification == "local": source = Path(remote).expanduser() if not source.exists(): return f"skipped {remote}", f"Local path does not exist: {remote}" if dry_run: return f"would copy {remote}", None # Stage into _repo to mirror clone layout (no .git for local copies). repo_dir = destination / "_repo" if repo_dir.exists(): shutil.rmtree(repo_dir) shutil.copytree(source, repo_dir, ignore=shutil.ignore_patterns(".git")) _publish_repo_contents(repo_dir, destination) return f"copied {remote}", None # URL clone return _clone_or_pull(remote, destination, dry_run=dry_run), Nonedef _clone_or_pull(remote: str, destination: Path, *, dry_run: bool = False) -> str: if shutil.which("git") is None: raise RuntimeError( "git is required for team manifests pointing to a remote URL. " "Install git or pass a local path with --allow-local." ) if dry_run: return f"would clone/update {remote}" repo_dir = destination / "_repo" repo_dir.parent.mkdir(parents=True, exist_ok=True) if (repo_dir / ".git").exists(): completed = subprocess.run( ["git", "-C", str(repo_dir), "pull", "--ff-only"], check=False, capture_output=True, text=True, timeout=300, ) if completed.returncode == 0: _publish_repo_contents(repo_dir, destination) return f"updated {remote}" return f"kept existing checkout for {remote}; git pull failed" # Fresh clone (or recovery from a stale partial checkout) if destination.exists(): # Move aside, don't delete user-visible content. legacy = destination.with_name(destination.name + f".legacy-{datetime.now(UTC):%Y%m%d-%H%M%S}") destination.rename(legacy) repo_dir.parent.mkdir(parents=True, exist_ok=True) completed = subprocess.run( ["git", "clone", remote, str(repo_dir)], check=False, capture_output=True, text=True, timeout=300, ) if completed.returncode != 0: raise RuntimeError(f"Could not clone {remote}: {completed.stderr.strip()}") _publish_repo_contents(repo_dir, destination) return f"cloned {remote}"def _publish_repo_contents(repo_dir: Path, destination: Path) -> None: """Copy *.md (and other manifest-allowed files) from repo_dir to destination root, skipping any .git directories at any depth. Idempotent — clears previously published markdown first.""" for stale in destination.iterdir() if destination.exists() else []: if stale.name == "_repo": continue if stale.is_dir(): shutil.rmtree(stale) else: stale.unlink() for path in repo_dir.rglob("*"): if ".git" in path.parts: continue if not path.is_file(): continue relative = path.relative_to(repo_dir) target = destination / relative target.parent.mkdir(parents=True, exist_ok=True) shutil.copy2(path, target)
Then update _sync_team_payload so:
It computes destinations via _team_destination(root, kind, remote) only — never under knowledge/.
It refuses any manifest-supplied knowledge.path that resolves under .coding-scaffold/knowledge/; emits a warning and falls back to the safe location.
It threads allow_local through to _sync_source / _clone_or_pull.
Update the public functions sync_team, preview_team, connect_team to accept allow_local: bool = False and pass it down.
Delete _remove_nested_git — _publish_repo_contents skips .git at any depth so the helper is no longer needed.
Step 4: Add --allow-local to CLI
In src/coding_scaffold/cli.py, find the team subparser block and add:
team.add_argument( "--allow-local", action="store_true", help="Permit local-path or file:// remotes for team manifests.",)
Then in the action dispatch (search for action == "connect" and action == "sync"):
elif action == "connect": result = connect_team(target, manifest=args.manifest, allow_local=args.allow_local)elif action == "sync": result = sync_team(target, dry_run=args.dry_run, allow_local=args.allow_local)
Add allow_local: bool = False to connect_team's and sync_team's signatures and thread through.
Step 5: Update existing team tests that use local paths
Search tests/test_team.py for existing tests that call sync_team or connect_team with a local path. Add allow_local=True to those calls (the protection is intentional; tests opt in explicitly). Then un-skip and implement the two pytest.skip regression tests from Step 1 — _repo/.git should exist after a stub-git clone; the agent path should not contain any .git directories after sync against a fixture with a subrepo/.git/.
Step 6: Add the trust-model paragraph to docs
In docs/docs/wiki/Team-Onboarding.md, append:
## Trust modelTeam manifest content is third-party input. `coding-scaffold team sync`treats every remote as untrusted: imports land under`.coding-scaffold/team/sources/<kind>/<slug>/`, never inside your curated`.coding-scaffold/knowledge/` tree. Review imported markdown beforelinking it from your own pages.`file://` and local-path remotes require `--allow-local` so a teammate'smanifest cannot redirect a sync at an arbitrary directory on yourmachine without explicit consent.
Step 7: Run tests
uv run ruff checkuv run pytest
All green, including the four new regression tests and any existing tests adapted with allow_local=True.
Step 8: Commit
git add src/coding_scaffold/team.py src/coding_scaffold/cli.py \ tests/test_team.py docs/docs/wiki/Team-Onboarding.mdgit commit -m "Confine team sync to team/sources; require --allow-local for file pathsSync now never writes to .coding-scaffold/knowledge/. Remote URLs cloneinto a hidden _repo subdir and use git pull --ff-only on subsequentsyncs (no more rmtree-and-reclone). Nested .git directories are filteredout at publish time, not stripped from the working tree. Local-path andfile:// remotes are gated behind --allow-local.Closes #30Closes #36Closes #43"
Step 1: Verify the integration branch is fully green
uv run ruff checkuv run pytest
Both must pass. Expect ~155-160 tests passing + 1 xfailed (#34 deferred).
Step 2: Open a PR from integration/review-batch-1 to main
gh pr create --base main --head integration/review-batch-1 \ --title "Code review batch 1: critical/important fixes" \ --body "$(cat <<'EOF'## SummaryResolves the design-bearing cluster of the batch-1 code review:- **#32** Drop article-stripping heuristic in context compression — identifiers in inline code and link targets survive.- **#31 / #35** Treat Azure endpoint and deployment as secret in serialized config (`Provider.redact_fields`).- **#39** Policy pack stages `opencode.json.new` and deep-merges `mcp.<server>` / `permission.<scope>` so user customizations survive.- **#30 / #36 / #43** `team sync` is confined to `.coding-scaffold/team/sources/`; clones keep `.git` inside a hidden `_repo` subdir for ff-pulls; local/`file://` remotes require `--allow-local`.Plus the mechanical-fix batch already merged into this branch (closes #33, #37, #38, #44, #45, #46, #47, #49) and the case-collision hotfix (`INDEX.md` vs `index.md`).## Verification- `uv run ruff check` clean- `uv run pytest` green (1 xfail for #34, deferred)- Linux CI must show the case-collision fix clears the failing run on `main`## Spec / plan- [Design spec](../specs/2026-05-18-code-review-batch-1-design.md)- [Implementation plan](./2026-05-18-code-review-batch-1.md)EOF)"