@websitelabs/n8n-nodes-software-teams 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/ARCHITECTURE.md +1232 -0
  2. package/CONTRACT.md +450 -0
  3. package/README.md +491 -0
  4. package/dist/agents/software-teams-architect.md +155 -0
  5. package/dist/agents/software-teams-backend.md +93 -0
  6. package/dist/agents/software-teams-codebase-mapper.md +67 -0
  7. package/dist/agents/software-teams-committer.md +90 -0
  8. package/dist/agents/software-teams-debugger.md +91 -0
  9. package/dist/agents/software-teams-dev-planner.md +175 -0
  10. package/dist/agents/software-teams-devops.md +92 -0
  11. package/dist/agents/software-teams-feedback-learner.md +118 -0
  12. package/dist/agents/software-teams-frontend.md +107 -0
  13. package/dist/agents/software-teams-game-ai-engineer.md +179 -0
  14. package/dist/agents/software-teams-game-art-pipeline.md +180 -0
  15. package/dist/agents/software-teams-game-designer.md +245 -0
  16. package/dist/agents/software-teams-game-devops.md +134 -0
  17. package/dist/agents/software-teams-game-engineer.md +146 -0
  18. package/dist/agents/software-teams-game-producer.md +288 -0
  19. package/dist/agents/software-teams-game-qa.md +297 -0
  20. package/dist/agents/software-teams-game-tech-artist.md +186 -0
  21. package/dist/agents/software-teams-head-engineering.md +37 -0
  22. package/dist/agents/software-teams-perf-analyst.md +124 -0
  23. package/dist/agents/software-teams-phase-researcher.md +75 -0
  24. package/dist/agents/software-teams-plan-checker.md +87 -0
  25. package/dist/agents/software-teams-planner.md +456 -0
  26. package/dist/agents/software-teams-pr-feedback.md +127 -0
  27. package/dist/agents/software-teams-pr-generator.md +107 -0
  28. package/dist/agents/software-teams-producer.md +203 -0
  29. package/dist/agents/software-teams-product-lead.md +51 -0
  30. package/dist/agents/software-teams-programmer.md +126 -0
  31. package/dist/agents/software-teams-qa-tester.md +165 -0
  32. package/dist/agents/software-teams-quality.md +153 -0
  33. package/dist/agents/software-teams-researcher.md +151 -0
  34. package/dist/agents/software-teams-security.md +126 -0
  35. package/dist/agents/software-teams-ux-designer.md +92 -0
  36. package/dist/agents/software-teams-verifier.md +87 -0
  37. package/dist/credentials/SoftwareTeamsApi.credentials.d.ts +18 -0
  38. package/dist/credentials/SoftwareTeamsApi.credentials.d.ts.map +1 -0
  39. package/dist/credentials/SoftwareTeamsApi.credentials.js +110 -0
  40. package/dist/credentials/SoftwareTeamsApi.credentials.js.map +1 -0
  41. package/dist/credentials/softwareTeamsApi.svg +14 -0
  42. package/dist/nodes/SoftwareTeamsAgent/SoftwareTeamsAgent.node.d.ts +23 -0
  43. package/dist/nodes/SoftwareTeamsAgent/SoftwareTeamsAgent.node.d.ts.map +1 -0
  44. package/dist/nodes/SoftwareTeamsAgent/SoftwareTeamsAgent.node.js +308 -0
  45. package/dist/nodes/SoftwareTeamsAgent/SoftwareTeamsAgent.node.js.map +1 -0
  46. package/dist/nodes/SoftwareTeamsAgent/softwareTeamsAgent.svg +18 -0
  47. package/dist/nodes/SoftwareTeamsCleanup/SoftwareTeamsCleanup.node.d.ts +24 -0
  48. package/dist/nodes/SoftwareTeamsCleanup/SoftwareTeamsCleanup.node.d.ts.map +1 -0
  49. package/dist/nodes/SoftwareTeamsCleanup/SoftwareTeamsCleanup.node.js +2635 -0
  50. package/dist/nodes/SoftwareTeamsCleanup/SoftwareTeamsCleanup.node.js.map +1 -0
  51. package/dist/nodes/SoftwareTeamsCleanup/SoftwareTeamsCleanup.svg +6 -0
  52. package/dist/nodes/SoftwareTeamsFinaliser/SoftwareTeamsFinaliser.node.d.ts +6 -0
  53. package/dist/nodes/SoftwareTeamsFinaliser/SoftwareTeamsFinaliser.node.d.ts.map +1 -0
  54. package/dist/nodes/SoftwareTeamsFinaliser/SoftwareTeamsFinaliser.node.js +231 -0
  55. package/dist/nodes/SoftwareTeamsFinaliser/SoftwareTeamsFinaliser.node.js.map +1 -0
  56. package/dist/nodes/SoftwareTeamsFinaliser/softwareTeamsFinaliser.svg +11 -0
  57. package/dist/nodes/SoftwareTeamsHitl/SoftwareTeamsHitl.node.d.ts +25 -0
  58. package/dist/nodes/SoftwareTeamsHitl/SoftwareTeamsHitl.node.d.ts.map +1 -0
  59. package/dist/nodes/SoftwareTeamsHitl/SoftwareTeamsHitl.node.js +366 -0
  60. package/dist/nodes/SoftwareTeamsHitl/SoftwareTeamsHitl.node.js.map +1 -0
  61. package/dist/nodes/SoftwareTeamsHitl/softwareTeamsHitl.svg +11 -0
  62. package/dist/nodes/SoftwareTeamsOrchestrator/SoftwareTeamsOrchestrator.node.d.ts +15 -0
  63. package/dist/nodes/SoftwareTeamsOrchestrator/SoftwareTeamsOrchestrator.node.d.ts.map +1 -0
  64. package/dist/nodes/SoftwareTeamsOrchestrator/SoftwareTeamsOrchestrator.node.js +373 -0
  65. package/dist/nodes/SoftwareTeamsOrchestrator/SoftwareTeamsOrchestrator.node.js.map +1 -0
  66. package/dist/nodes/SoftwareTeamsOrchestrator/softwareTeamsOrchestrator.svg +20 -0
  67. package/dist/nodes/SoftwareTeamsOutput/SoftwareTeamsOutput.node.d.ts +6 -0
  68. package/dist/nodes/SoftwareTeamsOutput/SoftwareTeamsOutput.node.d.ts.map +1 -0
  69. package/dist/nodes/SoftwareTeamsOutput/SoftwareTeamsOutput.node.js +2685 -0
  70. package/dist/nodes/SoftwareTeamsOutput/SoftwareTeamsOutput.node.js.map +1 -0
  71. package/dist/nodes/SoftwareTeamsOutput/SoftwareTeamsOutput.svg +6 -0
  72. package/dist/nodes/SoftwareTeamsPrFeedback/SoftwareTeamsPrFeedback.node.d.ts +22 -0
  73. package/dist/nodes/SoftwareTeamsPrFeedback/SoftwareTeamsPrFeedback.node.d.ts.map +1 -0
  74. package/dist/nodes/SoftwareTeamsPrFeedback/SoftwareTeamsPrFeedback.node.js +2655 -0
  75. package/dist/nodes/SoftwareTeamsPrFeedback/SoftwareTeamsPrFeedback.node.js.map +1 -0
  76. package/dist/nodes/SoftwareTeamsPrFeedback/softwareTeamsPrFeedback.svg +8 -0
  77. package/dist/nodes/SoftwareTeamsSlackHitl/SoftwareTeamsSlackHitl.node.d.ts +19 -0
  78. package/dist/nodes/SoftwareTeamsSlackHitl/SoftwareTeamsSlackHitl.node.d.ts.map +1 -0
  79. package/dist/nodes/SoftwareTeamsSlackHitl/SoftwareTeamsSlackHitl.node.js +198 -0
  80. package/dist/nodes/SoftwareTeamsSlackHitl/SoftwareTeamsSlackHitl.node.js.map +1 -0
  81. package/dist/nodes/SoftwareTeamsSlackHitl/softwareTeamsSlackHitl.svg +10 -0
  82. package/dist/nodes/SoftwareTeamsTrigger/SoftwareTeamsTrigger.node.d.ts +6 -0
  83. package/dist/nodes/SoftwareTeamsTrigger/SoftwareTeamsTrigger.node.d.ts.map +1 -0
  84. package/dist/nodes/SoftwareTeamsTrigger/SoftwareTeamsTrigger.node.js +2601 -0
  85. package/dist/nodes/SoftwareTeamsTrigger/SoftwareTeamsTrigger.node.js.map +1 -0
  86. package/dist/nodes/SoftwareTeamsTrigger/SoftwareTeamsTrigger.node.svg +6 -0
  87. package/dist/nodes/SoftwareTeamsWorkspace/SoftwareTeamsWorkspace.node.d.ts +20 -0
  88. package/dist/nodes/SoftwareTeamsWorkspace/SoftwareTeamsWorkspace.node.d.ts.map +1 -0
  89. package/dist/nodes/SoftwareTeamsWorkspace/SoftwareTeamsWorkspace.node.js +175 -0
  90. package/dist/nodes/SoftwareTeamsWorkspace/SoftwareTeamsWorkspace.node.js.map +1 -0
  91. package/dist/nodes/SoftwareTeamsWorkspace/softwareTeamsWorkspace.svg +13 -0
  92. package/dist/src/execution/single-turn.d.ts +6 -0
  93. package/dist/src/execution/single-turn.d.ts.map +1 -0
  94. package/dist/src/execution/single-turn.js +2662 -0
  95. package/dist/src/execution/single-turn.js.map +1 -0
  96. package/dist/src/hitl/channels.d.ts +48 -0
  97. package/dist/src/hitl/channels.d.ts.map +1 -0
  98. package/dist/src/hitl/channels.js +297 -0
  99. package/dist/src/hitl/channels.js.map +1 -0
  100. package/dist/src/hitl/conversation-state.d.ts +45 -0
  101. package/dist/src/hitl/conversation-state.d.ts.map +1 -0
  102. package/dist/src/hitl/conversation-state.js +69 -0
  103. package/dist/src/hitl/conversation-state.js.map +1 -0
  104. package/dist/src/hitl/slack.d.ts +32 -0
  105. package/dist/src/hitl/slack.d.ts.map +1 -0
  106. package/dist/src/hitl/slack.js +202 -0
  107. package/dist/src/hitl/slack.js.map +1 -0
  108. package/dist/src/ingestion/context.d.ts +38 -0
  109. package/dist/src/ingestion/context.d.ts.map +1 -0
  110. package/dist/src/ingestion/context.js +2501 -0
  111. package/dist/src/ingestion/context.js.map +1 -0
  112. package/dist/src/ingestion/pr-feedback.d.ts +48 -0
  113. package/dist/src/ingestion/pr-feedback.d.ts.map +1 -0
  114. package/dist/src/ingestion/pr-feedback.js +85 -0
  115. package/dist/src/ingestion/pr-feedback.js.map +1 -0
  116. package/dist/src/n8n-cast.d.ts +11 -0
  117. package/dist/src/n8n-cast.d.ts.map +1 -0
  118. package/dist/src/n8n-cast.js +17 -0
  119. package/dist/src/n8n-cast.js.map +1 -0
  120. package/dist/src/orchestration/run-state/global-store.d.ts +7 -0
  121. package/dist/src/orchestration/run-state/global-store.d.ts.map +1 -0
  122. package/dist/src/orchestration/run-state/global-store.js +27 -0
  123. package/dist/src/orchestration/run-state/global-store.js.map +1 -0
  124. package/dist/src/orchestration/run-state/ordering.d.ts +14 -0
  125. package/dist/src/orchestration/run-state/ordering.d.ts.map +1 -0
  126. package/dist/src/orchestration/run-state/ordering.js +59 -0
  127. package/dist/src/orchestration/run-state/ordering.js.map +1 -0
  128. package/dist/src/orchestration/run-state/persistence.d.ts +9 -0
  129. package/dist/src/orchestration/run-state/persistence.d.ts.map +1 -0
  130. package/dist/src/orchestration/run-state/persistence.js +29 -0
  131. package/dist/src/orchestration/run-state/persistence.js.map +1 -0
  132. package/dist/src/orchestration/run-state/planning.d.ts +17 -0
  133. package/dist/src/orchestration/run-state/planning.d.ts.map +1 -0
  134. package/dist/src/orchestration/run-state/planning.js +117 -0
  135. package/dist/src/orchestration/run-state/planning.js.map +1 -0
  136. package/dist/src/orchestration/run-state/readiness.d.ts +20 -0
  137. package/dist/src/orchestration/run-state/readiness.d.ts.map +1 -0
  138. package/dist/src/orchestration/run-state/readiness.js +105 -0
  139. package/dist/src/orchestration/run-state/readiness.js.map +1 -0
  140. package/dist/src/orchestration/run-state/shapes.d.ts +53 -0
  141. package/dist/src/orchestration/run-state/shapes.d.ts.map +1 -0
  142. package/dist/src/orchestration/run-state/shapes.js +3 -0
  143. package/dist/src/orchestration/run-state/shapes.js.map +1 -0
  144. package/dist/src/orchestration/run-state/transitions.d.ts +46 -0
  145. package/dist/src/orchestration/run-state/transitions.d.ts.map +1 -0
  146. package/dist/src/orchestration/run-state/transitions.js +133 -0
  147. package/dist/src/orchestration/run-state/transitions.js.map +1 -0
  148. package/dist/src/orchestration/run-state.d.ts +8 -0
  149. package/dist/src/orchestration/run-state.d.ts.map +1 -0
  150. package/dist/src/orchestration/run-state.js +35 -0
  151. package/dist/src/orchestration/run-state.js.map +1 -0
  152. package/dist/src/output/github.d.ts +39 -0
  153. package/dist/src/output/github.d.ts.map +1 -0
  154. package/dist/src/output/github.js +2492 -0
  155. package/dist/src/output/github.js.map +1 -0
  156. package/dist/src/repo/git.d.ts +71 -0
  157. package/dist/src/repo/git.d.ts.map +1 -0
  158. package/dist/src/repo/git.js +207 -0
  159. package/dist/src/repo/git.js.map +1 -0
  160. package/dist/src/repo/merge.d.ts +36 -0
  161. package/dist/src/repo/merge.d.ts.map +1 -0
  162. package/dist/src/repo/merge.js +133 -0
  163. package/dist/src/repo/merge.js.map +1 -0
  164. package/dist/src/repo/repo-context.d.ts +23 -0
  165. package/dist/src/repo/repo-context.d.ts.map +1 -0
  166. package/dist/src/repo/repo-context.js +10 -0
  167. package/dist/src/repo/repo-context.js.map +1 -0
  168. package/dist/src/repo/teardown.d.ts +38 -0
  169. package/dist/src/repo/teardown.d.ts.map +1 -0
  170. package/dist/src/repo/teardown.js +171 -0
  171. package/dist/src/repo/teardown.js.map +1 -0
  172. package/dist/src/repo/validate.d.ts +4 -0
  173. package/dist/src/repo/validate.d.ts.map +1 -0
  174. package/dist/src/repo/validate.js +42 -0
  175. package/dist/src/repo/validate.js.map +1 -0
  176. package/package.json +73 -0
