Skip to content

feat(repo): guidance:format=xml and guidance:as-skill arms - #333

Open
napetrov wants to merge 17 commits into
intel:mainfrom
napetrov:ce-formats
Open

napetrov wants to merge 17 commits into
intel:mainfrom
napetrov:ce-formats

Conversation

@napetrov

@napetrov napetrov commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two new repo run arms that hold the guidance content fixed and change only its shape, so a per-model comparison can say which packaging an agent follows.

guidance:raw            AGENTS.md as is (existing)
guidance:format=xml     same text; each heading -> <section level="N" title="..">
                        restore(render(text)) == text is checked before write
guidance:as-skill       same text, moved into one skill:
                        .claude/skills/<repo>-guidance/SKILL.md
                          ## File: AGENTS.md
                          <verbatim text>
                          ## File: sub/AGENTS.md
                          <verbatim text>
_apply_arm (workspace.py, step 5)
  guidance:format=<fmt>  -> _apply_format: to_format() per guidance file, FormatError -> PrepError;
                            a symlink must reach a matched guidance file, else PrepError
  guidance:as-skill      -> _apply_as_skill: one SKILL.md, guidance files removed,
                            packaged skill recorded in meta["skills"] (skill_usage sees it)

Not in this PR:

  • guidance:format=md is refused (it is guidance:raw).
  • POML is not offered. The documented POML components (role, task, example, document, table, img, stylesheet, let) have no heading element, so a headings-to-tags mapping needs a decision first.

