main · last commit
13 days ago ·
7g0stsfu
ah-1cx.16 runner: reject a blank rendered PROMPT.md at Start instead of burning an attempt
Past Stand
bd reopen ah-1cx.16
| Created by | Eugene Blikh |
| Owner | bigbes@gmail.com |
| Created | 2026-08-04T23:46:15Z |
| Started | 2026-08-04T23:54:12Z |
| Updated | 2026-08-05T00:23:21Z |
| Closed | 2026-08-05T00:23:21Z |
Found by the adversarial review of ah-tqc and empirically reproduced against the installed pi 0.82.x. pi's dist/modes/print-mode.js:94 guards the entire run with 'if (initialMessage)', so an empty or whitespace-only prompt makes pi do nothing and exit 0 with a one-line event stream containing only the session header. runner.prepareTaskDir writes s.Prompt verbatim with no non-empty check, and nothing in reconcile guards it either. ah-tqc's second Errored() arm (SessionID != "" && AssistantMessages == 0) now CATCHES this, so the card correctly fails instead of sailing to In Review — but only AFTER the attempt is spent, a worktree is created, a zellij pane is spawned and an attempt number is consumed. A strings.TrimSpace(s.Prompt) == "" rejection in Start would fail it loudly and instantly with an actionable message naming the role and template. Deliberately left out of the ah-tqc fix because the check arguably belongs on the reconcile side of the boundary — the reconciler is what renders the template and knows which role/prompt file produced the empty result, so it can say WHY. Decide which side owns it: a Start-side guard is a cheap backstop that cannot explain itself, a reconcile-side check can name the template but leaves the port unguarded against a future caller.
A role whose template renders to whitespace fails before a worktree or pane is created, with an error naming the role and the prompt file. The ah-tqc Errored() arm stays as the backstop and its test still passes.
ah-1cx
— Hardening: post-audit bug sweep (2026-07-20)
parent-child
open
Nothing depends on this issue.
| id | ah-1cx.16 |
| content_hash | 7eff6608348ecbc852835f358a9a699e40ec1398043f10acc52a4b282570f971 |
| title | runner: reject a blank rendered PROMPT.md at Start instead of burning an attempt |
| description | Found by the adversarial review of ah-tqc and empirically reproduced against the installed pi 0.82.x. pi's dist/modes/print-mode.js:94 guards the entire run with 'if (initialMessage)', so an empty or whitespace-only prompt makes pi do nothing and exit 0 with a one-line event stream containing only the session header. runner.prepareTaskDir writes s.Prompt verbatim with no non-empty check, and nothing in reconcile guards it either. ah-tqc's second Errored() arm (SessionID != "" && AssistantMessages == 0) now CATCHES this, so the card correctly fails instead of sailing to In Review — but only AFTER the attempt is spent, a worktree is created, a zellij pane is spawned and an attempt number is consumed. A strings.TrimSpace(s.Prompt) == "" rejection in Start would fail it loudly and instantly with an actionable message naming the role and template. Deliberately left out of the ah-tqc fix because the check arguably belongs on the reconcile side of the boundary — the reconciler is what renders the template and knows which role/prompt file produced the empty result, so it can say WHY. Decide which side owns it: a Start-side guard is a cheap backstop that cannot explain itself, a reconcile-side check can name the template but leaves the port unguarded against a future caller. |
| design | |
| acceptance_criteria | A role whose template renders to whitespace fails before a worktree or pane is created, with an error naming the role and the prompt file. The ah-tqc Errored() arm stays as the backstop and its test still passes. |
| notes | |
| status | closed |
| priority | 3 |
| issue_type | bug |
| assignee | NULL |
| estimated_minutes | NULL |
| created_at | 2026-08-04T23:46:15Z |
| created_by | Eugene Blikh |
| owner | bigbes@gmail.com |
| updated_at | 2026-08-05T00:23:21Z |
| closed_at | 2026-08-05T00:23:21Z |
| closed_by_session | |
| external_ref | NULL |
| spec_id | |
| compaction_level | 0 |
| compacted_at | NULL |
| compacted_at_commit | NULL |
| original_size | NULL |
| sender | |
| ephemeral | 0 |
| wisp_type | |
| pinned | 0 |
| is_template | 0 |
| mol_type | |
| work_type | |
| source_system | |
| metadata | �{} |
| source_repo | |
| close_reason | Done in 8716a2a. Confirmed: prepareTaskDir writes s.Prompt verbatim and nothing upstream checked it, so a role template rendering to whitespace produced a pi run that did nothing and exited 0 — caught by ah-tqc's second Errored() arm, but only AFTER a worktree, a zellij pane and an attempt number were spent, and with no way to say which role or template was at fault. The guard went on the RECONCILE side, inside renderPrompt: the single seam both claim and launchResume render through, and the only layer holding the role name and the template path — the two facts an operator needs. The error flows into the existing SPEC §12 claim-failure path (card → failed, 'claim failed' comment, claim_failed event) with no new machinery. The port is deliberately NOT guarded twice: ah-tqc's arm is the port's own backstop and stays intact, so a blank prompt reaching Start by some future route still finalizes as an error rather than sailing to In Review; ports.StartSpec.Prompt now documents that split as the contract. Note the check is on the TEMPLATE's output, before the artifact manifest and Context Pack are appended — a role whose own template says nothing is a broken config even if the card happens to carry text. Cover: two prompt_test.go subtests plus TestClaimRejectsBlankRenderedPrompt. |
| event_kind | |
| actor | |
| target | |
| payload | |
| await_type | |
| await_id | |
| timeout_ns | 0 |
| waiters | |
| hook_bead | |
| role_bead | |
| agent_state | |
| last_activity | NULL |
| role_type | |
| rig | |
| due_at | NULL |
| defer_until | NULL |
| no_history | 0 |
| started_at | 2026-08-04T23:54:12Z |
| is_blocked | 0 |
| issue_id | ah-1cx.16 |
| label | milestone:v0.2.0 |
| id | 952bae33-e2bd-50a4-b0af-21a89d8b111b |
| issue_id | ah-1cx.16 |
| type | parent-child |
| created_at | 2026-08-05T02:46:14Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-1cx |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 019fcf2b-b0cb-729f-86c8-2a97e7613e88 |
| issue_id | ah-1cx.16 |
| event_type | created |
| actor | Eugene Blikh |
| old_value | |
| new_value | |
| comment | NULL |
| created_at | 2026-08-05T02:46:14Z |
| id | 019fcf32-f6e8-7d23-9418-066666f4cd8e |
| issue_id | ah-1cx.16 |
| event_type | status_changed |
| actor | Eugene Blikh |
| old_value | {"id":"ah-1cx.16","title":"runner: reject a blank rendered PROMPT.md at Start instead of burning an attempt","description":"Found by the adversarial review of ah-tqc and empirically reproduced against the installed pi 0.82.x.\n\npi's dist/modes/print-mode.js:94 guards the entire run with 'if (initialMessage)', so an empty or whitespace-only prompt makes pi do nothing and exit 0 with a one-line event stream containing only the session header. runner.prepareTaskDir writes s.Prompt verbatim with no non-empty check, and nothing in reconcile guards it either.\n\nah-tqc's second Errored() arm (SessionID != \"\" \u0026\u0026 AssistantMessages == 0) now CATCHES this, so the card correctly fails instead of sailing to In Review — but only AFTER the attempt is spent, a worktree is created, a zellij pane is spawned and an attempt number is consumed. A strings.TrimSpace(s.Prompt) == \"\" rejection in Start would fail it loudly and instantly with an actionable message naming the role and template.\n\nDeliberately left out of the ah-tqc fix because the check arguably belongs on the reconcile side of the boundary — the reconciler is what renders the template and knows which role/prompt file produced the empty result, so it can say WHY. Decide which side owns it: a Start-side guard is a cheap backstop that cannot explain itself, a reconcile-side check can name the template but leaves the port unguarded against a future caller.","acceptance_criteria":"A role whose template renders to whitespace fails before a worktree or pane is created, with an error naming the role and the prompt file. The ah-tqc Errored() arm stays as the backstop and its test still passes.","status":"open","priority":3,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-08-04T23:46:15Z","created_by":"Eugene Blikh","updated_at":"2026-08-04T23:46:15Z"} |
| new_value | {"status":"in_progress"} |
| comment | NULL |
| created_at | 2026-08-05T02:54:11Z |
| id | 019fcf42-1efe-7eff-bdff-cb699b7c2c5f |
| issue_id | ah-1cx.16 |
| event_type | label_added |
| actor | Eugene Blikh |
| old_value | NULL |
| new_value | NULL |
| comment | Added label: milestone:v0.2.0 |
| created_at | 2026-08-05T03:10:44Z |
| id | 019fcf4d-a970-7822-9012-d72fa3dc5ad0 |
| issue_id | ah-1cx.16 |
| event_type | closed |
| actor | Eugene Blikh |
| old_value | |
| new_value | Done in 8716a2a. Confirmed: prepareTaskDir writes s.Prompt verbatim and nothing upstream checked it, so a role template rendering to whitespace produced a pi run that did nothing and exited 0 — caught by ah-tqc's second Errored() arm, but only AFTER a worktree, a zellij pane and an attempt number were spent, and with no way to say which role or template was at fault. The guard went on the RECONCILE side, inside renderPrompt: the single seam both claim and launchResume render through, and the only layer holding the role name and the template path — the two facts an operator needs. The error flows into the existing SPEC §12 claim-failure path (card → failed, 'claim failed' comment, claim_failed event) with no new machinery. The port is deliberately NOT guarded twice: ah-tqc's arm is the port's own backstop and stays intact, so a blank prompt reaching Start by some future route still finalizes as an error rather than sailing to In Review; ports.StartSpec.Prompt now documents that split as the contract. Note the check is on the TEMPLATE's output, before the artifact manifest and Context Pack are appended — a role whose own template says nothing is a broken config even if the card happens to carry text. Cover: two prompt_test.go subtests plus TestClaimRejectsBlankRenderedPrompt. |
| comment | NULL |
| created_at | 2026-08-05T03:23:21Z |
No comments.
Close reason