@bastani/atomic 0.5.18 → 0.5.19

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 (53) hide show
  1. package/.agents/skills/workflow-creator/SKILL.md +110 -1
  2. package/.agents/skills/workflow-creator/references/workflow-inputs.md +10 -0
  3. package/.mcp.json +9 -0
  4. package/.opencode/opencode.json +5 -2
  5. package/README.md +394 -645
  6. package/assets/settings.schema.json +0 -20
  7. package/dist/sdk/components/attached-statusline.d.ts +13 -0
  8. package/dist/sdk/components/attached-statusline.d.ts.map +1 -0
  9. package/dist/sdk/components/header.d.ts.map +1 -1
  10. package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
  11. package/dist/sdk/components/statusline.d.ts +1 -3
  12. package/dist/sdk/components/statusline.d.ts.map +1 -1
  13. package/dist/sdk/providers/claude.d.ts +16 -5
  14. package/dist/sdk/providers/claude.d.ts.map +1 -1
  15. package/dist/sdk/runtime/executor.d.ts +63 -0
  16. package/dist/sdk/runtime/executor.d.ts.map +1 -1
  17. package/dist/sdk/runtime/tmux.d.ts +0 -9
  18. package/dist/sdk/runtime/tmux.d.ts.map +1 -1
  19. package/dist/services/config/atomic-config.d.ts +1 -7
  20. package/dist/services/config/atomic-config.d.ts.map +1 -1
  21. package/dist/services/config/definitions.d.ts +0 -45
  22. package/dist/services/config/definitions.d.ts.map +1 -1
  23. package/dist/services/config/index.d.ts +1 -1
  24. package/dist/theme/colors.d.ts +33 -0
  25. package/dist/theme/colors.d.ts.map +1 -0
  26. package/package.json +3 -2
  27. package/src/cli.ts +16 -1
  28. package/src/commands/cli/chat/index.ts +1 -1
  29. package/src/commands/cli/footer.tsx +118 -0
  30. package/src/commands/cli/init/index.ts +6 -89
  31. package/src/commands/cli/workflow-command.test.ts +146 -0
  32. package/src/commands/cli/workflow.ts +43 -7
  33. package/src/completions/bash.ts +3 -8
  34. package/src/completions/fish.ts +1 -3
  35. package/src/completions/powershell.ts +1 -17
  36. package/src/completions/zsh.ts +0 -2
  37. package/src/scripts/bundle-configs.ts +0 -12
  38. package/src/sdk/components/attached-statusline.tsx +33 -0
  39. package/src/sdk/components/header.tsx +16 -2
  40. package/src/sdk/components/session-graph-panel.tsx +10 -51
  41. package/src/sdk/components/statusline.tsx +0 -17
  42. package/src/sdk/providers/claude.ts +179 -177
  43. package/src/sdk/runtime/executor-entry.ts +3 -1
  44. package/src/sdk/runtime/executor.test.ts +292 -1
  45. package/src/sdk/runtime/executor.ts +222 -1
  46. package/src/sdk/runtime/tmux.conf +35 -4
  47. package/src/sdk/runtime/tmux.ts +0 -22
  48. package/src/services/config/atomic-config.ts +1 -14
  49. package/src/services/config/definitions.ts +1 -102
  50. package/src/services/config/index.ts +1 -1
  51. package/src/services/config/settings.ts +2 -65
  52. package/src/services/system/skills.ts +2 -19
  53. package/src/commands/cli/init/scm.ts +0 -175
@@ -1,106 +1,23 @@
1
1
  /**
2
2
  * Automatic project setup — replaces the interactive `atomic init` command.
3
3
  *
4
- * Detects the repo's SCM, applies onboarding files (MCP configs, settings),
5
- * registers the workspace as trusted, and installs SCM-specific skills.
6
- *
7
- * Called transparently during `atomic chat` preflight so users never need
8
- * to think about initialization.
4
+ * Applies onboarding files (MCP configs, settings). Called transparently
5
+ * during `atomic chat` preflight so users never need to think about
6
+ * initialization.
9
7
  */
10
8
 
