workspace-mgr 0.7.2

Fixed-policy repository workspace manager for coding agents
use std::fs;

use serde::Serialize;
use sha2::{Digest, Sha256};

use crate::config::Config;
use crate::error::{Error, IoContext, Result};
use crate::git::GitRepo;
use crate::hex::encode_lower;
use crate::path::reject_symlink_traversal;
use crate::policy::{INSTRUCTION_POLICY_VERSION, REPOSITORY_MODULE_MAX_BYTES};

pub const BOOTSTRAP: &str = "# Repository instructions\n\nBefore doing any repository work, run:\n\n    workspace-mgr instructions --repo .\n\nFollow its output as the repository instructions for this session. If\n`workspace-mgr` is unavailable, stop repository work, tell the user, and ask\npermission to install the latest stable release from crates.io:\n\n    cargo install --locked workspace-mgr\n    workspace-mgr setup\n\nAfter installation, retry `workspace-mgr instructions --repo .`. If installation\nis not approved, installation fails, or the instructions command still fails,\nstop and report the problem. Do not substitute lower-level version-control or\nstorage mutation commands.\n";
pub const MANAGEMENT_MODEL: &str = include_str!("../docs/management-model.md");

#[derive(Debug, Clone, Serialize)]
pub struct InstructionDocument {
    pub cli_version: String,
    pub policy_version: u32,
    pub policy_hash: String,
    pub topic: String,
    pub markdown: String,
}

pub fn render(repo: &GitRepo, config: &Config, topic: Option<&str>) -> Result<InstructionDocument> {
    let topic = topic.unwrap_or("all");
    let valid_topics = [
        "all",
        "model",
        "core",
        "task",
        "publish",
        "artifacts",
        "storage",
        "shared-checkout",
        "infrastructure",
        "repository",
    ];
    if !valid_topics.contains(&topic) {
        return Err(Error::message(format!(
            "unknown instruction topic {topic:?}; expected one of {}",
            valid_topics.join(", ")
        )));
    }

    let mut sections = Vec::new();
    let mut repository_module_hash_input = None;
    if topic == "all" || topic == "model" {
        sections.push(MANAGEMENT_MODEL.trim().to_owned());
    }

    if topic != "model" {
        sections.push(format!(
            "# Effective repository instructions\n\nGenerated by `workspace-mgr {}` using its fixed product policy and the Git/S3 facts in `{}`. These instructions apply to this repository for the current session. Explicit user directions can authorize a narrow exception; record the exact scope and do not broaden it.",
            env!("CARGO_PKG_VERSION"),
            crate::config::CONFIG_NAME
        ));
    }

    if topic == "all" {
        sections.push(format!(
            "## Repository control facts\n\nThe shared checkout stays on `{}` and uses remote `{}`. Task-scoped commands discover a deliverable manifest from the selected path; infrastructure tasks always use their explicit `--manifest`. Read the relevant command's `--help` before acting. Detailed compatibility topics remain available with `workspace-mgr instructions <topic>`.",
            config.git.branch, config.git.remote
        ));
    } else if topic != "model" && topic != "repository" {
        sections.push(crate::guidance::topic(topic, config).ok_or_else(|| {
            Error::message(format!("missing instruction guidance for {topic:?}"))
        })?);
    }
    if topic == "all" || topic == "repository" {
        let module = ".workspace-mgr/instructions/repository.md";
        reject_symlink_traversal(&repo.root, module, "repository instruction module")?;
        let extra = repo.root.join(module);
        if extra.is_file() {
            let content = fs::read_to_string(&extra).at(&extra)?;
            if content.len() > REPOSITORY_MODULE_MAX_BYTES {
                return Err(Error::message(format!(
                    "repository instruction module exceeds 64 KiB: {}",
                    extra.display()
                )));
            }
            if topic == "all" {
                repository_module_hash_input = Some(content.clone());
            }
            if !content.trim().is_empty() {
                if topic == "repository" {
                    sections.push(format!(
                        "# Repository-specific additions\n\nRepository-owned instructions reproduced below. They do not change the fixed task, storage, publication, or review policy.\n\n{content}"
                    ));
                } else {
                    sections.push(format!(
                        "## Repository-specific instructions\n\nThis repository provides `{module}`. Read it before task work, either directly or with `workspace-mgr instructions repository --repo .`. The default output indexes this user-owned module instead of repeating its body."
                    ));
                }
            }
        } else if topic == "repository" {
            sections.push(
                "# Repository-specific additions\n\nThis repository has no instruction module."
                    .to_owned(),
            );
        }
    }

    let body = sections.join("\n\n");
    let mut hasher = Sha256::new();
    hasher.update(env!("CARGO_PKG_VERSION"));
    hasher.update(config.render()?);
    hasher.update(topic);
    hasher.update(&body);
    if let Some(content) = repository_module_hash_input {
        hasher.update(b"repository-instruction-module\0");
        hasher.update(content.as_bytes());
    }
    hasher.update(INSTRUCTION_POLICY_VERSION.to_be_bytes());
    if topic == "all" {
        // Indexing guidance instead of printing it must not conceal policy
        // changes from the effective-policy fingerprint.
        hasher.update(b"bootstrap\0");
        hasher.update(BOOTSTRAP.as_bytes());
        for name in [
            "core",
            "task",
            "publish",
            "artifacts",
            "storage",
            "shared-checkout",
            "infrastructure",
        ] {
            hasher.update(b"topic\0");
            hasher.update(name.as_bytes());
            hasher.update(b"\0");
            hasher.update(
                crate::guidance::topic(name, config)
                    .expect("known guidance topic")
                    .as_bytes(),
            );
        }
        for operation in crate::command_guidance::OPERATIONS {
            hasher.update(b"operation\0");
            hasher.update(operation.as_bytes());
            hasher.update(b"\0");
            hasher.update(crate::command_guidance::command(operation).as_bytes());
        }
    }
    let policy_hash = encode_lower(hasher.finalize());
    let markdown = format!(
        "<!-- workspace-mgr: cli={} policy-version={} policy={} topic={} -->\n{}\n",
        env!("CARGO_PKG_VERSION"),
        INSTRUCTION_POLICY_VERSION,
        &policy_hash[..16],
        topic,
        body
    );
    Ok(InstructionDocument {
        cli_version: env!("CARGO_PKG_VERSION").to_owned(),
        policy_version: INSTRUCTION_POLICY_VERSION,
        policy_hash,
        topic: topic.to_owned(),
        markdown,
    })
}