Skip to content

Commit 2c3bb88

Browse files
Merge commit 'b186fa87c1b5bb9ffd6562d305ebc611a53e1cce' into 3.x
# Conflicts: # packages/cli/test/integration/public-api/endpoints-with-scopes-enabled.test.ts # packages/cli/test/integration/public-api/workflows.test.ts # packages/cli/test/integration/workflows/workflows.controller.ee.test.ts # packages/cli/test/integration/workflows/workflows.controller.test.ts # pnpm-lock.yaml
2 parents 2d79851 + b186fa8 commit 2c3bb88

573 files changed

Lines changed: 30162 additions & 4981 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/content-design/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,10 @@ When suggesting new keys, follow the existing hierarchy. Browse nearby keys in
111111

112112
### Language and grammar
113113

114+
**ASD-STE100 Simplified Technical English.** Use short sentences, the active
115+
voice, and one instruction for each sentence. Use one approved word for each
116+
meaning.
117+
114118
**US English.** Always. No exceptions.
115119
- Do: "categorizing", "color", "analyze"
116120
- Don't: "categorising", "colour", "analyse"

.agents/skills/conventions/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Use this skill when you need quick reminders on critical patterns.
1313

1414
## Critical Rules (Must Follow)
1515

16+
**Technical writing (comments, PRs, issues, docs):**
17+
- Write in ASD-STE100 Simplified Technical English: short sentences, the
18+
active voice, one instruction for each sentence
19+
1620
**TypeScript:**
1721
- Never `any` → use `unknown`
1822
- Prefer `satisfies` over `as` (except tests)

.agents/skills/create-issue/SKILL.md

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,20 @@ Structure the description using markdown headers. Use the appropriate template:
7373
**For features / enhancements:**
7474

7575
```markdown
76-
## Summary
77-
[One-paragraph overview of what this adds or changes]
76+
## Goal
77+
[What this adds and why: the user problem it solves]
78+
79+
## Background
80+
[Current state and the gap, plus the technical context needed to plan the work: relevant constraints, prior findings, and links to any related investigation.]
7881

79-
## Problem
80-
[What limitation or gap exists today]
82+
## Scope
83+
[Concrete list of what changes. Name the files or areas to create or modify and any existing pattern to follow.]
8184

82-
## Proposed solution
83-
[How it should work — technical approach if known]
85+
## Acceptance criteria
86+
[Testable outcomes, including automated tests.]
8487

8588
## Out of scope
86-
[Explicitly note what this does NOT cover, if helpful]
89+
[What this explicitly does not cover]
8790
```
8891

8992
**For tech debt:**
@@ -109,13 +112,19 @@ Structure the description using markdown headers. Use the appropriate template:
109112

110113
```markdown
111114
## Goal
112-
[What question are we trying to answer]
115+
[What question(s) are we trying to answer]
113116

114117
## Context
115118
[Why this investigation is needed now]
116119

120+
## Questions
121+
1. [Specific question to resolve]
122+
117123
## Expected output
118-
[What deliverable is expected — RFC, PoC, decision document, etc.]
124+
[What deliverable is expected: RFC, PoC, decision document, path matrix, etc.]
125+
126+
## Acceptance criteria
127+
[How we know the spike is done: each question answered, deliverable produced]
119128
```
120129

121130
#### Attachments (Screenshots / Videos)

.agents/skills/create-pr/SKILL.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ Creates GitHub PRs with titles that pass n8n's `check-pr-title` CI validation.
9393
9494
Based on `.github/pull_request_template.md`:
9595
96+
Write the PR body in ASD-STE100 Simplified Technical English: use short
97+
sentences, the active voice, and one instruction for each sentence.
98+
9699
### Summary Section
97100
- Describe what the PR does
98101
- Include screenshots/videos for UI changes

.devcontainer/codespaces/README.md

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,19 @@ A turn stops after about 25 minutes (`TURN_TIMEOUT_MS`). This limit is below the
7171
n8n Wait limit. So the worker reports a clear message before n8n reports a
7272
generic timeout. Keep the worker limit below the n8n limit if you change either.
7373

