Skip to content

fix(lint): AB#3159 — ban PowerShell bareword Jinja renders in workflow YAMLs - #362

Merged
PolyphonyRequiem merged 1 commit into
mainfrom
fix/ab-3159-lint-pwsh-jinja-bareword
May 14, 2026
Merged

fix(lint): AB#3159 — ban PowerShell bareword Jinja renders in workflow YAMLs#362
PolyphonyRequiem merged 1 commit into
mainfrom
fix/ab-3159-lint-pwsh-jinja-bareword

Conversation

@PolyphonyRequiem

Copy link
Copy Markdown
Owner

Closes AB#3159 (F1 of Epic AB#3158 — Convert prose conventions to typed enforcement).

The bug class

AB#3156 Bug 1 (PR #354 / commit e48a31d) was a command: pwsh script in plan-level.yaml that did:

$hasTopics = {{ (architect.output.research_needs.topics | default([]) | length > 0) | string | lower }}

Jinja renders true / false (Python bool → lowercase string). PowerShell parses bareword true as a cmdlet name, finds nothing, errors silently — $hasTopics ends up $null. Serialized to JSON as null; downstream Jinja then evaluates null != false and routes to the wrong branch. Killed an AB#3127 dogfood relaunch.

The required form wraps the Jinja render in a string literal and string-compares (or casts) the result:

$hasTopics = '{{ (...) | string | lower }}' -eq 'true'   # bool
$count     = [int]'{{ ... | length }}'                   # int

Type is [bool] / [int] regardless of what Jinja emits, and a malformed render fails loudly instead of silently nulling.

