Edit

    /// A profile overlays `[strings]` like any section (leaf-replace): the
    /// profile's key wins, the parent's untouched string survives.
    // [unit->REQ-MANIFEST-3]
    #[test]

    /// `as_file_pointer` recognizes a pointer ONLY for a table with EXACTLY one
    /// key `file` (string) — the disambiguation that keeps every other table
    /// shape an opaque nested tree (back-compat). (REQ-MANIFEST-5)
    // [unit->REQ-MANIFEST-5]
    #[test]
    fn file_pointer_recognized_only_for_single_file_key_table() {
        // The pointer form.
        let ptr: Value = toml::from_str("k = { file = \"body.md\" }").unwrap();
        assert_eq!(as_file_pointer(ptr.get("k").unwrap()), Some("body.md"));

        // A literal string is not a pointer.
        assert_eq!(as_file_pointer(&Value::String("inline".into())), None);

        // A table with `file` PLUS another key stays opaque (not a pointer).
        let multi: Value = toml::from_str("k = { file = \"x\", extra = 1 }").unwrap();
        assert_eq!(as_file_pointer(multi.get("k").unwrap()), None);

        // A table whose sole key isn't `file` is opaque.
        let other: Value = toml::from_str("k = { path = \"x\" }").unwrap();
        assert_eq!(as_file_pointer(other.get("k").unwrap()), None);

        // `file` present but not a string → not a pointer.
        let nonstr: Value = toml::from_str("k = { file = 7 }").unwrap();
        assert_eq!(as_file_pointer(nonstr.get("k").unwrap()), None);
    }

    /// A profile overlays `[strings]` like any section (leaf-replace): the
    /// profile's key wins, the parent's untouched string survives.
    // [unit->REQ-MANIFEST-3]
    #[test]


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

    /// `as_file_pointer` recognizes a pointer ONLY for a table with EXACTLY one
    /// key `file` (string) — the disambiguation that keeps every other table
    /// shape an opaque nested tree (back-compat). (REQ-MANIFEST-5)
    // [unit->REQ-MANIFEST-5]
    #[test]
    fn file_pointer_recognized_only_for_single_file_key_table() {
        // The pointer form.
        let ptr: Value = toml::from_str("k = { file = \"body.md\" }").unwrap();
        assert_eq!(as_file_pointer(ptr.get("k").unwrap()), Some("body.md"));

        // A literal string is not a pointer.
        assert_eq!(as_file_pointer(&Value::String("inline".into())), None);

        // A table with `file` PLUS another key stays opaque (not a pointer).
        let multi: Value = toml::from_str("k = { file = \"x\", extra = 1 }").unwrap();
        assert_eq!(as_file_pointer(multi.get("k").unwrap()), None);

        // A table whose sole key isn't `file` is opaque.
        let other: Value = toml::from_str("k = { path = \"x\" }").unwrap();
        assert_eq!(as_file_pointer(other.get("k").unwrap()), None);

        // `file` present but not a string → not a pointer.
        let nonstr: Value = toml::from_str("k = { file = 7 }").unwrap();
        assert_eq!(as_file_pointer(nonstr.get("k").unwrap()), None);
    }

    /// A profile overlays `[strings]` like any section (leaf-replace): the
    /// profile's key wins, the parent's untouched string survives.
    // [unit->REQ-MANIFEST-3]
    #[test]

assert!(set_string_at(&mut t, "a..b", Value::String("z".into())).is_err());

+    /// `as_file_pointer` recognizes a pointer ONLY for a table with EXACTLY one

+    /// key `file` (string) — the disambiguation that keeps every other table

+    /// shape an opaque nested tree (back-compat). (REQ-MANIFEST-5)

