File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\registry.rs (file state is current in your context — no need to Read it back)

//! Adapter registration lifecycle (M5-D2, REQ-INSTALL-4) — the node-local
//! registered-adapter set under `{SPT_HOME}/adapters/`.
//!
//! `spt adapter add` validates the manifest **first** (nothing is recorded for
//! an invalid one), then records it (PRD R-INSTALL-4): a **copy** of the
//! manifest for `file_pull` / avenue-less adapters (spt-core owns the bytes),
//! a **pointer** back to the source tree for `delegated` ones (the adapter's
//! own updater owns its install; we re-read its manifest live so a delegated
//! self-update never leaves a stale copy behind). One command and one dir for
//! both `kind = "harness"` and `kind = "shell"`.
//!
//! Registration is **node-local capability** (may this node drive/launch the
//! adapter), not subnet advertisement — discovery rides the registry/broadcast
//! gates elsewhere. The registered set is the single source consumed by:
//! creation-time adapter selection (auto-if-one/ask-if-many over
//! `hostable_types`), shell discovery (D3d), and the R-UPD-5 self-update
//! ripple.
//!
//! `spt adapter remove` is a **soft-deregister**: the record stays with
//! `active = false`, so new creation no longer offers the adapter while
//! existing/live instances keep running off it. The manifest's optional
//! `[update] uninstall` template is *returned* to the caller to conduct once
//! the adapter is quiesced — this module records state, it never execs.
//! Re-adding a deregistered name re-activates it (refreshing the record).
//!
//! Layout per adapter: `adapters/<name>/record.toml` (+ `manifest.toml` in
//! copy mode). Records write atomically (REQ-HAZARD-EBUSY-RENAME stance via
//! `spt_store::atomic`).
// [impl->REQ-INSTALL-4]

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

use spt_store::atomic::atomic_write_string;

use crate::manifest::{AdapterKind, Manifest, UpdateAvenue};

/// How the manifest is held: a copy under `adapters/<name>/`, or a pointer
/// re-read from the source tree per load.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RecordMode {
    Copy,
    Pointer,
}

/// One registered adapter's durable record (`adapters/<name>/record.toml`).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AdapterRecord {
    pub name: String,
    pub kind: AdapterKind,
    pub mode: RecordMode,
    /// Where the adapter was registered from (the manifest's directory) —
    /// the live manifest location in pointer mode, provenance in copy mode.
    pub source_dir: String,
    pub registered_at_ms: u64,
    /// `false` = soft-deregistered: hidden from new-creation/discovery;
    /// existing instances keep running (REQ-INSTALL-4).
    pub active: bool,
}

/// Registration failures — adapter-author-facing, so they carry the why.
#[derive(Debug)]
pub enum RegistryError {
    /// The source manifest failed to read.
    Io(std::io::Error),
    /// The source manifest failed parse/validation — nothing was recorded.
    Manifest(crate::manifest::ManifestError),
    /// `remove`/lookup named an adapter that has no record.
    NotRegistered(String),
    /// A record on disk is unreadable/corrupt (named so the operator can fix).
    BadRecord(String),
}

impl std::fmt::Display for RegistryError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RegistryError::Io(e) => write!(f, "io: {e}"),
            RegistryError::Manifest(e) => write!(f, "manifest invalid: {e}"),
            RegistryError::NotRegistered(n) => write!(f, "adapter not registered: {n}"),
            RegistryError::BadRecord(n) => write!(f, "corrupt adapter record: {n}"),
        }
    }
}

impl From<std::io::Error> for RegistryError {
    fn from(e: std::io::Error) -> Self {
        RegistryError::Io(e)
    }
}

fn record_file(adapters_dir: &Path, name: &str) -> PathBuf {
    adapters_dir.join(name).join("record.toml")
}

fn manifest_copy_file(adapters_dir: &Path, name: &str) -> PathBuf {
    adapters_dir.join(name).join("manifest.toml")
}

/// Resolve the manifest file inside a source: `<dir>/manifest.toml`, or the
/// path itself when it already names a file.
fn source_manifest_file(source: &Path) -> PathBuf {
    if source.is_dir() {
        source.join("manifest.toml")
    } else {
        source.to_path_buf()
    }
}

/// Register (or re-register) the adapter whose manifest lives at `source`
/// (a directory containing `manifest.toml`, or the manifest path itself).
/// Validate-first: an invalid manifest records **nothing**. Mode falls out of
/// the `[update]` avenue — `delegated` ⇒ pointer, `file_pull`/absent ⇒ copy.
/// Re-adding an existing name refreshes the record and re-activates a
/// soft-deregistered one. Returns the record; the caller conducts
/// install-is-first-update through the manifest's `[update]` avenue.
// [impl->REQ-INSTALL-4]
pub fn register(
    adapters_dir: &Path,
    source: &Path,
    now_ms: u64,
) -> Result<(AdapterRecord, Manifest), RegistryError> {
    let manifest_file = source_manifest_file(source);
    let toml_src = std::fs::read_to_string(&manifest_file)?;
    let manifest = Manifest::from_toml_str(&toml_src).map_err(RegistryError::Manifest)?;

    let mode = match manifest.update.as_ref().map(|u| u.avenue) {
        Some(UpdateAvenue::Delegated) => RecordMode::Pointer,
        Some(UpdateAvenue::FilePull) | None => RecordMode::Copy,
    };
    let source_dir = manifest_file
        .parent()
        .unwrap_or(Path::new("."))
        .to_string_lossy()
        .into_owned();

    let record = AdapterRecord {
        name: manifest.adapter.name.clone(),
        kind: manifest.adapter.kind,
        mode,
        source_dir,
        registered_at_ms: now_ms,
        active: true,
    };

    let dir = adapters_dir.join(&record.name);
    std::fs::create_dir_all(&dir)?;
    if mode == RecordMode::Copy {
        atomic_write_string(&manifest_copy_file(adapters_dir, &record.name), &toml_src)?;
    }
    save_record(adapters_dir, &record)?;
    Ok((record, manifest))
}

/// Soft-deregister `name`: flip `active = false` (the record + any manifest
/// copy stay — existing instances keep running off them). Returns the
/// manifest's optional `[update] uninstall` template for the caller to
/// conduct once the adapter is quiesced (`spt adapter remove`'s optional
/// second half); `None` = spt-core default cleanup. Idempotent on an
/// already-inactive record.
// [impl->REQ-INSTALL-4]
pub fn deregister(adapters_dir: &Path, name: &str) -> Result<Option<String>, RegistryError> {
    let mut record = load_record(adapters_dir, name)?;
    record.active = false;
    save_record(adapters_dir, &record)?;
    let uninstall = load_manifest(adapters_dir, &record)
        .ok()
        .and_then(|m| m.update.and_then(|u| u.uninstall));
    Ok(uninstall)
}

/// One adapter's record by name.
pub fn load_record(adapters_dir: &Path, name: &str) -> Result<AdapterRecord, RegistryError> {
    let path = record_file(adapters_dir, name);
    if !path.exists() {
        return Err(RegistryError::NotRegistered(name.to_string()));
    }
    let s = std::fs::read_to_string(&path)?;
    toml::from_str(&s).map_err(|_| RegistryError::BadRecord(name.to_string()))
}

fn save_record(adapters_dir: &Path, record: &AdapterRecord) -> Result<(), RegistryError> {
    let s = toml::to_string_pretty(record)
        .map_err(|e| RegistryError::BadRecord(format!("{}: {e}", record.name)))?;
    atomic_write_string(&record_file(adapters_dir, &record.name), &s)?;
    Ok(())
}

/// The record's manifest: the held copy in copy mode, the live source tree in
/// pointer mode (a delegated adapter's self-updates are always honored).
// [impl->REQ-INSTALL-4]
pub fn load_manifest(
    adapters_dir: &Path,
    record: &AdapterRecord,
) -> Result<Manifest, RegistryError> {
    let path = match record.mode {
        RecordMode::Copy => manifest_copy_file(adapters_dir, &record.name),
        RecordMode::Pointer => source_manifest_file(Path::new(&record.source_dir)),
    };
    let s = std::fs::read_to_string(&path)?;
    Manifest::from_toml_str(&s).map_err(RegistryError::Manifest)
}

/// Every record under `adapters/` (active and not — callers filter), sorted
/// by name for a stable order. Tolerant: an unreadable entry is skipped, the
/// rest still list (the stale-registry degrade stance).
pub fn all_records(adapters_dir: &Path) -> Vec<AdapterRecord> {
    let Ok(entries) = std::fs::read_dir(adapters_dir) else {
        return Vec::new();
    };
    let mut out: Vec<AdapterRecord> = entries
        .filter_map(|e| e.ok())
        .filter_map(|e| load_record(adapters_dir, &e.file_name().to_string_lossy()).ok())
        .collect();
    out.sort_by(|a, b| a.name.cmp(&b.name));
    out
}

