compact-agent 1.33.6 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/crowcoder.js +94 -0
- package/dist/index.js +112 -3
- package/dist/index.js.map +1 -1
- package/dist/query.d.ts +56 -0
- package/dist/query.js +387 -4
- package/dist/query.js.map +1 -1
- package/package.json +1 -1
package/bin/crowcoder.js
CHANGED
|
@@ -61,4 +61,98 @@
|
|
|
61
61
|
}
|
|
62
62
|
})();
|
|
63
63
|
|
|
64
|
+
// ── --prompt / --prompt-file (non-interactive single-chain mode) ──
|
|
65
|
+
//
|
|
66
|
+
// When this CLI is being driven by an external harness (Terminal-Bench,
|
|
67
|
+
// CI scripts, etc.) we need a way to:
|
|
68
|
+
// 1. accept a prompt without opening the REPL
|
|
69
|
+
// 2. run one runQuery chain to completion
|
|
70
|
+
// 3. exit with a meaningful code (0 = success, 1 = error)
|
|
71
|
+
//
|
|
72
|
+
// Two surface forms:
|
|
73
|
+
// --prompt "do the thing" — inline text
|
|
74
|
+
// --prompt-file path/to/task.txt — read from disk; useful for long
|
|
75
|
+
// or multi-line task descriptions
|
|
76
|
+
// that would otherwise need careful
|
|
77
|
+
// shell quoting.
|
|
78
|
+
//
|
|
79
|
+
// We export the resolved prompt via COMPACT_AGENT_PROMPT (or
|
|
80
|
+
// COMPACT_AGENT_PROMPT_FILE, which the loader reads with fs.readFile).
|
|
81
|
+
// src/index.ts branches on these env vars near the top of main() and
|
|
82
|
+
// skips the REPL entirely.
|
|
83
|
+
//
|
|
84
|
+
// A bare `--non-interactive` flag is also accepted as a no-prompt
|
|
85
|
+
// signal — useful when paired with a config that already has
|
|
86
|
+
// `__crowcoderQueuedInput` set, but mostly an alias for the same path.
|
|
87
|
+
// ESM-safe sync FS load. import() is async (returns a promise) and the
|
|
88
|
+
// flag parser MUST run synchronously before the dynamic import of
|
|
89
|
+
// dist/index.js below. createRequire gives us a real CommonJS require
|
|
90
|
+
// inside ESM — same path Node recommends for sync fs in ESM scripts.
|
|
91
|
+
const { createRequire } = await import('node:module');
|
|
92
|
+
const __require = createRequire(import.meta.url);
|
|
93
|
+
|
|
94
|
+
(() => {
|
|
95
|
+
const fs = __require('node:fs');
|
|
96
|
+
const argv = process.argv;
|
|
97
|
+
for (let i = 2; i < argv.length; i++) {
|
|
98
|
+
const a = argv[i];
|
|
99
|
+
if (a === '--prompt') {
|
|
100
|
+
const next = argv[i + 1];
|
|
101
|
+
if (typeof next !== 'string') {
|
|
102
|
+
process.stderr.write('[compact-agent] --prompt requires an argument.\n');
|
|
103
|
+
process.exit(2);
|
|
104
|
+
}
|
|
105
|
+
process.env.COMPACT_AGENT_PROMPT = next;
|
|
106
|
+
process.env.COMPACT_AGENT_NON_INTERACTIVE = '1';
|
|
107
|
+
argv.splice(i, 2);
|
|
108
|
+
i--;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (a && a.startsWith('--prompt=')) {
|
|
112
|
+
process.env.COMPACT_AGENT_PROMPT = a.slice('--prompt='.length);
|
|
113
|
+
process.env.COMPACT_AGENT_NON_INTERACTIVE = '1';
|
|
114
|
+
argv.splice(i, 1);
|
|
115
|
+
i--;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (a === '--prompt-file') {
|
|
119
|
+
const next = argv[i + 1];
|
|
120
|
+
if (typeof next !== 'string') {
|
|
121
|
+
process.stderr.write('[compact-agent] --prompt-file requires a path.\n');
|
|
122
|
+
process.exit(2);
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
process.env.COMPACT_AGENT_PROMPT = fs.readFileSync(next, 'utf8');
|
|
126
|
+
} catch (err) {
|
|
127
|
+
process.stderr.write(`[compact-agent] could not read --prompt-file: ${err && err.message ? err.message : err}\n`);
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
process.env.COMPACT_AGENT_NON_INTERACTIVE = '1';
|
|
131
|
+
argv.splice(i, 2);
|
|
132
|
+
i--;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (a === '--non-interactive') {
|
|
136
|
+
process.env.COMPACT_AGENT_NON_INTERACTIVE = '1';
|
|
137
|
+
argv.splice(i, 1);
|
|
138
|
+
i--;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// --perm <mode>: override permission mode without touching saved
|
|
142
|
+
// config. Critical for harness runs that want yolo without
|
|
143
|
+
// mutating the user's interactive config file.
|
|
144
|
+
if (a === '--perm') {
|
|
145
|
+
const next = argv[i + 1];
|
|
146
|
+
if (next && /^(ask|auto|yolo)$/.test(next)) {
|
|
147
|
+
process.env.COMPACT_AGENT_PERM_OVERRIDE = next;
|
|
148
|
+
argv.splice(i, 2);
|
|
149
|
+
i--;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
process.stderr.write('[compact-agent] --perm requires ask|auto|yolo.\n');
|
|
153
|
+
process.exit(2);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
})();
|
|
157
|
+
|
|
64
158
|
import('../dist/index.js');
|
package/dist/index.js
CHANGED
|
@@ -2679,14 +2679,33 @@ async function main() {
|
|
|
2679
2679
|
}
|
|
2680
2680
|
catch { /* never block startup on this */ }
|
|
2681
2681
|
}
|
|
2682
|
-
// Load or create config
|
|
2682
|
+
// Load or create config.
|
|
2683
|
+
//
|
|
2684
|
+
// Non-interactive mode (COMPACT_AGENT_NON_INTERACTIVE=1) requires a
|
|
2685
|
+
// pre-existing config — the setup wizard would block on stdin
|
|
2686
|
+
// forever in a piped/headless environment. We bail with a clear
|
|
2687
|
+
// error if no config is on disk, so the caller knows to run the
|
|
2688
|
+
// wizard interactively first (`compact-agent` with no args).
|
|
2689
|
+
const nonInteractive = process.env.COMPACT_AGENT_NON_INTERACTIVE === '1';
|
|
2683
2690
|
let config;
|
|
2684
2691
|
if (!configExists()) {
|
|
2692
|
+
if (nonInteractive) {
|
|
2693
|
+
process.stderr.write('[compact-agent] non-interactive mode requires a pre-existing config at ~/.compact-agent/config.json.\n' +
|
|
2694
|
+
'Run `compact-agent` once interactively to walk through the setup wizard, OR write the config manually.\n');
|
|
2695
|
+
process.exit(2);
|
|
2696
|
+
}
|
|
2685
2697
|
config = await setupWizard(rl);
|
|
2686
2698
|
}
|
|
2687
2699
|
else {
|
|
2688
2700
|
config = loadConfig();
|
|
2689
2701
|
}
|
|
2702
|
+
// Per-invocation permission override (--perm flag). Doesn't touch
|
|
2703
|
+
// saved config — purely a runtime knob so harness runs can force
|
|
2704
|
+
// yolo without mutating the user's interactive permission setting.
|
|
2705
|
+
const permOverride = process.env.COMPACT_AGENT_PERM_OVERRIDE;
|
|
2706
|
+
if (permOverride === 'ask' || permOverride === 'auto' || permOverride === 'yolo') {
|
|
2707
|
+
config.permissionMode = permOverride;
|
|
2708
|
+
}
|
|
2690
2709
|
// Apply the user's chosen color palette before anything paints. setPalette
|
|
2691
2710
|
// mutates the exported `theme` object in place so the banner, prompt, and
|
|
2692
2711
|
// every subsequent log line render in the right colors.
|
|
@@ -2740,9 +2759,14 @@ async function main() {
|
|
|
2740
2759
|
if (memoryContext) {
|
|
2741
2760
|
messages.push({ role: 'system', content: memoryContext });
|
|
2742
2761
|
}
|
|
2743
|
-
// Show startup display based on theme setting
|
|
2762
|
+
// Show startup display based on theme setting. Skipped entirely in
|
|
2763
|
+
// non-interactive mode — banners are noise when a harness is parsing
|
|
2764
|
+
// our stdout.
|
|
2744
2765
|
const themeMode = config.theme || 'full';
|
|
2745
|
-
if (
|
|
2766
|
+
if (nonInteractive) {
|
|
2767
|
+
// intentionally no output
|
|
2768
|
+
}
|
|
2769
|
+
else if (themeMode === 'full') {
|
|
2746
2770
|
// Full mode: banner. ASCII splash removed per user request — both `full`
|
|
2747
2771
|
// and `compact` themes now render the same banner block.
|
|
2748
2772
|
printThemedBanner(config.provider, config.model, mode.current, config.permissionMode, session.id, ALL_TOOLS.map((t) => t.name));
|
|
@@ -2819,7 +2843,13 @@ async function main() {
|
|
|
2819
2843
|
// (the promises variant doesn't expose it). Some platforms / terminals
|
|
2820
2844
|
// don't deliver every F-key — failure here is a silent no-op; users can
|
|
2821
2845
|
// fall back to /dictate and /voice slash commands.
|
|
2846
|
+
//
|
|
2847
|
+
// Skipped in non-interactive mode — there's no user at the keyboard,
|
|
2848
|
+
// and listening to keypress would consume bytes from the harness's
|
|
2849
|
+
// piped stdin that may or may not look like F-keys.
|
|
2822
2850
|
try {
|
|
2851
|
+
if (nonInteractive)
|
|
2852
|
+
throw new Error('skip:nonInteractive');
|
|
2823
2853
|
const readlineCb = await import('node:readline');
|
|
2824
2854
|
const { describeStatus, describeLocation } = await import('./status.js');
|
|
2825
2855
|
readlineCb.emitKeypressEvents(stdin);
|
|
@@ -3530,6 +3560,85 @@ async function main() {
|
|
|
3530
3560
|
permissionMode: config.permissionMode,
|
|
3531
3561
|
cwd: process.cwd(),
|
|
3532
3562
|
});
|
|
3563
|
+
// ── Non-interactive single-chain mode ─────────────────
|
|
3564
|
+
//
|
|
3565
|
+
// Triggered by `--prompt <text>` / `--prompt-file <path>` (parsed in
|
|
3566
|
+
// bin/crowcoder.js and stashed on COMPACT_AGENT_PROMPT). We push the
|
|
3567
|
+
// prompt as one user message, run a single runQuery to completion,
|
|
3568
|
+
// and exit. No REPL, no banner, no hotkey listener, no live queue.
|
|
3569
|
+
//
|
|
3570
|
+
// This is the entrypoint that lets external harnesses (Terminal-Bench,
|
|
3571
|
+
// CI scripts, etc.) drive compact-agent with a single task and read
|
|
3572
|
+
// its output cleanly. Stdin is left untouched — readline never
|
|
3573
|
+
// attaches — so piped stdin won't confuse anything.
|
|
3574
|
+
if (process.env.COMPACT_AGENT_NON_INTERACTIVE === '1') {
|
|
3575
|
+
const promptText = process.env.COMPACT_AGENT_PROMPT;
|
|
3576
|
+
if (!promptText || !promptText.trim()) {
|
|
3577
|
+
process.stderr.write('[compact-agent] non-interactive mode requires --prompt <text> or --prompt-file <path>.\n');
|
|
3578
|
+
process.exit(2);
|
|
3579
|
+
}
|
|
3580
|
+
// ── F9: Empty-engagement guard (non-interactive nudge) ──
|
|
3581
|
+
//
|
|
3582
|
+
// Some failures in the 2026-05-25 baseline run came from the
|
|
3583
|
+
// model emitting a single no-tool-call response and exiting —
|
|
3584
|
+
// never actually attempting the work. polyglot-c-py, solana-data,
|
|
3585
|
+
// and vim-terminal-task all showed this pattern. The model
|
|
3586
|
+
// interpreted some aspect of the spec as "I can't do this" (e.g.
|
|
3587
|
+
// "use vim" suggesting interactive editing) and bailed.
|
|
3588
|
+
//
|
|
3589
|
+
// In non-interactive mode there's no human to push back, so we
|
|
3590
|
+
// prepend a system message that explicitly frames the contract:
|
|
3591
|
+
// the agent must DO the work, with tools. Responses without tool
|
|
3592
|
+
// calls are interpreted as "I'm done" — and F5+ DeCRIM will
|
|
3593
|
+
// then walk the agent through verification.
|
|
3594
|
+
//
|
|
3595
|
+
// This is system-prompt-level and doesn't repeat per-turn (that
|
|
3596
|
+
// would bloat context). It's a one-shot priming injection.
|
|
3597
|
+
messages.push({
|
|
3598
|
+
role: 'system',
|
|
3599
|
+
content: 'You are running in NON-INTERACTIVE mode: no human will answer follow-up questions. ' +
|
|
3600
|
+
'You must DO the work using the available tools (bash, write, edit, read, glob, grep, etc.) — ' +
|
|
3601
|
+
'not describe what would need to be done. ' +
|
|
3602
|
+
'If the task mentions a specific tool you do not have direct access to (e.g. "use vim"), ' +
|
|
3603
|
+
'achieve the equivalent effect with the tools you do have. ' +
|
|
3604
|
+
'If you lack information, USE A TOOL to investigate; do not ask the user. ' +
|
|
3605
|
+
'A response with no tool calls is interpreted as "I am done" and triggers final verification.',
|
|
3606
|
+
});
|
|
3607
|
+
messages.push({ role: 'user', content: promptText.trim() });
|
|
3608
|
+
try {
|
|
3609
|
+
await runQuery({
|
|
3610
|
+
config,
|
|
3611
|
+
messages,
|
|
3612
|
+
cwd: process.cwd(),
|
|
3613
|
+
rl,
|
|
3614
|
+
sessionId: session.id,
|
|
3615
|
+
mode: mode.current,
|
|
3616
|
+
});
|
|
3617
|
+
// Run any session-stop hooks the user registered.
|
|
3618
|
+
try {
|
|
3619
|
+
await runHooks({ event: 'SessionStop', sessionId: session.id, cwd: process.cwd(), permissionMode: config.permissionMode });
|
|
3620
|
+
}
|
|
3621
|
+
catch { /* never fail an otherwise-successful run on hook errors */ }
|
|
3622
|
+
// Close readline so the Node process can exit cleanly. Without
|
|
3623
|
+
// this, the readline interface keeps the event loop alive until
|
|
3624
|
+
// the user types something (which they can't, since stdin is
|
|
3625
|
+
// piped from a harness).
|
|
3626
|
+
try {
|
|
3627
|
+
rl.close();
|
|
3628
|
+
}
|
|
3629
|
+
catch { /* noop */ }
|
|
3630
|
+
process.exit(0);
|
|
3631
|
+
}
|
|
3632
|
+
catch (err) {
|
|
3633
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3634
|
+
process.stderr.write(`[compact-agent] chain failed: ${msg}\n`);
|
|
3635
|
+
try {
|
|
3636
|
+
rl.close();
|
|
3637
|
+
}
|
|
3638
|
+
catch { /* noop */ }
|
|
3639
|
+
process.exit(1);
|
|
3640
|
+
}
|
|
3641
|
+
}
|
|
3533
3642
|
// Main REPL loop
|
|
3534
3643
|
while (true) {
|
|
3535
3644
|
let input;
|