@picoai/tickets 0.3.0 → 0.5.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 +6 -0
- package/.tickets/spec/TICKETS.md +44 -11
- package/.tickets/spec/profile/defaults.yml +2 -0
- package/.tickets/spec/version/20260205-tickets-spec.md +1 -1
- package/.tickets/spec/version/20260311-tickets-spec.md +1 -1
- package/.tickets/spec/version/20260317-2-tickets-spec.md +106 -0
- package/.tickets/spec/version/20260317-3-tickets-spec.md +121 -0
- package/.tickets/spec/version/20260317-4-tickets-spec.md +120 -0
- package/.tickets/spec/version/20260317-tickets-spec.md +1 -1
- package/README.md +25 -2
- package/package.json +1 -1
- package/release-history.json +14 -0
- package/src/cli.js +388 -54
- package/src/lib/config.js +14 -0
- package/src/lib/constants.js +6 -1
- package/src/lib/index.js +241 -0
- package/src/lib/listing.js +6 -3
- package/src/lib/planning.js +249 -152
- package/src/lib/projections.js +13 -0
- package/src/lib/validation.js +454 -0
package/src/lib/planning.js
CHANGED
|
@@ -1,43 +1,18 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
import { loadWorkflowProfile } from "./config.js";
|
|
5
|
-
import { deriveActiveClaim, loadClaimEvents } from "./claims.js";
|
|
6
|
-
import { collectTicketPaths } from "./validation.js";
|
|
7
|
-
import { loadTicket, readJsonl } from "./util.js";
|
|
8
|
-
|
|
9
1
|
const TERMINAL_STATUSES = new Set(["done", "canceled"]);
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
const GROUP_NODE_TYPES = new Set(["group", "checkpoint"]);
|
|
3
|
+
const PRIORITY_ORDER = new Map([
|
|
4
|
+
["critical", 0],
|
|
5
|
+
["high", 1],
|
|
6
|
+
["medium", 2],
|
|
7
|
+
["low", 3],
|
|
8
|
+
["", 4],
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export function isTerminalStatus(status) {
|
|
12
12
|
return TERMINAL_STATUSES.has(status);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function
|
|
16
|
-
const logsDir = path.join(ticketDir, "logs");
|
|
17
|
-
let latest = "";
|
|
18
|
-
|
|
19
|
-
if (!fs.existsSync(logsDir)) {
|
|
20
|
-
return latest;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const logFiles = fs
|
|
24
|
-
.readdirSync(logsDir, { withFileTypes: true })
|
|
25
|
-
.filter((entry) => entry.isFile() && entry.name.endsWith(".jsonl"))
|
|
26
|
-
.map((entry) => path.join(logsDir, entry.name));
|
|
27
|
-
|
|
28
|
-
for (const logFile of logFiles) {
|
|
29
|
-
for (const entry of readJsonl(logFile)) {
|
|
30
|
-
const ts = entry.ts;
|
|
31
|
-
if (typeof ts === "string" && (latest === "" || ts > latest)) {
|
|
32
|
-
latest = ts;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return latest;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizePlanning(frontMatter, profile) {
|
|
15
|
+
export function normalizePlanning(frontMatter, profile) {
|
|
41
16
|
const planning = frontMatter.planning ?? {};
|
|
42
17
|
return {
|
|
43
18
|
node_type: planning.node_type ?? profile.defaults?.planning?.node_type ?? "work",
|
|
@@ -49,6 +24,30 @@ function normalizePlanning(frontMatter, profile) {
|
|
|
49
24
|
};
|
|
50
25
|
}
|
|
51
26
|
|
|
27
|
+
function cloneRow(row) {
|
|
28
|
+
return {
|
|
29
|
+
...row,
|
|
30
|
+
labels: Array.isArray(row.labels) ? [...row.labels] : [],
|
|
31
|
+
dependencies: Array.isArray(row.dependencies) ? [...row.dependencies] : [],
|
|
32
|
+
blocks: Array.isArray(row.blocks) ? [...row.blocks] : [],
|
|
33
|
+
related: Array.isArray(row.related) ? [...row.related] : [],
|
|
34
|
+
planning: {
|
|
35
|
+
node_type: row.planning?.node_type ?? "work",
|
|
36
|
+
group_ids: Array.isArray(row.planning?.group_ids) ? [...row.planning.group_ids] : [],
|
|
37
|
+
lane: row.planning?.lane ?? null,
|
|
38
|
+
rank: row.planning?.rank ?? null,
|
|
39
|
+
horizon: row.planning?.horizon ?? null,
|
|
40
|
+
precedes: Array.isArray(row.planning?.precedes) ? [...row.planning.precedes] : [],
|
|
41
|
+
},
|
|
42
|
+
active_claim: row.active_claim ? { ...row.active_claim } : null,
|
|
43
|
+
blocked_by: {
|
|
44
|
+
dependencies: Array.isArray(row.blocked_by?.dependencies) ? [...row.blocked_by.dependencies] : [],
|
|
45
|
+
predecessors: Array.isArray(row.blocked_by?.predecessors) ? [...row.blocked_by.predecessors] : [],
|
|
46
|
+
},
|
|
47
|
+
rollup: row.rollup ? { ...row.rollup } : null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
52
51
|
function collectGroupLeaves(groupId, membersByGroup, nodesById, seen = new Set(), leaves = new Set()) {
|
|
53
52
|
if (seen.has(groupId)) {
|
|
54
53
|
return leaves;
|
|
@@ -71,7 +70,7 @@ function collectGroupLeaves(groupId, membersByGroup, nodesById, seen = new Set()
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
function computeRollup(row, membersByGroup, nodesById) {
|
|
74
|
-
if (!
|
|
73
|
+
if (!GROUP_NODE_TYPES.has(row.planning.node_type)) {
|
|
75
74
|
return null;
|
|
76
75
|
}
|
|
77
76
|
|
|
@@ -97,6 +96,106 @@ function computeRollup(row, membersByGroup, nodesById) {
|
|
|
97
96
|
};
|
|
98
97
|
}
|
|
99
98
|
|
|
99
|
+
function compareNullableStrings(a, b) {
|
|
100
|
+
return String(a ?? "").localeCompare(String(b ?? ""));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function compareNullableNumbers(a, b) {
|
|
104
|
+
const left = Number.isInteger(a) ? a : Number.MAX_SAFE_INTEGER;
|
|
105
|
+
const right = Number.isInteger(b) ? b : Number.MAX_SAFE_INTEGER;
|
|
106
|
+
return left - right;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function comparePriority(a, b) {
|
|
110
|
+
const left = PRIORITY_ORDER.get(a.priority ?? "") ?? PRIORITY_ORDER.get("");
|
|
111
|
+
const right = PRIORITY_ORDER.get(b.priority ?? "") ?? PRIORITY_ORDER.get("");
|
|
112
|
+
return left - right;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compareLastUpdated(a, b) {
|
|
116
|
+
return String(b.last_updated ?? "").localeCompare(String(a.last_updated ?? ""));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function applyComparatorChain(a, b, comparators) {
|
|
120
|
+
for (const comparator of comparators) {
|
|
121
|
+
const result = comparator(a, b);
|
|
122
|
+
if (result !== 0) {
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function baseOperationalComparators() {
|
|
130
|
+
return [
|
|
131
|
+
(a, b) => compareNullableStrings(a.planning.lane, b.planning.lane),
|
|
132
|
+
(a, b) => compareNullableNumbers(a.planning.rank, b.planning.rank),
|
|
133
|
+
(a, b) => compareNullableStrings(a.title, b.title),
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatClaimSummary(activeClaim) {
|
|
138
|
+
if (!activeClaim?.holder_id) {
|
|
139
|
+
return "";
|
|
140
|
+
}
|
|
141
|
+
const until = activeClaim.expires_at ? ` until ${activeClaim.expires_at}` : "";
|
|
142
|
+
return `${activeClaim.holder_id}${until}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function buildPlanningSnapshotFromRows(rows, profile) {
|
|
146
|
+
const snapshotRows = rows.map((row) => cloneRow(row));
|
|
147
|
+
const nodesById = new Map(snapshotRows.map((row) => [row.id, row]));
|
|
148
|
+
const predecessorsById = new Map();
|
|
149
|
+
const membersByGroup = new Map();
|
|
150
|
+
|
|
151
|
+
for (const row of snapshotRows) {
|
|
152
|
+
for (const successorId of row.planning.precedes) {
|
|
153
|
+
if (!predecessorsById.has(successorId)) {
|
|
154
|
+
predecessorsById.set(successorId, []);
|
|
155
|
+
}
|
|
156
|
+
predecessorsById.get(successorId).push(row.id);
|
|
157
|
+
}
|
|
158
|
+
for (const groupId of row.planning.group_ids) {
|
|
159
|
+
if (!membersByGroup.has(groupId)) {
|
|
160
|
+
membersByGroup.set(groupId, []);
|
|
161
|
+
}
|
|
162
|
+
membersByGroup.get(groupId).push(row.id);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const row of snapshotRows) {
|
|
167
|
+
const unresolvedDependencies = row.dependencies.filter((id) => {
|
|
168
|
+
const dependency = nodesById.get(id);
|
|
169
|
+
return dependency ? !isTerminalStatus(dependency.status) : true;
|
|
170
|
+
});
|
|
171
|
+
const unresolvedPredecessors = (predecessorsById.get(row.id) ?? []).filter((id) => {
|
|
172
|
+
const predecessor = nodesById.get(id);
|
|
173
|
+
return predecessor ? !isTerminalStatus(predecessor.status) : true;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
row.blocked_by = {
|
|
177
|
+
dependencies: unresolvedDependencies,
|
|
178
|
+
predecessors: unresolvedPredecessors,
|
|
179
|
+
};
|
|
180
|
+
row.ready =
|
|
181
|
+
row.planning.node_type === "work" &&
|
|
182
|
+
!isTerminalStatus(row.status) &&
|
|
183
|
+
row.mode !== "human_only" &&
|
|
184
|
+
unresolvedDependencies.length === 0 &&
|
|
185
|
+
unresolvedPredecessors.length === 0;
|
|
186
|
+
row.claim_summary = formatClaimSummary(row.active_claim);
|
|
187
|
+
row.rollup = computeRollup(row, membersByGroup, nodesById);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
profile,
|
|
192
|
+
rows: snapshotRows,
|
|
193
|
+
nodesById,
|
|
194
|
+
predecessorsById,
|
|
195
|
+
membersByGroup,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
100
199
|
function passesFilters(row, filters) {
|
|
101
200
|
if (filters.status && row.status !== filters.status) {
|
|
102
201
|
return false;
|
|
@@ -144,90 +243,42 @@ function passesFilters(row, filters) {
|
|
|
144
243
|
return true;
|
|
145
244
|
}
|
|
146
245
|
|
|
147
|
-
export function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const rows = [];
|
|
151
|
-
|
|
152
|
-
for (const ticketPath of paths) {
|
|
153
|
-
try {
|
|
154
|
-
const [frontMatter, body] = loadTicket(ticketPath);
|
|
155
|
-
const ticketDir = path.dirname(ticketPath);
|
|
156
|
-
const activeClaim = deriveActiveClaim(loadClaimEvents(path.join(ticketDir, "logs")));
|
|
157
|
-
|
|
158
|
-
rows.push({
|
|
159
|
-
id: frontMatter.id ?? "",
|
|
160
|
-
title: frontMatter.title ?? "",
|
|
161
|
-
status: frontMatter.status ?? "",
|
|
162
|
-
priority: frontMatter.priority ?? "",
|
|
163
|
-
owner: frontMatter.assignment?.owner ?? null,
|
|
164
|
-
mode: frontMatter.assignment?.mode ?? null,
|
|
165
|
-
labels: Array.isArray(frontMatter.labels) ? frontMatter.labels.filter((label) => typeof label === "string") : [],
|
|
166
|
-
body: body ?? "",
|
|
167
|
-
path: ticketPath,
|
|
168
|
-
dependencies: Array.isArray(frontMatter.dependencies) ? frontMatter.dependencies : [],
|
|
169
|
-
blocks: Array.isArray(frontMatter.blocks) ? frontMatter.blocks : [],
|
|
170
|
-
related: Array.isArray(frontMatter.related) ? frontMatter.related : [],
|
|
171
|
-
planning: normalizePlanning(frontMatter, profile),
|
|
172
|
-
resolution: frontMatter.resolution ?? null,
|
|
173
|
-
active_claim: activeClaim,
|
|
174
|
-
last_updated: lastUpdated(ticketDir),
|
|
175
|
-
});
|
|
176
|
-
} catch {
|
|
177
|
-
// Invalid tickets are surfaced by `validate`; views skip them.
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const nodesById = new Map(rows.map((row) => [row.id, row]));
|
|
182
|
-
const predecessorsById = new Map();
|
|
183
|
-
const membersByGroup = new Map();
|
|
184
|
-
|
|
185
|
-
for (const row of rows) {
|
|
186
|
-
for (const successorId of row.planning.precedes) {
|
|
187
|
-
if (!predecessorsById.has(successorId)) {
|
|
188
|
-
predecessorsById.set(successorId, []);
|
|
189
|
-
}
|
|
190
|
-
predecessorsById.get(successorId).push(row.id);
|
|
191
|
-
}
|
|
192
|
-
for (const groupId of row.planning.group_ids) {
|
|
193
|
-
if (!membersByGroup.has(groupId)) {
|
|
194
|
-
membersByGroup.set(groupId, []);
|
|
195
|
-
}
|
|
196
|
-
membersByGroup.get(groupId).push(row.id);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
for (const row of rows) {
|
|
201
|
-
const depsSatisfied = row.dependencies.every((id) => {
|
|
202
|
-
const dependency = nodesById.get(id);
|
|
203
|
-
return dependency ? isTerminal(dependency.status) : false;
|
|
204
|
-
});
|
|
205
|
-
const predecessorsSatisfied = (predecessorsById.get(row.id) ?? []).every((id) => {
|
|
206
|
-
const predecessor = nodesById.get(id);
|
|
207
|
-
return predecessor ? isTerminal(predecessor.status) : false;
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
row.ready =
|
|
211
|
-
row.planning.node_type === "work" &&
|
|
212
|
-
!isTerminal(row.status) &&
|
|
213
|
-
row.mode !== "human_only" &&
|
|
214
|
-
depsSatisfied &&
|
|
215
|
-
predecessorsSatisfied;
|
|
216
|
-
row.rollup = computeRollup(row, membersByGroup, nodesById);
|
|
217
|
-
}
|
|
246
|
+
export function listPlanningRows(snapshot, filters = {}) {
|
|
247
|
+
return snapshot.rows.filter((row) => passesFilters(row, filters));
|
|
248
|
+
}
|
|
218
249
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
250
|
+
export function sortPlanningRows(rows, sortBy = null, reverse = false) {
|
|
251
|
+
const comparatorsBySort = {
|
|
252
|
+
default: [
|
|
253
|
+
(a, b) => Number(b.ready) - Number(a.ready),
|
|
254
|
+
comparePriority,
|
|
255
|
+
...baseOperationalComparators(),
|
|
256
|
+
compareLastUpdated,
|
|
257
|
+
(a, b) => compareNullableStrings(a.title, b.title),
|
|
258
|
+
],
|
|
259
|
+
ready: [
|
|
260
|
+
(a, b) => Number(b.ready) - Number(a.ready),
|
|
261
|
+
...baseOperationalComparators(),
|
|
262
|
+
comparePriority,
|
|
263
|
+
compareLastUpdated,
|
|
264
|
+
],
|
|
265
|
+
priority: [comparePriority, ...baseOperationalComparators(), compareLastUpdated],
|
|
266
|
+
lane: [...baseOperationalComparators(), comparePriority, compareLastUpdated],
|
|
267
|
+
rank: [
|
|
268
|
+
(a, b) => compareNullableNumbers(a.planning.rank, b.planning.rank),
|
|
269
|
+
(a, b) => compareNullableStrings(a.planning.lane, b.planning.lane),
|
|
270
|
+
comparePriority,
|
|
271
|
+
compareLastUpdated,
|
|
272
|
+
(a, b) => compareNullableStrings(a.title, b.title),
|
|
273
|
+
],
|
|
274
|
+
updated: [compareLastUpdated, ...baseOperationalComparators(), comparePriority],
|
|
275
|
+
title: [(a, b) => compareNullableStrings(a.title, b.title), comparePriority, ...baseOperationalComparators()],
|
|
225
276
|
};
|
|
226
|
-
}
|
|
227
277
|
|
|
228
|
-
|
|
229
|
-
const
|
|
230
|
-
|
|
278
|
+
const key = sortBy ?? "default";
|
|
279
|
+
const comparators = comparatorsBySort[key] ?? comparatorsBySort.default;
|
|
280
|
+
const sorted = [...rows].sort((a, b) => applyComparatorChain(a, b, comparators));
|
|
281
|
+
return reverse ? sorted.reverse() : sorted;
|
|
231
282
|
}
|
|
232
283
|
|
|
233
284
|
export function buildGraphData(snapshot, options = {}) {
|
|
@@ -304,52 +355,98 @@ export function buildGraphData(snapshot, options = {}) {
|
|
|
304
355
|
};
|
|
305
356
|
}
|
|
306
357
|
|
|
307
|
-
|
|
308
|
-
const
|
|
358
|
+
function collectScopedIds(groupId, membersByGroup) {
|
|
359
|
+
const seen = new Set();
|
|
360
|
+
const queue = [groupId];
|
|
361
|
+
|
|
362
|
+
while (queue.length > 0) {
|
|
363
|
+
const current = queue.shift();
|
|
364
|
+
if (!current || seen.has(current)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
seen.add(current);
|
|
368
|
+
for (const childId of membersByGroup.get(current) ?? []) {
|
|
369
|
+
queue.push(childId);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return seen;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function buildPlanRow(row) {
|
|
377
|
+
return {
|
|
378
|
+
id: row.id,
|
|
379
|
+
title: row.title,
|
|
380
|
+
status: row.status,
|
|
381
|
+
priority: row.priority,
|
|
382
|
+
node_type: row.planning.node_type,
|
|
383
|
+
lane: row.planning.lane,
|
|
384
|
+
rank: row.planning.rank,
|
|
385
|
+
horizon: row.planning.horizon,
|
|
386
|
+
group_ids: [...row.planning.group_ids],
|
|
387
|
+
active_claim: row.active_claim,
|
|
388
|
+
claim_summary: row.claim_summary,
|
|
389
|
+
owner: row.owner,
|
|
390
|
+
mode: row.mode,
|
|
391
|
+
blocked_by: {
|
|
392
|
+
dependencies: [...row.blocked_by.dependencies],
|
|
393
|
+
predecessors: [...row.blocked_by.predecessors],
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function buildPlanSummary(snapshot, options = {}) {
|
|
309
399
|
let rows = snapshot.rows;
|
|
310
400
|
|
|
311
401
|
if (options.group) {
|
|
312
|
-
|
|
402
|
+
const scopedIds = collectScopedIds(options.group, snapshot.membersByGroup);
|
|
403
|
+
rows = rows.filter((row) => scopedIds.has(row.id));
|
|
313
404
|
}
|
|
314
405
|
if (options.horizon) {
|
|
315
406
|
rows = rows.filter((row) => row.planning.horizon === options.horizon);
|
|
316
407
|
}
|
|
317
408
|
|
|
318
|
-
const
|
|
319
|
-
const ready =
|
|
320
|
-
.filter((row) => row.ready)
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
409
|
+
const workRows = rows.filter((row) => row.planning.node_type === "work" && !isTerminalStatus(row.status));
|
|
410
|
+
const ready = sortPlanningRows(
|
|
411
|
+
workRows.filter((row) => row.ready && row.status === "todo"),
|
|
412
|
+
"lane",
|
|
413
|
+
false,
|
|
414
|
+
).map(buildPlanRow);
|
|
415
|
+
const active = sortPlanningRows(
|
|
416
|
+
workRows.filter((row) => row.status === "doing"),
|
|
417
|
+
"lane",
|
|
418
|
+
false,
|
|
419
|
+
).map(buildPlanRow);
|
|
420
|
+
const blocked = sortPlanningRows(
|
|
421
|
+
workRows.filter(
|
|
422
|
+
(row) =>
|
|
423
|
+
!row.ready &&
|
|
424
|
+
(row.status === "blocked" || row.blocked_by.dependencies.length > 0 || row.blocked_by.predecessors.length > 0),
|
|
425
|
+
),
|
|
426
|
+
"lane",
|
|
427
|
+
false,
|
|
428
|
+
).map(buildPlanRow);
|
|
429
|
+
const groups = sortPlanningRows(
|
|
430
|
+
rows.filter((row) => GROUP_NODE_TYPES.has(row.planning.node_type)),
|
|
431
|
+
"lane",
|
|
432
|
+
false,
|
|
433
|
+
).map((row) => ({
|
|
434
|
+
id: row.id,
|
|
435
|
+
title: row.title,
|
|
436
|
+
node_type: row.planning.node_type,
|
|
437
|
+
lane: row.planning.lane,
|
|
438
|
+
rank: row.planning.rank,
|
|
439
|
+
horizon: row.planning.horizon,
|
|
440
|
+
rollup: row.rollup,
|
|
441
|
+
}));
|
|
333
442
|
|
|
334
443
|
return {
|
|
335
444
|
generated_at: new Date().toISOString(),
|
|
336
445
|
workflow_mode: snapshot.profile.workflow?.mode ?? "auto",
|
|
337
446
|
semantics: snapshot.profile.semantics?.terms ?? {},
|
|
338
|
-
ready
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
rank: row.planning.rank,
|
|
343
|
-
horizon: row.planning.horizon,
|
|
344
|
-
active_claim: row.active_claim,
|
|
345
|
-
})),
|
|
346
|
-
groups: groups.map((row) => ({
|
|
347
|
-
id: row.id,
|
|
348
|
-
title: row.title,
|
|
349
|
-
node_type: row.planning.node_type,
|
|
350
|
-
lane: row.planning.lane,
|
|
351
|
-
horizon: row.planning.horizon,
|
|
352
|
-
rollup: row.rollup,
|
|
353
|
-
})),
|
|
447
|
+
ready,
|
|
448
|
+
active,
|
|
449
|
+
blocked,
|
|
450
|
+
groups,
|
|
354
451
|
};
|
|
355
452
|
}
|
package/src/lib/projections.js
CHANGED
|
@@ -30,8 +30,14 @@ export function renderRepoSkill(profile = loadDefaultProfile()) {
|
|
|
30
30
|
"",
|
|
31
31
|
"## Required behavior",
|
|
32
32
|
"- Read `TICKETS.md` for the full repo contract when context is missing.",
|
|
33
|
+
"- Consult `.tickets/config.yml` for repo-local defaults and semantic overrides before interpreting planning terminology or creating new tickets.",
|
|
33
34
|
"- Validate assigned tickets before implementation with `npx @picoai/tickets validate`.",
|
|
35
|
+
"- Before setting a ticket to `done`, confirm the ticket's `## Acceptance Criteria` are met and its `## Verification` checks passed.",
|
|
36
|
+
"- If those completion gates are not satisfied, stop and ask a human whether to keep working or explicitly override the gates. Only move the ticket to `done` after that human decision.",
|
|
37
|
+
"- Record `completion` metadata every time a ticket is moved to `done`.",
|
|
38
|
+
"- When a human overrides incomplete completion gates, record the exception through `npx @picoai/tickets status --status done --acceptance-criteria ... --verification-state ... --override-by ... --override-reason ...` so `ticket.md` and the status log both reflect it.",
|
|
34
39
|
"- Use `npx @picoai/tickets status`, `log`, `claim`, `plan`, and `graph` instead of editing derived state manually.",
|
|
40
|
+
"- When humans use terms like feature, phase, milestone, roadmap, or repo-specific equivalents, translate them through `.tickets/config.yml` and then call the generic CLI fields.",
|
|
35
41
|
"- Respect repo overrides in `.tickets/config.yml` and any narrative guidance in `TICKETS.override.md` if present.",
|
|
36
42
|
"",
|
|
37
43
|
"## Core planning model",
|
|
@@ -44,12 +50,19 @@ export function renderRepoSkill(profile = loadDefaultProfile()) {
|
|
|
44
50
|
"## Default semantic mapping",
|
|
45
51
|
renderSemanticTerms(config),
|
|
46
52
|
"",
|
|
53
|
+
"Repo-specific semantic overrides live in `.tickets/config.yml`. Treat the list above as defaults only.",
|
|
54
|
+
"",
|
|
47
55
|
"## Claims",
|
|
48
56
|
"- Claims are optional advisory leases stored in ticket logs.",
|
|
49
57
|
"- Acquire or renew with `npx @picoai/tickets claim --ticket <id>`.",
|
|
50
58
|
"- Release with `npx @picoai/tickets claim --ticket <id> --release`.",
|
|
51
59
|
`- Default claim TTL is ${config.defaults?.claims?.ttl_minutes ?? 60} minutes unless the repo config overrides it.`,
|
|
52
60
|
"",
|
|
61
|
+
"## Planning views",
|
|
62
|
+
"- Use `npx @picoai/tickets list` for broad queue/reporting views.",
|
|
63
|
+
"- Use `npx @picoai/tickets plan` for operational state: ready, in-progress, blocked, and group rollups.",
|
|
64
|
+
"- Use `npx @picoai/tickets graph` for structural relationships, not execution state.",
|
|
65
|
+
"",
|
|
53
66
|
].join("\n");
|
|
54
67
|
}
|
|
55
68
|
|