Skip to content

[DRAFT] verify-pr fullsend CI deployment (TC-6180) - #299

Draft
mrizzi wants to merge 111 commits into
mainfrom
verify-pr-fullsend
Draft

mrizzi wants to merge 111 commits into
mainfrom
verify-pr-fullsend

Conversation

@mrizzi

@mrizzi mrizzi commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Draft — do not merge. Continuous-CI vehicle for the verify-pr-fullsend feature branch (TC-6180 CI-deployment feature).

Repo CI (validate-plugins, skillsaw, and the path-filtered evals) triggers only on pull_request/push to main. This draft PR gives the feature branch continuous CI against main as sub-tasks land — including the expensive, main-only evals that are intentionally not run on per-sub-task PRs into the feature branch.

Sub-tasks continue to PR into verify-pr-fullsend individually (e.g. #298). This PR is the aggregate gate; it stays draft until the feature is complete and TC-5816 (merge bookend) removes the temporary feature-branch CI entries.

🤖 Generated with Claude Code

mrizzi and others added 30 commits August 27, 2026 19:15
Confirm fullsend v0.37.0 toolchain, stock claude-runtime image binaries
(all present -> TC-5806 no-op), and root-level harness resolution of the
in-place sdlc-workflow plugin.

Note v0.37.0 constraints for TC-5807: --fullsend-dir must be the repo
root for relative children to resolve, and verify-pr is a valid harness
role but not a valid config.yaml role (enum: fullsend/triage/coder/
review/fix/retro/prioritize/e2e).

Implements TC-5805

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author harness/verify-pr.yaml at the repo root so its relative children resolve against the repo root and plugins/sdlc-workflow is delivered in place as a whole plugin. Declares every field explicitly with no base composition; omits security: (Go defaults supply it) and any forge block (GitHub is tier-1, Vertex-only provider). Split-trust env: Jira/GitHub tokens on the runner only, read-only context (JIRA_ISSUE_ID, JIRA_BASE_URL) in the sandbox. Also adds the sandbox agent prompt (agents/verify-pr.md) and the Vertex env file (env/gcp-vertex.env).

Referenced children (providers, policy, profile, schema, pre/post scripts) are authored by later epic tasks; this task validates the harness syntactically only.

Implements TC-5807

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vertex is the sole in-sandbox provider (tier 4). Jira and GitHub are tier-1 (prefetched host-side) and need no in-sandbox provider, so only the Vertex pair is declared locally for the standalone harness.

Copied verbatim from the stock agents Vertex pair: provider type fullsend-vertex-ai matches profile id fullsend-vertex-ai (endpoint *.googleapis.com:443), which the root harness/verify-pr.yaml references via providers:/openshell.profiles:.

Implements TC-5808

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add plugins/sdlc-workflow/policies/verify-pr.yaml for the verify-pr fullsend harness (referenced by harness/verify-pr.yaml). Filesystem is read-only (include_workdir: false); egress is reduced to Anthropic + Vertex AI (*.googleapis.com) inference and telemetry only.

No *.atlassian.net (Jira) and no api.github.com (GitHub) egress: those are tier-1 with runner-only tokens under the split-trust I/O model (prefetch/post-script run host-side). curl and gh are excluded from the binary allowlist to block raw HTTP with injected tokens.

Live fullsend egress verification is deferred to TC-5810/TC-5811, which author the harness's remaining siblings (pre/post scripts, result schema).

Implements TC-5809

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The verify-pr fullsend policy granted Vertex AI (*.googleapis.com) egress but omitted the pi runtime from the binary allowlist. Under fullsend's dual host-AND-binary match rule, the pi runtime — which brokers Vertex inference — would be blocked. Add **/pi alongside **/claude and **/node to match the composed fullsend-vertex-ai profile and every upstream vertex policy.

Implements TC-5880

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…on_loop

Add the split-trust I/O contract for the verify-pr fullsend harness: the
pre_script prefetches everything on the trusted runner (where the Jira and
GitHub tokens live) and the sandbox reads only JSON — no api.github.com or
atlassian egress.

- schemas/verify-pr-input.schema.json — tracker-agnostic pre-script input,
  extended with a `github` tier-1 read bundle (diff, stat, reviews,
  review_comments, issue_comments, commits, headRefName, commit_sha).
- schemas/verify-pr-result.schema.json — agent result contract (ported).
- scripts/pre-verify-pr.sh — validates env, fetches the Jira issue, resolves
  the linked PR, prefetches every GitHub read the skill performs, and writes
  verify-pr-input.json for host_files to mount.
- scripts/pre_verify_pr.py — PR-URL extraction + input transform, extended
  with build_github_bundle and a --github-dir transform mode.
- scripts/{validate-output-schema.sh,strip_extra_properties.py} — kept per the
  Step 5 native-validator decision (below); wired via validation_loop.script.
- scripts/test_pre_verify_pr.py — unit tests incl. the github bundle.
- harness/verify-pr.yaml — set validation_loop.script.

Deviations from the literal task steps, dictated by the verified fullsend
v0.37.0 contract (same class as TC-5805):

- Skip signal (step 4) uses the pre-script output protocol v1 — line-based
  `skipped=true` / `reason=...` appended to $FULLSEND_PRESCRIPT_OUTPUT (guarded,
  exit 0) — not a JSON `{"skipped":true}` document. That env var is the
  skip-signal file, not the input path.
- GitHub bundle is embedded in verify-pr-input.json (single file) rather than
  separate in-sandbox file paths, keeping the harness change scoped to
  validation_loop; input JSON stays at /tmp/fullsend-pre-output (host_files src),
  with PRE_DIR overridable for host-side testing.
- No host-side PR-head checkout (step 3): the prefetched PR diff is sufficient
  for verification, so the sandbox needs no writable checkout.
- Native validator decision (step 5): additionalProperties:false rejects benign
  agent extras, so validate-output-schema.sh + strip_extra_properties.py are
  KEPT and validation_loop.script is set (fullsend also requires a script for
  validation_loop — schema alone is insufficient).

Validated host-side: both schemas are valid JSON; 15 unit tests pass; the
validator strips benign extras and passes, and fails a bad enum (exit 1); the
skip path exits 0 with the correct signal; the happy path (stub gh + jira)
produces a verify-pr-input.json that validates against the input schema with
the full github bundle embedded.

Implements TC-5810

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
gh pr diff has no --stat flag, so the prefetch aborted under
set -euo pipefail before writing verify-pr-input.json. Derive the
per-file stat from the already-fetched patch with git apply --stat,
guarding the empty-diff case so the script still exits 0. Add
regression tests exercising the real stat command and guarding
against reintroducing the unsupported gh flag.

Implements TC-5884

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
The GitHub tier-1 prefetch fetched PR reviews, review comments, and issue
comments without --paginate, so GitHub REST returned only the first ~30
items and the bundle silently truncated on active PRs — degrading the very
review-comment analysis it feeds the verify-pr sub-agents. Add --paginate
--slurp and merge pages with `jq 'add'` into the flat arrays that
build_github_bundle expects. (--slurp is incompatible with gh's built-in
--jq, so a standalone jq performs the merge.)

Also make the sibling git-apply-stat test deterministic by running it from
a non-repo cwd: git apply --stat is CWD-sensitive and reports "0 files
changed" when launched from inside a repo subdirectory.

Implements TC-5885

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
transform_to_input set task.description via fields.get("description", {}),
whose default applies only to an ABSENT key. When Jira returns an explicit
null (an issue with no description), .get returns None, so task.description
became null — violating verify-pr-input.schema.json (which requires an
object) and producing a schema-invalid verify-pr-input.json. Use
`fields.get("description") or {}` to also coerce null to {}, matching the
existing `(fields.get("status") or {})` idiom in the same transform.

Add regression tests: a direct coercion assertion and a full-input
(task + github bundle) validation against verify-pr-input.schema.json that
fails on the pre-fix null description.

Implements TC-5886

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The github.com PR-URL match at pre-verify-pr.sh:100 was start- but not
end-anchored, so a malformed value like `.../pull/42abc` or
`.../pull/42/invalid` matched and BASH_REMATCH truncated the pull number
to 42 — making the pre-script prefetch and embed a different PR than the
Jira custom field identifies. Append `/?$` so the full URL must match: a
trailing non-numeric character or extra path segment is now rejected with
the existing "not a github.com pull request URL" error, while a canonical
URL with or without a trailing slash still parses to owner/repo and number.

Add regression tests that run the script's actual regex (extracted from
source) through bash's [[ =~ ]], asserting well-formed URLs parse to the
correct owner/repo and number and malformed ones are rejected rather than
truncated.