+    fn file_pointer_recognized_only_for_single_file_key_table() {

+        let ptr: Value = toml::from_str("k = { file = \"body.md\" }").unwrap();

+        assert_eq!(as_file_pointer(ptr.get("k").unwrap()), Some("body.md"));

+        // A literal string is not a pointer.

+        assert_eq!(as_file_pointer(&Value::String("inline".into())), None);

+        // A table with `file` PLUS another key stays opaque (not a pointer).

+        let multi: Value = toml::from_str("k = { file = \"x\", extra = 1 }").unwrap();

+        assert_eq!(as_file_pointer(multi.get("k").unwrap()), None);

+        // A table whose sole key isn't `file` is opaque.

+        let other: Value = toml::from_str("k = { path = \"x\" }").unwrap();

+        assert_eq!(as_file_pointer(other.get("k").unwrap()), None);

+        // `file` present but not a string → not a pointer.

+        let nonstr: Value = toml::from_str("k = { file = 7 }").unwrap();

+        assert_eq!(as_file_pointer(nonstr.get("k").unwrap()), None);


proceed with W1.5 next steps. first verify the plan with doyle


Now the registry integration tests. Check the `HARNESS_COPY` const shape (required harness fields):


712:    const HARNESS_COPY: &str = r#"
723:    const SHELL_COPY_PROFILED: &str = r#"
739:    const SHELL_DELEGATED: &str = r#"
770:        let harness_src = seed_source(tmp.path(), "h-src", HARNESS_COPY);
868:        let src = seed_source(tmp, "ms-src", SHELL_COPY_PROFILED);
937:        let src = seed_source(tmp.path(), "ms-src2", SHELL_COPY_PROFILED);


708	#[cfg(test)]
709	mod tests {
710	    use super::*;
711
712	    const HARNESS_COPY: &str = r#"
713	[adapter]
714	name = "mock-harness"
715	kind = "harness"
716	version = "1.0.0"
717	min_spt_core_version = "0.0.1"
718	hostable_types = ["LiveAgent"]
719	"#;
720
721	    // A copy-mode (avenue-less) shell adapter carrying one shipped profile that
722	    // tightens the floor — the base for local-profile tests.
723	    const SHELL_COPY_PROFILED: &str = r#"
724	[adapter]
725	name = "mock-shell"
726	kind = "shell"
727	version = "1.0.0"
728	min_spt_core_version = "0.0.1"
729
730	[shell]
731	spawn = "mock --link {link_token}"
732	require_approval = "remembered"
733
734	[profiles.locked]
735	[profiles.locked.shell]
736	require_approval = "always"
737	"#;
738
739	    const SHELL_DELEGATED: &str = r#"
740	[adapter]
741	name = "mock-shell"
742	kind = "shell"
743	version = "1.0.0"
744	min_spt_core_version = "0.0.1"
745
746	[update]
747	avenue = "delegated"

#[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"]
"#;

    // A copy-mode (avenue-less) shell adapter carrying one shipped profile that
    // tightens the floor — the base for local-profile tests.
    const SHELL_COPY_PROFILED: &str = r#"
[adapter]
name = "mock-shell"
kind = "shell"
version = "1.0.0"
min_spt_core_version = "0.0.1"

[shell]
spawn = "mock --link {link_token}"
require_approval = "remembered"

[profiles.locked]
[profiles.locked.shell]
require_approval = "always"
"#;

    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"


harness needs no `[session.self]` to validate. Read the test-module end to append:


999	    }
1000
1001	    // [unit->REQ-MANIFEST-3] get-string resolves through the overlay (a local
1002	    // profile's [strings] override the base; the merged view is what reads), and
1003	    // set-string edits a LOCAL profile only — a shipped name + a bare option are
1004	    // refused, a missing key reads None.
1005	    #[test]
1006	    fn strings_get_set_through_overlay() {
1007	        let tmp = tempfile::tempdir().unwrap();
1008	        // A shell adapter carrying base strings + a shipped profile.
1009	        let src_toml = "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1.0.0\"\n\
1010	             min_spt_core_version = \"0.0.1\"\n\n[shell]\nspawn = \"x\"\nrequire_approval = \"remembered\"\n\n\
1011	             [strings]\nbase = \"parent\"\n\n[profiles.locked]\n[profiles.locked.shell]\nrequire_approval = \"always\"\n";
1012	        let adapters = tmp.path().join("adapters");
1013	        let src = seed_source(tmp.path(), "ms-src", src_toml);
1014	        register(&adapters, &src, 1000).unwrap();
1015
1016	        // Bare option reads the base string; a missing key is None.
1017	        assert_eq!(
1018	            get_string(&adapters, "mock-shell", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
1019	            Some("parent".to_string())
1020	        );
1021	        assert!(get_string(&adapters, "mock-shell", "absent").unwrap().is_none());
1022
1023	        // set-string requires a LOCAL profile to exist first.
1024	        create_local_profile(&adapters, "mock-shell", "work", "").unwrap();
1025	        set_local_string(&adapters, "mock-shell", "work", "base", "overridden").unwrap();
1026	        set_local_string(&adapters, "mock-shell", "work", "hook.ctx", "deep").unwrap();
1027	        // The composite reads the overlaid value; the bare option is unchanged.
1028	        assert_eq!(
1029	            get_string(&adapters, "mock-shell:work", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
1030	            Some("overridden".to_string())
1031	        );
1032	        assert_eq!(
1033	            get_string(&adapters, "mock-shell:work", "hook.ctx").unwrap().and_then(|v| v.as_str().map(str::to_string)),
1034	            Some("deep".to_string())
1035	        );
1036	        assert_eq!(
1037	            get_string(&adapters, "mock-shell", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
1038	            Some("parent".to_string()),
1039	            "the bare parent is untouched"
1040	        );
1041
1042	        // A shipped profile is immutable; a missing local errs.
1043	        assert!(matches!(
1044	            set_local_string(&adapters, "mock-shell", "locked", "base", "x"),
1045	            Err(RegistryError::ShippedProfileImmutable(_))
1046	        ));
1047	        assert!(matches!(
1048	            set_local_string(&adapters, "mock-shell", "ghost", "base", "x"),
1049	            Err(RegistryError::ProfileNotFound(_))
1050	        ));
1051	    }
1052	}
1053

    }

    // [unit->REQ-MANIFEST-3] get-string resolves through the overlay (a local
    // profile's [strings] override the base; the merged view is what reads), and
    // set-string edits a LOCAL profile only — a shipped name + a bare option are
    // refused, a missing key reads None.
    #[test]
    fn strings_get_set_through_overlay() {
        let tmp = tempfile::tempdir().unwrap();
        // A shell adapter carrying base strings + a shipped profile.
        let src_toml = "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1.0.0\"\n\
             min_spt_core_version = \"0.0.1\"\n\n[shell]\nspawn = \"x\"\nrequire_approval = \"remembered\"\n\n\
             [strings]\nbase = \"parent\"\n\n[profiles.locked]\n[profiles.locked.shell]\nrequire_approval = \"always\"\n";
        let adapters = tmp.path().join("adapters");
        let src = seed_source(tmp.path(), "ms-src", src_toml);
        register(&adapters, &src, 1000).unwrap();

        // Bare option reads the base string; a missing key is None.
        assert_eq!(
            get_string(&adapters, "mock-shell", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("parent".to_string())
        );
        assert!(get_string(&adapters, "mock-shell", "absent").unwrap().is_none());

        // set-string requires a LOCAL profile to exist first.
        create_local_profile(&adapters, "mock-shell", "work", "").unwrap();
        set_local_string(&adapters, "mock-shell", "work", "base", "overridden").unwrap();
        set_local_string(&adapters, "mock-shell", "work", "hook.ctx", "deep").unwrap();
        // The composite reads the overlaid value; the bare option is unchanged.
        assert_eq!(
            get_string(&adapters, "mock-shell:work", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("overridden".to_string())
        );
        assert_eq!(
            get_string(&adapters, "mock-shell:work", "hook.ctx").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("deep".to_string())
        );
        assert_eq!(
            get_string(&adapters, "mock-shell", "base").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("parent".to_string()),
            "the bare parent is untouched"
        );

        // A shipped profile is immutable; a missing local errs.
        assert!(matches!(
            set_local_string(&adapters, "mock-shell", "locked", "base", "x"),
            Err(RegistryError::ShippedProfileImmutable(_))
        ));
        assert!(matches!(
            set_local_string(&adapters, "mock-shell", "ghost", "base", "x"),
            Err(RegistryError::ProfileNotFound(_))
        ));
    }
}


Append the file-backed-strings + fallback tests before the module close:


        assert!(matches!(
            set_local_string(&adapters, "mock-shell", "ghost", "base", "x"),
            Err(RegistryError::ProfileNotFound(_))
        ));
    }
}

        assert!(matches!(
            set_local_string(&adapters, "mock-shell", "ghost", "base", "x"),
            Err(RegistryError::ProfileNotFound(_))
        ));
    }

    // ───────────── File-backed [strings] (REQ-MANIFEST-5) ─────────────

    /// Seed a copy-mode harness adapter source whose `[strings]` carries `body`
    /// as a file pointer to `strings/<file_name>` (contents `body`) plus an
    /// inline literal; returns the `adapters/` dir after registration.
    fn seed_cc_with_file_string(tmp: &Path, file_rel: &str, body: &str) -> PathBuf {
        let toml_src = format!(
            "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1.0.0\"\n\
             min_spt_core_version = \"0.0.1\"\nhostable_types = [\"LiveAgent\"]\n\n\
             [strings]\ninline = \"literal\"\nbody = {{ file = \"{file_rel}\" }}\n"
        );
        let src = seed_source(tmp, "cc-src", &toml_src);
        let strings_dir = src.join("strings");
        std::fs::create_dir_all(strings_dir.join(Path::new(file_rel).parent().unwrap_or(Path::new(""))))
            .unwrap();
        std::fs::write(strings_dir.join(file_rel), body).unwrap();
        let adapters = tmp.join("adapters");
        register(&adapters, &src, 1000).unwrap();
        adapters
    }

    // [unit->REQ-MANIFEST-5] a `{file=…}` pointer resolves to the file's
    // contents at get-string time; a literal sibling reads as-is; the read is
    // LAZY — editing the file after register reflects with no re-register.
    #[test]
    fn file_backed_string_reads_contents_lazily() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "SKILL BODY");

        // Pointer → file contents; the inline literal still reads as itself.
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("SKILL BODY".to_string())
        );
        assert_eq!(
            get_string(&adapters, "mock-cc", "inline").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("literal".to_string())
        );

        // Lazy: edit the (copied) file; get-string reflects it without re-register.
        let copied = adapters.join("mock-cc").join("strings").join("skill.md");
        assert!(copied.exists(), "shipped strings/ copied into the held copy");
        std::fs::write(&copied, "EDITED BODY").unwrap();
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("EDITED BODY".to_string()),
            "lazy read reflects the live file edit"
        );
    }

    // [unit->REQ-MANIFEST-5] HAZARD-class containment: a file pointer that
    // escapes the strings/ dir (`..` traversal or an absolute path) is REFUSED
    // loudly at register — nothing is recorded. (Path-traversal hazard evidence.)
    #[test]
    fn string_file_pointer_escaping_the_dir_is_refused_at_register() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = tmp.path().join("adapters");

        // `..` traversal.
        let traverse = seed_source(
            tmp.path(),
            "esc1",
            "[adapter]\nname = \"esc1\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\nhostable_types = [\"LiveAgent\"]\n\n\
             [strings]\nx = { file = \"../../secret.txt\" }\n",
        );
        assert!(matches!(
            register(&adapters, &traverse, 1),
            Err(RegistryError::BadStringPointer(_))
        ));
        assert!(!adapters.join("esc1").exists(), "escaping pointer records nothing");

        // Absolute path.
        let abs_path = if cfg!(windows) { "C:\\\\windows\\\\system32\\\\x" } else { "/etc/passwd" };
        let absolute = seed_source(
            tmp.path(),
            "esc2",
            &format!(
                "[adapter]\nname = \"esc2\"\nkind = \"harness\"\nversion = \"1\"\n\
                 min_spt_core_version = \"0\"\nhostable_types = [\"LiveAgent\"]\n\n\
                 [strings]\nx = {{ file = \"{abs_path}\" }}\n"
            ),
        );
        assert!(matches!(
            register(&adapters, &absolute, 2),
            Err(RegistryError::BadStringPointer(_))
        ));
        assert!(!adapters.join("esc2").exists(), "absolute pointer records nothing");
    }

    // [unit->REQ-MANIFEST-5] a missing-at-READ file (deleted after a valid
    // register) skip-diagnostics: get-string returns Ok(None), never a panic or
    // error — mirrors the [digest] stance.
    #[test]
    fn missing_string_file_skips_at_read_without_error() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "BODY");
        std::fs::remove_file(adapters.join("mock-cc").join("strings").join("skill.md")).unwrap();
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap(),
            None,
            "a deleted string file skips to None (no crash, no error)"
        );
    }

    // [unit->REQ-MANIFEST-5] a LOCAL profile overlays a shipped file pointer by
    // leaf-replace: a literal override wins for the composite, the bare parent
    // still resolves the file.
    #[test]
    fn local_profile_overrides_a_shipped_file_pointer() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "SHIPPED BODY");
        create_local_profile(&adapters, "mock-cc", "work", "").unwrap();
        set_local_string(&adapters, "mock-cc", "work", "body", "LOCAL OVERRIDE").unwrap();

        assert_eq!(
            get_string(&adapters, "mock-cc:work", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("LOCAL OVERRIDE".to_string()),
            "local literal leaf-replaces the shipped pointer"
        );
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("SHIPPED BODY".to_string()),
            "the bare parent still resolves the shipped file"
        );
    }

    // [unit->REQ-MANIFEST-6] cross-adapter fallback TARGET addressing: a target
    // address resolves through the one composite resolver (resolve_option) in
    // both bare `<adapter>` and `<adapter>:<profile>` forms — the contract a
    // node-wide fallback (e.g. to `ccs` / `ccs:<profile>`) will address by. No
    // fallback SETTING is built this milestone (no consumer yet); this locks the
    // addressing contract only.
    #[test]
    fn cross_adapter_fallback_target_resolves_adapter_and_profile() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = registered_shell(tmp.path());

        // Bare adapter address resolves (the "ccs is its own adapter" form).
        assert!(
            resolve_option(&adapters, "mock-shell").is_ok(),
            "a bare fallback target resolves"
        );
        // adapter:profile address resolves (shipped profile).
        assert!(
            resolve_option(&adapters, "mock-shell:locked").is_ok(),
            "an <adapter>:<profile> fallback target resolves"
        );
        // A local profile target too.
        create_local_profile(&adapters, "mock-shell", "work", "[shell]\nrequire_approval = \"always\"\n")
            .unwrap();
        assert!(
            resolve_option(&adapters, "mock-shell:work").is_ok(),
            "an <adapter>:<local-profile> fallback target resolves"
        );
        // An unknown target is a clean NotRegistered (not a panic).
        assert!(matches!(
            resolve_option(&adapters, "ghost:work"),
            Err(RegistryError::NotRegistered(_))
        ));
    }
}


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

        assert!(matches!(
            set_local_string(&adapters, "mock-shell", "ghost", "base", "x"),
            Err(RegistryError::ProfileNotFound(_))
        ));
    }
}

        assert!(matches!(
            set_local_string(&adapters, "mock-shell", "ghost", "base", "x"),
            Err(RegistryError::ProfileNotFound(_))
        ));
    }

    // ───────────── File-backed [strings] (REQ-MANIFEST-5) ─────────────

    /// Seed a copy-mode harness adapter source whose `[strings]` carries `body`
    /// as a file pointer to `strings/<file_name>` (contents `body`) plus an
    /// inline literal; returns the `adapters/` dir after registration.
    fn seed_cc_with_file_string(tmp: &Path, file_rel: &str, body: &str) -> PathBuf {
        let toml_src = format!(
            "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1.0.0\"\n\
             min_spt_core_version = \"0.0.1\"\nhostable_types = [\"LiveAgent\"]\n\n\
             [strings]\ninline = \"literal\"\nbody = {{ file = \"{file_rel}\" }}\n"
        );
        let src = seed_source(tmp, "cc-src", &toml_src);
        let strings_dir = src.join("strings");
        std::fs::create_dir_all(strings_dir.join(Path::new(file_rel).parent().unwrap_or(Path::new(""))))
            .unwrap();
        std::fs::write(strings_dir.join(file_rel), body).unwrap();
        let adapters = tmp.join("adapters");
        register(&adapters, &src, 1000).unwrap();
        adapters
    }

    // [unit->REQ-MANIFEST-5] a `{file=…}` pointer resolves to the file's
    // contents at get-string time; a literal sibling reads as-is; the read is
    // LAZY — editing the file after register reflects with no re-register.
    #[test]
    fn file_backed_string_reads_contents_lazily() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "SKILL BODY");

        // Pointer → file contents; the inline literal still reads as itself.
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("SKILL BODY".to_string())
        );
        assert_eq!(
            get_string(&adapters, "mock-cc", "inline").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("literal".to_string())
        );

        // Lazy: edit the (copied) file; get-string reflects it without re-register.
        let copied = adapters.join("mock-cc").join("strings").join("skill.md");
        assert!(copied.exists(), "shipped strings/ copied into the held copy");
        std::fs::write(&copied, "EDITED BODY").unwrap();
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("EDITED BODY".to_string()),
            "lazy read reflects the live file edit"
        );
    }

    // [unit->REQ-MANIFEST-5] HAZARD-class containment: a file pointer that
    // escapes the strings/ dir (`..` traversal or an absolute path) is REFUSED
    // loudly at register — nothing is recorded. (Path-traversal hazard evidence.)
    #[test]
    fn string_file_pointer_escaping_the_dir_is_refused_at_register() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = tmp.path().join("adapters");

        // `..` traversal.
        let traverse = seed_source(
            tmp.path(),
            "esc1",
            "[adapter]\nname = \"esc1\"\nkind = \"harness\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\nhostable_types = [\"LiveAgent\"]\n\n\
             [strings]\nx = { file = \"../../secret.txt\" }\n",
        );
        assert!(matches!(
            register(&adapters, &traverse, 1),
            Err(RegistryError::BadStringPointer(_))
        ));
        assert!(!adapters.join("esc1").exists(), "escaping pointer records nothing");

        // Absolute path.
        let abs_path = if cfg!(windows) { "C:\\\\windows\\\\system32\\\\x" } else { "/etc/passwd" };
        let absolute = seed_source(
            tmp.path(),
            "esc2",
            &format!(
                "[adapter]\nname = \"esc2\"\nkind = \"harness\"\nversion = \"1\"\n\
                 min_spt_core_version = \"0\"\nhostable_types = [\"LiveAgent\"]\n\n\
                 [strings]\nx = {{ file = \"{abs_path}\" }}\n"
            ),
        );
        assert!(matches!(
            register(&adapters, &absolute, 2),
            Err(RegistryError::BadStringPointer(_))
        ));
        assert!(!adapters.join("esc2").exists(), "absolute pointer records nothing");
    }

    // [unit->REQ-MANIFEST-5] a missing-at-READ file (deleted after a valid
    // register) skip-diagnostics: get-string returns Ok(None), never a panic or
    // error — mirrors the [digest] stance.
    #[test]
    fn missing_string_file_skips_at_read_without_error() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "BODY");
        std::fs::remove_file(adapters.join("mock-cc").join("strings").join("skill.md")).unwrap();
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap(),
            None,
            "a deleted string file skips to None (no crash, no error)"
        );
    }

    // [unit->REQ-MANIFEST-5] a LOCAL profile overlays a shipped file pointer by
    // leaf-replace: a literal override wins for the composite, the bare parent
    // still resolves the file.
    #[test]
    fn local_profile_overrides_a_shipped_file_pointer() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "SHIPPED BODY");
        create_local_profile(&adapters, "mock-cc", "work", "").unwrap();
        set_local_string(&adapters, "mock-cc", "work", "body", "LOCAL OVERRIDE").unwrap();

        assert_eq!(
            get_string(&adapters, "mock-cc:work", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("LOCAL OVERRIDE".to_string()),
            "local literal leaf-replaces the shipped pointer"
        );
        assert_eq!(
            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),
            Some("SHIPPED BODY".to_string()),
            "the bare parent still resolves the shipped file"
        );
    }

    // [unit->REQ-MANIFEST-6] cross-adapter fallback TARGET addressing: a target
    // address resolves through the one composite resolver (resolve_option) in
    // both bare `<adapter>` and `<adapter>:<profile>` forms — the contract a
    // node-wide fallback (e.g. to `ccs` / `ccs:<profile>`) will address by. No
    // fallback SETTING is built this milestone (no consumer yet); this locks the
    // addressing contract only.
    #[test]
    fn cross_adapter_fallback_target_resolves_adapter_and_profile() {
        let tmp = tempfile::tempdir().unwrap();
        let adapters = registered_shell(tmp.path());

        // Bare adapter address resolves (the "ccs is its own adapter" form).
        assert!(
            resolve_option(&adapters, "mock-shell").is_ok(),
            "a bare fallback target resolves"
        );
        // adapter:profile address resolves (shipped profile).
        assert!(
            resolve_option(&adapters, "mock-shell:locked").is_ok(),
            "an <adapter>:<profile> fallback target resolves"
        );
        // A local profile target too.
        create_local_profile(&adapters, "mock-shell", "work", "[shell]\nrequire_approval = \"always\"\n")
            .unwrap();
        assert!(
            resolve_option(&adapters, "mock-shell:work").is_ok(),
            "an <adapter>:<local-profile> fallback target resolves"
        );
        // An unknown target is a clean NotRegistered (not a panic).
        assert!(matches!(
            resolve_option(&adapters, "ghost:work"),
            Err(RegistryError::NotRegistered(_))
        ));
    }
}