74+
**A turn is atomic, and the worker tells the session so.** The turn ends on the
75+
session's final message, and its children end with it: a background `Bash` task
76+
is killed, `Monitor` events never arrive, `PushNotification` has nowhere to go,
77+
and `ScheduleWakeup` never fires. The session also gets no turn of its own to
78+
report back in — the turn's resume URL continues one waiting n8n execution and is
79+
then spent, so nothing on the box can post to the thread unprompted. A session
80+
that backgrounds a build and signs off with "I'll verify once it finishes" is
81+
therefore describing something that cannot happen. The worker states this in
82+
`--append-system-prompt` on every turn (`turnContract`), together with a pointer
83+
to this file for the box-specific parts. This is only the n8n/Slack path: an
84+
interactive session (`pnpm session`, tmux) is long-lived, so background work,
85+
monitors and notifications behave normally there.
86+
7487
### Build and run the app in a session
7588

7689
The prebuild already installed the dependencies and warmed the build. So a
@@ -80,10 +93,15 @@ session rarely needs a cold `pnpm install` or a full `pnpm build`. Both are slow
8093
- **Bring the app up with one command: `pnpm dev:up`.** It installs missing
8194
dependencies, starts the backend, waits for health, and prints the URL. Add
8295
`--build` only when a frontend change must appear (see below).
83-
- **Open the app** at `https://<codespace-name>-5678.app.github.dev`. The port
84-
is private. It opens for you in a browser that is signed in to GitHub. You do
85-
not need a tunnel. An anonymous or server caller gets a 302. That is why the
86-
worker polls outward instead.
96+
- **Open the app** at `https://<codespace-name>-5678.app.github.dev`. `dev:up`
97+
makes that port visible to the org, thus any n8n member who is signed into
98+
GitHub can open it. You do not need a tunnel. GitHub makes every forwarded port
99+
private again at each container start, so `dev:up` shares it again on each run.
100+
To see the current state, run `gh codespace ports`. The share command needs `gh`
101+
with the codespace scope (see the one-time setup above). If it fails, `dev:up`
102+
starts the app, prints the reason, and gives you the command to try again. A
103+
private port opens for you only, in a browser that is signed in to GitHub. An
104+
anonymous or server caller gets a 302. That is why the worker polls outward.
87105
- **`pnpm dev` no longer exists.** Use `pnpm dev:be` for the backend (on 5678).
88106
Use `pnpm dev:fe:editor` for the editor UI with hot reload (on 8080).
89107
- **`dev:be` serves the editor from the `dist` build.** So a frontend edit does
@@ -95,8 +113,10 @@ session rarely needs a cold `pnpm install` or a full `pnpm build`. Both are slow
95113
turbo cache and is fast when warm.
96114
- To clear stale build outputs after a branch switch, run `pnpm reset`. Add
97115
`--full` if that does not clear it.
98-
- Give a long build its own turn. Do not chain an install and a full build
99-
behind other work in one turn.
116+
- Run a long build in the foreground and give it its own turn. Backgrounding it
117+
does not help: it is killed when the turn ends (see above). Do not chain an
118+
install and a full build behind other work in one turn either — that is what
119+
runs into the 25-minute limit.
100120

101121
## Flaky tools (MCP)
102122