/// The **active** registered set with manifests resolved — the single source
/// for creation-time adapter selection, shell discovery, and the R-UPD-5
/// ripple. A pointer-mode record whose source tree has vanished is skipped
/// (tolerant), not fatal.
// [impl->REQ-INSTALL-4]
pub fn registered(adapters_dir: &Path) -> Vec<(AdapterRecord, Manifest)> {
    all_records(adapters_dir)
        .into_iter()
        .filter(|r| r.active)
        .filter_map(|r| load_manifest(adapters_dir, &r).ok().map(|m| (r, m)))
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    const HARNESS_COPY: &str = r#"
[adapter]
name = "mock-harness"
kind = "harness"
version = "1.0.0"
min_spt_core_version = "0.0.1"
hostable_types = ["LiveAgent"]
"#;

    const SHELL_DELEGATED: &str = r#"
[adapter]
name = "mock-shell"
kind = "shell"
version = "1.0.0"
min_spt_core_version = "0.0.1"

[update]
avenue = "delegated"
command = "mock-shell-updater"
self_verifies = true
uninstall = "mock-shell-uninstaller --purge"

[shell]
spawn = "mock-shell --link {link_token}"
"#;

    fn seed_source(dir: &Path, name: &str, toml_src: &str) -> PathBuf {
        let src = dir.join(name);
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("manifest.toml"), toml_src).unwrap();
        src
    }

    // [unit->REQ-INSTALL-4] register validates-first and records by avenue:
    // an avenue-less harness is copied; a delegated shell is a pointer whose
    // manifest reads live from the source tree. One dir serves both kinds.
    #[test]
    fn register_copy_vs_pointer_by_avenue() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = tmp.path().join("adapters");
        let harness_src = seed_source(tmp.path(), "h-src", HARNESS_COPY);
        let shell_src = seed_source(tmp.path(), "s-src", SHELL_DELEGATED);

        let (h, hm) = register(&adapters, &harness_src, 1000).unwrap();
        assert_eq!((h.mode, h.kind), (RecordMode::Copy, AdapterKind::Harness));
        assert_eq!(hm.adapter.name, "mock-harness");
        assert!(adapters.join("mock-harness").join("manifest.toml").exists(), "copy held");

        let (s, _) = register(&adapters, &shell_src, 2000).unwrap();
        assert_eq!((s.mode, s.kind), (RecordMode::Pointer, AdapterKind::Shell));
        assert!(!adapters.join("mock-shell").join("manifest.toml").exists(), "no copy");
        // Pointer reads live: mutate the source, the loaded manifest follows.
        std::fs::write(
            shell_src.join("manifest.toml"),
            SHELL_DELEGATED.replace("1.0.0", "1.1.0"),
        )
        .unwrap();
        let live = load_manifest(&adapters, &s).unwrap();
        assert_eq!(live.adapter.version, "1.1.0");

        let set = registered(&adapters);
        assert_eq!(
            set.iter().map(|(r, _)| r.name.as_str()).collect::<Vec<_>>(),
            vec!["mock-harness", "mock-shell"],
            "stable name order"
        );
    }

    // [unit->REQ-INSTALL-4] an invalid manifest records nothing — the
    // validate-first rule (manifest-first, then install).
    #[test]
    fn invalid_manifest_records_nothing() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = tmp.path().join("adapters");
        // kind = "shell" without a [shell] body fails validation.
        let bad = seed_source(
            tmp.path(),
            "bad-src",
            "[adapter]\nname = \"bad\"\nkind = \"shell\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",
        );
        assert!(matches!(register(&adapters, &bad, 0), Err(RegistryError::Manifest(_))));
        assert!(!adapters.join("bad").exists(), "nothing recorded");
        assert!(registered(&adapters).is_empty());
    }

    // [unit->REQ-INSTALL-4] remove is a soft-deregister: hidden from the
    // active set, record + copy retained (live instances keep running), the
    // manifest's uninstall template surfaced to the caller; re-add
    // re-activates.
    #[test]
    fn deregister_is_soft_and_readd_reactivates() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = tmp.path().join("adapters");
        let shell_src = seed_source(tmp.path(), "s-src", SHELL_DELEGATED);
        register(&adapters, &shell_src, 1000).unwrap();

        let uninstall = deregister(&adapters, "mock-shell").unwrap();
        assert_eq!(uninstall.as_deref(), Some("mock-shell-uninstaller --purge"));
        assert!(registered(&adapters).is_empty(), "hidden from the active set");
        let rec = load_record(&adapters, "mock-shell").unwrap();
        assert!(!rec.active, "record retained, inactive");

        // Unknown name errs; double-deregister is idempotent.
        assert!(matches!(
            deregister(&adapters, "ghost"),
            Err(RegistryError::NotRegistered(_))
        ));
        assert!(deregister(&adapters, "mock-shell").is_ok());

        let (re, _) = register(&adapters, &shell_src, 2000).unwrap();
        assert!(re.active && re.registered_at_ms == 2000, "re-add re-activates + refreshes");
        assert_eq!(registered(&adapters).len(), 1);
    }
}


PowerShell

Add-Content crates\spt-runtime\src\lib.rs "pub mod registry;"; cargo test -p spt-runtime registry 2>&1 | Select-Object -Last 8

Wire registry module + run its tests


running 3 tests
test registry::tests::invalid_manifest_records_nothing ... ok
test registry::tests::register_copy_vs_pointer_by_avenue ... ok
test registry::tests::deregister_is_soft_and_readd_reactivates ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 21 filtered out; finished in 0.03s


Get-Content crates\spt-runtime\src\lib.rs -Tail 5

Check lib.rs tail after append


pub use manifest::{Manifest, ManifestError, PtyDigest};
pub use runtime::{
    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
};
pub mod registry;


No matches found


1	//! spt-core harness contract — outbound half (M2a).
2	//!
3	//! `spt-runtime` makes spt-core **drivable by a manifest-described harness** with
4	//! zero Claude Code (or any harness) conventions in-repo. It is the fourth layer
5	//! in the acyclic stack (`spt-proto -> spt-store -> spt-msg -> spt-runtime`).
6	//!
7	//! Two surfaces land here across M2a:
8	//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and
9	//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template
10	//!   substitution + spawn-session seam, T2).
11	//!
12	//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
13	//! over this crate. See `M2a-PLAN.md`.
14	//
15	// spt-runtime joins spt-proto + spt-msg as the third member of the public,
16	// semver-committed SDK surface (R-ARCH-2): a harness author programs against
17	// this crate's Manifest/AgentRuntime types.
18	// [impl->REQ-ARCH-2]
19
20	pub mod manifest;
21	pub mod runtime;
22
23	pub use manifest::{Manifest, ManifestError, PtyDigest};
24	pub use runtime::{
25	    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
26	};
27	pub mod registry;
28

//! spt-core harness contract — outbound half (M2a).
//!
//! `spt-runtime` makes spt-core **drivable by a manifest-described harness** with
//! zero Claude Code (or any harness) conventions in-repo. It is the fourth layer
//! in the acyclic stack (`spt-proto -> spt-store -> spt-msg -> spt-runtime`).
//!
//! Two surfaces land here across M2a:
//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and
//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template
//!   substitution + spawn-session seam, T2).
//!
//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.
//
// spt-runtime joins spt-proto + spt-msg as the third member of the public,
// semver-committed SDK surface (R-ARCH-2): a harness author programs against
// this crate's Manifest/AgentRuntime types.
// [impl->REQ-ARCH-2]

pub mod manifest;
pub mod runtime;

pub use manifest::{Manifest, ManifestError, PtyDigest};
pub use runtime::{
    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
};
pub mod registry;


