schub 0.1.10 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.js +342 -171
- package/package.json +2 -2
- package/skills/create-ticket/SKILL.md +8 -8
- package/skills/create-tickets-for-proposal/SKILL.md +11 -14
- package/skills/implement-task/SKILL.md +23 -29
- package/skills/review-proposal/SKILL.md +11 -9
- package/skills/save-proposal/SKILL.md +0 -22
package/README.md
CHANGED
|
@@ -64,9 +64,9 @@ schub proposals pull --proposal-id P0003
|
|
|
64
64
|
- `schub tickets create` - Create a ticket from the ticket template.
|
|
65
65
|
- `schub tickets list` - List all tickets in the current project context.
|
|
66
66
|
- `schub tickets check` - Toggle a checklist item in a ticket.
|
|
67
|
-
- `schub tickets update` - Move backlog tickets to
|
|
67
|
+
- `schub tickets update` - Move backlog tickets to any ticket status configured for the project in DB.
|
|
68
68
|
- `schub tickets implement` - Move a ticket to `wip` and launch Opencode.
|
|
69
|
-
- `schub
|
|
69
|
+
- `schub tickets save` - Validate and save a ticket to DB/storage.
|
|
70
70
|
|
|
71
71
|
##### Ticket implementation
|
|
72
72
|
|
|
@@ -93,7 +93,7 @@ schub tickets update --id T0004 --id T0005 --status reviewed
|
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
- `--id` (repeatable) selects backlog ticket IDs to move.
|
|
96
|
-
- `--status` sets the target status
|
|
96
|
+
- `--status` sets the target status and must match a project ticket status in DB.
|
|
97
97
|
|
|
98
98
|
##### Overview view shortcut
|
|
99
99
|
|
package/dist/index.js
CHANGED
|
@@ -31367,7 +31367,7 @@ var require_core = __commonJS((exports, module) => {
|
|
|
31367
31367
|
return match && match.index === 0;
|
|
31368
31368
|
}
|
|
31369
31369
|
var BACKREF_RE = /\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./;
|
|
31370
|
-
function
|
|
31370
|
+
function join21(regexps, separator = "|") {
|
|
31371
31371
|
let numCaptures = 0;
|
|
31372
31372
|
return regexps.map((regex2) => {
|
|
31373
31373
|
numCaptures += 1;
|
|
@@ -31650,7 +31650,7 @@ var require_core = __commonJS((exports, module) => {
|
|
|
31650
31650
|
this.exec = () => null;
|
|
31651
31651
|
}
|
|
31652
31652
|
const terminators = this.regexes.map((el) => el[1]);
|
|
31653
|
-
this.matcherRe = langRe(
|
|
31653
|
+
this.matcherRe = langRe(join21(terminators), true);
|
|
31654
31654
|
this.lastIndex = 0;
|
|
31655
31655
|
}
|
|
31656
31656
|
exec(s) {
|
|
@@ -84477,7 +84477,7 @@ var parseProposalsListOptions = (args) => {
|
|
|
84477
84477
|
`);
|
|
84478
84478
|
return;
|
|
84479
84479
|
}
|
|
84480
|
-
const lines = proposals.map((proposal) => `${proposal.id} ${proposal.
|
|
84480
|
+
const lines = proposals.map((proposal) => `${proposal.id} ${proposal.input}`);
|
|
84481
84481
|
process.stdout.write(`${lines.join(`
|
|
84482
84482
|
`)}
|
|
84483
84483
|
`);
|
|
@@ -84486,8 +84486,7 @@ var parseProposalsListOptions = (args) => {
|
|
|
84486
84486
|
const services = await createCliServices(options2.dbPath);
|
|
84487
84487
|
const proposal = services.proposals.create(options2.projectId, {
|
|
84488
84488
|
agent: "opencode",
|
|
84489
|
-
|
|
84490
|
-
description: options2.description
|
|
84489
|
+
input: options2.title
|
|
84491
84490
|
});
|
|
84492
84491
|
process.stdout.write(`[OK] Created proposal ${proposal.id}
|
|
84493
84492
|
`);
|
|
@@ -90778,12 +90777,12 @@ var runApi = (startDir, options = {}) => {
|
|
|
90778
90777
|
};
|
|
90779
90778
|
|
|
90780
90779
|
// src/commands/changes.ts
|
|
90781
|
-
import { existsSync as existsSync7, mkdirSync as
|
|
90780
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5, readdirSync as readdirSync7, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
90782
90781
|
import { dirname as dirname7, join as join12, relative as relative5 } from "node:path";
|
|
90783
90782
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
90784
90783
|
|
|
90785
90784
|
// src/features/changes/index.ts
|
|
90786
|
-
import { existsSync as existsSync6, mkdirSync as
|
|
90785
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readdirSync as readdirSync6, readFileSync as readFileSync9, renameSync, rmSync as rmSync2, statSync as statSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
90787
90786
|
import { dirname as dirname6, join as join11, relative as relative4 } from "node:path";
|
|
90788
90787
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
90789
90788
|
|
|
@@ -91039,8 +91038,17 @@ var toggleChecklistItem = (content, itemNumber) => {
|
|
|
91039
91038
|
throw new Error(`Checklist item ${itemNumber} not found.`);
|
|
91040
91039
|
};
|
|
91041
91040
|
// src/features/tickets/backlog/constants.ts
|
|
91042
|
-
var TICKET_STATUSES = [
|
|
91043
|
-
|
|
91041
|
+
var TICKET_STATUSES = [
|
|
91042
|
+
"backlog",
|
|
91043
|
+
"ready",
|
|
91044
|
+
"reviewed",
|
|
91045
|
+
"wip",
|
|
91046
|
+
"blocked",
|
|
91047
|
+
"done",
|
|
91048
|
+
"rejected",
|
|
91049
|
+
"archived"
|
|
91050
|
+
];
|
|
91051
|
+
var DEFAULT_TICKET_STATUSES = ["backlog", "ready", "reviewed", "wip", "blocked", "done", "archived"];
|
|
91044
91052
|
// src/features/tickets/backlog/create.ts
|
|
91045
91053
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
91046
91054
|
|
|
@@ -91159,8 +91167,8 @@ var resolveChangeSource = (rootDir, changeIdInput) => {
|
|
|
91159
91167
|
var BUNDLED_TICKET_TEMPLATE_PATH = fileURLToPath2(new URL("../../../templates/create-tickets/ticket-template.md", import.meta.url));
|
|
91160
91168
|
var lockWaitHandle = new Int32Array(new SharedArrayBuffer(4));
|
|
91161
91169
|
// src/features/tickets/backlog/filesystem.ts
|
|
91162
|
-
import { existsSync as existsSync2,
|
|
91163
|
-
import {
|
|
91170
|
+
import { existsSync as existsSync2, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync6, writeFileSync } from "node:fs";
|
|
91171
|
+
import { dirname as dirname5, join as join6, relative as relative2, resolve as resolve7 } from "node:path";
|
|
91164
91172
|
|
|
91165
91173
|
// src/features/tickets/backlog/sorting.ts
|
|
91166
91174
|
var ticketNumber = (id) => {
|
|
@@ -91198,22 +91206,43 @@ var findSchubRoot = (startDir = process.cwd()) => {
|
|
|
91198
91206
|
current = parent;
|
|
91199
91207
|
}
|
|
91200
91208
|
};
|
|
91201
|
-
var
|
|
91202
|
-
|
|
91203
|
-
|
|
91209
|
+
var isTicketFolder = (dirPath, fileName) => {
|
|
91210
|
+
const match = fileName.match(/^(TK\d+)_(.+)$/);
|
|
91211
|
+
if (!match)
|
|
91212
|
+
return false;
|
|
91213
|
+
const ticketMdPath = join6(dirPath, fileName, "ticket.md");
|
|
91214
|
+
try {
|
|
91215
|
+
return existsSync2(ticketMdPath) && statSync6(ticketMdPath).isFile();
|
|
91216
|
+
} catch {
|
|
91217
|
+
return false;
|
|
91204
91218
|
}
|
|
91205
|
-
|
|
91206
|
-
|
|
91207
|
-
|
|
91219
|
+
};
|
|
91220
|
+
var parseTicketFolderName = (folderName) => {
|
|
91221
|
+
const underscoreIndex = folderName.indexOf("_");
|
|
91222
|
+
if (underscoreIndex <= 0)
|
|
91208
91223
|
return null;
|
|
91209
|
-
|
|
91210
|
-
|
|
91211
|
-
if (!/^(T|TK)\d+$/.test(id)) {
|
|
91224
|
+
const id = folderName.slice(0, underscoreIndex);
|
|
91225
|
+
if (!/^TK\d+$/.test(id))
|
|
91212
91226
|
return null;
|
|
91213
|
-
|
|
91214
|
-
const titleSlug = baseName.slice(underscoreIndex + 1);
|
|
91227
|
+
const titleSlug = folderName.slice(underscoreIndex + 1);
|
|
91215
91228
|
return { id, title: titleSlug.replace(/-/g, " ") };
|
|
91216
91229
|
};
|
|
91230
|
+
var getTicketFilePath = (ticketsRoot, folderName) => {
|
|
91231
|
+
return join6(ticketsRoot, folderName, "ticket.md");
|
|
91232
|
+
};
|
|
91233
|
+
var parseTicketStatus = (value) => {
|
|
91234
|
+
if (typeof value !== "string") {
|
|
91235
|
+
return null;
|
|
91236
|
+
}
|
|
91237
|
+
const normalized = value.trim().toLowerCase();
|
|
91238
|
+
if (normalized === "in_review") {
|
|
91239
|
+
return "reviewed";
|
|
91240
|
+
}
|
|
91241
|
+
if (TICKET_STATUSES.includes(normalized)) {
|
|
91242
|
+
return normalized;
|
|
91243
|
+
}
|
|
91244
|
+
return null;
|
|
91245
|
+
};
|
|
91217
91246
|
var parseChecklistCounts = (content) => {
|
|
91218
91247
|
const lines = content.split(/\r?\n/);
|
|
91219
91248
|
let inSteps = false;
|
|
@@ -91268,10 +91297,11 @@ var parseTicketFile = (filePath, fallback) => {
|
|
|
91268
91297
|
try {
|
|
91269
91298
|
content = readFileSync5(filePath, "utf8");
|
|
91270
91299
|
} catch {
|
|
91271
|
-
return { ...fallback, dependsOn: [] };
|
|
91300
|
+
return { ...fallback, status: "backlog", dependsOn: [] };
|
|
91272
91301
|
}
|
|
91273
91302
|
const title = extractTitle(content, fallback.id);
|
|
91274
91303
|
const { data } = readFrontmatter(content);
|
|
91304
|
+
const status = parseTicketStatus(data.status) ?? "backlog";
|
|
91275
91305
|
const changeIdValue = data.change_id;
|
|
91276
91306
|
const changeId = typeof changeIdValue === "string" ? changeIdValue.trim() : undefined;
|
|
91277
91307
|
const dependsValue = data.depends_on;
|
|
@@ -91287,6 +91317,7 @@ var parseTicketFile = (filePath, fallback) => {
|
|
|
91287
91317
|
return {
|
|
91288
91318
|
id: fallback.id,
|
|
91289
91319
|
title,
|
|
91320
|
+
status,
|
|
91290
91321
|
dependsOn,
|
|
91291
91322
|
changeId,
|
|
91292
91323
|
blockedReason: blockedReason || undefined,
|
|
@@ -91301,33 +91332,29 @@ var loadTicketFiles = (schubDir, statuses = DEFAULT_TICKET_STATUSES) => {
|
|
|
91301
91332
|
const allowed = new Set(statuses);
|
|
91302
91333
|
const repoRoot = dirname5(schubDir);
|
|
91303
91334
|
const tickets = [];
|
|
91304
|
-
|
|
91305
|
-
|
|
91335
|
+
const entries = readdirSync4(ticketsRoot, { withFileTypes: true });
|
|
91336
|
+
for (const entry of entries) {
|
|
91337
|
+
if (!entry.isDirectory()) {
|
|
91306
91338
|
continue;
|
|
91307
91339
|
}
|
|
91308
|
-
|
|
91309
|
-
if (!existsSync2(statusDir) || !isDirectory3(statusDir)) {
|
|
91340
|
+
if (!isTicketFolder(ticketsRoot, entry.name)) {
|
|
91310
91341
|
continue;
|
|
91311
91342
|
}
|
|
91312
|
-
const
|
|
91313
|
-
|
|
91314
|
-
|
|
91315
|
-
|
|
91316
|
-
|
|
91317
|
-
|
|
91318
|
-
|
|
91319
|
-
continue;
|
|
91320
|
-
}
|
|
91321
|
-
const filePath = join6(statusDir, entry.name);
|
|
91322
|
-
const parsedFile = parseTicketFile(filePath, parsed);
|
|
91323
|
-
tickets.push({
|
|
91324
|
-
id: parsed.id,
|
|
91325
|
-
title: parsed.title,
|
|
91326
|
-
status,
|
|
91327
|
-
path: relative2(repoRoot, filePath),
|
|
91328
|
-
parsedFile
|
|
91329
|
-
});
|
|
91343
|
+
const parsed = parseTicketFolderName(entry.name);
|
|
91344
|
+
if (!parsed)
|
|
91345
|
+
continue;
|
|
91346
|
+
const filePath = getTicketFilePath(ticketsRoot, entry.name);
|
|
91347
|
+
const parsedFile = parseTicketFile(filePath, parsed);
|
|
91348
|
+
if (!allowed.has(parsedFile.status)) {
|
|
91349
|
+
continue;
|
|
91330
91350
|
}
|
|
91351
|
+
tickets.push({
|
|
91352
|
+
id: parsed.id,
|
|
91353
|
+
title: parsed.title,
|
|
91354
|
+
status: parsedFile.status,
|
|
91355
|
+
path: relative2(repoRoot, filePath),
|
|
91356
|
+
parsedFile
|
|
91357
|
+
});
|
|
91331
91358
|
}
|
|
91332
91359
|
return tickets;
|
|
91333
91360
|
};
|
|
@@ -91398,21 +91425,43 @@ var clearTicketsForChange = (schubDir, changeId) => {
|
|
|
91398
91425
|
}
|
|
91399
91426
|
return cleared;
|
|
91400
91427
|
};
|
|
91401
|
-
var
|
|
91402
|
-
const
|
|
91403
|
-
const
|
|
91404
|
-
|
|
91428
|
+
var updateTicketFileStatus = (ticketPath, status, blockedReason) => {
|
|
91429
|
+
const frontmatterStatus = status === "reviewed" ? "in_review" : status;
|
|
91430
|
+
const content = readFileSync5(ticketPath, "utf8");
|
|
91431
|
+
let updated = content;
|
|
91432
|
+
try {
|
|
91433
|
+
updated = updateFrontmatterValue(content, "status", frontmatterStatus);
|
|
91434
|
+
} catch {
|
|
91435
|
+
updated = ensureFrontmatterValue(content, "status", frontmatterStatus);
|
|
91436
|
+
}
|
|
91437
|
+
if (blockedReason !== undefined) {
|
|
91438
|
+
try {
|
|
91439
|
+
updated = updateFrontmatterValue(updated, "blocked_reason", blockedReason);
|
|
91440
|
+
} catch {
|
|
91441
|
+
updated = ensureFrontmatterValue(updated, "blocked_reason", blockedReason);
|
|
91442
|
+
}
|
|
91443
|
+
} else if (status !== "blocked") {
|
|
91444
|
+
try {
|
|
91445
|
+
updated = updateFrontmatterValue(updated, "blocked_reason", "");
|
|
91446
|
+
} catch {}
|
|
91447
|
+
}
|
|
91448
|
+
if (updated !== content) {
|
|
91449
|
+
writeFileSync(ticketPath, updated, "utf8");
|
|
91450
|
+
}
|
|
91451
|
+
};
|
|
91452
|
+
var updateTicketStatuses = (schubDir, ticketIds, status, blockedReason) => {
|
|
91405
91453
|
const repoRoot = dirname5(schubDir);
|
|
91406
|
-
const sourceTickets = listTickets(schubDir,
|
|
91454
|
+
const sourceTickets = listTickets(schubDir, TICKET_STATUSES);
|
|
91407
91455
|
const ticketsById = new Map(sourceTickets.map((ticket) => [ticket.id, ticket]));
|
|
91408
91456
|
return ticketIds.map((ticketId) => ticketsById.get(ticketId)).filter((ticket) => Boolean(ticket)).map((ticket) => {
|
|
91409
91457
|
const currentPath = join6(repoRoot, ticket.path);
|
|
91410
|
-
|
|
91411
|
-
|
|
91458
|
+
updateTicketFileStatus(currentPath, status, blockedReason);
|
|
91459
|
+
const updatedReason = blockedReason !== undefined ? blockedReason : status === "blocked" ? ticket.blockedReason : undefined;
|
|
91412
91460
|
return {
|
|
91413
91461
|
...ticket,
|
|
91414
91462
|
status,
|
|
91415
|
-
path:
|
|
91463
|
+
path: ticket.path,
|
|
91464
|
+
blockedReason: updatedReason
|
|
91416
91465
|
};
|
|
91417
91466
|
});
|
|
91418
91467
|
};
|
|
@@ -91422,54 +91471,28 @@ var assignTicketToWip = (schubDir, ticketId) => {
|
|
|
91422
91471
|
if (!normalizedId) {
|
|
91423
91472
|
throw new Error("Provide --id.");
|
|
91424
91473
|
}
|
|
91425
|
-
const ticketsRoot = join6(schubDir, "tickets");
|
|
91426
|
-
const targetRoot = join6(ticketsRoot, "wip");
|
|
91427
|
-
mkdirSync(targetRoot, { recursive: true });
|
|
91428
91474
|
const repoRoot = dirname5(schubDir);
|
|
91429
|
-
|
|
91430
|
-
|
|
91431
|
-
|
|
91432
|
-
continue;
|
|
91433
|
-
}
|
|
91434
|
-
const entries = readdirSync4(statusDir, { withFileTypes: true });
|
|
91435
|
-
for (const entry of entries) {
|
|
91436
|
-
if (!entry.isFile()) {
|
|
91437
|
-
continue;
|
|
91438
|
-
}
|
|
91439
|
-
const parsed = parseTicketFilename(entry.name);
|
|
91440
|
-
if (!parsed || parsed.id !== normalizedId) {
|
|
91441
|
-
continue;
|
|
91442
|
-
}
|
|
91443
|
-
const currentPath = join6(statusDir, entry.name);
|
|
91444
|
-
const nextPath = join6(targetRoot, entry.name);
|
|
91445
|
-
if (currentPath !== nextPath) {
|
|
91446
|
-
renameSync(currentPath, nextPath);
|
|
91447
|
-
}
|
|
91448
|
-
return {
|
|
91449
|
-
id: parsed.id,
|
|
91450
|
-
title: parsed.title,
|
|
91451
|
-
status: "wip",
|
|
91452
|
-
path: relative2(repoRoot, nextPath)
|
|
91453
|
-
};
|
|
91454
|
-
}
|
|
91475
|
+
const ticket = listTickets(schubDir, ACTIVE_TICKET_STATUSES).find((active) => active.id === normalizedId);
|
|
91476
|
+
if (!ticket) {
|
|
91477
|
+
throw new Error(`Ticket ${normalizedId} not found.`);
|
|
91455
91478
|
}
|
|
91456
|
-
|
|
91479
|
+
updateTicketFileStatus(join6(repoRoot, ticket.path), "wip");
|
|
91480
|
+
return {
|
|
91481
|
+
...ticket,
|
|
91482
|
+
status: "wip"
|
|
91483
|
+
};
|
|
91457
91484
|
};
|
|
91458
91485
|
var archiveTicketsForChange = (schubDir, changeId) => {
|
|
91459
91486
|
const normalizedChangeId = changeId.trim();
|
|
91460
|
-
const ticketsRoot = join6(schubDir, "tickets");
|
|
91461
|
-
const archiveRoot = join6(ticketsRoot, "archived");
|
|
91462
|
-
mkdirSync(archiveRoot, { recursive: true });
|
|
91463
91487
|
const repoRoot = dirname5(schubDir);
|
|
91464
91488
|
const tickets = loadTicketDependencies(schubDir, ACTIVE_TICKET_STATUSES).filter((ticket) => ticket.changeId === normalizedChangeId);
|
|
91465
91489
|
return tickets.map((ticket) => {
|
|
91466
91490
|
const currentPath = join6(repoRoot, ticket.path);
|
|
91467
|
-
|
|
91468
|
-
renameSync(currentPath, archivePath);
|
|
91491
|
+
updateTicketFileStatus(currentPath, "archived");
|
|
91469
91492
|
return {
|
|
91470
91493
|
...ticket,
|
|
91471
91494
|
status: "archived",
|
|
91472
|
-
path:
|
|
91495
|
+
path: ticket.path
|
|
91473
91496
|
};
|
|
91474
91497
|
});
|
|
91475
91498
|
};
|
|
@@ -91508,7 +91531,7 @@ var hasUnimplementedTemplateTokens = (content) => {
|
|
|
91508
91531
|
};
|
|
91509
91532
|
// src/features/tickets/backlog/worktree.ts
|
|
91510
91533
|
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
91511
|
-
import { existsSync as existsSync4, mkdirSync as
|
|
91534
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "node:fs";
|
|
91512
91535
|
import { join as join8, resolve as resolve9 } from "node:path";
|
|
91513
91536
|
|
|
91514
91537
|
// src/helpers/git.ts
|
|
@@ -91528,7 +91551,7 @@ var resolveGitRoot = (startDir) => {
|
|
|
91528
91551
|
};
|
|
91529
91552
|
|
|
91530
91553
|
// src/features/config/project-config.ts
|
|
91531
|
-
import { existsSync as existsSync3, mkdirSync
|
|
91554
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
|
|
91532
91555
|
import { join as join7 } from "node:path";
|
|
91533
91556
|
var readProjectConfig = (rootDir) => {
|
|
91534
91557
|
const configPath = join7(rootDir, ".schub", "config.json");
|
|
@@ -91546,7 +91569,7 @@ var writeProjectConfig = (rootDir, projectId) => {
|
|
|
91546
91569
|
throw new Error("Provide project_id.");
|
|
91547
91570
|
}
|
|
91548
91571
|
const configDir = join7(rootDir, ".schub");
|
|
91549
|
-
|
|
91572
|
+
mkdirSync(configDir, { recursive: true });
|
|
91550
91573
|
const configPath = join7(configDir, "config.json");
|
|
91551
91574
|
const payload = { project_id: projectId };
|
|
91552
91575
|
writeFileSync2(configPath, `${JSON.stringify(payload, null, 2)}
|
|
@@ -91589,7 +91612,7 @@ var createTicketWorktree = ({ repoRoot, ticketId, projectId, worktreeRoot }) =>
|
|
|
91589
91612
|
if (branchCheck.status === 0) {
|
|
91590
91613
|
throw new Error(`Branch ${branchName} already exists.`);
|
|
91591
91614
|
}
|
|
91592
|
-
|
|
91615
|
+
mkdirSync2(resolvedRoot, { recursive: true });
|
|
91593
91616
|
const worktreeResult = runGit(gitRoot, ["worktree", "add", "-b", branchName, worktreePath]);
|
|
91594
91617
|
if (worktreeResult.status !== 0) {
|
|
91595
91618
|
const message = worktreeResult.stderr?.trim();
|
|
@@ -91599,7 +91622,7 @@ var createTicketWorktree = ({ repoRoot, ticketId, projectId, worktreeRoot }) =>
|
|
|
91599
91622
|
return { worktreePath, branchName, gitRoot };
|
|
91600
91623
|
};
|
|
91601
91624
|
// src/features/tickets/create.ts
|
|
91602
|
-
import { existsSync as existsSync5, mkdirSync as
|
|
91625
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync as readdirSync5, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
91603
91626
|
import { join as join9 } from "node:path";
|
|
91604
91627
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
91605
91628
|
var BUNDLED_TICKET_TEMPLATE_PATH2 = fileURLToPath3(new URL("../../../templates/create-ticket/ticket-template.md", import.meta.url));
|
|
@@ -91618,7 +91641,7 @@ var waitForCreateTicketsLock = () => {
|
|
|
91618
91641
|
};
|
|
91619
91642
|
var acquireCreateTicketsLock = (schubDir) => {
|
|
91620
91643
|
const ticketsRoot = join9(schubDir, "tickets");
|
|
91621
|
-
|
|
91644
|
+
mkdirSync3(ticketsRoot, { recursive: true });
|
|
91622
91645
|
const lockPath = join9(ticketsRoot, ".create-tickets.lock");
|
|
91623
91646
|
while (true) {
|
|
91624
91647
|
try {
|
|
@@ -91637,16 +91660,15 @@ var acquireCreateTicketsLock = (schubDir) => {
|
|
|
91637
91660
|
};
|
|
91638
91661
|
};
|
|
91639
91662
|
var TICKET_PREFIX = "TK";
|
|
91640
|
-
var TICKET_PATTERN = new RegExp(`^${TICKET_PREFIX}(\\d+)(?:_.*)?\\.md$`);
|
|
91641
91663
|
var nextTicketId = (ticketsRoot) => {
|
|
91642
91664
|
const existingNumbers = new Set;
|
|
91643
91665
|
if (existsSync5(ticketsRoot)) {
|
|
91644
91666
|
const entries = readdirSync5(ticketsRoot, { withFileTypes: true });
|
|
91645
91667
|
for (const entry of entries) {
|
|
91646
|
-
if (entry.isDirectory()) {
|
|
91668
|
+
if (!entry.isDirectory()) {
|
|
91647
91669
|
continue;
|
|
91648
91670
|
}
|
|
91649
|
-
const match = entry.name.match(
|
|
91671
|
+
const match = entry.name.match(/^TK(\d+)_/);
|
|
91650
91672
|
if (!match) {
|
|
91651
91673
|
continue;
|
|
91652
91674
|
}
|
|
@@ -91673,17 +91695,19 @@ var createTicket2 = (schubDir, templateRoot, options) => {
|
|
|
91673
91695
|
const ticketsRoot = join9(schubDir, "tickets");
|
|
91674
91696
|
const ticketId = nextTicketId(ticketsRoot);
|
|
91675
91697
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
91676
|
-
const
|
|
91698
|
+
const ticketFolderName = `${ticketId}_${slug}`;
|
|
91699
|
+
const ticketFolderPath = join9(ticketsRoot, ticketFolderName);
|
|
91700
|
+
const ticketFilePath = join9(ticketFolderPath, "ticket.md");
|
|
91677
91701
|
const template = readTicketTemplate(templateRoot);
|
|
91678
91702
|
const today = new Date().toISOString().split("T")[0];
|
|
91679
91703
|
const complexity = (options.complexity || "[low|medium|high]").trim();
|
|
91680
91704
|
const rendered = template.replaceAll("{{TICKET_ID}}", ticketId).replace("{{TICKET_TITLE}}", title).replace("{{DATE}}", today).replace("{{STATUS}}", status).replace("[low|medium|high]", complexity).replace("{{INPUT}}", input).replace("{{CHANGE_ID}}", changeId);
|
|
91681
|
-
if (existsSync5(
|
|
91682
|
-
throw new Error(`Refusing to overwrite existing file: ${
|
|
91705
|
+
if (existsSync5(ticketFilePath) && !options.overwrite) {
|
|
91706
|
+
throw new Error(`Refusing to overwrite existing file: ${ticketFilePath}`);
|
|
91683
91707
|
}
|
|
91684
|
-
|
|
91685
|
-
writeFileSync3(
|
|
91686
|
-
return
|
|
91708
|
+
mkdirSync3(ticketFolderPath, { recursive: true });
|
|
91709
|
+
writeFileSync3(ticketFilePath, rendered, "utf8");
|
|
91710
|
+
return ticketFilePath;
|
|
91687
91711
|
} finally {
|
|
91688
91712
|
releaseLock();
|
|
91689
91713
|
}
|
|
@@ -91887,9 +91911,9 @@ Add a 'status' field in frontmatter before updating status.`);
|
|
|
91887
91911
|
if (existsSync6(archivePath)) {
|
|
91888
91912
|
throw new Error(`Archive already exists: ${archivePath}`);
|
|
91889
91913
|
}
|
|
91890
|
-
|
|
91914
|
+
mkdirSync4(archiveRoot, { recursive: true });
|
|
91891
91915
|
writeFileSync5(summary.proposalPath, updated, "utf8");
|
|
91892
|
-
|
|
91916
|
+
renameSync(summary.changeDir, archivePath);
|
|
91893
91917
|
return {
|
|
91894
91918
|
changeId: summary.changeId,
|
|
91895
91919
|
proposalPath: join11(archivePath, "proposal.md"),
|
|
@@ -91901,9 +91925,9 @@ Add a 'status' field in frontmatter before updating status.`);
|
|
|
91901
91925
|
if (existsSync6(activePath)) {
|
|
91902
91926
|
throw new Error(`Change already exists: ${activePath}`);
|
|
91903
91927
|
}
|
|
91904
|
-
|
|
91928
|
+
mkdirSync4(join11(schubDir, "proposals"), { recursive: true });
|
|
91905
91929
|
writeFileSync5(summary.proposalPath, updated, "utf8");
|
|
91906
|
-
|
|
91930
|
+
renameSync(summary.changeDir, activePath);
|
|
91907
91931
|
return {
|
|
91908
91932
|
changeId: summary.changeId,
|
|
91909
91933
|
proposalPath: join11(activePath, "proposal.md"),
|
|
@@ -91949,7 +91973,7 @@ var saveChange = (changeRoot, schubRoot, changeId) => {
|
|
|
91949
91973
|
const summary = readChangeSummary(changeRoot, changeId);
|
|
91950
91974
|
const sourceDir = summary.changeDir;
|
|
91951
91975
|
const targetDir = join11(schubRoot, "proposals", summary.changeId);
|
|
91952
|
-
|
|
91976
|
+
mkdirSync4(targetDir, { recursive: true });
|
|
91953
91977
|
const reviewPath = join11(sourceDir, "REVIEW_ME.md");
|
|
91954
91978
|
const targetReviewPath = join11(targetDir, "REVIEW_ME.md");
|
|
91955
91979
|
const hasReviewFile = existsSync6(reviewPath) || existsSync6(targetReviewPath);
|
|
@@ -91994,7 +92018,7 @@ var loadChange = (changeRoot, schubRoot, changeId) => {
|
|
|
91994
92018
|
const archiveDir = join11(schubRoot, "archive", "proposals", source.changeId);
|
|
91995
92019
|
const targetDir = shouldArchive ? archiveDir : activeDir;
|
|
91996
92020
|
const staleDir = shouldArchive ? activeDir : archiveDir;
|
|
91997
|
-
|
|
92021
|
+
mkdirSync4(targetDir, { recursive: true });
|
|
91998
92022
|
const loaded = [];
|
|
91999
92023
|
const removed = [];
|
|
92000
92024
|
for (const fileName of CHANGE_SAVE_FILES) {
|
|
@@ -92149,7 +92173,7 @@ var createChange = (schubDir, options) => {
|
|
|
92149
92173
|
if (existsSync6(proposalPath) && !options.overwrite) {
|
|
92150
92174
|
throw new Error(`Refusing to overwrite existing file: ${proposalPath}`);
|
|
92151
92175
|
}
|
|
92152
|
-
|
|
92176
|
+
mkdirSync4(changeDir, { recursive: true });
|
|
92153
92177
|
writeFileSync5(proposalPath, normalized, "utf8");
|
|
92154
92178
|
return proposalPath;
|
|
92155
92179
|
};
|
|
@@ -92853,7 +92877,7 @@ var pullChangeFromServices = async (schubRoot, changeIdInput, services, projectI
|
|
|
92853
92877
|
for (const directory of existingDirs) {
|
|
92854
92878
|
rmSync3(directory, { recursive: true, force: true });
|
|
92855
92879
|
}
|
|
92856
|
-
|
|
92880
|
+
mkdirSync5(targetDir, { recursive: true });
|
|
92857
92881
|
const writtenFiles = new Set;
|
|
92858
92882
|
writeFileSync6(join12(targetDir, "proposal.md"), proposalContent, "utf8");
|
|
92859
92883
|
writtenFiles.add("proposal.md");
|
|
@@ -92862,7 +92886,7 @@ var pullChangeFromServices = async (schubRoot, changeIdInput, services, projectI
|
|
|
92862
92886
|
continue;
|
|
92863
92887
|
}
|
|
92864
92888
|
const outputPath = join12(targetDir, file.file_name);
|
|
92865
|
-
|
|
92889
|
+
mkdirSync5(dirname7(outputPath), { recursive: true });
|
|
92866
92890
|
const data2 = readFileSync10(file.storage_path);
|
|
92867
92891
|
writeFileSync6(outputPath, data2);
|
|
92868
92892
|
writtenFiles.add(file.file_name);
|
|
@@ -93170,7 +93194,7 @@ var prepareDashboardApiEnv = async (env2, options = {}) => {
|
|
|
93170
93194
|
};
|
|
93171
93195
|
|
|
93172
93196
|
// src/commands/review.ts
|
|
93173
|
-
import { existsSync as existsSync8, mkdirSync as
|
|
93197
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
|
|
93174
93198
|
import { dirname as dirname9, join as join14, resolve as resolve11 } from "node:path";
|
|
93175
93199
|
import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
93176
93200
|
var BUNDLED_REVIEW_TEMPLATE_PATH = fileURLToPath6(new URL("../../templates/review-proposal/review-me-template.md", import.meta.url));
|
|
@@ -93248,7 +93272,7 @@ var runReviewComplete = (args, startDir) => {
|
|
|
93248
93272
|
};
|
|
93249
93273
|
|
|
93250
93274
|
// src/commands/template.ts
|
|
93251
|
-
import { existsSync as existsSync9, mkdirSync as
|
|
93275
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
|
|
93252
93276
|
import { dirname as dirname10, join as join15 } from "node:path";
|
|
93253
93277
|
import { fileURLToPath as fileURLToPath7 } from "node:url";
|
|
93254
93278
|
var BUNDLED_ADR_TEMPLATE_PATH = fileURLToPath7(new URL("../../templates/create-proposal/adr-template.md", import.meta.url));
|
|
@@ -93380,14 +93404,14 @@ var runTemplateCreate = (args, startDir) => {
|
|
|
93380
93404
|
const templatePath = resolveTemplatePath(schubDir, definition.templatePath, definition.bundledPath);
|
|
93381
93405
|
const template = readFileSync13(templatePath, "utf8");
|
|
93382
93406
|
const rendered = definition.render(template, summary, options);
|
|
93383
|
-
|
|
93407
|
+
mkdirSync7(dirname10(outputPath), { recursive: true });
|
|
93384
93408
|
writeFileSync8(outputPath, rendered, "utf8");
|
|
93385
93409
|
process.stdout.write(`[OK] Wrote template ${options.name}: ${outputPath}
|
|
93386
93410
|
`);
|
|
93387
93411
|
};
|
|
93388
93412
|
|
|
93389
93413
|
// src/commands/tickets.ts
|
|
93390
|
-
import { existsSync as existsSync10, mkdirSync as
|
|
93414
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync8, writeFileSync as writeFileSync9 } from "node:fs";
|
|
93391
93415
|
import { dirname as dirname11, join as join16 } from "node:path";
|
|
93392
93416
|
var parseTicketsCreateOptions = (args) => {
|
|
93393
93417
|
let title;
|
|
@@ -93567,10 +93591,6 @@ var runTicketsSave = async (options, startDir) => {
|
|
|
93567
93591
|
process.stdout.write(`[OK] Saved ticket: ${saved.path}
|
|
93568
93592
|
`);
|
|
93569
93593
|
};
|
|
93570
|
-
var frontmatterString2 = (data, key) => {
|
|
93571
|
-
const value = data[key];
|
|
93572
|
-
return typeof value === "string" ? value.trim() : undefined;
|
|
93573
|
-
};
|
|
93574
93594
|
var runTicketsPull = async (startDir) => {
|
|
93575
93595
|
const schubDir = resolveSchubRoot(startDir);
|
|
93576
93596
|
const { dbPath, storagePath } = resolveServicesConfig2(schubDir);
|
|
@@ -93579,24 +93599,40 @@ var runTicketsPull = async (startDir) => {
|
|
|
93579
93599
|
const tickets = services.tickets.list(projectId);
|
|
93580
93600
|
const activeTickets = tickets.filter((ticket) => ticket.archived !== 1);
|
|
93581
93601
|
const ticketsRoot = join16(schubDir, "tickets");
|
|
93582
|
-
|
|
93602
|
+
mkdirSync8(ticketsRoot, { recursive: true });
|
|
93583
93603
|
for (const ticket of activeTickets) {
|
|
93584
93604
|
if (!ticket.content)
|
|
93585
93605
|
continue;
|
|
93586
|
-
const { data } = readFrontmatter(ticket.content);
|
|
93587
|
-
const ticketId = frontmatterString2(data, "ticket_id") || ticket.id;
|
|
93588
93606
|
const titleMatch = ticket.content.match(/^#+\s+(.*)$/m);
|
|
93589
|
-
|
|
93590
|
-
|
|
93591
|
-
|
|
93592
|
-
|
|
93607
|
+
let title = titleMatch?.[1]?.trim();
|
|
93608
|
+
if (!title) {
|
|
93609
|
+
const firstLine = ticket.content.split(`
|
|
93610
|
+
`)[0]?.trim();
|
|
93611
|
+
title = firstLine || "untitled";
|
|
93612
|
+
}
|
|
93613
|
+
if (title.startsWith("Ticket:")) {
|
|
93614
|
+
title = title.replace(/^Ticket:\s*\w+\s*/, "").trim() || "untitled";
|
|
93615
|
+
}
|
|
93616
|
+
const baseSlug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
93617
|
+
let slug = baseSlug.slice(0, 50);
|
|
93618
|
+
if (baseSlug.length > 50 && !slug.endsWith("-")) {
|
|
93619
|
+
const lastHyphen = slug.lastIndexOf("-");
|
|
93620
|
+
if (lastHyphen > 0) {
|
|
93621
|
+
slug = slug.slice(0, lastHyphen);
|
|
93622
|
+
}
|
|
93623
|
+
}
|
|
93624
|
+
const folderName = `${ticket.shorthand}_${slug}`;
|
|
93625
|
+
const folderPath = join16(ticketsRoot, folderName);
|
|
93626
|
+
mkdirSync8(folderPath, { recursive: true });
|
|
93627
|
+
const filePath = join16(folderPath, "ticket.md");
|
|
93593
93628
|
writeFileSync9(filePath, ticket.content, "utf8");
|
|
93594
|
-
process.stdout.write(`[OK] Pulled ticket: ${
|
|
93629
|
+
process.stdout.write(`[OK] Pulled ticket: ${ticket.shorthand} -> ${filePath}
|
|
93595
93630
|
`);
|
|
93596
93631
|
}
|
|
93597
93632
|
};
|
|
93598
|
-
var runTicketSave = runTicketsSave;
|
|
93599
93633
|
|
|
93634
|
+
// src/commands/tickets-backlog.ts
|
|
93635
|
+
import { dirname as dirname14, join as join20 } from "node:path";
|
|
93600
93636
|
// src/commands/tickets-check.ts
|
|
93601
93637
|
import { readFileSync as readFileSync14, writeFileSync as writeFileSync10 } from "node:fs";
|
|
93602
93638
|
import { dirname as dirname12, join as join17 } from "node:path";
|
|
@@ -93691,7 +93727,7 @@ import { dirname as dirname13, join as join19 } from "node:path";
|
|
|
93691
93727
|
|
|
93692
93728
|
// src/executors/opencode-impl.ts
|
|
93693
93729
|
import { spawn as spawn3, spawnSync as spawnSync3 } from "node:child_process";
|
|
93694
|
-
import { basename as
|
|
93730
|
+
import { basename as basename3 } from "node:path";
|
|
93695
93731
|
var opencodeSpawnOptions = { stdio: "ignore", detached: true };
|
|
93696
93732
|
var spawnOpencode = (command2, args, spawner, overrides) => {
|
|
93697
93733
|
const child = spawner(command2, args, { ...opencodeSpawnOptions, ...overrides });
|
|
@@ -93708,7 +93744,7 @@ var runOpencodeCommand = (args) => {
|
|
|
93708
93744
|
var buildSessionTitle = (repoRoot, label, id, detail) => {
|
|
93709
93745
|
const trimmedDetail = detail?.trim();
|
|
93710
93746
|
const suffix = trimmedDetail ? ` ${trimmedDetail}` : "";
|
|
93711
|
-
return `(${
|
|
93747
|
+
return `(${basename3(repoRoot)}) ${label} ${id}${suffix}`;
|
|
93712
93748
|
};
|
|
93713
93749
|
var formatChangeId = (value) => {
|
|
93714
93750
|
const match = value.match(/^([CPcp]\d+)_/);
|
|
@@ -93821,7 +93857,7 @@ globalThis.__REAL_launchOpencodeImplement = launchOpencodeImplement;
|
|
|
93821
93857
|
globalThis.__REAL_launchOpencodeCreateTickets = launchOpencodeCreateTickets;
|
|
93822
93858
|
// src/features/context/prepare-implement-context.ts
|
|
93823
93859
|
import { readdirSync as readdirSync9, readFileSync as readFileSync15 } from "node:fs";
|
|
93824
|
-
import { basename as
|
|
93860
|
+
import { basename as basename4, join as join18 } from "node:path";
|
|
93825
93861
|
var listMarkdownFiles = (root, relativeRoot = "") => {
|
|
93826
93862
|
const entries = readdirSync9(join18(root, relativeRoot), { withFileTypes: true });
|
|
93827
93863
|
const paths = [];
|
|
@@ -93842,7 +93878,7 @@ var readChangeMarkdown = (schubDir, changeId) => {
|
|
|
93842
93878
|
return entries.sort().map((relativePath) => {
|
|
93843
93879
|
const filePath = join18(changeDir, relativePath);
|
|
93844
93880
|
return {
|
|
93845
|
-
filename:
|
|
93881
|
+
filename: basename4(relativePath),
|
|
93846
93882
|
content: readFileSync15(filePath, "utf8")
|
|
93847
93883
|
};
|
|
93848
93884
|
});
|
|
@@ -93991,24 +94027,59 @@ var runTicketsImplement = (schubDir, args) => {
|
|
|
93991
94027
|
};
|
|
93992
94028
|
|
|
93993
94029
|
// src/commands/tickets-backlog.ts
|
|
93994
|
-
var
|
|
94030
|
+
var CLI_IN_REVIEW_STATUS = "in_review";
|
|
94031
|
+
var INTERNAL_REVIEWED_STATUS = "reviewed";
|
|
94032
|
+
var formatCliTicketStatus = (status) => status === INTERNAL_REVIEWED_STATUS ? CLI_IN_REVIEW_STATUS : status;
|
|
94033
|
+
var parseListTicketStatus = (status) => {
|
|
94034
|
+
const normalized = status.trim().toLowerCase();
|
|
94035
|
+
if (normalized === CLI_IN_REVIEW_STATUS) {
|
|
94036
|
+
return INTERNAL_REVIEWED_STATUS;
|
|
94037
|
+
}
|
|
94038
|
+
if (normalized === INTERNAL_REVIEWED_STATUS) {
|
|
94039
|
+
return null;
|
|
94040
|
+
}
|
|
94041
|
+
if (TICKET_STATUSES.includes(normalized)) {
|
|
94042
|
+
return normalized;
|
|
94043
|
+
}
|
|
94044
|
+
return null;
|
|
94045
|
+
};
|
|
94046
|
+
var parseUpdateTicketStatus = (status) => {
|
|
94047
|
+
const normalized = status.trim().toLowerCase();
|
|
94048
|
+
if (normalized === CLI_IN_REVIEW_STATUS) {
|
|
94049
|
+
return INTERNAL_REVIEWED_STATUS;
|
|
94050
|
+
}
|
|
94051
|
+
if (normalized === INTERNAL_REVIEWED_STATUS) {
|
|
94052
|
+
return null;
|
|
94053
|
+
}
|
|
94054
|
+
if (TICKET_STATUSES.includes(normalized)) {
|
|
94055
|
+
return normalized;
|
|
94056
|
+
}
|
|
94057
|
+
return null;
|
|
94058
|
+
};
|
|
94059
|
+
var CLI_TICKET_STATUSES = TICKET_STATUSES.map(formatCliTicketStatus);
|
|
94060
|
+
var parseStatusFilter = (value, skipDefault = false) => {
|
|
93995
94061
|
if (!value) {
|
|
93996
|
-
return [...DEFAULT_TICKET_STATUSES];
|
|
94062
|
+
return skipDefault ? null : [...DEFAULT_TICKET_STATUSES];
|
|
93997
94063
|
}
|
|
93998
94064
|
const normalized = value.split(",").map((status) => status.trim().toLowerCase()).filter(Boolean);
|
|
93999
|
-
const
|
|
94000
|
-
const invalid = normalized.filter((
|
|
94065
|
+
const parsed = normalized.map(parseListTicketStatus);
|
|
94066
|
+
const invalid = normalized.filter((_, index) => parsed[index] === null);
|
|
94001
94067
|
if (invalid.length > 0) {
|
|
94002
94068
|
throw new Error(`Unknown status filter: ${invalid.join(", ")}`);
|
|
94003
94069
|
}
|
|
94004
|
-
return
|
|
94070
|
+
return parsed.filter((status) => status !== null);
|
|
94005
94071
|
};
|
|
94006
94072
|
var parseTicketListOptions = (args) => {
|
|
94007
94073
|
let statusValue;
|
|
94008
94074
|
let json = false;
|
|
94075
|
+
let openFilter;
|
|
94009
94076
|
const unknown = [];
|
|
94010
94077
|
for (let index = 0;index < args.length; index += 1) {
|
|
94011
94078
|
const arg = args[index];
|
|
94079
|
+
if (arg === "open") {
|
|
94080
|
+
openFilter = true;
|
|
94081
|
+
continue;
|
|
94082
|
+
}
|
|
94012
94083
|
if (arg === "--json") {
|
|
94013
94084
|
json = true;
|
|
94014
94085
|
continue;
|
|
@@ -94030,11 +94101,12 @@ var parseTicketListOptions = (args) => {
|
|
|
94030
94101
|
if (unknown.length > 0) {
|
|
94031
94102
|
throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
|
|
94032
94103
|
}
|
|
94033
|
-
|
|
94104
|
+
const statuses = parseStatusFilter(statusValue, openFilter !== undefined);
|
|
94105
|
+
return { statuses: statuses ?? [], json, openFilter };
|
|
94034
94106
|
};
|
|
94035
|
-
var BACKLOG_UPDATE_STATUSES = ["reviewed", "rejected", "archived"];
|
|
94036
94107
|
var parseTicketUpdateOptions = (args) => {
|
|
94037
94108
|
let statusValue;
|
|
94109
|
+
let blockedReason;
|
|
94038
94110
|
const ticketIds = [];
|
|
94039
94111
|
const unknown = [];
|
|
94040
94112
|
const rejectUnsupported = (flag) => {
|
|
@@ -94067,6 +94139,18 @@ var parseTicketUpdateOptions = (args) => {
|
|
|
94067
94139
|
ticketIds.push(arg.slice("--id=".length));
|
|
94068
94140
|
continue;
|
|
94069
94141
|
}
|
|
94142
|
+
if (arg === "--blocked-reason") {
|
|
94143
|
+
blockedReason = args[index + 1];
|
|
94144
|
+
if (!blockedReason) {
|
|
94145
|
+
throw new Error("Missing value for --blocked-reason.");
|
|
94146
|
+
}
|
|
94147
|
+
index += 1;
|
|
94148
|
+
continue;
|
|
94149
|
+
}
|
|
94150
|
+
if (arg.startsWith("--blocked-reason=")) {
|
|
94151
|
+
blockedReason = arg.slice("--blocked-reason=".length);
|
|
94152
|
+
continue;
|
|
94153
|
+
}
|
|
94070
94154
|
if (arg === "--schub-root" || arg === "--agent-root") {
|
|
94071
94155
|
rejectUnsupported(arg);
|
|
94072
94156
|
}
|
|
@@ -94084,9 +94168,9 @@ var parseTicketUpdateOptions = (args) => {
|
|
|
94084
94168
|
if (!statusValue) {
|
|
94085
94169
|
throw new Error("Provide --status.");
|
|
94086
94170
|
}
|
|
94087
|
-
const
|
|
94088
|
-
if (!
|
|
94089
|
-
throw new Error(`Invalid status '${statusValue}'. Use
|
|
94171
|
+
const parsedStatus = parseUpdateTicketStatus(statusValue);
|
|
94172
|
+
if (!parsedStatus) {
|
|
94173
|
+
throw new Error(`Invalid status '${statusValue}'. Use ${CLI_TICKET_STATUSES.join(", ")}.`);
|
|
94090
94174
|
}
|
|
94091
94175
|
const normalizedIds = ticketIds.map((id) => id.trim()).filter(Boolean);
|
|
94092
94176
|
if (normalizedIds.length === 0) {
|
|
@@ -94094,37 +94178,123 @@ var parseTicketUpdateOptions = (args) => {
|
|
|
94094
94178
|
}
|
|
94095
94179
|
const options = {
|
|
94096
94180
|
ticketIds: normalizedIds,
|
|
94097
|
-
status:
|
|
94181
|
+
status: parsedStatus,
|
|
94182
|
+
blockedReason
|
|
94098
94183
|
};
|
|
94099
94184
|
return options;
|
|
94100
94185
|
};
|
|
94101
|
-
var
|
|
94186
|
+
var resolveServicesConfig3 = (schubDir) => ({
|
|
94187
|
+
dbPath: process.env.SCHUB_DB_PATH ?? join20(schubDir, "schub.db"),
|
|
94188
|
+
storagePath: process.env.SCHUB_STORAGE_PATH ?? join20(schubDir, "storage")
|
|
94189
|
+
});
|
|
94190
|
+
var resolveProjectId2 = (schubDir, services) => {
|
|
94191
|
+
const repoRoot = dirname14(schubDir);
|
|
94192
|
+
const config = readProjectConfig(repoRoot);
|
|
94193
|
+
if (!config) {
|
|
94194
|
+
throw new Error(`Project config not found: ${join20(repoRoot, ".schub", "config.json")}`);
|
|
94195
|
+
}
|
|
94196
|
+
const project = services.projects.get(config.project_id);
|
|
94197
|
+
if (!project) {
|
|
94198
|
+
throw new Error(`Project '${config.project_id}' from .schub/config.json was not found in the database.`);
|
|
94199
|
+
}
|
|
94200
|
+
return config.project_id;
|
|
94201
|
+
};
|
|
94202
|
+
var listAllowedTicketUpdateStatuses = async (schubDir) => {
|
|
94203
|
+
const { dbPath, storagePath } = resolveServicesConfig3(schubDir);
|
|
94204
|
+
const services = await createCliServices(dbPath, storagePath);
|
|
94205
|
+
const projectId = resolveProjectId2(schubDir, services);
|
|
94206
|
+
const statuses = services.ticketStatuses.list(projectId).map((status) => status.name.trim().toLowerCase()).filter(Boolean);
|
|
94207
|
+
return [...new Set(statuses)];
|
|
94208
|
+
};
|
|
94209
|
+
var findStatusId2 = (services, projectId, status) => {
|
|
94210
|
+
const normalized = status.trim().toLowerCase();
|
|
94211
|
+
const match = services.ticketStatuses.list(projectId).find((ticketStatus) => ticketStatus.name.trim().toLowerCase() === normalized);
|
|
94212
|
+
return match?.id;
|
|
94213
|
+
};
|
|
94214
|
+
var runTicketsList = async (schubDir, args) => {
|
|
94102
94215
|
if (!schubDir) {
|
|
94103
94216
|
throw new Error("No .schub directory found.");
|
|
94104
94217
|
}
|
|
94105
94218
|
const options = parseTicketListOptions(args);
|
|
94106
|
-
const
|
|
94219
|
+
const { dbPath, storagePath } = resolveServicesConfig3(schubDir);
|
|
94220
|
+
const services = await createCliServices(dbPath, storagePath);
|
|
94221
|
+
const projectId = resolveProjectId2(schubDir, services);
|
|
94222
|
+
const allTickets = services.tickets.list(projectId);
|
|
94223
|
+
const allStatuses = services.ticketStatuses.list(projectId);
|
|
94224
|
+
const statusMap = new Map(allStatuses.map((s) => [s.id, s.name.toLowerCase()]));
|
|
94225
|
+
const statusOpenMap = new Map(allStatuses.map((s) => [s.id, s.is_open === 1]));
|
|
94226
|
+
const tickets = allTickets.filter((ticket) => {
|
|
94227
|
+
if (options.openFilter !== undefined) {
|
|
94228
|
+
const isOpen = ticket.status_id ? statusOpenMap.get(ticket.status_id) ?? true : true;
|
|
94229
|
+
if (isOpen !== options.openFilter) {
|
|
94230
|
+
return false;
|
|
94231
|
+
}
|
|
94232
|
+
}
|
|
94233
|
+
if (options.statuses.length === 0) {
|
|
94234
|
+
return true;
|
|
94235
|
+
}
|
|
94236
|
+
const status = ticket.status_id ? statusMap.get(ticket.status_id) : "backlog";
|
|
94237
|
+
return options.statuses.includes(status);
|
|
94238
|
+
}).map((ticket) => {
|
|
94239
|
+
const status = ticket.status_id ? statusMap.get(ticket.status_id) ?? "backlog" : "backlog";
|
|
94240
|
+
const titleMatch = ticket.content?.match(/^#+\s+(.*)$/m);
|
|
94241
|
+
let title = titleMatch?.[1]?.trim();
|
|
94242
|
+
if (!title) {
|
|
94243
|
+
const firstLine = ticket.content?.split(`
|
|
94244
|
+
`)[0]?.trim();
|
|
94245
|
+
title = firstLine || "untitled";
|
|
94246
|
+
}
|
|
94247
|
+
if (title.startsWith("Ticket:")) {
|
|
94248
|
+
title = title.replace(/^Ticket:\s*\w+\s*/, "").trim() || "untitled";
|
|
94249
|
+
}
|
|
94250
|
+
return {
|
|
94251
|
+
id: ticket.shorthand,
|
|
94252
|
+
title,
|
|
94253
|
+
status: formatCliTicketStatus(status)
|
|
94254
|
+
};
|
|
94255
|
+
}).sort((a, b) => a.id.localeCompare(b.id));
|
|
94107
94256
|
if (options.json) {
|
|
94108
94257
|
process.stdout.write(`${JSON.stringify(tickets, null, 2)}
|
|
94109
94258
|
`);
|
|
94110
94259
|
return;
|
|
94111
94260
|
}
|
|
94112
|
-
const lines = tickets.map((ticket) => {
|
|
94113
|
-
const checklistSuffix = typeof ticket.checklistRemaining === "number" && typeof ticket.checklistTotal === "number" ? ` (${ticket.checklistRemaining}/${ticket.checklistTotal})` : "";
|
|
94114
|
-
return `${ticket.id} ${ticket.title} (${ticket.status})${checklistSuffix}`;
|
|
94115
|
-
});
|
|
94261
|
+
const lines = tickets.map((ticket) => `${ticket.id} ${ticket.title} (${ticket.status})`);
|
|
94116
94262
|
process.stdout.write(`${lines.join(`
|
|
94117
94263
|
`)}
|
|
94118
94264
|
`);
|
|
94119
94265
|
};
|
|
94120
|
-
var runTicketsUpdate = (schubDir, args) => {
|
|
94266
|
+
var runTicketsUpdate = async (schubDir, args) => {
|
|
94121
94267
|
if (!schubDir) {
|
|
94122
94268
|
throw new Error("No .schub directory found.");
|
|
94123
94269
|
}
|
|
94124
94270
|
const options = parseTicketUpdateOptions(args);
|
|
94125
|
-
const
|
|
94271
|
+
const allowedStatuses = await listAllowedTicketUpdateStatuses(schubDir);
|
|
94272
|
+
if (allowedStatuses.length === 0) {
|
|
94273
|
+
throw new Error("No ticket statuses configured for this project.");
|
|
94274
|
+
}
|
|
94275
|
+
if (!allowedStatuses.includes(options.status)) {
|
|
94276
|
+
throw new Error(`Invalid status '${options.status}'. Use ${allowedStatuses.join(", ")}.`);
|
|
94277
|
+
}
|
|
94278
|
+
const updated = updateTicketStatuses(schubDir, options.ticketIds, options.status, options.blockedReason);
|
|
94279
|
+
const updatedIds = new Set(updated.map((t) => t.id));
|
|
94280
|
+
const missingIds = options.ticketIds.filter((id) => !updatedIds.has(id));
|
|
94281
|
+
if (missingIds.length > 0) {
|
|
94282
|
+
throw new Error(`Ticket(s) not found: ${missingIds.join(", ")}`);
|
|
94283
|
+
}
|
|
94284
|
+
const { dbPath, storagePath } = resolveServicesConfig3(schubDir);
|
|
94285
|
+
const services = await createCliServices(dbPath, storagePath);
|
|
94286
|
+
const projectId = resolveProjectId2(schubDir, services);
|
|
94287
|
+
const statusId = findStatusId2(services, projectId, options.status);
|
|
94126
94288
|
for (const ticket of updated) {
|
|
94127
|
-
|
|
94289
|
+
if (statusId) {
|
|
94290
|
+
services.tickets.save({
|
|
94291
|
+
id: ticket.id,
|
|
94292
|
+
project_id: projectId,
|
|
94293
|
+
status_id: statusId,
|
|
94294
|
+
blocked_reason: options.blockedReason !== undefined ? options.blockedReason : ticket.blockedReason
|
|
94295
|
+
});
|
|
94296
|
+
}
|
|
94297
|
+
process.stdout.write(`[OK] Updated ticket ${ticket.id}: ${formatCliTicketStatus(ticket.status)}
|
|
94128
94298
|
`);
|
|
94129
94299
|
}
|
|
94130
94300
|
};
|
|
@@ -99533,12 +99703,12 @@ var import_react61 = __toESM(require_react(), 1);
|
|
|
99533
99703
|
// src/tui/app.tsx
|
|
99534
99704
|
import { readFileSync as readFileSync18 } from "node:fs";
|
|
99535
99705
|
import { homedir } from "node:os";
|
|
99536
|
-
import { basename as
|
|
99706
|
+
import { basename as basename5, dirname as dirname17, resolve as resolve13 } from "node:path";
|
|
99537
99707
|
var import_react60 = __toESM(require_react(), 1);
|
|
99538
99708
|
// package.json
|
|
99539
99709
|
var package_default = {
|
|
99540
99710
|
name: "schub",
|
|
99541
|
-
version: "0.1.
|
|
99711
|
+
version: "0.1.11",
|
|
99542
99712
|
type: "module",
|
|
99543
99713
|
bin: {
|
|
99544
99714
|
schub: "./dist/index.js"
|
|
@@ -99561,7 +99731,6 @@ var package_default = {
|
|
|
99561
99731
|
node: ">=24"
|
|
99562
99732
|
},
|
|
99563
99733
|
dependencies: {
|
|
99564
|
-
"@pstdio/schub-services": "workspace:*",
|
|
99565
99734
|
"@inkjs/ui": "^2.0.0",
|
|
99566
99735
|
chalk: "^5.6.2",
|
|
99567
99736
|
ink: "^6.6.0",
|
|
@@ -99572,6 +99741,7 @@ var package_default = {
|
|
|
99572
99741
|
yargs: "^18.0.0"
|
|
99573
99742
|
},
|
|
99574
99743
|
devDependencies: {
|
|
99744
|
+
"@pstdio/schub-services": "workspace:*",
|
|
99575
99745
|
"@types/bun": "latest",
|
|
99576
99746
|
"@types/marked-terminal": "^6.0.0",
|
|
99577
99747
|
"@types/node": "^24.3.0",
|
|
@@ -104966,9 +105136,10 @@ function PreviewPage({ fileName, markdown, height, onClose }) {
|
|
|
104966
105136
|
|
|
104967
105137
|
// src/tui/components/status-view-data.ts
|
|
104968
105138
|
import { readFileSync as readFileSync17 } from "node:fs";
|
|
104969
|
-
import { dirname as
|
|
105139
|
+
import { dirname as dirname15, join as join21, normalize as normalize2, sep } from "node:path";
|
|
104970
105140
|
var TICKET_STATUS_LABELS = {
|
|
104971
105141
|
backlog: "Backlog",
|
|
105142
|
+
ready: "Ready",
|
|
104972
105143
|
reviewed: "Reviewed",
|
|
104973
105144
|
wip: "WIP",
|
|
104974
105145
|
blocked: "Blocked",
|
|
@@ -105078,7 +105249,7 @@ var buildChangeTicketCounts = (tickets) => {
|
|
|
105078
105249
|
return changeTicketCounts;
|
|
105079
105250
|
};
|
|
105080
105251
|
var hasDraftTokens = (repoRoot, ticket) => {
|
|
105081
|
-
const content = readFileSync17(
|
|
105252
|
+
const content = readFileSync17(join21(repoRoot, ticket.path), "utf8");
|
|
105082
105253
|
return hasUnimplementedTemplateTokens(content);
|
|
105083
105254
|
};
|
|
105084
105255
|
var isReadyTicket = (ticket, ticketsById) => {
|
|
@@ -105192,7 +105363,7 @@ var buildTicketListItems = (schubDir, statuses = DEFAULT_TICKET_STATUSES) => {
|
|
|
105192
105363
|
}
|
|
105193
105364
|
const allTickets = loadTicketDependencies(schubDir, TICKET_STATUSES);
|
|
105194
105365
|
const ticketsById = new Map(allTickets.map((ticket) => [ticket.id, ticket]));
|
|
105195
|
-
const repoRoot =
|
|
105366
|
+
const repoRoot = dirname15(schubDir);
|
|
105196
105367
|
const allowed = new Set(statuses);
|
|
105197
105368
|
const changeTitles = new Map(listChangeOverview(schubDir).map((change) => [change.id, formatChangeMenuName(change.id, change.title)]));
|
|
105198
105369
|
return allTickets.filter((ticket) => allowed.has(ticket.status)).map((ticket) => ({
|
|
@@ -105693,7 +105864,7 @@ function SessionView({ state }) {
|
|
|
105693
105864
|
}
|
|
105694
105865
|
|
|
105695
105866
|
// src/tui/components/status-view.tsx
|
|
105696
|
-
import { dirname as
|
|
105867
|
+
import { dirname as dirname16, resolve as resolve12 } from "node:path";
|
|
105697
105868
|
var import_react59 = __toESM(require_react(), 1);
|
|
105698
105869
|
|
|
105699
105870
|
// src/tui/hooks/use-refresh-interval.ts
|
|
@@ -106343,7 +106514,7 @@ function StatusView({
|
|
|
106343
106514
|
reviewed,
|
|
106344
106515
|
backlog: backlog2
|
|
106345
106516
|
} = buildStatusData(schubDir);
|
|
106346
|
-
const repoRoot = schubDir ?
|
|
106517
|
+
const repoRoot = schubDir ? dirname16(schubDir) : "";
|
|
106347
106518
|
const changeListItems = buildChangeListItems(schubDir);
|
|
106348
106519
|
const ticketListItems = buildTicketListItems(schubDir);
|
|
106349
106520
|
const showAllTicketListItems = buildTicketListItems(schubDir, TICKET_STATUSES);
|
|
@@ -106814,7 +106985,7 @@ function App2({
|
|
|
106814
106985
|
}
|
|
106815
106986
|
return targetDir;
|
|
106816
106987
|
})();
|
|
106817
|
-
const repoRoot = schubDir ?
|
|
106988
|
+
const repoRoot = schubDir ? dirname17(schubDir) : "";
|
|
106818
106989
|
const isPreviewActive = Boolean(preview);
|
|
106819
106990
|
const isDetailActive = Boolean(proposalDetail);
|
|
106820
106991
|
const isSessionActive = Boolean(sessionView);
|
|
@@ -106864,7 +107035,7 @@ function App2({
|
|
|
106864
107035
|
const handleOpenPreview = (repoRoot2, relativePath) => {
|
|
106865
107036
|
const targetPath = resolve13(repoRoot2, relativePath);
|
|
106866
107037
|
const markdown = readFileSync18(targetPath, "utf8");
|
|
106867
|
-
setPreview({ fileName:
|
|
107038
|
+
setPreview({ fileName: basename5(relativePath), markdown });
|
|
106868
107039
|
};
|
|
106869
107040
|
const handleClosePreview = () => {
|
|
106870
107041
|
setPreview(null);
|
|
@@ -107215,19 +107386,19 @@ var runCommand = async () => {
|
|
|
107215
107386
|
await runChangesList(rawArgs.slice(2), startDir);
|
|
107216
107387
|
}).demandCommand(1, "Provide a proposals command."), () => {}).command("tickets", "Ticket commands", (command2) => command2.strict().command("create [args..]", "Create ticket files for a proposal", (sub) => sub.strict(false), () => {
|
|
107217
107388
|
runTicketsCreate(rawArgs.slice(2), startDir);
|
|
107218
|
-
}).command("list [args..]", "List tickets", (sub) => sub.strict(false), () => {
|
|
107219
|
-
runTicketsList(resolveSchubDir(), rawArgs.slice(2));
|
|
107389
|
+
}).command("list [args..]", "List tickets", (sub) => sub.strict(false), async () => {
|
|
107390
|
+
await runTicketsList(resolveSchubDir(), rawArgs.slice(2));
|
|
107220
107391
|
}).command("check [args..]", "Update ticket checklist item", (sub) => sub.strict(false), () => {
|
|
107221
107392
|
runTicketsCheck(resolveSchubDir(), rawArgs.slice(2));
|
|
107222
|
-
}).command("update [args..]", "Update
|
|
107223
|
-
runTicketsUpdate(resolveSchubDir(), rawArgs.slice(2));
|
|
107393
|
+
}).command("update [args..]", "Update ticket status", (sub) => sub.strict(false), async () => {
|
|
107394
|
+
await runTicketsUpdate(resolveSchubDir(), rawArgs.slice(2));
|
|
107224
107395
|
}).command("implement [args..]", "Assign a ticket for implementation", (sub) => sub.strict(false), () => {
|
|
107225
107396
|
runTicketsImplement(resolveSchubDir(), rawArgs.slice(2));
|
|
107397
|
+
}).command("save [args..]", "Save a ticket", (sub) => sub.strict(false), async (argv) => {
|
|
107398
|
+
await runTicketsSave({ id: argv.id }, startDir);
|
|
107226
107399
|
}).command("pull [args..]", "Pull all non-archived tickets from the database", (sub) => sub.strict(false), async () => {
|
|
107227
107400
|
await runTicketsPull(startDir);
|
|
107228
|
-
}).demandCommand(1, "Provide a tickets command."), () => {}).command("
|
|
107229
|
-
await runTicketSave({ id: argv.id }, startDir);
|
|
107230
|
-
}).demandCommand(1, "Provide a ticket command."), () => {}).command("services", "Database service commands", (command2) => command2.strict().command("projects", "Project service commands", (projectCommand) => projectCommand.strict().command("list [args..]", "List projects", (sub) => sub.strict(false), async () => {
|
|
107401
|
+
}).demandCommand(1, "Provide a tickets command."), () => {}).command("services", "Database service commands", (command2) => command2.strict().command("projects", "Project service commands", (projectCommand) => projectCommand.strict().command("list [args..]", "List projects", (sub) => sub.strict(false), async () => {
|
|
107231
107402
|
const { runServicesProjectsList: runServicesProjectsList2 } = await Promise.resolve().then(() => (init_projects(), exports_projects));
|
|
107232
107403
|
await runServicesProjectsList2(rawArgs.slice(3));
|
|
107233
107404
|
}).command("create [args..]", "Create a project", (sub) => sub.strict(false), async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "schub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"schub": "./dist/index.js"
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
"node": ">=24"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@pstdio/schub-services": "workspace:*",
|
|
27
26
|
"@inkjs/ui": "^2.0.0",
|
|
28
27
|
"chalk": "^5.6.2",
|
|
29
28
|
"ink": "^6.6.0",
|
|
@@ -34,6 +33,7 @@
|
|
|
34
33
|
"yargs": "^18.0.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
36
|
+
"@pstdio/schub-services": "workspace:*",
|
|
37
37
|
"@types/bun": "latest",
|
|
38
38
|
"@types/marked-terminal": "^6.0.0",
|
|
39
39
|
"@types/node": "^24.3.0",
|
|
@@ -16,10 +16,7 @@ $ARGUMENTS
|
|
|
16
16
|
- Otherwise set status to `wip`.
|
|
17
17
|
2. Derive a concise, verb-led ticket title from the user request and immediately run the schub CLI to create the ticket.
|
|
18
18
|
- Run `bunx schub tickets create --title "<ticket title>" --input "<user prompt verbatim>" --status "<status>"`.
|
|
19
|
-
- If the ticket maps to a proposal, include `--proposal-id "<
|
|
20
|
-
- Optional flags: `--overwrite`.
|
|
21
|
-
- Capture the ticket file path from the command output (under `.schub/tickets/TK####_slug.md`).
|
|
22
|
-
|
|
19
|
+
- If the ticket maps to a proposal, include `--proposal-id "<shorthand>"`.
|
|
23
20
|
3. Fill the ticket template with concrete details:
|
|
24
21
|
- Priority (P1/P2/P3)
|
|
25
22
|
- Parallelizable (yes/no)
|
|
@@ -28,10 +25,13 @@ $ARGUMENTS
|
|
|
28
25
|
- Implementation Notes with key files/modules and decisions
|
|
29
26
|
- Acceptance with explicit tests, file paths, and exact commands
|
|
30
27
|
- Documentation updates, or an explicit “no docs” note
|
|
31
|
-
- If blocked,
|
|
32
|
-
4.
|
|
33
|
-
5.
|
|
28
|
+
- If blocked, run `bunx schub tickets update --id "<ticket-id>" --status blocked --blocked-reason <reason>`
|
|
29
|
+
4. When defining acceptance, list the exact test file paths, cases covered, and commands to run. Tests must live in the same ticket as the feature/bugfix work. Do not create standalone “add tests” tickets. Tests belong with the functional change they validate.
|
|
30
|
+
5. Resolve blockers by checking all existing tickets that are not done. If another ticket is a blocker, add it to `depends_on` in frontmatter.
|
|
31
|
+
6. Run `bunx schub tickets save --id "<ticket id>"` to persist the updated ticket content.
|
|
32
|
+
7. If the user only asked to create the ticket, stop after the ticket file(s) are created and saved and do not implement code. Otherwise start the ticket implementation.
|
|
34
33
|
|
|
35
34
|
## Output Locations
|
|
36
35
|
|
|
37
|
-
- `.schub/tickets
|
|
36
|
+
- Tickets: `.schub/tickets/<ticket-id>_<ticket-name>/ticket.md`
|
|
37
|
+
- Validation Artifacts: `.schub/tickets/<ticket-id>_<ticket-name>/`
|
|
@@ -12,7 +12,7 @@ $ARGUMENTS
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
14
14
|
1. Load the proposal files locally (they may not exist yet):
|
|
15
|
-
- Run `bunx schub proposals load --proposal-id "<
|
|
15
|
+
- Run `bunx schub proposals load --proposal-id "<shorthand>"`.
|
|
16
16
|
2. Confirm `.schub/proposals/<proposal-id>/proposal.md` exists and that it is marked accepted. If it is missing, ask the user to run the create-proposal skill first. If it is still a draft, ask the user to review it first.
|
|
17
17
|
3. Read the proposal and other files in the `<proposal-id>` folder.
|
|
18
18
|
4. Derive tickets, each ticket should be:
|
|
@@ -24,30 +24,27 @@ $ARGUMENTS
|
|
|
24
24
|
- Include `Acceptance` criteria with explicit tests (file paths, cases covered) and exact commands to run.
|
|
25
25
|
- Include corresponding documentation updates.
|
|
26
26
|
5. Create tickets via the CLI (one command per ticket).
|
|
27
|
-
- Run `bunx schub tickets create --title "<ticket title>" --proposal-id "<
|
|
28
|
-
|
|
29
|
-
- Capture the ticket file path from the command output (under `.schub/tickets/TK####_slug.md`).
|
|
30
|
-
|
|
31
|
-
6. Fill the template fields with concrete details
|
|
27
|
+
- Run `bunx schub tickets create --title "<ticket title>" --proposal-id "<shorthand>" --status "<status>"`.
|
|
28
|
+
6. Fill the ticket template with concrete details:
|
|
32
29
|
- Priority (P1/P2/P3)
|
|
33
30
|
- Parallelizable (yes/no)
|
|
34
|
-
- Goal, scope, steps, and evidence
|
|
35
|
-
- References
|
|
36
|
-
- Implementation Notes
|
|
37
|
-
- Acceptance
|
|
31
|
+
- Goal, scope, steps, acceptance, and evidence
|
|
32
|
+
- References to existing docs/specs (if any), otherwise record gaps as assumptions
|
|
33
|
+
- Implementation Notes with key files/modules and decisions
|
|
34
|
+
- Acceptance with explicit tests, file paths, and exact commands
|
|
38
35
|
- Documentation updates, or an explicit “no docs” note
|
|
39
|
-
- If
|
|
40
|
-
7. When defining acceptance, list the exact test file paths, cases covered, and commands to run. Tests must live in the same ticket as the feature/bugfix work
|
|
36
|
+
- If blocked, run `bunx schub tickets update --id "<ticket-id>" --status blocked --blocked-reason <reason>`
|
|
37
|
+
7. When defining acceptance, list the exact test file paths, cases covered, and commands to run. Tests must live in the same ticket as the feature/bugfix work. Do not create standalone “add tests” tickets. Tests belong with the functional change they validate.
|
|
41
38
|
8. Resolve blockers by checking all existing tickets that are not done. If another ticket is a blocker, add it to `depends_on` in frontmatter.
|
|
42
39
|
9. Run `bunx schub tickets save --id "<ticket id>"` to persist updated tickets.
|
|
43
40
|
10. Stop after the ticket files are created. Do not implement code or modify the plan or supporting artifacts.
|
|
44
41
|
|
|
45
42
|
## Output Locations
|
|
46
43
|
|
|
47
|
-
- `.schub/tickets
|
|
44
|
+
- Tickets: `.schub/tickets/<ticket-id>_<ticket-name>/ticket.md`
|
|
45
|
+
- Validation Artifacts: `.schub/tickets/<ticket-id>_<ticket-name>/`
|
|
48
46
|
|
|
49
47
|
## Ticket Notes
|
|
50
48
|
|
|
51
|
-
- Use the plan's acceptance alignment to define acceptance criteria.
|
|
52
49
|
- Split tickets that span multiple systems or large scopes.
|
|
53
50
|
- Record assumptions in Implementation Notes when cookbook references or key details are missing.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: implement-ticket
|
|
3
|
-
description: "Implement a single ticket end-to-end: locate the ticket file by id,
|
|
3
|
+
description: "Implement a single ticket end-to-end: locate the ticket file by id, then update its status (`in_review`, `wip`, `blocked`) via CLI. Use when asked to implement or complete a ticket."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## User Input
|
|
@@ -16,35 +16,30 @@ Use `--mode none` to skip worktree creation and run from the repo root. Use `--w
|
|
|
16
16
|
|
|
17
17
|
## Workflow
|
|
18
18
|
|
|
19
|
-
1.
|
|
19
|
+
1. Identify the ticket
|
|
20
20
|
- Accept: ticket id, full filename, or `next` / `continue`.
|
|
21
21
|
- If unclear or missing, ask for **one**: ticket id (kebab-case), exact filename, or `next`.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
- Otherwise search `.schub/tickets/{reviewed,backlog,wip,done,blocked}/` for `<ticket-id>_*.md`.
|
|
22
|
+
2. Find the ticket file
|
|
23
|
+
- If `next` / `continue`: run `bunx schub tickets list --status ready --json`, then pick the first ticket by id order.
|
|
24
|
+
- Otherwise run `bunx schub tickets list --id <shorthand> --json`.
|
|
26
25
|
- If multiple matches, ask the user to choose.
|
|
27
26
|
- If none found, ask the user to confirm the ticket id.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- If
|
|
31
|
-
- If
|
|
32
|
-
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
4. **TDD loop**
|
|
36
|
-
- **Write tests first (Red)**
|
|
27
|
+
3. Update the ticket status
|
|
28
|
+
- When starting work, run: `bunx schub tickets update --id "<ticket-id>" --status wip`.
|
|
29
|
+
- If blocked: run `bunx schub tickets update --id "<ticket-id>" --status blocked --blocked-reason <reason>` and stop the workflow.
|
|
30
|
+
- If complete: after confirming all checklists are checked, run `bunx schub tickets update --id "<ticket-id>" --status in_review`.
|
|
31
|
+
4. TDD loop
|
|
32
|
+
- Write tests first (Red)
|
|
37
33
|
- Add/extend tests based on `## Acceptance` (and key `## Steps`).
|
|
38
34
|
- Run tests and confirm they fail for the right reason.
|
|
39
|
-
-
|
|
35
|
+
- Implement the smallest change (Green)
|
|
40
36
|
- Write minimal code to make the failing test pass.
|
|
41
37
|
- Re-run tests until green.
|
|
42
|
-
-
|
|
38
|
+
- Refactor (Refactor)
|
|
43
39
|
- Clean up code and tests (names, structure, duplication) without changing behavior.
|
|
44
40
|
- Re-run tests to confirm still green.
|
|
45
41
|
- Repeat until all acceptance criteria are covered by tests and passing.
|
|
46
|
-
|
|
47
|
-
6. **Update ticket checklists as you go**
|
|
42
|
+
5. Update ticket checklists as you go
|
|
48
43
|
- Check off `## Steps` only when:
|
|
49
44
|
- A test exists that covers it, and
|
|
50
45
|
- Tests pass, and
|
|
@@ -52,21 +47,20 @@ Use `--mode none` to skip worktree creation and run from the repo root. Use `--w
|
|
|
52
47
|
- Check off `## Acceptance` only when:
|
|
53
48
|
- There’s test coverage for it, and
|
|
54
49
|
- The full test suite is green.
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
6. Evidence
|
|
51
|
+
- Store artifacts under `.schub/tickets/<ticket-id>_<ticket-name>/`
|
|
57
52
|
- Update the ticket file `## Evidence` section to reference generated artifacts.
|
|
58
53
|
- If tests/commands can’t be run, record why in `## Evidence`.
|
|
59
|
-
|
|
60
|
-
8. **Finish**
|
|
54
|
+
7. Finish
|
|
61
55
|
- Confirm everything in `## Steps` and `## Acceptance` is checked.
|
|
62
|
-
- If the ticket is not completed due to errors,
|
|
63
|
-
- If the ticket is completed,
|
|
56
|
+
- If the ticket is not completed due to errors, run `bunx schub tickets update --id "<ticket-id>" --status blocked --blocked-reason <reason>`, then run `bunx schub ticket save --id "<ticket-id>"`.
|
|
57
|
+
- If the ticket is completed, run `bunx schub tickets update --id "<ticket-id>" --status in_review`.
|
|
58
|
+
- Commit the implementation changes before handing off, including ticket updates and evidence references.
|
|
64
59
|
- Report completed files and artifacts.
|
|
65
60
|
|
|
66
61
|
## Validation
|
|
67
62
|
|
|
68
|
-
To be considered
|
|
69
|
-
e.g. by running `some_command &> some-logs.txt`
|
|
63
|
+
To be considered complete and ready for review, a ticket should provide Validation Artifacts. Validation Artifacts are **verifiable outputs** produced while doing the ticket e.g. by running `some_command &> some-logs.txt`
|
|
70
64
|
|
|
71
65
|
Agents should dump and review artifacts, including:
|
|
72
66
|
|
|
@@ -85,5 +79,5 @@ Artifacts **must** be:
|
|
|
85
79
|
|
|
86
80
|
## Output Locations
|
|
87
81
|
|
|
88
|
-
- Tickets: `.schub/tickets
|
|
89
|
-
- Validation Artifacts: `.schub/
|
|
82
|
+
- Tickets: `.schub/tickets/<ticket-id>_<ticket-name>/ticket.md`
|
|
83
|
+
- Validation Artifacts: `.schub/tickets/<ticket-id>_<ticket-name>/`
|
|
@@ -11,19 +11,21 @@ $ARGUMENTS
|
|
|
11
11
|
|
|
12
12
|
## Workflow
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
2. If
|
|
17
|
-
3.
|
|
14
|
+
1. Load the proposal files locally (they may not exist yet):
|
|
15
|
+
- Run `bunx schub proposals load --proposal-id "<shorthand>"`.
|
|
16
|
+
2. Confirm `.schub/proposals/<proposal-id>/proposal.md` exists. If it is missing, ask the user to run the create-proposal skill first. If it is still a draft, ask the user to review it first.
|
|
17
|
+
3. Review the proposal by checking the [MISSING INFORMATION] tags and the potential issues listed.
|
|
18
|
+
4. If there are issues, or missing information, run `bunx schub review create --proposal-id "<proposal-id>"` to scaffold `.schub/proposals/<proposal-id>/REVIEW_ME.md`.
|
|
19
|
+
5. Triage each item:
|
|
18
20
|
- **High-stakes** (scope, risks, dependencies, security, performance): create a bullet point item in the review checklist.
|
|
19
21
|
- **Minute details** (naming, copy, formatting, low-impact defaults): make a decision update the proposal and mark as an assumption in the proposal.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
6. Once the review checklist is completed, gather unchecked items (`- [ ]`) in order.
|
|
23
|
+
7. Start the review loop (process unchecked items in order), ask each item as a question. If unclear, ask a quick follow-up and do not advance.
|
|
22
24
|
- Update the review checklist accordingly inline with the answers.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
8. When no unchecked items remain, update the plan with the new information.
|
|
26
|
+
9. When no unchecked items remain, run `bunx schub review complete --proposal-id "<proposal-id>"` to create `.schub/proposals/<proposal-id>/Q&A.md`.
|
|
25
27
|
- Ask the LLM to migrate the TODO block into the Q&A sections and remove the TODO block.
|
|
26
|
-
|
|
28
|
+
10. Mark the proposal Status as "Accepted".
|
|
27
29
|
|
|
28
30
|
## Output Locations
|
|
29
31
|
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: save-proposal
|
|
3
|
-
description: "Persist proposal files via schub proposals save. Use when asked to save or persist a proposal."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
## User Input
|
|
7
|
-
|
|
8
|
-
```text
|
|
9
|
-
$ARGUMENTS
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
## Workflow
|
|
13
|
-
|
|
14
|
-
1. Parse the user input for a proposal-id.
|
|
15
|
-
- Accept `C####`, `C####_<suffix>`, or a full proposal-id.
|
|
16
|
-
- If the user provides shorthand `C####`, resolve it to the matching `C####_<suffix>` folder under `.schub/proposals/` (error if ambiguous or missing).
|
|
17
|
-
2. Run `bunx schub proposals save --proposal-id "<proposal-id>"`.
|
|
18
|
-
3. Use the CLI output as the confirmation of success. If the command errors, report it and ask for the correct proposal-id or for the user to run the create-proposal skill first.
|
|
19
|
-
|
|
20
|
-
## Notes
|
|
21
|
-
|
|
22
|
-
- **Do not start the implementation** only save the `.schub/proposals` folder.
|