@nyxa/nyx-agent 0.5.0 → 0.6.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/README.md +11 -2
- package/dist/cli.js +3 -0
- package/dist/commands/init.js +30 -1
- package/dist/commands/run.js +3 -1
- package/dist/commands/update.js +1 -0
- package/dist/config/loadConfig.js +1 -0
- package/dist/config/schema.js +2 -0
- package/dist/runtime/files.js +1 -0
- package/dist/runtime/git.js +1 -0
- package/dist/runtime/ledger.js +1 -0
- package/dist/runtime/paths.js +1 -0
- package/dist/runtime/runPipeline.js +108 -12
- package/dist/runtime/scm.js +6 -2
- package/dist/runtime/selectionConfirmation.js +85 -0
- package/dist/runtime/time.js +1 -0
- package/dist/runtime/validateResult.js +1 -0
- package/dist/runtime/workItems.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,7 +7,8 @@ GitHub issue at a time, each phase with fresh context.
|
|
|
7
7
|
|
|
8
8
|
For every run NyxAgent:
|
|
9
9
|
|
|
10
|
-
1. **Selects** open GitHub issues to work on (read-only)
|
|
10
|
+
1. **Selects** open GitHub issues to work on (read-only), then asks the user to
|
|
11
|
+
confirm the proposed checklist.
|
|
11
12
|
2. For each selected issue, in an isolated git **worktree**:
|
|
12
13
|
- **implements** it (the agent — the only customizable prompt),
|
|
13
14
|
- optionally **reviews** and **revises** it (bounded loop),
|
|
@@ -27,7 +28,8 @@ is editable.
|
|
|
27
28
|
|
|
28
29
|
```bash
|
|
29
30
|
nyxagent init # create .nyxagent/config.json (interactive)
|
|
30
|
-
nyxagent run # run the pipeline
|
|
31
|
+
nyxagent run # run the pipeline, confirming selected work items
|
|
32
|
+
nyxagent run --yes # accept the agent selection without prompting
|
|
31
33
|
nyxagent run --harness claude # override the configured harness for one run
|
|
32
34
|
nyxagent update # self-update to the latest published version
|
|
33
35
|
```
|
|
@@ -42,6 +44,7 @@ nyxagent update # self-update to the latest published version
|
|
|
42
44
|
"model": "gpt-5.5",
|
|
43
45
|
"reasoning_effort": "medium",
|
|
44
46
|
"review": "each",
|
|
47
|
+
"review_max_attempts": 4,
|
|
45
48
|
"tracker": { "type": "github", "repo": "owner/repo" },
|
|
46
49
|
"base_branch": "main",
|
|
47
50
|
"max_iterations": 5
|
|
@@ -50,10 +53,16 @@ nyxagent update # self-update to the latest published version
|
|
|
50
53
|
|
|
51
54
|
- `harness`: `codex` or `claude` (override per run with `--harness`).
|
|
52
55
|
- `review`: `each` (per task), `all` (global only), `both`, or `none`.
|
|
56
|
+
- `review_max_attempts`: review+revise rounds per stage before the run fails (default 4).
|
|
53
57
|
- `base_branch`: optional; defaults to the current branch at run time.
|
|
54
58
|
|
|
59
|
+
If a run fails review after exhausting its attempts but has already produced
|
|
60
|
+
commits, NyxAgent pushes the branch and opens a **draft** pull request with the
|
|
61
|
+
unresolved feedback, so the work is never stranded on an orphaned branch.
|
|
62
|
+
|
|
55
63
|
## Requirements
|
|
56
64
|
|
|
57
65
|
- A git repository with a GitHub remote.
|
|
58
66
|
- The `gh` CLI authenticated for the configured repository.
|
|
59
67
|
- The selected harness CLI (`codex` or `claude`) on `PATH`.
|
|
68
|
+
- An interactive terminal for `nyxagent run`, unless `--yes` is used.
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/** CLI entry point: declares the `init`, `run`, and `update` commands and dispatches them. */
|
|
2
3
|
import { Command } from "commander";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import pc from "picocolors";
|
|
@@ -19,6 +20,7 @@ program
|
|
|
19
20
|
.option("--model <name>", "model name")
|
|
20
21
|
.option("--reasoning-effort <level>", "reasoning effort (default: medium)")
|
|
21
22
|
.option("--review <mode>", "review strategy: each, all, both, or none")
|
|
23
|
+
.option("--review-attempts <count>", "max review attempts per stage (default: 4)")
|
|
22
24
|
.option("--repo <owner/repo>", "GitHub repository")
|
|
23
25
|
.option("--base-branch <branch>", "base branch (default: current branch)")
|
|
24
26
|
.option("--max-iterations <count>", "maximum work items per run")
|
|
@@ -31,6 +33,7 @@ program
|
|
|
31
33
|
.description("Run the NyxAgent pipeline")
|
|
32
34
|
.option("--config <path>", "config path (default: .nyxagent/config.json)")
|
|
33
35
|
.option("--harness <name>", "override the configured harness: codex or claude")
|
|
36
|
+
.option("-y, --yes", "accept the agent-selected work items without prompting")
|
|
34
37
|
.action(async (options) => {
|
|
35
38
|
await runCommand(options);
|
|
36
39
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** `nyxagent init`: scaffolds .nyxagent/config.json, the editable execution prompt, and .gitignore entries. */
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { input, number as numberPrompt, select } from "@inquirer/prompts";
|
|
3
4
|
import pc from "picocolors";
|
|
@@ -7,10 +8,16 @@ import { getNyxDir, relativeToProject } from "../runtime/paths.js";
|
|
|
7
8
|
import { EXECUTION_PROMPT_FILE } from "../runtime/prompts.js";
|
|
8
9
|
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
9
10
|
const GITIGNORE_MARKER = "# NyxAgent runtime artifacts";
|
|
11
|
+
// Everything under .nyxagent/ is NyxAgent's own plumbing. Ignoring it keeps it
|
|
12
|
+
// out of the agent's worktree checkout and out of the commits/diffs it reviews,
|
|
13
|
+
// so the agent can never accidentally edit (or review) NyxAgent's own files.
|
|
10
14
|
const GITIGNORE_ENTRIES = [
|
|
11
15
|
".nyxagent/runs/",
|
|
12
16
|
".nyxagent/worktrees/",
|
|
13
|
-
".nyxagent/state.json"
|
|
17
|
+
".nyxagent/state.json",
|
|
18
|
+
".nyxagent/config.json",
|
|
19
|
+
".nyxagent/config.toml",
|
|
20
|
+
".nyxagent/prompts/"
|
|
14
21
|
];
|
|
15
22
|
export async function initCommand(options, projectRoot = process.cwd()) {
|
|
16
23
|
const root = path.resolve(projectRoot);
|
|
@@ -62,6 +69,17 @@ async function resolveInitOptions(options) {
|
|
|
62
69
|
],
|
|
63
70
|
default: "each"
|
|
64
71
|
});
|
|
72
|
+
const review_max_attempts = review === "none"
|
|
73
|
+
? 4
|
|
74
|
+
: parseReviewAttempts(options.reviewAttempts) ??
|
|
75
|
+
(await numberPrompt({
|
|
76
|
+
message: "Max review attempts per stage",
|
|
77
|
+
default: 4,
|
|
78
|
+
required: true
|
|
79
|
+
}));
|
|
80
|
+
if (!Number.isInteger(review_max_attempts) || review_max_attempts <= 0) {
|
|
81
|
+
throw new Error("review attempts must be a positive integer");
|
|
82
|
+
}
|
|
65
83
|
const repo = options.repo ?? (await input({ message: "GitHub repository (owner/repo)" }));
|
|
66
84
|
validateRepository(repo);
|
|
67
85
|
const baseBranchInput = options.baseBranch ??
|
|
@@ -84,6 +102,7 @@ async function resolveInitOptions(options) {
|
|
|
84
102
|
model: model.trim(),
|
|
85
103
|
reasoning_effort: reasoning_effort.trim() || "medium",
|
|
86
104
|
review,
|
|
105
|
+
review_max_attempts,
|
|
87
106
|
repo,
|
|
88
107
|
base_branch,
|
|
89
108
|
max_iterations
|
|
@@ -98,6 +117,10 @@ function buildConfig(options) {
|
|
|
98
117
|
tracker: { type: "github", repo: options.repo },
|
|
99
118
|
max_iterations: options.max_iterations
|
|
100
119
|
};
|
|
120
|
+
// No point persisting an attempts cap when reviews are disabled.
|
|
121
|
+
if (options.review !== "none") {
|
|
122
|
+
config.review_max_attempts = options.review_max_attempts;
|
|
123
|
+
}
|
|
101
124
|
if (options.base_branch) {
|
|
102
125
|
config.base_branch = options.base_branch;
|
|
103
126
|
}
|
|
@@ -126,6 +149,12 @@ function parseMaxIterations(value) {
|
|
|
126
149
|
}
|
|
127
150
|
return Number.parseInt(value, 10);
|
|
128
151
|
}
|
|
152
|
+
function parseReviewAttempts(value) {
|
|
153
|
+
if (value === undefined) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return Number.parseInt(value, 10);
|
|
157
|
+
}
|
|
129
158
|
async function ensureGitignoreEntries(root) {
|
|
130
159
|
const gitignorePath = path.join(root, ".gitignore");
|
|
131
160
|
const current = (await pathExists(gitignorePath))
|
package/dist/commands/run.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/** `nyxagent run`: normalizes CLI options and hands off to the pipeline runner. */
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { harnessNames } from "../config/schema.js";
|
|
3
4
|
import { runPipeline } from "../runtime/runPipeline.js";
|
|
@@ -7,7 +8,8 @@ export async function runCommand(options, projectRoot = process.cwd()) {
|
|
|
7
8
|
configPath: options.config
|
|
8
9
|
? path.resolve(projectRoot, options.config)
|
|
9
10
|
: undefined,
|
|
10
|
-
harness: normalizeHarness(options.harness)
|
|
11
|
+
harness: normalizeHarness(options.harness),
|
|
12
|
+
autoAcceptSelection: options.yes ?? false
|
|
11
13
|
});
|
|
12
14
|
}
|
|
13
15
|
function normalizeHarness(value) {
|
package/dist/commands/update.js
CHANGED
package/dist/config/schema.js
CHANGED
|
@@ -18,6 +18,8 @@ export const nyxConfigSchema = z
|
|
|
18
18
|
reasoning_effort: z.string().min(1).default("medium"),
|
|
19
19
|
/** When the agent reviews its own work. */
|
|
20
20
|
review: z.enum(reviewModes).default("each"),
|
|
21
|
+
/** How many review+revise rounds a review stage gets before the run fails. */
|
|
22
|
+
review_max_attempts: z.number().int().positive().default(4),
|
|
21
23
|
/** Work item tracker. GitHub issues only in this version. */
|
|
22
24
|
tracker: z.object({
|
|
23
25
|
type: z.literal("github"),
|
package/dist/runtime/files.js
CHANGED
package/dist/runtime/git.js
CHANGED
package/dist/runtime/ledger.js
CHANGED
package/dist/runtime/paths.js
CHANGED
|
@@ -9,17 +9,18 @@ import { buildContextBlock, buildPhasePrompt, EXECUTION_PROMPT, GLOBAL_REVIEW_PR
|
|
|
9
9
|
import { runAgentPhase } from "./runPhase.js";
|
|
10
10
|
import { GLOBAL_REVIEW_SCHEMA, REVIEW_SCHEMA, SELECTION_SCHEMA } from "./schemas.js";
|
|
11
11
|
import { commitAll, commitsAhead, createPullRequest, pushBranch, rangeDiff, stageAllAndDiff } from "./scm.js";
|
|
12
|
+
import { confirmWorkItemSelection } from "./selectionConfirmation.js";
|
|
12
13
|
import { createRunId } from "./time.js";
|
|
13
14
|
import { filterAvailable, listGitHubIssues, resolveSelectedQueue } from "./workItems.js";
|
|
14
15
|
const MAX_CANDIDATES = 50;
|
|
15
16
|
const EXCERPT_CHARS = 800;
|
|
16
|
-
const REVIEW_MAX_ATTEMPTS = 3;
|
|
17
17
|
export function defaultPipelineDependencies() {
|
|
18
18
|
return {
|
|
19
19
|
listIssues: listGitHubIssues,
|
|
20
20
|
runPhase: runAgentPhase,
|
|
21
21
|
pushBranch,
|
|
22
|
-
createPullRequest
|
|
22
|
+
createPullRequest,
|
|
23
|
+
confirmSelection: confirmWorkItemSelection
|
|
23
24
|
};
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
@@ -56,7 +57,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
56
57
|
console.log("No open work items available. Nothing to do.");
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
const
|
|
60
|
+
const proposed = await runSelection({
|
|
60
61
|
projectRoot,
|
|
61
62
|
runDir,
|
|
62
63
|
harness,
|
|
@@ -64,10 +65,20 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
64
65
|
candidates,
|
|
65
66
|
runPhase: deps.runPhase
|
|
66
67
|
});
|
|
67
|
-
if (
|
|
68
|
+
if (proposed.length === 0) {
|
|
68
69
|
console.log("Selection chose no work items. Nothing to do.");
|
|
69
70
|
return;
|
|
70
71
|
}
|
|
72
|
+
const selected = await deps.confirmSelection({
|
|
73
|
+
candidates,
|
|
74
|
+
proposed,
|
|
75
|
+
maxItems: config.max_iterations,
|
|
76
|
+
autoAccept: input.autoAcceptSelection ?? false
|
|
77
|
+
});
|
|
78
|
+
if (selected.length === 0) {
|
|
79
|
+
console.log("No work items selected. Nothing to do.");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
71
82
|
const planned = selected.slice(0, config.max_iterations);
|
|
72
83
|
console.log(`Selected ${planned.length} work item(s):`);
|
|
73
84
|
for (const item of planned) {
|
|
@@ -106,6 +117,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
106
117
|
git,
|
|
107
118
|
harness,
|
|
108
119
|
config,
|
|
120
|
+
maxAttempts: config.review_max_attempts,
|
|
109
121
|
runPhase: deps.runPhase
|
|
110
122
|
});
|
|
111
123
|
}
|
|
@@ -132,6 +144,7 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
132
144
|
git,
|
|
133
145
|
harness,
|
|
134
146
|
config,
|
|
147
|
+
maxAttempts: config.review_max_attempts,
|
|
135
148
|
runPhase: deps.runPhase
|
|
136
149
|
});
|
|
137
150
|
if (corrections) {
|
|
@@ -155,6 +168,21 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
155
168
|
console.log(pc.green(`\nPull request opened: ${prUrl}`));
|
|
156
169
|
success = true;
|
|
157
170
|
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
// A failed run that already produced commits is salvaged into a draft PR so
|
|
173
|
+
// the work is never stranded on an orphaned branch; the error still
|
|
174
|
+
// propagates so the exit code reflects the failure.
|
|
175
|
+
await salvageFailedRun({
|
|
176
|
+
error,
|
|
177
|
+
projectRoot,
|
|
178
|
+
git,
|
|
179
|
+
producedCommits,
|
|
180
|
+
completed,
|
|
181
|
+
config,
|
|
182
|
+
deps
|
|
183
|
+
});
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
158
186
|
finally {
|
|
159
187
|
if (success) {
|
|
160
188
|
await removeRunWorktree({ projectRoot, worktree: git.worktree });
|
|
@@ -162,9 +190,54 @@ export async function runPipeline(input = {}, deps = defaultPipelineDependencies
|
|
|
162
190
|
await deleteBranch({ projectRoot, branch: git.branch });
|
|
163
191
|
}
|
|
164
192
|
}
|
|
165
|
-
|
|
166
|
-
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Failure handling that preserves work. If the run produced commits, push the
|
|
197
|
+
* branch and open a DRAFT pull request describing why it failed, so a human can
|
|
198
|
+
* finish it. Otherwise just keep the branch/worktree for debugging. The branch
|
|
199
|
+
* and worktree are always kept on failure.
|
|
200
|
+
*/
|
|
201
|
+
async function salvageFailedRun(input) {
|
|
202
|
+
const location = relativeToProject(input.projectRoot, input.git.worktree);
|
|
203
|
+
// Best-effort: never let salvage throw and mask the original failure.
|
|
204
|
+
let ahead = 0;
|
|
205
|
+
if (input.producedCommits) {
|
|
206
|
+
try {
|
|
207
|
+
ahead = await commitsAhead(input.git.worktree, input.git.base);
|
|
167
208
|
}
|
|
209
|
+
catch {
|
|
210
|
+
ahead = 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (ahead === 0) {
|
|
214
|
+
console.log(pc.red(`\nRun failed. Branch ${input.git.branch} and worktree kept for debugging: ${location}`));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const reason = input.error instanceof Error ? input.error.message : String(input.error);
|
|
218
|
+
try {
|
|
219
|
+
await input.deps.pushBranch({
|
|
220
|
+
cwd: input.git.worktree,
|
|
221
|
+
branch: input.git.branch
|
|
222
|
+
});
|
|
223
|
+
const url = await input.deps.createPullRequest({
|
|
224
|
+
cwd: input.git.worktree,
|
|
225
|
+
repo: input.config.tracker.repo,
|
|
226
|
+
base: input.git.base,
|
|
227
|
+
head: input.git.branch,
|
|
228
|
+
title: buildDraftPrTitle(input.completed),
|
|
229
|
+
body: buildDraftPrBody(input.completed, reason),
|
|
230
|
+
draft: true
|
|
231
|
+
});
|
|
232
|
+
console.log(pc.yellow(`\nRun failed, but the work was salvaged into a DRAFT pull request: ${url}`));
|
|
233
|
+
console.log(pc.yellow(`Branch ${input.git.branch} and worktree kept: ${location}`));
|
|
234
|
+
}
|
|
235
|
+
catch (salvageError) {
|
|
236
|
+
const detail = salvageError instanceof Error
|
|
237
|
+
? salvageError.message
|
|
238
|
+
: String(salvageError);
|
|
239
|
+
console.log(pc.red(`\nRun failed, and salvaging the work into a draft pull request also failed: ${detail}`));
|
|
240
|
+
console.log(pc.red(`Branch ${input.git.branch} and worktree kept for debugging: ${location}`));
|
|
168
241
|
}
|
|
169
242
|
}
|
|
170
243
|
async function runSelection(input) {
|
|
@@ -235,7 +308,7 @@ async function runExecution(input) {
|
|
|
235
308
|
}
|
|
236
309
|
}
|
|
237
310
|
async function runReviewLoop(input) {
|
|
238
|
-
for (let attempt = 1; attempt <=
|
|
311
|
+
for (let attempt = 1; attempt <= input.maxAttempts; attempt += 1) {
|
|
239
312
|
const diff = await stageAllAndDiff(input.git.worktree);
|
|
240
313
|
const reviewResult = await input.runPhase({
|
|
241
314
|
phaseId: "review",
|
|
@@ -263,8 +336,8 @@ async function runReviewLoop(input) {
|
|
|
263
336
|
if (review.outcome === "approved") {
|
|
264
337
|
return;
|
|
265
338
|
}
|
|
266
|
-
if (attempt ===
|
|
267
|
-
throw new Error(`Review for #${input.item.number} not approved after ${
|
|
339
|
+
if (attempt === input.maxAttempts) {
|
|
340
|
+
throw new Error(`Review for #${input.item.number} not approved after ${input.maxAttempts} attempts: ${review.summary}${formatRequiredChanges(review.required_changes)}`);
|
|
268
341
|
}
|
|
269
342
|
const revision = await input.runPhase({
|
|
270
343
|
phaseId: "revision",
|
|
@@ -289,7 +362,7 @@ async function runReviewLoop(input) {
|
|
|
289
362
|
}
|
|
290
363
|
async function runGlobalReviewLoop(input) {
|
|
291
364
|
let committedCorrections = false;
|
|
292
|
-
for (let attempt = 1; attempt <=
|
|
365
|
+
for (let attempt = 1; attempt <= input.maxAttempts; attempt += 1) {
|
|
293
366
|
const diff = await rangeDiff(input.git.worktree, input.git.base);
|
|
294
367
|
const reviewResult = await input.runPhase({
|
|
295
368
|
phaseId: "global_review",
|
|
@@ -320,8 +393,8 @@ async function runGlobalReviewLoop(input) {
|
|
|
320
393
|
if (review.outcome === "approved") {
|
|
321
394
|
return committedCorrections;
|
|
322
395
|
}
|
|
323
|
-
if (attempt ===
|
|
324
|
-
throw new Error(`Global review not approved after ${
|
|
396
|
+
if (attempt === input.maxAttempts) {
|
|
397
|
+
throw new Error(`Global review not approved after ${input.maxAttempts} attempts: ${review.summary}${formatRequiredChanges(review.required_changes)}`);
|
|
325
398
|
}
|
|
326
399
|
const revision = await input.runPhase({
|
|
327
400
|
phaseId: "global_revision",
|
|
@@ -393,3 +466,26 @@ function buildPrBody(items) {
|
|
|
393
466
|
closes
|
|
394
467
|
].join("\n");
|
|
395
468
|
}
|
|
469
|
+
function buildDraftPrTitle(items) {
|
|
470
|
+
return `[Draft] ${buildPrTitle(items)}`;
|
|
471
|
+
}
|
|
472
|
+
function buildDraftPrBody(items, reason) {
|
|
473
|
+
return [
|
|
474
|
+
"> [!WARNING]",
|
|
475
|
+
"> This pull request was opened automatically by NyxAgent after the run",
|
|
476
|
+
"> **failed review**. The work is preserved here for a human to finish.",
|
|
477
|
+
"",
|
|
478
|
+
`**Why the run failed:** ${reason}`,
|
|
479
|
+
"",
|
|
480
|
+
buildPrBody(items)
|
|
481
|
+
].join("\n");
|
|
482
|
+
}
|
|
483
|
+
/** Render review `required_changes` as a bullet list to append to a failure message. */
|
|
484
|
+
function formatRequiredChanges(changes) {
|
|
485
|
+
if (!changes || changes.length === 0) {
|
|
486
|
+
return "";
|
|
487
|
+
}
|
|
488
|
+
return `\n\nUnresolved review feedback:\n${changes
|
|
489
|
+
.map((change) => `- ${change}`)
|
|
490
|
+
.join("\n")}`;
|
|
491
|
+
}
|
package/dist/runtime/scm.js
CHANGED
|
@@ -54,7 +54,7 @@ export async function pushBranch(input) {
|
|
|
54
54
|
await git(input.cwd, ["push", "-u", "origin", input.branch], "push");
|
|
55
55
|
}
|
|
56
56
|
export async function createPullRequest(input) {
|
|
57
|
-
const
|
|
57
|
+
const args = [
|
|
58
58
|
"pr",
|
|
59
59
|
"create",
|
|
60
60
|
"--repo",
|
|
@@ -67,7 +67,11 @@ export async function createPullRequest(input) {
|
|
|
67
67
|
input.title,
|
|
68
68
|
"--body",
|
|
69
69
|
input.body
|
|
70
|
-
]
|
|
70
|
+
];
|
|
71
|
+
if (input.draft) {
|
|
72
|
+
args.push("--draft");
|
|
73
|
+
}
|
|
74
|
+
const result = await execa("gh", args, { cwd: input.cwd, reject: false });
|
|
71
75
|
if (result.exitCode !== 0) {
|
|
72
76
|
const detail = (result.stderr || result.stdout || "unknown error").trim();
|
|
73
77
|
throw new Error(`gh pr create failed: ${detail}`);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2
|
+
export async function confirmWorkItemSelection(input) {
|
|
3
|
+
if (input.autoAccept) {
|
|
4
|
+
return input.proposed;
|
|
5
|
+
}
|
|
6
|
+
const isInteractive = input.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
7
|
+
if (!isInteractive) {
|
|
8
|
+
throw new Error('Interactive work item selection requires a TTY. Re-run with "nyxagent run --yes" to accept the agent selection.');
|
|
9
|
+
}
|
|
10
|
+
const selectedKeys = await checkbox({
|
|
11
|
+
message: `Select work items to run (max ${input.maxItems})`,
|
|
12
|
+
choices: buildSelectionChoiceItems(input).map(toInquirerChoice),
|
|
13
|
+
pageSize: Math.min(Math.max(input.candidates.length, 7), 20),
|
|
14
|
+
required: false,
|
|
15
|
+
validate: (selected) => selected.length <= input.maxItems ||
|
|
16
|
+
`Select at most ${input.maxItems} work item(s).`,
|
|
17
|
+
shortcuts: {
|
|
18
|
+
all: null,
|
|
19
|
+
invert: null
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const selected = new Set(selectedKeys);
|
|
23
|
+
return input.candidates.filter((candidate) => selected.has(candidate.key));
|
|
24
|
+
}
|
|
25
|
+
export function buildSelectionChoiceItems(input) {
|
|
26
|
+
const proposedKeys = new Set(input.proposed.map((item) => item.key));
|
|
27
|
+
const items = [];
|
|
28
|
+
let currentGroup;
|
|
29
|
+
for (const candidate of input.candidates) {
|
|
30
|
+
const group = detectPlanGroup(candidate);
|
|
31
|
+
if (group && group !== currentGroup) {
|
|
32
|
+
items.push({ type: "separator", label: group });
|
|
33
|
+
}
|
|
34
|
+
currentGroup = group;
|
|
35
|
+
const proposed = proposedKeys.has(candidate.key);
|
|
36
|
+
items.push({
|
|
37
|
+
type: "choice",
|
|
38
|
+
value: candidate.key,
|
|
39
|
+
name: `#${candidate.number} ${candidate.title}${proposed ? " (agent)" : ""}`,
|
|
40
|
+
checked: proposed
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return items;
|
|
44
|
+
}
|
|
45
|
+
function toInquirerChoice(item) {
|
|
46
|
+
if (item.type === "separator") {
|
|
47
|
+
return new Separator(item.label);
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
value: item.value,
|
|
51
|
+
name: item.name,
|
|
52
|
+
checked: item.checked
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function detectPlanGroup(candidate) {
|
|
56
|
+
for (const label of candidate.labels ?? []) {
|
|
57
|
+
const group = parseGroupLabel(label);
|
|
58
|
+
if (group) {
|
|
59
|
+
return group;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return parseBracketedTitleGroup(candidate.title);
|
|
63
|
+
}
|
|
64
|
+
function parseGroupLabel(label) {
|
|
65
|
+
const match = /^(plan|prd)\s*[:/=-]\s*(.+)$/i.exec(label.trim());
|
|
66
|
+
if (!match) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return formatGroupLabel(match[1], match[2]);
|
|
70
|
+
}
|
|
71
|
+
function parseBracketedTitleGroup(title) {
|
|
72
|
+
const match = /^\[(plan|prd)\s*[:/=-]\s*([^\]]+)\]/i.exec(title.trim());
|
|
73
|
+
if (!match) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return formatGroupLabel(match[1], match[2]);
|
|
77
|
+
}
|
|
78
|
+
function formatGroupLabel(kind, rawName) {
|
|
79
|
+
const name = rawName.trim();
|
|
80
|
+
if (!name) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const prefix = kind.toLowerCase() === "prd" ? "PRD" : "Plan";
|
|
84
|
+
return `${prefix}: ${name}`;
|
|
85
|
+
}
|
package/dist/runtime/time.js
CHANGED