issy 0.10.0 → 0.10.1

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
@@ -90,6 +90,7 @@ Once installed globally, you can run commands from your terminal:
90
90
  ```bash
91
91
  issy # Start the web UI
92
92
  issy list # List open issues (roadmap order)
93
+ issy list --unblocked # List open issues with no open blockers
93
94
  issy next # Show next issue to work on
94
95
  issy create --title "Bug" # Create an issue
95
96
  issy learn # Print AI-agent instructions
@@ -137,12 +138,15 @@ Opens a local read-only UI at `http://localhost:1554` for browsing issues.
137
138
  issy init # Create .issy/issues/ directory
138
139
  issy init --seed # Create with a welcome issue
139
140
  issy list # List open issues (roadmap order)
141
+ issy list --unblocked # List open issues with no open blockers
140
142
  issy next # Show next issue to work on
141
143
  issy search "auth" # Fuzzy search
142
144
  issy read 0001 # View issue
143
145
  issy create --title "Bug" --after 0002 # Create issue after #0002
146
+ issy create --title "Feature" --depends-on 0012,0035 --last # Blocked by issues
144
147
  issy create --title "Bug" --body "Details here" --last # Create with body content
145
148
  issy update 0001 --before 0003 # Reposition in roadmap
149
+ issy update 0001 --depends-on 0012,0035 # Replace blockers
146
150
  issy update 0001 --body "New details" # Replace body content
147
151
  issy close 0001 # Close issue
148
152
  issy reopen 0001 --after 0004 # Reopen and place in roadmap
@@ -175,6 +179,14 @@ issy list --sort priority # Sort by priority instead
175
179
  issy list --sort created # Sort by creation date
176
180
  ```
177
181
 
182
+ The `Blk` column in `issy list` shows the count of currently open blockers. `-` means the issue is unblocked. Use `issy list --unblocked` to show only open issues that have no open blockers. Dependency IDs that do not match an existing issue are ignored.
183
+
184
+ Issues can declare blockers with `depends_on`:
185
+
186
+ ```yaml
187
+ depends_on: 0012, 0035
188
+ ```
189
+
178
190
  ### Hooks
179
191
 
180
192
  issy supports optional hook files in `.issy/` that inject context into stdout after successful operations. The file contents are printed directly, making them visible to AI agents in their command output.
@@ -226,6 +238,7 @@ scope: medium
226
238
  type: bug
227
239
  status: open
228
240
  order: a0
241
+ depends_on: 0012, 0035
229
242
  created: 2025-01-15T10:30:00
230
243
  ---
231
244
 
@@ -243,6 +256,7 @@ session isn't established, causing a redirect loop.
243
256
  | `status` | `open`, `closed` |
244
257
  | `labels` | comma-separated (optional) |
245
258
  | `order` | fractional index key (managed by issy) |
259
+ | `depends_on` | comma-separated blocking issue IDs (optional) |
246
260
 
247
261
  ## Configuration
248
262
 
package/dist/cli.js CHANGED
@@ -355,6 +355,9 @@ function generateFrontmatter(data) {
355
355
  if (data.order) {
356
356
  lines.push(`order: ${data.order}`);
357
357
  }
358
+ if (data.depends_on) {
359
+ lines.push(`depends_on: ${data.depends_on}`);
360
+ }
358
361
  lines.push(`created: ${data.created}`);
359
362
  if (data.updated) {
360
363
  lines.push(`updated: ${data.updated}`);
@@ -363,6 +366,44 @@ function generateFrontmatter(data) {
363
366
  return lines.join(`
