Skip to content

fix(adapters): escape YAML frontmatter values consistently across all command adapters - #1447

Merged
clay-good merged 8 commits into
Fission-AI:mainfrom
hsusul:fix/command-adapter-yaml-escaping
Jul 28, 2026
Merged

fix(adapters): escape YAML frontmatter values consistently across all command adapters#1447
clay-good merged 8 commits into
Fission-AI:mainfrom
hsusul:fix/command-adapter-yaml-escaping

Conversation

@hsusul

@hsusul hsusul commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Description

Centralize and enforce YAML frontmatter value escaping across all tool command adapters.

Root Cause

Command adapters emitting YAML frontmatter (amazon-q, antigravity, auggie, bob, codebuddy, continue, costrict, crush, factory, github-copilot, iflow, junie, kiro, lingma, opencode, qoder, qwen, trae, zcode) either omitted string escaping or re-implemented local un-escaped/duplicate variants of escapeYamlValue. When command descriptions, names, categories, or tags contained YAML special characters (e.g. colons : , quotes ", newlines \n, carriage returns \r, or brackets []), generated command files contained invalid YAML frontmatter that failed YAML parsers.

Fix Details

  1. Exported escapeYamlValue and formatTagsArray from src/core/command-generation/yaml.ts as the single authoritative helper module.
  2. Updated escapeYamlValue to return "" when given an empty string so empty fields produce valid YAML scalars instead of unquoted trailing whitespace.
  3. Refactored all YAML-emitting command adapters (amazon-q, antigravity, auggie, bob, codebuddy, continue, costrict, crush, factory, github-copilot, iflow, junie, kiro, lingma, opencode, qoder, qwen, trae, windsurf, zcode) to import and use shared escapeYamlValue and formatTagsArray helpers.
  4. Added an exhaustive Vitest matrix in test/core/command-generation/adapters.test.ts verifying that frontmatter emitted across all YAML adapters parses cleanly with yaml.parse.

Verification

  • Vitest Suite: pnpm test (112 test files passed, 2275 tests passed)
  • Build: pnpm run build (Clean build completion)
  • Linter: pnpm run lint (0 ESLint errors)
  • TypeScript: pnpm tsc --noEmit (0 type errors)
  • Git Diff: git diff --check (Clean formatting, no trailing whitespace)

Summary by CodeRabbit

  • Bug Fixes

    • Improved generated command files across supported integrations by escaping YAML frontmatter fields (including name/description/category) so special characters render safely and consistently as quoted YAML strings.
    • Ensured empty descriptions render as "" and tag frontmatter values are emitted in a consistent YAML-safe array format.
  • Tests

    • Expanded YAML round-trip and adapter coverage for escaping/quoting behavior.
    • Updated golden/template parity checks and hashes to match the new frontmatter formatting.
  • Documentation

    • Updated bulk-archive workflow guidance/templates to clarify inline sync, verification ordering, and handling of skipped vs excluded deltas.

@hsusul
hsusul requested a review from TabishB as a code owner July 26, 2026 16:23
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

Shared YAML serialization now consistently quotes and escapes frontmatter values, including tag arrays. Command-generation adapters use the shared helpers, with tests covering YAML round trips. Archive workflows now require inline synchronization and main-spec verification before archiving.

YAML frontmatter generation

Layer / File(s) Summary
Shared YAML serialization
src/core/command-generation/yaml.ts
escapeYamlValue now emits escaped quoted scalars, and formatTagsArray serializes escaped tag arrays.
Adapter frontmatter migration
src/core/command-generation/adapters/*
Adapters use shared escaping and tag formatting, replacing duplicated local helpers where applicable.
Frontmatter validation
test/core/command-generation/*, test/commands/artifact-workflow.test.ts, test/core/init.test.ts
Tests verify quoted output and YAML round trips for special descriptions and tags.

Archive workflow synchronization

Layer / File(s) Summary
Archive execution contracts
skills/openspec-bulk-archive-change/SKILL.md, src/core/templates/workflows/*
Archive instructions require inline synchronization, included-delta verification before moving changeRoot, updated conflict paths, and separate sync-skipped outcomes.
Archive template validation
test/core/templates/skill-templates-parity.test.ts
Parity hashes and behavioral assertions cover synchronization, verification sequencing, delta semantics, and archive gating.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Suggested reviewers: tabishb, alfred-openspec

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: consistent YAML frontmatter escaping across command adapters.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/core/command-generation/adapters.test.ts`:
- Around line 18-21: Update the YAML adapter matrix in the adapter tests to
include kiroAdapter and lingmaAdapter alongside the existing YAML-emitting
adapters. Keep kilocodeAdapter excluded from yamlAdapters because its dedicated
coverage verifies that it does not emit YAML frontmatter.
- Around line 977-997: Update escapeYamlValue, used by the adapter formatFile
flow, to quote values that YAML would interpret as non-string scalars, including
true, null, and numeric values, and to exclude indicator-prefixed text such as
“- starts here” from plain scalars. Extend the existing YAML frontmatter tests
with round-trip cases asserting these values parse back as the original strings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 5b1b1ec2-de80-4b21-8370-9916276171d4

📥 Commits

Reviewing files that changed from the base of the PR and between 19d4171 and c8fe7fe.

📒 Files selected for processing (23)
  • src/core/command-generation/adapters/amazon-q.ts
  • src/core/command-generation/adapters/antigravity.ts
  • src/core/command-generation/adapters/auggie.ts
  • src/core/command-generation/adapters/bob.ts
  • src/core/command-generation/adapters/claude.ts
  • src/core/command-generation/adapters/codebuddy.ts
  • src/core/command-generation/adapters/continue.ts
  • src/core/command-generation/adapters/costrict.ts
  • src/core/command-generation/adapters/crush.ts
  • src/core/command-generation/adapters/factory.ts
  • src/core/command-generation/adapters/github-copilot.ts
  • src/core/command-generation/adapters/iflow.ts
  • src/core/command-generation/adapters/junie.ts
  • src/core/command-generation/adapters/kiro.ts
  • src/core/command-generation/adapters/lingma.ts
  • src/core/command-generation/adapters/opencode.ts
  • src/core/command-generation/adapters/qoder.ts
  • src/core/command-generation/adapters/qwen.ts
  • src/core/command-generation/adapters/trae.ts
  • src/core/command-generation/adapters/windsurf.ts
  • src/core/command-generation/adapters/zcode.ts
  • src/core/command-generation/yaml.ts
  • test/core/command-generation/adapters.test.ts

Comment thread test/core/command-generation/adapters.test.ts
Comment thread test/core/command-generation/adapters.test.ts Outdated
@clay-good
clay-good requested a review from a team as a code owner July 27, 2026 17:31

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The affected encoder code is unchanged from the reproduced hold: values beginning with a block indicator can produce invalid YAML, while true, null, and numeric-looking strings round-trip as other scalar types. Please serialize every frontmatter string safely and add table-driven round-trip coverage across the changed adapters, including Kiro and Lingma.

@clay-good

Copy link
Copy Markdown
Collaborator

Code review — verdict: good to merge

I was initially wary of this one — 23 files touching 20 adapters is exactly the shape that quietly changes output for every user. So I didn't review the diff; I generated the full artifact tree for all 21 affected tools on origin/main and on this branch and compared them directly. The result strongly supports merging.

1. Real need — yes, and it's bigger than the description suggests

origin/main is currently emitting invalid YAML. Generated frontmatter, before:

---
name: OPSX: Apply
description: "Implement tasks from an OpenSpec change (Experimental)"
argument-hint: "[command arguments]"
---

name: OPSX: Apply is a syntax error — a plain scalar can't contain ": ". Parsing it:

BASE  /.codebuddy/commands/opsx/apply.md   PARSE ERROR: Nested mappings are not allowed in compact mappings at line 1, column 7
BASE  /.crush/commands/opsx/apply.md       PARSE ERROR: Nested mappings are not allowed in compact mappings at line 1, column 7
BASE  /.qoder/commands/opsx/apply.md       PARSE ERROR: Nested mappings are not allowed in compact mappings at line 1, column 7
BASE  /.lingma/commands/opsx/apply.md      PARSE ERROR: Nested mappings are not allowed in compact mappings at line 1, column 7

Swept across every generated frontmatter file in the tree:

BASE (origin/main): 246 frontmatter files, 24 UNPARSEABLE
   affected tool dirs: .codebuddy, .crush, .lingma, .qoder
PR:                 246 frontmatter files,  0 UNPARSEABLE

So every command file OpenSpec generates for CodeBuddy, Crush, Lingma, and Qoder has broken frontmatter today. That's a shipping bug affecting four integrations, and it's invisible unless you actually parse the output — which is presumably why it survived this long. Good find.

2. Does it work — yes, all 24 fixed, nothing else broken

0 unparseable files after. And critically, the fix doesn't just make things parse — it makes them parse to the right values.

3. Breaking changes — none semantically, and I verified this rather than assuming

This was my main concern, because the diff also removes quotes in places (several adapters previously always-quoted description, and now only quote when needed). So I parsed both trees and deep-compared the resulting objects for every file that was already valid:

previously-broken files (excluded):   24
semantically IDENTICAL frontmatter:  222
semantically CHANGED frontmatter:      0
files with body (non-frontmatter) differences: 0

Zero semantic changes across 222 files, and zero body changes. Every quoting difference is stylistic — description: Archive a completed change... and description: "Archive a completed change..." parse to the identical string. That also transitively proves the five deleted local escapeYamlValue copies (claude, qwen, trae, windsurf, zcode) were behaviourally equivalent to the shared one for real inputs, which is the thing I'd otherwise have had to take on faith.

pnpm generate:skills produces no drift, so the committed skills/*/SKILL.md golden files are unaffected.

The one thing to be aware of: on the next openspec update, users of the quote-style-affected tools will see cosmetic diffs in their generated command files (quotes added or removed). Nothing breaks, but it's churn worth a line in the changeset — and I'd note there's no changeset in this PR, which there probably should be given it's a user-visible fix.

4. Scope — correctly surgical, and net-negative complexity

escapeYamlValue already existed in src/core/command-generation/yaml.ts. This adds 14 lines to it (''"", plus formatTagsArray) and deletes ~80 lines of duplicated or missing escaping across adapters. No new architecture; it removes five divergent copies of a helper and routes everything through the existing one. That's exactly the right direction.

On the tests

Full suite on this branch: 112 files / 2275 tests, all passing, matching your reported numbers.

For transparency: my first run showed 3 failures, but they did not reproduce on a clean re-run (that run took 332s vs. 45s, so it was almost certainly resource contention on my machine from parallel worktree builds, not this PR). I'm calling it green.

The one modified existing expectation is correct and worth stating explicitly:

-      expect(output).toContain('description: \n');
+      expect(output).toContain('description: ""');

That's not a weakened test — a bare description: parses as null, not empty string, so the old output silently changed the value's type. "" round-trips properly. Good catch.

Nit (optional, no action needed)

escapeYamlValue's needsQuoting regex includes ,, which is only special in YAML flow context ([...], {...}), not in block mappings. That's why the explore description picks up quotes:

- description: Enter explore mode - think through ideas, investigate problems, clarify requirements
+ description: "Enter explore mode - think through ideas, investigate problems, clarify requirements"

Harmless and still correct, but it's the source of most of the cosmetic churn above. Narrowing the regex would reduce the diff users see. That regex is pre-existing on main though, so tightening it is arguably its own change — I'd leave it, and just make sure the changeset mentions that generated files will shift.

Nice work — this is a well-targeted fix to a genuinely broken output path.

@hsusul
hsusul force-pushed the fix/command-adapter-yaml-escaping branch from 97cbe7b to 2b63f0f Compare July 27, 2026 19:40
@hsusul

hsusul commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Updated YAML frontmatter serialization so all string values are safely double-quoted and preserve their type across edge cases. Added round-trip coverage for all 24 YAML adapters, including dedicated Kiro and Lingma tests. All 960 command-generation tests pass.

@hsusul

hsusul commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Addressed review feedback in commit 8c632a8: aligned archive commands with /opsx:sync, made spec paths root-aware, and updated bulk archive to sync and verify each change before moving it. Regenerated committed skills and added regression coverage for template parity.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
test/core/command-generation/adapters.test.ts (1)

897-905: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale test rationale: "quoting is triggered by another special char" no longer applies.

This test's title/comment describes the old conditional-quoting behavior (backslash alone didn't trigger quoting, only : did). Since escapeYamlValue now always double-quotes and escapes every value unconditionally, the premise is outdated and may confuse future readers about why this assertion holds.

