archal 0.9.18 → 0.9.20

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 (92) hide show
  1. package/README.md +9 -1
  2. package/agents/github-octokit/.archal.json +8 -0
  3. package/agents/github-octokit/Dockerfile +8 -0
  4. package/agents/github-octokit/README.md +113 -0
  5. package/agents/github-octokit/agent.mjs +54 -0
  6. package/agents/github-octokit/package.json +9 -0
  7. package/agents/github-octokit/scenarios/test-repo-access.md +27 -0
  8. package/agents/google-workspace-local-tools/Dockerfile +6 -0
  9. package/agents/google-workspace-local-tools/README.md +58 -0
  10. package/agents/google-workspace-local-tools/agent.mjs +196 -0
  11. package/agents/google-workspace-local-tools/archal-harness.json +7 -0
  12. package/agents/google-workspace-local-tools/run-input.yaml +16 -0
  13. package/agents/google-workspace-local-tools/scenario.md +29 -0
  14. package/agents/hermes/.archal.json +8 -0
  15. package/agents/hermes/Dockerfile +46 -0
  16. package/agents/hermes/README.md +87 -0
  17. package/agents/hermes/SOUL.md +27 -0
  18. package/agents/hermes/config.yaml +34 -0
  19. package/agents/hermes/drive.mjs +113 -0
  20. package/agents/hermes/scenarios/stripe-customers-read-only.md +32 -0
  21. package/agents/openclaw/.archal.json +8 -0
  22. package/agents/openclaw/Dockerfile +96 -0
  23. package/agents/openclaw/README.md +120 -0
  24. package/agents/openclaw/drive.mjs +311 -0
  25. package/agents/openclaw/package.json +9 -0
  26. package/agents/openclaw/scenarios/github-issue-triage-read-only.md +44 -0
  27. package/agents/openclaw/workspace/AGENTS.md +23 -0
  28. package/agents/openclaw/workspace/IDENTITY.md +8 -0
  29. package/agents/openclaw/workspace/SOUL.md +14 -0
  30. package/agents/openclaw/workspace/TOOLS.md +35 -0
  31. package/agents/pagination-test/README.md +24 -0
  32. package/agents/pagination-test/scenario.md +24 -0
  33. package/agents/replay-capsule-harness/README.md +29 -0
  34. package/agents/replay-capsule-harness/observability-install-offline-e2e.mts +1517 -0
  35. package/agents/replay-capsule-harness/replay-capsule-e2e.mjs +104 -0
  36. package/clone-assets/apify/tools.json +213 -13
  37. package/clone-assets/calcom/tools.json +510 -0
  38. package/clone-assets/clickup/tools.json +1258 -0
  39. package/clone-assets/customerio/tools.json +386 -0
  40. package/clone-assets/datadog/tools.json +734 -0
  41. package/clone-assets/github/tools.json +312 -25
  42. package/clone-assets/gitlab/tools.json +999 -0
  43. package/clone-assets/google-workspace/tools.json +18 -6
  44. package/clone-assets/hubspot/tools.json +1406 -0
  45. package/clone-assets/jira/fidelity.json +1 -1
  46. package/clone-assets/jira/tools.json +266 -543
  47. package/clone-assets/linear/tools.json +238 -40
  48. package/clone-assets/ownerrez/tools.json +548 -0
  49. package/clone-assets/pricelabs/tools.json +343 -0
  50. package/clone-assets/sentry/tools.json +745 -0
  51. package/clone-assets/slack/tools.json +1 -2
  52. package/clone-assets/stripe/tools.json +185 -46
  53. package/clone-assets/supabase/tools.json +511 -14
  54. package/clone-assets/unipile/tools.json +408 -0
  55. package/clone-assets/webflow/tools.json +415 -0
  56. package/dist/autoloop-worker-types-BEb_E44z.d.cts +196 -0
  57. package/dist/cli.cjs +151033 -75282
  58. package/dist/commands/autoloop-hosted-worker.cjs +43942 -0
  59. package/dist/commands/autoloop-hosted-worker.d.cts +143 -0
  60. package/dist/commands/autoloop-pr-verification.cjs +4227 -0
  61. package/dist/commands/autoloop-pr-verification.d.cts +17 -0
  62. package/dist/{vitest/chunk-IVXSSEYS.js → commands/autoloop-result-parser.cjs} +16515 -18857
  63. package/dist/commands/autoloop-result-parser.d.cts +39 -0
  64. package/dist/commands/autoloop-worker.cjs +36163 -0
  65. package/dist/commands/autoloop-worker.d.cts +97 -0
  66. package/dist/harness.cjs +1 -0
  67. package/dist/index.cjs +1 -1
  68. package/dist/replay.cjs +49624 -0
  69. package/dist/replay.d.cts +4625 -0
  70. package/dist/scenarios.cjs +80343 -0
  71. package/dist/scenarios.d.cts +562 -0
  72. package/dist/vitest/chunk-6CBYFCFK.js +4667 -0
  73. package/dist/vitest/chunk-ARVS45PP.js +2764 -0
  74. package/dist/vitest/index.cjs +6079 -75089
  75. package/dist/vitest/index.d.ts +7 -6
  76. package/dist/vitest/index.js +8 -8
  77. package/dist/vitest/runtime/hosted-session-reaper.cjs +801 -34187
  78. package/dist/vitest/runtime/hosted-session-reaper.js +1 -1
  79. package/dist/vitest/runtime/setup-files.js +2 -2
  80. package/package.json +14 -9
  81. package/skills/archal-agent/SKILL.md +87 -0
  82. package/skills/autoloop/SKILL.md +376 -0
  83. package/skills/autoloop/references/hosted-sources.md +62 -0
  84. package/skills/autoloop/references/trace-schema-mapping.md +73 -0
  85. package/skills/eval/SKILL.md +35 -1
  86. package/skills/install-agent/SKILL.md +221 -0
  87. package/skills/onboard/SKILL.md +80 -0
  88. package/skills/scenario/SKILL.md +19 -4
  89. package/skills/seed/SKILL.md +237 -0
  90. package/dist/seed/dynamic-generator.cjs +0 -45564
  91. package/dist/seed/dynamic-generator.d.cts +0 -106
  92. package/dist/vitest/chunk-CTSN67QR.js +0 -47188
