@nyxa/nyx-agent 0.3.0 → 0.3.2
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/dist/runtime/buildPrompt.js +10 -0
- package/dist/runtime/runPhase.js +16 -1
- package/dist/runtime/runWorkflow.js +347 -49
- package/dist/runtime/validateWorkItem.js +56 -0
- package/docs/nyxagent-v0-spec.md +45 -22
- package/package.json +1 -1
- package/templates/default/prompts/selection.md +22 -11
- package/templates/default/schemas/selection.schema.json +51 -7
|
@@ -41,6 +41,16 @@ async function buildRuntimeContract(input) {
|
|
|
41
41
|
JSON.stringify(input.context.available_work_items ?? [], null, 2),
|
|
42
42
|
"```",
|
|
43
43
|
"",
|
|
44
|
+
"Recommended work item queue:",
|
|
45
|
+
"```json",
|
|
46
|
+
JSON.stringify(input.context.recommended_work_item_queue ?? [], null, 2),
|
|
47
|
+
"```",
|
|
48
|
+
"",
|
|
49
|
+
"Selected work item queue:",
|
|
50
|
+
"```json",
|
|
51
|
+
JSON.stringify(input.context.selected_work_item_queue ?? [], null, 2),
|
|
52
|
+
"```",
|
|
53
|
+
"",
|
|
44
54
|
"Seen work item keys:",
|
|
45
55
|
"```json",
|
|
46
56
|
JSON.stringify(input.context.seen_work_item_keys ?? [], null, 2),
|
package/dist/runtime/runPhase.js
CHANGED
|
@@ -8,7 +8,7 @@ import { parseNyxAgentResult, getOutcome } from "./parseResult.js";
|
|
|
8
8
|
import { resolveNyxPath } from "./paths.js";
|
|
9
9
|
import { renderTemplate } from "./renderTemplate.js";
|
|
10
10
|
import { validateAgainstSchema } from "./validateResult.js";
|
|
11
|
-
import { validateWorkItemIdentity } from "./validateWorkItem.js";
|
|
11
|
+
import { validateWorkItemIdentity, validateWorkItemQueue } from "./validateWorkItem.js";
|
|
12
12
|
export async function runPhase(input) {
|
|
13
13
|
const phaseDir = path.join(input.iterationDir, "phases", input.phase.id);
|
|
14
14
|
await ensureDir(phaseDir);
|
|
@@ -83,6 +83,8 @@ function buildContext(input) {
|
|
|
83
83
|
workflow: input.config.workflow,
|
|
84
84
|
work_items: input.config.work_items ?? {},
|
|
85
85
|
available_work_items: input.state.available_work_items ?? [],
|
|
86
|
+
recommended_work_item_queue: input.state.recommended_work_item_queue ?? [],
|
|
87
|
+
selected_work_item_queue: input.state.selected_work_item_queue ?? [],
|
|
86
88
|
work_item: input.state.work_item ?? {},
|
|
87
89
|
seen_work_item_keys: input.state.seen_work_item_keys ?? [],
|
|
88
90
|
completed_work_item_keys: input.state.completed_work_item_keys ?? [],
|
|
@@ -190,6 +192,19 @@ async function parseAndValidatePhaseResult(input) {
|
|
|
190
192
|
return workItemValidation;
|
|
191
193
|
}
|
|
192
194
|
}
|
|
195
|
+
const workItems = readObjectProperty(parsed.value, "work_items");
|
|
196
|
+
if (workItems !== undefined) {
|
|
197
|
+
const workItemQueueValidation = validateWorkItemQueue({
|
|
198
|
+
config: input.input.config,
|
|
199
|
+
workItems,
|
|
200
|
+
availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
|
|
201
|
+
seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
|
|
202
|
+
completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys)
|
|
203
|
+
});
|
|
204
|
+
if (!workItemQueueValidation.ok) {
|
|
205
|
+
return workItemQueueValidation;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
193
208
|
await writeJson(path.join(input.phaseDir, "result.json"), parsed.value);
|
|
194
209
|
return {
|
|
195
210
|
ok: true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2
3
|
import pc from "picocolors";
|
|
3
4
|
import { loadConfig } from "../config/loadConfig.js";
|
|
4
5
|
import { ensureDir, writeJson } from "./files.js";
|
|
@@ -7,6 +8,7 @@ import { markWorkItemCompleted, readWorkItemLedger, writeWorkItemLedger } from "
|
|
|
7
8
|
import { getNyxDir, relativeToProject } from "./paths.js";
|
|
8
9
|
import { runPhase } from "./runPhase.js";
|
|
9
10
|
import { createRunId } from "./time.js";
|
|
11
|
+
import { validateWorkItemQueue } from "./validateWorkItem.js";
|
|
10
12
|
import { filterAvailableWorkItems, listWorkItemCandidates } from "./workItems.js";
|
|
11
13
|
export async function runWorkflow(options) {
|
|
12
14
|
const projectRoot = path.resolve(options.projectRoot);
|
|
@@ -24,6 +26,11 @@ export async function runWorkflow(options) {
|
|
|
24
26
|
status: "running",
|
|
25
27
|
current_iteration: 0,
|
|
26
28
|
completed_iterations: 0,
|
|
29
|
+
available_work_items: [],
|
|
30
|
+
recommended_work_item_queue: [],
|
|
31
|
+
selected_work_item_queue: [],
|
|
32
|
+
skipped_work_item_keys: [],
|
|
33
|
+
selection_groups: [],
|
|
27
34
|
seen_work_item_keys: [],
|
|
28
35
|
completed_work_item_keys: [...ledger.completed_work_item_keys],
|
|
29
36
|
last_completed_work_item: ledger.last_completed_work_item
|
|
@@ -46,21 +53,72 @@ export async function runWorkflow(options) {
|
|
|
46
53
|
console.log(`Model: ${config.model.name}`);
|
|
47
54
|
console.log("");
|
|
48
55
|
const phasesById = new Map(config.phases.map((phase) => [phase.id, phase]));
|
|
49
|
-
|
|
56
|
+
const selection = await runSelectionPhase({
|
|
57
|
+
projectRoot,
|
|
58
|
+
runDir,
|
|
59
|
+
config,
|
|
60
|
+
phasesById,
|
|
61
|
+
runState,
|
|
62
|
+
ledger
|
|
63
|
+
});
|
|
64
|
+
if (selection.status === "stopped") {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
runState.available_work_items = selection.availableWorkItems;
|
|
68
|
+
runState.recommended_work_item_queue = selection.workItems;
|
|
69
|
+
runState.selection_groups = selection.selectionGroups;
|
|
70
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
71
|
+
let confirmedWorkItems;
|
|
72
|
+
try {
|
|
73
|
+
confirmedWorkItems = normalizeConfirmedWorkItems({
|
|
74
|
+
availableWorkItems: selection.availableWorkItems,
|
|
75
|
+
confirmed: await (options.confirmWorkItems ?? confirmWorkItemsWithCheckbox)({
|
|
76
|
+
availableWorkItems: selection.availableWorkItems,
|
|
77
|
+
recommendedWorkItems: selection.workItems,
|
|
78
|
+
workItems: selection.availableWorkItems,
|
|
79
|
+
selectionGroups: selection.selectionGroups,
|
|
80
|
+
maxIterations: config.workflow.max_iterations
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
runState.status = "failed";
|
|
86
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
const selectedWorkItems = confirmedWorkItems.slice(0, config.workflow.max_iterations);
|
|
90
|
+
const selectedKeys = new Set(selectedWorkItems.map((item) => item.key));
|
|
91
|
+
runState.selected_work_item_queue = selectedWorkItems;
|
|
92
|
+
runState.skipped_work_item_keys = selection.workItems
|
|
93
|
+
.filter((item) => !selectedKeys.has(item.key))
|
|
94
|
+
.map((item) => item.key);
|
|
95
|
+
runState.seen_work_item_keys = selectedWorkItems.map((item) => item.key);
|
|
96
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
97
|
+
if (confirmedWorkItems.length > config.workflow.max_iterations) {
|
|
98
|
+
console.log(pc.yellow(`Only the first ${config.workflow.max_iterations} selected work items will run.`));
|
|
99
|
+
}
|
|
100
|
+
if (selectedWorkItems.length === 0) {
|
|
101
|
+
runState.status = "completed_no_selected_work";
|
|
102
|
+
await writeJson(path.join(runDir, "state.json"), runState);
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(`Done: ${runState.status}`);
|
|
105
|
+
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
for (let queueIndex = 0; queueIndex < selectedWorkItems.length; queueIndex += 1) {
|
|
109
|
+
const iterationNumber = queueIndex + 1;
|
|
110
|
+
const workItem = selectedWorkItems[queueIndex];
|
|
50
111
|
runState.current_iteration = iterationNumber;
|
|
51
112
|
await writeJson(path.join(runDir, "state.json"), runState);
|
|
52
113
|
const iterationDir = path.join(runDir, "iterations", String(iterationNumber).padStart(3, "0"));
|
|
53
114
|
await ensureDir(iterationDir);
|
|
54
|
-
const availableWorkItems = await loadAvailableWorkItems({
|
|
55
|
-
projectRoot,
|
|
56
|
-
config,
|
|
57
|
-
runState,
|
|
58
|
-
ledger
|
|
59
|
-
});
|
|
60
115
|
const iterationState = {
|
|
61
116
|
iteration: iterationNumber,
|
|
62
117
|
status: "running",
|
|
63
|
-
|
|
118
|
+
work_item: workItem,
|
|
119
|
+
available_work_items: selectedWorkItems,
|
|
120
|
+
recommended_work_item_queue: selection.workItems,
|
|
121
|
+
selected_work_item_queue: selectedWorkItems,
|
|
64
122
|
seen_work_item_keys: [...runState.seen_work_item_keys],
|
|
65
123
|
completed_work_item_keys: [...ledger.completed_work_item_keys],
|
|
66
124
|
last_completed_work_item: ledger.last_completed_work_item,
|
|
@@ -69,62 +127,31 @@ export async function runWorkflow(options) {
|
|
|
69
127
|
};
|
|
70
128
|
const iterationStateFile = path.join(iterationDir, "state.json");
|
|
71
129
|
await writeJson(iterationStateFile, iterationState);
|
|
72
|
-
let currentPhaseId =
|
|
130
|
+
let currentPhaseId = selection.nextPhaseId;
|
|
73
131
|
while (currentPhaseId) {
|
|
74
132
|
const phase = phasesById.get(currentPhaseId);
|
|
75
133
|
if (!phase) {
|
|
76
134
|
throw new Error(`Unknown phase "${currentPhaseId}"`);
|
|
77
135
|
}
|
|
78
|
-
const
|
|
79
|
-
iterationState.phase_visit_counts[phase.id] = visits;
|
|
80
|
-
if (visits > phase.max_visits_per_iteration) {
|
|
81
|
-
iterationState.status = "failed";
|
|
82
|
-
runState.status = "failed";
|
|
83
|
-
await writeJson(iterationStateFile, iterationState);
|
|
84
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
85
|
-
throw new Error(`Phase "${phase.id}" exceeded max_visits_per_iteration=${phase.max_visits_per_iteration}`);
|
|
86
|
-
}
|
|
87
|
-
process.stdout.write(`[${iterationNumber}/${config.workflow.max_iterations}] ${phase.id} ... `);
|
|
88
|
-
await writeJson(iterationStateFile, iterationState);
|
|
89
|
-
const phaseResult = await runPhase({
|
|
136
|
+
const phaseResult = await runWorkflowPhase({
|
|
90
137
|
projectRoot,
|
|
91
138
|
runDir,
|
|
92
139
|
iterationDir,
|
|
93
|
-
|
|
140
|
+
iterationStateFile,
|
|
94
141
|
config,
|
|
95
142
|
phase,
|
|
96
|
-
|
|
143
|
+
iterationState,
|
|
144
|
+
runState,
|
|
145
|
+
iterationNumber,
|
|
146
|
+
maxIterations: selectedWorkItems.length
|
|
97
147
|
});
|
|
98
148
|
if (!phaseResult.ok) {
|
|
99
|
-
console.log(pc.red("failed"));
|
|
100
|
-
iterationState.status = "failed";
|
|
101
|
-
iterationState.phase_results[phase.id] = {
|
|
102
|
-
status: "failed",
|
|
103
|
-
error: phaseResult.error
|
|
104
|
-
};
|
|
105
|
-
runState.status = "failed";
|
|
106
|
-
await writeJson(iterationStateFile, iterationState);
|
|
107
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
108
149
|
throw new Error(phaseResult.error);
|
|
109
150
|
}
|
|
110
|
-
iterationState.phase_results[phase.id] =
|
|
111
|
-
phaseResult.result ?? { status: "ok" };
|
|
112
|
-
const workItem = readObjectProperty(phaseResult.result, "work_item");
|
|
113
|
-
if (workItem !== undefined) {
|
|
114
|
-
iterationState.work_item = workItem;
|
|
115
|
-
const key = readObjectProperty(workItem, "key");
|
|
116
|
-
if (typeof key === "string" && !runState.seen_work_item_keys.includes(key)) {
|
|
117
|
-
runState.seen_work_item_keys.push(key);
|
|
118
|
-
iterationState.seen_work_item_keys = [...runState.seen_work_item_keys];
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
151
|
const nextTarget = resolveNextTarget(phase, phaseResult.outcome);
|
|
122
|
-
const label = phaseResult.outcome ?? "ok";
|
|
123
|
-
console.log(pc.green(label));
|
|
124
|
-
await writeJson(iterationStateFile, iterationState);
|
|
125
|
-
await writeJson(path.join(runDir, "state.json"), runState);
|
|
126
152
|
if (nextTarget === "stop_run") {
|
|
127
|
-
iterationState.status =
|
|
153
|
+
iterationState.status =
|
|
154
|
+
phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
128
155
|
runState.status =
|
|
129
156
|
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
130
157
|
await writeJson(iterationStateFile, iterationState);
|
|
@@ -154,12 +181,212 @@ export async function runWorkflow(options) {
|
|
|
154
181
|
currentPhaseId = nextTarget;
|
|
155
182
|
}
|
|
156
183
|
}
|
|
157
|
-
runState.status =
|
|
184
|
+
runState.status =
|
|
185
|
+
selectedWorkItems.length >= config.workflow.max_iterations
|
|
186
|
+
? "completed_max_iterations"
|
|
187
|
+
: "completed_queue";
|
|
158
188
|
await writeJson(path.join(runDir, "state.json"), runState);
|
|
159
189
|
console.log("");
|
|
160
190
|
console.log(`Done: ${runState.status}`);
|
|
161
191
|
console.log(`Run dir: ${relativeToProject(projectRoot, runDir)}`);
|
|
162
192
|
}
|
|
193
|
+
async function runSelectionPhase(input) {
|
|
194
|
+
const selectionPhase = input.phasesById.get(input.config.workflow.entry_phase);
|
|
195
|
+
if (!selectionPhase) {
|
|
196
|
+
throw new Error(`Unknown phase "${input.config.workflow.entry_phase}"`);
|
|
197
|
+
}
|
|
198
|
+
const availableWorkItems = await loadAvailableWorkItems({
|
|
199
|
+
projectRoot: input.projectRoot,
|
|
200
|
+
config: input.config,
|
|
201
|
+
runState: input.runState,
|
|
202
|
+
ledger: input.ledger
|
|
203
|
+
});
|
|
204
|
+
const selectionDir = path.join(input.runDir, "selection");
|
|
205
|
+
await ensureDir(selectionDir);
|
|
206
|
+
const selectionState = {
|
|
207
|
+
iteration: 0,
|
|
208
|
+
status: "running",
|
|
209
|
+
available_work_items: availableWorkItems,
|
|
210
|
+
recommended_work_item_queue: [],
|
|
211
|
+
selected_work_item_queue: [],
|
|
212
|
+
seen_work_item_keys: [...input.runState.seen_work_item_keys],
|
|
213
|
+
completed_work_item_keys: [...input.ledger.completed_work_item_keys],
|
|
214
|
+
last_completed_work_item: input.ledger.last_completed_work_item,
|
|
215
|
+
phase_results: {},
|
|
216
|
+
phase_visit_counts: {}
|
|
217
|
+
};
|
|
218
|
+
const selectionStateFile = path.join(selectionDir, "state.json");
|
|
219
|
+
await writeJson(selectionStateFile, selectionState);
|
|
220
|
+
const phaseResult = await runWorkflowPhase({
|
|
221
|
+
projectRoot: input.projectRoot,
|
|
222
|
+
runDir: input.runDir,
|
|
223
|
+
iterationDir: selectionDir,
|
|
224
|
+
iterationStateFile: selectionStateFile,
|
|
225
|
+
config: input.config,
|
|
226
|
+
phase: selectionPhase,
|
|
227
|
+
iterationState: selectionState,
|
|
228
|
+
runState: input.runState,
|
|
229
|
+
iterationNumber: "selection",
|
|
230
|
+
maxIterations: input.config.workflow.max_iterations
|
|
231
|
+
});
|
|
232
|
+
if (!phaseResult.ok) {
|
|
233
|
+
throw new Error(phaseResult.error);
|
|
234
|
+
}
|
|
235
|
+
const nextTarget = resolveNextTarget(selectionPhase, phaseResult.outcome);
|
|
236
|
+
if (nextTarget === "stop_run") {
|
|
237
|
+
selectionState.status =
|
|
238
|
+
phaseResult.outcome === "no_work" ? "no_work" : "stopped";
|
|
239
|
+
input.runState.status =
|
|
240
|
+
phaseResult.outcome === "no_work" ? "completed_no_work" : "stopped";
|
|
241
|
+
await writeJson(selectionStateFile, selectionState);
|
|
242
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
243
|
+
console.log("");
|
|
244
|
+
console.log(`Done: ${input.runState.status}`);
|
|
245
|
+
console.log(`Run dir: ${relativeToProject(input.projectRoot, input.runDir)}`);
|
|
246
|
+
return { status: "stopped" };
|
|
247
|
+
}
|
|
248
|
+
if (!nextTarget || isReservedTarget(nextTarget)) {
|
|
249
|
+
throw new Error(`Selection phase must transition to a runnable phase, got "${nextTarget ?? "none"}"`);
|
|
250
|
+
}
|
|
251
|
+
const workItems = readSelectedWorkItems({
|
|
252
|
+
config: input.config,
|
|
253
|
+
result: phaseResult.result,
|
|
254
|
+
availableWorkItems,
|
|
255
|
+
seenWorkItemKeys: selectionState.seen_work_item_keys,
|
|
256
|
+
completedWorkItemKeys: selectionState.completed_work_item_keys
|
|
257
|
+
});
|
|
258
|
+
const selectionGroups = readSelectionGroups(phaseResult.result);
|
|
259
|
+
selectionState.status = "completed";
|
|
260
|
+
selectionState.recommended_work_item_queue = workItems;
|
|
261
|
+
selectionState.selected_work_item_queue = workItems;
|
|
262
|
+
await writeJson(selectionStateFile, selectionState);
|
|
263
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
264
|
+
return {
|
|
265
|
+
status: "selected",
|
|
266
|
+
workItems,
|
|
267
|
+
selectionGroups,
|
|
268
|
+
availableWorkItems,
|
|
269
|
+
nextPhaseId: nextTarget
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function runWorkflowPhase(input) {
|
|
273
|
+
const visits = (input.iterationState.phase_visit_counts[input.phase.id] ?? 0) + 1;
|
|
274
|
+
input.iterationState.phase_visit_counts[input.phase.id] = visits;
|
|
275
|
+
if (visits > input.phase.max_visits_per_iteration) {
|
|
276
|
+
input.iterationState.status = "failed";
|
|
277
|
+
input.runState.status = "failed";
|
|
278
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
279
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
error: `Phase "${input.phase.id}" exceeded max_visits_per_iteration=${input.phase.max_visits_per_iteration}`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
process.stdout.write(`[${input.iterationNumber}/${input.maxIterations}] ${input.phase.id} ... `);
|
|
286
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
287
|
+
const phaseResult = await runPhase({
|
|
288
|
+
projectRoot: input.projectRoot,
|
|
289
|
+
runDir: input.runDir,
|
|
290
|
+
iterationDir: input.iterationDir,
|
|
291
|
+
stateFile: input.iterationStateFile,
|
|
292
|
+
config: input.config,
|
|
293
|
+
phase: input.phase,
|
|
294
|
+
state: input.iterationState
|
|
295
|
+
});
|
|
296
|
+
if (!phaseResult.ok) {
|
|
297
|
+
console.log(pc.red("failed"));
|
|
298
|
+
input.iterationState.status = "failed";
|
|
299
|
+
input.iterationState.phase_results[input.phase.id] = {
|
|
300
|
+
status: "failed",
|
|
301
|
+
error: phaseResult.error
|
|
302
|
+
};
|
|
303
|
+
input.runState.status = "failed";
|
|
304
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
305
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
306
|
+
return phaseResult;
|
|
307
|
+
}
|
|
308
|
+
input.iterationState.phase_results[input.phase.id] =
|
|
309
|
+
phaseResult.result ?? { status: "ok" };
|
|
310
|
+
const workItem = readObjectProperty(phaseResult.result, "work_item");
|
|
311
|
+
if (workItem !== undefined && input.iterationNumber !== "selection") {
|
|
312
|
+
input.iterationState.work_item = workItem;
|
|
313
|
+
const key = readObjectProperty(workItem, "key");
|
|
314
|
+
if (typeof key === "string" &&
|
|
315
|
+
!input.runState.seen_work_item_keys.includes(key)) {
|
|
316
|
+
input.runState.seen_work_item_keys.push(key);
|
|
317
|
+
input.iterationState.seen_work_item_keys = [
|
|
318
|
+
...input.runState.seen_work_item_keys
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
const label = phaseResult.outcome ?? "ok";
|
|
323
|
+
console.log(pc.green(label));
|
|
324
|
+
await writeJson(input.iterationStateFile, input.iterationState);
|
|
325
|
+
await writeJson(path.join(input.runDir, "state.json"), input.runState);
|
|
326
|
+
return phaseResult;
|
|
327
|
+
}
|
|
328
|
+
export async function confirmWorkItemsWithCheckbox(input) {
|
|
329
|
+
if (input.availableWorkItems.length === 0) {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
333
|
+
throw new Error("Work item selection confirmation requires an interactive TTY");
|
|
334
|
+
}
|
|
335
|
+
const confirmationView = buildConfirmationView(input);
|
|
336
|
+
const selectedKeys = await checkbox({
|
|
337
|
+
message: `Confirm work items to run (max ${input.maxIterations})`,
|
|
338
|
+
choices: confirmationView.choices,
|
|
339
|
+
required: false,
|
|
340
|
+
pageSize: Math.min(Math.max(input.availableWorkItems.length + 2, 5), 20),
|
|
341
|
+
validate: (selected) => selected.length <= input.maxIterations ||
|
|
342
|
+
`Select at most ${input.maxIterations} work items.`
|
|
343
|
+
});
|
|
344
|
+
const selected = new Set(selectedKeys);
|
|
345
|
+
return confirmationView.workItems.filter((item) => selected.has(item.key));
|
|
346
|
+
}
|
|
347
|
+
function buildConfirmationView(input) {
|
|
348
|
+
const choices = [];
|
|
349
|
+
const workItems = [];
|
|
350
|
+
const byKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
|
|
351
|
+
const recommendedKeys = new Set(input.recommendedWorkItems.map((item) => item.key));
|
|
352
|
+
const displayed = new Set();
|
|
353
|
+
for (const group of input.selectionGroups) {
|
|
354
|
+
const groupItems = group.work_item_keys
|
|
355
|
+
.map((key) => byKey.get(key))
|
|
356
|
+
.filter((item) => {
|
|
357
|
+
if (!item) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
return !displayed.has(item.key);
|
|
361
|
+
});
|
|
362
|
+
if (groupItems.length === 0) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
choices.push(new Separator(`-- ${group.title} --`));
|
|
366
|
+
for (const item of groupItems) {
|
|
367
|
+
choices.push(buildConfirmationChoice(item, recommendedKeys));
|
|
368
|
+
workItems.push(item);
|
|
369
|
+
displayed.add(item.key);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const ungroupedItems = input.availableWorkItems.filter((item) => !displayed.has(item.key));
|
|
373
|
+
if (input.selectionGroups.length > 0 && ungroupedItems.length > 0) {
|
|
374
|
+
choices.push(new Separator("-- Ungrouped --"));
|
|
375
|
+
}
|
|
376
|
+
for (const item of ungroupedItems) {
|
|
377
|
+
choices.push(buildConfirmationChoice(item, recommendedKeys));
|
|
378
|
+
workItems.push(item);
|
|
379
|
+
}
|
|
380
|
+
return { choices, workItems };
|
|
381
|
+
}
|
|
382
|
+
function buildConfirmationChoice(item, recommendedKeys) {
|
|
383
|
+
return {
|
|
384
|
+
value: item.key,
|
|
385
|
+
name: `${item.title} (${item.key})`,
|
|
386
|
+
description: item.excerpt,
|
|
387
|
+
checked: recommendedKeys.has(item.key)
|
|
388
|
+
};
|
|
389
|
+
}
|
|
163
390
|
function resolveNextTarget(phase, outcome) {
|
|
164
391
|
if (phase.transitions) {
|
|
165
392
|
if (!outcome) {
|
|
@@ -196,6 +423,74 @@ async function completeIterationWorkItem(input) {
|
|
|
196
423
|
await writeWorkItemLedger(input.nyxDir, ledger);
|
|
197
424
|
return ledger;
|
|
198
425
|
}
|
|
426
|
+
function readSelectedWorkItems(input) {
|
|
427
|
+
const workItems = readObjectProperty(input.result, "work_items");
|
|
428
|
+
const legacyWorkItem = readObjectProperty(input.result, "work_item");
|
|
429
|
+
const selection = workItems ?? (legacyWorkItem === undefined ? undefined : [legacyWorkItem]);
|
|
430
|
+
if (selection === undefined) {
|
|
431
|
+
throw new Error('Selection phase returned "selected" without work_items');
|
|
432
|
+
}
|
|
433
|
+
const validation = validateWorkItemQueue({
|
|
434
|
+
config: input.config,
|
|
435
|
+
workItems: selection,
|
|
436
|
+
availableWorkItems: input.availableWorkItems,
|
|
437
|
+
seenWorkItemKeys: input.seenWorkItemKeys,
|
|
438
|
+
completedWorkItemKeys: input.completedWorkItemKeys
|
|
439
|
+
});
|
|
440
|
+
if (!validation.ok) {
|
|
441
|
+
throw new Error(validation.error);
|
|
442
|
+
}
|
|
443
|
+
return validation.workItems;
|
|
444
|
+
}
|
|
445
|
+
function normalizeConfirmedWorkItems(input) {
|
|
446
|
+
const availableByKey = new Map(input.availableWorkItems.map((item) => [item.key, item]));
|
|
447
|
+
const selected = new Set();
|
|
448
|
+
const normalized = [];
|
|
449
|
+
for (const item of input.confirmed) {
|
|
450
|
+
const available = availableByKey.get(item.key);
|
|
451
|
+
if (!available) {
|
|
452
|
+
throw new Error(`Confirmed work item key "${item.key}" is not in available_work_items`);
|
|
453
|
+
}
|
|
454
|
+
if (selected.has(item.key)) {
|
|
455
|
+
throw new Error(`Confirmed work item key "${item.key}" was selected twice`);
|
|
456
|
+
}
|
|
457
|
+
selected.add(item.key);
|
|
458
|
+
normalized.push(available);
|
|
459
|
+
}
|
|
460
|
+
return normalized;
|
|
461
|
+
}
|
|
462
|
+
function readSelectionGroups(result) {
|
|
463
|
+
const groups = readObjectProperty(result, "selection_groups");
|
|
464
|
+
if (!Array.isArray(groups)) {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
return groups
|
|
468
|
+
.map((group) => {
|
|
469
|
+
if (!isRecord(group) || typeof group.title !== "string") {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
const keys = Array.isArray(group.work_item_keys)
|
|
473
|
+
? group.work_item_keys.filter((key) => typeof key === "string")
|
|
474
|
+
: [];
|
|
475
|
+
if (keys.length === 0) {
|
|
476
|
+
return undefined;
|
|
477
|
+
}
|
|
478
|
+
const normalized = {
|
|
479
|
+
title: group.title,
|
|
480
|
+
work_item_keys: keys
|
|
481
|
+
};
|
|
482
|
+
if (typeof group.kind === "string") {
|
|
483
|
+
normalized.kind = group.kind;
|
|
484
|
+
}
|
|
485
|
+
return normalized;
|
|
486
|
+
})
|
|
487
|
+
.filter((group) => Boolean(group));
|
|
488
|
+
}
|
|
489
|
+
function isReservedTarget(target) {
|
|
490
|
+
return (target === "stop_run" ||
|
|
491
|
+
target === "stop_iteration" ||
|
|
492
|
+
target === "next_iteration");
|
|
493
|
+
}
|
|
199
494
|
function readObjectProperty(value, key) {
|
|
200
495
|
if (value &&
|
|
201
496
|
typeof value === "object" &&
|
|
@@ -204,3 +499,6 @@ function readObjectProperty(value, key) {
|
|
|
204
499
|
}
|
|
205
500
|
return undefined;
|
|
206
501
|
}
|
|
502
|
+
function isRecord(value) {
|
|
503
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
504
|
+
}
|
|
@@ -73,6 +73,54 @@ export function validateWorkItemIdentity(input) {
|
|
|
73
73
|
}
|
|
74
74
|
return { ok: true };
|
|
75
75
|
}
|
|
76
|
+
export function validateWorkItemQueue(input) {
|
|
77
|
+
if (!Array.isArray(input.workItems)) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: "Selected work_items must be an array"
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
if (input.workItems.length === 0) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
error: "Selected work_items must contain at least one item"
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const normalized = [];
|
|
91
|
+
for (const [index, workItem] of input.workItems.entries()) {
|
|
92
|
+
const validation = validateWorkItemIdentity({
|
|
93
|
+
config: input.config,
|
|
94
|
+
workItem,
|
|
95
|
+
availableWorkItems: input.availableWorkItems,
|
|
96
|
+
seenWorkItemKeys: input.seenWorkItemKeys,
|
|
97
|
+
completedWorkItemKeys: input.completedWorkItemKeys
|
|
98
|
+
});
|
|
99
|
+
if (!validation.ok) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: `Selected work_items[${index}] ${validation.error}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const key = readObjectProperty(workItem, "key");
|
|
106
|
+
if (typeof key !== "string") {
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
error: `Selected work_items[${index}] key must be a string`
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
if (seen.has(key)) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: `Selected work_items contains duplicate key "${key}"`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
seen.add(key);
|
|
119
|
+
const candidate = input.availableWorkItems?.find((item) => item.key === key);
|
|
120
|
+
normalized.push(candidate ?? workItem);
|
|
121
|
+
}
|
|
122
|
+
return { ok: true, workItems: normalized };
|
|
123
|
+
}
|
|
76
124
|
function validateCandidateMembership(input) {
|
|
77
125
|
const candidate = input.availableWorkItems.find((item) => item.key === input.key);
|
|
78
126
|
if (!candidate) {
|
|
@@ -154,3 +202,11 @@ function normalizeRelativePath(value) {
|
|
|
154
202
|
function isRecord(value) {
|
|
155
203
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
156
204
|
}
|
|
205
|
+
function readObjectProperty(value, key) {
|
|
206
|
+
if (value &&
|
|
207
|
+
typeof value === "object" &&
|
|
208
|
+
Object.prototype.hasOwnProperty.call(value, key)) {
|
|
209
|
+
return value[key];
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
package/docs/nyxagent-v0-spec.md
CHANGED
|
@@ -16,7 +16,8 @@ configuration.
|
|
|
16
16
|
## Goals
|
|
17
17
|
|
|
18
18
|
- Install a `.nyxagent/` folder into a project with sensible templates.
|
|
19
|
-
-
|
|
19
|
+
- Select an ordered work-item queue once, then run a configurable phase workflow
|
|
20
|
+
for up to `max_iterations` confirmed work items.
|
|
20
21
|
- Launch a fresh harness process for each phase.
|
|
21
22
|
- Keep workflow structure generic through phase transitions and outcomes.
|
|
22
23
|
- Keep prompts focused on agent behavior, not runtime mechanics.
|
|
@@ -171,8 +172,8 @@ max_visits_per_iteration = 1
|
|
|
171
172
|
|
|
172
173
|
### Config Semantics
|
|
173
174
|
|
|
174
|
-
- `workflow.max_iterations` is the maximum number of distinct work
|
|
175
|
-
run.
|
|
175
|
+
- `workflow.max_iterations` is the maximum number of distinct confirmed work
|
|
176
|
+
items in a run.
|
|
176
177
|
- `phases[*].max_visits_per_iteration` prevents infinite loops inside one work
|
|
177
178
|
item.
|
|
178
179
|
- `model.reasoning_level` is a harness-neutral string.
|
|
@@ -180,13 +181,17 @@ max_visits_per_iteration = 1
|
|
|
180
181
|
- Per-phase `model` and `harness` blocks override global values.
|
|
181
182
|
- `work_items` supports only `local` and `github` in v0.
|
|
182
183
|
- `work_items.max_candidates` defaults to `50` and caps the inventory sent to
|
|
183
|
-
the selection prompt.
|
|
184
|
+
the initial selection prompt.
|
|
184
185
|
- `work_items.excerpt_chars` defaults to `800` and bounds candidate excerpts.
|
|
185
|
-
-
|
|
186
|
-
`available_work_items`, and
|
|
187
|
-
- The
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
- At run start, the engine scans the configured provider, normalizes candidates
|
|
187
|
+
into `available_work_items`, and sends that inventory to the selection phase.
|
|
188
|
+
- The selection phase returns an ordered recommended `work_items` queue.
|
|
189
|
+
NyxAgent validates each recommended identity against `[work_items]`, rejects
|
|
190
|
+
duplicates, and requires every selected key to exist in
|
|
191
|
+
`available_work_items`.
|
|
192
|
+
- Before executing, NyxAgent shows the full `available_work_items` inventory in
|
|
193
|
+
an interactive checkbox prompt. The recommended queue is pre-checked, and the
|
|
194
|
+
user can add or remove available items. Only confirmed items are executed.
|
|
190
195
|
- Local markdown work items do not have a required frontmatter schema. The
|
|
191
196
|
provider infers titles from the first heading or filename and includes a
|
|
192
197
|
bounded content excerpt.
|
|
@@ -194,7 +199,9 @@ max_visits_per_iteration = 1
|
|
|
194
199
|
- GitHub keys use `github:<owner>/<repo>#<issue-number>` and must match the
|
|
195
200
|
configured repository.
|
|
196
201
|
- NyxAgent does not decide whether an item is a plan, PRD, or task. The
|
|
197
|
-
selection agent makes that semantic choice from the deterministic inventory
|
|
202
|
+
selection agent makes that semantic choice from the deterministic inventory
|
|
203
|
+
and may include optional `selection_groups` for user review. Groups may cover
|
|
204
|
+
the full available inventory, not only the recommended queue.
|
|
198
205
|
|
|
199
206
|
## Workflow Model
|
|
200
207
|
|
|
@@ -214,10 +221,12 @@ Reserved next targets:
|
|
|
214
221
|
The engine does not know about development, review, approval, or closure. It
|
|
215
222
|
only knows phases, outcomes, transitions, and visit limits.
|
|
216
223
|
|
|
217
|
-
The default template expresses the standard
|
|
224
|
+
The default template expresses the standard run:
|
|
218
225
|
|
|
219
226
|
```text
|
|
220
|
-
selection ->
|
|
227
|
+
selection -> user confirms inventory with recommended queue pre-checked
|
|
228
|
+
for each confirmed work item:
|
|
229
|
+
execution -> review
|
|
221
230
|
review.approved -> closure -> next_iteration
|
|
222
231
|
review.changes_requested -> execution
|
|
223
232
|
selection.no_work -> stop_run
|
|
@@ -292,6 +301,8 @@ The runtime contract includes:
|
|
|
292
301
|
- work item context, when selected
|
|
293
302
|
- work item config from `[work_items]`
|
|
294
303
|
- `available_work_items`
|
|
304
|
+
- `recommended_work_item_queue`
|
|
305
|
+
- `selected_work_item_queue`
|
|
295
306
|
- `seen_work_item_keys`
|
|
296
307
|
- `completed_work_item_keys`
|
|
297
308
|
- `last_completed_work_item`
|
|
@@ -306,6 +317,8 @@ Prompts may use simple interpolation:
|
|
|
306
317
|
{{iteration_dir}}
|
|
307
318
|
{{phase_dir}}
|
|
308
319
|
{{state_file}}
|
|
320
|
+
{{recommended_work_item_queue}}
|
|
321
|
+
{{selected_work_item_queue}}
|
|
309
322
|
{{work_item.key}}
|
|
310
323
|
{{work_item.title}}
|
|
311
324
|
{{model.name}}
|
|
@@ -322,17 +335,20 @@ Each run creates a timestamped directory:
|
|
|
322
335
|
.nyxagent/runs/2026-05-23T12-30-00/
|
|
323
336
|
run.json
|
|
324
337
|
state.json
|
|
338
|
+
selection/
|
|
339
|
+
state.json
|
|
340
|
+
phases/
|
|
341
|
+
selection/
|
|
342
|
+
attempt-001/
|
|
343
|
+
prompt.md
|
|
344
|
+
stdout.log
|
|
345
|
+
stderr.log
|
|
346
|
+
meta.json
|
|
347
|
+
result.json
|
|
325
348
|
iterations/
|
|
326
349
|
001/
|
|
327
350
|
state.json
|
|
328
351
|
phases/
|
|
329
|
-
selection/
|
|
330
|
-
attempt-001/
|
|
331
|
-
prompt.md
|
|
332
|
-
stdout.log
|
|
333
|
-
stderr.log
|
|
334
|
-
meta.json
|
|
335
|
-
result.json
|
|
336
352
|
execution/
|
|
337
353
|
attempt-001/
|
|
338
354
|
prompt.md
|
|
@@ -368,6 +384,11 @@ Each run creates a timestamped directory:
|
|
|
368
384
|
- run status
|
|
369
385
|
- current iteration
|
|
370
386
|
- completed iterations
|
|
387
|
+
- available work items seen by the initial selection phase
|
|
388
|
+
- recommended work item queue returned by the selection agent
|
|
389
|
+
- selected work item queue confirmed for the run
|
|
390
|
+
- skipped recommended work item keys
|
|
391
|
+
- selection groups returned for user review
|
|
371
392
|
- seen work item keys
|
|
372
393
|
- completed work item keys
|
|
373
394
|
- last completed work item
|
|
@@ -382,7 +403,7 @@ Each run creates a timestamped directory:
|
|
|
382
403
|
|
|
383
404
|
- iteration number
|
|
384
405
|
- work item
|
|
385
|
-
-
|
|
406
|
+
- selected work item queue for the run
|
|
386
407
|
- seen work item keys
|
|
387
408
|
- completed work item keys
|
|
388
409
|
- last completed work item
|
|
@@ -426,13 +447,15 @@ Default prompts should be concise but operational.
|
|
|
426
447
|
|
|
427
448
|
Selection:
|
|
428
449
|
|
|
429
|
-
-
|
|
450
|
+
- recommend an ordered queue from `available_work_items`
|
|
430
451
|
- treat candidates agnostically: they may be plans, PRDs, or tasks
|
|
431
|
-
- prefer the most actionable
|
|
452
|
+
- prefer the most actionable items based on candidate titles and excerpts
|
|
432
453
|
- if a plan references concrete candidate tasks, choose the concrete task when
|
|
433
454
|
that is the best next work
|
|
434
455
|
- avoid keys already present in `seen_work_item_keys` or
|
|
435
456
|
`completed_work_item_keys`
|
|
457
|
+
- optionally include `selection_groups` to present related work by the most
|
|
458
|
+
useful grouping the agent can infer
|
|
436
459
|
- return `selected` or `no_work`
|
|
437
460
|
|
|
438
461
|
Execution:
|
package/package.json
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
|
|
1
|
+
Recommend the ordered work item queue for this run.
|
|
2
2
|
|
|
3
3
|
Use `available_work_items` from the runtime contract as the complete inventory
|
|
4
|
-
for
|
|
5
|
-
the most actionable
|
|
4
|
+
for selection. Some candidates may be plans, PRDs, or concrete tasks.
|
|
5
|
+
Recommend the most actionable queue for this run, up to
|
|
6
|
+
`workflow.max_iterations` items. NyxAgent will show the complete inventory to
|
|
7
|
+
the user afterward with your recommendation pre-selected.
|
|
6
8
|
|
|
7
|
-
If
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
`
|
|
9
|
+
If candidates appear to belong together, use `selection_groups` to describe the
|
|
10
|
+
most useful review grouping. Infer grouping from titles, paths, excerpts, and
|
|
11
|
+
declared relationships when available, but do not assume every project uses
|
|
12
|
+
plan/PRD subdirectories. Groups may include any key from `available_work_items`,
|
|
13
|
+
including items that are not part of your recommended `work_items` queue.
|
|
14
|
+
|
|
15
|
+
Prefer concrete tasks over their parent plan when both are present and the task
|
|
16
|
+
is ready to execute. Choose the plan itself only when it is the best next work.
|
|
17
|
+
If no candidate is exploitable, return `no_work`.
|
|
11
18
|
|
|
12
19
|
Do not choose any key listed in `seen_work_item_keys` or
|
|
13
|
-
`completed_work_item_keys`.
|
|
14
|
-
`available_work_items`; do not invent
|
|
20
|
+
`completed_work_item_keys`. Every selected work item must be copied from
|
|
21
|
+
`available_work_items`; do not invent keys.
|
|
15
22
|
|
|
16
|
-
The selected work item
|
|
23
|
+
The selected work item identities must match the inventory entries:
|
|
17
24
|
|
|
18
25
|
- local: `source.type` is `local`, `source.locator` is the project-relative
|
|
19
26
|
markdown path, and `key` is `local:<source.locator>`.
|
|
@@ -23,7 +30,11 @@ The selected work item identity must match the inventory entry:
|
|
|
23
30
|
|
|
24
31
|
Return one of these outcomes:
|
|
25
32
|
|
|
26
|
-
- `selected`: include
|
|
33
|
+
- `selected`: include recommended ordered `work_items`; optionally include
|
|
34
|
+
`selection_groups` with `title`, optional `kind`, and `work_item_keys`
|
|
27
35
|
- `no_work`: include a short `reason`
|
|
28
36
|
|
|
37
|
+
For compatibility, `work_item` is still accepted for a single selected item, but
|
|
38
|
+
new results should use `work_items`.
|
|
39
|
+
|
|
29
40
|
Do not modify project files or task files during selection.
|
|
@@ -2,12 +2,8 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"type": "object",
|
|
4
4
|
"required": ["outcome"],
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"type": "string",
|
|
8
|
-
"enum": ["selected", "no_work"]
|
|
9
|
-
},
|
|
10
|
-
"work_item": {
|
|
5
|
+
"$defs": {
|
|
6
|
+
"workItem": {
|
|
11
7
|
"type": "object",
|
|
12
8
|
"required": ["key", "title", "source"],
|
|
13
9
|
"properties": {
|
|
@@ -39,6 +35,47 @@
|
|
|
39
35
|
}
|
|
40
36
|
},
|
|
41
37
|
"additionalProperties": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"properties": {
|
|
41
|
+
"outcome": {
|
|
42
|
+
"type": "string",
|
|
43
|
+
"enum": ["selected", "no_work"]
|
|
44
|
+
},
|
|
45
|
+
"work_items": {
|
|
46
|
+
"type": "array",
|
|
47
|
+
"minItems": 1,
|
|
48
|
+
"items": {
|
|
49
|
+
"$ref": "#/$defs/workItem"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"work_item": {
|
|
53
|
+
"$ref": "#/$defs/workItem"
|
|
54
|
+
},
|
|
55
|
+
"selection_groups": {
|
|
56
|
+
"type": "array",
|
|
57
|
+
"items": {
|
|
58
|
+
"type": "object",
|
|
59
|
+
"required": ["title", "work_item_keys"],
|
|
60
|
+
"properties": {
|
|
61
|
+
"title": {
|
|
62
|
+
"type": "string",
|
|
63
|
+
"minLength": 1
|
|
64
|
+
},
|
|
65
|
+
"kind": {
|
|
66
|
+
"type": "string"
|
|
67
|
+
},
|
|
68
|
+
"work_item_keys": {
|
|
69
|
+
"type": "array",
|
|
70
|
+
"minItems": 1,
|
|
71
|
+
"items": {
|
|
72
|
+
"type": "string",
|
|
73
|
+
"minLength": 1
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"additionalProperties": true
|
|
78
|
+
}
|
|
42
79
|
},
|
|
43
80
|
"reason": {
|
|
44
81
|
"type": "string"
|
|
@@ -54,7 +91,14 @@
|
|
|
54
91
|
}
|
|
55
92
|
},
|
|
56
93
|
"then": {
|
|
57
|
-
"
|
|
94
|
+
"anyOf": [
|
|
95
|
+
{
|
|
96
|
+
"required": ["work_items"]
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"required": ["work_item"]
|
|
100
|
+
}
|
|
101
|
+
]
|
|
58
102
|
}
|
|
59
103
|
},
|
|
60
104
|
{
|