354-///
355-/// This is the primitive the **history subsystem** (spt-live, M2b) runs its
356-\ `fetcher` \ `normalize_command` strings through: those are adapter command
357-\ lines that live *outside* the role set, so [`ManifestRuntime::run_bounded`]
358-/// (which looks a role up in the manifest) does not apply. `stdin` feeds a
359-/// `normalize_command` the raw transcript on its standard input; it is written
360-/// on a thread so a child that reads slowly never deadlocks the poll.
361-// [impl->REQ-HAZARD-SUBPROCESS-TIMEOUT]
362:pub fn run_bounded_command(
363-    template: &str,
364-    keys: &BTreeMap<String, String>,
365-    timeout: Duration,
366-    stdin: Option<&[u8]>,
367-) -> Result<BoundedOutput, RuntimeError> {
368-    let filled = fill_template(template, keys)?;


Edit

pub mod manifest;
pub mod runtime;

pub use manifest::{Manifest, ManifestError, PtyDigest};
pub use runtime::{
    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
};
pub mod registry;

pub mod manifest;
pub mod registry;
pub mod runtime;

pub use manifest::{Manifest, ManifestError, PtyDigest};
pub use runtime::{
    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
};


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime\src\lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

pub mod manifest;
pub mod registry;
pub mod runtime;

pub use manifest::{Manifest, ManifestError, PtyDigest};
pub use runtime::{
    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
};

//! spt-core harness contract — outbound half (M2a).
//!
//! `spt-runtime` makes spt-core **drivable by a manifest-described harness** with
//! zero Claude Code (or any harness) conventions in-repo. It is the fourth layer
//! in the acyclic stack (`spt-proto -> spt-store -> spt-msg -> spt-runtime`).
//!
//! Two surfaces land here across M2a:
//! - the **runtime-manifest schema** + parse/validate ([`manifest`], T1), and
//! - the [`AgentRuntime`] trait + default `ManifestRuntime` (opaque-template
//!   substitution + spawn-session seam, T2).
//!
//! The inbound half (the `spt api` subcommand surface) lives in the `spt` binary
//! over this crate. See `M2a-PLAN.md`.
//
// spt-runtime joins spt-proto + spt-msg as the third member of the public,
// semver-committed SDK surface (R-ARCH-2): a harness author programs against
// this crate's Manifest/AgentRuntime types.
// [impl->REQ-ARCH-2]

pub mod manifest;
pub mod runtime;

pub use manifest::{Manifest, ManifestError, PtyDigest};
pub use runtime::{
    run_bounded_command, AgentRuntime, BoundedOutput, ManifestRuntime, RuntimeError, Spawned,
};
pub mod registry;


Add `adapters_dir()` to perch.rs, then CLI:


Grep

pub fn identity_dir|fn spt_home|pub fn owlery_dir


30-/// spt-plugin during the parity build-out. Once spt-core + the spt-plugin reach
31-/// full parity with modern spt this may simply stay `spt-core/` for good; the
32-/// override (`$SPT_HOME`) is the relocation knob either way.
33-// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
34:pub fn spt_home() -> PathBuf {
--
58-}
59-
60-\ The owlery directory holding all perches: `<spt_home>/owlery/`.
61-// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
62:pub fn owlery_dir() -> PathBuf {
--
76-/// (`docs/STORAGE.md`). Holds `node.key`, the [`trust_dir`] store, and
77-/// [`subnet_file`]. **Never git-tracked, never synced** — node-bound secret
78-/// material (the node keypair + subnet TOTP seeds). Pure path composition.
79-// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
80:pub fn identity_dir() -> PathBuf {
--
479-
480-    \ [unit->REQ-HAZARD-SINGLE-PATH-SOURCE] spt_home is pure: honors SPT_HOME,
481-    // creates nothing.
482-    #[test]
483:    fn spt_home_is_pure_and_honors_env() {


Edit

/// The consent grant store: `<spt_home>/identity/trust/grants.json`
/// (CONTEXT §Consent & security gates, REQ-CONSENT-1). Security material next
/// to the trust store — node-local, never git-tracked/synced. Default-deny
/// (the access whitelist's opposite polarity).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn grants_file() -> PathBuf {
    trust_dir().join("grants.json")
}

/// The consent grant store: `<spt_home>/identity/trust/grants.json`
/// (CONTEXT §Consent & security gates, REQ-CONSENT-1). Security material next
/// to the trust store — node-local, never git-tracked/synced. Default-deny
/// (the access whitelist's opposite polarity).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn grants_file() -> PathBuf {
    trust_dir().join("grants.json")
}

/// The registered-adapter set: `<spt_home>/adapters/` (`docs/STORAGE.md`,
/// REQ-INSTALL-4) — one subdir per adapter (`record.toml` + a manifest copy
/// or pointer). Node-local capability (drive/launch), not subnet
/// advertisement.
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn adapters_dir() -> PathBuf {
    spt_home().join("adapters")
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\perch.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// The consent grant store: `<spt_home>/identity/trust/grants.json`
/// (CONTEXT §Consent & security gates, REQ-CONSENT-1). Security material next
/// to the trust store — node-local, never git-tracked/synced. Default-deny
/// (the access whitelist's opposite polarity).
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn grants_file() -> PathBuf {
    trust_dir().join("grants.json")
}

/// The registered-adapter set: `<spt_home>/adapters/` (`docs/STORAGE.md`,
/// REQ-INSTALL-4) — one subdir per adapter (`record.toml` + a manifest copy
/// or pointer). Node-local capability (drive/launch), not subnet
/// advertisement.
// [impl->REQ-HAZARD-SINGLE-PATH-SOURCE]
pub fn adapters_dir() -> PathBuf {
    spt_home().join("adapters")
}

+/// The registered-adapter set: `<spt_home>/adapters/` (`docs/STORAGE.md`,

+/// REQ-INSTALL-4) — one subdir per adapter (`record.toml` + a manifest copy

+/// or pointer). Node-local capability (drive/launch), not subnet

/// Classification of a perch id by its naming convention. Stable, pure, no I/O.

/// - `<x>-psyche` → [`PerchKind::Psyche`]


125-
126-impl InfoJson {
127-    /// Construct a numeric-pid record.
128:    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {
129-        InfoJson {
130-            id: id.to_string(),
131-            started: started.to_string(),
132-            pid: PidValue::Numeric(pid),
133-            session_id: session_id.to_string(),
134-            state: state.to_string(),
135-            parent_pid: None,
136-            cwd: None,


    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
    /// capabilities on this node. Default-deny (the access whitelist's
    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
    /// escalates interactively; `add` is the durable allow-always answer.
    Grant {
        #[command(subcommand)]
        action: GrantCmd,
    },

    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
    /// capabilities on this node. Default-deny (the access whitelist's
    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
    /// escalates interactively; `add` is the durable allow-always answer.
    Grant {
        #[command(subcommand)]
        action: GrantCmd,
    },
    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one
    /// command for harness and shell adapters). The registered set feeds
    /// creation-time adapter selection, shell discovery, and the self-update
    /// ripple.
    Adapter {
        #[command(subcommand)]
        action: AdapterCmd,
    },


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// Consent grant store (REQ-CONSENT-1): which agents hold which gated
    /// capabilities on this node. Default-deny (the access whitelist's
    /// opposite polarity) — the innermost ADR-0009 gate. An ungranted ask
    /// escalates interactively; `add` is the durable allow-always answer.
    Grant {
        #[command(subcommand)]
        action: GrantCmd,
    },
    /// Adapter registration (REQ-INSTALL-4): the node-local registered set
    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one
    /// command for harness and shell adapters). The registered set feeds
    /// creation-time adapter selection, shell discovery, and the self-update
    /// ripple.
    Adapter {
        #[command(subcommand)]
        action: AdapterCmd,
    },

+    /// Adapter registration (REQ-INSTALL-4): the node-local registered set

+    /// under `{SPT_HOME}/adapters/` — what this node can drive/launch (one

+    /// command for harness and shell adapters). The registered set feeds

+    /// creation-time adapter selection, shell discovery, and the self-update

/// Issue a subnet-wide user notification (ADR-0007, REQ-NOTIF-2): produced

/// into the replicated notif spool and first-fired at the user's

/// most-recently-active endpoint in that subnet. Body from the trailing


/// `spt grant <action>` subcommands (REQ-CONSENT-1).

/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
#[derive(Subcommand)]
enum AdapterCmd {
    /// Register an adapter from a local path (a dir holding `manifest.toml`,
    /// or the manifest file itself) or from GitHub (`--github user/repo`,
    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
    /// registers nothing. Install is the first update — the declared
    /// `[update]` avenue is conducted once after recording.
    Add {
        /// Local manifest source (omit when using --github).
        path: Option<String>,
        /// GitHub source `user/repo` — manifest-first, then install via the
        /// declared `[update]` avenue.
        #[arg(long)]
        github: Option<String>,
    },
    /// Soft-deregister: hidden from new-creation/discovery; existing and live
    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until adapter-linked quiesce detection
    /// lands (D3).
    Remove {
        name: String,
        /// Conduct the manifest `uninstall` template now, without waiting for
        /// quiesce.
        #[arg(long)]
        force: bool,
    },
    /// List registered adapters (active and soft-deregistered).
    List,
}

/// `spt grant <action>` subcommands (REQ-CONSENT-1).


/// `spt adapter <action>` subcommands (REQ-INSTALL-4).
#[derive(Subcommand)]
enum AdapterCmd {
    /// Register an adapter from a local path (a dir holding `manifest.toml`,
    /// or the manifest file itself) or from GitHub (`--github user/repo`,
    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest
    /// registers nothing. Install is the first update — the declared
    /// `[update]` avenue is conducted once after recording.
    Add {
        /// Local manifest source (omit when using --github).
        path: Option<String>,
        /// GitHub source `user/repo` — manifest-first, then install via the
        /// declared `[update]` avenue.
        #[arg(long)]
        github: Option<String>,
    },
    /// Soft-deregister: hidden from new-creation/discovery; existing and live
    /// instances keep running. The manifest's optional `uninstall` template
    /// is conducted only with --force until adapter-linked quiesce detection
    /// lands (D3).
    Remove {
        name: String,
        /// Conduct the manifest `uninstall` template now, without waiting for
        /// quiesce.
        #[arg(long)]
        force: bool,
    },
    /// List registered adapters (active and soft-deregistered).
    List,
}

/// `spt grant <action>` subcommands (REQ-CONSENT-1).

+/// `spt adapter <action>` subcommands (REQ-INSTALL-4).

+    /// Register an adapter from a local path (a dir holding `manifest.toml`,

+    /// or the manifest file itself) or from GitHub (`--github user/repo`,

+    /// cloned under `adapters/_github/`). Manifest-first: an invalid manifest

+    /// registers nothing. Install is the first update — the declared

+    /// `[update]` avenue is conducted once after recording.

+        /// Local manifest source (omit when using --github).

+        /// GitHub source `user/repo` — manifest-first, then install via the

+        /// declared `[update]` avenue.

+    /// Soft-deregister: hidden from new-creation/discovery; existing and live

+    /// instances keep running. The manifest's optional `uninstall` template

+    /// is conducted only with --force until adapter-linked quiesce detection

+        /// Conduct the manifest `uninstall` template now, without waiting for

+    /// List registered adapters (active and soft-deregistered).


Cmd::Grant { action } => cmd_grant(action),

        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),


        Cmd::Grant { action } => cmd_grant(action),
        Cmd::Adapter { action } => cmd_adapter(action),

Cmd::Access { action } => cmd_access(action),

+        Cmd::Adapter { action } => cmd_adapter(action),

Cmd::Notify { body, subnet, from } => cmd_notify(body, subnet, from),

NotifCmd::List { subnet } => cmd_notif_list(subnet),


/// `spt grant add|revoke|list` — operate the consent grant store

/// `spt adapter add|remove|list` — the adapter registration lifecycle
/// (REQ-INSTALL-4) over `spt_runtime::registry`. `add` is manifest-first
/// (validate → record → conduct install-is-first-update through the declared
/// `[update]` avenue); `remove` is a soft-deregister.
// [impl->REQ-INSTALL-4]
fn cmd_adapter(action: AdapterCmd) -> i32 {
    use spt_runtime::registry;
    use std::time::Duration;

    let adapters = spt_store::perch::adapters_dir();
    let now_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0);

    /// Conduct one bounded adapter template (first-update / uninstall) —
    /// every subprocess gets a timeout (REQ-HAZARD-SUBPROCESS-TIMEOUT).
    fn conduct(label: &str, template: &str) -> i32 {
        match spt_runtime::run_bounded_command(
            template,
            &std::collections::BTreeMap::new(),
            Duration::from_secs(300),
            None,
        ) {
            Ok(out) if out.status.success() => {
                eprintln!("ADAPTER_{label}_OK");
                0
            }
            Ok(out) => {
                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status.code());
                1
            }
            Err(e) => {
                eprintln!("ADAPTER_{label}_FAIL: {e}");
                1
            }
        }
    }

    match action {
        AdapterCmd::Add { path, github } => {
            let source = match (path, github) {
                (Some(p), None) => std::path::PathBuf::from(p),
                (None, Some(spec)) => {
                    // Manifest-first from GitHub: shallow-clone under
                    // adapters/_github/ (a durable home — pointer-mode records
                    // may read their manifest from here for life).
                    let safe = spec.replace(['/', '\\'], "-");
                    let dest = adapters.join("_github").join(safe);
                    if dest.exists() {
                        let _ = std::fs::remove_dir_all(&dest);
                    }
                    if let Some(parent) = dest.parent() {
                        let _ = std::fs::create_dir_all(parent);
                    }
                    let url = format!("https://github.com/{spec}.git");
                    let cloned = std::process::Command::new("git")
                        .args(["clone", "--depth", "1", &url])
                        .arg(&dest)
                        .status();
                    match cloned {
                        Ok(s) if s.success() => dest,
                        Ok(s) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", s.code());
                            return 1;
                        }
                        Err(e) => {
                            eprintln!("ADAPTER_CLONE_FAIL: {e}");
                            return 1;
                        }
                    }
                }
                _ => {
                    eprintln!("ADAPTER_BAD_ARGS: exactly one of <path> or --github");
                    return 2;
                }
            };

            let (record, manifest) = match registry::register(&adapters, &source, now_ms) {
                Ok(r) => r,
                Err(e) => {
                    eprintln!("ADAPTER_ADD_FAIL: {e}");
                    return 1;
                }
            };
            eprintln!(
                "ADAPTER_ADD:{}:{:?}:{:?} (registered)",
                record.name, record.kind, record.mode
            );

            // Install is the first update (REQ-INSTALL-4) — conduct the
            // declared avenue once, through the same verdict logic the
            // self-update ripple uses (REQ-UPD-5).
            match manifest.update.as_ref() {
                None => {
                    eprintln!("ADAPTER_INSTALL_SKIP: no [update] avenue (manifest-only adapter)");
                    0
                }
                Some(u) => match spt_daemon::adapter_update::plan_adapter_update(u, None) {
                    spt_daemon::adapter_update::AdapterUpdateOutcome::Delegate(cmd) => {
                        conduct("INSTALL", &cmd)
                    }
                    spt_daemon::adapter_update::AdapterUpdateOutcome::Skipped(reason) => {
                        // file_pull with no payload yet: the pull rides the
                        // update engine (peer/self-fetch) — registration holds.
                        eprintln!("ADAPTER_INSTALL_DEFERRED: {reason:?} (rides the update engine)");
                        0
                    }
                    verdict => {
                        eprintln!("ADAPTER_INSTALL_DEFERRED: {verdict:?}");
                        0
                    }
                },
            }
        }
        AdapterCmd::Remove { name, force } => {
            match registry::deregister(&adapters, &name) {
                Ok(uninstall) => {
                    eprintln!("ADAPTER_REMOVE:{name} (soft-deregistered; live instances unaffected)");
                    match uninstall {
                        Some(tpl) if force => conduct("UNINSTALL", &tpl),
                        Some(_) => {
                            eprintln!(
                                "ADAPTER_UNINSTALL_DEFERRED: manifest declares an uninstall \
                                 template; run `spt adapter remove {name} --force` to conduct it"
                            );
                            0
                        }
                        None => 0,
                    }
                }
                Err(e) => {
                    eprintln!("ADAPTER_REMOVE_FAIL: {e}");
                    1
                }
            }
        }
        AdapterCmd::List => {
            let records = registry::all_records(&adapters);
            if records.is_empty() {
                println!("no adapters registered");
            }
            for r in records {
                let state = if r.active { "active" } else { "deregistered" };
                println!("{}: {:?} {:?} {state} (from {})", r.name, r.kind, r.mode, r.source_dir);
            }
            0
        }
    }
}

/// `spt grant add|revoke|list` — operate the consent grant store


/// `spt adapter add|remove|list` — the adapter registration lifecycle
/// (REQ-INSTALL-4) over `spt_runtime::registry`. `add` is manifest-first
/// (validate → record → conduct install-is-first-update through the declared
/// `[update]` avenue); `remove` is a soft-deregister.
// [impl->REQ-INSTALL-4]
fn cmd_adapter(action: AdapterCmd) -> i32 {
    use spt_runtime::registry;
    use std::time::Duration;

    let adapters = spt_store::perch::adapters_dir();
    let now_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0);

    /// Conduct one bounded adapter template (first-update / uninstall) —
    /// every subprocess gets a timeout (REQ-HAZARD-SUBPROCESS-TIMEOUT).
    fn conduct(label: &str, template: &str) -> i32 {
        match spt_runtime::run_bounded_command(
            template,
            &std::collections::BTreeMap::new(),
            Duration::from_secs(300),
            None,
        ) {
            Ok(out) if out.status.success() => {
                eprintln!("ADAPTER_{label}_OK");
                0
            }
            Ok(out) => {
                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status.code());
                1
            }
            Err(e) => {
                eprintln!("ADAPTER_{label}_FAIL: {e}");
                1
            }
        }
    }

    match action {
        AdapterCmd::Add { path, github } => {
            let source = match (path, github) {
                (Some(p), None) => std::path::PathBuf::from(p),
                (None, Some(spec)) => {
                    // Manifest-first from GitHub: shallow-clone under
                    // adapters/_github/ (a durable home — pointer-mode records
                    // may read their manifest from here for life).
                    let safe = spec.replace(['/', '\\'], "-");
                    let dest = adapters.join("_github").join(safe);
                    if dest.exists() {
                        let _ = std::fs::remove_dir_all(&dest);
                    }
                    if let Some(parent) = dest.parent() {
                        let _ = std::fs::create_dir_all(parent);
                    }
                    let url = format!("https://github.com/{spec}.git");
                    let cloned = std::process::Command::new("git")
                        .args(["clone", "--depth", "1", &url])
                        .arg(&dest)
                        .status();
                    match cloned {
                        Ok(s) if s.success() => dest,
                        Ok(s) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", s.code());
                            return 1;
                        }
                        Err(e) => {
                            eprintln!("ADAPTER_CLONE_FAIL: {e}");
                            return 1;
                        }
                    }
                }
                _ => {
                    eprintln!("ADAPTER_BAD_ARGS: exactly one of <path> or --github");
                    return 2;
                }
            };

            let (record, manifest) = match registry::register(&adapters, &source, now_ms) {
                Ok(r) => r,
                Err(e) => {
                    eprintln!("ADAPTER_ADD_FAIL: {e}");
                    return 1;
                }
            };
            eprintln!(
                "ADAPTER_ADD:{}:{:?}:{:?} (registered)",
                record.name, record.kind, record.mode
            );

            // Install is the first update (REQ-INSTALL-4) — conduct the
            // declared avenue once, through the same verdict logic the
            // self-update ripple uses (REQ-UPD-5).
            match manifest.update.as_ref() {
                None => {
                    eprintln!("ADAPTER_INSTALL_SKIP: no [update] avenue (manifest-only adapter)");
                    0
                }
                Some(u) => match spt_daemon::adapter_update::plan_adapter_update(u, None) {
                    spt_daemon::adapter_update::AdapterUpdateOutcome::Delegate(cmd) => {
                        conduct("INSTALL", &cmd)
                    }
                    spt_daemon::adapter_update::AdapterUpdateOutcome::Skipped(reason) => {
                        // file_pull with no payload yet: the pull rides the
                        // update engine (peer/self-fetch) — registration holds.
                        eprintln!("ADAPTER_INSTALL_DEFERRED: {reason:?} (rides the update engine)");
                        0
                    }
                    verdict => {
                        eprintln!("ADAPTER_INSTALL_DEFERRED: {verdict:?}");
                        0
                    }
                },
            }
        }
        AdapterCmd::Remove { name, force } => {
            match registry::deregister(&adapters, &name) {
                Ok(uninstall) => {
                    eprintln!("ADAPTER_REMOVE:{name} (soft-deregistered; live instances unaffected)");
                    match uninstall {
                        Some(tpl) if force => conduct("UNINSTALL", &tpl),
                        Some(_) => {
                            eprintln!(
                                "ADAPTER_UNINSTALL_DEFERRED: manifest declares an uninstall \
                                 template; run `spt adapter remove {name} --force` to conduct it"
                            );
                            0
                        }
                        None => 0,
                    }
                }
                Err(e) => {
                    eprintln!("ADAPTER_REMOVE_FAIL: {e}");
                    1
                }
            }
        }
        AdapterCmd::List => {
            let records = registry::all_records(&adapters);
            if records.is_empty() {
                println!("no adapters registered");
            }
            for r in records {
                let state = if r.active { "active" } else { "deregistered" };
                println!("{}: {:?} {:?} {state} (from {})", r.name, r.kind, r.mode, r.source_dir);
            }
            0
        }
    }
}