package/README.md CHANGED
@@ -108,11 +108,16 @@ config looks like this:
108
108
  | `archal clone status` | Inspect the active session |
109
109
  | `archal clone stop` | Stop the active session |
110
110
  | `archal clone list` | List all your active sessions |
111
- | `archal clone attach <uuid>` | Reattach to a session by id |
111
+ | `archal clone autoloop <uuid>` | Use an existing hosted session by id |
112
112
  | `archal clone renew <seconds>` | Extend the session lifetime |
113
113
  | `archal clone reset` | Reset clone state without tearing down the session |
114
114
  | `archal clone seed <clone> <name>` | Load a named seed into a running clone |
115
115
  | `archal run [scenario]` | Run a scenario file (or use `--config` for `.archal.json`) |
116
+ | `archal preprod plan` | Inspect a repo and propose a pre-production scenario loop |
117
+ | `archal preprod run [scenarios...]` | Run scenarios through a bounded fix/rerun loop |
118
+ | `archal autoloop [trace-dir] --repo <dir>` | Register a read-only trace source or start a local trace loop |
119
+ | `archal autoloop-status` | Show local autoloop trace job status |
120
+ | `archal detach <trace-dir>` | Stop a local file-backed autoloop loop |
116
121
  | `archal scenario list` | Browse local and hosted scenarios |
117
122
  | `archal seed list [clone]` | List prebuilt clone seeds |
118
123
  | `archal trace` | View recent scenario traces |
@@ -121,6 +126,9 @@ config looks like this:
121
126
 
122
127
  Run `archal <command> --help` for flag details.
123
128
 
129
+ For terminal-first autonomous loops, see
130
+ <https://docs.archal.ai/guides/autoloop-production-traces>.
131
+
124
132
  ## Vitest integration (secondary use case)
