Skip to content

feat(carve S5): orchestration DAG core, row store, and the channel abstraction (dormant) - #656

Open
isadeks wants to merge 3 commits into
carve/s4-linear-surfacefrom
carve/s5-dag-core-channel
Open

feat(carve S5): orchestration DAG core, row store, and the channel abstraction (dormant)#656
isadeks wants to merge 3 commits into
carve/s4-linear-surfacefrom
carve/s5-dag-core-channel

Conversation

@isadeks

@isadeks isadeks commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

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 a depends_on graph (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

  • 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 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

OrchestrationTable is 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) inside test/** 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 --noEmit clean
  • CDK: 2917 tests / 150 suites passing (up from 2598/137 on the base)
  • eslint clean on cdk and cli
  • Agent: 1437 tests passing, ruff check / ruff format --check / ty check all clean

Tracking

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) and #299 (auto-decomposition). Deliberately not Closes — no single slice closes either; they close when S8 activates the arc.

Stacked on #655 — review that first; this PR's diff is against its branch.

@isadeks isadeks changed the title S5: orchestration DAG core, row store, and the channel abstraction (dormant) feat(carve S5): orchestration DAG core, row store, and the channel abstraction (dormant) Jul 27, 2026
@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from 30aa161 to 0df8c2c Compare July 27, 2026 13:28
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from 08bc626 to d3c250f Compare July 27, 2026 13:28
@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from 0df8c2c to c58d51b Compare July 27, 2026 16:05
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from d3c250f to 7af69f3 Compare July 27, 2026 16:05
@isadeks

isadeks commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Automated review — carve S5 (#656)

Reviewed as its own diff only (carve/s4-linear-surface..carve/s5-dag-core-channel, 35 files, +8742/−29). Governance is noted once for the whole stack, not repeated here.

Slice standalone-ness: PASS

tsc --noEmit clean at this tip on its own base (compiler run, not inferred).

All 29 deletions checked — no export present on main is lost from any of the three pre-existing files this slice touches (linear-feedback.ts, linear-issue-lookup.ts, slack-api.ts). The changes to those three are additive plus a docs-only ABCA-ENG- example rename, which is the right direction for a public repo.

Nothing is introduced-but-unused. I checked all 16 new source modules for a consumer in cdk/src by the top of the stack — every one has at least one. No dead modules.

Dormancy claim: CONFIRMED — but not for the stated reason

The PR description's premise does not hold: ORCHESTRATION_TABLE_NAME does not exist anywhere in the tree at this slice. It first appears at #659 and only reaches linear-webhook-processor.ts at #662. So dormancy here cannot rest on that env-var gate. It rests entirely on non-wiring, and on that basis all three checks pass:

  1. No handler imports the new modules. The only importers of the 16 new modules are each other. I checked every hit outside shared/ — none.
  2. No construct is instantiated. OrchestrationTable does not reach cdk/src/stacks/agent.ts until feat(carve S8): wire the orchestration arc into the stack and activate it #662 (verified by grepping new OrchestrationTable( at every slice tip: absent at s5, s6, s7; present only at s8).
  3. Nothing in agent/src imports it — the diff touches no agent/ file.

One thing worth flagging, because it is the only place the dormancy argument is non-obvious: this slice modifies linear-feedback.ts, which is imported by live handlers (fanout-task-events.ts, linear-webhook-processor.ts, github-webhook-processor.ts, orchestrate-task.ts). It adds imports of two new modules (iteration-reply, orchestration-comment-trigger) used at 6 call sites inside it. I traced those sites: they are inside fetchRecentComments, upsertThreadedReply and sweepDecompositionNotes — three functions that do not exist on main and whose only callers at this slice are the new (unwired) orchestration channel modules. So no live code path reaches the new code. The claim holds, but it holds by one link, not by isolation — worth stating in the PR description rather than leaning on an env var that isn't there yet.


1. MAJOR — this slice turns off four lint rules for all 152 test files, to accommodate 13 new ones

cdk/eslint.config.mjs extends the test/**/*.ts override from one rule to five:

       '@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 (row, makeDdb) across sibling describe blocks."

But the override is repo-wide. At this slice there are 152 test files, 139 of which pre-existed this PR. So max-len and no-shadow are silently switched off across the entire existing suite in order to land 13 new files, and the relaxation persists through #662. no-shadow in particular is a correctness-adjacent rule in test code — a shadowed row or mock inside a nested describe is a common source of a test that asserts against the wrong fixture and still passes.

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 no-shadow on and relaxing only max-len.

2. MINOR — extractLinearIdentifierFromBranch is added here, but its only consumer arrives three slices later

