Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/alcode-inline-wake-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/alcode": patch
---

Prevented OpenClaw completion wake-ups from being lost by keeping the wake command's semicolon on the `alcode` command line.
5 changes: 5 additions & 0 deletions .changeset/alignfirst-merge-discipline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"alignfirst": patch
---

Tightened merge conflict resolution and validation guidance.
5 changes: 5 additions & 0 deletions .changeset/discord-thread-rename-fidelity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/openclaw-channel-mock-core": patch
---

Matched Discord thread behavior: inbound metadata uses native channel targets, `send` with `threadName` renames the resolved thread, and `thread-reply` leaves its name unchanged.
5 changes: 5 additions & 0 deletions .changeset/durable-thread-handoff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/alignfirst-developer-openclaw-plugin": minor
---

Added durable activation and claim handling for confirmed Slack and Discord thread starters.
5 changes: 5 additions & 0 deletions .changeset/fix-pnpm-fixture-install.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/openclaw-test": patch
---

Fixed the pnpm fixture-install example for current pnpm releases.
7 changes: 7 additions & 0 deletions .changeset/native-thread-mock-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@paleo/openclaw-channel-mock-core": minor
"@paleo/openclaw-slack-mock": minor
"@paleo/openclaw-discord-mock": minor
---

Added configurable Slack thread routing, native starter receipt shapes, and canonical thread-session delivery. A send whose target names a stored thread now lands in that thread under its parent conversation, with or without an accompanying `threadId`. Discord-shaped `thread-create` now returns the native `{ ok, thread }` shape, with `partial: true` when the thread exists but its starter was not delivered; the former `threadId`, `target` and `message` fields are gone. Discord-shaped `thread-reply` now accepts a bare `threadId` as its delivery target, as bundled Discord does. The test-bus fault injector accepts `threadOnly: true` to fail only a threaded send.
5 changes: 5 additions & 0 deletions .changeset/scenario-context-bus-url.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@paleo/openclaw-test": minor
---

Added `ctx.busUrl` to `ScenarioContext` for direct test-bus calls.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,9 @@ jobs:
cd "$(mktemp -d)"
npm init -y > /dev/null
# --omit=peer: auto-installed peers drag unrelated trees into the audit
for i in 1 2 3 4 5; do
for i in $(seq 1 10); do
if npm install --omit=peer $pkgs; then break; fi
if [ "$i" = 5 ]; then echo "Install failed after 5 attempts"; exit 1; fi
if [ "$i" = 10 ]; then echo "Install failed after 10 attempts"; exit 1; fi
echo "Registry not ready, retrying in 30s"
sleep 30
done
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ Nine Agent Skill stubs expose the CLI through commands such as `/alspec` in Clau

## OpenClaw Test toolkit

`@paleo/openclaw-test` and three companion channel packages: they are a Dockerised regression-test harness that drives OpenClaw through synthetic Discord and Slack channels. See [packages/openclaw-test/README.md](packages/openclaw-test/README.md).
`@paleo/openclaw-test` and three companion channel packages are a Dockerised regression-test harness that drives OpenClaw through synthetic Discord and Slack channels. See [packages/openclaw-test/README.md](packages/openclaw-test/README.md).

## AlignFirst Developer

AlignFirst Developer is an AI teammate for software work, currently packaged on OpenClaw. See [alignfirst-developer.md](alignfirst-developer.md).

[`@paleo/alignfirst-developer-openclaw-plugin`](packages/alignfirst-developer-openclaw-plugin/README.md) supplies its OpenClaw capabilities under plugin ID `alignfirst-developer`. Its first capability, thread handoff, durably activates the ordinary thread session after confirmed native starter delivery.

---

## Setup with your agent
Expand Down
4 changes: 2 additions & 2 deletions alignfirst-developer-tests/.env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ ALIGNFIRST_CODE_AGENT=codex
# ALIGNFIRST_CODE_MODELS=terra

