From f2255e9df15d786fb15b1cf054615286e3a3c823 Mon Sep 17 00:00:00 2001 From: rldyourmnd Date: Sat, 19 Sep 2026 22:27:52 +0500 Subject: [PATCH] feat: a release of this repository Published at 0.0.73. Propose changes through this repository's issues and pull requests. --- CHANGELOG.md | 14 + Cargo.lock | 8 +- Cargo.toml | 8 +- README.md | 2 +- crates/harness-runtime/src/facts.rs | 52 +++- crates/harness-runtime/src/human.rs | 5 + .../harness-runtime/src/instruction_region.rs | 224 ++++++++++++++++ crates/harness-runtime/src/lib.rs | 1 + crates/harness-runtime/src/wire.rs | 248 ++++++++++++++++++ crates/opencode-setup-system/src/main.rs | 1 + crates/provider-v3/src/argv.rs | 73 +++++- crates/provider-v3/src/info.rs | 1 + crates/provider-v3/src/plan.rs | 14 + crates/provider-v3/src/vocabulary.rs | 46 ++++ install.ps1 | 2 +- install.sh | 2 +- provider-kit/v3/KIT-IDENTITY.json | 4 +- provider-kit/v3/SHA256SUMS | 4 +- provider-kit/v3/manifest.json | 13 +- provider-kit/v3/provider-info.schema.json | 4 +- references/opencode-baseline.json | 6 +- .../references/authoring-agents.md | 12 +- 22 files changed, 720 insertions(+), 24 deletions(-) create mode 100644 crates/harness-runtime/src/instruction_region.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5861044..a7eb843 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,20 @@ cut and that this clone does not carry. ## [Unreleased] +## [0.0.73] - 2026-09-19 + +nddev-builder guidance is refreshed against each harness's current native +extension model. Software artifacts are refreshed from verified vendor bytes: +Claude Code 2.1.278, Codex 0.155.1, Grok Build 1.0.38, Cursor +2026.09.18-9a7762b and Antigravity CLI 1.2.7. OpenCode remains 1.18.31 and +Pi 0.85.1. Public reusable workflows pin ci-workflows 0.1.20 +(26749820fad5bde1f1726636af5b04d329ed2fe5). Previous artifact pins remain +available for rollback. + +Providers gain the optional patch_instruction_region operation, which replaces +the managed instruction region in place while preserving unmanaged native +state. Setup content, postures and ownership are unchanged. + ## [0.0.72] - 2026-09-16 nddev-builder guidance is refreshed against each harness's current native diff --git a/Cargo.lock b/Cargo.lock index 38a174b..7fa963b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,7 +66,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "harness-runtime" -version = "0.0.72" +version = "0.0.73" dependencies = [ "provider-v3", "serde", @@ -128,7 +128,7 @@ dependencies = [ [[package]] name = "opencode-setup-system" -version = "0.0.72" +version = "0.0.73" dependencies = [ "harness-runtime", "provider-v3", @@ -147,7 +147,7 @@ dependencies = [ [[package]] name = "provider-v3" -version = "0.0.72" +version = "0.0.73" dependencies = [ "serde", "serde_json", @@ -209,7 +209,7 @@ dependencies = [ [[package]] name = "setup-core" -version = "0.0.72" +version = "0.0.73" dependencies = [ "miniz_oxide", "serde", diff --git a/Cargo.toml b/Cargo.toml index 490984d..8e9c9f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ members = [ ] [workspace.package] -version = "0.0.72" +version = "0.0.73" edition = "2024" rust-version = "1.89" license = "AGPL-3.0-or-later" @@ -23,9 +23,9 @@ sha2 = "0.11" # `setup-core::archive`); an inflate loop is not, because its bugs are # memory-safety bugs and it is not improved by being hand-written here. miniz_oxide = "0.9" -setup-core = { path = "crates/setup-core", version = "0.0.72" } -provider-v3 = { path = "crates/provider-v3", version = "0.0.72" } -harness-runtime = { path = "crates/harness-runtime", version = "0.0.72" } +setup-core = { path = "crates/setup-core", version = "0.0.73" } +provider-v3 = { path = "crates/provider-v3", version = "0.0.73" } +harness-runtime = { path = "crates/harness-runtime", version = "0.0.73" } [workspace.lints.rust] unsafe_code = "forbid" diff --git a/README.md b/README.md index 0a8b1fc..50a36e9 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ release is a convenience, not the authorised copy. ```bash docker run --rm -v "$HOME/.config:/config" \ - ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.72 \ + ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.73 \ status --target /config/ --json ``` diff --git a/crates/harness-runtime/src/facts.rs b/crates/harness-runtime/src/facts.rs index 632cf60..9271d9e 100644 --- a/crates/harness-runtime/src/facts.rs +++ b/crates/harness-runtime/src/facts.rs @@ -234,7 +234,20 @@ pub struct Harness { /// [`Delivery::Manager`], which is a different statement -- the product is /// installable, but not by fetching bytes whose digest was fixed in advance /// -- and the refusal says which. + /// How the product's own software is installed, when this build can do it. + /// + /// `None` means the software lifecycle is not offered at all. So does a + /// [`Delivery::Manager`], which is a different statement -- the product is + /// installable, but not by fetching bytes whose digest was fixed in advance + /// -- and the refusal says which. pub software: Option, + /// Target-relative path of the user-global instruction attachment. + /// + /// `None` when the product has no catalogued global instruction surface + /// (Antigravity). Present, this build implements `patch_instruction_region` + /// against that path. The bytes travel on `--instruction-section`; they are + /// not a setup payload. + pub instruction_region: Option<&'static str>, } /// A second set of ownings, for a target that is not the product's own home. @@ -751,21 +764,44 @@ impl Harness { /// manager this provider does not run. #[must_use] pub fn operations(&self) -> &'static [Operation] { - match (self.can_launch(), self.software) { + match ( + self.can_launch(), + self.software, + self.instruction_region.is_some(), + ) { ( true, Some(Software { delivery: Delivery::Artifacts(_), .. }), + true, ) => Operation::ALL, + ( + true, + Some(Software { + delivery: Delivery::Artifacts(_), + .. + }), + false, + ) => Operation::ALL_WITHOUT_INSTRUCTION, + ( + false, + Some(Software { + delivery: Delivery::Artifacts(_), + .. + }), + true, + ) => Operation::CORE_AND_SOFTWARE_AND_INSTRUCTION, ( false, Some(Software { delivery: Delivery::Artifacts(_), .. }), + false, ) => Operation::CORE_AND_SOFTWARE, + (_, _, true) => Operation::CORE_AND_INSTRUCTION, _ => Operation::CORE, } } @@ -968,6 +1004,7 @@ mod tests { max_files: 4096, max_bytes: 1024, kit_identity: r#"{"aggregate_digest":"sha256:aa","protocol_version":3}"#, + instruction_region: None, }; #[test] @@ -1009,6 +1046,7 @@ mod tests { Operation::SoftwareInstall, Operation::SoftwareUpdate, Operation::SoftwareRemove, + Operation::PatchInstructionRegion, ] { assert!( !info.declares(optional), @@ -1017,6 +1055,18 @@ mod tests { } } + #[test] + fn an_instruction_surface_without_software_still_declares_the_patch() { + let named = Harness { + instruction_region: Some("AGENTS.md"), + ..SAMPLE + }; + let info = named.provider_info().unwrap(); + assert!(info.declares(Operation::PatchInstructionRegion)); + assert!(!info.declares(Operation::SoftwareInstall)); + assert!(!info.declares(Operation::Launch)); + } + #[test] fn the_build_digest_is_reproducible_and_binds_the_kit() { let once = SAMPLE.build_digest().unwrap(); diff --git a/crates/harness-runtime/src/human.rs b/crates/harness-runtime/src/human.rs index a5b9e56..61ab40c 100644 --- a/crates/harness-runtime/src/human.rs +++ b/crates/harness-runtime/src/human.rs @@ -1154,6 +1154,8 @@ fn mutate( // The human surface removes whole and carries no bundle, so no path // has a second sentence. end_state: Vec::new(), + instruction_path: None, + instruction_text: None, effects: effect_lines(harness, &effect, applied.setup_id.as_deref()), })?; let plan_digest = artifact.digest()?; @@ -1251,6 +1253,9 @@ fn effect_lines(harness: &Harness, effect: &Effect<'_>, setup_id: Option<&str>) )); lines } + Effect::PatchInstruction { path, .. } => { + vec![capture, format!("patch instruction region at {path}")] + } } } diff --git a/crates/harness-runtime/src/instruction_region.rs b/crates/harness-runtime/src/instruction_region.rs new file mode 100644 index 0000000..dde1517 --- /dev/null +++ b/crates/harness-runtime/src/instruction_region.rs @@ -0,0 +1,224 @@ +//! Marked user-global instruction attachment. +//! +//! The consumer owns the section text. This kernel splices it into one +//! target-relative file, preserves every byte outside the markers, and is +//! idempotent on identical bytes. Whole-setup apply/remove must not treat the +//! region as payload they are free to empty: [`preserve_in_replacement`] and +//! [`keep_region_on_withdraw`] are the two hooks. + +use std::path::Path; + +/// Visible begin marker. HTML comments are refused — Claude strips them. +pub const BEGIN: &str = ":::begin-ai-stp"; +/// Visible end marker. Inclusive of the following newline when present. +pub const END: &str = ":::end-ai-stp"; + +/// The marked region, including both markers, or `None` when either is missing +/// or they are out of order. +#[must_use] +pub fn extract(existing: &str) -> Option<&str> { + let begin = existing.find(BEGIN)?; + let end = existing.find(END)?; + if end < begin { + return None; + } + let mut end_at = end + END.len(); + if existing[end_at..].starts_with('\n') { + end_at += 1; + } + Some(&existing[begin..end_at]) +} + +/// Replace the marked region or append it. Preserve every other byte. +#[must_use] +pub fn splice(existing: &str, section: &str) -> String { + match extract(existing) { + None if existing.is_empty() => section.to_owned(), + None => { + let mut held = existing.to_owned(); + if !held.ends_with('\n') { + held.push('\n'); + } + held.push_str(section); + held + } + Some(held) => match existing.find(BEGIN) { + None => existing.to_owned(), + Some(begin) => format!( + "{}{}{}", + &existing[..begin], + section, + &existing[begin + held.len()..] + ), + }, + } +} + +/// Pure region patch. `wrote` is false when the marked bytes already match. +/// +/// An empty file takes `section` whole so bytes the consumer placed *before* +/// the markers (Cursor `alwaysApply` YAML) survive the first write. Later +/// calls splice only the marked region. +#[must_use] +pub fn patch(existing: &str, section: &str) -> (String, bool) { + let desired = extract(section).unwrap_or(section); + if let Some(current) = extract(existing) + && current == desired + { + return (existing.to_owned(), false); + } + if existing.is_empty() { + return (section.to_owned(), true); + } + (splice(existing, desired), true) +} + +/// Keep an existing attachment when a setup writes the same path. +#[must_use] +pub fn preserve_in_replacement(existing: &str, incoming: &str) -> String { + let Some(region) = extract(existing) else { + return incoming.to_owned(); + }; + if extract(incoming).is_some() { + let (updated, _) = patch(incoming, region); + return updated; + } + let spliced = splice(incoming, region); + let owned = owned_prefix(existing); + if owned.is_empty() || spliced.starts_with("---\n") { + return spliced; + } + format!("{owned}{spliced}") +} + +/// After withdrawing a recorded file, keep the attachment remainder when one +/// existed. Returns `None` when the file had no attachment (caller deletes). +/// +/// Cursor `alwaysApply` YAML sits before the markers. That prefix is the +/// initialize attachment, not setup payload, so it survives withdraw. +#[must_use] +pub fn keep_region_on_withdraw(existing: &str) -> Option { + let region = extract(existing)?; + let owned = owned_prefix(existing); + if owned.is_empty() { + Some(region.to_owned()) + } else { + Some(format!("{owned}{region}")) + } +} + +/// YAML frontmatter initialize placed before the markers, or empty. +/// +/// Not "everything before BEGIN": a setup body can sit between the fence and +/// the region, and withdraw must still drop that body. +fn owned_prefix(existing: &str) -> &str { + const OPEN: &str = "---\n"; + const CLOSE: &str = "\n---\n"; + if !existing.starts_with(OPEN) { + return ""; + } + let Some(rel) = existing[OPEN.len()..].find(CLOSE) else { + return ""; + }; + let mut end = OPEN.len() + rel + CLOSE.len(); + if existing[end..].starts_with('\n') { + end += 1; + } + if extract(&existing[end..]).is_none() && !existing[end..].contains(BEGIN) { + return ""; + } + &existing[..end] +} + +/// True when `relative` is the attachment path this harness named. +#[must_use] +pub fn is_attachment(relative: &str, named: Option<&str>) -> bool { + named.is_some_and(|path| path == relative) +} + +/// UTF-8 text of a file, or empty when it is missing. +pub fn read_utf8(path: &Path) -> String { + std::fs::read_to_string(path).unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + const SECTION: &str = ":::begin-ai-stp\nhello\n:::end-ai-stp\n"; + + #[test] + fn empty_file_receives_the_section() { + let (updated, wrote) = patch("", SECTION); + assert!(wrote); + assert_eq!(updated, SECTION); + } + + #[test] + fn user_bytes_outside_the_markers_are_kept() { + let (updated, wrote) = patch("keep-me\n", SECTION); + assert!(wrote); + assert_eq!(updated, format!("keep-me\n{SECTION}")); + } + + #[test] + fn identical_bytes_are_a_no_write() { + let first = splice("keep-me\n", SECTION); + let (second, wrote) = patch(&first, SECTION); + assert!(!wrote); + assert_eq!(second, first); + } + + #[test] + fn first_write_keeps_bytes_before_the_markers() { + let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n"; + let (updated, wrote) = patch("", section); + assert!(wrote); + assert_eq!(updated, section); + assert!(updated.starts_with("---\n")); + + let next = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nchanged\n:::end-ai-stp\n"; + let (spliced, wrote_again) = patch(&updated, next); + assert!(wrote_again); + assert!(spliced.starts_with("---\n")); + assert_eq!(spliced.matches("alwaysApply: true").count(), 1); + assert_eq!(extract(&spliced), extract(next)); + assert_eq!( + extract(&spliced), + Some(":::begin-ai-stp\nchanged\n:::end-ai-stp\n") + ); + } + + #[test] + fn replacement_of_a_setup_file_keeps_the_region() { + let existing = splice("old-setup\n", SECTION); + let outgoing = preserve_in_replacement(&existing, "new-setup\n"); + assert!(outgoing.contains("new-setup")); + assert_eq!(extract(&outgoing), Some(SECTION)); + } + + #[test] + fn withdraw_leaves_the_region_and_drops_the_rest() { + let existing = splice("setup-bytes\n", SECTION); + assert_eq!(keep_region_on_withdraw(&existing).as_deref(), Some(SECTION)); + assert_eq!(keep_region_on_withdraw("just setup"), None); + } + + #[test] + fn yaml_frontmatter_survives_setup_replace_and_withdraw() { + let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n"; + let (first, _) = patch("", section); + let replaced = preserve_in_replacement(&first, "new-setup\n"); + assert!(replaced.starts_with("---\n")); + assert_eq!(replaced.matches("alwaysApply: true").count(), 1); + assert!(replaced.contains("new-setup")); + assert_eq!(extract(&replaced), extract(section)); + + let remainder = keep_region_on_withdraw(&replaced).unwrap_or_default(); + assert!(!remainder.is_empty()); + assert!(remainder.starts_with("---\n")); + assert!(remainder.contains("alwaysApply: true")); + assert!(!remainder.contains("new-setup")); + assert_eq!(extract(&remainder), extract(section)); + } +} diff --git a/crates/harness-runtime/src/lib.rs b/crates/harness-runtime/src/lib.rs index 9e27700..a7b945f 100644 --- a/crates/harness-runtime/src/lib.rs +++ b/crates/harness-runtime/src/lib.rs @@ -37,6 +37,7 @@ pub mod catalog; pub mod expiry; pub mod facts; pub mod human; +pub(crate) mod instruction_region; pub mod probe; pub(crate) mod software; pub mod surfaces; diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs index 92399d4..7942c82 100644 --- a/crates/harness-runtime/src/wire.rs +++ b/crates/harness-runtime/src/wire.rs @@ -654,6 +654,8 @@ fn honourable(harness: &Harness, request: &PlanRequest) -> Result<()> { } } + honour_instruction_section(harness, request)?; + // The same rule for a bundle, and this one was worse than silently // dropped: only `install` and `replace` read one, but the plan **bound** // the five names into its artifact for every operation, so a remove plan @@ -722,6 +724,80 @@ fn honourable(harness: &Harness, request: &PlanRequest) -> Result<()> { Ok(()) } +fn honour_instruction_section(harness: &Harness, request: &PlanRequest) -> Result<()> { + if request.instruction_section.is_some() + && request.operation != Operation::PatchInstructionRegion + { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} takes no instruction section; patch_instruction_region is the \ + operation that reads --instruction-section", + request.operation + ), + )); + } + if request.operation != Operation::PatchInstructionRegion { + return Ok(()); + } + if harness.instruction_region.is_none() { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} does not declare a user-global instruction surface", + harness.provider_id + ), + )); + } + if request + .instruction_section + .as_deref() + .is_none_or(str::is_empty) + { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + "patch_instruction_region needs --instruction-section with marked bytes", + )); + } + Ok(()) +} + +fn plan_instruction_patch( + harness: &Harness, + target: &Target, + request: &PlanRequest, +) -> Result<(Vec, String, String)> { + let Some(relative) = harness.instruction_region else { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + format!( + "{} does not declare a user-global instruction surface", + harness.provider_id + ), + )); + }; + let Some(section) = request.instruction_section.as_deref() else { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + "patch_instruction_region needs --instruction-section with marked bytes", + )); + }; + if crate::instruction_region::extract(section).is_none() { + return Err(Error::refuse( + WireReason::UnsupportedOperation, + "instruction_section must contain :::begin-ai-stp and :::end-ai-stp", + )); + } + let existing = crate::instruction_region::read_utf8(&target.root().join(relative)); + let (updated, wrote) = crate::instruction_region::patch(&existing, section); + let effects = if wrote { + vec![format!("patch instruction region at {relative}")] + } else { + vec![format!("instruction region at {relative} already matches")] + }; + Ok((effects, relative.to_owned(), updated)) +} + /// Produce a plan without touching the target. /// What a mutation **takes away** before it writes, in the words the result /// line already uses. @@ -854,6 +930,15 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result { + let mut capture = owned.clone(); + if let Some(path) = harness.instruction_region + && !capture.iter().any(|held| held == path) + { + capture.push(path.to_owned()); + } + capture + } _ => owned.clone(), }; refuse_uncapturable(&resolved, &capture)?; @@ -863,6 +948,8 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result = None; let mut software_version_held: Option = None; let mut end_state = Vec::new(); + let mut instruction_path = None; + let mut instruction_text = None; let (effects, backup_ref, restore_target_digest) = match request.operation { Operation::SoftwareInstall | Operation::SoftwareUpdate | Operation::SoftwareRemove => { let (planned, effects, version) = software::plan( @@ -929,6 +1016,12 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result (bundle_effects(harness, request)?, None, None), + Operation::PatchInstructionRegion => { + let (lines, path, text) = plan_instruction_patch(harness, &resolved, request)?; + instruction_path = Some(path); + instruction_text = Some(text); + (lines, None, None) + } other @ Operation::Launch => { return Err(Error::refuse( WireReason::UnsupportedOperation, @@ -968,6 +1061,8 @@ fn plan(harness: &Harness, target: &Path, request: &PlanRequest) -> Result { /// Each declared file's bytes and mode, by target-relative path. files: &'a BTreeMap, u32)>, }, + /// Splice a marked instruction region into one owned-or-neighbour file. + PatchInstruction { + /// Target-relative path. + path: String, + /// Full file text after splicing. + text: String, + }, } /// One authorized mutation, whatever surface asked for it. @@ -1734,6 +1836,11 @@ fn apply( files: &ready.files, } } + Operation::PatchInstructionRegion => { + let path = string_field(&artifact, "instruction_path")?; + let text = string_field(&artifact, "instruction_text")?; + Effect::PatchInstruction { path, text } + } other => { return Err(Error::refuse( WireReason::UnsupportedOperation, @@ -1975,6 +2082,22 @@ pub(crate) fn perform( Effect::MaterializeBundle { files } => { write_bundle_files(harness, &resolved, files, mutation.target_scope) } + Effect::PatchInstruction { path, text } => { + let destination = resolved.root().join(path); + if let Some(parent) = destination.parent() { + fs::create_dir_all(parent).map_err(|error| { + Error::from( + setup_core::Error::new( + setup_core::ReasonCode::StateUnavailable, + format!("cannot create {}", parent.display()), + ) + .with_source(error), + ) + })?; + } + lock::atomic_write(&destination, text.as_bytes()).map_err(Error::from)?; + Ok(previous_written.clone()) + } // Keeping a predecessor's stamp aside writes nothing to the target, so // the inventory is what it was. Same reason as `Backup` above. Effect::Adopt { stamp } => { @@ -2754,6 +2877,21 @@ fn write_host_file( } else { bytes.to_vec() }; + let outgoing = if crate::instruction_region::is_attachment(relative, harness.instruction_region) + { + fs::read_to_string(&destination) + .ok() + .map(|existing| { + crate::instruction_region::preserve_in_replacement( + &existing, + &String::from_utf8_lossy(&outgoing), + ) + .into_bytes() + }) + .unwrap_or(outgoing) + } else { + outgoing + }; if merge_json && let Some(keys) = json_top_keys(bytes) { remember_written_fields(harness, target, relative, keys)?; } @@ -2794,6 +2932,13 @@ fn withdraw_written( if preserve_json_keys { forget_written_fields(harness, target, relative); } + if crate::instruction_region::is_attachment(relative, harness.instruction_region) + && let Ok(existing) = fs::read_to_string(&destination) + && let Some(region) = crate::instruction_region::keep_region_on_withdraw(&existing) + { + lock::atomic_write(&destination, region.as_bytes()).map_err(Error::from)?; + return Ok(()); + } remove_keeping(&destination, target.root(), harness.never_touch) } @@ -3317,6 +3462,7 @@ pub(crate) mod tests_support { max_files: 4096, max_bytes: 64 * 1024 * 1024, kit_identity: r#"{"aggregate_digest":"sha256:aa","protocol_version":3}"#, + instruction_region: Some("AGENTS.md"), }; } @@ -8154,6 +8300,7 @@ mod tests { assert!(info.declares(Operation::SoftwareInstall)); assert!(info.declares(Operation::SoftwareUpdate)); assert!(info.declares(Operation::SoftwareRemove)); + assert!(info.declares(Operation::PatchInstructionRegion)); } #[test] @@ -8166,6 +8313,10 @@ mod tests { assert!(!info.declares(Operation::SoftwareInstall)); assert!(!info.declares(Operation::SoftwareUpdate)); assert!(!info.declares(Operation::SoftwareRemove)); + assert!( + info.declares(Operation::PatchInstructionRegion), + "the instruction attachment is not a software lifecycle" + ); let error = software::plan( &bare, @@ -8176,6 +8327,103 @@ mod tests { .unwrap_err(); assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation)); } + + const INSTRUCTION_SECTION: &str = ":::begin-ai-stp\nhello from ai-stp\n:::end-ai-stp\n"; + + #[test] + fn patch_instruction_region_splices_markers_and_survives_a_recorded_withdraw() { + let target = seeded("patch-instruction-region"); + let applied = plan_then_apply( + &target, + "patch_instruction_region", + &["--instruction-section", INSTRUCTION_SECTION], + ); + assert_eq!(applied["state"], "verified", "{applied}"); + let after_patch = fs::read_to_string(target.join("AGENTS.md")).unwrap(); + assert!( + after_patch.starts_with("# first\n"), + "user bytes outside the markers were dropped: {after_patch:?}" + ); + assert_eq!( + crate::instruction_region::extract(&after_patch), + Some(INSTRUCTION_SECTION) + ); + assert!( + !recorded_written(&target).contains(&"AGENTS.md".to_owned()), + "the attachment was recorded as a setup receipt: {:?}", + recorded_written(&target) + ); + + install_global(&target, "with-agents", &[("AGENTS.md", "# setup\n", 0o644)]); + let after_install = fs::read_to_string(target.join("AGENTS.md")).unwrap(); + assert!( + after_install.contains("# setup"), + "the setup did not land: {after_install:?}" + ); + assert_eq!( + crate::instruction_region::extract(&after_install), + Some(INSTRUCTION_SECTION), + "install emptied the attachment: {after_install:?}" + ); + assert!( + recorded_written(&target).contains(&"AGENTS.md".to_owned()), + "install did not record the setup file: {:?}", + recorded_written(&target) + ); + + let removed = plan_then_apply(&target, "remove", &[]); + assert_eq!(removed["state"], "verified", "{removed}"); + let after_remove = fs::read_to_string(target.join("AGENTS.md")).unwrap(); + assert_eq!( + crate::instruction_region::extract(&after_remove), + Some(INSTRUCTION_SECTION), + "withdraw deleted the attachment: {after_remove:?}" + ); + assert!( + !after_remove.contains("# setup"), + "withdraw left setup bytes beside the region: {after_remove:?}" + ); + } + + #[test] + fn a_harness_without_an_instruction_surface_refuses_the_patch() { + let mut mute = TEST; + mute.instruction_region = None; + let target = seeded("no-instruction-surface"); + let error = refuse_for( + &mute, + args( + "plan-operation", + &target, + &[ + "--operation", + "patch_instruction_region", + "--provider-release-digest", + RELEASE, + "--operation-id", + "operation_01TEST", + "--expires-at", + far_future(), + "--instruction-section", + INSTRUCTION_SECTION, + ], + ), + ); + assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation)); + assert!( + error + .detail() + .contains("does not declare a user-global instruction surface"), + "{}", + error.detail() + ); + assert!( + !mute + .provider_info() + .unwrap() + .declares(Operation::PatchInstructionRegion) + ); + } /// `status` says which slots retention may not take, and whose they are. /// /// The pool has known this since held slots shipped; `status` did not diff --git a/crates/opencode-setup-system/src/main.rs b/crates/opencode-setup-system/src/main.rs index 22abff2..22ca3ed 100644 --- a/crates/opencode-setup-system/src/main.rs +++ b/crates/opencode-setup-system/src/main.rs @@ -186,6 +186,7 @@ pub const OPENCODE: Harness = Harness { // one on a disk it was never shipped to. embedded_setups: include!(concat!(env!("OUT_DIR"), "/embedded_setups.rs")), software: Some(software::SOFTWARE), + instruction_region: Some("AGENTS.md"), }; fn main() -> ExitCode { diff --git a/crates/provider-v3/src/argv.rs b/crates/provider-v3/src/argv.rs index aa95412..18d0d68 100644 --- a/crates/provider-v3/src/argv.rs +++ b/crates/provider-v3/src/argv.rs @@ -118,6 +118,10 @@ const fn plan_usage(command: Command) -> Usage { "--target-scope", "which scope this target is; accepted and not yet acted on", ), + ( + "--instruction-section", + "marked user-global instruction bytes for patch_instruction_region", + ), ], note: "Produce a plan. Always pure: reads the target and the local disk, opens no socket.", } @@ -360,7 +364,13 @@ pub struct PlanRequest { /// declarable since `0.0.7` and operable never, because no request field /// carried a scope; this is the field, arriving one release ahead of the /// behaviour on purpose. + /// Which scope the consumer resolved this target to be. pub target_scope: Option, + /// Marked instruction bytes for `patch_instruction_region`. + /// + /// Absent on every other operation. The consumer sends this only after a + /// provider declares both the operation and `instruction_section`. + pub instruction_section: Option, } /// Read `--target-scope`, refusing a value this build does not know. @@ -521,6 +531,7 @@ where prefix: flags.take_prefix()?, software_version: flags.take_optional("--software-version"), target_scope: take_target_scope(&mut flags)?, + instruction_section: flags.take_optional("--instruction-section"), }, } } @@ -565,6 +576,15 @@ struct Flags { /// two different things and only one of them would happen. const REPEATABLE: &[&str] = &["--software-artifact"]; +/// True when `token` is another flag, not a value that happens to start with +/// dashes. Cursor's `alwaysApply` payload begins with YAML `---`; treating +/// every `--` prefix as a missing value refused that first write. +fn looks_like_flag(token: &str) -> bool { + token + .strip_prefix("--") + .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_alphabetic())) +} + impl Flags { /// Split a bare `--` off the end, keeping what follows verbatim. /// @@ -601,7 +621,7 @@ impl Flags { let Some(value) = tokens.get(index + 1) else { return Err(local(format!("{token} has no value"))); }; - if value.starts_with("--") { + if looks_like_flag(value) { return Err(local(format!("{token} has no value"))); } let seen = values.entry(token.clone()).or_default(); @@ -1021,6 +1041,57 @@ mod tests { assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation)); } + #[test] + fn patch_instruction_region_parses_the_marked_section() { + let tokens = with_target( + "plan-operation", + &[ + "--operation", + "patch_instruction_region", + "--provider-release-digest", + DIGEST, + "--operation-id", + "operation_01TEST", + "--expires-at", + "2026-08-23T15:00:00Z", + "--instruction-section", + ":::begin-ai-stp\nhello\n:::end-ai-stp\n", + ], + ); + let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else { + panic!("expected a plan invocation"); + }; + assert_eq!(request.operation, Operation::PatchInstructionRegion); + assert_eq!( + request.instruction_section.as_deref(), + Some(":::begin-ai-stp\nhello\n:::end-ai-stp\n") + ); + } + + #[test] + fn patch_instruction_region_parses_yaml_frontmatter_before_markers() { + let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n"; + let tokens = with_target( + "plan-operation", + &[ + "--operation", + "patch_instruction_region", + "--provider-release-digest", + DIGEST, + "--instruction-section", + section, + "--operation-id", + "operation_01TEST", + "--expires-at", + "2026-08-23T15:00:00Z", + ], + ); + let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else { + panic!("expected a plan invocation"); + }; + assert_eq!(request.instruction_section.as_deref(), Some(section)); + } + /// A consumer that sends a scope must not be refused by a build that cannot /// yet use it. /// diff --git a/crates/provider-v3/src/info.rs b/crates/provider-v3/src/info.rs index b94fda9..1efdc2e 100644 --- a/crates/provider-v3/src/info.rs +++ b/crates/provider-v3/src/info.rs @@ -409,6 +409,7 @@ impl ProviderInfo { TargetScope::REQUEST_FIELD.to_owned(), EndState::REQUEST_FIELD.to_owned(), "capture_mode".to_owned(), + "instruction_section".to_owned(), ], // Declared 2026-09-02 in the same order as the two above: kit 0.2.9 // names the member (`provider-info.schema.json`), ai-stp-cli diff --git a/crates/provider-v3/src/plan.rs b/crates/provider-v3/src/plan.rs index 597abec..4385242 100644 --- a/crates/provider-v3/src/plan.rs +++ b/crates/provider-v3/src/plan.rs @@ -256,6 +256,12 @@ pub struct PlanArtifact { /// verifying. Every other operation has one sentence per path already. #[serde(skip_serializing_if = "Vec::is_empty")] pub end_state: Vec, + /// Target-relative path of a `patch_instruction_region` write. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_path: Option, + /// Full file text after splicing. Apply writes these bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub instruction_text: Option, /// What applying it will do, in order. Never empty. pub effects: Vec, } @@ -305,6 +311,10 @@ pub struct PlanInputs<'a> { /// Per-path end states, for a `remove` that carries a bundle. Empty /// otherwise, and refused on any other operation. pub end_state: Vec, + /// Target-relative attachment path, when this is a region patch. + pub instruction_path: Option, + /// Full file text after splicing, when this is a region patch. + pub instruction_text: Option, /// What applying it will do. Never empty. pub effects: Vec, } @@ -418,6 +428,8 @@ impl PlanArtifact { software_version: inputs.software_version.map(str::to_owned), software_artifacts: inputs.software_artifacts, end_state: inputs.end_state, + instruction_path: inputs.instruction_path, + instruction_text: inputs.instruction_text, effects: inputs.effects, }) } @@ -567,6 +579,8 @@ mod tests { native_capture: None, permission_profile: Some("default".to_owned()), expires_at: "2026-08-23T15:00:00Z", + instruction_path: None, + instruction_text: None, effects: vec!["write settings.json".to_owned()], } } diff --git a/crates/provider-v3/src/vocabulary.rs b/crates/provider-v3/src/vocabulary.rs index f18cafb..3f9c2c1 100644 --- a/crates/provider-v3/src/vocabulary.rs +++ b/crates/provider-v3/src/vocabulary.rs @@ -135,6 +135,12 @@ pub enum Operation { SoftwareRemove, /// Start the product. Optional. Launch, + /// Patch a marked user-global instruction region. Optional. + /// + /// The consumer sends the marked bytes on `--instruction-section`. This is + /// not a setup install: whole-setup apply must not treat the region as + /// payload it is free to empty. + PatchInstructionRegion, } impl Operation { @@ -150,6 +156,7 @@ impl Operation { Self::SoftwareUpdate, Self::SoftwareRemove, Self::Launch, + Self::PatchInstructionRegion, ]; /// The operations every provider must support. @@ -174,6 +181,44 @@ impl Operation { Self::SoftwareRemove, ]; + /// [`CORE`] plus the instruction-region patch, without software or launch. + pub const CORE_AND_INSTRUCTION: &'static [Self] = &[ + Self::Backup, + Self::Install, + Self::Remove, + Self::Replace, + Self::Restore, + Self::PatchInstructionRegion, + ]; + + /// [`CORE_AND_SOFTWARE`] plus the instruction-region patch, without launch. + pub const CORE_AND_SOFTWARE_AND_INSTRUCTION: &'static [Self] = &[ + Self::Backup, + Self::Install, + Self::Remove, + Self::Replace, + Self::Restore, + Self::Reset, + Self::SoftwareInstall, + Self::SoftwareUpdate, + Self::SoftwareRemove, + Self::PatchInstructionRegion, + ]; + + /// [`ALL`] without the instruction-region patch. + pub const ALL_WITHOUT_INSTRUCTION: &'static [Self] = &[ + Self::Install, + Self::Replace, + Self::Backup, + Self::Restore, + Self::Remove, + Self::Reset, + Self::SoftwareInstall, + Self::SoftwareUpdate, + Self::SoftwareRemove, + Self::Launch, + ]; + /// The optional operations that install the product itself. /// /// Declared together or not at all. A provider offering to install but not @@ -198,6 +243,7 @@ impl Operation { Self::SoftwareUpdate => "software_update", Self::SoftwareRemove => "software_remove", Self::Launch => "launch", + Self::PatchInstructionRegion => "patch_instruction_region", } } diff --git a/install.ps1 b/install.ps1 index f971024..e6962b4 100644 --- a/install.ps1 +++ b/install.ps1 @@ -7,7 +7,7 @@ # powershell -ExecutionPolicy Bypass -File install.ps1 -Version 0.1.0 [CmdletBinding()] param( - [string]$Version = "0.0.72", + [string]$Version = "0.0.73", [string]$InstallDir = "$env:LOCALAPPDATA\Programs\opencode-setup-system" ) $ErrorActionPreference = "Stop" diff --git a/install.sh b/install.sh index 4f4a023..b6b8b11 100644 --- a/install.sh +++ b/install.sh @@ -14,7 +14,7 @@ set -eu REPO="NDDev-OpenNetwork/opencode-setup-system" BINARY="opencode-setup-system" -VERSION="${1:-0.0.72}" +VERSION="${1:-0.0.73}" PREFIX="${OPENCODE_INSTALL_DIR:-$HOME/.local/bin}" case "$(uname -s)" in diff --git a/provider-kit/v3/KIT-IDENTITY.json b/provider-kit/v3/KIT-IDENTITY.json index ca45c47..1ed9e58 100644 --- a/provider-kit/v3/KIT-IDENTITY.json +++ b/provider-kit/v3/KIT-IDENTITY.json @@ -1,12 +1,12 @@ { - "aggregate_digest": "sha256:6ab195790be1c7dfacf6e6ab5a892244097cc2002f97090ff9a4b3c3174ed357", + "aggregate_digest": "sha256:e2a35eaf2e0f834913962d9a6337948eeb78290b4ad8c3af7674620215610b0e", "files": [ "conformance-cases.json", "manifest.json", "provider-info.schema.json", "status-response.schema.json" ], - "kit_version": "0.2.11", + "kit_version": "0.2.13", "protocol_version": 3, "schema": "ai-stp-provider-kit-identity/1" } diff --git a/provider-kit/v3/SHA256SUMS b/provider-kit/v3/SHA256SUMS index 6f9f240..e4d165d 100644 --- a/provider-kit/v3/SHA256SUMS +++ b/provider-kit/v3/SHA256SUMS @@ -1,4 +1,4 @@ fe04d03b15cfe8d5b61835eafd3ea788074684e2be69d419f9c22a37ea0461b7 conformance-cases.json -ae323ef26648fbaf441aa179008d0899ec79af88b3ae1e738e5bc75b1bbc8f30 manifest.json -64514da0c61012b5323987d72ee75f5ce8d6451ed557331a634b17aeac8d8966 provider-info.schema.json +cfc55bb6f301589ded449c12ba0309e12238db7bbce3f5e63fc45e9731d4bee5 manifest.json +2f28fef790dc2685e9790f4ae3123b1c6d581c94f87d15b75823ea8d575036aa provider-info.schema.json 7e67760f24bbc42a1242dad7ad3b378eef7ae15023c6813a33d582dfd2232293 status-response.schema.json diff --git a/provider-kit/v3/manifest.json b/provider-kit/v3/manifest.json index 9a4afcc..7516384 100644 --- a/provider-kit/v3/manifest.json +++ b/provider-kit/v3/manifest.json @@ -39,7 +39,7 @@ ], "decision": "docs/adr/ADR-0061-capability-negotiated-provider-protocol-v3.md", "generated_from": "apps/cli/src/ai_stp_cli/provider/protocol_v3.py", - "kit_version": "0.2.11", + "kit_version": "0.2.13", "operation_network": { "backup": [ { @@ -67,6 +67,16 @@ "phase": "execute" } ], + "patch_instruction_region": [ + { + "network_requirement": "none", + "phase": "plan" + }, + { + "network_requirement": "none", + "phase": "apply" + } + ], "remove": [ { "network_requirement": "none", @@ -151,6 +161,7 @@ ], "optional_operations": [ "launch", + "patch_instruction_region", "reset", "software_install", "software_remove", diff --git a/provider-kit/v3/provider-info.schema.json b/provider-kit/v3/provider-info.schema.json index 53ba00d..cc88b58 100644 --- a/provider-kit/v3/provider-info.schema.json +++ b/provider-kit/v3/provider-info.schema.json @@ -20,6 +20,7 @@ "enum": [ "capture_mode", "end_state", + "instruction_section", "target_scope" ], "type": "string" @@ -267,7 +268,8 @@ "software_install", "software_update", "software_remove", - "launch" + "launch", + "patch_instruction_region" ], "type": "string" }, diff --git a/references/opencode-baseline.json b/references/opencode-baseline.json index 052f148..500c9ff 100644 --- a/references/opencode-baseline.json +++ b/references/opencode-baseline.json @@ -109,7 +109,7 @@ "OPENCODE_DISABLE_PROJECT_CONFIG", "OPENCODE_DISABLE_SHARE" ], - "verified_at": "2026-09-16T13:22:29+00:00", + "verified_at": "2026-09-19T14:52:16+00:00", "native_surfaces": { "verified_at": "2026-08-31", "config_home": "~/.config/opencode", @@ -331,9 +331,9 @@ } }, "version": "1.18.31", - "verified_at": "2026-09-16T13:22:29+00:00" + "verified_at": "2026-09-19T14:52:16+00:00" }, - "setup_catalogue_digest": "sha256:637cc0372e587d7360dd8542554f59d8aa464af96475587cbecdaba7c36093ac", + "setup_catalogue_digest": "sha256:e251847ceb02bd92b12fd07ba83b6ac836d37e3cbdbf9aba1607719b281ab7e8", "previous_software_artifacts": { "command": "opencode", "shape": "gzip-tar", diff --git a/setups/nddev-builder/home/skills/nddev-builder/references/authoring-agents.md b/setups/nddev-builder/home/skills/nddev-builder/references/authoring-agents.md index 054243b..c5cdc68 100644 --- a/setups/nddev-builder/home/skills/nddev-builder/references/authoring-agents.md +++ b/setups/nddev-builder/home/skills/nddev-builder/references/authoring-agents.md @@ -18,8 +18,11 @@ Generated from the vendor's own reference and the pinned binary. Do not edit: th | `model` | no | Model override. | | `temperature` | no | Sampling temperature. | | `top_p` | no | Nucleus sampling. | -| `tools` | no | Enable or disable tools. | -| `permission` | no | `allow`, `deny` or `ask`; keys are matched as wildcard patterns against the tool name, so the same syntax covers built-ins, custom tools and MCP tools. | +| `steps` | no | Maximum agentic steps. Replaces the deprecated `maxSteps`; at the limit the agent is told to answer with what it has. | +| `prompt` | no | A system prompt, or `{file:...}` to load one -- the JSON-config form; in a Markdown file the body is already the prompt. | +| `hidden` | no | Keep a subagent out of the `@` autocomplete; for internal agents invoked by others. | +| `tools` | no | **Deprecated** -- write `permission` instead. | +| `permission` | no | `allow`, `deny` or `ask`; keys are matched as wildcard patterns against the tool name, so the same syntax covers built-ins, custom tools and MCP tools. Its `task` key gates which subagents this one may invoke. | | `disable` | no | Boolean. | | `color` | no | Display colour. | @@ -54,11 +57,16 @@ Generated from the same rows as the section above, for every harness in this est | `mode` | — | — | yes | — | | `temperature` | — | — | yes | — | | `top_p` | — | — | yes | — | +| `steps` | — | — | yes | — | +| `prompt` | — | — | yes | — | +| `hidden` | — | — | yes | yes | | `permission` | — | — | yes | — | | `disable` | — | — | yes | — | | `mainAgent` | — | — | — | yes | | `subagent` | — | — | — | yes | | `commandExecutionPolicy` | — | — | — | yes | +| `inheritMcp` | — | — | — | yes | +| `inheritCustomizations` | — | — | — | yes | **The part that does not, and says nothing when it does not**: a field absent from a column is not rejected there -- it is read past. Nothing warns, no run fails, and the component behaves differently with the same bytes. Where the field was carrying a restriction, the restriction is simply gone. Check the column before relying on one.