feat(jira): mature iteration feedback in place (#697) - #727
Conversation
aa8c877 to
519eb4d
Compare
389d556 to
4f3937c
Compare
ayushtr-aws
left a comment
There was a problem hiding this comment.
Submitting as a COMMENT review: GitHub blocks self-approval, but the substantive verdict is Approve with two non-blocking nits.
Verdict: Approve (with two non-blocking nits)
A well-structured, well-tested increment that matures a single Jira iteration comment from acknowledgement → heartbeat → terminal outcome, closing the last gap that made Jira feel second-class vs. Linear. Reviewed against the stack base feat/578-jira-orchestration-extension (PR 3 of 3, #725→#726→#727); I scoped out anything originating in the parent PRs.
Vision alignment
Fits the north star cleanly. It keeps the fire-and-forget path intact (all writes are advisory/best-effort, never gate the task), improves reviewable outcomes (a live heartbeat replaces the observed 22-min black box, plus a deterministic terminal comment carrying cost/turns/duration even on agent crash), and respects bounded blast radius: MAX_EDITS_PER_SWEEP cap retained, IAM scoped to the bgagent-jira-oauth-* prefix. No tenet is traded away; no ADR required.
Blocking issues
None.
Non-blocking suggestions / nits
-
Iteration-without-reply-id can duplicate the terminal comment on redelivery —
cdk/src/handlers/fanout-task-events.ts:1629. The marker read is now gated!isIteration, so an iteration is never short-circuited byjira_final_comment_event_id. TheiterationReplyIdpath is idempotent viaclaimTerminalReply, but an iteration whose webhook ack failed (no reply id captured) falls through to the ordinary post path at :1754, which writes the marker yet never reads it for iterations. On a partial-batch redelivery that iteration posts a second final comment. Narrow (needs a prior ack failure AND a sibling-channel-driven record redelivery) and advisory-only, so non-blocking — but consider having the no-reply-id iteration fallthrough either claim the terminal reply or honor the marker. -
let reply;is implicitly untyped —cdk/src/handlers/orchestration-reconciler.ts:1215. The Jira/Linear branches assign it fromupsertComment/postComment/upsertThreadedReply, and thereply === null/reply !== nullcontrol flow leans on that. An explicitlet reply: CommentRef | null | undefinedwould make the tri-state (undefined = surface can't mature, null = attempted-and-failed) self-documenting and guard against a future refactor collapsing the distinction the block below depends on.
Documentation
Updated and in sync. docs/guides/JIRA_SETUP_GUIDE.md was revised (flow diagram, terminal-owner split, idempotency/claim semantics, five signed Forge operations, the new platform doctor step) and the Starlight mirror docs/src/content/docs/using/Jira-setup-guide.md carries the identical edits — verified the changed sentences match, so CI's mutation check will pass. No ADR needed (ADR-015 already covers the Jira integration; this is an implementation refinement). The stale "Jira has no comment-edit API" claims were correctly retired everywhere.
Tests & CI
Strong, failure-path-oriented coverage — not just happy paths:
- Fan-out: standalone-iteration split (pointer edit + separate result), orchestrated-iteration deferral to the reconciler, lost-claim (ConditionalCheckFailed) no-double-edit, failed/transient update with claim release + batch-retry escalation.
- Reconciler: Jira orchestration iteration maturation, failed-update claim release, redelivery-matures-once.
- Heartbeat sweep: Jira adapter edit path + strongly-consistent terminal-claim guard preventing a late heartbeat from regressing a settled outcome.
jira-feedback: PUT/OAuth + Forgeupdate_comment+ 401→refresh→retry + missing-id rejection; comment-id extraction from the create response body.- Forge proxy:
update_commenthappy path + traversal/missing-field rejection (invalid_update_comment_request). - CLI:
checkJiraAppIdentitywarn/pass/incomplete/unreadable matrix. pipeline.py:_should_post_start_commentsuppresses only the Jira pr-iteration duplicate ack.
Bootstrap synth-coverage: not applicable. No new CloudFormation resource types are introduced — the change adds a scoped IAM statement (secretsmanager:GetSecretValue/PutSecretValue on bgagent-jira-oauth-*) and an env var to the existing IterationHeartbeat Lambda, mirroring the reconciler's existing Linear grant. JiraWorkspaceRegistryTableName is a pre-existing stack output (not new). No bootstrap/ update required.
CI: all four checks green (build agentcore, secrets/deps/workflow scan, dead-code advisory, PR-title).
Review agents run
Operating as a review subagent, I applied each pr-review-toolkit agent's lens directly over the diff and full-file context in the worktree (I cannot spawn nested agent processes here):
- code-reviewer — guidelines/style, routing, L2/IAM patterns: pass (nit 2).
- silent-failure-hunter — every new write is best-effort with explicit claim-release + retryable-escalation branches; the heartbeat terminal-claim check fails open (returns false) by design. No swallowed failure that should surface. One idempotency edge (nit 1).
- type-design-analyzer —
JiraPostResult/JiraUpdateResultdiscriminated unions are sound;JiraFinishedPointerKindis a tight enum. Nit 2 is the only typing observation. - comment-analyzer — comments were faithfully updated to match new behavior; no stale/misleading comments remain (the old "no comment-edit API" lines are all gone).
- pr-test-analyzer — coverage assessed above; failure paths are well exercised.
- security-review judgment — applied (IAM + Forge proxy + path-traversal); see Human heuristics/Appropriateness. Nothing omitted as out-of-scope.
No agent was skipped.
Human heuristics
- Proportionality — Pass. Complexity matches the problem: the surface-neutral heartbeat generalization and the shared
jira-status-comment.tsrenderer avoid Linear/Jira copy-paste; the terminal-owner split (fan-out for standalone, reconciler for orchestrated child) is justified by the restack-ordering constraint. - Coherence — Pass. Reuses the existing
claimTerminalReply/releaseReplyClaimprimitive and theChannel/channelForSourceabstraction rather than inventing a parallel mechanism;renderJiraFinalStatusCommentwas relocated to the shared module with re-export, not duplicated. - Clarity — Pass, modulo nit 2. Names communicate intent (
renderJiraFinishedPointer,terminalReplyAlreadyClaimed,pointerKind); error branches log with structuredevent:keys. - Appropriateness — Pass. Forge
update_commentis validated against real Jira REST v3 semantics (PUT to/comment/{id}, ADF body object, numeric comment-id regex, issue-key traversal guard) and tested at the proxy boundary, not only via self-written mocks. IAMPutSecretValueis genuinely required (token refresh rewrites the cached OAuth bundle) and scoped to the tenant secret prefix — least-privilege preserved and consistent with the reconciler.
Refs #697 Co-Authored-By: Codex <codex@openai.com>
4f3937c to
e279de5
Compare
519eb4d to
5348759
Compare
Principal-architect review — PR #727 (Jira iteration feedback maturation)Verdict: Request changes. This is a careful, well-tested change that cleanly extends the ack→heartbeat→terminal maturation model to Jira: contracts are mirrored ( Vision alignmentFits the "reviewable outcomes / fire-and-forget with maturing feedback" tenet — a single ABCA-owned comment matures in place rather than spamming the issue, terminal ownership is split so orchestrated restacking stays ordered (reconciler) while standalone settles on fan-out. Bounded blast radius preserved: Jira feedback stays strictly best-effort and never gates the coding task. No tenet trade-offs requiring an ADR. Blocking1. Orchestrated-child terminal comment renders degraded (literal markdown + no line breaks) — 2. Terminal-result post failure leaves the pointer permanently promising a result that never lands — Non-blocking suggestions / nits
DocumentationGood. Tests & CIAll CI green (build agentcore, secrets/deps, title, dead-code). Coverage is strong and behavior-focused: comment-id capture, ADF-vs-text payloads, OAuth refresh+retry, single-writer claim/release, redelivery idempotency, terminal guard, Forge create+update with traversal rejection, and Linear/ordinary-Jira non-regression are all asserted; the ownership split is pinned from both sides. Gaps worth adding (not blocking):
Bootstrap synth-coverage: not applicable. The new Review agents run
Human heuristics
|
|
Addressed the new principal-architect review in Blocking fixes:
Suggestions addressed:
Validation: CDK lint/compile pass; targeted suites pass; full CDK suite passes (186 suites, 3,893 tests). Secrets scan is clean. The masking scan still reports the unchanged repository-wide 78-finding baseline. |
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Approve
Matures a single Jira iteration comment from acknowledgement → heartbeat → terminal outcome, closing the last gap that made Jira feel second-class vs. Linear. Reviewed as PR 3 of 3 against the stack base feat/578-jira-orchestration-extension (#725→#726→#727); anything originating in the parent PRs was scoped out. Both nits from the author's self-review (COMMENT, since GitHub blocks self-approval) are verified resolved at head 77209a4, all four CI checks are green, and I found no new blocking issue.
Vision alignment
Fits the north star cleanly. Fire-and-forget is preserved — every Jira write is advisory/best-effort and never gates or retries the coding task (webhook ack failure is caught and logged; heartbeat/terminal edits fail open). Reviewable outcomes improve: a live heartbeat replaces the observed 22-minute black box, and a deterministic terminal comment carries cost/turns/duration even when the agent crashes. Bounded blast radius holds: MAX_EDITS_PER_SWEEP cap retained, IAM scoped to the bgagent-jira-oauth-* secret prefix. No tenet is traded away; ADR-015 already covers the Jira integration and this is an implementation refinement, so no new ADR is required.
Blocking issues
None.
Non-blocking suggestions / nits
The two nits from the prior self-review are both verified fixed in the current head and require no further action:
- Iteration-without-reply-id terminal-comment duplication (
cdk/src/handlers/fanout-task-events.ts) — fixed ine279de52. The marker gate is nowif ((!isIteration || !iterationReplyId) && task.jira_final_comment_event_id)(line 1630), so a no-ack-id iteration correctly falls through to the post-once marker path and a partial-batch redelivery no longer double-posts. Confirmed against current code. let reply;implicitly untyped (cdk/src/handlers/orchestration-reconciler.ts) — fixed ine279de52. Nowlet reply: CommentRef | null | undefined;(line 1220), which makes the load-bearing tri-state (undefined = surface can't mature, null = attempted-and-failed) self-documenting. Confirmed.
One minor observation, purely optional: renderJiraFinishedPointerText / renderJiraFinalStatusText in the new jira-status-comment.ts exist as plain-text mirrors of the ADF renderers but appear to be exercised only by tests in this PR (the live paths use the ADF forms). If no production caller adopts them shortly, consider dropping them to avoid dead surface. Not blocking.
Documentation
Updated and in sync. docs/guides/JIRA_SETUP_GUIDE.md was revised (flow diagram, ordinary-vs-iteration terminal-owner split, claim/idempotency semantics, the fifth signed Forge operation update_comment, and a new platform doctor step) and the Starlight mirror docs/src/content/docs/using/Jira-setup-guide.md carries the identical edits — verified line-for-line, so CI's mutation check will pass. The stale "Jira has no comment-edit API" claims were correctly retired in the guide, the handler comments, and the TaskRecord.jira_final_comment_event_id doc-comment.
Tests & CI
Strong, failure-path-oriented coverage across all touched surfaces:
- Fan-out: standalone-iteration pointer-edit + separate result, orchestrated-iteration deferral to the reconciler, lost-claim (ConditionalCheckFailed) no-double-edit, failed/transient update with claim release + batch-retry escalation, exhausted-retry fold-into-status-comment, and the no-reply-id marker-honoring redelivery regression test.
- Reconciler: Jira orchestration-iteration maturation with metrics, failed-update claim release, terminal-post-failure fold, redelivery-matures-once.
- Heartbeat sweep: Jira adapter edit path + strongly-consistent terminal-claim guard preventing a late heartbeat from regressing a settled outcome; per-task edit-failure isolation.
- jira-feedback: PUT/OAuth path, Forge
update_comment, 401→forced-refresh→retry, missing-comment (404) rejection, non-numeric comment-id rejection before credential resolution, and comment-id extraction (string, numeric-coerce, missing-id, non-JSON). - Forge proxy:
update_commenthappy path plus traversal / missing-field rejection (invalid_update_comment_request). - CLI:
checkJiraAppIdentitywarn/pass/incomplete/unreadable matrix. - pipeline.py:
_should_post_start_commentsuppresses only the Jira pr-iteration duplicate ack.
Bootstrap synth-coverage: not applicable. No new CloudFormation resource types are introduced — the change adds a scoped IAM statement (secretsmanager:GetSecretValue/PutSecretValue on bgagent-jira-oauth-*) and an env var to the existing IterationHeartbeat Lambda, mirroring the reconciler's existing Linear grant. JiraWorkspaceRegistryTableName is a pre-existing stack output. No cdk/src/bootstrap/ update required.
Test performance: no re-enabling of aws:cdk:bundling-stacks; the new agent.test.ts case reuses the shared template. Clean.
CI: all four checks green (build agentcore, secrets/deps/workflow scan, dead-code advisory, PR-title).
Review agents run
Operating as a review subagent, I applied each pr-review-toolkit agent's lens directly over the diff and full-file worktree context (nested agent-process spawning is not available in this harness, so the lenses were applied by hand against the exact changed lines):
- code-reviewer — guidelines/style, routing (agent/cdk/cli/docs all landed in the correct package per AGENTS.md), L2/IAM patterns (
formatArn+ArnFormat, no hardcoded ARNs): pass. - silent-failure-hunter — every new write is best-effort with explicit claim-release and retryable-escalation branches;
terminalReplyClaimedfails open (returns false) by design; the webhook ack post and its id-persistence are both wrapped and logged. No failure that should surface is swallowed. - type-design-analyzer —
JiraPostResult/JiraUpdateResultdiscriminated unions are sound; splitting the create result (commentId) from the update result (no id) is the right shape.JiraFinishedPointerKindis a tight enum. The reconcilerreplytri-state is now explicitly typed. - comment-analyzer — comments were faithfully updated to match new behavior; no stale/misleading comments remain (all "no comment-edit API" lines are gone from handlers, constructs, and the shared type doc).
- pr-test-analyzer — coverage assessed above; failure paths are well exercised, not just happy paths.
- security-review (lens) — applied to the IAM grant, the Forge proxy
update_commentop, and the comment-id path-traversal guard; see Human heuristics / Appropriateness.PutSecretValueis genuinely required (OAuth refresh rewrites the cached bundle) and scoped to the tenant secret prefix. Nothing omitted as out-of-scope.
No in-scope agent was skipped.
Human heuristics
- Proportionality — Pass. The surface-neutral heartbeat generalization and the shared
jira-status-comment.tsrenderer avoid Linear/Jira copy-paste; the terminal-owner split (fan-out for standalone, reconciler for orchestrated child) is justified by the restack-ordering constraint, not gratuitous. - Coherence — Pass. Reuses the existing
claimTerminalReply/releaseReplyClaimprimitive and theChannel/channelForSourceabstraction rather than inventing a parallel mechanism;renderJiraFinalStatusCommentwas relocated to the shared module and re-exported, not duplicated. - Clarity — Pass. Names communicate intent (
renderJiraFinishedPointer,terminalReplyClaimed,pointerKind,credentialsRef); error branches log with structuredevent:keys. Minor: the plain-text renderer mirrors are currently test-only (see nits). - Appropriateness — Pass. Forge
update_commentis validated against real Jira REST v3 semantics (PUT to/comment/{id}, ADF body object, numeric comment-id regex, issue-key traversal guard) and tested at the proxy boundary, not only via self-written mocks. The write helper'scommentId: ''sentinel on a create-succeeded-but-unparseable-id response correctly prefers not to duplicate — a sound fail-safe choice.
Matures one Jira iteration status comment from acknowledgement through heartbeat to terminal outcome. This is PR 3 of 3 for #580 and is based on #726.
Area
cdk- infrastructure, handlers, constructsagent- Python runtime / Docker imagecli-bgagentclientdocs- guides or design sources (docs/guides/,docs/design/)tooling- rootmise.toml, scripts, CI workflowsRelated
Closes #697
Parent: #580
Stack: #725 -> #726 -> #727
Changes
Verification
mise run build178suites,3,554tests429tests11testsGITLEAKS_RANGE=origin/main..HEAD mise run security:secrets:rangeAcknowledgment
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.