You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Project-domain sync resolves its target harnesses by probing for {projectRoot}/.claude and {projectRoot}/.rovodev, so a harness is targeted only when the repository already contains its directory. Nothing establishes that precondition: those directories hold generated output, and a repository has no reason to commit one. Claude is targeted incidentally wherever a repository commits .claude/ harness config; Rovo generally is not targeted at all. When the probe misses, every project-domain artifact for that harness — skills, subagents, and ambient rulebooks alike — goes undelivered, and the run reports zero targeted harnesses rather than naming the one it skipped.
Home-domain detection is sound, since ~/.claude and ~/.rovodev are created by each harness's own installer. But nothing there is declarable either, so a stale harness directory cannot be overridden.
Context
resolveHarnessIds(options.harness, baseDir) is the single resolution point, consumed by sync, install, uninstall, status, and configure-hooks. Passing --harness <id> bypasses detection and is the only workaround today.
Every declaration type already carries the {use, drop} shape (typeDeclarationSchema()); accumulateType merges across the scope chain and root: true resets it, so a harnesses block needs no new grammar.
resolveScopeChain currently walks exactly two tiers under a single base directory, so the project and home domains never cross.
init's scaffold comment still reads "inlined into PROJECT.md", stale since #1088 moved ambient content to the machine-local hosts. It sits in a file this change already edits.
Confirmed downstream: williamthorsen/operations declares an ambient rulebook and receives nothing, because it contains neither harness directory. sync resolves the rulebook and reports success having written no file.
Proposed solution
Add harnesses: {use, drop} to codeassembly.yaml, resolved on a machine-wide chain:
Resolution order: the --harness flag, then the declaration chain (~/.agents/codeassembly.yaml, its .local sibling, then the project pair), then a fallback of the harnesses installed on the machine — detected under the home directory, not in the repository.
Three tiers separate three distinct claims: the machine states which harnesses are installed, the project states which it requires, and codeassembly.local.yaml lets an individual developer override either. A committed project tier therefore never asserts what an individual runs.
A key resolves on the chain its meaning implies.harnesses is machine-scoped and therefore crosses tiers. Artifact keys are domain-scoped and must not: a home declaration carrying collections: use: [all] would otherwise deploy the entire catalog into every repository's harness dirs. The two-chain rule belongs in the schema documentation, not left implicit in one key's behavior.
root: true discards lower-precedence contributions from its own domain. For every artifact key, whose chain lies wholly within one domain, this describes current behavior exactly. It acquires force only for harnesses, where it stops a committed project file from discarding the machine's declaration. drop still reaches across the boundary, so codeassembly.local.yaml can override a home-declared harness.
An unknown harness id fails the run, naming the file and the offending entry. A declaration resolving to an empty set is honored — sync targets nothing and says so — and is distinct from no declaration at all, which falls back.
Every run reports its targeting; on the fallback path it also names the key that pins it:
Targeting claude, rovo (detected in ~).
Declare `harnesses.use` in .agents/codeassembly.yaml to pin this.
It does not prompt: the command runs unattended in postinstall and CI, and init has no interactive machinery either.
The declaration governs both sync domains. install, uninstall, status, and configure-hooks are home-domain and keep detection.
Two scopes, one word
#963 renamed the artifact-level frontmatter key to supported-harnesses:, so the two are distinguishable on sight. They compose, and the composition needs stating once:
Lives in
Governs
Says
harnesses:
codeassembly.yaml
a sync run
target these harnesses
supported-harnesses:
artifact frontmatter
one artifact
this artifact only deploys to these
Targeting selects the harness set; artifact narrowing filters within it. A project targeting [claude, rovo] with a skill declaring supported-harnesses: [rovo] deploys that skill to Rovo alone.
Also in scope
Fix init's stale scaffold comment, which names a destination that stopped receiving ambient content in #1088.
Rejected
Hosting the declaration in preferences.yaml. Its project → global cascade fits a machine fact, but the file is shared across tooling — commit and PR title formats, worktree conventions, artifact paths — so a CodeAssembly-only key there inverts the ownership boundary. Keys CodeAssembly owns live in the file CodeAssembly owns.
Hosting it only in codeassembly.local.yaml. Per-checkout and git-ignored, so a machine-wide policy would need recreating in every worktree — four, for williamthorsen/operations alone.
Acceptance criteria
Must have
codeassembly.yaml accepts harnesses: {use, drop}, merged along its chain. An unknown harness id fails the run, naming the file and the offending entry.
The harnesses block resolves across the home and project tiers; artifact keys continue to resolve on the project tier alone.
root: true discards only its own domain's contributions, so a project tier cannot discard a home-declared harness. drop still crosses the boundary.
sync targeting resolves in the order: --harness flag, declaration, harnesses installed on the machine.
A repository declaring no harnesses receives project-domain artifacts for every harness installed on the machine, whatever directories the repository itself contains.
A declaration resolving to an empty set targets no harnesses, distinct from an absent declaration, which falls back.
sync --global honors the declaration on the same terms.
Every sync run, dry-run included, names its targeted harnesses and how targeting resolved; the fallback path also names the key that pins them.
sync and init remain non-interactive.
init's scaffolds name the current ambient destination rather than PROJECT.md, and surface the harnesses key.
Documentation covers the key, the two-chain rule, the domain-scoped root rule, the resolution order, and how targeting composes with artifact-level supported-harnesses:, naming which commands honor the declaration.
New and modified behavior in this change is covered by tests.
Problem
Project-domain
syncresolves its target harnesses by probing for{projectRoot}/.claudeand{projectRoot}/.rovodev, so a harness is targeted only when the repository already contains its directory. Nothing establishes that precondition: those directories hold generated output, and a repository has no reason to commit one. Claude is targeted incidentally wherever a repository commits.claude/harness config; Rovo generally is not targeted at all. When the probe misses, every project-domain artifact for that harness — skills, subagents, and ambient rulebooks alike — goes undelivered, and the run reports zero targeted harnesses rather than naming the one it skipped.Home-domain detection is sound, since
~/.claudeand~/.rovodevare created by each harness's own installer. But nothing there is declarable either, so a stale harness directory cannot be overridden.Context
resolveHarnessIds(options.harness, baseDir)is the single resolution point, consumed bysync,install,uninstall,status, andconfigure-hooks. Passing--harness <id>bypasses detection and is the only workaround today.Every declaration type already carries the
{use, drop}shape (typeDeclarationSchema());accumulateTypemerges across the scope chain androot: trueresets it, so aharnessesblock needs no new grammar.resolveScopeChaincurrently walks exactly two tiers under a single base directory, so the project and home domains never cross.init's scaffold comment still reads "inlined into PROJECT.md", stale since #1088 moved ambient content to the machine-local hosts. It sits in a file this change already edits.Confirmed downstream:
williamthorsen/operationsdeclares an ambient rulebook and receives nothing, because it contains neither harness directory.syncresolves the rulebook and reports success having written no file.Proposed solution
Add
harnesses: {use, drop}tocodeassembly.yaml, resolved on a machine-wide chain:Resolution order: the
--harnessflag, then the declaration chain (~/.agents/codeassembly.yaml, its.localsibling, then the project pair), then a fallback of the harnesses installed on the machine — detected under the home directory, not in the repository.Three tiers separate three distinct claims: the machine states which harnesses are installed, the project states which it requires, and
codeassembly.local.yamllets an individual developer override either. A committed project tier therefore never asserts what an individual runs.A key resolves on the chain its meaning implies.
harnessesis machine-scoped and therefore crosses tiers. Artifact keys are domain-scoped and must not: a home declaration carryingcollections: use: [all]would otherwise deploy the entire catalog into every repository's harness dirs. The two-chain rule belongs in the schema documentation, not left implicit in one key's behavior.root: truediscards lower-precedence contributions from its own domain. For every artifact key, whose chain lies wholly within one domain, this describes current behavior exactly. It acquires force only forharnesses, where it stops a committed project file from discarding the machine's declaration.dropstill reaches across the boundary, socodeassembly.local.yamlcan override a home-declared harness.An unknown harness id fails the run, naming the file and the offending entry. A declaration resolving to an empty set is honored — sync targets nothing and says so — and is distinct from no declaration at all, which falls back.
Every run reports its targeting; on the fallback path it also names the key that pins it:
It does not prompt: the command runs unattended in
postinstalland CI, andinithas no interactive machinery either.The declaration governs both sync domains.
install,uninstall,status, andconfigure-hooksare home-domain and keep detection.Two scopes, one word
#963 renamed the artifact-level frontmatter key to
supported-harnesses:, so the two are distinguishable on sight. They compose, and the composition needs stating once:harnesses:codeassembly.yamlsupported-harnesses:Targeting selects the harness set; artifact narrowing filters within it. A project targeting
[claude, rovo]with a skill declaringsupported-harnesses: [rovo]deploys that skill to Rovo alone.Also in scope
Fix
init's stale scaffold comment, which names a destination that stopped receiving ambient content in #1088.Rejected
preferences.yaml. Its project → global cascade fits a machine fact, but the file is shared across tooling — commit and PR title formats, worktree conventions, artifact paths — so a CodeAssembly-only key there inverts the ownership boundary. Keys CodeAssembly owns live in the file CodeAssembly owns.codeassembly.local.yaml. Per-checkout and git-ignored, so a machine-wide policy would need recreating in every worktree — four, forwilliamthorsen/operationsalone.Acceptance criteria
Must have
codeassembly.yamlacceptsharnesses: {use, drop}, merged along its chain. An unknown harness id fails the run, naming the file and the offending entry.harnessesblock resolves across the home and project tiers; artifact keys continue to resolve on the project tier alone.root: truediscards only its own domain's contributions, so a project tier cannot discard a home-declared harness.dropstill crosses the boundary.synctargeting resolves in the order:--harnessflag, declaration, harnesses installed on the machine.sync --globalhonors the declaration on the same terms.syncrun, dry-run included, names its targeted harnesses and how targeting resolved; the fallback path also names the key that pins them.syncandinitremain non-interactive.init's scaffolds name the current ambient destination rather thanPROJECT.md, and surface theharnesseskey.rootrule, the resolution order, and how targeting composes with artifact-levelsupported-harnesses:, naming which commands honor the declaration.