11
- import { join, resolve } from "node:path";
12
- import {
13
- AGENT_CONFIG,
14
- type AgentKey,
15
- type SourceControlType,
16
- SCM_SKILLS_BY_TYPE,
17
- detectScmType,
18
- } from "../../../services/config/index.ts";
19
- import { pathExists } from "../../../services/system/copy.ts";
9
+ import type { AgentKey } from "../../../services/config/index.ts";
20
10
  import { getConfigRoot } from "../../../services/config/config-path.ts";
21
- import { getTemplateAgentFolder } from "../../../services/config/atomic-global-config.ts";
22
- import { upsertTrustedWorkspacePath } from "../../../services/config/settings.ts";
23
11
  import { applyManagedOnboardingFiles } from "./onboarding.ts";
24
- import { installLocalScmSkills, syncProjectScmSkills } from "./scm.ts";
25
12
 
26
13
  /**
27
- * Check whether all expected SCM skills are already present on disk.
28
- */
29
- async function areScmSkillsInstalled(
30
- agentKey: AgentKey,
31
- projectRoot: string,
32
- scmType: SourceControlType,
33
- ): Promise<boolean> {
34
- const skillNames = SCM_SKILLS_BY_TYPE[scmType];
35
- const skillsDir = join(projectRoot, AGENT_CONFIG[agentKey].folder, "skills");
36
-
37
- for (const name of skillNames) {
38
- if (!(await pathExists(join(skillsDir, name)))) {
39
- return false;
40
- }
41
- }
42
- return true;
43
- }
44
-
45
- function isInstalledPackage(): boolean {
46
- return import.meta.dir.includes("node_modules");
47
- }
48
-
49
- /**
50
- * Ensure the project is configured for the given agent.
51
- *
52
- * Idempotent — safe to call on every `atomic chat` invocation. Expensive
53
- * operations (skill installation via `bunx skills add`) are skipped when
54
- * the skills are already present on disk. Onboarding file merges are
55
- * always applied since they are cheap and self-healing.
56
- *
57
- * Errors in skill installation are swallowed so the agent can still launch.
14
+ * Ensure the project is configured for the given agent. Idempotent — safe
15
+ * to call on every `atomic chat` invocation.
58
16
  */
59
17
  export async function ensureProjectSetup(
60
18
  agentKey: AgentKey,
61
19
  projectRoot: string,
62
20
  ): Promise<void> {
63
21
  const configRoot = getConfigRoot();
64
- const detectedScm = await detectScmType(projectRoot);
65
-
66
- // Apply onboarding files (idempotent merge, SCM-gated entries handled internally)
67
22
  await applyManagedOnboardingFiles(agentKey, projectRoot, configRoot);
68
-
69
- // Register trusted workspace
70
- await upsertTrustedWorkspacePath(resolve(projectRoot), agentKey);
71
-
72
- // Install SCM skills if detected and not yet present (best-effort)
73
- if (detectedScm) {
74
- try {
75
- const alreadyInstalled = await areScmSkillsInstalled(
76
- agentKey,
77
- projectRoot,
78
- detectedScm,
79
- );
80
- if (!alreadyInstalled) {
81
- if (isInstalledPackage()) {
82
- // npm/bunx install: fetch via the skills CLI
83
- await installLocalScmSkills({
84
- scmType: detectedScm,
85
- agentKey,
86
- cwd: projectRoot,
87
- });
88
- } else {
89
- // Source checkout: copy from bundled templates
90
- const templateFolder = getTemplateAgentFolder(agentKey);
91
- await syncProjectScmSkills({
92
- scmType: detectedScm,
93
- sourceSkillsDir: join(configRoot, templateFolder, "skills"),
94
- targetSkillsDir: join(
95
- projectRoot,
96
- AGENT_CONFIG[agentKey].folder,
97
- "skills",
98
- ),
99
- });
100
- }
101
- }
102
- } catch {
103
- // Skills installation is best-effort — don't block the agent launch
104
- }
105
- }
106
23
  }
