@allocator-one/harness-cli 1.5.2 → 1.6.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/README.md
CHANGED
|
@@ -69,6 +69,24 @@ Example tool call: `issue_list` with `{"ready": true, "team": "ALL"}`
|
|
|
69
69
|
returns the ready queue as JSON; `issue_claim` with `{"id": "IO-123"}`
|
|
70
70
|
claims atomically (a conflict means someone else holds it).
|
|
71
71
|
|
|
72
|
+
Issue creation resolves `--label` names within the target team first, then at
|
|
73
|
+
workspace scope. Missing names are created at workspace scope before the issue
|
|
74
|
+
is filed, and the final label ids travel in the single issue-create request, so
|
|
75
|
+
a preparation or permission failure cannot leave an unlabeled issue behind.
|
|
76
|
+
Creating a missing label requires Harness administrator standing.
|
|
77
|
+
|
|
78
|
+
Label repair is additive and repeatable; it never implicitly replaces the
|
|
79
|
+
whole set:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
harness update IO-123 --add-label security --add-label needs-review
|
|
83
|
+
harness update IO-123 --remove-label security
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Both update flags accept a label name or id. Additions allow only labels from
|
|
87
|
+
the issue's team or workspace. Removal can target an attached legacy foreign
|
|
88
|
+
label; when attached normalized names are ambiguous, use the label id.
|
|
89
|
+
|
|
72
90
|
Auth resolution is identical to the CLI. Without a credential, tools return
|
|
73
91
|
an error telling the agent to run `harness login` in a terminal — the
|
|
74
92
|
server never opens a browser itself.
|
|
@@ -619,7 +619,6 @@ async function version(config) {
|
|
|
619
619
|
` };
|
|
620
620
|
}
|
|
621
621
|
async function create(ctx, opts) {
|
|
622
|
-
const labels = wantedLabels(opts);
|
|
623
622
|
if (opts.title === void 0) {
|
|
624
623
|
throw new UsageError("usage: harness create --title <title> [--team <key>] \u2026");
|
|
625
624
|
}
|
|
@@ -627,6 +626,8 @@ async function create(ctx, opts) {
|
|
|
627
626
|
const team = requiredTeam(ctx, opts.team);
|
|
628
627
|
const priority = optionalPriority(opts.priority);
|
|
629
628
|
const dueDate = optionalDueDate(opts.dueDate);
|
|
629
|
+
const labels = requestedLabelNames(wantedLabels(opts));
|
|
630
|
+
const labelIds = await prepareCreateLabelIds(ctx, team, labels);
|
|
630
631
|
let body = { title: opts.title };
|
|
631
632
|
body = putOpt(body, "description", opts.description);
|
|
632
633
|
body = putOpt(body, "design", opts.design);
|
|
@@ -636,23 +637,14 @@ async function create(ctx, opts) {
|
|
|
636
637
|
body = putPresent(body, "priority", priority);
|
|
637
638
|
body = putDueDate(body, dueDate);
|
|
638
639
|
body = putAssignee(body, opts.assignee);
|
|
640
|
+
if (labelIds.length > 0) body["label_ids"] = labelIds;
|
|
639
641
|
const created = await ctx.client.post(`/api/v1/teams/${team}/issues`, body);
|
|
640
642
|
const issue = created.data;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
json: labeled,
|
|
645
|
-
human: `Created ${str2(labeled["identifier"])}: ${clean(labeled["title"])}
|
|
643
|
+
return {
|
|
644
|
+
json: issue,
|
|
645
|
+
human: `Created ${str2(issue["identifier"])}: ${clean(issue["title"])}
|
|
646
646
|
`
|
|
647
|
-
|
|
648
|
-
} catch (error) {
|
|
649
|
-
if (error instanceof HttpError || error instanceof TransportError) {
|
|
650
|
-
throw new CommandError(
|
|
651
|
-
`created ${str2(issue["identifier"])}, but applying labels failed: ${errorText(error)}`
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
throw error;
|
|
655
|
-
}
|
|
647
|
+
};
|
|
656
648
|
}
|
|
657
649
|
async function update(ctx, identifier, opts) {
|
|
658
650
|
requireIdentifiers([identifier]);
|
|
@@ -666,10 +658,15 @@ async function update(ctx, identifier, opts) {
|
|
|
666
658
|
}
|
|
667
659
|
const priority = optionalPriority(opts.priority);
|
|
668
660
|
const dueDate = optionalDueDate(opts.dueDate);
|
|
669
|
-
|
|
661
|
+
const addLabels = requestedLabelInputs(opts.addLabel ?? []);
|
|
662
|
+
const removeLabels = requestedLabelInputs(opts.removeLabel ?? []);
|
|
663
|
+
const labelPlan = addLabels.length > 0 || removeLabels.length > 0 ? await prepareLabelUpdate(ctx, identifier, addLabels, removeLabels) : null;
|
|
664
|
+
let issue = labelPlan?.issue ?? null;
|
|
665
|
+
let wrote = false;
|
|
670
666
|
if (opts.claim === true) {
|
|
671
667
|
const body = await ctx.client.post(`/api/v1/issues/${identifier}/claim`);
|
|
672
668
|
issue = body.data;
|
|
669
|
+
wrote = true;
|
|
673
670
|
}
|
|
674
671
|
let patch = {};
|
|
675
672
|
patch = putOpt(patch, "title", opts.title);
|
|
@@ -692,15 +689,26 @@ ${opts.appendNotes}`;
|
|
|
692
689
|
if (Object.keys(patch).length > 0) {
|
|
693
690
|
const body = await ctx.client.patch(`/api/v1/issues/${identifier}`, patch);
|
|
694
691
|
issue = body.data;
|
|
692
|
+
wrote = true;
|
|
695
693
|
}
|
|
696
694
|
if (opts.state !== void 0) {
|
|
697
695
|
issue = await setState(ctx, identifier, opts.state);
|
|
696
|
+
wrote = true;
|
|
697
|
+
}
|
|
698
|
+
if (labelPlan?.changed === true) {
|
|
699
|
+
const body = await ctx.client.put(`/api/v1/issues/${identifier}/labels`, {
|
|
700
|
+
label_ids: labelPlan.ids
|
|
701
|
+
});
|
|
702
|
+
issue = body.data;
|
|
703
|
+
wrote = true;
|
|
698
704
|
}
|
|
699
705
|
if (issue === null) {
|
|
700
706
|
throw new UsageError("usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026");
|
|
701
707
|
}
|
|
702
|
-
|
|
703
|
-
` }
|
|
708
|
+
const human = wrote ? `Updated ${str2(issue["identifier"])}
|
|
709
|
+
` : `Labels already up to date for ${str2(issue["identifier"])}
|
|
710
|
+
`;
|
|
711
|
+
return { json: issue, human };
|
|
704
712
|
}
|
|
705
713
|
async function close(ctx, identifiers, opts) {
|
|
706
714
|
if (identifiers.length === 0) {
|
|
@@ -902,27 +910,137 @@ function validateType(type) {
|
|
|
902
910
|
throw new UsageError(`not an issue type: ${type} (expected ${ISSUE_TYPES.join(", ")})`);
|
|
903
911
|
}
|
|
904
912
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
const
|
|
913
|
+
var UUID_FORMAT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
914
|
+
function requestedLabelNames(names) {
|
|
915
|
+
const unique = /* @__PURE__ */ new Map();
|
|
916
|
+
for (const name of requestedLabelInputs(names)) {
|
|
917
|
+
const normalized = normalizeLabelName(name);
|
|
918
|
+
if (!unique.has(normalized)) unique.set(normalized, name);
|
|
919
|
+
}
|
|
920
|
+
return [...unique.values()];
|
|
921
|
+
}
|
|
922
|
+
function requestedLabelInputs(values) {
|
|
923
|
+
return values.map((value) => {
|
|
924
|
+
const trimmed = value.trim();
|
|
925
|
+
if (trimmed === "") throw new UsageError("label names and ids cannot be empty");
|
|
926
|
+
return trimmed;
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function normalizeLabelName(value) {
|
|
930
|
+
return str2(value).trim().toLowerCase();
|
|
931
|
+
}
|
|
932
|
+
async function prepareCreateLabelIds(ctx, teamKey, names) {
|
|
933
|
+
if (names.length === 0) return [];
|
|
934
|
+
const teams = await ctx.client.get("/api/v1/teams");
|
|
935
|
+
const target = teams.data.find(
|
|
936
|
+
(team) => str2(team["key"]).toLowerCase() === teamKey.toLowerCase()
|
|
937
|
+
);
|
|
938
|
+
if (target === void 0) throw new CommandError(`No team matches key "${teamKey}".`);
|
|
939
|
+
const teamId = str2(target["id"]);
|
|
940
|
+
if (teamId === "") throw new CommandError(`team ${teamKey} carried no id`);
|
|
941
|
+
return prepareApplicableLabelIds(ctx, teamId, names);
|
|
942
|
+
}
|
|
943
|
+
async function prepareApplicableLabelIds(ctx, teamId, names) {
|
|
944
|
+
let labels = (await ctx.client.get("/api/v1/labels")).data;
|
|
909
945
|
const ids = [];
|
|
910
946
|
for (const name of names) {
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
947
|
+
const resolved = await resolveOrCreateApplicableLabel(ctx, labels, name, teamId);
|
|
948
|
+
labels = resolved.catalog;
|
|
949
|
+
ids.push(labelId(resolved.label));
|
|
950
|
+
}
|
|
951
|
+
return [...new Set(ids)];
|
|
952
|
+
}
|
|
953
|
+
function applicableLabel(labels, input, teamId) {
|
|
954
|
+
const normalized = normalizeLabelName(input);
|
|
955
|
+
const named = labels.filter((label) => normalizeLabelName(label["name"]) === normalized);
|
|
956
|
+
const teamMatches = named.filter((label) => label["team_id"] === teamId);
|
|
957
|
+
if (teamMatches.length > 1) {
|
|
958
|
+
throw new CommandError(`more than one team label matches "${input}"; use a label id`);
|
|
959
|
+
}
|
|
960
|
+
if (teamMatches.length === 1) return teamMatches[0];
|
|
961
|
+
const workspaceMatches = named.filter(
|
|
962
|
+
(label) => label["team_id"] === null || label["team_id"] === void 0
|
|
963
|
+
);
|
|
964
|
+
if (workspaceMatches.length > 1) {
|
|
965
|
+
throw new CommandError(`more than one workspace label matches "${input}"; use a label id`);
|
|
966
|
+
}
|
|
967
|
+
return workspaceMatches[0];
|
|
968
|
+
}
|
|
969
|
+
async function resolveOrCreateApplicableLabel(ctx, catalog, name, teamId) {
|
|
970
|
+
const existing = applicableLabel(catalog, name, teamId);
|
|
971
|
+
if (existing !== void 0) return { label: existing, catalog };
|
|
972
|
+
try {
|
|
973
|
+
const created = await ctx.client.post("/api/v1/labels", {
|
|
974
|
+
name,
|
|
975
|
+
color: "gray"
|
|
976
|
+
});
|
|
977
|
+
return { label: created.data, catalog: [...catalog, created.data] };
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (!(error instanceof HttpError) || error.status !== 422) throw error;
|
|
980
|
+
const refreshed = (await ctx.client.get("/api/v1/labels")).data;
|
|
981
|
+
const winner = applicableLabel(refreshed, name, teamId);
|
|
982
|
+
if (winner === void 0) throw error;
|
|
983
|
+
return { label: winner, catalog: refreshed };
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
function labelId(label) {
|
|
987
|
+
const id = str2(label["id"]);
|
|
988
|
+
if (id === "") throw new CommandError("the label response carried no id");
|
|
989
|
+
return id;
|
|
990
|
+
}
|
|
991
|
+
async function prepareLabelUpdate(ctx, identifier, addInputs, removeInputs) {
|
|
992
|
+
const issue = (await ctx.client.get(`/api/v1/issues/${identifier}`)).data;
|
|
993
|
+
const attached = Array.isArray(issue["labels"]) ? issue["labels"] : [];
|
|
994
|
+
const originalIds = attached.map(labelId);
|
|
995
|
+
const removed = /* @__PURE__ */ new Set();
|
|
996
|
+
for (const input of removeInputs) {
|
|
997
|
+
const idInput = UUID_FORMAT.test(input);
|
|
998
|
+
const exact = attached.find((label) => {
|
|
999
|
+
const id = labelId(label);
|
|
1000
|
+
return idInput ? id.toLowerCase() === input.toLowerCase() : id === input;
|
|
1001
|
+
});
|
|
1002
|
+
if (exact !== void 0) {
|
|
1003
|
+
removed.add(labelId(exact));
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
const normalized = normalizeLabelName(input);
|
|
1007
|
+
const matches = attached.filter((label) => normalizeLabelName(label["name"]) === normalized);
|
|
1008
|
+
if (matches.length > 1) {
|
|
1009
|
+
throw new CommandError(`more than one attached label matches "${input}"; remove by label id`);
|
|
1010
|
+
}
|
|
1011
|
+
if (matches.length === 1) removed.add(labelId(matches[0]));
|
|
1012
|
+
}
|
|
1013
|
+
const ids = originalIds.filter((id) => !removed.has(id));
|
|
1014
|
+
if (addInputs.length > 0) {
|
|
1015
|
+
const team = issue["team"] ?? {};
|
|
1016
|
+
const teamId = str2(team["id"]);
|
|
1017
|
+
if (teamId === "") throw new CommandError(`issue ${identifier} carried no team id`);
|
|
1018
|
+
let catalog = (await ctx.client.get("/api/v1/labels")).data;
|
|
1019
|
+
for (const input of addInputs) {
|
|
1020
|
+
const idInput = UUID_FORMAT.test(input);
|
|
1021
|
+
let chosen = idInput ? catalog.find((label) => labelId(label).toLowerCase() === input.toLowerCase()) : void 0;
|
|
1022
|
+
if (chosen !== void 0) {
|
|
1023
|
+
const owner = chosen["team_id"];
|
|
1024
|
+
if (owner !== null && owner !== void 0 && owner !== teamId) {
|
|
1025
|
+
throw new CommandError(
|
|
1026
|
+
`label ${input} belongs to another team and cannot be added to ${identifier}`
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
} else {
|
|
1030
|
+
if (idInput) throw new CommandError(`No label matches id "${input}".`);
|
|
1031
|
+
const resolved = await resolveOrCreateApplicableLabel(ctx, catalog, input, teamId);
|
|
1032
|
+
chosen = resolved.label;
|
|
1033
|
+
catalog = resolved.catalog;
|
|
1034
|
+
}
|
|
1035
|
+
const id = labelId(chosen);
|
|
1036
|
+
if (!ids.includes(id)) ids.push(id);
|
|
920
1037
|
}
|
|
921
1038
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1039
|
+
return {
|
|
1040
|
+
issue,
|
|
1041
|
+
ids,
|
|
1042
|
+
changed: ids.length !== originalIds.length || ids.some((id, index) => id !== originalIds[index])
|
|
1043
|
+
};
|
|
926
1044
|
}
|
|
927
1045
|
async function fetchIssues(ctx, params, opts) {
|
|
928
1046
|
if (opts.all === true && opts.page !== void 0) {
|
package/dist/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
version,
|
|
71
71
|
workflowStates,
|
|
72
72
|
writeCredentials
|
|
73
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-DQDNAG5E.js";
|
|
74
74
|
|
|
75
75
|
// src/cli.ts
|
|
76
76
|
import { createInterface as createInterface2 } from "readline";
|
|
@@ -1208,10 +1208,15 @@ function requireNonBlankTitle(text, titleFile) {
|
|
|
1208
1208
|
titleFile === void 0 ? "--title is empty \u2014 an issue needs a title" : `--title-file ${titleFile} produced an empty title`
|
|
1209
1209
|
);
|
|
1210
1210
|
}
|
|
1211
|
-
program.command("create").description("create an issue").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
|
|
1211
|
+
program.command("create").description("create an issue atomically after resolving requested labels").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
|
|
1212
1212
|
"--type <type>",
|
|
1213
1213
|
"task | bug | feature | epic (applied as a type/ label)"
|
|
1214
|
-
).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option(
|
|
1214
|
+
).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option(
|
|
1215
|
+
"--label <name>",
|
|
1216
|
+
"resolve team-first, then workspace; create missing workspace labels before the issue (repeatable)",
|
|
1217
|
+
collect,
|
|
1218
|
+
[]
|
|
1219
|
+
).option("--json", "emit JSON").action(async (opts) => {
|
|
1215
1220
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1216
1221
|
if (opts.title === void 0 && opts.titleFile === void 0) {
|
|
1217
1222
|
throw new UsageError(
|
|
@@ -1227,6 +1232,16 @@ program.command("create").description("create an issue").option("--title <text>"
|
|
|
1227
1232
|
program.command("update").description("update an issue; --claim takes it atomically").argument("[id]").option("--claim", "claim atomically and move to the first started state").option("--state <name>", "workflow state by name or type").option("--title <text>").option("--description <text>").option("--design <text>").option("--notes <text>", "replace the notes").option("--append-notes <text>", "append to the notes").option("--acceptance <text>").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'replace the notes from a file ("-" for stdin)').option("--append-notes-file <path>", 'append to the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", 'YYYY-MM-DD; "" clears').option("--parent <id>").option(
|
|
1228
1233
|
"--assignee <who>",
|
|
1229
1234
|
'me, a user id, an email, or a full name; "" unassigns'
|
|
1235
|
+
).option(
|
|
1236
|
+
"--add-label <name-or-id>",
|
|
1237
|
+
"add a label without replacing existing labels (repeatable)",
|
|
1238
|
+
collect,
|
|
1239
|
+
[]
|
|
1240
|
+
).option(
|
|
1241
|
+
"--remove-label <name-or-id>",
|
|
1242
|
+
"remove an attached label without replacing others (repeatable)",
|
|
1243
|
+
collect,
|
|
1244
|
+
[]
|
|
1230
1245
|
).option("--json", "emit JSON").action(
|
|
1231
1246
|
async (id, opts) => {
|
|
1232
1247
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
@@ -1552,7 +1567,7 @@ program.command("upload").description("upload a file; prints markdown to referen
|
|
|
1552
1567
|
});
|
|
1553
1568
|
});
|
|
1554
1569
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
1555
|
-
const { startMcpServer } = await import("./server-
|
|
1570
|
+
const { startMcpServer } = await import("./server-WBZ5OPHK.js");
|
|
1556
1571
|
await startMcpServer();
|
|
1557
1572
|
});
|
|
1558
1573
|
program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
|
|
@@ -50,7 +50,7 @@ import {
|
|
|
50
50
|
upload,
|
|
51
51
|
userList,
|
|
52
52
|
workflowStates
|
|
53
|
-
} from "./chunk-
|
|
53
|
+
} from "./chunk-DQDNAG5E.js";
|
|
54
54
|
|
|
55
55
|
// src/mcp/server.ts
|
|
56
56
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -189,13 +189,17 @@ var TOOLS = [
|
|
|
189
189
|
due_date: dateField,
|
|
190
190
|
state: z.string().optional().describe("workflow state by name or type"),
|
|
191
191
|
parent: z.string().optional(),
|
|
192
|
-
assignee: z.string().optional().describe('"" unassigns')
|
|
192
|
+
assignee: z.string().optional().describe('"" unassigns'),
|
|
193
|
+
add_labels: z.array(z.string()).optional().describe("label names or ids to add without replacing existing labels"),
|
|
194
|
+
remove_labels: z.array(z.string()).optional().describe("attached label names or ids to remove without replacing others")
|
|
193
195
|
},
|
|
194
196
|
handler: (ctx, args) => update(ctx, args["id"], {
|
|
195
197
|
...args,
|
|
196
198
|
acceptance: args["acceptance_criteria"],
|
|
197
199
|
appendNotes: args["append_notes"],
|
|
198
|
-
dueDate: args["due_date"]
|
|
200
|
+
dueDate: args["due_date"],
|
|
201
|
+
addLabel: args["add_labels"],
|
|
202
|
+
removeLabel: args["remove_labels"]
|
|
199
203
|
})
|
|
200
204
|
},
|
|
201
205
|
{
|
package/package.json
CHANGED
package/spec/openapi.json
CHANGED
|
@@ -1038,6 +1038,14 @@
|
|
|
1038
1038
|
"format": "date",
|
|
1039
1039
|
"type": "string"
|
|
1040
1040
|
},
|
|
1041
|
+
"label_ids": {
|
|
1042
|
+
"description": "Optional label IDs from the workspace or the same team; attached atomically with the issue",
|
|
1043
|
+
"items": {
|
|
1044
|
+
"format": "uuid",
|
|
1045
|
+
"type": "string"
|
|
1046
|
+
},
|
|
1047
|
+
"type": "array"
|
|
1048
|
+
},
|
|
1041
1049
|
"notes": {
|
|
1042
1050
|
"description": "Working notes (markdown)",
|
|
1043
1051
|
"type": "string"
|