Skip to content

DOJ-3774, DOJ-3775: Add IDT CI workflow + ${CLAUDE_PLUGIN_ROOT} defensive note#17

Merged
lapc506 merged 3 commits into
mainfrom
andres/doj-3774-infra-add-minimal-ci-to-instructional-design-toolkit-json
May 5, 2026
Merged

DOJ-3774, DOJ-3775: Add IDT CI workflow + ${CLAUDE_PLUGIN_ROOT} defensive note#17
lapc506 merged 3 commits into
mainfrom
andres/doj-3774-infra-add-minimal-ci-to-instructional-design-toolkit-json

Conversation

@lapc506
Copy link
Copy Markdown
Collaborator

@lapc506 lapc506 commented May 5, 2026

Summary

  • Closes DOJ-3774 — adds .github/workflows/lint.yml with 3 pre-flight checks (JSON schema validation, YAML frontmatter linting, agent reference resolution)
  • Closes DOJ-3775 — adds ${CLAUDE_PLUGIN_ROOT} defensive-resolution note (§6.1 + new failure-mode row) to assets/runtime/overlay-protocol.md
  • Combined into a single PR per feedback_sequential_implementation.md — no parallel branches in same repo
  • Validated against post-DOJ-3773 main: all three lint checks pass cleanly on this PR's branch (3 schemas, 46 frontmatter files, 7 agents indexed). The workflow itself runs on this PR (it ships in the PR's HEAD), self-validating.

Files changed

  • .github/workflows/lint.yml (new, 161 LoC) — DOJ-3774
  • assets/runtime/overlay-protocol.md (+41 LoC) — DOJ-3775

DOJ-3774 detail — three CI checks