+    // ───────────── File-backed [strings] (REQ-MANIFEST-5) ─────────────

+    /// Seed a copy-mode harness adapter source whose `[strings]` carries `body`

+    /// as a file pointer to `strings/<file_name>` (contents `body`) plus an

+    /// inline literal; returns the `adapters/` dir after registration.

+    fn seed_cc_with_file_string(tmp: &Path, file_rel: &str, body: &str) -> PathBuf {

+            "[adapter]\nname = \"mock-cc\"\nkind = \"harness\"\nversion = \"1.0.0\"\n\

+             min_spt_core_version = \"0.0.1\"\nhostable_types = [\"LiveAgent\"]\n\n\

+             [strings]\ninline = \"literal\"\nbody = {{ file = \"{file_rel}\" }}\n"

+        let src = seed_source(tmp, "cc-src", &toml_src);

+        let strings_dir = src.join("strings");

+        std::fs::create_dir_all(strings_dir.join(Path::new(file_rel).parent().unwrap_or(Path::new(""))))

+        std::fs::write(strings_dir.join(file_rel), body).unwrap();

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

+        register(&adapters, &src, 1000).unwrap();

+    // [unit->REQ-MANIFEST-5] a `{file=…}` pointer resolves to the file's

+    // contents at get-string time; a literal sibling reads as-is; the read is

+    // LAZY — editing the file after register reflects with no re-register.

+    fn file_backed_string_reads_contents_lazily() {

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

+        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "SKILL BODY");

+        // Pointer → file contents; the inline literal still reads as itself.

+            get_string(&adapters, "mock-cc", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),

+            Some("SKILL BODY".to_string())

+            get_string(&adapters, "mock-cc", "inline").unwrap().and_then(|v| v.as_str().map(str::to_string)),

+            Some("literal".to_string())

+        // Lazy: edit the (copied) file; get-string reflects it without re-register.

+        let copied = adapters.join("mock-cc").join("strings").join("skill.md");

+        assert!(copied.exists(), "shipped strings/ copied into the held copy");

+        std::fs::write(&copied, "EDITED BODY").unwrap();

+            Some("EDITED BODY".to_string()),

+            "lazy read reflects the live file edit"

+    // [unit->REQ-MANIFEST-5] HAZARD-class containment: a file pointer that

+    // escapes the strings/ dir (`..` traversal or an absolute path) is REFUSED

+    // loudly at register — nothing is recorded. (Path-traversal hazard evidence.)

+    fn string_file_pointer_escaping_the_dir_is_refused_at_register() {

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

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

+            "[adapter]\nname = \"esc1\"\nkind = \"harness\"\nversion = \"1\"\n\

+             min_spt_core_version = \"0\"\nhostable_types = [\"LiveAgent\"]\n\n\

+             [strings]\nx = { file = \"../../secret.txt\" }\n",

+            register(&adapters, &traverse, 1),

+            Err(RegistryError::BadStringPointer(_))

+        assert!(!adapters.join("esc1").exists(), "escaping pointer records nothing");

+        let abs_path = if cfg!(windows) { "C:\\\\windows\\\\system32\\\\x" } else { "/etc/passwd" };

+                "[adapter]\nname = \"esc2\"\nkind = \"harness\"\nversion = \"1\"\n\

+                 [strings]\nx = {{ file = \"{abs_path}\" }}\n"

+            register(&adapters, &absolute, 2),

+        assert!(!adapters.join("esc2").exists(), "absolute pointer records nothing");

+    // [unit->REQ-MANIFEST-5] a missing-at-READ file (deleted after a valid

+    // register) skip-diagnostics: get-string returns Ok(None), never a panic or

+    // error — mirrors the [digest] stance.

+    fn missing_string_file_skips_at_read_without_error() {

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

+        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "BODY");

+        std::fs::remove_file(adapters.join("mock-cc").join("strings").join("skill.md")).unwrap();

+            get_string(&adapters, "mock-cc", "body").unwrap(),

+            "a deleted string file skips to None (no crash, no error)"

+    // [unit->REQ-MANIFEST-5] a LOCAL profile overlays a shipped file pointer by

+    // leaf-replace: a literal override wins for the composite, the bare parent

+    fn local_profile_overrides_a_shipped_file_pointer() {

+        let adapters = seed_cc_with_file_string(tmp.path(), "skill.md", "SHIPPED BODY");

+        create_local_profile(&adapters, "mock-cc", "work", "").unwrap();

+        set_local_string(&adapters, "mock-cc", "work", "body", "LOCAL OVERRIDE").unwrap();

+            get_string(&adapters, "mock-cc:work", "body").unwrap().and_then(|v| v.as_str().map(str::to_string)),

+            Some("LOCAL OVERRIDE".to_string()),

+            "local literal leaf-replaces the shipped pointer"

+            Some("SHIPPED BODY".to_string()),

+            "the bare parent still resolves the shipped file"

+    // [unit->REQ-MANIFEST-6] cross-adapter fallback TARGET addressing: a target

+    // address resolves through the one composite resolver (resolve_option) in

+    // both bare `<adapter>` and `<adapter>:<profile>` forms — the contract a

+    // node-wide fallback (e.g. to `ccs` / `ccs:<profile>`) will address by. No

+    // fallback SETTING is built this milestone (no consumer yet); this locks the

+    fn cross_adapter_fallback_target_resolves_adapter_and_profile() {

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

+        let adapters = registered_shell(tmp.path());

+        // Bare adapter address resolves (the "ccs is its own adapter" form).

+            resolve_option(&adapters, "mock-shell").is_ok(),

+            "a bare fallback target resolves"

+        // adapter:profile address resolves (shipped profile).

+            resolve_option(&adapters, "mock-shell:locked").is_ok(),

+            "an <adapter>:<profile> fallback target resolves"

+        create_local_profile(&adapters, "mock-shell", "work", "[shell]\nrequire_approval = \"always\"\n")

+            resolve_option(&adapters, "mock-shell:work").is_ok(),

+            "an <adapter>:<local-profile> fallback target resolves"

+        // An unknown target is a clean NotRegistered (not a panic).

+            resolve_option(&adapters, "ghost:work"),

+            Err(RegistryError::NotRegistered(_))


Run the spt-runtime tests:


Bash

cargo test -p spt-runtime 2>&1 | tail -35

Run spt-runtime tests


Command running in background with ID: bysgg8agf. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bysgg8agf.output. You will be notified when it completes. To check interim output, use Read on that file path.


While tests run, prep docs. Read CONTEXT.md:135-138 + locate the strings doc section (MANIFEST.md):


133
134	- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.
135	- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.
136	- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.
137	- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.
138	- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.
139	- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.
140

- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.
- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.
- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.
- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.
- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.
- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.


7:**SPT is not a harness.** The manifest declares only what *varies per harness*. Command templates are **opaque strings** — spt-core never parses out a model, tool list, or flag; the adapter writes the full command line and spt-core just runs it with substitution keys filled. Anything spt-core owns (sentinels, spool/registry/perch schemas, the event-block vocabulary, the version-trampoline mechanism, config knobs) is **not** in the manifest.


Check spt-runtime test results:


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


Tests still running. Locate where `[strings]` is documented (REQ-MANIFEST-3 doc tag) to place the new doc:


No matches found


Found 14 files
crates\spt-runtime\src\registry.rs
crates\spt-runtime\src\profile.rs
M12-W3-RULING.md
traceable-reqs.toml
M12-W3-PLAN.md
crates\spt\src\cli.rs
M12-PLAN.md
CHANGELOG.md
docs-site\src\cli\reference.md
crates\spt-runtime\src\manifest.rs
crates\spt-runtime\manifest.schema.json
ROADMAP.md
CONTEXT.md
M9-PLAN.md


76:**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):
80:**adapter strings** (ratified 2026-06-11, Gateway grill):
81:<!-- [doc->REQ-MANIFEST-3] -->
82:[Omitted long matching line]
83:_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry.


76	**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):
77	A named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local overlay file registered beside the adapter — user-authored, **surviving adapter updates** (the safe space for nodewise customization; the manifest file itself is adapter-owned and overwritten by updates). A local profile may not shadow a shipped profile's name (refused at registration). Profiles are never independently versioned (fork-drift, rejected). **Consent floors are tighten-only**: a profile may demand approval where the parent does not, never loosen a parent's `require_approval` floor — a loosening overlay is invalid at registration. CLI: `spt adapter create-profile` / `delete-profile` / `set-string` operate on **local** profiles only. Use cases: per-account variants, extra hook wiring, trust-narrowed shell profiles.
78	_Avoid_: "manifest fork", "child adapter", per-profile versioning.
79
80	**adapter strings** (ratified 2026-06-11, Gateway grill):
81	<!-- [doc->REQ-MANIFEST-3] -->
82	A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
83	_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry.
84
85	**keyword hints** (ratified 2026-06-12 — core milestone A):

**adapter profile** (ratified 2026-06-11, Gateway grill; future spt-core milestone — first beneficiaries `spt-claude-code` and the usbip shell):
A named **sparse overlay** on its parent adapter manifest. Merge semantics are **leaf-replace**: a profile key replaces the whole value at that path (arrays included — never spliced or appended). The merged result is a complete manifest, and the profile behaves as a distinct adapter option everywhere: canonical addressing is the composite **`<adapter>:<profile>`** (`claude-spt:work`, `spt-usbip-driver:hid-only`) in every place a bare `adapter_name` rides today (perch `info.json`, capability resolution, `api` invocations, `spt adapter list`); the bare name = the parent unmodified. **Two sources, one semantics:** a **shipped profile** is declared inside the parent manifest by the adapter dev and updates as one unit with it; a **local profile** is a node-local overlay file registered beside the adapter — user-authored, **surviving adapter updates** (the safe space for nodewise customization; the manifest file itself is adapter-owned and overwritten by updates). A local profile may not shadow a shipped profile's name (refused at registration). Profiles are never independently versioned (fork-drift, rejected). **Consent floors are tighten-only**: a profile may demand approval where the parent does not, never loosen a parent's `require_approval` floor — a loosening overlay is invalid at registration. CLI: `spt adapter create-profile` / `delete-profile` / `set-string` operate on **local** profiles only. Use cases: per-account variants, extra hook wiring, trust-narrowed shell profiles.
_Avoid_: "manifest fork", "child adapter", per-profile versioning.

**adapter strings** (ratified 2026-06-11, Gateway grill):
<!-- [doc->REQ-MANIFEST-3] -->
A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry.

**keyword hints** (ratified 2026-06-12 — core milestone A):


<task-notification>
<task-id>bysgg8agf</task-id>
<tool-use-id>toolu_01NuHnPPfR1V4X5JU22GYCyA</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bysgg8agf.output</output-file>
<status>completed</status>
<summary>Background command "Run spt-runtime tests" completed (exit code 0)</summary>
</task-notification>


Add the T3.1 file-backed doc to the adapter-strings section:


Edit

A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry.

A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
<!-- [doc->REQ-MANIFEST-5] -->
**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = "skill.md" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversal and absolute paths are refused at registration). A **local** profile's own file pointers resolve against the user-owned local-profile dir, not the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override survives updates; equivalently a local profile may just inline a literal. Pointers are **validated at registration** (fail-fast on an escaping/missing pointer — records nothing) and **read lazily** at `get-string` (live file edits reflect without re-register); a missing/unreadable file at read time **skip-diagnoses** (emits a diagnostic, returns "not set" — never a silent drop or hard error, mirroring `[digest]`).
_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).


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

A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry.

A `[strings]` manifest section — an adapter-authored JSON/TOML KV tree, dot-path-readable by anything on the node via `spt adapter get-string <adapter-option> <key.path>` (e.g. a harness hook fetching per-profile `additionalContext` — one hook script serves every profile, only the data differs). Resolution rides the **same leaf-replace profile overlay** as the rest of the manifest: a shipped or local profile may override base strings; `get-string` returns the merged view for the named adapter option. **Strings are data only** — nothing in spt-core ever executes a string (command templates live in manifest sections behind registration, never in the KV). Node-local like the registration itself; no cross-node sync. `set-string` is sugar that edits a **local** profile's `[strings]` (never adapter-shipped files).
<!-- [doc->REQ-MANIFEST-5] -->
**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = "skill.md" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversal and absolute paths are refused at registration). A **local** profile's own file pointers resolve against the user-owned local-profile dir, not the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override survives updates; equivalently a local profile may just inline a literal. Pointers are **validated at registration** (fail-fast on an escaping/missing pointer — records nothing) and **read lazily** at `get-string` (live file edits reflect without re-register); a missing/unreadable file at read time **skip-diagnoses** (emits a diagnostic, returns "not set" — never a silent drop or hard error, mirroring `[digest]`).
_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).

**adapter strings** (ratified 2026-06-11, Gateway grill):

-_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry.

+**File-backed strings** (M12-W3): a `[strings]` value MAY be a **file pointer** instead of an inline literal — a value-position table with **exactly one** key `file`: `skill = { file = "skill.md" }`. `get-string` resolves it to the file's **contents** (so large bodies — skill-instructions, hint text — stay out of the manifest). The exactly-one-key rule is the disambiguation: any other table shape stays an opaque nested strings tree (existing trees untouched), and `{ file = … }` is reserved as the pointer form (it can't double as inline data). Files live in the adapter's per-adapter aux dir **`adapters/<adapter>/strings/`** (sibling of `profiles/`), referenced by a path relative to it that **must stay inside that dir** (HAZARD-class containment: `..` traversal and absolute paths are refused at registration). A **local** profile's own file pointers resolve against the user-owned local-profile dir, not the adapter-shipped `strings/` (which adapter updates overwrite) — so a local override survives updates; equivalently a local profile may just inline a literal. Pointers are **validated at registration** (fail-fast on an escaping/missing pointer — records nothing) and **read lazily** at `get-string` (live file edits reflect without re-register); a missing/unreadable file at read time **skip-diagnoses** (emits a diagnostic, returns "not set" — never a silent drop or hard error, mirroring `[digest]`).

