Skip to content

Commit 369bde7

Browse files
committed
Support safe existing repository adoption
1 parent dd390f7 commit 369bde7

8 files changed

Lines changed: 251 additions & 27 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ engineering-process --root ../my-web-app init \
2828
--profile frontend --repository-name suyog19/my-web-app
2929
```
3030

31-
Or supply an immutable released revision explicitly with `--revision <40-hex-sha>`. Initialization creates the manifest and lock, compact `AGENTS.md`/`CLAUDE.md`, portable Skills, and a validation workflow. Add repository differences—validation commands, protected path hints, UX triggers—to `.engineering/process.yaml`, then re-render/re-lock using a reviewed process upgrade.
31+
Or supply an immutable released revision explicitly with `--revision <40-hex-sha>`. Initialization creates the manifest and lock, compact `AGENTS.md`/`CLAUDE.md`, portable Skills, and a validation workflow. Existing assistant context is never silently overwritten; follow the [mature repository migration](docs/adoption.md#mature-repository-migration) and use the explicit `--adopt-existing-context` flow. Add repository differences—validation commands, protected path hints, UX triggers—to `.engineering/process.yaml`, then re-render/re-lock using a reviewed process upgrade.
3232

3333
## Change flow
3434

docs/adoption.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,34 @@
99

1010
Repository-local technology is deliberately opaque to policy. Put commands under `overrides.validation.commands`; add sensitive paths/characteristics and domain triggers when they strengthen classification. Never copy the canonical policy into the manifest.
1111

12-
For upgrades, run `engineering-process upgrade --version X --revision SHA --dry-run`, review inherited/profile/generated-file changes and conflicts, then apply from the reviewed target process checkout. A central update never mutates an active repository automatically.
12+
## Mature repository migration
1313

14+
Initialization is non-destructive. If `AGENTS.md`, `CLAUDE.md`, or the process validation workflow is repository-owned, normal `init` stops before creating `.engineering/process.yaml`. Do not use `--force` to bypass this boundary.
15+
16+
Use this reviewed workflow:
17+
18+
1. Inventory existing assistant, architecture, branch/release, validation, security, product/UX, and deployment guidance. Identify copied generic policy versus genuine repository-specific context.
19+
2. Keep canonical requirements in this process. Keep local mechanics and stronger constraints in repository-owned documents. Existing `overrides` remain the only mechanism for validation commands, classification additions, UX triggers, controls, and native enforcement.
20+
3. Commit or otherwise preserve a baseline so the migration can be reviewed.
21+
4. Run `engineering-process --root <repo> init --profile frontend --adopt-existing-context`. The command moves existing `AGENTS.md` and/or `CLAUDE.md` verbatim into `docs/engineering/local-context/`, declares them in `local_context`, and writes compact generated bootstraps. It refuses to replace a repository-owned workflow.
22+
5. Split or rename the preserved documents as useful, then update `local_context`. Each entry has a category, repository-relative path, and optional contextual `load_when` instruction:
23+
24+
```yaml
25+
local_context:
26+
- category: operating_contract
27+
path: docs/engineering/operating-contract.md
28+
load_when: load before branch, issue, or release workflow changes
29+
- category: architecture
30+
path: docs/architecture/
31+
load_when: load for changes affecting component boundaries
32+
- category: ux_product_design
33+
path: docs/ux/ux-gates.md
34+
load_when: load for user-visible changes
35+
```
36+
37+
6. Review the full diff. Confirm branch responsibilities, issue-first workflow, agent roles, architecture/deployment rules, Senior UX Designer responsibilities, UX Gates A-D, and local implementation/review conventions remain referenced and readable.
38+
7. Run `engineering-process validate`. Missing paths, invalid context structure, stale generated bootstraps, modified generated files, and locked-control override attempts fail validation.
39+
40+
Local-context documents add detail and mechanics; they do not override the Effective Obligation Set. Progressive `load_when` hints keep the bootstraps compact and avoid loading every document for every change.
41+
42+
For upgrades, run `engineering-process upgrade --version X --revision SHA --dry-run`, review inherited/profile/generated-file changes, preserved local-context paths, and conflicts, then apply from the reviewed target process checkout. Upgrade verifies generated-file hashes before replacement and refuses modified or repository-owned targets. It never rewrites files referenced by `local_context`. A central update never mutates an active repository automatically.

schemas/repository-process.schema.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@
2020
"type": "object", "required": ["name"], "additionalProperties": true,
2121
"properties": {"name": {"type": "string", "minLength": 1}}
2222
},
23+
"local_context": {
24+
"type": "array",
25+
"items": {
26+
"type": "object", "additionalProperties": false,
27+
"required": ["category", "path"],
28+
"properties": {
29+
"category": {"enum": ["operating_contract", "architecture", "ux_product_design", "deployment_release", "validation_testing", "security_domain", "other"]},
30+
"path": {"type": "string", "minLength": 1, "pattern": "^(?!/|[A-Za-z]:|.*(?:^|/)\\.\\.(?:/|$)).+"},
31+
"load_when": {"type": "string", "minLength": 1}
32+
}
33+
}
34+
},
2335
"overrides": {
2436
"type": "object", "additionalProperties": false,
2537
"properties": {
@@ -33,4 +45,3 @@
3345
}
3446
}
3547
}
36-

src/engineering_process/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def parser() -> argparse.ArgumentParser:
4545
p = argparse.ArgumentParser(prog="engineering-process", description="Engineering policy and assurance CLI")
4646
p.add_argument("--root", default=".", help="participating repository root")
4747
sub = p.add_subparsers(dest="command", required=True)
48-
init = sub.add_parser("init"); init.add_argument("--profile", required=True, choices=["generic", "frontend", "backend"]); init.add_argument("--repository-name"); init.add_argument("--revision"); init.add_argument("--force", action="store_true")
48+
init = sub.add_parser("init"); init.add_argument("--profile", required=True, choices=["generic", "frontend", "backend"]); init.add_argument("--repository-name"); init.add_argument("--revision"); init.add_argument("--force", action="store_true"); init.add_argument("--adopt-existing-context", action="store_true")
4949
sub.add_parser("validate")
5050
for name in ("classify", "evaluate", "explain"):
5151
c = sub.add_parser(name); c.add_argument("--path", action="append"); c.add_argument("--declared"); c.add_argument("--semantic"); c.add_argument("--rationale"); c.add_argument("--sha")
@@ -63,7 +63,7 @@ def run(args: argparse.Namespace) -> dict:
6363
root = Path(args.root).resolve()
6464
if args.command == "init":
6565
revision = args.revision or current_revision(data_root())
66-
return initialize(root, args.profile, args.repository_name or root.name, revision, args.force)
66+
return initialize(root, args.profile, args.repository_name or root.name, revision, args.force, args.adopt_existing_context)
6767
manifest = _manifest(root)
6868
if args.command == "validate": return validate_repository(root)
6969
if args.command in {"classify", "evaluate", "explain"}:

src/engineering_process/render.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,22 @@
33
import json
44
from pathlib import Path
55

6-
from .io import digest_bytes
6+
from .errors import ProcessError
7+
from .io import digest_file
8+
9+
10+
GENERATED_MARKER = "<!-- GENERATED BY software-engineering-process; DO NOT EDIT. -->"
711

812

913
def bootstrap(manifest: dict, assistant: str) -> str:
1014
p = manifest["process"]
1115
title = "Codex" if assistant == "codex" else "Claude Code" if assistant == "claude" else "Engineering participant"
12-
return f"""<!-- GENERATED; DO NOT EDIT. -->
16+
local_context = manifest.get("local_context", [])
17+
context_lines = "\n".join(
18+
f"- `{item['path']}` ({item['category']}): {item.get('load_when', 'load when relevant to the change')}"
19+
for item in local_context
20+
) or "- None declared."
21+
return f"""{GENERATED_MARKER}
1322
# {title} engineering bootstrap
1423
1524
Process: `{p['source']}` `{p['version']}` @ `{p['revision']}`
@@ -18,6 +27,9 @@ def bootstrap(manifest: dict, assistant: str) -> str:
1827
Before changing code, run `engineering-process classify` and `engineering-process evaluate`.
1928
Follow the resolved Effective Obligation Set; load only its selected Skills.
2029
30+
Repository-owned local context (adds mechanics/detail; never overrides canonical policy):
31+
{context_lines}
32+
2133
Non-negotiable boundaries:
2234
- Keep issue/change traceability and work only within approved scope.
2335
- Never expose or commit secrets or private data.
@@ -31,11 +43,20 @@ def bootstrap(manifest: dict, assistant: str) -> str:
3143
"""
3244

3345

34-
def render_files(root: Path, manifest: dict) -> dict[str, str]:
46+
def is_generated_bootstrap(path: Path) -> bool:
47+
return path.is_file() and path.read_text(encoding="utf-8").startswith(GENERATED_MARKER)
48+
49+
50+
def render_files(root: Path, manifest: dict, allow_create: bool = True) -> dict[str, str]:
3551
files = {"AGENTS.md": bootstrap(manifest, "codex"), "CLAUDE.md": bootstrap(manifest, "claude")}
3652
for name, text in files.items():
37-
(root / name).write_text(text, encoding="utf-8")
38-
return {name: digest_bytes(text.encode()) for name, text in files.items()}
53+
path = root / name
54+
if path.exists() and not is_generated_bootstrap(path):
55+
raise ProcessError(f"refusing to overwrite repository-owned context: {name}; use init --adopt-existing-context")
56+
if not path.exists() and not allow_create:
57+
raise ProcessError(f"generated adapter is missing: {name}")
58+
path.write_text(text, encoding="utf-8")
59+
return {name: digest_file(root / name) for name in files}
3960

4061

4162
def metrics(root: Path, selected_skills: list[str] | None = None) -> dict:
@@ -46,4 +67,3 @@ def metrics(root: Path, selected_skills: list[str] | None = None) -> dict:
4667
result[name] = {"bytes": len(text.encode()), "words": len(text.split()), "approx_tokens": round(len(text) / 4)}
4768
result["activated_skills"] = len(selected_skills or [])
4869
return result
49-

0 commit comments

Comments
 (0)