@picoai/tickets 0.1.0 → 0.3.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 +5 -4
- package/.tickets/spec/TICKETS.md +295 -358
- package/.tickets/spec/profile/defaults.yml +29 -0
- package/.tickets/spec/version/20260311-tickets-spec.md +38 -0
- package/.tickets/spec/version/20260317-tickets-spec.md +82 -0
- package/README.md +122 -147
- package/package.json +4 -1
- package/release-history.json +19 -0
- package/src/cli.js +462 -137
- package/src/lib/claims.js +66 -0
- package/src/lib/config.js +162 -0
- package/src/lib/constants.js +8 -2
- package/src/lib/listing.js +21 -84
- package/src/lib/planning.js +355 -0
- package/src/lib/projections.js +70 -0
- package/src/lib/repair.js +75 -1
- package/src/lib/util.js +5 -1
- package/src/lib/validation.js +216 -0
- package/src/release-status.js +141 -0
package/src/cli.js
CHANGED
|
@@ -8,12 +8,20 @@ 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
|
+
PLANNING_NODE_TYPES,
|
|
13
16
|
PRIORITY_VALUES,
|
|
17
|
+
RESOLUTION_VALUES,
|
|
14
18
|
STATUS_VALUES,
|
|
15
19
|
} from "./lib/constants.js";
|
|
20
|
+
import { deriveActiveClaim, loadClaimEvents } from "./lib/claims.js";
|
|
21
|
+
import { loadWorkflowProfile, validateRepoConfig } from "./lib/config.js";
|
|
16
22
|
import { listTickets } from "./lib/listing.js";
|
|
23
|
+
import { buildGraphData, buildPlanSummary, buildPlanningSnapshot } from "./lib/planning.js";
|
|
24
|
+
import { syncRepoConfig, syncRepoSkill } from "./lib/projections.js";
|
|
17
25
|
import { applyRepairs, loadIssuesFile, runInteractive } from "./lib/repair.js";
|
|
18
26
|
import { collectTicketPaths, validateRunLog, validateTicket } from "./lib/validation.js";
|
|
19
27
|
import {
|
|
@@ -40,9 +48,55 @@ function hasErrors(issues) {
|
|
|
40
48
|
return issues.some((issue) => issue.severity === "error");
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
function isValidActorType(value) {
|
|
52
|
+
return ["human", "agent"].includes(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveActorId(explicitActorId) {
|
|
56
|
+
const candidate = explicitActorId ?? process.env.TICKETS_ACTOR_ID;
|
|
57
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
58
|
+
return candidate.trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const localUser = process.env.USER ?? process.env.USERNAME;
|
|
62
|
+
if (typeof localUser === "string" && localUser.trim()) {
|
|
63
|
+
return `@${localUser.trim()}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return "unknown";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveActorType(explicitActorType, actorId) {
|
|
70
|
+
const candidate = explicitActorType ?? process.env.TICKETS_ACTOR_TYPE;
|
|
71
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
72
|
+
if (!isValidActorType(candidate.trim())) {
|
|
73
|
+
throw new Error("Invalid actor type. Use one of: human, agent");
|
|
74
|
+
}
|
|
75
|
+
return candidate.trim();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof actorId === "string") {
|
|
79
|
+
if (actorId.startsWith("agent:")) {
|
|
80
|
+
return "agent";
|
|
81
|
+
}
|
|
82
|
+
if (actorId.startsWith("@")) {
|
|
83
|
+
return "human";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return "human";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function normalizeContextItems(values) {
|
|
91
|
+
if (!Array.isArray(values)) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return values.map((value) => String(value).trim()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
43
97
|
function printIssues(issues) {
|
|
44
98
|
for (const issue of issues) {
|
|
45
|
-
const location = issue.ticket_path ?? issue.log ?? "";
|
|
99
|
+
const location = issue.ticket_path ?? issue.log ?? issue.config_path ?? "";
|
|
46
100
|
process.stdout.write(`${String(issue.severity ?? "?").toUpperCase()}: ${issue.message} (${location})\n`);
|
|
47
101
|
}
|
|
48
102
|
}
|
|
@@ -65,11 +119,14 @@ function buildRepairsFromIssues(issues, options = {}) {
|
|
|
65
119
|
for (const issue of issues) {
|
|
66
120
|
const code = issue.code;
|
|
67
121
|
const ticketPath = issue.ticket_path;
|
|
68
|
-
|
|
122
|
+
const logLocation = issue.log;
|
|
123
|
+
const logPath = logLocation ? String(logLocation).replace(/:\d+$/, "") : null;
|
|
124
|
+
if (!ticketPath && !logPath) {
|
|
69
125
|
continue;
|
|
70
126
|
}
|
|
71
127
|
|
|
72
|
-
const
|
|
128
|
+
const targetPath = ticketPath ?? logPath;
|
|
129
|
+
const key = `${code}:${targetPath}`;
|
|
73
130
|
if (seen.has(key)) {
|
|
74
131
|
continue;
|
|
75
132
|
}
|
|
@@ -85,10 +142,31 @@ function buildRepairsFromIssues(issues, options = {}) {
|
|
|
85
142
|
id: nextId,
|
|
86
143
|
enabled: false,
|
|
87
144
|
issue_ids: [issue.id ?? ""],
|
|
88
|
-
ticket_path: ticketPath,
|
|
89
145
|
};
|
|
146
|
+
if (ticketPath) {
|
|
147
|
+
base.ticket_path = ticketPath;
|
|
148
|
+
}
|
|
149
|
+
if (logPath) {
|
|
150
|
+
base.log_path = logPath;
|
|
151
|
+
}
|
|
90
152
|
|
|
91
|
-
if (
|
|
153
|
+
if (["LOG_EVENT_TYPE_MISSING", "LOG_EVENT_TYPE_INVALID"].includes(code)) {
|
|
154
|
+
repairs.push({
|
|
155
|
+
...base,
|
|
156
|
+
safe: true,
|
|
157
|
+
action: "set_log_event_type",
|
|
158
|
+
params: {},
|
|
159
|
+
optional: false,
|
|
160
|
+
});
|
|
161
|
+
} else if (["CONTEXT_INVALID", "CONTEXT_EMPTY", "CONTEXT_ENTRY_INVALID", "CONTEXT_MISSING"].includes(code)) {
|
|
162
|
+
repairs.push({
|
|
163
|
+
...base,
|
|
164
|
+
safe: true,
|
|
165
|
+
action: "normalize_log_context",
|
|
166
|
+
params: {},
|
|
167
|
+
optional: false,
|
|
168
|
+
});
|
|
169
|
+
} else if (code === "MISSING_SECTION") {
|
|
92
170
|
repairs.push({ ...base, safe: true, action: "add_sections", params: {}, optional: false });
|
|
93
171
|
} else if (["VERSION_MISSING", "VERSION_INVALID"].includes(code)) {
|
|
94
172
|
repairs.push({
|
|
@@ -166,90 +244,7 @@ function buildRepairsFromIssues(issues, options = {}) {
|
|
|
166
244
|
return repairs;
|
|
167
245
|
}
|
|
168
246
|
|
|
169
|
-
function
|
|
170
|
-
const ticketPath = path.join(ticketsDir(), ticketId, "ticket.md");
|
|
171
|
-
if (fs.existsSync(ticketPath)) {
|
|
172
|
-
try {
|
|
173
|
-
const [frontMatter] = loadTicket(ticketPath);
|
|
174
|
-
return {
|
|
175
|
-
id: ticketId,
|
|
176
|
-
title: frontMatter.title ?? ticketId,
|
|
177
|
-
status: frontMatter.status ?? "",
|
|
178
|
-
priority: frontMatter.priority,
|
|
179
|
-
owner: frontMatter.assignment?.owner,
|
|
180
|
-
mode: frontMatter.assignment?.mode,
|
|
181
|
-
path: ticketPath,
|
|
182
|
-
};
|
|
183
|
-
} catch {
|
|
184
|
-
// ignore
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return {
|
|
189
|
-
id: ticketId,
|
|
190
|
-
title: ticketId,
|
|
191
|
-
status: "",
|
|
192
|
-
path: `/.tickets/${ticketId}/ticket.md`,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function loadTicketGraph(ticketRef) {
|
|
197
|
-
const nodes = new Map();
|
|
198
|
-
const edges = [];
|
|
199
|
-
const paths = collectTicketPaths(ticketRef);
|
|
200
|
-
let rootId = null;
|
|
201
|
-
|
|
202
|
-
for (const ticketPath of paths) {
|
|
203
|
-
const [frontMatter] = loadTicket(ticketPath);
|
|
204
|
-
const ticketId = frontMatter.id;
|
|
205
|
-
if (!ticketId) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (ticketRef && !rootId) {
|
|
210
|
-
rootId = ticketId;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
nodes.set(ticketId, {
|
|
214
|
-
id: ticketId,
|
|
215
|
-
title: frontMatter.title ?? "",
|
|
216
|
-
status: frontMatter.status ?? "",
|
|
217
|
-
priority: frontMatter.priority,
|
|
218
|
-
owner: frontMatter.assignment?.owner,
|
|
219
|
-
mode: frontMatter.assignment?.mode,
|
|
220
|
-
path: ticketPath,
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
for (const dependency of frontMatter.dependencies ?? []) {
|
|
224
|
-
if (!nodes.has(dependency)) {
|
|
225
|
-
nodes.set(dependency, loadNodeById(dependency));
|
|
226
|
-
}
|
|
227
|
-
edges.push({ type: "dependency", from: dependency, to: ticketId });
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
for (const blocked of frontMatter.blocks ?? []) {
|
|
231
|
-
if (!nodes.has(blocked)) {
|
|
232
|
-
nodes.set(blocked, loadNodeById(blocked));
|
|
233
|
-
}
|
|
234
|
-
edges.push({ type: "blocks", from: ticketId, to: blocked });
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
for (const related of frontMatter.related ?? []) {
|
|
238
|
-
if (!nodes.has(related)) {
|
|
239
|
-
nodes.set(related, loadNodeById(related));
|
|
240
|
-
}
|
|
241
|
-
edges.push({ type: "related", from: ticketId, to: related });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return {
|
|
246
|
-
nodes: [...nodes.values()],
|
|
247
|
-
edges,
|
|
248
|
-
root_id: rootId,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function renderMermaid(graph, includeRelated, timestamp) {
|
|
247
|
+
function renderMermaid(graph, includeRelated, timestamp, view) {
|
|
253
248
|
const statusClasses = {
|
|
254
249
|
todo: "fill:#ddd,stroke:#999",
|
|
255
250
|
doing: "fill:#d0e7ff,stroke:#3b82f6",
|
|
@@ -259,7 +254,7 @@ function renderMermaid(graph, includeRelated, timestamp) {
|
|
|
259
254
|
};
|
|
260
255
|
|
|
261
256
|
const lines = [
|
|
262
|
-
|
|
257
|
+
`# Ticket ${view} graph`,
|
|
263
258
|
`_Generated at ${timestamp} UTC_`,
|
|
264
259
|
"",
|
|
265
260
|
"```mermaid",
|
|
@@ -286,7 +281,8 @@ function renderMermaid(graph, includeRelated, timestamp) {
|
|
|
286
281
|
if (!source || !target) {
|
|
287
282
|
continue;
|
|
288
283
|
}
|
|
289
|
-
|
|
284
|
+
const connector = edge.type === "contains" ? "-.->" : "-->";
|
|
285
|
+
lines.push(` ${source} ${connector}|${edge.type}| ${target}`);
|
|
290
286
|
}
|
|
291
287
|
|
|
292
288
|
for (const [status, style] of Object.entries(statusClasses)) {
|
|
@@ -333,8 +329,8 @@ function renderDot(graph, includeRelated) {
|
|
|
333
329
|
if (!source || !target) {
|
|
334
330
|
continue;
|
|
335
331
|
}
|
|
336
|
-
const style = edge.type
|
|
337
|
-
lines.push(` ${source} -> ${target} [style=${style}];`);
|
|
332
|
+
const style = ["related", "contains"].includes(edge.type) ? "dashed" : "solid";
|
|
333
|
+
lines.push(` ${source} -> ${target} [style=${style}, label="${edge.type}"];`);
|
|
338
334
|
}
|
|
339
335
|
|
|
340
336
|
lines.push("}");
|
|
@@ -657,6 +653,7 @@ function generateExampleTickets() {
|
|
|
657
653
|
status: "doing",
|
|
658
654
|
priority: "high",
|
|
659
655
|
labels: ["epic", "planning"],
|
|
656
|
+
planning: { node_type: "group", lane: "build", rank: 1, horizon: "current" },
|
|
660
657
|
assignment: { mode: "mixed", owner: "team:core" },
|
|
661
658
|
related: ["backend", "frontend", "testing", "docs", "release"],
|
|
662
659
|
agent_limits: {
|
|
@@ -678,6 +675,7 @@ function generateExampleTickets() {
|
|
|
678
675
|
logs: [
|
|
679
676
|
{
|
|
680
677
|
summary: "Epic created and split into child tickets.",
|
|
678
|
+
context: ["Parent planning context for Feature Alpha", "Child tickets were split for parallel execution"],
|
|
681
679
|
tickets_created: ["backend", "frontend", "testing", "docs"],
|
|
682
680
|
next_steps: ["Coordinate release window", "Monitor blockers"],
|
|
683
681
|
},
|
|
@@ -689,6 +687,7 @@ function generateExampleTickets() {
|
|
|
689
687
|
status: "doing",
|
|
690
688
|
priority: "high",
|
|
691
689
|
labels: ["backend", "api"],
|
|
690
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "build", rank: 1, horizon: "current", precedes: ["frontend", "testing"] },
|
|
692
691
|
assignment: { mode: "agent_only", owner: "agent:codex" },
|
|
693
692
|
dependencies: ["parent"],
|
|
694
693
|
blocks: ["frontend", "testing", "release"],
|
|
@@ -709,7 +708,12 @@ function generateExampleTickets() {
|
|
|
709
708
|
summary: "Scaffolded API and outlined endpoints.",
|
|
710
709
|
decisions: ["Using UUID primary keys", "Respond with JSON:API style"],
|
|
711
710
|
created_from: "parent",
|
|
712
|
-
|
|
711
|
+
context: ["Acceptance criteria from parent", "Release target"],
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
summary: "Agent claimed backend work.",
|
|
715
|
+
event_type: "claim",
|
|
716
|
+
claim: { action: "acquire", holder_id: "agent:codex", holder_type: "agent", ttl_minutes: 60 },
|
|
713
717
|
},
|
|
714
718
|
],
|
|
715
719
|
},
|
|
@@ -719,6 +723,7 @@ function generateExampleTickets() {
|
|
|
719
723
|
status: "todo",
|
|
720
724
|
priority: "medium",
|
|
721
725
|
labels: ["frontend", "ui"],
|
|
726
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "build", rank: 2, horizon: "current" },
|
|
722
727
|
dependencies: ["backend"],
|
|
723
728
|
related: ["testing"],
|
|
724
729
|
verification: { commands: ["npm test", "npm run lint"] },
|
|
@@ -732,7 +737,7 @@ function generateExampleTickets() {
|
|
|
732
737
|
summary: "Waiting on API responses to stabilize.",
|
|
733
738
|
blockers: ["Backend contract not finalized"],
|
|
734
739
|
created_from: "parent",
|
|
735
|
-
|
|
740
|
+
context: ["Design mocks v1.2", "API schema draft"],
|
|
736
741
|
},
|
|
737
742
|
],
|
|
738
743
|
},
|
|
@@ -742,6 +747,7 @@ function generateExampleTickets() {
|
|
|
742
747
|
status: "todo",
|
|
743
748
|
priority: "medium",
|
|
744
749
|
labels: ["qa"],
|
|
750
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "verify", rank: 1, horizon: "current" },
|
|
745
751
|
dependencies: ["backend", "frontend"],
|
|
746
752
|
verification: { commands: ["npm test"] },
|
|
747
753
|
body: {
|
|
@@ -754,7 +760,7 @@ function generateExampleTickets() {
|
|
|
754
760
|
summary: "Outlined E2E scenarios to automate.",
|
|
755
761
|
next_steps: ["Set up test data fixtures"],
|
|
756
762
|
created_from: "parent",
|
|
757
|
-
|
|
763
|
+
context: ["Frontend flow chart", "Backend contract v1"],
|
|
758
764
|
},
|
|
759
765
|
],
|
|
760
766
|
},
|
|
@@ -764,6 +770,7 @@ function generateExampleTickets() {
|
|
|
764
770
|
status: "todo",
|
|
765
771
|
priority: "low",
|
|
766
772
|
labels: ["docs"],
|
|
773
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "launch", rank: 2, horizon: "next" },
|
|
767
774
|
dependencies: ["testing"],
|
|
768
775
|
verification: { commands: ["npm run lint:docs"] },
|
|
769
776
|
body: {
|
|
@@ -776,7 +783,7 @@ function generateExampleTickets() {
|
|
|
776
783
|
summary: "Preparing outline; waiting on test results.",
|
|
777
784
|
blockers: ["Integration tests pending"],
|
|
778
785
|
created_from: "parent",
|
|
779
|
-
|
|
786
|
+
context: ["Feature overview", "Known limitations"],
|
|
780
787
|
},
|
|
781
788
|
],
|
|
782
789
|
},
|
|
@@ -786,6 +793,7 @@ function generateExampleTickets() {
|
|
|
786
793
|
status: "todo",
|
|
787
794
|
priority: "high",
|
|
788
795
|
labels: ["release"],
|
|
796
|
+
planning: { node_type: "checkpoint", group_ids: ["parent"], lane: "launch", rank: 1, horizon: "current" },
|
|
789
797
|
dependencies: ["testing"],
|
|
790
798
|
blocks: ["bugfix"],
|
|
791
799
|
verification: { commands: ["npx @picoai/tickets validate"] },
|
|
@@ -797,6 +805,7 @@ function generateExampleTickets() {
|
|
|
797
805
|
logs: [
|
|
798
806
|
{
|
|
799
807
|
summary: "Drafted release checklist; waiting on test green.",
|
|
808
|
+
context: ["Release checklist draft", "Waiting on integration test completion"],
|
|
800
809
|
next_steps: ["Book release window"],
|
|
801
810
|
},
|
|
802
811
|
],
|
|
@@ -804,9 +813,11 @@ function generateExampleTickets() {
|
|
|
804
813
|
{
|
|
805
814
|
key: "bugfix",
|
|
806
815
|
title: "Bugfix: address regression found during Alpha",
|
|
807
|
-
status: "
|
|
816
|
+
status: "canceled",
|
|
808
817
|
priority: "high",
|
|
809
818
|
labels: ["bug", "regression"],
|
|
819
|
+
planning: { node_type: "work", group_ids: ["parent"], lane: "build", rank: 3, horizon: "current" },
|
|
820
|
+
resolution: "dropped",
|
|
810
821
|
dependencies: ["backend"],
|
|
811
822
|
related: ["testing"],
|
|
812
823
|
verification: { commands: ["npm test"] },
|
|
@@ -817,8 +828,9 @@ function generateExampleTickets() {
|
|
|
817
828
|
},
|
|
818
829
|
logs: [
|
|
819
830
|
{
|
|
820
|
-
summary: "
|
|
821
|
-
|
|
831
|
+
summary: "Dropped after mitigation in backend workstream.",
|
|
832
|
+
context: ["Regression repro identified", "Awaiting backend deployment before retry"],
|
|
833
|
+
decisions: ["Folded remediation into backend ticket"],
|
|
822
834
|
},
|
|
823
835
|
],
|
|
824
836
|
},
|
|
@@ -847,6 +859,16 @@ function generateExampleTickets() {
|
|
|
847
859
|
if (spec.assignment) {
|
|
848
860
|
frontMatter.assignment = spec.assignment;
|
|
849
861
|
}
|
|
862
|
+
if (spec.planning) {
|
|
863
|
+
frontMatter.planning = {
|
|
864
|
+
...spec.planning,
|
|
865
|
+
group_ids: spec.planning.group_ids?.map((value) => ids[value]) ?? spec.planning.group_ids,
|
|
866
|
+
precedes: spec.planning.precedes?.map((value) => ids[value]) ?? spec.planning.precedes,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
if (spec.resolution) {
|
|
870
|
+
frontMatter.resolution = spec.resolution;
|
|
871
|
+
}
|
|
850
872
|
for (const relationshipKey of ["dependencies", "blocks", "related"]) {
|
|
851
873
|
if (spec[relationshipKey]) {
|
|
852
874
|
frontMatter[relationshipKey] = spec[relationshipKey].map((value) => ids[value]);
|
|
@@ -886,16 +908,18 @@ function generateExampleTickets() {
|
|
|
886
908
|
actor_type: "agent",
|
|
887
909
|
actor_id: "tickets-init",
|
|
888
910
|
summary: logSpec.summary,
|
|
911
|
+
event_type: logSpec.event_type ?? "work",
|
|
889
912
|
written_by: "tickets",
|
|
890
913
|
};
|
|
891
914
|
|
|
892
915
|
for (const key of [
|
|
916
|
+
"context",
|
|
917
|
+
"claim",
|
|
893
918
|
"decisions",
|
|
894
919
|
"next_steps",
|
|
895
920
|
"blockers",
|
|
896
921
|
"tickets_created",
|
|
897
922
|
"created_from",
|
|
898
|
-
"context_carried_over",
|
|
899
923
|
]) {
|
|
900
924
|
if (!(key in logSpec)) {
|
|
901
925
|
continue;
|
|
@@ -905,6 +929,12 @@ function generateExampleTickets() {
|
|
|
905
929
|
logEntry[key] = logSpec[key].map((value) => ids[value]);
|
|
906
930
|
} else if (key === "created_from") {
|
|
907
931
|
logEntry[key] = ids[logSpec[key]] ?? logSpec[key];
|
|
932
|
+
} else if (key === "claim") {
|
|
933
|
+
logEntry.claim = {
|
|
934
|
+
...logSpec.claim,
|
|
935
|
+
claim_id: newUuidv7().toLowerCase(),
|
|
936
|
+
expires_at: iso8601(new Date(now.getTime() + (logSpec.claim.ttl_minutes ?? 60) * 60 * 1000)),
|
|
937
|
+
};
|
|
908
938
|
} else {
|
|
909
939
|
logEntry[key] = logSpec[key];
|
|
910
940
|
}
|
|
@@ -923,6 +953,8 @@ async function cmdInit(options) {
|
|
|
923
953
|
const apply = Boolean(options.apply);
|
|
924
954
|
|
|
925
955
|
syncTicketsMd(root, apply);
|
|
956
|
+
syncRepoConfig(root);
|
|
957
|
+
syncRepoSkill(root, apply);
|
|
926
958
|
|
|
927
959
|
const agentsPath = path.join(root, "AGENTS_EXAMPLE.md");
|
|
928
960
|
const agentsTemplatePath = path.join(".tickets", "spec", "AGENTS_EXAMPLE.md");
|
|
@@ -935,8 +967,18 @@ async function cmdInit(options) {
|
|
|
935
967
|
const versionDir = path.join(repoBaseDir, "version");
|
|
936
968
|
ensureDir(versionDir);
|
|
937
969
|
|
|
938
|
-
const
|
|
939
|
-
writeTemplateFile(
|
|
970
|
+
const currentSpecPath = path.join(versionDir, "20260317-tickets-spec.md");
|
|
971
|
+
writeTemplateFile(currentSpecPath, path.join(".tickets", "spec", "version", "20260317-tickets-spec.md"), apply);
|
|
972
|
+
|
|
973
|
+
const previousCurrentSpecPath = path.join(versionDir, "20260311-tickets-spec.md");
|
|
974
|
+
writeTemplateFile(
|
|
975
|
+
previousCurrentSpecPath,
|
|
976
|
+
path.join(".tickets", "spec", "version", "20260311-tickets-spec.md"),
|
|
977
|
+
apply,
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
const previousSpecPath = path.join(versionDir, "20260205-tickets-spec.md");
|
|
981
|
+
writeTemplateFile(previousSpecPath, path.join(".tickets", "spec", "version", "20260205-tickets-spec.md"), apply);
|
|
940
982
|
|
|
941
983
|
const proposedPath = path.join(versionDir, "PROPOSED-tickets-spec.md");
|
|
942
984
|
writeTemplateFile(proposedPath, path.join(".tickets", "spec", "version", "PROPOSED-tickets-spec.md"), apply);
|
|
@@ -951,6 +993,7 @@ async function cmdInit(options) {
|
|
|
951
993
|
|
|
952
994
|
async function cmdNew(options) {
|
|
953
995
|
ensureDir(ticketsDir());
|
|
996
|
+
const profile = loadWorkflowProfile();
|
|
954
997
|
const ticketId = newUuidv7().toLowerCase();
|
|
955
998
|
const ticketDir = path.join(ticketsDir(), ticketId);
|
|
956
999
|
ensureDir(path.join(ticketDir, "logs"));
|
|
@@ -1007,6 +1050,17 @@ async function cmdNew(options) {
|
|
|
1007
1050
|
if (options.verificationCommands?.length) {
|
|
1008
1051
|
frontMatter.verification = { commands: options.verificationCommands };
|
|
1009
1052
|
}
|
|
1053
|
+
frontMatter.planning = {
|
|
1054
|
+
node_type: options.nodeType || profile.defaults?.planning?.node_type || "work",
|
|
1055
|
+
group_ids: options.groupIds?.length ? options.groupIds : [],
|
|
1056
|
+
lane: options.lane ?? null,
|
|
1057
|
+
rank: options.rank ?? null,
|
|
1058
|
+
horizon: options.horizon ?? null,
|
|
1059
|
+
precedes: options.precedes?.length ? options.precedes : [],
|
|
1060
|
+
};
|
|
1061
|
+
if (options.resolution) {
|
|
1062
|
+
frontMatter.resolution = options.resolution;
|
|
1063
|
+
}
|
|
1010
1064
|
|
|
1011
1065
|
const body = [
|
|
1012
1066
|
"# Ticket",
|
|
@@ -1033,6 +1087,8 @@ async function cmdValidate(options) {
|
|
|
1033
1087
|
const ticketPaths = collectTicketPaths(options.ticket);
|
|
1034
1088
|
const issues = [];
|
|
1035
1089
|
|
|
1090
|
+
issues.push(...validateRepoConfig(repoRoot()));
|
|
1091
|
+
|
|
1036
1092
|
for (const ticketPath of ticketPaths) {
|
|
1037
1093
|
const [ticketIssues] = validateTicket(ticketPath, options.allFields);
|
|
1038
1094
|
issues.push(...ticketIssues);
|
|
@@ -1085,28 +1141,37 @@ async function cmdValidate(options) {
|
|
|
1085
1141
|
async function cmdStatus(options) {
|
|
1086
1142
|
const ticketPath = resolveTicketPath(options.ticket);
|
|
1087
1143
|
const [frontMatter, body] = loadTicket(ticketPath);
|
|
1144
|
+
const previousStatus = frontMatter.status;
|
|
1145
|
+
const actorId = resolveActorId(options.actorId);
|
|
1146
|
+
const actorType = resolveActorType(options.actorType, actorId);
|
|
1147
|
+
const context = normalizeContextItems(options.context);
|
|
1088
1148
|
|
|
1089
1149
|
frontMatter.status = options.status;
|
|
1090
1150
|
writeTicket(ticketPath, frontMatter, body);
|
|
1091
1151
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1152
|
+
const runId = options.runId || newUuidv7();
|
|
1153
|
+
const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
|
|
1154
|
+
const entry = {
|
|
1155
|
+
version: FORMAT_VERSION,
|
|
1156
|
+
version_url: FORMAT_VERSION_URL,
|
|
1157
|
+
ts: iso8601(nowUtc()),
|
|
1158
|
+
run_started: runStarted,
|
|
1159
|
+
actor_type: actorType,
|
|
1160
|
+
actor_id: actorId,
|
|
1161
|
+
summary:
|
|
1162
|
+
previousStatus === options.status
|
|
1163
|
+
? `Status reaffirmed as ${options.status}`
|
|
1164
|
+
: `Status changed from ${previousStatus ?? "unknown"} to ${options.status}`,
|
|
1165
|
+
event_type: "status",
|
|
1166
|
+
written_by: "tickets",
|
|
1167
|
+
};
|
|
1168
|
+
if (context.length > 0) {
|
|
1169
|
+
entry.context = context;
|
|
1108
1170
|
}
|
|
1109
1171
|
|
|
1172
|
+
const logPath = path.join(path.dirname(ticketPath), "logs", `${runStarted}-${runId}.jsonl`);
|
|
1173
|
+
appendJsonl(logPath, entry);
|
|
1174
|
+
|
|
1110
1175
|
return 0;
|
|
1111
1176
|
}
|
|
1112
1177
|
|
|
@@ -1114,16 +1179,26 @@ async function cmdLog(options) {
|
|
|
1114
1179
|
const ticketPath = resolveTicketPath(options.ticket);
|
|
1115
1180
|
const runId = options.runId || newUuidv7();
|
|
1116
1181
|
const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
|
|
1182
|
+
const actorId = resolveActorId(options.actorId);
|
|
1183
|
+
const actorType = resolveActorType(options.actorType, actorId);
|
|
1184
|
+
const context = normalizeContextItems(options.context);
|
|
1185
|
+
if (options.machine && context.length === 0) {
|
|
1186
|
+
throw new Error("Machine-written work logs require at least one --context item");
|
|
1187
|
+
}
|
|
1117
1188
|
|
|
1118
1189
|
const entry = {
|
|
1119
1190
|
version: FORMAT_VERSION,
|
|
1120
1191
|
version_url: FORMAT_VERSION_URL,
|
|
1121
1192
|
ts: iso8601(nowUtc()),
|
|
1122
1193
|
run_started: runStarted,
|
|
1123
|
-
actor_type:
|
|
1124
|
-
actor_id:
|
|
1194
|
+
actor_type: actorType,
|
|
1195
|
+
actor_id: actorId,
|
|
1125
1196
|
summary: options.summary,
|
|
1197
|
+
event_type: "work",
|
|
1126
1198
|
};
|
|
1199
|
+
if (context.length > 0) {
|
|
1200
|
+
entry.context = context;
|
|
1201
|
+
}
|
|
1127
1202
|
|
|
1128
1203
|
if (options.machine) {
|
|
1129
1204
|
entry.written_by = "tickets";
|
|
@@ -1146,9 +1221,6 @@ async function cmdLog(options) {
|
|
|
1146
1221
|
if (options.createdFrom) {
|
|
1147
1222
|
entry.created_from = options.createdFrom;
|
|
1148
1223
|
}
|
|
1149
|
-
if (options.contextCarriedOver?.length) {
|
|
1150
|
-
entry.context_carried_over = options.contextCarriedOver;
|
|
1151
|
-
}
|
|
1152
1224
|
if (options.verificationCommands?.length || options.verificationResults) {
|
|
1153
1225
|
entry.verification = {
|
|
1154
1226
|
commands: options.verificationCommands || [],
|
|
@@ -1169,6 +1241,13 @@ async function cmdList(options) {
|
|
|
1169
1241
|
owner: options.owner,
|
|
1170
1242
|
label: options.label,
|
|
1171
1243
|
text: options.text,
|
|
1244
|
+
nodeType: options.nodeType,
|
|
1245
|
+
group: options.group,
|
|
1246
|
+
lane: options.lane,
|
|
1247
|
+
horizon: options.horizon,
|
|
1248
|
+
claimed: Boolean(options.claimed),
|
|
1249
|
+
claimedBy: options.claimedBy,
|
|
1250
|
+
ready: Boolean(options.ready),
|
|
1172
1251
|
});
|
|
1173
1252
|
|
|
1174
1253
|
if (options.json) {
|
|
@@ -1181,7 +1260,20 @@ async function cmdList(options) {
|
|
|
1181
1260
|
return 0;
|
|
1182
1261
|
}
|
|
1183
1262
|
|
|
1184
|
-
const headers = [
|
|
1263
|
+
const headers = [
|
|
1264
|
+
"id",
|
|
1265
|
+
"title",
|
|
1266
|
+
"status",
|
|
1267
|
+
"priority",
|
|
1268
|
+
"node_type",
|
|
1269
|
+
"lane",
|
|
1270
|
+
"rank",
|
|
1271
|
+
"horizon",
|
|
1272
|
+
"ready",
|
|
1273
|
+
"owner",
|
|
1274
|
+
"mode",
|
|
1275
|
+
"last_updated",
|
|
1276
|
+
];
|
|
1185
1277
|
process.stdout.write(`${headers.join(" | ")}\n`);
|
|
1186
1278
|
for (const row of rows) {
|
|
1187
1279
|
process.stdout.write(`${headers.map((key) => String(row[key] ?? "")).join(" | ")}\n`);
|
|
@@ -1190,6 +1282,119 @@ async function cmdList(options) {
|
|
|
1190
1282
|
return 0;
|
|
1191
1283
|
}
|
|
1192
1284
|
|
|
1285
|
+
async function cmdPlan(options) {
|
|
1286
|
+
const summary = buildPlanSummary({
|
|
1287
|
+
group: options.group ?? options.root ?? null,
|
|
1288
|
+
horizon: options.horizon ?? null,
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
if (options.format === "json") {
|
|
1292
|
+
process.stdout.write(`${JSON.stringify(summary, null, 2)}\n`);
|
|
1293
|
+
return 0;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
process.stdout.write("Ready queue\n");
|
|
1297
|
+
for (const row of summary.ready) {
|
|
1298
|
+
process.stdout.write(
|
|
1299
|
+
`${row.id} | ${row.title} | lane=${row.lane ?? ""} | rank=${row.rank ?? ""} | horizon=${row.horizon ?? ""}\n`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
process.stdout.write("\nGroups\n");
|
|
1303
|
+
for (const group of summary.groups) {
|
|
1304
|
+
const rollup = group.rollup ?? {};
|
|
1305
|
+
process.stdout.write(
|
|
1306
|
+
`${group.id} | ${group.title} | ${group.node_type} | ${rollup.done_completed ?? 0}/${rollup.active_leaf ?? 0} complete | merged=${rollup.merged ?? 0} | dropped=${rollup.dropped ?? 0}\n`,
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
return 0;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
async function cmdClaim(options) {
|
|
1314
|
+
const ticketPath = resolveTicketPath(options.ticket);
|
|
1315
|
+
const logsDir = path.join(path.dirname(ticketPath), "logs");
|
|
1316
|
+
ensureDir(logsDir);
|
|
1317
|
+
const profile = loadWorkflowProfile();
|
|
1318
|
+
|
|
1319
|
+
const actorId = resolveActorId(options.actorId);
|
|
1320
|
+
const actorType = resolveActorType(options.actorType, actorId);
|
|
1321
|
+
const events = loadClaimEvents(logsDir);
|
|
1322
|
+
const activeClaim = deriveActiveClaim(events, nowUtc());
|
|
1323
|
+
const runId = options.runId || newUuidv7();
|
|
1324
|
+
const runStarted = (options.runStarted || isoBasic(nowUtc())).replaceAll(" ", "");
|
|
1325
|
+
const ttlMinutes = options.ttlMinutes || profile.defaults?.claims?.ttl_minutes || DEFAULT_CLAIM_TTL_MINUTES;
|
|
1326
|
+
const now = nowUtc();
|
|
1327
|
+
|
|
1328
|
+
let action;
|
|
1329
|
+
let summary;
|
|
1330
|
+
let claimId = newUuidv7().toLowerCase();
|
|
1331
|
+
let supersedesClaimId = null;
|
|
1332
|
+
|
|
1333
|
+
if (options.release) {
|
|
1334
|
+
if (!activeClaim) {
|
|
1335
|
+
process.stdout.write("No active claim.\n");
|
|
1336
|
+
return 1;
|
|
1337
|
+
}
|
|
1338
|
+
if (activeClaim.holder_id !== actorId && !options.force) {
|
|
1339
|
+
process.stdout.write(`Ticket is claimed by ${activeClaim.holder_id} until ${activeClaim.expires_at}.\n`);
|
|
1340
|
+
return 1;
|
|
1341
|
+
}
|
|
1342
|
+
if (activeClaim.holder_id !== actorId && options.force && !options.reason) {
|
|
1343
|
+
throw new Error("Forced claim release requires --reason");
|
|
1344
|
+
}
|
|
1345
|
+
action = "release";
|
|
1346
|
+
claimId = activeClaim.claim_id;
|
|
1347
|
+
summary = `Released claim ${claimId}`;
|
|
1348
|
+
} else if (activeClaim && activeClaim.holder_id === actorId) {
|
|
1349
|
+
action = "renew";
|
|
1350
|
+
claimId = activeClaim.claim_id;
|
|
1351
|
+
summary = `Renewed claim ${claimId}`;
|
|
1352
|
+
} else if (activeClaim && activeClaim.holder_id !== actorId) {
|
|
1353
|
+
if (!options.force) {
|
|
1354
|
+
process.stdout.write(`Ticket is claimed by ${activeClaim.holder_id} until ${activeClaim.expires_at}.\n`);
|
|
1355
|
+
return 1;
|
|
1356
|
+
}
|
|
1357
|
+
if (!options.reason) {
|
|
1358
|
+
throw new Error("Forced claim override requires --reason");
|
|
1359
|
+
}
|
|
1360
|
+
action = "override";
|
|
1361
|
+
supersedesClaimId = activeClaim.claim_id;
|
|
1362
|
+
summary = `Overrode claim ${activeClaim.claim_id}`;
|
|
1363
|
+
} else {
|
|
1364
|
+
action = "acquire";
|
|
1365
|
+
summary = `Acquired claim ${claimId}`;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
const entry = {
|
|
1369
|
+
version: FORMAT_VERSION,
|
|
1370
|
+
version_url: FORMAT_VERSION_URL,
|
|
1371
|
+
ts: iso8601(now),
|
|
1372
|
+
run_started: runStarted,
|
|
1373
|
+
actor_type: actorType,
|
|
1374
|
+
actor_id: actorId,
|
|
1375
|
+
summary,
|
|
1376
|
+
event_type: "claim",
|
|
1377
|
+
written_by: "tickets",
|
|
1378
|
+
claim: {
|
|
1379
|
+
action,
|
|
1380
|
+
claim_id: claimId,
|
|
1381
|
+
holder_id: actorId,
|
|
1382
|
+
holder_type: actorType,
|
|
1383
|
+
reason: options.reason ?? "",
|
|
1384
|
+
supersedes_claim_id: supersedesClaimId,
|
|
1385
|
+
},
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
if (action !== "release") {
|
|
1389
|
+
entry.claim.ttl_minutes = ttlMinutes;
|
|
1390
|
+
entry.claim.expires_at = iso8601(new Date(now.getTime() + ttlMinutes * 60 * 1000));
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
appendJsonl(path.join(logsDir, `${runStarted}-${runId}.jsonl`), entry);
|
|
1394
|
+
process.stdout.write(`${summary}\n`);
|
|
1395
|
+
return 0;
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1193
1398
|
async function cmdRepair(options) {
|
|
1194
1399
|
const nonInteractive = options.nonInteractive;
|
|
1195
1400
|
|
|
@@ -1226,6 +1431,24 @@ async function cmdRepair(options) {
|
|
|
1226
1431
|
autoEnableSafe: !options.interactive,
|
|
1227
1432
|
}),
|
|
1228
1433
|
);
|
|
1434
|
+
|
|
1435
|
+
const logsDir = path.join(path.dirname(ticketPath), "logs");
|
|
1436
|
+
if (!fs.existsSync(logsDir)) {
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
const logFiles = fs
|
|
1440
|
+
.readdirSync(logsDir, { withFileTypes: true })
|
|
1441
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
1442
|
+
.map((entry) => path.join(logsDir, entry.name))
|
|
1443
|
+
.sort((a, b) => a.localeCompare(b));
|
|
1444
|
+
for (const logFile of logFiles) {
|
|
1445
|
+
repairs.push(
|
|
1446
|
+
...buildRepairsFromIssues(validateRunLog(logFile, false), {
|
|
1447
|
+
includeOptional: options.allFields,
|
|
1448
|
+
autoEnableSafe: !options.interactive,
|
|
1449
|
+
}),
|
|
1450
|
+
);
|
|
1451
|
+
}
|
|
1229
1452
|
}
|
|
1230
1453
|
|
|
1231
1454
|
const changes = options.interactive
|
|
@@ -1243,7 +1466,12 @@ async function cmdRepair(options) {
|
|
|
1243
1466
|
}
|
|
1244
1467
|
|
|
1245
1468
|
async function cmdGraph(options) {
|
|
1246
|
-
const
|
|
1469
|
+
const snapshot = buildPlanningSnapshot();
|
|
1470
|
+
const graph = buildGraphData(snapshot, {
|
|
1471
|
+
ticket: options.ticket,
|
|
1472
|
+
view: options.view,
|
|
1473
|
+
includeRelated: options.related,
|
|
1474
|
+
});
|
|
1247
1475
|
if (graph.nodes.length === 0) {
|
|
1248
1476
|
process.stdout.write("No tickets found.\n");
|
|
1249
1477
|
return 1;
|
|
@@ -1254,8 +1482,8 @@ async function cmdGraph(options) {
|
|
|
1254
1482
|
|
|
1255
1483
|
const timestamp = isoBasic(nowUtc());
|
|
1256
1484
|
const base = options.ticket
|
|
1257
|
-
?
|
|
1258
|
-
:
|
|
1485
|
+
? `${options.view}_for_${graph.root_id || "subset"}`
|
|
1486
|
+
: options.view;
|
|
1259
1487
|
const ext = { mermaid: "md", dot: "dot", json: "json" }[options.format];
|
|
1260
1488
|
const outPath = options.output
|
|
1261
1489
|
? path.resolve(repoRoot(), options.output)
|
|
@@ -1267,7 +1495,7 @@ async function cmdGraph(options) {
|
|
|
1267
1495
|
} else if (options.format === "dot") {
|
|
1268
1496
|
fs.writeFileSync(outPath, renderDot(graph, options.related));
|
|
1269
1497
|
} else {
|
|
1270
|
-
fs.writeFileSync(outPath, renderMermaid(graph, options.related, timestamp));
|
|
1498
|
+
fs.writeFileSync(outPath, renderMermaid(graph, options.related, timestamp, options.view));
|
|
1271
1499
|
}
|
|
1272
1500
|
|
|
1273
1501
|
process.stdout.write(`${outPath}\n`);
|
|
@@ -1308,6 +1536,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1308
1536
|
.option("--checkpoint-every-minutes <minutes>")
|
|
1309
1537
|
.option("--verification-command <command>", "Verification command", collectOption, [])
|
|
1310
1538
|
.option("--created-at <timestamp>")
|
|
1539
|
+
.option("--node-type <nodeType>")
|
|
1540
|
+
.option("--group-id <groupId>", "Group membership ticket id", collectOption, [])
|
|
1541
|
+
.option("--lane <lane>")
|
|
1542
|
+
.option("--rank <rank>")
|
|
1543
|
+
.option("--horizon <horizon>")
|
|
1544
|
+
.option("--precedes <ticketId>", "Sequence successor ticket id", collectOption, [])
|
|
1545
|
+
.option("--resolution <resolution>")
|
|
1311
1546
|
.action(async (options) => {
|
|
1312
1547
|
if (!STATUS_VALUES.includes(options.status)) {
|
|
1313
1548
|
throw new Error(`Invalid --status. Use one of: ${STATUS_VALUES.join(", ")}`);
|
|
@@ -1320,6 +1555,21 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1320
1555
|
`Invalid --assignment-mode. Use one of: ${ASSIGNMENT_MODE_VALUES.join(", ")}`,
|
|
1321
1556
|
);
|
|
1322
1557
|
}
|
|
1558
|
+
if (options.nodeType && !PLANNING_NODE_TYPES.includes(options.nodeType)) {
|
|
1559
|
+
throw new Error(`Invalid --node-type. Use one of: ${PLANNING_NODE_TYPES.join(", ")}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (options.resolution && !RESOLUTION_VALUES.includes(options.resolution)) {
|
|
1562
|
+
throw new Error(`Invalid --resolution. Use one of: ${RESOLUTION_VALUES.join(", ")}`);
|
|
1563
|
+
}
|
|
1564
|
+
if (options.rank) {
|
|
1565
|
+
const rank = Number.parseInt(options.rank, 10);
|
|
1566
|
+
if (!Number.isInteger(rank) || rank <= 0) {
|
|
1567
|
+
throw new Error("Invalid --rank. Use a positive integer");
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (options.resolution && !["done", "canceled"].includes(options.status)) {
|
|
1571
|
+
throw new Error("Resolution requires terminal status done or canceled");
|
|
1572
|
+
}
|
|
1323
1573
|
process.exitCode = await cmdNew({
|
|
1324
1574
|
title: options.title,
|
|
1325
1575
|
status: options.status,
|
|
@@ -1340,6 +1590,13 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1340
1590
|
: undefined,
|
|
1341
1591
|
verificationCommands: options.verificationCommand,
|
|
1342
1592
|
createdAt: options.createdAt,
|
|
1593
|
+
nodeType: options.nodeType,
|
|
1594
|
+
groupIds: options.groupId,
|
|
1595
|
+
lane: options.lane,
|
|
1596
|
+
rank: options.rank ? Number.parseInt(options.rank, 10) : null,
|
|
1597
|
+
horizon: options.horizon,
|
|
1598
|
+
precedes: options.precedes,
|
|
1599
|
+
resolution: options.resolution,
|
|
1343
1600
|
});
|
|
1344
1601
|
});
|
|
1345
1602
|
|
|
@@ -1364,17 +1621,24 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1364
1621
|
.description("Update ticket status")
|
|
1365
1622
|
.requiredOption("--ticket <ticket>")
|
|
1366
1623
|
.requiredOption("--status <status>")
|
|
1367
|
-
.option("--
|
|
1624
|
+
.option("--actor-type <actorType>")
|
|
1625
|
+
.option("--actor-id <actorId>")
|
|
1626
|
+
.option("--context <items...>")
|
|
1368
1627
|
.option("--run-id <runId>")
|
|
1369
1628
|
.option("--run-started <runStarted>")
|
|
1370
1629
|
.action(async (options) => {
|
|
1371
1630
|
if (!STATUS_VALUES.includes(options.status)) {
|
|
1372
1631
|
throw new Error(`Invalid --status. Use one of: ${STATUS_VALUES.join(", ")}`);
|
|
1373
1632
|
}
|
|
1633
|
+
if (options.actorType && !isValidActorType(options.actorType)) {
|
|
1634
|
+
throw new Error("Invalid --actor-type. Use one of: human, agent");
|
|
1635
|
+
}
|
|
1374
1636
|
process.exitCode = await cmdStatus({
|
|
1375
1637
|
ticket: options.ticket,
|
|
1376
1638
|
status: options.status,
|
|
1377
|
-
|
|
1639
|
+
actorType: options.actorType,
|
|
1640
|
+
actorId: options.actorId,
|
|
1641
|
+
context: options.context,
|
|
1378
1642
|
runId: options.runId,
|
|
1379
1643
|
runStarted: options.runStarted,
|
|
1380
1644
|
});
|
|
@@ -1386,8 +1650,8 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1386
1650
|
.requiredOption("--ticket <ticket>")
|
|
1387
1651
|
.option("--run-id <runId>")
|
|
1388
1652
|
.option("--run-started <runStarted>")
|
|
1389
|
-
.
|
|
1390
|
-
.
|
|
1653
|
+
.option("--actor-type <actorType>")
|
|
1654
|
+
.option("--actor-id <actorId>")
|
|
1391
1655
|
.requiredOption("--summary <summary>")
|
|
1392
1656
|
.option("--machine")
|
|
1393
1657
|
.option("--changes <files...>")
|
|
@@ -1396,11 +1660,11 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1396
1660
|
.option("--blockers <blockers...>")
|
|
1397
1661
|
.option("--tickets-created <tickets...>")
|
|
1398
1662
|
.option("--created-from <ticketId>")
|
|
1399
|
-
.option("--context
|
|
1663
|
+
.option("--context <items...>")
|
|
1400
1664
|
.option("--verification-commands <commands...>")
|
|
1401
1665
|
.option("--verification-results <results>")
|
|
1402
1666
|
.action(async (options) => {
|
|
1403
|
-
if (!
|
|
1667
|
+
if (options.actorType && !isValidActorType(options.actorType)) {
|
|
1404
1668
|
throw new Error("Invalid --actor-type. Use one of: human, agent");
|
|
1405
1669
|
}
|
|
1406
1670
|
process.exitCode = await cmdLog({
|
|
@@ -1417,7 +1681,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1417
1681
|
blockers: options.blockers,
|
|
1418
1682
|
ticketsCreated: options.ticketsCreated,
|
|
1419
1683
|
createdFrom: options.createdFrom,
|
|
1420
|
-
|
|
1684
|
+
context: options.context,
|
|
1421
1685
|
verificationCommands: options.verificationCommands,
|
|
1422
1686
|
verificationResults: options.verificationResults,
|
|
1423
1687
|
});
|
|
@@ -1432,11 +1696,32 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1432
1696
|
.option("--owner <owner>")
|
|
1433
1697
|
.option("--label <label>")
|
|
1434
1698
|
.option("--text <text>")
|
|
1699
|
+
.option("--node-type <nodeType>")
|
|
1700
|
+
.option("--group <ticketId>")
|
|
1701
|
+
.option("--lane <lane>")
|
|
1702
|
+
.option("--horizon <horizon>")
|
|
1703
|
+
.option("--claimed", "Only show claimed tickets")
|
|
1704
|
+
.option("--claimed-by <actorId>")
|
|
1705
|
+
.option("--ready", "Only show ready tickets")
|
|
1435
1706
|
.option("--json", "JSON output")
|
|
1436
1707
|
.action(async (options) => {
|
|
1437
1708
|
process.exitCode = await cmdList(options);
|
|
1438
1709
|
});
|
|
1439
1710
|
|
|
1711
|
+
program
|
|
1712
|
+
.command("plan")
|
|
1713
|
+
.description("Summarize portfolio rollups and ready work")
|
|
1714
|
+
.option("--root <ticket>")
|
|
1715
|
+
.option("--group <ticket>")
|
|
1716
|
+
.option("--horizon <horizon>")
|
|
1717
|
+
.option("--format <format>", "table | json", "table")
|
|
1718
|
+
.action(async (options) => {
|
|
1719
|
+
if (!["table", "json"].includes(options.format)) {
|
|
1720
|
+
throw new Error("Invalid --format. Use one of: table, json");
|
|
1721
|
+
}
|
|
1722
|
+
process.exitCode = await cmdPlan(options);
|
|
1723
|
+
});
|
|
1724
|
+
|
|
1440
1725
|
program
|
|
1441
1726
|
.command("repair")
|
|
1442
1727
|
.description("Repair tickets")
|
|
@@ -1459,9 +1744,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1459
1744
|
|
|
1460
1745
|
program
|
|
1461
1746
|
.command("graph")
|
|
1462
|
-
.description("
|
|
1747
|
+
.description("Ticket graph")
|
|
1463
1748
|
.option("--ticket <ticket>")
|
|
1464
1749
|
.option("--format <format>", "mermaid | dot | json", "mermaid")
|
|
1750
|
+
.option("--view <view>", "dependency | sequence | portfolio | all", "dependency")
|
|
1465
1751
|
.option("--output <file>")
|
|
1466
1752
|
.option("--related", "Include related edges")
|
|
1467
1753
|
.option("--no-related", "Exclude related edges")
|
|
@@ -1469,14 +1755,53 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
1469
1755
|
if (!["mermaid", "dot", "json"].includes(options.format)) {
|
|
1470
1756
|
throw new Error("Invalid --format. Use one of: mermaid, dot, json");
|
|
1471
1757
|
}
|
|
1758
|
+
if (!GRAPH_VIEW_VALUES.includes(options.view)) {
|
|
1759
|
+
throw new Error(`Invalid --view. Use one of: ${GRAPH_VIEW_VALUES.join(", ")}`);
|
|
1760
|
+
}
|
|
1472
1761
|
process.exitCode = await cmdGraph({
|
|
1473
1762
|
ticket: options.ticket,
|
|
1474
1763
|
format: options.format,
|
|
1764
|
+
view: options.view,
|
|
1475
1765
|
output: options.output,
|
|
1476
1766
|
related: options.related,
|
|
1477
1767
|
});
|
|
1478
1768
|
});
|
|
1479
1769
|
|
|
1770
|
+
program
|
|
1771
|
+
.command("claim")
|
|
1772
|
+
.description("Acquire, renew, release, or override an advisory ticket claim")
|
|
1773
|
+
.requiredOption("--ticket <ticket>")
|
|
1774
|
+
.option("--actor-type <actorType>")
|
|
1775
|
+
.option("--actor-id <actorId>")
|
|
1776
|
+
.option("--run-id <runId>")
|
|
1777
|
+
.option("--run-started <runStarted>")
|
|
1778
|
+
.option("--ttl-minutes <minutes>")
|
|
1779
|
+
.option("--release", "Release the active claim")
|
|
1780
|
+
.option("--force", "Override an active claim held by another actor")
|
|
1781
|
+
.option("--reason <reason>")
|
|
1782
|
+
.action(async (options) => {
|
|
1783
|
+
if (options.actorType && !isValidActorType(options.actorType)) {
|
|
1784
|
+
throw new Error("Invalid --actor-type. Use one of: human, agent");
|
|
1785
|
+
}
|
|
1786
|
+
if (options.ttlMinutes) {
|
|
1787
|
+
const ttl = Number.parseInt(options.ttlMinutes, 10);
|
|
1788
|
+
if (!Number.isInteger(ttl) || ttl <= 0) {
|
|
1789
|
+
throw new Error("Invalid --ttl-minutes. Use a positive integer");
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
process.exitCode = await cmdClaim({
|
|
1793
|
+
ticket: options.ticket,
|
|
1794
|
+
actorType: options.actorType,
|
|
1795
|
+
actorId: options.actorId,
|
|
1796
|
+
runId: options.runId,
|
|
1797
|
+
runStarted: options.runStarted,
|
|
1798
|
+
ttlMinutes: options.ttlMinutes ? Number.parseInt(options.ttlMinutes, 10) : undefined,
|
|
1799
|
+
release: Boolean(options.release),
|
|
1800
|
+
force: Boolean(options.force),
|
|
1801
|
+
reason: options.reason,
|
|
1802
|
+
});
|
|
1803
|
+
});
|
|
1804
|
+
|
|
1480
1805
|
try {
|
|
1481
1806
|
await program.parseAsync(argv, { from: "user" });
|
|
1482
1807
|
} catch (error) {
|