lorenz 0.1.1 → 0.1.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 (112) hide show
  1. package/README.md +75 -738
  2. package/RELEASE-MANIFEST.json +1 -1
  3. package/apps/cli/dist/doctor.d.ts.map +1 -1
  4. package/apps/cli/dist/doctor.js +20 -1
  5. package/apps/cli/dist/doctor.js.map +1 -1
  6. package/apps/cli/dist/main.d.ts.map +1 -1
  7. package/apps/cli/dist/main.js +10 -1
  8. package/apps/cli/dist/main.js.map +1 -1
  9. package/extensions/docker-worker/package.json +1 -1
  10. package/extensions/jira-tracker/package.json +1 -1
  11. package/extensions/linear-tracker/package.json +1 -1
  12. package/extensions/local-tracker/package.json +1 -1
  13. package/extensions/memory-tracker/package.json +1 -1
  14. package/extensions/slack-tracker/dist/client.d.ts +13 -2
  15. package/extensions/slack-tracker/dist/client.d.ts.map +1 -1
  16. package/extensions/slack-tracker/dist/client.js +28 -2
  17. package/extensions/slack-tracker/dist/client.js.map +1 -1
  18. package/extensions/slack-tracker/dist/inMemoryTransport.d.ts +4 -1
  19. package/extensions/slack-tracker/dist/inMemoryTransport.d.ts.map +1 -1
  20. package/extensions/slack-tracker/dist/inMemoryTransport.js +4 -2
  21. package/extensions/slack-tracker/dist/inMemoryTransport.js.map +1 -1
  22. package/extensions/slack-tracker/dist/index.d.ts +3 -1
  23. package/extensions/slack-tracker/dist/index.d.ts.map +1 -1
  24. package/extensions/slack-tracker/dist/index.js +2 -1
  25. package/extensions/slack-tracker/dist/index.js.map +1 -1
  26. package/extensions/slack-tracker/dist/mapping.d.ts +9 -0
  27. package/extensions/slack-tracker/dist/mapping.d.ts.map +1 -1
  28. package/extensions/slack-tracker/dist/mapping.js +13 -0
  29. package/extensions/slack-tracker/dist/mapping.js.map +1 -1
  30. package/extensions/slack-tracker/dist/operations.d.ts.map +1 -1
  31. package/extensions/slack-tracker/dist/operations.js +8 -4
  32. package/extensions/slack-tracker/dist/operations.js.map +1 -1
  33. package/extensions/slack-tracker/dist/options.d.ts +21 -1
  34. package/extensions/slack-tracker/dist/options.d.ts.map +1 -1
  35. package/extensions/slack-tracker/dist/options.js +3 -0
  36. package/extensions/slack-tracker/dist/options.js.map +1 -1
  37. package/extensions/slack-tracker/dist/provider.d.ts.map +1 -1
  38. package/extensions/slack-tracker/dist/provider.js +21 -1
  39. package/extensions/slack-tracker/dist/provider.js.map +1 -1
  40. package/extensions/slack-tracker/dist/socketMode.d.ts +81 -0
  41. package/extensions/slack-tracker/dist/socketMode.d.ts.map +1 -0
  42. package/extensions/slack-tracker/dist/socketMode.js +226 -0
  43. package/extensions/slack-tracker/dist/socketMode.js.map +1 -0
  44. package/extensions/slack-tracker/dist/threadState.d.ts.map +1 -1
  45. package/extensions/slack-tracker/dist/threadState.js +8 -4
  46. package/extensions/slack-tracker/dist/threadState.js.map +1 -1
  47. package/extensions/slack-tracker/dist/webTransport.d.ts +1 -0
  48. package/extensions/slack-tracker/dist/webTransport.d.ts.map +1 -1
  49. package/extensions/slack-tracker/dist/webTransport.js +10 -4
  50. package/extensions/slack-tracker/dist/webTransport.js.map +1 -1
  51. package/extensions/slack-tracker/package.json +1 -1
  52. package/package.json +1 -2
  53. package/packages/acp/package.json +1 -1
  54. package/packages/agent-runner/package.json +1 -1
  55. package/packages/agent-sdk/package.json +1 -1
  56. package/packages/cli-kit/package.json +1 -1
  57. package/packages/config/dist/deprecations.d.ts +13 -0
  58. package/packages/config/dist/deprecations.d.ts.map +1 -0
  59. package/packages/config/dist/deprecations.js +25 -0
  60. package/packages/config/dist/deprecations.js.map +1 -0
  61. package/packages/config/dist/index.d.ts +3 -0
  62. package/packages/config/dist/index.d.ts.map +1 -1
  63. package/packages/config/dist/index.js +1 -0
  64. package/packages/config/dist/index.js.map +1 -1
  65. package/packages/config/dist/parse.d.ts +11 -1
  66. package/packages/config/dist/parse.d.ts.map +1 -1
  67. package/packages/config/dist/parse.js +6 -1
  68. package/packages/config/dist/parse.js.map +1 -1
  69. package/packages/config/dist/schemas.d.ts +15 -0
  70. package/packages/config/dist/schemas.d.ts.map +1 -1
  71. package/packages/config/dist/schemas.js +127 -14
  72. package/packages/config/dist/schemas.js.map +1 -1
  73. package/packages/config/package.json +1 -1
  74. package/packages/dispatch/package.json +1 -1
  75. package/packages/dispatch-coordinator/package.json +1 -1
  76. package/packages/domain/dist/index.d.ts +22 -0
  77. package/packages/domain/dist/index.d.ts.map +1 -1
  78. package/packages/domain/dist/index.js.map +1 -1
  79. package/packages/domain/package.json +1 -1
  80. package/packages/humanize/package.json +1 -1
  81. package/packages/issue/package.json +1 -1
  82. package/packages/log-file/package.json +1 -1
  83. package/packages/mcp/package.json +1 -1
  84. package/packages/orchestrator/package.json +1 -1
  85. package/packages/policies/package.json +1 -1
  86. package/packages/presenter/package.json +1 -1
  87. package/packages/projections/package.json +1 -1
  88. package/packages/prompt/package.json +1 -1
  89. package/packages/retry-scheduler/package.json +1 -1
  90. package/packages/runtime/dist/index.d.ts +24 -0
  91. package/packages/runtime/dist/index.d.ts.map +1 -1
  92. package/packages/runtime/dist/index.js +78 -0
  93. package/packages/runtime/dist/index.js.map +1 -1
  94. package/packages/runtime/package.json +1 -1
  95. package/packages/runtime-events/dist/index.d.ts +1 -1
  96. package/packages/runtime-events/dist/index.d.ts.map +1 -1
  97. package/packages/runtime-events/dist/index.js +3 -0
  98. package/packages/runtime-events/dist/index.js.map +1 -1
  99. package/packages/runtime-events/package.json +1 -1
  100. package/packages/server/package.json +1 -1
  101. package/packages/ssh/package.json +1 -1
  102. package/packages/static-worker/package.json +1 -1
  103. package/packages/tool-sdk/package.json +1 -1
  104. package/packages/traceviz-emitter/package.json +1 -1
  105. package/packages/traceviz-server/package.json +1 -1
  106. package/packages/tracker-sdk/package.json +1 -1
  107. package/packages/tui/package.json +1 -1
  108. package/packages/worker-host-pool/package.json +1 -1
  109. package/packages/worker-pool/package.json +1 -1
  110. package/packages/worker-sdk/package.json +1 -1
  111. package/packages/workflow/package.json +1 -1
  112. package/packages/workspace/package.json +1 -1
