diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b438df..c9ebf13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [0.14.8] — 2026-09-25 + +`b10x-harness` joins the catalog, and `b10x` chooses a prebuilt archive only where one exists for +the machine. + +- `catalog.json`: `b10x-harness` (the Beyond10x agent loop that Metaharness's `b10x` adapter runs) + is an optional binary of `aep`, installed with `b10x install b10x-harness`. It runs on Linux only: + a new `platforms` field on a catalog binary keeps `b10x` from planning it elsewhere. +- `b10x` reads which targets the newest release's `SHA256SUMS` lists and chooses prebuilt only when + this machine's target is among them; otherwise `cargo`, or a warning that names the target. A + release with an archive for some machines no longer sends the others to a download that fails. +- `b10x install ` follows the same default. Since 0.14.7 it still chose `cargo` whenever `cargo` + was on `PATH`, although its help and the docs said prebuilt. +- `aep:init`: `metaharness` is optional in the plan and installed with `b10x install metaharness`; + the skill said `b10x init aep` offers it. +- Docs name `b10x-harness` beside `metaharness`. + ## [0.14.7] — 2026-09-25 The operator's decision on the install method, asked for by a team adopting ESS: anyone with Rust diff --git a/Cargo.lock b/Cargo.lock index d11a574..49671d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "agentplugins-check" -version = "0.14.7" +version = "0.14.8" dependencies = [ "clap", "serde", @@ -76,7 +76,7 @@ dependencies = [ [[package]] name = "b10x" -version = "0.14.7" +version = "0.14.8" dependencies = [ "clap", "serde", diff --git a/Cargo.toml b/Cargo.toml index 34099f7..064f917 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["crates/agentplugins-check", "crates/b10x"] [workspace.package] -version = "0.14.7" +version = "0.14.8" edition = "2021" rust-version = "1.85" license = "Apache-2.0" diff --git a/catalog.json b/catalog.json index 37937cd..063cf4f 100644 --- a/catalog.json +++ b/catalog.json @@ -26,6 +26,15 @@ "archive": { "repository": "beyond10x/metaharness" }, "cargo": { "repository": "beyond10x/metaharness", "package": "metaharness-cli" } } + }, + { + "name": "b10x-harness", + "optional": true, + "platforms": ["linux"], + "install": { + "archive": { "repository": "beyond10x/harness" }, + "cargo": { "repository": "beyond10x/harness", "package": "b10x-harness-cli" } + } } ] }, diff --git a/crates/agentplugins-check/src/concept.rs b/crates/agentplugins-check/src/concept.rs index bbcbd54..14c5765 100644 --- a/crates/agentplugins-check/src/concept.rs +++ b/crates/agentplugins-check/src/concept.rs @@ -32,7 +32,14 @@ impl Plugins { const LIFECYCLE: &[&str] = &["init", "upgrade"]; /// CLIs whose versions a plugin must not quote (R5): the newest release is the only one they describe. -const CLIS: &[&str] = &["aep", "ess", "worktree", "metaharness", "protocol"]; +const CLIS: &[&str] = &[ + "aep", + "ess", + "worktree", + "metaharness", + "b10x-harness", + "protocol", +]; /// Every ` x.y.z` in a line, case-insensitive, with optional backticks and a `v`. #[must_use] diff --git a/crates/b10x/src/catalog.rs b/crates/b10x/src/catalog.rs index e45127d..52c489d 100644 --- a/crates/b10x/src/catalog.rs +++ b/crates/b10x/src/catalog.rs @@ -64,10 +64,28 @@ pub struct Binary { /// Reported but never installed unprompted. #[serde(default)] pub optional: bool, + /// Operating systems it runs on (`linux`, `macos`); empty means every one. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub platforms: Vec, /// The ways it can be installed. pub install: Install, } +impl Binary { + /// Whether it runs on the operating system of `target` (a release-archive triple). + #[must_use] + pub fn runs_on(&self, target: Option<&str>) -> bool { + self.platforms.is_empty() + || target.is_some_and(|target| { + self.platforms.iter().any(|os| match os.as_str() { + "linux" => target.contains("-linux"), + "macos" => target.contains("-apple-darwin"), + _ => false, + }) + }) + } +} + /// The ways a binary can be installed; at least one is present. #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] pub struct Install { @@ -189,6 +207,25 @@ impl Catalog { #[cfg(test)] mod tests { + #[test] + fn a_binary_limited_to_linux_runs_only_on_linux_targets() { + let mut binary = super::Binary { + name: "b10x-harness".to_owned(), + optional: true, + platforms: vec!["linux".to_owned()], + install: super::Install { + archive: None, + cargo: None, + }, + }; + assert!(binary.runs_on(Some("x86_64-unknown-linux-gnu"))); + assert!(binary.runs_on(Some("aarch64-unknown-linux-gnu"))); + assert!(!binary.runs_on(Some("aarch64-apple-darwin"))); + assert!(!binary.runs_on(None)); + binary.platforms.clear(); + assert!(binary.runs_on(None)); + } + use super::*; #[test] diff --git a/crates/b10x/src/install.rs b/crates/b10x/src/install.rs index 172b173..2101771 100644 --- a/crates/b10x/src/install.rs +++ b/crates/b10x/src/install.rs @@ -1,6 +1,7 @@ //! Installing one binary at one exact release: a checksummed release archive, or `cargo install` //! from the tag. Either way the binary is staged first and moved into place with one rename. +use std::collections::BTreeSet; use std::path::{Path, PathBuf}; use std::process::Command; @@ -36,15 +37,42 @@ fn run(program: &str, arguments: &[&str]) -> Result<(), String> { } } -/// The hex SHA-256 listed for `file` in a `SHA256SUMS` document. +/// The release archive of `name` at `tag` for `target`: `--.tar.gz`. #[must_use] -pub fn listed_digest<'a>(sums: &'a str, file: &str) -> Option<&'a str> { - sums.lines().find_map(|line| { +pub fn archive_name(name: &str, tag: &str, target: &str) -> String { + format!("{}{target}.tar.gz", archive_prefix(name, tag)) +} + +/// What every archive name of `name` at `tag` starts with, before the target. +fn archive_prefix(name: &str, tag: &str) -> String { + format!("{name}-{}-", tag.trim_start_matches('v')) +} + +/// `(digest, file)` for every line of a `SHA256SUMS` document. +fn listed(sums: &str) -> impl Iterator { + sums.lines().filter_map(|line| { let (digest, name) = line.split_once(char::is_whitespace)?; - (name.trim().trim_start_matches('*') == file).then_some(digest) + Some((digest, name.trim().trim_start_matches('*'))) }) } +/// The hex SHA-256 listed for `file` in a `SHA256SUMS` document. +#[must_use] +pub fn listed_digest<'a>(sums: &'a str, file: &str) -> Option<&'a str> { + listed(sums).find_map(|(digest, name)| (name == file).then_some(digest)) +} + +/// The targets a `SHA256SUMS` document lists an archive of `name` at `tag` for. +#[must_use] +pub fn listed_targets(sums: &str, name: &str, tag: &str) -> BTreeSet { + let prefix = archive_prefix(name, tag); + listed(sums) + .filter_map(|(_, file)| file.strip_prefix(&prefix)?.strip_suffix(".tar.gz")) + .filter(|target| !target.is_empty() && !target.contains('/')) + .map(str::to_owned) + .collect() +} + /// Lower-case hex of bytes. #[must_use] pub fn hex(bytes: &[u8]) -> String { @@ -72,7 +100,7 @@ fn staging(name: &str) -> Result { fn from_archive(name: &str, tag: &str, repository: &str, stage: &Path) -> Result { let target = target()?; let version = tag.trim_start_matches('v'); - let archive = format!("{name}-{version}-{target}.tar.gz"); + let archive = archive_name(name, tag, target); let base = format!("/{repository}/releases/download/{tag}"); let archive_path = stage.join(&archive); let sums_path = stage.join("SHA256SUMS"); @@ -199,4 +227,30 @@ mod tests { ); assert_eq!(listed_digest(sums, "ess-0.30.0.tar.gz"), None); } + + #[test] + fn targets_are_read_from_the_archive_names_sums_lists() { + let sums = "\ +aaa b10x-harness-0.13.2-x86_64-unknown-linux-gnu.tar.gz +bbb b10x-harness-0.13.2-x86_64-unknown-linux-gnu.tar.gz.sig +ccc *b10x-harness-0.13.1-aarch64-unknown-linux-gnu.tar.gz +ddd harness-0.13.2-aarch64-apple-darwin.tar.gz +eee b10x-harness-lsp-0.13.2-aarch64-unknown-linux-gnu.tar.gz +fff b10x-harness-0.13.2-.tar.gz +"; + assert_eq!( + listed_targets(sums, "b10x-harness", "0.13.2"), + BTreeSet::from(["x86_64-unknown-linux-gnu".to_owned()]) + ); + assert_eq!( + listed_targets(sums, "b10x-harness", "v0.13.2"), + listed_targets(sums, "b10x-harness", "0.13.2"), + "a leading v on the tag is not part of the archive name" + ); + assert!(listed_targets("", "b10x-harness", "0.13.2").is_empty()); + for target in listed_targets(sums, "b10x-harness", "0.13.2") { + let name = archive_name("b10x-harness", "0.13.2", &target); + assert_eq!(listed_digest(sums, &name), Some("aaa")); + } + } } diff --git a/crates/b10x/src/main.rs b/crates/b10x/src/main.rs index 154b657..c77fc8a 100644 --- a/crates/b10x/src/main.rs +++ b/crates/b10x/src/main.rs @@ -37,7 +37,7 @@ enum Top { /// Products, comma separated: aep, ess, worktree, connectors. #[arg(value_delimiter = ',')] products: Vec, - /// How to install CLIs; default: prebuilt, cargo when a release has no archive. + /// How to install CLIs; default: prebuilt, cargo when a release has no archive for this machine. #[arg(long, value_enum)] method: Option, /// Hosts to plan for. @@ -55,7 +55,7 @@ enum Top { /// Products, comma separated; default: every installed product. #[arg(value_delimiter = ',')] products: Vec, - /// How to install CLIs; default: prebuilt, cargo when a release has no archive. + /// How to install CLIs; default: prebuilt, cargo when a release has no archive for this machine. #[arg(long, value_enum)] method: Option, /// Hosts to plan for. @@ -91,7 +91,7 @@ enum Top { /// Target directory; defaults to `~/.local/bin` (prebuilt) or `~/.cargo/bin` (cargo). #[arg(long)] dir: Option, - /// How to install; default: prebuilt, cargo when a release has no archive. + /// How to install; default: prebuilt when the release has an archive for this machine, else cargo. #[arg(long, value_enum)] method: Option, }, @@ -108,7 +108,7 @@ enum Setup { /// Hosts to plan for. #[arg(long, value_enum, default_value_t = Hosts::All)] host: Hosts, - /// How to install CLIs; default: prebuilt, cargo when a release has no archive. + /// How to install CLIs; default: prebuilt, cargo when a release has no archive for this machine. #[arg(long, value_enum)] method: Option, /// Print the plan as JSON. @@ -327,6 +327,7 @@ fn make_plan( home: &home, only, method, + target: install::target().ok().map(str::to_owned), upgrade, }; Ok(plan::plan(&context, &inventory)) @@ -570,16 +571,34 @@ fn install_one( None => resolve::latest_tag(repository) .ok_or_else(|| format!("no release found for {repository}"))?, }; - let method = method.map_or_else( - || { - if inventory::copies_on_path("cargo").is_empty() { - catalog::Method::Prebuilt - } else { - catalog::Method::Cargo - } - }, - MethodArg::method, - ); + let target = install::target().ok(); + if !binary.runs_on(target) { + return Err(format!( + "`{name}` runs on {} only", + binary.platforms.join(", ") + )); + } + let method = if let Some(method) = method { + method.method() + } else { + let listed = binary.install.archive.is_some() + && target.is_some_and(|target| { + resolve::fetch(&format!( + "/{repository}/releases/download/{tag}/SHA256SUMS" + )) + .is_some_and(|sums| install::listed_targets(&sums, name, &tag).contains(target)) + }); + if listed { + catalog::Method::Prebuilt + } else if binary.install.cargo.is_some() && !inventory::copies_on_path("cargo").is_empty() { + catalog::Method::Cargo + } else { + return Err(format!( + "{tag} of `{name}` has no prebuilt archive for {} and `cargo` is not on PATH", + target.unwrap_or("this machine") + )); + } + }; let home = inventory::home(); let directory = dir.unwrap_or_else(|| match method { catalog::Method::Prebuilt => home.join(".local/bin"), diff --git a/crates/b10x/src/plan.rs b/crates/b10x/src/plan.rs index 25861da..d438e4d 100644 --- a/crates/b10x/src/plan.rs +++ b/crates/b10x/src/plan.rs @@ -12,7 +12,7 @@ use std::path::Path; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use crate::catalog::{Catalog, Install, Method}; +use crate::catalog::{Binary, Catalog, Install, Method}; use crate::inventory::{Host, HostState, Installed, Inventory}; use crate::resolve::Resolved; use crate::version; @@ -254,8 +254,11 @@ pub struct Context<'a> { /// Plan only the selected products (`init`, `upgrade`): nothing else is uninstalled, migrated or /// removed. `false` makes the selection the whole desired state (`setup plan`). pub only: bool, - /// How to install binaries; `None` picks the prebuilt archive when the release has one, else cargo. + /// How to install binaries; `None` picks the prebuilt archive when the release has one for + /// [`Context::target`], else cargo. pub method: Option, + /// This machine's release-archive target triple; `None` when no archive is built for it. + pub target: Option, /// Update only what is installed: no new plugins, and a host with nothing Beyond10x on it is /// left alone (`upgrade`). pub upgrade: bool, @@ -818,17 +821,23 @@ fn plan_settings( } } +/// Whether the newest release of `binary` carries a prebuilt archive for this machine's target. +fn has_archive(context: &Context<'_>, binary: &Binary) -> bool { + binary.install.archive.is_some() + && context.target.as_deref().is_some_and(|target| { + context + .resolved + .archive_targets + .get(&binary.name) + .is_some_and(|targets| targets.contains(target)) + }) +} + /// The install method for one binary: the explicit choice when it can be honoured, else the prebuilt -/// archive when the release has one, else cargo. -fn method(context: &Context<'_>, install: &Install) -> Option { - let archive = install.archive.is_some() - && context - .resolved - .archives - .get(install.repository()) - .copied() - .unwrap_or(false); - let cargo = install.cargo.is_some(); +/// archive when the release has one for this machine's target, else cargo. +fn method(context: &Context<'_>, binary: &Binary) -> Option { + let archive = has_archive(context, binary); + let cargo = binary.install.cargo.is_some(); if cargo && (matches!(context.method, Some(Method::Cargo)) || !archive) { Some(Method::Cargo) } else if archive { @@ -876,6 +885,27 @@ fn plan_binaries( .map(|state| state.copies.as_slice()) .unwrap_or_default(); let current = copies.first(); + if !binary.runs_on(context.target.as_deref()) { + if current.is_none() && !binary.optional { + findings.push(Finding { + level: Level::Warn, + host: None, + subject: subject.clone(), + detail: format!( + "runs on {} only; not installed on this machine", + binary.platforms.join(", ") + ), + }); + } else if current.is_none() { + findings.push(Finding { + level: Level::Note, + host: None, + subject: subject.clone(), + detail: format!("optional, runs on {} only", binary.platforms.join(", ")), + }); + } + continue; + } if current .is_some_and(|first| version::same(first.version.as_deref().unwrap_or(""), &tag)) { @@ -897,12 +927,13 @@ fn plan_binaries( ), }); } else { - let Some(method) = method(context, &binary.install) else { + let Some(method) = method(context, binary) else { + let target = context.target.as_deref().unwrap_or("this machine"); findings.push(Finding { level: Level::Warn, host: None, subject: subject.clone(), - detail: format!("{tag} has no prebuilt archive and `cargo` is not on PATH; install a Rust toolchain (https://rustup.rs) and plan again"), + detail: format!("{tag} has no prebuilt archive for {target} and `cargo` is not on PATH; install a Rust toolchain (https://rustup.rs) and plan again"), }); continue; }; @@ -1033,16 +1064,32 @@ mod tests { ("beyond10x/ess".to_owned(), "0.30.0".to_owned()), ("beyond10x/worktree".to_owned(), "0.7.0".to_owned()), ("beyond10x/metaharness".to_owned(), "0.7.0".to_owned()), + ("beyond10x/harness".to_owned(), "0.13.2".to_owned()), ]), - archives: BTreeMap::from([ - ("beyond10x/aep".to_owned(), true), - ("beyond10x/ess".to_owned(), true), - ("beyond10x/worktree".to_owned(), true), - ("beyond10x/metaharness".to_owned(), false), + archive_targets: BTreeMap::from([ + ("aep".to_owned(), every_target()), + ("ess".to_owned(), every_target()), + ("worktree".to_owned(), every_target()), + ("metaharness".to_owned(), BTreeSet::new()), + ( + "b10x-harness".to_owned(), + BTreeSet::from([X86_LINUX.to_owned()]), + ), ]), } } + const X86_LINUX: &str = "x86_64-unknown-linux-gnu"; + const ARM_LINUX: &str = "aarch64-unknown-linux-gnu"; + const ARM_MACOS: &str = "aarch64-apple-darwin"; + + fn every_target() -> BTreeSet { + [X86_LINUX, ARM_LINUX, "x86_64-apple-darwin", ARM_MACOS] + .into_iter() + .map(str::to_owned) + .collect() + } + fn binaries(ess: &[(&str, &str)]) -> Vec { vec![BinaryState { name: "ess".to_owned(), @@ -1067,6 +1114,7 @@ mod tests { home: Path::new("/opt/b10x-home"), only: false, method: None, + target: Some(X86_LINUX.to_owned()), upgrade: false, }; plan(&context, inventory) @@ -1306,6 +1354,7 @@ mod tests { home: Path::new("/opt/b10x-home"), only: true, method: None, + target: Some(X86_LINUX.to_owned()), upgrade: true, }; plan(&context, inventory) @@ -1428,6 +1477,7 @@ mod tests { home: Path::new("/opt/b10x-home"), only: true, method, + target: Some(X86_LINUX.to_owned()), upgrade: false, }; plan(&context, inventory) @@ -1477,6 +1527,98 @@ mod tests { assert_eq!(method_for(true, Some(Method::Cargo)), Some(Method::Cargo)); } + /// Plan `aep` on `target` with an older `b10x-harness` installed, whose newest release carries + /// an archive for x86-64 Linux only; `cargo_option` keeps or strips the catalog's cargo route. + fn plan_harness(target: Option<&str>, cargo_option: bool) -> Plan { + let mut catalog = Catalog::embedded(); + if !cargo_option { + for product in &mut catalog.products { + for binary in &mut product.binaries { + if binary.name == "b10x-harness" { + binary.install.cargo = None; + } + } + } + } + let resolved = resolved(); + let inventory = Inventory { + claude: Some(HostState::default()), + binaries: vec![BinaryState { + name: "b10x-harness".to_owned(), + copies: vec![Copy { + path: "/opt/b10x-home/.local/bin/b10x-harness".to_owned(), + version: Some("0.13.1".to_owned()), + }], + }], + ..Inventory::default() + }; + let context = Context { + catalog: &catalog, + resolved: &resolved, + selection: Some(BTreeSet::from(["aep".to_owned()])), + hosts: vec![Host::Claude], + home: Path::new("/opt/b10x-home"), + only: true, + method: None, + target: target.map(str::to_owned), + upgrade: false, + }; + plan(&context, &inventory) + } + + fn harness_method(plan: &Plan) -> Option { + plan.actions.iter().find_map(|action| match action { + Action::InstallBinary { name, method, .. } if name == "b10x-harness" => Some(*method), + _ => None, + }) + } + + #[test] + fn prebuilt_only_for_a_target_the_release_has_an_archive_for() { + assert_eq!( + harness_method(&plan_harness(Some(X86_LINUX), true)), + Some(Method::Prebuilt) + ); + assert_eq!( + harness_method(&plan_harness(Some(ARM_LINUX), true)), + Some(Method::Cargo) + ); + assert_eq!( + harness_method(&plan_harness(Some(ARM_MACOS), true)), + None, + "a Linux-only binary is not planned on macOS, not even with cargo" + ); + assert_eq!( + harness_method(&plan_harness(None, true)), + None, + "an unknown machine is not a platform the binary declares" + ); + } + + #[test] + fn no_archive_for_the_target_and_no_cargo_route_is_a_warning_not_an_action() { + assert_eq!( + harness_method(&plan_harness(Some(X86_LINUX), false)), + Some(Method::Prebuilt) + ); + { + let target = ARM_LINUX; + let plan = plan_harness(Some(target), false); + assert_eq!(harness_method(&plan), None); + assert!( + plan.findings + .iter() + .any(|finding| finding.level == Level::Warn + && finding.subject == "b10x-harness" + && finding + .detail + .contains(&format!("no prebuilt archive for {target}"))), + "{:#?}", + plan.findings + ); + } + } + #[test] fn a_release_without_archives_and_no_cargo_is_a_warning_not_an_action() { let inventory = Inventory { @@ -1534,6 +1676,7 @@ mod tests { home: Path::new("/opt/b10x-home"), only: true, method: None, + target: Some(X86_LINUX.to_owned()), upgrade: true, }; let plan = plan(&context, &inventory); diff --git a/crates/b10x/src/resolve.rs b/crates/b10x/src/resolve.rs index 9e389f4..f44f705 100644 --- a/crates/b10x/src/resolve.rs +++ b/crates/b10x/src/resolve.rs @@ -3,13 +3,14 @@ //! `plugin.json`, a plugin it points at from that repository's newest release, a binary bound to //! `latest` from the newest release. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::path::Path; use std::process::Command; use serde::{Deserialize, Serialize}; use crate::catalog::Catalog; +use crate::install::listed_targets; use crate::inventory::Inventory; /// Versions setup compares against. @@ -19,9 +20,10 @@ pub struct Resolved { pub plugins: BTreeMap, /// `owner/repo` → newest release tag. pub latest: BTreeMap, - /// `owner/repo` → whether its newest release carries prebuilt archives (`SHA256SUMS`). + /// Binary name → the targets its newest release carries a prebuilt archive for, as its + /// `SHA256SUMS` lists them; absent or empty when the release has none. #[serde(default)] - pub archives: BTreeMap, + pub archive_targets: BTreeMap>, } /// Where marketplace files are read from. @@ -58,15 +60,6 @@ pub fn fetch(url: &str) -> Option { .then(|| String::from_utf8_lossy(&output.stdout).into_owned()) } -/// Whether a URL answers (`curl -fsI`), following redirects. -#[must_use] -pub fn exists(url: &str) -> bool { - Command::new("curl") - .args(["-fsIL", "-o", "/dev/null", "--max-time", "30", url]) - .status() - .is_ok_and(|status| status.success()) -} - /// Where the newest release tags seen by the last plan are kept, for the offline session check. #[must_use] pub fn cache_path(home: &Path) -> std::path::PathBuf { @@ -210,21 +203,31 @@ pub fn resolve(catalog: &Catalog, source: &Source<'_>) -> Resolved { .and_then(|text| serde_json::from_str::(&text).ok()) .map(|marketplace| plugin_versions(&marketplace, source, &mut latest)) .unwrap_or_default(); - let mut archives = BTreeMap::new(); + let mut archive_targets = BTreeMap::new(); + let mut sums_of = BTreeMap::new(); for product in &catalog.products { for binary in &product.binaries { let repository = binary.install.repository(); if let (Some(tag), Some(_)) = (tag(&mut latest, repository), &binary.install.archive) { - let sums = - format!("/{repository}/releases/download/{tag}/SHA256SUMS"); - archives.insert(repository.to_owned(), exists(&sums)); + let sums: &Option = + sums_of.entry(repository.to_owned()).or_insert_with(|| { + fetch(&format!( + "/{repository}/releases/download/{tag}/SHA256SUMS" + )) + }); + archive_targets.insert( + binary.name.clone(), + sums.as_deref() + .map(|sums| listed_targets(sums, &binary.name, &tag)) + .unwrap_or_default(), + ); } } } Resolved { plugins, latest, - archives, + archive_targets, } } diff --git a/plugins/aep/.claude-plugin/plugin.json b/plugins/aep/.claude-plugin/plugin.json index 35b2f6a..f0a1f39 100644 --- a/plugins/aep/.claude-plugin/plugin.json +++ b/plugins/aep/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "name": "aep", "displayName": "AEP", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves: decomposition, plan critique, reverse engineering, story scoping, implementation and adversarial review.", - "version": "0.14.7", + "version": "0.14.8", "author": { "name": "Beyond10x" }, diff --git a/plugins/aep/.codex-plugin/plugin.json b/plugins/aep/.codex-plugin/plugin.json index e08a0cf..dd8492e 100644 --- a/plugins/aep/.codex-plugin/plugin.json +++ b/plugins/aep/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "aep", - "version": "0.14.7", + "version": "0.14.8", "description": "Plan governed work in the AEP artifact store and deliver it in reviewed waves.", "author": { "name": "Beyond10x" diff --git a/plugins/aep/skills/init/SKILL.md b/plugins/aep/skills/init/SKILL.md index 94eb2b4..df4c5eb 100644 --- a/plugins/aep/skills/init/SKILL.md +++ b/plugins/aep/skills/init/SKILL.md @@ -49,5 +49,5 @@ A skill that is not loaded yet in this session prints with `b10x skill aep:` adds +one. `b10x-harness` runs on Linux only. diff --git a/website/docs/plugins/b10x.md b/website/docs/plugins/b10x.md index 60f7dd3..ad2e417 100644 --- a/website/docs/plugins/b10x.md +++ b/website/docs/plugins/b10x.md @@ -34,8 +34,12 @@ It provides: | `b10x check` | the session-start drift check; offline, prints only problems | | `b10x install [--tag ] [--method cargo\|prebuilt]` | install one CLI | -CLIs install from the release's checksummed prebuilt archive by default, and with `cargo` when -`--method cargo` asks for it or a release has no archive. `B10X_MARKETPLACE=` makes `b10x` read and register that +The CLIs are `aep`, `ess` and `worktree`, plus the optional `metaharness` and `b10x-harness` of +the `aep` product. They install from the release's checksummed prebuilt archive by default, and with +`cargo` when `--method cargo` asks for it or the release has no archive for this machine's target +(its `SHA256SUMS` lists the targets it has). A binary limited to some operating systems +(`b10x-harness`: Linux) is not installed elsewhere. `B10X_MARKETPLACE=` makes `b10x` +read and register that checkout instead of this repository, for testing an unpublished marketplace. ## How it stays current