feat(carve S7): the orchestration compute plane — reconciler, release, rollup - #659
Conversation
9302c20 to
59ba7e2
Compare
b51246e to
18bcf34
Compare
59ba7e2 to
bc71750
Compare
18bcf34 to
4581e88
Compare
bc71750 to
c9ab31c
Compare
Automated review — carve S7 (#659)Reviewed as its own diff only ( Slice standalone-ness: PASS
CFN safety — the questions worth asking before this mergesStream enablement: in-place, no replacement. CONFIRMED. GSI addition: one per deploy. CONFIRMED.
Since GSI projection: correct for its purpose — but four call sites already pay for it.
So the routing lookups the index was designed for are fully covered, and the projection is right for them. But four call sites need attributes the index does not project, and each pays a per-item base-table
Each is deliberate and documented in-code. I am raising it because a GSI projection cannot be changed in place — DynamoDB rejects it, as the code comment itself records from experience. So this list is the permanent cost of the current shape. Four known consumers already working around it, two of them for the same field ( On the previously-flagged
|
Addendum to the review above — two further findingsA second pass surfaced two things the first review missed. MINOR (user-copy + public-fitness) — a user-facing retry instruction names the wrong label.
The trigger label is per-project configurable ( So a user who follows this instruction removes and re-applies a label the webhook does not filter on, and nothing happens. It should interpolate the resolved NIT —
Worth removing rather than leaving: the only reason a caller would reach for this prop is to point writes at a different table, and doing so would silently have no effect while appearing to work. NIT — third instance of the "cited artifact lands later" pattern.
This is the same shape as the ADR-016 citations in #654 and the model/IAM split in #654 → #662. See the stack-level note in the summary: the recurring boundary error in this stack is code landing in an earlier slice than the artifact that authorises it. |
4581e88 to
8b85d8e
Compare
c9ab31c to
f28f5ff
Compare
8b85d8e to
32e969a
Compare
f28f5ff to
007e462
Compare
Review findings addressedThank you for the CFN-safety pass — checking stream enablement and GSI count against six refs is exactly the verification this slice needed, and I'm glad the in-place claims held up under it. MINOR — user-facing retry copy named the wrong label. Fixed. This was the most valuable finding in the review: the hint told users to re-apply a hardcoded label while the shipped default is a different string and the label is per-project configurable, so a user following the instruction got silence. Note the existing test asserted the hardcoded string, which is why this shipped — so I fixed the assertion too and added two cases (a custom label, and the default fallback). All three fail if the hardcoded string comes back. MINOR — GSI projection —
Duplicated MINOR — ~600 lines of dead rendering logic. Confirmed: seven exports with no MINOR — raw UUID on the fallback path. Checked NIT — third instance of "cited artifact lands later". Your stack-level observation is the most useful thing in this review. Two of the three are addressed (the descriptor/YAML split, the model/grant split); the doc citations I have left, with reasoning on #654. The pattern is real and worth stating as a slicing rule: code must not land in an earlier slice than the artifact that authorises it. |
007e462 to
8f7f73f
Compare
32e969a to
f5afe7e
Compare
8f7f73f to
a510000
Compare
Operator note: this slice adds one GSI, but a STALE deployment may need two — DynamoDB allows one per updateFound while deploying this arc to a second environment. Correcting something I asserted earlier in this PR. What I verified, and why it was the wrong baseline. I checked that But DynamoDB does not constrain the delta against An environment whose stack predates #640 has three GSIs live ( The dev account did not surface this only because its stack was already current with Operator guidance for merging/deploying this:
Also worth recording, since it bears on how anyone verifies this: on that environment two |
f5afe7e to
0fb2cc1
Compare
62e29fc to
13338b8
Compare
Second-round review: my retry-hint fix was cosmetic — now actually wiredThe reviewer checked whether any production caller passes the Fixed properly. The label is now captured where the mapping is in hand — at seed time — and persisted on the meta row's release context; the reconciler reads it back and passes it to the panel. Rows seeded before the field existed simply have none and fall back to the platform default, as before. Only the settled-panel call site is wired, deliberately: the retry hint renders only when the epic is terminal and something failed, and the other two sites render an in-progress panel that has no hint. I verified that rather than assuming it. Guarded in both directions, and the read half is the one that mattered: dropping the write fails the seed test, but dropping the read hydration originally left the entire 3500-test suite green. Both are now pinned. On On the Verified and not changed: the |
Upgrade-ordering note added, from a second-environment deploy — and a correctionDeployed this arc to a different account (one whose stack was last updated at pre-#643 main) and hit the GSI limit I had reasoned about earlier from the wrong baseline: I had verified "exactly one new GSI" against Operator guidance: confirm the target stack is current with Nothing in the template needs changing, and splitting the index across slices would not help — the constraint is about the deployed baseline. A correction to something I wrote earlier in this thread. I claimed a stray artifact URI "would fail closed anyway" because the reconciler's grant is scoped to |
0fb2cc1 to
b1410ea
Compare
13338b8 to
3b1863f
Compare
…load the PDF Every PDF attachment was being refused with "Attachment '<name>' could not be stored." The S3 upload threw "Cannot perform Construct on a detached ArrayBuffer", and because attachment handling is fail-closed the whole task was rejected. pdf-parse transfers ownership of whatever it is handed to its worker, which DETACHES the underlying ArrayBuffer. It was handed `new Uint8Array(content.buffer, content.byteOffset, content.byteLength)` — a VIEW, which shares its backing store with the caller's Buffer. The caller uploads those same bytes to S3 after screening returns, so by then its buffer was dead. The existing comment reasoned about this and still got it wrong: it said "slice to the exact PDF bytes so a pooled Buffer's backing ArrayBuffer isn't handed over wholesale", which correctly avoids donating a whole pooled buffer but still hands over the caller's. A view narrows WHAT is transferred, not WHOSE it is. Now `Uint8Array.from(content)` — a copy. That costs one allocation of an already-size-capped payload, against silently rejecting every PDF upload. The unit tests could not catch this: they mock the PDFParse class, so no transfer ever happens and a view behaves like a copy. The new test asserts the OWNERSHIP boundary instead of the outcome — same bytes reach pdf-parse, different backing store — which is checkable without a real worker. Verified by restoring the view and watching it fail.
…straction
The dormant foundation for parent/sub-issue orchestration: pure graph logic,
the DynamoDB row store, and a surface-agnostic feedback layer. Nothing here is
instantiated in a stack yet — the executor and the stack wiring land later, so
this slice is synth-and-unit-test only and changes no deployed behaviour.
Graph core (pure, no I/O):
- orchestration-dag: validate a `depends_on` graph (duplicate id, dangling
edge, cycle) and return its topological layering via Kahn's algorithm. The
layers are what a reconciler releases children from, in dependency order.
- orchestration-graph-source: the seam between "where the graph came from" and
the executor. Three tiers — a tracker that has native sub-issues (read it), a
caller that supplies the DAG declaratively (CLI/API, or a planner), or a
structureless trigger (single task).
- orchestration-integration-node: when a graph fans out to several leaves,
append a synthetic node depending on all of them so there is one combined
artifact instead of N unrelated PRs.
- orchestration-epic-tip: where a newly-added, dependency-less node stacks — the
leaf frontier, so it inherits the epic's accumulated unmerged work rather than
branching off the default branch.
- orchestration-base-branch: pick a child's base branch from its predecessors.
Row store and table:
- OrchestrationTable construct: orchestration_id (PK) + sub_issue_id (SK), with
sparse GSIs to resolve a child back from its task id or its head branch.
- orchestration-store: the read/write surface over those rows, including the
idempotency markers that keep duplicate webhook deliveries from double-acting.
- orchestration-log-events: structured events for a run, plus the scheduled
sweep's backstop.
- linear-subissue-fetch: read a parent's children and their `blocks` relations
into a DAG. Fails loud rather than silently truncating an over-size epic.
Channel abstraction:
- orchestration-channel: the surface-agnostic interface — required operations
every channel must implement, optional ones gated by declared capability, so
the engine never branches on which tracker it is talking to.
- orchestration-channel-{linear,jira,slack} + factory: per-surface adapters
selected from the stored row.
- orchestration-comment-trigger: parse a mention comment into a command,
ignoring the bot's own comments.
- iteration-reply: one maturing threaded reply per iteration rather than a
stream of new comments; the two async writers of a reply converge instead of
overwriting each other.
…it repo-wide The test-scope lint relaxation switched off four rules across all 152 test files to accommodate 13 new ones. `max-len` and `no-magic-numbers` are fair there — literal fixtures and long expected strings are the point of a test. `no-shadow` is not: it is correctness-adjacent in test code, where a shadowed `row` or `mock` inside a nested describe is a common way to assert against the wrong fixture and still pass. So `no-shadow` stays on, and the two shadows it found are fixed rather than silenced. One was a redundant local `makeDdb` identical to the file-level helper (deleted). The other was a local `child` that builds a persisted ROW shadowing a file-level `child` that builds a graph node — genuinely two different shapes under one name, which is exactly the hazard. Renamed to say which it is.
…ontain
Review caught two inaccurate comments. Both were worth fixing, and looking for
more of the same class turned up two others.
The integration-node comment gave the synthetic id as `<orchestrationId>#integration`
while the code builds `__integration`. Not a harmless typo: the `#` form is exactly
what the comment thirty lines above warns will 400 the child, because the id flows
into an idempotency key validated against /^[a-zA-Z0-9_-]{1,128}$/. A reader
trusting the example would reach for the one separator that breaks it. Now points
at the suffix constant so the two cannot drift again.
Three comments referenced symbols that are not in this slice:
- `findOrchestrationIds` — real, but in the stranded-orchestration reconciler,
which lands later. Replaced the cross-reference with the property it was there to
convey: every paginated read of this table must follow LastEvaluatedKey, because a
single page is a silent partial answer rather than an error.
- `discoverOrchestration` — the consumer lands with the compute plane; now described
by role instead of by name.
- `renderFailureReply` — called "the existing" renderer when nothing of that name
exists here yet, which is the most misleading form: it reads as a claim about the
current tree. Now describes it as arriving with the activation slice.
A comment naming a symbol a reader cannot grep is a dead end, and in a stacked
series it is an easy defect to introduce, since the symbol does exist on the branch
the slice was carved from.
…ormant)
Turns "one plain issue" into a reviewable sub-issue graph, and makes a long
iteration legible while it runs. Like S5, none of this is wired into a stack
yet — no handler calls it and no construct is instantiated, so this slice is
unit-test-only and changes no deployed behaviour.
Auto-decomposition — plan, review, revise, then run:
- decomposition-mode: pure label parsing. The plain trigger label runs an issue
as one task (or runs its existing sub-issue graph); `…:decompose` plans first
and waits for approval; `…:auto` plans and starts immediately. A decompose
suffix on an issue that already has sub-issues is a no-op.
- decomposition-planner: parse and validate the plan a `coding/decompose-v1`
agent emits as an artifact. Budget is DERIVED from each piece's S/M/L size
rather than asked of the model, so the Σ is a stable, explainable ceiling.
Edges are indices into the plan's own list, validated as a DAG before anything
is created.
- decomposition-caps: per-project limits. Over-cap REJECTS with a message and
never trims — trimming can silently drop a node others depend on.
- decomposition-render: the proposal comment, and every note around it. Plain
English throughout: no "critical path" or "cost ceiling" jargon, the spend
figure is framed as the safety limit it is, and each decline says what actually
happened rather than a plausible-sounding stand-in.
- decomposition-store: the pending plan survives between the propose webhook and
the approve one. Create-once so a redelivery is a no-op; consume is a
conditional delete-and-return so two racing approvals can't both write back.
- decomposition-writeback: create the real sub-issues and their `blockedBy`
relations. Idempotent and resumable — a planned node whose title already
exists is reused, so a retry after a partial write-back doesn't double-create.
- decomposition-flow: ties the above into caps → propose-or-seed, plus the
approve/reject verdict path. All I/O injected, so the control flow is testable
without Linear or DynamoDB.
- plan-commands / plan-revise / plan-revise-interpret: a reviewer can edit the
plan directly ("drop 3", "merge 1 and 2") or in prose. Edits apply
deterministically to the CURRENT plan and the "What changed" line is a COMPUTED
before→after diff — never a model self-report, which used to let a dropped
piece quietly reappear with a fabricated justification.
Iteration feedback:
- iteration-heartbeat (+ construct and scheduled sweep): a long iteration used to
show "starting on this" and then nothing until it finished. The sweep edits the
SAME maturing reply in place to show elapsed time and the latest progress note
— no new comments. Eligibility keys on the reply-routing fields, so standalone
iterations are covered too, not just orchestrated ones.
- iteration-reply-claim: claim a reply so two writers converge on one comment.
- failure-reply: failure is answerable. A red build points at the build log by
task id (the agent runs the build itself, and the target repo may have no CI);
an agent crash gives the classified reason, a truncated excerpt, and whether to
retry or escalate. Never the raw build output — that's untrusted repo code.
- clarify-resume: resume a task that stopped to ask a question before spending.
… a dead regex **A private commit sha was the whole justification for skipping a PROMPT_ATTACK screen.** That is the one sentence a security reviewer needs to be able to follow, and it pointed at a commit no public reader can resolve. Replaced with the actual structural argument: the reviewer's instruction is embedded as delimited data inside a prompt whose only job is to classify it into a fixed edit vocabulary, the output is validated against that closed set before anything is applied, so a jailbreak cannot widen what the caller does — and a note not to reuse this shape where the output is executed rather than matched. **The multi-part conjunction regex could not match its own intended phrasings.** A trailing `\b` after the `plus,` and `;` alternatives requires a word character to follow, which inverts the intent: "the form, plus, a signup page" scored zero while "plus,a signup page" scored one, and "do a; b; c" scored zero while "do a;b;c" scored two. So the correctly-punctuated prose this is meant to catch was the case it missed, and both punctuation alternatives were dead weight. Word alternatives keep their boundaries; the punctuation ones no longer require one. **`latestProgressNote` was documented as a shipped capability but has no producer** anywhere in the stack — the sweep never sets it and there is no persisted attribute to read, so the heartbeat shows elapsed time only. The render path is written and tested, so rather than delete it or half-wire it, the field and the module/construct docs now say plainly that it is reserved and what wiring it needs. **`IterationHeartbeat` shipped without a construct test**, the one place in this slice where source and test coverage parted company. Added, following the repo's per-construct synth-assertion convention — including an assertion for the least-privilege claim the construct's own comment makes but nothing checked. Also: a cross-file line-number reference that pointed at a line which does not exist until a later slice (named the function instead), and the last of the private work-item shorthand.
…t overclaimed Two findings from a second review pass on my own fixes. **The conjunction regex now false-positived on pasted code.** Removing the trailing `\b` fixed the punctuation alternatives, but a bare `;` with no boundary matches EVERY semicolon — and an issue description written for a coding agent routinely contains a snippet, a stack trace or CSS. Four realistic single-task bug reports flipped to "multi-part" and would have nagged the user to decompose them. That is a false positive in the direction that costs attention, and my previous commit message argued for avoiding exactly that. The bare `;` is dropped. It never earned its place in either form: with the boundary it only matched unnatural no-space input, and without one it matched everything. `plus,` keeps the boundaryless form and the word alternatives keep theirs, which is enough for both real multi-part prose shapes; a numbered list is caught by the separate list-item path. A test now pins that a code paste stays single-task, which is what the previous test set never asserted. **The guardrail comment asserted a safety property the code did not have** — which is worse than the private commit sha it replaced, because a reviewer will trust prose. Three claims failed: the instruction was interpolated raw, so reviewer text containing the `"""` fence could close the data block and continue at prompt level; the free-text fields were type-checked but unbounded; and the computed diff is title-keyed, so an edit reusing an existing title reports as "modified" rather than "added" and cannot be relied on to surface a malicious edit. Rather than soften the prose, the first two are now true: the delimiter is neutralized in reviewer text and the instruction and returned fields are bounded. The comment names the real backstop — the downstream guardrail screen at task creation — and says plainly that the diff is a reviewer-facing summary, not a security control. All three new guards are mutation-verified.
…decomposition Decomposition stays on the development branch as experimental rather than landing on main: it presupposes one way of working — an issue split into a sub-issue graph with a human approval gate on the proposed plan — and main should not presuppose that. The iteration work in this slice does not: it applies to any task that opened a PR, however that task came to exist. Removes the 20 decomposition files (planner, caps, flow, store, types, writeback, render, the plan-command and plan-revise handlers, and their tests). What remains is the 12 files this slice is now about: the iteration heartbeat and its sweep, the reply claim, clarify-resume, and the failure reply. Two helpers in the deleted label module were NOT decomposition and had to survive: `DEFAULT_LABEL_FILTER`, which the project-mapping table treats as the default when a project sets no `label_filter` and the rollup renders in operator-facing copy, and `hasHelpLabel`, the one-time explainer label. They now live in `trigger-label.ts`, named for what they are. `triggerLabelVariants` was NOT kept — it existed to return the `:decompose` and `:auto` variants, so with those labels gone it would only ever return the bare base. Tests came across with the module, plus one pinning `DEFAULT_LABEL_FILTER`'s value directly. That constant is load-bearing in a way its size hides: changing it silently stops every project that never set a filter explicitly. Slice size drops from 8538 lines to 1918. The PR title and description need to change to match, since this is no longer a decomposition slice.
…, rollup The engine that actually runs a sub-issue graph: it watches child tasks reach a terminal state, releases whatever they unblocked, keeps the parent's status panel current, and re-stacks dependents when a branch moves. - orchestration-discovery: the composer that turns a trigger into a seeded run — read the graph, validate it, append the integration node if it fans out, then persist it and hand back the root set to start. - orchestration-reconcile: pure gating. Given a child that just went terminal, decide which dependents are now releasable, which must be skipped, and when the parent itself is done. Also covers two recovery shapes: comment-fixing a failed child un-fails it and re-releases the dependents that were skipped behind it, and re-triggering a finished epic retries only its failed/skipped children. - orchestration-release: create a child's task, with a claim so two concurrent releasers can't both launch it. A guardrail rejection or an un-onboarded repo is terminal with a reason the user can act on; a 5xx or a duplicate replay rolls back to ready so the sweep retries. - orchestration-rollup: the parent's single maturing panel — one comment that matures in place rather than a stream of updates. A failed row carries an indented sub-line saying what failed and where to read it. - orchestration-restack: when a child's branch changes, work out which dependents need their base moved. - orchestration-parent-comment: route a comment on the epic to the sub-issue it's about, and say so plainly when it looks like new work instead of a change. - orchestration-reconciler (handler + construct): the TaskTable-stream consumer that drives all of the above, with a DLQ for poison records, a filter so only terminal-status writes invoke it, and partial-batch reporting so one bad record can't re-drive its healthy siblings. - reconcile-stranded-orchestrations (handler + construct): a scheduled backstop for a run whose stream event was lost. Not dormant, unlike S5/S6 — read this part carefully: TaskTable gains a LinearIssueIndex GSI and has its stream enabled (NEW_IMAGE). Both are needed by this slice: the reconciler consumes that stream, and it queries that index to total an issue's iteration cost. The GSI's projection is deliberately narrow because a projection cannot be changed in place later. Enabling a stream and adding a GSI are both in-place CloudFormation updates, so no table replacement. The reconciler constructs themselves are NOT instantiated in any stack yet — the wiring lands in the next slice — so nothing consumes the stream on deploy.
The epic panel's retry fallback told the user to remove and re-apply a hardcoded label. The trigger label is per-project configurable through the project mapping's `label_filter`, and the platform default is a different string — so a user who followed the instruction re-applied a label the webhook does not filter on, and nothing happened. `renderEpicPanel` and `upsertEpicPanel` now take the resolved label and interpolate it, falling back to the platform default when a caller has none. Rendering the panel confirms the output; the existing test had asserted the hardcoded string, which is why it never caught this.
… hint The previous fix made `renderEpicPanel` take a `labelFilter` and interpolate it — but no production caller passed one, so in practice every user still saw the platform default and the hint was still wrong for any project that renamed its trigger label. The fix was cosmetic. The reconciler is where the panel is rendered, and it has no project id to look a mapping up with — it works from the orchestration row. So the label is now captured where the mapping IS in hand, at seed time, and persisted on the meta row's release context; the reconciler reads it back and passes it to the panel. Rows seeded before this field existed simply have no label and fall back to the platform default, as before. Only the settled-panel call site is wired, deliberately: the retry hint renders only when the epic is terminal AND something failed, and the other two call sites render an in-progress panel that has no hint. Guarded in both directions — dropping the write fails the seed test, dropping the read hydration fails the load test. The read half had no coverage until now, so removing it left the whole suite green.
… the default Review flagged trigger_label as only half-wired in this slice. Checking it, the gap is wider than that: the field is read, persisted, hydrated, and threaded to the panel, but NO code path on any branch ever assigns it a value — not this slice, not the activation slice, not the branch this was carved from. The whole plumbing was dead, so every epic's retry hint renders the default label regardless of what the project is configured to trigger on. That makes the hint actively misleading rather than merely generic: telling an operator to re-apply `bgagent` when their project triggers on something else sends them to a label that will not start anything. The seed site is in this slice, so the fix belongs here. The decompose task already carries its configuration through `channel_metadata` (mode, caps, revision round), so the label rides along the same way and is populated onto the release context at seed time. A blank or whitespace value is treated as absent so the renderer's default applies, rather than rendering an empty label in the hint — which also covers a task stamped before this field existed. The producer side (the webhook stamping `decompose_trigger_label` from the project mapping's resolved `label_filter`) lands with the webhook, in the activation slice. Until then this reads as absent and behaves exactly as before, so the two halves are safe to land apart in this order — consumer first, and never the reverse. Verified by deleting the parse and watching the assertion fail.
…raph EXECUTION With decomposition staying on the development branch, the reconciler no longer needs to build a graph — a graph on main is human-authored and seeded by the webhook. This removes the plan-consuming half and keeps the executing half. Gone: the planning-task event shape and its parser, the plan reconciler, the artifact fetch, the :auto seed path, and the seed-time attachment hydration. 656 contiguous lines, plus the six imports of the deleted planning modules and the stream-handler branch that dispatched to them. Kept, and worth naming because the file is now much smaller: terminal-child reconciliation, dependent release, the restack cascade, panel refresh and settle, failed-node recovery, and the iteration reply. Those are what the slice is for. The removal also made three things dead that were only ever used by seeding — the S3 client, the Bedrock screening config, and the artifacts-bucket env — so they went too. Net effect on IAM is a least-privilege improvement: nothing in this slice reads S3 any more. Two consumers needed repointing rather than deleting, since they were never about decomposition: - `orchestration-rollup` imported DEFAULT_LABEL_FILTER from the deleted label module; it now takes it from `trigger-label`. - A test asserted that a planning task falls through `parseTerminalTaskRecord`. The property it was really pinning is that the gate is the presence of orchestration_id, not the workflow id — a task outside a graph must not be mis-gated as a child and released against a graph it has nothing to do with. Rewritten around a plain terminal record so it still guards that. Also drops a probe mock that existed because the seed path failed closed on an attachment probe. With the seed gone the reconciler never probes, so the mock was asserting nothing.
…integration node The epic panel's preview came only from the synthetic integration node, so a chain got no preview at all — the case where it is most obviously wanted, because the reviewer is looking at the parent and a finished node holds exactly the artifact they want. An integration node exists only when a graph has SEVERAL leaves; it is there to merge them. With one leaf there is nothing to merge: everything already converged on the final node, so a synthetic node would re-run work that node just did and its "combined" preview would duplicate the leaf's own. That gate is correct. What was wrong is concluding "no integration node" means "no preview". The panel now falls back to the sole leaf. Deliberately only the SOLE leaf: with several leaves and no integration node (mid-flight, or an older row set) it still shows nothing, because picking one of several would present a single branch's result as the whole epic's — worse than showing nothing. Leaf-finding reuses `computeLeaves`, the same function the seeder uses to decide whether an integration node is needed, so the panel's idea of the final node cannot drift from the seeder's. The label is now conditional. "Combined preview" is only true when leaves were actually merged; on a chain it reads "Preview". Calling a chain's final-node preview "combined" tells a reviewer several branches were merged when none were. Both variants were RENDERED and read as output, not reviewed in source. Verified by mutation, three ways: reverting to integration-only, picking the first child instead of the leaf, and hardcoding the "Combined" label each fail the test and nothing else. Three existing panel tests changed expectations, which is the honest signal that this alters user-visible output rather than only adding a path.
…n child A released child was submitted with no workflow_ref, so it resolved to default/agent-v1 — the repo-OPTIONAL generic workflow. Every other channel (Linear, Jira, Slack) pins CODING_WORKFLOW_ID at its own call site precisely because of this; create-task-core.ts even says so in a comment. The orchestration release path was the one caller that never did. The consequence is not a clean failure. default/agent-v1's setup skips the stacked-base and predecessor-merge path entirely, so: - A CHAIN survives by luck. Its child needs no merge, works from the base it was handed, and the result looks correct. That is why this went unnoticed. - A DIAMOND does not. The integration node exists to merge two predecessor branches; it was handed both in channel_metadata, then given a clean default branch instead. The agent read a task description asking it to integrate, could not see either branch, and hand-wrote an approximation of ONE arm — silently dropping the other's work while producing a plausible-looking PR. Found live: a two-leaf epic's integration PR contained only the first arm's change, and the second arm's was absent from the merged file entirely. Everything upstream was correct — the node was seeded with both dependencies, selectBaseBranch returned shape 'diamond' with both branches, the task record carried both, and the orchestrator's payload wiring was deployed. Only the workflow was wrong. Two tests, one for a normal child and one for an integration node, asserting the REQUEST body rather than the context — nothing had asserted the body before, which is why the whole suite passed with the bug in place. Verified by deleting the pin and watching both fail.
… comment target the combined result Two fixes for one incident. An epic fanned out an API child and a UI child in parallel; each saw only its own sub-issue text and chose its own names — `kyoto` with `checkIn`/`checkOut` on one side, `wander-kyoto` with `startDate`/`endDate` on the other. Both passed their own tests, the branches merged with no conflict, and the deployed flow was broken. Neither child was wrong in isolation. The agreement existed only in the parent, which neither of them could see. Then the follow-up comment about the failure landed on the UI child, where the backend code was not present, so the agent adjusted how the error was displayed. SHARED CONTEXT. The epic's title and body are captured at seed time onto the meta row and prepended to every child's task description, delimited and labelled, with an explicit statement that siblings are working in parallel from the same text and that any name or shape it specifies is a contract rather than a suggestion. Seed time is the only point where the issue body is in hand — the reconciler releases from the stored row with no token to re-fetch it. Truncated at the single write site (4000 chars/field) for two independent reasons that both bind: the 400 KB DynamoDB item limit, and the fact that this text is guardrail-screened and counted against every child's prompt budget. Truncation is marked, so a reader never mistakes half a spec for the whole. The integration child gets it too — it is where a cross-boundary mismatch actually surfaces, and without the contract it cannot tell a real mismatch from two equally plausible conventions. TARGETING THE COMBINED RESULT. `parseParentNodeReference` already looked for `integration`/`combined`, but the keyword only ADMITTED the synthetic node and a title word still had to match. So `integration` worked by accident (it appears in the node's title) while `combined` — the word the panel itself shows, in "combined result", "Combined PR", "Combined preview" — matched nothing and fell through to a "which sub-issue?" reply. The node carries no Linear identifier, so that keyword is the only handle a user has. The keyword is now the match. The disambiguation reply also lists it, with the exact phrasing to type. It is listed but never auto-selected: the combined branch holds every sibling's work, so a misrouted change there is the most expensive kind to unpick, and an ambiguous comment must still ask rather than guess. A test asserts the advertised keyword and the accepted keyword are the same constant, because the failure mode of drift is a reply that tells someone to type a word the parser ignores. Verified by mutation: dropping the context, reverting the keyword to a gate, and hiding the hint each fail their own test and nothing else. Note the pre-existing keyword test passed against the gate behaviour — it asserted `integration` routes, which it did, for the wrong reason.
…l admits the child Caught on the first live run of the change: one child failed at admission with "Task description was blocked by content policy". The shared-context preamble was written as instructions to the agent — "use it exactly", "do not invent an alternative" — and stacked with the epic's own imperatives the whole task description trips the guardrail's PROMPT_ATTACK filter at MEDIUM confidence. The child never ran. That is a worse failure than the bug this feature fixes: the epic previously produced two children with mismatched contracts, and this version produced one child and one that could not start. Isolated it against the deployed guardrail rather than guessing. Each piece passes alone — the delimiters, the epic's own text, the child's own text, and every individual sentence of the preamble. It is the accumulation of instruction-shaped prose that trips it, and swapping only the preamble's voice flips the same assembled description from GUARDRAIL_INTERVENED to NONE with the epic text unchanged. Now descriptive: it says several sub-issues are being built in parallel, that each was given this text, and that the names in it are shared with those siblings — so an alternative chosen here would not match theirs. That carries the same constraint without addressing the model in the second person, and a model acts on it just as readily. A test asserts the VOICE, not the wording: no "use it exactly", no "do not invent", no "you must", while still requiring the parallel-siblings and shared-with framing. The failure mode this guards is subtle — imperative phrasing reads as an improvement and silently makes children unadmittable.
c5a6d3c to
e48273b
Compare
4415f8f to
e8aa93c
Compare
The predecessor slices were squash-merged into the shared base branch, which discards the commit history this branch shared with them. Git therefore sees the row-store module as added independently on both sides (add/add) rather than as one file with two sets of edits. Resolved to this branch's version for both files. That is lossless, not a preference: this branch's copy is a strict superset of the base's (+80 lines in the module and +114 in its suite, with zero deletions), so no line present on the base is dropped. Verified by diffing the resolved file against the base copy and confirming no removals.
…the terminal set Three review findings on the compute plane, all in the same area. The running-total cost sum queried the task index without following the pagination cursor, so past DynamoDB's 1 MB page boundary it summed one page and presented the result as the total. Truncation surfaced as a quietly wrong number rather than an error, which is the worse failure. It now paginates, and reports whether the figure is complete instead of leaving the caller to assume it. It also had no bound on the per-task read fan-out, on the reasoning that iteration counts are small. True today, but nothing enforced it, and the reads happen inside a stream handler with a two-minute budget. There is now a cap that trims the id list rather than merely stopping pagination: the projection is the task id alone, so a single page can hold far more rows than the cap, and breaking out without trimming would still fan out over the whole page. When the cap bites it says so and the total is flagged partial. The implementation moves to a shared module because a second near-copy lives in the fan-out dispatcher, and the two had already drifted — one parsed a string cost without a finite check, poisoning the total to NaN, while the other guarded correctly. Cost accounting that disagrees with itself depending on which handler ran is worse than either behaviour alone. The dispatcher adopts it in the activation slice. Separately, the handler restated the list of terminal statuses that the stream's event filter is built from. Two literal lists that must agree can drift, and either direction is silent: a status added to the filter but not honoured here is dropped on arrival, and one honoured here but absent from the filter never arrives. The set is now derived from the same constant. Tests: 11 for the cost sum, covering the page cursor, the cap bounding the fan-out both within and across pages, string and unparseable costs, read and query failures, and that this task is counted exactly once. Each was confirmed to fail against a deliberately broken version. The terminal set gets a guard driving every filtered status through the parser, plus a negative case so it cannot pass by accepting everything — verified by re-running the mutation that survived before it existed. Also drops an import left unused by the extraction.
Review response — nits (a), (c), §6, and the new conflictThanks — this was a useful pass. Current tip: Decomposition was withdrawn from this carve entirely. It's staying on the experimental branch: it presupposes a single workstyle (issue → sub-issue graph → approval gate on a proposed plan), and (c) — the cost fan-out. Fixed, and it was worse than "no cap". You were right that nothing bounded it. Two defects, not one:
When the cap bites, the total is flagged partial and the reply renders "total this PR: at least $X" rather than showing a short figure as settled. The implementation moved to a shared module, because your N2 on the activation slice was also right that a second near-copy existed in the fan-out dispatcher — and the two had already drifted: one parsed a string 11 tests, each confirmed to fail against a deliberately broken version. §6 — the TERMINAL drift guard. Taken, but structurally. Rather than assert two literal lists stay equal, the handler now derives its set from the same constant the stream's (a) — (b) — the stream + GSI in one deploy. Confirmed, empirically rather than by reasoning. A raw §5 — the dangling design citation. Good catch. One new thing, worth flagging as process rather than code. This PR was Full CDK suite green: 3310 tests, 167 suites. |
The build gate fails on any file the build itself modifies, and the import added with the drift guard was placed after a lower-sorted path.
aws-samples#695) * feat(carve S1): foundation contracts for the orchestration arc (additive, dormant) Slice 1 of the linear-vercel→main carve. Purely additive shared-contract changes that every later orchestration slice references — nothing here is wired or activated, so behavior on main is unchanged. - types.ts: additive optional TaskRecord fields the orchestration/Mode-B/A6 arc reads (orchestration_id, parent_task_id, depends_on, linear_issue_id, code_changed, head_sha, screenshot_url, answer_text, …). All optional → back-compatible. - validation.ts: attachment MIME maps + size constants. Deliberately keeps the max-turns default at main's value (the arc's bump to a higher default is a live behavioral change and belongs in its own PR, not this dormant slice). - workflows.ts: register the read-only planning + restack workflow definitions + the readOnly flag on the workflow type. Registry metadata only — the workflow definitions + planners land in later slices, unused until then. - repo-config.ts: additive optional build_command / lint_command on the repo + blueprint config (per-repo build/lint verification commands the agent runs before opening a PR; default to the platform commands when unset). Gates: cdk compile + eslint + full jest green. No new imports, no runtime wiring, no behavioral change. * test(validation): cover the UTF-8 rejection path for text attachments The full-buffer UTF-8 check had no test for its failure branch: the existing cases only fed NUL bytes, which the null-byte scan catches on its own, so the `TextDecoder` throw path was never taken. Deleting the rejection entirely left the suite green. Three cases, each of which fails if the check regresses: - bytes that are not decodable UTF-8 but carry no NUL (a truncated multi-byte sequence, and 0xFF which never appears in UTF-8) must be rejected, - a 9 KB ASCII preamble must not launder trailing invalid bytes — this is what the whole-buffer check exists for, and it passes under a prefix-only scan, - legitimate multi-byte text must still be accepted, so the decode can't be "fixed" by rejecting everything non-ASCII. * fix: don't admit a workflow whose definition ships later DESCRIPTORS is the live admission table — create-task-core resolves any submitted workflow_ref against it — so an entry here with no corresponding `agent/workflows/**` file is worse than a missing entry: the submission succeeds with a 201, the caller is told the task started, and the agent then dies when it cannot load the workflow. An accepted task that cannot run is a worse experience than a clean rejection at submission. `coding/restack-v1` and `coding/decompose-v1` were declared here while their YAML lands in a later change, and there is no platform-only submission gate to stop a user asking for them by id. Move both entries to travel with their files. The drift test only walked YAML → descriptor, which is why the orphans were invisible, so add the reverse direction: every declared descriptor must have a shipped workflow file. Verified by re-adding an orphan and watching it fail. * chore: keep a node_modules SYMLINK out of git `node_modules/` with a trailing slash matches directories only, so a symlink of that name — e.g. one pointing at a shared install in a sibling worktree — is not ignored and `git add -A` will commit it. CI then fails at install time with `EEXIST: file already exists, mkdir '.../node_modules'`, which reads as a runner cache glitch rather than a tracked file. Match both forms, and drop the one that had been committed. * docs(validation): say why the two magic-byte functions scan different amounts Review raised that `validateMagicBytes` and `detectMimeTypeFromMagicBytes` now disagree about how much of the buffer to look at, with nothing explaining it — a reader is left to guess whether the 8 KB prefix scan in the detector is an oversight. It isn't, and a future "consistency" fix would make things worse. The detector's answer is a guess used to fill in a missing content type, and everything it returns is re-checked by `validateMagicBytes` over the whole buffer one call later. So scanning more bytes there buys no safety, while making it strict would turn a detection miss into a spurious rejection: it would return null for content that validation would have accepted. Also documents that the workflow model allow-list and the Bedrock IAM grant are independent lists, and that a model must be on both. Allow-listing alone lets a workflow pass admission and then fail at turn 0 with AccessDenied, which is a confusing place to learn about it. Adds a test that every bare allow-listed id is paired with its us- inference- profile form, and vice versa. An id admitted in only one form is a latent rejection, since admission compares the literal string a workflow pinned. Verified by deleting one pair and watching the assertion fail. * fix(validation): full-buffer-check inline attachments whose type was detected Review found the magic-bytes check gated on a DECLARED content_type, so an inline attachment with no declared type — where the type is guessed by detectMimeTypeFromMagicBytes — never got the whole-buffer validation. That leaves the weaker of the two paths unguarded. Detection only scans an 8 KB prefix for NUL bytes, so it is a guess; a declared type at least states an intent. Reproduced before fixing: ~8 KB of clean ASCII followed by 0x00 0xFF 0xFE 0x00, submitted with no content_type, was guessed text/plain and ADMITTED. That is precisely the laundering the UTF-8 hardening was written to stop, reaching the same handlers through the door next to the one that was locked. Now every inline attachment is validated, detected types included. Confirmed this does not cost the detection path its purpose: a real PNG and real JSON with no declared content_type still resolve and pass, so the fix does not trade a laundering hole for spurious rejections. Full suite green with no other test needing a change, which is the useful signal here. Also corrects the comment on the detector. It asserted that whatever detection returns is re-checked whole-buffer by validateMagicBytes — true as a description of intent, false on this path until now. It now records that the re-check is load-bearing and that the call site must not be narrowed back to declared types. Verified by restoring the old gate and watching the laundering assertion fail. * feat(carve S2): configurable ECS Fargate task sizing + read-only planning task (#653) * feat(carve S3): agent runtime — deterministic issue feedback, build/lint gate, restack workflow (#654) * feat(carve S4): Linear issue-context surface — attachments, PDF/image screening, auth health (#655) * feat(carve S5): orchestration DAG core, row store, and the channel abstraction (dormant) (#656) * feat(carve S6): iteration feedback — heartbeat, reply claim, clarify-resume, failure reply (#657) * feat(carve S2): ECS rightsized planning task def + configurable task sizing Slice 2 of the linear-vercel→main carve (stacked on S1 foundation contracts). Adds a second, smaller read-only "planning" Fargate task definition alongside the existing build task def, and routes read-only workflows to it so a clone-and-read task (which never builds) doesn't allocate the full build task's CPU/memory. - ecs-agent-cluster.ts: add the planning task def (2 vCPU / 8 GB) next to the build def; make BOTH task sizes overridable via a new optional `taskSizing` prop, with the current values as defaults. The defaults are generous (tuned for a large monorepo build); a consumer with a lighter repo can shrink the build task to cut Fargate cost, so sizing is configuration rather than a fixed value. - ecs-strategy.ts / compute-strategy.ts / orchestrate-task.ts: select the planning task def for read-only workflows on the ECS substrate (ignored by AgentCore); the read-only flag comes from the workflow registry (S1). - orchestrator.ts: thread the per-repo build/lint commands + read-only routing. Also rewrote the comments in these files to plain what/why so they're understandable without internal tracking-ticket context (this is public-sample code). Comment-only edits — verified the non-comment source is identical to the source branch. Gates: cdk compile + eslint + full jest (135 suites / 2522 tests) green. * fix(carve S2): de-opinionate the build task — modest default, overridable env Two findings from the automated review, both instances of one-deployment's measurements becoming everyone's default. **The default build task was the Fargate maximum** — 16 vCPU / 120 GB plus a 100 GiB root filesystem. The docstring was honest about why (tuned for a large TypeScript + Python monorepo) and pointed at the sizing prop, but a default is what an adopter who changes nothing pays for, and that is roughly 5x the per-build cost of a modest size in us-east-1 on-demand. Now 4 vCPU / 16 GB on Fargate's own 20 GiB disk, with the ceiling reachable through `taskSizing` and a test showing that path. Under-provisioning is a slow or OOM-ing build, which is diagnosable and one prop from fixed; over-provisioning is a silent bill. The planning default (2 vCPU / 8 GB) was already well judged and is unchanged. **Four build-tool env vars were hardcoded with no override.** A verify timeout and parallelism caps for a mise/jest/pytest-shaped build are meaningful only for the toolchain a given repo uses — inert for an adopter who uses none of them, and actively wrong for one who uses mise with plenty of memory, since the cap serialises their build to solve a memory problem they may not have. Added `extraBuildEnvironment`, merged over the defaults so a caller's key wins. The measured reasoning stays in the repo as the comment it always was; it is no longer welded into the construct. Also clarifies that "read-only planning task" describes the workflow's behaviour rather than a reduced-privilege role — both task defs deliberately share one role so grants cannot drift between them, and the name should not be mistaken for a privilege boundary. * fix(carve S2): make the sizing knobs actually reachable, and refuse platform env keys A second review pass found my de-opinionation had swapped one problem for its mirror image, plus a footgun. **The prop had no caller, so the ceiling was unreachable.** Lowering the default to 4 vCPU / 16 GB is only defensible if a large monorepo can raise it without editing the construct — but nothing passed `taskSizing`, `AgentStack` takes no custom props, and no context key read it. So the fix traded "everyone pays for 16 vCPU" for "nobody can reach 16 vCPU", and the second is worse for exactly the repo the old default existed to serve. Sizing and the build-tool env are now read from deploy context, the same shape as the `compute_type` gate beside them: cdk deploy -c compute_type=ecs -c ecsBuildTaskCpu=16384 \ -c ecsBuildTaskMemoryMiB=122880 -c ecsBuildTaskEphemeralStorageGiB=100 A malformed value throws at synth rather than silently falling back — "I set the flag and the build still OOM'd" is a worse afternoon than a failed synth. **`extraBuildEnvironment` could clobber platform wiring.** It spreads over the whole container env, not just the four build-tool vars it was added for, so a mistyped key could reach table names, bucket names, or `AGENT_SESSION_ROLE_ARN` — whose absence makes the agent fall back to ambient credentials with per-tenant scoping silently OFF, which the agent's own session module calls its most dangerous failure mode. Reserved keys are now rejected at synth. Also corrects the docstrings, which still advertised 16 vCPU / 120 GB / 100 GiB two lines above code saying 4096 / 16384 / 21 — whichever an operator read first was wrong. Reachability is asserted end to end (context → resolver → construct → template): unwiring the call site fails that test, where before it left the whole suite green. * fix(carve S2): give the build task real disk margin — 21 → 50 GiB Stress-tested the modest default on the workload the old 120 GB default existed for: a full parallel `mise run build` of this monorepo (agent + CDK + CLI + docs) on ECS, with the build gate actually running. memory 3132 MiB peak of 16384 → ~5x headroom disk 14.68 GiB peak of ~21 → ~1.4x headroom result build_passed, ECS exit 0, no OOM, no ENOSPC So CPU and memory are comfortably right, and for a reason worth recording: `MISE_JOBS=1` serialises the packages, so peak memory is max-single-package rather than sum-of-all. The old ceiling was never the binding constraint. Disk is the outlier. 70% of the requested figure consumed by a single build with a warm clone means a heavier dependency cache, or a second build sharing the task, plausibly runs it out of space — and running out of space surfaces as a spurious build FAILURE rather than an obvious resource error, which is the worst way for this to present. 50 GiB restores real margin, and ephemeral storage is a small fraction of the per-task cost next to vCPU and RAM, so the cost win that motivated the modest default is intact. Sized on measurement rather than on the assumption that the old numbers were all equally over-provisioned. The docstring records the figures, and a test pins the default so it cannot drift back to the floor unnoticed. * fix(carve S2): make the planning task def reachable in the slice that adds it Review found the planning task def inert in this PR: nothing sets ECS_PLANNING_TASK_DEFINITION_ARN, so the strategy's `readOnly && ECS_PLANNING_TASK_DEFINITION_ARN` guard is always falsy and read-only workflows keep running on the build def. The three wiring lines existed, but in the LAST slice of this series rather than this one, so the feature works at the series tip and on the branch this was carved from — which is why it live-tested fine — while this slice ships a task def that is billed for and never used. Merges land one at a time, so there is a real window where the def is deployed and unreachable, and anyone testing rightsized planning in that window would conclude the feature is broken. Moved the wiring here, to the slice that introduces the def. `planningTaskDefinitionArn` is required rather than optional, matching its siblings, so an ecsConfig that omits it fails to compile instead of silently disabling the routing. No IAM change: the def shares the build def's task and execution roles, and the ecs:RunTask grant is scoped by ecs:cluster rather than by task-definition ARN. Adds a stack-level assertion that the orchestrator's environment carries BOTH ARNs and that they differ. This has to be at the stack level: the strategy's unit tests set the env var by hand to exercise the routing branch, so they pass whether or not the stack supplies it. Verified by deleting the wiring line and watching both the test and tsc fail. Also corrects four docstrings that still described a 64 GB / 16 vCPU build default after it was lowered to 4 vCPU / 16 GB, one of which claimed a 64 GB task had been OOM-killed when the measured figure was 32 GB. Routes the ECS MAX_TURNS fallback through DEFAULT_MAX_TURNS instead of a literal 200. main uses 100, so the literal was an unflagged behavioral change, and it disagreed with the hydrate path whenever a payload omitted max_turns — a turn ceiling that depends on which code path filled it in. * docs(carve S2): describe artifact workflows by their contract, not by one workflow These comments used the decomposition planner as the example for the read-only planning task def and the artifacts bucket. With decomposition staying on the development branch, that names a workflow main will not have — so a reader looking for it finds nothing, and the comment reads as describing absent code rather than the general rule. The rule is what matters and is what the code keys on: a repo-ful workflow whose terminal outcome is an ARTIFACT clones for context and delivers a document instead of opening a PR, and a read-only workflow runs on the smaller task def. Reworded around that, using coding/pr-review-v1 where a concrete read-only example helps. Two tests used the planner purely as a convenient read-only example; repointed to pr-review-v1. The properties they pin are unchanged and still real: a read-only task inherits the repo's compute substrate rather than being downgraded to a different substrate family, and it runs on the planning def rather than the build def. * feat(carve S3): the agent runtime (Linear no-MCP, attachments, build/lint gate, plan/restack) Slice 3 of the carve — the full autonomous-agent runtime, landed as ONE unit. Stacked on S2. Unlike the CDK subsystems, the agent Python evolved as a tightly- coupled whole: the changes to pipeline.py / repo.py / post_hooks.py / shell.py / models.py interleave WITHIN shared files (a single pipeline.py carries the no-MCP, decompose-plan, restack, and clarify-resume hunks). Splitting it along the CDK subsystem lines produces non-compiling intermediate states, so it lands atomically: all 27 agent/src modules + their tests + the two workflow definitions. What it brings: - Linear no-MCP: strip the Linear MCP server; the platform pre-hydrates comments + attachments deterministically instead of the agent fetching them at runtime. - Per-repo build/lint verification gate before opening a PR (build_command / lint_command), with a structured verify outcome (passed / timed-out / infra-failed). - Stacked-child branch handling + predecessor re-merge (for the sub-issue DAG). - Agent-native decompose + restack workflows (clone, plan/re-stack, emit artifact). - Attachment download + screening + version-pinned integrity read. Behavioral note for reviewers: this also moves the default agent model and a couple of runtime defaults to their current values — intrinsic to the runtime as it runs today; called out here rather than split out, to keep the runtime internally consistent. Gates: agent ruff + ty + full pytest (1421 tests) green. Files are verbatim from the source branch (combination-verified: 0 diffs vs source). Comment cleanup for public readability follows as a separate commit on this branch. * test: tolerate either frozenset layout when cross-checking the writeable-workflow set The agent-vs-CDK cross-check for the writeable-workflow constant matched only a single-line `frozenset((...))`. The formatter renders the same literal across several lines once it grows past the line limit, and the pattern then found nothing — so the check failed on a purely cosmetic reformat instead of on a real drift between the two lists. Allow whitespace between the frozenset call and its inner tuple so the assertion tests the set's contents, which is what it is for. * fix(carve S3): keep the Jira app-identity work when applying the agent-side changes The agent-side files in this slice were taken wholesale from the branch that carries the orchestration work. That branch predates the Jira app-identity feature already on the target, so copying its versions over silently dropped that feature from all 13 agent files — the app-actor env plumbing, the per-task credential scrub, and their tests — while the CDK, CLI and Forge halves of the same feature stayed. The result would have merged as a half-reverted feature. Reapply the agent-side changes as a three-way merge against the common ancestor instead of a wholesale copy, so both survive: the app-identity path and this slice's own agent changes. Verified per file that the result is exactly "branch version + app-identity delta" with no other drift, and that a symbol removed on purpose here (the Linear MCP endpoint) stays removed. One docstring conflicted, describing how Jira comments get posted; kept the target's wording since it names the app actor and is now correct. * fix(carve S3): review findings — close a clone-guard bypass, grant the model the agent defaults to Three fixes from the automated review of this slice, plus its comment cleanup. **The self-reclone guard let `git clone -b` through.** `-b` is `gh`'s `--body` short form, so it sits in the free-text-argument list, but it is ALSO `git clone`'s own `--branch`. The guard truncated the command at the first free-text flag BEFORE scanning for the clone verb, so the cut landed ahead of the very verb it was looking for and an ordinary `git clone -b main <own repo>` was allowed — reopening the stranded-work failure the guard exists to prevent. Two more shapes slipped through for the same reason: `gh repo clone -b main <own repo>`, and a clone chained after an unrelated `-m` (`git commit -m wip && gh repo clone <own repo>`), where the `-m` belongs to the commit, not the clone. Now each shell segment is checked on its own, and within a segment the verb is located first — a verb only counts as prose if a free-text argument opens before it in that same segment. Prose in a `--body` value is still ignored. **The agent's fallback model was not in the Bedrock grant list.** This slice flips the fallback to Opus 4.8 for a repo that pins no model, but the IAM grant is derived from `DEFAULT_BEDROCK_MODEL_IDS`, and that entry was landing several changes later. Merging this slice on its own would have produced AccessDenied at turn 0 on every task with no per-repo model. The grant now travels with the default, and a drift guard reads the agent's own fallback and asserts the grant covers it, so the two cannot diverge again. **Two workflow descriptors arrived here rather than in the base slice**, matching where their `agent/workflows/**` definitions live. `DESCRIPTORS` is the live admission table, so an entry without its file means a submission is accepted with a 201 and then dies when the agent cannot load it. Also removes private tracker ids and work-item shorthand used as the load-bearing explanation in comments, docstrings, log messages and test names. The reasoning is preserved in plain language — a comment that recorded a real defect still records it, just without an id only this team can resolve. * fix(carve S3): the clone-guard rewrite broke two things it was meant to protect A second review pass on my own fix found that it closed the `-b` bypass while opening a new one and introducing a false positive. Both are worse than the original bug in a security guard, so both are fixed and pinned. **New bypass: a wrapped clone walked straight through.** Splitting on a bare newline separated the verb from the repo, so `git clone \` + newline + url never had the slug in the verb's segment. That command was BLOCKED before my rewrite and ALLOWED after it — including via the `-b` form the rewrite existed to catch. Backslash-continuations are now joined before scanning, which also covers a continuation splitting the verb itself (`git \` + newline + `clone`) — the case that genuinely needs the joining, since no segment handling reunites those. **New false positive: a multi-line PR body was denied.** A `--body` or `-m` value spanning lines is how an agent normally writes a PR or commit body, and that text routinely documents a clone command. Treating `\n` as a command separator put the quoted line in its own segment with no preceding free-text flag, so a legitimate `gh pr create` was refused — exactly the false positive the free-text list exists to prevent. A bare newline is no longer a separator; the real separators still are, so a clone chained after an unrelated `-m` is still caught. Also removes `_CLONE_VERB_RE`, which was never load-bearing: across every segment shape it was the sole matcher zero times, and it actually MISSES a subshell (`( git clone …`) that the anchored pattern catches. Dead alternatives in a security path are worse than none. Both regressions are mutation-verified: restoring `\n` as a separator fails the multi-line-body test, and dropping the continuation join fails the wrapped-clone test. * fix(carve S3): report a signal-killed build as infra, not as broken code Review found the streamed path returning Popen's raw return code, which for a signal death is the NEGATIVE signal number (SIGKILL -> -9). The OOM classifier keys on the shell's 128+signal convention (137), so the two never met. This is reachable, not theoretical. A verify command only goes through a shell when it contains shell operators, and `mise run build` has none — so it is exec'd directly and its signal deaths arrive as -9. Reproduced: is_infra_failure returns False for -9 and True for 137, on identical input otherwise. The consequence is the exact mislabel that function exists to prevent. The container/cgroup OOM-killer writes "Killed process" to the KERNEL log, not the build's stderr, so an OOM'd build frequently has no stderr signature at all and the exit status is the only evidence. Left raw, that build is reported as a genuine failure — telling the user their code is broken when the box ran out of memory. Given the build tier's default was just lowered, this is the wrong moment to have OOM detection silently not fire. Normalized at the boundary where the signal is still visible rather than teaching each consumer both encodings, so SIGTERM (-15 -> 143) is covered too. Also replaces `proc.returncode or 0`, which mapped an unreaped None to 0. That cannot happen after wait(), but success is the one unsafe default for an unknown outcome, since it would let a build that never ran report as passing. Now -1. Tests assert 137 reaches is_infra_failure as infra, and that None maps to non-zero. Verified by restoring the old expression and watching the first fail. * refactor(carve S3): drop the decomposition workflow from the main-bound slices Decomposition is being kept on the development branch as experimental rather than landed on main. It presupposes one way of working — an issue split into a sub-issue graph with a human approval gate on the proposed plan — and main should not presuppose that. The rest of this arc is workstyle-neutral: iterating on a PR, heartbeats, clarify-resume, and the compute substrate work regardless. Removes from this slice the workflow YAML, its prompt, its registration in the prompt map, its DESCRIPTORS admission entry, and the two prompt-injection blocks that only a planning task used (the warm repo digest and the revise-round edit-in-place directive, both keyed on channel_metadata nothing else writes). Two things deliberately KEPT, because they are contracts rather than features: - The repo-ful artifact branch in the pipeline. It is selected by the workflow contract (terminal_outcomes.primary == artifact AND requires_repo), not by a workflow id, and no shipped workflow currently takes it. Deleting it would remove a declared capability and the failure mode is bad — an empty PR opened for a document task, or a build run that was never wanted. Its test now synthesizes the workflow by copying the real coding workflow and flipping only those two fields, so the test cannot drift from the production contract the way a hand-built stub would. Verified by forcing the gate false and watching it fail. - The trigger-label and :help helpers. Those are label plumbing, not decomposition. Tests that used the planning workflow purely as a convenient read-only or non-PR example now use coding/pr-review-v1, which is the surviving read-only workflow — the properties they assert (a read-only task inherits the repo's compute substrate; a non-PR workflow still takes the default clone path) are real and still worth pinning. Comments across the stack, cluster, orchestrator, and fan-out handlers described this branch in terms of the planning workflow; they now describe artifact workflows generally, since that is what the code actually keys on. No decompose reference survives in this slice's source or tests. * feat(carve S4): Linear issue-context surface — attachments, PDF/image screening, auth health Bring the Linear surface's issue-context and operator tooling onto main. All of this is reachable today through the existing Linear webhook path; none of it depends on the sub-issue orchestration arc, so it stands alone. - linear-attachments.ts / linear-issue-context-probe.ts: pull an issue's file attachments and recent comments into the task context, so the agent sees the screenshots and discussion a human would. - attachment-screening.ts: screen fetched attachments (images and PDFs) before they reach the model. This carries the pdf-parse v2 upgrade as one unit — the source uses the v2 `PDFParse` class, which requires pinning the dependency and dropping the hand-written module declaration that described the v1 function export. Splitting any of the three breaks the build, so they travel together. - error-classifier.ts: distinguish transient compute faults from user-visible build failures, and report whether work was preserved when a run does not produce a pull request. - screenshot-url.ts + github-screenshot-integration.ts: resolve screenshot URLs for the review path. - CLI: `linear-auth-health.ts` + `platform-doctor.ts` surface an expired or revoked Linear authorization as a diagnosable condition instead of silently dropped events; `linear.ts` / `platform.ts` expose it. The comments and test names in these files are rewritten to explain the what and why directly rather than pointing at internal tracker ids, and two CLI strings that printed an internal issue number to user-facing output are reworded. Stacked on the agent-runtime slice. Full CDK suite 2598 passing, CLI 675 passing, both type-checks clean. * fix(carve S4): review findings — two attachment defects, and stop shipping an inert marker **A quadratic regex could blow the webhook timeout.** Making the leading `!` optional in the markdown-link pattern stopped the engine anchoring each attempt on a literal `!`, so the unbounded label quantifier retried from every `[`. Measured on a description of unmatched brackets: 4 KB ≈ 7 ms, 12 KB ≈ 56 ms, 50 KB ≈ 940 ms — and around 150 KB exceeds the processor's 30 s timeout. No crafting is needed; a large pasted table does it, and the existing scan cap does not bound it because the backtracking happens inside a single `exec()` before any match is produced. The label quantifier is now bounded, which caps the work per start position while still matching every real markdown label. **Two attachments whose names differ only by `.` versus `-` silently became one.** The upload id collapsed `.`, `-` and `/` onto the same character and then squashed runs, so `design.v1.png` and `design-v1.png` produced an identical id — and both de-dupe sites resolve a collision by discarding the second file. A user who attached both got one, with nothing logged and no warning, and the agent planned against incomplete material. The id now carries a short digest of the full pathname, keeping the readable stem for logs. Both fixes are pinned by tests that were mutation-verified: reverting either change fails its own test and nothing else. **The revoked-authorization marker was on by default and could never succeed.** Every Lambda that resolves a token holds read-only registry access, and no slice in this arc grants write, so the marker's write failed AccessDenied on every revoked refresh and the failure was swallowed. The feature read as working while being permanently inert, which is worse than being visibly absent. It is now opt-in: a caller that holds the grant passes a recorder explicitly, and the default is to attempt nothing. When the grant lands, flip the default in that same change. * fix(carve S4): bound the scan input, not just the label — the label bound lost attachments A second review pass found my markdown-regex fix was wrong in both directions. **It silently dropped attachments.** A 300-char label bound makes a link with longer alt text stop matching entirely, so the file vanishes with nothing logged and the agent plans against incomplete material. That is the failure this slice argues elsewhere is the worst kind, and I introduced it while fixing a slow scan. The bound is now 4096 — far past any real label — and a test pins that a ~780-char descriptive label still yields its attachment. **And it did not fix the slow scan.** BOTH variable parts of the pattern backtrack per start position, and the URL part dominates: on `[](https://a` repeated, a 100 KB description takes ~820 ms with or without a label bound, and larger inputs still exceed the webhook timeout. Bounding the label only fixed the one shape the new test happened to assert. The real bound is on the input: descriptions longer than 64 KB have their tail left unscanned, and the truncation is LOGGED rather than passed over in silence. That fixes every hostile shape at once. Both are mutation-verified — restoring the tight bound fails the long-label test, removing the cap fails the URL-shape test. Two timing assertions are replaced with assertions on the observable work (the truncation warning). A wall-clock budget inside a parallel suite measures CPU contention as much as the code: both passed alone and failed under full-suite load, which would have been a flaky gate rather than a guard. * refactor(carve S4): drop the decomposition config fields from the table docs The project-mapping table's field list documented decompose_allowed, max_sub_issues, and max_parent_budget_usd. Nothing in the main-bound slices reads them, since decomposition stays on the development branch as experimental, and a doc describing configuration the code does not honour is worse than no doc — an operator would set those fields and wait for behaviour that never comes. The table is schemaless apart from its partition key, so this removes only prose; any row carrying those fields is still read back untouched. * refactor(carve S4): drop the CLI flags for a feature main will not have `linear onboard-project` offered --decompose-allowed, --max-sub-issues, and --max-parent-budget-usd. With decomposition staying on the development branch, those wrote project-mapping fields nothing reads. Worse than inert: the command printed "Auto-decomposition: ENABLED" and told the operator which labels to apply, so the only feedback they got said it had worked. They would then label an issue and get an ordinary single task, with nothing anywhere explaining why. Found by widening the search past cdk/ and agent/, where I had been looking. The table itself is schemaless, so a project row that already carries these fields is still read back unharmed — they are simply ignored. * fix(carve S4): give pdf-parse its own copy so the caller can still upload the PDF Every PDF attachment was being refused with "Attachment '<name>' could not be stored." The S3 upload threw "Cannot perform Construct on a detached ArrayBuffer", and because attachment handling is fail-closed the whole task was rejected. pdf-parse transfers ownership of whatever it is handed to its worker, which DETACHES the underlying ArrayBuffer. It was handed `new Uint8Array(content.buffer, content.byteOffset, content.byteLength)` — a VIEW, which shares its backing store with the caller's Buffer. The caller uploads those same bytes to S3 after screening returns, so by then its buffer was dead. The existing comment reasoned about this and still got it wrong: it said "slice to the exact PDF bytes so a pooled Buffer's backing ArrayBuffer isn't handed over wholesale", which correctly avoids donating a whole pooled buffer but still hands over the caller's. A view narrows WHAT is transferred, not WHOSE it is. Now `Uint8Array.from(content)` — a copy. That costs one allocation of an already-size-capped payload, against silently rejecting every PDF upload. The unit tests could not catch this: they mock the PDFParse class, so no transfer ever happens and a view behaves like a copy. The new test asserts the OWNERSHIP boundary instead of the outcome — same bytes reach pdf-parse, different backing store — which is checkable without a real worker. Verified by restoring the view and watching it fail. * feat(carve S5): orchestration DAG core, row store, and the channel abstraction The dormant foundation for parent/sub-issue orchestration: pure graph logic, the DynamoDB row store, and a surface-agnostic feedback layer. Nothing here is instantiated in a stack yet — the executor and the stack wiring land later, so this slice is synth-and-unit-test only and changes no deployed behaviour. Graph core (pure, no I/O): - orchestration-dag: validate a `depends_on` graph (duplicate id, dangling edge, cycle) and return its topological layering via Kahn's algorithm. The layers are what a reconciler releases children from, in dependency order. - orchestration-graph-source: the seam between "where the graph came from" and the executor. Three tiers — a tracker that has native sub-issues (read it), a caller that supplies the DAG declaratively (CLI/API, or a planner), or a structureless trigger (single task). - orchestration-integration-node: when a graph fans out to several leaves, append a synthetic node depending on all of them so there is one combined artifact instead of N unrelated PRs. - orchestration-epic-tip: where a newly-added, dependency-less node stacks — the leaf frontier, so it inherits the epic's accumulated unmerged work rather than branching off the default branch. - orchestration-base-branch: pick a child's base branch from its predecessors. Row store and table: - OrchestrationTable construct: orchestration_id (PK) + sub_issue_id (SK), with sparse GSIs to resolve a child back from its task id or its head branch. - orchestration-store: the read/write surface over those rows, including the idempotency markers that keep duplicate webhook deliveries from double-acting. - orchestration-log-events: structured events for a run, plus the scheduled sweep's backstop. - linear-subissue-fetch: read a parent's children and their `blocks` relations into a DAG. Fails loud rather than silently truncating an over-size epic. Channel abstraction: - orchestration-channel: the surface-agnostic interface — required operations every channel must implement, optional ones gated by declared capability, so the engine never branches on which tracker it is talking to. - orchestration-channel-{linear,jira,slack} + factory: per-surface adapters selected from the stored row. - orchestration-comment-trigger: parse a mention comment into a command, ignoring the bot's own comments. - iteration-reply: one maturing threaded reply per iteration rather than a stream of new comments; the two async writers of a reply converge instead of overwriting each other. * fix(carve S5): keep no-shadow enforced in tests rather than relaxing it repo-wide The test-scope lint relaxation switched off four rules across all 152 test files to accommodate 13 new ones. `max-len` and `no-magic-numbers` are fair there — literal fixtures and long expected strings are the point of a test. `no-shadow` is not: it is correctness-adjacent in test code, where a shadowed `row` or `mock` inside a nested describe is a common way to assert against the wrong fixture and still pass. So `no-shadow` stays on, and the two shadows it found are fixed rather than silenced. One was a redundant local `makeDdb` identical to the file-level helper (deleted). The other was a local `child` that builds a persisted ROW shadowing a file-level `child` that builds a graph node — genuinely two different shapes under one name, which is exactly the hazard. Renamed to say which it is. * docs(carve S5): fix comments that describe code this slice does not contain Review caught two inaccurate comments. Both were worth fixing, and looking for more of the same class turned up two others. The integration-node comment gave the synthetic id as `<orchestrationId>#integration` while the code builds `__integration`. Not a harmless typo: the `#` form is exactly what the comment thirty lines above warns will 400 the child, because the id flows into an idempotency key validated against /^[a-zA-Z0-9_-]{1,128}$/. A reader trusting the example would reach for the one separator that breaks it. Now points at the suffix constant so the two cannot drift again. Three comments referenced symbols that are not in this slice: - `findOrchestrationIds` — real, but in the stranded-orchestration reconciler, which lands later. Replaced the cross-reference with the property it was there to convey: every paginated read of this table must follow LastEvaluatedKey, because a single page is a silent partial answer rather than an error. - `discoverOrchestration` — the consumer lands with the compute plane; now described by role instead of by name. - `renderFailureReply` — called "the existing" renderer when nothing of that name exists here yet, which is the most misleading form: it reads as a claim about the current tree. Now describes it as arriving with the activation slice. A comment naming a symbol a reader cannot grep is a dead end, and in a stacked series it is an easy defect to introduce, since the symbol does exist on the branch the slice was carved from. * feat(carve S6): auto-decomposition planning and iteration feedback (dormant) Turns "one plain issue" into a reviewable sub-issue graph, and makes a long iteration legible while it runs. Like S5, none of this is wired into a stack yet — no handler calls it and no construct is instantiated, so this slice is unit-test-only and changes no deployed behaviour. Auto-decomposition — plan, review, revise, then run: - decomposition-mode: pure label parsing. The plain trigger label runs an issue as one task (or runs its existing sub-issue graph); `…:decompose` plans first and waits for approval; `…:auto` plans and starts immediately. A decompose suffix on an issue that already has sub-issues is a no-op. - decomposition-planner: parse and validate the plan a `coding/decompose-v1` agent emits as an artifact. Budget is DERIVED from each piece's S/M/L size rather than asked of the model, so the Σ is a stable, explainable ceiling. Edges are indices into the plan's own list, validated as a DAG before anything is created. - decomposition-caps: per-project limits. Over-cap REJECTS with a message and never trims — trimming can silently drop a node others depend on. - decomposition-render: the proposal comment, and every note around it. Plain English throughout: no "critical path" or "cost ceiling" jargon, the spend figure is framed as the safety limit it is, and each decline says what actually happened rather than a plausible-sounding stand-in. - decomposition-store: the pending plan survives between the propose webhook and the approve one. Create-once so a redelivery is a no-op; consume is a conditional delete-and-return so two racing approvals can't both write back. - decomposition-writeback: create the real sub-issues and their `blockedBy` relations. Idempotent and resumable — a planned node whose title already exists is reused, so a retry after a partial write-back doesn't double-create. - decomposition-flow: ties the above into caps → propose-or-seed, plus the approve/reject verdict path. All I/O injected, so the control flow is testable without Linear or DynamoDB. - plan-commands / plan-revise / plan-revise-interpret: a reviewer can edit the plan directly ("drop 3", "merge 1 and 2") or in prose. Edits apply deterministically to the CURRENT plan and the "What changed" line is a COMPUTED before→after diff — never a model self-report, which used to let a dropped piece quietly reappear with a fabricated justification. Iteration feedback: - iteration-heartbeat (+ construct and scheduled sweep): a long iteration used to show "starting on this" and then nothing until it finished. The sweep edits the SAME maturing reply in place to show elapsed time and the latest progress note — no new comments. Eligibility keys on the reply-routing fields, so standalone iterations are covered too, not just orchestrated ones. - iteration-reply-claim: claim a reply so two writers converge on one comment. - failure-reply: failure is answerable. A red build points at the build log by task id (the agent runs the build itself, and the target repo may have no CI); an agent crash gives the classified reason, a truncated excerpt, and whether to retry or escalate. Never the raw build output — that's untrusted repo code. - clarify-resume: resume a task that stopped to ask a question before spending. * fix(carve S6): review findings — explain the guardrail exception, fix a dead regex **A private commit sha was the whole justification for skipping a PROMPT_ATTACK screen.** That is the one sentence a security reviewer needs to be able to follow, and it pointed at a commit no public reader can resolve. Replaced with the actual structural argument: the reviewer's instruction is embedded as delimited data inside a prompt whose only job is to classify it into a fixed edit vocabulary, the output is validated against that closed set before anything is applied, so a jailbreak cannot widen what the caller does — and a note not to reuse this shape where the output is executed rather than matched. **The multi-part conjunction regex could not match its own intended phrasings.** A trailing `\b` after the `plus,` and `;` alternatives requires a word character to follow, which inverts the intent: "the form, plus, a signup page" scored zero while "plus,a signup page" scored one, and "do a; b; c" scored zero while "do a;b;c" scored two. So the correctly-punctuated prose this is meant to catch was the case it missed, and both punctuation alternatives were dead weight. Word alternatives keep their boundaries; the punctuation ones no longer require one. **`latestProgressNote` was documented as a shipped capability but has no producer** anywhere in the stack — the sweep never sets it and there is no persisted attribute to read, so the heartbeat shows elapsed time only. The render path is written and tested, so rather than delete it or half-wire it, the field and the module/construct docs now say plainly that it is reserved and what wiring it needs. **`IterationHeartbeat` shipped without a construct test**, the one place in this slice where source and test coverage parted company. Added, following the repo's per-construct synth-assertion convention — including an assertion for the least-privilege claim the construct's own comment makes but nothing checked. Also: a cross-file line-number reference that pointed at a line which does not exist until a later slice (named the function instead), and the last of the private work-item shorthand. * fix(carve S6): the regex fix over-corrected, and the guardrail comment overclaimed Two findings from a second review pass on my own fixes. **The conjunction regex now false-positived on pasted code.** Removing the trailing `\b` fixed the punctuation alternatives, but a bare `;` with no boundary matches EVERY semicolon — and an issue description written for a coding agent routinely contains a snippet, a stack trace or CSS. Four realistic single-task bug reports flipped to "multi-part" and would have nagged the user to decompose them. That is a false positive in the direction that costs attention, and my previous commit message argued for avoiding exactly that. The bare `;` is dropped. It never earned its place in either form: with the boundary it only matched unnatural no-space input, and without one it matched everything. `plus,` keeps the boundaryless form and the word alternatives keep theirs, which is enough for both real multi-part prose shapes; a numbered list is caught by the separate list-item path. A test now pins that a code paste stays single-task, which is what the previous test set never asserted. **The guardrail comment asserted a safety property the code did not have** — which is worse than the private commit sha it replaced, because a reviewer will trust prose. Three claims failed: the instruction was interpolated raw, so reviewer text containing the `"""` fence could close the data block and continue at prompt level; the free-text fields were type-checked but unbounded; and the computed diff is title-keyed, so an edit reusing an existing title reports as "modified" rather than "added" and cannot be relied on to surface a malicious edit. Rather than soften the prose, the first two are now true: the delimiter is neutralized in reviewer text and the instruction and returned fields are bounded. The comment names the real backstop — the downstream guardrail screen at task creation — and says plainly that the diff is a reviewer-facing summary, not a security control. All three new guards are mutation-verified. * refactor(carve S6): reduce this slice to the iteration work, without decomposition Decomposition stays on the development branch as experimental rather than landing on main: it presupposes one way of working — an issue split into a sub-issue graph with a human approval gate on the proposed plan — and main should not presuppose that. The iteration work in this slice does not: it applies to any task that opened a PR, however that task came to exist. Removes the 20 decomposition files (planner, caps, flow, store, types, writeback, render, the plan-command and plan-revise handlers, and their tests). What remains is the 12 files this slice is now about: the iteration heartbeat and its sweep, the reply claim, clarify-resume, and the failure reply. Two helpers in the deleted label module were NOT decomposition and had to survive: `DEFAULT_LABEL_FILTER`, which the project-mapping table treats as the default when a project sets no `label_filter` and the rollup renders in operator-facing copy, and `hasHelpLabel`, the one-time explainer label. They now live in `trigger-label.ts`, named for what they are. `triggerLabelVariants` was NOT kept — it existed to return the `:decompose` and `:auto` variants, so with those labels gone it would only ever return the bare base. Tests came across with the module, plus one pinning `DEFAULT_LABEL_FILTER`'s value directly. That constant is load-bearing in a way its size hides: changing it silently stops every project that never set a filter explicitly. Slice size drops from 8538 lines to 1918. The PR title and description need to change to match, since this is no longer a decomposition slice. * feat(carve S7): the orchestration compute plane — reconciler, release, rollup (#659) * feat(carve S8): wire the orchestration arc into the stack and activate it (#662) * style: drop a stray blank line left by the merge resolution The build gate rejects any file the build itself modifies, and the union of the two conflicting test blocks left a double blank line. * fix(agent): keep log-delivery ids stable by default, not behind a flag Updating an already-deployed stack failed at ``AWS::Logs::DeliverySource AlreadyExists`` and rolled the whole stack back. The AgentCore Runtime auto-creates a DeliverySource per logging config and names it after the construct path the library uses internally, so a library-side rename renames these resources — and CloudFormation creates a renamed resource before deleting the old one. A DeliverySource is unique per (resource ARN, log type) account-wide and the runtime ARN is untouched by the rename, so the new source is a second source for the same runtime and CloudWatch Logs refuses it. Worth stating plainly, because it is not intuitive: renaming these resources can never avoid the collision. The conflict is on the ARN they point at, not on their own names. Only holding the logical id steady avoids it, because that is what makes CloudFormation update in place instead of creating a duplicate. A mechanism for this already existed but was opt-in, via ``-c pinnedLogDeliveryStack=<name>``. That inverts the default: the safe path was the one an operator had to already know about, and the failure that would teach them is a mid-update rollback whose message never mentions the flag. I hit exactly that on a deploy of this branch, with the code in front of me. The pin now applies on a plain ``cdk deploy``, and the flag is gone. The opt-in existed for a real reason, so to be explicit about why keying on the stack's own name is safe. A stack that owns these resources gets ids matching what CloudFormation recorded, and updates in place — the broken case. A fresh stack or account owns none of these names yet, so they create normally; the ids being ours rather than the library's is the point, and the values carry no meaning beyond being stable. Any other stack name finds no table entry and keeps the library's naming, which is correct for it, so two stacks can never claim one account-unique name. Verified against the live stack: a diff with no flags shows zero delivery-source destroys, where before it destroyed and recreated all six delivery resources. The two remaining destroys are ordinary version churn. The old test asserted the opt-in was required, so it is replaced by tests for the behaviour that matters — pinned with no context, and no leakage into a stack that has no recorded ids. Both are confirmed to fail against a version that reintroduces a gate, and against one where a pinned id is "tidied" into the library's name. * fix(ci): stop a coverage-file race failing the agent image fingerprint, clear an advisory Two CI failures, neither caused by the change they landed on. The build failed in the ECS cluster suite with ENOENT on ``agent/.coverage.<host>.<pid>.<random>``. pytest-cov writes one temp file per process and deletes them as it combines; the agent suite and the CDK suite run in parallel, and the CDK suite fingerprints the agent tree for the image asset. So the fingerprint walk stats a file that coverage then removes, and the build dies on a path nothing will ever read. Timing-dependent, which is why it had not surfaced before — the tree is walked in the same seconds the coverage run is tearing down. Fixed by excluding coverage data from the fingerprint in two places, because the tree is used two ways: the root .dockerignore covers the real image (context is the repo root) and already excluded ``coverage/`` and ``.pytest_cache/`` as DIRECTORIES, which never matched these files; a new agent/.dockerignore covers the four CDK test call sites that fingerprint ``agent/`` on its own, where the root file does not apply. Verified by causation, not just by a green run: with a stray ``.coverage.*`` file present the asset hash is unchanged, and with the new .dockerignore removed the same file changes the hash. So the exclusion is what fixes it. Separately, osv-scanner flagged brace-expansion 5.0.8 (GHSA-rgw5-rvv9-x895) in both lockfiles. Pre-existing — main pins the same version — and an advisory published against a dependency already there rather than anything introduced here. Bumped via the mechanisms already in use for exactly this, yarn ``resolutions`` and npm ``overrides``, so it stays transitive. Using ``yarn upgrade`` / ``npm install`` would have added brace-expansion as a direct dependency of a package that does not use it.
Slice 7 of the staged carve. Stacked on #657 (
carve/s6-decompose-iteration) — review that first; this PR's diff is only its own commit.What this adds
The engine that actually runs a sub-issue graph. It watches child tasks reach a terminal state, releases whatever they unblocked, keeps the parent's status panel current, and re-stacks dependents when a branch moves.
orchestration-discovery— the composer that turns a trigger into a seeded run: read the graph, validate it, append the integration node if it fans out, persist it, and hand back the root set to start.orchestration-reconcile— pure gating. Given a child that just went terminal, decide which dependents are now releasable, which must be skipped, and when the parent itself is done. Two recovery shapes are covered too: comment-fixing a failed child un-fails it and re-releases the dependents that were skipped behind it, and re-triggering a finished epic retries only its failed/skipped children.orchestration-release— create a child's task, behind a claim so two concurrent releasers can't both launch it. A guardrail rejection or an un-onboarded repo is terminal with a reason the user can act on; a 5xx or a duplicate replay rolls back to ready so the sweep retries.orchestration-rollup— the parent's single maturing panel: one comment that matures in place rather than a stream of updates. A failed row carries an indented sub-line saying what failed and where to read it.orchestration-restack— when a child's branch changes, work out which dependents need their base moved.orchestration-parent-comment— route a comment on the epic to the sub-issue it's actually about, and say so plainly when it looks like new work rather than a change to existing work.orchestration-reconciler(handler + construct) — the TaskTable-stream consumer that drives all of the above. A DLQ for poison records, a FilterCriteria so only terminal-status writes invoke it (RUNNING/heartbeat churn is the bulk of TaskTable writes and would otherwise wake it constantly), and partial-batch reporting so one bad record can't re-drive its healthy siblings.reconcile-stranded-orchestrations(handler + construct) — a scheduled backstop for a run whose stream event was lost.TaskTablegains aLinearIssueIndexGSI and has its stream enabled (NEW_IMAGE).TaskTableis instantiated in the stack, so these two changes do land on deploy.Both are required by this slice and can't be deferred: the reconciler consumes that stream, and it queries that index to total an issue's iteration cost. Notes for review:
pr_url,pr_number,status,repo,user_id,channel_metadata). A GSI's projection cannot be changed in place afterwards, which is why the cost query does a per-taskGetItemforcost_usdrather than widening it.TaskTablerather thanTaskEventsTablebecause the latter is already at its 2-consumer limit.Verification
tsc --noEmitcleaneslintclean🤖 Generated with Claude Code
Tracking
Slice S7 of the linear-vercel → main carve. Tracking issue: #668 (slice table, why it is sliced rather than merged, and review guidance).
Lands part of #247 (parent/sub-issue orchestration). Deliberately not
Closes— no single slice closes it; it closes when S8 activates the arc. The auto-decomposition issue (#299) is no longer part of this carve — that feature stays on the development branch as experimental.Stacked on #657 — review that first; this PR's diff is against its branch.