/// `spt grant add|revoke|list` — operate the consent grant store

+/// `spt adapter add|remove|list` — the adapter registration lifecycle

+/// (REQ-INSTALL-4) over `spt_runtime::registry`. `add` is manifest-first

+/// (validate → record → conduct install-is-first-update through the declared

+/// `[update]` avenue); `remove` is a soft-deregister.

+fn cmd_adapter(action: AdapterCmd) -> i32 {

+    let adapters = spt_store::perch::adapters_dir();

+    let now_ms = std::time::SystemTime::now()

+        .duration_since(std::time::UNIX_EPOCH)

+    /// Conduct one bounded adapter template (first-update / uninstall) —

+    /// every subprocess gets a timeout (REQ-HAZARD-SUBPROCESS-TIMEOUT).

+    fn conduct(label: &str, template: &str) -> i32 {

+        match spt_runtime::run_bounded_command(

+            &std::collections::BTreeMap::new(),

+            Ok(out) if out.status.success() => {

+                eprintln!("ADAPTER_{label}_OK");

+                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status.code());

+                eprintln!("ADAPTER_{label}_FAIL: {e}");

+        AdapterCmd::Add { path, github } => {

+            let source = match (path, github) {

+                (Some(p), None) => std::path::PathBuf::from(p),

+                (None, Some(spec)) => {

+                    // Manifest-first from GitHub: shallow-clone under

+                    // adapters/_github/ (a durable home — pointer-mode records

+                    // may read their manifest from here for life).

+                    let dest = adapters.join("_github").join(safe);

+                        let _ = std::fs::remove_dir_all(&dest);

+                    if let Some(parent) = dest.parent() {

+                        let _ = std::fs::create_dir_all(parent);

+                    let url = format!("https://github.com/{spec}.git");

+                    let cloned = std::process::Command::new("git")

+                        Ok(s) if s.success() => dest,

+                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", s.code());

+                            eprintln!("ADAPTER_CLONE_FAIL: {e}");

+                    eprintln!("ADAPTER_BAD_ARGS: exactly one of <path> or --github");

+            let (record, manifest) = match registry::register(&adapters, &source, now_ms) {

+                    eprintln!("ADAPTER_ADD_FAIL: {e}");

+                "ADAPTER_ADD:{}:{:?}:{:?} (registered)",

+                record.name, record.kind, record.mode

+            // Install is the first update (REQ-INSTALL-4) — conduct the

+            // declared avenue once, through the same verdict logic the

+            // self-update ripple uses (REQ-UPD-5).

+            match manifest.update.as_ref() {

+                    eprintln!("ADAPTER_INSTALL_SKIP: no [update] avenue (manifest-only adapter)");

+                Some(u) => match spt_daemon::adapter_update::plan_adapter_update(u, None) {

+                    spt_daemon::adapter_update::AdapterUpdateOutcome::Delegate(cmd) => {

+                        conduct("INSTALL", &cmd)

+                    spt_daemon::adapter_update::AdapterUpdateOutcome::Skipped(reason) => {

+                        // file_pull with no payload yet: the pull rides the

+                        // update engine (peer/self-fetch) — registration holds.

+                        eprintln!("ADAPTER_INSTALL_DEFERRED: {reason:?} (rides the update engine)");

+                        eprintln!("ADAPTER_INSTALL_DEFERRED: {verdict:?}");

+        AdapterCmd::Remove { name, force } => {

+            match registry::deregister(&adapters, &name) {

+                    eprintln!("ADAPTER_REMOVE:{name} (soft-deregistered; live instances unaffected)");

+                        Some(tpl) if force => conduct("UNINSTALL", &tpl),

+                                "ADAPTER_UNINSTALL_DEFERRED: manifest declares an uninstall \

+                                 template; run `spt adapter remove {name} --force` to conduct it"

+                    eprintln!("ADAPTER_REMOVE_FAIL: {e}");

+            let records = registry::all_records(&adapters);

+                println!("no adapters registered");

+                let state = if r.active { "active" } else { "deregistered" };

+                println!("{}: {:?} {:?} {state} (from {})", r.name, r.kind, r.mode, r.source_dir);

/// (`identity/trust/grants.json`, REQ-CONSENT-1). Rows are scoped to **this

/// node** (enforcement is target-node-local; the daemon's gate re-reads the


Fix: git clone must be bounded (REQ-HAZARD-SUBPROCESS-TIMEOUT) — route through `run_bounded_command`:


                    let url = format!("https://github.com/{spec}.git");
                    let cloned = std::process::Command::new("git")
                        .args(["clone", "--depth", "1", &url])
                        .arg(&dest)
                        .status();
                    match cloned {
                        Ok(s) if s.success() => dest,
                        Ok(s) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", s.code());
                            return 1;
                        }
                        Err(e) => {
                            eprintln!("ADAPTER_CLONE_FAIL: {e}");
                            return 1;
                        }
                    }

                    // Bounded like every git subprocess
                    // (REQ-HAZARD-SUBPROCESS-TIMEOUT).
                    let keys = std::collections::BTreeMap::from([
                        ("url".to_string(), format!("https://github.com/{spec}.git")),
                        ("dest".to_string(), dest.to_string_lossy().into_owned()),
                    ]);
                    match spt_runtime::run_bounded_command(
                        "git clone --depth 1 \"{url}\" \"{dest}\"",
                        &keys,
                        Duration::from_secs(300),
                        None,
                    ) {
                        Ok(out) if out.status.success() => dest,
                        Ok(out) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status.code());
                            return 1;
                        }
                        Err(e) => {
                            eprintln!("ADAPTER_CLONE_FAIL: {e}");
                            return 1;
                        }
                    }


                    let url = format!("https://github.com/{spec}.git");
                    let cloned = std::process::Command::new("git")
                        .args(["clone", "--depth", "1", &url])
                        .arg(&dest)
                        .status();
                    match cloned {
                        Ok(s) if s.success() => dest,
                        Ok(s) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", s.code());
                            return 1;
                        }
                        Err(e) => {
                            eprintln!("ADAPTER_CLONE_FAIL: {e}");
                            return 1;
                        }
                    }

                    // Bounded like every git subprocess
                    // (REQ-HAZARD-SUBPROCESS-TIMEOUT).
                    let keys = std::collections::BTreeMap::from([
                        ("url".to_string(), format!("https://github.com/{spec}.git")),
                        ("dest".to_string(), dest.to_string_lossy().into_owned()),
                    ]);
                    match spt_runtime::run_bounded_command(
                        "git clone --depth 1 \"{url}\" \"{dest}\"",
                        &keys,
                        Duration::from_secs(300),
                        None,
                    ) {
                        Ok(out) if out.status.success() => dest,
                        Ok(out) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status.code());
                            return 1;
                        }
                        Err(e) => {
                            eprintln!("ADAPTER_CLONE_FAIL: {e}");
                            return 1;
                        }
                    }

let _ = std::fs::create_dir_all(parent);

-                    let url = format!("https://github.com/{spec}.git");

-                    let cloned = std::process::Command::new("git")

-                        Ok(s) if s.success() => dest,

-                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", s.code());

+                    // Bounded like every git subprocess

+                    // (REQ-HAZARD-SUBPROCESS-TIMEOUT).

+                    let keys = std::collections::BTreeMap::from([

+                        ("url".to_string(), format!("https://github.com/{spec}.git")),

+                        ("dest".to_string(), dest.to_string_lossy().into_owned()),

+                    match spt_runtime::run_bounded_command(

+                        "git clone --depth 1 \"{url}\" \"{dest}\"",

+                        Duration::from_secs(300),

+                        Ok(out) if out.status.success() => dest,

+                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status.code());


Add parse test + build:


        assert!(parse(&["spt", "grant", "add", "spawn-shell"]).is_err(), "agent required");
    }

        assert!(parse(&["spt", "grant", "add", "spawn-shell"]).is_err(), "agent required");
    }

    // [unit->REQ-INSTALL-4] the adapter surface parses: add takes a path or
    // --github; remove takes a name + --force; list stands alone.
    #[test]
    fn adapter_subcommands_parse() {
        assert!(matches!(
            parse(&["spt", "adapter", "add", "C:/adapters/mock"]).unwrap().cmd,
            Cmd::Adapter { action: AdapterCmd::Add { path: Some(_), github: None } }
        ));
        assert!(matches!(
            parse(&["spt", "adapter", "add", "--github", "user/repo"]).unwrap().cmd,
            Cmd::Adapter { action: AdapterCmd::Add { path: None, github: Some(_) } }
        ));
        match parse(&["spt", "adapter", "remove", "mock-shell", "--force"]).unwrap().cmd {
            Cmd::Adapter { action: AdapterCmd::Remove { name, force } } => {
                assert_eq!(name, "mock-shell");
                assert!(force);
            }
            _ => panic!("expected Adapter Remove"),
        }
        assert!(matches!(
            parse(&["spt", "adapter", "list"]).unwrap().cmd,
            Cmd::Adapter { action: AdapterCmd::List }
        ));
    }


        assert!(parse(&["spt", "grant", "add", "spawn-shell"]).is_err(), "agent required");
    }

    // [unit->REQ-INSTALL-4] the adapter surface parses: add takes a path or
    // --github; remove takes a name + --force; list stands alone.
    #[test]
    fn adapter_subcommands_parse() {
        assert!(matches!(
            parse(&["spt", "adapter", "add", "C:/adapters/mock"]).unwrap().cmd,
            Cmd::Adapter { action: AdapterCmd::Add { path: Some(_), github: None } }
        ));
        assert!(matches!(
            parse(&["spt", "adapter", "add", "--github", "user/repo"]).unwrap().cmd,
            Cmd::Adapter { action: AdapterCmd::Add { path: None, github: Some(_) } }
        ));
        match parse(&["spt", "adapter", "remove", "mock-shell", "--force"]).unwrap().cmd {
            Cmd::Adapter { action: AdapterCmd::Remove { name, force } } => {
                assert_eq!(name, "mock-shell");
                assert!(force);
            }
            _ => panic!("expected Adapter Remove"),
        }
        assert!(matches!(
            parse(&["spt", "adapter", "list"]).unwrap().cmd,
            Cmd::Adapter { action: AdapterCmd::List }
        ));
    }

+    // [unit->REQ-INSTALL-4] the adapter surface parses: add takes a path or

+    // --github; remove takes a name + --force; list stands alone.

+            parse(&["spt", "adapter", "add", "C:/adapters/mock"]).unwrap().cmd,

+            Cmd::Adapter { action: AdapterCmd::Add { path: Some(_), github: None } }

+            parse(&["spt", "adapter", "add", "--github", "user/repo"]).unwrap().cmd,

+            Cmd::Adapter { action: AdapterCmd::Add { path: None, github: Some(_) } }

+        match parse(&["spt", "adapter", "remove", "mock-shell", "--force"]).unwrap().cmd {

+            Cmd::Adapter { action: AdapterCmd::Remove { name, force } } => {

+                assert_eq!(name, "mock-shell");

+            _ => panic!("expected Adapter Remove"),

+            parse(&["spt", "adapter", "list"]).unwrap().cmd,

+            Cmd::Adapter { action: AdapterCmd::List }

// [unit->REQ-TERM-4] digest defaults: a one-shot, human-rendered snapshot.


<task-notification>
<task-id>bjktpeb8s</task-id>
<tool-use-id>toolu_01E1ZEVtPdutqczzd4VEueWb</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bjktpeb8s.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI for D1-complete sha until FINAL conclusion" completed (exit code 0)</summary>
</task-notification>


PowerShell

cargo test -p spt-runtime registry 2>&1 | Select-Object -Last 4; cargo test -p spt --bin spt adapter 2>&1 | Select-Object -Last 6; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 2

Test registry + CLI parse + workspace clippy


Error: Exit code 101
test registry::tests::deregister_is_soft_and_readd_reactivates ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 21 filtered out; finished in 0.02s

     |                                                                  ^^^^^^ unknown field
     |
     = note: available fields are: `status_code`, `stdout`, `stderr`

For more information about this error, try `rustc --explain E0609`.
error: could not compile `spt` (bin "spt" test) due to 4 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `spt` (bin "spt" test) due to 4 previous errors


Prompt:
<task-notification>
<task-id>bjktpeb8s</task-id>
<tool-use-id>toolu_01E1ZEVtPdutqczzd4VEueWb</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bjktpeb8s.output</output-file>
<status>completed</status>
<summary>Background command "Watch CI for D1-complete sha until FINAL conclusion" completed (exit code 0)</summary>
</task-notification>


1	ci: success
2

ci: success


153:pub struct BoundedOutput {
154-    pub status_code: Option<i32>,
155-    pub stdout: String,
156-    pub stderr: String,
157-}
158-
159-impl BoundedOutput {
160-    pub fn success(&self) -> bool {
161-        self.status_code == Some(0)
162-    }
163-}
164-
165-/// The spawn-session seam. M2a = local process; M3 daemon = loop/PTY child,


D1 CI green FINAL ✓. Fix BoundedOutput field usage:


Edit

            Ok(out) if out.status.success() => {
                eprintln!("ADAPTER_{label}_OK");
                0
            }
            Ok(out) => {
                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status.code());
                1
            }
            Err(e) => {
                eprintln!("ADAPTER_{label}_FAIL: {e}");
                1
            }

            Ok(out) if out.success() => {
                eprintln!("ADAPTER_{label}_OK");
                0
            }
            Ok(out) => {
                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status_code);
                1
            }
            Err(e) => {
                eprintln!("ADAPTER_{label}_FAIL: {e}");
                1
            }


            Ok(out) if out.status.success() => {
                eprintln!("ADAPTER_{label}_OK");
                0
            }
            Ok(out) => {
                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status.code());
                1
            }
            Err(e) => {
                eprintln!("ADAPTER_{label}_FAIL: {e}");
                1
            }

            Ok(out) if out.success() => {
                eprintln!("ADAPTER_{label}_OK");
                0
            }
            Ok(out) => {
                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status_code);
                1
            }
            Err(e) => {
                eprintln!("ADAPTER_{label}_FAIL: {e}");
                1
            }

-            Ok(out) if out.status.success() => {

+            Ok(out) if out.success() => {

-                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status.code());

+                eprintln!("ADAPTER_{label}_FAIL: exit {:?}", out.status_code);


                        Ok(out) if out.status.success() => dest,
                        Ok(out) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status.code());
                            return 1;
                        }

                        Ok(out) if out.success() => dest,
                        Ok(out) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status_code);
                            return 1;
                        }


                        Ok(out) if out.status.success() => dest,
                        Ok(out) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status.code());
                            return 1;
                        }

                        Ok(out) if out.success() => dest,
                        Ok(out) => {
                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status_code);
                            return 1;
                        }

-                        Ok(out) if out.status.success() => dest,

+                        Ok(out) if out.success() => dest,

-                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status.code());

+                            eprintln!("ADAPTER_CLONE_FAIL: git exit {:?}", out.status_code);


PowerShell

cargo test -p spt --bin spt adapter 2>&1 | Select-Object -Last 5; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 2

Re-test CLI parse + workspace clippy


test api::tests::adapter_is_required ... ok
test cli::tests::adapter_subcommands_parse ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 63 filtered out; finished in 0.00s

    Checking spt v0.0.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.54s


crates\spt-daemon\src\adapter_update.rs:2://! first, then each registered adapter via its manifest `[update]` avenue, with
crates\spt-daemon\src\adapter_update.rs:6://! ripple over its registered adapters. Each adapter declares how it updates in
crates\spt-daemon\src\adapter_update.rs:135:pub fn conduct_ripple(adapters: &[AdapterRipple<'_>]) -> Vec<(String, AdapterUpdateOutcome)> {
crates\spt-daemon\src\adapter_update.rs:281:        let out = conduct_ripple(&adapters);
crates\spt-daemon\src\lib.rs:60://! after self-update, spt-core updates each registered adapter via its manifest
crates\spt-daemon\src\lib.rs:149:    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
crates\spt-daemon\src\nethost.rs:414:        // the loop; a failed handshake is simply not registered.
crates\spt-daemon\src\nethost.rs:681:        assert_eq!(b.conn_count(), 1, "accept loop registered the inbound conn");


D2c — wire registered set as the ripple source:


Edit

/// Conduct the adapter ripple in **registration order** (REQ-UPD-5). spt-core's
/// own update runs *before* this (the C0–C2 self-update) — this is the
/// adapters-after-self half; the ordering is the caller's contract, preserved
/// here by mapping in order. Returns each adapter's id + verdict.
// [impl->REQ-UPD-5]
pub fn conduct_ripple(adapters: &[AdapterRipple<'_>]) -> Vec<(String, AdapterUpdateOutcome)> {
    adapters
        .iter()
        .map(|a| (a.id.clone(), plan_adapter_update(a.update, a.payload.as_ref())))
        .collect()
}

/// Conduct the adapter ripple in **registration order** (REQ-UPD-5). spt-core's
/// own update runs *before* this (the C0–C2 self-update) — this is the
/// adapters-after-self half; the ordering is the caller's contract, preserved
/// here by mapping in order. Returns each adapter's id + verdict.
// [impl->REQ-UPD-5]
pub fn conduct_ripple(adapters: &[AdapterRipple<'_>]) -> Vec<(String, AdapterUpdateOutcome)> {
    adapters
        .iter()
        .map(|a| (a.id.clone(), plan_adapter_update(a.update, a.payload.as_ref())))
        .collect()
}

/// Plan the ripple over the node's **registered adapter set** (M5-D2c,
/// REQ-INSTALL-4: "the registered set is what self-update ripples", R-UPD-5)
/// — the production source `spt adapter add/remove` maintains, in
/// registration order. Payloads are not supplied at this layer (`file_pull`
/// pulls ride the update engine's transport); a `delegated` adapter's verdict
/// is complete here. Soft-deregistered adapters are excluded by
/// [`spt_runtime::registry::registered`].
// [impl->REQ-INSTALL-4]
pub fn ripple_registered(adapters_dir: &std::path::Path) -> Vec<(String, AdapterUpdateOutcome)> {
    let mut set = spt_runtime::registry::registered(adapters_dir);
    set.sort_by_key(|(r, _)| r.registered_at_ms);
    set.iter()
        .map(|(r, m)| {
            let verdict = match m.update.as_ref() {
                None => AdapterUpdateOutcome::Skipped(SkipReason::NoAvenue),
                Some(u) => plan_adapter_update(u, None),
            };
            (r.name.clone(), verdict)
        })
        .collect()
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\adapter_update.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Conduct the adapter ripple in **registration order** (REQ-UPD-5). spt-core's
/// own update runs *before* this (the C0–C2 self-update) — this is the
/// adapters-after-self half; the ordering is the caller's contract, preserved
/// here by mapping in order. Returns each adapter's id + verdict.
// [impl->REQ-UPD-5]
pub fn conduct_ripple(adapters: &[AdapterRipple<'_>]) -> Vec<(String, AdapterUpdateOutcome)> {
    adapters
        .iter()
        .map(|a| (a.id.clone(), plan_adapter_update(a.update, a.payload.as_ref())))
        .collect()
}

/// Plan the ripple over the node's **registered adapter set** (M5-D2c,
/// REQ-INSTALL-4: "the registered set is what self-update ripples", R-UPD-5)
/// — the production source `spt adapter add/remove` maintains, in
/// registration order. Payloads are not supplied at this layer (`file_pull`
/// pulls ride the update engine's transport); a `delegated` adapter's verdict
/// is complete here. Soft-deregistered adapters are excluded by
/// [`spt_runtime::registry::registered`].
// [impl->REQ-INSTALL-4]
pub fn ripple_registered(adapters_dir: &std::path::Path) -> Vec<(String, AdapterUpdateOutcome)> {
    let mut set = spt_runtime::registry::registered(adapters_dir);
    set.sort_by_key(|(r, _)| r.registered_at_ms);
    set.iter()
        .map(|(r, m)| {
            let verdict = match m.update.as_ref() {
                None => AdapterUpdateOutcome::Skipped(SkipReason::NoAvenue),
                Some(u) => plan_adapter_update(u, None),
            };
            (r.name.clone(), verdict)
        })
        .collect()
}

+/// Plan the ripple over the node's **registered adapter set** (M5-D2c,

+/// REQ-INSTALL-4: "the registered set is what self-update ripples", R-UPD-5)

+/// — the production source `spt adapter add/remove` maintains, in

+/// registration order. Payloads are not supplied at this layer (`file_pull`

+/// pulls ride the update engine's transport); a `delegated` adapter's verdict

+/// is complete here. Soft-deregistered adapters are excluded by

+pub fn ripple_registered(adapters_dir: &std::path::Path) -> Vec<(String, AdapterUpdateOutcome)> {

+    let mut set = spt_runtime::registry::registered(adapters_dir);

+    set.sort_by_key(|(r, _)| r.registered_at_ms);

+            let verdict = match m.update.as_ref() {

+                None => AdapterUpdateOutcome::Skipped(SkipReason::NoAvenue),

+                Some(u) => plan_adapter_update(u, None),


255	        ));
256	    }
257
258	    // [unit->REQ-UPD-5] a file_pull adapter with no payload yet is skipped (the
259	    // pull has not run — M4 transport), never applied unverified.
260	    #[test]
261	    fn file_pull_without_payload_is_skipped() {
262	        let sk = SigningKey::from_bytes(&[11u8; 32]);
263	        let update = file_pull_update(Some(key_hex(&sk)));
264	        assert_eq!(
265	            plan_adapter_update(&update, None),
266	            AdapterUpdateOutcome::Skipped(SkipReason::NoPayload)
267	        );
268	    }
269
270	    // [unit->REQ-SEAM-UPDATE] a delegated adapter that attests self_verifies is
271	    // delegated to its own updater (the trusted command).
272	    #[test]
273	    fn delegated_attested_delegates_command() {
274	        let update = delegated_update(true, Some("claude plugin update spt".to_string()));
275	        assert_eq!(
276	            plan_adapter_update(&update, None),
277	            AdapterUpdateOutcome::Delegate("claude plugin update spt".to_string())
278	        );
279	    }
280
281	    // [unit->REQ-SEAM-UPDATE] a delegated adapter that does NOT attest is skipped
282	    // as unverifiable — spt-core never blindly trusts an opaque updater.
283	    #[test]
284	    fn delegated_unattested_is_skipped() {
285	        let update = delegated_update(false, Some("claude plugin update spt".to_string()));
286	        assert_eq!(
287	            plan_adapter_update(&update, None),
288	            AdapterUpdateOutcome::Skipped(SkipReason::UnverifiableDelegated)
289	        );
290	    }
291
292	    // [unit->REQ-UPD-5] the ripple is conducted in registration order, each
293	    // adapter getting its own verdict (self-update runs before this).
294	    #[test]
295	    fn ripple_conducts_each_adapter_in_order() {
296	        let sk = SigningKey::from_bytes(&[11u8; 32]);
297	        let fp = file_pull_update(Some(key_hex(&sk)));
298	        let dg = delegated_update(true, Some("upd".to_string()));
299	        let adapters = vec![
300	            AdapterRipple { id: "alpha".to_string(), update: &fp, payload: Some(sign(&sk, b"a")) },
301	            AdapterRipple { id: "beta".to_string(), update: &dg, payload: None },
302	        ];
303	        let out = conduct_ripple(&adapters);
304	        assert_eq!(out.len(), 2);
305	        assert_eq!(out[0].0, "alpha");
306	        assert!(matches!(out[0].1, AdapterUpdateOutcome::Verified(_)));
307	        assert_eq!(out[1].0, "beta");
308	        assert_eq!(out[1].1, AdapterUpdateOutcome::Delegate("upd".to_string()));
309	    }
310	}
311

        ));
    }

    // [unit->REQ-UPD-5] a file_pull adapter with no payload yet is skipped (the
    // pull has not run — M4 transport), never applied unverified.
    #[test]
    fn file_pull_without_payload_is_skipped() {
        let sk = SigningKey::from_bytes(&[11u8; 32]);
        let update = file_pull_update(Some(key_hex(&sk)));
        assert_eq!(
            plan_adapter_update(&update, None),
            AdapterUpdateOutcome::Skipped(SkipReason::NoPayload)
        );
    }

    // [unit->REQ-SEAM-UPDATE] a delegated adapter that attests self_verifies is
    // delegated to its own updater (the trusted command).
    #[test]
    fn delegated_attested_delegates_command() {
        let update = delegated_update(true, Some("claude plugin update spt".to_string()));
        assert_eq!(
            plan_adapter_update(&update, None),
            AdapterUpdateOutcome::Delegate("claude plugin update spt".to_string())
        );
    }

    // [unit->REQ-SEAM-UPDATE] a delegated adapter that does NOT attest is skipped
    // as unverifiable — spt-core never blindly trusts an opaque updater.
    #[test]
    fn delegated_unattested_is_skipped() {
        let update = delegated_update(false, Some("claude plugin update spt".to_string()));
        assert_eq!(
            plan_adapter_update(&update, None),
            AdapterUpdateOutcome::Skipped(SkipReason::UnverifiableDelegated)
        );
    }

    // [unit->REQ-UPD-5] the ripple is conducted in registration order, each
    // adapter getting its own verdict (self-update runs before this).
    #[test]
    fn ripple_conducts_each_adapter_in_order() {
        let sk = SigningKey::from_bytes(&[11u8; 32]);
        let fp = file_pull_update(Some(key_hex(&sk)));
        let dg = delegated_update(true, Some("upd".to_string()));
        let adapters = vec![
            AdapterRipple { id: "alpha".to_string(), update: &fp, payload: Some(sign(&sk, b"a")) },
            AdapterRipple { id: "beta".to_string(), update: &dg, payload: None },
        ];
        let out = conduct_ripple(&adapters);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].0, "alpha");
        assert!(matches!(out[0].1, AdapterUpdateOutcome::Verified(_)));
        assert_eq!(out[1].0, "beta");
        assert_eq!(out[1].1, AdapterUpdateOutcome::Delegate("upd".to_string()));
    }
}


        let out = conduct_ripple(&adapters);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].0, "alpha");
        assert!(matches!(out[0].1, AdapterUpdateOutcome::Verified(_)));
        assert_eq!(out[1].0, "beta");
        assert_eq!(out[1].1, AdapterUpdateOutcome::Delegate("upd".to_string()));
    }
}

        let out = conduct_ripple(&adapters);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].0, "alpha");
        assert!(matches!(out[0].1, AdapterUpdateOutcome::Verified(_)));
        assert_eq!(out[1].0, "beta");
        assert_eq!(out[1].1, AdapterUpdateOutcome::Delegate("upd".to_string()));
    }

    // [unit->REQ-INSTALL-4] the registered set IS the ripple source: a
    // delegated+attested adapter delegates, an avenue-less one is skipped,
    // a soft-deregistered one drops out entirely; registration order holds.
    #[test]
    fn ripple_registered_uses_the_registered_set() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters_dir = tmp.path().join("adapters");
        let seed = |name: &str, body: &str| {
            let src = tmp.path().join(format!("{name}-src"));
            std::fs::create_dir_all(&src).unwrap();
            std::fs::write(src.join("manifest.toml"), body).unwrap();
            src
        };
        let plain = seed(
            "plain",
            "[adapter]\nname = \"plain\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",
        );
        let updatable = seed(
            "updatable",
            "[adapter]\nname = \"updatable\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n\n\
             [update]\navenue = \"delegated\"\ncommand = \"upd\"\nself_verifies = true\n",
        );
        let gone = seed(
            "gone",
            "[adapter]\nname = \"gone\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",
        );

        // Register out of name order to prove registration-time ordering.
        spt_runtime::registry::register(&adapters_dir, &updatable, 100).unwrap();
        spt_runtime::registry::register(&adapters_dir, &plain, 200).unwrap();
        spt_runtime::registry::register(&adapters_dir, &gone, 300).unwrap();
        spt_runtime::registry::deregister(&adapters_dir, "gone").unwrap();

        let out = ripple_registered(&adapters_dir);
        assert_eq!(
            out,
            vec![
                ("updatable".to_string(), AdapterUpdateOutcome::Delegate("upd".to_string())),
                ("plain".to_string(), AdapterUpdateOutcome::Skipped(SkipReason::NoAvenue)),
            ]
        );
    }
}


        let out = conduct_ripple(&adapters);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].0, "alpha");
        assert!(matches!(out[0].1, AdapterUpdateOutcome::Verified(_)));
        assert_eq!(out[1].0, "beta");
        assert_eq!(out[1].1, AdapterUpdateOutcome::Delegate("upd".to_string()));
    }
}

        let out = conduct_ripple(&adapters);
        assert_eq!(out.len(), 2);
        assert_eq!(out[0].0, "alpha");
        assert!(matches!(out[0].1, AdapterUpdateOutcome::Verified(_)));
        assert_eq!(out[1].0, "beta");
        assert_eq!(out[1].1, AdapterUpdateOutcome::Delegate("upd".to_string()));
    }

    // [unit->REQ-INSTALL-4] the registered set IS the ripple source: a
    // delegated+attested adapter delegates, an avenue-less one is skipped,
    // a soft-deregistered one drops out entirely; registration order holds.
    #[test]
    fn ripple_registered_uses_the_registered_set() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters_dir = tmp.path().join("adapters");
        let seed = |name: &str, body: &str| {
            let src = tmp.path().join(format!("{name}-src"));
            std::fs::create_dir_all(&src).unwrap();
            std::fs::write(src.join("manifest.toml"), body).unwrap();
            src
        };
        let plain = seed(
            "plain",
            "[adapter]\nname = \"plain\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",
        );
        let updatable = seed(
            "updatable",
            "[adapter]\nname = \"updatable\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n\n\
             [update]\navenue = \"delegated\"\ncommand = \"upd\"\nself_verifies = true\n",
        );
        let gone = seed(
            "gone",
            "[adapter]\nname = \"gone\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",
        );

        // Register out of name order to prove registration-time ordering.
        spt_runtime::registry::register(&adapters_dir, &updatable, 100).unwrap();
        spt_runtime::registry::register(&adapters_dir, &plain, 200).unwrap();
        spt_runtime::registry::register(&adapters_dir, &gone, 300).unwrap();
        spt_runtime::registry::deregister(&adapters_dir, "gone").unwrap();

        let out = ripple_registered(&adapters_dir);
        assert_eq!(
            out,
            vec![
                ("updatable".to_string(), AdapterUpdateOutcome::Delegate("upd".to_string())),
                ("plain".to_string(), AdapterUpdateOutcome::Skipped(SkipReason::NoAvenue)),
            ]
        );
    }
}

