@nyxa/nyx-agent 0.2.1 → 0.3.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/dist/cli.js +2 -1
- package/dist/commands/init.js +51 -39
- package/dist/config/schema.js +37 -1
- package/dist/runtime/buildPrompt.js +25 -0
- package/dist/runtime/ledger.js +118 -0
- package/dist/runtime/runPhase.js +64 -0
- package/dist/runtime/runWorkflow.js +375 -43
- package/dist/runtime/validateWorkItem.js +212 -0
- package/dist/runtime/workItems.js +212 -0
- package/docs/nyxagent-v0-spec.md +80 -24
- package/package.json +1 -1
- package/templates/default/prompts/selection.md +28 -10
- package/templates/default/schemas/selection.schema.json +52 -8
package/dist/cli.js
CHANGED
|
@@ -19,8 +19,9 @@ program
|
|
|
19
19
|
.option("--model <name>", "model name")
|
|
20
20
|
.option("--reasoning-level <level>", "harness-neutral reasoning level")
|
|
21
21
|
.option("--max-iterations <count>", "maximum work items per run")
|
|
22
|
-
.option("--work-items-source <source>", "work item source template: local
|
|
22
|
+
.option("--work-items-source <source>", "work item source template: local or github")
|
|
23
23
|
.option("--work-items-path <path>", "local markdown work item directory")
|
|
24
|
+
.option("--work-items-repository <owner/repo>", "GitHub work item repository")
|
|
24
25
|
.action(async (options) => {
|
|
25
26
|
await initCommand(options);
|
|
26
27
|
});
|
package/dist/commands/init.js
CHANGED
|
@@ -24,8 +24,8 @@ export async function initCommand(options, projectRoot = process.cwd()) {
|
|
|
24
24
|
if (resolved) {
|
|
25
25
|
await writeText(configPath, buildConfigToml(resolved));
|
|
26
26
|
}
|
|
27
|
-
if (resolved?.workItemsSource === "local
|
|
28
|
-
await
|
|
27
|
+
if (resolved?.workItemsSource === "local" && resolved.workItemsPath) {
|
|
28
|
+
await ensureWorkItemsDirectory(root, resolved.workItemsPath);
|
|
29
29
|
}
|
|
30
30
|
console.log(pc.green("NyxAgent initialized."));
|
|
31
31
|
console.log(`Config: ${relativeToProject(root, configPath)}`);
|
|
@@ -59,23 +59,36 @@ async function resolveInitOptions(options, root) {
|
|
|
59
59
|
default: 5,
|
|
60
60
|
required: true
|
|
61
61
|
}));
|
|
62
|
-
const workItemsSource = options.workItemsSource
|
|
63
|
-
|
|
62
|
+
const workItemsSource = options.workItemsSource
|
|
63
|
+
? normalizeWorkItemsSource(options.workItemsSource)
|
|
64
|
+
: await select({
|
|
64
65
|
message: "Work item source template",
|
|
65
66
|
choices: [
|
|
66
|
-
{ name: "local
|
|
67
|
-
{ name: "github", value: "github" }
|
|
68
|
-
{ name: "custom", value: "custom" }
|
|
67
|
+
{ name: "local", value: "local" },
|
|
68
|
+
{ name: "github", value: "github" }
|
|
69
69
|
]
|
|
70
|
-
})
|
|
70
|
+
});
|
|
71
71
|
let workItemsPath = options.workItemsPath;
|
|
72
|
-
if (workItemsSource === "local
|
|
72
|
+
if (workItemsSource === "local" && !workItemsPath) {
|
|
73
73
|
const issuesPath = path.join(root, "issues");
|
|
74
74
|
workItemsPath = await input({
|
|
75
|
-
message: "Local
|
|
75
|
+
message: "Local work item path",
|
|
76
76
|
default: (await pathExists(issuesPath)) ? "issues" : ".nyxagent/tasks"
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
+
if (workItemsSource !== "local" && workItemsPath) {
|
|
80
|
+
throw new Error("--work-items-path can only be used with local work items");
|
|
81
|
+
}
|
|
82
|
+
let workItemsRepository = options.workItemsRepository;
|
|
83
|
+
if (workItemsSource === "github" && !workItemsRepository) {
|
|
84
|
+
workItemsRepository = await input({
|
|
85
|
+
message: "GitHub repository",
|
|
86
|
+
default: ""
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
if (workItemsSource === "github") {
|
|
90
|
+
validateGitHubRepository(workItemsRepository);
|
|
91
|
+
}
|
|
79
92
|
if (!Number.isInteger(maxIterations) || maxIterations <= 0) {
|
|
80
93
|
throw new Error("max iterations must be a positive integer");
|
|
81
94
|
}
|
|
@@ -85,9 +98,24 @@ async function resolveInitOptions(options, root) {
|
|
|
85
98
|
reasoningLevel,
|
|
86
99
|
maxIterations,
|
|
87
100
|
workItemsSource,
|
|
88
|
-
workItemsPath
|
|
101
|
+
workItemsPath,
|
|
102
|
+
workItemsRepository
|
|
89
103
|
};
|
|
90
104
|
}
|
|
105
|
+
function normalizeWorkItemsSource(source) {
|
|
106
|
+
if (source === "local" || source === "local-markdown") {
|
|
107
|
+
return "local";
|
|
108
|
+
}
|
|
109
|
+
if (source === "github") {
|
|
110
|
+
return source;
|
|
111
|
+
}
|
|
112
|
+
throw new Error('work item source must be "local" or "github"');
|
|
113
|
+
}
|
|
114
|
+
function validateGitHubRepository(repository) {
|
|
115
|
+
if (!repository || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
|
|
116
|
+
throw new Error('GitHub repository must use "owner/repo"');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
91
119
|
function getTemplatesDir() {
|
|
92
120
|
const currentFile = fileURLToPath(import.meta.url);
|
|
93
121
|
return path.resolve(path.dirname(currentFile), "../../templates/default");
|
|
@@ -108,31 +136,15 @@ async function copyTemplateTree(sourceDir, destinationDir, missingOnly) {
|
|
|
108
136
|
await writeText(destination, await readText(source));
|
|
109
137
|
}
|
|
110
138
|
}
|
|
111
|
-
async function
|
|
139
|
+
async function ensureWorkItemsDirectory(root, taskPath) {
|
|
112
140
|
const absoluteTaskPath = path.resolve(root, taskPath);
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
if (missingOnly && (await pathExists(sampleTaskPath))) {
|
|
116
|
-
return;
|
|
141
|
+
if (!(await pathExists(absoluteTaskPath))) {
|
|
142
|
+
await ensureDir(absoluteTaskPath);
|
|
117
143
|
}
|
|
118
144
|
const taskPathStat = await stat(absoluteTaskPath);
|
|
119
145
|
if (!taskPathStat.isDirectory()) {
|
|
120
146
|
throw new Error(`Work item path is not a directory: ${taskPath}`);
|
|
121
147
|
}
|
|
122
|
-
if (!(await pathExists(sampleTaskPath))) {
|
|
123
|
-
await writeText(sampleTaskPath, [
|
|
124
|
-
"---",
|
|
125
|
-
"nyx_id: TASK-0001",
|
|
126
|
-
"status: open",
|
|
127
|
-
"title: Replace this sample task",
|
|
128
|
-
"---",
|
|
129
|
-
"",
|
|
130
|
-
"# Replace this sample task",
|
|
131
|
-
"",
|
|
132
|
-
"Describe the task here.",
|
|
133
|
-
""
|
|
134
|
-
].join("\n"));
|
|
135
|
-
}
|
|
136
148
|
}
|
|
137
149
|
function buildConfigToml(options) {
|
|
138
150
|
const harness = buildHarnessToml(options.harness);
|
|
@@ -245,18 +257,18 @@ function buildCodexArgs(readOnly) {
|
|
|
245
257
|
return args;
|
|
246
258
|
}
|
|
247
259
|
function buildWorkItemsToml(options) {
|
|
248
|
-
if (options.workItemsSource === "local
|
|
260
|
+
if (options.workItemsSource === "local") {
|
|
249
261
|
return `[work_items]
|
|
250
|
-
source = "local
|
|
251
|
-
path = "${escapeTomlString(options.workItemsPath ?? ".nyxagent/tasks")}"
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return `[work_items]
|
|
255
|
-
source = "github"
|
|
256
|
-
repository = ""`;
|
|
262
|
+
source = "local"
|
|
263
|
+
path = "${escapeTomlString(options.workItemsPath ?? ".nyxagent/tasks")}"
|
|
264
|
+
max_candidates = 50
|
|
265
|
+
excerpt_chars = 800`;
|
|
257
266
|
}
|
|
258
267
|
return `[work_items]
|
|
259
|
-
source = "
|
|
268
|
+
source = "github"
|
|
269
|
+
repository = "${escapeTomlString(options.workItemsRepository ?? "")}"
|
|
270
|
+
max_candidates = 50
|
|
271
|
+
excerpt_chars = 800`;
|
|
260
272
|
}
|
|
261
273
|
function formatTomlArray(values) {
|
|
262
274
|
return `[${values.map((value) => `"${escapeTomlString(value)}"`).join(", ")}]`;
|
package/dist/config/schema.js
CHANGED
|
@@ -28,6 +28,42 @@ const phaseSchema = z
|
|
|
28
28
|
harness: harnessOverrideSchema.optional()
|
|
29
29
|
})
|
|
30
30
|
.passthrough();
|
|
31
|
+
const workItemsSourceSchema = z.preprocess((value) => (value === "local-markdown" ? "local" : value), z.enum(["local", "github"]));
|
|
32
|
+
const githubRepositoryPattern = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
33
|
+
const workItemsSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
source: workItemsSourceSchema,
|
|
36
|
+
path: z.string().min(1).optional(),
|
|
37
|
+
repository: z.string().min(1).optional(),
|
|
38
|
+
max_candidates: z.number().int().positive().default(50),
|
|
39
|
+
excerpt_chars: z.number().int().nonnegative().default(800)
|
|
40
|
+
})
|
|
41
|
+
.passthrough()
|
|
42
|
+
.superRefine((workItems, ctx) => {
|
|
43
|
+
if (workItems.source === "local" && !workItems.path) {
|
|
44
|
+
ctx.addIssue({
|
|
45
|
+
code: "custom",
|
|
46
|
+
path: ["path"],
|
|
47
|
+
message: 'Local work items require "path"'
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (workItems.source === "github") {
|
|
51
|
+
if (!workItems.repository) {
|
|
52
|
+
ctx.addIssue({
|
|
53
|
+
code: "custom",
|
|
54
|
+
path: ["repository"],
|
|
55
|
+
message: 'GitHub work items require "repository"'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else if (!githubRepositoryPattern.test(workItems.repository)) {
|
|
59
|
+
ctx.addIssue({
|
|
60
|
+
code: "custom",
|
|
61
|
+
path: ["repository"],
|
|
62
|
+
message: 'GitHub repository must use "owner/repo"'
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
31
67
|
export const nyxConfigSchema = z
|
|
32
68
|
.object({
|
|
33
69
|
workflow: z.object({
|
|
@@ -45,7 +81,7 @@ export const nyxConfigSchema = z
|
|
|
45
81
|
max_attempts: 1,
|
|
46
82
|
prompt: "prompts/repair-result.md"
|
|
47
83
|
}),
|
|
48
|
-
work_items:
|
|
84
|
+
work_items: workItemsSchema.optional(),
|
|
49
85
|
phases: z.array(phaseSchema).min(1)
|
|
50
86
|
})
|
|
51
87
|
.superRefine((config, ctx) => {
|
|
@@ -34,6 +34,31 @@ async function buildRuntimeContract(input) {
|
|
|
34
34
|
"Work item configuration:",
|
|
35
35
|
"```json",
|
|
36
36
|
JSON.stringify(input.config.work_items ?? {}, null, 2),
|
|
37
|
+
"```",
|
|
38
|
+
"",
|
|
39
|
+
"Available work items:",
|
|
40
|
+
"```json",
|
|
41
|
+
JSON.stringify(input.context.available_work_items ?? [], null, 2),
|
|
42
|
+
"```",
|
|
43
|
+
"",
|
|
44
|
+
"Selected work item queue:",
|
|
45
|
+
"```json",
|
|
46
|
+
JSON.stringify(input.context.selected_work_item_queue ?? [], null, 2),
|
|
47
|
+
"```",
|
|
48
|
+
"",
|
|
49
|
+
"Seen work item keys:",
|
|
50
|
+
"```json",
|
|
51
|
+
JSON.stringify(input.context.seen_work_item_keys ?? [], null, 2),
|
|
52
|
+
"```",
|
|
53
|
+
"",
|
|
54
|
+
"Completed work item keys:",
|
|
55
|
+
"```json",
|
|
56
|
+
JSON.stringify(input.context.completed_work_item_keys ?? [], null, 2),
|
|
57
|
+
"```",
|
|
58
|
+
"",
|
|
59
|
+
"Last completed work item:",
|
|
60
|
+
"```json",
|
|
61
|
+
JSON.stringify(input.context.last_completed_work_item ?? null, null, 2),
|
|
37
62
|
"```"
|
|
38
63
|
];
|
|
39
64
|
if (input.phase.transitions) {
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { writeJson } from "./files.js";
|
|
4
|
+
export async function readWorkItemLedger(nyxDir) {
|
|
5
|
+
const statePath = getLedgerPath(nyxDir);
|
|
6
|
+
let raw;
|
|
7
|
+
try {
|
|
8
|
+
raw = await readFile(statePath, "utf8");
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
12
|
+
return createEmptyLedger();
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
let parsed;
|
|
17
|
+
try {
|
|
18
|
+
parsed = JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
throw new Error(`Failed to parse .nyxagent/state.json: ${message}`);
|
|
23
|
+
}
|
|
24
|
+
return normalizeLedger(parsed);
|
|
25
|
+
}
|
|
26
|
+
export async function writeWorkItemLedger(nyxDir, ledger) {
|
|
27
|
+
await writeJson(getLedgerPath(nyxDir), ledger);
|
|
28
|
+
}
|
|
29
|
+
export function markWorkItemCompleted(input) {
|
|
30
|
+
const record = normalizeCompletedRecord(input.workItem, input.completedAt ?? new Date().toISOString());
|
|
31
|
+
if (!record) {
|
|
32
|
+
return input.ledger;
|
|
33
|
+
}
|
|
34
|
+
const completedWorkItemKeys = uniqueStrings([
|
|
35
|
+
...input.ledger.completed_work_item_keys,
|
|
36
|
+
record.key
|
|
37
|
+
]);
|
|
38
|
+
const previousHistory = input.ledger.completed_work_items.filter((item) => item.key !== record.key);
|
|
39
|
+
return {
|
|
40
|
+
completed_work_item_keys: completedWorkItemKeys,
|
|
41
|
+
last_completed_work_item: record,
|
|
42
|
+
completed_work_items: [...previousHistory, record]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function getLedgerPath(nyxDir) {
|
|
46
|
+
return path.join(nyxDir, "state.json");
|
|
47
|
+
}
|
|
48
|
+
function createEmptyLedger() {
|
|
49
|
+
return {
|
|
50
|
+
completed_work_item_keys: [],
|
|
51
|
+
last_completed_work_item: null,
|
|
52
|
+
completed_work_items: []
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function normalizeLedger(value) {
|
|
56
|
+
if (!isRecord(value)) {
|
|
57
|
+
return createEmptyLedger();
|
|
58
|
+
}
|
|
59
|
+
const completedWorkItems = Array.isArray(value.completed_work_items)
|
|
60
|
+
? value.completed_work_items
|
|
61
|
+
.map((item) => normalizeCompletedRecord(item))
|
|
62
|
+
.filter((item) => Boolean(item))
|
|
63
|
+
: [];
|
|
64
|
+
const completedKeys = uniqueStrings([
|
|
65
|
+
...readStringArray(value.completed_work_item_keys),
|
|
66
|
+
...completedWorkItems.map((item) => item.key)
|
|
67
|
+
]);
|
|
68
|
+
const lastCompleted = normalizeCompletedRecord(value.last_completed_work_item) ??
|
|
69
|
+
completedWorkItems.at(-1);
|
|
70
|
+
return {
|
|
71
|
+
completed_work_item_keys: completedKeys,
|
|
72
|
+
last_completed_work_item: lastCompleted ?? null,
|
|
73
|
+
completed_work_items: completedWorkItems
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function normalizeCompletedRecord(value, completedAt) {
|
|
77
|
+
if (!isRecord(value) || typeof value.key !== "string" || value.key.length === 0) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const record = {
|
|
81
|
+
key: value.key,
|
|
82
|
+
completed_at: completedAt ??
|
|
83
|
+
(typeof value.completed_at === "string"
|
|
84
|
+
? value.completed_at
|
|
85
|
+
: new Date().toISOString())
|
|
86
|
+
};
|
|
87
|
+
if (typeof value.title === "string" && value.title.length > 0) {
|
|
88
|
+
record.title = value.title;
|
|
89
|
+
}
|
|
90
|
+
if (typeof value.url === "string" && value.url.length > 0) {
|
|
91
|
+
record.url = value.url;
|
|
92
|
+
}
|
|
93
|
+
if (isRecord(value.source)) {
|
|
94
|
+
record.source = {};
|
|
95
|
+
if (typeof value.source.type === "string") {
|
|
96
|
+
record.source.type = value.source.type;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value.source.locator === "string") {
|
|
99
|
+
record.source.locator = value.source.locator;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return record;
|
|
103
|
+
}
|
|
104
|
+
function readStringArray(value) {
|
|
105
|
+
if (!Array.isArray(value)) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
return value.filter((item) => typeof item === "string");
|
|
109
|
+
}
|
|
110
|
+
function uniqueStrings(values) {
|
|
111
|
+
return [...new Set(values)];
|
|
112
|
+
}
|
|
113
|
+
function isRecord(value) {
|
|
114
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
115
|
+
}
|
|
116
|
+
function isNodeError(error) {
|
|
117
|
+
return error instanceof Error && "code" in error;
|
|
118
|
+
}
|
package/dist/runtime/runPhase.js
CHANGED
|
@@ -8,6 +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, validateWorkItemQueue } from "./validateWorkItem.js";
|
|
11
12
|
export async function runPhase(input) {
|
|
12
13
|
const phaseDir = path.join(input.iterationDir, "phases", input.phase.id);
|
|
13
14
|
await ensureDir(phaseDir);
|
|
@@ -81,8 +82,12 @@ function buildContext(input) {
|
|
|
81
82
|
phase: input.phase,
|
|
82
83
|
workflow: input.config.workflow,
|
|
83
84
|
work_items: input.config.work_items ?? {},
|
|
85
|
+
available_work_items: input.state.available_work_items ?? [],
|
|
86
|
+
selected_work_item_queue: input.state.selected_work_item_queue ?? [],
|
|
84
87
|
work_item: input.state.work_item ?? {},
|
|
85
88
|
seen_work_item_keys: input.state.seen_work_item_keys ?? [],
|
|
89
|
+
completed_work_item_keys: input.state.completed_work_item_keys ?? [],
|
|
90
|
+
last_completed_work_item: input.state.last_completed_work_item ?? null,
|
|
86
91
|
phase_results: input.state.phase_results ?? {},
|
|
87
92
|
state: input.state,
|
|
88
93
|
model,
|
|
@@ -171,6 +176,34 @@ async function parseAndValidatePhaseResult(input) {
|
|
|
171
176
|
error: `Phase "${input.input.phase.id}" has transitions but result has no string outcome`
|
|
172
177
|
};
|
|
173
178
|
}
|
|
179
|
+
const workItem = readObjectProperty(parsed.value, "work_item");
|
|
180
|
+
if (workItem !== undefined) {
|
|
181
|
+
const currentWorkItemKey = readObjectProperty(readObjectProperty(input.input.state, "work_item"), "key");
|
|
182
|
+
const workItemValidation = validateWorkItemIdentity({
|
|
183
|
+
config: input.input.config,
|
|
184
|
+
workItem,
|
|
185
|
+
availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
|
|
186
|
+
seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
|
|
187
|
+
completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys),
|
|
188
|
+
allowKnownKey: typeof currentWorkItemKey === "string" ? currentWorkItemKey : undefined
|
|
189
|
+
});
|
|
190
|
+
if (!workItemValidation.ok) {
|
|
191
|
+
return workItemValidation;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const workItems = readObjectProperty(parsed.value, "work_items");
|
|
195
|
+
if (workItems !== undefined) {
|
|
196
|
+
const workItemQueueValidation = validateWorkItemQueue({
|
|
197
|
+
config: input.input.config,
|
|
198
|
+
workItems,
|
|
199
|
+
availableWorkItems: readWorkItemCandidates(input.input.state.available_work_items),
|
|
200
|
+
seenWorkItemKeys: readStringArray(input.input.state.seen_work_item_keys),
|
|
201
|
+
completedWorkItemKeys: readStringArray(input.input.state.completed_work_item_keys)
|
|
202
|
+
});
|
|
203
|
+
if (!workItemQueueValidation.ok) {
|
|
204
|
+
return workItemQueueValidation;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
174
207
|
await writeJson(path.join(input.phaseDir, "result.json"), parsed.value);
|
|
175
208
|
return {
|
|
176
209
|
ok: true,
|
|
@@ -242,3 +275,34 @@ async function repairStructuredResult(input) {
|
|
|
242
275
|
error: lastError
|
|
243
276
|
};
|
|
244
277
|
}
|
|
278
|
+
function readObjectProperty(value, key) {
|
|
279
|
+
if (value &&
|
|
280
|
+
typeof value === "object" &&
|
|
281
|
+
Object.prototype.hasOwnProperty.call(value, key)) {
|
|
282
|
+
return value[key];
|
|
283
|
+
}
|
|
284
|
+
return undefined;
|
|
285
|
+
}
|
|
286
|
+
function readStringArray(value) {
|
|
287
|
+
if (!Array.isArray(value)) {
|
|
288
|
+
return undefined;
|
|
289
|
+
}
|
|
290
|
+
return value.filter((item) => typeof item === "string");
|
|
291
|
+
}
|
|
292
|
+
function readWorkItemCandidates(value) {
|
|
293
|
+
if (!Array.isArray(value)) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
return value.filter(isWorkItemCandidate);
|
|
297
|
+
}
|
|
298
|
+
function isWorkItemCandidate(value) {
|
|
299
|
+
const key = readObjectProperty(value, "key");
|
|
300
|
+
const title = readObjectProperty(value, "title");
|
|
301
|
+
const source = readObjectProperty(value, "source");
|
|
302
|
+
const type = readObjectProperty(source, "type");
|
|
303
|
+
const locator = readObjectProperty(source, "locator");
|
|
304
|
+
return (typeof key === "string" &&
|
|
305
|
+
typeof title === "string" &&
|
|
306
|
+
(type === "local" || type === "github") &&
|
|
307
|
+
typeof locator === "string");
|
|
308
|
+
}
|