Evidence

  • Before: guidance:format=xml and guidance:as-skill are unknown arms; parse_arm rejects them.
    After: tests/test_repo_guidance_format.py (54 collected, offline): round-trip over 11 inputs (CRLF, #no-space, seven hashes, tilde fences, a title with </section> and quotes); fenced # lines stay body text, with CommonMark fence rules (up to three spaces of indent, no backtick in a backtick opener's info string, a close needs the same character, at least the opener's length, and no info string); a body line that looks like a tag is refused; a non-UTF-8 or non-round-tripping file is a PrepError; a link is resolved as the filesystem resolves it (through linked directories), and a dangling link, a cycle, or a target that is not a matched guidance file is a PrepError; writes are byte-exact (no newline translation); the packaged skill's frontmatter parses with yaml.safe_load to exactly {name, description}; a base that ships its own skill keeps it beside the packaged one.
  • Guard: .claude/skills as a link, or a dangling link at the skill directory, is refused with the host left empty (tests in the same file).
  • Mutation checks, each made to fail a test: dropping the ~{3,} fence, dropping the info-string close condition, dropping or skill_dir.is_symlink(), reducing the guard loop to .claude; and the converter and frontmatter checks from round 1.
  • Full suite at 9b98c6a: 12076 passed, 8 skipped, rc=0 (local, Python 3.12 venv). Targeted files: 120 passed. ruff clean on touched files.
  • make ci-local on e734e16 (round 1): 0 FAIL, 30 pass, 7 skip, exit 0. Two timing-sensitive tests (test_reference_checks regex limit, test_repo_environment unbalanced quote) exceeded their budgets once under box load; both passed alone and on a clean rerun.
  • Self-review (pr-review, local mode): round 1 found three blockers, fixed in e734e16: SKILL.md description with : (invalid YAML), non-UTF-8 guidance in as-skill raising UnicodeDecodeError (now a PrepError), and a silently dropped symlinked guidance file (now packaged once, or a PrepError). Round 2 and round 3 (fresh sonnet pass over each round diff): 0 blocking.

Merge Danger

Door: two-way. Two new arm names and an additive grammar; no persisted format, no existing arm changed. Existing guidance:raw|none|bridge paths are untouched. Only the new arms write different files.

Known limits, stated so the results are read correctly:

  • guidance:as-skill uses one fixed sentence as the skill description (GUIDANCE_SKILL_DESCRIPTION). Whether the agent loads the skill depends on that sentence, so as-skill results measure "this description" as well as the packaging. The benchmarking guide says so.
  • Nested guidance files lose their directory scope when packaged (sub/AGENTS.md is read as a ## File: section, so the agent is not told it applies only under sub/).
  • No result data yet. Nothing was run against a real repo with docker and an LLM; that is the follow-up measurement.

Blast Radius: low. Only runs that select the new arms are affected. guidance:format and guidance:as-skill refuse a guidance symlink they cannot follow to a matched file (before, it was silently left as is or dropped). Docs: repo-evaluation-contract.md §3 step 5 and a new "Guidance formats and packaging" section in benchmarking-and-comparison.md. One changelog fragment (### Added).

🤖 Generated with Claude Code

Two guidance parts for `repo run --arm`, to settle format and packaging
debates with data: the same guidance content in another shape, compared
per model.

- guidance:format=xml renders each guidance file as a <guidance> element
  with one <section level title> per Markdown heading. The rendering must
  restore to the original file exactly, or the cell's preparation fails.
- guidance:as-skill removes the guidance files, as guidance:none does, and
  packages their text into .claude/skills/<repo>-guidance/SKILL.md; the
  skill is recorded in `skills`, so usage is measured like any skill.

guidance:format=md is refused (it is guidance:raw). POML is not offered:
its documented components have no heading tag.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…-skill

Self-review of 78457e2 found three faults in the new arms:

- The packaged SKILL.md had a description with ": " in a plain YAML
  scalar, so the frontmatter did not parse and the skill could not load.
- A guidance file that is not UTF-8 raised UnicodeDecodeError instead of a
  PrepError for the cell (guidance:format and guidance:as-skill share the
  reader now).
- A symlinked guidance file was dropped without a trace; a link to another
  guidance file is now packaged once under its target, and a link to any
  other file fails the cell.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Copilot AI balanced review requested due to automatic review settings November 1, 2026 01:27
@napetrov
napetrov requested a review from a team as a code owner November 1, 2026 01:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved defects misparse fenced code as guidance sections, corrupt source newlines on Windows, and incorrectly reject combined guidance-as-skill and skill installation arms.

3 open issues
What changed in this PR

3 new findings.

This PR adds two repo run arms for comparing how guidance packaging affects agent behavior while keeping the model and guidance content fixed.

Changes:

  • Adds XML-style heading conversion with a round-trip check.
  • Adds packaging of repository guidance into one discoverable skill, recorded for usage measurement.
  • Adds regression tests and documents the new arms and their limitations.
File Description
dokimi/​tests/​test_repo_guidance_format.py Tests conversion, packaging, parsing, and error handling.
dokimi/​dokimi/​repos/​workspace.py Integrates the new arms into workspace preparation and skill metadata.
dokimi/​dokimi/​repos/​guidance_format.py Implements reversible heading-to-section conversion.
dokimi/​docs/​repo-evaluation-contract.md Adds the arms to the workspace preparation contract.
dokimi/​docs/​benchmarking-and-comparison.md Explains comparisons, behavior, and known limitations.
dokimi/​changelog.d/​20261031_120000_napetrov_guidance-format-arms.md Announces the new guidance arms.

Review effort: Balanced

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
Comment on lines +62 to +63
if _FENCE.match(line):
fenced = not fenced

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: a fence closes only on a run at least as long as its opener, of the same character (CommonMark-like).
A four-backtick block with a three-backtick line inside now keeps the comment inside the block. Test added.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

Comment on lines +472 to +473
if arm.base == "guidance:as-skill":
return _apply_as_skill(ws, manifest, matched)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: guidance:as-skill now drops the base skill of the same name it packages (the drops rule in arms.py), so check_arms does not refuse it.
Test added in test_repo_skill_arms.py.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maintainer chose: add the as-skill no-collision test before merge. Added in 9b98c6a: a base that ships .claude/skills/lint keeps it, and guidance:as-skill installs its own skill beside it.
Full suite at 9b98c6a: 12076 passed. Left unresolved for your check.

Comment thread dokimi/dokimi/repos/workspace.py Outdated
rendered = to_format(_guidance_text(ws, rel, f"guidance:format={fmt}"), fmt)
except FormatError as exc:
raise PrepError(f"guidance:format={fmt}: {rel}: {exc}") from exc
(ws / rel).write_text(rendered, encoding="utf-8")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: guidance and the packaged SKILL.md are written as bytes (UTF-8), so no newline translation runs after the round-trip check.
The CRLF case cannot be reproduced on Linux; the fix is by code, not by a test.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

@exai-bot

exai-bot commented Nov 1, 2026 •

Copy link
Copy Markdown

🔍 OpenCodeReview found 1 issue(s) in this PR.

By severity: 1 medium

Severity Category Location Finding
medium bug guidance_format.py:143-144 Must-fix: stripping the list marker before checking _BLOCK makes a blockquote paragraph set para=False, although CommonMark allows it...
Review details
  • Commit reviewed: d89a240 against merge base 78fb39b
  • Coverage: 4 files in 2 groups
  • Engine: OpenCodeReview v1.13.3, us.openai.gpt-6.1-sol
  • Usage: 166k tokens (73k from cache), 1m20s
  • Tool calls: 16 (14 file_read, 1 code_comment, 1 code_search)
  • Arms repository and skill tests: 2 files
  • Guidance formatting and tests: 2 files

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
Comment on lines +62 to +63
if _FENCE.match(line):
fenced = not fenced

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: fence state changes on any backtick or tilde prefix, regardless of the opening delimiter and length. For input "\n```\n# example\n", the three-backtick line incorrectly closes the four-backtick fence and # example becomes a section even though it is code. Mixed delimiters and lines such as ```text inside a fence cause the same problem. Round-trip equality still passes, so it cannot detect this semantic change. Track the opening character and run length, and close only on a matching delimiter of sufficient length with whitespace-only trailing content. Add regression tests for these cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: same fence fix as the thread above: a close needs a run at least as long as the opener, of the same character.
Tests added in test_repo_guidance_format.py.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

Comment on lines +472 to +473
if arm.base == "guidance:as-skill":
return _apply_as_skill(ws, manifest, matched)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: update the arm preflight to recognize that this arm removes matched files. repo run calls arms.check_arms() before preparation, but its drops condition recognizes only guidance:none and revision arms. For a base containing .claude/skills/lint/SKILL.md, guidance globs including .claude/skills/**, and guidance:as-skill+skill:skills/lint, preflight incorrectly rejects the arm as already shipping lint. Preparation would package and remove that file, then _install_skills() would replace the emptied directory successfully. Include guidance:as-skill in the preflight removal condition and add a regression covering check_arms() plus preparation (the existing removal test in test_repo_skill_arms.py provides a fixture).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: same arm-preflight fix as the thread above (drops in arms.py covers as-skill).
The test checks an as-skill arm against a base that ships the skill.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

Comment on lines +493 to +495
if (ws / rel).is_symlink():
links.append(rel)
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: this silently leaves guidance in Markdown when its symlink target is not matched by the guidance globs. For example, with AGENTS.md -> docs/guide.md, docs/guide.md containing # Guide\n, and globs matching only AGENTS.md, _apply_format returns success with no files rendered. The XML arm then measures exactly the raw guidance despite being reported as a format treatment; recording the link in metadata does not prevent that misleading comparison. Validate that each link ultimately reaches a regular guidance file being rendered, and raise PrepError otherwise (or explicitly render its safely resolved target). Add a regression test for this unmatched-target case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: a link that ends outside the matched guidance files is a PrepError in guidance:format (_link_file), not a silent leftover.
Test added in test_repo_guidance_format.py.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

Comment thread dokimi/dokimi/repos/workspace.py Outdated
rendered = to_format(_guidance_text(ws, rel, f"guidance:format={fmt}"), fmt)
except FormatError as exc:
raise PrepError(f"guidance:format={fmt}: {rel}: {exc}") from exc
(ws / rel).write_text(rendered, encoding="utf-8")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: write_text uses platform-default newline translation. On Windows, LF guidance becomes CRLF, and preserved CRLF becomes CRCRLF because _guidance_text decodes raw bytes without newline normalization. The restore check runs before this write, so the actual file violates the exact-restoration guarantee. The SKILL.md write in _apply_as_skill has the same defect and violates verbatim packaging. Use write_bytes(text.encode("utf-8")) or write_text(..., newline="") at both sites, and verify on-disk bytes rather than newline-normalizing read_text() results.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: same byte-level write as the thread above, for guidance:format and SKILL.md.
No platform newline translation is left after the round trip.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

Comment thread dokimi/dokimi/repos/workspace.py Outdated
Comment on lines +528 to +531
target = posixpath.normpath(posixpath.join(posixpath.dirname(rel), os.readlink(ws / rel)))
if target not in matched:
raise PrepError(f"guidance:as-skill: {rel} links to {target}, which is not a guidance file")
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: membership of the immediate target does not establish that a link ultimately reaches a packaged regular file. With AGENTS.md → CLAUDE.md and CLAUDE.md → AGENTS.md, both targets are in matched, both files are skipped and deleted, and preparation succeeds with a frontmatter-only skill. The base checkout does not reject these cycles; _guidance_at validates links only for revision arms. Resolve each chain with cycle detection and require it to terminate at a regular matched guidance file before deleting anything. Add a cyclic-link regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in cf6ced2: _link_file follows the chain to its final target and checks membership there, and refuses a cycle with a PrepError.
Tests added for the chain and the cycle.
Pushed to ce-formats (cf6ced2); left unresolved for your check.

@codecov-commenter

codecov-commenter commented Nov 1, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 97.97980% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 94.31%. Comparing base (d3e2a26) to head (d89a240).
⚠️ Report is 8 commits behind head on main.

Files with missing lines Patch % Lines
dokimi/dokimi/repos/guidance_format.py 98.11% 1 Missing and 1 partial ⚠️
dokimi/dokimi/repos/workspace.py 97.26% 1 Missing and 1 partial ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #333      +/-   ##
==========================================
+ Coverage   94.20%   94.31%   +0.10%     
==========================================
  Files         304      309       +5     
  Lines       38748    39511     +763     
  Branches     6561     6692     +131     
==========================================
+ Hits        36504    37266     +762     
- Misses       1430     1431       +1     
  Partials      814      814              
Files with missing lines Coverage Δ
dokimi/dokimi/commands/repo_run.py 94.38% <100.00%> (ø)
dokimi/dokimi/repos/arms.py 96.75% <100.00%> (+0.28%) ⬆️
dokimi/dokimi/repos/guidance_format.py 98.11% <98.11%> (ø)
dokimi/dokimi/repos/workspace.py 91.06% <97.26%> (+0.88%) ⬆️

... and 20 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

- a fence closes only on a run at least as long as its opener, of the same
  character, and never inside a longer run of backticks or tildes
- guidance:as-skill and guidance:format follow symlink chains: a chain that
  cycles or ends outside the matched guidance files is a prep error
- guidance:as-skill removes the base skill of the same name the arm packages,
  so check_arms does not refuse it (drops rule also covers as-skill)
- guidance files and the packaged SKILL.md are written as bytes, so no
  platform newline translation runs after the round-trip check

Thread: PRRT_kwDOVBg7g86yWVOg
Thread: PRRT_kwDOVBg7g86yWVZ2
Thread: PRRT_kwDOVBg7g86yWVOl
Thread: PRRT_kwDOVBg7g86yWVZ4
Thread: PRRT_kwDOVBg7g86yWVOr
Thread: PRRT_kwDOVBg7g86yWVZ8
Thread: PRRT_kwDOVBg7g86yWVZ6
Thread: PRRT_kwDOVBg7g86yWVZ_
Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
Comment on lines +61 to +62
if fence is None:
return match.group(1) if match else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: validate Markdown fence openers before entering fence state. For input "```foobar\n# Heading\n", the backtick in the information string makes the first line invalid as a Markdown fence, but this branch opens one and leaves the real heading unconverted. Similarly, _FENCE` accepts a leading tab via `\s{0,3}`, although that is four-column indentation (indented code), not a fence opener. Both cases pass the round-trip check while producing the wrong section structure. Restrict indentation to up to three literal spaces and reject backtick openers whose information string contains a backtick; add tests asserting that subsequent headings become sections.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6bd48b4. An opener is a fence only with up to three literal spaces of indent, so a tab is body text. A backtick opener whose info string holds a backtick is not a fence (CommonMark).
Tests assert that the heading after each such line is a section.
Left unresolved for your check.

Comment thread dokimi/dokimi/repos/workspace.py Outdated
chain cycles or ends outside the matched guidance files: that file would be dropped or left unrendered."""
chain = [rel]
while (ws / chain[-1]).is_symlink():
target = posixpath.normpath(posixpath.join(posixpath.dirname(chain[-1]), os.readlink(ws / chain[-1])))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: lexical normalization is not equivalent to filesystem symlink resolution when .. follows a symlinked directory. For example, let sub -> other/sub, CLAUDE.md -> sub/../AGENTS.md, and have both root AGENTS.md and other/AGENTS.md, with only root AGENTS.md and CLAUDE.md matched. This code normalizes the link to root AGENTS.md and accepts it, but the filesystem resolves it to other/AGENTS.md. The format arm therefore leaves CLAUDE.md pointing to unrendered guidance, and the as-skill arm discards its distinct content. Resolve the actual target strictly, verify it is a regular file within the workspace and among the matched files, and translate dangling/cyclic-link errors into PrepError. Add this case for both callers.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 6bd48b4. _link_file resolves the link with Path.resolve(strict=True), as the filesystem does, so a .. after a linked directory reaches the file the filesystem reaches. That file must be a regular file inside the workspace and one of the matched guidance files.
A dangling link or a cycle is a PrepError. Tests cover guidance:format and guidance:as-skill: the linked-directory case, a link in a subdirectory, and a dangling link.
Left unresolved for your check.

@antonwolfy antonwolfy 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.

Review at cf6ced2. The arm wiring, the drops change and the byte-level writes hold up, and the earlier fence, cycle and CRLF points are fixed. Three things are worth fixing before merge, and there is one nit.

What I ran, on a Linux review host at cf6ced2:

  • make ci-local: 28 passed, 2 failed, 7 skipped. 12,013 tests passed, changed-line coverage passes. The two FAILs are Install gitleaks (a download that returned exit 22) and Scan commits, which depends on it. Neither reads the PR's tree.
  • Reproductions for the code findings, with no model or network involved.
  • A mutation run of 57 hand-made mutants against the new tests: 13 pass the full suite. The ones that decide something are in the inline comments.

Nit (outside this diff): the arm lists were not updated everywhere. dokimi/dokimi/commands/repo_run.py:463 (--arm help) still lists guidance:none | guidance:raw | guidance:bridge | guidance:rev:REV | patch:FILE | ..., so repo run --help omits the two new arms while the unknown-arm error, built from ARM_FORMS, lists them. dokimi/docs/preparing-a-repository.md:9 and :251 name only none, raw and bridge. Suggestion: add guidance:as-skill and guidance:format=xml to the help string and the line at :9.

All commits carry a Signed-off-by.

Bot points I checked that no longer apply at this head (8)
  • Copilot's point on a four-backtick block holding a three-backtick line does not apply: a fence now closes only on a run of the same character at least as long as its opener (guidance_format.py:63).
  • Copilot's point on guidance:as-skill+skill:... being refused by preflight does not apply: arms.py:200 now includes guidance:as-skill in drops, and the new test fails if it is removed.
  • Copilot's point on newline translation in write_text does not apply: both writers use write_bytes on UTF-8 bytes.
  • exai-bot's point on mixed fence delimiters and lengths does not apply: the close check compares character and length.
  • exai-bot's point on the preflight not knowing the removal arm does not apply: same drops change at arms.py:200.
  • exai-bot's point on a symlink whose target is outside the matched guidance files does not apply: _link_file raises a PrepError for it.
  • exai-bot's point on write_text newline translation in both writers does not apply: both write bytes.
  • exai-bot's point on a link cycle between two matched files does not apply: _link_file follows the chain and refuses a cycle.

Comment thread dokimi/dokimi/repos/guidance_format.py Resolved
Comment thread dokimi/dokimi/repos/workspace.py Resolved
Comment thread dokimi/dokimi/repos/workspace.py Resolved
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>

@antonwolfy antonwolfy 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.

I pushed a commit per nit, up to 1db497b, which fix the nit from my review:

  • 1db497b: dokimi/dokimi/commands/repo_run.py:463: the --arm help now lists guidance:as-skill and guidance:format=xml; dokimi/docs/preparing-a-repository.md:9 names both arms too.

make ci-local at 1db497b: 30 passed, 0 failed, 7 skipped.

Not approving yet. Still open: the three risks from my review.

…guard tests

A fence opener follows CommonMark: up to three spaces (not a tab), and a
backtick fence's info string holds no backtick. A link of a guidance file
is resolved as the filesystem resolves it, so a `..` after a linked
directory reaches the file it names, and a dangling link or a cycle is a
PrepError. The guard against writes through a linked .claude/skills and a
dangling skill directory are pinned by tests.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
Comment on lines +68 to +69
if fence is None:
return _opening_run(line)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: fence tracking misses list-contained openers but recognizes their closing delimiters as new openers. For example, to_format("- ```\n echo hi\n ```\n# Real\n", "xml") leaves # Real as Markdown: - ``` does not match _FENCE, while the closing ``` starts a fence that never closes. The round-trip check still succeeds, so the cell silently runs an incomplete heading transformation. Track Markdown container context when recognizing fences (including when the container ends), and add a regression asserting that the heading after this list-contained block becomes a section.

@napetrov napetrov Nov 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d94e063, with the tab and marker details in d1d0672. A fence opened by a list item (- ```` ) now records its item's content column: its closer may be indented up to three columns past it, and a line indented less than that column ends the item and its fence. Your example now makes # Reala section. Regression:test_a_fence_opened_by_a_list_item_holds_body_text_only_inside_the_item` (the bullet case fails without the fix; the list cases are pinned).
Local: the guidance-format and format-touching test modules pass (129 passed in the targeted set).

…sion

Pins the case the review asked for: a base that ships .claude/skills/lint
keeps that skill, and guidance:as-skill installs its own skill next to it.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
…d1031

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A fence opened in a list item (- ```) was not seen, while its closing
delimiter, indented to the item's column, opened a fence that never closed,
so the headings after the block stayed Markdown. A fence now records the
column of its item: its closer may be indented up to three columns past
that column, and a line indented less than it ends the item and the fence.

Red before the fix: the bullet and the ordered-item cases fail; the
list-item tests pass now.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
A list marker takes one to four spaces before its content, so five spaces
make indented code, not a fence; several markers on one line (- - ```)
open an item each. A tab counts to the next multiple of four in a closer as
it does in an item's indentation.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
match = _OPENER.match(line, column)
if match is None or (match.group(1)[0] == "`" and "`" in match.group(2)):
return None
return match.group(1), column

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: fences opened on a list continuation line lose their list indentation. For to_format('- Example:\n ```\n code\n# Real\n', 'xml'), _opened(' ```') returns a fence with column 0 because that line has no list marker. Consequently _advance does not end the list's fence at # Real, and the real top-level heading remains Markdown instead of becoming a section. The round-trip check still passes, so this silently measures a partially converted file. Track the active list's content column across continuation lines and add this case to the list-fence regression tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 45f2f4f. The open list items are now tracked across lines (outer and nested, not only the marker line), so a fence on a continuation line takes its item's column. # Real after the list is a section.
Regression: test_a_fence_opened_by_a_list_item_holds_body_text_only_inside_the_item. Your repro and the ordered variant were red on d1d0672, green now.

@antonwolfy antonwolfy 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.

The earlier threads are fixed, and the list-item fence work is the right direction. One risk remains in the same fence tracking, so I'd fix it before merge.

What I ran, on a Linux review host at d1d0672:

  • make ci-local: 30 passed, 0 failed, 7 skipped. Changed-line coverage passes.
  • Reproductions for the code finding, with no model or network involved.
  • Mutation runs on the new fence, list and tab logic, and on the link and .claude/skills guards. The link and guard mutants all fail a test. Several list-marker and tab mutants pass the full suite (see below).
  • A differential fuzz of about 60k fence/list/tab line combinations against a CommonMark parser. After the case in the inline comment, no heading mismatch remained.

Checked, no issues

  • Links: a link through a linked directory, a link in a subdirectory, a dangling link, a cycle and a link outside the workspace are refused or resolved as the filesystem does, for both arms. Replacing the resolution with normpath or a bare readlink fails the tests.
  • Guard: reducing the loop to .claude, or dropping or skill_dir.is_symlink(), fails a test each.
  • Commits: every non-merge commit carries a Signed-off-by.

Not blocking, for later: the tab and list-marker details () and + markers, the one-to-four-space bound, a closer indented one to three spaces at top level) are not pinned. Mutants for each pass the whole test file. Also, a # line inside a multi-line HTML comment or <details> block becomes a section, since only code fences are tracked.

Bot points I checked that no longer apply at this head (11)
  • Copilot's point on a four-backtick block holding a three-backtick line does not apply: a fence closes only on a run of the same character at least as long as its opener (guidance_format.py:72).
  • Copilot's point on guidance:as-skill+skill:... being refused by preflight does not apply: arms.py includes guidance:as-skill in drops, and a test pins it.
  • Copilot's point on newline translation in write_text does not apply: both writers use write_bytes on UTF-8 bytes.
  • exai-bot's point on mixed fence delimiters and lengths does not apply: the close check compares character and length.
  • exai-bot's point on the preflight not knowing the removal arm does not apply: same drops change in arms.py.
  • exai-bot's point on a symlink whose target is outside the matched guidance files does not apply: _link_file raises a PrepError for it.
  • exai-bot's point on write_text newline translation in both writers does not apply: both write bytes.
  • exai-bot's point on a link cycle between two matched files does not apply: _link_file resolves strictly and refuses a cycle.
  • exai-bot's point on a backtick in a fence info string and a tab-indented opener does not apply: _OPENER takes up to three spaces and _opened rejects a backtick in a backtick fence's info string.
  • exai-bot's point on lexical .. resolution does not apply: _link_file uses Path.resolve(strict=True) and checks the result is a matched file inside the workspace.
  • exai-bot's point on a fence opened by a list item does not apply at this head: - ``` now opens a fence, and the heading after a closed list-item block becomes a section.

Comment thread dokimi/dokimi/repos/guidance_format.py Resolved
A fence opened on a line without a list marker, indented to the open
item's content column, took column 0. Its dedent check then never ended
it, so a real heading after the list stayed body text, and the round
trip still passed.

Track the open items (outermost first) across lines: a non-blank line
ends the items it is indented less than, a marker opens one per marker,
and a fence takes the innermost column. Lines are read expanded for the
opener, as for the closer.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
_ROOT, _ROOT_CLOSE, _CLOSE = "<guidance>", "</guidance>", "</section>"
_HEADING = re.compile(r"^(#{1,6}) (\S[^\r]*)$") # canonical only: one space, no CR; anything else is body text
_FENCE = re.compile(r"^( *)(`{3,}|~{3,})(.*)$") # the indentation, a run of ``` or ~~~, and what follows it
_ITEM = re.compile(r" *(?:[-*+]|\d{1,9}[.)]) {1,4}(?=\S)") # a list marker and the one to four spaces after it

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: tab-separated list markers are not recognized, so their closing fence is mistaken for a new top-level opener. For to_format("-\t```\n echo hi\n ```\n# Real\n", "xml"), the first line opens a valid Markdown list-contained fence, but _ITEM requires literal spaces and misses it. The closing ``` then opens a fence with column 0, leaving # Real unconverted; the round-trip check still passes. Ordered markers followed by a tab have the same problem. Parse markers and their padding using expanded columns consistently with _opened, and add regression cases for both marker types.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partly fixed in 2c9b57c. The marker is now matched on the expanded line, as the fence opener is, and pinned.
Your example gives no section, and CommonMark (markdown-it-py 3.0.0) agrees: the item's content column after -<tab> is 4, so the 2-space body ends it and # Real is in the fence the closer opens. With a 4-space body the section appears (new cases, -<tab> and 1.<tab>). Before this change the sections were the same on the generated inputs I compared (0 differences), so this is a consistency fix, not a visible one.

Comment on lines +535 to +536
name = slug(f"{manifest.id}-guidance")
skill_dir = ws / SKILLS_DIR / name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: include the generated guidance skill in preflight collision validation. For repository id toy, guidance:as-skill+skill:skills/toy-guidance passes check_arms() when the base has no skill at that path, but this code creates .claude/skills/toy-guidance before _install_skills(), which then raises PrepError because the destination exists. A catalog containing toy-guidance has the same problem. Reject these duplicate destinations before the batch starts (passing the repository id into validation), and add a regression test covering both preflight and preparation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 2c9b57c. check_arms takes the repo id and refuses a guidance:as-skill arm whose generated .claude/skills/<repo>-guidance is one of its skills (skill: or catalog), before any cell.
Tests: the arm-skill and catalog-skill collisions; a base that tracks the directory's SKILL.md. Both via check_arms; repo run and repo tasks check pass the id.

@antonwolfy antonwolfy 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.

Review at a333c8b. The list-continuation fix for my open thread holds, and the fence tracking now agrees with a CommonMark parser on the cases I tried. One thing is worth fixing before merge: the guidance skill's name is not checked in the arm preflight.

What I ran, on a Linux review host at a333c8b:

  • make ci-local: 28 passed, 2 failed, 7 skipped. Tests pass and changed-line coverage passes. The two FAILs are Install gitleaks (a download that returned exit 22) and Scan commits, which depends on it. Neither reads the PR's tree.
  • Reproductions for the code finding and for the tab-marker point, with no model or network involved.
  • A differential run of about 100k fence/list/tab line combinations against markdown-it-py, and a mutation run of 52 hand-made mutants against the two test files.

The preflight does not know the skill that guidance:as-skill creates. See the inline comment at arms.py:200.

Checked, no issues

  • Fences: the new continuation-line tracking never converts a heading that markdown-it-py treats as fence body. The divergences left (a lazy unindented line inside an item, an empty - item) only leave a heading unconverted, which is the safe direction.
  • Docs and rules: the arm names, the .claude/skills/<repo id>-guidance/SKILL.md path and the ## File: layout in the docs match the code. The changelog fragment is in place, no new page needs a nav: entry, and every non-merge commit carries a Signed-off-by.
Bot points I checked that no longer apply at this head (13)
  • Copilot's point on a four-backtick block holding a three-backtick line does not apply: a fence closes only on a run of the same character at least as long as its opener.
  • Copilot's point on guidance:as-skill+skill:... being refused by the base-skill check does not apply: arms.py:200 includes guidance:as-skill in drops.
  • Copilot's point on newline translation in write_text does not apply: both writers use write_bytes on UTF-8 bytes.
  • exai-bot's point on mixed fence delimiters and lengths does not apply: the close check compares character and length.
  • exai-bot's point on the preflight not knowing the removal arm does not apply: same drops change at arms.py:200.
  • exai-bot's point on a symlink whose target is outside the matched guidance files does not apply: _link_file raises a PrepError for it.
  • exai-bot's point on write_text newline translation in both writers does not apply: both write bytes.
  • exai-bot's point on a link cycle between two matched files does not apply: _link_file resolves strictly and refuses a cycle.
  • exai-bot's point on a backtick in a fence info string and a tab-indented opener does not apply: _OPENER takes up to three spaces and _opened rejects a backtick in a backtick fence's info string.
  • exai-bot's point on lexical .. resolution does not apply: _link_file uses Path.resolve(strict=True).
  • exai-bot's point on a fence opened by a list item does not apply at this head: - ``` opens a fence and the heading after the closed block becomes a section.
  • exai-bot's point on a fence opened on a list continuation line does not apply at this head: - Example:\n ```\n code\n# Real\n now gives a section for # Real.
  • exai-bot's point on tab-separated list markers does not apply: for -\t```\n echo hi\n ```\n# Real\n and the 1.\t form, a CommonMark parser (markdown-it-py) also finds no heading, because the tab puts the item content at column 4 and the two-space line opens a top-level fence. The arm gives the same result.

Comment thread dokimi/dokimi/repos/arms.py Resolved
… marker

check_arms refuses a guidance:as-skill arm whose generated skill
.claude/skills/<repo>-guidance is one of its skills or is tracked by a task's
base, before any cell runs, instead of failing cell by cell in preparation.
A list marker followed by a tab is read at its expanded columns, as the fence
opener is, so the item's fence is recognised; pinned against CommonMark.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
return items
expanded = line.expandtabs(4)
indent = len(expanded) - len(expanded.lstrip(" "))
return tuple(column for column in items if column <= indent) + _marker_columns(line)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: an unindented lazy paragraph continuation does not end a Markdown list item, but this filter drops its content column. For - Example:\ncontinued text\n ```\n code\n# Real\n, the list remains open through continued text, so the fence belongs to the item at column 2 and # Real ends that item and is a heading. Here the fence is instead tracked at column 0, leaving # Real as body text. The round-trip check still succeeds, so the XML arm silently measures an incorrect heading transformation. Preserve list state across lazy paragraph continuations (while distinguishing actual block boundaries), and add this input to the section-recognition tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Real, fixed in 9b5e4f2. A non-blank line that continues a paragraph (not a heading, quote, rule, list marker or fence opener) no longer ends the open list items, so the fence after continued text sits at the item's column and # Real is a section. Red-then-green: your input and two block-start negatives (column-0 fence, heading after a lazy line) are in the section-recognition test.

… fence tracking

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment on lines +202 to +203
if repo_id is not None and arm.patch is None and arm.base == "guidance:as-skill":
_check_generated_skill(arm, skills, repo_id, tasks, git_dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: the generated skill is checked for collisions here but never included in the names passed to _check_expected_skills below. With repo id toy, a task declaring expected_skill: toy-guidance, and guidance:as-skill+skill-catalog:skills containing an unrelated lint skill, preflight raises "expected_skill 'toy-guidance' is not installed", although preparation installs it and records it in meta['skills']. Include the generated name in expected-skill validation as well as collision validation, and add a test for this composition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240: a catalog arm with guidance:as-skill now counts the generated <repo>-guidance skill as installed, so an expected_skill naming it passes. Pinned by a new test in tests/test_repo_skill_arms.py.

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
_ROOT, _ROOT_CLOSE, _CLOSE = "<guidance>", "</guidance>", "</section>"
_HEADING = re.compile(r"^(#{1,6}) (\S[^\r]*)$") # canonical only: one space, no CR; anything else is body text
_FENCE = re.compile(r"^( *)(`{3,}|~{3,})(.*)$") # the indentation, a run of ``` or ~~~, and what follows it
_ITEM = re.compile(r" *(?:[-*+]|\d{1,9}[.)]) {1,4}(?=\S)") # a list marker and the one to four spaces after it

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: empty list markers are valid Markdown, but this pattern requires content after the marker. For to_format('-\n ```\n# Real\n', 'xml'), the fence belongs to a list item at content column 2, and the unindented heading ends that item and its fence. Here the - is not recognized, so the fence is treated as top-level and # Real is incorrectly left as body text. Recognize marker-only items (including ordered markers), and add a regression asserting that Real becomes a section; the round-trip check cannot detect this structural error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240: an empty - or 1. is a list item, so its fence sits at the content column. Pinned by new cases in tests/test_repo_guidance_format.py.

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
indent = len(expanded) - len(expanded.lstrip(" "))
if lazy and not _marker_columns(line) and not _BLOCK.match(expanded) and not _OPENER.match(expanded):
return items
return tuple(column for column in items if column <= indent) + _marker_columns(line)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: thematic breaks take precedence over list markers in Markdown, but _marker_columns('- - -') returns (2, 4) here. For to_format('- - -\n ```\n# not a heading\n ```\n', 'xml'), the fence is consequently assigned column 2 and ends at the unindented hash line, which is incorrectly rendered as a section. In Markdown this is a thematic break followed by a top-level fence, so the hash line must remain code. The round-trip check still passes and does not detect this structural change. Exclude thematic breaks from list-marker detection (also covering * * *) and add a regression asserting that this input produces no sections.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240: - - - and * * * are thematic breaks, not list items. Pinned by new cases in tests/test_repo_guidance_format.py.

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
Comment on lines +112 to +113
items = _items_after(items, line, para and bool(items))
opened = _opened(line, items)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: ordered lists can interrupt a CommonMark paragraph only when their starting number is 1, but this path treats any numbered marker as a new item. For to_format('paragraph\n2. ```\n ```\n# Real\n', 'xml'), 2. ``` is paragraph text, and the following three-space-indented backticks open a top-level fence, so # Real must remain code. Here the numbered line opens a list-contained fence, the next line closes it, and # Real becomes a section. The round-trip check still passes, so it cannot detect this misclassification. Preserve paragraph context when deciding whether a numbered marker actually opens a list, and add this input as a regression test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240: an ordered marker not numbered 1 no longer interrupts a paragraph. Pinned by new cases in tests/test_repo_guidance_format.py.

@antonwolfy antonwolfy 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.

Review at 9b5e4f2. The preflight for the generated skill name is in, and the lazy-paragraph fix works for the case it targets. The fence tracking and the preflight each still have wrong results that the round-trip check cannot see. Four bugs, one risk and one nit are inline; fix the bugs before merge.

What I ran, on a Linux review host at 9b5e4f2:

  • make ci-local: 30 passed, 0 failed, 7 skipped. Changed-line coverage passes.
  • Reproductions for the code findings, with no model or network involved.
  • A differential run of about 120k random fence/list/blank/lazy line combinations against markdown-it-py, and a mutation run of 23 mutants against the two test files: 19 of them pass the full files (the ones that decide something are in the risk and the nit).

Still open from my earlier review: none. My earlier thread on the generated skill name is fixed.

Checked, no issues

  • Arm callers: both check_arms call sites (repo_run.py:196 and :285) pass ctx.manifest.id, and the name is slugged the same way at preflight and in workspace.py.
  • Docs and rules: arm names and the .claude/skills/<repo id>-guidance/SKILL.md path in the three doc pages match the code. The changelog fragment is in place, no new page needs a nav: entry, and every non-merge commit carries a Signed-off-by.
  • Lazy paragraph fix: - Example:\ncontinued text\n ```\n code\n# Real\n gives a section for # Real.
Bot points I checked that no longer apply at this head (15)
  • Copilot's point on a four-backtick block holding a three-backtick line does not apply: a fence closes only on a run of the same character at least as long as its opener.
  • Copilot's point on guidance:as-skill+skill:... being refused by the base-skill check does not apply: arms.py includes guidance:as-skill in drops.
  • Copilot's point on newline translation in write_text does not apply: both writers use write_bytes on UTF-8 bytes.
  • exai-bot's point on mixed fence delimiters and lengths does not apply: the close check compares character and length.
  • exai-bot's point on the preflight not knowing the removal arm does not apply: same drops change in arms.py.
  • exai-bot's point on a symlink whose target is outside the matched guidance files does not apply: _link_file raises a PrepError for it.
  • exai-bot's point on write_text newline translation in both writers does not apply: both write bytes.
  • exai-bot's point on a link cycle between two matched files does not apply: _link_file resolves strictly and refuses a cycle.
  • exai-bot's point on a backtick in a fence info string and a tab-indented opener does not apply: _OPENER takes up to three spaces and _opened rejects a backtick in a backtick fence's info string.
  • exai-bot's point on lexical .. resolution does not apply: _link_file uses Path.resolve(strict=True).
  • exai-bot's point on a fence opened by a list item does not apply at this head: - ``` opens a fence and the heading after the closed block becomes a section.
  • exai-bot's point on a fence opened on a list continuation line does not apply at this head: - Example:\n ```\n code\n# Real\n gives a section for # Real.
  • exai-bot's point on tab-separated list markers does not apply: for -\t```\n echo hi\n ```\n# Real\n a CommonMark parser also finds no heading, and the arm gives the same result.
  • exai-bot's point on the generated skill missing from the collision check does not apply: _check_generated_skill refuses it for a skill: or catalog skill and for a base that tracks the directory.
  • exai-bot's point on an unindented lazy paragraph line ending the item does not apply: - Example:\ncontinued text\n ```\n code\n# Real\n gives a section for # Real.

skills = resolve_skills(arm, content_dir)
drops = arm.patch is None and (arm.base == "guidance:none" or arm.rev is not None)
if repo_id is not None and arm.patch is None and arm.base == "guidance:as-skill":
_check_generated_skill(arm, skills, repo_id, tasks, git_dir)

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.

A task's expected_skill naming the generated guidance skill is refused when the arm also carries a catalog. I agree with exai-bot here; what I add is the run. _check_expected_skills at arms.py:210 gets only {s.name for s in skills}, so the skill that guidance:as-skill installs itself (workspace.py, recorded in meta["skills"]) is never in the set.

  • Repo toy, task with expected_skill: toy-guidance, arm guidance:as-skill+skill-catalog:skills with a catalog holding only lint: check_arms(..., "toy") raises ArmError: ... task t's expected_skill 'toy-guidance' is not installed (installed: lint).
  • The same task with plain guidance:as-skill passes, and expected_skill: lint with the catalog arm passes. Repo id My_Repo behaves the same.
  • Preparation would install the skill, so this is a false refusal of the very measurement the arm is for (does the agent load the packaged guidance).

Suggestion: when repo_id is given and the arm is guidance:as-skill, add slug(f'{repo_id}-guidance') to the names passed to _check_expected_skills. Add a test with a catalog arm, a task with that expected_skill, and check_arms(..., repo_id).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240, same change as the exai-bot thread: a catalog arm with guidance:as-skill counts the generated <repo>-guidance skill as installed, so an expected_skill naming it passes. Pinned by a new test in tests/test_repo_skill_arms.py.

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
_ROOT, _ROOT_CLOSE, _CLOSE = "<guidance>", "</guidance>", "</section>"
_HEADING = re.compile(r"^(#{1,6}) (\S[^\r]*)$") # canonical only: one space, no CR; anything else is body text
_FENCE = re.compile(r"^( *)(`{3,}|~{3,})(.*)$") # the indentation, a run of ``` or ~~~, and what follows it
_ITEM = re.compile(r" *(?:[-*+]|\d{1,9}[.)]) {1,4}(?=\S)") # a list marker and the one to four spaces after it

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.

A thematic break (- - -, * * *) is read as nested list markers, so a # line inside the fence after it becomes a section. I agree with exai-bot here; what I add is the CommonMark comparison. _ITEM matches - - - as two markers, and _marker_columns (:60) gives the fence column 2, so the unindented # line ends the fence.

  • to_format('- - -\n ```\n# not a heading\n ```\n', 'xml') gives <section level="1" title="not a heading">. markdown-it-py parses an hr and a top-level fence and no heading. '* * *\n ```\n# not a heading\n ```\n' behaves the same.
  • restore(...) equals the input, so the round-trip check passes and the cell measures a file whose code comment became a section.

Suggestion: in _marker_columns, return () when the line matches the rule alternative of _BLOCK (or reuse that pattern), and add both inputs to test_a_fence_opened_by_a_list_item_holds_body_text_only_inside_the_item with [] as the expected sections.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240, same change as the exai-bot thread: - - - and * * * are thematic breaks, not list items. Pinned by new cases in tests/test_repo_guidance_format.py.

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
return tuple(columns)


def _items_after(items: tuple[int, ...], line: str, lazy: bool = False) -> tuple[int, ...]:

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.

An empty list marker (- alone on a line) opens no item, so a column-0 line that CommonMark reads as a new fence is read as the closer, and a later # line inside that fence becomes a section. I agree with exai-bot here; what I add is the harmful input. _ITEM requires (?=\S) after the marker, so _items_after never records the item.

  • to_format('-\n ```\n```\n# Real\n', 'xml') gives a section for Real. markdown-it-py: the item ends at the column-0 line, which opens a second fence, so # Real is code.
  • to_format('-\n ```\n# H\n```\n# Real\n', 'xml') gives a section for Real and none for H. markdown-it-py gives the opposite: H is a heading, Real is code.
  • The simple case in the bot's report (-\n ```\n# Real\n) only leaves Real unconverted, which is the safe direction, but the inputs above are not.

Suggestion: let _ITEM match a marker followed by end of line (content column = marker width + 1), and add the three inputs to the list-fence test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240: an empty - opens an item, so the column-0 fence line closes its fence instead of being read as a section. Pinned by new cases in tests/test_repo_guidance_format.py.

Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
expanded = line.expandtabs(4) # a tab is indentation up to the next multiple of four
if not (line.strip() and len(expanded) - len(expanded.lstrip(" ")) < fence[1]):
return (None if _closes(fence, line) else fence), items, False
items = _items_after(items, line, para and bool(items))

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.

Any ordered marker is allowed to interrupt a paragraph, but only a list starting at 1 can. I agree with exai-bot here; what I add is the CommonMark comparison. _advance already knows para, but _items_after only uses it for the lazy test, so a numbered line after paragraph text always opens an item.

  • to_format('paragraph\n2. ```\n ```\n# Real\n', 'xml') gives a section for Real. markdown-it-py gives a paragraph and one top-level unclosed fence, so # Real is code.
  • The round-trip check passes, so nothing flags it.

Suggestion: when para is true and no item is open, treat an ordered marker whose number is not 1 (and any marker with empty content) as paragraph text in _marker_columns/_items_after. Add the input with [] as the expected sections.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240, same change as the exai-bot thread: an ordered marker not numbered 1 no longer interrupts a paragraph. Pinned by new cases in tests/test_repo_guidance_format.py.

def _check_generated_skill(arm: Any, skills: list[Skill], repo_id: str, tasks: list[Any], git_dir: Path | None) -> None:
"""Refuse a ``guidance:as-skill`` arm whose generated skill directory is a skill of the arm, or is tracked by the
base of a task: preparation writes that directory first, so it would fail cell by cell, not here."""
target = f"{SKILLS_DIR}/{slug(f'{repo_id}-guidance')}"

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.

The new preflight is not pinned where it decides whether a batch starts. Mutation check on _check_generated_skill and its callers: each of these passes both new test files (144 passed):

  • removing ctx.manifest.id from the call at repo_run.py:196 or at :285 (no test reaches either, so the preflight silently turns off and the collision fails per cell again; 313 repo-run tests also pass);
  • dropping arm.base == "guidance:as-skill" at arms.py:202, which makes guidance:none+skill:skills/toy-guidance refuse;
  • dropping path == target or (a base tracking a plain file at the skill path stops being refused), or changing target + "/" to target (a base file under .claude/skills/toy-guidance2/ is refused);
  • removing the slug(...) on this line (repo id my repo then misses a skill named my_repo-guidance).

Suggestion: add tests for a non-as-skill arm with that skill name (accepted), a base tracking a plain file at the target, a sibling toy-guidance2 (accepted), a repo id that slugs differently, and one repo tasks check or repo run test whose arm collides, asserting the refusal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240. Your mutations now fail tests: a repo run refusal test (dropping the repo id at repo_run.py:285 fails it), plus cases for a plain file at the skill path, the -guidance2 sibling, guidance:none, and the slug of the repo id.

Comment thread dokimi/dokimi/repos/guidance_format.py Resolved
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Comment thread dokimi/dokimi/repos/guidance_format.py Outdated
Comment on lines +114 to +115
text = line.expandtabs(4).strip()
return opened, items, opened is None and bool(text) and not _BLOCK.match(text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: indented code sets para=True, enabling an invalid lazy list continuation. For to_format('- a\n\n code\noutside\n ```\n# not a heading\n', 'xml'), the six-space-indented code is a code block within the list, not a paragraph. Therefore outside ends the list and the following fence opens at top level, keeping the hash line as code. Here outside retains the item's column 2; the fence is incorrectly attached to that item and ends at the hash line, which becomes a <section>. The round-trip check still passes despite this structural change. Account for indentation relative to the active list content column when setting paragraph state, and add a regression asserting that this input produces no section.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d89a240: indented code is no longer paragraph text, so a following outside line cannot continue it lazily. Pinned by a new case in tests/test_repo_guidance_format.py.

@antonwolfy antonwolfy 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.

I pushed a commit per nit, up to 78fb39b, which fix the nit from my review:

  • 78fb39b: dokimi/tests/test_repo_guidance_format.py:75: added the four requested inputs (quote, rule, heading, blank-then-lazy) to the section-recognition parametrize list.

make ci-local at 78fb39b: 30 passed, 0 failed, 7 skipped.

Not approving yet. Still open: the four bugs and the risk from my review.

The fence tracker of guidance:format=xml now follows CommonMark where the review threads of intel#333 showed it did not: an empty list item opens its content column one past the marker, a thematic break is not a list item, an ordered marker only interrupts a paragraph when numbered 1, indented code is not paragraph text, and a blank line ends an empty item. check_arms also counts the generated guidance skill of a guidance:as-skill arm as installed, so an expected_skill naming it is accepted.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
The refusal of a guidance:as-skill arm whose generated skill the arm also ships was tested through repo tasks check only; repo run passes the repo id on its own call, and dropping it there passed every test. Add a repo run test that asserts the refusal before any cell.

Signed-off-by: Nikolay Petrov <nikolay.a.petrov@intel.com>
Comment on lines +143 to +144
return (opened, items, opened is None and bool(text) and not _BLOCK.match(text) and not code,
bool(markers) and not text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

bug · medium
Must-fix: stripping the list marker before checking _BLOCK makes a blockquote paragraph set para=False, although CommonMark allows its text to continue lazily through both the quote and list containers. For to_format('- > quoted\nlazy\n ```\n# Real\n', 'xml'), lazy consequently drops the list, the fence opens at column 0, and # Real incorrectly remains code instead of becoming a section. Preserve paragraph-continuation state for quoted paragraphs (distinguishing them from non-paragraph quote contents), and add this input to the fence-boundary regression cases with expected sections ['Real'].

This branch has not been deployed

No deployments
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.

5 participants