From 40b34def61a45fa23aaabb9cb6b9dfaa914aa1f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=20Villase=C3=B1or=20Montfort?= <195970+montfort@users.noreply.github.com> Date: Wed, 5 Aug 2026 22:05:30 -0600 Subject: [PATCH] =?UTF-8?q?feat(framework+cli):=20fw-4.41.0=20/=20cli-3.42?= =?UTF-8?q?.0=20=E2=80=94=20Qwen=20Code=20agent=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Qwen Code had zero integration. Verified against the installed runtime, its context-file resolver defaults to ["QWEN.md"] and AGENTS.md appears only in a system-prompt string listing files it must not modify — so a Qwen Code user received no StrayMark governance at all, a strictly worse position than Codex or Qoder, which are covered via AGENTS.md. Adds the surface: QWEN.md directive injection, dist/.qwen/skills/ (byte-for-byte mirror of the Claude variants — Qwen Code parses allowed-tools and resolves skills at both project and user scope), install-skills --agent qwen against $QWEN_HOME (the dir Storage.getGlobalQwenDir() resolves), validate --agent qwen, and remove cleanup. Two defects surfaced by the review are fixed alongside: - update-framework skipped every injection target absent from disk, so a new agent surface reached fresh inits only — while STRAYMARK.md § "Directive Injection Markers" documents that init, update-framework and repair all create missing targets. The code was the outlier; missing targets are now created, which is what makes QWEN.md land on existing installations. - README and CLI-REFERENCE (3 languages) claimed Qoder reads skills from ~/.qoder/skills/ "not from the project tree". The runtime resolves a Project scope and watches it, so install-skills --agent qoder is a convenience. Codex remains the only agent for which it is mandatory. Also: validate --agent generalized off its Codex-only shape (Qoder and Qwen legitimately carry allowed-tools, so the claude-only-key check is now conditional); new skill-mirror-parity CI job, since the .qoder mirror was guarded by a test the pipeline never ran; ADOPTION-GUIDE compatibility tables (3 languages) resynced with the README, which had been two releases ahead. Decision recorded in AIDEC-2026-08-05-001, work in AILOG-2026-08-05-003. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 30 ++ ...-2026-08-05-003-qwen-code-agent-surface.md | 148 ++++++++ ...-2026-08-05-001-qwen-code-agent-surface.md | 167 +++++++++ .straymark/follow-ups-backlog.md | 14 +- CHANGELOG.md | 31 ++ Cargo.lock | 2 +- README.md | 20 +- cli/Cargo.toml | 2 +- cli/src/commands/install_skills.rs | 36 +- cli/src/commands/remove.rs | 3 + cli/src/commands/update_framework.rs | 65 +++- cli/src/commands/validate.rs | 105 ++++-- cli/src/main.rs | 28 +- cli/tests/architecture_skill_test.rs | 40 ++- cli/tests/inject_test.rs | 52 ++- cli/tests/qwen_skills_test.rs | 142 ++++++++ dist/.qwen/skills/straymark-adr/SKILL.md | 117 +++++++ dist/.qwen/skills/straymark-aidec/SKILL.md | 96 ++++++ dist/.qwen/skills/straymark-ailog/SKILL.md | 96 ++++++ .../straymark-architecture-sync/SKILL.md | 80 +++++ .../skills/straymark-architecture/SKILL.md | 99 ++++++ .../skills/straymark-audit-execute/SKILL.md | 158 +++++++++ .../skills/straymark-audit-prompt/SKILL.md | 91 +++++ .../skills/straymark-audit-review/SKILL.md | 260 ++++++++++++++ .../skills/straymark-charter-new/SKILL.md | 116 +++++++ .../.qwen/skills/straymark-followups/SKILL.md | 102 ++++++ dist/.qwen/skills/straymark-loom/SKILL.md | 76 +++++ dist/.qwen/skills/straymark-mcard/SKILL.md | 323 ++++++++++++++++++ dist/.qwen/skills/straymark-new/SKILL.md | 192 +++++++++++ dist/.qwen/skills/straymark-sec/SKILL.md | 300 ++++++++++++++++ dist/.qwen/skills/straymark-status/SKILL.md | 132 +++++++ dist/.straymark/00-governance/AGENT-RULES.md | 4 +- .../00-governance/C4-DIAGRAM-GUIDE.md | 2 +- .../00-governance/DOCUMENTATION-POLICY.md | 2 +- .../FOLLOW-UPS-BACKLOG-PATTERN.md | 2 +- .../00-governance/QUICK-REFERENCE.md | 2 +- .../00-governance/i18n/es/AGENT-RULES.md | 4 +- .../00-governance/i18n/es/C4-DIAGRAM-GUIDE.md | 2 +- .../i18n/es/DOCUMENTATION-POLICY.md | 2 +- .../i18n/es/FOLLOW-UPS-BACKLOG-PATTERN.md | 2 +- .../00-governance/i18n/es/QUICK-REFERENCE.md | 2 +- .../00-governance/i18n/zh-CN/AGENT-RULES.md | 4 +- .../i18n/zh-CN/C4-DIAGRAM-GUIDE.md | 2 +- .../i18n/zh-CN/DOCUMENTATION-POLICY.md | 2 +- .../i18n/zh-CN/FOLLOW-UPS-BACKLOG-PATTERN.md | 2 +- .../i18n/zh-CN/QUICK-REFERENCE.md | 2 +- dist/STRAYMARK.md | 2 +- dist/dist-manifest.yml | 5 +- dist/dist-templates/directives/AGENTS.md | 3 +- dist/dist-templates/directives/QWEN.md | 54 +++ docs/adopters/ADOPTION-GUIDE.md | 5 +- docs/adopters/CLI-REFERENCE.md | 25 +- docs/i18n/es/README.md | 19 +- docs/i18n/es/adopters/ADOPTION-GUIDE.md | 5 +- docs/i18n/es/adopters/CLI-REFERENCE.md | 25 +- docs/i18n/zh-CN/README.md | 19 +- docs/i18n/zh-CN/adopters/ADOPTION-GUIDE.md | 5 +- docs/i18n/zh-CN/adopters/CLI-REFERENCE.md | 25 +- 58 files changed, 3204 insertions(+), 147 deletions(-) create mode 100644 .straymark/07-ai-audit/agent-logs/AILOG-2026-08-05-003-qwen-code-agent-surface.md create mode 100644 .straymark/07-ai-audit/decisions/AIDEC-2026-08-05-001-qwen-code-agent-surface.md create mode 100644 cli/tests/qwen_skills_test.rs create mode 100644 dist/.qwen/skills/straymark-adr/SKILL.md create mode 100644 dist/.qwen/skills/straymark-aidec/SKILL.md create mode 100644 dist/.qwen/skills/straymark-ailog/SKILL.md create mode 100644 dist/.qwen/skills/straymark-architecture-sync/SKILL.md create mode 100644 dist/.qwen/skills/straymark-architecture/SKILL.md create mode 100644 dist/.qwen/skills/straymark-audit-execute/SKILL.md create mode 100644 dist/.qwen/skills/straymark-audit-prompt/SKILL.md create mode 100644 dist/.qwen/skills/straymark-audit-review/SKILL.md create mode 100644 dist/.qwen/skills/straymark-charter-new/SKILL.md create mode 100644 dist/.qwen/skills/straymark-followups/SKILL.md create mode 100644 dist/.qwen/skills/straymark-loom/SKILL.md create mode 100644 dist/.qwen/skills/straymark-mcard/SKILL.md create mode 100644 dist/.qwen/skills/straymark-new/SKILL.md create mode 100644 dist/.qwen/skills/straymark-sec/SKILL.md create mode 100644 dist/.qwen/skills/straymark-status/SKILL.md create mode 100644 dist/dist-templates/directives/QWEN.md 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