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 +21 -0
- package/README.md +119 -2
- package/dist/cli-genesis.d.ts +2 -0
- package/dist/cli-genesis.js +222 -0
- package/dist/cli-samurai.d.ts +8 -0
- package/dist/cli-samurai.js +434 -0
- package/dist/cli-worker.d.ts +2 -0
- package/dist/cli-worker.js +205 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +26 -0
- package/package.json +36 -3
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
|
-
|
|
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
|
-
|
|
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,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,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
|
+
});
|
package/dist/index.d.ts
ADDED
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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
}
|