364
367
  `);
365
368
  }
369
+ function parseDependencyIds(dependsOn) {
370
+ if (!dependsOn)
371
+ return [];
372
+ const seen = new Set;
373
+ const ids = [];
374
+ for (const token of dependsOn.split(/[,\s]+/)) {
375
+ const value = token.trim().replace(/^['"]+|['"]+$/g, "").replace(/^#/, "");
376
+ if (!/^\d+$/.test(value))
377
+ continue;
378
+ const id = value.padStart(4, "0");
379
+ if (seen.has(id))
380
+ continue;
381
+ seen.add(id);
382
+ ids.push(id);
383
+ }
384
+ return ids;
385
+ }
386
+ function filterExistingDependencyIds(dependsOn, issues, excludeId) {
387
+ const existingIds = new Set(issues.map((issue) => issue.id));
388
+ const excludedId = excludeId?.padStart(4, "0");
389
+ return parseDependencyIds(dependsOn).filter((id) => existingIds.has(id) && id !== excludedId);
390
+ }
391
+ function formatExistingDependencyIds(dependsOn, issues, excludeId) {
392
+ return filterExistingDependencyIds(dependsOn, issues, excludeId).join(", ");
393
+ }
394
+ function getDependencyIssues(issue, issues) {
395
+ const dependencyIds = filterExistingDependencyIds(issue.frontmatter.depends_on, issues, issue.id);
396
+ if (dependencyIds.length === 0)
397
+ return [];
398
+ const issueById = new Map(issues.map((i) => [i.id, i]));
399
+ return dependencyIds.map((id) => issueById.get(id)).filter((dependency) => Boolean(dependency));
400
+ }
401
+ function getBlockingIssues(issue, issues) {
402
+ return getDependencyIssues(issue, issues).filter((dependency) => dependency.frontmatter.status === "open");
403
+ }
404
+ function isIssueUnblocked(issue, issues) {
405
+ return issue.frontmatter.status === "open" && getBlockingIssues(issue, issues).length === 0;
406
+ }
366
407
  function getIssueIdFromFilename(filename) {
367
408
  const match = filename.match(/^(\d+)-/);
368
409
  return match ? match[1] : filename.replace(".md", "");
@@ -496,6 +537,8 @@ async function createIssue(input) {
496
537
  const issueNumber = await getNextIssueNumber();
497
538
  const slug = createSlug(input.title);
498
539
  const filename = `${issueNumber}-${slug}.md`;
540
+ const existingIssues = await getAllIssues();
541
+ const dependsOn = formatExistingDependencyIds(input.depends_on, existingIssues, issueNumber);
499
542
  const frontmatter = {
500
543
  title: input.title,
501
544
  priority,
@@ -504,6 +547,7 @@ async function createIssue(input) {
504
547
  labels: input.labels || undefined,
505
548
  status: "open",
506
549
  order: input.order || undefined,
550
+ depends_on: dependsOn || undefined,
507
551
  created: formatDate()
508
552
  };
509
553
  const body = input.body ?? `
@@ -529,6 +573,8 @@ async function updateIssue(id, input) {
529
573
  if (!issue) {
530
574
  throw new Error(`Issue not found: ${id}`);
531
575
  }
576
+ const allIssues = await getAllIssues();
577
+ const dependsOn = input.depends_on !== undefined ? formatExistingDependencyIds(input.depends_on, allIssues, issue.id) : undefined;
532
578
  const updatedFrontmatter = {
533
579
  ...issue.frontmatter,
534
580
  ...input.title && { title: input.title },
@@ -540,6 +586,9 @@ async function updateIssue(id, input) {
540
586
  },
541
587
  ...input.status && { status: input.status },
542
588
  ...input.order && { order: input.order },
589
+ ...input.depends_on !== undefined && {
590
+ depends_on: dependsOn || undefined
591
+ },
543
592
  updated: formatDate()
544
593
  };
545
594
  const updatedContent = input.body !== undefined ? `
@@ -2039,6 +2088,14 @@ function filterByQuery(issues, query) {
2039
2088
  if (issue.frontmatter.status !== statusValue) {
2040
2089
  return false;
2041
2090
  }
2091
+ } else if (statusValue === "unblocked") {
2092
+ if (!isIssueUnblocked(issue, issues)) {
2093
+ return false;
2094
+ }
2095
+ } else if (statusValue === "blocked") {
2096
+ if (issue.frontmatter.status !== "open" || isIssueUnblocked(issue, issues)) {
2097
+ return false;
2098
+ }
2042
2099
  }
2043
2100
  }
2044
2101
  if (parsed.qualifiers.priority) {
@@ -2126,7 +2183,23 @@ function typeSymbol(type) {
2126
2183
  }
2127
2184
  function formatIssueRow(issue) {
2128
2185
  const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";
2129
- return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${issue.frontmatter.title}`;
2186
+ const blocked = issue.blockers > 0 ? String(issue.blockers) : "-";
2187
+ return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${blocked.padEnd(3)} ${issue.frontmatter.title}`;
2188
+ }
2189
+ function printDependsOn(issue, issues) {
2190
+ const dependencyIssues = getDependencyIssues(issue, issues);
2191
+ if (dependencyIssues.length === 0)
2192
+ return;
2193
+ console.log(` Depends on: ${dependencyIssues.map((i) => `#${i.id}`).join(", ")}`);
2194
+ }
2195
+ function getDependsOnArg(values) {
2196
+ const value = values["depends-on"];
2197
+ return typeof value === "string" ? value : undefined;
2198
+ }
2199
+ async function resolveDependsOn(value, excludeId) {
2200
+ if (value === undefined)
2201
+ return;
2202
+ return formatExistingDependencyIds(value, await getAllIssues(), excludeId);
2130
2203
  }
2131
2204
  async function resolvePosition(opts) {
2132
2205
  const openIssues = await getOpenIssuesByOrder();
@@ -2158,7 +2231,7 @@ function hasHelpFlag(commandArgs) {
2158
2231
  async function listIssues(options) {
2159
2232
  const allIssues = await getAllIssues();
2160
2233
  const queryParts = [];
2161
- if (!options.all)
2234
+ if (!options.all && !options.unblocked)
2162
2235
  queryParts.push("is:open");
2163
2236
  if (options.priority)
2164
2237
  queryParts.push(`priority:${options.priority}`);
@@ -2170,17 +2243,23 @@ async function listIssues(options) {
2170
2243
  queryParts.push(`sort:${options.sort}`);
2171
2244
  if (options.search)
2172
2245
  queryParts.push(options.search);
2173
- const query = queryParts.join(" ") || "is:open";
2174
- const issues = filterByQuery(allIssues, query);
2246
+ const query = queryParts.join(" ");
2247
+ let issues = query ? filterByQuery(allIssues, query) : allIssues;
2248
+ if (options.unblocked) {
2249
+ issues = issues.filter((issue) => isIssueUnblocked(issue, allIssues));
2250
+ }
2175
2251
  if (issues.length === 0) {
2176
2252
  console.log("No issues found.");
2177
2253
  return;
2178
2254
  }
2179
2255
  console.log(`
2180
- ID Pri Type Status Title`);
2256
+ ID Pri Type Status Blk Title`);
2181
2257
  console.log(` ${"-".repeat(100)}`);
2182
2258
  for (const issue of issues) {
2183
- console.log(formatIssueRow(issue));
2259
+ console.log(formatIssueRow({
2260
+ ...issue,
2261
+ blockers: getBlockingIssues(issue, allIssues).length
2262
+ }));
2184
2263
  }
2185
2264
  console.log(`
2186
2265
  Total: ${issues.length} issue(s)
@@ -2188,6 +2267,7 @@ async function listIssues(options) {
2188
2267
  }
2189
2268
  async function readIssue(id) {
2190
2269
  const issue = await getIssue(id);
2270
+ const allIssues = await getAllIssues();
2191
2271
  if (!issue) {
2192
2272
  console.error(`Issue not found: ${id}`);
2193
2273
  process.exit(1);
@@ -2206,6 +2286,7 @@ ${"=".repeat(70)}`);
2206
2286
  if (issue.frontmatter.labels) {
2207
2287
  console.log(` Labels: ${issue.frontmatter.labels}`);
2208
2288
  }
2289
+ printDependsOn(issue, allIssues);
2209
2290
  if (issue.frontmatter.order) {
2210
2291
  console.log(` Order: ${issue.frontmatter.order}`);
2211
2292
  }
