Run each Required work check: command as a deterministic required-work gate #814

Closed
opened 2026-09-28 20:50:47 +00:00 by jared · 3 comments
Owner

Problem

No deterministic gate checks a ticket's Required work list. The change-auditor copies each check: command into a WORK: line as text (agents/change-auditor.md:49). No gate or agent runs the command. Only the contract-auditor compares the diff with the work list, and it runs after suite-check. When a node before it escalates, or a hand-finish skips it, nothing checks the required work. plugins/os-sdlc/CLAUDE.md puts every pass/fail decision in tested Ruby, not in prompts.

In #808, implementation 67, this happened:

Required work item Proof in ticket Checked by Result
1. Add TestReport, move callers AC 1-4 (new class only) behavior-verifier pass, callers not moved
2. Add LintReport, move callers AC 5 (new class only) behavior-verifier pass, callers not moved
3. Delete old classes check: grep -rnE "RedTests|..." plugins/os-sdlc/lib prints nothing nobody never run

The behavior-verifier did what its charter says (agents/behavior-verifier.md:17: "A WORK: item missing from the diff is a GAP: item, not a verdict change"). suite-check then failed on the time limit (#813), and the run escalated before the contract-auditor ran.

programmer -> behavior-verifier -> suite-check -> contract-auditor -> merge-ready
                (ACs only)          (failed)       (never reached)

Reproduce

n/a: design ticket. Evidence is implementation 67 on #808.

Approach

Add a required-work check to the existing suite-check gate call. The suite-check agent still runs os-sdlc-gate DISPATCH_ID once; that one call now runs two checks. No new agent call and no new node.

# maps/poodr-ticket-implementation.yaml, suite-check node
verdict:
  source: gate
  gates: [required-work, suite]
edges:
  fail: { to: programmer-repair, bound: 2, note: "downstream regression or required work not done" }

required-work runs first because it takes about 1 s; the gate stops at the first failing check (model/gate.rb:125-131), so the suite runs only when the required work is done.

The step follows the retired-tests pattern: the registry in query/gate/commands.rb maps required-work to a script command, os-sdlc-required-work-check IMPLEMENTATION_ID. The script reads the ticket body, runs each item's check: command in the project root, prints RequiredWorkReport#lines (failing items only), and exits 1 when any item fails. The gate stores that short report as the step output.

R3 Delete the replaced classes
   check: ! grep -rnE "RedTests|MinitestSummary" plugins/os-sdlc/lib
   plugins/os-sdlc/lib/os_sdlc/query/brief/facts.rb:61: RedTests.parse(...)
   ... 6 more lines

Receiver: programmer-repair. Its enrich list already has gate_results, which holds every step of the latest completed dispatch (query/brief/facts.rb:49-59) — after suite-check fails, that is {"required-work" => {verdict, output: <report>}}. The brief and the map enrich lists do not change.

The check form is the one the capture template already uses: check: `<command>` . Exit 0 means pass. For "prints nothing", negate: check: `! grep -rnE "RedTests" plugins/os-sdlc/lib` . #808 wrote its check as free text.

Known limit: programmer-repair never edits tests. An item that needs a test change fails twice and escalates, with the report in the escalation. Routing by item owner is out of scope.

Decision record: ADR-0244 (amends ADR-0209).

Required work:

  1. TicketShape::RequiredWork returns the backticked check: command of each item that has one.
  2. Add RequiredWorkReport with lines(limit:) that lists only failing items, each with its item text, command, and at most limit output lines.
  3. Add the os-sdlc-required-work-check IMPLEMENTATION_ID script and register required-work in query/gate/commands.rb and Gate::Name.
  4. Set the suite-check node gates to [required-work, suite] in maps/poodr-ticket-implementation.yaml.
  5. TicketShape at implementation-open rejects a check: that has no backticked command and names the item. The os-backlog ticket body checker gets the same rule in #817 (ADR-0209 keeps the two in lockstep; one pipeline run takes one target).

Rejected: the contract-auditor runs the check — an agent would still read the output and decide the verdict, and it runs after suite-check, so it never ran on #808.
Rejected: add required-work to the programmer gate list — items owned by the test-writer or refactorer would block the programmer.
Rejected: [suite, required-work] — one wasted suite run per repair round.

Acceptance criteria

  1. Given a ticket with a check: command that exits non-zero, when the suite-check node runs, then its verdict is fail and the required-work step output names only the failing item, its command, and its output.
  2. Given a ticket whose check: commands all exit 0 and a green suite, when the suite-check node runs, then its verdict is pass.
  3. Given a ticket with no check: commands, when the suite-check node runs, then the required-work step passes.
  4. Given a failing required-work step, when the programmer-repair brief is built, then its gate_results contain the required-work report.
  5. Given a check whose output has more lines than the limit, when the report renders, then it shows no more than the limit for that item and says how many lines it left out.
  6. Given a ticket body with check: followed by text and no backticked command, when implementation-open runs on it, then it refuses and names the item.

Owner sketch

class TicketShape::RequiredWork
  def checks
end

class RequiredWorkReport
  def initialize(results)
  def lines(limit: nil)
end

Origin

  • Trigger: hand-finish of #808 after implementation 67 escalated; the behavior-verifier had passed with required work 1-3 undone
  • Improvised this session: required work 1-3 hand-finished; the #808 check grep was run by hand
  • Chain: required work undone at pass <- only the contract-auditor compares diff with work list, after suite-check (maps/poodr-ticket-implementation.yaml suite-check -> contract-auditor) <- check: commands are text, never run (agents/change-auditor.md:49) <- DESIGN (ADR-0209, change-auditor alignment work list)
  • Root candidate: this ticket
  • Where: plugins/os-sdlc/lib/os_sdlc/parser/ticket_shape/required_work.rb, plugins/os-sdlc/maps/poodr-ticket-implementation.yaml, plugins/os-sdlc/agents/behavior-verifier.md
  • Session: 47a537c2-08a1-4d3c-9688-75df56dfdd3d
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-808/47a537c2-08a1-4d3c-9688-75df56dfdd3d.jsonl
  • Related: #808, #813
## Problem No deterministic gate checks a ticket's `Required work` list. The change-auditor copies each `check:` command into a `WORK:` line as text (`agents/change-auditor.md:49`). No gate or agent runs the command. Only the contract-auditor compares the diff with the work list, and it runs after `suite-check`. When a node before it escalates, or a hand-finish skips it, nothing checks the required work. `plugins/os-sdlc/CLAUDE.md` puts every pass/fail decision in tested Ruby, not in prompts. In #808, implementation 67, this happened: | Required work item | Proof in ticket | Checked by | Result | | --- | --- | --- | --- | | 1. Add TestReport, move callers | AC 1-4 (new class only) | behavior-verifier | pass, callers not moved | | 2. Add LintReport, move callers | AC 5 (new class only) | behavior-verifier | pass, callers not moved | | 3. Delete old classes | `check: grep -rnE "RedTests\|..." plugins/os-sdlc/lib prints nothing` | nobody | never run | The behavior-verifier did what its charter says (`agents/behavior-verifier.md:17`: "A `WORK:` item missing from the diff is a `GAP:` item, not a verdict change"). `suite-check` then failed on the time limit (#813), and the run escalated before the contract-auditor ran. ``` programmer -> behavior-verifier -> suite-check -> contract-auditor -> merge-ready (ACs only) (failed) (never reached) ``` ## Reproduce n/a: design ticket. Evidence is implementation 67 on #808. ## Approach Add a `required-work` check to the existing suite-check gate call. The suite-check agent still runs `os-sdlc-gate DISPATCH_ID` once; that one call now runs two checks. No new agent call and no new node. ```yaml # maps/poodr-ticket-implementation.yaml, suite-check node verdict: source: gate gates: [required-work, suite] edges: fail: { to: programmer-repair, bound: 2, note: "downstream regression or required work not done" } ``` `required-work` runs first because it takes about 1 s; the gate stops at the first failing check (`model/gate.rb:125-131`), so the suite runs only when the required work is done. The step follows the `retired-tests` pattern: the registry in `query/gate/commands.rb` maps `required-work` to a script command, `os-sdlc-required-work-check IMPLEMENTATION_ID`. The script reads the ticket body, runs each item's `check:` command in the project root, prints `RequiredWorkReport#lines` (failing items only), and exits 1 when any item fails. The gate stores that short report as the step output. ``` R3 Delete the replaced classes check: ! grep -rnE "RedTests|MinitestSummary" plugins/os-sdlc/lib plugins/os-sdlc/lib/os_sdlc/query/brief/facts.rb:61: RedTests.parse(...) ... 6 more lines ``` Receiver: `programmer-repair`. Its `enrich` list already has `gate_results`, which holds every step of the latest completed dispatch (`query/brief/facts.rb:49-59`) — after suite-check fails, that is `{"required-work" => {verdict, output: <report>}}`. The brief and the map `enrich` lists do not change. The check form is the one the capture template already uses: ``check: `<command>` ``. Exit 0 means pass. For "prints nothing", negate: ``check: `! grep -rnE "RedTests" plugins/os-sdlc/lib` ``. #808 wrote its check as free text. Known limit: `programmer-repair` never edits tests. An item that needs a test change fails twice and escalates, with the report in the escalation. Routing by item owner is out of scope. Decision record: ADR-0244 (amends ADR-0209). Required work: 1. `TicketShape::RequiredWork` returns the backticked `check:` command of each item that has one. 2. Add `RequiredWorkReport` with `lines(limit:)` that lists only failing items, each with its item text, command, and at most `limit` output lines. 3. Add the `os-sdlc-required-work-check IMPLEMENTATION_ID` script and register `required-work` in `query/gate/commands.rb` and `Gate::Name`. 4. Set the suite-check node gates to `[required-work, suite]` in `maps/poodr-ticket-implementation.yaml`. 5. `TicketShape` at `implementation-open` rejects a `check:` that has no backticked command and names the item. The os-backlog ticket body checker gets the same rule in #817 (ADR-0209 keeps the two in lockstep; one pipeline run takes one target). Rejected: the contract-auditor runs the check — an agent would still read the output and decide the verdict, and it runs after suite-check, so it never ran on #808. Rejected: add `required-work` to the programmer gate list — items owned by the test-writer or refactorer would block the programmer. Rejected: `[suite, required-work]` — one wasted suite run per repair round. ## Acceptance criteria 1. Given a ticket with a `check:` command that exits non-zero, when the suite-check node runs, then its verdict is fail and the `required-work` step output names only the failing item, its command, and its output. 2. Given a ticket whose `check:` commands all exit 0 and a green suite, when the suite-check node runs, then its verdict is pass. 3. Given a ticket with no `check:` commands, when the suite-check node runs, then the `required-work` step passes. 4. Given a failing `required-work` step, when the programmer-repair brief is built, then its `gate_results` contain the `required-work` report. 5. Given a check whose output has more lines than the limit, when the report renders, then it shows no more than the limit for that item and says how many lines it left out. 6. Given a ticket body with `check:` followed by text and no backticked command, when `implementation-open` runs on it, then it refuses and names the item. ## Owner sketch ```ruby class TicketShape::RequiredWork def checks end class RequiredWorkReport def initialize(results) def lines(limit: nil) end ``` ## Origin - Trigger: hand-finish of #808 after implementation 67 escalated; the behavior-verifier had passed with required work 1-3 undone - Improvised this session: required work 1-3 hand-finished; the #808 check grep was run by hand - Chain: required work undone at pass <- only the contract-auditor compares diff with work list, after suite-check (maps/poodr-ticket-implementation.yaml suite-check -> contract-auditor) <- `check:` commands are text, never run (agents/change-auditor.md:49) <- DESIGN (ADR-0209, change-auditor alignment work list) - Root candidate: this ticket - Where: plugins/os-sdlc/lib/os_sdlc/parser/ticket_shape/required_work.rb, plugins/os-sdlc/maps/poodr-ticket-implementation.yaml, plugins/os-sdlc/agents/behavior-verifier.md - Session: 47a537c2-08a1-4d3c-9688-75df56dfdd3d - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os--claude-worktrees-ticket-808/47a537c2-08a1-4d3c-9688-75df56dfdd3d.jsonl - Related: #808, #813
Author
Owner

Work started on branch ticket-814 via /os-sdlc:implement, target os-sdlc. The os-backlog checker half moved to #817.

Work started on branch ticket-814 via /os-sdlc:implement, target os-sdlc. The os-backlog checker half moved to #817.
Author
Owner

Resolution

Done: required-work gate step added: suite-check runs [required-work, suite] in one gate call; os-sdlc-required-work-check runs each backticked check: command and prints RequiredWorkReport lines for failing items only; implementation-open refuses a check: without a backticked command; Dispatch::CheckResult replaces RetiredTestsCheckResult (lens challenge found the name a defect).

Evidence: Implementation 71 complete: 10 dispatches, 14 min 49 s, all verdicts pass after one test-reviewer fail. Commits d2fc40f6, dcade8ff, 4abb2cc3 on ticket-814. Full os-sdlc suite 1302 runs, 0 failures, 14.0 s. Rubocop clean on 13 changed files. Negated checks verified: '! grep' that matches exits 1. ADR-0244.

Follow-ups: #817 (os-backlog body checker half, next in this session). Worktree guard blocks subagent-stop heredocs in worktree-isolated sessions; handoffs were submitted from files this run (known from #803, not ticketed here: see session summary).

## Resolution **Done:** required-work gate step added: suite-check runs [required-work, suite] in one gate call; os-sdlc-required-work-check runs each backticked check: command and prints RequiredWorkReport lines for failing items only; implementation-open refuses a check: without a backticked command; Dispatch::CheckResult replaces RetiredTestsCheckResult (lens challenge found the name a defect). **Evidence:** Implementation 71 complete: 10 dispatches, 14 min 49 s, all verdicts pass after one test-reviewer fail. Commits d2fc40f6, dcade8ff, 4abb2cc3 on ticket-814. Full os-sdlc suite 1302 runs, 0 failures, 14.0 s. Rubocop clean on 13 changed files. Negated checks verified: '! grep' that matches exits 1. ADR-0244. **Follow-ups:** #817 (os-backlog body checker half, next in this session). Worktree guard blocks subagent-stop heredocs in worktree-isolated sessions; handoffs were submitted from files this run (known from #803, not ticketed here: see session summary).
jared closed this issue 2026-09-28 22:06:12 +00:00
Author
Owner

Follow-up from the close comment: the worktree guard defect is now #819.

Follow-up from the close comment: the worktree guard defect is now #819.
jared referenced this issue from a commit 2026-09-29 13:12:09 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#814
No description provided.