linear-issue-lookup.ts gains extractLinearIdentifierFromBranch with a substantial doc comment explaining why branch-first routing matters ("in a stacked sub-issue orchestration, an agent's PR body commonly narrates the predecessor issue … so body-first routing misattributes the screenshot").

Traced across the stack, no source file outside its own module references it at s5, s6 or s7. Its only consumer — github-webhook-processor.ts — appears at #662. So this function ships dormant for three slices with a rationale that references behaviour not yet present.

Not harmful, but it belongs next to the caller that motivates it. Same class as the ADR-016 citations in #654 and the orchestration-reconciler-correctness.md references in #659: see the stack-level note about artifacts landing on the wrong side of a boundary.

3. MINOR — a raw internal UUID can surface to a user on the label fallback path

In orchestration-rollup.ts (added at #659, but the pattern originates in this slice's channel/panel design), the child label falls back through display_id ?? title ?? sub_issue_id. When a child has neither a display_id nor a title, the user-visible line renders the raw internal id:

- ✅ issue-uuid-1 — succeeded
- ❌ issue-uuid-2 — failed

I produced that by executing the renderer, not by reading it. The specific function it appears in is dead code (see the #659 review), but the same fallback shape is used by the live panel, so it is worth checking there before activation. Prefer a human-meaningful fallback ("a sub-issue") over leaking a UUID.


Verified clean

  • No unbounded Scan. There is no ScanCommand anywhere in the new orchestration modules. All three QueryCommand call sites in orchestration-store.ts carry LastEvaluatedKey/ExclusiveStartKey handling — the DAG/row store is properly paginated. (The one unpaginated Query in the stack is sumIterationCostForIssue, addressed in the feat(carve S7): the orchestration compute plane — reconciler, release, rollup #659 review.)
  • No IAM in this slice — it adds no construct that is instantiated, so no grants take effect.
  • Channel abstraction is well tested. orchestration-channel.test.ts exercises the adapters directly — 17 tests for the Linear adapter, 5 for the capability-limited Jira surface. My initial pass flagged orchestration-channel-linear.ts as untested based on the absence of a same-named test file; that was wrong — the coverage is consolidated into orchestration-channel.test.ts. Noting it because a filename-based coverage check gives a misleading picture of this slice.

Reviewed with Claude Code. Verification: tsc --noEmit on this slice's own base; all 29 deletions checked for lost exports against main; dormancy established by import-graph + construct-instantiation grep at each slice tip rather than via the env-var gate; linear-feedback.ts's new call sites traced to their callers; test-file counts taken from the tree at s4 and s5.

Note: this review was written directly rather than by the review agent assigned to it — that agent terminated mid-run without producing a result, so I reviewed the diff myself. Same for #653.

@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from c58d51b to efd1765 Compare July 27, 2026 17:28
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from 7af69f3 to 83344ef Compare July 27, 2026 17:28
@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from efd1765 to 430576f Compare July 27, 2026 17:51
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from 83344ef to cc75f8c Compare July 27, 2026 17:51
@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from 430576f to 227058d Compare July 27, 2026 18:40
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from cc75f8c to fedea84 Compare July 27, 2026 18:40
@isadeks

isadeks commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Review findings addressed

I 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: max-len and no-magic-numbers are defensible in test code (literal fixtures are the point), but no-shadow is correctness-adjacent for exactly the reason you gave.

So no-shadow stays enforced, and I fixed the shadows rather than silencing them. It found three across the suite — and every one was the hazard you described, two different shapes under one name:

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 #2extractLinearIdentifierFromBranch ships three slices before its consumer. Confirmed, not moved. Its consumer is in #662 and moving it there means moving the function and its tests across a boundary for no behavioural gain, whereas the branch-first routing rationale is genuinely about this module's contract. I have recorded it under the stack-level pattern you identified rather than treating it as a one-off — see the note on #659. If you would rather it travel with the caller, it is a clean move.

MINOR #3 — raw internal UUID on the label fallback path. Confirmed the same display_id ?? sub_issue_id shape exists on the live renderEpicPanel path, not just the dead one. Left as-is for now: a row carrying neither a display id nor a title is a data defect in its own right, and substituting something friendlier there would hide it. Noted rather than closed.

Gates: 2920 CDK tests, tsc/eslint clean with no-shadow on.

@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from 2a8e7b2 to dadf00f Compare July 28, 2026 01:42
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from fedea84 to 99f1007 Compare July 28, 2026 01:42
@isadeks

isadeks commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

Second-round review: the shadow renames hold up

A 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 row(/child( touched outside the target block. The file-level child (a SubIssueNode) keeps all ~40 of its call sites; childRow is confined to its own describe block.

The renamed tests still assert the real thing. The reviewer mutation-tested them three ways — removing the legacy-name fallback, breaking toChildRow alone, and inverting the name precedence — and each killed the tests with the expected/received values. So there is no wrong-fixture hazard hiding behind the rename, which was the specific risk no-shadow exists to catch.

no-shadow is genuinely enforced, verified via eslint.calculateConfigForFile rather than by reading the config: @typescript-eslint/no-shadow = [2] for test/**, no-magic-numbers correctly off in test and on in src, no second override block and no inline eslint-disable in the changed files.

Worth noting the rule earned its keep immediately: re-enabling it surfaced a third shadow in #659 (a local row building an EpicPanelRow shadowing a file-level row building an OrchestrationChildRow) that only appeared once the relaxation came off at the tip of the stack. Three shadows, every one two different shapes under a single name.

The two MINOR findings stand as noted, unchanged: extractLinearIdentifierFromBranch ships three slices before its consumer, and the display_id ?? sub_issue_id fallback can surface a raw UUID on the live panel path. Both recorded rather than closed — the first is a boundary-placement judgement, and the second is a data defect in its own right that a friendlier fallback would hide.

@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from dadf00f to 1a3f6bf Compare July 28, 2026 12:17
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from 99f1007 to 3c02a74 Compare July 28, 2026 12:17
@isadeks
isadeks marked this pull request as ready for review July 28, 2026 12:53
@isadeks
isadeks requested review from a team as code owners July 28, 2026 12:53

@scottschreckengaust scottschreckengaust left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

  1. orchestration-integration-node.ts:79 — the withIntegrationNode doc says the synthetic id is `<orchestrationId>#integration`, but the code (line 53/92) produces `<orchestrationId>__integration` via INTEGRATION_NODE_SUFFIX. Worse, the # form is the exact idempotency-key-invalid shape that lines 46-52 explicitly warn against (createTaskCore validates ${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.
  2. orchestration-store.ts:836loadOrchestration's doc says it "mirrors findOrchestrationIds's Scan pagination," but no findOrchestrationIds exists at this slice (only findOrchestrationChildByBranch). 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.
  3. (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.
  4. (pre-noted by author, agree) the display_id ?? sub_issue_id label 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); fetchSubIssueGraph and linearGraphSource deliberately fail loud rather than degrade. No silent swallowing found.
  • type-design-analyzer — RAN (in scope: many new types). DagValidationResult, FetchSubIssueGraphResult, OrchestrationGraphResult, and Channel are 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-feedback pattern; 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 explicit OrchestrationIdCollisionError fail-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.ts is 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 BatchWriteItem 25-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.

* all leaves) when there is more than one leaf.
*
* ``orchestrationId`` namespaces the synthetic node's id so it is unique +
* recognizable (``<orchestrationId>#integration``). The node carries no

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (comment accuracy): this says the synthetic id is `<orchestrationId>#integration`, but the code builds `<orchestrationId>__integration` (line 53/92 via INTEGRATION_NODE_SUFFIX). The # form is precisely the idempotency-key-invalid shape lines 46-52 warn against — createTaskCore validates ${orch}_${sub} against /^[a-zA-Z0-9_-]{1,128}$/ and a # would 400 the child. Please update the comment to __integration so it doesn't misdirect.

* (many children + accumulated ``ack#`` marker rows) would otherwise silently
* truncate — dropping children from the completion check / panel and stranding
* the epic. Follows ``LastEvaluatedKey`` to read all rows (mirrors
* ``findOrchestrationIds``'s Scan pagination).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (comment accuracy): references findOrchestrationIds's Scan pagination as a sibling to mirror, but no findOrchestrationIds exists at this slice (only findOrchestrationChildByBranch). It's a forward-reference to a later slice — a reader who greps for it finds nothing. Consider dropping the reference or noting it lands later.

isadeks added 3 commits July 28, 2026 19:47
Test code already has an exemption from the magic-numbers rule, on the
grounds that inline literals ARE the fixture. The same reasoning covers
two more rules that only produce noise in tests:

- no-shadow: sibling describe blocks reuse small helper names (`row`,
  `makeDdb`, `child`) for their own local fixture. Renaming each to a
  unique name makes the tests harder to read, not safer — nothing closes
  over the outer binding.
- max-len: an assertion or a fixture URL is one expression; wrapping it
  mid-string to satisfy a column limit hurts readability.

Correctness rules stay on everywhere. Scoped to `test/**` only, so
production code is unaffected.
…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.
@isadeks
isadeks force-pushed the carve/s4-linear-surface branch from 1a3f6bf to 0a2610a Compare July 28, 2026 18:52
@isadeks
isadeks force-pushed the carve/s5-dag-core-channel branch from 3c02a74 to e777e0d Compare July 28, 2026 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants