diff --git a/CHANGELOG.md b/CHANGELOG.md
index dd625d5..c94414e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,6 +20,37 @@ sibling project the same week this note was added.
## [Unreleased]
+## [0.0.49] - 2026-08-31
+
+A software operation applied through `ai-stp` now answers with the
+plan digest it was handed. It did not before, in any of the seven, so `harness
+install`, `harness update` and `harness remove` through the consumer refused
+after the program had already been installed: the effect landed and the
+operation stayed unverified over a prefix holding a working build. The
+configuration operations always carried the echo; the program lifecycle owes the
+same one, and the contract says so.
+
+The consumer released tolerance ahead of this, as 0.0.12: a missing echo is
+accepted for the three program operations, a mismatched one is still refused,
+and a configuration operation must still carry both. That tolerance exists in
+0.0.12 and in no earlier published version, so 0.0.11 and before refuse this
+release's predecessor and 0.0.12 accepts both.
+
+Nothing here could have found it. Every test asked whether the provider does
+what its own answer says, and it did -- the answer was consistent with itself and
+identical across all seven. The consumer found it by running the released
+0.0.48 through its own install path. The new test asserts the echo against the
+digest the wire was handed, for all three program operations.
+
+Beside it, the rendered lock file is now projected from this workspace's own
+committed resolution rather than re-resolved inside each tree, so a published
+tree no longer depends on which machine rendered it; twenty-one recorded product
+measurements were re-asked against the artifacts their baselines pin, three of
+which returned a false negative because the search term was a label this project
+invented rather than a string the product carries; and the schema survey was
+completed for the four harnesses that had never been asked, including one that
+publishes a manifest schema this provider must not declare.
+
## [0.0.48] - 2026-08-31
Antigravity's official updater publishes native Windows x86_64 and
diff --git a/Cargo.lock b/Cargo.lock
index 6e25d82..c894eea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -66,7 +66,7 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "harness-runtime"
-version = "0.0.48"
+version = "0.0.49"
dependencies = [
"provider-v3",
"serde",
@@ -128,7 +128,7 @@ dependencies = [
[[package]]
name = "opencode-setup-system"
-version = "0.0.48"
+version = "0.0.49"
dependencies = [
"harness-runtime",
"provider-v3",
@@ -147,7 +147,7 @@ dependencies = [
[[package]]
name = "provider-v3"
-version = "0.0.48"
+version = "0.0.49"
dependencies = [
"serde",
"serde_json",
@@ -209,7 +209,7 @@ dependencies = [
[[package]]
name = "setup-core"
-version = "0.0.48"
+version = "0.0.49"
dependencies = [
"miniz_oxide",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 4c8998d..6fd9515 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ members = [
]
[workspace.package]
-version = "0.0.48"
+version = "0.0.49"
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.48" }
-provider-v3 = { path = "crates/provider-v3", version = "0.0.48" }
-harness-runtime = { path = "crates/harness-runtime", version = "0.0.48" }
+setup-core = { path = "crates/setup-core", version = "0.0.49" }
+provider-v3 = { path = "crates/provider-v3", version = "0.0.49" }
+harness-runtime = { path = "crates/harness-runtime", version = "0.0.49" }
[workspace.lints.rust]
unsafe_code = "forbid"
diff --git a/README.md b/README.md
index 1d612ed..fd183a7 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.48 \
+ ghcr.io/nddev-opennetwork/opencode-setup-system:0.0.49 \
status --target /config/
--json
```
diff --git a/SUPPORT.md b/SUPPORT.md
index f61c23d..497c04a 100644
--- a/SUPPORT.md
+++ b/SUPPORT.md
@@ -167,10 +167,10 @@ Configuration home as the product documents it: `~/.config/opencode`.
| Path | Component kinds routed here | Decided by |
| --- | --- | --- |
| `AGENTS.md` | `instruction` | [source](https://opencode.ai/docs/rules) |
-| `opencode.json` | `setting` | [source](https://opencode.ai/docs/config; route measured by running `debug config` against a temporary home) |
-| `skills` | `skill` | [source](https://opencode.ai/docs/skills; routing exercised against the pinned product) |
-| `agents` | `agent` | [source](https://opencode.ai/docs/agents; routing exercised against the pinned product) |
-| `commands` | `command` | [source](https://opencode.ai/docs/commands; routing exercised against the pinned product) |
+| `opencode.json` | `setting` | [source](https://opencode.ai/docs/config) -- route measured by running `debug config` against a temporary home |
+| `skills` | `skill` | [source](https://opencode.ai/docs/skills) -- routing exercised against the pinned product |
+| `agents` | `agent` | [source](https://opencode.ai/docs/agents) -- routing exercised against the pinned product |
+| `commands` | `command` | [source](https://opencode.ai/docs/commands) -- routing exercised against the pinned product |
| `plugins` | `plugin` | [source](https://opencode.ai/docs/plugins) |
| `tui.json` | -- | [source](https://opencode.ai/docs/tui) |
@@ -185,7 +185,7 @@ every path below is relative to that root.
| Path | Component kinds routed here | Decided by |
| --- | --- | --- |
-| `skills` | `skill` | [source](https://opencode.ai/docs/skills/, and measured by running the pinned 1.18.25 product with `debug skill` against a temporary HOME, 2026-08-29) |
+| `skills` | `skill` | [source](https://opencode.ai/docs/skills/) -- and measured by running the pinned 1.18.25 product with `debug skill` against a temporary HOME, 2026-08-29 |
This root is read by several products at once, so under this scope
`remove`, the backup and a restore act on the files this program
@@ -202,13 +202,13 @@ other file beside a target.
**`tui.jsonc`** -- Documented, and deliberately not owned, for the same reason as opencode.jsonc: it is the second spelling of one file, and owning both would let a target hold two documents that disagree with the product reading one and this provider reporting the other. ([source](https://opencode.ai/docs/tui))
-**`NDDEV-OPENCODE-PROVIDER.json`** -- This provider's own state file: which setup is applied, the identity it recorded, and which slot reverses the last operation. Written by every operation and excluded from target identity, because counting it would leave a target different from the identity the operation just wrote. Not a projection surface and never ownable as one. ([source](this provider's own contract; no vendor page is involved))
+**`NDDEV-OPENCODE-PROVIDER.json`** -- This provider's own state file: which setup is applied, the identity it recorded, and which slot reverses the last operation. Written by every operation and excluded from target identity, because counting it would leave a target different from the identity the operation just wrote. Not a projection surface and never ownable as one. (this provider's own contract; no vendor page is involved)
-**`.opencode-setup-system`** -- This provider's own control directory: the target lock, the backup slots and their payloads. Kept out of the declaration for the same reason as the state file, and recorded here because the declined list is where a reader looks before opening a file to find out what it is. ([source](this provider's own contract; no vendor page is involved))
+**`.opencode-setup-system`** -- This provider's own control directory: the target lock, the backup slots and their payloads. Kept out of the declaration for the same reason as the state file, and recorded here because the declined list is where a reader looks before opening a file to find out what it is. (this provider's own contract; no vendor page is involved)
-**`.gitignore`** -- The product writes this into its configuration home on first run, listing `node_modules`, `package.json`, `package-lock.json`, `bun.lock` and itself -- it treats the home as a place a package manager might run. Measured 2026-08-28 by launching the product through this provider. Not owned: nothing here projects a `.gitignore`, and a file the product rewrites on its own schedule is not a surface a setup can promise to restore. ([source](measured through launch; no vendor page names it))
+**`.gitignore`** -- The product writes this into its configuration home on first run, listing `node_modules`, `package.json`, `package-lock.json`, `bun.lock` and itself -- it treats the home as a place a package manager might run. Measured 2026-08-28 by launching the product through this provider. Not owned: nothing here projects a `.gitignore`, and a file the product rewrites on its own schedule is not a surface a setup can promise to restore. (measured through launch; no vendor page names it)
-**`$HOME/.claude/skills`** -- OpenCode also reads Claude Code's skills directory for compatibility -- `.claude/skills//SKILL.md` is a path literal in the pinned binary, and the vendor lists it as *Global Claude-compatible*. Another product's home, never this provider's to own, and recorded because claude-setup-system owns `skills` there. ([source](https://opencode.ai/docs/skills/ and measured from the pinned artifact, digest verified before reading (opencode 1.18.24)))
+**`$HOME/.claude/skills`** -- OpenCode also reads Claude Code's skills directory for compatibility -- `.claude/skills//SKILL.md` is a path literal in the pinned binary, and the vendor lists it as *Global Claude-compatible*. Another product's home, never this provider's to own, and recorded because claude-setup-system owns `skills` there. ([source](https://opencode.ai/docs/skills/) -- and measured from the pinned artifact, digest verified before reading (opencode 1.18.24))
**`opencode-runtime-state`** -- One row for what the product writes **outside its configuration home entirely**, because it writes to three other roots and none of them had a row.
@@ -221,15 +221,15 @@ The product is XDG-native and this is the rest of that specification: the config
**None of them gets a row of its own**, because every recorded path here is relative to the target and these are relative to roots this provider never evaluates against; the guard that enforces it refuses such a row, correctly. Recorded in this sentence instead, the same way antigravity's `~/.cache/ms-playwright-go` is, so a reader looking for everything the product writes does not stop at `~/.config/opencode` and get a wrong answer.
-**And it settles a search result that says otherwise.** Several pages claim opencode uses `%APPDATA%` on Windows; they are about third-party plugins rather than the vendor. The product's own resolver has **no `win32` branch at all**, so `~/.config/opencode` is correct on all three systems and this baseline's `config_home` stands. ([source](measured by running the pinned 1.18.25 binary in a clean HOME, and confirmed at the line in its own Global module, 2026-08-28))
+**And it settles a search result that says otherwise.** Several pages claim opencode uses `%APPDATA%` on Windows; they are about third-party plugins rather than the vendor. The product's own resolver has **no `win32` branch at all**, so `~/.config/opencode` is correct on all three systems and this baseline's `config_home` stands. (measured by running the pinned 1.18.25 binary in a clean HOME, and confirmed at the line in its own Global module, 2026-08-28)
-**`agent`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/agent(s)/.md` and the project row as `.opencode/agent/.md` or `.opencode/agents/.md`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. ([source](measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings))
+**`agent`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/agent(s)/.md` and the project row as `.opencode/agent/.md` or `.opencode/agents/.md`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. (measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings)
-**`command`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/command(s)/.md` and the project row as `.opencode/command/.md` or `.opencode/commands/.md`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. ([source](measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings))
+**`command`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/command(s)/.md` and the project row as `.opencode/command/.md` or `.opencode/commands/.md`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. (measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings)
-**`skill`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/skill(s)//SKILL.md` and the project row as `.opencode/skill//SKILL.md` or `.opencode/skills//SKILL.md`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. ([source](measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings))
+**`skill`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/skill(s)//SKILL.md` and the project row as `.opencode/skill//SKILL.md` or `.opencode/skills//SKILL.md`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. (measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings)
-**`plugin`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/plugin(s)//` and the project row as `.opencode/plugin//` or `.opencode/plugins//`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. ([source](measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings))
+**`plugin`** -- Opencode accepts the singular and the plural spelling of this directory. Its own embedded reference, carried in the pinned 1.18.25 binary, writes the global row as `~/.config/opencode/plugin(s)//` and the project row as `.opencode/plugin//` or `.opencode/plugins//`. This provider writes and owns the plural only. Owning both would let one target hold two definitions of the same component that disagree, with the product reading one and this provider reporting the other, and which of the two wins where both exist is not documented -- so a target holding the singular is reported rather than resolved. Recorded here because without a row the next reader repeats the search, and because a directory the product reads and this provider does not own is exactly what this block is for. (measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256 -- the product carries its own reference table and it names both spellings)
**`managed-config`** -- Not a path in the target, and named without an extension for that reason: the managed configuration directory is a **system** path, one per operating system, and every recorded path here is relative to the target.
@@ -243,9 +243,9 @@ All three are in the pinned 1.18.25 bundle's own emitted source, in one switch:
Recorded and never touched: it needs root or Administrator to write, it is not under the configuration home this provider is given, and a provider that edited an organisation's policy would be doing the one thing this estate refuses everywhere else.
-The user configuration home is **not** per-OS, which is why only this row is. The same bundle resolves it as `XDG_CONFIG_HOME || ~/.config` joined with the application name, with no platform branch at all. ([source](https://opencode.ai/docs/config; measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256))
+The user configuration home is **not** per-OS, which is why only this row is. The same bundle resolves it as `XDG_CONFIG_HOME || ~/.config` joined with the application name, with no platform branch at all. ([source](https://opencode.ai/docs/config) -- measured in the pinned 1.18.25 bundle, whose bytes match this baseline's own sha256)
-**`mcp_config.json`** -- MCP servers are the `mcp` key inside `opencode.json` -- `{"mcp": {"": {"type": "local", "command": [...]}}}` -- confirmed on the vendor's MCP page 2026-08-29 and in the product's own built-in `customize-opencode` skill. That file is owned here and written and restored whole, so MCP is covered by the `setting` kind. **A key inside a file is not a projection surface.** No separate MCP file exists under the config home. ([source](https://opencode.ai/docs/mcp-servers/; and the product's own built-in customize-opencode skill, read from the pinned 1.18.25 artifact))
+**`mcp_config.json`** -- MCP servers are the `mcp` key inside `opencode.json` -- `{"mcp": {"": {"type": "local", "command": [...]}}}` -- confirmed on the vendor's MCP page 2026-08-29 and in the product's own built-in `customize-opencode` skill. That file is owned here and written and restored whole, so MCP is covered by the `setting` kind. **A key inside a file is not a projection surface.** No separate MCP file exists under the config home. ([source](https://opencode.ai/docs/mcp-servers/) -- and the product's own built-in customize-opencode skill, read from the pinned 1.18.25 artifact)
**`hooks.json`** -- **Hooks are functions a plugin module exports**, not a file. The vendor's plugin page, read 2026-08-29: *"A plugin is a JavaScript/TypeScript module that exports one or more plugin functions. Each function receives a context object and returns a hooks object."* The names -- `tool.execute.before`, `session.created`, `permission.asked` and the rest -- are keys of that returned object. There is no `hooks.json` and no `hooks` key in `opencode.json`, so a hook reaches this product through `plugins/`, which is owned and routes `plugin`. ([source](https://opencode.ai/docs/plugins/))
diff --git a/crates/harness-runtime/src/wire.rs b/crates/harness-runtime/src/wire.rs
index 34e46a2..33b26c1 100644
--- a/crates/harness-runtime/src/wire.rs
+++ b/crates/harness-runtime/src/wire.rs
@@ -940,7 +940,7 @@ fn apply(
// touches the namespaces the effect machinery below exists to mutate, so it
// parts company here rather than pretending to be one of those effects.
if Operation::SOFTWARE.contains(&operation) {
- return software::apply(harness, prefix, operation, downloaded);
+ return apply_software(harness, prefix, operation, plan_digest, downloaded);
}
if let Some(named) = prefix {
return Err(Error::refuse(
@@ -1166,6 +1166,46 @@ pub(crate) fn perform(
}))
}
+/// The program lifecycle's answer, carrying the plan echo the wire owes.
+///
+/// The echo is added here rather than inside `software::apply`, because it is a
+/// fact about the wire call and not about the prefix: this layer is where the
+/// plan artifact and its digest exist, and `software::apply` deliberately knows
+/// about neither.
+///
+/// **It was missing from both of `software::apply`'s answer shapes, and the cost
+/// was carried by every one of the seven.** The consumer requires the echo for
+/// every operation, so `harness install`, `harness update` and `harness remove`
+/// through `ai-stp` refused *after* the program was installed -- the effect
+/// landed and the operation stayed `applied_unverified` over a prefix holding a
+/// working build. Reported by the consumer's own session on 2026-08-31 after
+/// running the released `0.0.48` through `harness install`, and confirmed here
+/// by reading both sites rather than by taking the report: the configuration
+/// answer carries `plan_digest`; neither software answer did.
+///
+/// Nothing on this side could have raised it. The producer tests asked whether
+/// the provider does what its own answer says, which it did, and the contract
+/// sentence saying the program lifecycle carries *"the same journal, backup and
+/// plan-digest"* was read as being about `plan-operation` alone. That is why the
+/// test beside it asserts the **wire** shape against the contract's list rather
+/// than against this function's output.
+fn apply_software(
+ harness: &Harness,
+ prefix: Option<&Path>,
+ operation: Operation,
+ plan_digest: &str,
+ downloaded: &[std::path::PathBuf],
+) -> Result {
+ let mut answer = software::apply(harness, prefix, operation, downloaded)?;
+ if let Some(fields) = answer.as_object_mut() {
+ fields.insert(
+ "plan_digest".to_owned(),
+ serde_json::Value::String(plan_digest.to_owned()),
+ );
+ }
+ Ok(answer)
+}
+
/// What the target's own state says before this operation touches it.
///
/// The setup it names, the definition digest that setup had, and the files this
@@ -5469,6 +5509,51 @@ mod tests {
);
}
+ #[test]
+ fn every_software_apply_echoes_the_plan_digest_it_was_handed() {
+ // **Red before 2026-08-31, and no test here could have been.**
+ //
+ // The consumer requires the plan echo on *every* apply, and neither of
+ // `software::apply`'s two answer shapes carried it. So `harness
+ // install`, `harness update` and `harness remove` through `ai-stp`
+ // refused after the program had already been installed: the effect
+ // landed, and the operation stayed `applied_unverified` over a prefix
+ // holding a working build. All seven released providers, and it reached
+ // this repository as a measurement from the consumer's own session
+ // rather than from anything here.
+ //
+ // Why nothing caught it: every test around this one asks whether the
+ // provider does what its own answer *says*, and it did. The contract
+ // owns the list of what an answer must carry -- "the same journal,
+ // backup and plan-digest" -- and that sentence was read as being about
+ // `plan-operation`. So this asserts the echo against the digest the wire
+ // was handed, which is the only value that can disagree, and it does it
+ // for all three operations rather than the one that was reported.
+ for operation in ["software_install", "software_update", "software_remove"] {
+ let target = seeded(&format!("software-echo-{operation}"));
+ let file = downloaded(&target, TEST_PAYLOAD);
+ if operation != "software_install" {
+ plan_then_install(&target, "software_install", Some(&file));
+ }
+ let prefix = ready_prefix(&target);
+ let planned = software_plan(&target, operation);
+ assert_eq!(planned["state"], "planned", "plan refused: {planned}");
+ let digest = planned["plan_digest"].as_str().unwrap().to_owned();
+ let artifact = if operation == "software_remove" {
+ None
+ } else {
+ Some(file.as_path())
+ };
+ let applied = apply_planned(&target, &prefix, operation, &planned, artifact);
+ assert_eq!(applied["state"], "verified", "apply refused: {applied}");
+ assert_eq!(
+ applied["plan_digest"],
+ serde_json::Value::String(digest),
+ "{operation} answered without the plan echo the wire owes: {applied}"
+ );
+ }
+ }
+
#[test]
fn installing_software_spends_no_backup_slot() {
// Ten slots exist and they hold configuration. If a software install
diff --git a/install.ps1 b/install.ps1
index e3fbe3a..b6dd69e 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.48",
+ [string]$Version = "0.0.49",
[string]$InstallDir = "$env:LOCALAPPDATA\Programs\opencode-setup-system"
)
$ErrorActionPreference = "Stop"
diff --git a/install.sh b/install.sh
index 4086b88..4b8560d 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.48}"
+VERSION="${1:-0.0.49}"
PREFIX="${OPENCODE_INSTALL_DIR:-$HOME/.local/bin}"
case "$(uname -s)" in
diff --git a/provider-kit/v3/README.md b/provider-kit/v3/README.md
index b42afc9..e69d603 100644
--- a/provider-kit/v3/README.md
+++ b/provider-kit/v3/README.md
@@ -8,6 +8,7 @@
- `manifest.json` фиксирует команды, operations, native vocabularies, provenance и
network phases.
- `provider-info.schema.json` является закрытой JSON Schema ответа `provider-info`.
+- `status-response.schema.json` является закрытой JSON Schema ответа `status`.
- `conformance-cases.json` перечисляет обязательные fail-closed классы.
- `SHA256SUMS` привязывает точные bytes остальных артефактов.
- `KIT-IDENTITY.json` называет ровно одну ревизию комплекта: агрегатный digest
@@ -31,7 +32,25 @@
Читателю комплекта принадлежит другая проверка, и она не требует ничего чужого:
`SHA256SUMS` привязывает точные байты остальных артефактов, а `KIT-IDENTITY.json`
-называет агрегатный digest от канонических байт `SHA256SUMS`. Для этого комплект
+называет SHA-256 самого файла `SHA256SUMS` без нормализации. Для этого комплект
их и несёт.
+## Status response
+
+Всегда обязательны protocol/provider/harness identity, canonical target,
+`state`, оба target digests, `cleanup_state`, `journal`, `backups`,
+`provider_state` и `shadowed_by`.
+
+Полный flat provenance обязателен условно: только когда вложенный
+`provider_state` сообщает `present=true`, `readable=true` и
+`drift_state=clean`. Тогда ответ называет state schema, provider build/release,
+setup definition/version/passport, components, bundle/artifact, projection/plan,
+operation/precondition, native ownership, written paths, backup и previous
+verified identity. Missing, foreign-schema и local-drift состояния не заполняют
+эти поля выдуманными значениями.
+
+Эта ревизия сначала публикует schema и conformance cases. Consumer enforcement
+включается только после того, как provider systems вендорят и выпускают kit;
+одного присутствия файла недостаточно, чтобы считать старый status отвергнутым.
+
Редактировать generated JSON вручную нельзя.
diff --git a/references/opencode-baseline.json b/references/opencode-baseline.json
index 0355797..2952c88 100644
--- a/references/opencode-baseline.json
+++ b/references/opencode-baseline.json
@@ -111,7 +111,7 @@
],
"verified_at": "2026-08-29T10:56:16+00:00",
"native_surfaces": {
- "verified_at": "2026-08-27",
+ "verified_at": "2026-08-31",
"config_home": "~/.config/opencode",
"surfaces": [
{
@@ -212,7 +212,8 @@
{
"path": "$HOME/.claude/skills",
"reason": "OpenCode also reads Claude Code's skills directory for compatibility -- `.claude/skills//SKILL.md` is a path literal in the pinned binary, and the vendor lists it as *Global Claude-compatible*. Another product's home, never this provider's to own, and recorded because claude-setup-system owns `skills` there.",
- "source": "https://opencode.ai/docs/skills/ and measured from the pinned artifact, digest verified before reading (opencode 1.18.24)"
+ "source": "https://opencode.ai/docs/skills/ and measured from the pinned artifact, digest verified before reading (opencode 1.18.24)",
+ "note": "**Re-asked at 1.18.25 on 2026-08-31.** The pinned `opencode-linux-x64-1.18.25.tgz` was fetched and its sha256 checked against this baseline's own artifact table before a byte was read; `.claude/skills` is still there. Searched with three controls -- `nddev_invented_reference_line`, `~/.nddev-not-a-home/skills/` and `NDDEV_INVENTED_SURFACE` -- each zero in the same bytes, so the search discriminates."
},
{
"path": "opencode-runtime-state",