@bastani/atomic 0.5.6 → 0.5.7-0

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.
package/README.md CHANGED
@@ -9,33 +9,59 @@
9
9
  [![Bun](https://img.shields.io/badge/Bun-Runtime-f9f1e1?logo=bun&logoColor=black)](./package.json)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)
11
11
 
12
- Atomic is an open-source **agent harness framework** that lets you build, compose, and run **multi-session coding workflows** on top of **Claude Code**, **OpenCode**, and **GitHub Copilot CLI** with **58 built-in skills**, **12 specialized sub-agents**, and **containerized execution**.
12
+ Atomic is an open-source **TypeScript SDK** for building **any harness you want** around your coding agent **Claude Code**, **OpenCode**, or **GitHub Copilot CLI**. Chain sessions into pipelines, add human-in-the-loop approval gates, plug in CI and notifications, dispatch **12 specialized sub-agents**, and tap **58 built-in skills** — then ship it as TypeScript your whole team runs.
13
13
 
14
- > Build any agent harness you want. Define workflows as TypeScript. Run them on any coding agent.
14
+ > Define how your agent works. Start for yourself, scale to your team.
15
15
 
16
16
  ---
17
17
 
18
18
  ## Why Atomic
19
19
 
20
- Building harnesses and workflows around coding agents is harder than it should be. Teams hit the same walls:
20
+ Coding agents keep getting more capable better reasoning, larger context windows, more reliable tool use. But a more capable model doesn't reduce the need for structure around it. It **increases** it.
21
21
 
22
- - **No way to chain agent sessions.** You can prompt an agent, but there's no standard way to feed one session's output into the next research into planning, planning into implementation, implementation into review. Teams resort to copy-pasting between terminals.
23
- - **Context degrades in long sessions.** A single agent asked to research, plan, implement, and review in one session produces increasingly unreliable output as its context window fills up. There's no built-in mechanism to isolate concerns across sessions.
24
- - **Agent-specific configuration is fragmented.** Claude Code, OpenCode, and Copilot CLI each have their own config directories, skill formats, and agent definitions. Building a workflow that works across agents means maintaining three separate configurations.
25
- - **Team processes live in wikis, not in code.** Every team has a process — triage bugs this way, ship features that way, review PRs with these checks. But those processes are prose in a wiki, not executable code that an agent can follow.
26
- - **Autonomous execution is unsafe without isolation.** Agents run shell commands, delete files, and execute arbitrary code. Running them autonomously on your host system is a risk most teams won't take.
27
- - **Specialized work requires specialized agents.** A single general-purpose agent juggling file search, code analysis, web research, and implementation will lose track of details. There's no framework for dispatching purpose-built sub-agents with scoped tools and isolated context windows.
28
- - **Agent workflows aren't deterministic.** Even when you do chain sessions together, there's no guarantee they'll execute in the same order, pass data the same way, or produce an inspectable record. Without strict ordering and controlled data flow, workflows become unpredictable — hard to debug, impossible to reproduce.
22
+ The bottleneck is shifting from "can my agent write this code?" to "can my agent follow my process?" Every team has a process — how code gets reviewed, what checks run before merging, who approves deployments, how production gets monitored. That process lives in wikis nobody reads, in one senior engineer's head, or nowhere at all. A powerful agent without a defined process is just a faster way to ship unreviewed code.
29
23
 
30
- Atomic solves these by giving you a **Workflow SDK** to define multi-session pipelines as TypeScript with **deterministic execution**strict step ordering, frozen definitions, and controlled transcript passing plus **12 specialized sub-agents** that keep context windows small and focused, and **containerized execution** via devcontainer features that isolate agents from your host system. Write a workflow once, run it on Claude Code, OpenCode, or Copilot CLI with a flag change.
24
+ **Harnesses are what turn a capable agent into a reliable part of your engineering workflow.** A harness encodes your processresearch, then implement, then review, then run CI, then create a PR, then notify the right person, then wait for approval, then merge. Without one, you're prompting manually and copy-pasting between terminal sessions. With one, you run a single command and the process executes itself.
25
+
26
+ Better models make harnesses **more** important, not less. The more you can trust an agent to execute complex tasks, the more value you get from defining exactly **what** it should execute, in **what order**, with **what checks** along the way. The harness is the durable layer — models will keep improving underneath it, but your process stays the same.
27
+
28
+ Atomic gives you the SDK to build that harness:
29
+
30
+ - **Start for yourself.** Automate the repetitive parts of your own workflow — research a codebase, add monitoring, generate specs. One developer, one afternoon, one TypeScript file.
31
+ - **Scale to your team.** Encode your team's review process, deployment gates, and quality checks as TypeScript that every team member runs identically. Your process becomes versioned, testable, and reproducible — not tribal knowledge.
32
+ - **Work across agents.** Write a harness once, run it on Claude Code, OpenCode, or Copilot CLI with a flag change. The harness is the constant; the agent is swappable.
33
+
34
+ ### What You Can Build
35
+
36
+ **Add production monitoring to your codebase.** Build a harness that researches your current observability setup, identifies gaps in metrics, health checks, and alerting, implements the missing pieces, and reviews the changes — all in one run.
37
+
38
+ ```bash
39
+ atomic workflow -n add-monitoring -a claude "add Prometheus metrics and health checks to all API endpoints"
40
+ ```
41
+
42
+ **Automate your team's review-to-merge pipeline.** Encode your exact process: review code changes → run security scans and linting in parallel → create a PR → notify the team lead on Slack → wait for human approval → merge. The [human-in-the-loop gate](#workflow-sdk--build-your-own-deterministic-harness) pauses execution until the right person approves. New team members inherit the same pipeline on day one.
43
+
44
+ ```bash
45
+ atomic workflow -n review-to-merge -a claude
46
+ ```
47
+
48
+ **Run parallel UX testing with 50 personas.** Spin up 50 agents — each with a distinct user persona (first-time user, power user, accessibility-dependent user, non-technical stakeholder) — each using [Playwright](#built-in-skills) to navigate your app and report usability issues from their perspective. Batch in groups, aggregate findings, and get feedback at a scale no manual process can match.
49
+
50
+ ```bash
51
+ atomic workflow -n ux-personas -a claude
52
+ ```
53
+
54
+ Each of these is a `.ts` file using Atomic's [Workflow SDK](#workflow-sdk--build-your-own-deterministic-harness). See [Build a Workflow](#5-build-a-workflow) for a working example, or read the full SDK reference below.
31
55
 
32
56
  ---
33
57
 
34
58
  ## Table of Contents
35
59
 
60
+ - [Why Atomic](#why-atomic)
61
+ - [What You Can Build](#what-you-can-build)
36
62
  - [Quick Start](#quick-start)
37
63
  - [Core Features](#core-features)
38
- - [Multi-Agent SDK Support](#multi-agent-sdk-support)
64
+ - [Multi-Agent Support](#multi-agent-support)
39
65
  - [Workflow SDK — Build Your Own Deterministic Harness](#workflow-sdk--build-your-own-deterministic-harness)
40
66
  - [Deep Codebase Research](#deep-codebase-research)
41
67
  - [Autonomous Execution (Ralph)](#autonomous-execution-ralph)
@@ -185,54 +211,78 @@ Create a workflow project, install the SDK, and add your workflow file:
185
211
 
186
212
  ```bash
187
213
  bun init && bun add @bastani/atomic
188
- mkdir -p .atomic/workflows/my-workflow/claude
214
+ mkdir -p .atomic/workflows/review-to-merge/claude
189
215
  ```
190
216
 
217
+ Here's one of the [canonical use cases](#what-you-can-build) — a team pipeline that reviews code, runs checks in parallel, creates a PR, notifies on Slack, waits for human approval, and merges:
218
+
191
219
  ```ts
192
- // .atomic/workflows/my-workflow/claude/index.ts
220
+ // .atomic/workflows/review-to-merge/claude/index.ts
193
221
  import { defineWorkflow } from "@bastani/atomic/workflows";
194
222
 
195
223
  export default defineWorkflow<"claude">({
196
- name: "my-workflow",
197
- description: "Research -> Implement -> Review",
224
+ name: "review-to-merge",
225
+ description: "Review CI PR → Notify → Approve → Merge",
198
226
  })
199
227
  .run(async (ctx) => {
200
- // Free-form workflows receive their positional prompt under
201
- // `ctx.inputs.prompt`. Destructure it once so every stage below
202
- // can close over a bare string.
203
- const prompt = ctx.inputs.prompt ?? "";
204
-
205
- const research = await ctx.stage(
206
- { name: "research", description: "Analyze the codebase" },
207
- {}, {},
208
- async (s) => {
209
- await s.session.query(`/research-codebase ${prompt}`);
210
- s.save(s.sessionId);
211
- },
212
- );
213
-
214
- await ctx.stage(
215
- { name: "implement", description: "Implement based on research" },
228
+ // Step 1: Review the changes
229
+ const review = await ctx.stage(
230
+ { name: "review", description: "Review code changes" },
216
231
  {}, {},
217
232
  async (s) => {
218
- const transcript = await s.transcript(research);
219
233
  await s.session.query(
220
- `Read ${transcript.path} and implement the changes. Run tests to verify.`,
234
+ "Review all uncommitted changes. Flag issues with correctness, security, and style.",
221
235
  );
222
236
  s.save(s.sessionId);
223
237
  },
224
238
  );
225
239
 
226
- await ctx.stage(
227
- { name: "review", description: "Review the implementation" },
228
- {}, {},
229
- async (s) => {
230
- await s.session.query(
231
- "Review all uncommitted changes. Flag any issues with correctness, tests, or style.",
232
- );
240
+ // Step 2: Run security and CI checks in parallel
241
+ await Promise.all([
242
+ ctx.stage({ name: "security-scan" }, {}, {}, async (s) => {
243
+ await s.session.query("Run `bun audit` and scan for leaked secrets or credentials.");
233
244
  s.save(s.sessionId);
234
- },
235
- );
245
+ }),
246
+ ctx.stage({ name: "ci-checks" }, {}, {}, async (s) => {
247
+ await s.session.query("Run `bun lint` and `bun test`. Report any failures.");
248
+ s.save(s.sessionId);
249
+ }),
250
+ ]);
251
+
252
+ // Step 3: Create a PR with the review summary
253
+ await ctx.stage({ name: "create-pr" }, {}, {}, async (s) => {
254
+ const transcript = await s.transcript(review);
255
+ await s.session.query(
256
+ `Read the review at ${transcript.path}. Create a pull request summarizing the changes.`,
257
+ );
258
+ s.save(s.sessionId);
259
+ });
260
+
261
+ // Step 4: Notify on Slack, then wait for human approval before merging.
262
+ // Stage callbacks are plain Bun code — fetch(), Bun.spawn(), and any
263
+ // Node API work here alongside agent session queries.
264
+ await ctx.stage({ name: "notify-and-merge" }, {}, {}, async (s) => {
265
+ await fetch("https://slack.com/api/chat.postMessage", {
266
+ method: "POST",
267
+ headers: {
268
+ Authorization: `Bearer ${process.env.SLACK_TOKEN}`,
269
+ "Content-Type": "application/json",
270
+ },
271
+ body: JSON.stringify({
272
+ channel: "#code-review",
273
+ text: "New PR ready for review — please approve in GitHub.",
274
+ }),
275
+ });
276
+
277
+ // Human-in-the-loop: AskUserQuestion pauses the session until the
278
+ // user responds. The agent won't merge until approval is given.
279
+ await s.session.query(
280
+ "The team has been notified on Slack. Ask the user to confirm the PR " +
281
+ "is approved, then merge it with `gh pr merge --squash`.",
282
+ { allowedTools: ["Bash", "Read", "AskUserQuestion"] },
283
+ );
284
+ s.save(s.sessionId);
285
+ });
236
286
  })
237
287
  .compile();
238
288
  ```
@@ -240,10 +290,10 @@ export default defineWorkflow<"claude">({
240
290
  Run it:
241
291
 
242
292
  ```bash
243
- atomic workflow -n my-workflow -a claude "add user avatars to the profile page"
293
+ atomic workflow -n review-to-merge -a claude
244
294
  ```
245
295
 
246
- Add a spec phase, parallelize independent sessions, swap in a different agent the workflow is yours to define. See [Workflow SDK — Build Your Own Harness](#workflow-sdk--build-your-own-harness) for the full API and more examples.
296
+ This single file demonstrates multi-step pipelines, parallel stages (`Promise.all`), transcript passing between sessions, external API calls (`fetch`), and human-in-the-loop approval — all in plain TypeScript. Swap `-a claude` for `-a opencode` or `-a copilot` to run the same harness on a different agent. See [Workflow SDK — Build Your Own Harness](#workflow-sdk--build-your-own-deterministic-harness) for the full API and more examples.
247
297
 
248
298
  > **Want something that works out of the box?** Atomic ships with `ralph`, a built-in workflow that plans, implements, reviews, and debugs autonomously — see [Autonomous Execution (Ralph)](#autonomous-execution-ralph).
249
299
 
@@ -251,15 +301,15 @@ Add a spec phase, parallelize independent sessions, swap in a different agent
251
301
 
252
302
  ## Core Features
253
303
 
254
- ### Multi-Agent SDK Support
304
+ ### Multi-Agent Support
255
305
 
256
- Atomic is the only harness that unifies **three production agent SDKs** behind a single interface. Switch between agents with a flag your workflows, skills, and sub-agents work across all of them.
306
+ Atomic works across **three production coding agents** switch between them with a flag and your workflows, skills, and sub-agents carry over.
257
307
 
258
- | Agent | SDK | Command |
259
- | ------------------ | -------------------------------- | ------------------------- |
260
- | Claude Code | `@anthropic-ai/claude-agent-sdk` | `atomic chat -a claude` |
261
- | OpenCode | `@opencode-ai/sdk` | `atomic chat -a opencode` |
262
- | GitHub Copilot CLI | `@github/copilot-sdk` | `atomic chat -a copilot` |
308
+ | Agent | Command |
309
+ | ------------------ | ------------------------- |
310
+ | Claude Code | `atomic chat -a claude` |
311
+ | OpenCode | `atomic chat -a opencode` |
312
+ | GitHub Copilot CLI | `atomic chat -a copilot` |
263
313
 
264
314
  Each agent gets its own configuration directory (`.claude/`, `.opencode/`, `.github/`), skills, and context files — all managed by Atomic. Write a workflow once, run it on any agent.
265
315
 
@@ -815,6 +865,7 @@ During `atomic chat`, there is no Atomic-owned TUI — `atomic chat -a <agent>`
815
865
  | `atomic init` | Interactive project setup (agent selection, SCM choice, config sync) |
816
866
  | `atomic chat` | Spawn the native agent CLI inside a tmux/psmux session |
817
867
  | `atomic workflow` | Run a multi-session agent workflow with the Atomic orchestrator panel |
868
+ | `atomic workflow list` | List available workflows, grouped by source |
818
869
  | `atomic session list` | List all running sessions on the atomic tmux socket |
819
870
  | `atomic session connect [name]` | Attach to a session (interactive picker when no name given) |
820
871
  | `atomic completions <shell>` | Output shell completion script (bash, zsh, fish, powershell) |
@@ -886,7 +937,6 @@ atomic chat -a claude --verbose # Forward --verbose to claude
886
937
  | -------------------------- | ------------------------------------------------------------------- |
887
938
  | `-n, --name <name>` | Workflow name (matches directory under `.atomic/workflows/<name>/`) |
888
939
  | `-a, --agent <name>` | Agent: `claude`, `opencode`, `copilot` |
889
- | `-l, --list` | List available workflows, grouped by source |
890
940
  | `--<field>=<value>` | Structured input for workflows that declare an `inputs` schema (also accepts `--<field> <value>`) |
891
941
  | `[prompt...]` | Positional prompt for free-form workflows (rejected on workflows with a declared schema) |
892
942
 
@@ -894,7 +944,8 @@ The workflow command supports four invocation shapes:
894
944
 
895
945
  ```bash
896
946
  # 1. List every workflow available to you, grouped by source
897
- atomic workflow -l
947
+ atomic workflow list
948
+ atomic workflow list -a claude # filter by agent
898
949
 
899
950
  # 2. Launch the interactive picker for an agent (no -n) — fuzzy-search
900
951
  # the list, fill the form rendered from the workflow's declared inputs,
@@ -34,16 +34,6 @@ export declare class PanelStore {
34
34
  * Switching to "graph" clears the active agent.
35
35
  */
36
36
  setViewMode(mode: ViewMode, agentId?: string): void;
37
- /**
38
- * Return non-orchestrator agents that have started (not pending).
39
- * Used for the tmux status bar agent count and active-agent index.
40
- */
41
- getSubagents(): SessionData[];
42
- /**
43
- * Return the 0-based index of the active agent within the subagent list,
44
- * or -1 if not found.
45
- */
46
- getActiveAgentIndex(): number;
47
37
  /** Safely invoke exitResolve at most once, guarding against rapid repeated calls. */
48
38
  resolveExit(): void;
49
39
  /** Safely invoke abortResolve at most once to signal mid-execution quit. */
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator-panel-store.d.ts","sourceRoot":"","sources":["../../../src/sdk/components/orchestrator-panel-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAiB,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAExG,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AAE3B,qBAAa,UAAU;IACrB,OAAO,SAAK;IACZ,YAAY,SAAM;IAClB,KAAK,SAAM;IACX,MAAM,SAAM;IACZ,QAAQ,EAAE,WAAW,EAAE,CAAM;IAC7B,cAAc,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAQ;IAChF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IACjC,iBAAiB,UAAS;IAC1B,WAAW,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IACxC,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IAEzC,0EAA0E;IAC1E,QAAQ,EAAE,QAAQ,CAAW;IAC7B,4FAA4F;IAC5F,aAAa,SAAM;IAEnB,OAAO,CAAC,SAAS,CAAuB;IAExC,SAAS,GAAI,IAAI,QAAQ,KAAG,CAAC,MAAM,IAAI,CAAC,CAGtC;IAEF,OAAO,CAAC,IAAI;IAKZ,eAAe,CACb,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,EAAE,EACxB,MAAM,EAAE,MAAM,GACb,IAAI;IAuBP,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQhC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQnC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAS9C,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAKtC,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI;IAUlE,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAWpC;;;;OAIG;IACH,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAMnD;;;OAGG;IACH,YAAY,IAAI,WAAW,EAAE;IAM7B;;;OAGG;IACH,mBAAmB,IAAI,MAAM;IAK7B,qFAAqF;IACrF,WAAW,IAAI,IAAI;IAQnB,4EAA4E;IAC5E,YAAY,IAAI,IAAI;IAQpB,gFAAgF;IAChF,WAAW,IAAI,IAAI;IAQnB,qBAAqB,IAAI,IAAI;CAI9B"}
1
+ {"version":3,"file":"orchestrator-panel-store.d.ts","sourceRoot":"","sources":["../../../src/sdk/components/orchestrator-panel-store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAiB,YAAY,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAExG,KAAK,QAAQ,GAAG,MAAM,IAAI,CAAC;AAE3B,qBAAa,UAAU;IACrB,OAAO,SAAK;IACZ,YAAY,SAAM;IAClB,KAAK,SAAM;IACX,MAAM,SAAM;IACZ,QAAQ,EAAE,WAAW,EAAE,CAAM;IAC7B,cAAc,EAAE;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAQ;IAChF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAQ;IACjC,iBAAiB,UAAS;IAC1B,WAAW,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IACxC,YAAY,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAAQ;IAEzC,0EAA0E;IAC1E,QAAQ,EAAE,QAAQ,CAAW;IAC7B,4FAA4F;IAC5F,aAAa,SAAM;IAEnB,OAAO,CAAC,SAAS,CAAuB;IAExC,SAAS,GAAI,IAAI,QAAQ,KAAG,CAAC,MAAM,IAAI,CAAC,CAGtC;IAEF,OAAO,CAAC,IAAI;IAKZ,eAAe,CACb,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,YAAY,EAAE,EACxB,MAAM,EAAE,MAAM,GACb,IAAI;IAuBP,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQhC,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQnC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAS9C,UAAU,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IAKtC,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI;IAUlE,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAWpC;;;;OAIG;IACH,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI;IAMnD,qFAAqF;IACrF,WAAW,IAAI,IAAI;IAQnB,4EAA4E;IAC5E,YAAY,IAAI,IAAI;IAQpB,gFAAgF;IAChF,WAAW,IAAI,IAAI;IAQnB,qBAAqB,IAAI,IAAI;CAI9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-graph-panel.d.ts","sourceRoot":"","sources":["../../../src/sdk/components/session-graph-panel.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;GAGG;AAgDH,wBAAgB,iBAAiB,8BA0ahC"}
1
+ {"version":3,"file":"session-graph-panel.d.ts","sourceRoot":"","sources":["../../../src/sdk/components/session-graph-panel.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;GAGG;AAoDH,wBAAgB,iBAAiB,8BAgchC"}
@@ -20,12 +20,11 @@ export declare const TMUX_DEFAULT_STATUS_LEFT = " ";
20
20
  export declare const TMUX_DEFAULT_STATUS_LEFT_LENGTH = "10";
21
21
  export declare const TMUX_DEFAULT_STATUS_RIGHT = " #{session_name} | %H:%M ";
22
22
  export declare const TMUX_DEFAULT_STATUS_RIGHT_LENGTH = "60";
23
- /**
24
- * Escape a string for safe interpolation into tmux format strings.
25
- * Replaces `#` with `##` to prevent tmux from interpreting `#[...]`
26
- * as style directives or `#(...)` as shell command expansions.
27
- */
28
- export declare function escapeTmuxFormat(value: string): string;
23
+ export declare const TMUX_ATTACHED_STATUS_RIGHT = "#[fg=#cdd6f4]ctrl+g #[fg=#6c7086]graph #[fg=#585b70]\u00B7 #[fg=#cdd6f4]ctrl+\\ #[fg=#6c7086]next ";
24
+ export declare const TMUX_ATTACHED_STATUS_RIGHT_LENGTH = "40";
25
+ export declare const TMUX_ATTACHED_WINDOW_FMT = "#{?#{==:#{window_index},0},, #W }";
26
+ export declare const TMUX_ATTACHED_WINDOW_STYLE = "fg=#6c7086";
27
+ export declare const TMUX_ATTACHED_WINDOW_CURRENT_STYLE = "fg=#cdd6f4,bold";
29
28
  /**
30
29
  * Resolve the terminal multiplexer binary for the current platform.
31
30
  *
@@ -1 +1 @@
1
- {"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/tmux.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tmux.d.ts","sourceRoot":"","sources":["../../../src/sdk/runtime/tmux.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bastani/atomic",
3
- "version": "0.5.6",
3
+ "version": "0.5.7-0",
4
4
  "description": "Configuration management CLI and SDK for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/cli.ts CHANGED
@@ -327,7 +327,8 @@ async function main(): Promise<void> {
327
327
  argv.includes("--version") ||
328
328
  argv.includes("-v") ||
329
329
  argv.includes("--help") ||
330
- argv.includes("-h");
330
+ argv.includes("-h") ||
331
+ argv[0] === "completions";
331
332
 
332
333
  if (!isInfoCommand) {
333
334
  const { autoSyncIfStale } = await import("./services/system/auto-sync.ts");
@@ -140,5 +140,5 @@ _atomic_session() {
140
140
  esac
141
141
  }
142
142
 
143
- _atomic "$@"
143
+ compdef _atomic atomic
144
144
  `;
package/src/lib/spawn.ts CHANGED
@@ -99,228 +99,19 @@ export interface EnsureOptions {
99
99
  }
100
100
 
101
101
  /**
102
- * Ensure npm is installed, attempting to install Node.js via available system
103
- * package managers when missing.
104
- *
105
- * No-op when npm is already on PATH.
106
- */
107
-
108
-
109
- async function installNodeViaFnm(quiet: boolean): Promise<boolean> {
110
- const inherit = !quiet;
111
- // Install fnm if not present.
112
- if (!Bun.which("fnm")) {
113
- let installed = false;
114
- // macOS: prefer Homebrew
115
- if (process.platform === "darwin" && Bun.which("brew")) {
116
- const brew = await runCommand(
117
- [Bun.which("brew")!, "install", "fnm"],
118
- { inherit },
119
- );
120
- installed = brew.success;
121
- }
122
- // Windows: prefer winget
123
- if (!installed && process.platform === "win32" && Bun.which("winget")) {
124
- const winget = await runCommand(
125
- [Bun.which("winget")!, "install", "Schniz.fnm"],
126
- { inherit },
127
- );
128
- if (winget.success) {
129
- // Refresh PATH — winget installs to a location on the user PATH.
130
- const userPath = process.env.LOCALAPPDATA
131
- ? join(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links")
132
- : null;
133
- if (userPath) prependPath(userPath);
134
- }
135
- installed = winget.success;
136
- }
137
- // Linux / fallback: use the curl installer (requires a shell)
138
- if (!installed) {
139
- const shell = Bun.which("bash") ?? Bun.which("sh");
140
- if (!shell) return false;
141
-
142
- const curl = await runCommand(
143
- [shell, "-lc", "curl -fsSL https://fnm.vercel.app/install | bash -s -- --skip-shell"],
144
- { inherit },
145
- );
146
- if (!curl.success) return false;
147
-
148
- // Add fnm to PATH for the current session.
149
- const home = getHomeDir() ?? "/tmp";
150
- const fnmDir = process.env.FNM_DIR ?? join(home, ".local", "share", "fnm");
151
- prependPath(fnmDir);
152
- // Some systems install to ~/.fnm instead
153
- prependPath(join(home, ".fnm"));
154
- }
155
- }
156
-
157
- const fnmPath = Bun.which("fnm");
158
- if (!fnmPath) return false;
159
-
160
- // Install LTS Node.js via fnm.
161
- const fnmInstall = await runCommand(
162
- [fnmPath, "install", "--lts"],
163
- { inherit },
164
- );
165
- if (!fnmInstall.success) return false;
166
-
167
- // Activate the installed version by adding its bin dir to PATH.
168
- const envShell = process.platform === "win32" ? "cmd" : "bash";
169
- const envProc = Bun.spawn({
170
- cmd: [fnmPath, "env", "--shell", envShell],
171
- stdout: "pipe",
172
- stderr: "pipe",
173
- });
174
- const [envOutput, envExitCode] = await Promise.all([
175
- new Response(envProc.stdout).text(),
176
- envProc.exited,
177
- ]);
178
- if (envExitCode === 0) {
179
- if (process.platform === "win32") {
180
- // cmd output: SET "PATH=C:\...\fnm_multishells\...;..."
181
- const pathMatch = envOutput.match(/SET "PATH=([^"]+?)"/i);
182
- if (pathMatch?.[1]) {
183
- const firstEntry = pathMatch[1].split(";")[0];
184
- if (firstEntry) prependPath(firstEntry);
185
- }
186
- } else {
187
- // bash output: export PATH="/.../fnm_multishells/...:..."
188
- const pathMatch = envOutput.match(/export PATH="([^"]+?):/);
189
- if (pathMatch?.[1]) {
190
- prependPath(pathMatch[1]);
191
- }
192
- }
193
- }
194
-
195
- return !!Bun.which("node");
196
- }
197
-
198
- export async function ensureNpmInstalled(options: EnsureOptions = {}): Promise<void> {
199
- const quiet = options.quiet ?? false;
200
- const inherit = !quiet;
201
-
202
- if (Bun.which("npm")) {
203
- return;
204
- }
205
-
206
- // Buffer captured failure output so a thrown error can surface the tail
207
- // through the spinner summary. Only populated when `quiet` is set.
208
- let capturedDetails = "";
209
- const record = (result: SpawnResult) => {
210
- if (quiet && !result.success && result.details) {
211
- capturedDetails = result.details;
212
- }
213
- };
214
-
215
- // Preferred: install via fnm (no root required, works on all platforms).
216
- if (await installNodeViaFnm(quiet)) {
217
- return;
218
- }
219
-
220
- if (process.platform === "win32") {
221
- // Fallback: direct Node.js installation via Windows package managers.
222
- if (Bun.which("winget")) {
223
- record(
224
- await runCommand(
225
- [
226
- "winget",
227
- "install",
228
- "--id",
229
- "OpenJS.NodeJS.LTS",
230
- "-e",
231
- "--silent",
232
- "--accept-source-agreements",
233
- "--accept-package-agreements",
234
- ],
235
- { inherit },
236
- ),
237
- );
238
- } else if (Bun.which("choco")) {
239
- record(
240
- await runCommand(
241
- ["choco", "install", "nodejs-lts", "-y", "--no-progress"],
242
- { inherit },
243
- ),
244
- );
245
- } else if (Bun.which("scoop")) {
246
- record(
247
- await runCommand(["scoop", "install", "nodejs-lts"], { inherit }),
248
- );
249
- }
250
-
251
- const programFiles = process.env.ProgramFiles;
252
- if (programFiles) {
253
- prependPath(join(programFiles, "nodejs"));
254
- }
255
- if (Bun.which("npm")) return;
256
- throw new Error(
257
- capturedDetails || "Could not install Node.js on Windows (no supported package manager found).",
258
- );
259
- }
260
-
261
- const shell = Bun.which("bash") ?? Bun.which("sh");
262
- if (!shell) {
263
- throw new Error("Neither bash nor sh is available to install Node.js.");
264
- }
265
-
266
- // Fallback: Homebrew, NodeSource, then system package managers.
267
- const installers = [
268
- 'if command -v brew >/dev/null 2>&1; then brew install node && brew link --overwrite node 2>/dev/null; fi',
269
- 'if command -v apt-get >/dev/null 2>&1; then SUDO=""; [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1 && SUDO="sudo"; $SUDO apt-get update && $SUDO apt-get install -y nodejs npm; fi',
270
- 'if command -v dnf >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo dnf install -y nodejs npm; elif [ "$(id -u)" -eq 0 ]; then dnf install -y nodejs npm; fi; fi',
271
- 'if command -v yum >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo yum install -y nodejs npm; elif [ "$(id -u)" -eq 0 ]; then yum install -y nodejs npm; fi; fi',
272
- 'if command -v pacman >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo pacman -Sy --noconfirm nodejs npm; elif [ "$(id -u)" -eq 0 ]; then pacman -Sy --noconfirm nodejs npm; fi; fi',
273
- 'if command -v zypper >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo zypper --non-interactive install nodejs npm; elif [ "$(id -u)" -eq 0 ]; then zypper --non-interactive install nodejs npm; fi; fi',
274
- 'if command -v apk >/dev/null 2>&1; then if command -v sudo >/dev/null 2>&1; then sudo apk add --no-cache nodejs npm; elif [ "$(id -u)" -eq 0 ]; then apk add --no-cache nodejs npm; fi; fi',
275
- ];
276
-
277
- for (const script of installers) {
278
- if (Bun.which("npm")) {
279
- return;
280
- }
281
- record(await runCommand([shell, "-lc", script], { inherit }));
282
- if (Bun.which("npm")) {
283
- return;
284
- }
285
- }
286
-
287
- throw new Error(
288
- capturedDetails || "Could not install Node.js — no supported package manager succeeded.",
289
- );
290
- }
291
-
292
- /**
293
- * Upgrade npm to the latest version.
294
- * Falls back to installing Node.js/npm if it is not yet present.
295
- */
296
- export async function upgradeNpm(): Promise<void> {
297
- const npmPath = Bun.which("npm");
298
- if (!npmPath) {
299
- await ensureNpmInstalled();
300
- return;
301
- }
302
- const result = await runCommand([npmPath, "install", "-g", "npm@latest"]);
303
- if (!result.success) {
304
- const hint =
305
- result.details?.includes("EACCES") || result.details?.includes("permission")
306
- ? "\nIf this is a permissions issue, try: sudo npm install -g npm@latest"
307
- : "";
308
- throw new Error(`npm self-upgrade failed: ${result.details}${hint}`);
309
- }
310
- }
311
-
312
- /**
313
- * Upgrade a global npm package to the latest version.
102
+ * Install a global package via bun. Uses `--trust` to allow postinstall
103
+ * lifecycle scripts (required by packages like @playwright/cli).
314
104
  */
315
105
  export async function upgradeGlobalPackage(pkg: string): Promise<void> {
316
106
  const versionedPkg = pkg.includes("@latest") ? pkg : `${pkg}@latest`;
317
- const npmPath = Bun.which("npm");
318
- if (npmPath) {
319
- const result = await runCommand([npmPath, "install", "-g", versionedPkg]);
320
- if (result.success) return;
321
- throw new Error(`Failed to upgrade ${pkg}: npm: ${result.details}`);
107
+ const bunPath = Bun.which("bun");
108
+ if (!bunPath) {
109
+ throw new Error(`bun is not available to install ${pkg}.`);
110
+ }
111
+ const result = await runCommand([bunPath, "install", "-g", "--trust", versionedPkg]);
112
+ if (!result.success) {
113
+ throw new Error(`Failed to install ${pkg}: ${result.details}`);
322
114
  }
323
- throw new Error(`npm is not available to upgrade ${pkg}.`);
324
115
  }
325
116
 
326
117
  /** Upgrade @playwright/cli to the latest version globally. */
@@ -455,11 +246,6 @@ export async function ensureBunInstalled(): Promise<void> {
455
246
  if (result.success && Bun.which("bun")) return;
456
247
  }
457
248
 
458
- const npmPath = Bun.which("npm");
459
- if (npmPath) {
460
- const result = await runCommand([npmPath, "install", "-g", "bun"], { inherit: true });
461
- if (result.success && Bun.which("bun")) return;
462
- }
463
249
  return;
464
250
  }
465
251
 
@@ -756,86 +756,4 @@ describe("PanelStore", () => {
756
756
  });
757
757
  });
758
758
 
759
- // ── getSubagents ───────────────────────────────────────────────────────────
760
-
761
- describe("getSubagents", () => {
762
- beforeEach(() => {
763
- store.setWorkflowInfo("wf", "claude", [
764
- { name: "planner", parents: [] },
765
- { name: "writer", parents: ["planner"] },
766
- { name: "reviewer", parents: ["writer"] },
767
- ], "prompt");
768
- });
769
-
770
- test("returns empty when all non-orchestrator sessions are pending", () => {
771
- expect(store.getSubagents()).toEqual([]);
772
- });
773
-
774
- test("excludes orchestrator from subagent list", () => {
775
- store.startSession("planner");
776
- const subs = store.getSubagents();
777
- expect(subs.every((s) => s.name !== "orchestrator")).toBe(true);
778
- });
779
-
780
- test("includes running and completed sessions", () => {
781
- store.startSession("planner");
782
- store.completeSession("planner");
783
- store.startSession("writer");
784
- const subs = store.getSubagents();
785
- expect(subs.map((s) => s.name)).toEqual(["planner", "writer"]);
786
- });
787
-
788
- test("includes errored sessions", () => {
789
- store.startSession("planner");
790
- store.failSession("planner", "timeout");
791
- const subs = store.getSubagents();
792
- expect(subs.map((s) => s.name)).toEqual(["planner"]);
793
- });
794
-
795
- test("excludes pending sessions", () => {
796
- store.startSession("planner");
797
- const subs = store.getSubagents();
798
- expect(subs.map((s) => s.name)).toEqual(["planner"]);
799
- expect(subs.some((s) => s.name === "writer")).toBe(false);
800
- expect(subs.some((s) => s.name === "reviewer")).toBe(false);
801
- });
802
- });
803
-
804
- // ── getActiveAgentIndex ────────────────────────────────────────────────────
805
-
806
- describe("getActiveAgentIndex", () => {
807
- beforeEach(() => {
808
- store.setWorkflowInfo("wf", "claude", [
809
- { name: "planner", parents: [] },
810
- { name: "writer", parents: ["planner"] },
811
- { name: "reviewer", parents: ["writer"] },
812
- ], "prompt");
813
- store.startSession("planner");
814
- store.startSession("writer");
815
- });
816
-
817
- test("returns -1 when no agent is active", () => {
818
- expect(store.getActiveAgentIndex()).toBe(-1);
819
- });
820
-
821
- test("returns correct index for first subagent", () => {
822
- store.setViewMode("attached", "planner");
823
- expect(store.getActiveAgentIndex()).toBe(0);
824
- });
825
-
826
- test("returns correct index for second subagent", () => {
827
- store.setViewMode("attached", "writer");
828
- expect(store.getActiveAgentIndex()).toBe(1);
829
- });
830
-
831
- test("returns -1 for orchestrator (not a subagent)", () => {
832
- store.setViewMode("attached", "orchestrator");
833
- expect(store.getActiveAgentIndex()).toBe(-1);
834
- });
835
-
836
- test("returns -1 for non-existent agent", () => {
837
- store.setViewMode("attached", "nonexistent");
838
- expect(store.getActiveAgentIndex()).toBe(-1);
839
- });
840
- });
841
759
  });
@@ -124,25 +124,6 @@ export class PanelStore {
124
124
  this.emit();
125
125
  }
126
126
 
127
- /**
128
- * Return non-orchestrator agents that have started (not pending).
129
- * Used for the tmux status bar agent count and active-agent index.
130
- */
131
- getSubagents(): SessionData[] {
132
- return this.sessions.filter(
133
- (s) => s.name !== "orchestrator" && s.status !== "pending",
134
- );
135
- }
136
-
137
- /**
138
- * Return the 0-based index of the active agent within the subagent list,
139
- * or -1 if not found.
140
- */
141
- getActiveAgentIndex(): number {
142
- const subs = this.getSubagents();
143
- return subs.findIndex((s) => s.name === this.activeAgentId);
144
- }
145
-
146
127
  /** Safely invoke exitResolve at most once, guarding against rapid repeated calls. */
147
128
  resolveExit(): void {
148
129
  if (this.exitResolve) {
@@ -20,11 +20,15 @@ import {
20
20
  } from "react";
21
21
  import {
22
22
  tmuxRun,
23
- escapeTmuxFormat,
24
23
  TMUX_DEFAULT_STATUS_LEFT,
25
24
  TMUX_DEFAULT_STATUS_LEFT_LENGTH,
26
25
  TMUX_DEFAULT_STATUS_RIGHT,
27
26
  TMUX_DEFAULT_STATUS_RIGHT_LENGTH,
27
+ TMUX_ATTACHED_STATUS_RIGHT,
28
+ TMUX_ATTACHED_STATUS_RIGHT_LENGTH,
29
+ TMUX_ATTACHED_WINDOW_FMT,
30
+ TMUX_ATTACHED_WINDOW_STYLE,
31
+ TMUX_ATTACHED_WINDOW_CURRENT_STYLE,
28
32
  } from "../runtime/tmux.ts";
29
33
  import {
30
34
  useStore,
@@ -353,52 +357,69 @@ export function SessionGraphPanel() {
353
357
  }
354
358
  }, [focusedId, focused, termW, termH, padX, padY, viewportH, layout.rowH]);
355
359
 
356
- // ── Detect return to graph via Ctrl+G ─────────────────
357
- // Ctrl+G is bound at the tmux level (select-window -t :0), so tmux
358
- // swallows the key and the React app never receives it. Poll the
359
- // active window index while attached; when window 0 becomes active
360
- // again we know the user returned and can reset viewMode immediately.
360
+ // ── Track active tmux window ──────────────────────────
361
+ // Ctrl+G and Ctrl+\ are bound at the tmux level, so the React app
362
+ // never receives them. Poll the active window to sync viewMode
363
+ // with tmux-level navigation in both directions.
364
+ const hasStartedAgent = useMemo(
365
+ () => store.sessions.some((s) => s.name !== "orchestrator" && s.status !== "pending"),
366
+ [storeVersion],
367
+ );
368
+
361
369
  useEffect(() => {
362
- if (store.viewMode !== "attached") return;
370
+ if (!hasStartedAgent) return;
363
371
 
364
372
  const check = () => {
365
373
  const result = tmuxRun([
366
- "display-message", "-t", tmuxSession, "-p", "#{window_index}",
374
+ "display-message", "-t", tmuxSession, "-p", "#{window_index} #{window_name}",
367
375
  ]);
368
- if (result.ok && result.stdout.trim() === "0") {
369
- store.setViewMode("graph");
376
+ if (!result.ok) return;
377
+
378
+ const output = result.stdout.trim();
379
+ const spaceIdx = output.indexOf(" ");
380
+ const idx = spaceIdx >= 0 ? output.slice(0, spaceIdx) : output;
381
+ const windowName = spaceIdx >= 0 ? output.slice(spaceIdx + 1) : "";
382
+
383
+ if (idx === "0") {
384
+ if (store.viewMode !== "graph") {
385
+ store.setViewMode("graph");
386
+ }
387
+ } else if (store.viewMode !== "attached" || store.activeAgentId !== windowName) {
388
+ store.setViewMode("attached", windowName);
370
389
  }
371
390
  };
372
391
 
373
- const id = setInterval(check, 300);
392
+ const id = setInterval(check, 500);
374
393
  return () => clearInterval(id);
375
- }, [store.viewMode, tmuxSession]);
394
+ }, [tmuxSession, hasStartedAgent]);
376
395
 
377
396
  // ── Tmux status bar sync ──────────────────────────────
378
- // When attached, the orchestrator panel is hidden (user views the agent's
379
- // tmux window). Mirror the status line hints into tmux's own status bar
380
- // so navigation keys remain discoverable.
381
- const subagentCount = store.getSubagents().length;
382
- const activeAgentIdx = store.getActiveAgentIndex();
383
-
397
+ // Attached mode: use tmux's native window list to show agent names
398
+ // (the current window is highlighted automatically by tmux).
399
+ // Graph mode: restore the minimal defaults.
384
400
  useEffect(() => {
385
- if (store.viewMode === "attached" && store.activeAgentId) {
386
- const safeName = escapeTmuxFormat(store.activeAgentId);
387
- const left = `#[bg=#6c7086,fg=#1e1e2e,bold] ATTACHED #[default] #[fg=#7f849c]\u203a #[fg=#cdd6f4]${safeName} #[fg=#7f849c]${activeAgentIdx + 1}/${subagentCount}`;
388
- const right = `#[fg=#7f849c]Graph: #[fg=#cdd6f4]ctrl+g #[fg=#7f849c]| Next: #[fg=#cdd6f4]ctrl+\\ `;
389
-
390
- tmuxRun(["set", "-g", "status-left", left]);
391
- tmuxRun(["set", "-g", "status-left-length", "50"]);
392
- tmuxRun(["set", "-g", "status-right", right]);
393
- tmuxRun(["set", "-g", "status-right-length", "40"]);
401
+ if (store.viewMode === "attached") {
402
+ tmuxRun(["set", "-g", "status-left", " "]);
403
+ tmuxRun(["set", "-g", "status-left-length", "1"]);
404
+ tmuxRun(["set", "-g", "status-right", TMUX_ATTACHED_STATUS_RIGHT]);
405
+ tmuxRun(["set", "-g", "status-right-length", TMUX_ATTACHED_STATUS_RIGHT_LENGTH]);
406
+ tmuxRun(["set", "-g", "window-status-format", TMUX_ATTACHED_WINDOW_FMT]);
407
+ tmuxRun(["set", "-g", "window-status-current-format", TMUX_ATTACHED_WINDOW_FMT]);
408
+ tmuxRun(["set", "-g", "window-status-style", TMUX_ATTACHED_WINDOW_STYLE]);
409
+ tmuxRun(["set", "-g", "window-status-current-style", TMUX_ATTACHED_WINDOW_CURRENT_STYLE]);
410
+ tmuxRun(["set", "-g", "window-status-separator", ""]);
394
411
  } else {
395
- // Graph mode: restore defaults (constants from tmux.ts match tmux.conf)
396
412
  tmuxRun(["set", "-g", "status-left", TMUX_DEFAULT_STATUS_LEFT]);
397
413
  tmuxRun(["set", "-g", "status-left-length", TMUX_DEFAULT_STATUS_LEFT_LENGTH]);
398
414
  tmuxRun(["set", "-g", "status-right", TMUX_DEFAULT_STATUS_RIGHT]);
399
415
  tmuxRun(["set", "-g", "status-right-length", TMUX_DEFAULT_STATUS_RIGHT_LENGTH]);
416
+ tmuxRun(["set", "-gu", "window-status-format"]);
417
+ tmuxRun(["set", "-gu", "window-status-current-format"]);
418
+ tmuxRun(["set", "-gu", "window-status-style"]);
419
+ tmuxRun(["set", "-gu", "window-status-current-style"]);
420
+ tmuxRun(["set", "-gu", "window-status-separator"]);
400
421
  }
401
- }, [store.viewMode, store.activeAgentId, activeAgentIdx, subagentCount]);
422
+ }, [store.viewMode]);
402
423
 
403
424
  // Restore default tmux status bar on unmount
404
425
  useEffect(() => {
@@ -407,6 +428,11 @@ export function SessionGraphPanel() {
407
428
  tmuxRun(["set", "-g", "status-left-length", TMUX_DEFAULT_STATUS_LEFT_LENGTH]);
408
429
  tmuxRun(["set", "-g", "status-right", TMUX_DEFAULT_STATUS_RIGHT]);
409
430
  tmuxRun(["set", "-g", "status-right-length", TMUX_DEFAULT_STATUS_RIGHT_LENGTH]);
431
+ tmuxRun(["set", "-gu", "window-status-format"]);
432
+ tmuxRun(["set", "-gu", "window-status-current-format"]);
433
+ tmuxRun(["set", "-gu", "window-status-style"]);
434
+ tmuxRun(["set", "-gu", "window-status-current-style"]);
435
+ tmuxRun(["set", "-gu", "window-status-separator"]);
410
436
  };
411
437
  }, []);
412
438
 
@@ -40,14 +40,17 @@ export const TMUX_DEFAULT_STATUS_LEFT_LENGTH = "10";
40
40
  export const TMUX_DEFAULT_STATUS_RIGHT = " #{session_name} | %H:%M ";
41
41
  export const TMUX_DEFAULT_STATUS_RIGHT_LENGTH = "60";
42
42
 
43
- /**
44
- * Escape a string for safe interpolation into tmux format strings.
45
- * Replaces `#` with `##` to prevent tmux from interpreting `#[...]`
46
- * as style directives or `#(...)` as shell command expansions.
47
- */
48
- export function escapeTmuxFormat(value: string): string {
49
- return value.replace(/#/g, "##");
50
- }
43
+ // Attached-mode status bar — agent list via tmux window list + shortcut hints.
44
+ // The window-status formats hide window 0 (orchestrator) and style agent names.
45
+ // tmux natively highlights the current window, so no React state sync is needed
46
+ // for agent cycling via Ctrl+\.
47
+ export const TMUX_ATTACHED_STATUS_RIGHT =
48
+ "#[fg=#cdd6f4]ctrl+g #[fg=#6c7086]graph #[fg=#585b70]\u00b7 #[fg=#cdd6f4]ctrl+\\ #[fg=#6c7086]next ";
49
+ export const TMUX_ATTACHED_STATUS_RIGHT_LENGTH = "40";
50
+ export const TMUX_ATTACHED_WINDOW_FMT =
51
+ "#{?#{==:#{window_index},0},, #W }";
52
+ export const TMUX_ATTACHED_WINDOW_STYLE = "fg=#6c7086";
53
+ export const TMUX_ATTACHED_WINDOW_CURRENT_STYLE = "fg=#cdd6f4,bold";
51
54
 
52
55
  // ---------------------------------------------------------------------------
53
56
  // Core tmux primitives
@@ -11,19 +11,18 @@
11
11
  * comparing the bundled `VERSION` constant against a marker file at
12
12
  * `~/.atomic/.synced-version`. On a mismatch we run the same setup the
13
13
  * production bootstrap installers (`install.sh` / `install.ps1`) provide,
14
- * grouped into two parallel phases:
14
+ * as a single parallel phase:
15
15
  *
16
- * Phase 1 (parallel no inter-dependencies):
17
- * 1. Node.js / npm (installed via fnm, no system pkg-mgr)
18
- * 2. tmux / psmux (terminal multiplexer for `chat` / `workflow`)
19
- * 3. global agent configs (file copies — no network)
16
+ * 1. tmux / psmux (terminal multiplexer for `chat` / `workflow`)
17
+ * 2. global agent configs (file copies no network)
18
+ * 3. @playwright/cli (bun install -g)
19
+ * 4. @llamaindex/liteparse (bun install -g)
20
+ * 5. global skills (bunx skills add ...)
20
21
  *
21
- * Phase 2 (parallel all need npm from Phase 1):
22
- * 4. @playwright/cli (npm install -g)
23
- * 5. @llamaindex/liteparse (npm install -g)
24
- * 6. global skills (npx skills add ...)
22
+ * All steps run concurrently using bun (already our runtime) for package
23
+ * installs and `bunx` for CLI tools, avoiding a ~48 s Node.js/npm
24
+ * download via fnm that previously gated Phase 2.
25
25
  *
26
- * Steps within each phase run concurrently; phases run sequentially.
27
26
  * Failures are collected and reported as a summary at the end, but never
28
27
  * abort the run — partial setup matches the production installer's
29
28
  * "best-effort" semantics. The marker is written after every run (success
@@ -36,7 +35,6 @@ import { homedir } from "node:os";
36
35
  import { VERSION } from "../../version.ts";
37
36
  import { COLORS } from "../../theme/colors.ts";
38
37
  import {
39
- ensureNpmInstalled,
40
38
  ensureTmuxInstalled,
41
39
  upgradePlaywrightCli,
42
40
  upgradeLiteparse,
@@ -93,33 +91,21 @@ export async function autoSyncIfStale(): Promise<void> {
93
91
  `\n ${COLORS.dim}Setting up atomic ${COLORS.reset}${COLORS.bold}v${VERSION}${COLORS.reset}${COLORS.dim}…${COLORS.reset}`,
94
92
  );
95
93
 
96
- // Steps are split into two parallel phases:
97
- //
98
- // Phase 1core tools + file copies (no inter-dependencies):
99
- // npm is installed via fnm (not a system package manager), so it
100
- // won't contend with tmux's apt-get/dnf install. Agent config
101
- // copies are pure file I/O with no network or npm dependency.
102
- //
103
- // Phase 2 — npm-dependent tasks (run after Phase 1):
104
- // @playwright/cli, @llamaindex/liteparse, and `npx skills` all
105
- // need npm/npx. They install independent packages, so they can
106
- // run concurrently.
94
+ // All steps run in a single parallel phase. bun (already our runtime)
95
+ // handles global package installs and `bunx` execution, so there is no
96
+ // need to install Node.js/npm first eliminating a ~48 s fnm download
97
+ // that previously dominated the loading screen.
107
98
  //
108
99
  // Each step's failure is caught inside `runSteps` (not thrown), so
109
100
  // subsequent steps still run even if one fails — matches install.sh's
110
101
  // best-effort contract.
111
102
  const results = await runSteps([
112
- // Phase 1 — parallel
113
103
  [
114
- { label: "Node.js / npm", fn: () => ensureNpmInstalled({ quiet: true }) },
115
104
  { label: "tmux / psmux", fn: () => ensureTmuxInstalled({ quiet: true }) },
116
105
  { label: "global agent configs", fn: installGlobalAgents },
117
- ],
118
- // Phase 2 — parallel, after Phase 1
119
- [
120
- { label: "@playwright/cli", fn: upgradePlaywrightCli },
106
+ { label: "@playwright/cli", fn: upgradePlaywrightCli },
121
107
  { label: "@llamaindex/liteparse", fn: upgradeLiteparse },
122
- { label: "global skills", fn: installGlobalSkills },
108
+ { label: "global skills", fn: installGlobalSkills },
123
109
  ],
124
110
  ]);
125
111
 
@@ -11,10 +11,9 @@
11
11
  * that falls back gracefully through 256-color → basic ANSI.
12
12
  *
13
13
  * Steps are grouped into **phases**. Steps within a phase run in parallel
14
- * (via `Promise.all`); phases themselves run sequentially so later phases
15
- * can depend on earlier ones (e.g. npm must be available before
16
- * `npm install -g` tasks). The progress bar advances and the label
17
- * updates in real-time as individual steps complete within a phase.
14
+ * (via `Promise.all`); phases themselves run sequentially. The progress
15
+ * bar advances and the label updates in real-time as individual steps
16
+ * complete within a phase.
18
17
  *
19
18
  * A final summary (✓/✗ per step) is printed after all steps finish, and
20
19
  * any captured stderr/stdout from a failed step is shown beneath it.
@@ -18,15 +18,20 @@ interface NpxSkillsResult {
18
18
  }
19
19
 
20
20
  async function runNpxSkills(args: string[]): Promise<NpxSkillsResult> {
21
- const npxPath = Bun.which("npx");
22
- if (!npxPath) {
23
- return { ok: false, details: "npx not found on PATH" };
21
+ // Prefer bunx (already available as our runtime) over npx to avoid
22
+ // depending on a full Node.js/npm installation.
23
+ const runner = Bun.which("bunx") ?? Bun.which("npx");
24
+ if (!runner) {
25
+ return { ok: false, details: "neither bunx nor npx found on PATH" };
24
26
  }
25
27
 
26
28
  // Capture stdout/stderr so the outer spinner UI owns terminal output and
27
- // can surface the tail of any failure. `npx skills add` is otherwise
28
- // very chatty.
29
- const proc = Bun.spawn([npxPath, "--yes", "skills", ...args], {
29
+ // can surface the tail of any failure.
30
+ const isBunx = runner.endsWith("bunx");
31
+ const cmd = isBunx
32
+ ? [runner, "skills", ...args]
33
+ : [runner, "--yes", "skills", ...args];
34
+ const proc = Bun.spawn(cmd, {
30
35
  stdout: "pipe",
31
36
  stderr: "pipe",
32
37
  });