@@ -2228,10 +2309,13 @@ async function searchIssuesCommand(query, options) {
2228
2309
  console.log(`
2229
2310
  Search results for "${query}":`);
2230
2311
  console.log(`
2231
- ID Pri Type Status Title`);
2312
+ ID Pri Type Status Blk Title`);
2232
2313
  console.log(` ${"-".repeat(100)}`);
2233
2314
  for (const issue of issues) {
2234
- console.log(formatIssueRow(issue));
2315
+ console.log(formatIssueRow({
2316
+ ...issue,
2317
+ blockers: getBlockingIssues(issue, allIssues).length
2318
+ }));
2235
2319
  }
2236
2320
  console.log(`
2237
2321
  Found: ${issues.length} issue(s)
@@ -2257,6 +2341,7 @@ async function createIssueCommand(options) {
2257
2341
  scope: options.scope,
2258
2342
  type: options.type,
2259
2343
  labels: options.labels,
2344
+ depends_on: await resolveDependsOn(options.dependsOn) || undefined,
2260
2345
  order
2261
2346
  };
2262
2347
  const issue = await createIssue(input);
@@ -2293,6 +2378,9 @@ async function updateIssueCommand(id, options) {
2293
2378
  scope: options.scope,
2294
2379
  type: options.type,
2295
2380
  labels: options.labels,
2381
+ ...options.dependsOn !== undefined && {
2382
+ depends_on: await resolveDependsOn(options.dependsOn, id) || ""
2383
+ },
2296
2384
  order
2297
2385
  });
2298
2386
  console.log(`Updated issue: ${issue.filename}`);
@@ -2341,6 +2429,7 @@ async function reopenIssueCommand(id, options) {
2341
2429
  }