125
133
 
126
134
  You can also import `archal/vitest` to route SDK traffic from a vitest
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "A thin single-file Octokit GitHub agent.",
3
+ "agent": {
4
+ "command": "node",
5
+ "args": ["agent.mjs"]
6
+ },
7
+ "clones": ["github"]
8
+ }
@@ -0,0 +1,8 @@
1
+ FROM node:22-bookworm
2
+
3
+ WORKDIR /app
4
+ COPY package.json ./
5
+ RUN npm install --omit=dev
6
+ COPY agent.mjs ./
7
+
8
+ CMD ["node", "agent.mjs"]
@@ -0,0 +1,113 @@
1
+ # GitHub Octokit Harness
2
+
3
+ This example shows how to run `@octokit/rest` against the Archal GitHub clone
4
+ without changing the client-visible GitHub API origin.
5
+
6
+ The harness owns its own REST calls. It does not expose `archal-*` tools to a
7
+ model — it thinks it is talking to GitHub's API.
8
+
9
+ ## What this demonstrates
10
+
11
+ - `GITHUB_TOKEN` is auto-seeded as `ghp_test_bootstrap_token` (from PR #2895).
12
+ - The harness calls Octokit's normal `https://api.github.com` origin.
13
+ - Docker harness networking maps that real domain to Archal's TLS intercept.
14
+ - The clone speaks the same REST contract as `api.github.com`.
15
+
16
+ Run this example with Docker routing as shown below. If you run the same
17
+ Octokit code as an uncontainerized local harness, `https://api.github.com`
18
+ is the real GitHub API; configure Octokit's `baseUrl` to the clone REST URL
19
+ pattern from `archal clone start github` / `archal clone status`, and use
20
+ `ARCHAL_TOKEN` for that request.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ cd examples/agents/github-octokit
26
+ npm install
27
+ ```
28
+
29
+ `@octokit/rest` is the only dependency.
30
+
31
+ ## Syntax check
32
+
33
+ ```bash
34
+ node --check agent.mjs
35
+ ```
36
+
37
+ ## Seed and auth requirements
38
+
39
+ This example requires the `small-project` seed (declared in the scenario
40
+ file). The seed pre-populates:
41
+
42
+ - An authenticated user (`octocat`) mapped to the bootstrap token
43
+ - A public repository `octocat/webapp`
44
+
45
+ The bootstrap token `ghp_test_bootstrap_token` is injected as `GITHUB_TOKEN`
46
+ by the Archal Docker harness. `GET /user` authenticates against this token
47
+ and returns the seeded `octocat` user. If you reuse an existing clone session
48
+ with `--keep-state`, make sure the session was started with the
49
+ `small-project` seed -- otherwise `GET /user` will return 403 because the
50
+ bootstrap token is not present in the clone's auth state.
51
+
52
+ ## Run manually with a live clone
53
+
54
+ ```bash
55
+ cd examples/agents/github-octokit
56
+ archal run scenarios/test-repo-access.md \
57
+ --harness . \
58
+ --dockerfile Dockerfile \
59
+ -n 1
60
+ ```
61
+
62
+ The scenario declares the hosted `small-project` GitHub seed so SDK smoke runs
63
+ use stable catalog state instead of dynamic seed generation. Docker mode
64
+ (`--dockerfile`) is required for TLS interception of `api.github.com`.
65
+
66
+ To run without a model (harness-only, no LLM evaluation):
67
+
68
+ ```bash
69
+ archal run scenarios/test-repo-access.md \
70
+ --harness . \
71
+ --dockerfile Dockerfile \
72
+ --agent-model none \
73
+ -n 1
74
+ ```
75
+
76
+ ## Wire into `.archal.json`
77
+
78
+ The `.archal.json` in this directory is already configured:
79
+
80
+ ```json
81
+ {
82
+ "agent": {
83
+ "command": "node",
84
+ "args": ["agent.mjs"]
85
+ },
86
+ "clones": ["github"]
87
+ }
88
+ ```
89
+
90
+ From the project root, bare `archal run` picks up the config automatically when
91
+ run from inside the `examples/agents/github-octokit/` directory.
92
+
93
+ ## Environment variables
94
+
95
+ | Variable | Source | Value |
96
+ | -------------- | ------------------ | -------------------------- |
97
+ | `GITHUB_TOKEN` | Injected by Archal | `ghp_test_bootstrap_token` |
98
+
99
+ The token is automatically seeded into the clone's auth state by the
100
+ `small-project` seed. The Docker harness injects it as an environment
101
+ variable so Octokit picks it up without code changes.
102
+
103
+ ## Further reading
104
+
105
+ See `apps/web/docs/quickstart.mdx` for the full conceptual model: how runs
106
+ work, what service clones are, and how the harness fits into the `archal run`
107
+ loop.
108
+
109
+ ## Relationship to other examples
110
+
111
+ The `google-workspace-local-tools` example uses the same local-tools
112
+ interception pattern but targets the Google Workspace clone via raw `fetch`
113
+ calls. This example is the GitHub equivalent, using `@octokit/rest` instead.
@@ -0,0 +1,54 @@
1
+ #!/usr/bin/env node
2
+ // GitHub Octokit harness — docs-as-code example for Archal.
3
+ //
4
+ // The harness uses Octokit's default API origin, https://api.github.com.
5
+ // In Docker harness runs, Archal maps that real domain to the TLS intercept
6
+ // listener and routes the requests to the GitHub clone underneath the process.
7
+ //
8
+ // This file is intentionally simple: two calls, stdout output, exit 0.
9
+ // Copy it as a starting point for your own GitHub harness.
10
+
11
+ import { Octokit } from '@octokit/rest';
12
+
13
+ // Optional manual smoke test: run with ARCHAL_PREFLIGHT=1 to verify this file
14
+ // starts without making service calls.
15
+ if (process.env.ARCHAL_PREFLIGHT === '1') {
16
+ console.log('OK');
17
+ process.exit(0);
18
+ }
19
+
20
+ const token = process.env.GITHUB_TOKEN?.trim() || 'ghp_test_bootstrap_token';
21
+
22
+ const octokit = new Octokit({ auth: token });
23
+
24
+ async function main() {
25
+ // Call 1: prove auth + routing work.
26
+ // The clone returns a realistic user object from the default seed.
27
+ const { data: user } = await octokit.rest.users.getAuthenticated();
28
+
29
+ // Call 2: prove stateful object access works.
30
+ // The scenario pins the small-project seed, which includes octocat/webapp.
31
+ const { data: repo } = await octokit.rest.repos.get({
32
+ owner: 'octocat',
33
+ repo: 'webapp',
34
+ });
35
+
36
+ // Print to stdout so the trace captures the result.
37
+ console.log(JSON.stringify({
38
+ user: {
39
+ login: user.login,
40
+ id: user.id,
41
+ type: user.type,
42
+ },
43
+ repo: {
44
+ full_name: repo.full_name,
45
+ private: repo.private,
46
+ default_branch: repo.default_branch,
47
+ },
48
+ }, null, 2));
49
+ }
50
+
51
+ main().catch((error) => {
52
+ console.error(error instanceof Error ? error.message : String(error));
53
+ process.exit(1);
54
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@archal-examples/github-octokit",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "private": true,
6
+ "dependencies": {
7
+ "@octokit/rest": "^22.0.1"
8
+ }
9
+ }
@@ -0,0 +1,27 @@
1
+ # Test GitHub Repository Access
2
+
3
+ ## Setup
4
+
5
+ The GitHub clone starts with the `small-project` seed, which includes:
6
+
7
+ - an authenticated user (`octocat`)
8
+ - a public repository `octocat/webapp`
9
+
10
+ ## Prompt
11
+
12
+ Fetch the authenticated user and retrieve the `octocat/webapp` repository. Print both results.
13
+
14
+ ## Success Criteria
15
+
16
+ - [D] The run exits successfully
17
+ - [D] The harness prints a `user.login` field
18
+ - [D] The harness prints a `repo.full_name` of `octocat/webapp`
19
+ - [P] The output contains realistic GitHub-shaped fields (id, type, private, default_branch)
20
+
21
+ ## Config
22
+
23
+ clones: github
24
+ seed: small-project
25
+ timeout: 60
26
+ runs: 1
27
+ tags: smoke, github, harness
@@ -0,0 +1,6 @@
1
+ FROM node:22-bookworm
2
+
3
+ WORKDIR /app
4
+ COPY agent.mjs ./
5
+
6
+ CMD ["node", "agent.mjs"]
@@ -0,0 +1,58 @@
1
+ # Google Workspace Local-Tools Harness
2
+
3
+ This example packages the same shape we proved with Exo:
4
+
5
+ - the harness owns its own local tools
6
+ - those local tools call the normal Google REST endpoints
7
+ - Archal sits underneath the Google traffic and intercepts it into the hosted Google Workspace clone
8
+
9
+ It does not expose `archal-*` tools to the model. The harness thinks it is talking to Google.
10
+
11
+ ## Current status
12
+
13
+ This fixture is the reference shape and the hosted path now works end to end. It is the lightest way to validate the same local-tools interception pattern we proved with Exo.
14
+
15
+ ## Intended run command
16
+
17
+ ```bash
18
+ archal run examples/agents/google-workspace-local-tools/scenario.md \
19
+ --harness examples/agents/google-workspace-local-tools \
20
+ --dockerfile Dockerfile \
21
+ -n 1
22
+ ```
23
+
24
+ Or use the structured task-first input path:
25
+
26
+ ```bash
27
+ archal run --input examples/agents/google-workspace-local-tools/run-input.yaml \
28
+ --harness examples/agents/google-workspace-local-tools \
29
+ --dockerfile Dockerfile \
30
+ -n 1
31
+ ```
32
+
33
+ The harness uses three local tools:
34
+
35
+ - `read_email`
36
+ - `get_calendar`
37
+ - `generate_draft`
38
+
39
+ Under the hood those tools call the real Google Workspace API origins:
40
+
41
+ - `GET https://gmail.googleapis.com/gmail/v1/users/me/messages`
42
+ - `GET https://gmail.googleapis.com/gmail/v1/users/me/messages/:id`
43
+ - `GET https://calendar.googleapis.com/calendar/v3/calendars/primary/events`
44
+ - `POST https://gmail.googleapis.com/gmail/v1/users/me/drafts`
45
+
46
+ The default bearer token is `ya29.self-local-invalid`, which matches the default `assistant-baseline` Google Workspace seed.
47
+
48
+ ## Why this example exists
49
+
50
+ Exo is a real Electron app with its own tool server and Google SDK clients. That is the strongest proof, but it is heavy for regression coverage and contributor onboarding.
51
+
52
+ This example is the lightweight reference fixture:
53
+
54
+ - same local-tools interception pattern
55
+ - much smaller than Exo
56
+ - easy to inspect and adapt under `archal run --harness`
57
+
58
+ If you want to write your own Gmail/Calendar smoke test, copy either `scenario.md` or `run-input.yaml`. They both encode the same read-email, inspect-calendar, and draft-reply flow, and Google Workspace falls back to `assistant-baseline` automatically when you do not pin a seed.
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ const originalGoogleBearer = process.env.EXAMPLE_GOOGLE_ACCESS_TOKEN?.trim() || 'ya29.self-local-invalid';
4
+ const calendarDate = process.env.EXAMPLE_CALENDAR_DATE?.trim() || '2026-03-29';
5
+ const task = process.env.AGENT_TASK?.trim()
6
+ || 'Read the unread inbox email, inspect the next calendar event, and create a draft reply.';
7
+
8
+ function toBase64Url(input) {
9
+ return Buffer.from(input, 'utf-8').toString('base64url');
10
+ }
11
+
12
+ function getGoogleHeaders(json = false) {
13
+ return {
14
+ authorization: `Bearer ${originalGoogleBearer}`,
15
+ ...(json ? { 'content-type': 'application/json' } : {}),
16
+ };
17
+ }
18
+
19
+ function toGoogleUrl(path) {
20
+ if (path.startsWith('/gmail/')) {
21
+ return `https://gmail.googleapis.com${path}`;
22
+ }
23
+ if (path.startsWith('/calendar/')) {
24
+ return `https://calendar.googleapis.com${path}`;
25
+ }
26
+ throw new Error(`Unsupported Google Workspace API path: ${path}`);
27
+ }
28
+
29
+ async function fetchJson(path, options = {}) {
30
+ const url = toGoogleUrl(path);
31
+ let response;
32
+ try {
33
+ response = await fetch(url, options);
34
+ } catch (error) {
35
+ const message = error instanceof Error ? error.message : String(error);
36
+ throw new Error(`fetch failed for ${url}: ${message}`);
37
+ }
38
+ const text = await response.text();
39
+ let body = null;
40
+ if (text.length > 0) {
41
+ try {
42
+ body = JSON.parse(text);
43
+ } catch {
44
+ body = text;
45
+ }
46
+ }
47
+
48
+ if (!response.ok) {
49
+ throw new Error(`Request failed ${response.status} for ${path}: ${JSON.stringify(body)}`);
50
+ }
51
+
52
+ return body;
53
+ }
54
+
55
+ function getHeader(payload, name) {
56
+ const headers = Array.isArray(payload?.headers) ? payload.headers : [];
57
+ const match = headers.find(
58
+ (header) =>
59
+ header
60
+ && typeof header.name === 'string'
61
+ && header.name.toLowerCase() === name.toLowerCase(),
62
+ );
63
+ return typeof match?.value === 'string' ? match.value : null;
64
+ }
65
+
66
+ async function readEmail() {
67
+ const unreadQuery = encodeURIComponent('is:unread in:inbox');
68
+ const unreadList = await fetchJson(`/gmail/v1/users/me/messages?q=${unreadQuery}&maxResults=1`, {
69
+ headers: getGoogleHeaders(),
70
+ });
71
+ const list = Array.isArray(unreadList?.messages) && unreadList.messages.length > 0
72
+ ? unreadList
73
+ : await fetchJson('/gmail/v1/users/me/messages?maxResults=1', {
74
+ headers: getGoogleHeaders(),
75
+ });
76
+
77
+ const [messageRef] = Array.isArray(list?.messages) ? list.messages : [];
78
+ if (!messageRef?.id) {
79
+ throw new Error('No Gmail message available in the selected seed');
80
+ }
81
+
82
+ const message = await fetchJson(
83
+ `/gmail/v1/users/me/messages/${encodeURIComponent(String(messageRef.id))}?format=full`,
84
+ { headers: getGoogleHeaders() },
85
+ );
86
+ const payload = message?.payload ?? {};
87
+
88
+ return {
89
+ id: String(message.id),
90
+ threadId: String(message.threadId),
91
+ subject: getHeader(payload, 'Subject'),
92
+ from: getHeader(payload, 'From'),
93
+ snippet: typeof message.snippet === 'string' ? message.snippet : null,
94
+ };
95
+ }
96
+
97
+ function buildDayWindow(dateString) {
98
+ const start = new Date(`${dateString}T00:00:00.000Z`);
99
+ if (Number.isNaN(start.valueOf())) {
100
+ throw new Error(`Invalid EXAMPLE_CALENDAR_DATE: ${dateString}`);
101
+ }
102
+ const end = new Date(start);
103
+ end.setUTCDate(end.getUTCDate() + 1);
104
+ return {
105
+ timeMin: start.toISOString(),
106
+ timeMax: end.toISOString(),
107
+ };
108
+ }
109
+
110
+ async function getCalendar() {
111
+ const { timeMin, timeMax } = buildDayWindow(calendarDate);
112
+ const query = new URLSearchParams({
113
+ maxResults: '5',
114
+ singleEvents: 'true',
115
+ orderBy: 'startTime',
116
+ timeMin,
117
+ timeMax,
118
+ });
119
+ const response = await fetchJson(`/calendar/v3/calendars/primary/events?${query.toString()}`, {
120
+ headers: getGoogleHeaders(),
121
+ });
122
+ const [firstEvent] = Array.isArray(response?.items) ? response.items : [];
123
+ if (!firstEvent) {
124
+ throw new Error(`No calendar event found for ${calendarDate}`);
125
+ }
126
+
127
+ return {
128
+ id: String(firstEvent.id),
129
+ summary: typeof firstEvent.summary === 'string' ? firstEvent.summary : null,
130
+ start: firstEvent.start ?? null,
131
+ };
132
+ }
133
+
134
+ function parseEmailAddress(value) {
135
+ if (typeof value !== 'string' || value.trim().length === 0) {
136
+ return 'self@local.invalid';
137
+ }
138
+ const bracketMatch = value.match(/<([^>]+)>/);
139
+ if (bracketMatch?.[1]) {
140
+ return bracketMatch[1];
141
+ }
142
+ const directMatch = value.match(/[^\s<>,;]+@[^\s<>,;]+/);
143
+ return directMatch?.[0] ?? 'self@local.invalid';
144
+ }
145
+
146
+ async function generateDraft(email, calendarEvent) {
147
+ const replyTo = parseEmailAddress(email.from);
148
+ const draftMime = [
149
+ `To: ${replyTo}`,
150
+ `Subject: Re: ${email.subject ?? 'Quick follow-up'}`,
151
+ '',
152
+ `Thanks for the note. I saw your message and my next event is "${calendarEvent.summary ?? 'scheduled'}".`,
153
+ ].join('\r\n');
154
+
155
+ const response = await fetchJson('/gmail/v1/users/me/drafts', {
156
+ method: 'POST',
157
+ headers: getGoogleHeaders(true),
158
+ body: JSON.stringify({
159
+ message: {
160
+ threadId: email.threadId,
161
+ raw: toBase64Url(draftMime),
162
+ },
163
+ }),
164
+ });
165
+
166
+ return {
167
+ id: typeof response?.id === 'string' ? response.id : null,
168
+ messageId: response?.message?.id ?? null,
169
+ };
170
+ }
171
+
172
+ async function main() {
173
+ const toolCalls = [];
174
+
175
+ toolCalls.push('read_email');
176
+ const email = await readEmail();
177
+
178
+ toolCalls.push('get_calendar');
179
+ const calendarEvent = await getCalendar();
180
+
181
+ toolCalls.push('generate_draft');
182
+ const draft = await generateDraft(email, calendarEvent);
183
+
184
+ console.log(JSON.stringify({
185
+ task,
186
+ toolCalls,
187
+ email,
188
+ calendarEvent,
189
+ draft,
190
+ }, null, 2));
191
+ }
192
+
193
+ main().catch((error) => {
194
+ console.error(error instanceof Error ? error.message : String(error));
195
+ process.exit(1);
196
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 1,
3
+ "local": {
4
+ "command": "node",
5
+ "args": ["agent.mjs"]
6
+ }
7
+ }
@@ -0,0 +1,16 @@
1
+ title: Google Workspace Local-Tools Reply Draft
2
+ task: |
3
+ Read the unread inbox email, inspect the next calendar event for `2026-03-29`,
4
+ and create a draft reply.
5
+
6
+ Do not send the email.
7
+ checks:
8
+ - The run exits successfully
9
+ - The harness reports the local tool calls `read_email`, `get_calendar`, and `generate_draft`
10
+ - The harness creates a Gmail draft id
11
+ - No `archal-*` tool prompting is required
12
+ config:
13
+ clones: google-workspace
14
+ timeout: 120
15
+ runs: 1
16
+ tags: smoke, google-workspace, harness
@@ -0,0 +1,29 @@
1
+ # Google Workspace Local-Tools Reply Draft
2
+
3
+ ## Setup
4
+
5
+ The Google Workspace account has:
6
+
7
+ - one unread inbox message in the primary mailbox
8
+ - a primary calendar event on `2026-03-29`
9
+ - a valid seed access token for the baseline account
10
+
11
+ ## Task
12
+
13
+ Read the unread inbox email, inspect the next calendar event for `2026-03-29`, and create a draft reply.
14
+
15
+ Do not send the email.
16
+
17
+ ## Checks
18
+
19
+ - [D] The run exits successfully
20
+ - [D] The harness reports the local tool calls `read_email`, `get_calendar`, and `generate_draft`
21
+ - [D] The harness creates a Gmail draft id
22
+ - [D] No `archal-*` tool prompting is required
23
+
24
+ ## Config
25
+
26
+ clones: google-workspace
27
+ timeout: 120
28
+ runs: 1
29
+ tags: smoke, google-workspace, harness
@@ -0,0 +1,8 @@
1
+ {
2
+ "description": "Hermes — a full third-party Stripe support agent.",
3
+ "agent": {
4
+ "command": "node",
5
+ "args": ["drive.mjs"]
6
+ },
7
+ "clones": ["stripe"]
8
+ }
@@ -0,0 +1,46 @@
1
+ # Hermes agent harness — runs the Nous Research `hermes-agent` against Archal clones.
2
+ #
3
+ # The harness keeps calling the real service domains (e.g. api.stripe.com). In a
4
+ # Docker harness run, Archal maps those domains to the TLS intercept listener and
5
+ # routes the requests to the clone underneath the process — the agent is unaware.
6
+ # api.openai.com is allowlisted and forwarded to the real model with the host key
7
+ # injected by the proxy, so the agent's reasoning runs against a real LLM.
8
+ #
9
+ # Because the harness blocks egress to everything except clones + LLM providers,
10
+ # the Stripe MCP is installed at BUILD time and invoked by direct path (no runtime
11
+ # `npx` registry fetch, which would 403).
12
+ FROM node:22-bookworm-slim
13
+
14
+ # Pin the agent version. Override at build time: --build-arg HERMES_VERSION=0.17.0
15
+ ARG HERMES_VERSION=0.16.0
16
+
17
+ ENV PYTHONUNBUFFERED=1 \
18
+ HERMES_HOME=/root/.hermes \
19
+ HERMES_ACCEPT_HOOKS=1 \
20
+ PATH="/opt/hermes-venv/bin:${PATH}"
21
+
22
+ # Python (hermes core) + node/npx (base image, for @stripe/mcp) + tools hermes expects.
23
+ RUN apt-get update && apt-get install -y --no-install-recommends \
24
+ python3 python3-venv python3-pip git ca-certificates ripgrep ffmpeg curl \
25
+ && rm -rf /var/lib/apt/lists/*
26
+
27
+ # Install the agent into an isolated venv, pinned to HERMES_VERSION.
28
+ RUN python3 -m venv /opt/hermes-venv \
29
+ && /opt/hermes-venv/bin/pip install --no-cache-dir --upgrade pip \
30
+ && /opt/hermes-venv/bin/pip install --no-cache-dir "hermes-agent==${HERMES_VERSION}"
31
+
32
+ # Pre-install the Stripe MCP so no runtime registry fetch is needed (egress is
33
+ # blocked). The config invokes its cli.js directly. Fail the build loudly if the
34
+ # entry path moves between @stripe/mcp releases.
35
+ RUN npm install -g @stripe/mcp \
36
+ && test -f /usr/local/lib/node_modules/@stripe/mcp/dist/cli.js
37
+
38
+ WORKDIR /app
39
+
40
+ # Scoped, non-interactive config + the demo persona + the drive entrypoint.
41
+ COPY config.yaml /root/.hermes/config.yaml
42
+ COPY SOUL.md /root/.hermes/SOUL.md
43
+ COPY drive.mjs /app/drive.mjs
44
+
45
+ # The .archal.json launch command overrides this; kept for standalone debugging.
46
+ CMD ["node", "/app/drive.mjs"]