+    // [unit->REQ-INSTALL-4] the registered set IS the ripple source: a

+    // delegated+attested adapter delegates, an avenue-less one is skipped,

+    // a soft-deregistered one drops out entirely; registration order holds.

+    fn ripple_registered_uses_the_registered_set() {

+        let tmp = tempfile::tempdir().unwrap();

+        let adapters_dir = tmp.path().join("adapters");

+        let seed = |name: &str, body: &str| {

+            let src = tmp.path().join(format!("{name}-src"));

+            std::fs::create_dir_all(&src).unwrap();

+            std::fs::write(src.join("manifest.toml"), body).unwrap();

+            "[adapter]\nname = \"plain\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",

+            "[adapter]\nname = \"updatable\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n\n\

+             [update]\navenue = \"delegated\"\ncommand = \"upd\"\nself_verifies = true\n",

+            "[adapter]\nname = \"gone\"\nversion = \"1\"\nmin_spt_core_version = \"0\"\n",

+        // Register out of name order to prove registration-time ordering.

+        spt_runtime::registry::register(&adapters_dir, &updatable, 100).unwrap();

+        spt_runtime::registry::register(&adapters_dir, &plain, 200).unwrap();

+        spt_runtime::registry::register(&adapters_dir, &gone, 300).unwrap();

+        spt_runtime::registry::deregister(&adapters_dir, "gone").unwrap();

+        let out = ripple_registered(&adapters_dir);

+                ("updatable".to_string(), AdapterUpdateOutcome::Delegate("upd".to_string())),

+                ("plain".to_string(), AdapterUpdateOutcome::Skipped(SkipReason::NoAvenue)),


PowerShell

cargo test -p spt-daemon --lib adapter_update 2>&1 | Select-Object -Last 12; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 1

Test adapter_update incl. ripple_registered + clippy


running 8 tests
test adapter_update::tests::delegated_unattested_is_skipped ... ok
test adapter_update::tests::delegated_attested_delegates_command ... ok
test adapter_update::tests::file_pull_without_payload_is_skipped ... ok
test adapter_update::tests::file_pull_tampered_bytes_rejected ... ok
test adapter_update::tests::ripple_conducts_each_adapter_in_order ... ok
test adapter_update::tests::file_pull_verified_against_adapter_key ... ok
test adapter_update::tests::file_pull_wrong_key_rejected ... ok
test adapter_update::tests::ripple_registered_uses_the_registered_set ... ok

test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 133 filtered out; finished in 0.02s

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.27s


Activate REQ-INSTALL-4, re-export, commit D2:


145
146	pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
147	pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
148	pub use adapter_update::{
149	    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
150	    SkipReason,
151	};
152	pub use brain::{Brain, BrainState, BrokerEvent};

pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};
pub use brain::{Brain, BrainState, BrokerEvent};


Edit

pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};