Implements TC-5891

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gh's `pr view` commits connection is bounded, so `.commits[-1].oid` returns
the last commit of a truncated page rather than the PR head on large PRs — a
silently-wrong commit_sha that still satisfies the result schema's hex pattern.
Read the head ref tip OID directly (`--json headRefOid`), which is correct
regardless of commit count. Also document that the bundled commits list uses the
same bounded connection (best-effort context; head SHA is authoritative).

Adds a behavioral regression test that runs the shipped COMMIT_SHA command
against a gh stub whose headRefOid and commits[-1].oid disagree, plus a source
guard, both of which fail on the old logic and pass on the new.

Implements TC-5924

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rivations

Two field descriptions in the `github` read bundle of
verify-pr-input.schema.json cited commands that no longer produce their
values (doc-string drift only; the field values are correct):

- `commit_sha` cited `.commits[-1].oid`, but pre-verify-pr.sh derives it
  from `gh pr view --json headRefOid` (TC-5924 — the pr-view commits
  connection is bounded, so `.commits[-1]` can be a truncated-page tip).
- `stat` cited `gh pr diff --stat`, an unsupported flag; it is derived
  from `git apply --stat` on the already-fetched pr.diff (TC-5884).

Documentation-only; the schema remains valid JSON.

Implements TC-5929
Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the verify-pr write executor to run host-side after the sandbox is
destroyed. Jira comments and the verification report are posted through the
native `fullsend issues post-comment` sticky-comment CLI (idempotent via a
stable marker), while the irreducible custom Jira writes with no native
primitive yet — sub-tasks, links, and root-cause tasks — call jira-client.py
directly. GitHub writes stay host-side on `gh`.

