Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .conductor/registry/workflows/actionable.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@
- "-NoProfile"
- "-Command"
- >-
$prNumber = {{ open_evidence_pr.output.pr_number }};
$prNumber = [int]'{{ open_evidence_pr.output.pr_number }}';
$prUrl = '{{ open_evidence_pr.output.pr_url }}';
try {
gh pr merge $prNumber --squash --auto --delete-branch 2>&1 | Out-Null;
Expand Down Expand Up @@ -872,16 +872,16 @@
**Detail:** {{ executor_router.output.error }}
{%- elif ensure_evidence_branch is defined and ensure_evidence_branch.output.error is defined and ensure_evidence_branch.output.error != '' -%}
**Stage:** ensure_evidence_branch
**Detail:** {{ ensure_evidence_branch.output.error }}

Check warning on line 875 in .conductor/registry/workflows/actionable.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'ensure_evidence_branch.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if ensure_evidence_branch.output.error is defined %}', '{% if ensure_evidence_branch.output is defined %}', or pipe through '| default(...)'.
{%- elif compose_addendum is defined and compose_addendum.output.error is defined and compose_addendum.output.error != '' -%}
**Stage:** compose_addendum
**Detail:** {{ compose_addendum.output.error }}{% if compose_addendum.output.error_code is defined %} ({{ compose_addendum.output.error_code }}){% endif %}

Check warning on line 878 in .conductor/registry/workflows/actionable.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'compose_addendum.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if compose_addendum.output.error is defined %}', '{% if compose_addendum.output is defined %}', or pipe through '| default(...)'.
{%- elif open_evidence_pr is defined and open_evidence_pr.output.error is defined and open_evidence_pr.output.error != '' -%}
**Stage:** open_evidence_pr
**Detail:** {{ open_evidence_pr.output.error }}

Check warning on line 881 in .conductor/registry/workflows/actionable.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'open_evidence_pr.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if open_evidence_pr.output.error is defined %}', '{% if open_evidence_pr.output is defined %}', or pipe through '| default(...)'.
{%- elif evidence_floor_check is defined and evidence_floor_check.output.error_code is defined and evidence_floor_check.output.error_code != null -%}
**Stage:** evidence_floor_check
**Detail:** [{{ evidence_floor_check.output.error_code }}] {{ evidence_floor_check.output.error_message }}

Check warning on line 884 in .conductor/registry/workflows/actionable.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'evidence_floor_check.output.error_message' has can_omit_when_null=true and is not guarded. Wrap in '{% if evidence_floor_check.output.error_message is defined %}', '{% if evidence_floor_check.output is defined %}', or pipe through '| default(...)'.

Check warning on line 884 in .conductor/registry/workflows/actionable.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'evidence_floor_check.output.error_code' has can_omit_when_null=true and is not guarded. Wrap in '{% if evidence_floor_check.output.error_code is defined %}', '{% if evidence_floor_check.output is defined %}', or pipe through '| default(...)'.
{%- elif evidence_reviewer is defined and evidence_reviewer.output.decision == 'block' -%}
**Stage:** evidence_reviewer
**Detail:** {{ evidence_reviewer.output.comment }}
Expand Down
4 changes: 2 additions & 2 deletions .conductor/registry/workflows/apex-driver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@
- "-NoProfile"
- "-Command"
- >-
$failed = {{ wave_dispatch_loop.errors | length }};
$total = {{ wave_dispatch_loop.count }};
$failed = [int]'{{ wave_dispatch_loop.errors | length }}';
$total = [int]'{{ wave_dispatch_loop.count }}';
[ordered]@{
total_waves = $total;
failed_waves = $failed;
Expand Down Expand Up @@ -977,7 +977,7 @@

```
{%- if build_worklist.output.error is defined -%}
{{ build_worklist.output.error }}

Check warning on line 980 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'build_worklist.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if build_worklist.output.error is defined %}', '{% if build_worklist.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error message captured — check polyphony logs)
{%- endif -%}
Expand Down Expand Up @@ -1013,7 +1013,7 @@
### `polyphony state next-ready` error
```
{%- if preflight_apex_state is defined and preflight_apex_state.output is defined and preflight_apex_state.output.error is defined and preflight_apex_state.output.error != '' -%}
{{ preflight_apex_state.output.error }}

Check warning on line 1016 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'preflight_apex_state.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if preflight_apex_state.output.error is defined %}', '{% if preflight_apex_state.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
Expand All @@ -1022,7 +1022,7 @@
### `polyphony branch ensure-feature` error
```
{%- if preflight_ensure_branch is defined and preflight_ensure_branch.output is defined and preflight_ensure_branch.output.error is defined and preflight_ensure_branch.output.error != '' -%}
{{ preflight_ensure_branch.output.error }}

Check warning on line 1025 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'preflight_ensure_branch.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if preflight_ensure_branch.output.error is defined %}', '{% if preflight_ensure_branch.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
Expand All @@ -1040,7 +1040,7 @@
### `declare_root` error
```
{%- if declare_root is defined and declare_root.output is defined and declare_root.output.error is defined and declare_root.output.error != '' -%}
{{ declare_root.output.error }}

Check warning on line 1043 in .conductor/registry/workflows/apex-driver.yaml

View workflow job for this annotation

GitHub Actions / build-and-test

JINJA002: 'declare_root.output.error' has can_omit_when_null=true and is not guarded. Wrap in '{% if declare_root.output.error is defined %}', '{% if declare_root.output is defined %}', or pipe through '| default(...)'.
{%- else -%}
(no error — this step succeeded or did not run)
{%- endif -%}
Expand Down
2 changes: 1 addition & 1 deletion .conductor/registry/workflows/implement-merge-group.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ agents:
- >-
$ErrorActionPreference = 'Stop';
$PSNativeCommandUseErrorActionPreference = $true;
$taskId = {{ primary_router.output.primary_id }};
$taskId = [int]'{{ primary_router.output.primary_id }}';
twig set $taskId;
twig note --text 'Item implementation merged into MG branch via impl PR';
twig sync;
Expand Down
8 changes: 4 additions & 4 deletions .conductor/registry/workflows/plan-level.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -665,7 +665,7 @@ agents:
- "-NoProfile"
- "-Command"
- |
$count = {{ context.history | select('eq', 'research_dispatch') | list | length }}
$count = [int]'{{ context.history | select('eq', 'research_dispatch') | list | length }}'
$maxLoops = 3
$capReached = $count -ge $maxLoops
$hasTopics = '{{ (architect.output.research_needs.topics | default([]) | length > 0) | string | lower }}' -eq 'true'
Expand Down Expand Up @@ -784,8 +784,8 @@ agents:
- "-NoProfile"
- "-Command"
- |
$count = {{ context.history | select('eq', 'open_questions_gate') | list | length }}
$maxLoops = {{ open_questions_policy.output.max_question_loops }}
$count = [int]'{{ context.history | select('eq', 'open_questions_gate') | list | length }}'
$maxLoops = [int]'{{ open_questions_policy.output.max_question_loops }}'
$capReached = $count -ge $maxLoops
@{ iteration = $count; max_loops = $maxLoops; cap_reached = $capReached } | ConvertTo-Json
routes:
Expand Down Expand Up @@ -1494,7 +1494,7 @@ agents:
- "-Command"
- |
$ErrorActionPreference = 'Stop'
$prNumber = {{ poll_status.output.pr_number }}
$prNumber = [int]'{{ poll_status.output.pr_number }}'
$headSha = '{{ poll_status.output.head_sha }}'
$repoSlug = '{{ poll_status.output.repo_slug }}'
$body = "polyphony:approve $headSha"
Expand Down
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,23 @@ jobs:
$result = Invoke-Pester -Path .conductor/registry/tests/lint-strict-undefined.Tests.ps1 -Output Detailed -PassThru
if ($result.FailedCount -gt 0) { exit 1 }

- name: Lint workflow YAMLs (PowerShell + Jinja bareword booleans)
# Catches the AB#3156 Bug 1 class: a `command: pwsh` script body
# that assigns a Jinja render unquoted (`$x = {{ … }}`). When
# Jinja emits `true` / `false` (Python bool → lowercase string),
# PowerShell parses the bareword as a cmdlet name, errors silently,
# and leaves `$x = $null` — which downstream Jinja then treats as
# `null != false`, routing to the wrong branch (killed an AB#3127
# dogfood relaunch). PR #354 fixed plan-level.yaml; this lint stops
# the pattern from coming back across every workflow YAML. Required
# form: `$x = '{{ … }}' -eq 'true'` (or, for a numeric render,
# `[int]'{{ … }}'`). Sequenced after `Install powershell-yaml` so
# the YAML parser is available.
if: always()
shell: pwsh
working-directory: polyphony
run: pwsh -NoProfile -File tests/lint-pwsh-jinja-bareword.ps1 -Format github

- name: Lint workflow YAMLs (Jinja2 resolver)
# Resolves every `{{ <step>.output.<path> }}` reference in workflow
# YAMLs against the verb-output schema registry (#173 / PR #184).
Expand Down
303 changes: 303 additions & 0 deletions tests/lint-pwsh-jinja-bareword.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
BeforeAll {
$script:LintScript = Join-Path $PSScriptRoot 'lint-pwsh-jinja-bareword.ps1'

function New-TempWorkflowsDir {
$dir = Join-Path ([System.IO.Path]::GetTempPath()) `
"lint-pwsh-jinja-bareword-$([guid]::NewGuid().ToString('N').Substring(0,8))"
New-Item -ItemType Directory -Path $dir -Force | Out-Null
return $dir
}

function Invoke-Lint {
param([string] $WorkflowsDir, [string] $Format = 'human')
$output = pwsh -NoProfile -File $script:LintScript `
-WorkflowsDir $WorkflowsDir -Format $Format 2>&1
return @{ Output = ($output -join "`n"); ExitCode = $global:LASTEXITCODE }
}
}

Describe 'lint-pwsh-jinja-bareword.ps1' {

BeforeEach {
$script:WorkflowsDir = New-TempWorkflowsDir
$global:LASTEXITCODE = 0
}

AfterEach {
Remove-Item $script:WorkflowsDir -Recurse -Force -ErrorAction SilentlyContinue
}

Context 'Clean inputs (exit 0)' {

It 'Passes on an empty workflows directory' {
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}

It 'Passes when workflows directory does not exist' {
$missing = Join-Path $script:WorkflowsDir 'does-not-exist'
$r = Invoke-Lint -WorkflowsDir $missing
$r.ExitCode | Should -Be 0
}

It 'Passes a single-quoted Jinja render compared to a string (the canonical safe form)' {
$body = @'
agents:
- name: ok-quoted-bool
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- |
$hasTopics = '{{ (architect.output.topics | default([]) | length > 0) | string | lower }}' -eq 'true'
Write-Host $hasTopics
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'ok.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}

It 'Passes a double-quoted Jinja render' {
$body = @'
agents:
- name: ok-double-quote
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- |
$name = "{{ architect.output.name }}"
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'ok-double.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}

It 'Passes Jinja renders embedded inside an array literal of quoted strings' {
$body = @'
agents:
- name: ok-array
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- |
$arr = @('{{ architect.output.a }}', '{{ architect.output.b }}')
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'ok-array.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}

It 'Passes the quote-and-cast pattern: $count = [int]''{{ ... }}''' {
$body = @'
agents:
- name: ok-int-cast
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- |
$count = [int]'{{ context.history | length }}'
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'ok-int.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}

It 'Ignores agents whose command is not pwsh' {
$body = @'
agents:
- name: a-twig-step
type: script
command: twig
args:
- "set"
- "{{ workflow.input.id }}"
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'twig.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}
}

Context 'Violations (exit 1)' {

It 'Flags $hasTopics = {{ ... }} (the AB#3156 Bug 1 shape)' {
$body = @'
agents:
- name: bad-bool
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- |
$hasTopics = {{ (architect.output.topics | default([]) | length > 0) | string | lower }}
Write-Host $hasTopics
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'bad-bool.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 1
$r.Output | Should -Match '\$hasTopics'
$r.Output | Should -Match 'bad-bool\.yaml'
}

It 'Flags $count = {{ items | length }} (integer bareword)' {
$body = @'
agents:
- name: bad-int
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- |
$count = {{ items | length }}
Write-Host $count
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'bad-int.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 1
$r.Output | Should -Match '\$count'
}

It 'Flags a bareword statement inside a folded (>-) block scalar with semicolons' {
$body = @'
agents:
- name: bad-folded
type: script
command: pwsh
args:
- "-NoProfile"
- "-Command"
- >-
$prNumber = {{ poll.output.pr_number }};
Write-Host $prNumber
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'bad-folded.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 1
$r.Output | Should -Match '\$prNumber'
}

It 'Aggregates violations across multiple agents in the same file' {
$body = @'
agents:
- name: bad-a
type: script
command: pwsh
args:
- "-Command"
- |
$a = {{ x.output.a }}
- name: bad-b
type: script
command: pwsh
args:
- "-Command"
- |
$b = {{ x.output.b }}
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'multi.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 1
$r.Output | Should -Match '\$a'
$r.Output | Should -Match '\$b'
}

It 'Reports the source line number of the violation' {
$body = @'
agents:
- name: bad-line
type: script
command: pwsh
args:
- "-Command"
- |
$hasTopics = {{ x.output.topics | length > 0 }}
'@
$path = Join-Path $script:WorkflowsDir 'bad-line.yaml'
Set-Content -Path $path -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 1
# The body line `$hasTopics = …` lives on line 8 of the file
# (1-based). Match against the file:line snippet pattern the
# human formatter emits.
$r.Output | Should -Match 'bad-line\.yaml:8'
}
}

Context 'Whitelist marker' {

It 'Suppresses a violation when # bareword-ok appears on the preceding line' {
$body = @'
agents:
- name: ok-whitelisted
type: script
command: pwsh
args:
- "-Command"
- |
# bareword-ok: integer used in arithmetic; render is always digits
$count = {{ items | length }}
Write-Host $count
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'wl.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}

It 'Suppresses a violation when # bareword-ok appears on the same line' {
$body = @'
agents:
- name: ok-same-line
type: script
command: pwsh
args:
- "-Command"
- |
$count = {{ items | length }} # bareword-ok: integer literal
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'wl-same.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir
$r.ExitCode | Should -Be 0
}
}

Context 'GitHub Actions output format' {

It 'Emits ::error annotations under -Format github' {
$body = @'
agents:
- name: gha-bad
type: script
command: pwsh
args:
- "-Command"
- |
$hasTopics = {{ x.output.topics | length > 0 }}
'@
Set-Content -Path (Join-Path $script:WorkflowsDir 'gha.yaml') -Value $body
$r = Invoke-Lint -WorkflowsDir $script:WorkflowsDir -Format 'github'
$r.ExitCode | Should -Be 1
$r.Output | Should -Match '::error file=\.conductor/registry/workflows/gha\.yaml,line=\d+::'
$r.Output | Should -Match '\$hasTopics'
}
}

Context 'Live tree' {

It 'The real .conductor/registry/workflows/ tree passes the lint' {
# Defense: this test is the gate that catches a real workflow
# introducing the bareword pattern. If it ever fails, fix the
# workflow — do not loosen the lint.
$r = pwsh -NoProfile -File $script:LintScript 2>&1
$global:LASTEXITCODE | Should -Be 0
}
}
}
Loading
Loading