diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e3d26bb2..39256b70 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -5,14 +5,22 @@ on:
paths:
- 'dist/.claude/skills/**'
- 'dist/.codex/skills/**'
+ - 'dist/.qoder/skills/**'
+ - 'dist/.qwen/skills/**'
- 'cli/src/bin/gen_codex_skills.rs'
+ - 'cli/tests/qoder_skills_test.rs'
+ - 'cli/tests/qwen_skills_test.rs'
- '.github/workflows/ci.yml'
push:
branches: [main]
paths:
- 'dist/.claude/skills/**'
- 'dist/.codex/skills/**'
+ - 'dist/.qoder/skills/**'
+ - 'dist/.qwen/skills/**'
- 'cli/src/bin/gen_codex_skills.rs'
+ - 'cli/tests/qoder_skills_test.rs'
+ - 'cli/tests/qwen_skills_test.rs'
- '.github/workflows/ci.yml'
permissions:
@@ -41,3 +49,25 @@ jobs:
# produce from dist/.claude/skills/. Fix locally with:
# cd cli && cargo run --bin gen_codex_skills --features dev-tools
cargo run --quiet --bin gen_codex_skills --features dev-tools -- --check
+
+ skill-mirror-parity:
+ name: Qoder/Qwen skills mirror the Claude source
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - name: Install OpenSSL (Linux)
+ run: sudo apt-get update && sudo apt-get install -y libssl-dev pkg-config
+
+ - name: Verify .qoder/skills and .qwen/skills mirror .claude/skills
+ run: |
+ # Qoder and Qwen Code both parse the full Claude-format frontmatter,
+ # so their trees are byte-for-byte copies rather than generated
+ # variants. Fix locally with:
+ # rm -rf dist/.qoder/skills dist/.qwen/skills
+ # cp -a dist/.claude/skills dist/.qoder/skills
+ # cp -a dist/.claude/skills dist/.qwen/skills
+ cargo test -p straymark-cli --test qoder_skills_test --test qwen_skills_test
diff --git a/.straymark/07-ai-audit/agent-logs/AILOG-2026-08-05-003-qwen-code-agent-surface.md b/.straymark/07-ai-audit/agent-logs/AILOG-2026-08-05-003-qwen-code-agent-surface.md
new file mode 100644
index 00000000..4b534d2d
--- /dev/null
+++ b/.straymark/07-ai-audit/agent-logs/AILOG-2026-08-05-003-qwen-code-agent-surface.md
@@ -0,0 +1,148 @@
+---
+id: AILOG-2026-08-05-003
+title: Qwen Code agent surface (QWEN.md + .qwen/skills) and the update path that would have withheld it
+status: accepted
+created: 2026-08-05
+agent: claude-opus-5-v1.0
+confidence: high
+review_required: false
+risk_level: medium
+eu_ai_act_risk: not_applicable
+nist_genai_risks: []
+iso_42001_clause: []
+lines_changed: 445
+files_modified:
+ - dist/dist-manifest.yml
+ - dist/dist-templates/directives/QWEN.md
+ - dist/dist-templates/directives/AGENTS.md
+ - dist/.qwen/skills/
+ - dist/STRAYMARK.md
+ - dist/.straymark/00-governance/AGENT-RULES.md
+ - cli/src/main.rs
+ - cli/src/commands/install_skills.rs
+ - cli/src/commands/update_framework.rs
+ - cli/src/commands/remove.rs
+ - cli/src/commands/validate.rs
+ - cli/tests/qwen_skills_test.rs
+ - cli/tests/inject_test.rs
+ - cli/tests/architecture_skill_test.rs
+ - .github/workflows/ci.yml
+ - README.md
+ - docs/adopters/CLI-REFERENCE.md
+ - docs/adopters/ADOPTION-GUIDE.md
+ - CHANGELOG.md
+observability_scope: none
+tags: [multi-agent, qwen-code, qoder, directive-injection, update-path, ci, i18n]
+related:
+ - AIDEC-2026-08-05-001
+ - AILOG-2026-08-04-002
+---
+
+# AILOG: Qwen Code agent surface
+
+## Summary
+
+Qwen Code went from zero integration to a first-class surface — `QWEN.md`
+directive injection, 15 skills at `.qwen/skills/`, `install-skills --agent
+qwen`, `validate --agent qwen`, `remove` cleanup. Along the way, two defects
+that the review surfaced were fixed: `update-framework` never created
+newly-declared injection targets (so the new surface would have reached fresh
+installations only), and the Qoder documentation asserted the opposite of what
+the Qoder runtime does.
+
+## Context
+
+The question asked was narrow — *do the Qwen and Qoder CLIs get the same rules
+and configuration Claude Code gets, at `init` and at later `update`?* — and the
+answer split cleanly:
+
+- **Qoder**: yes for skills (`dist/.qoder/skills/`, #399) and yes for rules,
+ via `AGENTS.md`. Verified in the shipped application: it file-searches
+ `**/AGENTS.md` and adds each match as an instruction source.
+- **Qwen Code**: no, on both counts. Nothing referenced it anywhere in the
+ product; the only occurrences in the repo were prose inside the audit skills
+ warning auditors not to write `qwen-code` in the `auditor:` field.
+
+The severity of the Qwen gap depended on one empirical question — does Qwen
+Code read `AGENTS.md`? — so it was answered against the installed runtime
+rather than from memory. Its context-filename resolver ends in
+`["QWEN.md"] : Array.isArray(configured) ? configured : [configured]`: the
+default is `QWEN.md` alone. `AGENTS.md` appears exactly once in the bundle, in
+a system-prompt string enumerating files the agent must not modify without
+permission — recognized, never loaded. So Qwen Code users were receiving no
+governance at all, which is a strictly worse position than Codex or Qoder.
+
+Two further runtime facts settled the design (see [AIDEC-2026-08-05-001] for
+the alternatives): `Storage.getGlobalQwenDir()` resolves `$QWEN_HOME` →
+`$HOME/.qwen`, and skills resolve at **both** project and user scope with
+`allowed-tools` supported. The channel is therefore a byte-for-byte mirror of
+`.claude/`, exactly as Qoder's is.
+
+## Changes
+
+**Framework (`dist/`)**
+
+- `dist/.qwen/skills/` — 15 skills, byte-identical to `dist/.claude/skills/`.
+- `dist/dist-templates/directives/QWEN.md` — cloned from the `GEMINI.md`
+ template (Qwen Code is a Gemini CLI fork) with the `qwen-code-v{version}`
+ identity.
+- `dist-manifest.yml` — `.qwen/skills/` in `files:`, `QWEN.md` in `injections:`.
+- `AGENT-RULES.md` (EN/es/zh-CN) and the `AGENTS.md` template gained
+ `qwen-code-v1.0` **and** `qoder-v1.0`; the latter had been missing since #399.
+- `STRAYMARK.md` § "Directive Injection Markers" lists `QWEN.md`.
+
+**CLI**
+
+- `install_skills.rs`: `--agent qwen` branch + `resolve_qwen_home()` mirroring
+ the runtime's own resolution. The doc-comment now states which agents
+ *require* the user-level install (only Codex) and which merely benefit.
+- `validate.rs`: `validate_codex_skills()` generalized to
+ `validate_agent_skills(agent)` behind an `AgentSkillsSpec`. The `claude-only-key`
+ check is now conditional — Qoder and Qwen legitimately carry `allowed-tools`,
+ so flagging it there would have been a false positive by construction.
+- `remove.rs`: `.qwen/skills`, the `.qwen` parent, and `QWEN.md` in
+ `LEGACY_DIRECTIVE_TARGETS`.
+- `update_framework.rs`: dropped the `if !target_path.exists() { continue; }`
+ guard — see §Risk R1.
+
+**CI** — new `skill-mirror-parity` job. The `.qoder` mirror had a parity test
+since #399, but the pipeline runs no `cargo test`, so the only gate in CI was
+`.codex`'s generator check. The job runs exactly the two mirror suites, which
+keeps it narrow enough not to reopen the deferred "no full test suite in CI"
+decision.
+
+**Docs (EN/es/zh-CN)** — Qwen added to README (agent list, architecture tree,
+directory table, platform table), CLI-REFERENCE (`install-skills`,
+`validate --agent`, "5 parallel forms" → 6) and ADOPTION-GUIDE. The i18n
+READMEs also gained Qoder, which #399 had updated only in English.
+
+## Verification
+
+- `cargo test`: 949 passed, 0 failed.
+- New tests: `qwen_skills_test.rs` (tree parity, `install-skills` e2e under
+ `QWEN_HOME`, manifest surface, `validate --agent` acceptance);
+ `update_framework::tests::update_creates_injection_targets_that_are_missing_on_disk`;
+ `inject_test::test_manifest_declares_every_directive_injection` (was
+ `AGENTS.md`-only, now all seven targets plus template-on-disk).
+- Manual e2e against a simulated post-`init` tree: `install-skills --agent qwen`
+ installed 15 skills into a temp `$QWEN_HOME`; `validate --agent qwen` reported
+ all 15 passing with no `allowed-tools` false positives; `validate --agent
+ qoder` degraded to its install hint on an empty directory.
+
+`straymark init` downloads the published release ZIP, so it cannot exercise a
+local `dist/`. The `init`-level check of `QWEN.md` is therefore deferred to
+after the `fw-4.41.0` tag lands.
+
+## Risk
+
+| Id | Risk | Handling |
+|----|------|----------|
+| R1 | `update-framework` creating missing targets means a directive file the operator deleted on purpose is now restored on the next update. | Accepted: `STRAYMARK.md` § "Directive Injection Markers" already documents exactly this ("no opt-out per target short of editing the manifest"), and `repair` has always behaved this way. The code was the outlier, not the doc. If adopters turn out to delete these deliberately, the fix is a per-target opt-out in `config.yml`. |
+| R2 (new, not in Charter) | The manual e2e caught that `validate --agent`'s clap `value_parser` is declared in `main.rs`, separate from the dispatch in `validate.rs` — widening only the latter left the flag rejected at parse time. | Fixed, and pinned by `validate_accepts_every_user_level_agent`. Worth remembering as a shape: this CLI keeps its accepted-value lists at a distance from the code that consumes them, so `install-skills` and `validate` each need both ends touched. |
+| R3 (new, not in Charter) | Six directive files now land in an adopter's project root at `init`, and the count grows with every supported CLI. | Not addressed here. Noted as a real ergonomics cost of the per-CLI directive model; the alternative (AGENTS.md only) is not available while runtimes like Qwen Code default to their own filename. |
+
+## Follow-ups
+
+- Verify `QWEN.md` and `.qwen/skills/` land through a real `straymark init` and
+ a real `straymark update` once `fw-4.41.0` is published, and confirm in the
+ live `qwen` and `qoder` CLIs that the context file and skills are picked up.
diff --git a/.straymark/07-ai-audit/decisions/AIDEC-2026-08-05-001-qwen-code-agent-surface.md b/.straymark/07-ai-audit/decisions/AIDEC-2026-08-05-001-qwen-code-agent-surface.md
new file mode 100644
index 00000000..9b19f6dd
--- /dev/null
+++ b/.straymark/07-ai-audit/decisions/AIDEC-2026-08-05-001-qwen-code-agent-surface.md
@@ -0,0 +1,167 @@
+---
+id: AIDEC-2026-08-05-001
+title: Qwen Code as a first-class agent surface — mirror the Claude channel, and make update-framework create new injection targets
+status: accepted
+created: 2026-08-05
+agent: claude-opus-5-v1.0
+confidence: high
+review_required: false
+risk_level: medium
+eu_ai_act_risk: not_applicable
+nist_genai_risks: []
+iso_42001_clause: []
+tags: [multi-agent, qwen-code, qoder, directive-injection, dist-manifest, update-path]
+related:
+ - AILOG-2026-08-05-003
+ - AILOG-2026-08-04-002
+---
+
+# AIDEC: Qwen Code as a first-class agent surface
+
+## Context
+
+A review of StrayMark's integration with the Qwen Code and Qoder CLIs — asked
+for both at `init` time and at later `update` time — surfaced one silent hole
+and one false claim.
+
+**Qwen Code had zero integration.** No `dist/.qwen/`, no manifest entry, no
+`install-skills` branch, no row in any compatibility table. That matters more
+than the absence suggests: verified against the installed runtime
+(`@qwen-code/qwen-code`), its context-file resolver falls back to `["QWEN.md"]`
+when nothing is configured, and `AGENTS.md` appears only in a safety string
+listing files the agent must not modify without permission. A Qwen Code user
+therefore received **no StrayMark governance at all** — not even the AGENTS.md
+fallback that covers Codex and Qoder.
+
+Two runtime facts shaped the design, both read directly from the shipped code
+rather than assumed:
+
+- `Storage.getGlobalQwenDir()` returns `$QWEN_HOME` when set, `$HOME/.qwen`
+ otherwise; user-level skills live at `
/skills/`.
+- Skills resolve at **both** scopes — `/.qwen/skills/` and the
+ global dir — and the frontmatter parser accepts `allowed-tools`.
+
+**Qoder's documentation was wrong.** README and CLI-REFERENCE (three languages)
+stated Qoder reads skills from `~/.qoder/skills/` "not from the project tree".
+The application bundle resolves a `Project` scope
+(`/.qoder/skills/`) and watches it, so `install-skills --agent
+qoder` is a convenience, not a prerequisite. It also searches `**/AGENTS.md`
+and injects the matches as instructions, which is why Qoder was in fact
+governed while Qwen Code was not.
+
+## Problem
+
+Two decisions had to be made together, because the second determines whether
+the first reaches anyone who already installed StrayMark.
+
+1. What shape should the Qwen Code channel take — which skill frontmatter, and
+ which directive file?
+2. `update_framework.rs` skipped every injection target absent from disk
+ (`if !target_path.exists() { continue; }`). A new agent surface added to the
+ manifest would therefore land only on fresh `init`s. Meanwhile
+ `STRAYMARK.md` § "Directive Injection Markers" states that `init`,
+ `update-framework` **and** `repair` all "create any missing target file".
+ Code and documentation contradicted each other; one of them had to move.
+
+## Alternatives Considered
+
+### Alternative 1: Mirror `.claude/` byte-for-byte, pinned by a parity test
+
+**Description**: `dist/.qwen/skills/` is a straight copy of
+`dist/.claude/skills/`, guarded by the same shape of test that guards Qoder
+(#399), plus a CI job. `QWEN.md` clones the `GEMINI.md` template with a
+`qwen-code-v{version}` identity.
+
+**Pros**:
+- Qwen Code parses `allowed-tools`, so the richer frontmatter is usable, not
+ merely tolerated. Stripping it would lose real tool-scoping information.
+- Identical to the precedent already set for Qoder — one pattern, not two.
+- A parity test states the invariant in one line and fails loudly on drift.
+
+**Cons**:
+- Duplicates 15 files in the repo (~2,300 lines) that a generator could emit.
+
+### Alternative 2: Clone `.gemini/` (minimal frontmatter)
+
+**Description**: Qwen Code is a Gemini CLI fork, so ship the reduced
+`name` + `description` frontmatter.
+
+**Pros**:
+- Conservative if the parser turned out to be strict about unknown keys.
+
+**Cons**:
+- Contradicted by the runtime: 94 `allowedTools` references in the bundle, and
+ `allowed-tools` present in the skill frontmatter path. The conservatism buys
+ nothing and discards tool scoping.
+
+### Alternative 3: Generate `.qwen/` from `.claude/` like `.codex/`
+
+**Description**: Add a `gen_qwen_skills` binary and a `--check` CI gate.
+
+**Pros**:
+- No duplicated content in the repo.
+
+**Cons**:
+- A generator earns its keep when it *transforms* (Codex strips frontmatter
+ keys). Here the transformation is the identity function, so it would be a
+ build step whose only output is a copy — more moving parts guarding less.
+
+### Alternative 4 (problem 2): Fix the documentation instead of the code
+
+**Description**: Leave `update-framework` skipping missing targets and amend
+`STRAYMARK.md` to say `straymark repair` is required after a new agent lands.
+
+**Pros**:
+- Smallest diff; no behavior change for existing installations.
+
+**Cons**:
+- Makes every future agent surface depend on adopters reading a release note
+ and running a second command. The failure is silent: the adopter's Qwen Code
+ keeps starting with no governance and nothing reports it.
+- The documented contract is the better one. Choosing the worse behavior to
+ match a stale sentence inverts the relationship between intent and code.
+
+## Decision
+
+**Chosen**: Alternative 1 for the channel shape, and *fix the code* (rejecting
+Alternative 4) for the update path.
+
+**Justification**: The mirror matches both the runtime's actual capabilities
+and the precedent set for Qoder, and the parity test plus the new CI job make
+the duplication self-correcting rather than a maintenance liability. On the
+update path, `STRAYMARK.md` already described the behavior adopters need; the
+code was the outlier. Creating missing targets during `update-framework` is
+also what makes this release meaningful for existing installations rather than
+for new ones only — the exact question that prompted the review.
+
+## Consequences
+
+### Positive
+- Qwen Code users receive the governance pointer (`QWEN.md`) and all 15 skills.
+- Any *future* agent surface now reaches existing installations through the
+ ordinary `straymark update`, with no second command and no release note to
+ read.
+- The Qoder and Qwen mirrors are gated in CI; previously only `.codex` was, and
+ the Qoder parity test lived in a suite the pipeline never ran.
+- `validate --agent` stopped being Codex-shaped and is now per-agent.
+
+### Negative
+- Six directive files are now written into an adopter's project root at `init`
+ (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `QWEN.md`, plus the two Cursor files).
+ Root clutter grows with each supported CLI, and there is still no opt-out
+ short of editing the manifest.
+- `dist/` carries a third byte-identical copy of the skill corpus.
+
+### Risks
+- **A future release could now overwrite a file the operator had deleted on
+ purpose.** Deleting `QWEN.md` no longer survives `straymark update` — it is
+ restored. Mitigation: this is the documented contract (`STRAYMARK.md` §
+ "Directive Injection Markers": "There is no opt-out per target short of
+ editing the manifest"), and it matches what `repair` has always done. If
+ adopter feedback shows people deleting directive files deliberately, the
+ right fix is a per-target opt-out in `config.yml`, not re-introducing the
+ skip.
+- **Qwen Code's default context filename could change.** It is a fallback in
+ the resolver, not a constant an adopter can rely on forever. Mitigation:
+ `AGENTS.md` is also shipped, so a runtime that adopts the open standard
+ degrades to covered rather than uncovered.
diff --git a/.straymark/follow-ups-backlog.md b/.straymark/follow-ups-backlog.md
index a608828d..c58aa034 100644
--- a/.straymark/follow-ups-backlog.md
+++ b/.straymark/follow-ups-backlog.md
@@ -1,7 +1,7 @@
---
-last_scan: 2026-08-04
+last_scan: 2026-08-05
schema_version: v1
-total_open: 1
+total_open: 2
total_promoted: 0
total_closed_in_session: 0
total_phase_blocked: 0
@@ -14,6 +14,7 @@ buckets:
- operational
fully_extracted_ailogs:
- AILOG-2026-08-04-003
+ - AILOG-2026-08-05-003
---
# Follow-ups Backlog
@@ -49,6 +50,15 @@ Entry shape (v1 — optional fields marked):
- **Cost**: TBD
- **Notes**: Auto-appended by `straymark followups drift --apply` 2026-08-04.
+### FU-002 — Verify `QWEN.md` and `.qwen/skills/` land through a real `straymark init` and a real `straymark update` once…
+- **Origin**: AILOG-2026-08-05-003 §Follow-ups
+- **Source-hash**: db86ed449fc7
+- **Status**: open
+- **Trigger**: TBD
+- **Destination**: TBD
+- **Cost**: TBD
+- **Notes**: Auto-appended by `straymark followups drift --apply` 2026-08-05.
+
## Bucket: time-triggered
## Bucket: charter-triggered
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d8a4d02..658a1e8f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,37 @@ and this project uses [independent versioning](README.md#versioning) for Framewo
---
+## Framework 4.41.0 / CLI 3.42.0 — 2026-08-05
+
+Qwen Code becomes a first-class agent surface, and the "does a later `update` reach me?" gap that would have silently kept it away from existing installations is closed. Also corrects a documented claim about Qoder that the runtime contradicts.
+
+### Added (Framework)
+
+- **`QWEN.md` directive injection**: Qwen Code's default context filename is `QWEN.md` — it does **not** read `AGENTS.md` unless the adopter configures `contextFileName` by hand — so until now a Qwen Code user received no StrayMark governance at all. The manifest now declares the target, with a template carrying the `qwen-code-v{version}` identity.
+- **`dist/.qwen/skills/`**: 15 skills, byte-for-byte mirror of the Claude variants (Qwen Code parses the full Claude frontmatter, `allowed-tools` included), materialized by `init` / `update-framework` via the dist manifest.
+- **`qwen-code-v1.0` and `qoder-v1.0`** added to the agent identity examples in `AGENT-RULES.md` (EN/es/zh-CN) and the `AGENTS.md` directive template.
+
+### Added (CLI)
+
+- **`straymark install-skills --agent qwen`**: installs from `/.qwen/skills/` into `$QWEN_HOME/skills/` (fallback `~/.qwen/skills/`) — the directory Qwen Code's own `Storage.getGlobalQwenDir()` resolves.
+- **`straymark validate --agent qoder|qwen`**: the agent-targeted validator was hardcoded to Codex. It is now per-agent, and skips the "Claude-only key" check for Qoder and Qwen Code, which legitimately carry `allowed-tools`.
+
+### Fixed (CLI)
+
+- **`update-framework` never created newly-declared directive targets.** It skipped every injection target absent from disk, so an agent surface added to the manifest after installation only reached fresh `init`s (or an explicit `straymark repair`) — while `STRAYMARK.md` § "Directive Injection Markers" documented the opposite. Missing targets are now created, which is what makes `QWEN.md` land on existing installations via `straymark update`.
+- **`straymark remove`** now cleans `.qwen/skills/` and drops `QWEN.md` via the legacy target list, matching the other agent surfaces.
+
+### Changed (Framework)
+
+- **Qoder is documented accurately** (README + CLI-REFERENCE, EN/es/zh-CN): the docs claimed Qoder reads skills from `~/.qoder/skills/` "not from the project tree". The runtime resolves a project scope too (`/.qoder/skills/`, watched for changes), so `install-skills --agent qoder` is a convenience, not a requirement. Codex remains the only agent for which the user-level install is mandatory.
+- **`ADOPTION-GUIDE.md` compatible-environments table** (EN/es/zh-CN) was two releases behind the README: Codex CLI, Qoder and Qwen Code added.
+
+### Changed (CI)
+
+- New `skill-mirror-parity` job keeps `dist/.qoder/skills/` and `dist/.qwen/skills/` byte-identical to the Claude source. Only `.codex` had a gate; the Qoder mirror was guarded by a test the pipeline never ran.
+
+---
+
## CLI 3.41.1 — 2026-08-05
### Fixed (CLI)
diff --git a/Cargo.lock b/Cargo.lock
index 4c27c40d..9060b7b2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2574,7 +2574,7 @@ dependencies = [
[[package]]
name = "straymark-cli"
-version = "3.41.1"
+version = "3.42.0"
dependencies = [
"anyhow",
"arborist-metrics",
diff --git a/README.md b/README.md
index 4796b01d..f0e61a75 100644
--- a/README.md
+++ b/README.md
@@ -128,6 +128,7 @@ Pre-configured for popular AI coding assistants:
- **Cursor** → `.cursorrules`
- **GitHub Copilot CLI** → `.github/copilot-instructions.md`
- **Gemini CLI** (Google) → `GEMINI.md`
+- **Qwen Code** (Alibaba) → `QWEN.md`
Each configuration instructs the AI to:
- Identify itself in every document
@@ -277,8 +278,8 @@ StrayMark uses independent version tags for each component:
| Component | Tag prefix | Example | Includes |
| --- | --- | --- | --- |
-| Framework | `fw-` | `fw-4.40.0` | Templates (12 types), governance, directives, Charter template + schema |
-| CLI | `cli-` | `cli-3.41.1` | The `straymark` binary |
+| Framework | `fw-` | `fw-4.41.0` | Templates (12 types), governance, directives, Charter template + schema |
+| CLI | `cli-` | `cli-3.42.0` | The `straymark` binary |
| Loom (EXPERIMENTAL) | `loom-` | `loom-0.4.2` | The `straymark-loom` visualization server, downloaded on demand by `straymark loom serve` |
Check installed versions with `straymark status` or `straymark about`.
@@ -566,7 +567,10 @@ your-project/
├── .codex/skills/ # 🟢 Codex CLI (OpenAI) — installed to ~/.codex/skills/
│ ├── straymark-new/SKILL.md
│ └── ...
-└── .qoder/skills/ # 🔴 Qoder CLI — installed to ~/.qoder/skills/
+├── .qoder/skills/ # 🔴 Qoder CLI — read from the project tree
+│ ├── straymark-new/SKILL.md
+│ └── ...
+└── .qwen/skills/ # 🟠 Qwen Code (Alibaba) — read from the project tree
├── straymark-new/SKILL.md
└── ...
```
@@ -576,10 +580,13 @@ your-project/
| `.agent/workflows/` | Antigravity, generic | VS Code/Cursor extensions | `skill-name.md` with YAML frontmatter |
| `.gemini/skills/` | Gemini CLI | Google's terminal CLI | `skill-name/SKILL.md` |
| `.claude/skills/` | Claude Code | Anthropic's coding agent | `skill-name/SKILL.md` |
-| `.codex/skills/` *(fw-4.19.0+)* | Codex CLI | OpenAI's coding agent | `skill-name/SKILL.md` (minimal frontmatter) — installed to `~/.codex/skills/` via `straymark install-skills --agent codex` |
-| `.qoder/skills/` | Qoder CLI | Qoder's terminal coding agent | `skill-name/SKILL.md` (full frontmatter, same as Claude) — installed to `~/.qoder/skills/` via `straymark install-skills --agent qoder` |
+| `.codex/skills/` *(fw-4.19.0+)* | Codex CLI | OpenAI's coding agent | `skill-name/SKILL.md` (minimal frontmatter) — **must** be installed to `~/.codex/skills/` via `straymark install-skills --agent codex` |
+| `.qoder/skills/` | Qoder CLI | Qoder's terminal coding agent | `skill-name/SKILL.md` (full frontmatter, same as Claude) — read from the project tree; `straymark install-skills --agent qoder` optionally also populates `~/.qoder/skills/` |
+| `.qwen/skills/` *(fw-4.41.0+)* | Qwen Code | Alibaba's terminal coding agent | `skill-name/SKILL.md` (full frontmatter, same as Claude) — read from the project tree; `straymark install-skills --agent qwen` optionally also populates `~/.qwen/skills/` |
> **Note**: `.agent/` is the **vendor-agnostic** standard. Agent-specific directories (`.gemini/`, `.claude/`) provide compatibility for those platforms while following their native conventions.
+>
+> **User-level vs project-level.** Only Codex *requires* `install-skills`: it resolves skills exclusively from `$CODEX_HOME/skills/`. Claude, Gemini, Qoder and Qwen Code all discover a project-scoped skills directory, so for Qoder and Qwen the user-level install is a convenience — it makes the StrayMark skills available in every project, not just the ones where the framework is installed.
All skill implementations are **functionally identical**—only the format differs to match each agent's requirements.
@@ -597,7 +604,8 @@ All skill implementations are **functionally identical**—only the format diffe
| GitHub Copilot CLI | `.github/copilot-instructions.md` | ✅ Full support |
| Gemini CLI | `GEMINI.md` | ✅ Full support |
| Codex CLI (OpenAI) *(fw-4.19.0+)* | `AGENTS.md` + `~/.codex/skills/` | ✅ Full support (run `straymark install-skills --agent codex`) |
-| Qoder CLI | `AGENTS.md` + `~/.qoder/skills/` | ✅ Full support (run `straymark install-skills --agent qoder`) |
+| Qoder CLI | `AGENTS.md` + `.qoder/skills/` | ✅ Full support |
+| Qwen Code (Alibaba) *(fw-4.41.0+)* | `QWEN.md` + `.qwen/skills/` | ✅ Full support |
### Operating Systems
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 1b9883b2..a9156dc0 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "straymark-cli"
-version = "3.41.1"
+version = "3.42.0"
edition = "2021"
description = "CLI for StrayMark — the cognitive discipline your AI-assisted projects need"
license = "MIT"
diff --git a/cli/src/commands/install_skills.rs b/cli/src/commands/install_skills.rs
index b954d793..d38142e0 100644
--- a/cli/src/commands/install_skills.rs
+++ b/cli/src/commands/install_skills.rs
@@ -8,9 +8,17 @@ use crate::utils;
/// Install StrayMark skills into an AI agent's user-level skills directory.
///
/// Supports `--agent codex` (skills land in `$CODEX_HOME/skills/` or
-/// `$HOME/.codex/skills/`) and `--agent qoder` (skills land in
-/// `$QODER_CONFIG_DIR/skills/` or `$HOME/.qoder/skills/`, GH #399). Claude
-/// and Gemini consume skills directly from the project tree
+/// `$HOME/.codex/skills/`), `--agent qoder` (`$QODER_CONFIG_DIR/skills/` or
+/// `$HOME/.qoder/skills/`, GH #399) and `--agent qwen` (`$QWEN_HOME/skills/`
+/// or `$HOME/.qwen/skills/`).
+///
+/// Only Codex *requires* this step. Qoder and Qwen Code also resolve a
+/// project-scoped skills directory (`/.qoder/skills/`,
+/// `/.qwen/skills/`), so for those two a user-level install is a
+/// convenience: it makes the StrayMark skills available in every project,
+/// not just the ones where the framework is installed.
+///
+/// Claude and Gemini consume skills exclusively from the project tree
/// (`.claude/skills/`, `.gemini/skills/`); for those agents this command
/// exits with an explanatory error.
pub fn run(agent: &str, project_path: &str, dry_run: bool, symlink: bool) -> Result<()> {
@@ -31,6 +39,14 @@ pub fn run(agent: &str, project_path: &str, dry_run: bool, symlink: bool) -> Res
dry_run,
symlink,
),
+ "qwen" => install_user_level(
+ "qwen",
+ ".qwen",
+ resolve_qwen_home,
+ project_path,
+ dry_run,
+ symlink,
+ ),
"claude" | "gemini" => {
bail!(
"Skills for {agent} are read directly from the project tree (.{agent}/skills/). \
@@ -38,7 +54,7 @@ pub fn run(agent: &str, project_path: &str, dry_run: bool, symlink: bool) -> Res
to refresh them in the project."
)
}
- other => bail!("unknown agent: {other} (supported: codex, qoder)"),
+ other => bail!("unknown agent: {other} (supported: codex, qoder, qwen)"),
}
}
@@ -194,6 +210,18 @@ fn resolve_qoder_home() -> Result {
Ok(PathBuf::from(home).join(".qoder"))
}
+/// Mirrors Qwen Code's own `Storage.getGlobalQwenDir()`: `$QWEN_HOME` when
+/// set, `$HOME/.qwen` otherwise. User-level skills live under `/skills/`.
+fn resolve_qwen_home() -> Result {
+ if let Ok(v) = std::env::var("QWEN_HOME") {
+ if !v.is_empty() {
+ return Ok(PathBuf::from(v));
+ }
+ }
+ let home = std::env::var("HOME").context("$HOME is not set")?;
+ Ok(PathBuf::from(home).join(".qwen"))
+}
+
fn remove_entry(p: &Path) -> Result<()> {
let meta = fs::symlink_metadata(p).with_context(|| format!("stat {}", p.display()))?;
if meta.file_type().is_symlink() || meta.is_file() {
diff --git a/cli/src/commands/remove.rs b/cli/src/commands/remove.rs
index e92a238d..e23e3e23 100644
--- a/cli/src/commands/remove.rs
+++ b/cli/src/commands/remove.rs
@@ -13,6 +13,7 @@ const LEGACY_DIRECTIVE_TARGETS: &[&str] = &[
"AGENTS.md",
"CLAUDE.md",
"GEMINI.md",
+ "QWEN.md",
".github/copilot-instructions.md",
".cursorrules",
".cursor/rules/straymark.md",
@@ -76,6 +77,7 @@ pub fn run(full: bool) -> Result<()> {
remove_dir_if_exists(&target.join(".gemini/skills"))?;
remove_dir_if_exists(&target.join(".codex/skills"))?;
remove_dir_if_exists(&target.join(".qoder/skills"))?;
+ remove_dir_if_exists(&target.join(".qwen/skills"))?;
remove_dir_if_exists(&target.join(".agent/workflows"))?;
// Clean up empty parent dirs
@@ -83,6 +85,7 @@ pub fn run(full: bool) -> Result<()> {
remove_empty_dir(&target.join(".gemini"))?;
remove_empty_dir(&target.join(".codex"))?;
remove_empty_dir(&target.join(".qoder"))?;
+ remove_empty_dir(&target.join(".qwen"))?;
remove_empty_dir(&target.join(".agent"))?;
// Clean up .cursor directories (injections already handled by clean_directives)
diff --git a/cli/src/commands/update_framework.rs b/cli/src/commands/update_framework.rs
index 650ea66e..28492d7c 100644
--- a/cli/src/commands/update_framework.rs
+++ b/cli/src/commands/update_framework.rs
@@ -244,11 +244,12 @@ fn inject_directives(target: &Path, source_root: &Path, manifest: &DistManifest)
for injection in &manifest.injections {
let target_path = target.join(&injection.target);
- // In update mode, only update targets that already exist
- if !target_path.exists() {
- continue;
- }
-
+ // Missing targets are created, not skipped. A release that adds a new
+ // agent surface (e.g. QWEN.md in fw-4.41.0) has to reach existing
+ // installations through `update`, not only through `repair` — which is
+ // what STRAYMARK.md § "Directive Injection Markers" already promises.
+ // `inject::inject_directive` writes the full template when the file is
+ // absent and manages only the marker block when it is not.
let template_path = source_root.join(&injection.template);
let template_content = match std::fs::read_to_string(&template_path) {
Ok(content) => content,
@@ -478,4 +479,58 @@ mod tests {
assert!(!matches_manifest(".github/workflows/release-cli.yml", &files));
assert!(!matches_manifest(".claude/agents/foo.md", &files));
}
+
+ /// A release that introduces a new agent surface must reach *existing*
+ /// installations through `update`, not only through `repair`. Until
+ /// fw-4.41.0 / cli-3.42.0 this loop skipped every target that was absent
+ /// on disk, so `QWEN.md` would only ever have landed on fresh `init`s.
+ #[test]
+ fn update_creates_injection_targets_that_are_missing_on_disk() {
+ let tmp = tempfile::tempdir().unwrap();
+ let target = tmp.path().join("project");
+ let source_root = tmp.path().join("release");
+ std::fs::create_dir_all(&target).unwrap();
+ std::fs::create_dir_all(source_root.join("dist-templates/directives")).unwrap();
+ std::fs::write(
+ source_root.join("dist-templates/directives/QWEN.md"),
+ "# StrayMark - Qwen Code Configuration\n\n\
+ \n> rules\n\n",
+ )
+ .unwrap();
+
+ // An existing target is refreshed in place; a missing one is created.
+ std::fs::write(target.join("CLAUDE.md"), "# My own notes\n").unwrap();
+ std::fs::write(
+ source_root.join("dist-templates/directives/CLAUDE.md"),
+ "# StrayMark - Claude Code Configuration\n\n\
+ \n> rules\n\n",
+ )
+ .unwrap();
+
+ let manifest = crate::manifest::DistManifest::from_str(
+ "version: \"4.41.0\"\ndescription: \"test\"\nrepository: \"x\"\nfiles: []\n\
+ injections:\n\
+ \x20 - target: CLAUDE.md\n template: dist-templates/directives/CLAUDE.md\n\
+ \x20 - target: QWEN.md\n template: dist-templates/directives/QWEN.md\n",
+ )
+ .unwrap();
+
+ super::inject_directives(&target, &source_root, &manifest).unwrap();
+
+ let qwen = target.join("QWEN.md");
+ assert!(
+ qwen.exists(),
+ "update must create the newly-declared QWEN.md target"
+ );
+ let qwen_content = std::fs::read_to_string(&qwen).unwrap();
+ assert!(qwen_content.contains(""));
+ assert!(qwen_content.contains("Qwen Code Configuration"));
+
+ let claude_content = std::fs::read_to_string(target.join("CLAUDE.md")).unwrap();
+ assert!(
+ claude_content.contains("# My own notes"),
+ "a pre-existing target keeps the operator's content"
+ );
+ assert!(claude_content.contains(""));
+ }
}
diff --git a/cli/src/commands/validate.rs b/cli/src/commands/validate.rs
index 3c6c9213..e9f5bfc0 100644
--- a/cli/src/commands/validate.rs
+++ b/cli/src/commands/validate.rs
@@ -21,8 +21,8 @@ pub fn run(
// external skills directory (e.g. ~/.codex/skills/), not StrayMark docs.
if let Some(agent) = agent {
return match agent {
- "codex" => validate_codex_skills(),
- other => bail!("unknown --agent: {other} (supported: codex)"),
+ "codex" | "qoder" | "qwen" => validate_agent_skills(agent),
+ other => bail!("unknown --agent: {other} (supported: codex, qoder, qwen)"),
};
}
@@ -279,37 +279,73 @@ fn exit_with_code(result: &validation::ValidationResult) -> Result<()> {
}
}
-/// Validate the user-level Codex skills installation at `$CODEX_HOME/skills/`
-/// (or `$HOME/.codex/skills/`). Checks every `straymark-*` skill for:
-/// presence of `SKILL.md`, parseable YAML frontmatter, required `name` and
-/// `description`, and absence of Claude-only keys like `allowed-tools` (whose
-/// presence indicates someone copied skills from `.claude/` by mistake).
-fn validate_codex_skills() -> Result<()> {
- let codex_home = if let Ok(v) = std::env::var("CODEX_HOME") {
- if v.is_empty() {
- std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".codex")
- } else {
- std::path::PathBuf::from(v)
- }
- } else {
- std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".codex")
+/// Per-agent shape of a user-level skills installation.
+///
+/// `env_var`/`default_dir` mirror the agent's own home resolution (see
+/// `install_skills.rs`). `minimal_frontmatter` says whether the agent expects
+/// the reduced Codex-style frontmatter (`name` + `description` only) or the
+/// full Claude-style one: Qoder and Qwen Code both parse `allowed-tools`, so
+/// for them its presence is correct rather than a copy-paste mistake.
+struct AgentSkillsSpec {
+ env_var: &'static str,
+ default_dir: &'static str,
+ minimal_frontmatter: bool,
+}
+
+fn agent_skills_spec(agent: &str) -> AgentSkillsSpec {
+ match agent {
+ "qoder" => AgentSkillsSpec {
+ env_var: "QODER_CONFIG_DIR",
+ default_dir: ".qoder",
+ minimal_frontmatter: false,
+ },
+ "qwen" => AgentSkillsSpec {
+ env_var: "QWEN_HOME",
+ default_dir: ".qwen",
+ minimal_frontmatter: false,
+ },
+ // codex
+ _ => AgentSkillsSpec {
+ env_var: "CODEX_HOME",
+ default_dir: ".codex",
+ minimal_frontmatter: true,
+ },
+ }
+}
+
+/// Validate an agent's user-level skills installation (e.g.
+/// `$CODEX_HOME/skills/`, `$QODER_CONFIG_DIR/skills/`, `$QWEN_HOME/skills/`).
+/// Checks every `straymark-*` skill for: presence of `SKILL.md`, parseable YAML
+/// frontmatter, required `name` and `description`, and — for agents that expect
+/// the minimal frontmatter — absence of Claude-only keys like `allowed-tools`
+/// (whose presence indicates someone copied skills from `.claude/` by mistake).
+fn validate_agent_skills(agent: &str) -> Result<()> {
+ let spec = agent_skills_spec(agent);
+ let home_fallback = || {
+ std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(spec.default_dir)
+ };
+ let agent_home = match std::env::var(spec.env_var) {
+ Ok(v) if !v.is_empty() => std::path::PathBuf::from(v),
+ _ => home_fallback(),
};
- let skills_dir = codex_home.join("skills");
+ let skills_dir = agent_home.join("skills");
+ let install_hint = format!("straymark install-skills --agent {agent}");
println!();
- println!(" {}", "StrayMark Validate (codex)".bold().cyan());
+ println!(" {}", format!("StrayMark Validate ({agent})").bold().cyan());
println!(" {}", skills_dir.display().to_string().dimmed());
println!();
if !skills_dir.is_dir() {
utils::warn(&format!(
- "Codex skills directory not found: {}",
+ "{} skills directory not found: {}",
+ agent,
skills_dir.display()
));
println!(
" {} Run {} to populate it.",
"→".blue().bold(),
- "straymark install-skills --agent codex".cyan()
+ install_hint.cyan()
);
println!();
std::process::exit(1);
@@ -335,7 +371,7 @@ fn validate_codex_skills() -> Result<()> {
println!(
" {} Run {} to install them.",
"→".blue().bold(),
- "straymark install-skills --agent codex".cyan()
+ install_hint.cyan()
);
println!();
std::process::exit(1);
@@ -379,16 +415,18 @@ fn validate_codex_skills() -> Result<()> {
"frontmatter missing `description`".into(),
));
}
- if let Some(map) = &fm {
- for forbidden in &["allowed-tools", "argument-hint", "model"] {
- if map.contains_key(*forbidden) {
- file_issues.push((
- false,
- "claude-only-key".into(),
- format!(
- "frontmatter contains `{forbidden}` — Codex skills should keep only `name` and `description`"
- ),
- ));
+ if spec.minimal_frontmatter {
+ if let Some(map) = &fm {
+ for forbidden in &["allowed-tools", "argument-hint", "model"] {
+ if map.contains_key(*forbidden) {
+ file_issues.push((
+ false,
+ "claude-only-key".into(),
+ format!(
+ "frontmatter contains `{forbidden}` — {agent} skills should keep only `name` and `description`"
+ ),
+ ));
+ }
}
}
}
@@ -415,9 +453,10 @@ fn validate_codex_skills() -> Result<()> {
if errors == 0 && warnings == 0 {
println!(
- " {} All {} Codex skill(s) passed validation",
+ " {} All {} {} skill(s) passed validation",
"✓".green().bold(),
- entries.len()
+ entries.len(),
+ agent
);
println!();
return Ok(());
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 39fdda03..fc2b2b31 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -90,16 +90,21 @@ enum Commands {
},
/// Install StrayMark skills into an AI agent's user-level skills directory
/// (`--agent codex` targets `$CODEX_HOME/skills/` or `$HOME/.codex/skills/`;
- /// `--agent qoder` targets `$QODER_CONFIG_DIR/skills/` or `$HOME/.qoder/skills/`).
- /// Claude and Gemini read skills directly from the project tree
- /// (`.claude/skills/`, `.gemini/skills/`) and do not require this command.
+ /// `--agent qoder` targets `$QODER_CONFIG_DIR/skills/` or `$HOME/.qoder/skills/`;
+ /// `--agent qwen` targets `$QWEN_HOME/skills/` or `$HOME/.qwen/skills/`).
+ /// Required only for Codex: Qoder and Qwen Code also read the project-scoped
+ /// `.qoder/skills/` / `.qwen/skills/`, so there the user-level install just
+ /// makes the skills available outside this project. Claude and Gemini read
+ /// skills exclusively from the project tree (`.claude/skills/`,
+ /// `.gemini/skills/`) and do not accept this command.
InstallSkills {
/// AI agent whose user-level skills directory we should populate.
- #[arg(long, value_parser = ["codex", "qoder", "claude", "gemini"])]
+ #[arg(long, value_parser = ["codex", "qoder", "qwen", "claude", "gemini"])]
agent: String,
/// Project directory (default: current directory). The source of the
- /// skills is `/.codex/skills/` (or `/.qoder/skills/` for
- /// qoder), materialized by `straymark init` or `straymark update`.
+ /// skills is `/.codex/skills/` (or `/.qoder/skills/`,
+ /// `/.qwen/skills/`), materialized by `straymark init` or
+ /// `straymark update`.
#[arg(long = "path", default_value = ".")]
path: String,
/// Print what would be installed without writing anything.
@@ -122,10 +127,13 @@ enum Commands {
#[arg(long)]
staged: bool,
/// Inspect an AI agent's user-level skills installation instead of
- /// validating documents. Currently supports `codex` — checks
- /// `~/.codex/skills/straymark-*` for presence, parseable frontmatter,
- /// required `name`/`description`, and absence of Claude-only keys.
- #[arg(long, value_parser = ["codex"])]
+ /// validating documents. Supports `codex`, `qoder` and `qwen` — checks
+ /// `~/.codex/skills/straymark-*` (resp. `~/.qoder/`, `~/.qwen/`) for
+ /// presence, parseable frontmatter and required `name`/`description`.
+ /// For `codex` it also flags Claude-only keys like `allowed-tools`;
+ /// Qoder and Qwen Code parse the full Claude frontmatter, so there
+ /// those keys are expected.
+ #[arg(long, value_parser = ["codex", "qoder", "qwen"])]
agent: Option,
/// Also validate Charters in .straymark/charters/ against the Charter schema
/// and referential integrity (originating_ailogs IDs exist;
diff --git a/cli/tests/architecture_skill_test.rs b/cli/tests/architecture_skill_test.rs
index cbe033f5..e332880f 100644
--- a/cli/tests/architecture_skill_test.rs
+++ b/cli/tests/architecture_skill_test.rs
@@ -1,9 +1,10 @@
//! Sanity tests for the architecture / Loom skills shipped under `dist/`.
//!
//! These verify that each skill file exists with the expected frontmatter
-//! shape (Claude has `allowed-tools`, Gemini has `name` but no `allowed-tools`,
-//! agent workflow has only `description`) and that all three platforms carry
-//! the load-bearing guidance. They run against the source tree, not against an
+//! shape (Claude/Qoder/Qwen have `allowed-tools`, Gemini and Codex have `name`
+//! but no `allowed-tools`, agent workflow has only `description`) and that all
+//! six surfaces carry the load-bearing guidance. They run against the source
+//! tree, not against an
//! `init`-ed project, because the manifest already includes the parent
//! directories recursively — if the files exist in `dist/`, `init` copies them.
@@ -32,12 +33,22 @@ fn codex(skill: &str) -> String {
read(dist_root().join(".codex").join("skills").join(skill).join("SKILL.md"))
}
+fn qoder(skill: &str) -> String {
+ read(dist_root().join(".qoder").join("skills").join(skill).join("SKILL.md"))
+}
+
+fn qwen(skill: &str) -> String {
+ read(dist_root().join(".qwen").join("skills").join(skill).join("SKILL.md"))
+}
+
fn agent(skill: &str) -> String {
read(dist_root().join(".agent").join("workflows").join(format!("{skill}.md")))
}
-/// The four-variant frontmatter contract every shipped skill must honor.
-fn assert_four_variant_shape(skill: &str) {
+/// The six-variant frontmatter contract every shipped skill must honor.
+/// Qoder and Qwen Code parse the full Claude-format frontmatter, so their
+/// variants are byte-for-byte mirrors of the Claude one.
+fn assert_six_variant_shape(skill: &str) {
let c = claude(skill);
assert!(c.starts_with("---\n"), "{skill}: claude missing YAML frontmatter");
assert!(c.contains(&format!("name: {skill}")), "{skill}: claude missing name field");
@@ -59,6 +70,13 @@ fn assert_four_variant_shape(skill: &str) {
"{skill}: codex must not declare allowed-tools"
);
+ for (surface, body) in [("qoder", qoder(skill)), ("qwen", qwen(skill))] {
+ assert_eq!(
+ body, c,
+ "{skill}: {surface} must mirror the Claude variant byte-for-byte"
+ );
+ }
+
let a = agent(skill);
assert!(a.starts_with("---\n"), "{skill}: agent workflow missing YAML frontmatter");
assert!(
@@ -75,8 +93,8 @@ fn assert_four_variant_shape(skill: &str) {
// ── straymark-architecture ──────────────────────────────────────────────────
#[test]
-fn architecture_skill_has_four_variant_shape() {
- assert_four_variant_shape("straymark-architecture");
+fn architecture_skill_has_six_variant_shape() {
+ assert_six_variant_shape("straymark-architecture");
}
#[test]
@@ -125,8 +143,8 @@ fn architecture_skill_shares_core_guidance() {
// ── straymark-loom ──────────────────────────────────────────────────────────
#[test]
-fn loom_skill_has_four_variant_shape() {
- assert_four_variant_shape("straymark-loom");
+fn loom_skill_has_six_variant_shape() {
+ assert_six_variant_shape("straymark-loom");
}
#[test]
@@ -166,8 +184,8 @@ fn loom_skill_shares_core_guidance() {
// ── straymark-architecture-sync ─────────────────────────────────────────────
#[test]
-fn architecture_sync_skill_has_four_variant_shape() {
- assert_four_variant_shape("straymark-architecture-sync");
+fn architecture_sync_skill_has_six_variant_shape() {
+ assert_six_variant_shape("straymark-architecture-sync");
}
#[test]
diff --git a/cli/tests/inject_test.rs b/cli/tests/inject_test.rs
index 0f44f921..4a8f8c47 100644
--- a/cli/tests/inject_test.rs
+++ b/cli/tests/inject_test.rs
@@ -74,17 +74,51 @@ fn test_agents_md_template_has_markers() {
}
#[test]
-fn test_manifest_declares_agents_md_injection() {
- // The dist-manifest.yml must list AGENTS.md among injection targets so init/update/remove
- // pick it up via the data-driven path (not just the legacy fallback in remove.rs).
- let manifest_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+fn test_manifest_declares_every_directive_injection() {
+ // The dist-manifest.yml is the single source of truth for the agent
+ // surfaces: init/update/repair/remove all walk `injections:`. A target
+ // missing here silently drops an entire CLI's governance — which is what
+ // happened to Qwen Code until fw-4.41.0. Every declared target must also
+ // have its template on disk, or `init` warns and skips it at runtime.
+ let dist_root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
- .join("dist/dist-manifest.yml");
- let manifest = fs::read_to_string(&manifest_path).expect("dist-manifest.yml must exist");
-
- assert!(manifest.contains("target: AGENTS.md"));
- assert!(manifest.contains("template: dist-templates/directives/AGENTS.md"));
+ .join("dist");
+ let manifest =
+ fs::read_to_string(dist_root.join("dist-manifest.yml")).expect("dist-manifest.yml must exist");
+
+ let expected: &[(&str, &str)] = &[
+ ("AGENTS.md", "dist-templates/directives/AGENTS.md"),
+ ("CLAUDE.md", "dist-templates/directives/CLAUDE.md"),
+ ("GEMINI.md", "dist-templates/directives/GEMINI.md"),
+ ("QWEN.md", "dist-templates/directives/QWEN.md"),
+ (
+ ".github/copilot-instructions.md",
+ "dist-templates/directives/copilot-instructions.md",
+ ),
+ (".cursorrules", "dist-templates/directives/cursorrules"),
+ (
+ ".cursor/rules/straymark.md",
+ "dist-templates/directives/cursor-rules-straymark.md",
+ ),
+ ];
+
+ for (target, template) in expected {
+ assert!(
+ manifest.contains(&format!("target: {target}")),
+ "dist-manifest.yml must declare the {target} injection"
+ );
+ assert!(
+ manifest.contains(&format!("template: {template}")),
+ "the {target} injection must point at {template}"
+ );
+ let template_path = dist_root.join(template);
+ assert!(
+ template_path.is_file(),
+ "missing template on disk: {}",
+ template_path.display()
+ );
+ }
}
#[test]
diff --git a/cli/tests/qwen_skills_test.rs b/cli/tests/qwen_skills_test.rs
new file mode 100644
index 00000000..f7a0dfae
--- /dev/null
+++ b/cli/tests/qwen_skills_test.rs
@@ -0,0 +1,142 @@
+//! Qwen Code CLI support.
+//!
+//! Two guarantees are pinned down here:
+//!
+//! 1. `dist/.qwen/skills/` is a byte-for-byte copy of `dist/.claude/skills/`
+//! (Qwen Code parses the full Claude-format SKILL.md frontmatter, including
+//! `allowed-tools`). If the two trees drift, adopters get stale skills on
+//! one agent.
+//! 2. `straymark install-skills --agent qwen` installs the project's
+//! `.qwen/skills/` into `$QWEN_HOME/skills/` — the same directory Qwen
+//! Code's own `Storage.getGlobalQwenDir()` resolves.
+
+use assert_cmd::cargo_bin_cmd;
+use predicates::prelude::*;
+use std::collections::BTreeSet;
+use std::path::{Path, PathBuf};
+use tempfile::TempDir;
+
+fn dist_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR")).join("..").join("dist")
+}
+
+fn walk_files(root: &Path) -> BTreeSet {
+ let mut out = BTreeSet::new();
+ let mut stack = vec![root.to_path_buf()];
+ while let Some(dir) = stack.pop() {
+ for entry in std::fs::read_dir(&dir).unwrap() {
+ let entry = entry.unwrap();
+ let path = entry.path();
+ if path.is_dir() {
+ stack.push(path);
+ } else {
+ out.insert(path.strip_prefix(root).unwrap().to_path_buf());
+ }
+ }
+ }
+ out
+}
+
+#[test]
+fn qwen_skills_mirror_claude_skills() {
+ let claude = dist_root().join(".claude").join("skills");
+ let qwen = dist_root().join(".qwen").join("skills");
+ assert!(claude.is_dir(), "missing {}", claude.display());
+ assert!(qwen.is_dir(), "missing {}", qwen.display());
+
+ let claude_files = walk_files(&claude);
+ let qwen_files = walk_files(&qwen);
+ assert_eq!(
+ claude_files, qwen_files,
+ "dist/.qwen/skills/ must mirror dist/.claude/skills/ file-for-file"
+ );
+ assert!(!qwen_files.is_empty());
+
+ for rel in &qwen_files {
+ let a = std::fs::read(claude.join(rel)).unwrap();
+ let b = std::fs::read(qwen.join(rel)).unwrap();
+ assert_eq!(a, b, "content drift in {}", rel.display());
+ }
+}
+
+#[test]
+fn install_skills_qwen_uses_qwen_home() {
+ let project = TempDir::new().unwrap();
+ let qwen_home = TempDir::new().unwrap();
+
+ let skill_dir = project
+ .path()
+ .join(".qwen")
+ .join("skills")
+ .join("straymark-foo");
+ std::fs::create_dir_all(&skill_dir).unwrap();
+ std::fs::write(
+ skill_dir.join("SKILL.md"),
+ "---\nname: straymark-foo\ndescription: test skill\n---\n# Foo\n",
+ )
+ .unwrap();
+
+ cargo_bin_cmd!("straymark")
+ .args(["install-skills", "--agent", "qwen", "--path"])
+ .arg(project.path().to_str().unwrap())
+ .env("QWEN_HOME", qwen_home.path())
+ .assert()
+ .success()
+ .stdout(predicate::str::contains("1 skill(s) installed"))
+ .stdout(predicate::str::contains("qwen will discover them"));
+
+ let installed = qwen_home.path().join("skills").join("straymark-foo").join("SKILL.md");
+ assert!(installed.exists(), "expected {}", installed.display());
+ assert_eq!(
+ std::fs::read_to_string(&installed).unwrap(),
+ "---\nname: straymark-foo\ndescription: test skill\n---\n# Foo\n"
+ );
+}
+
+/// `validate --agent` has its own clap `value_parser`, separate from the one on
+/// `install-skills`. Widening only the dispatch inside `validate.rs` leaves the
+/// flag rejected at parse time — the exact gap this test pins down.
+#[test]
+fn validate_accepts_every_user_level_agent() {
+ for agent in ["codex", "qoder", "qwen"] {
+ let home = TempDir::new().unwrap();
+ // Point every home-resolution env var at an empty dir so the command
+ // reaches its "skills directory not found" branch instead of touching
+ // the developer's real installation.
+ cargo_bin_cmd!("straymark")
+ .args(["validate", "--agent", agent])
+ .env("CODEX_HOME", home.path())
+ .env("QODER_CONFIG_DIR", home.path())
+ .env("QWEN_HOME", home.path())
+ .assert()
+ .failure()
+ .stdout(predicate::str::contains("skills directory not found"))
+ .stderr(predicate::str::contains("invalid value").not());
+ }
+}
+
+/// The directive target has to be declared in the manifest, or `init` /
+/// `update` / `repair` never write `QWEN.md` — Qwen Code's default context
+/// filename is `QWEN.md`, so without it the agent loads no governance at all.
+#[test]
+fn manifest_declares_qwen_surface() {
+ let manifest = std::fs::read_to_string(dist_root().join("dist-manifest.yml")).unwrap();
+ assert!(
+ manifest.contains("- .qwen/skills/"),
+ "dist-manifest.yml must ship .qwen/skills/"
+ );
+ assert!(
+ manifest.contains("target: QWEN.md"),
+ "dist-manifest.yml must declare the QWEN.md injection"
+ );
+ assert!(
+ manifest.contains("template: dist-templates/directives/QWEN.md"),
+ "the QWEN.md injection must point at its template"
+ );
+ assert!(
+ dist_root()
+ .join("dist-templates/directives/QWEN.md")
+ .is_file(),
+ "the QWEN.md directive template must exist"
+ );
+}
diff --git a/dist/.qwen/skills/straymark-adr/SKILL.md b/dist/.qwen/skills/straymark-adr/SKILL.md
new file mode 100644
index 00000000..bd083c1e
--- /dev/null
+++ b/dist/.qwen/skills/straymark-adr/SKILL.md
@@ -0,0 +1,117 @@
+---
+name: straymark-adr
+description: Create an ADR (Architecture Decision Record) for significant architectural decisions. Requires human review.
+allowed-tools: Read, Write, Glob, Bash(git diff *, git log *, git status *, date *, wc *)
+---
+
+# StrayMark ADR Skill
+
+Create an Architecture Decision Record (ADR) for significant architectural decisions.
+
+> **Note**: ADRs created by AI agents are marked as `draft` and `review_required: true` by default.
+
+## Instructions
+
+Use this skill for major architectural decisions that affect the system structure, technology stack, or design patterns.
+
+### 1. Gather Context
+
+```bash
+# Get current date
+date +%Y-%m-%d
+
+# Summarize the CURRENT work (staged + unstaged + untracked) — label each block.
+# Avoid `HEAD~1`: it describes the previous commit, not the work being decided on.
+git diff --cached --stat # staged changes
+git diff --stat # unstaged changes
+git status --porcelain # includes untracked files
+
+# Check for related ADRs
+ls .straymark/02-design/decisions/ADR-*.md 2>/dev/null | tail -5
+```
+
+### 2. Confirm with User
+
+**Always confirm before creating:**
+
+```
+╔══════════════════════════════════════════════════════════════════╗
+║ StrayMark ADR ║
+╠══════════════════════════════════════════════════════════════════╣
+║ ║
+║ ⚠️ ADRs require human review after creation. ║
+║ ║
+║ 📋 An ADR documents architectural decisions that affect: ║
+║ • System structure ║
+║ • Technology choices ║
+║ • Design patterns ║
+║ • Integration approaches ║
+║ ║
+║ Please provide: ║
+║ 1. Decision title (what architectural decision) ║
+║ 2. Context (why is this decision needed) ║
+║ 3. The decision and alternatives considered ║
+║ ║
+╚══════════════════════════════════════════════════════════════════╝
+```
+
+### 3. Determine Sequence Number
+
+```bash
+# Count existing ADRs for today
+ls .straymark/02-design/decisions/ADR-$(date +%Y-%m-%d)-*.md 2>/dev/null | wc -l
+```
+
+### 4. Check Language and Load Template
+
+Read `.straymark/config.yml` for language setting:
+- `en` (default): `.straymark/templates/TEMPLATE-ADR.md`
+- `es`: `.straymark/templates/i18n/es/TEMPLATE-ADR.md`
+
+### 5. Create Document
+
+Fill template with:
+- `id`: ADR-YYYY-MM-DD-NNN
+- `title`: Architectural decision title
+- `status`: **draft** (always for AI-created ADRs)
+- `created`: Current date
+- `updated`: Current date
+- `agent`: your runtime's canonical agent identity (see AGENT-RULES.md §1 — e.g. `claude-code-v1.0`, `gemini-cli-v1.0`, `codex-cli-v1.0`; do not assume Claude)
+- `confidence`: based on research done
+- `review_required`: **true** (always for ADRs)
+- `risk_level`: minimum `medium` for architectural decisions
+
+**Key sections to fill:**
+- Status: Note that this was created by AI agent
+- Context: Technical and business context, forces at play
+- Decision: The architectural decision with justification
+- Alternatives Considered: Other options with pros/cons/why not
+- Consequences: Positive, negative, neutral
+- Affected Components: Table of impacted parts
+- Implementation Plan: High-level steps
+- Success Metrics: How to validate the decision
+
+Save to: `.straymark/02-design/decisions/ADR-YYYY-MM-DD-NNN-description.md`
+
+### 6. Report Result
+
+```
+⚠️ ADR created (requires human review):
+ .straymark/02-design/decisions/ADR-YYYY-MM-DD-NNN-description.md
+
+ Status: draft
+ Review Required: YES
+
+StrayMark: Created ADR-YYYY-MM-DD-NNN-description.md (review required)
+```
+
+## Examples of Architectural Decisions
+
+- Use PostgreSQL over MongoDB for persistence
+- Adopt microservices vs. monolith architecture
+- Choose REST vs. GraphQL for API
+- Select authentication strategy (JWT, OAuth, etc.)
+- Define module boundaries and dependencies
+- Establish caching strategy
+
+> **Terminal compatibility**: If the terminal does not support box-drawing characters (Unicode), use plain-text formatting with dashes and pipes instead (e.g., `+--+` instead of `╔══╗`).
diff --git a/dist/.qwen/skills/straymark-aidec/SKILL.md b/dist/.qwen/skills/straymark-aidec/SKILL.md
new file mode 100644
index 00000000..fd2472ab
--- /dev/null
+++ b/dist/.qwen/skills/straymark-aidec/SKILL.md
@@ -0,0 +1,96 @@
+---
+name: straymark-aidec
+description: Create an AIDEC (AI Decision) document to record a technical decision with alternatives considered.
+allowed-tools: Read, Write, Glob, Bash(git diff *, git log *, git status *, date *, wc *)
+---
+
+# StrayMark AIDEC Skill
+
+Create an AI Decision (AIDEC) document to record technical decisions and the alternatives considered.
+
+## Instructions
+
+Use this skill when you've made a decision between multiple technical approaches and want to document the reasoning.
+
+### 1. Gather Context
+
+```bash
+# Get current date
+date +%Y-%m-%d
+
+# Summarize the CURRENT work (staged + unstaged + untracked) — label each block.
+# Avoid `HEAD~1`: it describes the previous commit, not the work being decided on.
+git diff --cached --stat # staged changes
+git diff --stat # unstaged changes
+git status --porcelain # includes untracked files
+```
+
+### 2. Confirm with User
+
+**Always confirm before creating:**
+
+```
+╔══════════════════════════════════════════════════════════════════╗
+║ StrayMark AIDEC ║
+╠══════════════════════════════════════════════════════════════════╣
+║ ║
+║ 📋 AIDEC documents a decision between alternatives. ║
+║ ║
+║ Please provide: ║
+║ 1. Decision title (what was decided) ║
+║ 2. Alternatives considered (2-3 options) ║
+║ 3. Which alternative was chosen and why ║
+║ ║
+╚══════════════════════════════════════════════════════════════════╝
+```
+
+### 3. Determine Sequence Number
+
+```bash
+# Count existing AIDECs for today
+ls .straymark/07-ai-audit/decisions/AIDEC-$(date +%Y-%m-%d)-*.md 2>/dev/null | wc -l
+```
+
+### 4. Check Language and Load Template
+
+Read `.straymark/config.yml` for language setting:
+- `en` (default): `.straymark/templates/TEMPLATE-AIDEC.md`
+- `es`: `.straymark/templates/i18n/es/TEMPLATE-AIDEC.md`
+
+### 5. Create Document
+
+Fill template with:
+- `id`: AIDEC-YYYY-MM-DD-NNN
+- `title`: Decision title from user
+- `created`: Current date
+- `agent`: your runtime's canonical agent identity (see AGENT-RULES.md §1 — e.g. `claude-code-v1.0`, `gemini-cli-v1.0`, `codex-cli-v1.0`; do not assume Claude)
+- `confidence`: based on decision clarity
+- `risk_level`: based on decision impact
+
+**Key sections to fill:**
+- Context: Why was a decision needed?
+- Problem: What specific problem needed solving?
+- Alternatives Considered: At least 2 options with pros/cons
+- Decision: Which was chosen and justification
+- Consequences: Positive, negative, and risks
+
+Save to: `.straymark/07-ai-audit/decisions/AIDEC-YYYY-MM-DD-NNN-description.md`
+
+### 6. Report Result
+
+```
+✅ AIDEC created:
+ .straymark/07-ai-audit/decisions/AIDEC-YYYY-MM-DD-NNN-description.md
+
+StrayMark: Created AIDEC-YYYY-MM-DD-NNN-description.md
+```
+
+## When to Create an AIDEC
+
+- Chose between frameworks/libraries
+- Selected an algorithm or approach
+- Decided on a data structure
+- Made a performance vs. readability trade-off
+- Selected between design patterns
+
+> **Terminal compatibility**: If the terminal does not support box-drawing characters (Unicode), use plain-text formatting with dashes and pipes instead (e.g., `+--+` instead of `╔══╗`).
diff --git a/dist/.qwen/skills/straymark-ailog/SKILL.md b/dist/.qwen/skills/straymark-ailog/SKILL.md
new file mode 100644
index 00000000..01cfac1f
--- /dev/null
+++ b/dist/.qwen/skills/straymark-ailog/SKILL.md
@@ -0,0 +1,96 @@
+---
+name: straymark-ailog
+description: Create an AILOG (AI Action Log) document for the current changes. Quick shortcut for the most common document type.
+allowed-tools: Read, Write, Glob, Bash(git diff *, git log *, git status *, date *, wc *)
+---
+
+# StrayMark AILOG Skill
+
+Quickly create an AI Action Log (AILOG) document for the current changes.
+
+## Instructions
+
+This is a shortcut skill that creates AILOG documents directly.
+
+### 1. Gather Context
+
+```bash
+# Get current date
+date +%Y-%m-%d
+
+# Get modified files
+git status --porcelain
+
+# Summarize the CURRENT work (staged + unstaged + untracked) — label each block.
+# Avoid `HEAD~1`: it summarizes the previous commit, not the work being logged.
+git diff --cached --stat # staged changes
+git diff --stat # unstaged changes
+git status --porcelain # includes untracked files
+```
+
+### 2. Confirm with User
+
+**Always confirm before creating:**
+
+```
+╔══════════════════════════════════════════════════════════════════╗
+║ StrayMark AILOG ║
+╠══════════════════════════════════════════════════════════════════╣
+║ ║
+║ 📊 Changes detected: ║
+║ • Files: [list of modified files] ║
+║ • Lines: [+X / -Y] ║
+║ ║
+║ 📝 Will create: ║
+║ AILOG-YYYY-MM-DD-NNN-[description].md ║
+║ ║
+║ Please provide a brief description of what was done: ║
+╚══════════════════════════════════════════════════════════════════╝
+```
+
+### 3. Determine Sequence Number
+
+```bash
+# Count existing AILOGs for today
+ls .straymark/07-ai-audit/agent-logs/AILOG-$(date +%Y-%m-%d)-*.md 2>/dev/null | wc -l
+```
+
+Next number = count + 1, formatted as 3 digits (001, 002, etc.)
+
+### 4. Check Language and Load Template
+
+Read `.straymark/config.yml` for language setting:
+- `en` (default): `.straymark/templates/TEMPLATE-AILOG.md`
+- `es`: `.straymark/templates/i18n/es/TEMPLATE-AILOG.md`
+
+### 5. Create Document
+
+Fill template with:
+- `id`: AILOG-YYYY-MM-DD-NNN
+- `title`: User-provided description
+- `created`: Current date
+- `agent`: your runtime's canonical agent identity (see AGENT-RULES.md §1 — e.g. `claude-code-v1.0`, `gemini-cli-v1.0`, `codex-cli-v1.0`; do not assume Claude)
+- `confidence`: based on change complexity
+- `risk_level`: based on files modified
+
+Save to: `.straymark/07-ai-audit/agent-logs/AILOG-YYYY-MM-DD-NNN-description.md`
+
+### 6. Report Result
+
+```
+✅ AILOG created:
+ .straymark/07-ai-audit/agent-logs/AILOG-YYYY-MM-DD-NNN-description.md
+
+StrayMark: Created AILOG-YYYY-MM-DD-NNN-description.md
+```
+
+## Risk Level Guidelines
+
+| Indicator | Risk Level |
+|-----------|------------|
+| Config/settings changes | low |
+| Business logic changes | medium |
+| Auth, security, payments | high |
+| Database schema, migrations | critical |
+
+> **Terminal compatibility**: If the terminal does not support box-drawing characters (Unicode), use plain-text formatting with dashes and pipes instead (e.g., `+--+` instead of `╔══╗`).
diff --git a/dist/.qwen/skills/straymark-architecture-sync/SKILL.md b/dist/.qwen/skills/straymark-architecture-sync/SKILL.md
new file mode 100644
index 00000000..a197e237
--- /dev/null
+++ b/dist/.qwen/skills/straymark-architecture-sync/SKILL.md
@@ -0,0 +1,80 @@
+---
+name: straymark-architecture-sync
+description: Keep the architecture model alive as code grows — wrap `straymark architecture sync` (append-only) to detect new source dirs / ADR components, surface them, reconcile against human edits, apply, and re-validate. Never re-refines from scratch. EXPERIMENTAL (Loom A1.3).
+allowed-tools: Read, Bash(straymark architecture sync *, straymark architecture validate *, git diff *, git status *, ls *)
+---
+
+# StrayMark Architecture Sync Skill
+
+Keep an already-refined architecture model current as the codebase grows. `straymark architecture sync` is **append-only**: it detects new top-level source directories and ADR components not yet in the model and appends them — it **never** clobbers your edits or your DrawIO geometry. This skill runs it as a guided dry-run → confirm → apply → re-validate loop.
+
+> ⚠️ **EXPERIMENTAL.** The `architecture` model and Loom are an opt-in Loom A1.x experiment — not part of the supported Framework/CLI contract. The model lives at `.straymark/architecture/{model.yml,plan.drawio}`. See `docs/adopters/LOOM.md`.
+
+## When to use this skill
+
+Trigger on any of:
+
+- A new module / source directory was added and the architecture model should reflect it.
+- A new ADR introduced components not yet in the model.
+- The operator asks to "update" or "refresh" the architecture model without re-refining it.
+
+If `.straymark/architecture/model.yml` does **not** exist yet, or is still a raw seed (every component in `unassigned`), use `/straymark-architecture` instead — sync extends a curated model, it does not create or refine one.
+
+## Instructions
+
+### 1. Dry-run — see what's new
+
+```bash
+straymark architecture sync # dry-run (default): lists components that would be added
+```
+
+Surface the proposed additions to the operator verbatim — each new component shows its id, globs, and any inferred links:
+
+```
+2 new components would be added (dry-run — pass --apply to write):
+ + internal-modules-billing (globs: internal/modules/billing/**) → links: core
+ + internal-modules-reports (globs: internal/modules/reports/**)
+```
+
+If it reports nothing new, stop and report **`Model is up to date — nothing to append.`**
+
+### 2. Reconcile against human edits
+
+Before applying, check the proposed additions against the curated model:
+
+- New components land in the placeholder `unassigned` layer — note that they will need reassigning to a real layer afterward (refine them the same way `/straymark-architecture` does).
+- Confirm the new ids don't collide with an existing `layer.id` (a `component.id` must never equal a `layer.id`).
+- **Confirm with the operator before writing** — sync is append-only but it still mutates `model.yml`.
+
+### 3. Apply
+
+```bash
+straymark architecture sync --apply # appends the new components to model.yml (+ plan.drawio cells if it exists)
+```
+
+It appends with a `# Added by 'straymark architecture sync'` marker and, when `plan.drawio` is a recognized DrawIO document, appends matching cells. Existing geometry and edits are untouched.
+
+### 4. Re-validate
+
+```bash
+straymark architecture validate # exits 1 on any signal
+```
+
+Resolve any new `undrawn` / `unmodeled` / `empty` signals, then remind the operator to **refine the appended components** — reassign them out of `unassigned` into real layers and wire their `links` (that's a `/straymark-architecture` refinement on just the new entries).
+
+## Report result
+
+Surface the CLI output verbatim and name the follow-up. Example:
+
+```
+✓ Appended 2 components to model.yml (billing, reports) — now in `unassigned`.
+ Next: reassign them to real layers + add links via /straymark-architecture, then /straymark-loom to view.
+```
+
+## What this skill does NOT do
+
+- **It does not re-refine the model.** It only appends what's new; the existing curated layers, labels, links, and geometry are left exactly as they are.
+- **It does not assign new components to real layers.** Appended components land in `unassigned`; refining them is a `/straymark-architecture` step.
+- **It does not run when no curated model exists.** Use `/straymark-architecture` to generate and refine the first model.
+
+> **Terminal compatibility**: If the terminal does not support box-drawing characters (Unicode), use plain-text formatting with dashes and pipes instead.
diff --git a/dist/.qwen/skills/straymark-architecture/SKILL.md b/dist/.qwen/skills/straymark-architecture/SKILL.md
new file mode 100644
index 00000000..986f88c2
--- /dev/null
+++ b/dist/.qwen/skills/straymark-architecture/SKILL.md
@@ -0,0 +1,99 @@
+---
+name: straymark-architecture
+description: Generate the architecture model and refine it in one guided pass — seed with `straymark architecture generate`, then reassign components into real layers, wire dependency links, sync the DrawIO, and validate to green. The agent-native counterpart to the manual DrawIO refinement. EXPERIMENTAL (Loom A1.x).
+allowed-tools: Read, Write, Edit, Glob, Grep, Bash(straymark architecture *, git diff *, git log *, git status *, ls *, find *)
+---
+
+# StrayMark Architecture Skill
+
+Drive the `generate → refine → validate` arc of the StrayMark architecture model from the agent window. `straymark architecture generate` mines *structure* (top-level source dirs → components); the model encodes *intent* (real layers, dependency links, human labels) that the filesystem does not contain. This skill performs the refinement a human would otherwise do in DrawIO, using the agent's knowledge of the repo, and iterates `validate` to green in a single pass.
+
+> ⚠️ **EXPERIMENTAL.** The `architecture` model, its on-disk format, and Loom are an opt-in Loom A1.x experiment — not part of the supported Framework/CLI contract. See `docs/adopters/LOOM.md`. The model lives at `.straymark/architecture/{model.yml,plan.drawio}`.
+
+## When to use this skill
+
+Trigger on any of:
+
+- The operator asks to "map the architecture", "set up the architecture model", or "refine the architecture seed".
+- A `model.yml` was just generated and every component still sits in the placeholder `unassigned` layer.
+- The operator wants the 2D/3D Loom views to show real layers and dependency arrows.
+
+If `.straymark/architecture/model.yml` already exists **and is already refined** (components assigned to real layers, links present), prefer `/straymark-architecture-sync` to extend it append-only instead of re-refining from scratch.
+
+## Instructions
+
+### 1. Generate the seed
+
+```bash
+straymark architecture generate # writes model.yml + plan.drawio
+straymark architecture generate --force # only if a seed already exists and the operator wants a fresh one
+```
+
+`generate` enriches from ADRs (C4 diagrams + "Affected Components" tables) **only if they exist**; with none, the seed is structure-only. It seeds every component into a placeholder `unassigned` layer and the `.straymark` stages 00–09 as placeholder layers. **The seed is a draft, not the answer** — the next step is where it becomes meaningful.
+
+### 2. Refine the model (the phase that matters)
+
+Read `model.yml` and the codebase, then edit `model.yml` to encode real intent:
+
+- **Replace the placeholder doc-stage layers with real architecture layers**, inferred from directory conventions and ADRs (e.g. `entrypoints`, `domain`, `persistence`, `web`). **Ask the operator when the layering is ambiguous** — do not guess at the system's intended shape.
+- **Reassign every component out of `unassigned`** into a real layer.
+- **Fix labels** to human names (`internal-modules-commshub` → "CommsHub").
+- **Tighten globs** so each component owns exactly its files.
+- **Infer `links`** between components from the import graph / directory structure / ADR "Affected Components" tables.
+
+The model schema (`.straymark/architecture/model.yml`):
+
+```yaml
+version: 0
+layers:
+ - { id: "domain", label: "Domain", order: 0 } # id is the join key from component.layer; order is render order (low first)
+components:
+ - id: "commshub" # stable join key to the DrawIO cell + status overlay
+ label: "CommsHub" # human label
+ layer: "domain" # MUST name an existing layer id
+ globs: ["internal/modules/commshub/**"] # the join to governance state
+ links: ["audittrail"] # list of target component ids (strings)
+ docs: [] # optional explicit doc ids; normally inferred via globs
+ external: false # true only for third-party / external systems
+```
+
+**Gotchas — each one costs a debugging cycle; pre-empt them:**
+
+- **A `component.id` must not equal any `layer.id`.** For a single-component layer use a suffixed id (`core` layer → `core-infra` component). fw-4.27.0 emits a clear error for this, but write the model so it never trips.
+- **`links` is a list of target component ids (strings)** — `["audittrail", "core"]`, **not** objects like `[{to: …}]` (which fails to parse).
+- **Never delete a layer that a component still points at** via `component.layer` — that yields `references unknown layer`. Reassign the components first, then drop the empty placeholder layer.
+- The placeholder `unassigned` layer is not required by the schema once empty — but every `component.layer` must still name a layer that exists.
+
+### 3. Sync the DrawIO so 2D shows arrows
+
+**3D renders edges from `model.yml` `links`; 2D renders them from `plan.drawio` edges.** Write **both**, or arrows show in only one view. After editing `model.yml`, update `plan.drawio` so each component has a vertex and each `link` has an edge between the matching vertices (the DrawIO cell id joins on the component `id`). Keep human-authored geometry where it exists.
+
+### 4. Validate — iterate to green
+
+```bash
+straymark architecture validate # text; exits 1 on any signal
+straymark architecture validate --output json
+```
+
+Resolve every signal, then re-run until it exits 0:
+
+- **`undrawn`** — a component with no cell in `plan.drawio` → add the vertex.
+- **`unmodeled`** — a DrawIO cell with no component in `model.yml` → add the component or remove the stale cell.
+- **`empty`** — a component whose globs match no files on disk → fix the globs.
+
+### 5. Report result
+
+Summarize what changed (layers created, components reassigned, links added) and surface the final `validate` output verbatim. Point the operator at the live views:
+
+```
+✓ Architecture model refined: 4 layers, 13 components, 10 links — validate is green.
+ Next: `/straymark-loom` up to see the 2D/3D overlay, or `straymark status --where` for the terminal view.
+```
+
+## What this skill does NOT do
+
+- **It does not invent the intended architecture.** When layering or boundaries are ambiguous, it asks the operator — the model encodes human intent, not a filesystem heuristic.
+- **It does not maintain the status overlay.** `active` / `in-progress` / `implemented` / `has-debt` / `uncharted` are computed live from governance signals every time you look; this skill authors *structure* only.
+- **It does not re-refine an already-curated model.** Use `/straymark-architecture-sync` (append-only) once real layers and links exist.
+
+> **Terminal compatibility**: If the terminal does not support box-drawing characters (Unicode), use plain-text formatting with dashes and pipes instead.
diff --git a/dist/.qwen/skills/straymark-audit-execute/SKILL.md b/dist/.qwen/skills/straymark-audit-execute/SKILL.md
new file mode 100644
index 00000000..3193d1f1
--- /dev/null
+++ b/dist/.qwen/skills/straymark-audit-execute/SKILL.md
@@ -0,0 +1,158 @@
+---
+name: straymark-audit-execute
+description: Execute an external audit of a Charter inside an auditor-side CLI (gemini-cli, claude-cli, copilot-cli, codex-cli). Reads the resolved audit prompt from the canonical location, audits with tool use, writes the report. Operator invokes one instance per CLI.
+allowed-tools: Read, Write, Glob, Grep, Bash(go vet *, go build *, go test *, cargo check *, cargo clippy *, cargo test --no-run, npm run lint, npm run typecheck, npm test --, pytest --co, mypy *, ruff check *, ls *, find *, wc *, git log *, git diff *, git status *)
+argument-hint: "CHARTER-NN [AUDITOR-SLUG] (auditor id is operator-provided, never self-detected)"
+---
+
+# StrayMark Audit Execute Skill
+
+Execute an external audit of a Charter inside this CLI session. Read the resolved audit prompt that StrayMark prepared at the canonical location, audit the implementation with tool use (citing `path:line` of files actually opened), and write the report at the canonical location for the audit-review skill to consolidate later.
+
+## When to invoke
+
+This skill runs **inside an auditor-side CLI** (gemini-cli, claude-cli, copilot-cli, codex-cli, or any agent runtime configured with read-only access to the adopter's repo). The operator opens the CLI in the repo, then invokes `/straymark-audit-execute `.
+
+The skill is the second step of the v1 audit cycle:
+
+1. In the main IDE: operator runs `/straymark-audit-prompt CHARTER-NN` → StrayMark writes `.straymark/audits/CHARTER-NN/audit-prompt.md`.
+2. **(this skill)** Operator opens an auditor-side CLI in the repo and runs `/straymark-audit-execute CHARTER-NN`. Repeat in N CLIs (recommended: ≥2 of different model families).
+3. When ALL audits commissioned have completed, operator returns to the main IDE and runs `/straymark-audit-review CHARTER-NN`.
+
+## Instructions
+
+### 1. Resolve the Charter
+
+Two positional arguments: `` and an optional `` — e.g. `/straymark-audit-execute CHARTER-06 deepseek-v4-pro`. The second argument is the operator-provided auditor identity (see step 2); it is never inferred from the CLI you are running in.
+
+**Case A — Charter provided** (`/straymark-audit-execute CHARTER-04 [AUDITOR-SLUG]`):
+Use the literal Charter value. Construct the audit dir path: `.straymark/audits/CHARTER-04/` — or, when the operator passed `--round