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::{
AUTO_S3_ABOVE_BYTES, INSTRUCTION_POLICY_VERSION, RECOMMENDED_S3_MINIMUM_BYTES,
TASK_BRANCH_PREFIX, TASK_DIRECTORY_PATTERN,
};
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",
];
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();
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" || topic == "core" {
sections.push(core_section());
}
if topic == "all" || topic == "task" {
sections.push(task_section());
}
if topic == "all" || topic == "publish" {
sections.push(publication_section(config));
}
if topic == "all" || topic == "artifacts" {
sections.push(artifact_hygiene_section());
}
if topic == "all" || topic == "storage" {
sections.push(storage_section(config));
}
if topic == "all" || topic == "shared-checkout" {
sections.push(shared_checkout_section(config));
}
if topic == "all" || topic == "infrastructure" {
sections.push(infrastructure_section());
}
if topic == "all" {
let extra = repo.root.join(".workspace-mgr/instructions/repository.md");
reject_symlink_traversal(
&repo.root,
".workspace-mgr/instructions/repository.md",
"repository instruction module",
)?;
if extra.is_file() {
let content = fs::read_to_string(&extra).at(&extra)?;
if content.len() > 65_536 {
return Err(Error::message(format!(
"repository instruction module exceeds 64 KiB: {}",
extra.display()
)));
}
if !content.trim().is_empty() {
sections.push(format!(
"# Repository-specific additions\n\nThese additions may describe repository domain, build, validation, or content constraints. They do not change the fixed task, storage, publication, or review policy.\n\n{}",
content.trim()
));
}
}
}
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);
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,
})
}
fn core_section() -> String {
"## Operating model\n\n- Read-only requests do not require a task directory.\n- User authorization can make a narrow exception to the fixed workspace policy only for the requested paths and actions; higher-priority safety and platform rules still apply.\n- `workspace-mgr` is the only repository-content mutation interface. Treat a refusal as a guard to investigate, not a reason to bypass it with lower-level Git or storage tools. Repository-hosting review metadata remains the agent's responsibility; merge-state transitions remain the user's responsibility.\n- If any invocation reports `workspace-mgr: update available`, tell the user the current and available versions and ask before updating. Never update the CLI without explicit approval. After an approved update, run `workspace-mgr setup`.\n- `workspace-mgr init` is the deterministic scaffold reconciliation and upgrade operation. Product-owned files are identified by the initialized repository and fixed path, never by their old contents. Do not hand-edit them; after a CLI update or scaffold-drift report, reconcile them in an infrastructure task so the resulting repository-wide diff is reviewed.\n- Run `workspace-mgr doctor` when configuration, dependencies, or repository state appears inconsistent."
.to_owned()
}
fn task_section() -> String {
format!(
"## Task lifecycle\n\n- Create ordinary writable work with `workspace-mgr task create <slug> --title <title> --purpose <purpose>`. Create repository-wide work with `workspace-mgr task create <slug> --kind infrastructure --title <title> --purpose <purpose> --scope <path> --scope-note <reason>`.\n- Deliverable task directories follow `{0}` and use branch `{1}<slug>`. Infrastructure tasks use a private manifest, isolated worktree, and branch `{1}infra-<slug>` instead of a repository task directory.\n- Reuse the same task for the full conversation or work item. Do not write into another active task without explicit authorization.\n- Keep a deliverable task README concise and current. It describes purpose and outputs, and includes a `## Directory map`; it is not a chronological log.\n- The task manifest is the authoritative task scope. Additional repository paths require a concise authorization reason and may not overlap another declared scope.\n- Use `workspace-mgr task status` to inspect the resolved task before publishing.\n- If the user explicitly decides not to retain an unmerged task, run `workspace-mgr task discard --dry-run`, close or verify absence of its pull request, then confirm the exact task ID from the shared checkout with the reported manifest. Never discard a merged task. Discard reports but does not permanently delete retained S3 versions.",
TASK_DIRECTORY_PATTERN, TASK_BRANCH_PREFIX
)
}
fn publication_section(config: &Config) -> String {
let review = review_section();
format!(
"## Publication\n\n- Run `workspace-mgr plan` before publication. A plan may inspect remote metadata but does not create a revision, upload stored data, or publish a branch.\n- Run `workspace-mgr publish -m <message>` to publish only the declared scopes to the configured target branch.\n- Do not interpret a lower-level status command as the complete task state; use the task-targeted plan.\n- Verify the reported revision, remote revision, and a final no-change plan before claiming repository content is current.\n- `{}` is the configured base branch on remote `{}`.\n\n{review}",
config.git.branch, config.git.remote
)
}
fn review_section() -> String {
"## Pull request responsibility\n\n- `workspace-mgr` publishes and verifies repository state but never calls a hosting-provider API. The agent owns pull-request operations.\n- A task is not fully synchronized until its one matching pull request is current.\n- After the first successful publish, the agent must query the hosting provider for the task's head branch. Reuse its existing open pull request or create exactly one draft pull request; never create a duplicate.\n- The agent owns the pull-request title and living description. Keep them aligned with the current goal, declared scope, important deliverables, validation evidence, and known limitations. Update them after every materially changed publication.\n- After creating or updating the pull request, verify that it is open, its base and head branches are correct, its review state is draft, and its head revision equals the remote revision reported by `workspace-mgr publish`.\n- If hosting authentication, permissions, connectivity, or metadata verification fails, report the blocker immediately and do not claim the task is fully synchronized.\n- The agent must not merge, enable auto-merge, approve, close, or change a draft pull request to ready unless the user explicitly requests that exact transition. A request to discard one specific unmerged task authorizes closing only its matching pull request; verify closure before `task discard --confirm`."
.to_owned()
}
fn artifact_hygiene_section() -> String {
"## Artifact hygiene\n\n- Keep every eligible task-owned input, deliverable, and reproducibility artifact in the declared scope.\n- Before publication, identify external Git checkouts, generated build output, and retained large content.\n- Ignore safely reproducible output and external nested repositories using the narrowest applicable rule.\n- Never flatten a nested repository into the parent repository or create a gitlink unless explicitly requested.\n- Choose retained-content placement through `workspace-mgr storage`; do not introduce another large-file mechanism.\n- Keep credentials and private runtime configuration out of tracked files and command output."
.to_owned()
}
fn storage_section(config: &Config) -> String {
let s3_note = if config.requires_object_versioning() {
"S3 is configured. The bucket must have object versioning enabled; exact object versions are verified before the Git revision is published."
} else if config.s3_enabled() {
"An S3-compatible test or local storage adapter is configured; remote presence is verified before the Git revision is published."
} else {
"S3 is not configured, so paths may only be placed in Git until repository configuration is updated."
};
format!(
"## Storage placement\n\n- Every retained path is stored either directly in Git or as versioned content in S3. `workspace-mgr` owns the underlying mechanics; do not invoke lower-level storage tools.\n- Treat Git as the collaboration and control plane: choose it when content belongs in ordinary clones and its value comes from direct review, diff, merge, or joint evolution with repository source. Treat S3 as the artifact and data plane: choose it when content is consumed as an exact object, changes atomically, or should be hydrated on demand. Do not guess semantics from a filename extension.\n- Before relying on size, decide whether the retained content has clear collaboration or artifact semantics. Express that decision with `workspace-mgr storage set <path> --to git|s3 --reason <reason>`. A user's explicit choice wins at any size.\n- Size is only the fallback for new, unclassified files. Below 1 MiB ({0} bytes) Git is the strong default. From 1 through 10 MiB ({0} through {1} bytes) Git remains the fallback, but `plan` and `storage status` ask the agent to review the semantic choice. Above 10 MiB ({1} bytes) S3 is the fallback.\n- A standalone S3 boundary below 1 MiB ({0} bytes) is usually wasteful because its metadata and remote operations may outweigh the payload. Prefer Git or select a larger meaningful directory boundary. An explicit S3 choice still succeeds but reports `small-s3-boundary`. Boundary size is the aggregate size of its materialized regular files.\n- Automatic evaluation treats unclassified files independently. Selecting a directory boundary is an intentional semantic operation and does not promise one packed remote object.\n- Published placement stays stable when size changes. `workspace-mgr storage reset <path>` removes an explicit choice, then preserves published history or reapplies the size fallback for new content. Never silently move published content between Git and S3.\n- Use `workspace-mgr storage status [<path> ...]` to inspect `target`, `basis`, semantic `reason`, boundary size/file count, and structured warnings. `plan` reports automatic decisions in the review band or S3 range and warning-relevant existing boundaries.\n- Placement changes, resets, and `workspace-mgr move <old> <new>` update local desired state only. `workspace-mgr publish` is the only command that publishes content to Git or S3 remotes. Confirmed task discard may delete only its exact remote Git branch and never purges S3 versions.\n- Use `workspace-mgr storage hydrate [<path> ...]` to materialize S3 content locally without publishing. Never hand-edit or directly delete workspace-mgr storage metadata.\n- Storage credentials stay outside tracked repository configuration. Never print, copy, or commit credentials.\n- {s3_note}\n- Never request permanent remote or cache garbage collection without explicit authorization for shared-data deletion.",
RECOMMENDED_S3_MINIMUM_BYTES, AUTO_S3_ABOVE_BYTES
)
}
fn shared_checkout_section(config: &Config) -> String {
format!(
"## Shared checkout\n\n- Keep the shared checkout on `{}` and publish task branches without switching it.\n- Preserve unrelated working-tree overlays. Do not use broad stash, clean, reset, or deletion operations.\n- After a task is merged, use `workspace-mgr refresh`; an ordinary pull may conflict with active overlays.",
config.git.branch
)
}
fn infrastructure_section() -> String {
"## Repository infrastructure\n\n- Treat a change to shared policy, root entrypoints, CI, or repository-wide storage configuration as one infrastructure task with one target branch and one draft pull request.\n- Create it with `workspace-mgr task create <slug> --kind infrastructure --title <title> --purpose <purpose> --scope <path> --scope-note <reason>`. Repeat `--scope` for every authorized shared path.\n- Work only in the isolated worktree returned by the command. An infrastructure task has private task metadata and no timestamped repository task directory.\n- Run task, storage, plan, and publish commands from that worktree. Do not add unrelated deliverable paths to its declared infrastructure scope.\n- Infrastructure storage tests use fresh temporary repositories and local or mock remotes. They must not read user cloud credentials or contact a real storage service."
.to_owned()
}