# Optional: run N matrix cells concurrently, each on its own worker stack
# (`run --parallel` wins over this).
# OPENCLAW_TEST_PARALLEL=6
# (`run --parallel` wins over this). A development laptop handles 3.
OPENCLAW_TEST_PARALLEL=3

# Optional overrides (defaults relative to the project dir):
# OPENCLAW_CONFIG_PATH=./openclaw.json
Expand Down
20 changes: 11 additions & 9 deletions alignfirst-developer-tests/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,13 @@ RUN chmod +x /opt/openclaw-test/scripts/*.mjs
USER claw

# Pin pnpm's content-addressable store outside /home/claw/projects/, which is a
# docker volume on its own filesystem. Without this, pnpm walks up from a
# worktree install target and creates /home/claw/projects/.pnpm-store, polluting
# the project listing the agent grounds project names in. Cost: pnpm falls back
# to copy instead of hardlinks across filesystems — irrelevant at fixture size.
RUN printf 'store-dir=/home/claw/.pnpm-store\n' > /home/claw/.pnpmrc && \
# docker volume on its own filesystem. Without this, pnpm creates a fresh
# /home/claw/projects/.pnpm-store per cell and re-downloads every fixture
# dependency (10 s alone, minutes under three parallel cells); with it, the
# template install below warms the store the fixtures reuse offline. pnpm 12
# reads this from ~/.config/pnpm/config.yaml, which `pnpm config set` writes.
# Cost: copy instead of hardlinks across filesystems — irrelevant at fixture size.
RUN pnpm config set store-dir /home/claw/.pnpm-store --global && \
mkdir -p /home/claw/.pnpm-store

# The @paleo/openclaw-* packages install from locally-built tarballs under
Expand All @@ -67,13 +69,13 @@ COPY --chown=claw:claw openclaw.json /home/claw/.openclaw/openclaw.json
RUN npm ci --include=dev && \
OPENCLAW_CONFIG_PATH=/home/claw/.openclaw/openclaw.json \
npx openclaw plugins install --force --accept-capabilities \
npm:@openclaw/codex@2026.8.2 && \
npm:@openclaw/codex@2026.9.2 && \
OPENCLAW_CONFIG_PATH=/home/claw/.openclaw/openclaw.json \
npx openclaw plugins install --force --accept-capabilities \
npm:@openclaw/zai-provider@2026.8.2 && \
npm:@openclaw/zai-provider@2026.9.2 && \
OPENCLAW_CONFIG_PATH=/home/claw/.openclaw/openclaw.json \
npx openclaw plugins install --force --accept-capabilities \
npm:@openclaw/perplexity-plugin@2026.8.2 && \
npm:@openclaw/perplexity-plugin@2026.9.2 && \
OPENCLAW_CONFIG_PATH=/home/claw/.openclaw/openclaw.json \
npx openclaw plugins registry --refresh && \
npm cache clean --force
Expand All @@ -83,7 +85,7 @@ RUN npm ci --include=dev && \
# lumen, and external-team project orion with per-name patches.
COPY --chown=claw:claw projects-fixture/template/ /opt/alignfirst-developer-tests/fixtures/template/
RUN cd /opt/alignfirst-developer-tests/fixtures/template && \
pnpm install --frozen-lockfile --prod=false
pnpm install --frozen-lockfile

# Install AlignFirst skills under /home/claw/.agents/skills/ (the path OpenClaw scans).
RUN npx -y skills add https://github.com/paleo/alignfirst --global --yes \
Expand Down
45 changes: 40 additions & 5 deletions alignfirst-developer-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,14 @@ The root and its nested `external-projects` and `lifecycle-projects` directories

## Scenarios

Drop `scenarios/<id>.ts`, default-export `async (ctx: ScenarioContext) => void`. Shared helpers under `scenarios/_lib/` (skipped by the runner's discovery). Current scenarios: `A01`–`A21` and `A23`–`A26`.
Drop `scenarios/<id>.ts`, default-export `async (ctx: ScenarioContext) => void`. Shared helpers under `scenarios/_lib/` (skipped by the runner's discovery). Current scenarios: `A01`–`A21` and `A23`–`A28`.

Almost every one starts with `bootstrapThreadFromChannel` (`_lib/thread-bootstrap.ts`): it sends the channel message, waits for the starter, and asserts the channel session stopped right there — one thread post, no second one, no worktree on disk, no coding-agent call, nothing substantive leaked to the channel root. `sendInThread` then wakes the thread session, which owns the actual work. A scenario that seeds a worktree first passes its absolute path as `seededWorktreePaths` so the check still catches anything the channel session created.
Almost every one starts with `bootstrapThreadFromChannel` (`_lib/thread-bootstrap.ts`). It sends the
channel message, waits for exactly one confirmed native starter and one `thread_handoff start`, and
checks the parent session's attributed tool trace for target work. The plugin wakes the thread
session automatically; complete requests need no mechanical follow-up. `sendInThread` remains for
genuine missing values, explicit holds, confirmations, and later requests. Target work may begin
before the parent emits its final `NO_REPLY`, so assertions follow the starter's original cursor.

`A10` exercises the real `alcode` foreground run driven as an OpenClaw background exec and rejects direct Claude or Codex launches. `A11` covers an explicit user hold. `A12` chains two delegations in one thread, exposing the heartbeat-cooldown wake gate. `A13` drives alcode directly for deterministic selected-agent new/resume coverage and Codex failure handling. The shared mock serves a bundled Codex model catalog and both agents' JSONL protocols.

Expand All @@ -69,6 +74,14 @@ Almost every one starts with `bootstrapThreadFromChannel` (`_lib/thread-bootstra

`A23` resolves a PR URL through review and its reported outcome. `A24` carries a multi-project base refresh through one no-protocol delegation per project. `A25` captures a detailed request before workspace setup and coding. `A26` reserves the next side ticket `side-N` before workspace setup for explicit no-ticket work.

`A27-human-reply-racing-startup` sends a genuine missing-ticket answer immediately after native
starter delivery. `A28-recoverable-handoff-failure` injects one test-bus delivery failure, then
requires one successful starter and automatic work without creating a replacement target.

`A01`, `A04`, `A05` and `A21` open a thread whose starter asks for a value, then pin the
silent seed turn (`_lib/silent-seed-turn.ts`): the thread session claims the handoff, posts nothing
for 90 s and reads no thread history.

Rebuild the CLIs and harness image before focused coverage:

```sh
Expand All @@ -80,17 +93,39 @@ ALIGNFIRST_CODE_AGENT=codex npm run e2e -- --channel all A06-off-projects A14-so
ALIGNFIRST_CODE_AGENT=codex npm run e2e -- --channel all A17-project-creation A18-project-removal A19-project-removal-failure
ALIGNFIRST_CODE_AGENT=codex npm run e2e -- --channel all A23-resource-url-handoff A24-multi-project-handoff A25-detailed-request-handoff A26-explicit-no-ticket
ALIGNFIRST_CODE_AGENT=codex npm run e2e -- --channel all A10-coding-session A12-sequential-coding-sessions
ALIGNFIRST_CODE_AGENT=codex npm run e2e -- --channel all A27-human-reply-racing-startup A28-recoverable-handoff-failure
ALIGNFIRST_CODE_AGENT=claude npm run e2e -- --channel all A13-alcode-agent-contract A10-coding-session
npm run e2e -- --model gpt-5.6-terra --channel all --all
```

**Ticket-id convention:** scenario `A<S>` uses `ABC-0<S>N` (`A1` → `ABC-010`, `A2` → `ABC-020`, …; `A10` → `ABC-0100`). The mechanical mapping is a leak signal: while running `A<S>`, any `ABC-0<X>N` with `X ≠ S` is bleed from another scenario. The test sender is `ROBIN01`, listed in [`workspace/USER.md`](workspace/USER.md). A5's `aurora` is deliberately **not** a fixture name (unknown-project path).

## Vendored `@paleo/openclaw-*` packages
## Vendored packages

This harness vendors the **local** sources of the four generic `@paleo/openclaw-*` packages and
`@paleo/alignfirst-developer-openclaw-plugin`.
The dependencies are `file:vendor/<pkg>.tgz`; [`scripts/vendor-packages.mjs`](scripts/vendor-packages.mjs)
builds each package and `npm pack`s it into `vendor/` (gitignored). The Docker build context is this
directory, so the tarballs must live here.

`npm run env:build` chains `vendor` → `npm install` (refreshing `package-lock.json`) →
`openclaw-test env build`, so a source edit in any of the five packages is picked up on the next
build. Run `npm run vendor` before the first standalone `npm install`; the tarballs must exist for
resolution.

This harness always tests the **local** `@paleo/openclaw-*` sources, never npmjs — the four packages iterate in lockstep with the mocks and are frequently ahead of a publish. The dependencies are `file:vendor/<pkg>.tgz`; [`scripts/vendor-packages.mjs`](scripts/vendor-packages.mjs) (`npm run vendor`) builds each package and `npm pack`s it into `vendor/` (gitignored). The Docker build context is this dir, so the tarballs must live here — `../packages/*` is out of reach at build time.
The plugin is explicitly allowlisted, loaded from its installed package path, and exposes optional
tool `thread_handoff`. Slack uses `replyToMode: "off"`; Discord remains non-automatic. Both surface
IDs map to their native receipt contract in `plugins.entries.alignfirst-developer.config.channelSurfaces`.

The complementary deterministic suite makes no model calls and runs outside Docker against the
pinned OpenClaw 2026.9.2 executable:

```sh
KEEP_THREAD_HANDOFF_ARTIFACTS=1 npm run test:integration --workspace @paleo/alignfirst-developer-openclaw-plugin --prefix ..
```

`npm run env:build` chains `vendor` → `npm install` (refreshes `package-lock.json` against the new tarballs) → `openclaw-test env build`, so a source edit in any of the four packages is picked up on the next `env:build` with no manual step. `npm pack` is byte-reproducible, so unchanged sources produce no lockfile churn. Run `npm run vendor` by hand before the first `npm install` (the tarballs must exist for it to resolve).
It retains test-owned gateway logs, scripted-provider requests, configuration, and SQLite restart
state under `/tmp/thread-handoff-*`. Omit the environment variable to clean fixtures automatically.

## Layout

Expand Down
18 changes: 15 additions & 3 deletions alignfirst-developer-tests/openclaw.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@
"auth": { "mode": "none" }
},
"plugins": {
"allow": ["codex", "browser", "discord-mock", "slack-mock", "alignfirst-developer"],
"load": {
"paths": [
"/opt/openclaw-test/src/node_modules/@paleo/openclaw-discord-mock",
"/opt/openclaw-test/src/node_modules/@paleo/openclaw-slack-mock"
"/opt/openclaw-test/src/node_modules/@paleo/openclaw-slack-mock",
"/opt/openclaw-test/src/node_modules/@paleo/alignfirst-developer-openclaw-plugin"
]
},
"entries": {
"codex": { "enabled": true },
"discord-mock": { "enabled": true },
"slack-mock": { "enabled": true }
"slack-mock": { "enabled": true },
"alignfirst-developer": {
"enabled": true,
"config": {
"channelSurfaces": {
"slack-mock": "slack",
"discord-mock": "discord"
}
}
}
},
"slots": { "memory": "none" }
},
Expand All @@ -26,7 +37,7 @@
"update": { "checkOnStart": false },
"tools": {
"profile": "coding",
"alsoAllow": ["message", "browser"],
"alsoAllow": ["message", "browser", "thread_handoff"],
"deny": ["ask_user"]
},
"models": {
Expand Down Expand Up @@ -120,6 +131,7 @@
"botUserId": "openclaw",
"botDisplayName": "myclaw",
"allowFrom": ["*"],
"replyToMode": "off",
"blockStreaming": true
}
}
Expand Down
Loading