Skip to main content

decapod/core/
scaffold.rs

1//! Project scaffolding for Decapod initialization.
2//!
3//! This module handles the creation of Decapod project structure, including:
4//! - Root entrypoints (AGENTS.md, CLAUDE.md, GEMINI.md, CODEX.md)
5//! - Constitution directory (.decapod/constitution/)
6//! - Embedded methodology documents
7
8use crate::core::assets;
9use crate::core::capsule_policy::{GENERATED_POLICY_REL_PATH, default_policy_json_pretty};
10use crate::core::error;
11use crate::core::project_specs::{
12    LOCAL_PROJECT_SPECS, LOCAL_PROJECT_SPECS_ARCHITECTURE, LOCAL_PROJECT_SPECS_INTENT,
13    LOCAL_PROJECT_SPECS_INTERFACES, LOCAL_PROJECT_SPECS_MANIFEST,
14    LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA, LOCAL_PROJECT_SPECS_OPERATIONS,
15    LOCAL_PROJECT_SPECS_README, LOCAL_PROJECT_SPECS_SECURITY, LOCAL_PROJECT_SPECS_SEMANTICS,
16    LOCAL_PROJECT_SPECS_VALIDATION, ProjectSpecManifestEntry, ProjectSpecsManifest, hash_text,
17    repo_signal_fingerprint,
18};
19use crate::plugins::container;
20use std::fs;
21use std::path::{Path, PathBuf};
22
23/// Scaffolding operation configuration.
24///
25/// Controls how project initialization templates are written to disk.
26pub struct ScaffoldOptions {
27    /// Target directory for scaffold output (usually project root)
28    pub target_dir: PathBuf,
29    /// Force overwrite of existing files
30    pub force: bool,
31    /// Preview mode - log actions without writing files
32    pub dry_run: bool,
33    /// Which agent entrypoint files to generate (empty = all)
34    pub agent_files: Vec<String>,
35    /// Whether .bak files were created during init
36    pub created_backups: bool,
37    /// Force creation of all 5 entrypoint files regardless of existing state
38    pub all: bool,
39    /// Generate project-facing specs/ scaffolding.
40    pub generate_specs: bool,
41    /// Diagram style for generated architecture document.
42    pub diagram_style: DiagramStyle,
43    /// Intent/architecture seed captured from inferred or user-confirmed repo context.
44    pub specs_seed: Option<SpecsSeed>,
45}
46
47pub struct ScaffoldSummary {
48    pub entrypoints_created: usize,
49    pub entrypoints_unchanged: usize,
50    pub entrypoints_preserved: usize,
51    pub config_created: usize,
52    pub config_unchanged: usize,
53    pub config_preserved: usize,
54    pub specs_created: usize,
55    pub specs_unchanged: usize,
56    pub specs_preserved: usize,
57}
58
59#[derive(Clone, Copy, Debug)]
60pub enum DiagramStyle {
61    Ascii,
62    Mermaid,
63}
64
65#[derive(Clone, Debug)]
66pub struct SpecsSeed {
67    pub product_name: Option<String>,
68    pub product_summary: Option<String>,
69    pub architecture_direction: Option<String>,
70    pub product_type: Option<String>,
71    pub primary_languages: Vec<String>,
72    pub detected_surfaces: Vec<String>,
73    pub done_criteria: Option<String>,
74}
75
76fn joined_or_fallback(items: &[String], fallback: &str) -> String {
77    if items.is_empty() {
78        fallback.to_string()
79    } else {
80        items.join(", ")
81    }
82}
83
84fn default_test_commands(seed: Option<&SpecsSeed>) -> Vec<String> {
85    let mut commands = Vec::new();
86    let langs = seed.map(|s| s.primary_languages.as_slice()).unwrap_or(&[]);
87    let surfaces = seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]);
88
89    if langs.iter().any(|l| l.contains("rust")) {
90        commands.push("cargo test".to_string());
91    }
92    if surfaces.iter().any(|s| s == "npm")
93        || langs
94            .iter()
95            .any(|l| l.contains("typescript") || l.contains("javascript"))
96    {
97        commands.push("npm test".to_string());
98    }
99    if langs.iter().any(|l| l == "python") {
100        commands.push("pytest".to_string());
101    }
102    if langs.iter().any(|l| l == "go") {
103        commands.push("go test ./...".to_string());
104    }
105    commands
106}
107
108fn has_language(seed: Option<&SpecsSeed>, needle: &str) -> bool {
109    let needle = needle.to_ascii_lowercase();
110    seed.map(|s| {
111        s.primary_languages
112            .iter()
113            .any(|l| l.to_ascii_lowercase().contains(&needle))
114    })
115    .unwrap_or(false)
116}
117
118fn primary_language_name(seed: Option<&SpecsSeed>) -> String {
119    seed.and_then(|s| s.primary_languages.first().cloned())
120        .unwrap_or_else(|| "not detected yet".to_string())
121}
122
123fn language_specific_test_criteria(seed: Option<&SpecsSeed>) -> Vec<String> {
124    if has_language(seed, "rust") {
125        return vec![
126            "`cargo test` passes for unit/integration coverage".to_string(),
127            "`cargo clippy -- -D warnings` passes with no denied lints".to_string(),
128            "`cargo fmt --check` passes on the repo".to_string(),
129        ];
130    }
131    if has_language(seed, "python") {
132        return vec![
133            "`pytest -q` passes for unit/integration scenarios".to_string(),
134            "`ruff check .` passes for lint quality".to_string(),
135            "`mypy .` passes for typed modules in production paths".to_string(),
136        ];
137    }
138    if has_language(seed, "go") {
139        return vec![
140            "`go test ./...` passes for all packages".to_string(),
141            "`go vet ./...` passes with no diagnostics".to_string(),
142            "`gofmt -l .` returns no files".to_string(),
143        ];
144    }
145    if has_language(seed, "typescript") || has_language(seed, "javascript") {
146        return vec![
147            "`npm test` (or `pnpm test`) passes for unit/integration suites".to_string(),
148            "`npm run lint` passes".to_string(),
149            "`npm run typecheck` passes for strict TS projects".to_string(),
150        ];
151    }
152    vec!["Repository test/lint/typecheck commands are defined and wired into CI.".to_string()]
153}
154
155fn language_specific_error_example(seed: Option<&SpecsSeed>) -> String {
156    if has_language(seed, "rust") {
157        return r#"```rust
158#[derive(Debug, thiserror::Error)]
159pub enum ApiError {
160    #[error("validation failed: {0}")]
161    Validation(String),
162    #[error("upstream timeout")]
163    UpstreamTimeout,
164    #[error("conflict: {0}")]
165    Conflict(String),
166}
167```"#
168            .to_string();
169    }
170    if has_language(seed, "python") {
171        return r#"```python
172class ApiError(Exception):
173    def __init__(self, code: str, message: str) -> None:
174        self.code = code
175        self.message = message
176        super().__init__(f"{code}: {message}")
177```"#
178            .to_string();
179    }
180    if has_language(seed, "go") {
181        return r#"```go
182var (
183    ErrValidation = errors.New("validation_failed")
184    ErrTimeout    = errors.New("upstream_timeout")
185    ErrConflict   = errors.New("conflict")
186)
187```"#
188            .to_string();
189    }
190    r#"```ts
191export enum ApiErrorCode {
192  Validation = "validation_failed",
193  UpstreamTimeout = "upstream_timeout",
194  Conflict = "conflict"
195}
196```"#
197        .to_string()
198}
199
200fn language_specific_supply_chain_tools(seed: Option<&SpecsSeed>) -> Vec<String> {
201    if has_language(seed, "rust") {
202        return vec!["cargo audit", "cargo deny", "cargo vet"]
203            .into_iter()
204            .map(str::to_string)
205            .collect();
206    }
207    if has_language(seed, "python") {
208        return vec!["pip-audit", "safety", "bandit"]
209            .into_iter()
210            .map(str::to_string)
211            .collect();
212    }
213    if has_language(seed, "go") {
214        return vec!["govulncheck", "gosec", "nancy"]
215            .into_iter()
216            .map(str::to_string)
217            .collect();
218    }
219    vec!["npm audit", "osv-scanner", "snyk"]
220        .into_iter()
221        .map(str::to_string)
222        .collect()
223}
224
225fn language_specific_logging_hint(seed: Option<&SpecsSeed>) -> String {
226    if has_language(seed, "rust") {
227        return "Use `tracing` + `tracing-subscriber` with structured JSON output and request correlation ids.".to_string();
228    }
229    if has_language(seed, "python") {
230        return "Use `structlog` (or stdlib logging JSON formatter) with request_id, task_id, and outcome fields.".to_string();
231    }
232    if has_language(seed, "go") {
233        return "Use `zap` or `zerolog` with structured fields and propagated context ids."
234            .to_string();
235    }
236    "Use structured logging (pino/winston) with request_id, actor, latency_ms, and error_code fields.".to_string()
237}
238
239fn adaptive_topology_diagram(style: DiagramStyle, seed: Option<&SpecsSeed>) -> String {
240    let product_type = seed
241        .and_then(|s| s.product_type.as_deref())
242        .unwrap_or("service");
243    match style {
244        DiagramStyle::Ascii => {
245            if product_type.contains("cli") {
246                r#"```text
247User -> CLI Entrypoint -> Command Router -> Core Engine -> Local Store
248                                      \-> External API / Filesystem
249```"#
250                    .to_string()
251            } else if product_type.contains("frontend") {
252                r#"```text
253Browser -> UI Shell -> API Client -> Backend Gateway -> Datastores / Events
254```"#
255                    .to_string()
256            } else if product_type.contains("library") {
257                r#"```text
258Host Application -> Library API -> Domain Core -> Adapters (Store / Network)
259```"#
260                    .to_string()
261            } else {
262                r#"```text
263Client -> API Gateway -> Service Core -> Worker Queue -> Datastores
264```"#
265                    .to_string()
266            }
267        }
268        DiagramStyle::Mermaid => {
269            if product_type.contains("cli") {
270                r#"```mermaid
271flowchart LR
272  U[User] --> C[CLI Entrypoint]
273  C --> R[Command Router]
274  R --> E[Core Engine]
275  E --> S[(Local Store)]
276  E --> X[External APIs / Filesystem]
277```"#
278                    .to_string()
279            } else if product_type.contains("frontend") {
280                r#"```mermaid
281flowchart LR
282  B[Browser] --> UI[UI Shell]
283  UI --> A[API Client]
284  A --> G[Backend Gateway]
285  G --> DB[(Datastore)]
286  G --> Q[(Event Bus)]
287```"#
288                    .to_string()
289            } else if product_type.contains("library") {
290                r#"```mermaid
291flowchart LR
292  H[Host Application] --> L[Library API]
293  L --> D[Domain Core]
294  D --> AD[Adapter Layer]
295  AD --> DB[(Store)]
296  AD --> N[Network]
297```"#
298                    .to_string()
299            } else {
300                r#"```mermaid
301flowchart LR
302  C[Client] --> G[API Gateway]
303  G --> S[Service Core]
304  S --> W[Workers]
305  S --> DB[(Primary Datastore)]
306  W --> Q[(Queue)]
307```"#
308                    .to_string()
309            }
310        }
311    }
312}
313
314fn adaptive_happy_path_sequence(style: DiagramStyle, seed: Option<&SpecsSeed>) -> String {
315    let product_type = seed
316        .and_then(|s| s.product_type.as_deref())
317        .unwrap_or("service");
318    match style {
319        DiagramStyle::Ascii => {
320            if product_type.contains("cli") {
321                r#"```text
322User invokes command -> CLI parses args -> Core executes action -> state persists -> result printed
323```"#
324                    .to_string()
325            } else if product_type.contains("frontend") {
326                r#"```text
327User action -> UI validates input -> API request -> backend persists -> UI renders success
328```"#
329                    .to_string()
330            } else {
331                r#"```text
332Client request -> API validation -> domain execution -> persistence -> response with trace id
333```"#
334                    .to_string()
335            }
336        }
337        DiagramStyle::Mermaid => {
338            if product_type.contains("cli") {
339                r#"```mermaid
340sequenceDiagram
341  participant U as User
342  participant C as CLI
343  participant E as Core Engine
344  participant S as Store
345  U->>C: Run command
346  C->>E: Parse + validate
347  E->>S: Persist mutation
348  S-->>E: Ack
349  E-->>C: Result
350  C-->>U: Structured output
351```"#
352                    .to_string()
353            } else if product_type.contains("frontend") {
354                r#"```mermaid
355sequenceDiagram
356  participant U as User
357  participant UI as Frontend
358  participant API as Backend API
359  participant DB as Datastore
360  U->>UI: Submit action
361  UI->>API: Authenticated request
362  API->>DB: Write transaction
363  DB-->>API: Commit ok
364  API-->>UI: 200 + payload
365  UI-->>U: Updated view
366```"#
367                    .to_string()
368            } else {
369                r#"```mermaid
370sequenceDiagram
371  participant C as Client
372  participant G as API
373  participant D as Domain
374  participant DB as Datastore
375  C->>G: Request
376  G->>D: Validate + execute
377  D->>DB: Commit transaction
378  DB-->>D: Commit ok
379  D-->>G: Domain result
380  G-->>C: Response + trace_id
381```"#
382                    .to_string()
383            }
384        }
385    }
386}
387
388fn specs_readme_template(seed: Option<&SpecsSeed>) -> String {
389    let product = seed
390        .and_then(|s| s.product_name.as_deref())
391        .unwrap_or("this repository");
392    let summary = seed
393        .and_then(|s| s.product_summary.as_deref())
394        .unwrap_or("Define the intended user-visible outcome.");
395    let languages = joined_or_fallback(
396        seed.map(|s| s.primary_languages.as_slice()).unwrap_or(&[]),
397        "not detected yet",
398    );
399    let surfaces = joined_or_fallback(
400        seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]),
401        "not detected yet",
402    );
403
404    format!(
405        r#"# Project Specs
406
407Canonical path: `.decapod/generated/specs/`.
408These files are the project-local contract for humans and agents.
409
410## Snapshot
411- Project: {product}
412- Outcome: {summary}
413- Detected languages: {languages}
414- Detected surfaces: {surfaces}
415
416## How to use this folder
417- `INTENT.md`: what success means and what is explicitly out of scope.
418- `ARCHITECTURE.md`: topology, runtime model, data boundaries, and ADR trail.
419- `INTERFACES.md`: API/CLI/events/storage contracts and failure behavior.
420- `VALIDATION.md`: proof commands, quality gates, and evidence artifacts.
421- `SEMANTICS.md`: state machines, invariants, replay rules, and idempotency.
422- `OPERATIONS.md`: SLOs, monitoring, incident response, and rollout strategy.
423- `SECURITY.md`: threat model, trust boundaries, auth/authz, and supply-chain posture.
424
425## Canonical `.decapod/` Layout
426- `.decapod/data/`: canonical control-plane state (SQLite + ledgers).
427- `.decapod/generated/specs/`: living project specs for humans and agents.
428- `.decapod/generated/context/`: deterministic context capsules.
429- `.decapod/generated/policy/context_capsule_policy.json`: repo-native JIT context policy contract.
430- `.decapod/generated/artifacts/provenance/`: promotion manifests and convergence checklist.
431- `.decapod/generated/artifacts/inventory/`: deterministic release inventory.
432- `.decapod/generated/artifacts/diagnostics/`: opt-in diagnostics artifacts.
433- `.decapod/workspaces/`: isolated todo-scoped git worktrees.
434
435## Day-0 Onboarding Checklist
436- [ ] Replace all placeholders in all 8 spec files.
437- [ ] Confirm primary user outcome and acceptance criteria in `INTENT.md`.
438- [ ] Confirm topology and runtime model in `ARCHITECTURE.md`.
439- [ ] Document all inbound/outbound contracts in `INTERFACES.md`.
440- [ ] Define validation gates and CI proof surfaces in `VALIDATION.md`.
441- [ ] Define state machines and invariants in `SEMANTICS.md`.
442- [ ] Define SLOs, alerting, and incident process in `OPERATIONS.md`.
443- [ ] Define threat model and auth/authz decisions in `SECURITY.md`.
444- [ ] Ensure architecture diagram, docs, changelog, and tests are mapped to promotion gates.
445- [ ] Run all validation/test commands and attach evidence artifacts.
446
447## Agent Directive
448- Treat these files as executable governance surfaces. Before implementation: resolve ambiguity and update specs. After implementation: refresh drifted sections, rerun proof gates, and attach evidence.
449"#
450    )
451}
452
453fn specs_intent_template(seed: Option<&SpecsSeed>) -> String {
454    let product_outcome = seed
455        .and_then(|s| s.product_summary.as_deref())
456        .unwrap_or("Define the user-visible outcome in one paragraph.");
457    let done_criteria = seed
458        .and_then(|s| s.done_criteria.as_deref())
459        .unwrap_or("Functional behavior is demonstrably correct.");
460    let product_name = seed
461        .and_then(|s| s.product_name.as_deref())
462        .unwrap_or("this repository");
463    let product_type = seed
464        .and_then(|s| s.product_type.as_deref())
465        .unwrap_or("not classified yet");
466    let languages = joined_or_fallback(
467        seed.map(|s| s.primary_languages.as_slice()).unwrap_or(&[]),
468        "not detected yet",
469    );
470    let surfaces = joined_or_fallback(
471        seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]),
472        "not detected yet",
473    );
474    let language_criteria = language_specific_test_criteria(seed)
475        .into_iter()
476        .map(|s| format!("- [ ] {}", s))
477        .collect::<Vec<_>>()
478        .join("\n");
479
480    format!(
481        r#"# Intent
482
483## Product Outcome
484- {product_outcome}
485
486## Product View
487```mermaid
488flowchart LR
489  U[Primary User] --> P[{product_name}]
490  P --> O[User-visible Outcome]
491  P --> G[Proof Gates]
492  G --> E[Evidence Artifacts]
493```
494
495## Inferred Baseline
496- Repository: {product_name}
497- Product type: {product_type}
498- Primary languages: {languages}
499- Detected surfaces: {surfaces}
500
501## Scope
502| Area | In Scope | Proof Surface |
503|---|---|---|
504| Core workflow | Define a concrete user-visible workflow | Acceptance criteria + tests |
505| Data contracts | Document canonical inputs/outputs | `INTERFACES.md` and schema checks |
506| Delivery quality | Block promotion on broken proof surfaces | `VALIDATION.md` blocking gates |
507
508## Non-Goals (Falsifiable)
509| Non-goal | How to falsify |
510|---|---|
511| Feature creep beyond the primary outcome | Any PR adds capability not tied to outcome criteria |
512| Shipping without evidence | Missing validation artifacts for promoted changes |
513| Ambiguous ownership boundaries | Missing owner/system-of-record in interfaces |
514
515## Constraints
516- Technical: runtime, dependency, and topology boundaries are explicit.
517- Operational: deployment, rollback, and incident ownership are defined.
518- Security/compliance: sensitive data handling and authz are mandatory.
519
520## Acceptance Criteria (must be objectively testable)
521- [ ] {done_criteria}
522- [ ] Non-functional targets are met (latency, reliability, cost, etc.).
523- [ ] Validation gates pass and artifacts are attached.
524{language_criteria}
525
526## Tradeoffs Register
527| Decision | Benefit | Cost | Review Trigger |
528|---|---|---|---|
529| Simplicity vs extensibility | Faster iteration | Potential rework | Feature set expands |
530| Strict gates vs dev speed | Higher confidence | More upfront discipline | Lead time regressions |
531
532## First Implementation Slice
533- [ ] Define the smallest user-visible workflow to ship first.
534- [ ] Define required data/contracts for that workflow.
535- [ ] Define what is intentionally postponed until v2.
536
537## Open Questions (with decision deadlines)
538| Question | Owner | Deadline | Decision |
539|---|---|---|---|
540| Which interfaces are versioned at launch? | TBD | YYYY-MM-DD | |
541| Which non-functional target is hardest to hit? | TBD | YYYY-MM-DD | |
542"#
543    )
544}
545
546fn specs_architecture_template(style: DiagramStyle, seed: Option<&SpecsSeed>) -> String {
547    let summary = seed
548        .and_then(|s| s.architecture_direction.as_deref())
549        .unwrap_or(
550            "Describe architecture in deployment-reality terms: runtime boundaries, operational ownership, and failure containment.",
551        );
552    let runtime_langs = seed
553        .map(|s| s.primary_languages.join(", "))
554        .filter(|s| !s.trim().is_empty())
555        .unwrap_or_else(|| "to be confirmed".to_string());
556    let surfaces = seed
557        .map(|s| s.detected_surfaces.join(", "))
558        .filter(|s| !s.trim().is_empty())
559        .unwrap_or_else(|| "to be confirmed".to_string());
560    let product_type = seed
561        .and_then(|s| s.product_type.as_deref())
562        .unwrap_or("to be confirmed");
563
564    format!(
565        r#"# Architecture
566
567## Direction
568{summary}
569
570## Current Facts
571- Runtime/languages: {runtime_langs}
572- Detected surfaces/framework hints: {surfaces}
573- Product type: {product_type}
574
575## Topology
576{topology}
577
578## Store Boundaries
579```mermaid
580flowchart LR
581  I[Inbound Requests] --> C[Core Runtime]
582  C --> W[(Write Store)]
583  C --> R[(Read Store)]
584  C --> E[External Dependency]
585  E --> DLQ[(DLQ / Retry Queue)]
586```
587
588## Happy Path Sequence
589{happy_path}
590
591## Error Path
592```mermaid
593sequenceDiagram
594  participant Client
595  participant API
596  participant Upstream
597  Client->>API: Request
598  API->>Upstream: Call with timeout budget
599  Upstream--xAPI: Timeout / failure
600  API-->>Client: Typed error + retry guidance + trace_id
601```
602
603## Execution Path
604- Ingress parse + validation:
605- Policy/interlock checks:
606- Core execution + persistence:
607- Verification and artifact emission:
608
609## Concurrency and Runtime Model
610- Execution model:
611- Isolation boundaries:
612- Backpressure strategy:
613- Shared state synchronization:
614
615## Deployment Topology
616- Runtime units:
617- Region/zone model:
618- Rollout strategy (blue/green/canary):
619- Rollback trigger and blast-radius scope:
620
621## Data and Contracts
622- Inbound contracts (CLI/API/events):
623- Outbound dependencies (datastores/queues/external APIs):
624- Data ownership boundaries:
625- Schema evolution + migration policy:
626
627## ADR Register
628| ADR | Title | Status | Rationale | Date |
629|---|---|---|---|---|
630| ADR-001 | Initial topology choice | Proposed | Define first stable architecture | YYYY-MM-DD |
631
632## Delivery Plan (first 3 slices)
633- Slice 1 (ship first):
634- Slice 2:
635- Slice 3:
636
637## Risks and Mitigations
638| Risk | Likelihood | Impact | Mitigation |
639|---|---|---|---|
640| Contract drift across components | Medium | High | Spec + schema checks in CI |
641| Runtime saturation under peak load | Medium | High | Capacity model + load tests |
642"#,
643        topology = adaptive_topology_diagram(style, seed),
644        happy_path = adaptive_happy_path_sequence(style, seed),
645    )
646}
647
648fn specs_interfaces_template(seed: Option<&SpecsSeed>) -> String {
649    let surfaces = joined_or_fallback(
650        seed.map(|s| s.detected_surfaces.as_slice()).unwrap_or(&[]),
651        "not detected yet",
652    );
653    let product_type = seed
654        .and_then(|s| s.product_type.as_deref())
655        .unwrap_or("not classified yet");
656    let error_example = language_specific_error_example(seed);
657
658    format!(
659        r#"# Interfaces
660
661## Contract Principles
662- Prefer explicit schemas over implicit behavior.
663- Every mutating interface defines idempotency semantics.
664- Every failure path maps to a typed, documented error code.
665
666## API / RPC Contracts
667| Interface | Method | Request Schema | Response Schema | Errors | Idempotency |
668|---|---|---|---|---|---|
669| `TODO` | `TODO` | `TODO` | `TODO` | `TODO` | `TODO` |
670
671## Event Consumers
672| Consumer | Event | Ordering Requirement | Retry Policy | DLQ Policy |
673|---|---|---|---|---|
674| `TODO` | `TODO` | `TODO` | `TODO` | `TODO` |
675
676## Outbound Dependencies
677| Dependency | Purpose | SLA | Timeout | Circuit-Breaker |
678|---|---|---|---|---|
679| `TODO` | `TODO` | `TODO` | `TODO` | `TODO` |
680
681## Inbound Contracts
682- API / RPC entrypoints:
683- CLI surfaces:
684- Event/webhook consumers:
685- Repository-detected surfaces: {surfaces}
686
687## Data Ownership
688- Source-of-truth tables/collections:
689- Cross-boundary read models:
690- Consistency expectations:
691
692## Error Taxonomy Example ({product_type})
693{error_example}
694
695## Failure Semantics
696| Failure Class | Retry/Backoff | Client Contract | Observability |
697|---|---|---|---|
698| Validation | No retry | 4xx typed error | warn log + metric |
699| Dependency timeout | Exponential backoff | 503 with retryable code | error log + alert |
700| Conflict | Conditional retry | 409 with conflict detail | info log + metric |
701
702## Timeout Budget
703| Hop | Budget (ms) | Notes |
704|---|---|---|
705| Client -> Edge/API | 500 | Includes auth + routing |
706| API -> Domain | 300 | Includes validation |
707| Domain -> Store/Dependency | 200 | Includes retry overhead |
708
709## Interface Versioning
710- Version strategy (`v1`, date-based, semver):
711- Backward-compatibility guarantees:
712- Deprecation window and removal policy:
713"#
714    )
715}
716
717fn specs_validation_template(seed: Option<&SpecsSeed>) -> String {
718    let commands = default_test_commands(seed);
719    let test_commands = if commands.is_empty() {
720        "- Add repository-specific test command(s) here.".to_string()
721    } else {
722        commands
723            .into_iter()
724            .map(|c| format!("- `{}`", c))
725            .collect::<Vec<_>>()
726            .join("\n")
727    };
728    format!(
729        r#"# Validation
730
731## Validation Philosophy
732> Validation is a release gate, not documentation theater.
733
734## Validation Decision Tree
735```mermaid
736flowchart TD
737  S[Start] --> W{{Workspace valid?}}
738  W -->|No| F1[Fail: workspace gate]
739  W -->|Yes| T{{Tests pass?}}
740  T -->|No| F2[Fail: test gate]
741  T -->|Yes| D{{Docs + diagrams + changelog updated?}}
742  D -->|No| F3[Fail: docs gate]
743  D -->|Yes| V[Run decapod validate]
744  V --> P{{All blocking gates pass?}}
745  P -->|No| F4[Fail: promotion blocked]
746  P -->|Yes| E[Emit promotion evidence]
747```
748
749## Promotion Flow
750```mermaid
751flowchart LR
752  A[Plan] --> B[Implement]
753  B --> C[Test]
754  C --> D[Validate]
755  D --> E[Assemble Evidence]
756  E --> F[Promote]
757```
758
759## Proof Surfaces
760- `decapod validate`
761- Required test commands:
762{test_commands}
763- Required integration/e2e commands:
764
765## Promotion Gates
766
767## Blocking Gates
768| Gate | Command | Evidence |
769|---|---|---|
770| Architecture + interface drift check | `decapod validate` | Gate output |
771| Tests pass | project test command | CI + local logs |
772| Docs + changelog current | repo docs checks | PR diff |
773| Security critical checks pass | security scanner suite | scanner reports |
774
775## Warning Gates
776| Gate | Trigger | Follow-up SLA |
777|---|---|---|
778| Coverage regression warning | Coverage drops below target | 48h |
779| Non-blocking perf drift | P95 regression below hard threshold | 72h |
780
781## Evidence Artifacts
782| Artifact | Path | Required For |
783|---|---|---|
784| Validation report | `.decapod/generated/artifacts/provenance/*` | Promotion |
785| Test logs | CI artifact store | Promotion |
786| Architecture diagram snapshot | `ARCHITECTURE.md` | Promotion |
787| Changelog entry | `CHANGELOG.md` | Promotion |
788
789## Regression Guardrails
790- Baseline references:
791- Statistical thresholds (if non-deterministic):
792- Rollback criteria:
793
794## Bounded Execution
795| Operation | Timeout | Failure Mode |
796|---|---|---|
797| Validation | 30s | timeout or lock |
798| Unit test suite | project-defined | non-zero exit |
799| Integration suite | project-defined | non-zero exit |
800
801## Coverage Checklist
802- [ ] Unit tests cover critical branches.
803- [ ] Integration tests cover key user flows.
804- [ ] Failure-path tests cover retries/timeouts.
805- [ ] Docs/diagram/changelog updates included.
806"#
807    )
808}
809
810fn specs_semantics_template(seed: Option<&SpecsSeed>) -> String {
811    let lang = primary_language_name(seed);
812    format!(
813        r#"# Semantics
814
815## State Machines
816```mermaid
817stateDiagram-v2
818  [*] --> Draft
819  Draft --> InProgress
820  InProgress --> Verified
821  InProgress --> Blocked
822  Blocked --> InProgress
823  Verified --> [*]
824```
825
826| From | Event | To | Guard | Side Effect |
827|---|---|---|---|---|
828| Draft | start | InProgress | owner assigned | emit state change |
829| InProgress | validate_pass | Verified | all blocking gates pass | persist receipt hash |
830| InProgress | dependency_blocked | Blocked | external dependency unavailable | emit alert event |
831
832## Invariants
833| Invariant | Type | Validation |
834|---|---|---|
835| No promoted change without proof | System | validation gate |
836| Canonical source-of-truth per entity | Data | interface/spec review |
837| Mutation events are replayable | Data | deterministic replay |
838
839## Event Sourcing Schema
840| Field | Type | Description |
841|---|---|---|
842| event_id | string | globally unique event id |
843| aggregate_id | string | entity/workflow id |
844| event_type | string | semantic transition |
845| payload | object | transition data |
846| recorded_at | timestamp | append time |
847
848## Replay Semantics
849- Replay order:
850- Conflict resolution:
851- Snapshot cadence:
852- Determinism proof strategy:
853
854## Error Code Semantics
855- Namespace:
856- Stable compatibility window:
857- Mapping to retry/degrade behavior:
858
859## Domain Rules
860- Business rule 1:
861- Business rule 2:
862- Business rule 3:
863
864## Idempotency Contracts
865| Operation | Idempotency Key | Duplicate Behavior |
866|---|---|---|
867| create/update mutation | request_id | return original result |
868| async enqueue | event_id | ignore duplicate enqueue |
869
870## Language Note
871- Primary language inferred: {lang}
872"#
873    )
874}
875
876fn specs_operations_template(seed: Option<&SpecsSeed>) -> String {
877    let logging_hint = language_specific_logging_hint(seed);
878    format!(
879        r#"# Operations
880
881## Operational Readiness Checklist
882- [ ] On-call ownership defined.
883- [ ] SLOs and alert thresholds defined.
884- [ ] Dashboards for latency/errors/throughput are live.
885- [ ] Runbooks linked for all Sev1/Sev2 alerts.
886- [ ] Rollback plan validated.
887- [ ] Capacity guardrails documented.
888
889## Service Level Objectives
890| SLI | SLO Target | Measurement Window | Owner |
891|---|---|---|---|
892| Availability | 99.9% | 30d | TBD |
893| P95 latency | TBD | 7d | TBD |
894| Error rate | < 1% | 7d | TBD |
895
896## Monitoring
897| Signal | Metric | Threshold | Alert |
898|---|---|---|---|
899| Traffic | requests/sec | baseline drift | warn |
900| Latency | p95/p99 | threshold breach | page |
901| Reliability | error ratio | threshold breach | page |
902| Saturation | cpu/memory/queue depth | sustained high | page |
903
904## Health Checks
905- Liveness:
906- Readiness:
907- Dependency health:
908- Synthetic transaction:
909
910## Alerting and Runbooks
911| Alert | Severity | Runbook Link | Escalation |
912|---|---|---|---|
913| API error rate spike | Sev2 | TBD | App on-call |
914| Persistent dependency timeout | Sev1 | TBD | App + platform |
915| Validation gate outage | Sev2 | TBD | Maintainers |
916
917## Incident Response
918- Incident commander model:
919- Communication channels:
920- Postmortem SLA:
921- Corrective action tracking:
922
923## Structured Logging
924- {logging_hint}
925
926## Severity Definitions
927| Severity | Definition | Response Time |
928|---|---|---|
929| Sev1 | Production outage or data integrity risk | Immediate |
930| Sev2 | Major functionality impaired | 30 minutes |
931| Sev3 | Minor degradation | Next business day |
932
933## Deployment Strategy
934- Primary strategy:
935- Change validation process:
936- Rollback and forward-fix policy:
937
938## Environment Configuration
939| Variable | Purpose | Default | Secret |
940|---|---|---|---|
941| APP_ENV | runtime environment | dev | no |
942| LOG_LEVEL | observability verbosity | info | no |
943| API_TOKEN | external auth | none | yes |
944
945## Capacity Planning
946- Peak request assumption:
947- Storage growth model:
948- Queue/worker headroom:
949"#
950    )
951}
952
953fn specs_security_template(seed: Option<&SpecsSeed>) -> String {
954    let scanners = language_specific_supply_chain_tools(seed)
955        .into_iter()
956        .map(|tool| format!("`{}`", tool))
957        .collect::<Vec<_>>()
958        .join(", ");
959    format!(
960        r#"# Security
961
962## Threat Model
963```mermaid
964flowchart LR
965  U[User/Client] --> A[Application Boundary]
966  A --> D[(Data Stores)]
967  A --> X[External Dependencies]
968  I[Identity Provider] --> A
969  A --> L[Audit Logs]
970```
971
972## STRIDE Table
973| Threat | Surface | Mitigation | Verification |
974|---|---|---|---|
975| Spoofing | Auth boundary | strong auth + token validation | auth tests |
976| Tampering | State mutation APIs | integrity checks + RBAC | integration tests |
977| Repudiation | Critical actions | immutable audit logs | log review |
978| Information disclosure | Data at rest/in transit | encryption + classification | security scans |
979| Denial of service | Hot paths | rate limit + backpressure | load tests |
980| Elevation of privilege | Admin interfaces | least privilege + policy checks | authz tests |
981
982## Authentication
983- Identity source:
984- Token/session lifetime:
985- Rotation and revocation:
986
987## Authorization
988- Role model:
989- Resource-level policy:
990- Privilege escalation controls:
991
992## Data Classification
993| Data Class | Examples | Storage Rules | Access Rules |
994|---|---|---|---|
995| Public | docs, non-sensitive metadata | standard | unrestricted |
996| Internal | operational telemetry | controlled | team access |
997| Sensitive | tokens, PII, secrets | encrypted | least privilege |
998
999## Sensitive Data Handling
1000- Encryption at rest:
1001- Encryption in transit:
1002- Redaction in logs:
1003- Retention + deletion policy:
1004
1005## Supply Chain Security
1006- Recommended scanners: {scanners}
1007- Dependency update cadence:
1008- Signed artifact/provenance strategy:
1009
1010## Secrets Management
1011| Secret | Source | Rotation | Consumer |
1012|---|---|---|---|
1013| API credentials | secret manager/env | periodic | runtime services |
1014| Signing keys | HSM/KMS/local secure store | periodic | release pipeline |
1015
1016## Security Testing
1017| Test Type | Cadence | Tooling |
1018|---|---|---|
1019| SAST | each PR | language linters/scanners |
1020| Dependency scan | each PR + weekly | supply-chain tools |
1021| DAST/pentest | scheduled | external/internal |
1022
1023## Compliance and Audit
1024- Regulatory scope:
1025- Audit evidence location:
1026- Exception process:
1027
1028## Pre-Promotion Security Checklist
1029- [ ] Threat model updated for changed surfaces.
1030- [ ] Auth/authz tests pass.
1031- [ ] Dependency vulnerability scan reviewed.
1032- [ ] No unresolved critical/high security findings.
1033"#
1034    )
1035}
1036
1037/// Canonical .gitignore rules managed by `decapod init`.
1038///
1039/// These rules are appended (if missing) to the user's root `.gitignore`.
1040/// Keep this as the source of truth so new allowlists/denylists evolve through code review.
1041pub const DECAPOD_GITIGNORE_RULES: &[&str] = &[
1042    ".decapod/data",
1043    ".decapod/data/*",
1044    ".decapod/.stfolder",
1045    ".decapod/workspaces",
1046    ".decapod/generated/*",
1047    "!.decapod/data/",
1048    "!.decapod/data/knowledge.promotions.jsonl",
1049    "!.decapod/generated/Dockerfile",
1050    "!.decapod/generated/context/",
1051    "!.decapod/generated/context/*.json",
1052    "!.decapod/generated/policy/",
1053    "!.decapod/generated/policy/context_capsule_policy.json",
1054    "!.decapod/generated/artifacts/",
1055    "!.decapod/generated/artifacts/provenance/",
1056    "!.decapod/generated/artifacts/provenance/*.json",
1057    "!.decapod/generated/artifacts/provenance/kcr_trend.jsonl",
1058    "!.decapod/generated/specs/",
1059    "!.decapod/generated/specs/*.md",
1060    "!.decapod/generated/specs/.manifest.json",
1061];
1062
1063/// Ensure a given entry exists in the project's .gitignore file.
1064/// Creates the file if it doesn't exist. Appends the entry if not already present.
1065fn ensure_gitignore_entry(target_dir: &Path, entry: &str) -> Result<(), error::DecapodError> {
1066    let gitignore_path = target_dir.join(".gitignore");
1067    let content = fs::read_to_string(&gitignore_path).unwrap_or_default();
1068
1069    // Check if the entry already exists (exact line match)
1070    if content.lines().any(|line| line.trim() == entry) {
1071        return Ok(());
1072    }
1073
1074    let mut new_content = content;
1075    if !new_content.is_empty() && !new_content.ends_with('\n') {
1076        new_content.push('\n');
1077    }
1078    new_content.push_str(entry);
1079    new_content.push('\n');
1080    fs::write(&gitignore_path, new_content).map_err(error::DecapodError::IoError)?;
1081    Ok(())
1082}
1083
1084fn ensure_parent(path: &Path) -> Result<(), error::DecapodError> {
1085    if let Some(p) = path.parent() {
1086        fs::create_dir_all(p).map_err(error::DecapodError::IoError)?;
1087    }
1088    Ok(())
1089}
1090
1091#[derive(Clone, Copy, Debug)]
1092pub enum FileAction {
1093    Created,
1094    Unchanged,
1095    Preserved,
1096}
1097
1098fn write_file(
1099    opts: &ScaffoldOptions,
1100    rel_path: &str,
1101    content: &str,
1102) -> Result<FileAction, error::DecapodError> {
1103    use sha2::{Digest, Sha256};
1104
1105    let dest = opts.target_dir.join(rel_path);
1106
1107    if dest.exists() {
1108        if let Ok(existing_content) = fs::read_to_string(&dest) {
1109            let mut template_hasher = Sha256::new();
1110            template_hasher.update(content.as_bytes());
1111            let template_hash = format!("{:x}", template_hasher.finalize());
1112
1113            let mut existing_hasher = Sha256::new();
1114            existing_hasher.update(existing_content.as_bytes());
1115            let existing_hash = format!("{:x}", existing_hasher.finalize());
1116
1117            if template_hash == existing_hash {
1118                return Ok(FileAction::Unchanged);
1119            }
1120        }
1121
1122        if !opts.force {
1123            if opts.dry_run {
1124                return Ok(FileAction::Unchanged);
1125            }
1126            return Err(error::DecapodError::ValidationError(format!(
1127                "Refusing to overwrite existing path without --force: {}",
1128                dest.display()
1129            )));
1130        }
1131    }
1132
1133    if opts.dry_run {
1134        return Ok(FileAction::Created);
1135    }
1136
1137    ensure_parent(&dest)?;
1138    fs::write(&dest, content).map_err(error::DecapodError::IoError)?;
1139
1140    Ok(FileAction::Created)
1141}
1142
1143pub fn scaffold_project_entrypoints(
1144    opts: &ScaffoldOptions,
1145) -> Result<ScaffoldSummary, error::DecapodError> {
1146    let data_dir_rel = ".decapod/data";
1147
1148    // Ensure .decapod/data directory exists (constitution is embedded, not scaffolded)
1149    fs::create_dir_all(opts.target_dir.join(data_dir_rel)).map_err(error::DecapodError::IoError)?;
1150
1151    // Ensure Decapod-managed ignore/allowlist rules are present in the user's .gitignore.
1152    if !opts.dry_run {
1153        for rule in DECAPOD_GITIGNORE_RULES {
1154            ensure_gitignore_entry(&opts.target_dir, rule)?;
1155        }
1156    }
1157
1158    // Determine which agent files to generate
1159    // If --all flag is set, force generate all five regardless of existing state
1160    // If agent_files is empty, generate all five
1161    // If agent_files has entries, only generate those
1162    let files_to_generate = if opts.all || opts.agent_files.is_empty() {
1163        vec!["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"]
1164    } else {
1165        opts.agent_files.iter().map(|s| s.as_str()).collect()
1166    };
1167
1168    // Root entrypoints from embedded templates
1169    let readme_md = assets::get_template("README.md").expect("Missing template: README.md");
1170    let override_md = assets::get_template("OVERRIDE.md").expect("Missing template: OVERRIDE.md");
1171
1172    // AGENT ENTRYPOINTS - Neural Interfaces (only generate specified files)
1173    let mut ep_created = 0usize;
1174    let mut ep_unchanged = 0usize;
1175    let mut ep_preserved = 0usize;
1176    for file in files_to_generate {
1177        let content =
1178            assets::get_template(file).unwrap_or_else(|| panic!("Missing template: {}", file));
1179        match write_file(opts, file, &content)? {
1180            FileAction::Created => ep_created += 1,
1181            FileAction::Unchanged => ep_unchanged += 1,
1182            FileAction::Preserved => ep_preserved += 1,
1183        }
1184    }
1185
1186    let mut cfg_created = 0usize;
1187    let mut cfg_unchanged = 0usize;
1188    let mut cfg_preserved = 0usize;
1189
1190    match write_file(opts, ".decapod/README.md", &readme_md)? {
1191        FileAction::Created => cfg_created += 1,
1192        FileAction::Unchanged => cfg_unchanged += 1,
1193        FileAction::Preserved => cfg_preserved += 1,
1194    }
1195
1196    // Preserve existing OVERRIDE.md - it contains project-specific customizations.
1197    let override_path = opts.target_dir.join(".decapod/OVERRIDE.md");
1198    if override_path.exists() {
1199        cfg_preserved += 1;
1200    } else {
1201        match write_file(opts, ".decapod/OVERRIDE.md", &override_md)? {
1202            FileAction::Created => cfg_created += 1,
1203            FileAction::Unchanged => cfg_unchanged += 1,
1204            FileAction::Preserved => cfg_preserved += 1,
1205        }
1206    }
1207
1208    // Blend legacy agent files if they existed before init
1209    if !opts.dry_run {
1210        blend_legacy_entrypoints(&opts.target_dir)?;
1211    }
1212
1213    // Generate .decapod/generated/Dockerfile from Rust-owned template component.
1214    let generated_dir = opts.target_dir.join(".decapod/generated");
1215    fs::create_dir_all(&generated_dir).map_err(error::DecapodError::IoError)?;
1216    fs::create_dir_all(generated_dir.join("context")).map_err(error::DecapodError::IoError)?;
1217    fs::create_dir_all(generated_dir.join("policy")).map_err(error::DecapodError::IoError)?;
1218    fs::create_dir_all(generated_dir.join("artifacts").join("provenance"))
1219        .map_err(error::DecapodError::IoError)?;
1220    fs::create_dir_all(generated_dir.join("artifacts").join("inventory"))
1221        .map_err(error::DecapodError::IoError)?;
1222    fs::create_dir_all(
1223        generated_dir
1224            .join("artifacts")
1225            .join("diagnostics")
1226            .join("validate"),
1227    )
1228    .map_err(error::DecapodError::IoError)?;
1229    fs::create_dir_all(generated_dir.join("migrations")).map_err(error::DecapodError::IoError)?;
1230    let dockerfile_path = generated_dir.join("Dockerfile");
1231    if !dockerfile_path.exists() {
1232        let dockerfile_content = container::generated_dockerfile_for_repo(&opts.target_dir);
1233        fs::write(&dockerfile_path, dockerfile_content).map_err(error::DecapodError::IoError)?;
1234    }
1235    let version_counter_path = generated_dir.join("version_counter.json");
1236    if !version_counter_path.exists() {
1237        let now = crate::core::time::now_epoch_z();
1238        let version_counter = serde_json::json!({
1239            "schema_version": "1.0.0",
1240            "version_count": 1,
1241            "initialized_with_version": env!("CARGO_PKG_VERSION"),
1242            "last_seen_version": env!("CARGO_PKG_VERSION"),
1243            "updated_at": now,
1244        });
1245        let body = serde_json::to_string_pretty(&version_counter).map_err(|e| {
1246            error::DecapodError::ValidationError(format!(
1247                "Failed to serialize version counter: {}",
1248                e
1249            ))
1250        })?;
1251        fs::write(version_counter_path, body).map_err(error::DecapodError::IoError)?;
1252    }
1253
1254    let generated_policy_path = opts.target_dir.join(GENERATED_POLICY_REL_PATH);
1255    if !generated_policy_path.exists() {
1256        let policy_body = default_policy_json_pretty()?;
1257        fs::write(generated_policy_path, policy_body).map_err(error::DecapodError::IoError)?;
1258    }
1259
1260    let (specs_created, specs_unchanged, specs_preserved) = if opts.generate_specs {
1261        let mut created = 0usize;
1262        let mut unchanged = 0usize;
1263        let mut preserved = 0usize;
1264        let mut manifest_entries: Vec<ProjectSpecManifestEntry> = Vec::new();
1265
1266        let seed = opts.specs_seed.as_ref();
1267        let mut specs_files: Vec<(&str, String)> = Vec::new();
1268        for spec in LOCAL_PROJECT_SPECS {
1269            let content = match spec.path {
1270                LOCAL_PROJECT_SPECS_README => specs_readme_template(seed),
1271                LOCAL_PROJECT_SPECS_INTENT => specs_intent_template(seed),
1272                LOCAL_PROJECT_SPECS_ARCHITECTURE => {
1273                    specs_architecture_template(opts.diagram_style, seed)
1274                }
1275                LOCAL_PROJECT_SPECS_INTERFACES => specs_interfaces_template(seed),
1276                LOCAL_PROJECT_SPECS_VALIDATION => specs_validation_template(seed),
1277                LOCAL_PROJECT_SPECS_SEMANTICS => specs_semantics_template(seed),
1278                LOCAL_PROJECT_SPECS_OPERATIONS => specs_operations_template(seed),
1279                LOCAL_PROJECT_SPECS_SECURITY => specs_security_template(seed),
1280                _ => continue,
1281            };
1282            specs_files.push((spec.path, content));
1283        }
1284
1285        for (rel_path, content) in specs_files {
1286            let template_hash = hash_text(&content);
1287            match write_file(opts, rel_path, &content)? {
1288                FileAction::Created => created += 1,
1289                FileAction::Unchanged => unchanged += 1,
1290                FileAction::Preserved => preserved += 1,
1291            }
1292            manifest_entries.push(ProjectSpecManifestEntry {
1293                path: rel_path.to_string(),
1294                template_hash: template_hash.clone(),
1295                content_hash: template_hash,
1296            });
1297        }
1298
1299        if !opts.dry_run {
1300            let manifest = ProjectSpecsManifest {
1301                schema_version: LOCAL_PROJECT_SPECS_MANIFEST_SCHEMA.to_string(),
1302                template_version: "scaffold-v2".to_string(),
1303                generated_at: crate::core::time::now_epoch_z(),
1304                repo_signal_fingerprint: repo_signal_fingerprint(&opts.target_dir)?,
1305                files: manifest_entries,
1306            };
1307            let manifest_path = opts.target_dir.join(LOCAL_PROJECT_SPECS_MANIFEST);
1308            ensure_parent(&manifest_path)?;
1309            let manifest_body = serde_json::to_string_pretty(&manifest).map_err(|e| {
1310                error::DecapodError::ValidationError(format!(
1311                    "Failed to serialize specs manifest: {}",
1312                    e
1313                ))
1314            })?;
1315            fs::write(manifest_path, manifest_body).map_err(error::DecapodError::IoError)?;
1316        }
1317        (created, unchanged, preserved)
1318    } else {
1319        (0usize, 0usize, 0usize)
1320    };
1321
1322    Ok(ScaffoldSummary {
1323        entrypoints_created: ep_created,
1324        entrypoints_unchanged: ep_unchanged,
1325        entrypoints_preserved: ep_preserved,
1326        config_created: cfg_created,
1327        config_unchanged: cfg_unchanged,
1328        config_preserved: cfg_preserved,
1329        specs_created,
1330        specs_unchanged,
1331        specs_preserved,
1332    })
1333}
1334
1335/// Automatically blends content from non-Decapod AGENT.md/CLAUDE.md/GEMINI.md backups
1336/// into .decapod/OVERRIDE.md and deletes the backups.
1337pub fn blend_legacy_entrypoints(target_dir: &Path) -> Result<(), error::DecapodError> {
1338    let override_path = target_dir.join(".decapod/OVERRIDE.md");
1339    let mut overrides_added = false;
1340    let mut content_to_add = String::new();
1341
1342    for file in ["AGENTS.md", "CLAUDE.md", "GEMINI.md", "CODEX.md"] {
1343        let bak_path = target_dir.join(format!("{}.bak", file));
1344        if bak_path.exists() {
1345            if let Ok(bak_content) = fs::read_to_string(&bak_path) {
1346                // Only add if not empty
1347                let trimmed = bak_content.trim();
1348                if !trimmed.is_empty() {
1349                    content_to_add.push_str(&format!(
1350                        "\n\n### Blended from Legacy {} Entrypoint\n\n{}\n",
1351                        file.replace(".md", ""),
1352                        trimmed
1353                    ));
1354                    overrides_added = true;
1355                }
1356            }
1357            // Delete backup file after blending (or if empty)
1358            let _ = fs::remove_file(&bak_path);
1359        }
1360    }
1361
1362    if overrides_added && override_path.exists() {
1363        let mut existing = fs::read_to_string(&override_path).unwrap_or_default();
1364        existing.push_str(&content_to_add);
1365        fs::write(&override_path, existing).map_err(error::DecapodError::IoError)?;
1366    }
1367
1368    Ok(())
1369}