package/README.md CHANGED
@@ -1,18 +1,13 @@
1
1
  # Lorenz
2
2
 
3
- Lorenz is heavily birthed from [OpenAI's Symphony orchestrator](https://github.com/openai/symphony), reimplemented and extended in TypeScript.
3
+ [![Documentation](https://img.shields.io/badge/docs-ryanlyn.github.io%2Florenz-14b8a6)](https://ryanlyn.github.io/lorenz/)
4
4
 
5
- Lorenz turns tracker issues into agent runs. It polls for eligible work, prepares a workspace,
6
- renders the workflow prompt with issue context, starts Codex or Claude, and records the run so
7
- operators can inspect state, retries, cost, and logs.
8
-
9
- This repository owns the TypeScript CLI, runtime packages, tracker adapters, terminal dashboard,
10
- local observability server, trace viewer packages, and tests.
5
+ Originated from [OpenAI Symphony](https://openai.com/index/open-source-codex-orchestration-symphony/), Lorenz lets you declare work on trackers (in-memory, Obsidian markdown files, Linear, Jira, Slack etc.) and manage the dispatch, execution, and convergence of concurrent agent sessions until they reach a specified terminal state. It is harness-agnostic through the [Agent-Client Protocol](https://agentclientprotocol.com/get-started/introduction) with support for local, static SSH boxes, or (experimental) cloud-brokered VMs.
11
6
 
12
7
  ## Screenshots
13
8
 
14
- Lorenz ships two operator views over the same runtime snapshot: an Ink terminal
15
- dashboard (TUI) and a web dashboard served by the observability API.
9
+ Lorenz ships two operator views over the same runtime snapshot: an Ink terminal dashboard (TUI)
10
+ and a web dashboard served by the observability API.
16
11
 
17
12
  ### Terminal dashboard (TUI)
18
13
 
@@ -22,734 +17,109 @@ dashboard (TUI) and a web dashboard served by the observability API.
22
17
 
23
18
  ![Lorenz web dashboard](docs/images/lorenz-dashboard.png)
24
19
 
25
- ## How it works
26
-
27
- 1. Polls Linear for issues in active states (e.g. `Todo`, `In Progress`)
28
- 2. Creates a workspace per issue and bootstraps it via `hooks.after_create`
29
- 3. Launches the configured agent executor (Codex or Claude Code) inside the workspace
30
- 4. Renders a Liquid-templated prompt from `WORKFLOW.md` with issue context and sends it to the agent
31
- 5. Re-runs the agent on subsequent polling cycles if the issue remains active, up to `max_turns`
32
- 6. When an issue moves to a terminal state (`Done`, `Closed`, `Cancelled`, `Duplicate`), stops the
33
- agent and cleans up the workspace
20
+ ## Documentation
34
21
 
35
- The workflow file (`WORKFLOW.md`) defines both the orchestrator configuration (YAML front matter) and
36
- the agent session prompt (Markdown body). Editing the workflow while Lorenz is running reloads the
37
- configuration automatically - no restart needed.
22
+ The published documentation site is at **[ryanlyn.github.io/lorenz](https://ryanlyn.github.io/lorenz/)**, built from [`docs/`](./docs). Start here:
38
23
 
39
- ## Extensions
24
+ - [Getting started](./docs/getting-started.md) - install, write a `WORKFLOW.md`, run your first issue.
25
+ - [How it works](./docs/how-it-works.md) - the polling, dispatch, and run lifecycle.
26
+ - [Configuration reference](./docs/reference/configuration.md) - every front-matter key, default, and meaning.
27
+ - [Trackers](./docs/trackers/index.md) - Linear, Jira, Slack, local, and memory sources of issues.
28
+ - [CLI](./docs/cli.md) - commands, flags, and run history.
40
29
 
41
- | Extension | What it adds |
42
- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | Context Ensembles | Adds configurable multi-agent issue fan-out with per-slot workspaces, prompt/dashboard ensemble context, `ensemble:*` label overrides, and a dedicated `WORKFLOW_ENSEMBLE.md` example for independent workpads. |
44
- | Claude Code executor | Adds `agent.kind: "claude"` support, including Claude CLI execution, JSONL event parsing, built-in `/mcp` tool serving instead of the Python MCP sidecar, authenticated remote worker access, and Claude-specific runtime settings. |
45
- | Workflow and runtime hardening | Defaults Codex workflows to sandboxed `workspace-write`, honors Linear `Retry-After` backoff on `429`, tightens remote workspace path validation, and improves long-running orchestrator reliability. |
46
- | Claude parity and MCP handling | Routes Claude and Codex through the same Linear tool backend, removes the Python MCP sidecar, and improves remote cleanup behavior. |
47
- | Dispatch routing | Adds tracker-scoped static routing with Linear labels such as `Lorenz:shard-a`, so multiple Lorenz instances can split work by configured route labels. |
48
- | Run history CLI | Adds an orchestrator run history command (`lorenz runs`) exposing completed attempts, retries, token totals, and per-run forensic context beyond live state. |
49
- | Secret resolution | Resolves `op://` references in workflow secrets (e.g. `LINEAR_API_KEY`) through the 1Password CLI. |
30
+ ## Quickstart
50
31
 
51
- ## Requirements
52
-
53
- [mise](https://mise.jdx.dev/) is recommended for managing Node and pnpm:
32
+ Running Lorenz is as easy as:
54
33
 
55
34
  ```sh
56
- mise trust
57
- mise install
58
- pnpm install
35
+ npx lorenz WORKFLOW.md
59
36
  ```
60
37
 
61
- The workspace uses Node 24 and pnpm 9 from `mise.toml`.
62
-
63
- Runtime requirements depend on the workflow:
64
-
65
- - `LINEAR_API_KEY` for Linear-backed workflows.
66
- - `codex` on `PATH` for Codex runs and live Codex tests.
67
- - A Claude ACP bridge, usually `claude-agent-acp`, for Claude runs and live Claude tests.
68
- - SSH access for remote workers and live SSH tests.
69
- - Docker and `ssh-keygen` for disposable live SSH workers when no real SSH hosts are configured.
70
-
71
- Run commands from the repository root unless a command says otherwise.
72
-
73
- ## Run
74
-
75
- ```sh
76
- pnpm build
77
- pnpm start -- WORKFLOW.md
78
- pnpm start:once -- --dry-run --no-tui WORKFLOW.md
79
- pnpm runs -- --port 4000 --failed
80
- ```
81
-
82
- The built CLI is `lorenz`:
38
+ with full CLI options:
83
39
 
84
40
  ```sh
85
41
  lorenz [--once] [--dry-run] [--no-tui] [--port <port>] [--logs-root <path>] [path-to-WORKFLOW.md]
86
- lorenz runs [--issue ID] [--failed] [--cost] [--retries] [--id RUN_ID] [--limit N] [--url URL | --port PORT] [--json]
42
+ lorenz runs [--issue ID] [--failed] [--cost] [--retries] [--id RUN_ID] [--limit N] [--json]
87
43
  ```
88
44
 
89
- Optional flags:
90
-
91
- - `--logs-root <path>` writes logs under `<path>/log/lorenz.log`.
92
- - `--port <port>` starts the local observability dashboard and JSON API.
93
- - `--once` polls once and exits.
94
- - `--dry-run` evaluates candidates without dispatching agents.
95
- - `--no-tui` disables the terminal dashboard and prints JSON snapshots.
96
-
97
- With no workflow path, the CLI reads `LORENZ_WORKFLOW`, then `./WORKFLOW.md`.
98
-
99
- The runtime reloads the workflow before each poll. If startup cannot read or parse the workflow,
100
- the CLI exits with an error. If a later reload fails, the runtime keeps the last good workflow and
101
- records a `workflow_reload_failed` event.
102
-
103
- ## Workspace Layout
104
-
105
- See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) for the layering rules and the tracker extension
106
- contract (including the recipe for adding a new tracker backend).
107
-
108
- - `apps/cli` is the composition root: it invokes the built-in extensions' registration and wires
109
- configuration, agent runners, the runtime, the TUI, and the observability server into the
110
- shipped binary.
111
- - `apps/traceviz` renders trace event streams for local inspection.
112
- - `packages/tracker-sdk` is the extension SDK: the `TrackerProvider` contract, the provider
113
- registry, and the helpers tracker backends build on.
114
- - `extensions/*` are the backend extensions: `linear-tracker`, `local-tracker`,
115
- `memory-tracker`, and `jira-tracker` are self-contained tracker providers (config
116
- parsing, runtime client, tool packs) that each export their own registration; the CLI
117
- invokes the built-in set at its composition root.
118
- - The remaining `packages/*` are the provider-agnostic engine: domain model, configuration
119
- loader, prompt renderer, runtime, policies, MCP server, dashboards, logging, SSH, and
120
- support libraries.
121
- - `test/` contains workspace-level integration, contract, sandbox, and live tests.
122
- - Package- and app-owned unit tests live under `packages/<name>/test/` or `apps/<name>/test/` as
123
- `.test.ts` or `.test.tsx` files.
124
-
125
- Create a package when a boundary has a clear owner. Keep curated exports in `src/index.ts` and
126
- declare internal dependencies as `workspace:*`.
45
+ `--logs-root <path>` writes logs under `<path>/log/lorenz.log`. With no workflow path the CLI reads
46
+ `LORENZ_WORKFLOW`, then `./WORKFLOW.md`. See [CLI](./docs/cli.md) for every flag and command.
47
+
48
+ Runtime needs depend on the workflow: `LINEAR_API_KEY` for Linear, `codex` on `PATH` for Codex
49
+ runs, a Claude ACP bridge for Claude runs, and SSH access for remote workers. See
50
+ [Getting started](./docs/getting-started.md) for the full list. Run commands from the repository
51
+ root unless a command says otherwise.
127
52
 
128
53
  ## Configuration
129
54
 
130
55
  Configuration lives in the YAML front matter of a workflow file. The Markdown body below the front
131
- matter is the agent session prompt, rendered as Liquid with issue context variables.
132
-
133
- ### Quickstart
134
-
135
- ```yaml
136
- ---
137
- tracker:
138
- kind: linear
139
- trackers:
140
- linear:
141
- provider: linear
142
- project_slug: "your-project-slug"
143
- workspace:
144
- root: ~/code/workspaces
145
- hooks:
146
- after_create: |
147
- git clone git@github.com:your-org/your-repo.git .
148
- agent:
149
- kind: codex
150
- ---
151
-
152
- You are working on {{ issue.identifier }}: {{ issue.title }}
153
-
154
- {{ issue.description }}
155
- ```
156
-
157
- Set `LINEAR_API_KEY` in your environment before running a Linear workflow.
56
+ matter is the agent session prompt, rendered as Liquid with issue-context variables. See
57
+ [Workflows](./docs/workflows.md) for the file format and a quickstart example.
158
58
 
159
59
  ### Full Reference
160
60
 
161
- ```yaml
162
- ---
163
- tracker:
164
- kind: linear # linear, jira, jira-mcp, local, or memory
165
- trackers:
166
- linear:
167
- provider: linear
168
- api_key: $LINEAR_API_KEY # defaults to $LINEAR_API_KEY when unset
169
- endpoint: "https://api.linear.app/graphql"
170
- project_slug: "my-project" # right-click a Linear project and copy the URL slug
171
- assignee: $LINEAR_ASSIGNEE # optional; filters issues by assignee
172
- active_states:
173
- - Todo # default: ["Todo", "In Progress"]
174
- - In Progress
175
- - Agent Review
176
- - Merging
177
- - Rework
178
- terminal_states:
179
- - Closed # default: ["Closed", "Cancelled", "Canceled",
180
- - Cancelled # "Duplicate", "Done"]
181
- - Canceled
182
- - Duplicate
183
- - Done
184
- dispatch:
185
- accept_unrouted: true # accept issues without a route label; default: true
186
- only_routes: null # null accepts any route, [] accepts none
187
- route_label_prefix: "Lorenz:" # route labels look like "Lorenz:backend"
188
-
189
- tools:
190
- local:
191
- path: .lorenz/local # explicit extra pack config; not needed for Linear-owned tools
192
-
193
- polling:
194
- interval_ms: 30000 # default: 30000
195
-
196
- workspace:
197
- root: ~/code/workspaces # default: $TMPDIR/lorenz_workspaces
198
-
199
- worker:
200
- # Either list static SSH hosts here, or set kind to select a top-level workers.<name> profile.
201
- # kind: static-prod
202
- ssh_hosts:
203
- - worker1.example.com # standard OpenSSH targets and Host aliases work
204
- - worker2.example.com:2222
205
- ssh_timeout_ms: 60000 # default: 60000
206
- max_concurrent_agents_per_host: 2 # optional; defaults to the global agent cap per host
207
- # Alternative to ssh_hosts (mutually exclusive): a warm pool of leased workers
208
- # provisioned by a worker driver. Disabled by default.
209
- worker_pool:
210
- enabled: false
211
- # driver: fake # compatibility fallback when worker.kind is omitted
212
- min: 0 # warm-inventory floor the reaper keeps alive
213
- max: 1 # ceiling on concurrent workers
214
- warm: 1 # pre-warmed idle workers the reaper tops up toward
215
- max_in_flight: 1 # run slots per machine; >1 requires co_residence: true
216
- ttl_ms: 3600000 # hard worker lifetime before recycle
217
- idle_reap_ms: 300000 # idle window before a warm worker above min is reaped
218
- acquire_timeout_ms: 30000 # how long an acquire waits for capacity
219
- spend: # optional caps, all in worker count / wall-clock worker-seconds
220
- max_concurrent_workers: 4
221
- max_worker_seconds: 86400
222
- daily_worker_seconds: 28800
223
-
224
- workers:
225
- static-prod: # selected by worker.kind, options pass through verbatim (snake_case preserved)
226
- driver: static-ssh
227
- ssh_hosts: ["user@worker1:22"]
228
-
229
- agent:
230
- kind: codex # default: "codex"; "claude" is configured below
231
- max_concurrent_agents: 10 # default: 10
232
- max_turns: 20 # default: 20
233
- max_retry_backoff_ms: 300000 # default: 300000
234
- ensemble_size: 1 # default: 1
235
- skills: # skill directories copied to .lorenz/skills/ before the agent starts
236
- - ./skills/lorenz-land # one entry per skill directory
237
-
238
- agents:
239
- turn_timeout_ms: 3600000 # default: 3600000
240
- stall_timeout_ms: 300000 # default: 300000
241
- codex:
242
- executor: acp
243
- bridge_command: codex-acp
244
- claude:
245
- executor: acp
246
- bridge_command: claude-agent-acp
247
- bridge_args:
248
- - --permission-mode
249
- - dontAsk
250
- - --model
251
- - claude-opus-4-6[1m]
252
-
253
- status_overrides:
254
- in progress:
255
- agent:
256
- max_concurrent_agents: 5
257
- merging:
258
- agent:
259
- max_concurrent_agents: 2
260
-
261
- codex:
262
- command: codex-acp # legacy alias for agents.codex.bridge_command
263
- turn_timeout_ms: 3600000 # default: 3600000
264
- stall_timeout_ms: 300000 # default: 300000
265
-
266
- claude:
267
- command: claude-agent-acp # ACP bridge command
268
- model: claude-opus-4-6[1m]
269
- permission_mode: dontAsk
270
- strict_mcp_config: true # default: true
271
-
272
- hooks:
273
- after_create: | # runs after a workspace directory is created
274
- git clone --depth 1 git@github.com:org/repo.git .
275
- before_run: | # runs before each agent turn
276
- git pull origin main
277
- after_run: | # best effort; runs after each agent turn
278
- echo "turn complete"
279
- before_remove: | # best effort; runs before workspace cleanup
280
- echo "cleaning up"
281
- timeout_ms: 60000 # default: 60000
282
-
283
- observability:
284
- dashboard_enabled: true # terminal dashboard; default: true
285
- refresh_ms: 1000 # default: 1000
286
- render_interval_ms: 16 # default: 16
287
-
288
- server:
289
- port: 4000 # enables the web dashboard; default: disabled
290
- host: 127.0.0.1 # default: 127.0.0.1
291
-
292
- logging:
293
- log_file: ./log/lorenz.log # default: ~/.lorenz/log/lorenz.log
294
- ---
295
- ```
296
-
297
- Notes:
298
-
299
- - `tracker.kind` is always required. When `trackers` is present it selects a named bundle, and
300
- `trackers.<name>.provider` selects the tracker implementation.
301
- - The older flat `tracker.kind: linear` shape is still accepted when no `trackers` map is present.
302
- - `trackers.linear.project_slug`, `trackers.linear.project_slugs`, or
303
- `trackers.linear.project_labels` is required for Linear workflows.
304
- - `trackers.linear.api_key` falls back to `LINEAR_API_KEY`; `trackers.linear.assignee` falls back
305
- to `LINEAR_ASSIGNEE`.
306
- - Shared tracker secrets can use `op://` references when the 1Password CLI is installed.
307
- - `tools.<pack>` mounts or configures extra tool packs. Tracker-owned tools are implicit, so a
308
- Linear tracker does not need a matching `tools.linear` entry.
309
- - `workspace.root` supports `~` and whole-value `$VAR` expansion. `LORENZ_WORKSPACE_ROOT`
310
- overrides `workspace.root` at runtime.
311
- - `LORENZ_SSH_CONFIG` points SSH worker commands at a custom OpenSSH config file.
312
- - Hooks run through `bash -lc` locally or over SSH with the workspace as `cwd`. Use
313
- fail-fast shell options in bootstrap hooks so clone and dependency setup failures stop workspace
314
- creation immediately.
315
- - `codex.command` runs through `bash -lc`, so shell expansion happens in the launched process.
316
- - If the Markdown body is blank, Lorenz uses a default prompt with the issue identifier, title,
317
- and body.
61
+ Every front-matter key, its type, verified default, and meaning are in the
62
+ [Configuration reference](./docs/reference/configuration.md). `workspace.root` supports `~` and
63
+ whole-value `$VAR` expansion, and `LORENZ_WORKSPACE_ROOT` overrides it at runtime.
318
64
 
319
65
  ## Linear
320
66
 
321
- Prerequisites:
322
-
323
- 1. Create a personal API token in Linear Settings, Security & access, Personal API keys.
324
- 2. Export it as `LINEAR_API_KEY`, or set `trackers.linear.api_key: $LINEAR_API_KEY`.
325
- 3. Find the project slug by right-clicking a Linear project and copying its URL. The slug is in the
326
- path.
327
- 4. The example workflows use non-standard states such as `Agent Review`, `Rework`, `Human Review`,
328
- and `Merging`. Add those states under Team Settings, Workflow, or adjust `active_states` and
329
- `terminal_states` to match your team.
330
-
331
- Route labels let multiple Lorenz instances share one Linear project. With the default
332
- `route_label_prefix`, labels such as `Lorenz:backend` and `Lorenz:frontend` become route names.
333
-
334
- ## Trackers
335
-
336
- A tracker is the source of issues Lorenz works on. `tracker.kind` selects a named bundle under
337
- `trackers`, and the selected bundle's `provider` selects the implementation. Every tracker exposes
338
- the same read surface to the runtime (poll for candidate issues, refresh in-flight issues by id)
339
- and a set of agent tools. Those tools are read+write symmetric across kinds, mirroring
340
- `linear_graphql` (which both reads and writes): each tracker gives the agent at least one write tool
341
- and one read tool. The tools differ per kind; their descriptions are self-documenting and surface
342
- to the agent via the MCP `tools/list` call.
343
-
344
- Supported kinds:
345
-
346
- - `linear` - issues live in a Linear project. Read access uses `trackers.linear.api_key` (resolved
347
- from `LINEAR_API_KEY`) and project selection uses `trackers.linear.project_slug`,
348
- `trackers.linear.project_slugs`, or `trackers.linear.project_labels`. Agents can use
349
- provider-neutral `tracker_*` tools or the legacy
350
- `linear_graphql` tool.
351
- - `jira` - issues live in Jira Cloud and are accessed directly over Jira REST. Configure
352
- `trackers.jira.base_url`, `trackers.jira.email`, `trackers.jira.api_key`, and either
353
- `trackers.jira.project_keys` or `trackers.jira.jql`. `JIRA_BASE_URL`, `JIRA_EMAIL`, and
354
- `JIRA_API_KEY` are used as fallbacks.
355
- - `jira-mcp` - issues live in Jira, but Lorenz reaches them through an external MCP server.
356
- Configure `trackers.jira-mcp.mcp.url` and either `trackers.jira-mcp.project_keys` or
357
- `trackers.jira-mcp.jql`. Tool names can be overridden under `trackers.jira-mcp.mcp.tools`.
358
- - `local` - issues live as Markdown files on disk. No external service required.
359
- - `slack` - an @-mention of the bot (in a channel message or a thread reply) is an issue, the
360
- thread carries the status (`@bot !` commands and bot `status:` replies), and a thread reply is
361
- a comment.
362
- - `memory` - an in-process tracker used for tests and dry runs.
363
-
364
- All non-memory providers expose the provider-neutral agent tools:
365
-
366
- - `tracker_read_issue`
367
- - `tracker_query`
368
- - `tracker_update_status`
369
- - `tracker_list_comments`
370
- - `tracker_comment`
371
- - `tracker_update_comment`
372
- - `tracker_create_issue`
373
-
374
- Provider-specific tools are compatibility escape hatches, not the preferred workflow contract.
375
-
376
- All kinds share the dispatch routing block under the selected tracker bundle:
377
-
378
- ```yaml
379
- tracker:
380
- kind: linear
381
- trackers:
382
- linear:
383
- provider: linear
384
- dispatch:
385
- accept_unrouted: true # process issues that carry no matching route label (default)
386
- only_routes: null # or a list of route names this instance handles
387
- route_label_prefix: "Lorenz:" # the label prefix that names a route
388
- ```
389
-
390
- ### Jira tracker
391
-
392
- For both `jira` and `jira-mcp`, Lorenz only picks up issues that are assigned to the configured
393
- user (`trackers.jira.assignee` or `trackers.jira-mcp.assignee`, defaulting to the authenticated
394
- user via `assignee = currentUser()`) and labeled `agent`. This holds even when the configured JQL
395
- widens the scope, so issues must be explicitly delegated before Lorenz will dispatch them.
396
- Jira REST issues created through `tracker_create_issue` are assigned to that same owner by
397
- default. Jira MCP creation forwards a concrete configured or caller-provided `assignee` to the
398
- external MCP server.
399
- Jira REST supports the same persistent workpad-comment flow as Linear through
400
- `tracker_list_comments`, `tracker_comment`, and `tracker_update_comment`. Jira MCP maps those
401
- neutral comment tools to `jira_get_comments`, `jira_add_comment`, and `jira_update_comment` by
402
- default; override `trackers.<name>.mcp.tools.list_comments` or `update_comment` when the external
403
- MCP server uses different tool names.
404
-
405
- Direct Jira REST configuration:
406
-
407
- ```yaml
408
- tracker:
409
- kind: jira
410
- trackers:
411
- jira:
412
- provider: jira
413
- base_url: https://example.atlassian.net
414
- email: $JIRA_EMAIL
415
- api_key: $JIRA_API_KEY
416
- project_keys: ["ENG"]
417
- # Optional provider-native scope. When present, Lorenz combines it with active_states.
418
- # jql: 'project = ENG AND labels in ("lorenz")'
419
- ```
420
-
421
- Jira via an external MCP server:
422
-
423
- ```yaml
424
- tracker:
425
- kind: jira-mcp
426
- trackers:
427
- jira-mcp:
428
- provider: jira-mcp
429
- base_url: https://example.atlassian.net # optional; used for issue URLs when MCP payloads omit them
430
- project_keys: ["ENG"]
431
- mcp:
432
- url: http://127.0.0.1:5123/mcp
433
- token: $JIRA_MCP_TOKEN
434
- tools:
435
- search: atlassian_search_jira
436
- read_issue: atlassian_get_jira_issue
437
- update_status: atlassian_transition_jira_issue
438
- list_comments: atlassian_get_jira_comments
439
- comment: atlassian_add_jira_comment
440
- update_comment: atlassian_update_jira_comment
441
- create_issue: atlassian_create_jira_issue
442
- ```
443
-
444
- ### Local tracker (filesystem board)
445
-
446
- The local tracker runs Lorenz against a directory of Markdown files, with no Linear API key or
447
- workspace. See `WORKFLOW.local.md` for a complete example workflow.
448
-
449
- Configure it with `kind: local` and a board `path` (default `.lorenz/local`):
450
-
451
- ```yaml
452
- tracker:
453
- kind: local
454
- trackers:
455
- local:
456
- provider: local
457
- path: .lorenz/local
458
- id_prefix: "BOARD-" # optional, default "BOARD-"
459
- active_states:
460
- - Todo
461
- - In Progress
462
- terminal_states:
463
- - Done
464
- - Cancelled
465
- ```
466
-
467
- Both `path` and `id_prefix` are local-specific and always defaulted, so a local workflow is valid
468
- with just `kind: local`. `id_prefix` sets the issue-id prefix for the board: the tracker only treats
469
- `<prefix><n>.md` files as issues and mints new ids with it, so one board can be `BOARD-1`, `BOARD-2`
470
- and another `XXX-1`, `FEAT-1`, etc. It must be filesystem-safe (start alphanumeric, then only
471
- letters, digits, `_` or `-`); an unsafe prefix is rejected at config load. Changing the prefix of an
472
- existing board orphans files written under the old prefix (they stop matching), so set it up front.
473
-
474
- Each issue is one file named `<prefix><n>.md` (for example `.lorenz/local/BOARD-7.md`, or
475
- `.lorenz/local/XXX-7.md` with `id_prefix: "XXX-"`). The identifier is the file stem (`BOARD-7`).
476
- The format is YAML front matter followed by a `# Title`
477
- heading, the description, and an optional `## Comments` section:
478
-
479
- <!-- prettier-ignore -->
480
- ```markdown
481
- ---
482
- status: In Progress
483
- labels:
484
- - backend
485
- ---
486
-
487
- # Fix the retry queue
488
-
489
- The retry slot is not released when a worker fails.
490
-
491
- <!-- lorenz:comments -->
492
- ## Comments
493
- - 2026-05-29T12:00:00.000Z agent: Reproduced the leak; fix in progress.
494
- ```
495
-
496
- - `status` (required) is the issue state. Active states (`Todo`, `In Progress`) mean the issue is
497
- available to work; terminal states (`Done`, `Cancelled`) mean it is finished and must not be
498
- reopened. Configure the exact sets with `active_states` / `terminal_states`.
499
- - `labels` (optional) is a YAML list. Labels feed dispatch routing the same way Linear labels do.
500
- - The `# Title` heading is the issue title; the text below it is the description.
501
- - The `## Comments` section is managed by the `local_comment` tool. The hidden
502
- `<!-- lorenz:comments -->` marker delimits it so a description that itself contains a
503
- `## Comments` heading is never misparsed; treat the most recent comment block as the live
504
- workpad.
505
-
506
- Agent tools for `kind: local` (read and write, symmetric with `linear_graphql`):
507
-
508
- - `local_update_status` - move an issue to a new status (args: `issueId`, `status`).
509
- - `local_comment` - append a progress note to the issue's `## Comments` section (args: `issueId`,
510
- `body`).
511
- - `local_create_issue` - create a new board issue for out-of-scope follow-up work (args: `title`,
512
- optional `body`, optional `status`).
513
- - `local_read_issue` - read an issue's authoritative state: its current status, title, description,
514
- and comments (args: `issueId`). Use it to re-read state and recover prior progress notes on a
515
- continuation turn.
516
-
517
- Concurrent writes (multiple agents or ensemble slots) to the same board file are serialized
518
- in-process so a status change and comments are never lost. This assumes a single Lorenz daemon
519
- owns the board; editing the `BOARD-<n>.md` files from another process at the same time is out of
520
- scope.
521
-
522
- To seed a board so you can try `kind: local` immediately, use the demo seeder, which writes
523
- sample `BOARD-<n>.md` files through the same `BoardStore` the running tracker uses:
524
-
525
- ```sh
526
- npx tsx sandbox/seed-local.ts # seeds ./.lorenz/local
527
- npx tsx sandbox/seed-local.ts /tmp/demo-board # seeds an explicit directory
528
- npx tsx sandbox/seed-local.ts .lorenz/local 2 # seeds only the first 2 issues
529
- npx tsx sandbox/seed-local.ts /tmp/demo-board 3 XXX- # seeds XXX-1..XXX-3 (match trackers.local.id_prefix)
530
- ```
531
-
532
- Point `trackers.local.path` at the directory you seeded and run Lorenz as usual. If you set a
533
- custom `id_prefix`, pass the same prefix to the seeder so the seeded ids match what the tracker
534
- expects.
535
-
536
- ### Slack tracker (mention + thread commands)
537
-
538
- The Slack tracker treats an @-mention of a bot as an issue - in a channel message or in a thread
539
- reply (a reply mention tracks its thread, anchored at the root, with the reply as the request).
540
- The request's text is the issue title/description, threaded replies are comments, and the
541
- issue's STATUS lives in the thread: the bot posts `status: <Name>` replies and humans transition
542
- with `@bot !` command mentions; the latest event wins, and the bot mirrors the state onto its own
543
- reaction for glanceability. See `WORKFLOW.slack.md` for a complete example workflow.
544
-
545
- Set up a Slack app:
546
-
547
- 1. Create a Slack app at <https://api.slack.com/apps> (from scratch) in your workspace.
548
- 2. Under "OAuth & Permissions", add these **bot token scopes**:
549
- - `channels:history` - read messages in public channels.
550
- - `groups:history` - read messages in private channels (only if you watch private channels).
551
- - `reactions:read` - read reactions (legacy status fallback and the tracking marker).
552
- - `reactions:write` - mirror status onto the bot's own reaction and mark tracked threads.
553
- - `chat:write` - post threaded replies (comments and `status:` transitions).
554
- - `users:read` - resolve user ids to names for the `slack_user_info` tool (optional but
555
- recommended).
556
-
557
- Lorenz discovers issues by paging `conversations.history` and matching the bot's @-mention
558
- in message text, so it does not need `app_mentions:read`. Only add that scope if you separately
559
- wire up the Events API / `app_mention` subscription, which Lorenz does not use today.
560
-
561
- `conversations.history` is rate-limited (newer non-Marketplace apps can be throttled to roughly
562
- one request per minute), and each poll re-scans recent channel history. The shipped Slack
563
- workflow therefore sets a conservative `polling.interval_ms` of `60000` (one minute), and you
564
- should point it at dedicated, low-traffic channels so a busy channel does not trigger sustained
565
- `429`s. The transport's `429`/`Retry-After` backoff and per-channel `poll_error` handling cover
566
- transient limits on top of that.
567
-
568
- 3. Install the app to the workspace and copy the **Bot User OAuth Token** (starts with `xoxb-`).
569
- Export it as `SLACK_BOT_TOKEN`; Lorenz resolves it into `trackers.slack.api_key`.
570
- 4. Find the app's **bot user id** (the `U...` id, shown on the app's "App Home" / via
571
- `auth.test`). Export it as `SLACK_BOT_USER_ID` and reference it as
572
- `trackers.slack.bot_user_id`.
573
- 5. Invite the bot to each channel you want it to watch (`/invite @your-bot`). A bot only sees
574
- `*:history` for channels it has joined.
575
- 6. Collect the **channel IDs** (`C...`, from the channel's "About" panel) for those channels and
576
- list them under `trackers.slack.channels`.
577
-
578
- Configure it with `kind: slack`:
579
-
580
- ```yaml
581
- tracker:
582
- kind: slack
583
- trackers:
584
- slack:
585
- provider: slack
586
- channels:
587
- - C0123456789
588
- bot_user_id: $SLACK_BOT_USER_ID
589
- emoji_states:
590
- eyes: In Progress
591
- white_check_mark: Done
592
- x: Cancelled
593
- active_states:
594
- - Todo
595
- - In Progress
596
- terminal_states:
597
- - Done
598
- - Cancelled
599
- ```
600
-
601
- `SLACK_BOT_TOKEN` (the bot token), a non-empty `channels` list, and `trackers.slack.bot_user_id`
602
- (`SLACK_BOT_USER_ID`) are all **required**. The bot user id scopes issue creation to the bot's own
603
- mentions: only messages that mention that exact user become issues, and only that leading mention
604
- is stripped from the title. It is required so that ordinary human-to-human `<@U...>` mentions in a
605
- watched channel never spawn agents or expose their text to workers. If it is unset or resolves
606
- empty, config validation fails and the production transport fails closed (it scans nothing).
607
- Channel entries resolve `$VAR` references the same way `bot_user_id` does.
608
- `trackers.slack.assignee` is rejected for `kind: slack`: messages carry no assignee, so an
609
- assignee-partitioned deployment would otherwise silently dispatch everything everywhere.
610
-
611
- The issue identifier is the message reference in `<channel>:<ts>` form (for example
612
- `C0123456789:1717000000.000100`); that is the `issueId` passed to the write tools. Issues also
613
- carry a permalink (`{{ issue.url }}`, dashboard links) built from the workspace URL that
614
- `auth.test` reports, and `slack_read_thread` returns the same permalink for linking the source
615
- message from commits and PRs.
616
-
617
- Status is derived from the issue's thread: the bot's own `status: <Name>` replies (posted by
618
- `slack_update_status`) and human command mentions are ts-ordered events, and the latest wins.
619
- The human commands are:
620
-
621
- - `@bot !done` / `@bot !cancel` / `@bot !in progress` / `@bot !todo` - transition to the
622
- standard state.
623
- - `@bot !status <Name>` - transition to any configured active/terminal state (custom names
624
- too).
625
- - `@bot !reopen` - back to the first active state.
626
- - Any other `@bot` mention on a terminal issue re-opens it: mentioning the bot again always
627
- means "this needs attention".
628
-
629
- Reactions are per-author in Slack (the bot cannot remove a human's reaction and vice versa), so
630
- they are only the bot's visibility mirror, controlled by `emoji_states` (`:eyes:` ->
631
- `In Progress`, `:white_check_mark:` -> `Done`, `:x:` -> `Cancelled` by default). Threads that
632
- have never seen a status event fall back to the reaction-derived reading, so reaction-managed
633
- threads keep working. Two optional keys tune tracking: `marker_emoji` (default `robot_face`) is
634
- the reaction the bot drops on a reply-tracked thread's root, and `reply_lookback_days` (default
635
- `2`) bounds how far back untracked threads are inspected for new reply-mention requests.
636
-
637
- Agent tools for `kind: slack`, served by the `slack` tool pack (mounted by default alongside the
638
- provider-neutral `tracker` pack):
639
-
640
- - `slack_update_status` - set the issue's status by posting the bot's authoritative `status:`
641
- thread reply, then mirror the bot's reaction (args: `issueId`, `status` - any configured
642
- active/terminal state name).
643
- - `slack_comment` - post a threaded reply on the source message as a comment (args: `issueId`,
644
- `body`).
645
- - `slack_read_thread` - read the issue's authoritative state: thread-derived status, source
646
- message, request reply (for thread-tracked issues), reactions, permalink, and all replies
647
- (args: `issueId`). Use it to re-read state, catch new human replies/commands, and recover
648
- prior progress notes on a continuation turn.
649
- - `slack_query` - read-only query over the tracked issues in the watched channels (bot-mention
650
- roots plus bot-marked threads), with thread-derived state: filter with the shared JSON
651
- predicate DSL, project fields, order, and page; `expand` adds `thread` and `reactions` (args:
652
- `channels?`, `where?`, `select?`, `expand?`, `order_by?`, `limit?`, `offset?`).
653
- - `slack_user_info` - resolve a `U...` user id to its profile: name, real name, display name,
654
- bot flag (args: `userId`).
655
- - `slack_channel_context` - read the channel conversation around a tracked issue's source
656
- message, ascending (args: `issueId`, `before?` default 10 max 50, `after?` default 10 max 50).
657
-
658
- There is no `slack_create_issue`, and the neutral `tracker_create_issue` reports itself as
659
- unavailable on Slack: issues are created by humans @-mentioning the bot, not by the agent.
660
-
661
- Routing note: Slack issues carry only hashtag-derived labels (a `#tag` in the message text
662
- becomes the label `tag`); they are not otherwise routed or assigned. Dispatch treats a label as a
663
- route only when it starts with `route_label_prefix`, so the Slack workflow sets
664
- `route_label_prefix: route-`. Tag a message `#route-<name>` to route it: `#route-backend` becomes
665
- the label `route-backend`, which dispatch resolves to the route `backend` (set `only_routes`
666
- accordingly). Plain hashtags such as `#backend` stay non-route labels; with the default
667
- `accept_unrouted: true` all Slack mentions are still picked up.
67
+ Linear is the default tracker: issues live in a Linear project, read access uses `LINEAR_API_KEY`,
68
+ and project selection uses `project_slug`. Route labels such as `Lorenz:backend` let multiple
69
+ instances share one project. Setup and configuration are in
70
+ [Linear tracker](./docs/trackers/linear.md). Other sources (Jira, Slack, local, memory) are covered
71
+ under [Trackers](./docs/trackers/index.md).
668
72
 
669
73
  ## Workflow Prompt
670
74
 
671
- The prompt body can read these public issue and run fields:
672
-
673
- - `{{ issue.identifier }}`
674
- - `{{ issue.title }}`
675
- - `{{ issue.description }}`
676
- - `{{ issue.state }}`
677
- - `{{ issue.state_type }}`
678
- - `{{ issue.labels }}`
679
- - `{{ issue.url }}`
680
- - `{{ issue.id }}`
681
- - `{{ issue.priority }}`
682
- - `{{ issue.branch_name }}`
683
- - `{{ issue.assignee_id }}`
684
- - `{{ issue.created_at }}`
685
- - `{{ issue.updated_at }}`
686
- - `{{ issue.assigned_to_worker }}`
687
- - `{{ issue.blocked_by }}`
688
- - `{{ attempt }}`
689
- - `{{ ensemble.enabled }}`
690
- - `{{ ensemble.slot_index }}`
691
- - `{{ ensemble.size }}`
692
-
693
- Workspace tests render representative Liquid constructs: conditionals, null fallbacks, loops,
694
- `forloop` metadata, nested blocker refs, and common filters.
75
+ The prompt body reads public issue and run fields as Liquid variables, such as
76
+ `{{ issue.identifier }}`, `{{ issue.title }}`, `{{ issue.description }}`, and `{{ attempt }}`. The
77
+ complete variable list is in the [Workflow prompt reference](./docs/reference/workflow-prompt.md).
695
78
 
696
79
  ## Skills
697
80
 
698
- The `skills/` directory in this repo contains orchestration skills referenced by the example
699
- workflow files:
700
-
701
- - `lorenz-commit` produces clean, logical commits.
702
- - `lorenz-push` pushes branches and creates or updates PRs.
703
- - `lorenz-pull` merges the latest `origin/main` into a working branch.
704
- - `lorenz-land` monitors and merges approved PRs.
705
- - `lorenz-debug` investigates stuck runs and execution failures.
81
+ The `skills/` directory holds orchestration skills (`lorenz-commit`, `lorenz-push`, `lorenz-pull`,
82
+ `lorenz-land`, `lorenz-debug`) referenced by the example workflows. Lorenz copies skills into
83
+ `.lorenz/skills/` in each prepared workspace before the agent starts. See
84
+ [Skills](./docs/agents/skills.md) for how `agent.skills` and tool-pack skills are resolved.
706
85
 
707
- Lorenz copies skills into `.lorenz/skills/` in each prepared workspace before the agent starts.
708
- A `.gitignore` containing `*` is written alongside the copied skills so they are never committed.
709
- Skills come from two places:
710
-
711
- - **`agent.skills`** - a list of skill directories you maintain. Each entry is one skill directory
712
- (e.g. `./skills/lorenz-land`) and is copied to `.lorenz/skills/<directory-name>`. Relative
713
- paths resolve from the workflow file directory.
714
- - **Tool packs** - a mounted tool pack can bundle the skill that documents it, so the skill ships
715
- automatically when the tool is in use. The Linear pack bundles `lorenz-linear` (raw Linear
716
- access via the injected `linear_graphql` tool for Codex or the `/mcp` endpoint for Claude), so
717
- enabling Linear tools overlays that skill without listing it under `agent.skills`.
86
+ ## Observability
718
87
 
719
- It is up to the user to reference `.lorenz/skills` in their WORKFLOW.md (or the agent's
720
- equivalent configuration) so the agent knows where to find the overlaid skills at runtime.
88
+ The terminal dashboard shows agents, throughput, runtime, token usage, rate limits, sessions, the
89
+ retry queue, and dispatch blocks. The web dashboard exposes the same runtime snapshot over a local
90
+ HTTP server, started with `--port` or `server.port`. Routes, the WebSocket stream, and `/mcp` tool
91
+ serving are documented in [Observability](./docs/observability.md).
721
92
 
722
- ## Observability
93
+ ## Contributing
723
94
 
724
- The terminal dashboard shows agents, throughput, runtime, token usage, rate limits, running
725
- sessions, retry queue, and dispatch blocks. The web dashboard exposes the same runtime snapshot
726
- through a local HTTP server.
95
+ ### Requirements
727
96
 
728
- Start the web dashboard with `--port` or `server.port`:
97
+ [mise](https://mise.jdx.dev/) manages Node 24 and pnpm 9 from `mise.toml`:
729
98
 
730
99
  ```sh
731
- pnpm start -- WORKFLOW.md --port 4000
100
+ mise trust
101
+ mise install
102
+ pnpm install
732
103
  ```
733
104
 
734
- API routes:
105
+ ### Run
735
106
 
736
- - `/`
737
- - `/api/v1/state`
738
- - `/api/v1/runs`
739
- - `/api/v1/runs?id=<run-id>`
740
- - `/api/v1/refresh`
741
- - `/api/v1/:issue_identifier`
107
+ Build, then run the CLI against a workflow file:
742
108
 
743
- Live updates (ops state and trace events) stream over the `/ws` WebSocket endpoint.
109
+ ```sh
110
+ pnpm build
111
+ pnpm start -- WORKFLOW.md
112
+ pnpm start:once -- --dry-run --no-tui WORKFLOW.md
113
+ ```
744
114
 
745
- Claude sessions use `/mcp` for injected dynamic tools when the runtime has started an
746
- observability server. The server also starts automatically for Claude workflows so the ACP bridge
747
- can reach those tools.
115
+ ### Workspace Layout
748
116
 
749
- `lorenz runs` queries the same API for run history, cost summaries, retry summaries, and raw
750
- JSON output.
117
+ `apps/cli` is the composition root; `packages/*` is the provider-agnostic engine;
118
+ `extensions/*` are the tracker backends; `test/` holds workspace-level tests. The layering rules
119
+ and the recipe for adding a tracker live in [Architecture](./docs/architecture.md) and the
120
+ [Source map](./docs/source-map.md).
751
121
 
752
- ## Testing
122
+ ### Testing
753
123
 
754
124
  ```sh
755
125
  mise run tidy
@@ -757,52 +127,25 @@ mise run check
757
127
  ```
758
128
 
759
129
  `mise run tidy` formats and applies lint fixes. `mise run check` runs typecheck, build, tests, and
760
- lint.
130
+ lint. When running Vitest directly, rebuild first so tests exercise the current compiled packages.
761
131
 
762
- Useful direct commands:
763
-
764
- ```sh
765
- pnpm typecheck
766
- pnpm build
767
- pnpm lint
768
- pnpm test
769
- pnpm test:watch
770
- ```
132
+ ### Live Tests
771
133
 
772
- When running Vitest directly, rebuild first so tests exercise the current compiled packages.
773
-
774
- ## Live Tests
775
-
776
- Live tests are opt-in and launch real CLIs or services in isolated workspaces.
134
+ Live tests are opt-in and launch real CLIs or services in isolated workspaces:
777
135
 
778
136
  ```sh
779
137
  pnpm test:live:codex
780
138
  pnpm test:live:linear-codex
781
139
  pnpm test:live:claude
782
140
  pnpm test:live:ssh
783
- pnpm test:live:linear-sandbox
784
141
  ```
785
142
 
786
- `pnpm test:live` runs the Codex, Linear plus Codex, and Claude live tests.
787
-
788
- Environment knobs:
789
-
790
- - `LORENZ_TS_CODEX_ACP_COMMAND` overrides the Codex ACP bridge command for live tests.
791
- - `LORENZ_TS_CLAUDE_ACP_BRIDGE_COMMAND` enables Claude live tests.
792
- - `LORENZ_TS_CLAUDE_ACP_BRIDGE_ARGS` supplies Claude ACP bridge args as a JSON string array.
793
- - `LINEAR_API_KEY` is required for Linear live tests and MCP canaries.
794
- - `LINEAR_PROJECT_SLUG` selects the Linear project for `pnpm test:live:linear-codex`.
795
- - `LORENZ_LIVE_SSH_WORKER_HOSTS` is a comma-separated list of real SSH workers.
796
- - When `LORENZ_LIVE_SSH_WORKER_HOSTS` is unset, the SSH live test can use disposable local
797
- workers if Docker, `ssh-keygen`, and Codex auth are available.
798
- - `LORENZ_LIVE_DOCKER_CODEX_AUTH_JSON` points disposable workers at a Codex auth file. The
799
- default is `~/.codex/auth.json`.
800
- - `CLAUDE_CODE_OAUTH_TOKEN` or `LORENZ_LIVE_DOCKER_CLAUDE_CODE_OAUTH_TOKEN` lets disposable
801
- workers run the remote Claude canary.
802
- - `LORENZ_TS_REQUIRE_REMOTE_CLAUDE=1` makes the remote Claude canary mandatory in the SSH live
803
- test.
143
+ `LORENZ_LIVE_SSH_WORKER_HOSTS` is a comma-separated list of real SSH workers. When it is unset, the
144
+ SSH live test can use disposable local workers if Docker, `ssh-keygen`, and Codex auth are
145
+ available. `LINEAR_API_KEY` is required for Linear live tests, and
146
+ `LORENZ_TS_CLAUDE_ACP_BRIDGE_COMMAND` enables the Claude live tests.
804
147
 
805
- ## Packaging
148
+ ### Packaging
806
149
 
807
150
  ```sh
808
151
  pnpm build
@@ -812,18 +155,12 @@ pnpm --filter @lorenz/cli pack --dry-run
812
155
  The CLI package includes the built binary. Workspace documentation, workflow fixtures, and test
813
156
  evidence stay at the workspace root.
814
157
 
815
- ## Compatibility Contracts
816
-
817
- The checked-in workflow files are executable fixtures:
818
-
819
- - `WORKFLOW.md`
820
- - `WORKFLOW_FULL_ACCESS.md`
158
+ ### Compatibility Contracts
821
159
 
160
+ The checked-in workflow files (`WORKFLOW.md`, `WORKFLOW_FULL_ACCESS.md`) are executable fixtures.
822
161
  `pnpm test` guards workflow docs, prompt rendering, dashboard snapshots, runtime behavior, and CLI
823
162
  documentation. Update the fixture and the matching test together when the public contract changes.
824
163
 
825
164
  ## License
826
165
 
827
- See [CHANGELOG.md](CHANGELOG.md) for notable fork-specific changes.
828
-
829
- This project is licensed under the [Apache License 2.0](LICENSE).
166
+ See [CHANGELOG.md](CHANGELOG.md) for notable changes. This project is licensed under the [Apache License 2.0](LICENSE).