Each check is a distinct workflow step with localized error messages, designed to catch the bug class Greptile flagged across the DOJ-3708 migration chain (DOJ-3771/3772/3773):

  1. JSON schema syntax validation — every assets/schemas/**/*.schema.json must parse as valid JSON.
  2. YAML frontmatter linting — every commands/*.md, skills/*/SKILL.md, agents/*.md either has no frontmatter (skipped, e.g. translation pipeline support docs) or has a parseable mapping with a non-empty description field.
  3. Agent reference resolution — every ${CLAUDE_PLUGIN_ROOT}/agents/<name>.md reference inside skills/commands resolves to a file shipping in this repo.

Calibration note for Step 3

The agent reference check deliberately scopes to ${CLAUDE_PLUGIN_ROOT}/agents/<name>.md patterns and ignores bare agents/<name>.md mentions. The latter are consumer-relative references (e.g. commands/_translation-pipeline.md references consumer-shipped agents/translator.md, agents/proofreader.md, agents/translation-reviewer.md from the consumer repo, not from IDT). Flagging those would produce false positives on legitimate cross-plugin patterns. The IDT-internal scope is the correct boundary.

DOJ-3775 detail — defensive-resolution note

Added two pieces to assets/runtime/overlay-protocol.md:

  • New failure-mode row in §6 for "${CLAUDE_PLUGIN_ROOT} is empty or unresolved" — graceful skip with visible warning, no crash, no silent no-op.
  • New §6.1 "Plugin runtime expectations (${CLAUDE_PLUGIN_ROOT})" — documents the contract with the host runtime, the in-plugin and out-of-plugin cases, and the defensive behavior authoring skills must implement. Note explicitly references DOJ-3710 (Phase 2) integration tests as the verification mechanism.

Test plan

  • Workflow YAML parses (validated locally via yaml.safe_load)
  • All 3 lint steps pass against this PR's branch state (validated locally pre-push)
  • Workflow run completes green on this PR (CI verifies the workflow itself)
  • Greptile review 5/5 functional

Out of scope

  • Backporting the workflow to dojo-academy — DOJ-3776 is a separate decision (currently filed as deferred)
  • Adding tests for individual SKILL.md prose correctness — Greptile handles that
  • Type-checking — markdown-driven plugin, no compiled code
  • DOJ-3710 integration tests for the ${CLAUDE_PLUGIN_ROOT} failure path — that issue owns the test coverage; this PR documents the expected behavior so authoring skills can be written defensively now

Created by Claude Code on behalf of @andres

…sive note

DOJ-3774 — Add .github/workflows/lint.yml with three checks:
  1. JSON schema syntax validation (assets/schemas/**/*.schema.json)
  2. YAML frontmatter linting (commands/, skills/*/SKILL.md, agents/)
     with required `description` field
  3. Agent reference resolution — every ${CLAUDE_PLUGIN_ROOT}/agents/<name>.md
     reference inside skills/commands must point to a file shipping in
     this repo. Bare `agents/<name>.md` references are deliberately not
     checked (consumer-shipped overlays like the translation pipeline use
     consumer-relative paths).

Validated against post-DOJ-3773 main: all three checks pass cleanly
(3 schemas, 46 frontmatter files, 7 agents indexed). Targets the bug
class Greptile flagged across the DOJ-3708 chain.

DOJ-3775 — Add §6.1 "Plugin runtime expectations" to
assets/runtime/overlay-protocol.md plus a new failure-mode row in §6
covering empty / unresolved ${CLAUDE_PLUGIN_ROOT}. Documents that
authoring skills must emit a visible warning and skip overlay invocation
gracefully when the host runtime does not expand the token (e.g. direct
`claude` CLI invocation outside a plugin context). Contract is verified
by integration tests planned for DOJ-3710 (Phase 2).

Both issues bundled in a single PR per feedback_sequential_implementation
(no parallel branches in same repo).

Closes DOJ-3774
Closes DOJ-3775

Created by Claude Code on behalf of @andres

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lapc506
Copy link
Copy Markdown
Collaborator Author

lapc506 commented May 5, 2026

@greptileai review

…tion bug

The first workflow run failed with "workflow file issue" because the
indented `python3 - <<'PY'` heredoc in each step passed the script body
to Python with leading whitespace, making the body unparseable. Moving
the three checks to standalone Python scripts under scripts/ci/ keeps
the workflow YAML minimal and the scripts independently runnable
locally for development.

Behavior is identical:
  - check_json_schemas.py — assets/schemas/**/*.schema.json must parse
  - check_frontmatter.py — commands/, skills/*/SKILL.md, agents/ must
    have either no frontmatter or a parseable mapping with `description`
  - check_agent_references.py — every ${CLAUDE_PLUGIN_ROOT}/agents/<name>.md
    reference must resolve

Verified locally: 3 schemas, 46 frontmatter files, 7 agents — all pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lapc506
Copy link
Copy Markdown
Collaborator Author

lapc506 commented May 5, 2026

@greptile review

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR introduces a three-step CI lint workflow (JSON schema validation, YAML frontmatter linting, agent reference resolution) and extends overlay-protocol.md with a defensive-resolution contract for ${CLAUDE_PLUGIN_ROOT}. All findings are P2; the exit-code logic is correct and the overall intent is sound.


Path to 5/5 Confidence

  1. scripts/ci/check_frontmatter.py line 55–59 — Add a continue after the description-error append so checked only counts files that fully passed.

  2. scripts/ci/check_agent_references.py line 33 — Expand the character class from [a-z0-9-] to [a-z0-9_-] so agent names with underscores are not silently skipped.

  3. .github/workflows/lint.yml lines 33–35 — Add a permissions block to restrict the GITHUB_TOKEN: permissions: contents: read.

  4. .github/workflows/lint.yml lines 38–42 — Pin both actions to full commit SHAs instead of mutable version tags (e.g. actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4).

  5. .github/workflows/lint.yml line 46 — Pin pyyaml to a specific version (pip install --quiet pyyaml==6.0.2) for reproducible runs.

Confidence Score: 4/5

Safe to merge; all findings are P2 and do not affect CI correctness or runtime behavior.

P2s only → ceiling of 4/5. The checked counter bug produces misleading output but exit code is still correct. The regex gap and missing workflow permissions are hygiene issues with no immediate impact.

scripts/ci/check_frontmatter.py (counter logic), scripts/ci/check_agent_references.py (regex coverage), .github/workflows/lint.yml (permissions + SHA pinning)

Security Review

  • Overly-permissive workflow token (.github/workflows/lint.yml): no permissions block means the job inherits the repo default (read+write in many org configurations). Should be locked to contents: read.
  • Mutable action tags (.github/workflows/lint.yml): actions/checkout@v4 and actions/setup-python@v5 are not pinned to commit SHAs, creating a supply-chain attack surface if either tag is re-pointed.

Important Files Changed

Filename Overview
.github/workflows/lint.yml New CI workflow with 3 lint steps; missing permissions block and actions not pinned to commit SHAs (two P2 security hygiene issues).
scripts/ci/check_frontmatter.py YAML frontmatter linter; checked counter incremented even when description validation fails, making the success message inaccurate.
scripts/ci/check_agent_references.py Agent reference resolver; regex [a-z][a-z0-9-]+ silently skips agent names containing underscores, producing false negatives.
scripts/ci/check_json_schemas.py JSON schema syntax validator; straightforward and correct — no issues found.
assets/runtime/overlay-protocol.md Adds §6.1 and a new failure-mode row documenting ${CLAUDE_PLUGIN_ROOT} defensive behavior; prose is clear and well-structured.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    PR[PR / push to main] --> WF[lint workflow]
    WF --> S1[Step 1: JSON schema validation]
    WF --> S2[Step 2: YAML frontmatter linting]
    WF --> S3[Step 3: Agent reference resolution]
    S1 -->|parse error| FAIL1[Fail]
    S1 -->|all valid| PASS1[N schemas valid]
    S2 -->|missing/invalid description| FAIL2[Fail]
    S2 -->|no frontmatter| SKIP[Skip file]
    S2 -->|all valid| PASS2[N files valid]
    S3 -->|ref not in agents/| FAIL3[Fail]
    S3 -->|all resolve| PASS3[All refs resolve]
    PASS1 & PASS2 & PASS3 --> GREEN[Workflow passes]
    FAIL1 & FAIL2 & FAIL3 --> RED[Workflow fails]
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
scripts/ci/check_frontmatter.py:54-59
`checked` is incremented even when the description is invalid. When `description` is present but empty or the wrong type, the error is appended but there's no `continue` before `checked += 1`, so the success message (e.g. "46 file(s) valid") includes files that actually failed validation. The exit code is still correct, but the output is misleading and could mask issues during triage.

```suggestion
            desc = fm.get('description')
            if not isinstance(desc, str) or not desc.strip():
                errors.append(
                    f'{path}: frontmatter "description" is empty or not a string'
                )
                continue
            checked += 1
```

### Issue 2 of 4
scripts/ci/check_agent_references.py:32-34
The regex `[a-z][a-z0-9-]+` excludes underscores from agent names. If any agent file uses underscores (e.g. `agents/translation_reviewer.md`), any `${CLAUDE_PLUGIN_ROOT}/agents/translation_reviewer.md` reference in skills/commands would silently pass the check even if the file doesn't exist — a false negative in the guard this CI step is meant to provide.

```suggestion
    ref_pattern = re.compile(
        r'\$\{CLAUDE_PLUGIN_ROOT\}/agents/([a-z][a-z0-9_-]+)\.md'
    )
```

### Issue 3 of 4
.github/workflows/lint.yml:33-35
**Missing `permissions` block** — without explicit permissions, the workflow inherits the repo's default token scope (read+write in many org setups). Since this job only reads files, locking it down to `contents: read` reduces blast radius if the workflow is ever compromised. Add a `permissions: contents: read` block at the job or workflow level.

### Issue 4 of 4
.github/workflows/lint.yml:38-42
**Actions not pinned to commit SHAs**`actions/checkout@v4` and `actions/setup-python@v5` are mutable tags. If either action's tag is re-pointed to a malicious commit, this workflow would silently execute attacker-controlled code on every PR. Pinning to a full SHA (e.g. `actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683`) provides supply-chain integrity.

Reviews (1): Last reviewed commit: "DOJ-3774: Move lint logic to scripts/ci/..." | Re-trigger Greptile

Comment on lines +54 to +59
desc = fm.get('description')
if not isinstance(desc, str) or not desc.strip():
errors.append(
f'{path}: frontmatter "description" is empty or not a string'
)
checked += 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.

P2 checked is incremented even when the description is invalid. When description is present but empty or the wrong type, the error is appended but there's no continue before checked += 1, so the success message (e.g. "46 file(s) valid") includes files that actually failed validation. The exit code is still correct, but the output is misleading and could mask issues during triage.

Suggested change
desc = fm.get('description')
if not isinstance(desc, str) or not desc.strip():
errors.append(
f'{path}: frontmatter "description" is empty or not a string'
)
checked += 1
desc = fm.get('description')
if not isinstance(desc, str) or not desc.strip():
errors.append(
f'{path}: frontmatter "description" is empty or not a string'
)
continue
checked += 1
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/ci/check_frontmatter.py
Line: 54-59

Comment:
`checked` is incremented even when the description is invalid. When `description` is present but empty or the wrong type, the error is appended but there's no `continue` before `checked += 1`, so the success message (e.g. "46 file(s) valid") includes files that actually failed validation. The exit code is still correct, but the output is misleading and could mask issues during triage.

```suggestion
            desc = fm.get('description')
            if not isinstance(desc, str) or not desc.strip():
                errors.append(
                    f'{path}: frontmatter "description" is empty or not a string'
                )
                continue
            checked += 1
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +32 to +34
ref_pattern = re.compile(
r'\$\{CLAUDE_PLUGIN_ROOT\}/agents/([a-z][a-z0-9-]+)\.md'
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The regex [a-z][a-z0-9-]+ excludes underscores from agent names. If any agent file uses underscores (e.g. agents/translation_reviewer.md), any ${CLAUDE_PLUGIN_ROOT}/agents/translation_reviewer.md reference in skills/commands would silently pass the check even if the file doesn't exist — a false negative in the guard this CI step is meant to provide.

Suggested change
ref_pattern = re.compile(
r'\$\{CLAUDE_PLUGIN_ROOT\}/agents/([a-z][a-z0-9-]+)\.md'
)
ref_pattern = re.compile(
r'\$\{CLAUDE_PLUGIN_ROOT\}/agents/([a-z][a-z0-9_-]+)\.md'
)
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/ci/check_agent_references.py
Line: 32-34

Comment:
The regex `[a-z][a-z0-9-]+` excludes underscores from agent names. If any agent file uses underscores (e.g. `agents/translation_reviewer.md`), any `${CLAUDE_PLUGIN_ROOT}/agents/translation_reviewer.md` reference in skills/commands would silently pass the check even if the file doesn't exist — a false negative in the guard this CI step is meant to provide.

```suggestion
    ref_pattern = re.compile(
        r'\$\{CLAUDE_PLUGIN_ROOT\}/agents/([a-z][a-z0-9_-]+)\.md'
    )
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +33 to +35
jobs:
lint-plugin:
runs-on: ubuntu-latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security Missing permissions block — without explicit permissions, the workflow inherits the repo's default token scope (read+write in many org setups). Since this job only reads files, locking it down to contents: read reduces blast radius if the workflow is ever compromised. Add a permissions: contents: read block at the job or workflow level.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/lint.yml
Line: 33-35

Comment:
**Missing `permissions` block** — without explicit permissions, the workflow inherits the repo's default token scope (read+write in many org setups). Since this job only reads files, locking it down to `contents: read` reduces blast radius if the workflow is ever compromised. Add a `permissions: contents: read` block at the job or workflow level.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread .github/workflows/lint.yml Outdated
Comment on lines +38 to +42
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 security Actions not pinned to commit SHAsactions/checkout@v4 and actions/setup-python@v5 are mutable tags. If either action's tag is re-pointed to a malicious commit, this workflow would silently execute attacker-controlled code on every PR. Pinning to a full SHA (e.g. actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683) provides supply-chain integrity.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/lint.yml
Line: 38-42

Comment:
**Actions not pinned to commit SHAs**`actions/checkout@v4` and `actions/setup-python@v5` are mutable tags. If either action's tag is re-pointed to a malicious commit, this workflow would silently execute attacker-controlled code on every PR. Pinning to a full SHA (e.g. `actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683`) provides supply-chain integrity.

How can I resolve this? If you propose a fix, please make it concise.

1. check_frontmatter.py — add `continue` after empty-description error
   so `checked` only counts files that fully passed validation. Output
   now reflects reality even when some files have malformed descriptions.

2. check_agent_references.py — expand regex to allow underscores in
   agent names ([a-z0-9_-]). The previous pattern would silently skip
   references to e.g. agents/translation_reviewer.md, producing false
   negatives in the very guard this step provides.

3. lint.yml — add `permissions: contents: read` block at workflow
   level. The job only reads files; locking the GITHUB_TOKEN to
   read-only reduces blast radius if the workflow is ever compromised.

4. lint.yml — pin actions/checkout and actions/setup-python to full
   commit SHAs (v4.2.2 → 11bd719..., v5.6.0 → a26af69...) instead of
   mutable version tags. Tags can be re-pointed; SHAs cannot. Also pin
   pyyaml to 6.0.2 for reproducible runs.

All 4 are P2 hygiene issues per Greptile review on PR #17. Verified
locally: 3 schemas, 46 frontmatter files, 7 agents — all still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lapc506 lapc506 merged commit e09fa0a into main May 5, 2026
1 check passed
@lapc506 lapc506 deleted the andres/doj-3774-infra-add-minimal-ci-to-instructional-design-toolkit-json branch May 5, 2026 03:39
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.

1 participant