issy 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/cli.js +166 -47
- package/dist/main.js +131 -41
- package/package.json +3 -3
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,16 @@ 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
|
+
|
|
190
|
+
Roadmap placement is validated against open dependencies. An open issue must appear after every open issue it depends on, and `create`, `update`, or `reopen` fails if the requested position would put a blocked issue before one of its blockers.
|
|
191
|
+
|
|
178
192
|
### Hooks
|
|
179
193
|
|
|
180
194
|
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 +240,7 @@ scope: medium
|
|
|
226
240
|
type: bug
|
|
227
241
|
status: open
|
|
228
242
|
order: a0
|
|
243
|
+
depends_on: 0012, 0035
|
|
229
244
|
created: 2025-01-15T10:30:00
|
|
230
245
|
---
|
|
231
246
|
|
|
@@ -243,6 +258,7 @@ session isn't established, causing a redirect loop.
|
|
|
243
258
|
| `status` | `open`, `closed` |
|
|
244
259
|
| `labels` | comma-separated (optional) |
|
|
245
260
|
| `order` | fractional index key (managed by issy) |
|
|
261
|
+
| `depends_on` | comma-separated blocking issue IDs (optional) |
|
|
246
262
|
|
|
247
263
|
## Configuration
|
|
248
264
|
|
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,79 @@ 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
|
+
}
|
|
407
|
+
function compareIssuesByRoadmapOrder(a, b) {
|
|
408
|
+
const orderA = a.frontmatter.order;
|
|
409
|
+
const orderB = b.frontmatter.order;
|
|
410
|
+
if (orderA && orderB) {
|
|
411
|
+
if (orderA < orderB)
|
|
412
|
+
return -1;
|
|
413
|
+
if (orderA > orderB)
|
|
414
|
+
return 1;
|
|
415
|
+
}
|
|
416
|
+
if (orderA && !orderB)
|
|
417
|
+
return -1;
|
|
418
|
+
if (!orderA && orderB)
|
|
419
|
+
return 1;
|
|
420
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
421
|
+
}
|
|
422
|
+
function validateRoadmapDependencyOrder(issues) {
|
|
423
|
+
const openIssues = issues.filter((issue) => issue.frontmatter.status === "open").sort(compareIssuesByRoadmapOrder);
|
|
424
|
+
const positionById = new Map(openIssues.map((issue, index) => [issue.id, index]));
|
|
425
|
+
const violations = [];
|
|
426
|
+
for (const issue of openIssues) {
|
|
427
|
+
const issuePosition = positionById.get(issue.id);
|
|
428
|
+
if (issuePosition === undefined)
|
|
429
|
+
continue;
|
|
430
|
+
for (const dependency of getBlockingIssues(issue, issues)) {
|
|
431
|
+
const dependencyPosition = positionById.get(dependency.id);
|
|
432
|
+
if (dependencyPosition === undefined || dependencyPosition < issuePosition) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
violations.push(`#${issue.id} depends on #${dependency.id}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (violations.length > 0) {
|
|
439
|
+
throw new Error(`Roadmap dependency order invalid: ${violations.join("; ")}. Issues must be placed after all open issues they depend on.`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
366
442
|
function getIssueIdFromFilename(filename) {
|
|
367
443
|
const match = filename.match(/^(\d+)-/);
|
|
368
444
|
return match ? match[1] : filename.replace(".md", "");
|
|
@@ -419,17 +495,7 @@ async function getAllIssues() {
|
|
|
419
495
|
content: body
|
|
420
496
|
});
|
|
421
497
|
}
|
|
422
|
-
return issues.sort(
|
|
423
|
-
const orderA = a.frontmatter.order;
|
|
424
|
-
const orderB = b.frontmatter.order;
|
|
425
|
-
if (orderA && orderB)
|
|
426
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
427
|
-
if (orderA && !orderB)
|
|
428
|
-
return -1;
|
|
429
|
-
if (!orderA && orderB)
|
|
430
|
-
return 1;
|
|
431
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
432
|
-
});
|
|
498
|
+
return issues.sort(compareIssuesByRoadmapOrder);
|
|
433
499
|
}
|
|
434
500
|
async function getOpenIssuesByOrder() {
|
|
435
501
|
const allIssues = await getAllIssues();
|
|
@@ -496,6 +562,8 @@ async function createIssue(input) {
|
|
|
496
562
|
const issueNumber = await getNextIssueNumber();
|
|
497
563
|
const slug = createSlug(input.title);
|
|
498
564
|
const filename = `${issueNumber}-${slug}.md`;
|
|
565
|
+
const existingIssues = await getAllIssues();
|
|
566
|
+
const dependsOn = formatExistingDependencyIds(input.depends_on, existingIssues, issueNumber);
|
|
499
567
|
const frontmatter = {
|
|
500
568
|
title: input.title,
|
|
501
569
|
priority,
|
|
@@ -504,6 +572,7 @@ async function createIssue(input) {
|
|
|
504
572
|
labels: input.labels || undefined,
|
|
505
573
|
status: "open",
|
|
506
574
|
order: input.order || undefined,
|
|
575
|
+
depends_on: dependsOn || undefined,
|
|
507
576
|
created: formatDate()
|
|
508
577
|
};
|
|
509
578
|
const body = input.body ?? `
|
|
@@ -514,8 +583,7 @@ async function createIssue(input) {
|
|
|
514
583
|
const content = `${generateFrontmatter(frontmatter)}
|
|
515
584
|
${body}
|
|
516
585
|
`;
|
|
517
|
-
|
|
518
|
-
return {
|
|
586
|
+
const newIssue = {
|
|
519
587
|
id: issueNumber,
|
|
520
588
|
filename,
|
|
521
589
|
frontmatter,
|
|
@@ -523,12 +591,17 @@ ${body}
|
|
|
523
591
|
${body}
|
|
524
592
|
`
|
|
525
593
|
};
|
|
594
|
+
validateRoadmapDependencyOrder([...existingIssues, newIssue]);
|
|
595
|
+
await writeFile(join(getIssuesDir(), filename), content);
|
|
596
|
+
return newIssue;
|
|
526
597
|
}
|
|
527
598
|
async function updateIssue(id, input) {
|
|
528
599
|
const issue = await getIssue(id);
|
|
529
600
|
if (!issue) {
|
|
530
601
|
throw new Error(`Issue not found: ${id}`);
|
|
531
602
|
}
|
|
603
|
+
const allIssues = await getAllIssues();
|
|
604
|
+
const dependsOn = input.depends_on !== undefined ? formatExistingDependencyIds(input.depends_on, allIssues, issue.id) : undefined;
|
|
532
605
|
const updatedFrontmatter = {
|
|
533
606
|
...issue.frontmatter,
|
|
534
607
|
...input.title && { title: input.title },
|
|
@@ -540,6 +613,9 @@ async function updateIssue(id, input) {
|
|
|
540
613
|
},
|
|
541
614
|
...input.status && { status: input.status },
|
|
542
615
|
...input.order && { order: input.order },
|
|
616
|
+
...input.depends_on !== undefined && {
|
|
617
|
+
depends_on: dependsOn || undefined
|
|
618
|
+
},
|
|
543
619
|
updated: formatDate()
|
|
544
620
|
};
|
|
545
621
|
const updatedContent = input.body !== undefined ? `
|
|
@@ -547,12 +623,16 @@ ${input.body}
|
|
|
547
623
|
` : issue.content;
|
|
548
624
|
const content = `${generateFrontmatter(updatedFrontmatter)}
|
|
549
625
|
${updatedContent}`;
|
|
550
|
-
|
|
551
|
-
return {
|
|
626
|
+
const updatedIssue = {
|
|
552
627
|
...issue,
|
|
553
628
|
frontmatter: updatedFrontmatter,
|
|
554
629
|
content: updatedContent
|
|
555
630
|
};
|
|
631
|
+
if (input.order !== undefined || input.depends_on !== undefined || input.status === "open") {
|
|
632
|
+
validateRoadmapDependencyOrder(allIssues.map((current) => current.id === issue.id ? updatedIssue : current));
|
|
633
|
+
}
|
|
634
|
+
await writeFile(join(getIssuesDir(), issue.filename), content);
|
|
635
|
+
return updatedIssue;
|
|
556
636
|
}
|
|
557
637
|
async function closeIssue(id) {
|
|
558
638
|
return updateIssue(id, { status: "closed" });
|
|
@@ -1953,17 +2033,7 @@ function createSearchIndex(issues) {
|
|
|
1953
2033
|
function sortIssues(issues, sortBy) {
|
|
1954
2034
|
const sortOption = sortBy.toLowerCase();
|
|
1955
2035
|
if (sortOption === "roadmap") {
|
|
1956
|
-
issues.sort(
|
|
1957
|
-
const orderA = a.frontmatter.order;
|
|
1958
|
-
const orderB = b.frontmatter.order;
|
|
1959
|
-
if (orderA && orderB)
|
|
1960
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
1961
|
-
if (orderA && !orderB)
|
|
1962
|
-
return -1;
|
|
1963
|
-
if (!orderA && orderB)
|
|
1964
|
-
return 1;
|
|
1965
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1966
|
-
});
|
|
2036
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
1967
2037
|
} else if (sortOption === "priority") {
|
|
1968
2038
|
const priorityOrder = {
|
|
1969
2039
|
high: 0,
|
|
@@ -2017,17 +2087,7 @@ function sortIssues(issues, sortBy) {
|
|
|
2017
2087
|
} else if (sortOption === "id") {
|
|
2018
2088
|
issues.sort((a, b) => b.id.localeCompare(a.id));
|
|
2019
2089
|
} else {
|
|
2020
|
-
issues.sort(
|
|
2021
|
-
const orderA = a.frontmatter.order;
|
|
2022
|
-
const orderB = b.frontmatter.order;
|
|
2023
|
-
if (orderA && orderB)
|
|
2024
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
2025
|
-
if (orderA && !orderB)
|
|
2026
|
-
return -1;
|
|
2027
|
-
if (!orderA && orderB)
|
|
2028
|
-
return 1;
|
|
2029
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2030
|
-
});
|
|
2090
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
2031
2091
|
}
|
|
2032
2092
|
}
|
|
2033
2093
|
function filterByQuery(issues, query) {
|
|
@@ -2039,6 +2099,14 @@ function filterByQuery(issues, query) {
|
|
|
2039
2099
|
if (issue.frontmatter.status !== statusValue) {
|
|
2040
2100
|
return false;
|
|
2041
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
|
+
}
|
|
2042
2110
|
}
|
|
2043
2111
|
}
|
|
2044
2112
|
if (parsed.qualifiers.priority) {
|
|
@@ -2126,7 +2194,23 @@ function typeSymbol(type) {
|
|
|
2126
2194
|
}
|
|
2127
2195
|
function formatIssueRow(issue) {
|
|
2128
2196
|
const status = issue.frontmatter.status === "open" ? "OPEN " : "CLOSED";
|
|
2129
|
-
|
|
2197
|
+
const blocked = issue.blockers > 0 ? String(issue.blockers) : "-";
|
|
2198
|
+
return ` ${issue.id} ${prioritySymbol(issue.frontmatter.priority)} ${typeSymbol(issue.frontmatter.type)} ${status} ${blocked.padEnd(3)} ${issue.frontmatter.title}`;
|
|
2199
|
+
}
|
|
2200
|
+
function printDependsOn(issue, issues) {
|
|
2201
|
+
const dependencyIssues = getDependencyIssues(issue, issues);
|
|
2202
|
+
if (dependencyIssues.length === 0)
|
|
2203
|
+
return;
|
|
2204
|
+
console.log(` Depends on: ${dependencyIssues.map((i) => `#${i.id}`).join(", ")}`);
|
|
2205
|
+
}
|
|
2206
|
+
function getDependsOnArg(values) {
|
|
2207
|
+
const value = values["depends-on"];
|
|
2208
|
+
return typeof value === "string" ? value : undefined;
|
|
2209
|
+
}
|
|
2210
|
+
async function resolveDependsOn(value, excludeId) {
|
|
2211
|
+
if (value === undefined)
|
|
2212
|
+
return;
|
|
2213
|
+
return formatExistingDependencyIds(value, await getAllIssues(), excludeId);
|
|
2130
2214
|
}
|
|
2131
2215
|
async function resolvePosition(opts) {
|
|
2132
2216
|
const openIssues = await getOpenIssuesByOrder();
|
|
@@ -2158,7 +2242,7 @@ function hasHelpFlag(commandArgs) {
|
|
|
2158
2242
|
async function listIssues(options) {
|
|
2159
2243
|
const allIssues = await getAllIssues();
|
|
2160
2244
|
const queryParts = [];
|
|
2161
|
-
if (!options.all)
|
|
2245
|
+
if (!options.all && !options.unblocked)
|
|
2162
2246
|
queryParts.push("is:open");
|
|
2163
2247
|
if (options.priority)
|
|
2164
2248
|
queryParts.push(`priority:${options.priority}`);
|
|
@@ -2170,17 +2254,23 @@ async function listIssues(options) {
|
|
|
2170
2254
|
queryParts.push(`sort:${options.sort}`);
|
|
2171
2255
|
if (options.search)
|
|
2172
2256
|
queryParts.push(options.search);
|
|
2173
|
-
const query = queryParts.join(" ")
|
|
2174
|
-
|
|
2257
|
+
const query = queryParts.join(" ");
|
|
2258
|
+
let issues = query ? filterByQuery(allIssues, query) : allIssues;
|
|
2259
|
+
if (options.unblocked) {
|
|
2260
|
+
issues = issues.filter((issue) => isIssueUnblocked(issue, allIssues));
|
|
2261
|
+
}
|
|
2175
2262
|
if (issues.length === 0) {
|
|
2176
2263
|
console.log("No issues found.");
|
|
2177
2264
|
return;
|
|
2178
2265
|
}
|
|
2179
2266
|
console.log(`
|
|
2180
|
-
ID Pri Type Status
|
|
2267
|
+
ID Pri Type Status Blk Title`);
|
|
2181
2268
|
console.log(` ${"-".repeat(100)}`);
|
|
2182
2269
|
for (const issue of issues) {
|
|
2183
|
-
console.log(formatIssueRow(
|
|
2270
|
+
console.log(formatIssueRow({
|
|
2271
|
+
...issue,
|
|
2272
|
+
blockers: getBlockingIssues(issue, allIssues).length
|
|
2273
|
+
}));
|
|
2184
2274
|
}
|
|
2185
2275
|
console.log(`
|
|
2186
2276
|
Total: ${issues.length} issue(s)
|
|
@@ -2188,6 +2278,7 @@ async function listIssues(options) {
|
|
|
2188
2278
|
}
|
|
2189
2279
|
async function readIssue(id) {
|
|
2190
2280
|
const issue = await getIssue(id);
|
|
2281
|
+
const allIssues = await getAllIssues();
|
|
2191
2282
|
if (!issue) {
|
|
2192
2283
|
console.error(`Issue not found: ${id}`);
|
|
2193
2284
|
process.exit(1);
|
|
@@ -2206,6 +2297,7 @@ ${"=".repeat(70)}`);
|
|
|
2206
2297
|
if (issue.frontmatter.labels) {
|
|
2207
2298
|
console.log(` Labels: ${issue.frontmatter.labels}`);
|
|
2208
2299
|
}
|
|
2300
|
+
printDependsOn(issue, allIssues);
|
|
2209
2301
|
if (issue.frontmatter.order) {
|
|
2210
2302
|
console.log(` Order: ${issue.frontmatter.order}`);
|
|
2211
2303
|
}
|
|
@@ -2228,10 +2320,13 @@ async function searchIssuesCommand(query, options) {
|
|
|
2228
2320
|
console.log(`
|
|
2229
2321
|
Search results for "${query}":`);
|
|
2230
2322
|
console.log(`
|
|
2231
|
-
ID Pri Type Status
|
|
2323
|
+
ID Pri Type Status Blk Title`);
|
|
2232
2324
|
console.log(` ${"-".repeat(100)}`);
|
|
2233
2325
|
for (const issue of issues) {
|
|
2234
|
-
console.log(formatIssueRow(
|
|
2326
|
+
console.log(formatIssueRow({
|
|
2327
|
+
...issue,
|
|
2328
|
+
blockers: getBlockingIssues(issue, allIssues).length
|
|
2329
|
+
}));
|
|
2235
2330
|
}
|
|
2236
2331
|
console.log(`
|
|
2237
2332
|
Found: ${issues.length} issue(s)
|
|
@@ -2257,6 +2352,7 @@ async function createIssueCommand(options) {
|
|
|
2257
2352
|
scope: options.scope,
|
|
2258
2353
|
type: options.type,
|
|
2259
2354
|
labels: options.labels,
|
|
2355
|
+
depends_on: await resolveDependsOn(options.dependsOn) || undefined,
|
|
2260
2356
|
order
|
|
2261
2357
|
};
|
|
2262
2358
|
const issue = await createIssue(input);
|
|
@@ -2293,6 +2389,9 @@ async function updateIssueCommand(id, options) {
|
|
|
2293
2389
|
scope: options.scope,
|
|
2294
2390
|
type: options.type,
|
|
2295
2391
|
labels: options.labels,
|
|
2392
|
+
...options.dependsOn !== undefined && {
|
|
2393
|
+
depends_on: await resolveDependsOn(options.dependsOn, id) || ""
|
|
2394
|
+
},
|
|
2296
2395
|
order
|
|
2297
2396
|
});
|
|
2298
2397
|
console.log(`Updated issue: ${issue.filename}`);
|
|
@@ -2341,6 +2440,7 @@ async function reopenIssueCommand(id, options) {
|
|
|
2341
2440
|
}
|
|
2342
2441
|
async function nextIssueCommand() {
|
|
2343
2442
|
const issue = await getNextIssue();
|
|
2443
|
+
const allIssues = await getAllIssues();
|
|
2344
2444
|
if (!issue) {
|
|
2345
2445
|
console.log("No open issues.");
|
|
2346
2446
|
return;
|
|
@@ -2359,6 +2459,7 @@ ${"=".repeat(70)}`);
|
|
|
2359
2459
|
if (issue.frontmatter.labels) {
|
|
2360
2460
|
console.log(` Labels: ${issue.frontmatter.labels}`);
|
|
2361
2461
|
}
|
|
2462
|
+
printDependsOn(issue, allIssues);
|
|
2362
2463
|
if (issue.frontmatter.order) {
|
|
2363
2464
|
console.log(` Order: ${issue.frontmatter.order}`);
|
|
2364
2465
|
}
|
|
@@ -2386,6 +2487,7 @@ Options:
|
|
|
2386
2487
|
Commands:
|
|
2387
2488
|
list List all open issues (roadmap order)
|
|
2388
2489
|
--all, -a Include closed issues
|
|
2490
|
+
--unblocked Only show open issues with no open blockers
|
|
2389
2491
|
--priority, -p <p> Filter by priority (high, medium, low)
|
|
2390
2492
|
--scope <s> Filter by scope (small, medium, large)
|
|
2391
2493
|
--type, -t <t> Filter by type (bug, improvement)
|
|
@@ -2410,6 +2512,7 @@ Commands:
|
|
|
2410
2512
|
--scope <s> Scope (small, medium, large)
|
|
2411
2513
|
--type <t> Type (bug, improvement)
|
|
2412
2514
|
--labels, -l <l> Comma-separated labels
|
|
2515
|
+
--depends-on <ids> Comma-separated blocking issue IDs
|
|
2413
2516
|
--before <id> Insert before this issue in roadmap
|
|
2414
2517
|
--after <id> Insert after this issue in roadmap
|
|
2415
2518
|
--first Insert at the beginning of the roadmap
|
|
@@ -2422,6 +2525,7 @@ Commands:
|
|
|
2422
2525
|
--scope <s> New scope
|
|
2423
2526
|
--type <t> New type
|
|
2424
2527
|
--labels, -l <l> New labels
|
|
2528
|
+
--depends-on <ids> Replace blocking issue IDs (empty clears)
|
|
2425
2529
|
--before <id> Move before this issue in roadmap
|
|
2426
2530
|
--after <id> Move after this issue in roadmap
|
|
2427
2531
|
--first Move to the beginning of the roadmap
|
|
@@ -2439,6 +2543,7 @@ Commands:
|
|
|
2439
2543
|
|
|
2440
2544
|
Examples:
|
|
2441
2545
|
issy list
|
|
2546
|
+
issy list --unblocked
|
|
2442
2547
|
issy list --priority high --type bug
|
|
2443
2548
|
issy next
|
|
2444
2549
|
issy learn
|
|
@@ -2462,6 +2567,7 @@ List all open issues in roadmap order.
|
|
|
2462
2567
|
|
|
2463
2568
|
Options:
|
|
2464
2569
|
--all, -a Include closed issues
|
|
2570
|
+
--unblocked Only show open issues with no open blockers
|
|
2465
2571
|
--priority, -p <p> Filter by priority (high, medium, low)
|
|
2466
2572
|
--scope <s> Filter by scope (small, medium, large)
|
|
2467
2573
|
--type, -t <t> Filter by type (bug, improvement)
|
|
@@ -2474,6 +2580,7 @@ Options:
|
|
|
2474
2580
|
args: args.slice(1),
|
|
2475
2581
|
options: {
|
|
2476
2582
|
all: { type: "boolean", short: "a" },
|
|
2583
|
+
unblocked: { type: "boolean" },
|
|
2477
2584
|
priority: { type: "string", short: "p" },
|
|
2478
2585
|
scope: { type: "string" },
|
|
2479
2586
|
type: { type: "string", short: "t" },
|
|
@@ -2551,6 +2658,7 @@ Options:
|
|
|
2551
2658
|
--scope <s> Scope: small, medium, large
|
|
2552
2659
|
--type <t> Type: bug, improvement (default: improvement)
|
|
2553
2660
|
--labels, -l <l> Comma-separated labels
|
|
2661
|
+
--depends-on <ids> Comma-separated blocking issue IDs
|
|
2554
2662
|
--before <id> Insert before this issue in roadmap
|
|
2555
2663
|
--after <id> Insert after this issue in roadmap
|
|
2556
2664
|
--first Insert at the beginning of the roadmap
|
|
@@ -2558,6 +2666,7 @@ Options:
|
|
|
2558
2666
|
|
|
2559
2667
|
Examples:
|
|
2560
2668
|
issy create --title "Fix login bug" --type bug --priority high --after 0002
|
|
2669
|
+
issy create --title "Add export" --depends-on 0001,0002 --last
|
|
2561
2670
|
issy create --title "Add dark mode" --last
|
|
2562
2671
|
`);
|
|
2563
2672
|
return;
|
|
@@ -2571,6 +2680,7 @@ Examples:
|
|
|
2571
2680
|
scope: { type: "string" },
|
|
2572
2681
|
type: { type: "string" },
|
|
2573
2682
|
labels: { type: "string", short: "l" },
|
|
2683
|
+
"depends-on": { type: "string" },
|
|
2574
2684
|
before: { type: "string" },
|
|
2575
2685
|
after: { type: "string" },
|
|
2576
2686
|
first: { type: "boolean" },
|
|
@@ -2578,7 +2688,10 @@ Examples:
|
|
|
2578
2688
|
},
|
|
2579
2689
|
allowPositionals: true
|
|
2580
2690
|
});
|
|
2581
|
-
await createIssueCommand(
|
|
2691
|
+
await createIssueCommand({
|
|
2692
|
+
...values,
|
|
2693
|
+
dependsOn: getDependsOnArg(values)
|
|
2694
|
+
});
|
|
2582
2695
|
break;
|
|
2583
2696
|
}
|
|
2584
2697
|
case "update": {
|
|
@@ -2594,6 +2707,7 @@ Options:
|
|
|
2594
2707
|
--scope <s> New scope: small, medium, large
|
|
2595
2708
|
--type <t> New type: bug, improvement
|
|
2596
2709
|
--labels, -l <l> New labels (comma-separated)
|
|
2710
|
+
--depends-on <ids> Replace blocking issue IDs (empty clears)
|
|
2597
2711
|
--before <id> Move before this issue in roadmap
|
|
2598
2712
|
--after <id> Move after this issue in roadmap
|
|
2599
2713
|
--first Move to the beginning of the roadmap
|
|
@@ -2601,6 +2715,7 @@ Options:
|
|
|
2601
2715
|
|
|
2602
2716
|
Examples:
|
|
2603
2717
|
issy update 0001 --priority low --after 0003
|
|
2718
|
+
issy update 0002 --depends-on 0001,0003
|
|
2604
2719
|
issy update 0002 --title "Renamed issue" --first
|
|
2605
2720
|
`);
|
|
2606
2721
|
return;
|
|
@@ -2619,6 +2734,7 @@ Examples:
|
|
|
2619
2734
|
scope: { type: "string" },
|
|
2620
2735
|
type: { type: "string" },
|
|
2621
2736
|
labels: { type: "string", short: "l" },
|
|
2737
|
+
"depends-on": { type: "string" },
|
|
2622
2738
|
before: { type: "string" },
|
|
2623
2739
|
after: { type: "string" },
|
|
2624
2740
|
first: { type: "boolean" },
|
|
@@ -2626,7 +2742,10 @@ Examples:
|
|
|
2626
2742
|
},
|
|
2627
2743
|
allowPositionals: true
|
|
2628
2744
|
});
|
|
2629
|
-
await updateIssueCommand(id,
|
|
2745
|
+
await updateIssueCommand(id, {
|
|
2746
|
+
...values,
|
|
2747
|
+
dependsOn: getDependsOnArg(values)
|
|
2748
|
+
});
|
|
2630
2749
|
break;
|
|
2631
2750
|
}
|
|
2632
2751
|
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,79 @@ 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
|
+
}
|
|
418
|
+
function compareIssuesByRoadmapOrder(a, b) {
|
|
419
|
+
const orderA = a.frontmatter.order;
|
|
420
|
+
const orderB = b.frontmatter.order;
|
|
421
|
+
if (orderA && orderB) {
|
|
422
|
+
if (orderA < orderB)
|
|
423
|
+
return -1;
|
|
424
|
+
if (orderA > orderB)
|
|
425
|
+
return 1;
|
|
426
|
+
}
|
|
427
|
+
if (orderA && !orderB)
|
|
428
|
+
return -1;
|
|
429
|
+
if (!orderA && orderB)
|
|
430
|
+
return 1;
|
|
431
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
432
|
+
}
|
|
433
|
+
function validateRoadmapDependencyOrder(issues) {
|
|
434
|
+
const openIssues = issues.filter((issue) => issue.frontmatter.status === "open").sort(compareIssuesByRoadmapOrder);
|
|
435
|
+
const positionById = new Map(openIssues.map((issue, index) => [issue.id, index]));
|
|
436
|
+
const violations = [];
|
|
437
|
+
for (const issue of openIssues) {
|
|
438
|
+
const issuePosition = positionById.get(issue.id);
|
|
439
|
+
if (issuePosition === undefined)
|
|
440
|
+
continue;
|
|
441
|
+
for (const dependency of getBlockingIssues(issue, issues)) {
|
|
442
|
+
const dependencyPosition = positionById.get(dependency.id);
|
|
443
|
+
if (dependencyPosition === undefined || dependencyPosition < issuePosition) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
violations.push(`#${issue.id} depends on #${dependency.id}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (violations.length > 0) {
|
|
450
|
+
throw new Error(`Roadmap dependency order invalid: ${violations.join("; ")}. Issues must be placed after all open issues they depend on.`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
377
453
|
function getIssueIdFromFilename(filename) {
|
|
378
454
|
const match = filename.match(/^(\d+)-/);
|
|
379
455
|
return match ? match[1] : filename.replace(".md", "");
|
|
@@ -430,17 +506,7 @@ async function getAllIssues() {
|
|
|
430
506
|
content: body
|
|
431
507
|
});
|
|
432
508
|
}
|
|
433
|
-
return issues.sort(
|
|
434
|
-
const orderA = a.frontmatter.order;
|
|
435
|
-
const orderB = b.frontmatter.order;
|
|
436
|
-
if (orderA && orderB)
|
|
437
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
438
|
-
if (orderA && !orderB)
|
|
439
|
-
return -1;
|
|
440
|
-
if (!orderA && orderB)
|
|
441
|
-
return 1;
|
|
442
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
443
|
-
});
|
|
509
|
+
return issues.sort(compareIssuesByRoadmapOrder);
|
|
444
510
|
}
|
|
445
511
|
async function getOpenIssuesByOrder() {
|
|
446
512
|
const allIssues = await getAllIssues();
|
|
@@ -507,6 +573,8 @@ async function createIssue(input) {
|
|
|
507
573
|
const issueNumber = await getNextIssueNumber();
|
|
508
574
|
const slug = createSlug(input.title);
|
|
509
575
|
const filename = `${issueNumber}-${slug}.md`;
|
|
576
|
+
const existingIssues = await getAllIssues();
|
|
577
|
+
const dependsOn = formatExistingDependencyIds(input.depends_on, existingIssues, issueNumber);
|
|
510
578
|
const frontmatter = {
|
|
511
579
|
title: input.title,
|
|
512
580
|
priority,
|
|
@@ -515,6 +583,7 @@ async function createIssue(input) {
|
|
|
515
583
|
labels: input.labels || undefined,
|
|
516
584
|
status: "open",
|
|
517
585
|
order: input.order || undefined,
|
|
586
|
+
depends_on: dependsOn || undefined,
|
|
518
587
|
created: formatDate()
|
|
519
588
|
};
|
|
520
589
|
const body = input.body ?? `
|
|
@@ -525,8 +594,7 @@ async function createIssue(input) {
|
|
|
525
594
|
const content = `${generateFrontmatter(frontmatter)}
|
|
526
595
|
${body}
|
|
527
596
|
`;
|
|
528
|
-
|
|
529
|
-
return {
|
|
597
|
+
const newIssue = {
|
|
530
598
|
id: issueNumber,
|
|
531
599
|
filename,
|
|
532
600
|
frontmatter,
|
|
@@ -534,12 +602,17 @@ ${body}
|
|
|
534
602
|
${body}
|
|
535
603
|
`
|
|
536
604
|
};
|
|
605
|
+
validateRoadmapDependencyOrder([...existingIssues, newIssue]);
|
|
606
|
+
await writeFile(join(getIssuesDir(), filename), content);
|
|
607
|
+
return newIssue;
|
|
537
608
|
}
|
|
538
609
|
async function updateIssue(id, input) {
|
|
539
610
|
const issue = await getIssue(id);
|
|
540
611
|
if (!issue) {
|
|
541
612
|
throw new Error(`Issue not found: ${id}`);
|
|
542
613
|
}
|
|
614
|
+
const allIssues = await getAllIssues();
|
|
615
|
+
const dependsOn = input.depends_on !== undefined ? formatExistingDependencyIds(input.depends_on, allIssues, issue.id) : undefined;
|
|
543
616
|
const updatedFrontmatter = {
|
|
544
617
|
...issue.frontmatter,
|
|
545
618
|
...input.title && { title: input.title },
|
|
@@ -551,6 +624,9 @@ async function updateIssue(id, input) {
|
|
|
551
624
|
},
|
|
552
625
|
...input.status && { status: input.status },
|
|
553
626
|
...input.order && { order: input.order },
|
|
627
|
+
...input.depends_on !== undefined && {
|
|
628
|
+
depends_on: dependsOn || undefined
|
|
629
|
+
},
|
|
554
630
|
updated: formatDate()
|
|
555
631
|
};
|
|
556
632
|
const updatedContent = input.body !== undefined ? `
|
|
@@ -558,12 +634,16 @@ ${input.body}
|
|
|
558
634
|
` : issue.content;
|
|
559
635
|
const content = `${generateFrontmatter(updatedFrontmatter)}
|
|
560
636
|
${updatedContent}`;
|
|
561
|
-
|
|
562
|
-
return {
|
|
637
|
+
const updatedIssue = {
|
|
563
638
|
...issue,
|
|
564
639
|
frontmatter: updatedFrontmatter,
|
|
565
640
|
content: updatedContent
|
|
566
641
|
};
|
|
642
|
+
if (input.order !== undefined || input.depends_on !== undefined || input.status === "open") {
|
|
643
|
+
validateRoadmapDependencyOrder(allIssues.map((current) => current.id === issue.id ? updatedIssue : current));
|
|
644
|
+
}
|
|
645
|
+
await writeFile(join(getIssuesDir(), issue.filename), content);
|
|
646
|
+
return updatedIssue;
|
|
567
647
|
}
|
|
568
648
|
async function closeIssue(id) {
|
|
569
649
|
return updateIssue(id, { status: "closed" });
|
|
@@ -1964,17 +2044,7 @@ function createSearchIndex(issues) {
|
|
|
1964
2044
|
function sortIssues(issues, sortBy) {
|
|
1965
2045
|
const sortOption = sortBy.toLowerCase();
|
|
1966
2046
|
if (sortOption === "roadmap") {
|
|
1967
|
-
issues.sort(
|
|
1968
|
-
const orderA = a.frontmatter.order;
|
|
1969
|
-
const orderB = b.frontmatter.order;
|
|
1970
|
-
if (orderA && orderB)
|
|
1971
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
1972
|
-
if (orderA && !orderB)
|
|
1973
|
-
return -1;
|
|
1974
|
-
if (!orderA && orderB)
|
|
1975
|
-
return 1;
|
|
1976
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
1977
|
-
});
|
|
2047
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
1978
2048
|
} else if (sortOption === "priority") {
|
|
1979
2049
|
const priorityOrder = {
|
|
1980
2050
|
high: 0,
|
|
@@ -2028,17 +2098,7 @@ function sortIssues(issues, sortBy) {
|
|
|
2028
2098
|
} else if (sortOption === "id") {
|
|
2029
2099
|
issues.sort((a, b) => b.id.localeCompare(a.id));
|
|
2030
2100
|
} else {
|
|
2031
|
-
issues.sort(
|
|
2032
|
-
const orderA = a.frontmatter.order;
|
|
2033
|
-
const orderB = b.frontmatter.order;
|
|
2034
|
-
if (orderA && orderB)
|
|
2035
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
2036
|
-
if (orderA && !orderB)
|
|
2037
|
-
return -1;
|
|
2038
|
-
if (!orderA && orderB)
|
|
2039
|
-
return 1;
|
|
2040
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2041
|
-
});
|
|
2101
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
2042
2102
|
}
|
|
2043
2103
|
}
|
|
2044
2104
|
function filterByQuery(issues, query) {
|
|
@@ -2050,6 +2110,14 @@ function filterByQuery(issues, query) {
|
|
|
2050
2110
|
if (issue.frontmatter.status !== statusValue) {
|
|
2051
2111
|
return false;
|
|
2052
2112
|
}
|
|
2113
|
+
} else if (statusValue === "unblocked") {
|
|
2114
|
+
if (!isIssueUnblocked(issue, issues)) {
|
|
2115
|
+
return false;
|
|
2116
|
+
}
|
|
2117
|
+
} else if (statusValue === "blocked") {
|
|
2118
|
+
if (issue.frontmatter.status !== "open" || isIssueUnblocked(issue, issues)) {
|
|
2119
|
+
return false;
|
|
2120
|
+
}
|
|
2053
2121
|
}
|
|
2054
2122
|
}
|
|
2055
2123
|
if (parsed.qualifiers.priority) {
|
|
@@ -2137,12 +2205,13 @@ var compact = markdown([
|
|
|
2137
2205
|
"",
|
|
2138
2206
|
"## Essential workflow",
|
|
2139
2207
|
"",
|
|
2140
|
-
"- List work: `issy list` or `issy list --
|
|
2208
|
+
"- List work: `issy list`, `issy list --all`, or `issy list --unblocked`.",
|
|
2141
2209
|
'- Search work: `issy search "query"` or `issy search "query" --all`.',
|
|
2142
2210
|
"- Read before changing: `issy read <id>`.",
|
|
2143
2211
|
"- Pick next work: `issy next`.",
|
|
2144
2212
|
'- Create work: `issy create --title "..." --type bug|improvement --priority high|medium|low <position>`.',
|
|
2145
|
-
|
|
2213
|
+
'- Create blocked work: `issy create --title "..." --depends-on 0001,0002 <position>`.',
|
|
2214
|
+
"- Update work: `issy update <id> [options]`, including `--depends-on <ids>` to replace blockers.",
|
|
2146
2215
|
"- Close work: `issy close <id>` after the work is complete and verified.",
|
|
2147
2216
|
"- Reopen work: `issy reopen <id> <position>`.",
|
|
2148
2217
|
"",
|
|
@@ -2159,6 +2228,10 @@ var compact = markdown([
|
|
|
2159
2228
|
"- When creating an issue and open issues already exist, include exactly one position flag: `--before <id>`, `--after <id>`, `--first`, or `--last`.",
|
|
2160
2229
|
"- When reopening an issue and other open issues exist, include exactly one position flag.",
|
|
2161
2230
|
"- Use dependency order: prerequisites first, dependent/user-facing work later. Use `--last` when placement is unclear.",
|
|
2231
|
+
"- Use `depends_on` / `--depends-on` when an issue cannot start until specific blocking issues are closed.",
|
|
2232
|
+
"- Roadmap placement is enforced against open dependencies: an open issue cannot be placed before an open issue it depends on.",
|
|
2233
|
+
"- `issy list` shows a `Blk` column with open blocker counts; `-` means unblocked.",
|
|
2234
|
+
"- `issy list --unblocked` shows only open issues with no open blockers.",
|
|
2162
2235
|
"",
|
|
2163
2236
|
"## Closing",
|
|
2164
2237
|
"",
|
|
@@ -2242,6 +2315,17 @@ var topics = [
|
|
|
2242
2315
|
"- Updating an issue: position flags are optional and reposition the issue when provided.",
|
|
2243
2316
|
"- Never provide more than one position flag.",
|
|
2244
2317
|
"",
|
|
2318
|
+
"## Explicit blockers",
|
|
2319
|
+
"",
|
|
2320
|
+
"- Use `depends_on` when an issue cannot start until specific blocking issues are closed.",
|
|
2321
|
+
'- Create blocked work with `issy create --title "..." --depends-on 0001,0002 --last`.',
|
|
2322
|
+
"- Replace blockers with `issy update <id> --depends-on 0001,0003`.",
|
|
2323
|
+
'- Clear blockers with `issy update <id> --depends-on ""`.',
|
|
2324
|
+
"- Missing or malformed dependency IDs are ignored.",
|
|
2325
|
+
"- Roadmap placement is enforced against open dependencies: an open issue cannot be placed before an open issue it depends on, and a blocker cannot be moved after open issues that depend on it.",
|
|
2326
|
+
"- `issy list` shows a compact `Blk` column. `-` means unblocked; otherwise the value is the number of open blockers.",
|
|
2327
|
+
"- `issy list --unblocked` shows only open issues with no open blockers.",
|
|
2328
|
+
"",
|
|
2245
2329
|
"## Choosing placement",
|
|
2246
2330
|
"",
|
|
2247
2331
|
"- Place prerequisites before dependent issues.",
|
|
@@ -2253,9 +2337,12 @@ var topics = [
|
|
|
2253
2337
|
"## Useful commands",
|
|
2254
2338
|
"",
|
|
2255
2339
|
"- `issy list` shows open issues in roadmap order.",
|
|
2340
|
+
"- `issy list --unblocked` shows open issues with no open blockers.",
|
|
2256
2341
|
"- `issy next` shows the first open issue in roadmap order.",
|
|
2257
2342
|
'- `issy create --title "..." --last` appends a new issue.',
|
|
2343
|
+
'- `issy create --title "..." --depends-on 0001,0002 --last` creates an issue blocked by other issues.',
|
|
2258
2344
|
"- `issy update <id> --before <other-id>` repositions an issue.",
|
|
2345
|
+
"- `issy update <id> --depends-on 0001,0003` replaces blockers.",
|
|
2259
2346
|
"- `issy reopen <id> --after <other-id>` reopens and places a closed issue."
|
|
2260
2347
|
])
|
|
2261
2348
|
},
|
|
@@ -2272,6 +2359,7 @@ var topics = [
|
|
|
2272
2359
|
"",
|
|
2273
2360
|
"- `issy list`: list open issues in roadmap order.",
|
|
2274
2361
|
"- `issy list --all`: include closed issues.",
|
|
2362
|
+
"- `issy list --unblocked`: list open issues with no open blockers.",
|
|
2275
2363
|
"- `issy list --priority high|medium|low`: filter by priority.",
|
|
2276
2364
|
"- `issy list --scope small|medium|large`: filter by scope.",
|
|
2277
2365
|
"- `issy list --type bug|improvement`: filter by type.",
|
|
@@ -2289,10 +2377,11 @@ var topics = [
|
|
|
2289
2377
|
"",
|
|
2290
2378
|
'- `issy create --title "Fix login bug" --type bug --priority high --after 0002`.',
|
|
2291
2379
|
'- `issy create --title "Add dark mode" --type improvement --last --labels "ui, frontend"`.',
|
|
2380
|
+
'- `issy create --title "Add export" --depends-on 0001,0002 --last`.',
|
|
2292
2381
|
'- `issy create --title "Urgent fix" --first`.',
|
|
2293
2382
|
'- `issy create --title "Fix crash" --body "## Problem\\n\\nApp crashes on startup." --last`.',
|
|
2294
2383
|
"",
|
|
2295
|
-
"Create options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--before`, `--after`, `--first`, `--last`.",
|
|
2384
|
+
"Create options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--depends-on`, `--before`, `--after`, `--first`, `--last`.",
|
|
2296
2385
|
"",
|
|
2297
2386
|
"## Update",
|
|
2298
2387
|
"",
|
|
@@ -2300,9 +2389,10 @@ var topics = [
|
|
|
2300
2389
|
"- `issy update <id> --after 0003`.",
|
|
2301
2390
|
"- `issy update <id> --first`.",
|
|
2302
2391
|
'- `issy update <id> --labels "api, backend"`.',
|
|
2392
|
+
"- `issy update <id> --depends-on 0001,0003`.",
|
|
2303
2393
|
'- `issy update <id> --body "## Problem\\n\\nUpdated description."`.',
|
|
2304
2394
|
"",
|
|
2305
|
-
"Update options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--before`, `--after`, `--first`, `--last`.",
|
|
2395
|
+
"Update options: `--title`, `--body`, `--priority`, `--scope`, `--type`, `--labels`, `--depends-on`, `--before`, `--after`, `--first`, `--last`.",
|
|
2306
2396
|
"",
|
|
2307
2397
|
"## Close and reopen",
|
|
2308
2398
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "issy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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.
|
|
39
|
-
"@miketromba/issy-core": "^0.
|
|
38
|
+
"@miketromba/issy-app": "^0.11.0",
|
|
39
|
+
"@miketromba/issy-core": "^0.11.0",
|
|
40
40
|
"update-notifier": "^7.3.1"
|
|
41
41
|
}
|
|
42
42
|
}
|