@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.
- package/.agents/skills/workflow-creator/SKILL.md +110 -1
- package/.agents/skills/workflow-creator/references/workflow-inputs.md +10 -0
- package/.mcp.json +9 -0
- package/.opencode/opencode.json +5 -2
- package/README.md +394 -645
- package/assets/settings.schema.json +0 -20
- package/dist/sdk/components/attached-statusline.d.ts +13 -0
- package/dist/sdk/components/attached-statusline.d.ts.map +1 -0
- package/dist/sdk/components/header.d.ts.map +1 -1
- package/dist/sdk/components/session-graph-panel.d.ts.map +1 -1
- package/dist/sdk/components/statusline.d.ts +1 -3
- package/dist/sdk/components/statusline.d.ts.map +1 -1
- package/dist/sdk/providers/claude.d.ts +16 -5
- package/dist/sdk/providers/claude.d.ts.map +1 -1
- package/dist/sdk/runtime/executor.d.ts +63 -0
- package/dist/sdk/runtime/executor.d.ts.map +1 -1
- package/dist/sdk/runtime/tmux.d.ts +0 -9
- package/dist/sdk/runtime/tmux.d.ts.map +1 -1
- package/dist/services/config/atomic-config.d.ts +1 -7
- package/dist/services/config/atomic-config.d.ts.map +1 -1
- package/dist/services/config/definitions.d.ts +0 -45
- package/dist/services/config/definitions.d.ts.map +1 -1
- package/dist/services/config/index.d.ts +1 -1
- package/dist/theme/colors.d.ts +33 -0
- package/dist/theme/colors.d.ts.map +1 -0
- package/package.json +3 -2
- package/src/cli.ts +16 -1
- package/src/commands/cli/chat/index.ts +1 -1
- package/src/commands/cli/footer.tsx +118 -0
- package/src/commands/cli/init/index.ts +6 -89
- package/src/commands/cli/workflow-command.test.ts +146 -0
- package/src/commands/cli/workflow.ts +43 -7
- package/src/completions/bash.ts +3 -8
- package/src/completions/fish.ts +1 -3
- package/src/completions/powershell.ts +1 -17
- package/src/completions/zsh.ts +0 -2
- package/src/scripts/bundle-configs.ts +0 -12
- package/src/sdk/components/attached-statusline.tsx +33 -0
- package/src/sdk/components/header.tsx +16 -2
- package/src/sdk/components/session-graph-panel.tsx +10 -51
- package/src/sdk/components/statusline.tsx +0 -17
- package/src/sdk/providers/claude.ts +179 -177
- package/src/sdk/runtime/executor-entry.ts +3 -1
- package/src/sdk/runtime/executor.test.ts +292 -1
- package/src/sdk/runtime/executor.ts +222 -1
- package/src/sdk/runtime/tmux.conf +35 -4
- package/src/sdk/runtime/tmux.ts +0 -22
- package/src/services/config/atomic-config.ts +1 -14
- package/src/services/config/definitions.ts +1 -102
- package/src/services/config/index.ts +1 -1
- package/src/services/config/settings.ts +2 -65
- package/src/services/system/skills.ts +2 -19
- 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
|
-
*
|
|
5
|
-
*
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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(
|
|
250
|
+
if (!validAgents.includes(agentInput)) {
|
|
223
251
|
console.error(
|
|
224
|
-
`${COLORS.red}Error: Unknown agent '${
|
|
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 =
|
|
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
|
|
package/src/completions/bash.ts
CHANGED
|
@@ -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|-
|
|
23
|
-
-*)
|
|
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 -
|
|
49
|
+
COMPREPLY=( $(compgen -W "-a --agent -h --help" -- "$cur") )
|
|
55
50
|
;;
|
|
56
51
|
chat)
|
|
57
52
|
if [[ -z "$cmd2" ]]; then
|
package/src/completions/fish.ts
CHANGED
|
@@ -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 -
|
|
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|-
|
|
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' {
|
package/src/completions/zsh.ts
CHANGED
|
@@ -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 {
|
|
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} />
|