Skip to content

feat(jira): Jira Cloud integration — parity with Linear (#288)#10

Open
mayakost wants to merge 31 commits into
mainfrom
feat/288-jira-integration
Open

feat(jira): Jira Cloud integration — parity with Linear (#288)#10
mayakost wants to merge 31 commits into
mainfrom
feat/288-jira-integration

Conversation

@mayakost

Copy link
Copy Markdown
Owner

Summary

Full Jira Cloud integration, bringing Jira to parity with the existing Linear adapter: a Jira issue gets the bgagent label → ABCA picks it up → an agent run produces a PR → progress flows back as comments on the originating issue. Implements aws-samples#288.

The Linear adapter was the file-for-file template; this PR diverges only where Jira's API forces it (see Jira-specific divergences below). Design rationale is captured in ADR-014 (docs/decisions/ADR-014-jira-integration.md).

Scope: Jira Cloud only (REST v3 + Cloud webhooks), per-tenant OAuth 3LO, label-only trigger. Inbound-only adapter — no DynamoDB Streams consumer, no outbound-notify Lambda. Out of scope: Jira Server/Data Center, Forge/Connect distribution, status-transition/comment-command triggers, bidirectional state sync.

Architecture

Inbound (Jira → ABCA):

Jira Cloud webhook
  → POST /jira/webhook (API GW, no Cognito, HMAC-verified)
  → JiraWebhookFn      (verify X-Hub-Signature, dedup, async-invoke processor)
  → JiraWebhookProcessorFn (resolve cloudId→tenant, project→repo, build task, createTaskCore)
  → existing orchestrator pipeline (unchanged)

Outbound (Agent → Jira): progress comments posted on the originating issue at task start and completion.

What's included

Contracts (Phase 1)

  • 'jira' added to the ChannelSource union on both sides of the wire (cdk/src/handlers/shared/types.ts, cli/src/types.ts) plus the agent/src/models.py doc comment. check-types-sync.ts allowlists JiraLinkResponse as CLI-only (parity with Slack/Linear link responses).

CDK constructs & DynamoDB (Phases 2 & 4)

  • JiraIntegration construct (cdk/src/constructs/jira-integration.ts) — 3 mapping tables + an 8-hour-TTL webhook-dedup table, 3 Lambdas (webhook / processor / link), /jira/* API routes, per-tenant bgagent-jira-oauth-* IAM grants, and cdk-nag suppressions.
  • Three DDB table constructs, all keyed on cloudId as the tenant prefix so the same project key / account id stays unambiguous across tenants:
    Table PK
    JiraProjectMappingTable {cloudId}#{projectKey}owner/repo
    JiraUserMappingTable {cloudId}#{accountId} (+ PlatformUserIndex GSI)
    JiraWorkspaceRegistryTable jira_cloud_id → OAuth provider name
  • Stack wiring (cdk/src/stacks/agent.ts): grants the orchestrator read on the workspace registry + Get/PutSecretValue on the per-tenant prefix (mirrors Linear's pre-container failure-feedback path).

Lambda handlers & shared helpers (Phase 3)

  • jira-webhook.ts — HMAC-SHA256 verify over the raw body (X-Hub-Signature: sha256=<hex>, constant-time compare), event filtering (jira:issue_created / jira:issue_updated), dedup, async invoke. Silent 200 for unsupported events.
  • jira-webhook-processor.ts — label-add detection by diffing changelog.items[] (not issue.fields.labels), ADF→markdown for the task description, cloudId→tenant resolution, createTaskCore with channelSource: 'jira'.
  • jira-link.ts — Cognito-authenticated Jira-account → platform-user linking with dry-run preview.
  • Shared: jira-oauth-resolver.ts (per-tenant token resolve/refresh), jira-verify.ts (signature), jira-feedback.ts (REST-based failure comment at the orchestrator boundary).

Agent runtime (Phase 5)

  • channel_mcp.py refactored from a single-channel gate to a CHANNEL_MCP_BUILDERS dispatch dict — adding a channel is now one entry, not a rewrite.
  • resolve_jira_oauth_token() added to config.py, mirroring the Linear resolver's race-handling and fail-closed semantics (differs only in endpoint auth.atlassian.com + JSON body + JIRA_API_TOKEN).

Outbound progress comments — agent/src/jira_reactions.py (Phase 7)

Jira-origin tasks comment on the issue at start (🤖 picked up…) and completion (✅ finished — PR: <url> / ❌ …), wired into pipeline.py at start / finish / crash paths, parallel to the Linear hooks. Comments are advisory — all network/auth errors are swallowed and never gate the pipeline (with an auth circuit-breaker mirroring linear_reactions).

Note — outbound is REST, not MCP. ADR-014 originally specified the Atlassian Remote MCP server for outbound. In practice the hosted MCP (mcp.atlassian.com) requires an interactive browser-based OAuth 2.1 flow with dynamic client registration and will not accept the stored REST OAuth token as a Bearer header, so it fails to load from a headless agent. The Jira REST API accepts the same token (it carries write:jira-work), so comments go via POST /rest/api/3/issue/{key}/comment. This is the ADR's documented "Plan B" fallback, promoted to the implemented path.

CLI (Phase 6)

bgagent jira with 4 v1 subcommands (app-template, setup, link, map) + jira-oauth.ts (ports linear-oauth.ts; Atlassian uses a JSON token endpoint and requires offline_access for a refresh token). Deferred: add-workspace, update-webhook-secret, invite-user, list-projects.

Docs

JIRA_SETUP_GUIDE.md, ADR-014, README / USER_GUIDE / ROADMAP channel listings, and regenerated Starlight mirrors.

Jira-specific divergences from the Linear copy

  1. Label-add on updates — Jira reports label changes in changelog.items[] (field: "labels", fromString/toString), not as a full label list; the processor diffs the changelog so re-saving an already-labeled issue doesn't re-trigger.
  2. Operator-chosen signing secret — Atlassian doesn't auto-generate a per-subscription secret; the operator sets it at webhook-create time and pastes it during bgagent jira setup. Stored on the per-tenant OAuth bundle with a stack-wide fallback.
  3. cloudId is the tenant key everywhere — not domain or site name.
  4. UI-created webhooks omit cloudId — the processor falls back to the sole active tenant in the registry, and deliberately drops (rather than guesses) when zero or multiple active tenants exist, preserving multi-tenant safety.
  5. ADF descriptions — flattened to markdown (text/headings/lists), not a full ADF converter.
  6. Dedup key {issueKey}#{webhookEventTimestamp}, 8-hour TTL.

Testing

  • mise //cdk:compile — clean; check-types-sync.ts — passing (verified on this branch).
  • Per the final integration commit: 294 CLI + 837 agent + 1896 CDK tests passing, mise run build green.
  • New suites: jira-webhook.test.ts, jira-webhook-processor.test.ts, jira-link.test.ts, jira-oauth-resolver.test.ts (32), test_jira_reactions.py, test_channel_mcp.py (jira cases), plus agent.test.ts updated for the 4 new tables (13→17).

Branch rebased onto the fork's main so the diff is Jira-only (46 files); the 4 unrelated upstream commits it was originally cut from have been dropped.

bgagent added 12 commits June 5, 2026 14:09
Phase 1 of Jira Cloud integration (aws-samples#288). Extends the ChannelSource
discriminant on both sides of the wire and updates the agent-side
comment so the runtime knows 'jira' is a recognized channel value;
no behavior changes yet.
Phase 2 of Jira Cloud integration (aws-samples#288). Mirrors the Linear constructs
file-for-file. Composite PKs use cloudId as the tenant prefix
(`{cloudId}#{projectKey}`, `{cloudId}#{accountId}`) so the same project
key or account id stays unambiguous across distinct Atlassian tenants.
Tables are unwired until Phase 4 — JiraIntegration instantiates and
grants them.
Phase 3 of Jira Cloud integration (aws-samples#288). Mirrors Linear's adapter
shape: per-tenant OAuth resolver (auth.atlassian.com), X-Hub-Signature
HMAC verify with per-tenant + stack-wide fallback, REST-based feedback
poster (ADF-wrapped, no reaction primitive — marker folded into text),
and three Lambdas (webhook, processor, link).

Non-trivial bit: the processor diffs `changelog.items[]` where
`field === 'labels'` and tokenizes the space-separated `fromString` /
`toString` to detect a label add — Atlassian's diff format differs
from Linear's `updatedFrom.labelIds`. Includes a minimal ADF→markdown
walker for issue descriptions.

Handlers reference JIRA_* env vars set by the JiraIntegration construct
in Phase 4; they don't deploy yet.
Phase 4 of Jira Cloud integration (aws-samples#288). Mirrors LinearIntegration:
3 DDB tables, dedup table (8h TTL), 3 Lambdas (webhook/processor/link),
API routes under /jira/*, per-tenant `bgagent-jira-oauth-*` IAM grants,
cdk-nag suppressions.

Stack wiring grants the agent runtime GetSecretValue on the per-tenant
prefix and pipes the workspace registry table + Get/Put grant into the
orchestrator (matches Linear's path for pre-container failure feedback).
Synth confirms clean CloudFormation + no nag findings.
Phase 5 of Jira Cloud integration (aws-samples#288). Refactors channel_mcp.py from
a single-channel gate to a CHANNEL_MCP_BUILDERS dispatch dict so adding
future channels stays one-entry. Adds resolve_jira_oauth_token() to
config.py mirroring the Linear resolver — same race-handling, same
fail-closed semantics; only differences are the endpoint
(auth.atlassian.com, JSON body) and the env-var name (JIRA_API_TOKEN).

Pipeline now dispatches to the right resolver based on channel_source.
JIRA_MCP_URL is flagged in-source as needs-verification — Atlassian's
Remote MCP may still be preview-gated; if so, fall back to a REST shim
in a future jira_reactions.py module (Plan B).

Tests: 6 new Jira test cases in test_channel_mcp.py; full agent suite
remains green (825 passed).
Phase 6 of Jira Cloud integration (aws-samples#288). Minimal v1 surface (4 of 10
Linear subcommands), per scoping decision. Mirrors the Linear CLI shape
where the contracts are similar:

- jira-oauth.ts ports linear-oauth.ts. Atlassian's token endpoint takes
  JSON (Linear takes form-encoded). offline_access scope is required
  for a refresh_token. fetchAccessibleResources() resolves cloudId +
  siteUrl post-consent.
- commands/jira.ts: app-template prints dev-console values; setup
  drives the OAuth dance + writes the per-tenant secret + registry row
  + webhook signing secret; link does dry-run preview UX; map writes
  the project → repo row.

Deferred to follow-ups: add-workspace, update-webhook-secret,
invite-user (with self-link picker), list-projects.
Covers signature verify pass/fail, dedup, event filtering, label-add
detection (create vs update changelog), and Cognito-authenticated linking.
56 tests, mirrors the Linear handler test surface.
- docs/guides/JIRA_SETUP_GUIDE.md — OAuth 3LO app, scopes, webhook
  registration, label trigger, project mapping, troubleshooting
- docs/decisions/ADR-014-jira-integration.md — Jira Cloud only, OAuth 3LO,
  label trigger, MCP outbound; documents the Jira-vs-Linear divergences
- README, USER_GUIDE, ROADMAP — add Jira to channel listings
- sync-starlight.mjs + astro.config.mjs — register the Jira guide mirror;
  regenerate Starlight content under docs/src/content/docs/

Completes the docs phase of aws-samples#288.
Bring `mise run build` green on the jira integration branch:

- check-types-sync: allowlist JiraLinkResponse as CLI-only, matching
  SlackLinkResponse/LinearLinkResponse (link responses are inlined
  server-side; no CDK source-of-truth type)
- channel_mcp.py: move Callable into a TYPE_CHECKING block (ruff TC003;
  safe under `from __future__ import annotations`)
- agent.test.ts: bump expected DynamoDB table count 13 -> 17 for the
  four new Jira tables (project/user/workspace-registry/webhook-dedup)
- test_config.py: cover resolve_jira_oauth_token (cache, fallback,
  refresh, concurrent-refresh, malformed/expiry paths); agent coverage
  70.41% -> 72.91%
- jira-oauth-resolver.test.ts: new suite (32 tests) mirroring the Linear
  resolver tests; clears the CDK statement/line/function/branch gates
- jira.ts / jira-oauth.ts: ESLint --fix cosmetic edits (quote-props,
  redundant template literals)

Tests: 294 CLI + 837 agent + 1896 CDK, all passing.
Jira webhooks created via the Settings → System → Webhooks UI do not
include a top-level `cloudId` in their payload (only app/OAuth-registered
dynamic webhooks do). Without it the processor can't resolve the tenant,
so it dropped the event and never created a task — the inbound trigger
silently failed for the common single-tenant, UI-webhook setup.

Add a safe fallback: when `payload.cloudId` is absent, scan the workspace
registry and use the sole `active` tenant. Deliberately refuses to guess
when zero or multiple active tenants exist (returns undefined → event
dropped), so the multi-tenant design is preserved — a multi-tenant
operator must use a webhook that carries its own cloudId.

`grantReadData` on the registry table already covers the Scan, so no IAM
change is needed.

Adds tests for: sole-tenant recovery (task created), empty registry
(drop), and multiple active tenants (ambiguous → drop).
Jira-origin tasks now comment on the originating issue at start
("🤖 picked up…") and on completion ("✅ finished — PR: <url>" / "❌ …"),
matching the Linear integration's progress UX.

Why a REST shim instead of the Atlassian Remote MCP: the hosted MCP
(mcp.atlassian.com) requires an interactive, browser-based OAuth 2.1 flow
with dynamic client registration — it does NOT accept the stored Jira REST
OAuth token as a Bearer header, so it fails to connect from a headless
agent ("claude mcp list" → Failed to connect; no mcp__jira-server__* tools
load). The Jira REST API accepts the same stored token (it carries
write:jira-work), so comments go via POST /rest/api/3/issue/{key}/comment
on the cross-region api.atlassian.com/ex/jira/{cloudId} base.

- New `jira_reactions.py`: gated by channel_source=='jira' + required
  metadata; swallows all network/auth errors (comments are advisory, never
  gate the pipeline); auth circuit-breaker mirrors linear_reactions.
- Wired into pipeline.py at task start, normal finish (with PR url), and
  the crash path — parallel to the existing Linear reaction hooks.
- prompt_builder: Jira tasks now get NO MCP-comment addendum (the earlier
  Linear-only gate already skipped them); instructing the agent to use the
  non-loading MCP tools would just waste turns. Comments are out-of-band.

Adds test_jira_reactions.py (gate, ADF body, success/failure/PR variants,
error-swallowing, auth circuit breaker) and channel-addendum tests.
mayakost pushed a commit that referenced this pull request Jun 10, 2026
…rkflows (aws-samples#248) (aws-samples#296)

* Initial design docs

* docs(workflows): address principal-review findings on ADR-014

- Validator parity: declare JSON Schema as the single canonical shape
  contract (consumed, not re-implemented), keep exactly one cross-field
  validator impl in Phases 1-3, and add a contracts/workflow-validation/
  golden corpus to lock any Phase-4 second impl to parity (mirrors the
  cedar-parity mechanism).
- Cedar principal migration: split into its own isolated PR (Phase 2a)
  reviewed alone with regenerated parity fixtures, landing ahead of the
  pr_iteration/pr_review workflow migrations (Phase 2b) that depend on it.
- Repo-optionality: reframe web_research as a not-yet-runnable schema
  fixture (not an acceptance proof) and require the two blocking open
  questions (memory actorId, artifact delivery) to be resolved as a
  recorded ADR addendum before the Phase-0 schema is frozen.

Regenerated Starlight mirrors.

* feat(agent): workflow models + loader (aws-samples#248 Phase 1)

Add the agent/src/workflow/ package: Pydantic models mirroring
workflow.schema.json and a loader that resolves a first-party workflow
id, parses YAML, and shape-validates against the canonical JSON Schema.
Additive only — does not yet touch task_type (the breaking cutover lands
later in Phase 1).

- models.py: Workflow + sub-models; resolved_requires_repo applies the
  domain-derived default (coding=true, knowledge/hybrid=false).
- loader.py: JSON-Schema shape validation (one canonical contract,
  consumed not re-implemented), YAML load, id/path agreement, and
  path-traversal defense. Cross-field rules deliberately live in the
  separate validator (Task #2), so there is one cross-field impl.
- Promote pyyaml + jsonschema to direct deps (were only transitive); the
  loader must not depend on another package's transitive pin.
- Fix a latent schema bug found by the new tests: the requires_repo/
  read_only allOf conditionals fired vacuously when the property was
  absent, wrongly applying repo-less constraints to a coding workflow
  relying on the domain default. Guard each `if` with `required`.

17 new tests; agent ruff + ty + full suite (836) green.

* feat(agent): cross-field workflow validator + golden corpus (aws-samples#248 Phase 1)

The single CI-time implementation of the WORKFLOWS.md cross-field rules
the JSON Schema cannot express (rules 1-9, 11, 12, 14). The runtime loader
stays shape-only and trusts this verdict, so there is exactly one cross-field
implementation in Phases 1-3 (avoids the cedar-style two-language drift).

contracts/workflow-validation/ ships the golden parity corpus from Phase 1 so
the expected-verdict contract is fixed before aws-samples#246 adds a second validator,
mirroring contracts/cedar-parity/. test_workflow_tree_valid.py gates every
first-party workflow file through the validator at CI time.

* feat(agent): workflow step runner + handler registry (aws-samples#248 Phase 1)

The agent-side step runner that interprets workflow.steps inside the container
(ADR-014): a StepKind→handler registry, in-order execution honouring each step's
on_failure policy (fail / continue / skip_remaining), per-step
step:<name>:start/<status> progress milestones, and resume-aware checkpointing
(deterministic side-effect-free steps recorded to workflow_state.json on the
persistent mount are skipped on resume; agentic/side-effecting steps re-run
idempotently per WORKFLOWS.md §Step execution semantics).

Handlers are thin wrappers over the existing helpers (setup_repo, run_agent,
verify_build/lint, ensure_pr) so this is maximal structural change, minimal
logic change. post_review/deliver_artifact are registered but raise
NotImplementedError (Phase 2b / Phase 3) — fail-loud, not silent no-op, keeping
validator rule-8 handler parity honest. Orchestration core is unit-tested with
fakes; wiring into pipeline.run_task is task 5.

* feat(agent): first-party workflow files — coding/new-task-v1 + default/agent-v1 (aws-samples#248 Phase 1)

Migrates the new_task task type to coding/new-task-v1.yaml (the heavyweight
clone→build→open-PR coding path) and ships the platform default workflow
default/agent-v1.yaml — the conservative last rung of the resolution ladder
used when no workflow_ref and no Blueprint default apply (requires_repo:false,
read-leaning tool set, deliver-as-comment). pr_iteration/pr_review migrations
are Phase 2b (depend on the Cedar principal migration).

Both pass the cross-field validator and the test_workflow_tree_valid CI gate
(file path agrees with declared id; zero violations).

* fix(agent): address code-review findings in workflow validator + runner (aws-samples#248 Phase 1)

Fixes the group-A bugs surfaced by the high-recall code review of the Phase 1
commits:

runner.py
- Resume-skip product loss: clone_repo/hydrate_context populate in-memory
  products (ctx.setup, ctx.user_prompt) that can't be rebuilt from the JSON
  checkpoint, so skipping them on resume left ctx.setup None (breaking ensure_pr)
  and ctx.user_prompt empty (unguided agent). Narrow _RESUMABLE_SKIP_KINDS to
  verify_build/verify_lint — steps whose ENTIRE product is the checkpointed
  boolean re-applied via ctx.artifacts. clone_repo/hydrate_context now re-run on
  resume (handler-level idempotency), matching WORKFLOWS.md's intent.
- Skipped steps now emit step:<name>:start / :skipped milestones so a watcher
  sees them accounted for instead of a gap.

validator.py
- _HANDLER_KINDS (rule 8) is now derived from runner.STEP_HANDLERS — the single
  source of truth — instead of a hand-copied list that could silently drift.
- rule-6 tier ceiling now applies the elevated-only-fields check to read-only
  (the strictest tier) as well as standard; previously read-only could declare
  mcp_servers/plugins/skills unchecked.
- rule-11 now verifies a deliver_artifact step's target actually produces the
  declared comment/artifact outcome, not merely that the step kind is present.

Tests + corpus updated; rule3/rule7 fixtures switched to s3_and_comment target
to stay isolated to their intended rule, plus a new rule11 target-mismatch
fixture. Full agent suite green (909 passed), ruff + ty clean.

* feat(agent): wire workflow runner into pipeline behind a gate (aws-samples#248 Phase 1, task 5)

Builds the runner→pipeline seam and gates it off (default production behavior
unchanged). pipeline.run_task's agent invocation now goes through
_execute_agent_step: when WORKFLOW_RUNNER_ENABLED is set AND task_type maps to a
shipped workflow (only new_task→coding/new-task-v1 in Phase 1), the single
run_agent step is dispatched through workflow.run_workflow — exercising the real
handler registry, step milestones, and result threading — while clone, context
assembly, and post-hooks stay on the proven inline path. Otherwise it is exactly
the legacy asyncio.run(run_agent(...)) call. pr_iteration/pr_review have no
workflow file until Phase 2b, so they fall back to inline.

run_workflow gains an only_kinds filter so the seam drives just the run_agent
step (no double clone / double PR against the inline post-hooks). The flag is
flipped to default-on in the tasks 6-8 cutover.

Folds in the group-B code-review fixes (effective when the seam is enabled):
- system prompt: hydrate_context builds ctx.system_prompt via the existing
  build_system_prompt (was empty), reusing pipeline's logic rather than
  reimplementing it.
- verify gate: new shared _gate_status gives verify_build/verify_lint matching
  semantics — informational/read_only never gate, regression_only consults
  build_before/lint_before (mirrors pipeline's build_ok = passed or not
  build_before), strict always gates. Fixes the regression_only build bug and
  the verify_lint read_only asymmetry.
- clone_repo is idempotent (reuses a pre-populated ctx.setup).
- StepContext threads the --trace trajectory into run_agent (was dropped).

ensure_pr strategy reconciliation stays deferred to task 8 (it requires removing
the task_type branch inside ensure_pr); recorded in local-docs. Full agent suite
green (930 passed), ruff + ty clean.

* fix(agent): address second-round review findings on the workflow seam (aws-samples#248 Phase 1)

- Error-handling contract (highest severity): when the run_agent step's handler
  raises, run_workflow captures it into a failed StepOutcome rather than
  propagating. _execute_agent_step now RE-RAISES in that case so run_task's
  except block restores full fidelity — the log_error_cw APPLICATION_LOGS mirror
  (read by TaskDashboard / bgagent status), the span error, and the real
  exception text — instead of silently downgrading to a generic AgentResult.
  This also makes the previously-dead agent_result-is-None branch live.

- _gate_status: an unset gate now defaults to regression_only semantics (gate
  only a regression; a pre-existing failure does not gate), matching pipeline.py
  which is unconditionally 'build_ok = passed or not build_before'. Previously an
  unset gate fell through to strict, contradicting the docstring's 'mirrors
  pipeline.py' claim and would wrongly fail a broken-before repo.

- run_agent handler fails loud on an empty system prompt rather than running an
  unguided agent loop (repo-less prompt assembly is Phase 3; this turns the gap
  into an attributable failed step instead of a silent context-free run).

- _workflow_id_for_task_type: documented as a transitional bridge tied to the
  canonical task_type→workflow table in WORKFLOWS.md / API_CONTRACT.md, to be
  kept in sync until tasks 6-8 delete it.

Tests added for each behavior change. Full agent suite green (934 passed),
ruff + ty clean.

* feat(api,cli): replace task_type with workflow_ref/resolved_workflow (aws-samples#248 tasks 6-7)

Breaking API change (no alias): task_type is removed end-to-end; workflow_ref
selects a workflow and resolves to a pinned {id, version} at the create-task
boundary.

CDK/CLI types (task 6, in lockstep — check:types-sync):
- Remove TaskType + isPrTaskType; add ResolvedWorkflow.
- CreateTaskRequest.workflow_ref?; TaskRecord/TaskDetail/TaskSummary
  resolved_workflow (+ mappers).
- CLI: --pr/--review-pr now set workflow_ref (coding/pr-iteration-v1,
  coding/pr-review-v1); new --workflow flag; format reads resolved_workflow.id.

CDK resolution boundary (task 7):
- New workflows.ts: CDK-side descriptor mirror of agent/workflows/** +
  resolveWorkflowRef (ladder: explicit ref → default/agent-v1), isValidWorkflowRef,
  requires_repo/read_only/uses_pr accessors. Drift-guard test keeps the table in
  sync with the shipped YAML.
- validation.ts: drop VALID_TASK_TYPES/isValidTaskType; hasTaskSpec now takes the
  resolved workflow's required_inputs contract.
- create-task-core.ts: resolve workflow, validate inputs against the contract,
  persist workflow_ref + resolved_workflow. Repo stays required for ALL workflows
  in Phase 1 (repo-less admission is Phase 3).
- preflight.ts: taskType param → readOnly + requiresRepo; repo-less short-circuits
  to passed (seam for Phase 3).
- orchestrate-task/context-hydration/orchestrator/ecs-strategy: derive
  read_only/requires_repo/PR-ness from resolved_workflow; payload swaps task_type
  → resolved_workflow. ecs bootCommand passes resolved_workflow (lockstep with
  agent run_task in task 8).

CLI 294 tests + CDK suite green; check:types-sync OK. Agent cutover (task 8) lands
next to consume resolved_workflow.

* feat(agent): complete task_type→workflow cutover in the agent (aws-samples#248 task 8)

Removes the Python TaskType enum / PR_TASK_TYPES / _PROMPTS-by-task_type and the
gated seam; the workflow runner is now the sole agentic path, driven by the
resolved_workflow {id, version} threaded from the create-task boundary.

- models.py: delete TaskType enum; TaskConfig gains resolved_workflow,
  policy_principal, is_pr_workflow (drops task_type).
- config.py: build_config takes resolved_workflow (not task_type); derives
  policy_principal via workflow.policy_principal_for and is_pr_workflow; PR_TASK_TYPES
  → PR_WORKFLOW_IDS.
- Cedar UNCHANGED (Phase 2a owns the principal migration): the
  Agent::TaskAgent::"<id>" scheme and contracts/cedar-parity/ are untouched.
  policy_principal_for maps read_only⇒pr_review, else id→legacy {new_task,
  pr_iteration, pr_review}. Only policy.py edit: drop the WARN-only TaskType()
  validation. runner.py passes config.policy_principal as the principal. All
  test_policy* pass unchanged.
- prompts rekeyed by workflow id (get_system_prompt falls back to the default
  coding prompt for an unknown id rather than raising).
- pipeline.py: _execute_agent_step loads from resolved_workflow, always-on;
  the 3 task_type=='pr_review' branches → workflow.read_only; ensure_pr takes an
  explicit strategy (create/push_resolve/resolve) from the workflow step,
  resolving the deferred code-review item.
- post_hooks.ensure_pr: strategy param replaces task_type self-inspection.
  repo.py: PR-branch resume keys off config.is_pr_workflow.
- server.py/entrypoint.py: thread resolved_workflow; validation keyed on PR
  workflow ids.
- New faithful Phase-1 workflow files coding/pr-iteration-v1 (push_resolve) +
  coding/pr-review-v1 (read_only, resolve, no Write/Edit per rule 4). All 4
  first-party files validate; CDK descriptor drift-guard green.
- docs: API_CONTRACT.md migration table + resolved_workflow responses; Starlight
  mirror synced.

Agent suite 922 passed, ruff + ty clean. With tasks 6-7 (e829c4f) this completes
the breaking cutover across API + CLI + agent.

* fix(agent): use ty ignore syntax + annotate workflow test handlers (aws-samples#248)

The full build was never completed on this branch; ty type-checking and ruff
format gates were red. No src logic changes — type/format hygiene only.

- Tests used mypy-style `# type: ignore[arg-type]`, but the project type
  checker is ty (`# ty: ignore[rule]`), so the suppression silently no-op'd.
  - test_workflow_runner.py: annotate inline step handlers as
    StepHandler-compatible `(step: Step, ctx: StepContext) -> StepOutcome`;
    type the `handlers` dicts as `dict[str, StepHandler]` (ty treats the alias
    params as positional-only → dict-value invariance rejects named-param
    funcs); convert the stale ignore to `# ty: ignore[invalid-argument-type]`.
  - test_entrypoint.py: assert `resolved_workflow is not None` before
    subscripting (TaskConfig.resolved_workflow is `dict | None`).
- Apply ruff format reflows (runner.py, validator.py,
  test_workflow_tree_valid.py) the branch was committed without.

* feat(agent): Cedar read-only enforcement keys off context.read_only (aws-samples#248 Phase 2a)

Migrate read-only enforcement from the literal `Agent::TaskAgent::"pr_review"`
principal match to the `context.read_only` attribute, so the Write/Edit
hard-deny attaches to the *property* and fires for every read-only workflow
uniformly — not just coding/pr-review. Removes the Phase-2b principal bridge.

This is the security-load-bearing step: an error here *silently weakens*
enforcement (the rule stops matching) rather than failing loudly. Guarded by
new contracts/cedar-parity/ golden fixtures verified on BOTH the cedarpy
(Python) and cedar-wasm (TypeScript) engines.

Policy (both bindings, kept byte-for-byte identical — drift guard enforces):
- agent/policies/hard_deny.cedar + cdk/.../builtin-policies.ts:
  pr_review_forbid_write/edit (principal == "pr_review") →
  read_only_forbid_write/edit (when context.read_only == true, any principal).

Wiring:
- policy.py: PolicyEngine gains read_only kwarg; threads read_only into every
  Cedar request context (probe + base_context).
- models.py: TaskConfig.read_only; config.py derives it from the resolved
  workflow; runner.py passes config.read_only to PolicyEngine.
- workflow/loader.py: remove the read_only ⇒ "pr_review" bridge in
  policy_principal_for — the principal is now an identity/audit tag only;
  pr-review keeps its own id-derived principal.

Parity fixtures (new): read-only-forbid-write, read-only-forbid-edit
(read_only=true ⇒ deny), read-only-false-permits-write (read_only=false ⇒
base_permit — proves the deny is gated on the property, not always-on).

Tests: TestPrReviewPermissions → TestReadOnlyPermissions, now asserting the
deny fires for any read-only principal AND that read_only=false permits Write;
test_hooks + test_config updated. agent 927 / cdk 1822 green; ty + ruff clean.

Docs: ADR-014 addendum (2026-06-08) records dropping the isolated-PR/ordering
requirement (2b shipped first behind the bridge, so read-only was never
unprotected); WORKFLOWS.md §"Replacing the Cedar principal" + phasing table
updated to past tense.

* docs(workflows): resolve repo-optional open questions + freeze schema (aws-samples#248)

ADR-014 addendum (2026-06-08) settles the two open questions that gated the
Phase-0 schema freeze (WORKFLOWS.md open questions #1, #2). With the one implied
schema reshape applied, the schema is frozen — later phases add handlers and
plumbing, not schema fields.

Decision 1 — Memory actorId for repo-less tasks: per-user user:{cognito_sub}.
Caller-scoped (no cross-tenant bleed; mirrors the per-user trace prefix); cross-
workflow pooling explicitly not adopted. NO schema field — a fixed platform
fallback, applied in memory.py in Phase 3 when repo is absent.

Decision 2 — Artifact delivery: named Python deliverers (open target string),
shared S3 plumbing pinned now. deliver_artifact.target resolves to a registered
deliverer (workflow/deliverers.py → DELIVERERS), same pattern as STEP_HANDLERS —
a new delivery method is a registered deliverer, not a schema change. Plumbing
frozen: task-scoped key artifacts/{task_id}/, prefix-scoped SessionRole grant,
size limit, TaskDetail URL surfacing, workflow:{id} repo-less tenant tag.

Schema reshape applied:
- workflow.schema.json: steps[].target drops its enum (stays type:string,
  minLength:1); the closed set moves into the deliverer registry.
- models.py: DeliverTarget Literal → str.
- New workflow/deliverers.py: Deliverer dataclass + DELIVERERS registry +
  produced_outcomes(); single source of truth for "what each target produces."
- validator.py rule 11: consults the registry (_deliverer_produces) instead of
  the hardcoded _DELIVER_TARGET_OUTCOMES map. The three first-party names keep
  their exact produced-outcome sets, so NO existing workflow / fixture / golden
  vector changes verdict; an unknown deliverer name now produces nothing (new
  guard, tested).

Tests: test_workflow_deliverers.py (registry contract) + a rule-11
unknown-deliverer case. agent 932 green; ty + ruff clean. deliver_artifact
remains a NotImplementedError stub until Phase 3 — only the contract is frozen.

* feat(api,agent): repo-optional admission for repo-less workflows (aws-samples#248 Phase 3)

First slice of Phase 3 (repo-optional tasks): the admission path now accepts a
submission with no repo when the resolved workflow does not require one, so a
repo-less knowledge workflow (e.g. default/agent-v1) can be admitted, hydrated,
and run from task_description alone. The deliverer/S3 + agent-runtime clone-skip
slices follow separately.

Semantics: requires_repo decides whether a repo is MANDATORY; requires_repo:false
means repo-OPTIONAL (a repo-less workflow may still target a repo). The repo-less
behavior keys off repo *absence*, not the workflow flag — a repo-optional workflow
given a repo still takes the repo-bound path.

Types (CDK + CLI mirror, kept in sync — check:types-sync green):
- CreateTaskRequest.repo, TaskRecord.repo → optional; TaskDetail.repo,
  TaskSummary.repo → string | null. Mappers null-coalesce.

Admission (create-task-core.ts): resolve the workflow FIRST, then gate repo on
workflow.requiresRepo (missing repo on a repo-bound workflow → 400). Onboarding +
blueprint-cap lookup runs only when a repo is present; repo-less takes the platform
default cap. Record omits repo when absent; replay-required-fields drops 'repo';
event metadata null-coalesces.

Hydration (context-hydration.ts): new repo-less branch (keyed on !task.repo) that
assembles from task_description + per-user memory, returning before the repo-bound
fetch path; assembleUserPrompt drops the Repository: line when repo is absent.

Memory (memory.ts + orchestrator.ts): loadMemoryContext/writeMinimalEpisode take
an actorNamespace (repo for coding, user:{user_id} for repo-less — ADR-014 addendum).

Preflight already honored requiresRepo (Phase-1 seam) — widened its repo param to
string|undefined. fanout-task-events skips the GitHub channel for a repo-less task.
loadBlueprintConfig skips the RepoTable lookup when repo is absent.

Agent (config.py): build_config loads the workflow up-front and requires
repo_url/github_token only when requires_repo; repo-less runs from
task_description. CLI shows '—' for a repo-less task.

Tests: repo-less acceptance (create-task-core, create-task, context-hydration,
CLI format) + repo-bound-missing-repo rejection; memory test log key repo →
actor_namespace. agent 932 / cdk 1825 / cli 296 green; ty + ruff + eslint clean.

deliver_artifact is still a NotImplementedError stub — a repo-less task can be
admitted and hydrated but not yet deliver an artifact (next slice).

* feat(agent): repo-less pipeline path via the workflow runner (aws-samples#248 Phase 3)

Second Phase-3 slice: a repo-less workflow now runs the agent loop in-container
with no clone / build / PR. When config.requires_repo is false, run_task delegates
to _run_repoless_task, which drives the workflow's steps (hydrate_context →
run_agent) through the workflow step runner — coding vs knowledge now differ by
the workflow's steps list, per ADR-014's end-state — and assembles a terminal
TaskResult with pr_url=None.

Scope boundary (deliberate): only hydrate_context + run_agent are driven here.
deliver_artifact is the workflow's terminal step but its handler is a stub until
the artifacts-bucket S3/IAM contract ships (next slice), so it is excluded via
only_kinds; delivery is deferred, the agent run is not. The coding path is left
byte-identical — its hard-won inline cancel-safety (skip ensure_pr on a cancelled
task) is NOT yet runner-driven, so routing the full coding step list through the
runner is a separate cancel-aware change, intentionally not done here.

- models.py: TaskConfig.requires_repo (defaults True; coding unaffected).
  config.py build_config sets it from the resolved workflow.
- pipeline.py: requires_repo branch before the repo-coupled segment;
  _run_repoless_task runs the runner + assembles/persists the terminal result.
  run_task repo_url now defaults to "" (repo-less callers omit it).
- prompts/default_agent.py: repo-less system prompt (no git/branch/PR
  placeholders); registered for default/agent-v1.
- prompt_builder.build_repoless_system_prompt + shared _render_memory_context.

Tests: repo-less pipeline run (no setup_repo, no PR, success, repo-less prompt);
repo-less prompt has no repo placeholders. agent 934 green; ty + ruff clean.

* fix(agent): address PR-review findings on Phase 2a/3 (aws-samples#248)

Findings from the plugin review (code-reviewer, silent-failure-hunter,
pr-test-analyzer, type-design-analyzer) on 4c34c8b..HEAD.

HIGH — repo-less task reported COMPLETED while delivering nothing
(silent-failure-hunter). _run_repoless_task skips deliver_artifact (stub until
the artifacts contract ships), but the workflow's terminal outcome (e.g.
`comment`) was never produced, yet status was success. Now: if the primary
terminal outcome is delivery-backed and delivery was skipped, the task is a loud
FAILED with an explicit error + delivery_deferred milestone naming the gap.

MEDIUM — config.py load-failure fallback bug + fail-open (code-reviewer,
silent-failure-hunter). The WorkflowValidationError branch compared workflow_id
against DEFAULT_WORKFLOW_ID (the *coding* default) for requires_repo, contradicting
its comment, and defaulted read_only=False (fail-open) for unknown ids. Now:
log the failure (was silent); fail CLOSED — read_only=True for any id not in
_KNOWN_WRITEABLE_WORKFLOW_IDS; requires_repo keyed off the real repo-less default
(REPO_LESS_DEFAULT_WORKFLOW_ID = default/agent-v1).

MEDIUM — TaskConfig missing cross-field invariant (type-design-analyzer). Added
_validate_requires_repo_has_repo (mirrors _validate_trace_requires_user_id):
requires_repo=True ⇒ non-empty repo_url, enforced at construction. repo_url and
github_token now default to "" so a repo-less TaskConfig is constructible; the
validator preserves the repo-bound invariant.

LOW/secondary — rule-8 now also checks deliver_artifact.target resolves in
DELIVERERS (type-design-analyzer), so an unknown target is caught universally,
not only when it collides with the primary outcome.

Test gaps closed (pr-test-analyzer): repo-OPTIONAL workflow given a valid repo
(admission + hydration); repo-less memory keyed user:{user_id} (asserted);
malformed-repo-on-repo-bound rejection; preflight repo-less short-circuit +
repo-bound-no-repo invariant; repo-less agent_result=None → FAILED; config
load-failure fail-closed; TaskConfig validator; rule-8 deliver target.

agent 941 / cdk 1831 / cli 296 green; ty + ruff + type-sync clean.

* feat(agent,api): deliver_artifact + repo-less memory — close criterion #4 (aws-samples#248 Phase 3)

Final Phase-3 slice: a repo-less knowledge task now runs end-to-end — admitted,
hydrated, agent loop, AND delivers its output. Closes acceptance criterion #4
(a non-coding task runs without a repo) for the s3/comment deliverers.

Infra (IAM + env):
- agent-session-role.ts: s3:PutObject grant scoped to
  artifacts/${aws:PrincipalTag/task_id}/* on the trace-artifacts bucket (mirrors
  the traces/ pattern; task_id-scoped per ADR-014 addendum). cdk-nag reason updated.
- agent.ts: ARTIFACTS_BUCKET_NAME runtime env (same bucket as traces).

Deliverers (agent/src/workflow/deliverers.py):
- DELIVERERS gains a deliver(target, ctx) dispatcher + DeliveryResult. The s3
  deliverer uploads the agent's result text to artifacts/{task_id}/result.md via
  the tenant-scoped S3 client (5 MiB cap); the comment deliverer records a
  delivered_comment milestone for the channel fanout; s3_and_comment does both.
- AgentResult.result_text captures ResultMessage.result (the knowledge-task
  deliverable); runner.py populates it on success.
- runner._handle_deliver_artifact: now implemented (was NotImplementedError),
  routes through deliver(); a delivery failure is a failed step (terminal FAILED),
  never a silent skip.

Pipeline (pipeline.py):
- _run_repoless_task drives the FULL step list (deliver_artifact included);
  replaces the delivery-deferred FAILED gate with the real runner verdict (agent
  succeeded but workflow failed ⇒ attributed FAILED). Sets TaskResult.artifact_uri.
- Repo-less episodic memory write keyed user:{user_id} (fail-open).

Memory (memory.py): write_task_episode repo→actor param; _validate_actor accepts
owner/repo OR user:{id} so the same write path serves coding + knowledge tasks.

Types: TaskResult/TaskRecord/TaskDetail + CLI mirror gain artifact_uri (mirrors
trace_s3_uri); CLI detail view shows an Artifact: line. check:types-sync green.

Tests: deliverer dispatch (s3 upload + key/body, comment milestone, s3_and_comment,
missing bucket, empty text, oversize); repo-less pipeline now succeeds + sets
artifact_uri=null for comment-only; session-role artifacts grant; deliver_artifact
dropped from the fail-loud stub list. agent 947 / cdk 1832 / cli 296 green; synth +
cdk-nag clean; ty + ruff + type-sync clean.

* feat(agent): make repo-less delivery retrievable + ship a knowledge workflow (aws-samples#248 Phase 3)

Three should-fix items closing the gap where "criterion #4" was met by machinery
but the default repo-less path delivered to a channel that isn't wired.

1. Stale docstring: _run_repoless_task said deliver_artifact was a deferred stub
   — it ships now. Updated to describe the real full-step-list run.

2. Default workflow delivers retrievably. The channel fan-out only knows
   slack/email/github; an api-origin repo-less task (the common default case) has
   no channel, so a `comment`-only deliverer surfaced nothing the user could get.
   default/agent-v1 now uses `target: s3_and_comment` (primary outcome `artifact`):
   the S3 upload to artifacts/{task_id}/ is always retrievable, the comment
   milestone is still rendered by the fan-out when a channel exists.

3. Ship a runnable knowledge workflow: agent/workflows/knowledge/web-research-v1.yaml
   — the concrete repo-less demonstration (research → S3 artifact, no repo).
   Built-in capabilities only (registry MCP/skills are Phase 4); CDK descriptor
   mirror + drift-guard + resolver tests updated.

Also fixes a latent bug the knowledge workflow surfaced: a repo-less id with no
registered prompt fell back to the CODING prompt (leaking unsubstitutable
{repo_url}/{branch_name}). get_system_prompt gains repo_less= so repo-less
workflows fall back to the repo-less default-agent prompt instead.

Tests: repo-less pipeline now asserts artifact_uri set (s3_and_comment);
repo-less prompt fallback; knowledge workflow validates/resolves. agent 949 /
cdk 1833 green; ty + ruff + type-sync clean; docs synced.

* fix(agent): address full-branch review — 2 repo-less blockers + cleanups (aws-samples#248)

Findings from the four-reviewer full-branch pass. Two were blockers that would
have shipped the repo-less feature broken despite green unit tests (the tests
didn't cross the server/task_state boundaries).

BLOCKER 1 — repo-less task rejected on the AgentCore backend. server.py
_validate_required_params required repo_url unconditionally, so /invocations
returned 400 for every repo-less task before the pipeline ran (ECS backend
dodged it by calling run_task directly). Now gates repo_url on the workflow's
requires_repo (resolved via load_workflow; fails SAFE — assume required — on a
load error). Test: repo-less payload accepted, repo-bound still requires repo.

BLOCKER 2 — artifact_uri computed but never persisted. task_state.write_terminal's
field allowlist had no artifact_uri branch, so the URI was dropped before
DynamoDB and TaskDetail.artifact_uri was always null (delivery not retrievable
despite the S3 object existing). Added the persist branch + tests.

Secondary:
- comment deliverer no longer overstates: _post_comment docstring + WORKFLOWS.md
  state plainly that delivered_comment is recorded for the event stream (visible
  in `bgagent watch`) but is NOT yet routed to an external channel (not in the
  fan-out ROUTABLE_MILESTONES). comment_posted=True only when actually recorded.
- test pins the config→engine read_only seam (runner.py PolicyEngine(read_only=)):
  dropping it now fails a test instead of silently disabling the Write/Edit deny.
- stale phase-boundary comments corrected (post_review docstring, runner module
  docstring + only_kinds + run_agent guard, pr-review/pr-iteration YAML headers)
  — they described Phase 2a/2b/3 as pending when those shipped on this branch.
- ADR-014: replaced the dangling pre-rebase commit hash 838c72e (unreachable
  after rebase) with a descriptive reference.

agent 954 / cdk 1833 green; ty + ruff + type-sync clean; docs synced.

* test(cli): cover artifact_uri render lines (aws-samples#248 Phase 3)

The Artifact: display lines added to formatTaskDetail / formatStatusSnapshot
introduced uncovered truthy branches that dropped CLI branch coverage below the
84% gate (full `mise run build` caught it; the per-package run had passed on the
prior content). Adds non-null + null cases for both renderers, mirroring the
existing trace_s3_uri tests. cli 300 passed; full build green.

* fix(agent): address PR review findings + green CI (aws-samples#248)

CI: ruff-format the 4 files that tripped the build self-mutation
(config.py, deliverers.py, test_pipeline.py, test_task_state.py) so
"Fail build on mutation" passes.

Review findings:
- prompts/__init__.py: get_system_prompt now logs WARN when an id has no
  registered prompt and falls back, restoring the signal the old
  build_system_prompt emitted (silent-failure-hunter).
- pipeline._run_repoless_task: chain wf_result.failed_step.error into the
  synthesized AgentResult so a run_agent failure on the repo-less path is
  diagnosable instead of a generic message (code-reviewer).
- server._validate_required_params: narrow the broad `except Exception`
  around load_workflow to WorkflowValidationError/ImportError so a genuine
  programming error surfaces loudly instead of being masked as "could not
  resolve requires_repo" (still fails safe — repo required).
- workflows.ts workflowIsReadOnly: document why the unknown-id default is
  `false` (conservative admission: needsWrite=!readOnly) and must NOT be
  "aligned" to the agent-side fail-closed read-only default.
- runner.py / pr-review-v1.yaml: comment fixes — artifact_key→artifact_uri,
  drop never-written review_posted, rule 4→6, post_review handler is
  registered-but-unimplemented not "no shipped handler" (comment-analyzer).

Tests (pr-test-analyzer gaps):
- test_memory.py: cover _validate_actor (user:{id} namespace + rejects) and
  a write_task_episode(actor="user:...") reaching create_event.
- test_pipeline.py: cover the repo-less delivery-gate-failure branch (agent
  succeeds but deliver_artifact fails → terminal FAILED naming the step).

Docs: ROADMAP.md "Task types" now reflects workflow-driven tasks + repo-less
knowledge workflows; Starlight mirror regenerated.

agent 962 passed, ruff+ty clean, cdk compile+workflows tests green.

* fix(agent): ship first-party workflow files in the image + sync user docs (aws-samples#248)

The Dockerfile copied agent/src, agent/policies, and contracts but never
agent/workflows, so the runtime loader (which resolves /app/workflows/<domain>/
<name>-vN.yaml) failed every workflow load with WorkflowValidationError. Add
COPY agent/workflows/ /app/workflows/ so the five shipped workflows + the JSON
schema land in the image. Verified in a local build: load_workflow(
'coding/new-task-v1') resolves to 1.0.0.

Also bring the user-facing guides in line with the workflow-driven model:
USER_GUIDE/QUICK_START now document workflow_ref, the --workflow flag, and
resolved_workflow responses (the retired task_type is shown only as a migration
mapping). Rename the "Task types" section to "Workflows", update the Starlight
sync map + sidebar slug, and regenerate the mirrors.

* fix(agent,api,cli): address PR-review findings on the workflow seam (aws-samples#248)

Resolve five verified findings from the aws-samples#248 review:

HIGH — thread agent_config.allowed_tools to the SDK. runner.py hardcoded the
full tool surface, so a workflow's declared allowed_tools never reached
ClaudeAgentOptions and the read_only:false read-leaning lanes (default/agent-v1,
web-research-v1) could still get Write/Bash. Add TaskConfig.allowed_tools
(populated from the resolved workflow), resolve it in run_agent via
_resolve_allowed_tools (drops Write/Edit when read_only), and add SDK-layer
tests.

Cedar missing-read_only fail-open — add the ADR-014-promised parity vector.
With context omitting read_only, both engines fail OPEN (Allow + base_permit);
safety rests on policy.py::_eval_tier re-raising on diagnostics.errors. Add
contracts/cedar-parity/read-only-missing-attribute.json (verified on cedarpy
AND cedar-wasm) plus test_policy.py tests pinning the re-raise + always-inject
discipline.

MEDIUM — repo-less artifact success gate. pipeline.py now fails the task when
the primary outcome is `artifact` but no artifact_uri was produced (matches the
WORKFLOWS.md "agent-success AND S3 key present" contract), not just when the
deliver step raised.

MEDIUM — rule 13 model allow-list admission. Add WORKFLOW_MODEL_ALLOWLIST +
disallowedWorkflowModel() and enforce it in create-task-core (unpermitted model
=> 400, no silent downgrade); descriptor sync-test asserts modelId matches the
YAML and stays on the allow-list.

LOW — CLI repo-less submission. --repo is no longer a hard requiredOption:
required unless --workflow selects a repo-less workflow (still required with
--pr/--review-pr). Lets bgagent reach knowledge/web-research-v1.

Rule 10 (one production version per lineage) remains intentionally unenforced
at the single-file layer — it is a registry/promotion-time property.

* fix(agent,api,cli): address PR-review aws-samples#296 resolution-ladder + deliverer findings (aws-samples#248)

Resolve the remaining findings from isadeks's review (findings #2/#4 were
already fixed in the prior commit):

#1 — bare submit silently stopped opening PRs. The server ladder resolves an
absent workflow_ref to the repo-less default/agent-v1 (by design), so a CLI
`submit --repo X --task Y` regressed from "opens a PR" to "S3 artifact". The CLI
now sends coding/new-task-v1 explicitly when a repo is present and no workflow/
pr flag is given, preserving the old new_task default.

#3 — repo-optional + repo disagreed across the boundary. The agent took the
repo-less path off config.requires_repo alone, while the orchestrator built a
repo-bound prompt off repo presence. pipeline.run_task now takes the repo-less
path only when requires_repo is false AND no repo was supplied; a repo-optional
workflow given a repo runs the repo-bound path (clone/build/PR), matching CDK
admission and the prompt.

#6 — resolveWorkflowRef silently discarded a @Version constraint. It now honors
the constraint (Phase 1: must match the single shipped version) and returns
null otherwise; create-task-core 400s with a distinct "version not available"
message via resolveWorkflowRefError, instead of quietly running the shipped
version.

#7 — deliver_artifact unset-target default disagreed between validator (lenient
full set) and runtime (s3). Introduce DEFAULT_DELIVER_TARGET as the single
source of truth; produced_outcomes(None) now models that exact default, so a
primary:comment workflow that omits target is correctly flagged by rule 11.

#10 — descriptor/YAML drift had no parity test. The sync test now asserts
required_inputs (allOf/oneOf) parity in addition to id/version/requires_repo/
read_only/model, and a new test cross-checks the agent-side
_KNOWN_WRITEABLE_WORKFLOW_IDS (the third hand-maintained copy) against the
writeable repo-bound descriptors so drift fails CI.

* fix(agent): close PR-review aws-samples#296 follow-ups — post-hook fail-soft, prompt, cap, parity (aws-samples#248)

#5 (High) — post-hook load_workflow had no fail-soft fallback, so a reload
failure AFTER run_agent mutated/committed the tree stranded the work as FAILED
with no PR. read_only now comes from config (already fallback-computed and the
verdict that drove Cedar); the reload — needed only for the ensure_pr strategy —
is wrapped in the same WorkflowValidationError fallback build_config uses,
defaulting to "create" so the PR still opens.

#8 — knowledge/web-research-v1 had no registered prompt and silently degraded to
the generic default-agent prompt. Add a research-specialized, repo-less prompt
(prompts/web_research.py) and register it.

#9 — the 5 MiB artifact cap was checked AFTER encoding to UTF-8 (a second full
copy). Reject on the character count up front (chars ≤ bytes in UTF-8) so the cap
actually bounds memory on the MicroVM before materializing the bytes.

#10 residual — add a parity test for the agent-side REPO_LESS_DEFAULT_WORKFLOW_ID
/ DEFAULT_WORKFLOW_ID constants: the repo-less default must match the CDK platform
default and be requires_repo:false in the YAML; the coding default must be
repo-bound. Closes the last hand-maintained-copy axis #10 named.

Perf — memoize load_workflow (@cache): files are image-baked and immutable per
process and Workflow is frozen, so the 3-4 loads per task now parse once.

Leaves the coding-lane decorative-`gate` item (run_workflow only drives run_agent
while verify_build/ensure_pr stay inline) as a tracked follow-up — it is a
larger half-migrated-runner refactor both reviewers flagged as non-blocking.

---------

Co-authored-by: bgagent <bgagent@noreply.github.com>
mayakost and others added 16 commits June 10, 2026 09:47
The 'Merge branch main' commit (c84cc66) left an invalid import block:
a missing comma after PR_WORKFLOW_IDS (syntax error) plus a stale
PR_TASK_TYPES import that main's aws-samples#248 removed from config.py. ruff
rejected the file, aborting the agentcore build.

Drop the orphaned PR_TASK_TYPES import and fix the comma.
test_prompts.py:_config built base from a homogeneous str literal, so ty
inferred dict[str, str] and rejected the spread into TaskConfig's
bool/int/list fields (17 invalid-argument-type errors). Annotate base as
dict[str, Any], matching the existing helper in test_runner.py. This
failure was previously masked by the lint syntax error that aborted the
build before typecheck ran.
Three more issues that were masked behind the earlier lint/typecheck
failures, all surfaced once the build progressed to its 'fail on
mutation' gate:

- ruff format reflowed two long boolean/string lines in jira_reactions.py
  and test_jira_reactions.py that were committed unformatted.
- USER_GUIDE.md still referenced the retired `pr_review` task_type on the
  intro 'For example' line (a aws-samples#248 merge leftover); the rest of the guide
  uses `coding/pr-review-v1`. Fixed the source and regenerated the
  Starlight mirror (using/Overview.md).
- Quick-start mirror was missing the Node.js prerequisite line present in
  the QUICK_START.md source; docs-sync adds it.

Full `mise run build` now completes with no working-tree mutation.
…ws-samples#295)

* feat(cdk): add Phase-0 deploy-then-verify integ tests (aws-samples#236)

Introduce automated deploy-then-verify integration testing via
@aws-cdk/integ-tests-alpha + integ-runner — the first synth→deploy→
assert→destroy coverage against real AWS (ADR-013 Tier 3 / Phase 0).

- cdk/test/integ/integ.task-api-smoke.ts: a trimmed TaskApiSmokeStack
  (TaskApi + task/events tables; omits orchestrator + AgentCore
  Runtime/Memory to avoid colliding with the live backgroundagent-dev
  stack and to keep the task at SUBMITTED). Asserts the create-and-
  persist happy path: Cognito adminCreateUser → adminSetUserPassword →
  initiateAuth → POST /tasks → GET /tasks/{id} (200, SUBMITTED) →
  DynamoDB getItem (user_id persisted). Forced teardown on success and
  failure.
- .github/workflows/integ.yml: workflow_dispatch + nightly schedule
  (never per-PR — it deploys billable resources to a shared account);
  OIDC creds, concurrency guard, pinned action SHAs, and an
  if: always() force-destroy safety net.
- cdk/mise.toml: mise //cdk:integ task.
- cdk/package.json: pin @aws-cdk/integ-runner 2.199.0 +
  @aws-cdk/integ-tests-alpha 2.257.0-alpha.0 (devDeps); exclude
  test/integ/ from jest test + coverage paths.
- .gitignore: ignore regenerated *.snapshot/ and cdk-integ.out.* dirs.
- docs(ROADMAP, ADR-013): record Phase 0 landed (+ Starlight mirrors).

* fix(integ): scrub account ID, randomize integ password, harden teardown

Address security-review findings on the Phase-0 integ test (aws-samples#236):

- Remove the hardcoded AWS account ID from three comments (integ.yml
  header, integ.task-api-smoke.ts constructor, cdk/mise.toml task desc).
  The runner deploys into whatever account the active credentials /
  secrets.AWS_ROLE_TO_ASSUME resolve to, so naming the account added no
  value and leaked it into source.
- Generate the throwaway Cognito password per-synth via randomBytes
  instead of a hardcoded literal, so no credential-shaped string lives in
  the tree. Still satisfies the default Cognito policy by construction.
- Fix the workflow teardown safety-net: `cdk destroy backgroundagent-integ`
  synthesizes src/main.ts, which does not contain the integ stack, so it
  was a no-op. Delete the stack by its literal CloudFormation name via the
  AWS CLI (idempotent) instead.
- Add a gitleaks rule to catch bare 12-digit AWS account IDs going forward,
  with allowlists for AWS-published placeholders and lockfiles/snapshots.

* fix(integ): gate dispatch to main, default region, drop dead snapshot flag

Address pre-merge review on the Phase-0 integ harness (aws-samples#236):

- Add `if: github.ref == 'refs/heads/main'` to the integ job. workflow_dispatch
  can target any branch and the job assumes the privileged integ role, so
  without this guard a feature-branch edit to test/integ/*.ts could be
  dispatched against that role. Complements the OIDC trust-policy restriction.
- Default aws-region to us-east-1 when vars.AWS_REGION is unset, so the
  credentials action never runs region-less.
- Drop --update-on-failed from the integ-runner invocation: .snapshot/ is
  gitignored, so there is no committed snapshot to diff against or update.
  --force still re-runs the deploy-then-verify unconditionally.

* fix(integ): run integ job in `integ` GitHub Environment

The job assumes AWS_ROLE_TO_ASSUME, which is an environment-scoped secret
(deploy.yml's diff/deploy jobs each declare an environment), so a job with
no environment resolves it to empty and configure-aws-credentials fails.
The GitHub role's OIDC trust also gates on `sub = repo:...:environment:<name>`,
so the integ workflow must run in a named environment whose name is added to
the trust policy.

Declare `environment: integ`. Account-side setup (separate, owner has IAM
access) creates the `integ` GitHub Environment, scopes the role/region
secrets to it, and adds `...:environment:integ` to the role's trust `sub`.

* feat(integ): drive integ smoke per-PR via workflow_run, admin-gated

Rework the integ workflow to mirror the deploy.yml model so a reviewer
sees a merge-blocking check on the PR instead of manually dispatching a
job. build.yml completes -> workflow_run resolves whether the diff
touches cdk/** or agent/** -> the `integ` environment's required
reviewer approves -> deploy/assert/destroy runs against the shared
account -> an `integ-smoke` commit status is posted back to the PR head.

Docs/cli-only PRs get an immediate green skip so the required check never
deadlocks. Fork PRs run behind the same approval gate (the approver
authorizes fork-authored test code to run with the privileged role).
Nightly schedule dropped; workflow_dispatch retained (main only).

Refs aws-samples#236.

* fix(integ): address review nits — error handling, test quality, fork-PR safety

- integ.yml: drop unused actions:read; add EXIT-trap error status, explicit
  /files failure handling (no false-green skip), and a safe-to-test fork label
  gate in resolve; guard report job on resolve success; region-pin and
  fail-loud the teardown wait.
- .gitleaks.toml: report aws-account-id capture group 1 (bare 12-digit ID).
- integ smoke test: add unauthenticated POST assertion (expect 401); clarify
  the user_id assertion comment (identity binding proven transitively by the
  authenticated GET).
- ADR-013: document residual-risk acceptance for fork-PR integ execution.

---------

Co-authored-by: bgagent <bgagent@noreply.github.com>
Co-authored-by: Sphia Sadek <isadeks@gmail.com>
Co-authored-by: Alain Krok <alkrok@amazon.com>
…les#301) (aws-samples#307)

The coding lane's inline post-hook gating now consults each declared
verify_build / verify_lint step's `gate` through the runner's gate_status
(made public from _gate_status) — one gate implementation for both lanes.
A coding workflow declaring `gate: strict` now fails the task on a build
failure; `informational` never gates. The three shipped coding workflows
keep their exact pre-change effective gating, locked by a parity test
matrix (inline == runner == legacy verdicts). The inline ordering is
preserved: ensure_pr still runs after a gating verify failure so the
agent's work surfaces as a reviewable PR.

Refs aws-samples#301

Co-authored-by: bgagent <bgagent@noreply.github.com>
Co-authored-by: Sphia Sadek <isadeks@gmail.com>
…n refresh, ADR/docs

Blocking (krokoko):
- Multi-tenant signature binding: webhook receiver flags stack-wide
  verification to the processor, which then ignores the body cloudId and
  binds to the sole active tenant (drops when ambiguous). CLI no longer
  mirrors the stack-wide secret into new per-tenant bundles; stack-wide is
  seeded once from the first tenant. Missing-timestamp replay skip is logged.
- Renumber ADR-014 -> ADR-015 (collides with workflow-driven-tasks); rewrite
  to the implemented REST-outbound reality (status accepted), correct dedup
  key, binding + refresh-ownership sections. Reconcile JIRA_SETUP_GUIDE,
  USER_GUIDE ("six ways"), ROADMAP, channel_mcp.py (placeholder + in-band
  log), jira-webhook-processor.ts, jira-integration.ts, agent.ts.
- Fix non-existent CLI command in feedback: onboard-project -> map
  <cloud-id> <project-key> --repo (processor + CLI next-steps hint).
- Implement notifyJiraOnConcurrencyCap (Linear parity) so the orchestrator
  IAM grant is used and Jira users aren't silently dropped on the cap.

Significant (ayushtr):
- Agent never refreshes the Jira token (Atlassian rotates refresh_tokens;
  agent has GetSecretValue only). Use the Lambda-written token verbatim and
  fail closed when expiring; Lambda path owns all refreshes.
- ADF media nodes (external images) now render to markdown so attachment
  extraction works; ADF->markdown computed once and reused.

Minor: one-sided clock-skew-tolerant timestamp freshness, base64-body guard,
replay window 24h->1h, resolveSoleTenantCloudId Scan comment.

Tests: agent no-refresh/fail-closed suite, multi-tenant binding tests,
base64/missing-timestamp/stack-wide-flag webhook tests, ADF media-node test,
notifyJiraOnConcurrencyCap parity suite. Docs mirrors regenerated.

Relates to aws-samples#288
…ostic) (aws-samples#241)

* feat(screenshot): preview-deploy screenshot pipeline (no stack wiring yet)

Lambda + AgentCore Browser plumbing for capturing screenshots of
preview deployments. Provider-agnostic — listens for GitHub
deployment_status events from any source (Vercel, Amplify Hosting,
Netlify, GitHub Actions custom CD).

This commit lands the handler / construct code only. Stack wiring
follows in the next commit.

* feat(screenshot): GitHubScreenshotIntegration construct + stack wiring

- New `GitHubScreenshotIntegration` construct (mirrors `LinearIntegration`):
  bundles the screenshot bucket, dedup table, signing-secret placeholder,
  receiver Lambda, processor Lambda, and the API Gateway route. cdk-nag
  suppressions added inline (HMAC auth instead of Cognito; AgentCore
  Browser sessions have no per-resource ARN; Secrets Manager rotation
  is owned by GitHub).

- Wired into `agent.ts` after the LinearIntegration block. Reuses the
  existing `githubTokenSecret` (the processor uses ABCA's main GitHub
  token to look up which PR a deploy SHA belongs to and post the
  screenshot comment — no new credential).

- Three new stack outputs: `GitHubWebhookUrl`, `GitHubWebhookSecretArn`,
  `ScreenshotBucketName`.

- Bumped agent.test.ts table count from 13 to 14 to account for the
  new dedup table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): suppress AwsSolutions-S2 on the public-read screenshot bucket

cdk-nag's S2 fires on any bucket that has `blockPublicPolicy: false`
even when the policy is intentionally permissive. Add the suppression
with the same rationale as S1/S5 — public reads are required by
GitHub Markdown renderers and Linear `imageUploadFromUrl`, and the
read grant is prefix-scoped to `screenshots/*`.

Caught when the first deploy attempt aborted at synth-time on the new
GitHubScreenshotIntegration construct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): private S3 bucket + CloudFront distribution

The first deploy attempt failed at CFN-execute time on the bucket
policy:

  s3:PutBucketPolicy ... because public policies are prevented by
  the BlockPublicPolicy setting in S3 Block Public Access.

Account-level Block Public Access is on for this AWS account, which
overrides per-bucket BPA settings. Disabling it would change the
security posture of the whole account, so route around the constraint
with the AWS-recommended pattern: private S3 + CloudFront with Origin
Access Control.

Changes:
- `ScreenshotBucket` is now `BLOCK_ALL` BPA, no public bucket policy.
  Adds a `cloudfront.Distribution` whose origin is the bucket via
  `S3BucketOrigin.withOriginAccessControl`. The distribution policy is
  scoped to the CloudFront service principal only, so account-level
  BPA accepts it.
- Processor reads `SCREENSHOT_PUBLIC_HOST` (the CloudFront domain)
  instead of building an S3 URL. PR comments now embed
  `https://<dist>.cloudfront.net/screenshots/...` URLs.
- New stack output `ScreenshotCloudFrontDomain`.
- Bucket-level S2/S5 suppressions removed (no longer applicable —
  bucket is private). Distribution gets CFR1/CFR2/CFR3/CFR4/CFR7
  suppressions with rationales.

Heads up on deploy time: CloudFront distributions take 5-15 min to
provision on first create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(waf): exempt /v1/github/webhook from CRS like /v1/linear/webhook

The CommonRuleSet was 403'ing GitHub deployment_status webhooks before
the request reached our Lambda — the deployment payload contains
absolute Vercel preview URLs in the body, which trips GenericRFI_BODY.

Mirror the Linear webhook exemption: the GitHub webhook path is
HMAC-verified in the Lambda, parsed as strict JSON, never
interpolated into SQL/HTML, and rate-limited by the priority-3 rule.
CRS still applies to every other route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): read environment_url from deployment_status, not deployment

GitHub's `deployment_status` webhook puts the deployed URL on the
*status* object, not the deployment itself. The deployment object is
immutable per (sha, environment); the status changes through the
deploy lifecycle (`pending` → `success`) and carries the URL only
once the deploy finishes.

Symptom: receiver kept short-circuiting `success` events from Vercel
with `{ok: true, skipped_no_url: true}` because we read the wrong
field. Verified by inspecting the webhook delivery payload via
`gh api .../deliveries/<id> --jq .request.payload.deployment_status` —
URL was there all along.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agentcore-browser): use ws package for SigV4-signed WebSocket handshake

Node 24's global WebSocket (from undici) does NOT support arbitrary
HTTP headers on the upgrade request — passing them as the second arg
gets silently ignored. AgentCore Browser's WSS handshake requires
SigV4-signed Authorization + X-Amz-* headers, so the connection was
opening but then getting rejected, which surfaced as an empty
`error` event ("AgentCore Browser WebSocket error: ").

Switch to the `ws` package which natively supports `options.headers`.
Also add an `unexpected-response` handler so HTTP-level handshake
failures (403, 400) surface with status codes instead of empty errors.

Smoke verified locally — the ws-based path opens cleanly against
example.com and Vercel preview URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agentcore-browser): SigV4-presign WSS URL instead of signing headers

Lambda runtime returned a 403 on the WSS upgrade despite well-formed
SigV4 headers — `ws` rewrites the Host header during the upgrade
GET, which invalidates the canonical-request signature we computed
against the original Host. This works locally because Node's tooling
on macOS keeps the original Host through the handshake, but the
Lambda runtime's TLS stack normalizes differently.

Switch to query-parameter SigV4 (presigned URL): SignatureV4.presign
returns a wss://...?X-Amz-Algorithm=...&X-Amz-Signature=... URL where
the auth lives in the URL itself, so any Host-header rewriting
downstream doesn't break the signature.

Smoke verified locally — presigned URL connects cleanly to AgentCore
Browser and the screenshot pipeline runs end-to-end (6.3s, valid
PNG, captures example.com correctly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(iam): grant bedrock-agentcore:* to the screenshot processor

The minimal IAM I shipped earlier (`StartBrowserSession`,
`StopBrowserSession`, `GetBrowserSession`, `UpdateBrowserStream`)
wasn't enough — the WSS automation-stream connect requires an
additional `ConnectBrowserAutomationStream`-flavored action that
isn't in the public CLI command list. Lambda invocations were
opening sessions cleanly but 403'ing on the WSS upgrade.

Widen to `bedrock-agentcore:*` to unblock the e2e flow. Followup:
scope back down to the specific connect action once it's documented
or surfaced via CloudTrail decoded-message-on-deny.

Smoke verified: PR #1 on isadeks/vercel-abca-linear now receives a
screenshot comment within ~7s of the deployment_status webhook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(screenshot): also post screenshot comment to linked Linear issue

Extends the screenshot processor to find a Linear issue via the PR's
title/body and post the same image comment there.

Approach (no GSI write-back needed):
- Regex-extract Linear identifier (e.g. `ABCA-42`) from PR title/body.
  These are present whether the agent put them there
  (`task_description` carries the identifier) or Linear's own GitHub
  integration auto-injected the back-reference on PR open.
- Scan `LinearWorkspaceRegistryTable` for `status=active` workspaces.
  Per-workspace, query Linear's `issueVcsBranchSearch` (which accepts
  the human-readable identifier) and accept the first exact-match
  hit.
- Post the markdown image comment via the existing `postIssueComment`
  helper from Phase 2.0b.

The Linear post is best-effort — if the registry table isn't wired,
the identifier doesn't extract, or the lookup misses, the GitHub PR
comment still lands. New env var `LINEAR_WORKSPACE_REGISTRY_TABLE_NAME`
is optional on the processor; the construct only sets it when the
prop is provided.

CDK: `GitHubScreenshotIntegrationProps` gains an optional
`linearWorkspaceRegistryTable`. When provided, the processor's IAM
grows: ReadData on the registry, GetSecretValue+PutSecretValue on
`bgagent-linear-oauth-*`. `agent.ts` wires
`linearIntegration.workspaceRegistryTable` into the screenshot
construct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): retry PR lookup to handle deploy-before-PR race

Some providers (Vercel, Netlify) post deployment_status faster than
the agent can run `gh pr create`. Retry the GitHub PR-lookup with
backoff so the screenshot finds the open PR rather than dropping the
event when the timing is reversed.

* fix(linear): silent label gate + default to 'abca' to stop unlabeled-issue comment spam

Move the trigger-label check ahead of every user-facing comment path in
the Linear webhook processor, and switch the default trigger label from
'bgagent' to 'abca'. An unlabeled issue is now a true no-op: no comment,
no reaction, no createTaskCore, no DDB writes — regardless of whether
the project is onboarded.

Why: workspace webhooks fire workspace-wide. A single un-onboarded team
in the same Linear workspace produced 47 identical "❌ project isn't
onboarded" comments on GRO-783 in 5 minutes because every Issue event
(create/update/label-change) hit the not-onboarded gate before the
label gate. With the gate order flipped, only issues that explicitly
opt in via the trigger label can ever generate user-facing feedback.

Per-project label_filter override is still respected — the project
mapping lookup now happens once, before the label gate, instead of after.

Tests: two new regression tests pin the spam scenario (unlabeled issue
in a non-onboarded project, and unlabeled issue with no projectId) to
zero side effects. Full CDK suite (89 suites / 1572 tests) passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(screenshots): add the screenshot pipeline guide

Adds the operator walkthrough for wiring up the AgentCore-Browser
preview-deploy screenshot pipeline.

* feat(github): bgagent github webhook-info + set-webhook-secret

Mirrors the Linear webhook-info pattern so docs and onboarding don't
have to embed stack-specific URLs or copy-paste aws CLI invocations.

Two subcommands:
- `webhook-info` — read-only. Reads GitHubWebhookUrl + GitHubWebhookSecretArn
  from the CFN stack outputs and prints values to paste into a GitHub
  repo's webhook config (Settings → Webhooks → Add webhook). Includes
  the event-type ('Deployment statuses') and content-type guidance
  that operators consistently miss.
- `set-webhook-secret` — interactive PutSecretValue against the stack
  output ARN. Replaces the cargo-cult `aws secretsmanager put-secret-
  value` operators were copy-pasting from the screenshot setup notes.
  Warns before overwriting an existing real secret (heuristic: a CDK-
  seeded JSON placeholder starts with `{`; a real GitHub secret won't).

No CDK changes — both stack outputs were already there. Pure CLI add.

* docs/code(screenshots): de-Vercel-ize the screenshot pipeline

The pipeline was always provider-agnostic — it listens for GitHub
deployment_status events, which Vercel, AWS Amplify, Netlify, and
any GitHub-Actions-driven CD pipeline all post. Code comments,
inline strings, and the setup guide referenced Vercel as if it were
the only supported path; this commit aligns the surfacing with what
the code actually does.

Code:
- Linear comment body: "after the Vercel preview deploy finished"
  → "after the deploy finished" (the GitHub PR comment already
  said this; just the Linear path was inconsistent)
- Webhook receiver doc-comment + envelope interface comment: drop
  Vercel-only language; explain that the `environment` filter
  (`SCREENSHOT_TARGET_ENVIRONMENT` env var) is configurable per-
  provider, with a table of common values
- Processor PR-race comment: explain that the gap is also seen on
  Netlify/Amplify, not unique to Vercel
- AgentCore Browser comment: drop Vercel-specific phrasing on
  "what we don't try to be clever about"
- GitHubScreenshotIntegration construct prop docstring: explain
  the per-provider env-name conventions

Docs:
- Rename VERCEL_SETUP_GUIDE.md → DEPLOY_PREVIEW_SCREENSHOTS_GUIDE.md
- Lead with a "works with any provider that posts deployment_status"
  table (Vercel / Amplify / Netlify / GitHub Actions custom CD,
  with "out-of-the-box?" yes/no per provider)
- Keep Vercel as the worked example since it's what we smoke-tested,
  but add a "skip Steps 1-2" callout for non-Vercel providers
- New "Configuring for non-Vercel providers" section with the
  SCREENSHOT_TARGET_ENVIRONMENT override pointer
- Replace 4a/4b's CFN-output spelunking with `bgagent github
  webhook-info` + `bgagent github set-webhook-secret` (commands
  shipped in 1c1b618)
- Troubleshooting: mention that 401 "Invalid signature" is the
  set-webhook-secret-mismatch case
- Sync registration: register as DEPLOY_PREVIEW_SCREENSHOTS_GUIDE
  in sync-starlight.mjs route map + the explicit mirror call;
  added to astro.config.mjs sidebar after the PAK runbook

No CDK structural changes — the construct prop, env-var, and code
behaviour were already provider-agnostic. Pure surfacing fix.

* docs(screenshots): drop redundant Step 3 + condescending hardening preamble

Step 3 (repo onboarding + Linear project mapping) duplicated work
the Prerequisites section already establishes ('Linear OAuth installed
for at least one workspace'). If the user followed the Linear setup
guide, both are done. If they didn't, Step 4's smoke test fails fast
and the troubleshooting routes them back. Net: 30 lines of doc gone,
no information lost.

Renumbered Step 4 → 3 and Step 5 → 4 (and the 4a/b/c → 3a/b/c
sub-steps).

Also dropped the 'demo configuration optimizes for "look, it works"
rather than security posture' framing on the production-hardening
section. The list of followups stands on its own; the framing reads
as condescending toward someone reaching the bottom of the guide.

* docs(screenshots): drop 'followup' framing — describe gaps as current state

Public docs that say 'followup' read as commitments to do that work.
Reframe gaps as current limitations with neutral language:
- 'Production hardening (followups)' → 'Production hardening
  considerations'; bullets describe what to think about, not what
  ABCA promises to ship
- Netlify table row: 'followup to support pattern matching' →
  '⚠ workable today only by picking one specific PR's
  environment string; broader pattern matching isn't shipped'
- Vercel auth callout: 'tracked as a followup' → 'currently not
  implemented'
- Non-Vercel providers table: drop 'followup aws-samples#96 covers prefix
  routing' reference (issue numbers don't belong in user-facing
  docs)

Net: same information, no implicit roadmap commitments.

* docs(screenshots): de-Linear-ize — Linear is opt-in, not required

The screenshot pipeline only needs GitHub. Linear-side posting was
phrased as a hard requirement throughout the guide because the demo
flow happens to use Linear, but a non-Linear team gets a perfectly
useful integration: screenshots land on GitHub PRs, the Linear
lookup silently no-ops.

Reframings:
- Lead-in: 'on both the open GitHub PR AND the linked Linear issue'
  → 'on the open GitHub PR. If you also have Linear configured,
  the same screenshot is posted to the linked Linear issue as a
  bonus.' Plus a note on the gating (LinearWorkspaceRegistryTable
  having active rows is what flips the Linear path on).
- 'How it works': step 4 (Linear post) marked optional with the
  silent-skip behaviour spelled out
- Architecture comment: 'GitHub PR comment + Linear issue comment'
  → '... (+ Linear issue comment if linked)'
- Prerequisites: Linear OAuth marked optional with rationale
- Smoke test: rewritten as PR-driven by default ('open any PR on
  the configured repo'), with Linear-driven path as a follow-on
  paragraph ('If you also have Linear configured...')
- Troubleshooting: 'Linear is best-effort' → 'opt-in and best-
  effort', explicit note that skipping is normal without Linear

* feat(screenshot): hide URL behind 'preview link' label in comments

GitHub PR comment now reads 'From [preview link](url)' and Linear
comment reads '[Preview link](url)' instead of pasting the bare URL.
Cleaner visual when the same comment is posted on both surfaces.

* docs(screenshots): add USER_GUIDE / COST_MODEL / ROADMAP coverage

Closes the doc gaps from the screenshot feature followup list:

- USER_GUIDE.md: new 'Preview-deploy screenshots (optional)' subsection
  under Notifications, points at DEPLOY_PREVIEW_SCREENSHOTS_GUIDE.md.
- COST_MODEL.md: 'Optional: deploy-preview screenshots' table covering
  AgentCore Browser session, Lambda processor, S3, CloudFront line items
  (~$0.01 per screenshot, dominated by Browser session time).
- ROADMAP.md: marks the feature shipped under Notification plane with a
  one-line description of the trigger model and post-deploy latency.

Mirrors regenerated via docs/scripts/sync-starlight.mjs.

* docs(linear): clarify teammate-onboarding handshake

The 'Inviting teammates' section was missing the prerequisite that the
teammate needs their own ABCA account (Cognito user + configured CLI)
before they can redeem a Linear invite code. New flow walks through:

  Admin: invite-user (Cognito) → invite-user <slug> (Linear)
  Teammate: configure --from-bundle → login → linear link <code>

with cross-references to USER_GUIDE.md's 'Joining an existing
deployment' for the Cognito-side details. Also corrects the stale
'auto-links the person running the wizard' claim — setup now offers
an inline picker (opt-in by admin), not an automatic mapping.

* fix(github-cli): de-Vercel-ize webhook-info / set-webhook-secret strings

Last batch of stale 'Vercel' framing in CLI command output, missed in
the original de-Vercel-ize sweep. Provider-agnostic now: webhook-info
header reads 'preview-deploy screenshot pipeline', the closing note
lists Vercel/Amplify/Netlify/GitHub Actions as example providers, and
the smoke-test instruction says 'push to a PR-attached branch' rather
than 'trigger a Vercel preview deploy'.

No behaviour change; pure copy.

* fix(github-cli): replace template literal with single quotes (eslint mutation)

The local build's eslint --fix step rewrote a no-interpolation
template literal to single quotes; CI's 'Fail build on mutation'
guard caught that the mutation wasn't committed. Apply the fix.

* fix(screenshot): krokoko PR-241 review — scope IAM + cosmetic Vercel mention

Closes aws-samples#94 (the existing 'scope IAM down from bedrock-agentcore:*'
followup task).

Addresses krokoko's PR aws-samples#241 review:

1. (BLOCKING per review #1) IAM action wildcard — narrow
   bedrock-agentcore:* to the three calls the screenshot processor
   actually makes:
   - StartBrowserSession (control plane, public CLI command)
   - StopBrowserSession (control plane, public CLI command)
   - ConnectBrowserAutomationStream (SigV4-presigned WSS dial; not
     in the public CLI list but verified live against the deployed
     dev stack — IAM accepts the action name)

   Resource wildcard remains because AgentCore Browser sessions are
   ephemeral with no stable ARN; the IAM5 suppression on the construct
   already documents that.

   Previous behaviour granted every AgentCore action surface (memory,
   runtime, gateway, identity, code-interpreter) the screenshot path
   doesn't use. Tightening to the call set leaves a precise audit
   surface; if a future API change needs another action, IAM denies
   with the action name in CloudTrail and we add it explicitly.

2. (NIT per review #7) Stale 'Vercel' wording on ScreenshotBucketName
   CfnOutput description, plus an adjacent comment in agent.ts that
   said 'Vercel-style preview deploys'. Both replaced with
   provider-agnostic phrasing — the pipeline listens for any provider
   that posts deployment_status (Vercel, Amplify, Netlify, GitHub
   Actions custom CD).

No behavioural change in either fix.

* fix(screenshot): krokoko PR-241 review — WS leak + commit-pulls guard

Two non-blocking review nits:

- agentcore-browser.ts: failure paths in the WS open-promise (error,
  unexpected-response, timeout) now call ws.terminate() so a hung
  handshake doesn't leak the underlying TCP connection per failed
  attempt.
- github-webhook-processor.ts: findPullRequestForSha now guards the
  GitHub commit-pulls JSON parse and the Array.isArray contract. A
  transient 2xx HTML body or malformed payload would have crashed the
  un-DLQ'd processor; treat non-array as no-PR.

* docs(screenshot): krokoko PR-241 review — reconcile WAF rationale

CloudWatch BlockedRequests metric for TaskApiWebAcl (us-east-1) shows
SizeRestrictions_BODY fired 2x on 2026-05-21, matching commit 36e8d14's
smoke-test window 1:1. GenericRFI_BODY has never fired on this WebACL —
the original commit message + scope-down comment + screenshots guide
blamed RFI when the actual blocker was the 8 KB body-size limit (the
deployment_status payload carries workflow run history + deploy URLs +
deployment metadata).

The code is correct as written (only excludes SizeRestrictions_BODY);
only the rationale was wrong. Updated the inline comment in
task-api.ts and the WAF-exemption section of the screenshots guide;
the guide wording also overstated the scope ('excluded from the
CommonRuleSet') when only one CRS rule is exempted, so reworded to
make clear LFI/RFI/XSS/SQLi still evaluate. Starlight mirror re-synced.

* fix(linear): revert DEFAULT_LABEL_FILTER to 'bgagent'; scope PR-241 to screenshot pipeline

Per krokoko PR-241 review item #2: the bgagent->abca rename is unrelated
to the screenshot pipeline and was bundled into this PR by accident.
Issue aws-samples#285 owns landing the rename in its own PR with all four sites
(processor / CLI / setup guide / mapping doc) aligned in one commit.

Restores DEFAULT_LABEL_FILTER to 'bgagent' so PR aws-samples#241 only carries the
screenshot pipeline. Test fixture in linear-webhook-processor.test.ts
updated from 'lbl-abca'/'abca' to 'lbl-bgagent'/'bgagent' to match the
restored default; behavior under test is unchanged.

* fix(screenshot): theagenticguy PR-241 review — blockers + nits + tests

Closes the principal-architect review on PR aws-samples#241.

B1. Shared wall-clock deadline.
github-webhook-processor.ts now threads one TOTAL_BUDGET_MS=110s
deadline across findPullRequestForShaWithRetry + captureScreenshot +
S3 PUT + comment POST. PR lookup is capped so the screenshot half is
guaranteed at least MIN_CAPTURE_BUDGET_MS=15s; if PR lookup eats the
budget we fail fast with budget_exhausted instead of starting an
AgentCore session that's already doomed. Lambda timeout stays 120s;
the 10s headroom covers SDK retries + shutdown grace. Construct
comment updated to reflect the actual math.

B2. Empty-secret HMAC fail-open closed.
getGitHubWebhookSecret now returns null when SecretString is empty or
whitespace-only (logs error, drops cache entry). verifyGitHubSignature
adds defense-in-depth early-return when secret is empty. New
table-driven test covers the exact attacker shape: HMAC('', body)
signature against an empty stored secret.

B3. Stale public-read comments reworded.
github-screenshot-integration.ts topology JSDoc + field doc + inline
all said 'Public-read screenshot S3 bucket'; the bucket has been
BlockPublicAccess.BLOCK_ALL the whole time. Reworded to 'Private
bucket; served via CloudFront OAC.'

Non-blocking nits.
- SSRF allowlist on environment_url (https-only, no literal-IP /
  localhost / link-local / loopback) — new shared module
  src/handlers/shared/screenshot-url.ts.
- High-entropy 64-bit suffix on screenshot S3 keys
  (screenshots/<owner>_<repo>/<sha>-<id>-<suffix>.png) so the public
  CloudFront URL is unguessable from the public PR.
- Single GitHubDeploymentStatusPayload + validateDeploymentStatusPayload
  hoisted to src/handlers/shared/github-deployment-status.ts; receiver
  and processor share one validation contract (closes the gap where
  the receiver admitted payloads missing deployment.sha that the
  processor would drop).
- AbortController on the GitHub commit-pulls fetch (5s per-attempt
  cap; caller still owns the wall-clock).
- Replaced delays.indexOf-based retry log with an indexed loop
  (broken if delays repeats).
- replaceAll('/', '_') for repo slug.
- Scoped nextCdpId to runCdpScreenshot (was module-global).
- Stale 'Page.frameStoppedLoading' docstring fixed (code waits on
  Page.loadEventFired).
- IAM PutSecretValue intent comment added (Linear refresh-token
  rotation; not a typo).
- Type-narrowed CDP result accessors instead of `as` casts in
  agentcore-browser.ts.
- Promoted warn → error with tagged event_id on screenshot.* paths
  (pr_lookup_exhausted, capture_failed, s3_put_failed,
  pr_comment_post_failed) so CloudWatch metric filters / alarms can
  fire on what was previously invisible.

IAM scope rationale (with Service Authorization Reference cite).
The reference confirms ConnectBrowserAutomationStream is a real
published IAM action under bedrock-agentcore. Scope is narrowed to
the three actions the handler calls; resource is '*' because the
two Connect*Stream actions declare no resource types or condition
keys (must be granted on Resource:'*'), and Browser sessions are
ephemeral anyway. Source link added in both the construct comment
and the screenshots guide.

Doc fixes.
- DEPLOY_PREVIEW_SCREENSHOTS_GUIDE.md hardening section: was 'IAM
  grants bedrock-agentcore:*' (stale); now describes the scope-down
  with the auth-reference citation, plus SSRF and screenshot-URL
  enumerability mitigations.
- step 3c reference from '4b' → '3b' (the secret is generated in
  3b, not 4b which doesn't exist).

Tests (4 new files, 58 tests).
- github-webhook-verify.test.ts: table-driven verifier including
  the exact empty-secret attack shape that B2 fixes.
- screenshot-url.test.ts: buildScreenshotKey shape + entropy +
  replaceAll; isAllowedScreenshotUrl with public hosts, schemes,
  IPv4/IPv6 literals, localhost, RFC1918, IMDS.
- linear-issue-lookup.test.ts: extractLinearIdentifier including
  the back-to-back-call test that pins the g-flag lastIndex reset.
- screenshot-bucket.test.ts: synth-time assertion that
  BlockPublicAccess is all-true so a future 'simplify' can't drop
  public-access protection.

Full handler suite still green: 1387/1387 across 70 suites.

* fix(screenshot): preserve env_url skip-path; update tests for budget+key suffix

After cherry-picking the theagenticguy review fixes onto linear-vercel:

- github-webhook.ts: validateDeploymentStatusPayload was 400-rejecting
  payloads missing environment_url, but GitHub fires success events
  without the URL during intermediate states. Restore the explicit
  200-skip on env_url BEFORE validate so we don't make GitHub retry.

- Test fixture updates for:
  - captureScreenshot now called with { timeoutMs } (B1 budget)
  - S3 key now ends with -<16hex>.png (high-entropy suffix nit)

* fix(screenshot): encode markdown URL + sync stale key-layout comments (aws-samples#240)

krokoko PR aws-samples#241 round-3 review:

- Finding 1 (security): percent-encode `(`/`)` in the payload-derived
  `environment_url` before interpolating it into the PR/Linear comment
  markdown. A clean-hostname URL like
  `https://preview.vercel.app/x)](https://evil/a.png)` passes
  `isAllowedScreenshotUrl` yet breaks out of the `](…)` link and injects
  content into a comment posted under ABCA's token (reachable in fork-PR
  configs). New `encodeMarkdownUrl` helper + unit tests.

- Finding 3 (cosmetic): screenshot-bucket.ts and the deploy guide still
  described the old `screenshots/<repo>/<sha>.png` layout; updated both to
  the actual `screenshots/<owner>_<repo>/<sha>-<deploymentId>-<16hex>.png`.
  Removed the unused `SCREENSHOT_KEY_PREFIX` export.

Findings 2 (IPv6 unique-local) and 4 (validateDeploymentStatusPayload
coverage) folded into aws-samples#286 and aws-samples#275 respectively per the reviewer's
routing.

* fix(screenshot): reject all IPv6 literals + cover the deployment-status validator (aws-samples#240)

krokoko PR aws-samples#241 round-3 findings 2 and 4, brought onto aws-samples#240 because the
code they touch (screenshot-url.ts, the shared github-deployment-status
validator) lives only on this branch — aws-samples#275 predates the aws-samples#240 refactor
that extracted them, and aws-samples#286 is an issue with no code branch.

- Finding 2 (security): isAllowedScreenshotUrl enumerated IPv6 ranges
  (::1, fe80:, ::ffff:) and missed unique-local fc00::/7 (e.g.
  [fc00::1]) and NAT64. Replace with a blanket reject of any IPv6
  literal — a bracketed host or a `:` in the host — since preview URLs
  are always DNS names. Also documents that the WHATWG parser
  normalizes integer-form IPv4 (2130706433, 0x7f000001) to dotted-quad,
  so the existing regex catches those too. New table-driven tests for
  unique-local, NAT64, IPv4-mapped, and integer IPv4; screenshot-url.ts
  now at 100% branch coverage (closes the old ::ffff: gap).

- Finding 4 (test gap): validateDeploymentStatusPayload had zero
  coverage. New github-deployment-status.test.ts — happy path, each
  missing/empty required field, absent nested objects, and the id:0
  type-vs-truthiness edge. Validator at 100%.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: bgagent <bgagent@noreply.github.com>
Co-authored-by: Alain Krok <alkrok@amazon.com>
The 'Merge branch main' commit (0f47343) dropped the closing ');' on the
Jira mirrorMarkdownFile() call where it interleaved with main's new
'Deploy preview screenshots' mirror block, producing a SyntaxError that
broke the //docs:sync build step (and thus the whole build job).
…rkflows (aws-samples#309)

Three automation workflows modeled on awslabs/generative-ai-cdk-constructs:
auto-approve approves PRs labeled 'auto-approve'; upgrade-main runs a new
root `mise run upgrade` task daily (yarn workspaces + agent uv lockfile,
within declared ranges so the Cedar parity exact pins are never rewritten)
and opens an auto-approve-labeled PR; monthly-repo-metrics files monthly
issue/PR metric reports via github/issue-metrics.

Refs aws-samples#308

Co-authored-by: bgagent <bgagent@noreply.github.com>
The 'Merge branch main' (0f47343) dropped the closing '});' on the
JiraWorkspaceRegistryTableName CfnOutput where main's new
GitHubScreenshotIntegration block was spliced in, cascading into ~40
TS1005 errors and breaking //cdk:compile.
…ws-samples#258) (aws-samples#310)

* feat(ci): no-magic-numbers (eslint) + PLR2004 (ruff) with allowlist (aws-samples#258)

Inline magic numbers are a classic AI-generated-code smell (AI007):
locally plausible, globally inconsistent. Add lint gates that steer
hard-coded values into named constants — or contracts/constants.json
when they must agree across languages:

- cli: @typescript-eslint/no-magic-numbers as blocking 'error';
  baseline fixed (20 violations promoted to named constants).
- cdk: same rule as advisory 'warn' (162 baseline hits); flips to
  'error' in a follow-up once the baseline is clean.
- agent: ruff PLR2004 blocking; baseline fixed (5 violations).
- Shared allowlist: 0/1/-1/2, HTTP status codes, radix and
  unit-conversion factors. Tests are exempt in all three packages.
- max_budget_usd bounds (0.01–100) promoted to contracts/constants.json
  — they were duplicated as literals in cdk validation.ts and cli
  submit.ts, exactly the drift this contract exists to prevent.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cli): promote post-merge magic number 72 to BANNER_WIDTH (aws-samples#258)

PR aws-samples#241 merged into main with a literal banner width in the new
`bgagent github` command, tripping the no-magic-numbers rule this
branch enables. Same constant name as linear.ts.

---------

Co-authored-by: bgagent <bgagent@noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…/duration) (aws-samples#243)

* feat(notifications): platform-side Linear final-status comment with cost/turns/duration

Closes aws-samples#239.

Adds a Linear dispatcher to the fanout consumer alongside the existing
slack/github/email dispatchers. Posts a deterministic final-status
comment on terminal task events for Linear-origin tasks, with cost,
turns/max_turns, duration, and PR link rendered.

Three framing modes by (event_type, pr_url):

  ✅ task_completed                 → 'Task completed'
  ⚠️ error_max_turns + pr_url       → 'Shipped a PR but stopped early'
  ❌ all other terminal states      → 'Task <subtype>: <classifier title>'

The ⚠️ case is the motivating ABCA-91 scenario: agent hit max_turns on
turn 101 after shipping PR aws-samples#35; previous behaviour was a silent ❌
reaction with no metrics surfaced to the requester. The platform-side
comment fires deterministically even when the agent crashes (OOM,
SDK buffer overflow, max_turns) before reaching its own step-3
completion comment.

Architecture:
- One new entry in NotificationChannel + CHANNEL_DEFAULTS + DISPATCHERS
  in fanout-task-events.ts. Dispatcher gates on channel_source ===
  'linear' so non-Linear tasks short-circuit after one DDB Get.
- Reuses the existing postIssueComment helper from
  shared/linear-feedback.ts (already in use by the screenshot pipeline
  + orchestrator failure-reporting paths).
- New construct props linearWorkspaceRegistryTable + linearOauthSecretArnPattern
  guard the IAM grants the same way slackSecretArnPattern does — a
  deployment without LinearIntegration gets no dangling permission to
  bgagent-linear-oauth-*.
- FanOutConsumer instantiation moved below LinearIntegration in agent.ts
  so it can receive the registry table reference.

Tests: 92 passing in fanout-task-events.test.ts (1816 across full
CDK suite). New Linear-dispatcher describe block covers happy path,
failed without PR, ABCA-91 max_turns-with-PR, channel_source short-
circuit, missing metadata, and postIssueComment-returning-false
graceful no-op.

* fix(linear-dispatcher): krokoko PR-243 review nits + test coverage

Addresses the non-blocking nits from aws-samples#239 review:

- JSDoc on renderLinearFinalStatusComment now describes the actual
  (eventType, prUrl) discriminator rather than 'error_max_turns' as
  an event type. The agent_status discrimination lives in the error
  classifier, not in the dispatcher's framing logic.
- Inline comment on classifyError result corrected: returns null only
  for empty error_message, UNKNOWN_CLASSIFICATION (title 'Unexpected
  error') for any non-empty unmatched message.
- ⚠️ frame now appends classifier title — for ABCA-91 the requester
  sees 'Shipped a PR but stopped early — Exceeded max turns', not
  just the bare PR-shipped frame.
- missing_env and post_failed log paths bumped from INFO to WARN
  with error_id tags so missing-env / post-failure are alarmable.
  The Linear comment is the only completion signal for the
  agent-crash case, so silent drops defeat the dispatcher's purpose.
- Stale 'at most 3 channels' comment in routeEvent updated to 4.

Test coverage:
- New test: LINEAR_WORKSPACE_REGISTRY_TABLE_NAME unset → WARN + skip
  (the deploy-misconfig safety valve was unexercised).
- New test: error_max_turns WITHOUT pr_url renders ❌, not ⚠️
  (the ⚠️↔❌ boundary the other direction).
- New describe block: 8 direct tests of renderLinearFinalStatusComment
  covering null-metric fallbacks, formatDuration boundaries (<60s,
  exact-minute Nm, mixed Nm Ss), classifier-title rendering on ⚠️ and
  ❌ frames, and the no-trailing-colon when errorTitle is null.

Total: 102 tests in fanout-task-events.test.ts (was 92), 1826
passing across the full CDK suite.

* fix(linear): drop redundant PR url + agent step-3 comment after first dev smoke

After the first dev deploy of the Linear dispatcher (aws-samples#239), two
near-duplicate things showed up on the Linear thread:

1. The platform's ✅ comment carried PR: <url> while the agent's
   step-2 'PR opened' comment had already posted the same link one
   slot earlier. Two clickable copies of the same URL adds noise.
2. The agent's step-3 'task completed' free-form comment stacked
   right next to the platform's ✅ structured comment with full
   metrics. Two completion comments back-to-back with overlapping
   intent — the platform one is strictly more informative.

Changes:

- renderLinearFinalStatusComment: render PR url ONLY on the ⚠️
  shipped-but-stopped-early path. On ✅, the agent's step-2 comment is
  guaranteed to have fired with the PR link; on ⚠️ the agent may have
  crashed before step-2 (e.g. ABCA-91 max-turns on turn 101), so the
  platform comment is the backup signal and the PR url has to be
  there.
- Updated the corresponding test to assert not.toContain on the ✅
  fixture's PR URL.
- Removed step 3 from the Linear-channel prompt contract in
  prompt_builder.py. Replaced with an explicit prohibition against
  posting a final 'task completed/failed' comment, with a sentence
  pointing the agent at the platform fan-out plane (aws-samples#239) as the
  source of truth for terminal status.

Net Linear thread shape post-task: agent posts start (1) + PR-opened
(2); platform posts terminal ✅/⚠️/❌ (3). One PR url, one completion
comment. Krokoko predicted this exact migration in their PR-243
review — 'the agent prompt can drop step 3 once the platform side is
reliable.'

Targeted suite still 102 passing.
…ws-samples#273)

* feat(screenshot): preview-deploy screenshot pipeline (no stack wiring yet)

Lambda + AgentCore Browser plumbing for capturing screenshots of
preview deployments. Provider-agnostic — listens for GitHub
deployment_status events from any source (Vercel, Amplify Hosting,
Netlify, GitHub Actions custom CD).

This commit lands the handler / construct code only. Stack wiring
follows in the next commit.

* feat(screenshot): GitHubScreenshotIntegration construct + stack wiring

- New `GitHubScreenshotIntegration` construct (mirrors `LinearIntegration`):
  bundles the screenshot bucket, dedup table, signing-secret placeholder,
  receiver Lambda, processor Lambda, and the API Gateway route. cdk-nag
  suppressions added inline (HMAC auth instead of Cognito; AgentCore
  Browser sessions have no per-resource ARN; Secrets Manager rotation
  is owned by GitHub).

- Wired into `agent.ts` after the LinearIntegration block. Reuses the
  existing `githubTokenSecret` (the processor uses ABCA's main GitHub
  token to look up which PR a deploy SHA belongs to and post the
  screenshot comment — no new credential).

- Three new stack outputs: `GitHubWebhookUrl`, `GitHubWebhookSecretArn`,
  `ScreenshotBucketName`.

- Bumped agent.test.ts table count from 13 to 14 to account for the
  new dedup table.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): suppress AwsSolutions-S2 on the public-read screenshot bucket

cdk-nag's S2 fires on any bucket that has `blockPublicPolicy: false`
even when the policy is intentionally permissive. Add the suppression
with the same rationale as S1/S5 — public reads are required by
GitHub Markdown renderers and Linear `imageUploadFromUrl`, and the
read grant is prefix-scoped to `screenshots/*`.

Caught when the first deploy attempt aborted at synth-time on the new
GitHubScreenshotIntegration construct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): private S3 bucket + CloudFront distribution

The first deploy attempt failed at CFN-execute time on the bucket
policy:

  s3:PutBucketPolicy ... because public policies are prevented by
  the BlockPublicPolicy setting in S3 Block Public Access.

Account-level Block Public Access is on for this AWS account, which
overrides per-bucket BPA settings. Disabling it would change the
security posture of the whole account, so route around the constraint
with the AWS-recommended pattern: private S3 + CloudFront with Origin
Access Control.

Changes:
- `ScreenshotBucket` is now `BLOCK_ALL` BPA, no public bucket policy.
  Adds a `cloudfront.Distribution` whose origin is the bucket via
  `S3BucketOrigin.withOriginAccessControl`. The distribution policy is
  scoped to the CloudFront service principal only, so account-level
  BPA accepts it.
- Processor reads `SCREENSHOT_PUBLIC_HOST` (the CloudFront domain)
  instead of building an S3 URL. PR comments now embed
  `https://<dist>.cloudfront.net/screenshots/...` URLs.
- New stack output `ScreenshotCloudFrontDomain`.
- Bucket-level S2/S5 suppressions removed (no longer applicable —
  bucket is private). Distribution gets CFR1/CFR2/CFR3/CFR4/CFR7
  suppressions with rationales.

Heads up on deploy time: CloudFront distributions take 5-15 min to
provision on first create.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(waf): exempt /v1/github/webhook from CRS like /v1/linear/webhook

The CommonRuleSet was 403'ing GitHub deployment_status webhooks before
the request reached our Lambda — the deployment payload contains
absolute Vercel preview URLs in the body, which trips GenericRFI_BODY.

Mirror the Linear webhook exemption: the GitHub webhook path is
HMAC-verified in the Lambda, parsed as strict JSON, never
interpolated into SQL/HTML, and rate-limited by the priority-3 rule.
CRS still applies to every other route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): read environment_url from deployment_status, not deployment

GitHub's `deployment_status` webhook puts the deployed URL on the
*status* object, not the deployment itself. The deployment object is
immutable per (sha, environment); the status changes through the
deploy lifecycle (`pending` → `success`) and carries the URL only
once the deploy finishes.

Symptom: receiver kept short-circuiting `success` events from Vercel
with `{ok: true, skipped_no_url: true}` because we read the wrong
field. Verified by inspecting the webhook delivery payload via
`gh api .../deliveries/<id> --jq .request.payload.deployment_status` —
URL was there all along.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agentcore-browser): use ws package for SigV4-signed WebSocket handshake

Node 24's global WebSocket (from undici) does NOT support arbitrary
HTTP headers on the upgrade request — passing them as the second arg
gets silently ignored. AgentCore Browser's WSS handshake requires
SigV4-signed Authorization + X-Amz-* headers, so the connection was
opening but then getting rejected, which surfaced as an empty
`error` event ("AgentCore Browser WebSocket error: ").

Switch to the `ws` package which natively supports `options.headers`.
Also add an `unexpected-response` handler so HTTP-level handshake
failures (403, 400) surface with status codes instead of empty errors.

Smoke verified locally — the ws-based path opens cleanly against
example.com and Vercel preview URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agentcore-browser): SigV4-presign WSS URL instead of signing headers

Lambda runtime returned a 403 on the WSS upgrade despite well-formed
SigV4 headers — `ws` rewrites the Host header during the upgrade
GET, which invalidates the canonical-request signature we computed
against the original Host. This works locally because Node's tooling
on macOS keeps the original Host through the handshake, but the
Lambda runtime's TLS stack normalizes differently.

Switch to query-parameter SigV4 (presigned URL): SignatureV4.presign
returns a wss://...?X-Amz-Algorithm=...&X-Amz-Signature=... URL where
the auth lives in the URL itself, so any Host-header rewriting
downstream doesn't break the signature.

Smoke verified locally — presigned URL connects cleanly to AgentCore
Browser and the screenshot pipeline runs end-to-end (6.3s, valid
PNG, captures example.com correctly).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(iam): grant bedrock-agentcore:* to the screenshot processor

The minimal IAM I shipped earlier (`StartBrowserSession`,
`StopBrowserSession`, `GetBrowserSession`, `UpdateBrowserStream`)
wasn't enough — the WSS automation-stream connect requires an
additional `ConnectBrowserAutomationStream`-flavored action that
isn't in the public CLI command list. Lambda invocations were
opening sessions cleanly but 403'ing on the WSS upgrade.

Widen to `bedrock-agentcore:*` to unblock the e2e flow. Followup:
scope back down to the specific connect action once it's documented
or surfaced via CloudTrail decoded-message-on-deny.

Smoke verified: PR #1 on isadeks/vercel-abca-linear now receives a
screenshot comment within ~7s of the deployment_status webhook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(screenshot): also post screenshot comment to linked Linear issue

Extends the screenshot processor to find a Linear issue via the PR's
title/body and post the same image comment there.

Approach (no GSI write-back needed):
- Regex-extract Linear identifier (e.g. `ABCA-42`) from PR title/body.
  These are present whether the agent put them there
  (`task_description` carries the identifier) or Linear's own GitHub
  integration auto-injected the back-reference on PR open.
- Scan `LinearWorkspaceRegistryTable` for `status=active` workspaces.
  Per-workspace, query Linear's `issueVcsBranchSearch` (which accepts
  the human-readable identifier) and accept the first exact-match
  hit.
- Post the markdown image comment via the existing `postIssueComment`
  helper from Phase 2.0b.

The Linear post is best-effort — if the registry table isn't wired,
the identifier doesn't extract, or the lookup misses, the GitHub PR
comment still lands. New env var `LINEAR_WORKSPACE_REGISTRY_TABLE_NAME`
is optional on the processor; the construct only sets it when the
prop is provided.

CDK: `GitHubScreenshotIntegrationProps` gains an optional
`linearWorkspaceRegistryTable`. When provided, the processor's IAM
grows: ReadData on the registry, GetSecretValue+PutSecretValue on
`bgagent-linear-oauth-*`. `agent.ts` wires
`linearIntegration.workspaceRegistryTable` into the screenshot
construct.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(screenshot): retry PR lookup to handle deploy-before-PR race

Some providers (Vercel, Netlify) post deployment_status faster than
the agent can run `gh pr create`. Retry the GitHub PR-lookup with
backoff so the screenshot finds the open PR rather than dropping the
event when the timing is reversed.

* fix(linear): silent label gate + default to 'abca' to stop unlabeled-issue comment spam

Move the trigger-label check ahead of every user-facing comment path in
the Linear webhook processor, and switch the default trigger label from
'bgagent' to 'abca'. An unlabeled issue is now a true no-op: no comment,
no reaction, no createTaskCore, no DDB writes — regardless of whether
the project is onboarded.

Why: workspace webhooks fire workspace-wide. A single un-onboarded team
in the same Linear workspace produced 47 identical "❌ project isn't
onboarded" comments on GRO-783 in 5 minutes because every Issue event
(create/update/label-change) hit the not-onboarded gate before the
label gate. With the gate order flipped, only issues that explicitly
opt in via the trigger label can ever generate user-facing feedback.

Per-project label_filter override is still respected — the project
mapping lookup now happens once, before the label gate, instead of after.

Tests: two new regression tests pin the spam scenario (unlabeled issue
in a non-onboarded project, and unlabeled issue with no projectId) to
zero side effects. Full CDK suite (89 suites / 1572 tests) passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(screenshots): add the screenshot pipeline guide

Adds the operator walkthrough for wiring up the AgentCore-Browser
preview-deploy screenshot pipeline.

* feat(github): bgagent github webhook-info + set-webhook-secret

Mirrors the Linear webhook-info pattern so docs and onboarding don't
have to embed stack-specific URLs or copy-paste aws CLI invocations.

Two subcommands:
- `webhook-info` — read-only. Reads GitHubWebhookUrl + GitHubWebhookSecretArn
  from the CFN stack outputs and prints values to paste into a GitHub
  repo's webhook config (Settings → Webhooks → Add webhook). Includes
  the event-type ('Deployment statuses') and content-type guidance
  that operators consistently miss.
- `set-webhook-secret` — interactive PutSecretValue against the stack
  output ARN. Replaces the cargo-cult `aws secretsmanager put-secret-
  value` operators were copy-pasting from the screenshot setup notes.
  Warns before overwriting an existing real secret (heuristic: a CDK-
  seeded JSON placeholder starts with `{`; a real GitHub secret won't).

No CDK changes — both stack outputs were already there. Pure CLI add.

* docs/code(screenshots): de-Vercel-ize the screenshot pipeline

The pipeline was always provider-agnostic — it listens for GitHub
deployment_status events, which Vercel, AWS Amplify, Netlify, and
any GitHub-Actions-driven CD pipeline all post. Code comments,
inline strings, and the setup guide referenced Vercel as if it were
the only supported path; this commit aligns the surfacing with what
the code actually does.

Code:
- Linear comment body: "after the Vercel preview deploy finished"
  → "after the deploy finished" (the GitHub PR comment already
  said this; just the Linear path was inconsistent)
- Webhook receiver doc-comment + envelope interface comment: drop
  Vercel-only language; explain that the `environment` filter
  (`SCREENSHOT_TARGET_ENVIRONMENT` env var) is configurable per-
  provider, with a table of common values
- Processor PR-race comment: explain that the gap is also seen on
  Netlify/Amplify, not unique to Vercel
- AgentCore Browser comment: drop Vercel-specific phrasing on
  "what we don't try to be clever about"
- GitHubScreenshotIntegration construct prop docstring: explain
  the per-provider env-name conventions

Docs:
- Rename VERCEL_SETUP_GUIDE.md → DEPLOY_PREVIEW_SCREENSHOTS_GUIDE.md
- Lead with a "works with any provider that posts deployment_status"
  table (Vercel / Amplify / Netlify / GitHub Actions custom CD,
  with "out-of-the-box?" yes/no per provider)
- Keep Vercel as the worked example since it's what we smoke-tested,
  but add a "skip Steps 1-2" callout for non-Vercel providers
- New "Configuring for non-Vercel providers" section with the
  SCREENSHOT_TARGET_ENVIRONMENT override pointer
- Replace 4a/4b's CFN-output spelunking with `bgagent github
  webhook-info` + `bgagent github set-webhook-secret` (commands
  shipped in 1c1b618)
- Troubleshooting: mention that 401 "Invalid signature" is the
  set-webhook-secret-mismatch case
- Sync registration: register as DEPLOY_PREVIEW_SCREENSHOTS_GUIDE
  in sync-starlight.mjs route map + the explicit mirror call;
  added to astro.config.mjs sidebar after the PAK runbook

No CDK structural changes — the construct prop, env-var, and code
behaviour were already provider-agnostic. Pure surfacing fix.

* docs(screenshots): drop redundant Step 3 + condescending hardening preamble

Step 3 (repo onboarding + Linear project mapping) duplicated work
the Prerequisites section already establishes ('Linear OAuth installed
for at least one workspace'). If the user followed the Linear setup
guide, both are done. If they didn't, Step 4's smoke test fails fast
and the troubleshooting routes them back. Net: 30 lines of doc gone,
no information lost.

Renumbered Step 4 → 3 and Step 5 → 4 (and the 4a/b/c → 3a/b/c
sub-steps).

Also dropped the 'demo configuration optimizes for "look, it works"
rather than security posture' framing on the production-hardening
section. The list of followups stands on its own; the framing reads
as condescending toward someone reaching the bottom of the guide.

* docs(screenshots): drop 'followup' framing — describe gaps as current state

Public docs that say 'followup' read as commitments to do that work.
Reframe gaps as current limitations with neutral language:
- 'Production hardening (followups)' → 'Production hardening
  considerations'; bullets describe what to think about, not what
  ABCA promises to ship
- Netlify table row: 'followup to support pattern matching' →
  '⚠ workable today only by picking one specific PR's
  environment string; broader pattern matching isn't shipped'
- Vercel auth callout: 'tracked as a followup' → 'currently not
  implemented'
- Non-Vercel providers table: drop 'followup aws-samples#96 covers prefix
  routing' reference (issue numbers don't belong in user-facing
  docs)

Net: same information, no implicit roadmap commitments.

* docs(screenshots): de-Linear-ize — Linear is opt-in, not required

The screenshot pipeline only needs GitHub. Linear-side posting was
phrased as a hard requirement throughout the guide because the demo
flow happens to use Linear, but a non-Linear team gets a perfectly
useful integration: screenshots land on GitHub PRs, the Linear
lookup silently no-ops.

Reframings:
- Lead-in: 'on both the open GitHub PR AND the linked Linear issue'
  → 'on the open GitHub PR. If you also have Linear configured,
  the same screenshot is posted to the linked Linear issue as a
  bonus.' Plus a note on the gating (LinearWorkspaceRegistryTable
  having active rows is what flips the Linear path on).
- 'How it works': step 4 (Linear post) marked optional with the
  silent-skip behaviour spelled out
- Architecture comment: 'GitHub PR comment + Linear issue comment'
  → '... (+ Linear issue comment if linked)'
- Prerequisites: Linear OAuth marked optional with rationale
- Smoke test: rewritten as PR-driven by default ('open any PR on
  the configured repo'), with Linear-driven path as a follow-on
  paragraph ('If you also have Linear configured...')
- Troubleshooting: 'Linear is best-effort' → 'opt-in and best-
  effort', explicit note that skipping is normal without Linear

* feat(screenshot): hide URL behind 'preview link' label in comments

GitHub PR comment now reads 'From [preview link](url)' and Linear
comment reads '[Preview link](url)' instead of pasting the bare URL.
Cleaner visual when the same comment is posted on both surfaces.

* docs(screenshots): add USER_GUIDE / COST_MODEL / ROADMAP coverage

Closes the doc gaps from the screenshot feature followup list:

- USER_GUIDE.md: new 'Preview-deploy screenshots (optional)' subsection
  under Notifications, points at DEPLOY_PREVIEW_SCREENSHOTS_GUIDE.md.
- COST_MODEL.md: 'Optional: deploy-preview screenshots' table covering
  AgentCore Browser session, Lambda processor, S3, CloudFront line items
  (~$0.01 per screenshot, dominated by Browser session time).
- ROADMAP.md: marks the feature shipped under Notification plane with a
  one-line description of the trigger model and post-deploy latency.

Mirrors regenerated via docs/scripts/sync-starlight.mjs.

* docs(linear): clarify teammate-onboarding handshake

The 'Inviting teammates' section was missing the prerequisite that the
teammate needs their own ABCA account (Cognito user + configured CLI)
before they can redeem a Linear invite code. New flow walks through:

  Admin: invite-user (Cognito) → invite-user <slug> (Linear)
  Teammate: configure --from-bundle → login → linear link <code>

with cross-references to USER_GUIDE.md's 'Joining an existing
deployment' for the Cognito-side details. Also corrects the stale
'auto-links the person running the wizard' claim — setup now offers
an inline picker (opt-in by admin), not an automatic mapping.

* fix(github-cli): de-Vercel-ize webhook-info / set-webhook-secret strings

Last batch of stale 'Vercel' framing in CLI command output, missed in
the original de-Vercel-ize sweep. Provider-agnostic now: webhook-info
header reads 'preview-deploy screenshot pipeline', the closing note
lists Vercel/Amplify/Netlify/GitHub Actions as example providers, and
the smoke-test instruction says 'push to a PR-attached branch' rather
than 'trigger a Vercel preview deploy'.

No behaviour change; pure copy.

* fix(github-cli): replace template literal with single quotes (eslint mutation)

The local build's eslint --fix step rewrote a no-interpolation
template literal to single quotes; CI's 'Fail build on mutation'
guard caught that the mutation wasn't committed. Apply the fix.

* fix(screenshot): krokoko PR-241 review — scope IAM + cosmetic Vercel mention

Closes aws-samples#94 (the existing 'scope IAM down from bedrock-agentcore:*'
followup task).

Addresses krokoko's PR aws-samples#241 review:

1. (BLOCKING per review #1) IAM action wildcard — narrow
   bedrock-agentcore:* to the three calls the screenshot processor
   actually makes:
   - StartBrowserSession (control plane, public CLI command)
   - StopBrowserSession (control plane, public CLI command)
   - ConnectBrowserAutomationStream (SigV4-presigned WSS dial; not
     in the public CLI list but verified live against the deployed
     dev stack — IAM accepts the action name)

   Resource wildcard remains because AgentCore Browser sessions are
   ephemeral with no stable ARN; the IAM5 suppression on the construct
   already documents that.

   Previous behaviour granted every AgentCore action surface (memory,
   runtime, gateway, identity, code-interpreter) the screenshot path
   doesn't use. Tightening to the call set leaves a precise audit
   surface; if a future API change needs another action, IAM denies
   with the action name in CloudTrail and we add it explicitly.

2. (NIT per review #7) Stale 'Vercel' wording on ScreenshotBucketName
   CfnOutput description, plus an adjacent comment in agent.ts that
   said 'Vercel-style preview deploys'. Both replaced with
   provider-agnostic phrasing — the pipeline listens for any provider
   that posts deployment_status (Vercel, Amplify, Netlify, GitHub
   Actions custom CD).

No behavioural change in either fix.

* feat(linear): prefix-route multi-workspace issue lookup by team key

Closes aws-samples#96.

The screenshot processor's findLinearIssueByIdentifier scanned every
active workspace until one returned a hit. Cheap for 1-2 workspaces, but
wasteful for stacks onboarding several Linear workspaces.

Cache each workspace's team keys (e.g. ['ABCA', 'PLAT']) on the registry
row at install time. The lookup now prefix-matches the identifier's team
key (ABCA-42 -> ABCA) and queries that workspace first; iterates the rest
only on miss or when team_keys is absent (legacy rows / stale cache).

- linear-issue-lookup.ts: split into prefix-match + fallback-iterate
- cli/commands/linear.ts: queryLinearTeamKeys helper, called from setup
  and add-workspace; team_keys persisted on registry row
- DynamoDB schemaless — no CDK schema change. Legacy rows back-fill on
  next 'bgagent linear setup' / 'add-workspace' re-run.

* test(linear): cover queryLinearTeamKeys to clear coverage gate

The new helper added in 059450e (PR aws-samples#273) brought the global statement
+ line coverage in cli/ to 67.74% — under the 68% floor enforced by
the coverage-thresholds contract. CI was failing on this.

Export queryLinearTeamKeys for testing and add 5 unit tests covering:
- happy path: uppercase + dedup + sort
- empty / non-string keys filtered
- non-2xx response → []
- fetch throws → []
- malformed GraphQL response → []

Coverage: 67.74% → 68.09% (clears 68% floor). 299/299 tests pass.

* chore(cli): apply eslint formatting to queryLinearTeamKeys tests

CI's mutation guard rejected the previous commit's compact JSON
fixture style; eslint --fix expands the nested data/teams/nodes
literal across multiple lines. No behavior change; 26/26 tests
still pass.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: bgagent <bgagent@noreply.github.com>
Co-authored-by: Alain Krok <alkrok@amazon.com>
…ws-samples#289)

* feat(screenshot): skip screenshot when preview URL returns non-2xx

Closes aws-samples#287.

Before: the processor only checked Page.navigate's errorText. An HTTP
4xx/5xx response or a 3xx redirect that doesn't resolve cleanly
navigates 'successfully' from CDP's perspective, and a 404 / 503 / etc.
PNG gets posted as if it were the deployed app.

After: enable the Network CDP domain before navigating; tap
ws.on('message') for Network.responseReceived events; record the latest
type==='Document' response status for the navigated frame. After
Page.loadEventFired + the 2s settle wait, throw if the captured status
is non-2xx. The processor's existing catch logs and skips the comment
post cleanly.

Auth walls that return 200 (e.g. Vercel deployment protection) are out
of scope — caller-side fix.

Tests: 6 new in agentcore-browser.test.ts:
- 200 → captures as before
- 404 / 503 → throw with status in message
- 301 redirect → throw (defensive)
- non-Document responses (Stylesheet etc.) ignored
- no Network events fire → falls through (pre-aws-samples#287 behaviour)

Architecture-notes update + Starlight mirror sync. Test file is new on
this branch; PR aws-samples#275 (the broader screenshot test PR) doesn't touch
this file.

* test(screenshot): cover the screenshot pipeline

Closes aws-samples#97.

Adds 53 jest tests across the four screenshot files that landed with
PR aws-samples#241 + aws-samples#273 with no existing coverage:

- github-webhook-verify.test.ts (14): SHA256 sign/verify, sm cache TTL +
  forceRefresh, ResourceNotFound, transparent re-fetch on signature
  mismatch / null fresh.
- github-webhook.test.ts (15): missing body/sig, ping ack, non-deploy
  events ignored, malformed JSON, state/environment filters,
  SCREENSHOT_TARGET_ENVIRONMENT override, missing fields, dedup hit,
  happy path, rollback-on-invoke-failure, non-condition DDB error.
- linear-issue-lookup.test.ts (18): regex covers extract / multi /
  bounds / case-sensitivity, prefix-routing happy path, case-insensitive
  prefix match, fallback for legacy rows + post-prefix-miss, null token
  skip, fuzzy-match guard, GraphQL errors / non-2xx / network failure.
- github-webhook-processor.test.ts (15): empty / malformed body, missing
  fields, token resolve failure, PR retry exhaustion, OPEN-only filter,
  happy path with CloudFront-host URL assertion, screenshot/S3/comment
  failure modes (each non-fatal where appropriate), Linear branch fires
  / falls back to body / skips on no-id / no-resolve / non-fatal post.
- agentcore-browser.test.ts (6): StartBrowserSession failures, full CDP
  exchange (Target.getTargets -> attach -> enable -> navigate ->
  loadEventFired -> captureScreenshot) returning PNG bytes, Stop
  invoked in finally even on CDP error, Stop's own failure logged not
  thrown, 403 unexpected-response surfaced, navigate errorText raised.

All tests use jest mocks for AWS SDK clients + an in-test FakeWebSocket
for the CDP stream so they run hermetically without real AWS or
network. Existing 286/286 handler tests still pass.

* test(screenshot): adapt cherry-picked pipeline tests to post-aws-samples#240 API

Updates captureScreenshot budget-arg + high-entropy S3 key assertions
to match main's aws-samples#240 versions, plus eslint formatting.

---------

Co-authored-by: Alain Krok <alkrok@amazon.com>
bgagent added 3 commits June 10, 2026 17:52
Collapse the duplicated 17/14 table-count assertions left by an earlier
botched main-merge into a single correct count of 18 (17 enumerated
tables incl. the 4 Jira tables, plus github-webhook-dedup from
GitHubScreenshotIntegration). Also fix import ordering (github before
jira) flagged by eslint import/order.
…ration

# Conflicts:
#	cdk/src/stacks/agent.ts
The Lambda-side Jira feedback helper built its REST URL from the tenant
site host (`*.atlassian.net`), but the per-tenant 3LO token is minted
with `audience=api.atlassian.com` and is only valid against the gateway
base `https://api.atlassian.com/ex/jira/{cloudId}/rest/...`. Every
pre-container feedback comment (unmapped project, unlinked user,
concurrency-cap rejection, createTaskCore non-201) therefore 401'd
silently, since these comments are best-effort and swallow errors.

Build the URL from `cloudId` (already on `JiraFeedbackContext`) against
the gateway base — matching the agent-side path in jira_reactions.py —
and drop the unused `siteUrl`. Correct the misleading
jira-workspace-registry-table comment that called site_url a "REST base".

Adds jira-feedback.test.ts asserting the request host is
api.atlassian.com (never atlassian.net), plus encoding and
never-throws coverage.
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.

4 participants