+_Avoid_: treating strings as config knobs for spt-core itself (those are global settings); "adapter KV store" as a separate registry; putting user files in the adapter-shipped `strings/` dir (clobbered by updates — use a local profile).

**keyword hints** (ratified 2026-06-12 — core milestone A):


Prompt:
<task-notification>
<task-id>bysgg8agf</task-id>
<tool-use-id>toolu_01NuHnPPfR1V4X5JU22GYCyA</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bysgg8agf.output</output-file>
<status>completed</status>
<summary>Background command "Run spt-runtime tests" completed (exit code 0)</summary>
</task-notification>


1	test profile::tests::leaf_replace_contract ... ok
2	test manifest::tests::round_trips_through_toml ... ok
3	test manifest::tests::manifest_schema_shape ... ok
4	test manifest::tests::checked_in_schema_is_current ... ok
5	test registry::tests::invalid_manifest_records_nothing ... ok
6	test manifest::tests::hint_keyword_matching ... ok
7	test runtime::tests::fills_known_placeholders ... ok
8	test runtime::tests::missing_key_errors_before_spawn ... ok
9	test registry::tests::string_file_pointer_escaping_the_dir_is_refused_at_register ... ok
10	test runtime::tests::tokenize_honors_double_quotes ... ok
11	test runtime::tests::unknown_role_errors ... ok
12	test registry::tests::create_local_profile_guards ... ok
13	test runtime::tests::spawn_session_detached_returns_pid ... ok
14	test registry::tests::missing_string_file_skips_at_read_without_error ... ok
15	test registry::tests::delete_local_profile_rules ... ok
16	test registry::tests::resolve_option_in_matches_disk ... ok
17	test registry::tests::cross_adapter_fallback_target_resolves_adapter_and_profile ... ok
18	test registry::tests::register_copy_vs_pointer_by_avenue ... ok
19	test registry::tests::resolve_option_bare_shipped_and_local ... ok
20	test registry::tests::file_backed_string_reads_contents_lazily ... ok
21	test registry::tests::deregister_is_soft_and_readd_reactivates ... ok
22	test registry::tests::local_profile_survives_readd ... ok
23	test registry::tests::local_profile_overrides_a_shipped_file_pointer ... ok
24	test registry::tests::strings_get_set_through_overlay ... ok
25	test runtime::tests::bounded_run_captures_success ... ok
26	test runtime::tests::bounded_run_kills_on_timeout ... ok
27
28	test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.34s
29
30	   Doc-tests spt_runtime
31
32	running 0 tests
33
34	test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
35
36

test profile::tests::leaf_replace_contract ... ok
test manifest::tests::round_trips_through_toml ... ok
test manifest::tests::manifest_schema_shape ... ok
test manifest::tests::checked_in_schema_is_current ... ok
test registry::tests::invalid_manifest_records_nothing ... ok
test manifest::tests::hint_keyword_matching ... ok
test runtime::tests::fills_known_placeholders ... ok
test runtime::tests::missing_key_errors_before_spawn ... ok
test registry::tests::string_file_pointer_escaping_the_dir_is_refused_at_register ... ok
test runtime::tests::tokenize_honors_double_quotes ... ok
test runtime::tests::unknown_role_errors ... ok
test registry::tests::create_local_profile_guards ... ok
test runtime::tests::spawn_session_detached_returns_pid ... ok
test registry::tests::missing_string_file_skips_at_read_without_error ... ok
test registry::tests::delete_local_profile_rules ... ok
test registry::tests::resolve_option_in_matches_disk ... ok
test registry::tests::cross_adapter_fallback_target_resolves_adapter_and_profile ... ok
test registry::tests::register_copy_vs_pointer_by_avenue ... ok
test registry::tests::resolve_option_bare_shipped_and_local ... ok
test registry::tests::file_backed_string_reads_contents_lazily ... ok
test registry::tests::deregister_is_soft_and_readd_reactivates ... ok
test registry::tests::local_profile_survives_readd ... ok
test registry::tests::local_profile_overrides_a_shipped_file_pointer ... ok
test registry::tests::strings_get_set_through_overlay ... ok
test runtime::tests::bounded_run_captures_success ... ok
test runtime::tests::bounded_run_kills_on_timeout ... ok

test result: ok. 49 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.34s

   Doc-tests spt_runtime

running 0 tests

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


All 49 pass (incl. all 6 new tests; `checked_in_schema_is_current` ok → no schema regen needed). Now T3.2 doc — reconcile CONTEXT.md:137:


- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.

- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.


- **Command templates are opaque.** spt-core never parses out a model/tool/flag — the adapter writes the whole command line; spt-core fills substitution keys and runs it.

- **Hook output capability is declared per harness-event** (`can_inject`). CC's Stop hook cannot inject context — that single fact drives the echo-gate sentinel + relay fallback. The manifest expresses it so spt-core knows when to fall back.

- **Env injection is asymmetric** (file-bridge-only-when-not-launcher, applied to env): spt-hosted sessions inherit env from the broker that spawns them; harness-hosted sessions need the harness's declared env channel. With `spt` on PATH the env table is small.

-- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs`, now its own adapter — not a binary-swap). Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.

+- **Cross-adapter fallback** is a **node-wide setting**, not a manifest field: if a Psyche/echo invocation under one adapter is rate-limited, spt-core falls back to another adapter (e.g. `ccs` — its own adapter, not a binary-swap). <!-- [doc->REQ-MANIFEST-6] --> A fallback **target is addressed as `<adapter>:<profile>`** (not just a bare adapter_name) and resolves through the one composite-addressing resolver (`registry::resolve_option`), so a fallback may select a shipped or local profile (`ccs`, `ccs:<profile>`) exactly as any other adapter-option read site does. *Contract only at M12-W3 — the addressing resolves; the node-wide setting + its rate-limit invocation belong to the consuming milestone (no reader exists yet, so no config field is added).* Adapter-agnostic IDs make this safe; an endpoint's Psyche may run under a fallback adapter temporarily.

- **Config knobs** (pulse period, echo-commune window, route-guard window, daily refresh) are spt-core **global settings** with optional **per-endpoint override** — never per-adapter.

- **Event-block vocabulary and file-drop filenames are fixed spt-core constants** (documented for adapter authors), not manifest-configurable.


Now traceable check for the two new REQs, then gate sweep. Run traceable first:


Bash

cd "C:\Users\decid\Documents\projects\spt-core"; traceable-reqs check >/tmp/tr.txt 2>&1; echo "TRACEABLE_EXIT=$?"; grep -iE "MANIFEST-5|MANIFEST-6" /tmp/tr.txt

traceable check for MANIFEST-5/6


TRACEABLE_EXIT=0
  [OK] REQ-MANIFEST-5  required: [doc, impl, unit]  stages: +doc +impl +unit -int
  [OK] REQ-MANIFEST-6  required: [doc, unit]  stages: +doc -impl +unit -int


Both **[OK]**, traceable EXIT=0. Full gate sweep — suite (runtime + dependents) real exit + clippy:


cd "C:\Users\decid\Documents\projects\spt-core"; echo "=== SUITE ==="; cargo test -p spt-runtime -p spt-daemon -p spt -p spt-term 2>&1 | tee /tmp/w3suite.txt >/dev/null; echo "SUITE_EXIT=${PIPESTATUS[0]}"; grep -oE "[0-9]+ passed" /tmp/w3suite.txt | awk '{s+=$1} END{print s" passed total"}'; grep -E "test result: FAILED|[1-9][0-9]* failed" /tmp/w3suite.txt || echo "NO FAILURES"; echo "=== CLIPPY ==="; cargo clippy -p spt-runtime -p spt-daemon -p spt -p spt-term --all-targets -- -D warnings 2>&1 | tail -4; echo "CLIPPY_EXIT=${PIPESTATUS[0]}"

W3 gate sweep: suite + clippy


Command running in background with ID: bpt2pve61. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bpt2pve61.output. You will be notified when it completes. To check interim output, use Read on that file path.


While the sweep runs, update memory (W3 built):


Edit

**NOW: W3 in DESIGN-CHECK with doyle (build HELD).**

**W3 BUILT (todlando 2026-06-14) — sweep running, then gate-ping doyle.** doyle design ruling = `M12-W3-RULING.md` (all 4 Qs answered). REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered + [OK] traceable EXIT=0. T3.1: `profile::as_file_pointer` (table with EXACTLY one key `file` = pointer, else opaque — back-compat); `[strings]` value `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copies it at register); HAZARD containment (reject `..`/absolute, refuse loud at register via RegistryError::BadStringPointer); validate-at-register + lazy-read + skip-diag (eprintln STRINGS_FILE_SKIP, returns None) on missing-at-read; local-profile pointers resolve to user-owned profiles dir (update-safe). T3.2: resolve_option ALREADY adapter:profile-aware → contract-lock unit only + CONTEXT.md:137 reconcile, NO config field (doyle escalation clause if spt-claude-code blocked). spt-runtime 49/0 (6 new: single-key detector, escape-hazard, lazy-read, missing-skip, local-override, fallback-resolve). manifest.schema.json unchanged (strings opaque Value — schema tests green). UNCOMMITTED.
**(superseded design-check note below)** W3 was in DESIGN-CHECK with doyle (now ruled + built).


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\m12-w1-progress.md has been updated successfully. (file state is current in your context — no need to Read it back)

**W3 BUILT (todlando 2026-06-14) — sweep running, then gate-ping doyle.** doyle design ruling = `M12-W3-RULING.md` (all 4 Qs answered). REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered + [OK] traceable EXIT=0. T3.1: `profile::as_file_pointer` (table with EXACTLY one key `file` = pointer, else opaque — back-compat); `[strings]` value `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copies it at register); HAZARD containment (reject `..`/absolute, refuse loud at register via RegistryError::BadStringPointer); validate-at-register + lazy-read + skip-diag (eprintln STRINGS_FILE_SKIP, returns None) on missing-at-read; local-profile pointers resolve to user-owned profiles dir (update-safe). T3.2: resolve_option ALREADY adapter:profile-aware → contract-lock unit only + CONTEXT.md:137 reconcile, NO config field (doyle escalation clause if spt-claude-code blocked). spt-runtime 49/0 (6 new: single-key detector, escape-hazard, lazy-read, missing-skip, local-override, fallback-resolve). manifest.schema.json unchanged (strings opaque Value — schema tests green). UNCOMMITTED.
**(superseded design-check note below)** W3 was in DESIGN-CHECK with doyle (now ruled + built).

---
name: m12-w1-progress
description: "M12 Wave 1 (spt endpoint run + spt rc) build state, decisions, and the self-dial-dead blocker"
metadata:
  node_type: memory
  type: project
  originSessionId: 1ae6a063-4622-4f5b-86b8-b4506d12ccfb
---

M12 W1 (spt-hosted harness bringup + `spt rc` PTY attach) — doyle handed execution to todlando 2026-06-14. Full spec: `M12-PLAN.md` + `M12-W1-PLAN.md` (durable build plan, resumable).

**Built + green:**
- Daemon `spt-daemon/src/harnesshost.rs`: `prepare_harness_spawn` (validate kind=harness + fill `[session.self]` template) + `launch_harness_brokered_in` (spawn into broker PTY via `spawn_session_pid`, mirrors `shellhost::launch_shell_brokered_in`) + `mint_session_id`. 3 unit tests.
- `spt/src/rc.rs`: operator attach pump (single-Brain main thread + stdin reader thread→mpsc, `cold_start_pump` Split carrier for bounded reads, seq-dedup, ctrl-b detach state machine). 3 unit tests.
- cli.rs: `EndpointCmd::Run` + `cmd_endpoint_run`; top-level `Cmd::Rc`; crossterm dep added.
- REQs registered: REQ-HOST-RUN-1 (spawn), REQ-RC-1 (attach), REQ-HOST-RUN-2 (project cwd — doyle-mandated, must land before M12 gate; W1 ships broker-inherited cwd as proof-shortcut only).

**KEY DECISIONS (doyle-validated):** spawn-first (harness self-registers its perch on bind via `api/startup.rs establish_perch`, no daemon pre-register); id rides argv via `{id}` template (NO SpawnReq wire change); one-pump invariant = transport swappable under one `serve_attach` pump.

**BLOCKER RESOLVED (doyle ruling 2026-06-14):** loopback self-dial IMPOSSIBLE (iroh refuses `net_dial(own_addr)`, pinned green test). doyle picked **B1 (broker in-process loopback-conn — local rides byte-identical pump+serve path, covered by cross-node tests; B2's pump-branch rejected as the lesser-tested local path warned against), scoped as a tight W1.5** (NOT in W1). Rationale: cross-node attach + endpoint-run bringup + pump are built+green → gate W1 NOW (bringup verifiable via cross-node attach to the spawned PTY); B1 is a sensitive broker-layer addition (6.7 zone) deserving its own focused task+gate; local lands W1.5 before M12 close. One-pump assertion (both transports → one pump) kept in W1.5.

**REQ-RC-1 split:** cross-node int evidence at W1 gate; local int evidence at W1.5 (per-wave activation).

**W1 GATE: PASS (doyle 2026-06-14).** Independently verified: traceable-reqs check reproduced (196 complete / 0 findings; REQ-HOST-RUN-1 + REQ-RC-1 impl+unit+int, REQ-HOST-RUN-2 []); int test `attach.rs:217` inspected — real `launch_harness_brokered_in` spawns [session.self] into broker-A PTY, B attaches cross-node, HOSTED_OK round-trips, detach→Detached+survives, origin=handshake-identity (KH 7.5); self-dial-refused pin genuine. Trusted reported full-suite (46 bins/0) + clippy -D. **UNCOMMITTED** (awaiting operator).

**W1.5 GATE PASS (doyle 2026-06-14, independently verified — read shared helper + both int tests + reproduced trace).** B1 broker loopback-conn done (design in `M12-W1.5-PLAN.md` "Crystallized design"). Transport seam = `tokio::io::duplex` (ONE full-duplex pair wires a whole operator↔peer stream); only the leaf send/recv become enums (`SendHalf`/`RecvHalf`), so StreamLog/subscribe/send_stream/read-pump are byte-identical across QUIC + loopback. `nethost::dial_loopback` = reused SINGLETON, `remote_id_hex` = own-node id; `open_stream` on a `ConnKind::Loopback` mints the cross-wired PAIR (operator row + peer row); peer row (initiated_locally=false, own origin) → dispatch loop → serve_attach. Explicit `net-dial-loopback` frame (brain.net_dial_loopback + broker.dispatch_net_dial_loopback) — net_dial untouched. access same-node short-circuit (access.rs:89) Allows own-node → NO access change. rc.rs ONE self-dial line swapped → net_dial_loopback. ONE-PUMP PROOF by construction: shared `attach_drive_detach` helper called by BOTH cross-node `remote_attach_drives_a_real_pty_cross_daemon` AND new `local_attach_via_loopback_conn_rides_the_same_pump` — diverge only in conn acquisition + expected_origin. EVIDENCE: attach int 6/6 + new nethost unit `loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair`; full spt+spt-daemon suite **498 passed/0 failed**; clippy -D EXIT=0; traceable EXIT=0 (REQ-RC-1 [OK] impl+unit+int). **UNCOMMITTED** (operator-gated, m12-w1-bringup-rc).

**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).

**NOW: W3 in DESIGN-CHECK with doyle (build HELD).** Operator said CONTINUE; doyle ordered W3 first (unblocks spt-claude-code). Plan = `M12-W3-PLAN.md`. W3 = T3.1 file-backed `[strings]` (entry points to a FILE; aux storage location) + T3.2 cross-adapter fallback targets `<adapter>:<profile>`. KEY FINDINGS: no per-adapter aux file location exists (only profiles/); **no node-wide cross-adapter fallback SETTING implemented** (CONTEXT.md:137 only describes it) → T3.2 is a SCOPE FORK ringed to doyle (recommend contract-only resolution + CONTEXT reconcile, defer fallback-invocation wiring). Proposals pending doyle: inline-table `key={file=".."}` syntax, `adapters/<adapter>/strings/` aux dir, validate-at-register + lazy-read + skip-diag, new REQ-MANIFEST-5. Remaining after W3: W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

-**NOW: W3 in DESIGN-CHECK with doyle (build HELD).** Operator said CONTINUE; doyle ordered W3 first (unblocks spt-claude-code). Plan = `M12-W3-PLAN.md`. W3 = T3.1 file-backed `[strings]` (entry points to a FILE; aux storage location) + T3.2 cross-adapter fallback targets `<adapter>:<profile>`. KEY FINDINGS: no per-adapter aux file location exists (only profiles/); **no node-wide cross-adapter fallback SETTING implemented** (CONTEXT.md:137 only describes it) → T3.2 is a SCOPE FORK ringed to doyle (recommend contract-only resolution + CONTEXT reconcile, defer fallback-invocation wiring). Proposals pending doyle: inline-table `key={file=".."}` syntax, `adapters/<adapter>/strings/` aux dir, validate-at-register + lazy-read + skip-diag, new REQ-MANIFEST-5. Remaining after W3: W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

+**W3 BUILT (todlando 2026-06-14) — sweep running, then gate-ping doyle.** doyle design ruling = `M12-W3-RULING.md` (all 4 Qs answered). REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered + [OK] traceable EXIT=0. T3.1: `profile::as_file_pointer` (table with EXACTLY one key `file` = pointer, else opaque — back-compat); `[strings]` value `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copies it at register); HAZARD containment (reject `..`/absolute, refuse loud at register via RegistryError::BadStringPointer); validate-at-register + lazy-read + skip-diag (eprintln STRINGS_FILE_SKIP, returns None) on missing-at-read; local-profile pointers resolve to user-owned profiles dir (update-safe). T3.2: resolve_option ALREADY adapter:profile-aware → contract-lock unit only + CONTEXT.md:137 reconcile, NO config field (doyle escalation clause if spt-claude-code blocked). spt-runtime 49/0 (6 new: single-key detector, escape-hazard, lazy-read, missing-skip, local-override, fallback-resolve). manifest.schema.json unchanged (strings opaque Value — schema tests green). UNCOMMITTED.