@@ -0,0 +1,1232 @@
1
+ # ADR-001: `@websitelabs/n8n-nodes-software-teams` — package shape, execution model & canvas handoff
2
+
3
+ > **Status:** Accepted (W1 foundation). Pairs with [`CONTRACT.md`](./CONTRACT.md),
4
+ > which pins the inter-node data envelope this document references.
5
+ > **Plan:** `1-01-n8n-nodes` · **Task:** T1 (`software-teams-architect`).
6
+ > **Decides:** AC2 (single-turn execution model) and the package/canvas shape;
7
+ > defers AC3's wire format to `CONTRACT.md`. Answers AC2/AC3 from docs alone.
8
+
9
+ This is the single source of truth every downstream slice builds against. It
10
+ fixes four things: (a) the package layout, (b) the **inline single-turn
11
+ execution model**, (c) the **canvas handoff** that replaces Claude's native Task
12
+ tool, and (d) the open ecosystem questions T2 must resolve. No production
13
+ TypeScript is written here.
14
+
15
+ ---
16
+
17
+ ## Context
18
+
19
+ Software Teams runs today in three places: an interactive Claude Code session, the
20
+ `software-teams …` CLI, and the GitHub-Actions headless runner (`action/`). Agent
21
+ collaboration (Task tool / SendMessage) only works *inside* a live session — it
22
+ cannot be split across processes. The teams want event-driven, composable agents
23
+ on a visual canvas with Slack HITL. n8n is the substrate; each specialist becomes
24
+ a node and agents hand off **node-to-node** over an explicit JSON contract rather
25
+ than through Claude's in-session Task tool.
26
+
27
+ The grounding primitive already exists: `spawnClaude()` in
28
+ [`src/utils/claude.ts`](../src/utils/claude.ts) runs
29
+ `claude -p --output-format stream-json` via `Bun.spawn`, streams events, and
30
+ returns `{ exitCode, response }`. The GHA path
31
+ ([`src/commands/action/run.ts`](../src/commands/action/run.ts) ~L820–940) drives a
32
+ **full, multi-turn session** with `Task` enabled and a conversation-history
33
+ prompt. n8n nodes need the opposite: **one turn, Task disabled, structured I/O.**
34
+
35
+ ---
36
+
37
+ ## Decision A — Package layout
38
+
39
+ `@websitelabs/n8n-nodes-software-teams` is a standard n8n community-node npm package
40
+ living under `n8n/` in this repo. It **depends on the root `@websitelabs/software-teams`
41
+ package** to reuse one source of truth for `spawnClaude`, the agent catalogue
42
+ (`agents/*.md`), and the ClickUp/Datadog PII-scrubbing fetch utils — the n8n
43
+ package never forks that logic.
44
+
45
+ ```
46
+ n8n/
47
+ ├── ARCHITECTURE.md ← this file (T1)
48
+ ├── CONTRACT.md ← inter-node envelope (T1, implemented by T3)
49
+ ├── package.json ← name, n8n.nodes[] / n8n.credentials[] registry (T4)
50
+ ├── tsconfig.json ← build config (T4)
51
+ ├── .eslintrc.js ← eslint-plugin-n8n-nodes-base (T4/T12)
52
+ ├── credentials/
53
+ │ └── SoftwareTeamsApi.credentials.ts ← ANTHROPIC_API_KEY + optional model (T4)
54
+ ├── nodes/
55
+ │ ├── Agent/ ← Agent node: one specialist, one turn (T5)
56
+ │ ├── Orchestrator/ ← epic → waved breakdown → drives agents (T9)
57
+ │ ├── Trigger/ ← ClickUp-label / Datadog ingestion (T6)
58
+ │ └── GitHubOutput/ ← final PR/issue (T7)
59
+ ├── shared/ ← envelope types + adapter wrapper, canvas helpers
60
+ │ ├── envelope.ts ← NodeEnvelope type (mirrors CONTRACT.md) (T3)
61
+ │ └── runAgentTurn.ts ← single-turn wrapper over spawnClaude (T3, src-backed)
62
+ └── __tests__/ ← contract + node tests (T8/T11/T15)
63
+ ```
64
+
65
+ The **single-turn execution adapter is implemented in `src/`** (per the
66
+ orchestration's W2 split: T3 touches `src/`), re-exported through `n8n/shared/`.
67
+ This keeps the adapter unit-testable by the root `bun test` and importable by both
68
+ runtimes.
69
+
70
+ **Node style — recommendation pending T2.** Agent and Orchestrator nodes shell out
71
+ to a binary and stream output, which the **programmatic** style (an `execute()`
72
+ method on `INodeType`) supports; the **declarative** style targets pure REST and
73
+ cannot host inline process spawning. **Recommend programmatic for Agent/Orchestrator/
74
+ GitHubOutput; declarative is acceptable for any pure-REST trigger helper.** T2
75
+ confirms against current n8n conventions before T4/T5 build.
76
+
77
+ ---
78
+
79
+ ## Decision B — Inline single-turn execution model (AC2, addresses R-03)
80
+
81
+ Each Agent node runs **exactly one specialist turn** by shelling out to `claude`
82
+ in the n8n worker, built on `spawnClaude`. The contract for the wrapper
83
+ (`runAgentTurn`, T3) is:
84
+
85
+ 1. **Single turn.** One `claude -p` call. No conversation loop; no follow-up turns
86
+ inside the node. A multi-step workflow is multiple nodes, not multiple turns.
87
+ 2. **Task tool disabled.** The wrapper passes an `allowedTools` list that **omits
88
+ `Task`** — i.e. `DEFAULT_ALLOWED_TOOLS` minus `"Task"`. This is the mechanical
89
+ enforcement of "no internal sub-agent spawning": a node cannot fan out to
90
+ sub-agents, so all multi-agent work flows over the n8n canvas (Decision C).
91
+ 3. **Agent selection.** The node's `agentId` (e.g. `software-teams-frontend`) selects a
92
+ specialist from `agents/*.md`; the agent's system prompt/persona is loaded the
93
+ same way the existing runtimes resolve it.
94
+ 4. **Prompt assembly.** The wrapper composes the `claude -p` prompt string from the
95
+ envelope's `input` per `CONTRACT.md` → *Upstream-context merge* (a fenced JSON
96
+ `## Upstream context` block prepended to `input.prompt`). Unlike the GHA path it
97
+ does **not** inject GitHub conversation history.
98
+ 5. **Structured output capture.** `spawnClaude` returns `{ exitCode, response }`.
99
+ The wrapper maps:
100
+ - `response` → `result.text`,
101
+ - `exitCode === 0` → `status: 'ok'`; `exitCode !== 0` → `status: 'error'`,
102
+ - a detected human-input request → `status: 'needs-input'` (the hook T10's Slack
103
+ HITL keys on; marker convention pinned in `CONTRACT.md`),
104
+ - `artifacts` defaults to `[]`; T7's GitHub node appends `{ type, url }` refs.
105
+ 6. **Model selection per node (R-03).** The node exposes a `model` parameter passed
106
+ through to `spawnClaude({ model })`, letting expensive agents downshift. Cost is
107
+ one full invocation per node — documented in `n8n/` docs (T14).
108
+ 7. **Self-hosted fail-fast (AC9, R-01).** `runAgentTurn` calls `findClaude()` first;
109
+ if the binary or `ANTHROPIC_API_KEY` is absent it throws a clear, actionable
110
+ error before any work — surfacing the self-hosted constraint at execution time.
111
+
112
+ **Why not reuse the GHA session runner?** It is multi-turn, Task-enabled, and
113
+ GitHub-comment-coupled. Nodes need a stateless, Task-disabled, envelope-in /
114
+ envelope-out turn. Single-turn is **new logic** layered on the same `spawnClaude`
115
+ primitive, not a refactor of `run.ts`.
116
+
117
+ ---
118
+
119
+ ## Decision C — Canvas handoff replaces the native Task tool (AC4, addresses R-04)
120
+
121
+ With `Task` disabled inside every node, agent-to-agent collaboration is
122
+ re-implemented as **n8n data flow**: node A's output port → node B's input port,
123
+ both speaking the `NodeEnvelope` (`CONTRACT.md`). Handoff = the downstream node
124
+ folds the upstream node's `result`/`artifacts` into its own `input.context`
125
+ (see `CONTRACT.md` → *Upstream-context merge* and the worked example).
126
+
127
+ **Orchestrator → agent delegation.** The Orchestrator node (T9) accepts an
128
+ epic/goal, re-derives the waved task breakdown the in-session orchestrator would
129
+ have produced, and **emits one envelope per planned sub-task** with `agentId` set
130
+ to the chosen specialist and `input.prompt` set to that sub-task's brief. On the
131
+ canvas these route to agent nodes by one of two equivalent patterns:
132
+
133
+ - **Static wiring** — the designer wires `Orchestrator → AgentA → AgentB`; each
134
+ agent node is pre-bound to one `agentId`. Simple, fully visual.
135
+ - **Dynamic routing** — a `Switch`/`Filter` keyed on the envelope's `agentId` fans
136
+ each item to the matching agent node. Lets one canvas serve a variable plan.
137
+
138
+ Either way the Orchestrator owns sequencing (waves/deps) explicitly — there is no
139
+ hidden Task-tool graph. This is the deliberate AC4 design and the R-04 mitigation;
140
+ the `contract-check` gate guards the envelope the whole graph depends on.
141
+
142
+ **Partial-failure & resume (addresses R-05).** Every envelope carries an immutable
143
+ `correlationId` and a `status`. A node emitting `status: 'error'` short-circuits its
144
+ branch; a node emitting `status: 'needs-input'` parks the run for Slack HITL (T10)
145
+ and resumes the **same** `correlationId` once a human replies. The Orchestrator
146
+ persists run state keyed by `correlationId` (T9) so a half-done run is resumable and
147
+ re-runs are idempotent. `correlationId` is the join key for both run-state (T9) and
148
+ Slack resume (T10).
149
+
150
+ ---
151
+
152
+ ## Risk resolutions (explicit design choices)
153
+
154
+ | Risk | Resolved by |
155
+ |------|-------------|
156
+ | **R-03** cost/latency per node | Single-turn + Task-disabled caps per-node work; per-node `model` selection; Orchestrator parallelises independent waves (Decision B). |
157
+ | **R-04** loss of native Task handoff | Explicit `NodeEnvelope` + canvas data-flow handoff; Orchestrator re-derives waves/deps; `contract-check` gate guards drift (Decision C). |
158
+ | **R-05** partial failure mid-run | `correlationId` + `status` on every envelope; `error` short-circuits, `needs-input` parks for HITL; Orchestrator persists state for resumable/idempotent re-runs (Decision C). |
159
+
160
+ ---
161
+
162
+ ## Open ecosystem questions for T2 (scoping handoff)
163
+
164
+ T2 (`software-teams-researcher`) must resolve, before T4/T5 build:
165
+
166
+ 1. **Node style** — confirm programmatic (`execute()` on `INodeType`) is current
167
+ best practice for inline-spawning nodes; capture any declarative constraints.
168
+ 2. **Credential API** — how to define the `SoftwareTeamsApi` credential type
169
+ (`ANTHROPIC_API_KEY`, optional default `model`), and the exact mechanism for
170
+ exporting a credential into the child `claude` process's env (R-02: secrets via
171
+ credentials only, never node params, never echoed into the envelope/logs).
172
+ 3. **Registration & build** — the `package.json` `n8n.nodes[]` / `n8n.credentials[]`
173
+ block, file-naming conventions, and a `tsc` build that satisfies
174
+ `eslint-plugin-n8n-nodes-base` (AC8). Confirm the lint ruleset and any
175
+ community-node packaging requirements.
176
+ 4. **Slack wait/resume primitive** — the n8n mechanism for pause→resume
177
+ (`Wait` node + resume webhook vs. `putExecutionToWait`) that T10 will build the
178
+ HITL state machine on, and how execution state survives the wait (R-05).
179
+
180
+ ---
181
+ ---
182
+
183
+ # ADR-002: Repo-scoped coding execution — queue-safe × worktree-per-agent × Finaliser-resolves-all-conflicts
184
+
185
+ > **Status:** Accepted (plan `1-04-n8n-repo-execution`, T1, `software-teams-architect`).
186
+ > Extends ADR-001 (single-turn model, canvas handoff, `correlationId` resume) and the
187
+ > [`CONTRACT.md`](./CONTRACT.md) addendum (the additive `repoContext`/`changeRef`
188
+ > fields). **This is the single source of truth every repo-execution slice (T2, T4,
189
+ > T7, T8, T9) builds against** — each one implements the decisions fixed here and
190
+ > re-opens NONE of them. No production TypeScript is written here.
191
+ > **Decides:** AC5, AC6, AC8, AC9, AC10, AC11. **Resolves** R-15, R-16, R-17, R-18
192
+ > (and reaffirms R-02). No "OR" / "and/or" is left for any implementer.
193
+
194
+ ---
195
+
196
+ ## Context
197
+
198
+ ADR-001 made Software Teams specialists run as n8n nodes, one single turn each,
199
+ handing off node-to-node over the `NodeEnvelope`. It cannot yet make **real code
200
+ changes against a target repository**. The spec (`n8n-repo-execution.spec.md`)
201
+ names four gaps and two shaping constraints:
202
+
203
+ 1. **No working directory.** `runAgentTurn` (`src/execution/single-turn.ts`) calls
204
+ `spawnClaude` with no `cwd`; every turn runs in the worker's `process.cwd()`.
205
+ Nothing clones/checks out the configured repo.
206
+ 2. **No portable change.** Agents emit `result.text` only; nothing captures the
207
+ files an agent changed in a form another worker can re-apply.
208
+ 3. **No aggregation.** The Orchestrator fans out one envelope per task and persists
209
+ run-state to workflow static data, but never collects results — nothing to merge.
210
+ 4. **Queue-mode constraint.** n8n **queue mode** dispatches items across workers, so a
211
+ working copy made on one worker is not on the next. The run must be re-establishable
212
+ on **any** worker — never assuming one worker or a shared `/tmp`.
213
+ 5. **DAG-only constraint.** An n8n canvas is a directed acyclic graph; there is **no
214
+ native return edge** from an Agent node back to the Orchestrator. Results cannot be
215
+ "collected back" up an edge.
216
+
217
+ This ADR fixes six decisions (D–I) that reconcile these into one buildable design.
218
+
219
+ ---
220
+
221
+ ## Decision D — `RepoContext` shape + the SINGLE threading mechanism (AC5, AC10; resolves the T1/T2 "OR")
222
+
223
+ The run's repository checkout is described by ONE typed interface. Its members are
224
+ fixed and exhaustive:
225
+
226
+ ```ts
227
+ export interface RepoContext {
228
+ /** Clone URL of the target repo (https/ssh). NEVER contains an embedded token (R-02). */
229
+ cloneUrl: string;
230
+ /** Canonical "owner/repo" — the validated form used for gh/PR addressing. */
231
+ ownerRepo: string;
232
+ /** Branch the run is based on (e.g. "main"). All worktrees fork from here. */
233
+ baseBranch: string;
234
+ /** ADR-001 run id; the join key for run-state + aggregation. Carried unchanged. */
235
+ correlationId: string;
236
+ /** Absolute path to THIS turn's isolated git worktree on the current worker. */
237
+ worktreePath: string;
238
+ /** This turn's captured portable change (Decision E). Absent until the turn produces one. */
239
+ changeRef?: ChangeRef;
240
+ }
241
+ ```
242
+
243
+ **Amendment (revision 2 follow-up):** Repo *coordinates* (`cloneUrl`, `ownerRepo`,
244
+ `baseBranch`) cross Workspace→Agent via the additive optional top-level `repo` field
245
+ (`RepoDescriptor`) on the envelope — the DAG canvas has no other channel for seeding
246
+ these from the Workspace node to downstream Agent nodes. The `repo` field is non-secret
247
+ and never reaches `assemblePrompt` because it is a top-level sibling of `input`. The
248
+ full `RepoContext` (which adds the off-wire `worktreePath`) remains off-wire; each
249
+ Agent node constructs it locally from `envelope.repo` + `correlationId` + a locally
250
+ created worktree, and owns the worktree lifecycle. The thread-via-typed-param mechanism
251
+ for `runAgentTurn` is unchanged.
252
+
253
+ **Threading mechanism — CHOSEN: a typed optional parameter on `runAgentTurn`.**
254
+ `runAgentTurn` gains exactly one new optional argument, `repoContext?: RepoContext`.
255
+ This is the single mechanism; the alternative (a reserved envelope field consumed by
256
+ the adapter) is **rejected**.
257
+
258
+ - **Chosen** — typed param: explicit, type-checked at the call site, **never on the
259
+ wire**, and trivially impossible to leak into the prompt (it is a function
260
+ argument, not part of the envelope `assemblePrompt` reads). The Workspace/Agent
261
+ node constructs it and passes it; with no argument, `runAgentTurn` behaves
262
+ **byte-for-byte as today** (back-compat, AC10).
263
+ - **Rejected** — reserved envelope field: would put repo internals on the wire,
264
+ require `assemblePrompt` to explicitly exclude a key, and risk serialisation into
265
+ logs. The typed param removes the leak surface entirely.
266
+
267
+ So `runAgentTurn(input: NodeEnvelope, repoContext?: RepoContext)`. T2 implements this
268
+ ONE mechanism — no design choice remains. Inside the adapter, when `repoContext` is
269
+ present, `cwd` passed to `spawnClaude` is `repoContext.worktreePath` (resolves gap 1).
270
+
271
+ **Prompt isolation (tactical default `workDir_threading`).** `assemblePrompt`
272
+ ([`src/execution/single-turn.ts`](../src/execution/single-turn.ts)) composes the
273
+ `claude -p` string from `input.prompt` + the §4 `## Upstream context` block **only**.
274
+ `RepoContext` is NOT part of `input`, NOT part of `input.context`, and NOT a parameter
275
+ `assemblePrompt` receives — it is consumed solely to set `cwd`. There is therefore no
276
+ path by which any `RepoContext` member (including `cloneUrl`) reaches the model prompt.
277
+ T2 MUST keep `assemblePrompt`'s signature/inputs free of `RepoContext` and assert this
278
+ in the back-compat test. **(Mitigates R-18: the field never bleeds into prompt/§4 merge.)**
279
+
280
+ ---
281
+
282
+ ## Decision E — The SINGLE canonical portable-change representation: `changeRef` (AC5, AC9; resolves the T1/T4/T9 "OR")
283
+
284
+ Each agent's file changes are captured as ONE canonical, self-contained artifact —
285
+ **base64-encoded `git format-patch` bytes** — carried on the envelope as `changeRef`:
286
+
287
+ ```ts
288
+ export interface ChangeRef {
289
+ /** Discriminant — the ONE canonical form. Reserved for future additive kinds. */
290
+ kind: 'format-patch';
291
+ /** base64 of `git format-patch` output (one or more patch files concatenated). */
292
+ patchBase64: string;
293
+ }
294
+ ```
295
+
296
+ **Why this one, and the trade-off vs the rejected option:**
297
+
298
+ - **Chosen** — base64 `git format-patch` bytes: fully **self-contained**. It needs
299
+ **no shared storage**, so it is **queue-mode-safe** by construction — any worker
300
+ re-establishes the change with `git apply` / `git am` regardless of which worker
301
+ produced it. The patch rides the envelope as plain JSON-safe text.
302
+ Trade-off accepted: a larger envelope payload for big diffs.
303
+ - **Rejected** — a commit SHA pushed to a per-agent ref on mandated shared storage
304
+ (a shared remote/volume): a **smaller** envelope, but it requires every worker to
305
+ reach the same remote/volume, reintroducing the single-worker / shared-`/tmp`
306
+ assumption the spec forbids (constraint 4). The size win does not justify breaking
307
+ queue-mode safety.
308
+
309
+ T4 **captures** exactly this (`git format-patch base..HEAD` in the worktree →
310
+ base64), and T9 **applies** exactly this (`git apply`/`git am` of the decoded bytes).
311
+ Neither slice references a second representation; the `kind` discriminant is the only
312
+ extension point. **(Mitigates R-15: filesystem worktree loss mid-run is recoverable
313
+ because the change lives on the envelope, not the disk.)**
314
+
315
+ ---
316
+
317
+ ## Decision F — Aggregation topology: FORWARD to the Finaliser via run-state on static data (AC5, AC8; resolves the T8 DAG gap)
318
+
319
+ An n8n canvas is **DAG-only** — there is no return edge from an Agent node to the
320
+ Orchestrator, so completed envelopes cannot flow back. The ONE buildable topology:
321
+
322
+ ```
323
+ Orchestrator ──(one envelope per task)──▶ Agent (worktree turn)
324
+
325
+ │ T8 aggregation transition:
326
+ │ getWorkflowStaticData('node').runs[correlationId]
327
+ │ .tasks[taskId] ← { status, changeRef, detail }
328
+ │ (serialiseRunState writes the plain object)
329
+
330
+ workflow static data (runs[correlationId]: RunState)
331
+
332
+ │ T9 reads directly:
333
+ │ deserialiseRunState(runs[correlationId])
334
+
335
+ Finaliser ──▶ merge → push → branch artifact → summary
336
+ ```
337
+
338
+ **Mechanism — reusing the ADR-001/T9 run-state primitive verbatim:**
339
+
340
+ - The Orchestrator already writes `staticData['runs'][correlationId] = serialiseRunState(plan.state)`
341
+ where `RunState = { correlationId, createdAt, tasks: RunTaskState[] }`
342
+ (`src/orchestration/run-state/shapes.ts`). Each `RunTaskState` is keyed by `taskId`
343
+ and carries `agent`, `status`, `detail`.
344
+ - **Writer = T8** (the aggregation transition, invoked on each returning Agent item).
345
+ It reads `getWorkflowStaticData('node')`, deserialises `runs[correlationId]`, finds
346
+ the task entry by `taskId` (and `agentId` where a wave has parallel agents), records
347
+ the agent's terminal `status` and its `changeRef`, and re-serialises via
348
+ `serialiseRunState`. The key is `correlationId` + `taskId`/`agentId`. `RunTaskState`
349
+ gains one additive optional field, `changeRef?: ChangeRef`, to carry the captured patch.
350
+ - **Reader = T9** (the Finaliser). It calls `getWorkflowStaticData('node')`, runs
351
+ `deserialiseRunState(runs[correlationId])`, and **enumerates every task's `changeRef`**
352
+ to build the merge set. It reads run-state directly; it does not receive envelopes
353
+ back from agents.
354
+
355
+ **Direction is one-way and explicit:** aggregation flows **FORWARD to the Finaliser**
356
+ (which has the run-state read input on the canvas), **never backward to the
357
+ Orchestrator**. The Orchestrator stays a pure fan-out; no new wire-contract, no return
358
+ edge. **(Mitigates R-04: aggregation rides the existing `correlationId` run-state
359
+ mechanism, additive only.)**
360
+
361
+ ---
362
+
363
+ ## Decision G — Workspace node boundary (AC1, AC5; new node)
364
+
365
+ The **Workspace node** establishes the run's checkout and seeds `RepoContext`.
366
+
367
+ - **Inputs:** target repo (`owner/repo` or clone URL) + base branch from node params
368
+ (validated/sanitised per T5, R-08); the inbound envelope (carries `correlationId`).
369
+ - **Fail-fast (R-01):** verifies the `git` binary is present before any work and
370
+ throws a clear, actionable error if absent — the same pattern ADR-001 §B.7 used for
371
+ `claude`. (T9 adds the equivalent fail-fast for `gh`.)
372
+ - **What it does:** clones the target repo at `baseBranch` (shallow per R-03) into a
373
+ run-scoped checkout, then — for each Agent turn — a `git worktree` is forked from that
374
+ checkout so parallel agents never collide on one index/working tree
375
+ (worktree-per-agent). The worktree's absolute path becomes `RepoContext.worktreePath`.
376
+ - **What it seeds:** it constructs the `RepoContext` (`cloneUrl`, `ownerRepo`,
377
+ `baseBranch`, `correlationId`, `worktreePath`) for downstream Agent turns per the
378
+ Decision-D typed-param mechanism. `RepoContext` is **not** placed on the envelope —
379
+ it is threaded as the `runAgentTurn` argument. `changeRef` is left absent until a
380
+ turn produces a change. The GitHub token is **never** seeded into `RepoContext`
381
+ (R-02; see Decision I).
382
+
383
+ **Amendment (revision 2 follow-up):** The non-secret repo coordinates (`cloneUrl`,
384
+ `ownerRepo`, `baseBranch`) are also written to the additive optional `repo` field
385
+ (`RepoDescriptor`) on the outbound envelope so downstream Agent nodes can read them
386
+ from the canvas wire without relying on a shared side-channel. `RepoContext` (with
387
+ `worktreePath`) remains off-wire; each Agent node constructs it locally. The Workspace
388
+ node owns seeding `envelope.repo`; the `SoftwareTeamsApi` credential (not `envelope.repo`)
389
+ carries the GitHub token (R-02).
390
+
391
+ ---
392
+
393
+ ## Decision H — Finaliser node boundary + BOUNDED conflict-free merge (AC6, AC7, AC8; resolves the T9 unbounded-loop gap, R-16)
394
+
395
+ The **Finaliser node** is the run's terminus. It reads the aggregated run-state
396
+ (Decision F), merges every agent's change onto one branch, **guarantees a conflict-free,
397
+ marker-free tree within a bound or fails cleanly**, pushes, and emits the `branch`
398
+ artifact + run summary.
399
+
400
+ **Merge strategy — bounded and guaranteed:**
401
+
402
+ 1. Enumerate every task's `changeRef` from `runs[correlationId]` (Decision F).
403
+ 2. Apply them onto a fresh branch off `baseBranch` via git's automatic merge
404
+ (`git apply`/`git am`/three-way). Apply in a stable order (wave, then `taskId`).
405
+ 3. **If git cannot auto-resolve** a conflict (textual or semantic), run an intelligent
406
+ **claude conflict-resolver turn** (a single-turn invocation per ADR-001, Task
407
+ disabled) over the conflicted files to produce a clean, buildable tree.
408
+ 4. The resolver loop is **BOUNDED at max 3 conflict-resolver turns**. After each turn,
409
+ the tree is checked for conflict markers and a clean apply.
410
+ 5. **Within the bound:** as soon as a marker-free, conflict-free tree is reached, commit
411
+ the merged work, push a feature branch, and proceed to artifact + summary.
412
+ 6. **Bound exceeded:** if no clean, marker-free tree is reached within 3 resolver turns,
413
+ the Finaliser **FAILS with a structured error** that surfaces the **conflicting file
414
+ list** in the run summary. It **never loops indefinitely** and **never pushes a tree
415
+ containing conflict markers**.
416
+
417
+ The conflict-free guarantee is therefore **bounded**: *"resolves within 3 attempts or
418
+ fails cleanly with a structured error (conflicting files surfaced)."* This bounded
419
+ failure is a **defined outcome**, not an escalation — T11 asserts both the within-bound
420
+ success and the bound-exceeded structured-failure paths. **(Resolves R-16.)**
421
+
422
+ **Artifact + summary (AC7, AC8, R-17):** on success the Finaliser emits a `branch`
423
+ artifact in **exactly** the shape `extractBranchName` / `resolveOutputRef` consume
424
+ (`{ type: 'branch', url: '…/tree/<branch>' }`, per CONTRACT.md §3 example) so the
425
+ existing Output node takes the **PR** path, not the issue fallback. It synthesises a
426
+ human-readable run summary (per-agent result, the branch, the PR) that rides the
427
+ envelope `result.text` and is included in the PR body. Fail-fast on a missing `gh`
428
+ binary (R-01).
429
+
430
+ ---
431
+
432
+ ## Decision I — Additive-only contract + secret handling (AC10, AC11; R-02, R-18)
433
+
434
+ - **Additive-only.** The repo-execution work adds **only** optional fields: the
435
+ `RepoContext` typed param (off-wire, Decision D), the envelope `changeRef` carry and
436
+ the `RunTaskState.changeRef` carry (CONTRACT.md addendum), and the `branch` artifact
437
+ `type` (already open-vocabulary per CONTRACT.md §1). The **six top-level envelope
438
+ fields, their invariants, and the §4 upstream-context merge are UNCHANGED.** Existing
439
+ envelope/contract tests and `contract-check` stay green. **(Resolves R-18.)**
440
+ - **Secrets (R-02, AC11).** The GitHub token is **never** part of `RepoContext` (note
441
+ `cloneUrl` carries no embedded credential) and **never** any envelope field. It is
442
+ injected into the claude child process and git/gh environment from the
443
+ `SoftwareTeamsApi` credential only (T3, mirroring `ANTHROPIC_API_KEY`), and never
444
+ appears in the envelope, node output, logs, or the model prompt. T5 audits every new
445
+ surface; tests assert its absence.
446
+
447
+ ---
448
+
449
+ ## Risk resolutions (downstream slices implement these verbatim)
450
+
451
+ | Risk | Resolved by (this ADR) |
452
+ |------|------------------------|
453
+ | **R-15** queue-mode worker loses the worktree mid-run | Decision E: the change lives on the envelope as base64 `format-patch` (`changeRef`), not on disk; any worker re-applies it. T4 makes the working copy re-establishable; T11 tests cross-directory reconstruction. |
454
+ | **R-16** Finaliser loops forever / pushes conflict markers | Decision H: claude resolver loop BOUNDED at max 3 turns; within bound guarantees a clean, buildable, marker-free tree before push; bound exceeded ⇒ structured error surfacing the conflicting file list. Never loops, never pushes markers. |
455
+ | **R-17** Output opens an issue instead of a PR | Decision H: the Finaliser emits a `branch` artifact in exactly the `extractBranchName`/`resolveOutputRef` shape; T11 asserts the Output node takes the PR path. |
456
+ | **R-18** new additive field breaks §4 merge / existing nodes | Decisions D + I: `RepoContext` is off-wire (typed param), `changeRef` is additive/optional, `assemblePrompt` excludes repo context; the six invariants + §4 merge are unchanged; contract-gate + T11 new-field tests stay green. |
457
+ | **R-02** token leak (reaffirmed) | Decision I: token via the `SoftwareTeamsApi` credential into child-process env only; never in `RepoContext`, any envelope field, output, logs, or prompt. |
458
+
459
+ ---
460
+
461
+ ## What downstream slices build (no design decisions remain)
462
+
463
+ - **T2** — add `repoContext?: RepoContext` to `runAgentTurn`; set `cwd =
464
+ repoContext.worktreePath`; keep `assemblePrompt` free of repo context; back-compat
465
+ with no arg. (Decision D)
466
+ - **T4** — git/worktree/patch core lib: shallow clone, `git worktree` per agent,
467
+ capture `changeRef` (base64 `format-patch`), apply `changeRef` on any worker. (Decision E)
468
+ - **T7** — Workspace node: clone + worktree + seed `RepoContext`; fail-fast on `git`. (Decision G)
469
+ - **T8** — aggregation transition: write `{ status, changeRef }` into
470
+ `runs[correlationId].tasks[taskId]` on workflow static data via `serialiseRunState`;
471
+ forward-only, DAG-safe. (Decision F)
472
+ - **T9** — Finaliser node: read aggregated run-state, bounded (≤3) conflict-resolver
473
+ merge, push feature branch, emit `branch` artifact, synthesise summary; fail-fast on
474
+ `gh`. (Decisions F + H)
475
+
476
+ ---
477
+ ---
478
+
479
+ # ADR-003: n8n package module format = CommonJS for Node loadability
480
+
481
+ > **Status:** Accepted (plan `1-05-n8n-node-runtime`, T1, `software-teams-architect`).
482
+ > Extends ADR-001/ADR-002 — neither is re-opened. **This is the single source of truth
483
+ > the module-format slices (T2 mechanical edits, T3 Node-load gate) build against.** No
484
+ > production TypeScript or config is written here; this ADR only fixes the decisions.
485
+ > **Decides:** AC2 (CJS emit), AC3 (real entry), AC4 (shared lib resolves as CJS), AC5
486
+ > (`n8n-workflow` resolves as CJS), AC6 (Node-load gate contract); underwrites AC1/AC7.
487
+ > **Resolves** R-20, R-21, R-22, R-23. No "OR" / "and/or" is left for any implementer.
488
+
489
+ ---
490
+
491
+ ## Context
492
+
493
+ The package is built (`n8n-node build`, `tsc`-only, no bundler) but **not loadable by
494
+ Node** — so a self-hosted n8n (which runs on Node) registers none of its seven nodes.
495
+ `packages/n8n/tsconfig.json` sets `module: "preserve"` + `moduleResolution: "bundler"`,
496
+ so the emitted `dist/nodes/**/*.node.js` mix ESM `import`/`export` with CJS `require(...)`
497
+ and use **extensionless** relative imports; `package.json` declares no `"type"` while
498
+ `"main": "index.js"` names a file that does not exist anywhere in the package or its `dist`.
499
+
500
+ Empirically (re-verified at T1, with CWD = `packages/n8n/`):
501
+
502
+ ```
503
+ $ node -e "require('./dist/nodes/SoftwareTeamsAgent/SoftwareTeamsAgent.node.js')"
504
+ Error [ERR_MODULE_NOT_FOUND]: Cannot find module '…/.bun/n8n-workflow@2.16.0/
505
+ node_modules/n8n-workflow/dist/esm/logger-proxy' imported from …/dist/esm/index.js
506
+ ```
507
+
508
+ The emitted node opens with `import { NodeConnectionTypes … } from 'n8n-workflow';` +
509
+ extensionless `import … from '../../src/n8n-cast';`. Node treats the file as ESM, the
510
+ extensionless relative imports do not resolve, and the load **cascades into `n8n-workflow`
511
+ resolving its ESM build** (`dist/esm/index.js`) instead of its CJS build (`dist/cjs/index.js`).
512
+ The existing suite runs under **Bun**, which tolerates the mixed format — so it passed green
513
+ while the Node `dist` was unloadable. There is no gate that catches a Node-load regression.
514
+
515
+ A second-order constraint ripples across packages: the shared lib `@websitelabs/software-teams`
516
+ (`packages/cli`) is `"type": "module"` and its `exports` map resolves `require → ./lib/n8n-api.js`
517
+ (a real CJS build that exists) but `import → ./src/n8n-api.ts` (TS source Node cannot execute).
518
+ The n8n package MUST consume it via the **CJS (`require`) condition**. The fix therefore
519
+ touches BOTH packages and the `n8n-workflow` peer — the riskiest, most-novel decision, hence
520
+ architecture-first.
521
+
522
+ **Re-verified facts (T1):** `packages/cli/lib/n8n-api.js` exists and is CJS (`"use strict";`);
523
+ `packages/cli/package.json` `exports['.'].require === "./lib/n8n-api.js"`; `n8n-workflow`
524
+ `package.json` `main === "dist/cjs/index.js"` with **no `"type"`** (so its `main` is CJS under
525
+ Node), and both `dist/cjs/index.js` and `dist/esm/index.js` are present in the workspace
526
+ `node_modules`.
527
+
528
+ ---
529
+
530
+ ## Decision 1 — tsconfig deltas (CJS emit; mirrors the official `@n8n/node-cli` template)
531
+
532
+ `packages/n8n/tsconfig.json` `compilerOptions` change to exactly these values (replacing the
533
+ two bug values; the rest of the block is unchanged):
534
+
535
+ | key | from (bug) | to (CHOSEN) | why |
536
+ |-----|------------|-------------|-----|
537
+ | `module` | `"preserve"` | `"commonjs"` | emit CJS `require`/`exports` — no ESM `import`/`export` in `dist` (AC2). |
538
+ | `moduleResolution` | `"bundler"` | `"node"` | Node's classic CJS resolver: extensionless relative imports resolve, and a CJS importer selects the `require` condition of any `exports` map (Decisions 3, 4). |
539
+ | `target` | `"ES2020"` | `"es2019"` | mirror the official `@n8n/node-cli` template. |
540
+ | `esModuleInterop` | `true` | `true` (**unchanged — keep on**) | the existing `import { … } from 'n8n-workflow'` / `import { randomUUID } from 'node:crypto'` default+named imports compile to interop-correct `require` calls. **Do NOT remove it** — removing it would break those interop sites. |
541
+
542
+ `lib` stays `["ES2020"]` (or may track `target`; not load-bearing). `declaration`,
543
+ `declarationMap`, `sourceMap`, `strict`, `skipLibCheck`, `forceConsistentCasingInFileNames`,
544
+ `resolveJsonModule`, `outDir`, `rootDir`, `include`, `exclude` are **unchanged**. No `"type"`
545
+ key is added to `tsconfig` (that is a `package.json` concern — Decision 2).
546
+
547
+ **This is the entire compiler-side fix. No bundler is introduced (non-goal).**
548
+
549
+ ---
550
+
551
+ ## Decision 2 — package entry + `"type"` (real `main`, CJS `.js`)
552
+
553
+ - **`"type"`: leave UNSET.** Do **not** add `"type": "module"`. With no `"type"`, every
554
+ emitted `.js` is interpreted as **CommonJS** by Node — which is exactly what
555
+ `module: "commonjs"` produces. Adding `"type": "module"` would re-introduce the ESM
556
+ interpretation this ADR removes. (R-20.)
557
+ - **`"main"`: repoint to a real, always-present, side-effect-free built entry —
558
+ `"dist/credentials/SoftwareTeamsApi.credentials.js"`.** The current `"index.js"` resolves
559
+ to nothing (no root `index.*` source exists and the build emits no `dist` root index — the
560
+ official `@n8n/node-cli` build emits only `dist/nodes/**` and `dist/credentials/**`).
561
+ **No new index file is created** (that would be unbacked source / a build-shape change).
562
+ The credential entry is in the `n8n.credentials[]` registry, exists in every build, and
563
+ loads cleanly under CJS with the smallest dependency surface, satisfying AC3 (`main`
564
+ resolves to a real file that loads). n8n itself loads nodes/credentials via the unchanged
565
+ `n8n.nodes[]` / `n8n.credentials[]` block — `main` is the package's generic Node entry, not
566
+ the n8n load path, so this choice does not affect node registration.
567
+
568
+ ---
569
+
570
+ ## Decision 3 — shared-lib `@websitelabs/software-teams` resolves via its `require` condition
571
+
572
+ Under Node, with the n8n package now CJS (Decision 1, `moduleResolution: "node"`), a `require`
573
+ of the shared lib selects the **`require` condition** of its `exports` map →
574
+ `./lib/n8n-api.js` (the real CJS build, confirmed present, `"use strict";`). It does **NOT**
575
+ select the `import` condition (`./src/n8n-api.ts`, TS source Node cannot execute). The
576
+ `NodeEnvelope` type, `slugify`, `sanitizeUserInput`, `enumerateAgentResults`, etc. are usable
577
+ at runtime via CJS. T2 changes nothing in `packages/cli` — the shared lib's `exports` map is
578
+ already correct; the n8n package consuming it as CJS is what flips the condition. (R-21; AC4.)
579
+
580
+ ---
581
+
582
+ ## Decision 4 — `n8n-workflow` peer + the exact Node-load gate mechanism
583
+
584
+ - **Peer resolution.** Under Node + CJS `moduleResolution: "node"`, `require('n8n-workflow')`
585
+ resolves the peer's `main` → `dist/cjs/index.js` (confirmed present; `n8n-workflow` has no
586
+ `"type"`, so its `main` is CJS). The ESM cascade (`dist/esm/logger-proxy`) that currently
587
+ fails is the **Bun-install ESM path** — not the path a real n8n host or this gate uses. (AC5.)
588
+ - **The exact gate mechanism — PINNED, no "OR": the Node-load verifier runs with
589
+ `CWD = packages/n8n/`.** From that CWD the workspace `node_modules` supplies
590
+ `n8n-workflow`'s `dist/cjs/index.js` (present) and resolves the shared lib's `exports.require`
591
+ condition → `lib/n8n-api.js` (present). **This is the host-equivalent resolution boundary**,
592
+ reproducing locally how a real n8n host resolves the peer from its own Node-resolved
593
+ `node_modules`. It is explicitly **NOT** the Bun-install ESM path
594
+ (`.bun/n8n-workflow@…/…/dist/esm/index.js`) that currently fails. **T3 sets the verifier's
595
+ CWD to `packages/n8n/` verbatim** and re-opens no part of this. (R-22; AC5.)
596
+
597
+ ---
598
+
599
+ ## Decision 5 — Node-load gate contract (the exact shape T3 implements)
600
+
601
+ A repeatable post-build verification that loads, **under Node (not Bun)**, every entry in the
602
+ `package.json` `n8n.nodes[]` array (all **seven**) plus the one `n8n.credentials[]` entry:
603
+
604
+ ```
605
+ # run AFTER `bun run build`, with CWD = packages/n8n/ (Decision 4)
606
+ node -e "require('<built node/credential path>')" # for each of the 8 entries
607
+ ```
608
+
609
+ - The eight paths are read from / match the `n8n.nodes[]` + `n8n.credentials[]` arrays
610
+ (the seven `dist/nodes/**/*.node.js` + `dist/credentials/SoftwareTeamsApi.credentials.js`).
611
+ - **Any** load failure (`ERR_MODULE_NOT_FOUND`, "require is not defined in ES module scope",
612
+ "exports is not defined", or any throw) ⇒ the gate exits **non-zero**.
613
+ - It runs **explicitly under Node**, separate from the Bun test run, so a Bun-tolerated
614
+ mixed-format regression (the exact original failure mode) is caught. T8/CI wires it as a
615
+ distinct step. (R-23; AC6.)
616
+
617
+ ---
618
+
619
+ ## Resolution boundary (one statement)
620
+
621
+ The Node-load gate verifies the **host-equivalent CommonJS resolution boundary** — CWD =
622
+ `packages/n8n/`, where the workspace `node_modules` provides `n8n-workflow/dist/cjs/index.js`
623
+ and the shared lib's `require` condition (`lib/n8n-api.js`). It does **NOT** verify, and does
624
+ not depend on, the Bun-install ESM path (`.bun/.../dist/esm/index.js`) that is the current
625
+ failure. On a real n8n host the peer is provided by n8n's own Node-resolved `node_modules`;
626
+ running from `packages/n8n/` reproduces that resolution locally.
627
+
628
+ ---
629
+
630
+ ## Non-goals (explicit)
631
+
632
+ - **No bundler** (no esbuild/tsup/rollup). The fix is a tsconfig module-format change
633
+ consistent with the official `@n8n/node-cli` template. (Spec Out of Scope.)
634
+ - **No return edge** from an Agent node to the Orchestrator — ADR-002 Decision F's
635
+ forward-only DAG aggregation is untouched.
636
+ - **`spawnClaude` stays on `node:child_process`** — do NOT touch it (it is already
637
+ Node-compatible). (Spec Out of Scope.)
638
+ - **`NodeEnvelope` contract unchanged** — its six top-level fields, invariants, and the §4
639
+ upstream-context merge are not altered. This is a module **FORMAT** change only;
640
+ behaviour is preserved (the existing Bun suite, typecheck, lint stay green — AC7). (R-20.)
641
+
642
+ ---
643
+
644
+ ## Implementer checklist for T2/T3 (no design decisions remain)
645
+
646
+ **T2 — mechanical CommonJS edits (`tsconfig.json` / `package.json` / imports only):**
647
+
648
+ 1. `packages/n8n/tsconfig.json` → set `module: "commonjs"`, `moduleResolution: "node"`,
649
+ `target: "es2019"`; **keep `esModuleInterop: true`**; leave every other key as-is.
650
+ 2. `packages/n8n/package.json` → set `"main": "dist/credentials/SoftwareTeamsApi.credentials.js"`;
651
+ do **NOT** add `"type"`; create no index file.
652
+ 3. Make relative imports resolvable under Node's CJS resolver — `module: commonjs` +
653
+ `moduleResolution: node` emits extension-correct `require`s for the existing
654
+ `../../src/...` imports; do not hand-edit import specifiers beyond what the compiler needs.
655
+ 4. Change nothing in `packages/cli` — the shared lib's `exports.require` is already correct
656
+ (Decision 3). Do not touch `spawnClaude`, `NodeEnvelope`, or the §4 merge.
657
+ 5. Gate: existing Bun suite + `tsc --noEmit` + lint + build stay green (AC7).
658
+
659
+ **T3 — Node-load verification gate:**
660
+
661
+ 1. After `bun run build`, run `node -e "require('<path>')"` for each of the 8
662
+ `n8n.nodes[]` + `n8n.credentials[]` entries, **with CWD = `packages/n8n/`** (Decision 4).
663
+ 2. Exit non-zero on any load failure; run under Node, separate from the Bun suite (Decision 5).
664
+ 3. Wire as a distinct CI/quality-gate step (T8) so a mixed-format regression is caught.
665
+
666
+ ---
667
+ ---
668
+
669
+ # ADR-004: Cross-node forward-aggregation via global static data + both-layout persona resolution + publish-ready packaging
670
+
671
+ > **Status:** Accepted (plan `1-06-n8n-repo-pr-e2e`, T1, `software-teams-architect`).
672
+ > Extends ADR-001/ADR-002/ADR-003 — none is re-opened. **This is the single source of
673
+ > truth the Wave-2 slices (T2 Gap A, T3 Gap B, T4 packaging, T5 entry affordance) build
674
+ > against** — each one implements the decisions fixed here and re-opens NONE of them. No
675
+ > production TypeScript or config is written here; this ADR only fixes the decisions.
676
+ > **Decides:** AC1 (mechanism pinned); underwrites AC2/AC3/AC4/AC5/AC6 (Gap A),
677
+ > AC7/AC8/AC9 (Gap B persona), AC10 (packaging), AC11 (entry affordance).
678
+ > **Resolves** R-25, R-26, R-27, R-28, R-29, R-30 (and reaffirms R-02). No "OR" / "and/or"
679
+ > is left for any implementer.
680
+
681
+ ---
682
+
683
+ ## Context
684
+
685
+ The 7-step repo→agents→PR workflow does NOT genuinely work on a fresh Node-based n8n
686
+ host even though the Bun suite is green (the suite mocks `getWorkflowStaticData` as one
687
+ shared object — the "Bun-masks-runtime" pattern 1-05 exposed for module loading, now
688
+ recurring for cross-node state). Two blocking gaps plus packaging polish stand between
689
+ the implemented nodes and the acceptance scenario.
690
+
691
+ **Re-verified facts (T1, empirical — not guessed):**
692
+
693
+ - **Gap A is real.** The plan Orchestrator writes run-state to
694
+ `getWorkflowStaticData('node')['runs'][correlationId]`
695
+ (`SoftwareTeamsOrchestrator.node.ts:144`). The summary mode reads its OWN node store
696
+ (`:194`). The Finaliser reads its OWN node store (`SoftwareTeamsFinaliser.node.ts:184`)
697
+ and THROWS "No aggregated run-state found" when empty (`:189-197`). The Agent node only
698
+ emits `changeRef` on the wire (`SoftwareTeamsAgent.node.ts:354-355`) and NEVER calls
699
+ `getWorkflowStaticData`/`recordAgentResult`. The only writer, `recordAgentResult`
700
+ (`transitions.ts:114`), is reached ONLY by the Orchestrator continue-run path
701
+ (`:164-182`, the `recordAgentResult` call at `:169`) — a forbidden return edge a
702
+ forward-only DAG never traverses. n8n keys `'node'` static data per node NAME, so each
703
+ of the three consumers gets a DIFFERENT empty object on a real host.
704
+ - **The transitions are correct.** `recordAgentResult` (`:114`), `enumerateAgentResults`
705
+ (`:166`), `summarise`, and `serialiseRunState`/`deserialiseRunState` need NO change. The
706
+ gap is purely WHO calls them and WHERE the state lives (node vs global).
707
+ - **Gap B climb is off by one.** `single-turn.ts` builds to `dist/src/execution/single-turn.js`,
708
+ so `__dirname` at runtime is `<pkg>/dist/src/execution`. The current `resolveAgentSpecPath`
709
+ climbs 4 (`join(__dirname, "../../../..")`, `:135`) → `…/software-teams/packages` (NOT the
710
+ repo root `…/software-teams`). Reaching the repo root needs climb 5. The package ships NO
711
+ specs (`packages/n8n/agents/` and `dist/agents/` do not exist today), so on a fresh
712
+ npm/custom-extensions install `resolveAgentSpecPath` returns `null` → `agentSpecBody` empty
713
+ (`:198-209`) → every specialist runs an identical bare prompt.
714
+ - **The bundled-spec set is unambiguous.** `SPECIALIST_OPTIONS`
715
+ (`SoftwareTeamsAgent.node.ts:30-64`) is EXACTLY 33 `software-teams-*` values; the repo
716
+ has EXACTLY 33 `.claude/agents/software-teams-*.md` files; a both-ways `comm` diff is
717
+ EMPTY. "Lean (only SPECIALIST_OPTIONS)" and "all 33" are the SAME set. There is no choice.
718
+ - **Packaging.** `package.json` has a real `main` (`dist/credentials/…`, 1-05) and the
719
+ `n8n.nodes[]`/`n8n.credentials[]` registry, but NO `files` allowlist and NO `publishConfig`
720
+ (`:1-58`), so `npm pack` would be incorrect/bloated and would omit the bundled specs.
721
+ - **Entry.** The runnable example (`examples/repo-pr.workflow.json`) starts at the Workspace
722
+ node (`targetRepo`/`baseBranch`/`correlationId` params) feeding the Orchestrator (`epic`
723
+ param holds the prompt); the live entry is Manual Trigger → Workspace. There is no
724
+ dedicated repo+prompt input affordance.
725
+
726
+ ---
727
+
728
+ ## Decision J — Gap A forward-aggregation mechanism: GLOBAL static data (`getWorkflowStaticData('global')`)
729
+
730
+ **CHOSEN: option (a) — global static data. Option (b) wire-based is REJECTED.**
731
+
732
+ All run-state for a correlationId lives in ONE workflow-global object every node shares:
733
+ `getWorkflowStaticData('global')['runs'][correlationId]` (a serialised `RunState`). This is
734
+ the SAME `runs[correlationId]` shape used today — only the static-data SCOPE moves from
735
+ `'node'` to `'global'`. ADR-002 Decision F's `runs[correlationId]` mechanism is preserved
736
+ verbatim; only the keying scope changes.
737
+
738
+ **Why chosen (and the trade-off vs the rejected option):**
739
+
740
+ - **Queue-mode-safe (R-27).** n8n's `'global'` static data is persisted to n8n's database and
741
+ shared across every node AND every worker, so an Agent persisting on worker A and a
742
+ Finaliser reading on worker B see the same state. `'node'` static data is keyed per node
743
+ NAME (the root cause of Gap A) and is the wrong sharing boundary.
744
+ - **Smallest behaviour-preserving diff.** It is a one-token change at each of the existing
745
+ three call sites (`'node'` → `'global'`) plus ONE new Agent persist call. It reuses
746
+ `recordAgentResult`/`enumerateAgentResults`/`summarise`/`serialiseRunState`/`deserialiseRunState`
747
+ UNCHANGED. No new wire contract, no envelope change, no topology change.
748
+ - **Holds the forward-only DAG (R-25).** The Agent writes FORWARD into shared state; the
749
+ Finaliser/summary read it forward. No Agent→Orchestrator edge is added; the example
750
+ topology `Workspace → Orchestrator(plan) → [Agent×N] → Orchestrator(summary) → Finaliser
751
+ → Output(PR)` is unchanged — the scope move is invisible in the workflow JSON.
752
+ - **REJECTED — wire-based input-item aggregation:** the Finaliser/summary would aggregate
753
+ `changeRef`s/results from their INPUT ITEMS. It avoids global state, but (1) it can only
754
+ see the items wired into THAT node's single input port — partial-failure / resumable runs
755
+ (R-05) and a summary that reads a run independent of the current item set both need the
756
+ durable run-state, not the transient wire; (2) it would bypass `RunState`/`summarise`
757
+ entirely, duplicating aggregation logic the transitions already own (DRY, R-31); (3) it
758
+ would require re-wiring the example so every agent fans into one collector node. Global
759
+ static data keeps the durable, resumable, single-source run-state the design already has.
760
+
761
+ ### The exact contract the three consumers implement (T2)
762
+
763
+ The key is `correlationId` + `taskId` (`taskId` read from `envelope.input.context.taskId`,
764
+ exactly as `recordAgentResult` already does at `transitions.ts:120-122`). A shared accessor
765
+ (extracted into the run-state module to keep the node files ≤400 lines and DRY, R-31) reads
766
+ and writes `getWorkflowStaticData('global')['runs']`.
767
+
768
+ - **Agent node — WRITES (new; AC2).** After it computes its terminal envelope (the
769
+ `{ ...agentResult, changeRef }` returned at `SoftwareTeamsAgent.node.ts:355`, and the
770
+ non-repo path), and BEFORE pushing to `returnData`, the Agent node:
771
+ 1. `const staticData = this.getWorkflowStaticData('global')`;
772
+ 2. reads `runs[envelope.correlationId]`, `deserialiseRunState` it (skip persist if `null` —
773
+ the plan Orchestrator must have seeded it, see Decision J-seed);
774
+ 3. `recordAgentResult(state, envelope)` (UNCHANGED transition — it records terminal
775
+ `status` + `changeRef`, keyed by `taskId`; idempotent for an already-terminal task);
776
+ 4. `runs[envelope.correlationId] = serialiseRunState(updated)`.
777
+ The Agent still emits its envelope on the wire unchanged (the wire carry is additive and
778
+ preserved); persistence is in ADDITION to, not instead of, the wire emit. Only terminal
779
+ `status` + `changeRef` enter run-state — never credentials (R-02 / AC12).
780
+ - **Finaliser — READS (re-pointed; AC4).** Change `getWorkflowStaticData('node')` →
781
+ `getWorkflowStaticData('global')` at `SoftwareTeamsFinaliser.node.ts:184`. Then
782
+ `deserialiseRunState(runs[correlationId])` and `enumerateAgentResults` (UNCHANGED, `:199`)
783
+ to build the merge set. **Remove the false-trigger THROW path's premise** (`:189-197`): the
784
+ Finaliser must no longer throw because its OWN node store is empty — on the global store the
785
+ state is present once agents have run. (A genuinely-empty global run-state for a real
786
+ correlationId — i.e. zero agents ran — may still be a structured failure; but the
787
+ per-node-isolation false trigger is removed.)
788
+ - **Summary Orchestrator — READS (re-pointed; AC3).** Change
789
+ `getWorkflowStaticData('node')` → `getWorkflowStaticData('global')` at
790
+ `SoftwareTeamsOrchestrator.node.ts:144` (the single `staticData`/`runs` binding used by both
791
+ the plan and summary branches). The summary branch (`:186-227`) then reads the SHARED
792
+ `runs[resolvedId]` aggregated by distinct Agent nodes and reports per-agent results +
793
+ outcome via `enumerateAgentResults`/`summarise` (UNCHANGED) — it no longer emits "No
794
+ run-state found" when agents have run.
795
+
796
+ ### Decision J-seed — the plan Orchestrator must seed into the SAME global store
797
+
798
+ The plan Orchestrator writes the initial `RunState` at `SoftwareTeamsOrchestrator.node.ts:144`
799
+ via `getWorkflowStaticData('node')`. T2 changes that ONE binding to
800
+ `getWorkflowStaticData('global')` so the Agent's later write lands in a state the
801
+ Finaliser/summary can read. This is the single binding shared by the plan and summary branches,
802
+ so flipping it to `'global'` fixes seeding AND the summary read together.
803
+
804
+ **The continue-run path is NOT the forward mechanism.** `SoftwareTeamsOrchestrator.node.ts:164-182`
805
+ (the `recordAgentResult` at `:169`) is the FORBIDDEN return edge — it presumes an Agent envelope
806
+ flows BACK into the Orchestrator. It stays in place and behaviourally harmless under a
807
+ forward-only DAG (it is simply never reached on the pinned topology), but it is explicitly NOT
808
+ the Gap A fix. T2 does NOT route agents back through it. (R-25.)
809
+
810
+ ### Confirms no transition change
811
+
812
+ `recordAgentResult` (`:114`), `enumerateAgentResults` (`:166`), `summarise`, and
813
+ `serialiseRunState`/`deserialiseRunState` are CORRECT and UNCHANGED. The keying
814
+ (`correlationId` + `taskId`) is what these functions already use. The ONLY new logic is the
815
+ Agent-side persist (write) and the two scope flips (`'node'`→`'global'`). (R-04.)
816
+
817
+ ### Aggregation-wiring note for T5 (sole owner of the example)
818
+
819
+ Under the global-static-data mechanism the example topology does NOT change — the scope move
820
+ is internal and invisible in `examples/repo-pr.workflow.json`. **T2's hand-off note to T5 is:
821
+ "NO example node-wiring change required — global static-data scope move only."** T5 edits the
822
+ example for the entry affordance (Decision N) only.
823
+
824
+ ---
825
+
826
+ ## Decision K — Gap B persona resolution: bundled location `dist/agents/` + both-layout `__dirname`-relative candidates
827
+
828
+ **Packaged location — CHOSEN: `dist/agents/`** (a sibling of `dist/src/`, `dist/nodes/`,
829
+ `dist/credentials/`). The specs are copied there by the build (Decision M), so the SAME `dist`
830
+ that ships the nodes ships the personas, and `__dirname`-relative resolution from the built
831
+ `single-turn.js` is a short, stable climb. Rejected alternatives: an un-built top-level
832
+ `agents/` dir (would need a separate `files` entry and a longer climb that re-introduces the
833
+ repo-vs-package ambiguity) — `dist/agents/` keeps everything under the one built tree.
834
+
835
+ ### The VERBATIM `resolveAgentSpecPath` candidate list (T3 implements this literally)
836
+
837
+ At runtime `__dirname` is `<pkg>/dist/src/execution` (empirically confirmed: the file builds
838
+ to `dist/src/execution/single-turn.js`). T3 replaces the body of `resolveAgentSpecPath` with
839
+ the following ORDERED candidate list and `return candidates.find(existsSync) ?? null;`
840
+ (the graceful `null` degrade is preserved):
841
+
842
+ ```ts
843
+ function resolveAgentSpecPath(agentId: string): string | null {
844
+ const candidates = [
845
+ // 1. Installed/packaged layout: bundled specs under dist/agents/
846
+ // __dirname = <pkg>/dist/src/execution → ../../agents = <pkg>/dist/agents
847
+ join(__dirname, "..", "..", "agents", `${agentId}.md`),
848
+ // 2. Dev layout (running from the repo): repo-root .claude/agents/
849
+ // climb 5 from dist/src/execution reaches the repo root (the old climb-4
850
+ // landed in packages/ — the off-by-one this fixes).
851
+ join(__dirname, "..", "..", "..", "..", "..", ".claude", "agents", `${agentId}.md`),
852
+ // 3. Dev layout fallback: repo-root agents/
853
+ join(__dirname, "..", "..", "..", "..", "..", "agents", `${agentId}.md`),
854
+ ];
855
+ return candidates.find(existsSync) ?? null;
856
+ }
857
+ ```
858
+
859
+ - **Candidate 1 (installed layout, AC8):** `join(__dirname, "..", "..", "agents", …)` resolves
860
+ to `<pkg>/dist/agents/<agentId>.md` — the bundled spec, present in a fresh
861
+ npm/custom-extensions install. This is checked FIRST so the shipped persona wins in
862
+ production.
863
+ - **Candidates 2 & 3 (dev layout, AC8):** climb 5 (`../../../../..`) from
864
+ `dist/src/execution` reaches the repo root `…/software-teams`, where `.claude/agents/` (33
865
+ specs, confirmed) and an optional `agents/` live. This FIXES the climb-4 off-by-one (old
866
+ `join(__dirname, "../../../..")` → `…/packages`). Dev resolution keeps working from a repo
867
+ checkout. (R-28.)
868
+ - **Null degrade preserved (AC8 close):** an unknown `agentId` matches no candidate →
869
+ `null` → `agentSpecBody` empty → bare prompt, exactly as today. The fix makes bundled specs
870
+ FINDABLE; it does not make a missing spec fatal.
871
+
872
+ Two different known `agentId`s therefore resolve to two DIFFERENT spec files → two DIFFERENT
873
+ persona bodies (AC9). `stripSpecFrontmatter` (`:143-150`) is unchanged.
874
+
875
+ ---
876
+
877
+ ## Decision L — Bundled-spec SET: all 33 `software-teams-*` specs (== SPECIALIST_OPTIONS)
878
+
879
+ The set to ship is the 33 specialist specs at `.claude/agents/software-teams-*.md`. This
880
+ EQUALS `SPECIALIST_OPTIONS` one-to-one (empirically confirmed — the both-ways `comm` diff is
881
+ empty), so **lean (only SPECIALIST_OPTIONS) == all 33; there is no "lean vs all 33" choice for
882
+ T4 to make.** (R-29.)
883
+
884
+ - **Source:** the tracked plugin specs at `packages/cli/agents/software-teams-*.md`. This is
885
+ the version-controlled origin. The equivalent repo-root `.claude/agents/` copy is
886
+ gitignored/generated (by `sync-agents`), so the bundler MUST read the tracked
887
+ `packages/cli/agents/` — otherwise a fresh clone / CI / `npm` build finds no specs (ENOENT).
888
+ - **Packaged destination:** `dist/agents/software-teams-*.md` (Decision K).
889
+ - **Copy:** wired into `n8n-node build` (Decision M). Only `software-teams-*.md` is copied —
890
+ no other `packages/cli/agents/*.md` (the framework/JDI specs) ship.
891
+
892
+ ---
893
+
894
+ ## Decision M — Spec-bundling build glue + Decision N's packaging
895
+
896
+ - **Build glue (T4).** The `package.json` `build` script (`"n8n-node build"`) gains a copy
897
+ step that, AFTER the tsc build emits `dist`, copies `packages/cli/agents/software-teams-*.md` →
898
+ `dist/agents/`. The lowest-risk shape consistent with ADR-003's no-bundler constraint is a
899
+ tiny Node `.cjs` script (mirroring `scripts/verify-node-load.cjs`) invoked as a postbuild,
900
+ e.g. `"build": "n8n-node build && node scripts/bundle-specs.cjs"`. It introduces NO bundler
901
+ (esbuild/tsup) — it is a file copy. The script reads from the tracked `packages/cli/agents/`
902
+ (NOT the gitignored `.claude/agents/`) and writes to `dist/agents/`, creating the dir if
903
+ absent. The 1-05 node-load gate and `main` entry are unchanged.
904
+
905
+ ---
906
+
907
+ ## Decision N — Packaging: `files` allowlist + `publishConfig` (publish-READY, not published)
908
+
909
+ `packages/n8n/package.json` gains:
910
+
911
+ ```jsonc
912
+ "files": [
913
+ "dist",
914
+ "README.md",
915
+ "CONTRACT.md",
916
+ "ARCHITECTURE.md",
917
+ "LICENSE"
918
+ ],
919
+ "publishConfig": {
920
+ "access": "public"
921
+ }
922
+ ```
923
+
924
+ - **`files` allowlist (AC10).** `"dist"` covers the seven built nodes
925
+ (`dist/nodes/**/*.node.js`), the credential (`dist/credentials/…`), the compiled `dist/src/`,
926
+ AND the bundled specs (`dist/agents/*.md`, Decision M) — so one entry ships the code AND the
927
+ personas. Docs/licence are included for npm hygiene. Everything NOT listed —
928
+ `nodes/`/`credentials/`/`src/` TS source, `__tests__/`, `scripts/`, `tsconfig.json`,
929
+ `.eslintrc`, `examples/`, dev configs — is EXCLUDED from the tarball, so `npm pack` is
930
+ correct/minimal and carries no source/test/dev cruft (R-29) and no env/secret files (R-02).
931
+ (`package.json`, `README.md`, `LICENSE`, and `main` are always included by npm regardless.)
932
+ - **`publishConfig` (AC10).** `{ "access": "public" }` makes the scoped
933
+ `@websitelabs/...` package publishable as public — the minimal shape that makes `npm pack`
934
+ and a future publish correct.
935
+ - **Publish-READY only (Out of Scope).** This makes `npm pack` produce a correct/minimal
936
+ tarball; it does NOT publish. No `npm publish`, no verified-community submission.
937
+
938
+ ---
939
+
940
+ ## Decision O — Entry affordance (steps 1-2): documented Form-Trigger recipe + minor additive param ergonomics
941
+
942
+ **CHOSEN: a documented Form-Trigger recipe + minor additive parameter ergonomics — NOT a new
943
+ node.** This is the lowest-risk additive option (R-30): n8n ships a first-party Form Trigger;
944
+ no new node code, registry entry, build surface, or node-load-gate entry is added.
945
+
946
+ - **The recipe (T5, documented + in the example).** A built-in n8n **Form Trigger** with three
947
+ fields — "Target Repository" (`owner/repo`), "Base Branch" (default `main`), and "Prompt /
948
+ Epic" — feeds the Workspace node (`targetRepo`, `baseBranch`) and the Orchestrator (`epic`)
949
+ via n8n expressions (`{{ $json["Target Repository"] }}` etc.). Topology becomes Form
950
+ Trigger → Workspace → Orchestrator(plan) → … with NO change to any node's contract.
951
+ - **Minor additive param ergonomics (T5, optional + additive).** If a node param needs to
952
+ accept the form value, it is supplied via an n8n expression on the EXISTING param
953
+ (`targetRepo`/`baseBranch`/`epic`) — no new required params, no renamed params, no changed
954
+ defaults. Any ergonomics are purely additive (e.g. an updated param description /
955
+ placeholder), preserving `epic` semantics (R-30).
956
+ - **Back-compat (AC11, R-30).** The existing Manual Trigger → Workspace → Orchestrator entry
957
+ MUST keep working byte-for-byte — the Form Trigger is an ALTERNATIVE entry, additive only.
958
+ - **Example ownership.** T5 is the SOLE owner of `examples/repo-pr.workflow.json`; it adds the
959
+ Form-Trigger recipe (and applies T2's hand-off note, which under Decision J is "no wiring
960
+ change"). T2 does NOT touch the example.
961
+
962
+ ---
963
+
964
+ ## Non-goals (explicit — no implementer re-opens these)
965
+
966
+ - **No Agent→Orchestrator return edge.** Aggregation is forward-only (ADR-002 Decision F);
967
+ the continue-run path (`Orchestrator:164-182`) is NOT the mechanism. (R-25.)
968
+ - **No bundler.** The spec-copy is a file copy (Decision M), consistent with ADR-003's
969
+ tsc-only build. No esbuild/tsup/rollup.
970
+ - **`spawnClaude` unchanged.** It stays on `node:child_process` (ADR-003 non-goal).
971
+ - **`NodeEnvelope` unchanged.** Six top-level fields + additive `repo?`/`changeRef?` (1-04) +
972
+ the §4 upstream-context merge are not altered. `recordAgentResult`/`enumerateAgentResults`/
973
+ `summarise`/`serialise`/`deserialise` are unchanged.
974
+ - **Not published.** Publish-READY (`files`/`publishConfig` + correct `npm pack`) only.
975
+ - **No new node for the entry affordance** — a Form-Trigger recipe, not a node (Decision O).
976
+ - **No new "lean vs all 33" decision** — the set is the 33 `software-teams-*` specs (Decision L).
977
+
978
+ ---
979
+
980
+ ## Risk resolutions (downstream slices implement these verbatim)
981
+
982
+ | Risk | Resolved by (this ADR) |
983
+ |------|------------------------|
984
+ | **R-25** aggregation fix introduces an implicit return edge / breaks the DAG | Decision J: forward-only — Agent writes global static data; Finaliser/summary read it; the continue-run path is explicitly NOT the mechanism; example topology + Finaliser PR path unchanged. |
985
+ | **R-26** aggregation gate passes by mocking `getWorkflowStaticData` into one shared object | Decision J pins the global-store contract; T6 (not this slice) MUST exercise DISTINCT per-node objects (and a shared `'global'` object only for `('global')`) — a shared object for `('node')` across node names is forbidden and is itself the regression. |
986
+ | **R-27** `'global'` static data behaves differently under queue mode | Decision J chose `'global'` BECAUSE it is n8n DB-backed and shared across workers (queue-mode-safe); the rejected wire path is moot. |
987
+ | **R-28** `resolveAgentSpecPath` fixed for one layout regresses the other | Decision K pins an ORDERED candidate list covering BOTH layouts (installed `dist/agents/` first, dev repo-root `.claude/agents/`+`agents/` next); null degrade preserved. |
988
+ | **R-29** bundled specs bloat the tarball / wrong set ships | Decision L: the set is the 33 `software-teams-*` specs == SPECIALIST_OPTIONS (no ambiguity); Decision N's `files` allowlist scopes the tarball to `dist` + docs only. |
989
+ | **R-30** entry affordance breaks the existing entry / `epic` semantics | Decision O: a Form-Trigger recipe + additive-only param ergonomics, NOT a new node; existing Manual Trigger entry unchanged; `epic` semantics preserved. |
990
+ | **R-02** secrets leak via the new aggregation surface / tarball | Decision J persists ONLY terminal `status` + `changeRef` (no credentials enter run-state); Decision N's `files` allowlist excludes source/env/secret files; T8 audits run-state/global static data/summary/tarball. |
991
+
992
+ ---
993
+
994
+ ## Implementer checklist for T2/T3/T4/T5 (no design decisions remain)
995
+
996
+ **T2 — Gap A forward-aggregation (backend; `Agent`/`Finaliser`/`Orchestrator` nodes +
997
+ run-state module):**
998
+
999
+ 1. Extract a shared global-store accessor into the run-state module (DRY, R-31): read/write
1000
+ `getWorkflowStaticData('global')['runs']`, `deserialiseRunState`/`serialiseRunState` round-trip.
1001
+ 2. **Agent node — add the persist (write):** after the terminal envelope is computed and BEFORE
1002
+ `returnData.push`, read the global store, `recordAgentResult(state, envelope)` (UNCHANGED
1003
+ transition), write back via `serialiseRunState`. Keyed by `correlationId` + `taskId`. Skip if
1004
+ the global state for `correlationId` is `null`. Keep the wire emit unchanged.
1005
+ 3. **Orchestrator — flip the scope:** `getWorkflowStaticData('node')` →
1006
+ `getWorkflowStaticData('global')` at `:144` (seeds the plan run-state AND backs the summary
1007
+ read at `:186-227`). Do NOT route agents through the continue-run path (`:164-182`).
1008
+ 4. **Finaliser — flip the scope + remove the false throw:** `getWorkflowStaticData('node')` →
1009
+ `getWorkflowStaticData('global')` at `:184`; the `:189-197` THROW no longer fires from
1010
+ per-node isolation (state is present on the global store once agents ran).
1011
+ 5. Hand-off note to T5: "NO example node-wiring change required — global static-data scope move
1012
+ only."
1013
+ 6. Do NOT change `recordAgentResult`/`enumerateAgentResults`/`summarise`/`serialise`/`deserialise`,
1014
+ `NodeEnvelope`, the §4 merge, or `spawnClaude`. Keep files ≤400 lines (extract the accessor).
1015
+
1016
+ **T3 — Gap B persona resolution (backend; `single-turn.ts` only):**
1017
+
1018
+ 1. Replace `resolveAgentSpecPath`'s body with the VERBATIM ordered candidate list in Decision K
1019
+ (installed `join(__dirname,"..","..","agents",…)` FIRST; dev climb-5
1020
+ `.claude/agents` then `agents` next); `return candidates.find(existsSync) ?? null`.
1021
+ 2. Preserve the graceful `null` degrade and `agentSpecBody`'s empty-⇒-bare-prompt behaviour
1022
+ (`:198-209`). Do NOT change `stripSpecFrontmatter`, `assemblePrompt`, or `spawnClaude`.
1023
+
1024
+ **T4 — Packaging + spec bundling (programmer; `package.json` + `scripts/`):**
1025
+
1026
+ 1. Add `scripts/bundle-specs.cjs`: copy `packages/cli/agents/software-teams-*.md` → `dist/agents/`
1027
+ (create the dir; copy ONLY `software-teams-*.md`). No bundler.
1028
+ 2. `package.json` `"build": "n8n-node build && node scripts/bundle-specs.cjs"`.
1029
+ 3. Add the `files` allowlist (`["dist","README.md","CONTRACT.md","ARCHITECTURE.md","LICENSE"]`)
1030
+ and `"publishConfig": { "access": "public" }`.
1031
+ 4. Keep `main`, the `n8n.nodes[]`/`n8n.credentials[]` registry, and the node-load gate unchanged.
1032
+ Do NOT publish.
1033
+
1034
+ **T5 — repo+prompt entry affordance (backend; example + docs, SOLE owner of the example):**
1035
+
1036
+ 1. Add a documented Form-Trigger recipe (fields: Target Repository, Base Branch, Prompt/Epic)
1037
+ feeding Workspace (`targetRepo`/`baseBranch`) + Orchestrator (`epic`) via expressions.
1038
+ 2. Apply T2's hand-off note (under Decision J: no wiring change). Refresh the example/docs.
1039
+ 3. Keep the existing Manual Trigger → Workspace → Orchestrator entry working byte-for-byte; any
1040
+ param ergonomics are additive only (no new required params, no `epic`-semantics change).
1041
+
1042
+ ---
1043
+ ---
1044
+
1045
+ # ADR-005: PR-tag correlationId re-entry + merge-triggered cleanup
1046
+
1047
+ > **Status:** Accepted (plan `1-01-n8n-e2e-gaps`, T12, `software-teams-devops`).
1048
+ > Extends ADR-001/ADR-002/ADR-003/ADR-004 — none is re-opened. **This is the single
1049
+ > source of truth for the PR-tag mechanism and the merge-triggered cleanup.** Pairs with
1050
+ > [`CONTRACT.md`](./CONTRACT.md) §7 (PR-feedback and HITL additive fields).
1051
+ > **Decides:** AC2 (PR-feedback re-entry), AC5 (merge-triggered cleanup), AC8
1052
+ > (secrets isolation). **Honours:** ADR-001 forward-only DAG (no return edge), ADR-002
1053
+ > `correlationId` run-state join key, ADR-003 CJS load boundary, ADR-004 global static
1054
+ > data and additive-only contract.
1055
+
1056
+ ---
1057
+
1058
+ ## Context
1059
+
1060
+ Two gaps remain open after the repo-execution work (1-04, ADR-002/ADR-004):
1061
+
1062
+ 1. **No PR-feedback re-entry.** When humans request changes via a GitHub PR review, no node
1063
+ reads them back into the run. The Orchestrator's `continue-run` path exists but has no
1064
+ producer feeding it the original `correlationId` from a PR.
1065
+ 2. **No teardown.** After a human merges the PR there is no automatic cleanup of run-state,
1066
+ conversation-state, worktrees/clones, agent memories, or plan/task artefacts, so the
1067
+ workflow is not immediately reusable.
1068
+
1069
+ This ADR fixes both gaps with three new nodes (`SoftwareTeamsPrFeedback`,
1070
+ `SoftwareTeamsHitl`, `SoftwareTeamsCleanup`) and one contract extension (the PR-tag in the
1071
+ Output node's PR body). It re-opens no decisions from ADR-001..004.
1072
+
1073
+ ---
1074
+
1075
+ ## Decision P — PR→run mapping via an HTML-comment tag in the PR body
1076
+
1077
+ **CHOSEN: an invisible HTML-comment tag embedded in the PR body by the Output node.**
1078
+ The tag format is:
1079
+
1080
+ ```
1081
+ <!-- software-teams:correlationId=<correlationId> -->
1082
+ ```
1083
+
1084
+ Two canonical helpers are exported from the shared contract (`packages/cli/src/contract/envelope.ts`):
1085
+
1086
+ | Helper | Signature | Purpose |
1087
+ |--------|-----------|---------|
1088
+ | `buildCorrelationTag` | `(correlationId: string) => string` | Writer (Output node, T11) |
1089
+ | `parseCorrelationTag` | `(body: string) => string \| null` | Reader (PR-Feedback + Cleanup nodes) |
1090
+ | `CORRELATION_TAG_PREFIX` | `string` | The prefix `"software-teams:correlationId="` |
1091
+
1092
+ Round-trip invariant: `parseCorrelationTag(buildCorrelationTag(id)) === id` for any non-empty
1093
+ `id` without whitespace or `>`. The PR body is the ONE shared channel — no separate database
1094
+ record, no webhook payload field, no envelope field.
1095
+
1096
+ **Why HTML comment:** invisible to human readers, immune to Markdown rendering, stable across
1097
+ GitHub's PR body edit UI, and parseable by a simple regex on the raw body text. The format is
1098
+ machine-readable and human-ignorable.
1099
+
1100
+ **Rejected alternatives:**
1101
+
1102
+ - **Envelope field:** would require surfacing the PR number/URL to an envelope producer that
1103
+ does not yet know it; the Output node emits the envelope BEFORE the PR is opened. A post-open
1104
+ tag in the PR body is the only write path that does not mutate the envelope retroactively.
1105
+ - **External database lookup:** adds a stateful store not part of n8n's built-in primitives.
1106
+ The PR body is already the canonical, durable record of the PR, stored by GitHub with no extra
1107
+ infrastructure. (ADR invariant: reuse existing primitives over adding new stores.)
1108
+
1109
+ ---
1110
+
1111
+ ## Decision Q — PR-feedback re-entry: forward-only continue path, no return edge (AC2; honours ADR-001 Decision C)
1112
+
1113
+ The PR-Feedback node (`SoftwareTeamsPrFeedback`) is triggered by a GitHub PR review webhook
1114
+ and emits a **continue-run `NodeEnvelope`** that re-enters the Orchestrator's existing
1115
+ `continue-run` path — the same path that handles a resumed `correlationId` after HITL (the
1116
+ `recordAgentResult` / `run-state merge` transition at `SoftwareTeamsOrchestrator.node.ts:164-182`).
1117
+
1118
+ **No new return edge is added.** The PR-Feedback node is a new webhook-triggered SOURCE node.
1119
+ Its output is a standard `NodeEnvelope` with the ORIGINAL `correlationId` (recovered from the
1120
+ PR tag via `parseCorrelationTag`) and the categorised feedback in the optional `feedback` field
1121
+ (CONTRACT.md §7.2). It flows FORWARD into the Orchestrator's continue path — the same forward
1122
+ flow that carries any resumed envelope. The DAG remains forward-only (ADR-001 Decision C,
1123
+ ADR-002 Decision F).
1124
+
1125
+ **Feedback categorisation:** the PR-Feedback node shells out to `feedback --json` (the existing
1126
+ CLI primitive from `packages/cli/src/commands/feedback.ts:132-133`) to headlessly categorise
1127
+ the review comments into the `FeedbackComment[]` shape before emitting. This reuses existing
1128
+ tested logic — the node adds no new categorisation code.
1129
+
1130
+ **Secrets (AC8):** the GitHub token is read from the `SoftwareTeamsApi` credential and injected
1131
+ into the child process env (`GITHUB_TOKEN`). It never appears on the envelope, node output, logs,
1132
+ or the model prompt.
1133
+
1134
+ ---
1135
+
1136
+ ## Decision R — Multi-round HITL: re-park on resume instead of delete-on-resume (AC3, AC4)
1137
+
1138
+ The `SoftwareTeamsHitl` node replaces the single-round `SlackHitl` pattern with genuine
1139
+ multi-round back-and-forth by changing ONE behaviour: on resume it **re-saves conversation
1140
+ state** (for the next round) instead of deleting it.
1141
+
1142
+ **State machine:**
1143
+
1144
+ ```
1145
+ status: 'needs-input'
1146
+
1147
+
1148
+ saveState(correlationId, ...) ← Ask mode: persist state
1149
+ putExecutionToWait()
1150
+
1151
+ (human replies on the channel)
1152
+
1153
+
1154
+ loadState(correlationId) ← Resume mode: load state
1155
+ runAgentTurn(...)
1156
+
1157
+ ├── status: 'needs-input' → saveState again (re-park for next round)
1158
+ │ putExecutionToWait()
1159
+ └── status: 'ok' / 'error' → emit terminal envelope (conversation done)
1160
+ ```
1161
+
1162
+ The `deleteState` call from the legacy Slack HITL (which fired on the FIRST resume) is moved to
1163
+ the Cleanup node — it is called as part of the merge-triggered cleanup (Decision S), not on
1164
+ resume. This ensures a multi-round conversation can progress without losing state between rounds.
1165
+
1166
+ **Multi-channel (AC4):** the `SoftwareTeamsHitl` node supports `slack`, `email` (`smtpUrl`),
1167
+ `notify` (n8n native notification), and `discord` (priority for the first live test). Channel
1168
+ selection: (1) the explicit node `Channel` param; (2) the optional `hitlChannel` envelope field
1169
+ (CONTRACT.md §7.3); (3) default `discord`. Channel tokens are read from the `SoftwareTeamsApi`
1170
+ credential — `discordBotToken` and `smtpUrl` (added in the T8 credential extension, T12 verifies
1171
+ they load in the rebuilt dist). Tokens are never on the envelope, output, or logs (AC8).
1172
+
1173
+ **Why not refactor `SlackHitl`:** the legacy `SoftwareTeamsSlackHitl` node is preserved
1174
+ unchanged. It is an existing contract surface used in deployed workflows. The new `SoftwareTeamsHitl`
1175
+ is additive — existing `SlackHitl`-based workflows are unaffected (ADR invariant: additive only).
1176
+
1177
+ ---
1178
+
1179
+ ## Decision S — Merge-triggered cleanup: idempotent + safe (AC5)
1180
+
1181
+ The `SoftwareTeamsCleanup` node is triggered by a GitHub MERGE webhook and performs an
1182
+ **idempotent, safe teardown** of all state for the `correlationId` extracted from the merged
1183
+ PR's body tag (Decision P).
1184
+
1185
+ **Cleanup order (idempotent — each step is a no-op if already absent):**
1186
+
1187
+ 1. `deleteRunState(correlationId)` — removes `runs[correlationId]` from workflow global static
1188
+ data (the new `deleteRunState` function, symmetric to `writeRunState`/`readRunState`).
1189
+ 2. `deleteState(correlationId)` — removes the HITL conversation-state record from `HITL_STATE_PATH`.
1190
+ 3. Worktree removal — calls the `worktree-remove` CLI primitive for each registered worktree scoped
1191
+ to the `correlationId`. Uses the existing `worktree-remove.ts` / `worktree-merge.ts` primitives
1192
+ (`packages/cli/src/commands/`).
1193
+ 4. Repo clone removal — removes the run-scoped checkout directory (the shallow clone the Workspace
1194
+ node created).
1195
+ 5. Agent memory removal — clears per-agent memory files scoped to the `correlationId`.
1196
+ 6. Plan/task artefact removal — removes `.software-teams/` plan and task files for the run.
1197
+
1198
+ **Idempotency:** each step checks for existence before acting. Running the Cleanup node twice for
1199
+ the same `correlationId` is a no-op on the second call.
1200
+
1201
+ **Safety constraints (R-02):** the GitHub token is used ONLY to verify the PR was actually merged
1202
+ (not just closed) before cleanup starts. It is never written to the envelope, node output, or logs.
1203
+ No run-state or plan content is logged.
1204
+
1205
+ **Triggered by MERGE, not by CLOSE:** a closed (but not merged) PR does NOT trigger cleanup.
1206
+ The Cleanup node verifies merge status via the GitHub API before proceeding.
1207
+
1208
+ ---
1209
+
1210
+ ## ADR-001..004 invariants honoured
1211
+
1212
+ | Invariant | Source | How this ADR honours it |
1213
+ |-----------|--------|------------------------|
1214
+ | Forward-only DAG — no return edge from Agent to Orchestrator | ADR-001 Decision C | PR-Feedback is a new SOURCE node (webhook-triggered). Its output flows FORWARD into the existing Orchestrator continue path. No Agent→Orchestrator edge is added. |
1215
+ | `correlationId` is the run/conversation join key | ADR-001 Decision C, ADR-002 Decision D | PR-Feedback recovers `correlationId` from the PR body tag (Decision P). Cleanup uses it to scope all teardown operations. HITL uses it for conversation-state keying (unchanged from ADR-001). |
1216
+ | Run-state lives in global static data (`'global'`) | ADR-004 Decision J | `deleteRunState` operates on `getWorkflowStaticData('global')['runs']` — symmetric to the existing read/write operations. |
1217
+ | Additive-only contract | ADR-004 Decision N | The `feedback` and `hitlChannel` envelope fields are optional (CONTRACT.md §7). The PR-tag is written to the PR body, not the envelope. No existing field is changed. |
1218
+ | Secrets via credential only, never on envelope/logs | ADR-001 Decision B (R-02), ADR-002 Decision I | Discord token, SMTP URL, and GitHub token live in `SoftwareTeamsApi` credential. Channel delivery functions receive tokens as function arguments from the credential — never from the envelope or node params. |
1219
+ | CJS load boundary — every node loads under Node (not just Bun) | ADR-003 Decision 5 | All three new nodes are registered in `n8n.nodes[]` and verified by the `verify:node-load` gate (`bun run verify:node-load`, CWD = `packages/n8n/`). Gate passes green (11/11). |
1220
+
1221
+ ---
1222
+
1223
+ ## Non-goals (explicit — no implementer re-opens these)
1224
+
1225
+ - **No new return edge** from any node to the Orchestrator. PR-feedback re-enters via a SOURCE
1226
+ node (PR-Feedback) feeding the existing continue path, not via a backward edge.
1227
+ - **No refactor of `SoftwareTeamsSlackHitl`.** It is preserved unchanged. `SoftwareTeamsHitl`
1228
+ is the additive multi-channel, multi-round replacement.
1229
+ - **No canvas wiring guide / example workflow JSON.** Deferred to a separate
1230
+ `/st:create-dev-plan` follow-up after these gaps ship (spec Out of Scope).
1231
+ - **No new credential.** Discord and email tokens are additional fields on the existing
1232
+ `SoftwareTeamsApi` credential — no new credential type is introduced.