diff --git a/.decapod/OVERRIDE.md b/.decapod/OVERRIDE.md index 8dd84161..1a05497b 100644 --- a/.decapod/OVERRIDE.md +++ b/.decapod/OVERRIDE.md @@ -316,3 +316,32 @@ over the embedded JSON constitution. ### docs/SKILL_TRANSLATION_MAP --- + + +### methodology/ENGINEERING_MANAGEMENT +### methodology/RESEARCH_PRODUCTION +### docs/agent/README.md +### docs/agent/api-index.md +### docs/agent/command-contracts.md +### docs/agent/config-schema.md +### docs/agent/error-recovery.md +### docs/agent/payload-examples.md +### docs/agent/state-model.md +### docs/book/src/SUMMARY.md +### docs/book/src/concepts/constitution.md +### docs/book/src/concepts/intent.md +### docs/book/src/concepts/overrides.md +### docs/book/src/concepts/proof.md +### docs/book/src/concepts/workspaces.md +### docs/book/src/configuration.md +### docs/book/src/introduction.md +### docs/book/src/mental-model.md +### docs/book/src/quickstart.md +### docs/book/src/reference/artifacts.md +### docs/book/src/reference/cli.md +### docs/book/src/reference/config-toml.md +### docs/book/src/reference/errors.md +### docs/book/src/workflows/external-trackers.md +### docs/book/src/workflows/multi-agent.md +### docs/book/src/workflows/single-agent.md +### docs/book/src/workflows/workspace-isolation.md diff --git a/.decapod/config.toml b/.decapod/config.toml index da7d6f2a..b83c77be 100644 --- a/.decapod/config.toml +++ b/.decapod/config.toml @@ -19,3 +19,4 @@ done_criteria = "Decapod validate passes, required tests pass, and promotion-rel primary_languages = ["Rust"] detected_surfaces = ["cargo"] external_tracker = false +container_workspaces = true diff --git a/.decapod/generated/specs/.manifest.json b/.decapod/generated/specs/.manifest.json index 9783570c..b1b01b29 100644 --- a/.decapod/generated/specs/.manifest.json +++ b/.decapod/generated/specs/.manifest.json @@ -1,8 +1,8 @@ { "schema_version": "1.0.0", "template_version": "scaffold-v2", - "generated_at": "1779678743Z", - "repo_signal_fingerprint": "c199830b21bba0c2265dbe139b358db4575f9a673a9c4248f44b288d572f9c6c", + "generated_at": "1779767907Z", + "repo_signal_fingerprint": "7c94ce67139223a21c919d78216f74ce2584a6ee816e2367e7a314e0081e7dc4", "files": [ { "path": ".decapod/generated/specs/README.md", diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..d56e4076 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,49 @@ +name: Deploy Docs + +on: + push: + branches: + - main + paths: + - 'docs/book/**' + pull_request: + paths: + - 'docs/book/**' + +permissions: + contents: read + pages: write + id-token: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install mdBook + run: | + mkdir bin + curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.40/mdbook-v0.4.40-x86_64-unknown-linux-gnu.tar.gz | tar -xz -C bin + echo "$(pwd)/bin" >> $GITHUB_PATH + + - name: Build with mdBook + run: mdbook build docs/book + + - name: Upload artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: docs/book/book + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/docs_sync.yml b/.github/workflows/docs_sync.yml new file mode 100644 index 00000000..5d061837 --- /dev/null +++ b/.github/workflows/docs_sync.yml @@ -0,0 +1,54 @@ +name: Documentation Sync + +on: + pull_request: + branches: [ master ] + paths: + - 'src/**/*.rs' + - 'docs/**/*.md' + +permissions: + contents: write + +jobs: + sync-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + fetch-depth: 0 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Build Decapod + run: cargo build + + - name: Get changed files + id: changed-files + run: | + git fetch origin master + FILES=$(git diff --name-only origin/master HEAD | xargs) + echo "files=$FILES" >> $GITHUB_OUTPUT + + - name: Run Docs Build + run: | + FILES="${{ steps.changed-files.outputs.files }}" + if [ -n "$FILES" ]; then + ./target/debug/decapod docs build --touched $FILES + else + ./target/debug/decapod docs build + fi + + - name: Commit and push changes + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git add docs/agent/command-contracts.md docs/agent/config-schema.md + if git diff --staged --quiet; then + echo "No doc changes detected." + else + git commit -m "docs: autogenerate/sync command contracts" + git push + fi diff --git a/AGENTS.md b/AGENTS.md index 0221cc42..28839ad5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,13 +2,20 @@ This is a Decapod-managed repository. **Strict Dependency: You are strictly bound to the Decapod governance kernel.** -This contract applies equally to Claude, Codex, Gemini, Cursor, Kilo, and any other agent operating here. +## Mandatory Orientation + +**Before performing any Decapod operations beyond help/version checks, you MUST read the Agent API Orientation corpus:** + +1. Read **[docs/agent/api-index.md](docs/agent/api-index.md)** for high-level orientation. +2. Consult **[docs/agent/command-contracts.md](docs/agent/command-contracts.md)** for specific command requirements and recovery paths. +3. Reference **[docs/agent/payload-examples.md](docs/agent/payload-examples.md)** for valid call shapes. ## Mandatory Initialization ```bash -cargo install decapod -decapod validate && decapod docs ingest && decapod session acquire +# Smart bootstrap: only install/init if a newer version is available +(decapod capabilities --format json | grep -q '"is_latest":true') || (cargo install decapod && decapod init) +decapod docs ingest && decapod validate && decapod session acquire decapod rpc --op agent.init decapod workspace status decapod todo add "" && decapod todo claim --id @@ -22,11 +29,8 @@ decapod rpc --op context.resolve ## Control-Plane First Loop ```bash -# Discover what this binary actually supports in this repo decapod capabilities --format json decapod data schema --deterministic - -# Resolve precise orientation before implementation decapod infer orientation --intent "" --task-id decapod govern capsule query --topic "" --scope interfaces --task-id decapod rpc --op context.scope --params '{"query":"","limit":8}' @@ -44,76 +48,44 @@ decapod rpc --op context.scope --params '{"query":"","limit":8}' 8. **MUST NOT** invent capabilities that are not exposed by the binary. 9. **MUST** stop if requirements conflict or intent is ambiguous. 10. **MUST** respect the interface abstraction boundary. -11. **MUST** maintain **Living Specs**: treat `.decapod/generated/specs/*` as dynamic documents; align them with reality before and after every implementation. +11. **MUST** maintain **Living Specs**: treat `.decapod/generated/specs/*` as dynamic documents. +12. **MUST** use the command contracts in `docs/agent/command-contracts.md` instead of guessing arguments. ## Decapod Invocation Contract -Agents act. Decapod orients. - -Decapod is not your executor, model runtime, or workflow replacement. You remain responsible for implementation. Call Decapod as the repo-native pressure relief valve when the next responsible step requires explicit intent, boundaries, context, coordination, or proof. +Agents act. Decapod orients. Call Decapod at decision boundaries: ambiguous requests, public impact, unclear proof, todo lifecycle, scope expansion, context loss, or multi-agent collision risk. ## Living Specs & Governance -The files under `.decapod/generated/specs/` are not static documentation; they are living contracts. -- **Before Changes**: Review the specs. If the task changes intent, update [INTENT.md](.decapod/generated/specs/INTENT.md) first. -- **During Implementation**: If architectural or interface decisions shift, update [ARCHITECTURE.md](.decapod/generated/specs/ARCHITECTURE.md) and [INTERFACES.md](.decapod/generated/specs/INTERFACES.md). -- **After Changes**: Ensure all specs align with the new reality. Clarify code changes in the context of these spec updates. Spec changes should generally only occur when user intent has evolved. - -End users and host agents may use any task manager alongside Decapod. That external tracker does not replace Decapod todos: Decapod uses its own todo claims to isolate worktrees, scope containers, prove completion, and prevent multiple agents from working the same Decapod work item concurrently. - -Call Decapod before proceeding when continuing would require guessing about: -- **Intent pressure:** what you are actually trying to do. -- **Boundary/Context/Coordination/Proof/Completion pressure:** what matters, collides, or is allowed. - -Concrete triggers: ambiguous requests, public impact, unclear proof, todo lifecycle, scope expansion, conflicting intent/specs, context loss, multi-agent collision risk, or readiness to claim completion. +The files under `.decapod/generated/specs/` are living contracts. Review and update [INTENT.md](.decapod/generated/specs/INTENT.md), [ARCHITECTURE.md](.decapod/generated/specs/ARCHITECTURE.md), and [INTERFACES.md](.decapod/generated/specs/INTERFACES.md) to align with evolving intent and reality. -Do not call Decapod for every trivial file read, local edit, or mechanical command. Call it at decision boundaries that need governance, memory, boundaries, coordination, or proof. Decapod calls should produce or update explicit artifacts. ## Epistemic Custody -**Epistemic custody** is the preserved chain between intent, context, assumptions, action, evidence, contradiction, and proof, so agent work remains inspectable, bounded, and falsifiable across time, agents, and recursive passes. - -| Term | Meaning | -|---|---| -| Intent | Original human goal; the "Why" that must not be lost during compression. | -| Assumption | A belief or prior used to proceed when evidence is incomplete. | -| Contradiction | Evidence or observation that conflicts with a prior assumption or intent. | -| Measured | Direct observation (e.g., test output, file content, shell exit code). | -| Inferred | Derived conclusion or guess (e.g., "this looks like a bug in the parser"). | -| Provenance | The source of a fact or change (e.g., "User said X", "File Y contains Z"). | - -### Custody Rules -1. **Preserve Uncertainty**: Summaries must preserve uncertainty, risk, and unresolved contradictions instead of compressing them away into polished prose. -2. **Recursive Continuity**: Prior assumptions and unresolved contradictions MUST carry forward across recursive agent passes until resolved. -3. **Evidence-Based Claims**: Any claim of success or completion must be tied to measured evidence (artifacts, tests, or explicit user instruction). -4. **Falsifiability**: Work must be structured so a human can quickly identify where an assumption was wrong or where proof failed. -5. **Clarification Trigger**: If a critical assumption cannot be proven or a contradiction cannot be resolved, you MUST stop and request human clarification. - -## Custody artifacts -Decapod maintains custody via: -- `.decapod/generated/specs/INTENT.md`: Tracks active assumptions and stop conditions. -- `.decapod/generated/artifacts/custody/`: Directory for detailed evidence, contradiction logs, and deferred questions. +Preserve the chain between intent, context, assumptions, action, and proof. +1. **Preserve Uncertainty**: Summaries must preserve risk instead of compressing it. +2. **Recursive Continuity**: Prior assumptions MUST carry forward until resolved. +3. **Evidence-Based Claims**: Claims of completion must be tied to measured evidence. +4. **Clarification Trigger**: Stop if a critical assumption cannot be proven. ## Invariants (Normative) - **INV-DAEMONLESS**: Decapod MUST NOT leave background processes running. - **INV-BOUNDED-VALIDATE**: `decapod validate` MUST terminate within bounded time. -- **INV-STORE-BOUNDARY**: Agents MUST NOT directly mutate `.decapod/*`; all access MUST use CLI. -- **INV-SESSION-AUTH**: Mutations require active session with valid credentials. -- **INV-PROOF-GATED**: Workunit status `VERIFIED` MUST have passed proof-plan gates. -- **INV-ROOT-ISOLATION**: Agents MUST NOT check out branches or mutate files in the main repository checkout. +- **INV-STORE-BOUNDARY**: Agents MUST NOT directly mutate `.decapod/*`. +- **INV-SESSION-AUTH**: Mutations require active session. +- **INV-PROOF-GATED**: `VERIFIED` status requires passed proof-plan gates. +- **INV-ROOT-ISOLATION**: Agents MUST NOT mutate files in the main repository checkout. ## Safety Invariants - ✅ Router: `decapod rpc --op constitution.get --params '{"section":"core/DECAPOD"}'` -- ✅ Gates: `decapod validate` | `decapod docs ingest` | `cargo install decapod` +- ✅ Gates: `decapod capabilities` | `decapod docs ingest` | `decapod validate` - ✅ Claims: `decapod todo claim --id ` | ✅ Auth: `DECAPOD_SESSION_PASSWORD` - ✅ Workspace: Docker git workspaces | ✅ Privilege: request elevated permissions before Docker/container workspace commands ## Operating Notes - Read `.decapod/config.toml` and `.decapod/OVERRIDE.md`. - External task managers are allowed, but Decapod todos are still mandatory for workspace isolation. -- DO NOT mutate `.decapod/` state directly; use decapod CLI. Access to `.decapod/` is strictly via decapod CLI. -- Use `decapod capabilities --format json` to discover available operations. -- Stop if requirements conflict or intent is ambiguous. Respect the Interface abstraction boundary. -- Treat lock/contention failures as blocking until resolved. +- DO NOT mutate `.decapod/` state directly; all access MUST use CLI. Access to `.decapod/` is strictly via decapod CLI. +- Stop if requirements conflict or intent is ambiguous. Respect the interface abstraction boundary. \n"); + for line in missing_lines { + updated_content.push_str(&line); + updated_content.push('\n'); + } + + fs::write(&override_path, updated_content).map_err(error::DecapodError::IoError)?; + Ok(FileAction::Created) +} + pub fn scaffold_project_entrypoints( opts: &ScaffoldOptions, ) -> Result { diff --git a/src/core/validate.rs b/src/core/validate.rs index bb7e8412..e4b7bd5e 100644 --- a/src/core/validate.rs +++ b/src/core/validate.rs @@ -302,6 +302,7 @@ fn validate_embedded_self_contained( || line.contains(".decapod/generated/specs/") || line.contains(".decapod/generated/policy/") || line.contains(".decapod/policy/") + || line.contains(".decapod/config.toml") || line.contains("repo-scoped"); if is_legitimate_line { legitimate_ref_count += refs_on_line; diff --git a/src/core/workspace.rs b/src/core/workspace.rs index 3e5c4a2e..87f4ec4d 100644 --- a/src/core/workspace.rs +++ b/src/core/workspace.rs @@ -795,6 +795,17 @@ fn get_main_repo_root(current_dir: &Path) -> Result { Ok(common_path.parent().unwrap_or(common_path).to_path_buf()) } +/// Discover the Decapod repository root by searching upwards from a directory. +/// +/// If `start_dir` is None, it starts from the current working directory. +pub fn discover_repo_root(start_dir: Option<&Path>) -> Result { + let start = match start_dir { + Some(p) => p.to_path_buf(), + None => std::env::current_dir()?, + }; + get_repo_root(&start) +} + fn get_repo_root(start_dir: &Path) -> Result { let output = Command::new("git") .args([ diff --git a/src/lib.rs b/src/lib.rs index 1f735610..a656badb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1345,37 +1345,6 @@ fn config_from_init_with(init: &InitWithCli, repo: RepoContext) -> DecapodProjec } } -fn interactive_init_with( - config: &DecapodProjectConfig, - target_dir: PathBuf, - force: bool, - dry_run: bool, -) -> Result { - print_init_block( - "Decapod Setup", - "Existing .decapod/config.toml detected. Confirm your setup profile.", - ); - let mut next = init_with_from_config(config, target_dir, force, dry_run); - if config.init.entrypoints.is_empty() { - let all_entrypoints = prompt_yes_no( - "Include all default agent entrypoints (AGENTS/CLAUDE/GEMINI/CODEX)?", - true, - )?; - if all_entrypoints { - next.all = true; - next.agents = true; - next.claude = true; - next.gemini = true; - next.cdx_ep = true; - } - } - if config.init.specs { - next.specs = true; - } - next.diagram_style = prompt_diagram_style(next.diagram_style)?; - Ok(next) -} - fn enrich_repo_context_interactive(repo: &mut RepoContext) -> Result<(), error::DecapodError> { print_init_block( "Repository Context", @@ -1445,12 +1414,17 @@ fn run_init_apply( if setup_decapod_root.exists() && !init_with.force { use crate::core::ansi::AnsiExt; println!( - "{} {}", - "init:".bright_yellow(), - "already initialized (.decapod exists); rerun with --force, or use `decapod init with --force`" - .bright_red() + "{} Existing Decapod project detected. Refreshing environment...", + "init:".bright_yellow() ); - return Ok(target_dir); + // Blend OVERRIDE.md additions + let _ = scaffold::blend_overrides(&target_dir)?; + // Sync config (adds missing default fields) + if let Some(cfg) = load_project_config_if_present(&target_dir)? { + write_project_config(&target_dir, &cfg, false)?; + } + // Sync override checksums + let _ = docs_cli::sync_override_checksum(&target_dir, false)?; } use sha2::{Digest, Sha256}; @@ -1655,21 +1629,13 @@ pub fn run() -> Result<(), error::DecapodError> { }; let maybe_cfg = load_project_config_if_present(&target)?; if let Some(cfg) = maybe_cfg { - let mut with = if io::stdin().is_terminal() { - interactive_init_with( - &cfg, - target.clone(), - init_group.force, - init_group.dry_run, - )? - } else { - init_with_from_config( - &cfg, - target.clone(), - init_group.force, - init_group.dry_run, - ) - }; + // REFRESH FLOW: Sidestep manual entries if .decapod already exists + let mut with = init_with_from_config( + &cfg, + target.clone(), + init_group.force, + init_group.dry_run, + ); // Keep base command flags as explicit runtime overrides. if init_group.all { with.all = true; @@ -1763,7 +1729,10 @@ pub fn run() -> Result<(), error::DecapodError> { apply_repo_context_cli_overrides(&mut repo_ctx, &init_with); apply_substrate_adoption(&mut repo_ctx, &init_target); apply_architecture_language_recommendation(&mut repo_ctx); - if base_init_invocation && io::stdin().is_terminal() { + + // Only do full TUI experience if not refreshing an existing project + let is_refresh = init_target.join(".decapod").exists(); + if base_init_invocation && io::stdin().is_terminal() && !is_refresh { enrich_repo_context_interactive(&mut repo_ctx)?; } let target_dir = run_init_apply(&init_with, ¤t_dir, &repo_ctx)?; diff --git a/tests/doc_alignment.rs b/tests/doc_alignment.rs new file mode 100644 index 00000000..886a8777 --- /dev/null +++ b/tests/doc_alignment.rs @@ -0,0 +1,93 @@ +use std::fs; +use std::process::Command; + +#[test] +fn test_agent_docs_command_contracts_alignment() { + let output = Command::new(env!("CARGO_BIN_EXE_decapod")) + .arg("--help") + .output() + .expect("failed to execute decapod --help"); + + let help_text = String::from_utf8_lossy(&output.stdout); + + // Read the command contracts + let contracts_path = "docs/agent/command-contracts.md"; + let contracts_content = + fs::read_to_string(contracts_path).expect("failed to read docs/agent/command-contracts.md"); + + // Extract documented commands from ## decapod headers + // Note: The docs use `decapod todo claim` format + let re = regex::Regex::new(r"## `decapod ([\w\s]+)`").unwrap(); + + for cap in re.captures_iter(&contracts_content) { + let full_cmd = &cap[1]; + let parts: Vec<&str> = full_cmd.split_whitespace().collect(); + + if parts.is_empty() { + continue; + } + + let root_cmd = parts[0]; + + // Verify root command exists in top-level help + assert!( + help_text.contains(root_cmd), + "Documented root command '{}' not found in decapod --help", + root_cmd + ); + + // If it's a sub-command (e.g. todo claim), verify it exists in sub-help + if parts.len() > 1 { + let sub_cmd = parts[1]; + let sub_help = Command::new(env!("CARGO_BIN_EXE_decapod")) + .arg(root_cmd) + .arg("--help") + .output() + .expect("failed to execute sub-command help"); + + let sub_help_text = String::from_utf8_lossy(&sub_help.stdout); + assert!( + sub_help_text.contains(sub_cmd), + "Documented sub-command '{} {}' not found in decapod {} --help", + root_cmd, + sub_cmd, + root_cmd + ); + } + } +} + +#[test] +fn test_config_schema_alignment() { + // Verify that keys documented in docs/agent/config-schema.md exist in the project config + let config_docs = fs::read_to_string("docs/agent/config-schema.md") + .expect("failed to read docs/agent/config-schema.md"); + + // Documented keys are in ### repo.key or ### init.key format + let re = regex::Regex::new(r"### `(repo|init)\.(\w+)`").unwrap(); + + // Use capabilities --format json to get the full project config + let output = Command::new(env!("CARGO_BIN_EXE_decapod")) + .args(["capabilities", "--format", "json"]) + .output() + .expect("failed to get capabilities"); + + let caps_json: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("failed to parse capabilities JSON"); + + let config = &caps_json["config"]; + + for cap in re.captures_iter(&config_docs) { + let section = &cap[1]; + let key = &cap[2]; + + // Check if the key exists in the corresponding section of the config + let section_val = &config[section]; + assert!( + !section_val[key].is_null(), + "Documented config key '{}.{}' not found in decapod capabilities config", + section, + key + ); + } +}