+**(superseded design-check note below)** W3 was in DESIGN-CHECK with doyle (now ruled + built). Operator said CONTINUE; doyle ordered W3 first (unblocks spt-claude-code). Plan = `M12-W3-PLAN.md`. W3 = T3.1 file-backed `[strings]` (entry points to a FILE; aux storage location) + T3.2 cross-adapter fallback targets `<adapter>:<profile>`. KEY FINDINGS: no per-adapter aux file location exists (only profiles/); **no node-wide cross-adapter fallback SETTING implemented** (CONTEXT.md:137 only describes it) → T3.2 is a SCOPE FORK ringed to doyle (recommend contract-only resolution + CONTEXT reconcile, defer fallback-invocation wiring). Proposals pending doyle: inline-table `key={file=".."}` syntax, `adapters/<adapter>/strings/` aux dir, validate-at-register + lazy-read + skip-diag, new REQ-MANIFEST-5. Remaining after W3: W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).


1	=== SUITE ===
2

=== SUITE ===


Sweep running. Awaiting completion before pinging doyle for the W3 gate.


<task-notification>
<task-id>bpt2pve61</task-id>
<tool-use-id>toolu_018npnRLcX1aSpg68NEgsPLR</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bpt2pve61.output</output-file>
<status>completed</status>
<summary>Background command "W3 gate sweep: suite + clippy" completed (exit code 0)</summary>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


1	=== SUITE ===
2	SUITE_EXIT=0
3	576 passed total
4	NO FAILURES
5	=== CLIPPY ===
6	    Checking spt-live v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
7	    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
8	    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
9	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.84s
10	CLIPPY_EXIT=0
11

=== SUITE ===
SUITE_EXIT=0
576 passed total
NO FAILURES
=== CLIPPY ===
    Checking spt-live v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking spt-daemon v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.6.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.84s
CLIPPY_EXIT=0


W3 gate sweep all green: **SUITE 576/0, CLIPPY EXIT=0** + traceable EXIT=0 (MANIFEST-5 & 6 [OK]). Ping doyle for the W3 gate:


Bash

$OWL ring doyle todlando <<'EOF'
W3 SUITE-GREEN — gate verdict requested. Built per your full ruling (M12-W3-RULING.md).