2342
2430
  async function nextIssueCommand() {
2343
2431
  const issue = await getNextIssue();
2432
+ const allIssues = await getAllIssues();
2344
2433
  if (!issue) {
2345
2434
  console.log("No open issues.");
2346
2435
  return;
@@ -2359,6 +2448,7 @@ ${"=".repeat(70)}`);
2359
2448
  if (issue.frontmatter.labels) {
2360
2449
  console.log(` Labels: ${issue.frontmatter.labels}`);
2361
2450
  }
2451
+ printDependsOn(issue, allIssues);
2362
2452
  if (issue.frontmatter.order) {
2363
2453
  console.log(` Order: ${issue.frontmatter.order}`);
2364
2454
  }
@@ -2386,6 +2476,7 @@ Options:
2386
2476
  Commands:
2387
2477
  list List all open issues (roadmap order)
2388
2478
  --all, -a Include closed issues
2479
+ --unblocked Only show open issues with no open blockers
2389
2480
  --priority, -p <p> Filter by priority (high, medium, low)
2390
2481
  --scope <s> Filter by scope (small, medium, large)
2391
2482
  --type, -t <t> Filter by type (bug, improvement)
@@ -2410,6 +2501,7 @@ Commands:
2410
2501
  --scope <s> Scope (small, medium, large)
2411
2502
  --type <t> Type (bug, improvement)
2412
2503
  --labels, -l <l> Comma-separated labels
2504
+ --depends-on <ids> Comma-separated blocking issue IDs
2413
2505
  --before <id> Insert before this issue in roadmap
2414
2506
  --after <id> Insert after this issue in roadmap
2415
2507
  --first Insert at the beginning of the roadmap
@@ -2422,6 +2514,7 @@ Commands:
2422
2514
  --scope <s> New scope
2423
2515
  --type <t> New type
2424
2516
  --labels, -l <l> New labels
2517
+ --depends-on <ids> Replace blocking issue IDs (empty clears)
2425
2518
  --before <id> Move before this issue in roadmap
2426
2519
  --after <id> Move after this issue in roadmap
2427
2520
  --first Move to the beginning of the roadmap
@@ -2439,6 +2532,7 @@ Commands:
2439
2532
 
2440
2533
  Examples:
2441
2534
  issy list
2535
+ issy list --unblocked
2442
2536
  issy list --priority high --type bug
2443
2537
  issy next
2444
2538
  issy learn
@@ -2462,6 +2556,7 @@ List all open issues in roadmap order.
2462
2556
 
2463
2557
  Options:
2464
2558
  --all, -a Include closed issues
2559
+ --unblocked Only show open issues with no open blockers
2465
2560
  --priority, -p <p> Filter by priority (high, medium, low)
2466
2561
  --scope <s> Filter by scope (small, medium, large)
2467
2562
  --type, -t <t> Filter by type (bug, improvement)
@@ -2474,6 +2569,7 @@ Options:
2474
2569
  args: args.slice(1),
2475
2570
  options: {
2476
2571
  all: { type: "boolean", short: "a" },
2572
+ unblocked: { type: "boolean" },
2477
2573
  priority: { type: "string", short: "p" },
2478
2574
  scope: { type: "string" },
2479
2575
  type: { type: "string", short: "t" },
@@ -2551,6 +2647,7 @@ Options:
2551
2647
  --scope <s> Scope: small, medium, large
2552
2648
  --type <t> Type: bug, improvement (default: improvement)
2553
2649
  --labels, -l <l> Comma-separated labels
2650
+ --depends-on <ids> Comma-separated blocking issue IDs
2554
2651
  --before <id> Insert before this issue in roadmap
2555
2652
  --after <id> Insert after this issue in roadmap
2556
2653
  --first Insert at the beginning of the roadmap
@@ -2558,6 +2655,7 @@ Options:
2558
2655
 
2559
2656
  Examples:
2560
2657
  issy create --title "Fix login bug" --type bug --priority high --after 0002
2658
+ issy create --title "Add export" --depends-on 0001,0002 --last
2561
2659
  issy create --title "Add dark mode" --last
2562
2660
  `);
2563
2661
  return;
@@ -2571,6 +2669,7 @@ Examples:
2571
2669
  scope: { type: "string" },
2572
2670
  type: { type: "string" },
2573
2671
  labels: { type: "string", short: "l" },
2672
+ "depends-on": { type: "string" },
2574
2673
  before: { type: "string" },
2575
2674
  after: { type: "string" },
2576
2675
  first: { type: "boolean" },
@@ -2578,7 +2677,10 @@ Examples:
2578
2677
  },
2579
2678
  allowPositionals: true
2580
2679
  });
2581
- await createIssueCommand(values);
2680
+ await createIssueCommand({
2681
+ ...values,
2682
+ dependsOn: getDependsOnArg(values)
2683
+ });
2582
2684
  break;
2583
2685
  }