- post-verify-pr.sh: locate the latest agent-result.json, validate, delegate
- execute-actions.py: resolve {{ref.key}}/{{ref.url}} placeholders, route
  post_comment/post_report Jira side through the native CLI, keep gh for
  GitHub, keep jira-client.py for sub-tasks/links/root-cause
- jira-client.py: create_issue accepts a pre-rendered ADF description and an
  optional parent key (reconciled additively; existing callers unaffected)
- test_execute_actions.py: ref-resolution tests plus native-CLI argv/env,
  stdin body, and post_comment/post_report routing tests

Implements TC-5811

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ira CLI

execute_post_comment read action["body_md"], but the result schema requires
body_adf (an ADF object) and strip_extra_properties.py removes any extra key,
so every schema-valid post_comment raised KeyError before posting. Add
adf_to_markdown to render the ADF body back to the markdown the native fullsend
CLI consumes on stdin, and update the masking test to feed a schema-valid
body_adf action.

TC-5930

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
post-verify-pr.sh only located agent-result.json, but its sibling
validate-output-schema.sh accepts result.json as a fallback. A result
that passed validation via the fallback name was then rejected by the
write path. Prefer agent-result.json per iteration output dir, and fall
back to result.json when absent — matching the validator's precedence.

Implements TC-5931

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
post-verify-pr.sh selected the "most recent" iteration by keeping the last
match in shell glob order, which is lexicographic. Once there are >= 10
iterations, iteration-9 sorts after iteration-20 and a stale iteration is
chosen, so the write path would execute the wrong (older) result.

Iterate the iteration-*/output directories in ascending numeric order via
`sort -V` and keep the highest-numbered one that has a result file. Preserves
the `set -euo pipefail` behavior and the result.json fallback (TC-5931); a
`[[ -d ]]` guard skips the literal glob pattern when no iteration dir exists.

TC-5932

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The ADF block renderer had no taskList/taskItem case, so a schema-valid post_comment task list fell through to the unknown-block fallback and was flattened to plain paragraph text, losing the checklist markers. Add _render_adf_task_list rendering DONE/TODO items as markdown checkboxes, handling both paragraph-wrapped and inline taskItem content. The comment route calls adf_to_markdown directly (not sanitize_adf), so the renderer is the correct fix site.

Implements TC-5936

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… retry

execute_post_report posted the verification report to GitHub via a plain gh pr comment (no sticky marker) before the idempotent Jira post, so a retry after a failed Jira post double-posted the GitHub report comment. Embed a commit-scoped marker in the GitHub report body and, before posting, list the PR comments and PATCH-update an existing same-commit report comment instead of creating a duplicate. Dedup is scoped to the commit SHA so a later commit still gets a fresh comment, preserving the per-run verification history. The Jira side is unchanged and receives the clean body. Updated the module docstring so its idempotency claim matches actual behaviour.

Implements TC-5937

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_find_report_comment_id listed a PR's issue comments with `gh api ... --paginate`
and parsed the result with a single json.loads. Without `--slurp`, `gh --paginate`
concatenates one JSON array per page (`[...][...]`), which is not valid combined
JSON once the PR has more than one page of comments (>30). json.loads then raised
JSONDecodeError, which the code swallowed by returning None; execute_post_report
read that as "no existing report comment" and created a duplicate GitHub report
comment instead of PATCH-updating the existing one, silently defeating the retry
idempotency TC-5937 introduced.

Add `--slurp` so gh emits a single array-of-pages and flatten the pages into one
comment list, so a same-commit report comment is found even when it lands on a
later page. Stop swallowing JSONDecodeError: with `--slurp` a parse error is a
real failure and now surfaces via sys.exit(1) rather than being misread as "no
existing comment". Added a multi-page test (report comment on a non-first page →
PATCH-update, not duplicate) and a parse-failure test (exits instead of returning
None).

Implements TC-5948

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…teral text

_render_adf_inline emitted a text node's literal value verbatim into the
markdown handed to the native `fullsend issues post-comment` CLI. Because that
CLI re-parses the markdown, literal markdown-active sequences inside ADF text
(`*`, `_`, `[`, `]`, backticks, and backslash) were reinterpreted as formatting
— e.g. a literal `*note*` rendered as emphasized text and `[x](y)` as a link.

Add _escape_markdown, which backslash-escapes those characters (backslash first
so its escapes are not re-escaped), and apply it to a text node's plain value
before the mark wrapping the renderer intentionally adds, so the mark syntax is
not double-escaped. Inline code is exempt: its content is literal to the CLI and
escaping would corrupt inline-code semantics. Link hrefs are also left untouched.
Added tests: literal `* _ [ ]` and backtick are escaped; strong/link marks,
inline-code content, and a link href with an underscore are unaffected.

