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 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 `reviewed`, `rejected`, or `archived`.
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 ticket save` - Validate and save a ticket to DB/storage.
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: `reviewed`, `rejected`, or `archived`.
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 join20(regexps, separator = "|") {
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(join20(terminators), true);
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.title}`);
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
- title: options2.title,
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 mkdirSync6, readdirSync as readdirSync7, readFileSync as readFileSync10, rmSync as rmSync3, writeFileSync as writeFileSync6 } from "node:fs";
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 mkdirSync5, readdirSync as readdirSync6, readFileSync as readFileSync9, renameSync as renameSync2, rmSync as rmSync2, statSync as statSync8, writeFileSync as writeFileSync5 } from "node:fs";
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 = ["backlog", "reviewed", "wip", "blocked", "done", "rejected", "archived"];
91043
- var DEFAULT_TICKET_STATUSES = ["backlog", "reviewed", "wip", "blocked", "done", "archived"];
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, mkdirSync, readdirSync as readdirSync4, readFileSync as readFileSync5, renameSync, statSync as statSync6, writeFileSync } from "node:fs";
91163
- import { basename as basename3, dirname as dirname5, join as join6, relative as relative2, resolve as resolve7 } from "node:path";
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 parseTicketFilename = (fileName) => {
91202
- if (!fileName.endsWith(".md")) {
91203
- return null;
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
- const baseName = fileName.slice(0, -3);
91206
- const underscoreIndex = baseName.indexOf("_");
91207
- if (underscoreIndex <= 0) {
91219
+ };
91220
+ var parseTicketFolderName = (folderName) => {
91221
+ const underscoreIndex = folderName.indexOf("_");
91222
+ if (underscoreIndex <= 0)
91208
91223
  return null;
91209
- }
91210
- const id = baseName.slice(0, underscoreIndex);
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
- for (const status of TICKET_STATUSES) {
91305
- if (!allowed.has(status)) {
91335
+ const entries = readdirSync4(ticketsRoot, { withFileTypes: true });
91336
+ for (const entry of entries) {
91337
+ if (!entry.isDirectory()) {
91306
91338
  continue;
91307
91339
  }
91308
- const statusDir = join6(ticketsRoot, status);
91309
- if (!existsSync2(statusDir) || !isDirectory3(statusDir)) {
91340
+ if (!isTicketFolder(ticketsRoot, entry.name)) {
91310
91341
  continue;
91311
91342
  }
91312
- const entries = readdirSync4(statusDir, { withFileTypes: true });
91313
- for (const entry of entries) {
91314
- if (!entry.isFile()) {
91315
- continue;
91316
- }
91317
- const parsed = parseTicketFilename(entry.name);
91318
- if (!parsed) {
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 updateTicketStatuses = (schubDir, ticketIds, status) => {
91402
- const ticketsRoot = join6(schubDir, "tickets");
91403
- const targetRoot = join6(ticketsRoot, status);
91404
- mkdirSync(targetRoot, { recursive: true });
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, ["backlog", "reviewed"]);
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
- const nextPath = join6(targetRoot, basename3(ticket.path));
91411
- renameSync(currentPath, nextPath);
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: relative2(repoRoot, nextPath)
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
- for (const status of ACTIVE_TICKET_STATUSES) {
91430
- const statusDir = join6(ticketsRoot, status);
91431
- if (!existsSync2(statusDir) || !isDirectory3(statusDir)) {
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
- throw new Error(`Ticket ${normalizedId} not found.`);
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
- const archivePath = join6(archiveRoot, basename3(ticket.path));
91468
- renameSync(currentPath, archivePath);
91491
+ updateTicketFileStatus(currentPath, "archived");
91469
91492
  return {
91470
91493
  ...ticket,
91471
91494
  status: "archived",
91472
- path: relative2(repoRoot, archivePath)
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 mkdirSync3 } from "node:fs";
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 as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
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
- mkdirSync2(configDir, { recursive: true });
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
- mkdirSync3(resolvedRoot, { recursive: true });
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 mkdirSync4, readdirSync as readdirSync5, readFileSync as readFileSync7, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
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
- mkdirSync4(ticketsRoot, { recursive: true });
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(TICKET_PATTERN);
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 ticketPath = join9(ticketsRoot, `${ticketId}_${slug}.md`);
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(ticketPath) && !options.overwrite) {
91682
- throw new Error(`Refusing to overwrite existing file: ${ticketPath}`);
91705
+ if (existsSync5(ticketFilePath) && !options.overwrite) {
91706
+ throw new Error(`Refusing to overwrite existing file: ${ticketFilePath}`);
91683
91707
  }
91684
- mkdirSync4(ticketsRoot, { recursive: true });
91685
- writeFileSync3(ticketPath, rendered, "utf8");
91686
- return ticketPath;
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
- mkdirSync5(archiveRoot, { recursive: true });
91914
+ mkdirSync4(archiveRoot, { recursive: true });
91891
91915
  writeFileSync5(summary.proposalPath, updated, "utf8");
91892
- renameSync2(summary.changeDir, archivePath);
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
- mkdirSync5(join11(schubDir, "proposals"), { recursive: true });
91928
+ mkdirSync4(join11(schubDir, "proposals"), { recursive: true });
91905
91929
  writeFileSync5(summary.proposalPath, updated, "utf8");
91906
- renameSync2(summary.changeDir, activePath);
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
- mkdirSync5(targetDir, { recursive: true });
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
- mkdirSync5(targetDir, { recursive: true });
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
- mkdirSync5(changeDir, { recursive: true });
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
- mkdirSync6(targetDir, { recursive: true });
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
- mkdirSync6(dirname7(outputPath), { recursive: true });
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 mkdirSync7, readFileSync as readFileSync12, unlinkSync, writeFileSync as writeFileSync7 } from "node:fs";
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 mkdirSync8, readFileSync as readFileSync13, writeFileSync as writeFileSync8 } from "node:fs";
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
- mkdirSync8(dirname10(outputPath), { recursive: true });
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 mkdirSync9, readdirSync as readdirSync8, writeFileSync as writeFileSync9 } from "node:fs";
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
- mkdirSync9(ticketsRoot, { recursive: true });
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
- const title = titleMatch?.[1]?.trim() || "Untitled";
93590
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
93591
- const fileName = `${ticketId}${slug ? `_${slug}` : ""}.md`;
93592
- const filePath = join16(ticketsRoot, fileName);
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: ${ticketId} -> ${filePath}
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 basename4 } from "node:path";
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 `(${basename4(repoRoot)}) ${label} ${id}${suffix}`;
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 basename5, join as join18 } from "node:path";
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: basename5(relativePath),
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 parseStatusFilter = (value) => {
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 allowed = new Set(TICKET_STATUSES);
94000
- const invalid = normalized.filter((status) => !allowed.has(status));
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 normalized;
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
- return { statuses: parseStatusFilter(statusValue), json };
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 normalizedStatus = statusValue.trim().toLowerCase();
94088
- if (!BACKLOG_UPDATE_STATUSES.includes(normalizedStatus)) {
94089
- throw new Error(`Invalid status '${statusValue}'. Use reviewed, rejected, or archived.`);
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: normalizedStatus
94181
+ status: parsedStatus,
94182
+ blockedReason
94098
94183
  };
94099
94184
  return options;
94100
94185
  };
94101
- var runTicketsList = (schubDir, args) => {
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 tickets = listTickets(schubDir, options.statuses);
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 updated = updateTicketStatuses(schubDir, options.ticketIds, options.status);
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
- process.stdout.write(`[OK] Updated ticket ${ticket.id}: backlog -> ${ticket.status}
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 basename6, dirname as dirname16, resolve as resolve13 } from "node:path";
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.10",
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 dirname14, join as join20, normalize as normalize2, sep } from "node:path";
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(join20(repoRoot, ticket.path), "utf8");
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 = dirname14(schubDir);
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 dirname15, resolve as resolve12 } from "node:path";
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 ? dirname15(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 ? dirname16(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: basename6(relativePath), markdown });
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 backlog ticket status", (sub) => sub.strict(false), () => {
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("ticket", "Single-ticket commands", (command2) => command2.strict().command("save [args..]", "Save a ticket", (sub) => sub.strict(false), async (argv) => {
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.10",
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 "<proposal-id or shorthand>"`.
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, set `blocked_reason` and add `depends_on`
32
- 4. Run `bunx schub tickets save --id "<ticket id>"` to persist the updated ticket content.
33
- 5. 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.
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/TK####_slug.md`
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 "<proposal-id or shorthand>"`.
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 "<proposal-id or shorthand>" --status "<status>"`.
28
- - Optional flags: `--overwrite`.
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 (link to cookbook/proposal/spec files when available)
36
- - Implementation Notes (assumptions, key decisions, and code pointers; record missing cookbook assumptions here)
37
- - Acceptance (explicit tests with file paths, cases covered, and exact commands)
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 the ticket is blocked, set `blocked_reason` in the ticket frontmatter
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—do not create standalone “add tests” tickets. Tests belong with the functional change they validate.
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/TK####_slug.md`
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, move it across status folders (`reviewed`, `wip`, `blocked`, `done`). Use when asked to implement or complete a ticket."
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. **Identify the ticket**
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
- 2. **Find the ticket file**
24
- - If `next` / `continue`: sort `.schub/tickets/reviewed/`, pick the first file.
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
- 3. **Move the ticket before work**
30
- - If starting work: move to `.schub/tickets/wip/` (create folder if needed).
31
- - If blocked: move to `.schub/tickets/blocked/` and set `blocked_reason` in the ticket frontmatter.
32
- - If complete: move to `.schub/tickets/done/` after confirming all checklists are checked.
33
- - Keep the ticket in **exactly one** status folder.
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
- - **Implement the smallest change (Green)**
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
- - **Refactor (Refactor)**
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
- 7. **Evidence**
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, move the ticket file to `.schub/tickets/blocked/`.
63
- - If the ticket is completed, move the ticket file to `.schub/tickets/done/`.
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 "done", a ticket should provide Validation Artifacts. Validation Artifacts are **verifiable outputs** produced while doing the ticket.
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/{reviewed,backlog,wip,done,blocked}/<ticket-id>_<ticket-name>.md`
89
- - Validation Artifacts: `.schub/artifacts/<ticket-id>/`
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
- 0. If the user provides shorthand `C####`, resolve it to the matching `C####_<suffix>` proposal folder (error if ambiguous). Mark the proposal Status as "In Review"
15
- 1. Review the proposal by checking the [MISSING INFORMATION] tags and the potential issues listed.
16
- 2. 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`.
17
- 3. Triage each item:
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
- 4. Once the review checklist is completed, gather unchecked items (`- [ ]`) in order.
21
- 5. 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
+ 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
- 6. When no unchecked items remain, update the plan with the new information.
24
- 7. When no unchecked items remain, run `bunx schub review complete --proposal-id "<proposal-id>"` to create `.schub/proposals/<proposal-id>/Q&A.md`.
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
- 8. Mark the proposal Status as "Accepted".
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.