pub use adapter_update::{
    conduct_ripple, plan_adapter_update, ripple_registered, AdapterRipple, AdapterUpdateOutcome,
    FilePullPayload, SkipReason,
};


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

pub use adapter_update::{
    conduct_ripple, plan_adapter_update, ripple_registered, AdapterRipple, AdapterUpdateOutcome,
    FilePullPayload, SkipReason,
};

//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).
//!
//! ADR-0004 splits the logical daemon into two implementation layers so a
//! routine self-update swaps the logic without disturbing live endpoints:
//!
//! - a **broker** (stable kernel) holds *only* the un-transferable resources a
//!   live consumer would lose continuity on if the logic restarted — PTY
//!   masters (from [`spt_term`]), spawned harness children, accepted/listening
//!   sockets — and runs no logic;
//! - a **brain** (restartable logic) holds routing, registry, manifest parse,
//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a
//!   **versioned local IPC** so a *newer brain talks to an older broker*.
//!
//! **B0 (this commit)** lands the crate's layer position plus that versioned
//! IPC seam — the load-bearing contract everything else rides:
//!
//! - [`frame`] — the forward-compatible [`Envelope`] schema + the version
//!   [`Hello`] handshake (KNOWN-HAZARDS 2.3, [`REQ-HAZARD-HANDOFF-ARGV-COMPAT`]);
//! - [`codec`] — length-prefixed JSON framing over any [`std::io::Read`]/`Write`;
//! - [`transport`] — the OS-split carrier (named pipe / UDS) behind one
//!   [`DaemonTransport`] trait, mirroring `spt-term`'s `SessionSurface`.
//!
//! **B1 (this commit)** adds the **broker kernel** ([`broker`]) — the stable
//! half that hosts a real `spt-term` PTY child and relays its output/input over
//! the IPC, holding only the ADR-0004 §B resources and no logic — plus the
//! broker↔brain message catalog ([`msg`]).
//!
//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:
//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled
//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,
//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s
//! constant; the only residue left in the harness-hosted session is the thin,
//! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
//! `BrainLifecycle` (re-host, not rewrite) ([`REQ-DAEMON-1`]).
//!
//! **B5 (this commit)** makes the broker↔brain boundary **exactly-once across a
//! brain crash** ([`effect::EffectJournal`]): an idempotent `input` carries a
//! durable `op_id`, and the broker dedups at the effect keyed by that id over a
//! broker-owned recovery anchor (KNOWN-HAZARDS 7.2, [`REQ-HAZARD-RESTART-IDEMPOTENT`];
//! Spike #6's two constraints — broker-owned anchor, dedup-at-effect-by-id —
//! instantiated on the real PTY-write boundary).
//!
//! **C0 (this commit)** opens M3c — the self-update engine ([`update`]): the
//! ADR-0004 §A update-class taxonomy ([`UpdateClass`] + [`classify`]) and the
//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →
//! drop → re-attach — so the live PTY child + its output stream survive the logic
//! swap untouched; it never re-implements the handoff and never touches a broker
//! resource.
//!
//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a
//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback
//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key
//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a
//! SHA-256 binding the signature to the actual binary bytes. [`plan_verified`] is
//! the front door: it produces an [`UpdatePlan`] *only* for a release that passes
//! every check, so an unverified binary can never reach [`apply_brain_only`].
//!
//! **C3 (this commit)** conducts the **adapter ripple** ([`adapter_update`]):
//! after self-update, spt-core updates each registered adapter via its manifest
//! `[update]` avenue ([`REQ-UPD-5`], [`REQ-SEAM-UPDATE`]). `file_pull` payloads
//! are verified against the **adapter's own** key (manifest-declared) + SHA-256
//! (adapter content signing, ADR-0004 §D); `delegated` updaters are opaque, so
//! spt-core delegates the trust only when the adapter attests `self_verifies`,
//! else skips them as unverifiable.
//!
//! **D4b (this commit)** makes broker-held QUIC streams survive brain restarts
//! **gaplessly and exactly-once** (Spike #3 invariants A–D as real tests): each
//! stream's reads are buffered in a sequenced ring (the B2 `OutputLog`
//! discipline) a brain subscribes to with a resume cursor; sends/opens carry
//! durable `op_id`s journaled at the broker, so a restarted brain's re-drive
//! dedups instead of double-writing the wire.
//!
//! **D4a (this commit)** moves the node's network presence into the broker
//! ([`nethost::NetHost`], ADR-0004 §B): a dedicated tokio runtime hosts the
//! iroh endpoint + every accepted/dialed QUIC connection, so a brain restart
//! never interrupts a cross-node connection. The brain drives net operations
//! (endpoint-status, dial) over new IPC frames in the same forward-compatible
//! catalog; an `op_id`-carrying dial is exactly-once through the B5
//! [`effect::EffectJournal`]. The endpoint binds as the durable node identity
//! (`spt-store`'s `node.key`), with the relay stance from `daemon.json`.
//!
//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a
//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a
//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009
//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS
//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local
//! REQ-MSG delivery path.
//!
//! **D5b (this commit)** adds remote terminal attach ([`attach`], REQ-INST-8):
//! a byte-stream viewport onto a local session for an operator daemon on
//! another node — session output pumped as sequenced
//! [`AttachRecord`](spt_net::net::attach::AttachRecord)s (deduped at the
//! operator's render cursor), operator input through the B5 exactly-once PTY
//! path, the whole loop gated and restart-survivable purely from the D4/B5
//! machinery.
//!
//! The brain + gapless handoff (B2) and the rest of the M3b task table build on
//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK
//! (R-ARCH-2).
//!
//! [`Envelope`]: frame::Envelope
//! [`Hello`]: frame::Hello
//! [`DaemonTransport`]: transport::DaemonTransport