Implements TC-5949

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_render_adf_inline previously handled only text and hardBreak nodes; every other inline node fell into the else branch and recursed into a nonexistent content array, so mention/emoji/inlineCard/date/status inline nodes (whose value lives in attrs, not content) rendered as empty strings and were silently dropped -- contradicting the module's own _INLINE_NODE_TYPES set.

Render each from its attrs: mention/emoji/status from attrs.text (escaped as literal text; emoji falls back to attrs.shortName), inlineCard from attrs.url (bare, unescaped), and date from attrs.timestamp via a new _render_adf_date helper (epoch millis to UTC YYYY-MM-DD). The final else is kept for genuinely unknown container-like inline nodes. Works in both paragraph and taskItem inline contexts.

Implements TC-5950

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…result schema

The post_comment action schema accepted any non-empty string for "issue", but the executor post_jira_comment_native requires a hyphenated Jira key and sys.exit(1)s otherwise — a schema-valid {"issue":"12345"} (numeric ID, URL, or other non-key identifier) passed validation and then failed at execution, aborting the whole post_script run.

Constrain the schema "issue" field to the Jira-key pattern ^[A-Z]+-[0-9]+$ (matching jira_issue_id/parent in the same schema) so non-key identifiers are rejected at the producer boundary. The executor rpartition fail-fast guard is kept as defence in depth. Add schema-validation tests asserting the pattern accepts a valid key and rejects non-key issues (numeric ID, URL, lowercase, missing hyphen/number/project).

Implements TC-5953

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 9 'Post to GitHub PR' claimed every verification run creates a new PR
comment and never overwrites previous reports, and showed a plain
`gh pr comment` create. That contradicted the shipped executor behavior
(TC-5937): execute_post_report embeds a commit-scoped marker and, via
_find_report_comment_id, PATCH-updates an existing same-commit report comment
instead of duplicating it — a new comment is created only for a later commit.

Rewrite Step 9 to match the code: describe the commit-scoped marker, the
find-then-PATCH-or-create path (gh api --paginate --slurp + PATCH, else
gh pr comment), and reframe the history claim as per-commit (a later commit
gets a new comment) rather than per-run. Keep the (commit <short-sha>) header
convention. Add an eval assertion (evals/verify-pr/evals.json case 3) covering
the commit-scoped report header / per-commit history.

Documentation-only alignment; executor behavior is unchanged.

Implements TC-5954

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…schema

TC-5953 tightened the post_comment 'issue' field to the Jira-key pattern
'^[A-Z]+-[0-9]+$', but execute_post_comment resolves the field through
resolve_refs, which supports the {{<ref>.key}} placeholder so a comment can
target an issue created by an earlier action. Since fullsend's validation_loop
validates the result schema before post_script runs, a placeholder issue was
rejected before the reference could be resolved.

Widen the pattern to '^([A-Z]+-[0-9]+|\\{\\{[a-z0-9-]+\\.key\\}\\})$' so it
accepts either a literal Jira key or a {{<ref>.key}} placeholder, scoped to
.key (not .url) since the field must resolve to a key. Bare numeric IDs, URLs,
and other non-key/non-ref identifiers stay rejected (TC-5953's intent).

Add a schema-validation test that a {{<ref>.key}} post_comment action passes
jsonschema.validate, and extend the negative test with .url placeholders,
uppercase refs, and unanchored/brace-less forms.

Implements TC-5959

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
datetime.fromtimestamp can raise OverflowError/OSError (or ValueError) for an
out-of-range epoch-ms value. The call sat outside the guarded try, so a single
malformed ADF date node in a post_comment/post_report body aborted the entire
execute-actions.py post_script and posted nothing. Move the fromtimestamp call
inside the try and catch OverflowError/OSError alongside the int() parse errors,
falling back to the literal timestamp string so the node degrades gracefully.

Implements TC-5965

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
create_issue declared issue_type: str = "" (reconciled under TC-5811 for the
new description_adf/parent params), so an omitted value serialized to
{"name": ""} and surfaced as an opaque Jira 400 instead of failing fast at
the call site. Restore the prior fail-fast contract: reject an empty (or
whitespace-only) issue_type with a clear stderr message and sys.exit(1)
before any HTTP request, matching the module's existing error style.

Added test_create_issue_fails_fast_on_empty_issue_type (asserts SystemExit
and no request issued); jira-client suite 19 -> 20 passing.

Implements TC-5966

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
post_comment and post_report both posted via post_jira_comment_native with
the single shared STICKY_COMMENT_MARKER, which the native fullsend CLI treats
as the sticky-comment identity. Two comments targeting the same Jira issue in
one run would share that marker, so the second post would overwrite the first
and silently lose a comment.

Thread a marker parameter through post_jira_comment_native (default preserves
the report path's historical marker) and give post_comment a distinct
POST_COMMENT_STICKY_MARKER. Each path keeps its own stable marker, so per-path
re-run idempotency is preserved while cross-purpose clobbering is prevented.
GitHub report dedup (GITHUB_REPORT_MARKER_PREFIX) is untouched.

Added test_post_comment_and_report_use_distinct_sticky_markers; execute_actions
suite 26 -> 27 passing.

Implements TC-5967

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
execute_post_report embedded the raw commit_sha in the GitHub dedup marker and
_find_report_comment_id matched it by substring. The result schema permits
commit_sha to be 7-40 hex chars, so a run recording a full SHA and a retry
recording an abbreviation (or vice-versa) for the same commit produced
different marker strings; the substring lookup missed and the retry posted a
duplicate report comment, defeating the TC-5937 idempotency guarantee.

Normalize the SHA to the schema-minimum 7 chars in the marker (and thus the
lookup, which reuses the same marker). Git abbreviations are always prefixes of
the full SHA, so first-7 is the only fixed length that unifies a full SHA with
any valid abbreviation of the same commit; a longer length (e.g. 12) would
leave a 7-char abbreviation unchanged and still mismatch a full SHA.

Added test_execute_post_report_dedup_marker_invariant_to_sha_length (full-SHA
create then abbreviated-SHA retry -> single PATCH-updated comment);
execute_actions suite 27 -> 28 passing.

Implements TC-5968

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
execute_post_report calls resolve_refs on report_md, which raises an
uncaught KeyError for any {{ref.key}} placeholder whose entity has not yet
been registered. The registry is populated by create_subtask /
create_root_cause_task as main()'s sequential loop runs, so a post_report
ordered before an action it references would abort the whole run with a bare
KeyError. verify-pr emits post_report last today, but that ordering was
undocumented and unenforced.

Defer every post_report until after the actions loop in main() so the
registry is fully populated before any report_md is resolved. post_report is
still a recognized action; only its execution moves to the end. Observed
behavior is unchanged for the always-last emission order.

Added test_post_report_resolves_ref_created_by_later_action, which drives
main() with a temp result JSON that orders post_report before the
create_subtask its report_md interpolates and asserts the resolved key
reaches the GitHub report body; execute_actions suite 28 -> 29 passing.

Implements TC-5969

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_render_adf_block handled paragraph/heading/rule/codeBlock/bullet+ordered
list/taskList; every other block hit the unknown-block fallback that recurses
into content and flattens structure. So table cells were concatenated with no
grid, blockquote/panel lost their framing, and media/mediaSingle/mediaGroup
(which carry no text content) were dropped entirely. A schema-valid
post_comment.body_adf can carry any of these, so a comment silently lost
content — the same silent-degradation class already closed for taskList
(TC-5936) and inline leaf nodes (TC-5950).

Add explicit renderers: blockquote/panel -> "> "-prefixed lines (panelType
surfaced as a bold label), table -> a GitHub-flavored markdown table (first
tableRow is the header, pipes in cells escaped), and media/mediaSingle/
mediaGroup -> an image link or a non-empty [alt] placeholder, never dropped.
The recursing fallback is kept for genuinely unknown container nodes, and the
adf_to_markdown docstring's node-set contract now lists the covered nodes.

Added rendering tests for table, blockquote+panel, and media, plus one
asserting an unknown block still degrades via the fallback; execute_actions
suite 29 -> 33 passing.

Implements TC-5970

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
mrizzi and others added 11 commits September 15, 2026 11:29
…us alias)

Under fullsend CLI v0.43.0 the `opus` alias resolves to claude-opus-4-6, which
the Red Hat Vertex deployment has not enabled (run 34951983374 failed:
"model claude-opus-4-6 is not available on your vertex deployment"). Pin to the
fleet's target generation, opus 4.8.

Refs TC-6186.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Carries the harness model pin (claude-opus-4-8) into the URL-pinned base the
CI fetch resolves.

Refs TC-6186.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sandbox no longer receives JIRA_ISSUE_ID (dropped from env.sandbox in
harness/verify-pr.yaml by TC-6190), but the agent prompt still read
$JIRA_ISSUE_ID and passed a hardcoded TC-4715 arg, so the sandbox started
with an empty issue ID. Derive the Jira key from the pre-fetched
verify-pr-input.json `task_id` (a required schema field the pre_script
resolves from the triggering PR URL) and fail fast if it is missing.

Update fullsend.md accordingly: the sandbox gets the key via task_id, the
local-run entry point is FULLSEND_WORK_ITEM_URL (not JIRA_ISSUE_ID), and the
file table describes the agent reading task_id.

Implements TC-6232

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
…s not missed

The pre_script's JQL search used a single page (<=50 results), so an exact
PR-linked issue falling beyond the first 50 broad `~` recall results was
dropped and resolve-gated-issue emitted a false ADR-0072 skip even though a
unique gated issue existed.

Add search_jql_all() to jira-client.py: it follows the enhanced endpoint's
nextPageToken cursor across every page, aggregates the issues into one
response, and fails loud (exit 1) if a runaway server never stops offering a
cursor rather than truncating silently. Expose it via a `search_jql --all`
flag and switch pre-verify-pr.sh's gating search to `--all`. The Python
exact-match verification in resolve_gated_issue remains the source of truth.

Tests: search_jql_all collects issues beyond page 1, stops without a token,
and guards against runaway pagination; resolve_gated_issue resolves an exact
match at index 55 of a 60-issue aggregated result; and a shell-source guard
asserts the gating search uses --all.

Implements TC-6233

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
…iting sandbox input

The status and label gates were checked on the lightweight JQL search
response, but the full issue used to write verify-pr-input.json is fetched
in a second request. If the issue left Review, lost the ai-generated-jira
label, or had its Git Pull Request field changed between the two, the stale
successful gate still launched the verification (TOCTOU).

Re-run the exact PR-URL + status + label gate on the full issue immediately
before the write via a new revalidate-gate command (reuses resolve_gated_issue
so the acceptance rule stays single-sourced). A failure maps to the same
ADR-0072 skip as the initial gate.

Implements TC-6234

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
Re-pin the base to the commit carrying the TC-6232/6233/6234 script fixes and
re-lock so every transitive child resolves at the new SHA. Without this the
fullsend run path (local + CI) serves the pre-edit pinned content and never
exercises the fixes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
…tage

Add privilege_levels to harness/verify-pr.yaml mapping run-stages to the
review role's named mint levels (fullsend ADR-0073): pre_script and runtime
get read, post_script keeps write. pre_script only reads GitHub, the sandbox
has no api.github.com egress (split-trust I/O), and only post_script writes PR
review comments/replies/report. Applies least-privilege now and survives
fullsend #6516 flipping the mint default from write to read.

Implements TC-6243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
harness/verify-pr.yaml gained privilege_levels; re-pin the .fullsend base to
a9099de (sha256 3c9dc221) and re-lock so local and CI resolve the new bytes.

Implements TC-6243

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
The push/pull_request branch filters still named the old feature branch
TC-5787-verify-pr-fullsend-v037, which was renamed to verify-pr-fullsend
(TC-6180). The stale name matched no live branch, so the pytest suite never
ran as a check on PRs targeting the feature branch (e.g. #298). Replace it
with verify-pr-fullsend in both filters and comments; keep the TC-5816 note
to drop the non-main entry after the feature branch merges to main.

Implements TC-6244

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
Add verify-pr-fullsend to the push/pull_request branch filters of both
workflows so plugin validation and skill lint run on sub-task PRs into the
feature branch, matching python-tests.yml (TC-6244). Entries carry a TC-5816
cleanup note to be removed when the feature branch merges to main.

Implements TC-6245

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
feat(verify-pr): derive + gate Jira key from PR URL via JQL in pre_script (TC-6190)
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 15, 2026

Copy link
Copy Markdown

🤖 Finished Verify Pr · ⏭️ Skipped (no Jira issue links PR #299 in its Git Pull Request field) · Started 2:29 PM UTC · Completed 2:29 PM UTC

Commit: 6229a91 · View workflow run →

Effort: high

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Eval Results

Eval Results: verify-pr

Eval Passed Failed Pass Rate
eval-1 13/13 0 100%
eval-2 11/11 0 100%
eval-3 17/17 0 100%
eval-4 10/10 0 100%
eval-5 10/10 0 100%
eval-6 9/10 1 90%

Failed Assertions

eval-6: 1 failing assertion
  • Assertion: "The human reviewer comment from reviewer-b (comment id 50001) is NOT misidentified as an eval result — it is processed as a normal review comment (classified in review-50001.md) and its classification does not reference eval detection or eval metrics"
    Evidence: "review-50001.md correctly classifies the comment as 'Code Change Request' (not an eval result), and the file exists as review-50001.md confirming normal review processing. However, the classification DOES reference eval detection: line 6 says 'Type: Inline review comment (NOT an eval result)' and reasoning point 5 (lines 28-30) explicitly discusses eval detection markers: 'Not an eval result: This comment is from a human reviewer (reviewer-b), not from github-actions[bot]. It does not contain "## Eval Results" or "sdlc-workflow/run-evals" markers.' The assertion requires the classification does not reference eval detection, but it clearly does."

Pass rate: 98% · Tokens: 89,093 · Duration: 802s


Generated by sdlc-workflow/run-evals v0.13.9

mrizzi and others added 3 commits September 16, 2026 09:34
Add fullsend.md §Acceptance run → "E2E acceptance via CI dispatch
(TC-6192)" documenting the CI-gated verify-pr dispatch proven end to end
on PR #300 / TC-6254: qualifying PR triggers the workflow, wait-for-checks
releases after CI reaches terminal, verify-pr posts to both the PR and
Jira. Records all four acceptance criteria — CI-pass run (python-tests
success → Overall WARN), CI-fail run (python-tests failure still releases
the wait → CI Status FAIL, sub-task auto-created), report on both surfaces,
and idempotency (re-run same head, no duplicate reports). Notes DENIED
egress = split-trust policy working, and the interim personal-token Jira
authorship pending the SA (TC-6191).

Implements TC-6192

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The acceptance results table lists five criteria; the lead-in sentence
said "four". Corrects the count to match (Sourcery nit surfaced by the
verify-pr WARN on PR #301).

Implements TC-6192

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 16, 2026

Copy link
Copy Markdown

🤖 Finished Verify Pr · ⏭️ Skipped (no Jira issue links PR #299 in its Git Pull Request field) · Started 7:36 AM UTC · Completed 7:36 AM UTC

Commit: 60dffa2 · View workflow run →

Effort: high

mrizzi and others added 5 commits September 16, 2026 09:48
In the CI-dispatched (sandbox) verify-pr path the CI Status check had no
data — no gh CLI or egress in the sandbox — so it degraded to WARN
"CI status could not be retrieved" (PR #301, TC-6192). Extend the
split-trust prefetch so the pre_script fetches the PR head-SHA CI
check-run outcomes on the trusted runner and embeds them in the sandbox
bundle; correctness.md Check 1 reads them in sandbox mode and keeps the
`gh` on-demand path for interactive mode.

- pre-verify-pr.sh: prefetch repos/<repo>/commits/<sha>/check-runs into
  check-runs.json, reduced to name/status/conclusion/details_url
- pre_verify_pr.py: build_github_bundle embeds github.check_runs
  (defaults to []); _github_from_dir reads check-runs.json
- verify-pr-input.schema.json: add required github.check_runs (github is
  additionalProperties:false, so the new key must be declared)
- correctness.md: Check 1 sandbox-mode branch reads pre-fetched
  check-runs; 1b notes failure logs are unavailable in the sandbox
- dispatch-template.md, SKILL.md Step 0.7: document github.check_runs
- test_pre_verify_pr.py: cover check_runs in the bundle and transform

E2E re-verification on a live dispatched PR (PASS on green, FAIL on red)
is deferred to a CI run.

Implements TC-6257

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Assisted-by: Claude Code
The CI-status prefetch gives the sandbox check-run outcomes, but on a FAIL
correctness.md Check 1b still had no failure logs to analyse: the sandbox has
no gh CLI or egress to run `gh run view --log-failed`, so it inferred the
what/why/fix from the diff alone.

Extend the split-trust prefetch to capture the logs too. The pre_script (on the
trusted runner, where GH_TOKEN lives) extracts the distinct GitHub Actions run
IDs from each FAILED check-run's details_url and concatenates their
`gh run view --log-failed` output into a single check-run-logs.txt, mounted
read-only beside verify-pr-input.json. host_files mounts single files only and
the failed set is dynamic, so one combined file is used rather than per-check
paths. The file is always created (empty when nothing failed) so its mount is
never missing; the bundle carries only its sandbox path (github.check_run_logs_path,
"" when empty), never the log text — so the large content stays out of the schema
and off the sub-agent's context until Check 1b Reads it on a FAIL. The
wait-for-checks job (fullsend-verify-pr.yml) guarantees terminal conclusions
before dispatch, so a failed check's log is complete and fetchable at that point.
Non-Actions checks (no actions/runs URL) are skipped; 1b falls back to the diff +
details_url for those. Interactive mode is unchanged (fetches via gh on demand).

- pre-verify-pr.sh: extract failed Actions run IDs (deduped) and pack their
  --log-failed output into check-run-logs.txt; always create the file
- pre_verify_pr.py: build_github_bundle gains check_run_logs_path (default "");
  _github_from_dir emits the mounted sandbox path when the log file is non-empty
- verify-pr-input.schema.json: add required github.check_run_logs_path (string)
- .fullsend/harness/verify-pr.yaml: mount check-run-logs.txt (optional)
- correctness.md / dispatch-template.md / SKILL.md: Check 1b reads the prefetched
  log file via the CI Failure Logs input on a FAIL; document the new key
- test_pre_verify_pr.py: cover the new bundle field and the path derivation
  (present/empty/absent)

TC-6257

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The split-trust prefetch children (pre-verify-pr.sh, correctness.md,
SKILL.md, dispatch-template.md, verify-pr-input.schema.json) added in
14b8be6/fa3f4b7d only take effect in a CI-dispatched run when the
URL-pinned harness base advances to a commit that contains them. The
base file bytes are unchanged (sha256 3c9dc221…), so only the pinned
commit moves a9099defa3f4b7; `fullsend lock` refreezes all 11
children (notably pre-verify-pr.sh) at fa3f4b7.

Without this the sandbox keeps running the pre-TC-6257 pinned skill,
whose bundle carries no github.check_runs — CI Status falls back to the
WARN "unavailable in sandbox" path instead of sourcing prefetched data.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI Status is now sourced from prefetched github.check_runs, and on a CI
failure Correctness Check 1b reads the prefetched check-run-logs.txt.
Proven live via CI dispatch on vehicle PR #303 / Jira TC-6266:
- post-re-pin run 35075966985 sourced CI Status from prefetched check_runs
  (data the pre-TC-6257 sandbox could not obtain);
- vehicle-only red commit 8865e11 (canary test) → verify-pr 35077404421
  reported CI Status FAIL and Check 1b surfaced the runtime-computed,
  log-only canary cc08a78a24bc66f6 (never a literal in the diff),
  auto-creating CI-failure sub-task TC-6271.
Also documents that the harness base re-pin is load-bearing (the pinned
children must advance to a commit containing the prefetch code) and
supersedes the TC-6192 "CI status unavailable in sandbox" caveat.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat(verify-pr): prefetch CI check-run outcomes into sandbox input
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 16, 2026

Copy link
Copy Markdown

🤖 Finished Verify Pr · ⏭️ Skipped (no Jira issue links PR #299 in its Git Pull Request field) · Started 10:27 AM UTC · Completed 10:27 AM UTC

Commit: 59ed61d · View workflow run →

Effort: high

…cedures

Add a CI deployment section (per-repo fullsend github setup / inference
provision / org GitHub App install), a dispatch-design section (inline
pull_request wait via lewagon/wait-on-check-action, CEL trigger, pre_script
Jira gating), a day-2 variables/secrets management table, and maintenance
procedures for bumping the pinned fullsend version and keeping the two
plugin-version files in sync. Refresh stale v0.37.0 references.

Implements TC-6193

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Sep 17, 2026

Copy link
Copy Markdown

🤖 Finished Verify Pr · ⏭️ Skipped (no Jira issue links PR #299 in its Git Pull Request field) · Started 10:52 AM UTC · Completed 10:52 AM UTC

Commit: 2b0785a · View workflow run →

Effort: high

mrizzi and others added 5 commits September 17, 2026 15:45
…o-test

Switch fullsend-verify-pr.yml from pull_request to pull_request_target so
fork-origin PRs get the upstream vars/secrets/OIDC the mint step needs (fixes
"FULLSEND_MINT_URL is not set"; fullsend ADR-0009). The workflow still never
checks out or executes fork code — it builds the dispatch matrix from
github.event alone and hands off to reusable-dispatch.yml.

Add an ok-to-test maintainer-label gate as defense-in-depth: fork PRs dispatch
only after a maintainer applies the label; the remove-ok-to-test job strips it
on every new push (synchronize) to force re-review. Same-repo PRs are unchanged.

Update fullsend.md with the pull_request_target + ok-to-test model and remove
the obsolete "upstream head branch required" framing.

Implements TC-6331

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The head-SHA check-run prefetch enumerated every check-run on the commit,
including verify-pr's own workflow (`fullsend verify-pr`) runs — the
in-progress dispatch and any superseded prior attempt. Those are
non-terminal/failed at evaluation time, so correctness.md Check 1a mapped
them to pending/failed and forced a permanent self-referential CI Status
WARN/FAIL: verify-pr could never report CI Status = PASS (TC-6332).

Add the CI-Status analogue of Step 1's `running-workflow-name` self-
exclusion. pre-verify-pr.sh enumerates this workflow's own check-run names
for the head SHA — across all of its runs at that SHA, so superseded
attempts are covered — and passes them to a pure, unit-testable Python
helper `filter_own_check_runs`, which drops them before they reach
`github.check_runs`. Exclusion is by workflow NAME, not run ID. Name
gathering is best-effort: a token lacking actions:read degrades to no
self-exclusion (prior behavior) with a warning rather than aborting.

Fixes TC-6332.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reword correctness.md Check 1a sandbox-mode note: the own-check-run
self-exclusion is best-effort (needs actions:read) and degrades to a
no-op with a WARNING when enumeration fails, so the CI Status input can
still contain verify-pr's own runs. Instruct the evaluator to map each
entry on its merits and note own-run-only non-passing entries in the
evidence rather than treating the PR as blocked.

Implements TC-6349

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
TC-6343

Option A self-pin: move the base URL commit and re-lock so the verify-pr
CI run fetches the fixed pre-verify-pr.sh (CI-Status own-workflow
self-exclusion) and the reworded correctness.md Check 1a from this PR's
own fix commits, rather than the stale fa3f4b7 pin.

- .fullsend/harness/verify-pr.yaml: base URL commit fa3f4b7 -> da053ba
  (root harness/verify-pr.yaml is byte-identical, sha256 unchanged)
- .fullsend/lock.yaml: fullsend lock regenerates all 11 child pins at
  da053ba; pre-verify-pr.sh, pre_verify_pr.py, and correctness.md
  hashes now reflect the TC-6343 / TC-6349 fixes

Merge via merge commit (squash disabled) so da053ba stays reachable and
the pin keeps resolving after merge.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(verify-pr): self-exclude own workflow check-runs from CI Status
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