@@ -174,13 +174,19 @@ function captureOutput(): CapturedOutput {
174
174
  // so the env var must already be set by the time workflow.ts gets imported.
175
175
 
176
176
  let originalNoColor: string | undefined;
177
+ let originalAtomicAgent: string | undefined;
177
178
  beforeAll(() => {
178
179
  originalNoColor = process.env.NO_COLOR;
179
180
  process.env.NO_COLOR = "1";
181
+ // Snapshot once so tests can freely set/unset ATOMIC_AGENT without
182
+ // leaking into unrelated suites in the same bun-test process.
183
+ originalAtomicAgent = process.env.ATOMIC_AGENT;
180
184
  });
181
185
  afterAll(() => {
182
186
  if (originalNoColor === undefined) delete process.env.NO_COLOR;
183
187
  else process.env.NO_COLOR = originalNoColor;
188
+ if (originalAtomicAgent === undefined) delete process.env.ATOMIC_AGENT;
189
+ else process.env.ATOMIC_AGENT = originalAtomicAgent;
184
190
  });
185
191
 
186
192
  // ─── Temp workspace plumbing ────────────────────────────────────────────────
@@ -192,6 +198,12 @@ let tempDir: string;
192
198
 
193
199
  beforeEach(async () => {
194
200
  tempDir = await mkdtemp(join(tmpdir(), "atomic-workflow-cmd-test-"));
201
+ // Clear ATOMIC_AGENT by default — `workflowCommand` falls back to this
202
+ // env var when `-a` is omitted, and we don't want the ambient env (e.g.
203
+ // a developer running tests from inside an atomic chat pane) to silently
204
+ // change the agent any given test sees. Tests that need it explicitly
205
+ // set it themselves.
206
+ delete process.env.ATOMIC_AGENT;
195
207
  // Reset every mock to its default pass-through / no-op so tests are
196
208
  // independent — no leftover state from prior overrides. `mockClear` wipes
197
209
  // call history; `mockImplementation` replaces the queued implementation
@@ -634,6 +646,41 @@ describe("workflowCommand named-mode success paths", () => {
634
646
  expect(executeWorkflowMock.mock.calls[0]![0].inputs).toEqual({});
635
647
  });
636
648
 
649
+ test("detach flag is threaded through to the executor", async () => {
650
+ await writeCompiledWorkflow({ name: "detached", agent: "copilot" });
651
+
652
+ const cap = captureOutput();
653
+ const code = await workflowCommand({
654
+ name: "detached",
655
+ agent: "copilot",
656
+ detach: true,
657
+ passthroughArgs: ["run", "in", "bg"],
658
+ cwd: tempDir,
659
+ });
660
+ cap.restore();
661
+
662
+ expect(code).toBe(0);
663
+ expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
664
+ expect(executeWorkflowMock.mock.calls[0]![0].detach).toBe(true);
665
+ });
666
+
667
+ test("detach defaults to false when not provided", async () => {
668
+ await writeCompiledWorkflow({ name: "default-attach", agent: "copilot" });
669
+
670
+ const cap = captureOutput();
671
+ const code = await workflowCommand({
672
+ name: "default-attach",
673
+ agent: "copilot",
674
+ passthroughArgs: [],
675
+ cwd: tempDir,
676
+ });
677
+ cap.restore();
678
+
679
+ expect(code).toBe(0);
680
+ expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
681
+ expect(executeWorkflowMock.mock.calls[0]![0].detach).toBe(false);
682
+ });
683
+
637
684
  test("structured workflow resolves flags and calls executor with merged inputs", async () => {
638
685
  await writeCompiledWorkflow({
639
686
  name: "struct-run",
@@ -714,6 +761,105 @@ export default defineWorkflow({
714
761
  });
715
762
  });
716
763
 
764
+ // ─── ATOMIC_AGENT env var inference ────────────────────────────────────────
765
+
766
+ describe("workflowCommand ATOMIC_AGENT inference", () => {
767
+ // Top-level beforeEach already clears ATOMIC_AGENT; tests that need it
768
+ // set it explicitly and rely on the next test's clear to reset.
769
+
770
+ test("infers -a from ATOMIC_AGENT when omitted", async () => {
771
+ // Agents spawned inside an atomic chat/workflow pane inherit
772
+ // ATOMIC_AGENT. Re-passing their own provider back through `-a` is
773
+ // boilerplate we can eliminate.
774
+ await writeCompiledWorkflow({ name: "inferred", agent: "claude" });
775
+ process.env.ATOMIC_AGENT = "claude";
776
+
777
+ const cap = captureOutput();
778
+ const code = await workflowCommand({
779
+ name: "inferred",
780
+ // no agent passed
781
+ passthroughArgs: ["go"],
782
+ cwd: tempDir,
783
+ });
784
+ cap.restore();
785
+
786
+ expect(code).toBe(0);
787
+ expect(executeWorkflowMock).toHaveBeenCalledTimes(1);
788
+ expect(executeWorkflowMock.mock.calls[0]![0].agent).toBe("claude");
789
+ });
790
+
791
+ test("forces detach=true when ATOMIC_AGENT is set", async () => {
792
+ // Attaching from inside the atomic socket would switch-client the
793
+ // caller's own terminal onto the new workflow session — hijacking the
794
+ // very pane the agent is running in. Force detach so the command
795
+ // returns immediately and the caller can attach on their own terms.
796
+ await writeCompiledWorkflow({ name: "auto-detach", agent: "copilot" });
797
+ process.env.ATOMIC_AGENT = "copilot";
798
+
799
+ const cap = captureOutput();
800
+ const code = await workflowCommand({
801
+ name: "auto-detach",
802
+ agent: "copilot",
803
+ // detach intentionally omitted
804
+ passthroughArgs: [],
805
+ cwd: tempDir,
806
+ });
807
+ cap.restore();
808
+
809
+ expect(code).toBe(0);
810
+ expect(executeWorkflowMock.mock.calls[0]![0].detach).toBe(true);
811
+ });
812
+
813
+ test("explicit -a wins over ATOMIC_AGENT", async () => {
814
+ // Users running on Claude who want to invoke a Copilot workflow must
815
+ // be able to override — the env var is a fallback, not a pin.
816
+ await writeCompiledWorkflow({ name: "override", agent: "copilot" });
817
+ process.env.ATOMIC_AGENT = "claude";
818
+
819
+ const cap = captureOutput();
820
+ const code = await workflowCommand({
821
+ name: "override",
822
+ agent: "copilot",
823
+ passthroughArgs: [],
824
+ cwd: tempDir,
825
+ });
826
+ cap.restore();
827
+
828
+ expect(code).toBe(0);
829
+ expect(executeWorkflowMock.mock.calls[0]![0].agent).toBe("copilot");
830
+ });
831
+
832
+ test("no ATOMIC_AGENT + no -a still errors", async () => {
833
+ // Baseline: outside an atomic session, `-a` is still required.
834
+ const cap = captureOutput();
835
+ const code = await workflowCommand({
836
+ name: "anything",
837
+ cwd: tempDir,
838
+ });
839
+ cap.restore();
840
+
841
+ expect(code).toBe(1);
842
+ expect(cap.stderr).toContain("Missing agent");
843
+ });
844
+
845
+ test("empty ATOMIC_AGENT is treated as unset", async () => {
846
+ // Shells sometimes export empty strings; don't let that poison the
847
+ // agent fallback with an empty value that fails validation with a
848
+ // misleading "unknown agent ''" message.
849
+ process.env.ATOMIC_AGENT = "";
850
+
851
+ const cap = captureOutput();
852
+ const code = await workflowCommand({
853
+ name: "anything",
854
+ cwd: tempDir,
855
+ });
856
+ cap.restore();
857
+
858
+ expect(code).toBe(1);
859
+ expect(cap.stderr).toContain("Missing agent");
860
+ });
861
+ });
862
+
717
863
  // ─── Prereq checks (runPrereqChecks) ───────────────────────────────────────
718
864
 
719
865
  describe("workflowCommand prereq checks", () => {
@@ -6,6 +6,7 @@
6
6
  * atomic workflow -n <name> -a <agent> <prompt> free-form workflow
7
7
  * atomic workflow -n <name> -a <agent> --<field>=<value> ...
8
8
  * structured-input workflow
9
+ * atomic workflow -n <name> -a <agent> -d <args> run detached (background)
9
10
  * atomic workflow list [-a <agent>] list discoverable workflows
10
11
  */
11
12
 
@@ -174,6 +175,12 @@ export async function workflowCommand(options: {
174
175
  name?: string;
175
176
  agent?: string;
176
177
  list?: boolean;
178
+ /**
179
+ * When true, create the tmux session and return immediately without
180
+ * attaching. Callers can use `atomic workflow session connect <id>`
181
+ * to attach later. Useful for scripting / background automation.
182
+ */
183
+ detach?: boolean;
177
184
  /**
178
185
  * Everything commander parked in `cmd.args` — a mix of positional
179
186
  * prompt tokens and unknown `--<name>` flags that the
@@ -191,6 +198,23 @@ export async function workflowCommand(options: {
191
198
  const passthroughArgs = options.passthroughArgs ?? [];
192
199
  const cwd = options.cwd;
193
200
 
201
+ // `ATOMIC_AGENT` is set by `atomic chat` and `atomic workflow` on the tmux
202
+ // session they create, so every pane in that session inherits it. Its
203
+ // presence is a reliable signal that this command is being invoked from
204
+ // inside an atomic-managed session (i.e., the caller is an agent running
205
+ // in a chat or a workflow pane rather than a plain shell). We use that in
206
+ // two places:
207
+ // 1. If `-a` was omitted, fall back to ATOMIC_AGENT so agents don't have
208
+ // to pass their own provider back to themselves.
209
+ // 2. Force `detach = true`, because attaching from inside the atomic
210
+ // socket would `switch-client` the caller's own terminal onto the new
211
+ // session — i.e., the agent would hijack the user's view. Detach is
212
+ // always the safe choice here; the CLI prints attach hints so the
213
+ // user can switch to the workflow whenever they want.
214
+ const atomicAgentEnv = process.env.ATOMIC_AGENT;
215
+ const insideAtomicSession = atomicAgentEnv !== undefined && atomicAgentEnv !== "";
216
+ const detach = insideAtomicSession ? true : (options.detach ?? false);
217
+
194
218
  // ── List mode ──
195
219
  // `merge: false` keeps local and global entries independent so the
196
220
  // list can show both copies of a non-reserved name when they coexist
@@ -211,7 +235,11 @@ export async function workflowCommand(options: {
211
235
  }
212
236
 
213
237
  // ── Agent validation (required for every non-list branch) ──
214
- if (!options.agent) {
238
+ // Explicit `-a` wins; otherwise fall back to the ATOMIC_AGENT env var so
239
+ // workflows launched from inside an atomic chat/workflow session don't
240
+ // need to re-specify the provider they're already running under.
241
+ const agentInput = options.agent ?? atomicAgentEnv;
242
+ if (!agentInput) {
215
243
  console.error(
216
244
  `${COLORS.red}Error: Missing agent. Use -a <agent>.${COLORS.reset}`,
217
245
  );
@@ -219,14 +247,14 @@ export async function workflowCommand(options: {
219
247
  }
220
248
 
221
249
  const validAgents = Object.keys(AGENT_CONFIG);
222
- if (!validAgents.includes(options.agent)) {
250
+ if (!validAgents.includes(agentInput)) {
223
251
  console.error(
224
- `${COLORS.red}Error: Unknown agent '${options.agent}'.${COLORS.reset}`,
252
+ `${COLORS.red}Error: Unknown agent '${agentInput}'.${COLORS.reset}`,
225
253
  );
226
254
  console.error(`Valid agents: ${validAgents.join(", ")}`);
227
255
  return 1;
228
256
  }
229
- const agent = options.agent as AgentKey;
257
+ const agent = agentInput as AgentKey;
230
258
 
231
259
  // ── Preflight checks (shared between picker and named modes) ──
232
260
  const preflightCode = await runPrereqChecks(agent);
@@ -234,11 +262,11 @@ export async function workflowCommand(options: {
234
262
 
235
263
  // ── Picker mode: -a <agent>, no -n ──
236
264
  if (!options.name) {
237
- return runPickerMode(agent, passthroughArgs, cwd);
265
+ return runPickerMode(agent, passthroughArgs, cwd, detach);
238
266
  }
239
267
 
240
268
  // ── Named mode: -n <name> -a <agent> [args...] ──
241
- return runNamedMode(options.name, agent, passthroughArgs, cwd);
269
+ return runNamedMode(options.name, agent, passthroughArgs, cwd, detach);
242
270
  }
243
271
 
244
272
  // ─── Shared helpers ─────────────────────────────────────────────────────────
@@ -313,6 +341,7 @@ async function runLoadedWorkflow(args: {
313
341
  agent: AgentKey;
314
342
  inputs: Record<string, string>;
315
343
  workflowFile: string;
344
+ detach: boolean;
316
345
  }): Promise<number> {
317
346
  try {
318
347
  await executeWorkflow({
@@ -320,6 +349,7 @@ async function runLoadedWorkflow(args: {
320
349
  agent: args.agent,
321
350
  inputs: args.inputs,
322
351
  workflowFile: args.workflowFile,
352
+ detach: args.detach,
323
353
  });
324
354
  return 0;
325
355
  } catch (error) {
@@ -341,6 +371,7 @@ async function runPickerMode(
341
371
  agent: AgentKey,
342
372
  passthroughArgs: string[],
343
373
  cwd: string | undefined,
374
+ detach: boolean,
344
375
  ): Promise<number> {
345
376
  if (passthroughArgs.length > 0) {
346
377
  console.error(
@@ -386,7 +417,7 @@ async function runPickerMode(
386
417
  return 0;
387
418
  }
388
419
 
389
- return runResolvedSelection(result.workflow, agent, result.inputs);
420
+ return runResolvedSelection(result.workflow, agent, result.inputs, detach);
390
421
  }
391
422
 
392
423
  /**
@@ -399,6 +430,7 @@ async function runResolvedSelection(
399
430
  workflow: WorkflowWithMetadata,
400
431
  agent: AgentKey,
401
432
  inputs: Record<string, string>,
433
+ detach: boolean,
402
434
  ): Promise<number> {
403
435
  const loaded = await WorkflowLoader.loadWorkflow(workflow, {
404
436
  warn(warnings) {
@@ -417,6 +449,7 @@ async function runResolvedSelection(
417
449
  agent,
418
450
  inputs,
419
451
  workflowFile: workflow.path,
452
+ detach,
420
453
  });
421
454
  }
422
455
 
@@ -427,6 +460,7 @@ async function runNamedMode(
427
460
  agent: AgentKey,
428
461
  passthroughArgs: string[],
429
462
  cwd: string | undefined,
463
+ detach: boolean,
430
464
  ): Promise<number> {
431
465
  // Find the workflow
432
466
  const discovered = await findWorkflow(name, agent, cwd);
@@ -517,6 +551,7 @@ async function runNamedMode(
517
551
  agent,
518
552
  inputs: resolvedInputs,
519
553
  workflowFile: discovered.path,
554
+ detach,
520
555
  });
521
556
  }
522
557
 
@@ -543,6 +578,7 @@ async function runNamedMode(
543
578
  agent,
544
579
  inputs,
545
580
  workflowFile: discovered.path,
581
+ detach,
546
582
  });
547
583
  }
548
584
 
@@ -10,7 +10,6 @@ _atomic_completions() {
10
10
 
11
11
  local commands="init chat workflow session config completions"
12
12
  local agents="claude opencode copilot"
13
- local scms="github sapling"
14
13
  local global_opts="-y --yes --no-banner -v --version -h --help"
15
14
 
16
15
  # Walk the words to find the command chain (skip flags and their values)
@@ -19,8 +18,8 @@ _atomic_completions() {
19
18
  while [[ $i -lt $cword ]]; do
20
19
  local w="\${words[$i]}"
21
20
  case "$w" in
22
- -a|--agent|-s|--scm|-n|--name) (( i++ )) ;; # skip flag value
23
- -*) ;; # skip other flags
21
+ -a|--agent|-n|--name) (( i++ )) ;; # skip flag value
22
+ -*) ;; # skip other flags
24
23
  *)
25
24
  if [[ -z "$cmd1" ]]; then cmd1="$w"
26
25
  elif [[ -z "$cmd2" ]]; then cmd2="$w"
@@ -37,10 +36,6 @@ _atomic_completions() {
37
36
  COMPREPLY=( $(compgen -W "$agents" -- "$cur") )
38
37
  return
39
38
  ;;
40
- -s|--scm)
41
- COMPREPLY=( $(compgen -W "$scms" -- "$cur") )
42
- return
43
- ;;
44
39
  esac
45
40
 
46
41
  # Top-level (no subcommand yet)
@@ -51,7 +46,7 @@ _atomic_completions() {
51
46
 
52
47
  case "$cmd1" in
53
48
  init)
54
- COMPREPLY=( $(compgen -W "-a --agent -s --scm -h --help" -- "$cur") )
49
+ COMPREPLY=( $(compgen -W "-a --agent -h --help" -- "$cur") )
55
50
  ;;
56
51
  chat)
57
52
  if [[ -z "$cmd2" ]]; then
@@ -11,7 +11,6 @@ complete -c atomic -f
11
11
  # ── Helpers ─────────────────────────────────────────────────────────────────
12
12
 
13
13
  set -l agents claude opencode copilot
14
- set -l scms github sapling
15
14
 
16
15
  # Condition helpers — true when the command line matches a specific depth.
17
16
  # "__fish_seen_subcommand_from X" is true once token X has appeared.
@@ -36,7 +35,7 @@ function __atomic_using_cmd
36
35
  set idx (math $idx + 1)
37
36
  # Skip flag value for known value-flags
38
37
  switch $tokens[(math $idx - 1)]
39
- case -a --agent -s --scm -n --name
38
+ case -a --agent -n --name
40
39
  set idx (math $idx + 1)
41
40
  end
42
41
  case '*'
@@ -78,7 +77,6 @@ complete -c atomic -n __atomic_no_subcommand -a completions -d 'Output shell com
78
77
  # ── init ────────────────────────────────────────────────────────────────────
79
78
 
80
79
  complete -c atomic -n '__atomic_using_cmd init' -s a -l agent -d 'Agent to configure' -r -a "$agents"
81
- complete -c atomic -n '__atomic_using_cmd init' -s s -l scm -d 'Source control system' -r -a "$scms"
82
80
 
83
81
  # ── chat ────────────────────────────────────────────────────────────────────
84
82
 
@@ -12,7 +12,6 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
12
12
  Where-Object { $_ -ne '' }
13
13
 
14
14
  $agents = @('claude', 'opencode', 'copilot')
15
- $scms = @('github', 'sapling')
16
15
  $shells = @('bash', 'zsh', 'fish', 'powershell')
17
16
 
18
17
  # Parse command chain, skipping flags and their values
@@ -23,7 +22,7 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
23
22
  $t = $tokens[$i]
24
23
  if ($skipNext) { $skipNext = $false; continue }
25
24
  if ($t -match '^-') {
26
- if ($t -match '^(-a|--agent|-s|--scm|-n|--name)$') { $skipNext = $true }
25
+ if ($t -match '^(-a|--agent|-n|--name)$') { $skipNext = $true }
27
26
  $prevToken = $t
28
27
  continue
29
28
  }
@@ -47,19 +46,6 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
47
46
  }
48
47
  return
49
48
  }
50
- if ($prevFullToken -match '^(-s|--scm)$' -or $lastToken -match '^(-s|--scm)$') {
51
- if ($lastToken -match '^(-s|--scm)$') {
52
- $scms | ForEach-Object {
53
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
54
- }
55
- return
56
- }
57
- $scms | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
58
- [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
59
- }
60
- return
61
- }
62
-
63
49
  $completions = @()
64
50
 
65
51
  switch ($cmds.Count) {
@@ -80,8 +66,6 @@ Register-ArgumentCompleter -Native -CommandName atomic -ScriptBlock {
80
66
  $completions = @(
81
67
  @{ text = '-a'; tip = 'Agent to configure' }
82
68
  @{ text = '--agent'; tip = 'Agent to configure' }
83
- @{ text = '-s'; tip = 'Source control system' }
84
- @{ text = '--scm'; tip = 'Source control system' }
85
69
  )
86
70
  }
87
71
  'chat' {
@@ -8,7 +8,6 @@ export const zshCompletionScript = `
8
8
 
9
9
  _atomic() {
10
10
  local -a agents=('claude' 'opencode' 'copilot')
11
- local -a scms=('github' 'sapling')
12
11
 
13
12
  _arguments -C \\
14
13
  '(-y --yes)'{-y,--yes}'[Auto-confirm all prompts]' \\
@@ -35,7 +34,6 @@ _atomic() {
35
34
  init)
36
35
  _arguments \\
37
36
  '(-a --agent)'{-a,--agent}'[Agent to configure]:agent:(claude opencode copilot)' \\
38
- '(-s --scm)'{-s,--scm}'[Source control system]:scm:(github sapling)' \\
39
37
  '(-h --help)'{-h,--help}'[Show help]'
40
38
  ;;
41
39
  chat)
@@ -25,14 +25,6 @@ const HOME = homedir();
25
25
  /** Source repo for the global skills install. */
26
26
  const SKILLS_REPO = "https://github.com/flora131/atomic.git";
27
27
 
28
- /** Skills excluded from the global install (VCS-specific commit/PR helpers). */
29
- const EXCLUDED_SKILLS = [
30
- "gh-commit",
31
- "gh-create-pr",
32
- "sl-commit",
33
- "sl-submit-diff",
34
- ];
35
-
36
28
  /** Agent CLI flags accepted by `bunx skills`. */
37
29
  const AGENT_FLAGS = ["claude-code", "opencode", "github-copilot"];
38
30
 
@@ -62,11 +54,7 @@ async function installGlobalSkills(): Promise<void> {
62
54
  console.log("Installing global skills…");
63
55
 
64
56
  const agentArgs = AGENT_FLAGS.flatMap((a) => ["-a", a]);
65
-
66
57
  await $`bunx skills add ${SKILLS_REPO} --skill "*" -g ${agentArgs} -y`;
67
-
68
- const removeArgs = EXCLUDED_SKILLS.flatMap((s) => ["--skill", s]);
69
- await $`bunx skills remove ${removeArgs} -g ${agentArgs} -y`;
70
58
  }
71
59
 
72
60
  async function copyBundledAgents(): Promise<void> {
@@ -0,0 +1,33 @@
1
+ /** @jsxImportSource @opentui/react */
2
+ /**
3
+ * Footer rendered inside each agent tmux window. Lives in a 1-row bottom
4
+ * pane created by the executor after the agent window is spawned. Mirrors
5
+ * the orchestrator Statusline style — colored badge on the left, dimmed
6
+ * keyboard hints on the right.
7
+ */
8
+
9
+ import type { GraphTheme } from "./graph-theme.ts";
10
+
11
+ export function AttachedStatusline({ name, theme }: { name: string; theme: GraphTheme }) {
12
+ return (
13
+ <box height={1} flexDirection="row" backgroundColor={theme.backgroundElement}>
14
+ <box backgroundColor={theme.primary} paddingLeft={1} paddingRight={1} alignItems="center">
15
+ <text fg={theme.backgroundElement}>
16
+ <strong>{name}</strong>
17
+ </text>
18
+ </box>
19
+
20
+ <box flexGrow={1} />
21
+
22
+ <box paddingRight={2} alignItems="center">
23
+ <text>
24
+ <span fg={theme.text}>ctrl+g</span>
25
+ <span fg={theme.textMuted}> graph</span>
26
+ <span fg={theme.textDim}> {"\u00B7"} </span>
27
+ <span fg={theme.text}>{"ctrl+\\"}</span>
28
+ <span fg={theme.textMuted}> next</span>
29
+ </text>
30
+ </box>
31
+ </box>
32
+ );
33
+ }
@@ -1,8 +1,13 @@
1
1
  /** @jsxImportSource @opentui/react */
2
2
 
3
- import { useMemo } from "react";
3
+ import { useContext, useMemo } from "react";
4
4
  import type { SessionStatus } from "./orchestrator-panel-types.ts";
5
- import { useStore, useGraphTheme, useStoreVersion } from "./orchestrator-panel-contexts.ts";
5
+ import {
6
+ useStore,
7
+ useGraphTheme,
8
+ useStoreVersion,
9
+ TmuxSessionContext,
10
+ } from "./orchestrator-panel-contexts.ts";
6
11
 
7
12
  function CountBadge({ color, icon, count }: { color: string; icon: string; count: number }) {
8
13
  if (count <= 0) return null;
@@ -16,6 +21,7 @@ function CountBadge({ color, icon, count }: { color: string; icon: string; count
16
21
  export function Header() {
17
22
  const store = useStore();
18
23
  const theme = useGraphTheme();
24
+ const tmuxSession = useContext(TmuxSessionContext);
19
25
  const storeVersion = useStoreVersion(store);
20
26
 
21
27
  const counts = useMemo(() => {
@@ -47,6 +53,14 @@ export function Header() {
47
53
  </span>
48
54
  </text>
49
55
 
56
+ {tmuxSession ? (
57
+ <box paddingLeft={1} alignItems="center">
58
+ <text fg={theme.text}>
59
+ <strong>{tmuxSession}</strong>
60
+ </text>
61
+ </box>
62
+ ) : null}
63
+
50
64
  <box flexGrow={1} justifyContent="flex-end" flexDirection="row" gap={2}>
51
65
  <CountBadge color={theme.success} icon={"\u2713"} count={counts.complete} />
52
66
  <CountBadge color={theme.warning} icon={"\u25CF"} count={counts.running} />