pub mod access;
pub mod adapter_update;
pub mod attach;
pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod consent;
pub mod daemon;
pub mod digest;
pub mod digesthub;
pub mod dispatch;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod grants;
pub mod lifecycle;
pub mod msg;
pub mod nethost;
pub mod notif;
pub mod notifsync;
pub mod peerloop;
pub mod propagate;
pub mod psyrelay;
pub mod reconcile;
pub mod registryhost;
pub mod relay;
pub mod relcache;
pub mod release;
pub mod resting;
pub mod seedmap;
pub mod sync;
pub mod transport;
pub mod update;
pub mod wan;
pub mod xfer;

#[cfg(test)]
mod test_home;

pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};
pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::{DaemonConfig, RelayChoice};
pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
pub use daemon::{ensure_running, is_running, Daemon};
pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
pub use notif::{
    first_fire, most_recently_active_visible, produce_and_first_fire, produce_consent_notif,
    resurface_at_boundary, FirstFireOutcome, NotifSurfacePolicy, ResurfaceOutcome,
    NOTIF_KIND_AGENT, NOTIF_KIND_CONSENT, NOTIF_KIND_PSYCHE, SUPPRESSION_WINDOW_MS,
};
pub use notifsync::{apply_notif_feed, emit_notif_feed, NotifApplyVerdict, NotifPolicy};
pub use propagate::{request_update, serve_update, UpdatePullOutcome, UpdateServeOutcome};
pub use psyrelay::{
    psyche_turn_and_relay, relay_psyche_outbound, PsycheNotifCtx, RelayOutcome, PSYCHE_BODY_CAP,
};
pub use relay::Relay;
pub use relcache::ReleaseCache;
pub use resting::{
    apply_event, arm_transition_echo, daemon_rest_event, effective_auto_suspend,
    fire_wake_effects, read_rest, request_freshness_pull, take_freshness_pull, transition,
    write_rest, EdgeReport, RestEvent, RestRecord, RestState, PULL_MARKER_FILE,
};
pub use seedmap::{put_seed, take_seed, SeedRegistry};
pub use frame::{
    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
    MIN_COMPATIBLE_VERSION,
};
pub use transport::{DaemonTransport, LocalSocketTransport};
pub use release::{
    parse_verifying_key, sha256_hex, verify_artifact, verify_metadata, verify_signature,
    RejectReason, ReleaseMetadata, SignedRelease, VerifyPolicy,
};
pub use update::{
    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
    UpdateClass, UpdatePlan, BROKER_RESOURCE_ABI,
};
pub use sync::{
    reconcile_after_sync, request_sync, select_refs, serve_sync, ReconcileWiring, SyncPolicy,
    SyncPullReport, SyncServeOutcome,
};
pub use xfer::{fetch_file, push_file, serve_xfer, XferServeOutcome};
pub use wan::{receive_wan, WanOutcome};

-    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,

+    conduct_ripple, plan_adapter_update, ripple_registered, AdapterRipple, AdapterUpdateOutcome,


required_stages = []  # rule 5: M5 — adapter registration lifecycle lands with the adapter-hosting work (manifest seams already shipped, REQ-SEAM-*)

required_stages = ["impl", "unit"]  # activated M5-D2: spt-runtime::registry (validate-first register; copy-for-file_pull/avenue-less vs pointer-for-delegated under {SPT_HOME}/adapters/; soft-deregister retains record+copy, surfaces the manifest uninstall template; re-add re-activates) + spt adapter add/--github/remove/list CLI (install-is-first-update conducted through plan_adapter_update; bounded clone/conduct) + spt-daemon::adapter_update::ripple_registered (the registered set IS the R-UPD-5 ripple source). int = D3 shell-spawn consumes a registered shell adapter E2E


The file C:\Users\decid\Documents\projects\spt-core\traceable-reqs.toml has been updated successfully. (file state is current in your context — no need to Read it back)

title = "Adapter registration lifecycle: spt adapter add (--github, manifest-first, install-is-first-update) + soft-deregister remove + optional manifest uninstall template; node-local registered-adapter set self-update ripples over"

-required_stages = []  # rule 5: M5 — adapter registration lifecycle lands with the adapter-hosting work (manifest seams already shipped, REQ-SEAM-*)

+required_stages = ["impl", "unit"]  # activated M5-D2: spt-runtime::registry (validate-first register; copy-for-file_pull/avenue-less vs pointer-for-delegated under {SPT_HOME}/adapters/; soft-deregister retains record+copy, surfaces the manifest uninstall template; re-add re-activates) + spt adapter add/--github/remove/list CLI (install-is-first-update conducted through plan_adapter_update; bounded clone/conduct) + spt-daemon::adapter_update::ripple_registered (the registered set IS the R-UPD-5 ripple source). int = D3 shell-spawn consumes a registered shell adapter E2E

title = "Auto-detect and migrate a legacy claude_skill_owl install"


PowerShell

cargo clippy --workspace --all-targets -- -D warnings 2>&1 | Select-Object -Last 1; cargo test --workspace 2>&1 | Select-String "test result|FAILED|error\[" | Select-Object -Last 25; traceable-reqs check 2>&1 | Select-Object -First 1

Full workspace test + clippy + traceability before D2 commit


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.22s

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.43s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test reconcile::tests::failed_turn_preserves_everything ... ok
test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 6.31s
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.17s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.20s
test result: ok. 90 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.21s
test result: ok. 54 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 24 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.32s
test result: ok. 133 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 9.62s
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.04s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 1.01s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
134 requirements: 134 complete, 0 incomplete, 0 findings


git add crates/spt-runtime/src/registry.rs crates/spt-runtime/src/lib.rs crates/spt-store/src/perch.rs crates/spt-daemon/src/adapter_update.rs crates/spt-daemon/src/lib.rs crates/spt/src/cli.rs traceable-reqs.toml && git commit -m @'
feat(runtime,daemon,cli): M5-D2 adapter registration lifecycle

spt-runtime::registry — the node-local registered-adapter set under
{SPT_HOME}/adapters/ (one dir + record.toml per adapter, both kinds).
Validate-first: an invalid manifest records nothing. Record mode falls out
of the [update] avenue: copy for file_pull/avenue-less (spt-core owns the
bytes), pointer for delegated (manifest re-read live from the source tree
so a delegated self-update never leaves a stale copy). Remove is a
soft-deregister: record + copy retained (live instances keep running),
hidden from the active set, the manifest uninstall template surfaced to
the caller; re-add re-activates.

spt adapter add/remove/list CLI — local path or --github user/repo
(bounded shallow clone under adapters/_github/); install-is-first-update:
the declared avenue is conducted once after recording via
plan_adapter_update (delegated+attested execs bounded; file_pull defers to
the update engine transport). Uninstall conducts under --force until
adapter-linked quiesce detection lands (D3).

spt-daemon::adapter_update::ripple_registered — the registered set is now
the production R-UPD-5 ripple source, in registration order,
soft-deregistered adapters excluded.

REQ-INSTALL-4 activated impl+unit; int rides the D3 shell-spawn consumer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@ && git push

Commit and push M5-D2 slice


warning: in the working copy of 'crates/spt-daemon/src/adapter_update.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-daemon/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/src/lib.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt/src/cli.rs', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'traceable-reqs.toml', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'crates/spt-runtime/src/registry.rs', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 23c22a7] feat(runtime,daemon,cli): M5-D2 adapter registration lifecycle
 7 files changed, 650 insertions(+), 3 deletions(-)
 create mode 100644 crates/spt-runtime/src/registry.rs
To https://github.com/SaberMage/spt-core.git
   5a96c3e..23c22a7  dev-freeform -> dev-freeform


$sha = git rev-parse 23c22a7; for ($i=0; $i -lt 60; $i++) { $r = gh run list --commit $sha --json status,conclusion,workflowName | ConvertFrom-Json; if ($r.Count -gt 0 -and ($r | Where-Object { $_.status -ne "completed" }).Count -eq 0) { $r | ForEach-Object { "$($_.workflowName): $($_.conclusion)" }; break }; Start-Sleep -Seconds 20 }

Watch CI for D2 sha until FINAL conclusion


Command running in background with ID: bxsv95q5t. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bxsv95q5t.output. You will be notified when it completes. To check interim output, use Read on that file path.