T3.1 file-backed [strings] (REQ-MANIFEST-5 doc+impl+unit):
- profile::as_file_pointer = table with EXACTLY one key `file` (string) → pointer; any other shape stays opaque nested tree (back-compat).
- get_string resolves a pointer to file CONTENTS lazily; literal siblings read as-is.
- aux dir adapters/<adapter>/strings/ (Copy-mode copy_dir_all at register; Pointer reads live from source).
- HAZARD containment: contained_string_file rejects `..`/absolute; register REFUSES loud (RegistryError::BadStringPointer, records nothing); read-time skip-diag (eprintln STRINGS_FILE_SKIP → None, no crash/error).
- validate-at-register (base manifest + shipped profiles' strings) + lazy read.
- provenance: a LOCAL profile's own pointer resolves to the user-owned profiles dir (update-safe), not adapter-shipped strings/.

T3.2 (REQ-MANIFEST-6 doc+unit, contract-only): resolve_option already adapter:profile-aware → NO config field added (no consumer; escalation clause not triggered — spt-claude-code addressing is satisfied by resolution). Unit asserts bare + adapter:profile + local-profile targets resolve, unknown = NotRegistered. CONTEXT.md:137 reconciled.

5 LOAD-BEARING TESTS (your gate list), all green:
1. file_pointer_recognized_only_for_single_file_key_table (profile.rs)
2. string_file_pointer_escaping_the_dir_is_refused_at_register (..+absolute, hazard)
3. file_backed_string_reads_contents_lazily (edit-after-register reflects)
4. local_profile_overrides_a_shipped_file_pointer (leaf-replace pointer→literal)
5. cross_adapter_fallback_target_resolves_adapter_and_profile
+ missing_string_file_skips_at_read_without_error (skip-diag).

EVIDENCE: spt-runtime 49/0; full spt-runtime+spt-daemon+spt+spt-term suite 576 passed/0 failed; clippy -D EXIT=0; traceable EXIT=0 (MANIFEST-5 doc+impl+unit [OK], MANIFEST-6 doc+unit [OK]). manifest.schema.json UNCHANGED (strings stays opaque Value — schema-current test green). UNCOMMITTED on m12-w1-bringup-rc.

GATE PASS? Remaining M12: W2 picker → W4 subnet QR → W5 whoami.
EOF

Ring doyle for W3 gate


Command running in background with ID: b2uyw7mnv. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b2uyw7mnv.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>b2uyw7mnv</task-id>
<tool-use-id>toolu_019hW8cTng6BBdCcmjG4ZUY5</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b2uyw7mnv.output</output-file>
<status>completed</status>
<summary>Background command "Ring doyle for W3 gate" completed (exit code 0)</summary>
</task-notification>


Ring sent (verdict via poll). Tidy the memory file — the W3 section got duplicated. Read + clean:


28	**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.
29
30	**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).
31
32	**W3 BUILT (todlando 2026-06-14) — sweep running, then gate-ping doyle.** doyle design ruling = `M12-W3-RULING.md` (all 4 Qs answered). REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered + [OK] traceable EXIT=0. T3.1: `profile::as_file_pointer` (table with EXACTLY one key `file` = pointer, else opaque — back-compat); `[strings]` value `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copies it at register); HAZARD containment (reject `..`/absolute, refuse loud at register via RegistryError::BadStringPointer); validate-at-register + lazy-read + skip-diag (eprintln STRINGS_FILE_SKIP, returns None) on missing-at-read; local-profile pointers resolve to user-owned profiles dir (update-safe). T3.2: resolve_option ALREADY adapter:profile-aware → contract-lock unit only + CONTEXT.md:137 reconcile, NO config field (doyle escalation clause if spt-claude-code blocked). spt-runtime 49/0 (6 new: single-key detector, escape-hazard, lazy-read, missing-skip, local-override, fallback-resolve). manifest.schema.json unchanged (strings opaque Value — schema tests green). UNCOMMITTED.
33	**(superseded design-check note below)** W3 was in DESIGN-CHECK with doyle (now ruled + built). Operator said CONTINUE; doyle ordered W3 first (unblocks spt-claude-code). Plan = `M12-W3-PLAN.md`. W3 = T3.1 file-backed `[strings]` (entry points to a FILE; aux storage location) + T3.2 cross-adapter fallback targets `<adapter>:<profile>`. KEY FINDINGS: no per-adapter aux file location exists (only profiles/); **no node-wide cross-adapter fallback SETTING implemented** (CONTEXT.md:137 only describes it) → T3.2 is a SCOPE FORK ringed to doyle (recommend contract-only resolution + CONTEXT reconcile, defer fallback-invocation wiring). Proposals pending doyle: inline-table `key={file=".."}` syntax, `adapters/<adapter>/strings/` aux dir, validate-at-register + lazy-read + skip-diag, new REQ-MANIFEST-5. Remaining after W3: W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).
34

**REQ-HOST-RUN-2 GATE PASS (doyle 2026-06-14) — verified end-to-end.** doyle: "the confirmed-missing gating foundation is DONE + gated; the hard part of M12 is done." CONTINUE-vs-CHECKPOINT (W2-W5) routed to OPERATOR; HOLD building until operator relays. doyle suggested order if continue: **W3 first** (file-backed strings + adapter:profile fallback — most directly unblocks spt-claude-code) → then W2/W4/W5. Additive `SpawnReq.cwd: Option<String>` (serde default+skip_if_none, N-1 safe) → spt-term `PtySession::spawn_program_in(cwd)` (portable-pty CommandBuilder::cwd; `spawn_program` delegates None) → broker `dispatch_spawn` honors `req.cwd` → harnesshost `launch_harness_brokered_in(.., cwd: Option<&str>)` → cli `cmd_endpoint_run` derives PROJECT cwd from `std::env::current_dir()` (None-fallback, never aborts). Shells unchanged (cwd:None, out of scope). 17 SpawnReq literals patched +cwd:None. EVIDENCE: unit `spawn_program_in_lands_the_child_in_the_requested_cwd` (spt-term, unix-gated like capture test); int `broker_spawns_the_pty_child_in_the_requested_cwd` (CROSS-PLATFORM, passes Windows — wire→broker→PTY cwd e2e); full suite **527/0**; clippy EXIT=0; traceable EXIT=0 (REQ-HOST-RUN-2 [OK] impl+unit+int). UNCOMMITTED.

**COMMITTED 0c687fe (2026-06-14): W1.5 + REQ-HOST-RUN-2** on m12-w1-bringup-rc (operator chose commit-then-continue; 24 files, 668+/92-). W1.x bringup+rc foundation COMPLETE + gated (spt-hosted bringup + cross-node + LOCAL rc + project cwd).

**W3 BUILT (todlando 2026-06-14) — sweep running, then gate-ping doyle.** doyle design ruling = `M12-W3-RULING.md` (all 4 Qs answered). REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered + [OK] traceable EXIT=0. T3.1: `profile::as_file_pointer` (table with EXACTLY one key `file` = pointer, else opaque — back-compat); `[strings]` value `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copies it at register); HAZARD containment (reject `..`/absolute, refuse loud at register via RegistryError::BadStringPointer); validate-at-register + lazy-read + skip-diag (eprintln STRINGS_FILE_SKIP, returns None) on missing-at-read; local-profile pointers resolve to user-owned profiles dir (update-safe). T3.2: resolve_option ALREADY adapter:profile-aware → contract-lock unit only + CONTEXT.md:137 reconcile, NO config field (doyle escalation clause if spt-claude-code blocked). spt-runtime 49/0 (6 new: single-key detector, escape-hazard, lazy-read, missing-skip, local-override, fallback-resolve). manifest.schema.json unchanged (strings opaque Value — schema tests green). UNCOMMITTED.
**(superseded design-check note below)** W3 was in DESIGN-CHECK with doyle (now ruled + built). Operator said CONTINUE; doyle ordered W3 first (unblocks spt-claude-code). Plan = `M12-W3-PLAN.md`. W3 = T3.1 file-backed `[strings]` (entry points to a FILE; aux storage location) + T3.2 cross-adapter fallback targets `<adapter>:<profile>`. KEY FINDINGS: no per-adapter aux file location exists (only profiles/); **no node-wide cross-adapter fallback SETTING implemented** (CONTEXT.md:137 only describes it) → T3.2 is a SCOPE FORK ringed to doyle (recommend contract-only resolution + CONTEXT reconcile, defer fallback-invocation wiring). Proposals pending doyle: inline-table `key={file=".."}` syntax, `adapters/<adapter>/strings/` aux dir, validate-at-register + lazy-read + skip-diag, new REQ-MANIFEST-5. Remaining after W3: W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).


Prompt:
<task-notification>
<task-id>b2uyw7mnv</task-id>
<tool-use-id>toolu_019hW8cTng6BBdCcmjG4ZUY5</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\b2uyw7mnv.output</output-file>
<status>completed</status>
<summary>Background command "Ring doyle for W3 gate" completed (exit code 0)</summary>
</task-notification>


Clean up the duplicated/superseded W3 note + mark sweep-green:


Edit

**W3 BUILT (todlando 2026-06-14) — sweep running, then gate-ping doyle.** doyle design ruling = `M12-W3-RULING.md` (all 4 Qs answered). REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered + [OK] traceable EXIT=0. T3.1: `profile::as_file_pointer` (table with EXACTLY one key `file` = pointer, else opaque — back-compat); `[strings]` value `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copies it at register); HAZARD containment (reject `..`/absolute, refuse loud at register via RegistryError::BadStringPointer); validate-at-register + lazy-read + skip-diag (eprintln STRINGS_FILE_SKIP, returns None) on missing-at-read; local-profile pointers resolve to user-owned profiles dir (update-safe). T3.2: resolve_option ALREADY adapter:profile-aware → contract-lock unit only + CONTEXT.md:137 reconcile, NO config field (doyle escalation clause if spt-claude-code blocked). spt-runtime 49/0 (6 new: single-key detector, escape-hazard, lazy-read, missing-skip, local-override, fallback-resolve). manifest.schema.json unchanged (strings opaque Value — schema tests green). UNCOMMITTED.
**(superseded design-check note below)** W3 was in DESIGN-CHECK with doyle (now ruled + built). Operator said CONTINUE; doyle ordered W3 first (unblocks spt-claude-code). Plan = `M12-W3-PLAN.md`. W3 = T3.1 file-backed `[strings]` (entry points to a FILE; aux storage location) + T3.2 cross-adapter fallback targets `<adapter>:<profile>`. KEY FINDINGS: no per-adapter aux file location exists (only profiles/); **no node-wide cross-adapter fallback SETTING implemented** (CONTEXT.md:137 only describes it) → T3.2 is a SCOPE FORK ringed to doyle (recommend contract-only resolution + CONTEXT reconcile, defer fallback-invocation wiring). Proposals pending doyle: inline-table `key={file=".."}` syntax, `adapters/<adapter>/strings/` aux dir, validate-at-register + lazy-read + skip-diag, new REQ-MANIFEST-5. Remaining after W3: W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).

**W3 BUILT + SUITE-GREEN (todlando 2026-06-14) — gate pinged doyle, verdict pending.** doyle design ruling = `M12-W3-RULING.md`; plan = `M12-W3-PLAN.md`. REQ-MANIFEST-5 (T3.1 doc+impl+unit) + REQ-MANIFEST-6 (T3.2 doc+unit, contract-only) registered, traceable EXIT=0 both [OK]. **T3.1** file-backed `[strings]`: `profile::as_file_pointer` = table with EXACTLY one key `file` (else opaque — back-compat); `{file="rel"}` resolves to file contents lazily at get_string; aux dir `adapters/<adapter>/strings/` (Copy-mode copy_dir_all at register, Pointer reads live); HAZARD containment (reject `..`/absolute → register REFUSES loud via `RegistryError::BadStringPointer`, records nothing); read-time skip-diag (eprintln STRINGS_FILE_SKIP → None, never crash/error); local-profile pointers resolve to user-owned profiles dir (update-safe). **T3.2**: resolve_option ALREADY adapter:profile-aware → contract-lock unit + CONTEXT.md:137 reconcile, NO config field added (no consumer; escalation clause untriggered). EVIDENCE: spt-runtime 49/0 (6 new tests); full spt-runtime+spt-daemon+spt+spt-term suite **576/0**; clippy EXIT=0; traceable EXIT=0. manifest.schema.json UNCHANGED (strings opaque Value). UNCOMMITTED on m12-w1-bringup-rc.

**Remaining M12 after W3 gate:** W2 picker → W4 subnet QR → W5 whoami. Commit only when operator asks. See [[traceable-per-wave-activation]], [[adapter-glue-model-boundary]], [[spt-claude-code-next]] (perri waits on M12 gate).