📝 Suggested comment update
-    it('should escape backslashes when quoting is triggered by another special char', () => {
-      // Backslash alone does not trigger quoting, but once quoting is on (via ':')
-      // every backslash must be doubled. Locks the replace(/\\/g, '\\\\') branch.
+    it('should escape backslashes in description', () => {
+      // Every value is now unconditionally double-quoted, so backslashes must
+      // always be doubled. Locks the replace(/\\/g, '\\\\') branch.
       const output = zcodeAdapter.formatFile({
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/core/command-generation/adapters.test.ts` around lines 897 - 905, Update
the test title and inline comments in the backslash-escaping case within the
relevant adapter test to reflect that values are now always double-quoted and
backslashes are escaped unconditionally. Keep the existing input and assertion
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/core/command-generation/adapters.test.ts`:
- Around line 897-905: Update the test title and inline comments in the
backslash-escaping case within the relevant adapter test to reflect that values
are now always double-quoted and backslashes are escaped unconditionally. Keep
the existing input and assertion unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 14af19e8-fa20-4755-ba9e-7bed75fba36d

📥 Commits

Reviewing files that changed from the base of the PR and between c8fe7fe and 456f006.

📒 Files selected for processing (9)
  • src/core/command-generation/adapters/continue.ts
  • src/core/command-generation/adapters/cursor.ts
  • src/core/command-generation/adapters/iflow.ts
  • src/core/command-generation/yaml.ts
  • test/commands/artifact-workflow.test.ts
  • test/core/command-generation/adapters.test.ts
  • test/core/command-generation/generator.test.ts
  • test/core/command-generation/yaml.test.ts
  • test/core/init.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/core/command-generation/adapters/continue.ts
  • src/core/command-generation/adapters/iflow.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@skills/openspec-bulk-archive-change/SKILL.md`:
- Around line 132-146: Honor recorded conflict-resolution exclusions by making
delta-spec sync and verification conditional: route excluded, unimplemented
deltas through an explicit “sync skipped” path and allow their archive without
requiring verification, while retaining sync and verification for included
deltas. Apply this in skills/openspec-bulk-archive-change/SKILL.md (132-146),
both corresponding sections of
src/core/templates/workflows/bulk-archive-change.ts (134-146 and 405-417), and
update test/core/templates/skill-templates-parity.test.ts (253-277) to assert
the rendered instructions preserve the skipped-sync path and omit verification
requirements for excluded deltas.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ac99c1df-b10d-4443-a904-b41d07d59e88

📥 Commits

Reviewing files that changed from the base of the PR and between 456f006 and 206522d.

📒 Files selected for processing (4)
  • skills/openspec-bulk-archive-change/SKILL.md
  • src/core/templates/workflows/archive-change.ts
  • src/core/templates/workflows/bulk-archive-change.ts
  • test/core/templates/skill-templates-parity.test.ts

Comment thread skills/openspec-bulk-archive-change/SKILL.md Outdated

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The YAML hold is resolved at 206522d: all 24 YAML adapters now round-trip the scalar edge cases as strings, root-aware inline sync/verification is covered, and build, lint, focused tests, full tests, and generated-skill parity are clean. One blocker remains in bulk archive: step 5 can exclude an unimplemented delta from sync, but step 8 still syncs and verifies every delta unconditionally; please carry the recorded inclusion/exclusion set into execution, sync and verify only included deltas, explicitly report excluded ones as “sync skipped,” and cover the mixed included/excluded path in both generated variants.

@hsusul

hsusul commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the remaining bulk-archive blocker in b3b037b. Both generated variants now carry the per-delta inclusion/exclusion decisions into execution. Only included deltas are synced and verified; excluded deltas are reported explicitly as sync skipped. Added mixed included/excluded regression coverage for both generated variants.

Resolve conflicts between this branch's bulk-archive rework and main's
runtime-context/artifact-rules guidance (Fission-AI#1062) by unioning both sides:

- archive-change: keep main's specs-instruction fetch gate; keep this
  branch's /opsx:sync naming in the opsx command variant.
- bulk-archive: step 8a syncs only includedDeltas AND carries main's
  specs-rule snapshot + artifact-rule scoping bullets; guidelines lists
  keep both sides' bullets.
- Regenerated skills/ and parity hashes rather than hand-merging them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator

Resolved the conflicts with main in 62c7eab (pushed to the fork branch — thanks for leaving maintainer-edit on).

What conflicted: only the archive/bulk-archive templates and their parity hashes. main had landed #1062 (fetch openspec instructions specs --json before any main-spec write; artifact rules scoped to written specs), which rewrote the same blocks this branch reworked for per-delta include/exclude.

How it was resolved — union, nothing dropped from either side:

  • archive-change.ts: kept main's specs-instruction fetch gate; kept this branch's /opsx:sync naming in the opsx command variant (the skill variant still says openspec-sync-specs, matching the parity assertion).
  • bulk-archive-change.ts: step 8a now syncs only includedDeltas and carries main's specs-rule-snapshot and artifact-rule-scoping bullets. Both guidelines lists carry both sides' bullets.
  • skills/*/SKILL.md and the parity hash tables were regenerated (pnpm build && pnpm generate:skills && pnpm regen:parity-hashes), not hand-merged.

Verified: no it() block lost from either side, pnpm-lock.yaml untouched, build + lint + tsc --noEmit clean, full suite 3095/3095 passing locally, and CI is now green on the merge commit (Lint & Type Check, Test on linux/macOS/Windows, Audit, Dependency Review).

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 62c7eab. The YAML serializers round-trip edge-case strings across all adapters, and bulk archive now carries per-delta included/excluded decisions through inline sync, verification, and sync-skipped reporting in both generated variants while preserving current runtime guidance. 1,094 focused tests, build, lint, generated-skill parity, the cross-platform suite, audit, dependency review, and release tracking all pass.

… close test gaps

Adversarial review of the merged branch turned up four gaps:

- Bulk archive tells the sync workflow to ignore `excludedDeltas`, but
  main's sync-specs calls `existingOutputPaths` the "complete list" of
  delta specs. An agent following both would sync the delta the caller
  withheld, step 8b would not catch it (it verifies only included
  deltas), and the run would still report `sync skipped`. Sync now
  honors a caller-supplied subset, mirroring the inline rule-snapshot
  handoff main already added.
- escapeYamlValue left C0/DEL/C1 control characters raw. The repo's own
  parser accepts them, so tests passed while stricter parsers used by
  other tools reject the document. Emit them as \xHH.
- The adapter matrix was a hand-maintained list driving only
  `description`, so raw interpolation in lingma's name/category/tags —
  and any newly registered adapter — passed green. It now derives from
  the registry and drives every string field.
- Four bulk-archive template lines were guarded only by golden hashes,
  which this repo regenerates as routine.

Also corrects the escapeYamlValue docstring, which still described the
pre-PR conditional-quoting behavior, and adds the missing changeset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator

Ran four adversarial review passes over the merged branch (YAML correctness, template integrity, mutation testing, downstream blast radius). Fixes pushed in 62bb846.

One real functional defect, introduced by the merge. Step 8a tells the sync workflow to sync only includedDeltas and ignore the rest — but main had independently hardened sync-specs.ts to call artifactPaths.specs.existingOutputPaths the "complete list of delta spec files" (permissive at the merge base, which is what this branch was written against). An agent following both would sync the delta bulk archive deliberately withheld; step 8b wouldn't catch it, because it verifies only included deltas; and the run would still report sync skipped. Sync now honors a caller-supplied subset, mirroring the inline rule-snapshot handoff main already added for the same caller.

Escaping hardened one class further. escapeYamlValue left C0/DEL/C1 control characters raw. The yaml package this repo tests with accepts them; js-yaml and PyYAML reject the document outright — so the suite would stay green while the command failed to load in other tools. They now emit as \xHH.

Two test gaps that mattered:

  • The adapter matrix was a hand-maintained list driving only description. Raw interpolation in lingma's name, category, or tags survived, and a newly registered adapter with raw interpolation shipped 960/960 green. It now derives from CommandAdapterRegistry.getAll() minus an explicit non-YAML set, and drives every string field. Verified by re-running each mutation: all now fail 27 tests.
  • Four bulk-archive template lines were guarded only by the golden hash, which this repo regenerates as routine. They have assertions now.

Also corrected the escapeYamlValue docstring (it still described the pre-PR conditional-quoting behavior) and added the missing changeset — this PR carries three user-visible changes and had no release tracking.

Left alone deliberately, as pre-existing and outside this PR:

  • src/core/shared/skill-generation.ts:139 builds SKILL.md frontmatter by raw interpolation — the same bug class, on the skills surface. Worth a follow-up.
  • adapters/gemini.ts:23 interpolates a description raw into TOML.
  • The bulk-archive skill variant is missing a blank line before step 4, so CommonMark lazy-continues the heading into the previous bullet. Present on main at the same line.

Full suite 3113 passing, lint and tsc --noEmit clean, generated skills and hashes regenerated.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 62bb846. The latest fixes correctly preserve caller-supplied delta subsets in both sync variants, escape C0/DEL/C1 control characters for strict YAML parsers, and derive adapter/string-field coverage from the registry. Build, lint, 1,112 focused tests, strict spec validation, generated-skill parity, cross-platform CI, audit, dependency review, and release tracking all pass.

@clay-good
clay-good self-requested a review July 28, 2026 02:22
A second adversarial pass found the previous fix incomplete. Narrowing
step 3 ("Find delta specs") left step 4 — the loop that actually applies
the changes — still reading "for each capability delta spec path
returned by the CLI". An agent treating step 3 as descriptive and step 4
as operative re-widens to the full list and syncs the delta bulk archive
withheld: the original defect, one step further down the template.

Step 4 now iterates the step-3 selection, and the parity test pins both
the new wording and the absence of the old.

Also from that pass:
- Generalize the carve-out beyond archive. It was conditioned on
  "archive invoked this workflow inline", so a user asking /opsx:sync to
  sync one delta read as an instruction to ignore them.
- Define the two undefined edges: a named path outside
  existingOutputPaths, and an empty named list. Both stop and report
  rather than proceeding on a guess.
- Drive control characters through the adapter matrix. It drove none, so
  the escaping this suite exists to prove had no adapter-level coverage
  and the raw-CR assertion could never fail. Verified live by mutation.
- Give contentDerivedFields two markers that differ in length and shape.
  Same-shaped markers render identically for a length- or slice-derived
  field, which would drop it from every assertion silently.

Drops the `not.toContain('complete list of delta spec files')`
assertion: it banned one exact synonym while any reword of the same
conflicting instruction passed, so it read as coverage without being it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@clay-good

Copy link
Copy Markdown
Collaborator

Second adversarial round, on the previous round's own fixes rather than the original diff. Pushed as 5bfd574.

The earlier fix was incomplete, in the same way and for the same reason. Narrowing step 3 ("Find delta specs") and the guardrails left step 4 — the loop that actually applies the changes — still reading "For each capability delta spec path returned by the CLI." An agent that reads step 3 as descriptive and step 4 as operative re-widens to the full list and syncs the delta bulk archive withheld. Same wrong outcome as the original defect, one step further down the template. Step 4 now iterates the step-3 selection, and the parity test pins both the new wording and the absence of the old, verified by mutation.

Two more from that pass:

  • The carve-out was conditioned on archive invoking sync, so a user asking /opsx:sync to sync only one delta read as an instruction to ignore them. Generalized to any caller.
  • A named path outside existingOutputPaths, and an empty named list, were both undefined. Both now stop and report rather than proceeding on a guess.

The control-character escaping was independently validated against js-yaml, PyYAML and the repo's yaml. Worth recording what the probes showed, because the bug is worse than "strict parsers reject it": for 0x7f/0x85/0x9f the character is silently dropped by js-yaml and the repo's own parser, and PyYAML turns NEL into a space. \x85 is the only form that round-trips in all three. Escaping every code point 0x00–0xFF now round-trips with 0 mismatches across all three parsers, and output is byte-identical for all 12 real command templates across all 28 adapters — the change is purely defensive.

Test hardening, both found by mutating the previous round's tests:

  • The matrix drove no control character at all, so the escaping this suite exists to prove had zero adapter-level coverage and the raw-CR assertion was dead code. Six control-char cases added; confirmed the assertion now fires.
  • contentDerivedFields used two same-shaped markers, which would render identically for a length- or slice-derived field and drop it from every assertion silently. Markers now differ in length and shape.

Exhaustive mutation of the rebuilt matrix — every adapter × every content-derived field × all cases — found 0 survivors, plus 14 in-tree mutations all killed.

I also dropped the not.toContain('complete list of delta spec files') assertion I added last round. It banned one exact synonym while "the authoritative list" would have sailed through reintroducing the same conflicting instruction — coverage-shaped, but not coverage.

Non-blocking, not fixed here: sync's success output has no place to list withheld paths, so a caller can't cross-check what was skipped; and gemini.ts interpolates a description raw into TOML (pre-existing, correctly outside the YAML adapter set).

3257 tests passing, lint and tsc --noEmit clean, CI green.

@alfred-openspec alfred-openspec left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 5bfd574. The sync workflow now carries the step-3 delta selection into the operative step-4 loop, stops on out-of-scope or empty named subsets, and supports caller narrowing beyond archive without widening back to the CLI list. The adapter matrix now exercises control characters and uses shape/length-distinct markers. Build, lint, 1,256 focused tests, strict spec validation, generated-skill parity, cross-platform CI, audit, dependency review, and release tracking all pass.

@clay-good
clay-good added this pull request to the merge queue Jul 28, 2026
Merged via the queue into Fission-AI:main with commit fb19699 Jul 28, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants