xcoder-origin 0.0.0-stage → 1.4.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neural Node Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,120 @@
1
- # Temporary Holding Version
1
+ <!-- xcoder:version 2 | xcoder:task task-691585 | xcoder:updated 2026-11-05T05:49:47.429Z | xcoder:subtask code-st-b5251b -->
2
+ # Xcoder SDLC
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ A **SDLC-aware, ReAct-driven multi-agent CLI.** One Master
5
+ Orchestrator, a swarm of role-mapped Sub Agents, and a workspace index as
6
+ the LLM's source of truth for what files actually exist. It classifies
7
+ your task into a full SDLC pipeline, dispatches concurrent agents,
8
+ validates every deliverable independently, and heals or escalates on
9
+ failure — all without ever crashing on a subordinate's error.
10
+
11
+ ## Quick start
12
+
13
+ ```bash
14
+ # Prerequisites: Node.js ≥ 20 and an Anthropic API key
15
+ export ANTHROPIC_API_KEY=sk-ant-...
16
+
17
+ # Install and run
18
+ npm install -g xcoder-sdlc-cli
19
+ xcoder --help
20
+
21
+ # Or, clone and build (for contributors)
22
+ git clone https://github.com/org/xcoder-sdlc-cli.git && cd xcoder-sdlc-cli
23
+ npm install && npm run build && npm link
24
+ xcoder write code for a JWT auth endpoint
25
+
26
+ # unlink
27
+ npm uninstall -g xcoder-sdlc-cli
28
+ ```
29
+
30
+ Xcoder SDLC is a pure command-line tool: there is no web UI, HTTP
31
+ server or WebSocket endpoint, so nothing listens on a network port.
32
+
33
+ Configure the LLM once with `xcoder --config "provider=deepseek,key=<your-key>"` (saved to `~/.xcoder/config.json`).
34
+
35
+ > **[SETUP.md](docs/SETUP.md)** covers two installation paths (npm global
36
+ > and clone-and-build) with verification steps and environment variable
37
+ > configuration.
38
+
39
+ ## Agent CLIs
40
+
41
+ The aggregator routes each top-level command name to the agent that owns it: `xcoder` and `ba-v1` to the SDLC agent, `bugfinder` to the QA agent, `litebro` to the litebro planner, `samurai-v1` / `genesis-v1` / `worker-v1` to the persona CLIs, and `jarvis` to jarvis.
42
+
43
+ `jarvis` is delegated: `xcoder jarvis [args...]` runs the jarvis binary in a child process and forwards every argument unchanged, so `xcoder jarvis --chat` is an ordinary jarvis chat session. Exit codes are jarvis's own, plus `127` when the binary is missing and `130` when the run was interrupted. See [DELEGATION.md](https://github.com/org/xcoder-sdlc/blob/main/docs/DELEGATION.md).
44
+
45
+ ## Feature highlights
46
+
47
+ - **[Business Analyst one-shot](./docs/USAGE.md#business-analyst-one-shot)** —
48
+ `xcoder --ba` accepts three input modes (file, directory, or bare title)
49
+ and produces a structured technical requirements document with numbered
50
+ acceptance criteria.
51
+ - **[CLI with crash isolation and healing](./docs/USAGE.md#crash-isolation)** —
52
+ Sub Agent failures never take down the orchestrator. Bounded healing
53
+ retries failures; exhausted attempts escalate clearly so you can resume
54
+ later.
55
+ - **[Bounded benchmarking](./docs/USAGE.md#benchmarking)** — `xcoder bench` runs
56
+ real fixture sets through the live orchestrator and model, producing
57
+ per-fixture scores and comparative reports.
58
+ - **[Sprint framework with quality gates](./docs/USAGE.md#sprint-framework)** —
59
+ `xcoder sprint start` builds a DAG pipeline interleaved with Scrum
60
+ Master, System Architect, and Security Lead quality gates that can block
61
+ the sprint before downstream stages waste tokens.
62
+ - **[QA agent — requirement-to-coverage testing](./docs/QA_AGENT.md#4-usage)** —
63
+ author-owned `REQ-*` requirements become Playwright specs and are gated;
64
+ a requirement with no passing tagged test fails the run.
65
+
66
+ ## Architecture
67
+
68
+ ```
69
+ xcoder -task "..."
70
+ │
71
+ ▼
72
+ ┌─────────────────┐ ┌──────────────────┐
73
+ │ Task Factory │────►│ SDLC Orchestrator │
74
+ │ (classifies │ │ (DAG execution, │
75
+ │ intake, builds │ │ validate, heal, │
76
+ │ initial DAG) │ │ escalate) │
77
+ └─────────────────┘ └────────┬───────────┘
78
+ │ ready nodes
79
+ ┌────────▼───────────┐
80
+ │ Swarm dispatcher │
81
+ │ (N concurrent Sub │
82
+ │ Agents, isolated) │
83
+ └────────┬───────────┘
84
+ │
85
+ ┌────────────────────┼────────────────────┐
86
+ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐
87
+ │Sub Agent│ │Sub Agent│ │Sub Agent│
88
+ │(react │ │(react │ │(react │
89
+ │ loop) │ │ loop) │ │ loop) │
90
+ └────┬────┘ └────┬────┘ └────┬────┘
91
+ └────────────────────┼────────────────────┘
92
+ ┌────────▼───────────┐
93
+ │ Tool Registry │
94
+ │ + Workspace Index │
95
+ └────────────────────┘
96
+ ```
97
+
98
+ → **[blueprint.md](./docs/blueprint.md)** for the full architecture reference:
99
+ component decomposition, ReAct protocol contract, SDLC stage pipeline,
100
+ Instance Swarm protocol, data flow, technology stack, tradeoffs, and
101
+ internals (logging, benchmarking, scoring, validation, healing,
102
+ credentials, environment awareness).
103
+
104
+ ## Documentation
105
+
106
+ | Document | Audience | Contents |
107
+ |------------------------------------------------------|---|---|
108
+ | **[USAGE.md](./docs/USAGE.md)** | Users with Xcoder SDLC installed | Full CLI flag reference, workspace/backlog/sprint commands, benchmarking, BA one-shot, user-facing behaviour. |
109
+ | **[SETUP.md](./docs/SETUP.md)** | Someone installing from scratch | Prerequisites, two installation paths, environment variables, verification, optional Playwright setup. |
110
+ | **[blueprint.md](./docs/blueprint.md)** | Contributors and developers | Architecture diagram, component decomposition (16+ src/ components), ReAct contract, SDLC pipeline, swarm protocol, data flow, tech stack, tradeoffs, internals. |
111
+ | **[TESTING.md](./docs/TESTING.md)** | Contributors & developers | How to run narrow test subsets for fast iteration during development. |
112
+ | **[SDLC_ORCHESTRATOR_REFERENCE.md](./docs/SDLC_ORCHESTRATOR_REFERENCE.md)** | Maintainers repairing or comparing Xcoder versions | Implemented process, flow, rules, validation, recovery limits, and behavior preservation checklist. |
113
+ | **[SECURE_MODE.md](./docs/SECURE_MODE.md)** | Security reviewers & maintainers | Prompt-injection threat model, defence-in-depth diagram, per-component verdicts, residual risk. |
114
+ | **[PROMPT_INJECTION_HARDENING.md](./docs/PROMPT_INJECTION_HARDENING.md)** | Contributors adding an agent, tool or LLM call site | Integration guide: resolve secure mode, envelope untrusted text, screen input, guard tool calls, redact logs, checklist. |
115
+ | **[other-clis.md](./docs/other-clis.md)** | Users running ba / samurai / genesis / worker | Per-binary synopsis, options with defaults, exit codes, env vars, examples. |
116
+ | **[QA_AGENT.md](./docs/QA_AGENT.md)** | Users and contributors using or extending the QA agent | Five-stage pipeline, module map, stage data contracts, CLI + library usage, exit codes, failure classes, bounded autonomy, security posture, troubleshooting. |
117
+
118
+ ## License
119
+
120
+ see [LICENSE](./LICENSE) for details.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env node
2
+ // xcoder:version 2 | xcoder:task task-762833 | xcoder:updated 2026-11-05T05:21:29.498Z | xcoder:subtask code-st-bb4289
3
+ import "dotenv/config";
4
+ import { Command } from "commander";
5
+ import path from "node:path";
6
+ import { GenesisReActOrchestrator } from "xcoder-dna";
7
+ import { getOrBuildIndex } from "xcoder-dna";
8
+ import { log } from "xcoder-dna";
9
+ import { maybePrintBanner } from "xcoder-dna";
10
+ import { createSpinner } from "xcoder-dna";
11
+ import { enableProtocolErrorLogging } from "xcoder-dna";
12
+ import { enableToolErrorLogging } from "xcoder-dna";
13
+ import { XCODER_VERSION } from "xcoder-dna";
14
+ import * as readline from "node:readline";
15
+ // ---------------------------------------------------------------------------
16
+ // Colour helpers — same conventions as XcoderCLIController + design doc
17
+ // ---------------------------------------------------------------------------
18
+ const COLOURS = {
19
+ reset: "\x1b[0m",
20
+ dim: "\x1b[2m",
21
+ magenta: "\x1b[35m",
22
+ yellow: "\x1b[33m",
23
+ cyan: "\x1b[36m",
24
+ green: "\x1b[32m",
25
+ red: "\x1b[31m",
26
+ };
27
+ function useColor() {
28
+ return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
29
+ }
30
+ function paint(code, text) {
31
+ return useColor() ? `${code}${text}${COLOURS.reset}` : text;
32
+ }
33
+ function dim(s) {
34
+ return paint(COLOURS.dim, s);
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // Spinner manager
38
+ // ---------------------------------------------------------------------------
39
+ let spinner = null;
40
+ function ensureSpinner() {
41
+ if (!spinner) {
42
+ spinner = createSpinner("Genesis thinking…");
43
+ }
44
+ return spinner;
45
+ }
46
+ function stopSpinner(msg) {
47
+ if (spinner) {
48
+ spinner.stop(msg ?? "");
49
+ spinner = null;
50
+ }
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // CLI
54
+ // ---------------------------------------------------------------------------
55
+ const program = new Command();
56
+ program
57
+ .name("genesis")
58
+ .description("Genesis — a single-agent ReAct engine that can call an Instance Swarm to parallelize complex work. " +
59
+ "One goal, one agent, optional swarm.")
60
+ .version(XCODER_VERSION)
61
+ .argument("<goal>", "The goal / task for Genesis to accomplish")
62
+ .option("--model <name>", "LLM model name", process.env.LLM_MODEL || "gpt-4o")
63
+ .option("--max-tokens <n>", "Max completion tokens", (v) => parseInt(v, 10), parseInt(process.env.LLM_MAX_TOKENS || "4096", 10))
64
+ .option("--workspace <dir>", "Workspace root directory", process.cwd())
65
+ .option("--skills <list>", "Comma-separated skill labels to inject", "")
66
+ .option("--no-color", "Disable colour output", false)
67
+ .option("--lesson-learned", "Enable record_lesson tool and inject .xcoder/lesson.md context", false)
68
+ .option("--log-output <path>", "enable SQLite output logging for ReAct signals (also settable via XCODER_OUTPUT_DB env var)")
69
+ .action(async (goal, opts) => {
70
+ // Respect --no-color by setting NO_COLOR early
71
+ if (opts.noColor) {
72
+ process.env.NO_COLOR = "1";
73
+ }
74
+ const workspaceRoot = path.resolve(opts.workspace);
75
+ const skills = (opts.skills || "")
76
+ .split(",")
77
+ .map((s) => s.trim())
78
+ .filter(Boolean);
79
+ // Optional diagnostics from parent CLI pattern
80
+ if (process.env.PROTOCOL_ERROR_LOG)
81
+ enableProtocolErrorLogging();
82
+ if (process.env.DEBUG_TOOLS)
83
+ enableToolErrorLogging();
84
+ // Initialize OutputHandler for ReAct signal logging
85
+ const logOutputPath = opts.logOutput ||
86
+ process.env.XCODER_OUTPUT_DB ||
87
+ undefined;
88
+ const { getOutputHandler: getOH } = await import("xcoder-dna");
89
+ const outputHandler = getOH({
90
+ printToTerminal: process.env.XCODER_OUTPUT_SILENT !== "1",
91
+ logToSqlite: !!logOutputPath,
92
+ dbPath: logOutputPath,
93
+ });
94
+ // Build index so the agent sees the workspace
95
+ const index = await getOrBuildIndex(workspaceRoot);
96
+ log.dim(`Workspace indexed: ${index.entries.length} files`);
97
+ const orchestrator = new GenesisReActOrchestrator({
98
+ skills,
99
+ agentName: "genesis",
100
+ directive: "You are Genesis — a single-agent ReAct engine. Use call_swarm to parallelize complex work.",
101
+ llmOptions: {
102
+ model: opts.model,
103
+ maxTokens: opts.maxTokens,
104
+ },
105
+ lessonLearned: opts.lessonLearned,
106
+ });
107
+ // -----------------------------------------------------------------------
108
+ // Wire up real-time event → coloured output + spinner
109
+ // -----------------------------------------------------------------------
110
+ let currentState = "IDLE";
111
+ orchestrator.on("state:change", (newState) => {
112
+ currentState = newState;
113
+ switch (newState) {
114
+ case "THINKING":
115
+ ensureSpinner();
116
+ break;
117
+ case "EXECUTING_TOOL":
118
+ // spinner update happens in action:start
119
+ break;
120
+ case "COMPLETED":
121
+ stopSpinner(paint(COLOURS.green, "Done."));
122
+ break;
123
+ case "FAILED":
124
+ stopSpinner(paint(COLOURS.red, "Failed."));
125
+ break;
126
+ case "PAUSED":
127
+ stopSpinner(dim("Paused…"));
128
+ break;
129
+ }
130
+ });
131
+ orchestrator.on("thought", (thought) => {
132
+ // Print thought while spinner is running
133
+ if (spinner) {
134
+ // ora doesn't have a reliable "write above spinner" on all terminals;
135
+ // stop/restart pattern is safest across platforms.
136
+ stopSpinner();
137
+ }
138
+ log.raw(paint(COLOURS.magenta, `[Thought]`) + ` ${thought}`);
139
+ outputHandler.thought(thought);
140
+ if (currentState === "THINKING") {
141
+ ensureSpinner();
142
+ }
143
+ });
144
+ orchestrator.on("action:start", (action) => {
145
+ stopSpinner();
146
+ log.raw(paint(COLOURS.yellow, `[Action]`) +
147
+ ` ${action.tool}` +
148
+ dim(` ${JSON.stringify(action.input).slice(0, 200)}`));
149
+ outputHandler.action(action.tool, action.input);
150
+ spinner = createSpinner(`Executing ${action.tool}…`);
151
+ });
152
+ orchestrator.on("action:end", (toolName, observation) => {
153
+ stopSpinner();
154
+ const obsStr = typeof observation === "string"
155
+ ? observation
156
+ : JSON.stringify(observation, null, 0);
157
+ const truncated = obsStr.length > 800 ? obsStr.slice(0, 800) + dim("… (truncated)") : obsStr;
158
+ // Check for crash/warning patterns in observation
159
+ const isError = (typeof observation === "object" &&
160
+ observation !== null &&
161
+ observation.ok === false) ||
162
+ obsStr.includes("CRASHED");
163
+ if (isError) {
164
+ log.raw(paint(COLOURS.red, `[CRASHED]`) + ` ${toolName}: ${truncated}`);
165
+ outputHandler.error(`${toolName}: ${obsStr.slice(0, 800)}`);
166
+ }
167
+ else {
168
+ log.raw(paint(COLOURS.cyan, `[Observation]`) + ` ${truncated}`);
169
+ outputHandler.observation(obsStr.slice(0, 800));
170
+ }
171
+ // Re-enable spinner if loop continues
172
+ if (currentState === "THINKING") {
173
+ spinner = createSpinner("Genesis thinking…");
174
+ }
175
+ });
176
+ orchestrator.on("paused", () => {
177
+ stopSpinner();
178
+ log.raw(dim("── Genesis paused (Ctrl+C again to quit) ──"));
179
+ // Interactive steering: read one line of user input
180
+ const rl = readline.createInterface({
181
+ input: process.stdin,
182
+ output: process.stdout,
183
+ });
184
+ rl.question(paint(COLOURS.yellow, "Steering input") +
185
+ dim(" (press Enter to resume, type guidance to inject): "), (answer) => {
186
+ rl.close();
187
+ if (answer.trim()) {
188
+ orchestrator.injectUserInput(answer.trim());
189
+ }
190
+ else {
191
+ // Resume without injection — the orchestrator handles this
192
+ // by simply continuing the loop (interruptPending already cleared
193
+ // by injectUserInput; for a pure resume we rely on the
194
+ // orchestrator's internal handling).
195
+ orchestrator.injectUserInput("");
196
+ }
197
+ });
198
+ });
199
+ // -----------------------------------------------------------------------
200
+ // Run
201
+ // -----------------------------------------------------------------------
202
+ log.raw(dim("── Genesis starting ──") +
203
+ `\n${dim("Goal:")} ${goal}` +
204
+ `\n${dim("Model:")} ${opts.model}` +
205
+ `\n${dim("Workspace:")} ${workspaceRoot}`);
206
+ try {
207
+ const finalAnswer = await orchestrator.run(goal);
208
+ stopSpinner();
209
+ log.raw("");
210
+ log.raw(paint(COLOURS.green, "[final_answer]") + ` ${finalAnswer}`);
211
+ log.ok("Genesis completed successfully.");
212
+ }
213
+ catch (e) {
214
+ stopSpinner();
215
+ log.fail(`Genesis crashed: ${e.message}`);
216
+ log.raw(paint(COLOURS.red, "[CRASHED]") +
217
+ dim(" — Genesis agent threw unexpectedly. This should not happen; check the error above."));
218
+ process.exitCode = 1;
219
+ }
220
+ });
221
+ maybePrintBanner(process.argv);
222
+ program.parse();
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
3
+ /**
4
+ * Read package.json from workspace root if present, extract name/version/
5
+ * scripts/dependencies, and format as a context block injected into the
6
+ * system prompt. Non-fatal if absent.
7
+ */
8
+ export declare function readPackageJsonContext(root: string): Promise<string | null>;
@@ -0,0 +1,434 @@
1
+ #!/usr/bin/env node
2
+ // xcoder:version 3 | xcoder:task task-762833 | xcoder:updated 2026-11-05T05:21:26.007Z | xcoder:subtask code-st-bb4289
3
+ // xcoder:version 3 | xcoder:task task-c49600 | xcoder:updated 2026-08-13T13:52:36.713Z | xcoder:subtask code-st-6d79ef
4
+ import "dotenv/config";
5
+ import { Command } from "commander";
6
+ import path from "node:path";
7
+ import { promises as fs } from "node:fs";
8
+ import { createLlmClient } from "xcoder-dna";
9
+ import { SamuraiAgent } from "xcoder-dna";
10
+ import { runDebate } from "xcoder-dna";
11
+ import { writeReport } from "xcoder-dna";
12
+ import { InstanceSwarmOrchestrator } from "xcoder-dna";
13
+ import { getOrBuildIndex } from "xcoder-dna";
14
+ import { log } from "xcoder-dna";
15
+ import { maybePrintBanner } from "xcoder-dna";
16
+ import { enableProtocolErrorLogging } from "xcoder-dna";
17
+ import { enableToolErrorLogging } from "xcoder-dna";
18
+ import { XCODER_VERSION } from "xcoder-dna";
19
+ import { getOutputPaths } from "xcoder-dna";
20
+ import { xcoderEvents } from "xcoder-dna";
21
+ import { randomUUID } from "node:crypto";
22
+ import { resolveLessonFlag, loadLessons, checkLessonFileSize } from "xcoder-dna";
23
+ /**
24
+ * Read package.json from workspace root if present, extract name/version/
25
+ * scripts/dependencies, and format as a context block injected into the
26
+ * system prompt. Non-fatal if absent.
27
+ */
28
+ export async function readPackageJsonContext(root) {
29
+ try {
30
+ const raw = await fs.readFile(path.join(root, "package.json"), "utf8");
31
+ const pkg = JSON.parse(raw);
32
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
33
+ const depNames = Object.keys(deps).sort();
34
+ const depLines = depNames.length
35
+ ? depNames.map((n) => ` ${n}: ${deps[n]}`).join("\n")
36
+ : " (none)";
37
+ const scriptLines = pkg.scripts
38
+ ? Object.entries(pkg.scripts)
39
+ .map(([k, v]) => ` ${k}: ${v}`)
40
+ .join("\n")
41
+ : " (none)";
42
+ return `
43
+ ## Project package.json
44
+ - name: ${pkg.name ?? "(not set)"}
45
+ - version: ${pkg.version ?? "(not set)"}
46
+ - description: ${pkg.description ?? "(not set)"}
47
+ - scripts:
48
+ ${scriptLines}
49
+ - dependencies:
50
+ ${depLines}
51
+ `.trim();
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ // ── Debate CLI renderer ──
58
+ // Color-coded real-time output for debate events during `samurai ai-chat`.
59
+ // Uses ANSI colors: Sumurai = amber (warm/gold), Ninj = cyan (cool/teal).
60
+ // Falls back to text prefixes when NO_COLOR is set or stdout is not a TTY.
61
+ const COLOR_DEB = {
62
+ reset: "\x1b[0m",
63
+ dim: "\x1b[2m",
64
+ amber: "\x1b[38;5;214m",
65
+ cyan: "\x1b[38;5;80m",
66
+ green: "\x1b[38;5;78m",
67
+ coral: "\x1b[38;5;203m",
68
+ };
69
+ function useColor() {
70
+ return process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
71
+ }
72
+ function paintDeb(code, text) {
73
+ return useColor() ? `${code}${text}${COLOR_DEB.reset}` : text;
74
+ }
75
+ function renderDebateEvent(ev) {
76
+ const data = ev.data;
77
+ if (!data)
78
+ return;
79
+ const agent = data.agent ?? "?";
80
+ const color = agent === "Sumurai" ? COLOR_DEB.amber : COLOR_DEB.cyan;
81
+ const prefix = useColor() ? paintDeb(color, `[${agent}]`) : `[${agent}]`;
82
+ switch (ev.type) {
83
+ case "debate:intro": {
84
+ const topic = data.topic;
85
+ log.dim(`── Debate started ──`);
86
+ log.raw(`${paintDeb(COLOR_DEB.dim, "Topic:")} ${topic}`);
87
+ break;
88
+ }
89
+ case "debate:turn-start": {
90
+ const role = data.role;
91
+ const tn = data.turnNumber;
92
+ log.raw(`${prefix} ${paintDeb(COLOR_DEB.dim, `─ Turn ${tn} (${role}) ─`)}`);
93
+ break;
94
+ }
95
+ case "debate:agent-thought": {
96
+ const thought = data.thought ?? "";
97
+ log.raw(`${prefix} ${paintDeb(COLOR_DEB.dim, "Thought:")} ${thought}`);
98
+ break;
99
+ }
100
+ case "debate:agent-action": {
101
+ const tool = data.tool ?? "?";
102
+ const input = data.input;
103
+ const inputStr = typeof input === "string" ? input : JSON.stringify(input).slice(0, 120);
104
+ log.raw(`${prefix} ${paintDeb(COLOR_DEB.green, `Action · ${tool}`)} ${inputStr}`);
105
+ break;
106
+ }
107
+ case "debate:agent-observation": {
108
+ const ok = data.ok;
109
+ const observation = data.observation ?? "";
110
+ const statusIcon = ok ? paintDeb(COLOR_DEB.green, "✓") : paintDeb(COLOR_DEB.coral, "✗");
111
+ log.raw(`${prefix} ${paintDeb(COLOR_DEB.dim, "Observed:")} ${statusIcon} ${observation.slice(0, 200)}`);
112
+ break;
113
+ }
114
+ case "debate:agent-final": {
115
+ const summary = data.summary ?? "";
116
+ log.raw(`${prefix} ${paintDeb(COLOR_DEB.green, "Final:")} ${summary}`);
117
+ break;
118
+ }
119
+ case "debate:agent-tokens": {
120
+ const cum = data.cumulative;
121
+ if (cum) {
122
+ log.raw(`${prefix} ${paintDeb(COLOR_DEB.dim, `Tokens: ${cum.inputTokens} in / ${cum.outputTokens} out`)}`);
123
+ }
124
+ break;
125
+ }
126
+ case "debate:outro": {
127
+ const status = data.status;
128
+ const turns = data.turns;
129
+ log.raw("");
130
+ log.dim(`── Debate ended: ${status} (${turns} turns) ──`);
131
+ break;
132
+ }
133
+ }
134
+ }
135
+ /**
136
+ * Subscribe to xcoderEvents for debate:* events matching the given debateId,
137
+ * rendering each event in real-time to stdout. Returns an unsubscribe function.
138
+ */
139
+ function subscribeDebateCli(debateId) {
140
+ const handler = (ev) => {
141
+ if (ev.type.startsWith("debate:") &&
142
+ ev.taskId === debateId) {
143
+ renderDebateEvent(ev);
144
+ }
145
+ };
146
+ xcoderEvents.on("event", handler);
147
+ return () => {
148
+ xcoderEvents.off("event", handler);
149
+ };
150
+ }
151
+ // ── CLI definition ──
152
+ const program = new Command();
153
+ program
154
+ .name("samurai")
155
+ .description("samurai — a standalone single-agent CLI with full tool suite access (File Management, OS, Security, Windows/Linux/Mac).")
156
+ .version(XCODER_VERSION);
157
+ program
158
+ .command("chat", { isDefault: true })
159
+ .alias("-chat")
160
+ .description("Start an interactive REPL session with the samurai agent.")
161
+ .option("-m, --model <name>", "override the LLM model (default: env XCODER_MODEL or deepseek-chat)")
162
+ .option("-w, --workspace <dir>", "workspace root", process.cwd())
163
+ .option("--max-iterations <n>", "max ReAct loop iterations per turn", "200")
164
+ .option("--debug-protocol-error", "enable protocol error diagnostic logging (also settable via PROTOCOL_ERROR_LOG env var)")
165
+ .option("--debug-tools [path]", "enable tool-error diagnostic logging to tools.log (also settable via DEBUG_TOOLS env var)")
166
+ .option("--lesson-learned", "enable lesson-learned: agent can record lessons into .xcoder/lesson.md and prior lessons are injected into the prompt (also settable via XCODER_LESSON_LEARNED env var)")
167
+ .option("--no-lesson-learned", "explicitly disable lesson-learned")
168
+ .option("--log-output <path>", "enable SQLite output logging for ReAct signals (also settable via XCODER_OUTPUT_DB env var)")
169
+ .action(async (opts) => {
170
+ if (opts.protocolErrorLog || process.env.PROTOCOL_ERROR_LOG) {
171
+ enableProtocolErrorLogging();
172
+ }
173
+ if (opts.debugTools || process.env.DEBUG_TOOLS) {
174
+ const customPath = typeof opts.debugTools === "string" ? opts.debugTools : process.env.DEBUG_TOOLS;
175
+ enableToolErrorLogging(typeof customPath === "string" ? customPath : undefined);
176
+ }
177
+ // Initialize OutputHandler for ReAct signal logging
178
+ const logOutputPath = opts.logOutput ||
179
+ process.env.XCODER_OUTPUT_DB ||
180
+ undefined;
181
+ const { getOutputHandler } = await import("xcoder-dna");
182
+ getOutputHandler({
183
+ printToTerminal: process.env.XCODER_OUTPUT_SILENT !== "1",
184
+ logToSqlite: !!logOutputPath,
185
+ dbPath: logOutputPath,
186
+ });
187
+ const root = path.resolve(opts.workspace);
188
+ // Resolve lesson-learned
189
+ const lessonLearned = resolveLessonFlag(opts.lessonLearned, process.env);
190
+ let lessonContext;
191
+ if (lessonLearned) {
192
+ const warning = await checkLessonFileSize(root);
193
+ if (warning)
194
+ log.raw(warning);
195
+ lessonContext = await loadLessons(root);
196
+ }
197
+ // Ensure the workspace is indexed so the agent sees the file structure
198
+ await getOrBuildIndex(root);
199
+ const pkgCtx = await readPackageJsonContext(root);
200
+ let llm;
201
+ try {
202
+ llm = createLlmClient("orchestrator", { model: opts.model });
203
+ }
204
+ catch (e) {
205
+ log.fail(e.message);
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+ const agent = new SamuraiAgent({
210
+ workspaceRoot: root,
211
+ llm,
212
+ maxIterations: parseInt(opts.maxIterations, 10),
213
+ model: opts.model,
214
+ packageJsonContext: pkgCtx,
215
+ protocolErrorLog: !!(opts.protocolErrorLog || process.env.PROTOCOL_ERROR_LOG),
216
+ lessonLearned,
217
+ lessonContext,
218
+ });
219
+ await agent.runChat();
220
+ });
221
+ program
222
+ .command("task <prompt...>")
223
+ .alias("-task")
224
+ .description("Run a one-shot task with the samurai agent. Provide the task prompt as arguments (quoted for multi-word).")
225
+ .option("-m, --model <name>", "override the LLM model (default: env XCODER_MODEL or deepseek-chat)")
226
+ .option("-w, --workspace <dir>", "workspace root", process.cwd())
227
+ .option("--max-iterations <n>", "max ReAct loop iterations", "200")
228
+ .option("--debug-protocol-error", "enable protocol error diagnostic logging (also settable via PROTOCOL_ERROR_LOG env var)")
229
+ .option("--debug-tools [path]", "enable tool-error diagnostic logging to tools.log (also settable via DEBUG_TOOLS env var)")
230
+ .option("--lesson-learned", "enable lesson-learned: agent can record lessons into .xcoder/lesson.md and prior lessons are injected into the prompt (also settable via XCODER_LESSON_LEARNED env var)")
231
+ .option("--no-lesson-learned", "explicitly disable lesson-learned")
232
+ .option("--log-output <path>", "enable SQLite output logging for ReAct signals (also settable via XCODER_OUTPUT_DB env var)")
233
+ .action(async (promptParts, opts) => {
234
+ if (opts.protocolErrorLog || process.env.PROTOCOL_ERROR_LOG) {
235
+ enableProtocolErrorLogging();
236
+ }
237
+ if (opts.debugTools || process.env.DEBUG_TOOLS) {
238
+ const customPath = typeof opts.debugTools === "string" ? opts.debugTools : process.env.DEBUG_TOOLS;
239
+ enableToolErrorLogging(typeof customPath === "string" ? customPath : undefined);
240
+ }
241
+ // Initialize OutputHandler for ReAct signal logging
242
+ const logOutputPath = opts.logOutput ||
243
+ process.env.XCODER_OUTPUT_DB ||
244
+ undefined;
245
+ const { getOutputHandler } = await import("xcoder-dna");
246
+ getOutputHandler({
247
+ printToTerminal: process.env.XCODER_OUTPUT_SILENT !== "1",
248
+ logToSqlite: !!logOutputPath,
249
+ dbPath: logOutputPath,
250
+ });
251
+ const root = path.resolve(opts.workspace);
252
+ const prompt = promptParts.join(" ");
253
+ // Resolve lesson-learned
254
+ const lessonLearned = resolveLessonFlag(opts.lessonLearned, process.env);
255
+ let lessonContext;
256
+ if (lessonLearned) {
257
+ const warning = await checkLessonFileSize(root);
258
+ if (warning)
259
+ log.raw(warning);
260
+ lessonContext = await loadLessons(root);
261
+ }
262
+ await getOrBuildIndex(root);
263
+ const pkgCtx = await readPackageJsonContext(root);
264
+ let llm;
265
+ try {
266
+ llm = createLlmClient("orchestrator", { model: opts.model });
267
+ }
268
+ catch (e) {
269
+ log.fail(e.message);
270
+ process.exitCode = 1;
271
+ return;
272
+ }
273
+ const agent = new SamuraiAgent({
274
+ workspaceRoot: root,
275
+ llm,
276
+ maxIterations: parseInt(opts.maxIterations, 10),
277
+ model: opts.model,
278
+ packageJsonContext: pkgCtx,
279
+ protocolErrorLog: !!(opts.protocolErrorLog || process.env.PROTOCOL_ERROR_LOG),
280
+ lessonLearned,
281
+ lessonContext,
282
+ });
283
+ log.dim(`samurai task: ${prompt}`);
284
+ const result = await agent.runOnce(prompt);
285
+ if (result.summary) {
286
+ log.ok(result.summary);
287
+ }
288
+ if (result.deliverable !== null && result.deliverable !== undefined) {
289
+ log.raw(JSON.stringify(result.deliverable, null, 2));
290
+ }
291
+ log.dim(`iterations: ${result.iterations} files_touched: ${result.filesTouched.length} tokens: ${result.tokenUsage.inputTokens} in / ${result.tokenUsage.outputTokens} out`);
292
+ if (result.crashed)
293
+ process.exitCode = 1;
294
+ else if (result.stuckLoop)
295
+ process.exitCode = 2;
296
+ });
297
+ program
298
+ .command("ai-chat <topic...>")
299
+ .alias("-ai-chat")
300
+ .description("Run a dual-AI debate between Sumurai and Ninj on the given topic. Writes a Markdown report.")
301
+ .option("-m, --model <name>", "override the LLM model (default: env XCODER_MODEL or deepseek-chat)")
302
+ .option("-w, --workspace <dir>", "workspace root", process.cwd())
303
+ .option("--max-exchanges <n>", "max debate exchanges (default: 20)", "20")
304
+ .option("-o, --output-dir <dir>", "output directory for the report (default: .reports/ai-chat/)")
305
+ .option("--debug-protocol-error", "enable protocol error diagnostic logging (also settable via PROTOCOL_ERROR_LOG env var)")
306
+ .option("--debug-tools [path]", "enable tool-error diagnostic logging to tools.log (also settable via DEBUG_TOOLS env var)")
307
+ .action(async (topicParts, opts) => {
308
+ if (opts.protocolErrorLog || process.env.PROTOCOL_ERROR_LOG) {
309
+ enableProtocolErrorLogging();
310
+ }
311
+ if (opts.debugTools || process.env.DEBUG_TOOLS) {
312
+ const customPath = typeof opts.debugTools === "string" ? opts.debugTools : process.env.DEBUG_TOOLS;
313
+ enableToolErrorLogging(typeof customPath === "string" ? customPath : undefined);
314
+ }
315
+ const root = path.resolve(opts.workspace);
316
+ const topic = topicParts.join(" ");
317
+ const outputDir = opts.outputDir
318
+ ? path.resolve(opts.outputDir)
319
+ : getOutputPaths({ cwd: root }).aiChatReportsDir();
320
+ const debateId = randomUUID();
321
+ await getOrBuildIndex(root);
322
+ let llm;
323
+ try {
324
+ llm = createLlmClient("orchestrator", { model: opts.model });
325
+ }
326
+ catch (e) {
327
+ log.fail(e.message);
328
+ process.exitCode = 1;
329
+ return;
330
+ }
331
+ log.dim(`ai-chat debate topic: ${topic}`);
332
+ log.dim(`max exchanges: ${opts.maxExchanges}`);
333
+ log.raw("");
334
+ // Subscribe to debate events for real-time color-coded output
335
+ const unsub = subscribeDebateCli(debateId);
336
+ const debateResult = await runDebate({
337
+ workspaceRoot: root,
338
+ llm,
339
+ topic,
340
+ maxExchanges: parseInt(opts.maxExchanges, 10),
341
+ model: opts.model,
342
+ debateId,
343
+ });
344
+ // Unsubscribe from debate events — report writing is synchronous from here
345
+ unsub();
346
+ log.raw("");
347
+ // Write report regardless of debate outcome
348
+ let reportPath;
349
+ try {
350
+ reportPath = await writeReport(debateResult, outputDir);
351
+ }
352
+ catch (e) {
353
+ log.fail(`failed to write report: ${e.message}`);
354
+ process.exitCode = 1;
355
+ return;
356
+ }
357
+ log.ok(`report written: ${reportPath}`);
358
+ log.dim(`status: ${debateResult.status} turns: ${debateResult.turns.length} tokens: ${debateResult.totalTokenUsage.inputTokens} in / ${debateResult.totalTokenUsage.outputTokens} out`);
359
+ if (debateResult.status === "error") {
360
+ log.fail(debateResult.errorMessage ?? "debate failed");
361
+ process.exitCode = 1;
362
+ }
363
+ });
364
+ program
365
+ .command("swarm <goal...>")
366
+ .alias("-swarm")
367
+ .description("Run an instance swarm: multiple SubAgents dispatched concurrently against a goal, with a protocol section injected.")
368
+ .option("--protocol <name>", "protocol name (coder, tester, architect, ui-ux, secops, devops)")
369
+ .option("-m, --model <name>", "override the LLM model (default: env XCODER_MODEL or deepseek-chat)")
370
+ .option("-w, --workspace <dir>", "workspace root", process.cwd())
371
+ .option("--concurrency <n>", "number of concurrent SubAgents", "1")
372
+ .option("--max-iterations <n>", "max iterations per SubAgent", "200")
373
+ .action(async (goalParts, opts) => {
374
+ const root = path.resolve(opts.workspace);
375
+ const goal = goalParts.join(" ");
376
+ if (!opts.protocol) {
377
+ log.fail("--protocol is required (coder, tester, architect, ui-ux, secops, devops)");
378
+ process.exitCode = 1;
379
+ return;
380
+ }
381
+ await getOrBuildIndex(root);
382
+ let llm;
383
+ try {
384
+ llm = createLlmClient("orchestrator", { model: opts.model });
385
+ }
386
+ catch (e) {
387
+ log.fail(e.message);
388
+ process.exitCode = 1;
389
+ return;
390
+ }
391
+ const orchestrator = new InstanceSwarmOrchestrator({
392
+ workspaceRoot: root,
393
+ llm,
394
+ concurrency: parseInt(opts.concurrency, 10),
395
+ maxIterations: parseInt(opts.maxIterations, 10),
396
+ });
397
+ log.dim(`swarm goal: ${goal}`);
398
+ log.dim(`protocol: ${opts.protocol} concurrency: ${opts.concurrency}`);
399
+ let swarmResult;
400
+ try {
401
+ swarmResult = await orchestrator.run(goal, opts.protocol);
402
+ }
403
+ catch (e) {
404
+ log.fail(`swarm orchestration failed: ${e.message}`);
405
+ process.exitCode = 1;
406
+ return;
407
+ }
408
+ log.ok(`instanceId: ${swarmResult.instanceId}`);
409
+ log.dim(`status: ${swarmResult.status}`);
410
+ log.dim(`outputDir: ${swarmResult.outputDir}`);
411
+ log.dim(`errors: ${swarmResult.errors.length} workers: ${swarmResult.results.length} tokens: ${swarmResult.totalTokenUsage.inputTokens} in / ${swarmResult.totalTokenUsage.outputTokens} out`);
412
+ if (swarmResult.errors.length > 0) {
413
+ for (const err of swarmResult.errors) {
414
+ log.fail(err);
415
+ }
416
+ }
417
+ if (swarmResult.status === "failed") {
418
+ process.exitCode = 1;
419
+ }
420
+ });
421
+ // Also support `samurai -chat`, `samurai -task`, and `samurai -ai-chat` as top-level options
422
+ // when no subcommand is given (Commander option-like routing).
423
+ program
424
+ .option("-chat", "start interactive chat REPL")
425
+ .option("-task <prompt...>", "run one-shot task")
426
+ .option("-ai-chat <topic...>", "run dual-AI debate")
427
+ .option("-m, --model <name>", "override model")
428
+ .option("-w, --workspace <dir>", "workspace root", process.cwd())
429
+ .option("--max-iterations <n>", "max iterations", "200");
430
+ maybePrintBanner(process.argv);
431
+ program.parseAsync().catch((e) => {
432
+ log.fail(e.message);
433
+ process.exitCode = 1;
434
+ });
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "dotenv/config";
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ // xcoder:version 2 | xcoder:task task-762833 | xcoder:updated 2026-11-05T05:21:32.402Z | xcoder:subtask code-st-bb4289
3
+ // `worker` bin — generic (non-SDLC) task delegation.
4
+ //
5
+ // Deliberately minimal: parse, build one LLM client, run the orchestrator,
6
+ // print the report. No stage flags, no healing flags, no steering, and no
7
+ // spinner that could corrupt `--json` output.
8
+ import "dotenv/config";
9
+ import { Command } from "commander";
10
+ import path from "node:path";
11
+ import { promises as fs } from "node:fs";
12
+ import { createLlmClient } from "xcoder-dna";
13
+ import { getOrBuildIndex } from "xcoder-dna";
14
+ import { log } from "xcoder-dna";
15
+ import { maybePrintBanner } from "xcoder-dna";
16
+ import { enableProtocolErrorLogging } from "xcoder-dna";
17
+ import { enableToolErrorLogging } from "xcoder-dna";
18
+ import { XCODER_VERSION } from "xcoder-dna";
19
+ import { WorkerOrchestrator } from "xcoder-dna";
20
+ /** Usage errors exit 2 and never print a report. */
21
+ class UsageError extends Error {
22
+ }
23
+ /** Normalises one input entry; ids default to `st-<n>` in order. */
24
+ function normaliseSubtask(raw, index) {
25
+ const fallbackId = `st-${index + 1}`;
26
+ if (typeof raw === "string") {
27
+ const goal = raw.trim();
28
+ if (!goal)
29
+ throw new UsageError(`subtask ${fallbackId} has an empty goal`);
30
+ return { id: fallbackId, goal };
31
+ }
32
+ // JSON arrays may contain null/number/boolean entries; anything that is not
33
+ // an object is a usage error, exactly like an entry with an empty goal.
34
+ if (raw === null || typeof raw !== "object") {
35
+ throw new UsageError(`subtask ${fallbackId} must be a string or an object with a goal (got ${raw === null ? "null" : typeof raw})`);
36
+ }
37
+ const goal = typeof raw.goal === "string" ? raw.goal.trim() : "";
38
+ if (!goal)
39
+ throw new UsageError(`subtask ${fallbackId} has an empty goal`);
40
+ const id = typeof raw.id === "string" && raw.id.trim() ? raw.id.trim() : fallbackId;
41
+ const sub = { id, goal };
42
+ if (typeof raw.role === "string" && raw.role.trim())
43
+ sub.role = raw.role.trim();
44
+ if (typeof raw.deliverableSpec === "string" && raw.deliverableSpec.trim()) {
45
+ sub.deliverableSpec = raw.deliverableSpec;
46
+ }
47
+ if (raw.upstreamContext && typeof raw.upstreamContext === "object") {
48
+ sub.upstreamContext = raw.upstreamContext;
49
+ }
50
+ return sub;
51
+ }
52
+ /** Reads `--subtasks <file>` (JSON: string[] or {id,goal,role?}[]); "-" means stdin. */
53
+ async function readSubtasksFile(spec) {
54
+ let raw;
55
+ try {
56
+ raw = spec === "-" ? await readStdin() : await fs.readFile(path.resolve(spec), "utf8");
57
+ }
58
+ catch (e) {
59
+ throw new UsageError(`could not read subtasks file '${spec}': ${e.message}`);
60
+ }
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ }
65
+ catch (e) {
66
+ throw new UsageError(`subtasks file '${spec}' is not valid JSON: ${e.message}`);
67
+ }
68
+ if (!Array.isArray(parsed))
69
+ throw new UsageError("subtasks file must contain a JSON array");
70
+ return parsed;
71
+ }
72
+ function readStdin() {
73
+ return new Promise((resolve, reject) => {
74
+ let buf = "";
75
+ process.stdin.setEncoding("utf8");
76
+ process.stdin.on("data", (chunk) => (buf += chunk));
77
+ process.stdin.on("end", () => resolve(buf));
78
+ process.stdin.on("error", reject);
79
+ });
80
+ }
81
+ function printHuman(report) {
82
+ log.raw("");
83
+ log.orchestrator(`task ${report.taskId} — ${report.status}`);
84
+ for (const s of report.subtasks) {
85
+ const label = `${s.id} (${s.attempts} attempt${s.attempts === 1 ? "" : "s"})`;
86
+ if (s.status === "done") {
87
+ log.ok(`${label}: ${s.summary || "done"}`);
88
+ }
89
+ else {
90
+ log.fail(`${label}: ${s.error ?? "failed"}`);
91
+ }
92
+ for (const f of s.filesTouched)
93
+ log.dim(` touched ${f}`);
94
+ for (const h of s.history) {
95
+ if (h.error)
96
+ log.dim(` attempt ${h.attempt} failed: ${h.error}`);
97
+ }
98
+ }
99
+ log.dim(`${report.totals.done} done, ${report.totals.failed} failed, ` +
100
+ `${report.totals.totalAttempts} attempt(s), ` +
101
+ `${report.totals.inputTokens}/${report.totals.outputTokens} tokens, ` +
102
+ `${report.totals.durationMs}ms`);
103
+ for (const e of report.errors)
104
+ log.fail(`run error: ${e}`);
105
+ }
106
+ const program = new Command();
107
+ program
108
+ .name("worker")
109
+ .description("Generic worker orchestrator for non-SDLC work: accepts a task description and a list of " +
110
+ "subtasks, spawns one worker agent per subtask, retries failures, and prints an aggregated report.")
111
+ .version(XCODER_VERSION);
112
+ program
113
+ .command("run")
114
+ .description("Run a task by delegating its subtasks to worker agents")
115
+ .option("--task <text>", "Task description")
116
+ .option("--subtasks <file>", 'JSON file: string[] or {id,goal,role?}[]; "-" reads stdin')
117
+ .option("--subtask <goal>", "Repeatable single subtask (collector)", (v, acc) => {
118
+ acc.push(v);
119
+ return acc;
120
+ }, [])
121
+ .option("--concurrency <n>", "Max in-flight workers", (v) => parseInt(v, 10), 3)
122
+ .option("--max-attempts <n>", "Total attempts per subtask", (v) => parseInt(v, 10), 2)
123
+ .option("--workspace <dir>", "Workspace root", process.cwd())
124
+ .option("--model <name>", "Overrides the configured LLM model")
125
+ .option("--task-id <id>", "Explicit run id")
126
+ .option("--json", "Machine output: the report only, no prose on stdout", false)
127
+ .option("--quiet", "Suppress progress notes", false)
128
+ .option("--no-color", "Disable colour output")
129
+ .action(async (opts) => {
130
+ if (!opts.color)
131
+ process.env.NO_COLOR = "1";
132
+ try {
133
+ // `--task` is validated here rather than with `requiredOption` so a
134
+ // missing value is a UsageError (exit 2), like every other usage error,
135
+ // instead of commander's own exit code 1.
136
+ if (typeof opts.task !== "string" || !opts.task.trim()) {
137
+ throw new UsageError("required option '--task <text>' not specified");
138
+ }
139
+ // ---- collect inputs -------------------------------------------------
140
+ const rawFromFile = opts.subtasks ? await readSubtasksFile(opts.subtasks) : [];
141
+ const rawAll = [
142
+ ...rawFromFile,
143
+ ...(opts.subtask ?? []).map((g) => g),
144
+ ];
145
+ if (rawAll.length === 0) {
146
+ throw new UsageError("no subtasks given; pass --subtasks <file> or at least one --subtask <goal>");
147
+ }
148
+ const subtasks = rawAll.map((raw, i) => normaliseSubtask(raw, i));
149
+ const workspaceRoot = path.resolve(opts.workspace);
150
+ // ---- build the client and warm the index (best-effort) ----------------
151
+ const llm = createLlmClient("sub_agent", opts.model ? { model: opts.model } : {});
152
+ try {
153
+ await getOrBuildIndex(workspaceRoot);
154
+ }
155
+ catch (e) {
156
+ if (!opts.json)
157
+ log.dim(`workspace index unavailable: ${e.message}`);
158
+ }
159
+ enableProtocolErrorLogging();
160
+ enableToolErrorLogging();
161
+ // ---- run -------------------------------------------------------------
162
+ const report = await new WorkerOrchestrator({
163
+ workspaceRoot,
164
+ llm,
165
+ quiet: opts.quiet ?? false,
166
+ }).run({
167
+ taskDescription: opts.task,
168
+ subtasks,
169
+ concurrency: opts.concurrency,
170
+ maxAttempts: opts.maxAttempts,
171
+ taskId: opts.taskId,
172
+ signal: makeSignal(),
173
+ });
174
+ if (opts.json) {
175
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
176
+ }
177
+ else {
178
+ printHuman(report);
179
+ }
180
+ process.exitCode = report.status === "failed" ? 1 : 0;
181
+ }
182
+ catch (e) {
183
+ if (opts.json) {
184
+ // Machine mode: stdout carries ONLY the JSON document so consumers can
185
+ // pipe straight into JSON.parse/jq. Diagnostics go to stderr.
186
+ process.stdout.write(JSON.stringify({ error: e.message, usage: e instanceof UsageError }, null, 2) + "\n");
187
+ process.stderr.write(`worker: ${e.message}\n`);
188
+ }
189
+ else {
190
+ log.fail(e.message);
191
+ }
192
+ process.exitCode = e instanceof UsageError ? 2 : 1;
193
+ }
194
+ });
195
+ /** Ctrl+C aborts *new* waves; in-flight agents finish naturally. */
196
+ function makeSignal() {
197
+ const controller = new AbortController();
198
+ process.once("SIGINT", () => controller.abort());
199
+ return controller.signal;
200
+ }
201
+ maybePrintBanner(process.argv);
202
+ program.parseAsync().catch((e) => {
203
+ log.fail(e?.message ?? String(e));
204
+ process.exitCode = 2;
205
+ });
@@ -0,0 +1,4 @@
1
+ /** Which agent owns which top-level command name. */
2
+ export declare const DISPATCH: Record<string, (argv: string[]) => Promise<number>>;
3
+ /** Combined help text listing every command that exists today. */
4
+ export declare function renderHelp(): string;
package/dist/index.js ADDED
@@ -0,0 +1,26 @@
1
+ // Aggregator public surface: argv dispatch across agents (design 4.6).
2
+ import { runSdlcAgent } from "@xcoder/sdlc-agent";
3
+ import { runQaAgent } from "@xcoder/qa-agent";
4
+ import { runLitebro } from "@xcoder/litebro";
5
+ /** Which agent owns which top-level command name. */
6
+ export const DISPATCH = {
7
+ xcoder: runSdlcAgent,
8
+ "ba-v1": runSdlcAgent,
9
+ bugfinder: runQaAgent,
10
+ litebro: runLitebro,
11
+ };
12
+ /** Combined help text listing every command that exists today. */
13
+ export function renderHelp() {
14
+ return [
15
+ "Xcoder SDLC - multi-agent CLI",
16
+ "",
17
+ "Commands:",
18
+ " xcoder SDLC agent (plan, implement, test, review)",
19
+ " ba-v1 Business-analyst intake",
20
+ " bugfinder QA agent",
21
+ " litebro Tool-less planner/broker",
22
+ " samurai-v1 Samurai persona",
23
+ " genesis-v1 Genesis persona",
24
+ " worker-v1 Worker persona",
25
+ ].join("\n");
26
+ }
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "xcoder-origin",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.4.1",
4
+ "description": "Xcoder SDLC — a SDLC-aware, ReAct-driven multi-agent CLI. One Master Orchestrator, a swarm of role-mapped Sub Agents, a workspace index as the source of truth.",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=20"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "bin": {
22
+ "samurai-v1": "./dist/cli-samurai.js",
23
+ "genesis-v1": "./dist/cli-genesis.js",
24
+ "worker-v1": "./dist/cli-worker.js"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -p tsconfig.json",
28
+ "typecheck": "tsc --noEmit -p tsconfig.json",
29
+ "test": "vitest run"
30
+ },
31
+ "dependencies": {
32
+ "xcoder-dna": "^1.4.0",
33
+ "xcoder-jarvis-agent": "^1.4.0",
34
+ "xcoder-qa-agent": "^1.4.0",
35
+ "xcoder-litebro": "^1.4.0",
36
+ "commander": "^12.1.0",
37
+ "dotenv": "^16.4.5"
38
+ }
6
39
  }