@picoai/tickets 0.2.0 → 0.4.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/.tickets/spec/AGENTS_EXAMPLE.md +7 -4
- package/.tickets/spec/TICKETS.md +302 -383
- package/.tickets/spec/profile/defaults.yml +31 -0
- package/.tickets/spec/version/20260317-2-tickets-spec.md +106 -0
- package/.tickets/spec/version/20260317-tickets-spec.md +82 -0
- package/README.md +140 -184
- package/package.json +2 -1
- package/release-history.json +14 -0
- package/src/cli.js +479 -115
- package/src/lib/claims.js +66 -0
- package/src/lib/config.js +176 -0
- package/src/lib/constants.js +11 -2
- package/src/lib/index.js +241 -0
- package/src/lib/listing.js +24 -84
- package/src/lib/planning.js +452 -0
- package/src/lib/projections.js +79 -0
- package/src/lib/repair.js +1 -1
- package/src/lib/validation.js +380 -2
package/src/cli.js
CHANGED
|
@@ -8,14 +8,24 @@ import yaml from "yaml";
|
|
|
8
8
|
import {
|
|
9
9
|
ASSIGNMENT_MODE_VALUES,
|
|
10
10
|
BASE_DIR,
|
|
11
|
+
DEFAULT_CLAIM_TTL_MINUTES,
|
|
11
12
|
FORMAT_VERSION,
|
|
12
13
|
FORMAT_VERSION_URL,
|
|
14
|
+
GRAPH_VIEW_VALUES,
|
|
15
|
+
LIST_SORT_VALUES,
|
|
16
|
+
PLANNING_NODE_TYPES,
|
|
13
17
|
PRIORITY_VALUES,
|
|
18
|
+
RESOLUTION_VALUES,
|
|
14
19
|
STATUS_VALUES,
|
|
15
20
|
} from "./lib/constants.js";
|
|
21
|
+
import { deriveActiveClaim, loadClaimEvents } from "./lib/claims.js";
|
|
22
|
+
import { loadWorkflowProfile, validateRepoConfig } from "./lib/config.js";
|
|
23
|
+
import { invalidatePlanningIndex, loadPlanningSnapshot, refreshPlanningIndexIfPresent } from "./lib/index.js";
|
|
16
24
|
import { listTickets } from "./lib/listing.js";
|
|
25
|
+
import { buildGraphData, buildPlanSummary } from "./lib/planning.js";
|
|
26
|
+
import { syncRepoConfig, syncRepoSkill } from "./lib/projections.js";
|
|
17
27
|
import { applyRepairs, loadIssuesFile, runInteractive } from "./lib/repair.js";
|
|
18
|
-
import { collectTicketPaths, validateRunLog, validateTicket } from "./lib/validation.js";
|
|
28
|
+
import { collectTicketPaths, validatePlanningTopology, validateRunLog, validateTicket } from "./lib/validation.js";
|
|
19
29
|
import {
|
|
20
30
|
appendJsonl,
|
|
21
31
|
ensureDir,
|
|
@@ -86,9 +96,64 @@ function normalizeContextItems(values) {
|
|
|
86
96
|
return values.map((value) => String(value).trim()).filter(Boolean);
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
function groupIdSignature(groupIds = []) {
|
|
100
|
+
return [...groupIds].sort((a, b) => a.localeCompare(b)).join(",");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveGroupTargets(groupIds, snapshot) {
|
|
104
|
+
const targets = [];
|
|
105
|
+
for (const groupId of groupIds ?? []) {
|
|
106
|
+
const row = snapshot.nodesById.get(groupId);
|
|
107
|
+
if (!row) {
|
|
108
|
+
throw new Error(`Unknown --group-id target: ${groupId}`);
|
|
109
|
+
}
|
|
110
|
+
if (!["group", "checkpoint"].includes(row.planning.node_type)) {
|
|
111
|
+
throw new Error(`--group-id must reference a group or checkpoint ticket: ${groupId}`);
|
|
112
|
+
}
|
|
113
|
+
targets.push(row);
|
|
114
|
+
}
|
|
115
|
+
return targets;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function inferInheritedScalar(groupTargets, key, optionName) {
|
|
119
|
+
const values = [...new Set(groupTargets.map((row) => row.planning[key]).filter((value) => value))];
|
|
120
|
+
if (values.length > 1) {
|
|
121
|
+
throw new Error(`Cannot infer --${optionName}; referenced groups disagree.`);
|
|
122
|
+
}
|
|
123
|
+
return values[0] ?? null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function inferNextRank(snapshot, planning) {
|
|
127
|
+
if (!planning.lane) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const signature = groupIdSignature(planning.group_ids);
|
|
132
|
+
let maxRank = 0;
|
|
133
|
+
for (const row of snapshot.rows) {
|
|
134
|
+
if (row.planning.node_type !== planning.node_type) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (groupIdSignature(row.planning.group_ids) !== signature) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (row.planning.lane !== planning.lane) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if ((row.planning.horizon ?? null) !== (planning.horizon ?? null)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (Number.isInteger(row.planning.rank) && row.planning.rank > maxRank) {
|
|
147
|
+
maxRank = row.planning.rank;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return maxRank + 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
89
154
|
function printIssues(issues) {
|
|
90
155
|
for (const issue of issues) {
|
|
91
|
-
const location = issue.ticket_path ?? issue.log ?? "";
|
|
156
|
+
const location = issue.ticket_path ?? issue.log ?? issue.config_path ?? "";
|
|
92
157
|
process.stdout.write(`${String(issue.severity ?? "?").toUpperCase()}: ${issue.message} (${location})\n`);
|
|
93
158
|
}
|
|
94
159
|
}
|
|
@@ -236,90 +301,7 @@ function buildRepairsFromIssues(issues, options = {}) {
|
|
|
236
301
|
return repairs;
|
|
237
302
|
}
|
|
238
303
|
|
|
239
|
-
function
|
|
240
|
-
const ticketPath = path.join(ticketsDir(), ticketId, "ticket.md");
|
|
241
|
-
if (fs.existsSync(ticketPath)) {
|
|
242
|
-
try {
|
|
243
|
-
const [frontMatter] = loadTicket(ticketPath);
|
|
244
|
-
return {
|
|
245
|
-
id: ticketId,
|
|
246
|
-
title: frontMatter.title ?? ticketId,
|
|
247
|
-
status: frontMatter.status ?? "",
|
|
248
|
-
priority: frontMatter.priority,
|
|
249
|
-
owner: frontMatter.assignment?.owner,
|
|
250
|
-
mode: frontMatter.assignment?.mode,
|
|
251
|
-
path: ticketPath,
|
|
252
|
-
};
|
|
253
|
-
} catch {
|
|
254
|
-
// ignore
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
id: ticketId,
|
|
260
|
-
title: ticketId,
|
|
261
|
-
status: "",
|
|
262
|
-
path: `/.tickets/${ticketId}/ticket.md`,
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function loadTicketGraph(ticketRef) {
|
|
267
|
-
const nodes = new Map();
|
|
268
|
-
const edges = [];
|
|
269
|
-
const paths = collectTicketPaths(ticketRef);
|
|
270
|
-
let rootId = null;
|
|
271
|
-
|
|
272
|
-
for (const ticketPath of paths) {
|
|
273
|
-
const [frontMatter] = loadTicket(ticketPath);
|
|
274
|
-
const ticketId = frontMatter.id;
|
|
275
|
-
if (!ticketId) {
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (ticketRef && !rootId) {
|
|
280
|
-
rootId = ticketId;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
nodes.set(ticketId, {
|
|
284
|
-
id: ticketId,
|
|
285
|
-
title: frontMatter.title ?? "",
|
|
286
|
-
status: frontMatter.status ?? "",
|
|
287
|
-
priority: frontMatter.priority,
|
|
288
|
-
owner: frontMatter.assignment?.owner,
|
|
289
|
-
mode: frontMatter.assignment?.mode,
|
|
290
|
-
path: ticketPath,
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
for (const dependency of frontMatter.dependencies ?? []) {
|
|
294
|
-
if (!nodes.has(dependency)) {
|
|
295
|
-
nodes.set(dependency, loadNodeById(dependency));
|
|
296
|
-
}
|
|
297
|
-
edges.push({ type: "dependency", from: dependency, to: ticketId });
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
for (const blocked of frontMatter.blocks ?? []) {
|
|
301
|
-
if (!nodes.has(blocked)) {
|
|
302
|
-
nodes.set(blocked, loadNodeById(blocked));
|
|
303
|
-
}
|
|
304
|
-
edges.push({ type: "blocks", from: ticketId, to: blocked });
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
for (const related of frontMatter.related ?? []) {
|
|
308
|
-
if (!nodes.has(related)) {
|
|
309
|
-
nodes.set(related, loadNodeById(related));
|
|
310
|
-
}
|
|
311
|
-
edges.push({ type: "related", from: ticketId, to: related });
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return {
|
|
316
|
-
nodes: [...nodes.values()],
|
|
317
|
-
edges,
|
|
318
|
-
root_id: rootId,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function renderMermaid(graph, includeRelated, timestamp) {
|
|
304
|
+
function renderMermaid(graph, includeRelated, timestamp, view) {
|
|
323
305
|
const statusClasses = {
|
|
324
306
|
todo: "fill:#ddd,stroke:#999",
|
|
325
307
|
doing: "fill:#d0e7ff,stroke:#3b82f6",
|
|
@@ -329,7 +311,7 @@ function renderMermaid(graph, includeRelated, timestamp) {
|
|
|
329
311
|
};
|
|
330
312
|
|
|
331
313
|
const lines = [
|
|
332
|
-
|
|
314
|
+
`# Ticket ${view} graph`,
|
|
333
315
|
`_Generated at ${timestamp} UTC_`,
|
|
334
316
|
"",
|
|
335
317
|
"```mermaid",
|
|
@@ -341,7 +323,17 @@ function renderMermaid(graph, includeRelated, timestamp) {
|
|
|
341
323
|
const nodeRef = `n${idx}`;
|
|
342
324
|
nodeIds.set(node.id, nodeRef);
|
|
343
325
|
const title = (node.title || node.id).replaceAll('"', '\\"');
|
|
344
|
-
const
|
|
326
|
+
const metadata = [
|
|
327
|
+
`status=${node.status || "todo"}`,
|
|
328
|
+
`type=${node.planning.node_type ?? ""}`,
|
|
329
|
+
`lane=${node.planning.lane ?? "-"}`,
|
|
330
|
+
`rank=${node.planning.rank ?? "-"}`,
|
|
331
|
+
`horizon=${node.planning.horizon ?? "-"}`,
|
|
332
|
+
];
|
|
333
|
+
if (node.resolution) {
|
|
334
|
+
metadata.push(`resolution=${node.resolution}`);
|
|
335
|
+
}
|
|
336
|
+
const label = `${title}\\n(${node.id})\\n${metadata.join("\\n")}`;
|
|
345
337
|
const status = (node.status || "todo").toLowerCase();
|
|
346
338
|
lines.push(` ${nodeRef}["${label}"]:::status_${status}`);
|
|
347
339
|
lines.push(` click ${nodeRef} "/.tickets/${node.id}/ticket.md" "_blank"`);
|
|
@@ -356,7 +348,8 @@ function renderMermaid(graph, includeRelated, timestamp) {
|
|
|
356
348
|
if (!source || !target) {
|
|
357
349
|
continue;
|
|
358
350
|
}
|
|
359
|
-
|
|
351
|
+
const connector = edge.type === "contains" ? "-.->" : "-->";
|
|
352
|
+
lines.push(` ${source} ${connector}|${edge.type}| ${target}`);
|
|
360
353
|
}
|
|
361
354
|
|
|
362
355
|
for (const [status, style] of Object.entries(statusClasses)) {
|
|
@@ -388,7 +381,17 @@ function renderDot(graph, includeRelated) {
|
|
|
388
381
|
nodeIds.set(node.id, nodeRef);
|
|
389
382
|
const status = (node.status || "todo").toLowerCase();
|
|
390
383
|
const color = colors[status] ?? colors.todo;
|
|
391
|
-
const
|
|
384
|
+
const metadata = [
|
|
385
|
+
`status=${status}`,
|
|
386
|
+
`type=${node.planning.node_type ?? ""}`,
|
|
387
|
+
`lane=${node.planning.lane ?? "-"}`,
|
|
388
|
+
`rank=${node.planning.rank ?? "-"}`,
|
|
389
|
+
`horizon=${node.planning.horizon ?? "-"}`,
|
|
390
|
+
];
|
|
391
|
+
if (node.resolution) {
|
|
392
|
+
metadata.push(`resolution=${node.resolution}`);
|
|
393
|
+
}
|
|
394
|
+
const label = `${node.title || node.id}\\n(${node.id})\\n${metadata.join("\\n")}`;
|
|
392
395
|
lines.push(
|
|
393
396
|
` ${nodeRef} [label="${label}", fillcolor="${color}", URL="/.tickets/${node.id}/ticket.md", target="_blank"];`,
|
|
394
397
|
);
|
|
@@ -403,8 +406,8 @@ function renderDot(graph, includeRelated) {
|
|
|
403
406
|
if (!source || !target) {
|
|
404
407
|
continue;
|
|
405
408
|
}
|
|
406
|
-
const style = edge.type
|
|
407
|
-
lines.push(` ${source} -> ${target} [style=${style}];`);
|
|
409
|
+
const style = ["related", "contains"].includes(edge.type) ? "dashed" : "solid";
|
|
410
|
+
lines.push(` ${source} -> ${target} [style=${style}, label="${edge.type}"];`);
|
|
408
411
|
}
|
|
409
412
|
|
|
410
413
|
lines.push("}");
|
|
@@ -423,6 +426,14 @@ function renderJson(graph, includeRelated) {
|
|
|
423
426
|
priority: node.priority,
|
|
424
427
|
owner: node.owner,
|
|
425
428
|
mode: node.mode,
|
|
429
|
+
node_type: node.planning.node_type,
|
|
430
|
+
group_ids: node.planning.group_ids,
|
|
431
|
+
lane: node.planning.lane,
|
|
432
|
+
rank: node.planning.rank,
|
|
433
|
+
horizon: node.planning.horizon,
|
|
434
|
+
precedes: node.planning.precedes,
|
|
435
|
+
resolution: node.resolution,
|
|
436
|
+
ready: node.ready,
|
|
426
437
|
href: `/.tickets/${node.id}/ticket.md`,
|
|
427
438
|
})),
|
|
428
439
|
};
|
|
@@ -727,6 +738,7 @@ function generateExampleTickets() {
|
|
|
727
738
|
status: "doing",
|
|
728
739
|
priority: "high",
|
|
729
740
|
labels: ["epic", "planning"],
|
|
741
|
+
planning: { node_type: "group", lane: "build", rank: 1, horizon: "current" },
|
|
730
742
|
assignment: { mode: "mixed", owner: "team:core" },
|
|
731
743
|
related: ["backend", "frontend", "testing", "docs", "release"],
|
|
732
744
|
agent_limits: {
|
|
@@ -760,6 +772,7 @@ function generateExampleTickets() {
|
|
|
760
772
|
status: "doing",
|
|
761
773
|
priority: "high",
|
|
762
774
|
labels: ["backend", "api"],
|
|
775
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "build", rank: 1, horizon: "current", precedes: ["frontend", "testing"] },
|
|
763
776
|
assignment: { mode: "agent_only", owner: "agent:codex" },
|
|
764
777
|
dependencies: ["parent"],
|
|
765
778
|
blocks: ["frontend", "testing", "release"],
|
|
@@ -782,6 +795,11 @@ function generateExampleTickets() {
|
|
|
782
795
|
created_from: "parent",
|
|
783
796
|
context: ["Acceptance criteria from parent", "Release target"],
|
|
784
797
|
},
|
|
798
|
+
{
|
|
799
|
+
summary: "Agent claimed backend work.",
|
|
800
|
+
event_type: "claim",
|
|
801
|
+
claim: { action: "acquire", holder_id: "agent:codex", holder_type: "agent", ttl_minutes: 60 },
|
|
802
|
+
},
|
|
785
803
|
],
|
|
786
804
|
},
|
|
787
805
|
{
|
|
@@ -790,6 +808,7 @@ function generateExampleTickets() {
|
|
|
790
808
|
status: "todo",
|
|
791
809
|
priority: "medium",
|
|
792
810
|
labels: ["frontend", "ui"],
|
|
811
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "build", rank: 2, horizon: "current" },
|
|
793
812
|
dependencies: ["backend"],
|
|
794
813
|
related: ["testing"],
|
|
795
814
|
verification: { commands: ["npm test", "npm run lint"] },
|
|
@@ -813,6 +832,7 @@ function generateExampleTickets() {
|
|
|
813
832
|
status: "todo",
|
|
814
833
|
priority: "medium",
|
|
815
834
|
labels: ["qa"],
|
|
835
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "verify", rank: 1, horizon: "current" },
|
|
816
836
|
dependencies: ["backend", "frontend"],
|
|
817
837
|
verification: { commands: ["npm test"] },
|
|
818
838
|
body: {
|
|
@@ -835,6 +855,7 @@ function generateExampleTickets() {
|
|
|
835
855
|
status: "todo",
|
|
836
856
|
priority: "low",
|
|
837
857
|
labels: ["docs"],
|
|
858
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "launch", rank: 2, horizon: "next" },
|
|
838
859
|
dependencies: ["testing"],
|
|
839
860
|
verification: { commands: ["npm run lint:docs"] },
|
|
840
861
|
body: {
|
|
@@ -857,6 +878,7 @@ function generateExampleTickets() {
|
|
|
857
878
|
status: "todo",
|
|
858
879
|
priority: "high",
|
|
859
880
|
labels: ["release"],
|
|
881
|
+
planning: { node_type: "checkpoint", group_ids: ["parent"], lane: "launch", rank: 1, horizon: "current" },
|
|
860
882
|
dependencies: ["testing"],
|
|
861
883
|
blocks: ["bugfix"],
|
|
862
884
|
verification: { commands: ["npx @picoai/tickets validate"] },
|
|
@@ -876,9 +898,11 @@ function generateExampleTickets() {
|
|
|
876
898
|
{
|
|
877
899
|
key: "bugfix",
|
|
878
900
|
title: "Bugfix: address regression found during Alpha",
|
|
879
|
-
status: "
|
|
901
|
+
status: "canceled",
|
|
880
902
|
priority: "high",
|
|
881
903
|
labels: ["bug", "regression"],
|
|
904
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "build", rank: 3, horizon: "current" },
|
|
905
|
+
resolution: "dropped",
|
|
882
906
|
dependencies: ["backend"],
|
|
883
907
|
related: ["testing"],
|
|
884
908
|
verification: { commands: ["npm test"] },
|
|
@@ -889,9 +913,9 @@ function generateExampleTickets() {
|
|
|
889
913
|
},
|
|
890
914
|
logs: [
|
|
891
915
|
{
|
|
892
|
-
summary: "
|
|
916
|
+
summary: "Dropped after mitigation in backend workstream.",
|
|
893
917
|
context: ["Regression repro identified", "Awaiting backend deployment before retry"],
|
|
894
|
-
|
|
918
|
+
decisions: ["Folded remediation into backend ticket"],
|
|
895
919
|
},
|
|
896
920
|
],
|
|
897
921
|
},
|
|
@@ -920,6 +944,16 @@ function generateExampleTickets() {
|
|
|
920
944
|
if (spec.assignment) {
|
|
921
945
|
frontMatter.assignment = spec.assignment;
|
|
922
946
|
}
|
|
947
|
+
if (spec.planning) {
|
|
948
|
+
frontMatter.planning = {
|
|
949
|
+
...spec.planning,
|
|
950
|
+
group_ids: spec.planning.group_ids?.map((value) => ids[value]) ?? spec.planning.group_ids,
|
|
951
|
+
precedes: spec.planning.precedes?.map((value) => ids[value]) ?? spec.planning.precedes,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
if (spec.resolution) {
|
|
955
|
+
frontMatter.resolution = spec.resolution;
|
|
956
|
+
}
|
|
923
957
|
for (const relationshipKey of ["dependencies", "blocks", "related"]) {
|
|
924
958
|
if (spec[relationshipKey]) {
|
|
925
959
|
frontMatter[relationshipKey] = spec[relationshipKey].map((value) => ids[value]);
|
|
@@ -959,12 +993,13 @@ function generateExampleTickets() {
|
|
|
959
993
|
actor_type: "agent",
|
|
960
994
|
actor_id: "tickets-init",
|
|
961
995
|
summary: logSpec.summary,
|
|
962
|
-
event_type: "work",
|
|
996
|
+
event_type: logSpec.event_type ?? "work",
|
|
963
997
|
written_by: "tickets",
|
|
964
998
|
};
|
|
965
999
|
|
|
966
1000
|
for (const key of [
|
|
967
1001
|
"context",
|
|
1002
|
+
"claim",
|
|
968
1003
|
"decisions",
|
|
969
1004
|
"next_steps",
|
|
970
1005
|
"blockers",
|
|
@@ -979,6 +1014,12 @@ function generateExampleTickets() {
|
|
|
979
1014
|
logEntry[key] = logSpec[key].map((value) => ids[value]);
|
|
980
1015
|
} else if (key === "created_from") {
|
|
981
1016
|
logEntry[key] = ids[logSpec[key]] ?? logSpec[key];
|
|
1017
|
+
} else if (key === "claim") {
|
|
1018
|
+
logEntry.claim = {
|
|
1019
|
+
...logSpec.claim,
|
|
1020
|
+
claim_id: newUuidv7().toLowerCase(),
|
|
1021
|
+
expires_at: iso8601(new Date(now.getTime() + (logSpec.claim.ttl_minutes ?? 60) * 60 * 1000)),
|
|
1022
|
+
};
|
|
982
1023
|
} else {
|
|
983
1024
|
logEntry[key] = logSpec[key];
|
|
984
1025
|
}
|
|
@@ -997,6 +1038,8 @@ async function cmdInit(options) {
|
|
|
997
1038
|
const apply = Boolean(options.apply);
|
|
998
1039
|
|
|
999
1040
|
syncTicketsMd(root, apply);
|
|
1041
|
+
syncRepoConfig(root);
|
|
1042
|
+
syncRepoSkill(root, apply);
|
|
1000
1043
|
|
|
1001
1044
|
const agentsPath = path.join(root, "AGENTS_EXAMPLE.md");
|
|
1002
1045
|
const agentsTemplatePath = path.join(".tickets", "spec", "AGENTS_EXAMPLE.md");
|
|
@@ -1009,8 +1052,15 @@ async function cmdInit(options) {
|
|
|
1009
1052
|
const versionDir = path.join(repoBaseDir, "version");
|
|
1010
1053
|
ensureDir(versionDir);
|
|
1011
1054
|
|
|
1012
|
-
const currentSpecPath = path.join(versionDir, "
|
|
1013
|
-
writeTemplateFile(currentSpecPath, path.join(".tickets", "spec", "version", "
|
|
1055
|
+
const currentSpecPath = path.join(versionDir, "20260317-2-tickets-spec.md");
|
|
1056
|
+
writeTemplateFile(currentSpecPath, path.join(".tickets", "spec", "version", "20260317-2-tickets-spec.md"), apply);
|
|
1057
|
+
|
|
1058
|
+
const previousCurrentSpecPath = path.join(versionDir, "20260311-tickets-spec.md");
|
|
1059
|
+
writeTemplateFile(
|
|
1060
|
+
previousCurrentSpecPath,
|
|
1061
|
+
path.join(".tickets", "spec", "version", "20260311-tickets-spec.md"),
|
|
1062
|
+
apply,
|
|
1063
|
+
);
|
|
1014
1064
|
|
|
1015
1065
|
const previousSpecPath = path.join(versionDir, "20260205-tickets-spec.md");
|
|
1016
1066
|
writeTemplateFile(previousSpecPath, path.join(".tickets", "spec", "version", "20260205-tickets-spec.md"), apply);
|
|
@@ -1022,15 +1072,48 @@ async function cmdInit(options) {
|
|
|
1022
1072
|
generateExampleTickets();
|
|
1023
1073
|
}
|
|
1024
1074
|
|
|
1075
|
+
invalidatePlanningIndex();
|
|
1025
1076
|
process.stdout.write("Initialized.\n");
|
|
1026
1077
|
return 0;
|
|
1027
1078
|
}
|
|
1028
1079
|
|
|
1029
1080
|
async function cmdNew(options) {
|
|
1030
1081
|
ensureDir(ticketsDir());
|
|
1082
|
+
const profile = loadWorkflowProfile();
|
|
1083
|
+
const snapshot = loadPlanningSnapshot({ persist: false });
|
|
1031
1084
|
const ticketId = newUuidv7().toLowerCase();
|
|
1032
1085
|
const ticketDir = path.join(ticketsDir(), ticketId);
|
|
1033
1086
|
ensureDir(path.join(ticketDir, "logs"));
|
|
1087
|
+
const groupIds = options.groupIds?.length ? options.groupIds : [];
|
|
1088
|
+
const groupTargets = resolveGroupTargets(groupIds, snapshot);
|
|
1089
|
+
|
|
1090
|
+
let lane = options.lane ?? null;
|
|
1091
|
+
if (lane === null && groupTargets.length > 0) {
|
|
1092
|
+
lane = inferInheritedScalar(groupTargets, "lane", "lane");
|
|
1093
|
+
}
|
|
1094
|
+
if (lane === null) {
|
|
1095
|
+
lane = profile.defaults?.planning?.lane ?? null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
let horizon = options.horizon ?? null;
|
|
1099
|
+
if (horizon === null && groupTargets.length > 0) {
|
|
1100
|
+
horizon = inferInheritedScalar(groupTargets, "horizon", "horizon");
|
|
1101
|
+
}
|
|
1102
|
+
if (horizon === null) {
|
|
1103
|
+
horizon = profile.defaults?.planning?.horizon ?? null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const planning = {
|
|
1107
|
+
node_type: options.nodeType || profile.defaults?.planning?.node_type || "work",
|
|
1108
|
+
group_ids: groupIds,
|
|
1109
|
+
lane,
|
|
1110
|
+
rank: options.rank ?? null,
|
|
1111
|
+
horizon,
|
|
1112
|
+
precedes: options.precedes?.length ? options.precedes : [],
|
|
1113
|
+
};
|
|
1114
|
+
if (planning.rank === null && planning.lane) {
|
|
1115
|
+
planning.rank = inferNextRank(snapshot, planning);
|
|
1116
|
+
}
|
|
1034
1117
|
|
|
1035
1118
|
const frontMatter = {
|
|
1036
1119
|
id: ticketId,
|
|
@@ -1084,6 +1167,10 @@ async function cmdNew(options) {
|
|
|
1084
1167
|
if (options.verificationCommands?.length) {
|
|
1085
1168
|
frontMatter.verification = { commands: options.verificationCommands };
|
|
1086
1169
|
}
|
|
1170
|
+
frontMatter.planning = planning;
|
|
1171
|
+
if (options.resolution) {
|
|
1172
|
+
frontMatter.resolution = options.resolution;
|
|
1173
|
+
}
|
|
1087
1174
|
|
|
1088
1175
|
const body = [
|
|
1089
1176
|
"# Ticket",
|
|
@@ -1102,14 +1189,18 @@ async function cmdNew(options) {
|
|
|
1102
1189
|
].join("\n");
|
|
1103
1190
|
|
|
1104
1191
|
writeTicket(path.join(ticketDir, "ticket.md"), frontMatter, body);
|
|
1192
|
+
refreshPlanningIndexIfPresent();
|
|
1105
1193
|
process.stdout.write(`${ticketId}\n`);
|
|
1106
1194
|
return 0;
|
|
1107
1195
|
}
|
|
1108
1196
|
|
|
1109
1197
|
async function cmdValidate(options) {
|
|
1110
1198
|
const ticketPaths = collectTicketPaths(options.ticket);
|
|
1199
|
+
const allTicketPaths = collectTicketPaths(null);
|
|
1111
1200
|
const issues = [];
|
|
1112
1201
|
|
|
1202
|
+
issues.push(...validateRepoConfig(repoRoot()));
|
|
1203
|
+
|
|
1113
1204
|
for (const ticketPath of ticketPaths) {
|
|
1114
1205
|
const [ticketIssues] = validateTicket(ticketPath, options.allFields);
|
|
1115
1206
|
issues.push(...ticketIssues);
|
|
@@ -1130,6 +1221,8 @@ async function cmdValidate(options) {
|
|
|
1130
1221
|
}
|
|
1131
1222
|
}
|
|
1132
1223
|
|
|
1224
|
+
issues.push(...validatePlanningTopology(ticketPaths, allTicketPaths));
|
|
1225
|
+
|
|
1133
1226
|
issues.forEach((issue, index) => {
|
|
1134
1227
|
if (!issue.id) {
|
|
1135
1228
|
issue.id = `I${String(index + 1).padStart(4, "0")}`;
|
|
@@ -1192,6 +1285,7 @@ async function cmdStatus(options) {
|
|
|
1192
1285
|
|
|
1193
1286
|
const logPath = path.join(path.dirname(ticketPath), "logs", `${runStarted}-${runId}.jsonl`);
|
|
1194
1287
|
appendJsonl(logPath, entry);
|
|
1288
|
+
refreshPlanningIndexIfPresent();
|
|
1195
1289
|
|
|
1196
1290
|
return 0;
|
|
1197
1291
|
}
|
|
@@ -1251,18 +1345,41 @@ async function cmdLog(options) {
|
|
|
1251
1345
|
|
|
1252
1346
|
const logPath = path.join(path.dirname(ticketPath), "logs", `${runStarted}-${runId}.jsonl`);
|
|
1253
1347
|
appendJsonl(logPath, entry);
|
|
1348
|
+
refreshPlanningIndexIfPresent();
|
|
1254
1349
|
return 0;
|
|
1255
1350
|
}
|
|
1256
1351
|
|
|
1352
|
+
function renderTable(headers, rows) {
|
|
1353
|
+
process.stdout.write(`${headers.join(" | ")}\n`);
|
|
1354
|
+
for (const row of rows) {
|
|
1355
|
+
process.stdout.write(`${headers.map((key) => String(row[key] ?? "")).join(" | ")}\n`);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1257
1359
|
async function cmdList(options) {
|
|
1258
|
-
const
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1360
|
+
const snapshot = loadPlanningSnapshot();
|
|
1361
|
+
const rows = listTickets(
|
|
1362
|
+
snapshot,
|
|
1363
|
+
{
|
|
1364
|
+
status: options.status,
|
|
1365
|
+
priority: options.priority,
|
|
1366
|
+
mode: options.mode,
|
|
1367
|
+
owner: options.owner,
|
|
1368
|
+
label: options.label,
|
|
1369
|
+
text: options.text,
|
|
1370
|
+
nodeType: options.nodeType,
|
|
1371
|
+
group: options.group,
|
|
1372
|
+
lane: options.lane,
|
|
1373
|
+
horizon: options.horizon,
|
|
1374
|
+
claimed: Boolean(options.claimed),
|
|
1375
|
+
claimedBy: options.claimedBy,
|
|
1376
|
+
ready: Boolean(options.ready),
|
|
1377
|
+
},
|
|
1378
|
+
{
|
|
1379
|
+
sortBy: options.sort,
|
|
1380
|
+
reverse: Boolean(options.reverse),
|
|
1381
|
+
},
|
|
1382
|
+
);
|
|
1266
1383
|
|
|
1267
1384
|
if (options.json) {
|
|
1268
1385
|
process.stdout.write(`${JSON.stringify(rows, null, 2)}\n`);
|
|
@@ -1274,12 +1391,159 @@ async function cmdList(options) {
|
|
|
1274
1391
|
return 0;
|
|
1275
1392
|
}
|
|
1276
1393
|
|
|
1277
|
-
const headers = [
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1394
|
+
const headers = [
|
|
1395
|
+
"id",
|
|
1396
|
+
"title",
|
|
1397
|
+
"status",
|
|
1398
|
+
"priority",
|
|
1399
|
+
"node_type",
|
|
1400
|
+
"lane",
|
|
1401
|
+
"rank",
|
|
1402
|
+
"horizon",
|
|
1403
|
+
"ready",
|
|
1404
|
+
"claim",
|
|
1405
|
+
"owner",
|
|
1406
|
+
"last_updated",
|
|
1407
|
+
];
|
|
1408
|
+
renderTable(headers, rows.map((row) => ({ ...row, claim: row.claim_summary })));
|
|
1409
|
+
|
|
1410
|
+
return 0;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async function cmdPlan(options) {
|
|
1414
|
+
const snapshot = loadPlanningSnapshot();
|
|
1415
|
+
const summary = buildPlanSummary(snapshot, {
|
|
1416
|
+
group: options.group ?? options.root ?? null,
|
|
1417
|
+
horizon: options.horizon ?? null,
|
|
1418
|
+
});
|
|
1419
|
+
|
|
1420
|
+
if (options.format === "json") {
|
|
1421
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
1422
|
+
return 0;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const itemHeaders = [
|
|
1426
|
+
"id",
|
|
1427
|
+
"title",
|
|
1428
|
+
"status",
|
|
1429
|
+
"priority",
|
|
1430
|
+
"node_type",
|
|
1431
|
+
"lane",
|
|
1432
|
+
"rank",
|
|
1433
|
+
"horizon",
|
|
1434
|
+
"claim",
|
|
1435
|
+
"owner",
|
|
1436
|
+
];
|
|
1437
|
+
const groupHeaders = ["id", "title", "node_type", "lane", "rank", "horizon", "rollup_summary"];
|
|
1438
|
+
|
|
1439
|
+
const activeRows = summary.active.map((row) => ({ ...row }));
|
|
1440
|
+
const blockedRows = summary.blocked.map((row) => ({
|
|
1441
|
+
...row,
|
|
1442
|
+
blocked_by: JSON.stringify(row.blocked_by),
|
|
1443
|
+
}));
|
|
1444
|
+
const groupRows = summary.groups.map((group) => {
|
|
1445
|
+
const rollup = group.rollup ?? {};
|
|
1446
|
+
return {
|
|
1447
|
+
...group,
|
|
1448
|
+
rollup_summary: `${rollup.done_completed ?? 0}/${rollup.active_leaf ?? 0} complete | merged=${rollup.merged ?? 0} | dropped=${rollup.dropped ?? 0}`,
|
|
1449
|
+
};
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
process.stdout.write("Ready\n");
|
|
1453
|
+
renderTable(itemHeaders, summary.ready.map((row) => ({ ...row, claim: row.claim_summary })));
|
|
1454
|
+
process.stdout.write("\nIn Progress\n");
|
|
1455
|
+
renderTable(itemHeaders, activeRows.map((row) => ({ ...row, claim: row.claim_summary })));
|
|
1456
|
+
process.stdout.write("\nBlocked\n");
|
|
1457
|
+
renderTable([...itemHeaders, "blocked_by"], blockedRows.map((row) => ({ ...row, claim: row.claim_summary })));
|
|
1458
|
+
process.stdout.write("\nGroups / Checkpoints\n");
|
|
1459
|
+
renderTable(groupHeaders, groupRows);
|
|
1460
|
+
|
|
1461
|
+
return 0;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
async function cmdClaim(options) {
|
|
1465
|
+
const ticketPath = resolveTicketPath(options.ticket);
|
|
1466
|
+
const logsDir = path.join(path.dirname(ticketPath), "logs");
|
|
1467
|
+
ensureDir(logsDir);
|
|
1468
|
+
const profile = loadWorkflowProfile();
|
|
1469
|
+
|
|
1470
|
+
const actorId = resolveActorId(options.actorId);
|
|
1471
|
+
const actorType = resolveActorType(options.actorType, actorId);
|
|
1472
|
+
const events = loadClaimEvents(logsDir);
|
|
1473
|
+
const activeClaim = deriveActiveClaim(events, nowUtc());
|
|
1474
|
+
const runId = options.runId || newUuidv7();
|
|
1475
|
+
const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
|
|
1476
|
+
const ttlMinutes = options.ttlMinutes || profile.defaults?.claims?.ttl_minutes || DEFAULT_CLAIM_TTL_MINUTES;
|
|
1477
|
+
const now = nowUtc();
|
|
1478
|
+
|
|
1479
|
+
let action;
|
|
1480
|
+
let summary;
|
|
1481
|
+
let claimId = newUuidv7().toLowerCase();
|
|
1482
|
+
let supersedesClaimId = null;
|
|
1483
|
+
|
|
1484
|
+
if (options.release) {
|
|
1485
|
+
if (!activeClaim) {
|
|
1486
|
+
process.stdout.write("No active claim.\n");
|
|
1487
|
+
return 1;
|
|
1488
|
+
}
|
|
1489
|
+
if (activeClaim.holder_id !== actorId && !options.force) {
|
|
1490
|
+
process.stdout.write(`Ticket is claimed by ${activeClaim.holder_id} until ${activeClaim.expires_at}.\n`);
|
|
1491
|
+
return 1;
|
|
1492
|
+
}
|
|
1493
|
+
if (activeClaim.holder_id !== actorId && options.force && !options.reason) {
|
|
1494
|
+
throw new Error("Forced claim release requires --reason");
|
|
1495
|
+
}
|
|
1496
|
+
action = "release";
|
|
1497
|
+
claimId = activeClaim.claim_id;
|
|
1498
|
+
summary = `Released claim ${claimId}`;
|
|
1499
|
+
} else if (activeClaim && activeClaim.holder_id === actorId) {
|
|
1500
|
+
action = "renew";
|
|
1501
|
+
claimId = activeClaim.claim_id;
|
|
1502
|
+
summary = `Renewed claim ${claimId}`;
|
|
1503
|
+
} else if (activeClaim && activeClaim.holder_id !== actorId) {
|
|
1504
|
+
if (!options.force) {
|
|
1505
|
+
process.stdout.write(`Ticket is claimed by ${activeClaim.holder_id} until ${activeClaim.expires_at}.\n`);
|
|
1506
|
+
return 1;
|
|
1507
|
+
}
|
|
1508
|
+
if (!options.reason) {
|
|
1509
|
+
throw new Error("Forced claim override requires --reason");
|
|
1510
|
+
}
|
|
1511
|
+
action = "override";
|
|
1512
|
+
supersedesClaimId = activeClaim.claim_id;
|
|
1513
|
+
summary = `Overrode claim ${activeClaim.claim_id}`;
|
|
1514
|
+
} else {
|
|
1515
|
+
action = "acquire";
|
|
1516
|
+
summary = `Acquired claim ${claimId}`;
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
const entry = {
|
|
1520
|
+
version: FORMAT_VERSION,
|
|
1521
|
+
version_url: FORMAT_VERSION_URL,
|
|
1522
|
+
ts: iso8601(now),
|
|
1523
|
+
run_started: runStarted,
|
|
1524
|
+
actor_type: actorType,
|
|
1525
|
+
actor_id: actorId,
|
|
1526
|
+
summary,
|
|
1527
|
+
event_type: "claim",
|
|
1528
|
+
written_by: "tickets",
|
|
1529
|
+
claim: {
|
|
1530
|
+
action,
|
|
1531
|
+
claim_id: claimId,
|
|
1532
|
+
holder_id: actorId,
|
|
1533
|
+
holder_type: actorType,
|
|
1534
|
+
reason: options.reason ?? "",
|
|
1535
|
+
supersedes_claim_id: supersedesClaimId,
|
|
1536
|
+
},
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
if (action !== "release") {
|
|
1540
|
+
entry.claim.ttl_minutes = ttlMinutes;
|
|
1541
|
+
entry.claim.expires_at = iso8601(new Date(now.getTime() + ttlMinutes * 60 * 1000));
|
|
1281
1542
|
}
|
|
1282
1543
|
|
|
1544
|
+
appendJsonl(path.join(logsDir, `${runStarted}-${runId}.jsonl`), entry);
|
|
1545
|
+
refreshPlanningIndexIfPresent();
|
|
1546
|
+
process.stdout.write(`${summary}\n`);
|
|
1283
1547
|
return 0;
|
|
1284
1548
|
}
|
|
1285
1549
|
|
|
@@ -1354,7 +1618,12 @@ async function cmdRepair(options) {
|
|
|
1354
1618
|
}
|
|
1355
1619
|
|
|
1356
1620
|
async function cmdGraph(options) {
|
|
1357
|
-
const
|
|
1621
|
+
const snapshot = loadPlanningSnapshot();
|
|
1622
|
+
const graph = buildGraphData(snapshot, {
|
|
1623
|
+
ticket: options.ticket,
|
|
1624
|
+
view: options.view,
|
|
1625
|
+
includeRelated: options.related,
|
|
1626
|
+
});
|
|
1358
1627
|
if (graph.nodes.length === 0) {
|
|
1359
1628
|
process.stdout.write("No tickets found.\n");
|
|
1360
1629
|
return 1;
|
|
@@ -1365,8 +1634,8 @@ async function cmdGraph(options) {
|
|
|
1365
1634
|
|
|
1366
1635
|
const timestamp = isoBasic(nowUtc());
|
|
1367
1636
|
const base = options.ticket
|
|
1368
|
-
?
|
|
1369
|
-
:
|
|
1637
|
+
? `${options.view}_for_${graph.root_id || "subset"}`
|
|
1638
|
+
: options.view;
|
|
1370
1639
|
const ext = { mermaid: "md", dot: "dot", json: "json" }[options.format];
|
|
1371
1640
|
const outPath = options.output
|
|
1372
1641
|
? path.resolve(repoRoot(), options.output)
|
|
@@ -1378,7 +1647,7 @@ async function cmdGraph(options) {
|
|
|
1378
1647
|
} else if (options.format === "dot") {
|
|
1379
1648
|
fs.writeFileSync(outPath, renderDot(graph, options.related));
|
|
1380
1649
|
} else {
|
|
1381
|
-
fs.writeFileSync(outPath, renderMermaid(graph, options.related, timestamp));
|
|
1650
|
+
fs.writeFileSync(outPath, renderMermaid(graph, options.related, timestamp, options.view));
|
|
1382
1651
|
}
|
|
1383
1652
|
|
|
1384
1653
|
process.stdout.write(`${outPath}\n`);
|
|
@@ -1419,6 +1688,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1419
1688
|
.option("--checkpoint-every-minutes <minutes>")
|
|
1420
1689
|
.option("--verification-command <command>", "Verification command", collectOption, [])
|
|
1421
1690
|
.option("--created-at <timestamp>")
|
|
1691
|
+
.option("--node-type <nodeType>")
|
|
1692
|
+
.option("--group-id <groupId>", "Group membership ticket id", collectOption, [])
|
|
1693
|
+
.option("--lane <lane>")
|
|
1694
|
+
.option("--rank <rank>")
|
|
1695
|
+
.option("--horizon <horizon>")
|
|
1696
|
+
.option("--precedes <ticketId>", "Sequence successor ticket id", collectOption, [])
|
|
1697
|
+
.option("--resolution <resolution>")
|
|
1422
1698
|
.action(async (options) => {
|
|
1423
1699
|
if (!STATUS_VALUES.includes(options.status)) {
|
|
1424
1700
|
throw new Error(`Invalid --status. Use one of: ${STATUS_VALUES.join(", ")}`);
|
|
@@ -1431,6 +1707,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1431
1707
|
`Invalid --assignment-mode. Use one of: ${ASSIGNMENT_MODE_VALUES.join(", ")}`,
|
|
1432
1708
|
);
|
|
1433
1709
|
}
|
|
1710
|
+
if (options.nodeType && !PLANNING_NODE_TYPES.includes(options.nodeType)) {
|
|
1711
|
+
throw new Error(`Invalid --node-type. Use one of: ${PLANNING_NODE_TYPES.join(", ")}`);
|
|
1712
|
+
}
|
|
1713
|
+
if (options.resolution && !RESOLUTION_VALUES.includes(options.resolution)) {
|
|
1714
|
+
throw new Error(`Invalid --resolution. Use one of: ${RESOLUTION_VALUES.join(", ")}`);
|
|
1715
|
+
}
|
|
1716
|
+
if (options.rank) {
|
|
1717
|
+
const rank = Number.parseInt(options.rank, 10);
|
|
1718
|
+
if (!Number.isInteger(rank) || rank <= 0) {
|
|
1719
|
+
throw new Error("Invalid --rank. Use a positive integer");
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (options.resolution && !["done", "canceled"].includes(options.status)) {
|
|
1723
|
+
throw new Error("Resolution requires terminal status done or canceled");
|
|
1724
|
+
}
|
|
1434
1725
|
process.exitCode = await cmdNew({
|
|
1435
1726
|
title: options.title,
|
|
1436
1727
|
status: options.status,
|
|
@@ -1451,6 +1742,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1451
1742
|
: undefined,
|
|
1452
1743
|
verificationCommands: options.verificationCommand,
|
|
1453
1744
|
createdAt: options.createdAt,
|
|
1745
|
+
nodeType: options.nodeType,
|
|
1746
|
+
groupIds: options.groupId,
|
|
1747
|
+
lane: options.lane,
|
|
1748
|
+
rank: options.rank ? Number.parseInt(options.rank, 10) : null,
|
|
1749
|
+
horizon: options.horizon,
|
|
1750
|
+
precedes: options.precedes,
|
|
1751
|
+
resolution: options.resolution,
|
|
1454
1752
|
});
|
|
1455
1753
|
});
|
|
1456
1754
|
|
|
@@ -1550,11 +1848,37 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1550
1848
|
.option("--owner <owner>")
|
|
1551
1849
|
.option("--label <label>")
|
|
1552
1850
|
.option("--text <text>")
|
|
1851
|
+
.option("--node-type <nodeType>")
|
|
1852
|
+
.option("--group <ticketId>")
|
|
1853
|
+
.option("--lane <lane>")
|
|
1854
|
+
.option("--horizon <horizon>")
|
|
1855
|
+
.option("--claimed", "Only show claimed tickets")
|
|
1856
|
+
.option("--claimed-by <actorId>")
|
|
1857
|
+
.option("--ready", "Only show ready tickets")
|
|
1858
|
+
.option("--sort <sort>", "ready | priority | lane | rank | updated | title")
|
|
1859
|
+
.option("--reverse", "Reverse the sort order")
|
|
1553
1860
|
.option("--json", "JSON output")
|
|
1554
1861
|
.action(async (options) => {
|
|
1862
|
+
if (options.sort && !LIST_SORT_VALUES.includes(options.sort)) {
|
|
1863
|
+
throw new Error(`Invalid --sort. Use one of: ${LIST_SORT_VALUES.join(", ")}`);
|
|
1864
|
+
}
|
|
1555
1865
|
process.exitCode = await cmdList(options);
|
|
1556
1866
|
});
|
|
1557
1867
|
|
|
1868
|
+
program
|
|
1869
|
+
.command("plan")
|
|
1870
|
+
.description("Summarize portfolio rollups and ready work")
|
|
1871
|
+
.option("--root <ticket>")
|
|
1872
|
+
.option("--group <ticket>")
|
|
1873
|
+
.option("--horizon <horizon>")
|
|
1874
|
+
.option("--format <format>", "table | json", "table")
|
|
1875
|
+
.action(async (options) => {
|
|
1876
|
+
if (!["table", "json"].includes(options.format)) {
|
|
1877
|
+
throw new Error("Invalid --format. Use one of: table, json");
|
|
1878
|
+
}
|
|
1879
|
+
process.exitCode = await cmdPlan(options);
|
|
1880
|
+
});
|
|
1881
|
+
|
|
1558
1882
|
program
|
|
1559
1883
|
.command("repair")
|
|
1560
1884
|
.description("Repair tickets")
|
|
@@ -1577,9 +1901,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1577
1901
|
|
|
1578
1902
|
program
|
|
1579
1903
|
.command("graph")
|
|
1580
|
-
.description("
|
|
1904
|
+
.description("Ticket graph")
|
|
1581
1905
|
.option("--ticket <ticket>")
|
|
1582
1906
|
.option("--format <format>", "mermaid | dot | json", "mermaid")
|
|
1907
|
+
.option("--view <view>", "dependency | sequence | portfolio | all", "dependency")
|
|
1583
1908
|
.option("--output <file>")
|
|
1584
1909
|
.option("--related", "Include related edges")
|
|
1585
1910
|
.option("--no-related", "Exclude related edges")
|
|
@@ -1587,14 +1912,53 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1587
1912
|
if (!["mermaid", "dot", "json"].includes(options.format)) {
|
|
1588
1913
|
throw new Error("Invalid --format. Use one of: mermaid, dot, json");
|
|
1589
1914
|
}
|
|
1915
|
+
if (!GRAPH_VIEW_VALUES.includes(options.view)) {
|
|
1916
|
+
throw new Error(`Invalid --view. Use one of: ${GRAPH_VIEW_VALUES.join(", ")}`);
|
|
1917
|
+
}
|
|
1590
1918
|
process.exitCode = await cmdGraph({
|
|
1591
1919
|
ticket: options.ticket,
|
|
1592
1920
|
format: options.format,
|
|
1921
|
+
view: options.view,
|
|
1593
1922
|
output: options.output,
|
|
1594
1923
|
related: options.related,
|
|
1595
1924
|
});
|
|
1596
1925
|
});
|
|
1597
1926
|
|
|
1927
|
+
program
|
|
1928
|
+
.command("claim")
|
|
1929
|
+
.description("Acquire, renew, release, or override an advisory ticket claim")
|
|
1930
|
+
.requiredOption("--ticket <ticket>")
|
|
1931
|
+
.option("--actor-type <actorType>")
|
|
1932
|
+
.option("--actor-id <actorId>")
|
|
1933
|
+
.option("--run-id <runId>")
|
|
1934
|
+
.option("--run-started <runStarted>")
|
|
1935
|
+
.option("--ttl-minutes <minutes>")
|
|
1936
|
+
.option("--release", "Release the active claim")
|
|
1937
|
+
.option("--force", "Override an active claim held by another actor")
|
|
1938
|
+
.option("--reason <reason>")
|
|
1939
|
+
.action(async (options) => {
|
|
1940
|
+
if (options.actorType && !isValidActorType(options.actorType)) {
|
|
1941
|
+
throw new Error("Invalid --actor-type. Use one of: human, agent");
|
|
1942
|
+
}
|
|
1943
|
+
if (options.ttlMinutes) {
|
|
1944
|
+
const ttl = Number.parseInt(options.ttlMinutes, 10);
|
|
1945
|
+
if (!Number.isInteger(ttl) || ttl <= 0) {
|
|
1946
|
+
throw new Error("Invalid --ttl-minutes. Use a positive integer");
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
process.exitCode = await cmdClaim({
|
|
1950
|
+
ticket: options.ticket,
|
|
1951
|
+
actorType: options.actorType,
|
|
1952
|
+
actorId: options.actorId,
|
|
1953
|
+
runId: options.runId,
|
|
1954
|
+
runStarted: options.runStarted,
|
|
1955
|
+
ttlMinutes: options.ttlMinutes ? Number.parseInt(options.ttlMinutes, 10) : undefined,
|
|
1956
|
+
release: Boolean(options.release),
|
|
1957
|
+
force: Boolean(options.force),
|
|
1958
|
+
reason: options.reason,
|
|
1959
|
+
});
|
|
1960
|
+
});
|
|
1961
|
+
|
|
1598
1962
|
try {
|
|
1599
1963
|
await program.parseAsync(argv, { from: "user" });
|
|
1600
1964
|
} catch (error) {
|