@@ -187,6 +207,14 @@ After a stop, `pnpm session <name>` restarts the codespace (~30–60 s); run
187207
Code shows `Missing environment variables: FLAKY_MCP_TOKEN`, the shell that
188208
started Claude did not source the file. Run
189209
`. /usr/local/lib/codespaces-env.sh` and start Claude again.
210+
- **Do not read `CODESPACE_NAME` or `GITHUB_USER` from the process env** — use
211+
`scripts/codespace-env.mjs`. Codespaces gives these variables to VS Code
212+
sessions only. Other processes read them from `codespaces-env.sh`, and a
213+
process that tmux starts can get an empty copy: tmux keeps the environment of
214+
its own start, and `update-environment` does not refresh these keys. A worker
215+
polled correctly as its owner while `dev:up` in the same session saw an empty
216+
box name, printed the localhost URL, and did not share the port. The helper
217+
reads `/workspaces/.codespaces/shared`, which is always correct.
190218
- **You cannot paste images into a remote Claude session.** Image paste reads
191219
the clipboard of the machine where `claude` runs — the codespace, not your
192220
laptop. Drag the file into the VS Code explorer (or

.devcontainer/codespaces/agent-worker.mjs

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@
1010
// Env:
1111
// N8N_DEQUEUE_URL n8n webhook that hands back one pending turn (required)
1212
// AGENT_WORKER_TOKEN shared bearer sent on every dequeue (required)
13-
// GITHUB_USER box owner's login; the bootstrap route for a new thread (codespaces set this)
14-
// CODESPACE_NAME stable box id; routes a thread back to the box holding its session (codespaces set this)
13+
// GITHUB_USER box owner's login; the bootstrap route for a new thread (see codespace-env.mjs)
14+
// CODESPACE_NAME stable box id; routes a thread back to the box holding its session (same source)
1515
// TURN_TIMEOUT_MS per-turn limit; keep below the n8n Wait limit (default 25 min)
1616
import { execFile } from 'node:child_process';
1717
import { resolve as resolvePath, sep } from 'node:path';
1818
import { setTimeout as sleep } from 'node:timers/promises';
1919

20+
import { codespaceEnv } from '../../scripts/codespace-env.mjs';
21+
2022
const DEQUEUE_URL = process.env.N8N_DEQUEUE_URL;
2123
const TOKEN = process.env.AGENT_WORKER_TOKEN;
22-
const GITHUB_USER = process.env.GITHUB_USER;
23-
const BOX_ID = process.env.CODESPACE_NAME;
24+
// Read both identities from the codespace. tmux can give an empty copy of either.
25+
const GITHUB_USER = codespaceEnv('GITHUB_USER');
26+
const BOX_ID = codespaceEnv('CODESPACE_NAME');
2427
const ROOT = '/workspaces';
2528

2629
const POLL_INTERVAL_MS = 3000;
@@ -53,20 +56,71 @@ for (const [k, v] of Object.entries({
5356

5457
// Not fatal, but box pinning needs it: without a box id every turn routes by
5558
// owner, so a thread cannot follow the box holding its session.
56-
if (!BOX_ID) console.error('CODESPACE_NAME is not set — box pinning disabled; turns route by githubUser only.');
59+
if (!BOX_ID)
60+
console.error(
61+
'CODESPACE_NAME did not resolve — box pinning disabled; turns route by githubUser only.',
62+
);
63+
64+
// A turn gets a copy of this environment. Put the correct values in it, because
65+
// `pnpm dev:up` and `gh -c $CODESPACE_NAME` in the session need them.
66+
const TURN_ENV = { ...process.env };
67+
if (BOX_ID) TURN_ENV.CODESPACE_NAME = BOX_ID;
68+
if (GITHUB_USER) TURN_ENV.GITHUB_USER = GITHUB_USER;
69+
70+
const CODESPACE_DOCS = '.devcontainer/codespaces/README.md';
71+
72+
// A session cannot be told any of this after its final message, and a system
73+
// prompt is not part of the resumed transcript, so send it on every turn. State
74+
// the turn's hard limits inline: a session that has to read a file to learn them
75+
// can reply before it gets there. Point at the box docs for the rest — they
76+
// already cover dev:up, ports, and build cost, and AGENTS.md does not.
77+
function turnContract(author) {
78+
return [
79+
'# Your runtime',
80+
'You are one turn of a Slack thread, driven by an n8n workflow that runs you as a headless',
81+
'`claude -p` on a GitHub codespace. Your final message is the reply that reaches Slack, so keep',
82+
'it short and skip heavy markdown.',
83+
author ? `You are replying to ${author}.` : '',
84+
'',
85+
'# A turn is atomic',
86+
'The turn ends when you emit your final message, and everything you started ends with it:',
87+
'background Bash tasks are killed, Monitor events never arrive, PushNotification has nowhere to',
88+
'go, and ScheduleWakeup never fires. You get no turn of your own afterwards — you cannot speak',
89+
'again until a human writes again. So run long work (builds, test suites, restarts) in the',
90+
'foreground of this turn and wait for it, or do not start it at all. Never end a turn promising',
91+
`to verify, check back, or follow up. Work that will not fit the turn limit of ~${Math.round(
92+
TURN_TIMEOUT_MS / 60_000,
93+
)} minutes`,
94+
'should be split: do the part that fits, then say what to ask for next.',
95+
'',
96+
'# This box',
97+
`You are on codespace ${BOX_ID ?? '(unknown)'}, not a laptop. Before you build, start, or expose`,
98+
`the app, read ${CODESPACE_DOCS} ("Build and run the app in a session"). It is box-specific and`,
99+
'the repo AGENTS.md does not cover it.',
100+
]
101+
.filter(Boolean)
102+
.join('\n');
103+
}
57104

58-
function runClaude({ message, sessionId, cwd }) {
105+
function runClaude({ message, sessionId, cwd, author }) {
59106
const safeCwd = resolvePath(typeof cwd === 'string' && cwd ? cwd : `${ROOT}/n8n`);
60107
if (safeCwd !== ROOT && !safeCwd.startsWith(ROOT + sep))
61108
throw new Error(`cwd must be under ${ROOT}`);
62-
const args = ['-p', '--output-format', 'json', '--dangerously-skip-permissions'];
109+
const args = [
110+
'-p',
111+
'--output-format',
112+
'json',
113+
'--dangerously-skip-permissions',
114+
'--append-system-prompt',
115+
turnContract(typeof author === 'string' ? author : ''),
116+
];
63117
if (sessionId) args.push('--resume', sessionId);
64118
args.push(message);
65119
return new Promise((res, rej) => {
66120
execFile(
67121
'claude',
68122
args,
69-
{ cwd: safeCwd, timeout: TURN_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024 },
123+
{ cwd: safeCwd, env: TURN_ENV, timeout: TURN_TIMEOUT_MS, maxBuffer: 64 * 1024 * 1024 },
70124
(err, stdout, stderr) => {
71125
try {
72126
res(JSON.parse(stdout));

.devcontainer/codespaces/docker-compose.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ services:
2828
DB_POSTGRESDB_PASSWORD: password
2929
# Prompt-cache TTL in seconds. 1 hour keeps the cache warm between turns.
3030
ANTHROPIC_PROMPT_CACHE_TTL: "3600"
31-
# Compose does not inherit the codespace env. Pass the box name through so
32-
# the worker can stamp boxId and dev:up can print the forwarded URL.
33-
CODESPACE_NAME: ${CODESPACE_NAME:-}
31+
# Do not add CODESPACE_NAME here. Codespaces does not give it to compose, so
32+
# this file can set an empty value only. An empty value is worse than no
33+
# value, because tmux keeps it for each new pane. The consumers read the name
34+
# with scripts/codespace-env.mjs.

.github/WORKFLOWS.md

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,8 @@ Push to master/1.x
382382

383383
| Schedule (UTC) | Workflow | Purpose |
384384
|---------------------------|-----------------------------------|--------------------------|
385-
| Hourly :00 | `sec-sync-public-to-private.yml` | Mirror public → private, refresh bundle branches |
385+
| Hourly :00 | `sec-sync-public-to-private.yml` | Mirror public → private |
386+
| Every 6h :00 | `sec-rebase-bundle-branches.yml` | Replay `bundle/*` onto their base |
386387
| Daily 00:00 | `docker-build-push.yml` | Nightly Docker images |
387388
| Daily 00:00 | `test-db.yml` | Database compatibility |
388389
| Daily 00:00 | `test-e2e-performance-reusable.yml`| Performance E2E |
@@ -503,6 +504,18 @@ Scripts in `.github/scripts/`:
503504
| `send-build-stats.mjs` | Build telemetry | `setup-nodejs` action |
504505
| `db-test-matrix.mjs` | DB test matrix from `postgres-versions.json` | `ci-pull-requests.yml` |
505506

507+
### Branch Replay Scripts
508+
509+
Both keep a long-lived branch that is "base + its own commits" in sync by rebasing those
510+
commits onto the base and force-pushing, sharing the merge-tree content guard that makes the
511+
rewrite safe.
512+
513+
| Script | Purpose | Called By |
514+
|----------------------------|----------------------------------------------------------------------|------------------------------------|
515+
| `branch-replay.mjs` | Shared primitives: merge-tree, tree guard, marker scan | the two scripts below |
516+
| `sync-master-to-3x.mjs` | master → `3.x`; auto-resolves mechanical files, opens a conflict PR | `util-sync-master-to-3x.yml` |
517+
| `rebase-bundle-branch.mjs` | base → `bundle/*` in n8n-private; fail-loud, never resolves conflicts | `sec-rebase-bundle-branches.yml` |
518+
506519
### Slack Scripts
507520

508521
See [Slack Notifications](#slack-notifications) for the calling pattern.
@@ -698,14 +711,30 @@ mirroring public `master` and `1.x` into private with `reset --hard` +
698711
commits when judging "ahead". Fixes are never committed to private `master`/`1.x`
699712
directly: `ci-restrict-private-merges.yml` requires PRs into them to come from the
700713
long-lived integration branches `bundle/2.x` and `bundle/1.x` (a `bundle/2.x` merge is
701-
backported to `bundle/1.x` by `util-backport-bundle.yml`). The sync creates those
702-
branches if missing and then **merges `master` into `bundle/2.x` and `1.x` into
703-
`bundle/1.x`** so they don't drift; on a conflict it aborts the merge, leaves the branch
704-
untouched, and emits a warning annotation while **keeping the run green** — the other
705-
bundle branch still syncs, and a human resolves the conflict by hand. Once a bundle
706-
branch is merged into private `master`/`1.x` as a `chore: Bundle/*` PR,
707-
`sec-publish-fix.yml` / `sec-publish-fix-1x.yml` cherry-pick that merge commit onto a
708-
fresh branch in the public repo and open the PR there.
714+
backported to `bundle/1.x` by `util-backport-bundle.yml`). Once a bundle branch is merged
715+
into private `master`/`1.x` as a `chore: Bundle/*` PR, `sec-publish-fix.yml` /
716+
`sec-publish-fix-1x.yml` cherry-pick that commit onto a fresh branch in the public repo and
717+
open the PR there. That PR **must stay a single-parent squash** — the publish step is a bare
718+
`git cherry-pick` of `HEAD`, which aborts on a merge commit.
719+
720+
`sec-rebase-bundle-branches.yml` keeps those branches current, every 6 hours plus whenever a
721+
PR is merged into one (and on `workflow_dispatch`). It **replays** the bundle-only commits
722+
onto the base with `git rebase` and force-pushes, via
723+
[`scripts/rebase-bundle-branch.mjs`](scripts/rebase-bundle-branch.mjs) — so a clean run
724+
adds **no commit at all** and `base..bundle` stays a readable list of the fixes not yet
725+
published. Rebasing is safe here precisely because a bundle ships as one squashed,
726+
deliberately obfuscated commit: SHAs and dates on these branches carry no meaning downstream,
727+
and the `n8n-assistant` app is a bypass actor on the `bundle/*` ruleset's `non_fast_forward`
728+
rule. Every push is verified to carry exactly the tree a merge of the two sides would produce
729+
(`git merge-tree`); a mismatch, or a conflict marker, fails the run instead of pushing. Fixes
730+
already published come back through the base and are dropped by `--empty=drop`.
731+
732+
There is **one job per bundle branch**. A conflict is detected from the merge tree before the
733+
working tree is touched, so the branch is left exactly as it was, that job **fails** (no more
734+
green runs hiding a stalled branch) and `#alerts-security` gets a run link — while the other
735+
branch still syncs. Recovery is deliberate: rebase the branch onto its base locally, resolve,
736+
force-push, then re-run the workflow. The replay never resolves a conflict itself, unlike
737+
`util-sync-master-to-3x.yml`.
709738

710739
See **[`../AGENTS.md`](../AGENTS.md)** ("Security Fix Hygiene") for the naming rules that
711740
keep the vulnerability out of public branch names, commits, and test descriptions.

0 commit comments

Comments
 (0)