feat(carve S5): orchestration DAG core, row store, and the channel abstraction (dormant) - #656
Conversation
30aa161 to
0df8c2c
Compare
08bc626 to
d3c250f
Compare
0df8c2c to
c58d51b
Compare
d3c250f to
7af69f3
Compare
Automated review — carve S5 (#656)Reviewed as its own diff only ( Slice standalone-ness: PASS
All 29 deletions checked — no Nothing is introduced-but-unused. I checked all 16 new source modules for a consumer in Dormancy claim: CONFIRMED — but not for the stated reasonThe PR description's premise does not hold:
One thing worth flagging, because it is the only place the dormancy argument is non-obvious: this slice modifies 1. MAJOR — this slice turns off four lint rules for all 152 test files, to accommodate 13 new ones
'@typescript-eslint/no-magic-numbers': 'off',
+ '@typescript-eslint/no-shadow': 'off',
+ 'no-shadow': 'off',
+ '@stylistic/max-len': 'off',
+ 'max-len': 'off',The stated reason is this slice's own tests: "long fixture/assertion lines, and reuse small helper names ( But the override is repo-wide. At this slice there are 152 test files, 139 of which pre-existed this PR. So This is also precisely the "source file and the lint config that permits it, split across a boundary" case — except here they are in the same slice (good) with a blast radius far wider than the code that needed it (not good). Prefer scoping the override to the new files' paths, or fixing the ~13 files, or at minimum keeping 2. MINOR —
|
c58d51b to
efd1765
Compare
7af69f3 to
83344ef
Compare
efd1765 to
430576f
Compare
83344ef to
cc75f8c
Compare
430576f to
227058d
Compare
cc75f8c to
fedea84
Compare
Review findings addressedI had missed this review initially — apologies. MAJOR #1 — four lint rules switched off across 152 test files to land 13. Fixed, taking your minimum ask. This was my change and your blast-radius point is right: So
Any of those three could have had a test asserting against the wrong fixture and still passing. Worth noting the rule only surfaced the third one once the relaxation came off at the tip of the stack, which is itself an argument for your position. MINOR #2 — MINOR #3 — raw internal UUID on the label fallback path. Confirmed the same Gates: 2920 CDK tests, |
2a8e7b2 to
dadf00f
Compare
fedea84 to
99f1007
Compare
Second-round review: the shadow renames hold upA second adversarial pass attacked this fix specifically. Nothing broke, which is worth recording as explicitly as a defect would be. The regex-driven renames are clean. The whole delta is the three intended call sites plus the definition and the deleted duplicate helper — no property access, no string literal, and no The renamed tests still assert the real thing. The reviewer mutation-tested them three ways — removing the legacy-name fallback, breaking
Worth noting the rule earned its keep immediately: re-enabling it surfaced a third shadow in #659 (a local The two MINOR findings stand as noted, unchanged: |
dadf00f to
1a3f6bf
Compare
99f1007 to
3c02a74
Compare
scottschreckengaust
left a comment
There was a problem hiding this comment.
Review — carve S5: orchestration DAG core, row store, channel abstraction (dormant)
Reviewed as a Principal AWS Solutions Architect, as this PR's own incremental diff only (carve/s4-linear-surface..carve/s5-dag-core-channel, 35 files, +8748/−29). This is a stacked PR; its base (#655) is reviewed separately and its code is out of scope here.
1. Verdict — Approve (with two non-blocking comment-accuracy nits)
This is exceptionally well-engineered, purely-additive foundation code: pure graph logic (Kahn's-algorithm DAG validation + layering), a DynamoDB row store, and a surface-agnostic Channel abstraction with Linear/Jira/Slack adapters. Every module is fail-closed where it matters, best-effort where feedback is advisory, and carries a rationale comment for each non-obvious decision. It is genuinely dormant and deploy-safe. The two findings are stale doc comments on dormant code — not worth blocking a merge.
2. Vision alignment
Strongly aligned. Bounded blast radius: fetchSubIssueGraph fails loud on an over-100 epic rather than silently truncating children (and dropping their dependency edges → out-of-order release) — the correct choice for the orchestration tenet. Fire-and-forget preserved: the comment-trigger parser has a robust self-loop guard (bot-authored-prefix detection) that prevents the agent talking to itself, and a deliberately-narrow near-miss allowlist so a mistyped handle nudges rather than silently drops. Reviewable outcomes: orchestration-log-events.ts centralizes greppable event names as an explicit test contract for an event-driven plane with no synchronous API. Backing issues #247 and #299 both carry approved + P0; tracking epic #668 documents the slicing.
3. Blocking issues
None.
4. Non-blocking suggestions / nits
orchestration-integration-node.ts:79— thewithIntegrationNodedoc says the synthetic id is`<orchestrationId>#integration`, but the code (line 53/92) produces`<orchestrationId>__integration`viaINTEGRATION_NODE_SUFFIX. Worse, the#form is the exact idempotency-key-invalid shape that lines 46-52 explicitly warn against (createTaskCorevalidates${orch}_${sub}against/^[a-zA-Z0-9_-]{1,128}$/, and a#would 400 the child). The code is correct; the comment misdirects toward a broken value. Fix the comment to__integration.orchestration-store.ts:836—loadOrchestration's doc says it "mirrorsfindOrchestrationIds's Scan pagination," but nofindOrchestrationIdsexists at this slice (onlyfindOrchestrationChildByBranch). It's a forward-reference to a later slice; a reader who greps for it finds nothing. Either drop the reference or note it lands later.- (pre-noted by author, agree)
extractLinearIdentifierFromBranch(linear-issue-lookup.ts) ships three slices before its only consumer (#662). Clean, but its motivating rationale references behaviour not yet present. Boundary-placement judgement; fine to leave. - (pre-noted by author, agree) the
display_id ?? sub_issue_idlabel fallback can surface a raw internal UUID to a user. On dormant panel code here; a friendlier fallback would arguably mask a data defect, so recording rather than fixing is defensible.
5. Documentation
No user-facing docs, ADRs, or Starlight mirror needed — this slice deploys no resource and changes no runtime behaviour or contract. The changes to live files (linear-issue-lookup.ts, linear-feedback.ts, slack-api.ts) are additive exports plus a docs-only ABCA- → ENG- example rename (the right direction for a public repo). No doc drift.
6. Tests & CI
Strong. 13 new test files, table-driven, covering failure paths (cycles, dangling edges, duplicate ids, truncation-fail-loud, idempotent replay, tenant-collision refusal, diamond/linear/root base selection, self-trigger loop guard, reaction-replace incompleteness). CI build (agentcore) — the full CDK suite + eslint — passed; author reports 2917 tests / 150 suites. no-shadow is correctly kept enforced in tests (the final commit reverted the earlier over-broad relaxation and fixed the 3 real shadows it surfaced).
Bootstrap synth-coverage: N/A. OrchestrationTable is a DynamoDB table (AWS::DynamoDB::Table already in resource-action-map.ts) and is not instantiated in any stack at this slice (verified: no new OrchestrationTable( in cdk/src). No new CFN resource type, no IAM grant takes effect, so no bootstrap-policy / BOOTSTRAP_VERSION bump is required in this PR. Correct to defer to the wiring slice (#662).
7. Review agents run
- code-reviewer /
/code-review(high effort,HEAD~3..HEAD) — RAN. Surfaced the two comment-accuracy nits above; no correctness or style defects. - comment-analyzer — RAN (folded into above); the two findings are its output.
- silent-failure-hunter — RAN (in scope: heavy error handling). The channel adapters and store are best-effort-with-structured-logging by design (feedback must never gate orchestration);
fetchSubIssueGraphandlinearGraphSourcedeliberately fail loud rather than degrade. No silent swallowing found. - type-design-analyzer — RAN (in scope: many new types).
DagValidationResult,FetchSubIssueGraphResult,OrchestrationGraphResult, andChannelare well-formed discriminated unions with fail-closed defaults;ChannelKind/registry are deliberately open to satisfy the extensibility tenet. No concerns. - pr-test-analyzer — RAN. Coverage is comprehensive and asserts failure paths, not just happy paths; CI full suite green.
- /security-review — RAN (in scope: DynamoDB construct, GraphQL Bearer auth, comment input-gateway). No deployed IAM/Cedar/network change (construct un-instantiated); Bearer-token handling and 8s-timeout+AbortController mirror the proven
linear-feedbackpattern; the comment-trigger self-loop guard and near-miss allowlist are defensive. Orchestration id derivation is documented as non-tenant-scoped but safe for workspace-unique UUIDs, with an explicitOrchestrationIdCollisionErrorfail-closed at seed for non-unique refs. No secrets exposure. - Omitted: none in scope.
8. Human heuristics
- Proportionality — PASS. The registry-over-switch and three-tier graph-source seam are justified by the extensibility tenet, not speculative; the Slack adapter exists specifically to prove capability-gating against a non-tracker surface.
orchestration-store.tsis large (954 lines) but cohesive (one table's surface) rather than accreted. - Coherence — PASS. Consistent vocabulary (
orchestration_id/sub_issue_id/depends_on) across construct, store, DAG, and channel. The three adapters are parallel with real substance, not copy-paste boilerplate (Jira genuinely omits optional capabilities; Slack genuinely maps thread→issue). - Clarity — CONCERN (minor). Two stale doc comments misdescribe real values (
integration-node.ts:79,store.ts:836); see nits. Naming and error surfacing are otherwise excellent. - Appropriateness — PASS. Integration code is written against the real Linear GraphQL shape and the documented DynamoDB
BatchWriteItem25-item / 1MB-page limits (paginated Query, chunked BatchWrite, meta-row-last partial-failure ordering, TTL-aliased reserved keyword). Tests assert intended behaviour (mutation-tested by the author's second pass), not merely current behaviour.
Reviewed with Claude Code as scottschreckengaust. Dormancy independently verified via import-graph + new OrchestrationTable( grep across cdk/src; the only caller of the new linear-feedback exports is the new dormant orchestration-channel-slack.ts; linear-issue-context-probe.ts references fetchRecentComments only in a doc comment, not a call.
1a3f6bf to
0a2610a
Compare
3c02a74 to
e777e0d
Compare
0a2610a to
c0bcadc
Compare
e777e0d to
a112305
Compare
c0bcadc to
d884d51
Compare
…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.
…ble-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.
…t-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.
…e 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.
…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.
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.
…nd 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.
… 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.
…pping 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.
…ound 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.
…le 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.
b2e4ac2 to
5d3612a
Compare
6532a1d to
decd661
Compare
`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.
5d3612a to
fbf6516
Compare
decd661 to
2fd6671
Compare
Scope change: auto-decomposition is no longer part of this carveHeads-up before you re-read this — every slice was force-pushed, so a review in progress is against a stale diff. Sorry for the churn; the reason is a scope decision, not a fix. Auto-decomposition is staying on the development branch as experimental and is not landing on What moved~7,900 lines removed. 23 dedicated files, the agent workflow and its prompt, the admission-table entry, the CLI flags, and about 314 lines embedded in files that survive.
Three things worth knowing as a reviewerSome of the review fixes from the last round are gone with the feature. The verdict-marker fix (B1 on #662) and the retry-hint Two things looked decomposition-only and were not. Removing the feature exposed dead IAM. The reconciler held an Verification
|
…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.
2fd6671 to
8633acf
Compare
Slice 5 of the staged carve. Stacked on #655 (
carve/s4-linear-surface) — review that first; this PR's diff is only its own commits.What this adds
The dormant foundation for parent/sub-issue orchestration. Three groups, all inert:
Pure graph logic — no I/O, so it is unit-testable in isolation.
orchestration-dag— validate adepends_ongraph (duplicate id, dangling edge, cycle) and return its topological layering (Kahn's algorithm). A reconciler releases children layer by layer.orchestration-graph-source— the seam between where a graph came from and the executor that runs it. Three tiers: a tracker with native sub-issues (read them), a caller that supplies the DAG (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's one combined artifact instead of N unrelated PRs. Reuses the existing multi-predecessor merge path; no new merge code.orchestration-epic-tip— where a newly-added, dependency-less node stacks: the leaf frontier, so it inherits the epic's accumulated unmerged work instead of branching off the default branch.orchestration-base-branch— pick a child's base branch from its predecessors.Row store and table
OrchestrationTableconstruct —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 stop a duplicate webhook delivery from double-acting.orchestration-log-events— structured per-run events, 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 (dropping children also drops their dependency edges, so survivors would release out of order).Channel abstraction — the engine never branches on which tracker it's talking to.
orchestration-channel— the surface-agnostic interface: operations every channel must implement, plus optional ones gated by declared capability.orchestration-channel-{linear,jira,slack}+ factory — per-surface adapters, selected from the stored row.orchestration-comment-trigger— parse a mention comment into a command; ignore the bot's own comments.iteration-reply— one maturing threaded reply per iteration instead of a stream of new comments; the two async writers of a reply converge rather than overwrite each other.Why this is safe to merge early
OrchestrationTableis not instantiated in any stack in this slice, and no handler calls into the store or the executor seam — the wiring lands in a later slice behind an env-var gate. So this is synth-and-unit-test only: it changes no deployed resource and no runtime behaviour.Also here
One separate commit relaxes two stylistic ESLint rules (
no-magic-numbers,no-shadow,max-len) insidetest/**only. Table-driven tests with literal fixtures fight those rules constantly, and a lint-policy change shouldn't hide inside a feature commit — hence its own commit.Verification
tsc --noEmitcleaneslintclean oncdkandcliruff check/ruff format --check/ty checkall cleanTracking
Slice S5 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 #655 — review that first; this PR's diff is against its branch.