What this PR ships

  1. tests/lint-pwsh-jinja-bareword.ps1 — Pester-tested lint that scans every .conductor/registry/workflows/*.yaml, parses via powershell-yaml, walks each command: pwsh agents args: list, and flags any line matching \$\w+\s*=\s*\{\{[^}]+\}\} where the {{ is not preceded by a quote. Whitelist marker # bareword-ok: <reason> on the same or preceding line opts a line out (use sparingly).

  2. tests/lint-pwsh-jinja-bareword.Tests.ps1 — 16 Pester tests covering the canonical safe form, double-quoted form, array-literal form, [int] cast form, the AB#3156 Bug 1 shape, integer-bareword shape, folded-block-scalar (>-) shape, multi-agent aggregation, line-number reporting, both whitelist marker positions, GitHub annotations format, and a live-tree gate.

  3. .github/workflows/ci.yml — new Lint workflow YAMLs (PowerShell + Jinja bareword booleans) step sequenced after Install powershell-yaml (the lint requires that module).

  4. Audit + remediation of every other in-tree case the lint surfaced. PR fix(plan-level): AB#3156 — research path StrictUndefined + PowerShell bareword #354 fixed the boolean case in plan-level.yaml; this PR remediates 8 integer-bareword cases (defense-in-depth — bareword integers happen to parse correctly today, but every Jinja render should be quoted-and-cast so a future render-time anomaly fails loudly):

    File Agent Variable
    actionable.yaml merge_evidence_pr $prNumber
    apex-driver.yaml wave_loop_summary $failed, $total
    implement-merge-group.yaml primary_completer $taskId
    plan-level.yaml research_loop_counter $count
    plan-level.yaml open_questions_counter $count, $maxLoops
    plan-level.yaml auto_approve_plan_pr $prNumber

    All converted to [int]'{{ … }}'.

Verification

  • pwsh -NoProfile -File tests/lint-pwsh-jinja-bareword.ps1 exits 0 against main after these remediations.
  • Invoke-Pester -Path tests/lint-pwsh-jinja-bareword.Tests.ps1: 16/16 green.
  • Invoke-Pester -Path tests/harness/run-scenarios.Tests.ps1: 5/5 green (architect_research_loop, cascade_remedy_no_stale, close_out_happy_path, research_happy_path).
  • Existing lints (lint-jinja-resolver, lint-strict-undefined, lint-prose-children, lint-type-agnostic): all clean.
  • lint-conductor-validate.Tests.ps1 + workflow-terminal-satisfied-observable.Tests.ps1: 13/13 green.

…w YAMLs

## Bug class

AB#3156 Bug 1 (PR #354 / commit e48a31d) was a `command: pwsh` script in
plan-level.yaml that did:

    $hasTopics = {{ (architect.output.research_needs.topics
                     | default([]) | length > 0) | string | lower }}

Jinja renders `true` / `false` (Python bool → lowercase string). PowerShell
parses bareword `true` as a cmdlet name, finds nothing, errors silently —
`$hasTopics` ends up `$null`. Serialized to JSON as `null`; downstream
Jinja then evaluates `null != false` and routes to the wrong branch.
Killed an AB#3127 dogfood relaunch.

The required form is to wrap the Jinja render in a string literal and
string-compare (or cast) the result:

    $hasTopics = '{{ (...) | string | lower }}' -eq 'true'   # bool
    $count     = [int]'{{ ... | length }}'                   # int

Type is `[bool]` / `[int]` regardless of what Jinja emits, and a malformed
render fails loudly instead of silently nulling.

## What this PR ships

1. `tests/lint-pwsh-jinja-bareword.ps1` — Pester-tested lint that scans
   every `.conductor/registry/workflows/*.yaml`, parses via
   `powershell-yaml`, walks each `command: pwsh` agents `args:` list, and
   flags any line matching `\$\w+\s*=\s*\{\{[^}]+\}\}` where the `{{`
   is not preceded by a quote. Whitelist marker `# bareword-ok: <reason>`
   on the same or preceding line opts a line out (use sparingly).

2. `tests/lint-pwsh-jinja-bareword.Tests.ps1` — 16 Pester tests covering
   the canonical safe form, double-quoted form, array-literal form,
   `[int]'...'` cast form, the AB#3156 Bug 1 shape, integer-bareword
   shape, folded-block-scalar (`>-`) shape, multi-agent aggregation,
   line-number reporting, both whitelist marker positions, GitHub
   annotations format, and a live-tree gate.

3. `.github/workflows/ci.yml` — new `Lint workflow YAMLs (PowerShell +
   Jinja bareword booleans)` step sequenced after `Install
   powershell-yaml` (the lint requires that module).

4. Audit + remediation of every other in-tree case the lint surfaced. PR
   #354 fixed the boolean case in plan-level.yaml; this PR remediates 8
   integer-bareword cases (defense-in-depth — bareword integers happen to
   parse correctly today, but every Jinja render should be quoted-and-cast
   so a future render-time anomaly fails loudly):

   - actionable.yaml          merge_evidence_pr.$prNumber
   - apex-driver.yaml         wave_loop_summary.$failed, .$total
   - implement-merge-group.yaml  primary_completer.$taskId
   - plan-level.yaml          research_loop_counter.$count
                              open_questions_counter.$count, .$maxLoops
                              auto_approve_plan_pr.$prNumber

   All converted to `[int]'{{ … }}'`.

## Verification

- `pwsh -NoProfile -File tests/lint-pwsh-jinja-bareword.ps1` exits 0
  against `main` after these remediations.
- `Invoke-Pester -Path tests/lint-pwsh-jinja-bareword.Tests.ps1`: 16/16
  green.
- `Invoke-Pester -Path tests/harness/run-scenarios.Tests.ps1`: 5/5 green
  (architect_research_loop, cascade_remedy_no_stale, close_out_happy_path,
  research_happy_path).
- Existing lints (lint-jinja-resolver, lint-strict-undefined,
  lint-prose-children, lint-type-agnostic) all clean.
- lint-conductor-validate.Tests.ps1 + workflow-terminal-satisfied-observable.Tests.ps1:
  13/13 green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@PolyphonyRequiem
PolyphonyRequiem merged commit 5aeec4f into main May 14, 2026
1 check passed
@PolyphonyRequiem
PolyphonyRequiem deleted the fix/ab-3159-lint-pwsh-jinja-bareword branch May 14, 2026 01:06
PolyphonyRequiem added a commit that referenced this pull request May 14, 2026
…Item 2) (#370)

Adds Phase 2.5 to Invoke-PolyphonySdlc.ps1: a 	wig show $ApexId --output
json pre-flight that refuses to dispatch when the target work item is in a
terminal state (Done, Closed, Removed, Resolved).

Closes the AB#3127 reproducer documented in the AB#3165 epic: re-running
the apex driver against a completed work item produces a false-positive
empty_merge_group_structural_violation at scope_revise_cap_gate, wasting
15+ min before hitting the cap. Outputs of run N pollute inputs of run N+1
because the work this item describes is already on main; the implementer
agent has nothing to do; the scope reviewer mistakes the empty MG for an
upstream classifier failure.

Bypassed by:
* -Intent resume / replan  (operator is intentionally rejoining)
* -SkipStateCheck          (escape hatch for transition / debug scenarios)

Refusal message points the operator at the canonical remediation:
	wig state $ApexId 'To Do' then re-launch.

Tests: 11 new tests in scripts/Invoke-PolyphonySdlc.Tests.ps1 covering
each terminal state, both bypass intents, the SkipStateCheck switch, and
the error-message remediation pointers. Existing 32 tests pass
transparently via a 	wig.cmd shim injected by New-BareRepoFixture
(default state: 'To Do', overridable via $env:TWIG_FAKE_STATE).

Empirical reproducer: AB#3159 dogfood 2026-05-13 launched against a
work item whose implementation had landed on main 2h earlier via PR
#362. Implementer correctly produced an empty branch; scope reviewer
hit the cap gate after 3 cycles. This change would have refused the
launch upfront — provided the operator had marked the item Done first.
The To-Do-but-already-merged variant remains an open class of bug
(filed under AB#3165 Item 4: branch-tip zero-diff short-circuit at
apex dispatch time).

Co-authored-by: Daniel Green <dangreen@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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