2584
2686
  case "update": {
@@ -2594,6 +2696,7 @@ Options:
2594
2696
  --scope <s> New scope: small, medium, large
2595
2697
  --type <t> New type: bug, improvement
2596
2698
  --labels, -l <l> New labels (comma-separated)
2699
+ --depends-on <ids> Replace blocking issue IDs (empty clears)
2597
2700
  --before <id> Move before this issue in roadmap
2598
2701
  --after <id> Move after this issue in roadmap
2599
2702
  --first Move to the beginning of the roadmap
@@ -2601,6 +2704,7 @@ Options:
2601
2704
 
2602
2705
  Examples:
2603
2706
  issy update 0001 --priority low --after 0003
2707
+ issy update 0002 --depends-on 0001,0003
2604
2708
  issy update 0002 --title "Renamed issue" --first
2605
2709
  `);
2606
2710
  return;
@@ -2619,6 +2723,7 @@ Examples:
2619
2723
  scope: { type: "string" },
2620
2724
  type: { type: "string" },
2621
2725
  labels: { type: "string", short: "l" },
2726
+ "depends-on": { type: "string" },
2622
2727
  before: { type: "string" },
2623
2728
  after: { type: "string" },
2624
2729
  first: { type: "boolean" },
@@ -2626,7 +2731,10 @@ Examples:
2626
2731
  },
2627
2732
  allowPositionals: true
2628
2733
  });
2629
- await updateIssueCommand(id, values);
2734
+ await updateIssueCommand(id, {
2735
+ ...values,
2736
+ dependsOn: getDependsOnArg(values)
2737
+ });
2630
2738
  break;
2631
2739
  }
2632
2740
  case "close": {
package/dist/main.js CHANGED
@@ -366,6 +366,9 @@ function generateFrontmatter(data) {
366
366
  if (data.order) {
367
367
  lines.push(`order: ${data.order}`);
368
368
  }
369
+ if (data.depends_on) {
370
+ lines.push(`depends_on: ${data.depends_on}`);
371
+ }
369
372
  lines.push(`created: ${data.created}`);
370
373
  if (data.updated) {
371
374
  lines.push(`updated: ${data.updated}`);
@@ -374,6 +377,44 @@ function generateFrontmatter(data) {
374
377
  return lines.join(`
375
378
  `);
376
379
  }
380
+ function parseDependencyIds(dependsOn) {
381
+ if (!dependsOn)
382
+ return [];
383
+ const seen = new Set;
384
+ const ids = [];
385
+ for (const token of dependsOn.split(/[,\s]+/)) {
386
+ const value = token.trim().replace(/^['"]+|['"]+$/g, "").replace(/^#/, "");
387
+ if (!/^\d+$/.test(value))
388
+ continue;
389
+ const id = value.padStart(4, "0");
390
+ if (seen.has(id))
391
+ continue;
392
+ seen.add(id);
393
+ ids.push(id);
394
+ }
395
+ return ids;
396
+ }
397
+ function filterExistingDependencyIds(dependsOn, issues, excludeId) {
398
+ const existingIds = new Set(issues.map((issue) => issue.id));
399
+ const excludedId = excludeId?.padStart(4, "0");
400
+ return parseDependencyIds(dependsOn).filter((id) => existingIds.has(id) && id !== excludedId);
401
+ }
402
+ function formatExistingDependencyIds(dependsOn, issues, excludeId) {
403
+ return filterExistingDependencyIds(dependsOn, issues, excludeId).join(", ");
404
+ }
405
+ function getDependencyIssues(issue, issues) {
406
+ const dependencyIds = filterExistingDependencyIds(issue.frontmatter.depends_on, issues, issue.id);
407
+ if (dependencyIds.length === 0)
408
+ return [];
409
+ const issueById = new Map(issues.map((i) => [i.id, i]));
410
+ return dependencyIds.map((id) => issueById.get(id)).filter((dependency) => Boolean(dependency));
411
+ }
412
+ function getBlockingIssues(issue, issues) {
413
+ return getDependencyIssues(issue, issues).filter((dependency) => dependency.frontmatter.status === "open");
414
+ }
415
+ function isIssueUnblocked(issue, issues) {
416
+ return issue.frontmatter.status === "open" && getBlockingIssues(issue, issues).length === 0;
417
+ }
377
418
  function getIssueIdFromFilename(filename) {
378
419
  const match = filename.match(/^(\d+)-/);
379
420
  return match ? match[1] : filename.replace(".md", "");
@@ -507,6 +548,8 @@ async function createIssue(input) {
507
548
  const issueNumber = await getNextIssueNumber();
508
549
  const slug = createSlug(input.title);
509
550
  const filename = `${issueNumber}-${slug}.md`;
551
+ const existingIssues = await getAllIssues();
552
+ const dependsOn = formatExistingDependencyIds(input.depends_on, existingIssues, issueNumber);
510
553
  const frontmatter = {
511
554
  title: input.title,
512
555
  priority,
@@ -515,6 +558,7 @@ async function createIssue(input) {
515
558
  labels: input.labels || undefined,
516
559
  status: "open",
517
560
  order: input.order || undefined,
561
+ depends_on: dependsOn || undefined,
518
562
  created: formatDate()
519
563
  };
520
564
  const body = input.body ?? `
@@ -540,6 +584,8 @@ async function updateIssue(id, input) {
540
584
  if (!issue) {
541
585
  throw new Error(`Issue not found: ${id}`);
542
586
  }
587
+ const allIssues = await getAllIssues();
588
+ const dependsOn = input.depends_on !== undefined ? formatExistingDependencyIds(input.depends_on, allIssues, issue.id) : undefined;
543
589
  const updatedFrontmatter = {
544
590
  ...issue.frontmatter,
545
591
  ...input.title && { title: input.title },
@@ -551,6 +597,9 @@ async function updateIssue(id, input) {
551
597
  },
552
598
  ...input.status && { status: input.status },
553
599
  ...input.order && { order: input.order },
600
+ ...input.depends_on !== undefined && {
601
+ depends_on: dependsOn || undefined
602
+ },
554
603
  updated: formatDate()
555
604
  };
556
605
  const updatedContent = input.body !== undefined ? `
@@ -2050,6 +2099,14 @@ function filterByQuery(issues, query) {
2050
2099
  if (issue.frontmatter.status !== statusValue) {
2051
2100
  return false;
2052
2101
  }
2102
+ } else if (statusValue === "unblocked") {
2103
+ if (!isIssueUnblocked(issue, issues)) {
2104
+ return false;
2105
+ }
2106
+ } else if (statusValue === "blocked") {
2107
+ if (issue.frontmatter.status !== "open" || isIssueUnblocked(issue, issues)) {
2108
+ return false;
2109
+ }
2053
2110
  }
2054
2111
  }
2055
2112
  if (parsed.qualifiers.priority) {
@@ -2137,12 +2194,13 @@ var compact = markdown([
2137
2194
  "",
2138
2195
  "## Essential workflow",
2139
2196
  "",
2140
- "- List work: `issy list` or `issy list --all`.",
2197
+ "- List work: `issy list`, `issy list --all`, or `issy list --unblocked`.",
2141
2198
  '- Search work: `issy search "query"` or `issy search "query" --all`.',
2142
2199
  "- Read before changing: `issy read <id>`.",
2143
2200
  "- Pick next work: `issy next`.",
2144
2201
  '- Create work: `issy create --title "..." --type bug|improvement --priority high|medium|low <position>`.',
2145
- "- Update work: `issy update <id> [options]`.",
2202
+ '- Create blocked work: `issy create --title "..." --depends-on 0001,0002 <position>`.',
2203
+ "- Update work: `issy update <id> [options]`, including `--depends-on <ids>` to replace blockers.",
2146
2204
  "- Close work: `issy close <id>` after the work is complete and verified.",
2147
2205
  "- Reopen work: `issy reopen <id> <position>`.",
2148
2206
  "",
@@ -2159,6 +2217,9 @@ var compact = markdown([
2159
2217
  "- When creating an issue and open issues already exist, include exactly one position flag: `--before <id>`, `--after <id>`, `--first`, or `--last`.",
2160
2218
  "- When reopening an issue and other open issues exist, include exactly one position flag.",
2161
2219
  "- Use dependency order: prerequisites first, dependent/user-facing work later. Use `--last` when placement is unclear.",
2220
+ "- Use `depends_on` / `--depends-on` when an issue cannot start until specific blocking issues are closed.",
2221
+ "- `issy list` shows a `Blk` column with open blocker counts; `-` means unblocked.",
2222
+ "- `issy list --unblocked` shows only open issues with no open blockers.",
2162
2223
  "",
2163
2224
  "## Closing",
2164
2225
  "",
@@ -2242,6 +2303,16 @@ var topics = [
2242
2303
  "- Updating an issue: position flags are optional and reposition the issue when provided.",
2243
2304
  "- Never provide more than one position flag.",
2244
2305
  "",
2306
+ "## Explicit blockers",
2307
+ "",
2308
+ "- Use `depends_on` when an issue cannot start until specific blocking issues are closed.",
2309
+ '- Create blocked work with `issy create --title "..." --depends-on 0001,0002 --last`.',
2310
+ "- Replace blockers with `issy update <id> --depends-on 0001,0003`.",
2311
+ '- Clear blockers with `issy update <id> --depends-on ""`.',
2312
+ "- Missing or malformed dependency IDs are ignored.",
2313
+ "- `issy list` shows a compact `Blk` column. `-` means unblocked; otherwise the value is the number of open blockers.",
2314
+ "- `issy list --unblocked` shows only open issues with no open blockers.",
2315
+ "",
2245
2316
  "## Choosing placement",
2246
2317
  "",
2247
2318
  "- Place prerequisites before dependent issues.",
@@ -2253,9 +2324,12 @@ var topics = [
2253
2324
  "## Useful commands",
2254
2325
  "",
2255
2326
  "- `issy list` shows open issues in roadmap order.",
2327
+ "- `issy list --unblocked` shows open issues with no open blockers.",
2256
2328
  "- `issy next` shows the first open issue in roadmap order.",
2257
2329
  '- `issy create --title "..." --last` appends a new issue.',
2330
+ '- `issy create --title "..." --depends-on 0001,0002 --last` creates an issue blocked by other issues.',
2258
2331
  "- `issy update <id> --before <other-id>` repositions an issue.",
2332
+ "- `issy update <id> --depends-on 0001,0003` replaces blockers.",
2259
2333
  "- `issy reopen <id> --after <other-id>` reopens and places a closed issue."
2260
2334
  ])
2261
2335
  },
@@ -2272,6 +2346,7 @@ var topics = [
2272
2346
  "",
2273
2347
  "- `issy list`: list open issues in roadmap order.",
2274
2348
  "- `issy list --all`: include closed issues.",
2349
+ "- `issy list --unblocked`: list open issues with no open blockers.",
2275
2350
  "- `issy list --priority high|medium|low`: filter by priority.",
2276
2351
  "- `issy list --scope small|medium|large`: filter by scope.",
2277
2352
  "- `issy list --type bug|improvement`: filter by type.",
@@ -2289,10 +2364,11 @@ var topics = [
2289
2364
  "",
2290
2365
  '- `issy create --title "Fix login bug" --type bug --priority high --after 0002`.',
2291
2366
  '- `issy create --title "Add dark mode" --type improvement --last --labels "ui, frontend"`.',
2367
+ '- `issy create --title "Add export" --depends-on 0001,0002 --last`.',
2292
2368
  '- `issy create --title "Urgent fix" --first`.',
2293
2369
  '- `issy create --title "Fix crash" --body "## Problem\\n\\nApp crashes on startup." --last`.',
2294
2370
  "",
2295
- "Create options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--before`, `--after`, `--first`, `--last`.",
2371
+ "Create options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--depends-on`, `--before`, `--after`, `--first`, `--last`.",
2296
2372
  "",
2297
2373
  "## Update",
2298
2374
  "",
@@ -2300,9 +2376,10 @@ var topics = [
2300
2376
  "- `issy update <id> --after 0003`.",
2301
2377
  "- `issy update <id> --first`.",
2302
2378
  '- `issy update <id> --labels "api, backend"`.',
2379
+ "- `issy update <id> --depends-on 0001,0003`.",
2303
2380
  '- `issy update <id> --body "## Problem\\n\\nUpdated description."`.',
2304
2381
  "",
2305
- "Update options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--before`, `--after`, `--first`, `--last`.",
2382
+ "Update options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--depends-on`, `--before`, `--after`, `--first`, `--last`.",
2306
2383
  "",
2307
2384
  "## Close and reopen",
2308
2385
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "issy",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "AI-native issue tracking. Markdown files in .issues/, managed by your coding assistant.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,8 +35,8 @@
35
35
  "lint": "biome check src bin"
36
36
  },
37
37
  "dependencies": {
38
- "@miketromba/issy-app": "^0.10.0",
39
- "@miketromba/issy-core": "^0.10.0",
38
+ "@miketromba/issy-app": "^0.10.1",
39
+ "@miketromba/issy-core": "^0.10.1",
40
40
  "update-notifier": "^7.3.1"
41
41
  }
42
42
  }