feat(carve S1): foundation contracts for the orchestration arc (additive, dormant) - #647
feat(carve S1): foundation contracts for the orchestration arc (additive, dormant)#647isadeks wants to merge 5 commits into
Conversation
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #647 +/- ##
=======================================
Coverage ? 90.10%
=======================================
Files ? 241
Lines ? 58995
Branches ? 6321
=======================================
Hits ? 53160
Misses ? 5835
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…ive, 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.
Automated review — carve S1 (#647)Reviewed as its own diff only ( Slice standalone-ness: typecheck PASS, but the "dormant" claim does not fully hold
The other two files are not dormant, which is the substance of this review. 1. BLOCKING — two workflows are admitted here, but their agent YAML does not exist until #654This PR adds Verified per-slice:
Same for Failure: merge #647 or #653 alone and deploy. A user submits Fix: move the two 2. MAJOR — a live, security-relevant validation tightening ships here with its tests in #655
The 3. MAJOR — private work-item shorthand as the stated reason fields existThe new doc comments carry explanation-bearing private labels that resolve nowhere in the public repo — not at this slice and not at #662:
This PR is 4 files — it is the cheapest place in the stack to get this perfect, and it sets the tone for the other seven. Replace with what the field is for ("set when a task is spawned as part of a multi-issue breakdown"), and drop the labels. 4. MINOR —
|
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.
|
Addressed the Codecov report above. The 2 uncovered lines were the I confirmed it was a genuine gap rather than a reporting artifact by mutation-testing it: deleting the rejection entirely left all 109 tests passing. The existing cases only fed NUL bytes, which the separate null-byte scan catches on its own, so the Added three cases, each verified to fail when the check is broken:
Re-running coverage, Note the remaining uncovered lines in that file are pre-existing and unrelated to this diff. Codecov also flags a missing BASE report for |
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.
9d4914a to
111dcd6
Compare
Addendum — a committed symlink to the author's local filesystemFound while reviewing #653 (which deletes it). This is in this PR's diff and I missed it on the first pass. #647 commits Traced across the stack:
Two things follow:
Fix: drop the symlink from this commit. Optionally tighten |
Note: this PR was force-pushed during the review — status of each finding against the current headMy review above was written against
Everything else in the original review stands as written. |
Review findings addressedBLOCKING #1 — two workflows admitted before their definitions exist. Fixed. Confirmed independently: The root cause was the drift test: it walked YAML → descriptor only, so an orphan descriptor was invisible to it. Added the reverse direction — every declared descriptor must have a shipped workflow file — and verified it by re-adding an orphan and watching it fail. That guard is what stops this recurring, not the move. MAJOR #2 — the UTF-8 tightening shipped without its tests. Fixed (see the Codecov reply above). Three cases added, each mutation-verified to fail when the check is broken. Worth noting the coupling you identified was real in both directions: the later slice had its own equivalent tests, and its version covered a case mine missed (a multi-byte character straddling the old 8 KB cutoff), so I merged both rather than keeping just one. MAJOR #3 — private work-item shorthand as the stated reason fields exist. Fixed across this slice and the rest of the stack: tracker ids, MINOR #4 — MINOR #5 — NIT #6 — |
`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.
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Approve with nits (posted as COMMENT — no code blockers, but the PR description materially misstates the diff)
Reviewing as a Principal AWS Solutions Architect. This is S1, the base of the 8-slice linear-vercel → main carve (tracking epic #668), a cross-repository (fork) PR from external author @isadeks. The code is clean, well-tested, and safe to land. My only substantive concerns are description/framing accuracy, not defects — hence COMMENT rather than a clean APPROVE.
Governance (ADR-003)
Gate is an approved backing issue. The feature issues this slice lands part of — #247 and #299 — both carry approved + P0. The tracking epic #668 is the correct place for the slice rationale. Backing-issue gate: satisfied. Branch name carve/s1-foundation-contracts does not match (feat|fix|chore|docs)/<issue>-..., but per the carve convention this is a de-facto-waived nit, not a blocker.
Vision alignment
Advances bounded blast radius: the slice is dormant-by-design (TypeScript compiles unimported library code; the orchestration path is env-gated in later slices), keeping main deployable while the arc lands reviewably. The validateMagicBytes change is a fail-closed tightening of the input gateway — squarely aligned with the admission-guard tenet. No tenet is traded away.
Blocking issues
None.
Non-blocking suggestions / nits
-
The PR body says
behavior on main is unchangedand calls the slicepurely additive … nothing is wired or activated— that is not accurate for two of the files, both of which are LIVE paths:cdk/src/handlers/shared/validation.ts—validateMagicBytesis called fromjira-attachments.tsandresolve-url-attachments.ts(live admission). Switching text validation from an 8 KB NUL-scan to a whole-bufferTextDecoder({fatal:true})+ full NUL scan is a real, live behavioral change. It is a strict tightening (a long ASCII preamble can no longer launder trailing binary; non-UTF-8 is now rejected), which is good — but it should be described as a live security fix, not a dormant contract. Note also thatdetectMimeTypeFromMagicBytes(validation.ts:390) still uses the 8 KB prefix scan (TEXT_MAGIC_BYTE_CHECK_BYTES); that is a detection heuristic rather than the validation gate, so it is defensible, but the two now use different rules — worth a one-line comment so a future reader does not "fix" the asymmetry.cdk/src/handlers/shared/workflows.ts— the body claims this fileregister[s] the read-only planning + restack workflow definitions + the readOnly flag on the workflow type. The actual diff adds only two model-allowlist entries (anthropic.claude-opus-4-8,us.anthropic.claude-opus-4-8);readOnlyand all fiveDESCRIPTORSalready exist onmain. Adding toWORKFLOW_MODEL_ALLOWLISTis itself a live policy change (it widens the set of models a submitted workflow may pin). Please correct the changelog so reviewers of later slices can trust the per-file descriptions.
-
Model-ID coverage (workflows.ts:89-90). The allowlist adds
anthropic.andus.anthropic.forms ofclaude-opus-4-8but not aglobal.anthropic.inference-profile form. If the deployment uses global cross-region inference profiles anywhere (the repo references aglobal.prefix elsewhere), a pinnedglobal.anthropic.claude-opus-4-8would be rejected. Confirm the intended prefix set. Non-blocking. -
repo-config.tsbuild_command/lint_command. Additive optional fields, fine for a dormant slice. When the orchestrator actually threads these into the agent payload (later slice), the repo-config docs underdocs/guides/should gain these knobs. Flag for the activation slice, not this one.
Documentation
No docs change is required for S1: all additions are optional persistence fields, an internal validation tightening, and allowlist metadata — none change a user-facing command or env var yet. No docs/guides / docs/design / Starlight-mirror edits are in the diff, so there is no mirror-sync obligation to check. The slice rationale is well-captured in #668. OK.
Tests & CI
- Bootstrap synth-coverage: N/A — the diff adds no CDK constructs/stacks/handlers and introduces no new CloudFormation resource types, so the ADR-002 bootstrap-policy checklist does not apply.
- New tests are solid and target the right trees (
cdk/test/handlers/shared/): the threevalidateMagicBytescases cover the non-UTF-8 reject, the 9 KB-preamble laundering reject, and the multi-byte-UTF-8 accept — i.e. failure paths, not just happy path. The newevery CDK descriptor has a shipped workflow filetest closes a real orphan-admission gap (accepted task that the agent then cannot run). I verified all five declared descriptor ids resolve to shippedagent/workflows/**YAMLs, so the test passes as written. Theid: '…'regex is anchored to 4-space indentation matching the currentDESCRIPTORSblock — a valid but slightly brittle source-scrape (it breaks if the table is ever reformatted or a nested 4-spaceid:appears); acceptable givenDESCRIPTORSis intentionally unexported. - I could not execute the jest suites in the review worktree (
node_modulesis not installed there andaws-cdk-lib/testhelpers/jest-autocleanis absent), so I verified the new tests by static analysis against source rather than by running them. CI on the PR is the authoritative gate.
Review agents run
- code-reviewer (style/guidelines): applied — code matches repo conventions; JSDoc is thorough; no routing issues (changes correctly land in
cdk/src/handlers/shared/). - silent-failure-hunter: applied — the one
catch {}invalidateMagicBytesdeliberately maps a decode failure toreturn false(fail-closed), which is correct and commented. No swallowed errors. - type-design-analyzer: applied — new
TaskRecordfields are optional and back-compatible;EXTENSION_TO_MIME/SUPPORTED_ATTACHMENT_EXTENSIONS_LABELare derived from the canonicalMIME_TO_EXTENSION(no drift by construction). Confirmed theseTaskRecordadditions are persistence-only and are not surfaced in any API response, so nocli/src/types.tssync is owed for this slice. - comment-analyzer: applied — this is where the PR-body/diff mismatch surfaced (nit #1). The in-file comments themselves are accurate.
- pr-test-analyzer: applied — see Tests section; coverage is appropriate for the live changes.
- /security-review: applied —
validation.ts(input gateway) and the model allowlist (policy gate) are in scope. Both changes are fail-closed / tightening; no IAM, Cedar, network, or secrets surface is touched. No findings.
Human heuristics
- Proportionality: pass — small, dormant foundation slice; complexity matches the carve strategy.
- Coherence: concern — the PR description's per-file claims do not match the
workflows.tsdiff and overstate dormancy forvalidation.ts(nit #1). The code itself is coherent. - Clarity: pass — names and JSDoc communicate intent well (
orchestration_id,depends_on,head_sha, etc. are precisely documented). - Appropriateness: pass — maintainable, derives reverse maps rather than duplicating, tests assert intended contract (reject laundered binary) not merely current behavior.
Net: safe to merge as part of the stack once the PR body is corrected. Approving in spirit; posting as COMMENT so the description-accuracy nits are addressed before the later, behavior-changing slices lean on this one.
… 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.
scottschreckengaust
left a comment
There was a problem hiding this comment.
Verdict: Approve with nits ✅
Re-review at head b619108a (the branch was reset/rebased and the PR body rewritten since my earlier COMMENT). This slice is a net security improvement with excellent tests and clean governance — it belongs. Two prior threads I raised are resolved (see below), and I found one new in-diff nit worth fixing plus a latent (pre-existing) gap the new comment inadvertently papers over.
Vision alignment
Advances the staged carve landing #247/#299 (both approved, P0) tracked under epic #668 (approved, P1). Bounded blast radius respected: the validateMagicBytes change is fail-closed (tightens admission), and the ~11 new TaskRecord fields are optional/back-compatible and provably dormant. Slicing keeps outcomes reviewable — this is exactly the "reviewable outcomes" tenet in action.
Prior threads — verified resolved
- validation.ts (live admission path): ✅ addressed. PR body now correctly labels this a live security tightening, and the hardening (full-buffer fatal UTF-8 + end-to-end NUL scan) is the right fix, with tests for the invalid-continuation-byte,
0xFF, 9000-byte-preamble laundering, and multi-byte-UTF-8-accept cases. - workflows.ts (2 allowlist entries only): ✅ addressed. Changelog corrected;
readOnly/DESCRIPTORSconfirmed already onmain. The bare↔us.pairing invariant now has a test in both directions.
Nits (non-blocking)
-
Inaccurate new comment claims a re-validation backstop that doesn't cover one path —
validation.ts(added lines ~388–390). The comment states a detected type is "handed straight toisAllowedMimeTypeand then tovalidateMagicBytes, which re-checks the WHOLE buffer. A wrong guess is caught one call later." That backstop does not run on the inline-attachment-without-content_typepath:validateAttachmentsgates the full check onif (decoded && att.content_type)(validation.ts:620), so an inline attachment with no declaredcontent_typegets its type fromdetectMimeTypeFromMagicBytes(8 KB-prefix NUL scan only, validation.ts:406) and is never run throughvalidateMagicBytes. Net effect: the very "8 KB clean ASCII + trailing binary" case the hardening targets can still be admitted on that path (bounded — inline data is ≤500 KB and still guardrail-screened downstream).- This gap is pre-existing (line 620 is not in this diff), so it's not a regression and not a blocker on this PR. But the PR's new comment asserts a safety property that isn't true. Suggested fix (cheap, and it makes the comment honest): drop the
&& att.content_typecondition at validation.ts:620 so inline detected types are also full-validated — closes the latent gap and makes the new comment accurate. Otherwise, soften the comment to stop claiming detection results are re-validated.
- This gap is pre-existing (line 620 is not in this diff), so it's not a regression and not a blocker on this PR. But the PR's new comment asserts a safety property that isn't true. Suggested fix (cheap, and it makes the comment honest): drop the
-
Doc example lists the wrong label order —
validation.ts(SUPPORTED_ATTACHMENT_EXTENSIONS_LABELcomment). Comment says"PNG, JPG, JPEG, TXT, ..."; the actual module-load value is"PNG, JPG, TXT, CSV, MD, JSON, PDF, LOG, JPEG"(thejpegalias is spread in last). Label is computed correctly; only the example is wrong. Thenew Set(...)dedup is redundant (object keys are already unique) but harmless. -
Newly-exported symbols have no consumers yet —
EXTENSION_TO_MIME,SUPPORTED_ATTACHMENT_EXTENSIONS_LABEL, and the newly-exportedMIME_TO_EXTENSION(validation.ts) have zero importers incdk/src,cli/src, or tests. Documented as scaffolding for later slices; acceptable for a staged carve, but it's speculative surface landing ahead of its code — and the derivation/label have no direct unit test (a future duplicate extension would silently collide inEXTENSION_TO_MIMEwith nothing to catch it). -
linear_issue_iddoc describes unwired behavior — types.ts documents it as "hoisted to top level ... at task-create time" for a GSI, but live code still reads/writes the nestedchannel_metadata.linear_issue_id(e.g.orchestrate-task.ts,linear-webhook-processor.ts); the top-level field is neither written nor read in this PR. Fine as forward-declaration, but the doc describes behavior that arrives in a later slice — consider a "wired in S_n" note like the orchestration fields have.
Documentation
No docs/guides or docs/design change required (internal contracts, dormant). .gitignore note (symlink node_modules) is well-explained. No Starlight mirror impact.
Tests & CI
CI green; PR reports 135 suites / 2509 tests. New tests are high-quality and cover the real risk paths (UTF-8 rejection, preamble laundering, multi-byte accept, orphan-descriptor admission, bare↔us. pairing). CDK bootstrap synth-coverage: not applicable — no new CloudFormation resource types (additive interface fields, allowlist strings, derived maps only). Minor gaps: empty-buffer text (currently returns true) and EXTENSION_TO_MIME/label are untested.
Review agents run
- code-reviewer / silent-failure-hunter (security lens on the admission path): run — surfaced the nit-1 re-validation gap; confirmed the decode is bounded by
MAX_ATTACHMENT_SIZE_BYTES(size checks are upstream in both hydration callers) and fail-closed. - type-design-analyzer: run — new optional fields are back-compatible; flagged
depends_onself-admitted dup-of-OrchestrationTable(dormant) and the coupled-optionals (screenshot_url/_preview_url,code_changed/answer_text) not expressed as invariants. - pr-test-analyzer: run — coverage assessed above.
- comment-analyzer: folded into nits 1, 2, 4 (comment accuracy).
- /security-review: covered by the admission-path pass above (no IAM/Cedar/network change in this diff).
- Omitted: none in scope.
Human heuristics
- Proportionality — pass. Change matches the problem; no over-abstraction.
- Coherence — pass. Fields/maps use consistent repo terms; parallel structure has real substance (tests, not boilerplate).
- Clarity — concern (nit 1): a security-path comment asserts a backstop that doesn't hold on one path. Names are otherwise clear.
- Appropriateness — pass. Maintainable; tests assert intended behavior (whole-buffer validation), not just current output.
| * - Here the answer is only a GUESS at what the caller probably sent, used to | ||
| * fill in a missing `content_type`. Guessing `text/plain` grants nothing: | ||
| * whatever this returns is handed straight to `isAllowedMimeType` and then | ||
| * to `validateMagicBytes`, which re-checks the WHOLE buffer. A wrong guess |
There was a problem hiding this comment.
The re-validation this comment promises does not run on the inline-attachment-without-content_type path: validateAttachments gates validateMagicBytes on if (decoded && att.content_type) (validation.ts:620), so a detected (guessed) type is never re-checked whole-buffer. An inline payload of ~8 KB clean ASCII + trailing binary (no declared content_type) is guessed text/plain here (8 KB-prefix NUL scan only, line 406) and admitted without the full-buffer check — the exact laundering case the hardening targets. Pre-existing (line 620 is not in this diff), so non-blocking, but the comment claims a safety property that is not true on that path. Cheap fix: drop && att.content_type at line 620 so inline detected types are full-validated too, which also makes this comment accurate.
|
Follow-up: cross-stack check of the new exports + the nit-1 validation gap (per reviewer request). I traced how the S1 additions are used by the downstream stacked PRs (checked at the current #662 tip). Two things worth recording:
Net: nit-1 remains a pre-existing, non-blocking gap localized to the legacy |
Slice 1 of a staged carve — foundation contracts
First slice landing the Linear sub-issue orchestration + Mode-B + iteration arc (developed on the
linear-vercelline) ontomain, in dependency-ordered, independently-reviewable pieces. This slice is mostly additive shared-contract changes that later slices reference. Two exceptions, both flagged in review and both live policy changes onmain(see the correctedvalidation.ts/workflows.tsentries below): the text-attachment validation gate is tightened fail-closed, and the workflow model allow-list is widened. Everything else is unwired and inert.Contents
types.ts— additive optionalTaskRecordfields the 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, plus a live security tightening ofvalidateMagicBytes(not additive).mainscanned only the first 8 KB for NUL bytes and never validated UTF-8 despite its comment claiming it did; text attachments must now decode as valid, NUL-free UTF-8 end to end. Both callers (jira-attachments.ts,resolve-url-attachments.ts) exist onmain, so this is a live admission path: contentmainaccepts (an 8 KB ASCII preamble followed by binary) is now rejected.detectMimeTypeFromMagicBytesdeliberately keeps the cheap 8 KB prefix scan — it is a detection heuristic whose outputvalidateMagicBytesre-checks in full; a comment there explains why the two differ. (Deliberately keeps the max-turns default unchanged; the arc's bump to a higher default is a live behavioral change that will come as its own PR.)workflows.ts— two model allow-list entries only (anthropic.claude-opus-4-8and itsus.inference-profile form). An earlier version of this description claimed it registered the planning/restack workflow definitions and thereadOnlyflag; that was wrong — those already exist onmain. Adding toWORKFLOW_MODEL_ALLOWLISTis itself a live policy widening. Note the allow-list and the Bedrock IAM grant inbedrock-models.tsare independent lists and a model must be on both; the grant for this id lands in S3 (feat(carve S3): agent runtime — deterministic issue feedback, build/lint gate, plan and restack workflows #654), so on S1/S2 the id is admissible but not invocable. Not reachable (the allow-list gates only models a workflow YAML pins, and none pin it on S1–S3) and documented at the list, with a test pinning the bare/us.pairing invariant.repo-config.ts— additive optionalbuild_command/lint_commandon the repo/blueprint config (per-repo build/lint verification commands; default to the platform commands when unset).Combination safety
Part of a staged sequence; every later slice is built + verified so that the union of all slices reproduces the deploy-verified
linear-verceltree. This slice's files are byte-identical to that source (comments aside).Gates
cdk compile + eslint + full jest (135 suites / 2509 tests) green.
🤖 Generated with Claude Code
Tracking
Slice S1 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) and #299 (auto-decomposition). Deliberately not
Closes— no single slice closes either; they close when S8 activates the arc.