@nemus-cli/nemus 0.2.12 → 0.3.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/CHANGELOG.md +28 -0
- package/README.md +35 -0
- package/dist/cli/ai-prompt.js +13 -25
- package/dist/commands/completion.js +143 -0
- package/dist/commands/reflect.js +111 -0
- package/dist/program.js +4 -0
- package/dist/utils/agent-judge.js +94 -0
- package/dist/utils/reflect.js +345 -0
- package/package.json +1 -1
- package/src/cli/ai-prompt.ts +12 -22
- package/src/commands/completion.test.ts +60 -0
- package/src/commands/completion.ts +160 -0
- package/src/commands/reflect.ts +120 -0
- package/src/program.ts +4 -0
- package/src/utils/agent-judge.test.ts +54 -0
- package/src/utils/agent-judge.ts +109 -0
- package/src/utils/reflect.test.ts +103 -0
- package/src/utils/reflect.ts +386 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] - 2026-08-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`nemus reflect` (alias `retro`) — LLM-as-a-judge retrospective.** Analyzes
|
|
15
|
+
your most recent workspaces (default 10) by reading their agent session
|
|
16
|
+
transcripts, distilling the human prompts + tool failures + tools used, and
|
|
17
|
+
asking your own configured agent (claude/pi/opencode — no API key of ours) to
|
|
18
|
+
recommend concrete setup improvements: which **skills** to add and where,
|
|
19
|
+
missing **AGENTS.md/context** rules, missing **connectivity/smoke tests**, and
|
|
20
|
+
**prompt/workflow** habits to change — each with a priority and a concrete
|
|
21
|
+
example snippet. Reads both Claude and pi transcript formats. Flags:
|
|
22
|
+
`--limit <n>`, `--json` (structured report, same `{ok:false,error}` failure
|
|
23
|
+
contract as the other JSON commands), and `--dry-run` (print the assembled
|
|
24
|
+
corpus + judge prompt without calling the agent). Idea courtesy of
|
|
25
|
+
**@lightpriest** — thank you for the great suggestion!
|
|
26
|
+
|
|
27
|
+
## [0.2.13] - 2026-08-27
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- **Shell completions**: `nemus completion bash|zsh|fish` prints a completion
|
|
32
|
+
script for that shell. Completes subcommands (names + aliases) and, for a
|
|
33
|
+
workspace-scoped command, live **workspace names** — the script calls back
|
|
34
|
+
into `nemus completion --workspaces`, so completions stay fresh without
|
|
35
|
+
regenerating. Registered for both the `nemus` and `nem` binaries. Install e.g.
|
|
36
|
+
`nemus completion zsh > "${fpath[1]}/_nemus"` (see README).
|
|
37
|
+
|
|
10
38
|
## [0.2.12] - 2026-08-27
|
|
11
39
|
|
|
12
40
|
### Added
|
package/README.md
CHANGED
|
@@ -243,6 +243,24 @@ The workspace-scoped ones (`status`/`doctor`/`analyze-deps`) need an explicit
|
|
|
243
243
|
workspace name with `--json` (they never prompt). On failure, `--json` prints a
|
|
244
244
|
parseable `{ "ok": false, "error": … }` to stdout and exits non-zero.
|
|
245
245
|
|
|
246
|
+
### Shell completions
|
|
247
|
+
|
|
248
|
+
Tab-complete subcommands and workspace names. `nemus completion <shell>` prints
|
|
249
|
+
a script for `bash`, `zsh`, or `fish` (works for both the `nemus` and `nem`
|
|
250
|
+
binaries):
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
# bash
|
|
254
|
+
nemus completion bash > /etc/bash_completion.d/nemus # or >> ~/.bashrc
|
|
255
|
+
# zsh (a directory on your $fpath)
|
|
256
|
+
nemus completion zsh > "${fpath[1]}/_nemus"
|
|
257
|
+
# fish
|
|
258
|
+
nemus completion fish > ~/.config/fish/completions/nemus.fish
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Workspace names are resolved live (the script calls back into the CLI), so they
|
|
262
|
+
stay current without regenerating.
|
|
263
|
+
|
|
246
264
|
### Suites (reusable repo collections)
|
|
247
265
|
|
|
248
266
|
```bash
|
|
@@ -262,6 +280,23 @@ nemus snapshot save ws # (ss) capture exact branches/commits/dirty state
|
|
|
262
280
|
nemus snapshot restore <id> # (sr)
|
|
263
281
|
```
|
|
264
282
|
|
|
283
|
+
### Reflect — improve your setup over time
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
nemus reflect # (retro) analyze your last 10 workspaces' sessions
|
|
287
|
+
nemus reflect --limit 5 # narrow the window
|
|
288
|
+
nemus reflect --json # structured report for tooling
|
|
289
|
+
nemus reflect --dry-run # show what the judge sees, without calling the agent
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
`reflect` reads your recent agent **session transcripts** (Claude + pi), distills
|
|
293
|
+
the prompts you sent, the failures the agent hit, and the tools it used, then asks
|
|
294
|
+
**your own configured agent** (LLM-as-a-judge — no extra API key) to recommend
|
|
295
|
+
concrete improvements: which **skills** to add and where, missing
|
|
296
|
+
**AGENTS.md/context** rules, missing **connectivity/smoke tests**, and
|
|
297
|
+
**prompt/workflow** habits — each with a priority and an example snippet. It's a
|
|
298
|
+
fast retrospective on *how you drive the agent*, so next time works better.
|
|
299
|
+
|
|
265
300
|
### AI assistant
|
|
266
301
|
|
|
267
302
|
```bash
|
package/dist/cli/ai-prompt.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.extractIntent = extractIntent;
|
|
|
41
41
|
exports.run = run;
|
|
42
42
|
exports.main = main;
|
|
43
43
|
const child_process_1 = require("child_process");
|
|
44
|
+
const agent_judge_1 = require("../utils/agent-judge");
|
|
44
45
|
const util_1 = require("util");
|
|
45
46
|
const fs = __importStar(require("fs"));
|
|
46
47
|
const path = __importStar(require("path"));
|
|
@@ -267,35 +268,22 @@ async function extractIntent(prompt) {
|
|
|
267
268
|
];
|
|
268
269
|
result = runExtraction('pi', [...piLean, ...piCore], piCore);
|
|
269
270
|
}
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
const parsed = JSON.parse(jsonStr);
|
|
277
|
-
// Handle different output formats:
|
|
278
|
-
// - Claude: { structured_output: {...} } or { result: "..." }
|
|
279
|
-
// - Pi: may return the object directly or wrap it
|
|
271
|
+
// Unwrap the agent's reply (code fences + the structured_output / result-string
|
|
272
|
+
// / result-object / bare-object envelopes) with the shared parser, so this and
|
|
273
|
+
// the reflect judge can't drift when a new agent shape is learned. The
|
|
274
|
+
// extraction-specific INVOCATION (lean flags + tailored timeout/auth errors)
|
|
275
|
+
// deliberately stays here — those messages are part of the `nemus --` UX.
|
|
280
276
|
let intent;
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
try {
|
|
286
|
-
intent = JSON.parse(parsed.result);
|
|
287
|
-
}
|
|
288
|
-
catch { /* ignore */ }
|
|
289
|
-
}
|
|
290
|
-
else if (typeof parsed.result === 'object' && parsed.result !== null) {
|
|
291
|
-
intent = parsed.result;
|
|
277
|
+
try {
|
|
278
|
+
const parsed = (0, agent_judge_1.parseAgentJson)(result);
|
|
279
|
+
if (parsed && typeof parsed === 'object')
|
|
280
|
+
intent = parsed;
|
|
292
281
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
intent = parsed;
|
|
282
|
+
catch {
|
|
283
|
+
throw new Error(`Could not extract intent from agent response: ${result.slice(0, 200)}`);
|
|
296
284
|
}
|
|
297
285
|
if (!intent) {
|
|
298
|
-
throw new Error(`Could not extract intent from agent response
|
|
286
|
+
throw new Error(`Could not extract intent from agent response: ${result.slice(0, 200)}`);
|
|
299
287
|
}
|
|
300
288
|
// Coerce/validate field types so malformed model output can't crash the
|
|
301
289
|
// downstream .trim()/sanitize path (a stringly-typed workspaceName or a
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.COMPLETION_BINS = void 0;
|
|
4
|
+
exports.generateCompletion = generateCompletion;
|
|
5
|
+
exports.specsFromProgram = specsFromProgram;
|
|
6
|
+
exports.registerCompletionCommand = registerCompletionCommand;
|
|
7
|
+
const workspace_meta_1 = require("../utils/workspace-meta");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
/** Binaries that get completion registered (the CLI's bins). */
|
|
10
|
+
exports.COMPLETION_BINS = ['nemus', 'nem'];
|
|
11
|
+
/** Every token (name + aliases) that should complete as a subcommand. */
|
|
12
|
+
function allTokens(cmds) {
|
|
13
|
+
return cmds.flatMap((c) => [c.name, ...c.aliases]);
|
|
14
|
+
}
|
|
15
|
+
/** Tokens (names + aliases) of the commands that take a workspace argument. */
|
|
16
|
+
function workspaceTokens(cmds) {
|
|
17
|
+
return cmds.filter((c) => c.takesWorkspace).flatMap((c) => [c.name, ...c.aliases]);
|
|
18
|
+
}
|
|
19
|
+
/** Escape a description for a fish single-quoted string. */
|
|
20
|
+
function fishDesc(s) {
|
|
21
|
+
return s.replace(/\n/g, ' ').replace(/'/g, "'\\''");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a shell completion script. Pure (no I/O) so it's unit-tested. The
|
|
25
|
+
* generated script completes subcommands at position 1, and for a subcommand
|
|
26
|
+
* that takes a workspace it completes workspace names by calling back into the
|
|
27
|
+
* CLI: `<bin> completion --workspaces`. Dynamic values stay fresh without
|
|
28
|
+
* regenerating the script.
|
|
29
|
+
*/
|
|
30
|
+
function generateCompletion(shell, cmds, bins = exports.COMPLETION_BINS) {
|
|
31
|
+
const commands = allTokens(cmds).join(' ');
|
|
32
|
+
const wsCommands = workspaceTokens(cmds).join(' ');
|
|
33
|
+
if (shell === 'bash') {
|
|
34
|
+
return `# nemus bash completion. Install: nemus completion bash > /etc/bash_completion.d/nemus
|
|
35
|
+
# (or: nemus completion bash >> ~/.bashrc)
|
|
36
|
+
_nemus_complete() {
|
|
37
|
+
local cur bin sub
|
|
38
|
+
# bash does not clear COMPREPLY between completions; reset so a stale result
|
|
39
|
+
# from a previous TAB can't leak when we return without setting it.
|
|
40
|
+
COMPREPLY=()
|
|
41
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
42
|
+
bin="\${COMP_WORDS[0]}"
|
|
43
|
+
local commands="${commands}"
|
|
44
|
+
local ws_commands="${wsCommands}"
|
|
45
|
+
if [ "\$COMP_CWORD" -eq 1 ]; then
|
|
46
|
+
COMPREPLY=( \$(compgen -W "\$commands" -- "\$cur") )
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
if [ "\$COMP_CWORD" -eq 2 ]; then
|
|
50
|
+
sub="\${COMP_WORDS[1]}"
|
|
51
|
+
if [[ " \$ws_commands " == *" \$sub "* ]]; then
|
|
52
|
+
local names
|
|
53
|
+
names="\$("\$bin" completion --workspaces 2>/dev/null)"
|
|
54
|
+
COMPREPLY=( \$(compgen -W "\$names" -- "\$cur") )
|
|
55
|
+
return 0
|
|
56
|
+
fi
|
|
57
|
+
fi
|
|
58
|
+
return 0
|
|
59
|
+
}
|
|
60
|
+
${bins.map((b) => `complete -F _nemus_complete ${b}`).join('\n')}
|
|
61
|
+
`;
|
|
62
|
+
}
|
|
63
|
+
if (shell === 'zsh') {
|
|
64
|
+
// Autoloaded form: save as a file named `_nemus` on your $fpath.
|
|
65
|
+
return `#compdef ${bins.join(' ')}
|
|
66
|
+
# nemus zsh completion. Install: nemus completion zsh > "\${fpath[1]}/_nemus"
|
|
67
|
+
local -a _nemus_commands
|
|
68
|
+
_nemus_commands=(${allTokens(cmds).map((t) => `'${t}'`).join(' ')})
|
|
69
|
+
local _nemus_ws_commands="${wsCommands}"
|
|
70
|
+
if (( CURRENT == 2 )); then
|
|
71
|
+
compadd -- $_nemus_commands
|
|
72
|
+
return
|
|
73
|
+
fi
|
|
74
|
+
if (( CURRENT == 3 )); then
|
|
75
|
+
local sub=\${words[2]}
|
|
76
|
+
if [[ " $_nemus_ws_commands " == *" $sub "* ]]; then
|
|
77
|
+
local -a _nemus_names
|
|
78
|
+
_nemus_names=(\${(f)"$(\${words[1]} completion --workspaces 2>/dev/null)"})
|
|
79
|
+
compadd -- $_nemus_names
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
// fish
|
|
85
|
+
const lines = ['# nemus fish completion. Install: nemus completion fish > ~/.config/fish/completions/nemus.fish'];
|
|
86
|
+
for (const bin of bins) {
|
|
87
|
+
lines.push(`complete -c ${bin} -f`);
|
|
88
|
+
for (const c of cmds) {
|
|
89
|
+
for (const tok of [c.name, ...c.aliases]) {
|
|
90
|
+
lines.push(`complete -c ${bin} -n __fish_use_subcommand -a '${tok}' -d '${fishDesc(c.description)}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const wsToks = workspaceTokens(cmds).join(' ');
|
|
94
|
+
if (wsToks) {
|
|
95
|
+
lines.push(`complete -c ${bin} -n '__fish_seen_subcommand_from ${wsToks}' -a '(${bin} completion --workspaces)'`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n') + '\n';
|
|
99
|
+
}
|
|
100
|
+
/** Distill the program's top-level commands into CommandSpecs. */
|
|
101
|
+
function specsFromProgram(program) {
|
|
102
|
+
return program.commands
|
|
103
|
+
.map((c) => {
|
|
104
|
+
const args = c.registeredArguments ?? [];
|
|
105
|
+
const firstArg = args[0]?.name?.();
|
|
106
|
+
return {
|
|
107
|
+
name: c.name(),
|
|
108
|
+
aliases: c.aliases(),
|
|
109
|
+
takesWorkspace: typeof firstArg === 'string' && firstArg.toLowerCase().includes('workspace'),
|
|
110
|
+
description: c.description() ?? '',
|
|
111
|
+
};
|
|
112
|
+
})
|
|
113
|
+
// The completion command itself and any hidden helper needn't clutter, but
|
|
114
|
+
// keeping them is harmless; only drop entries with no name.
|
|
115
|
+
.filter((s) => s.name);
|
|
116
|
+
}
|
|
117
|
+
function registerCompletionCommand(program) {
|
|
118
|
+
program
|
|
119
|
+
.command('completion [shell]')
|
|
120
|
+
.description('Output a shell completion script (bash|zsh|fish)')
|
|
121
|
+
.option('--workspaces', 'Print workspace names (used internally by completion scripts)')
|
|
122
|
+
.action(async (shell, opts) => {
|
|
123
|
+
// Data helper the generated scripts call back into.
|
|
124
|
+
if (opts.workspaces) {
|
|
125
|
+
try {
|
|
126
|
+
const workspaces = await (0, workspace_meta_1.listWorkspaces)(false);
|
|
127
|
+
for (const ws of workspaces)
|
|
128
|
+
process.stdout.write(ws.name + '\n');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Silent: completion must never error out the user's shell.
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const shells = ['bash', 'zsh', 'fish'];
|
|
136
|
+
if (!shell || !shells.includes(shell)) {
|
|
137
|
+
(0, logger_1.logError)(`completion: specify a shell — one of ${shells.join(', ')}`);
|
|
138
|
+
(0, logger_1.logError)('e.g. nemus completion bash');
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
process.stdout.write(generateCompletion(shell, specsFromProgram(program)));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerReflectCommand = registerReflectCommand;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
const output_1 = require("../utils/output");
|
|
6
|
+
const colors_1 = require("../utils/colors");
|
|
7
|
+
const reflect_1 = require("../utils/reflect");
|
|
8
|
+
const agent_judge_1 = require("../utils/agent-judge");
|
|
9
|
+
function registerReflectCommand(parent) {
|
|
10
|
+
parent
|
|
11
|
+
.command('reflect')
|
|
12
|
+
.alias('retro')
|
|
13
|
+
.description('Analyze your recent workspace sessions and suggest skill/prompt/context improvements (LLM-as-a-judge)')
|
|
14
|
+
.option('-n, --limit <n>', 'How many recent workspaces to analyze', '10')
|
|
15
|
+
.option('--json', 'Output the report as JSON')
|
|
16
|
+
.option('--dry-run', 'Print the assembled corpus + judge prompt without calling the agent')
|
|
17
|
+
.action(async (opts) => {
|
|
18
|
+
await handleReflect(opts);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async function handleReflect(opts) {
|
|
22
|
+
const limit = Math.max(1, Number.parseInt(opts.limit ?? '10', 10) || 10);
|
|
23
|
+
try {
|
|
24
|
+
if (!opts.json && !opts.dryRun) {
|
|
25
|
+
(0, logger_1.logStep)(`Analyzing your ${(0, colors_1.colorize)(String(limit), 'cyan')} most recent workspaces…`);
|
|
26
|
+
(0, logger_1.logInfo)('Reading sessions and distilling prompts + failures…');
|
|
27
|
+
}
|
|
28
|
+
const corpus = await (0, reflect_1.gatherReflectionCorpus)(limit);
|
|
29
|
+
const withSessions = corpus.workspaces.filter((w) => w.session).length;
|
|
30
|
+
const prompt = (0, reflect_1.buildJudgePrompt)(corpus);
|
|
31
|
+
if (opts.dryRun) {
|
|
32
|
+
// No LLM call — surface exactly what the judge would see.
|
|
33
|
+
if (opts.json)
|
|
34
|
+
(0, output_1.outputJson)({ corpus, prompt });
|
|
35
|
+
else {
|
|
36
|
+
process.stdout.write(prompt + '\n');
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (withSessions === 0) {
|
|
41
|
+
const msg = 'No recent agent sessions found to analyze (need Claude/pi session transcripts).';
|
|
42
|
+
if (opts.json)
|
|
43
|
+
(0, output_1.outputJsonError)(msg);
|
|
44
|
+
else
|
|
45
|
+
(0, logger_1.logError)(msg);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
if (!opts.json)
|
|
49
|
+
(0, logger_1.logInfo)(`Judging ${withSessions} session(s) with your configured agent…`);
|
|
50
|
+
const parsed = (0, agent_judge_1.runAgentJson)(prompt, { schema: reflect_1.REFLECT_SCHEMA });
|
|
51
|
+
const report = (0, reflect_1.parseReflectionReport)(parsed);
|
|
52
|
+
if (opts.json) {
|
|
53
|
+
(0, output_1.outputJson)({ analyzed: withSessions, workspaces: corpus.workspaces.length, ...report });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
printReport(report, corpus.workspaces.length, withSessions);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
const msg = error instanceof Error ? error.message : 'reflect failed';
|
|
60
|
+
if (opts.json)
|
|
61
|
+
(0, output_1.outputJsonError)(msg);
|
|
62
|
+
else {
|
|
63
|
+
(0, logger_1.logError)('Failed to analyze sessions');
|
|
64
|
+
(0, logger_1.logError)(msg);
|
|
65
|
+
}
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const KIND_LABEL = {
|
|
70
|
+
skill: 'Skill',
|
|
71
|
+
context: 'Context/AGENTS.md',
|
|
72
|
+
test: 'Test',
|
|
73
|
+
prompt: 'Prompt',
|
|
74
|
+
connectivity: 'Connectivity',
|
|
75
|
+
workflow: 'Workflow',
|
|
76
|
+
other: 'Other',
|
|
77
|
+
};
|
|
78
|
+
function priorityBadge(p) {
|
|
79
|
+
if (p === 'high')
|
|
80
|
+
return (0, colors_1.colorize)('● high', 'red');
|
|
81
|
+
if (p === 'medium')
|
|
82
|
+
return (0, colors_1.colorize)('● med', 'yellow');
|
|
83
|
+
return (0, colors_1.colorize)('● low', 'gray');
|
|
84
|
+
}
|
|
85
|
+
function printReport(report, workspaces, analyzed) {
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log((0, colors_1.colorize)(' Reflection', 'bright') + (0, colors_1.colorize)(` (${analyzed} sessions across ${workspaces} workspaces)`, 'dim'));
|
|
88
|
+
console.log((0, colors_1.colorize)(' ' + '─'.repeat(56), 'dim'));
|
|
89
|
+
if (report.summary) {
|
|
90
|
+
console.log('\n ' + report.summary.replace(/\n/g, '\n '));
|
|
91
|
+
}
|
|
92
|
+
if (report.recommendations.length === 0) {
|
|
93
|
+
console.log('\n ' + (0, colors_1.colorize)('No specific recommendations — looks solid.', 'green') + '\n');
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
// High priority first.
|
|
97
|
+
const order = { high: 0, medium: 1, low: 2 };
|
|
98
|
+
const recs = [...report.recommendations].sort((a, b) => order[a.priority] - order[b.priority]);
|
|
99
|
+
console.log('');
|
|
100
|
+
for (const r of recs) {
|
|
101
|
+
const target = r.target ? (0, colors_1.colorize)(` [${r.target}]`, 'cyan') : '';
|
|
102
|
+
console.log(` ${priorityBadge(r.priority)} ${(0, colors_1.colorize)(KIND_LABEL[r.kind], 'bright')} ${r.title}${target}`);
|
|
103
|
+
if (r.detail)
|
|
104
|
+
console.log(` ${r.detail.replace(/\n/g, '\n ')}`);
|
|
105
|
+
if (r.example) {
|
|
106
|
+
console.log((0, colors_1.colorize)(' example:', 'dim'));
|
|
107
|
+
console.log((0, colors_1.colorize)(r.example.replace(/^/gm, ' '), 'dim'));
|
|
108
|
+
}
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/program.js
CHANGED
|
@@ -88,6 +88,8 @@ const ghq_status_1 = require("./commands/ghq-status");
|
|
|
88
88
|
const save_context_1 = require("./commands/save-context");
|
|
89
89
|
const migrate_1 = require("./commands/migrate");
|
|
90
90
|
const report_bug_1 = require("./commands/report-bug");
|
|
91
|
+
const completion_1 = require("./commands/completion");
|
|
92
|
+
const reflect_1 = require("./commands/reflect");
|
|
91
93
|
(0, create_1.registerCreateCommand)(exports.program);
|
|
92
94
|
(0, list_1.registerListCommand)(exports.program);
|
|
93
95
|
(0, update_1.registerUpdateCommand)(exports.program);
|
|
@@ -111,6 +113,8 @@ const report_bug_1 = require("./commands/report-bug");
|
|
|
111
113
|
(0, save_context_1.registerSaveContextCommand)(exports.program);
|
|
112
114
|
(0, migrate_1.registerMigrateCommand)(exports.program);
|
|
113
115
|
(0, report_bug_1.registerReportBugCommand)(exports.program);
|
|
116
|
+
(0, completion_1.registerCompletionCommand)(exports.program);
|
|
117
|
+
(0, reflect_1.registerReflectCommand)(exports.program);
|
|
114
118
|
// Register TUI (delegates to existing Ink/React implementation)
|
|
115
119
|
exports.program
|
|
116
120
|
.command('tui')
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runAgentRaw = runAgentRaw;
|
|
4
|
+
exports.runAgentJson = runAgentJson;
|
|
5
|
+
exports.parseAgentJson = parseAgentJson;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const agent_config_1 = require("./agent-config");
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = 180000; // judging N transcripts is heavier than extraction
|
|
9
|
+
const DEFAULT_MAX_BUFFER = 32 * 1024 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Invoke the agent with `prompt` and return the raw stdout. Throws a clear error
|
|
12
|
+
* on timeout / non-zero exit. Kept separate from parsing so callers can inspect
|
|
13
|
+
* raw output (e.g. `--dry-run`, debugging).
|
|
14
|
+
*/
|
|
15
|
+
function runAgentRaw(prompt, opts = {}) {
|
|
16
|
+
const agentType = opts.agentType ?? (0, agent_config_1.getPrimaryAgent)().type;
|
|
17
|
+
const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
18
|
+
const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
|
|
19
|
+
const exec = opts.exec ??
|
|
20
|
+
((cmd, args, o) => (0, child_process_1.execFileSync)(cmd, args, { encoding: 'utf-8', timeout: o.timeout, maxBuffer: o.maxBuffer }));
|
|
21
|
+
const attempt = (cmd, args) => exec(cmd, args, { timeout, maxBuffer });
|
|
22
|
+
try {
|
|
23
|
+
if (agentType === 'claude') {
|
|
24
|
+
const preferred = ['-p', prompt, '--output-format', 'json', '--bare', '--strict-mcp-config', '--disable-slash-commands'];
|
|
25
|
+
if (opts.schema)
|
|
26
|
+
preferred.push('--json-schema', opts.schema);
|
|
27
|
+
try {
|
|
28
|
+
return attempt('claude', preferred);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Older claude may reject the newer flags — fall back to the plainest form.
|
|
32
|
+
return attempt('claude', ['-p', prompt]);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (agentType === 'opencode') {
|
|
36
|
+
return attempt('opencode', ['run', prompt]);
|
|
37
|
+
}
|
|
38
|
+
// pi (and any other): run as lean as possible so a bloated env can't hang it.
|
|
39
|
+
const piLean = ['--no-extensions', '--no-skills', '--no-prompt-templates', '--no-context-files', '--no-tools', '--no-session'];
|
|
40
|
+
try {
|
|
41
|
+
return attempt('pi', [...piLean, '-p', prompt]);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return attempt('pi', ['-p', prompt]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const detail = (err?.stderr || err?.stdout || err?.message || 'unknown error').toString().trim().slice(0, 500);
|
|
49
|
+
throw new Error(`agent judge failed (${agentType}): ${detail}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run the agent and parse its reply as JSON, tolerating the shapes different
|
|
54
|
+
* agents emit: `{ structured_output }`, `{ result: "<json>" }`, a ```json fence,
|
|
55
|
+
* or a bare object. Returns `unknown`; callers validate/normalize their shape.
|
|
56
|
+
*/
|
|
57
|
+
function runAgentJson(prompt, opts = {}) {
|
|
58
|
+
const raw = runAgentRaw(prompt, opts);
|
|
59
|
+
return parseAgentJson(raw);
|
|
60
|
+
}
|
|
61
|
+
/** Extract a JSON object from an agent's raw stdout. Exported for tests. */
|
|
62
|
+
function parseAgentJson(raw) {
|
|
63
|
+
let text = raw.trim();
|
|
64
|
+
const fence = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
65
|
+
if (fence)
|
|
66
|
+
text = fence[1].trim();
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(text);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Last resort: grab the outermost {...} span.
|
|
73
|
+
const span = text.match(/\{[\s\S]*\}/);
|
|
74
|
+
if (!span)
|
|
75
|
+
throw new Error('agent did not return JSON');
|
|
76
|
+
parsed = JSON.parse(span[0]);
|
|
77
|
+
}
|
|
78
|
+
// Unwrap the common agent envelopes.
|
|
79
|
+
if (parsed && typeof parsed === 'object') {
|
|
80
|
+
if (parsed.structured_output && typeof parsed.structured_output === 'object')
|
|
81
|
+
return parsed.structured_output;
|
|
82
|
+
if (typeof parsed.result === 'string') {
|
|
83
|
+
try {
|
|
84
|
+
return JSON.parse(parsed.result);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
/* fall through — result was plain text, return the envelope */
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (parsed.result && typeof parsed.result === 'object')
|
|
91
|
+
return parsed.result;
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|