issy 0.10.1 → 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 +2 -0
- package/dist/cli.js +48 -37
- package/dist/main.js +50 -37
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -187,6 +187,8 @@ Issues can declare blockers with `depends_on`:
|
|
|
187
187
|
depends_on: 0012, 0035
|
|
188
188
|
```
|
|
189
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
|
+
|
|
190
192
|
### Hooks
|
|
191
193
|
|
|
192
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.
|
package/dist/cli.js
CHANGED
|
@@ -404,6 +404,41 @@ function getBlockingIssues(issue, issues) {
|
|
|
404
404
|
function isIssueUnblocked(issue, issues) {
|
|
405
405
|
return issue.frontmatter.status === "open" && getBlockingIssues(issue, issues).length === 0;
|
|
406
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
|
+
}
|
|
407
442
|
function getIssueIdFromFilename(filename) {
|
|
408
443
|
const match = filename.match(/^(\d+)-/);
|
|
409
444
|
return match ? match[1] : filename.replace(".md", "");
|
|
@@ -460,17 +495,7 @@ async function getAllIssues() {
|
|
|
460
495
|
content: body
|
|
461
496
|
});
|
|
462
497
|
}
|
|
463
|
-
return issues.sort(
|
|
464
|
-
const orderA = a.frontmatter.order;
|
|
465
|
-
const orderB = b.frontmatter.order;
|
|
466
|
-
if (orderA && orderB)
|
|
467
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
468
|
-
if (orderA && !orderB)
|
|
469
|
-
return -1;
|
|
470
|
-
if (!orderA && orderB)
|
|
471
|
-
return 1;
|
|
472
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
473
|
-
});
|
|
498
|
+
return issues.sort(compareIssuesByRoadmapOrder);
|
|
474
499
|
}
|
|
475
500
|
async function getOpenIssuesByOrder() {
|
|
476
501
|
const allIssues = await getAllIssues();
|
|
@@ -558,8 +583,7 @@ async function createIssue(input) {
|
|
|
558
583
|
const content = `${generateFrontmatter(frontmatter)}
|
|
559
584
|
${body}
|
|
560
585
|
`;
|
|
561
|
-
|
|
562
|
-
return {
|
|
586
|
+
const newIssue = {
|
|
563
587
|
id: issueNumber,
|
|
564
588
|
filename,
|
|
565
589
|
frontmatter,
|
|
@@ -567,6 +591,9 @@ ${body}
|
|
|
567
591
|
${body}
|
|
568
592
|
`
|
|
569
593
|
};
|
|
594
|
+
validateRoadmapDependencyOrder([...existingIssues, newIssue]);
|
|
595
|
+
await writeFile(join(getIssuesDir(), filename), content);
|
|
596
|
+
return newIssue;
|
|
570
597
|
}
|
|
571
598
|
async function updateIssue(id, input) {
|
|
572
599
|
const issue = await getIssue(id);
|
|
@@ -596,12 +623,16 @@ ${input.body}
|
|
|
596
623
|
` : issue.content;
|
|
597
624
|
const content = `${generateFrontmatter(updatedFrontmatter)}
|
|
598
625
|
${updatedContent}`;
|
|
599
|
-
|
|
600
|
-
return {
|
|
626
|
+
const updatedIssue = {
|
|
601
627
|
...issue,
|
|
602
628
|
frontmatter: updatedFrontmatter,
|
|
603
629
|
content: updatedContent
|
|
604
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;
|
|
605
636
|
}
|
|
606
637
|
async function closeIssue(id) {
|
|
607
638
|
return updateIssue(id, { status: "closed" });
|
|
@@ -2002,17 +2033,7 @@ function createSearchIndex(issues) {
|
|
|
2002
2033
|
function sortIssues(issues, sortBy) {
|
|
2003
2034
|
const sortOption = sortBy.toLowerCase();
|
|
2004
2035
|
if (sortOption === "roadmap") {
|
|
2005
|
-
issues.sort(
|
|
2006
|
-
const orderA = a.frontmatter.order;
|
|
2007
|
-
const orderB = b.frontmatter.order;
|
|
2008
|
-
if (orderA && orderB)
|
|
2009
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
2010
|
-
if (orderA && !orderB)
|
|
2011
|
-
return -1;
|
|
2012
|
-
if (!orderA && orderB)
|
|
2013
|
-
return 1;
|
|
2014
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2015
|
-
});
|
|
2036
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
2016
2037
|
} else if (sortOption === "priority") {
|
|
2017
2038
|
const priorityOrder = {
|
|
2018
2039
|
high: 0,
|
|
@@ -2066,17 +2087,7 @@ function sortIssues(issues, sortBy) {
|
|
|
2066
2087
|
} else if (sortOption === "id") {
|
|
2067
2088
|
issues.sort((a, b) => b.id.localeCompare(a.id));
|
|
2068
2089
|
} else {
|
|
2069
|
-
issues.sort(
|
|
2070
|
-
const orderA = a.frontmatter.order;
|
|
2071
|
-
const orderB = b.frontmatter.order;
|
|
2072
|
-
if (orderA && orderB)
|
|
2073
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
2074
|
-
if (orderA && !orderB)
|
|
2075
|
-
return -1;
|
|
2076
|
-
if (!orderA && orderB)
|
|
2077
|
-
return 1;
|
|
2078
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2079
|
-
});
|
|
2090
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
2080
2091
|
}
|
|
2081
2092
|
}
|
|
2082
2093
|
function filterByQuery(issues, query) {
|
package/dist/main.js
CHANGED
|
@@ -415,6 +415,41 @@ function getBlockingIssues(issue, issues) {
|
|
|
415
415
|
function isIssueUnblocked(issue, issues) {
|
|
416
416
|
return issue.frontmatter.status === "open" && getBlockingIssues(issue, issues).length === 0;
|
|
417
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
|
+
}
|
|
418
453
|
function getIssueIdFromFilename(filename) {
|
|
419
454
|
const match = filename.match(/^(\d+)-/);
|
|
420
455
|
return match ? match[1] : filename.replace(".md", "");
|
|
@@ -471,17 +506,7 @@ async function getAllIssues() {
|
|
|
471
506
|
content: body
|
|
472
507
|
});
|
|
473
508
|
}
|
|
474
|
-
return issues.sort(
|
|
475
|
-
const orderA = a.frontmatter.order;
|
|
476
|
-
const orderB = b.frontmatter.order;
|
|
477
|
-
if (orderA && orderB)
|
|
478
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
479
|
-
if (orderA && !orderB)
|
|
480
|
-
return -1;
|
|
481
|
-
if (!orderA && orderB)
|
|
482
|
-
return 1;
|
|
483
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
484
|
-
});
|
|
509
|
+
return issues.sort(compareIssuesByRoadmapOrder);
|
|
485
510
|
}
|
|
486
511
|
async function getOpenIssuesByOrder() {
|
|
487
512
|
const allIssues = await getAllIssues();
|
|
@@ -569,8 +594,7 @@ async function createIssue(input) {
|
|
|
569
594
|
const content = `${generateFrontmatter(frontmatter)}
|
|
570
595
|
${body}
|
|
571
596
|
`;
|
|
572
|
-
|
|
573
|
-
return {
|
|
597
|
+
const newIssue = {
|
|
574
598
|
id: issueNumber,
|
|
575
599
|
filename,
|
|
576
600
|
frontmatter,
|
|
@@ -578,6 +602,9 @@ ${body}
|
|
|
578
602
|
${body}
|
|
579
603
|
`
|
|
580
604
|
};
|
|
605
|
+
validateRoadmapDependencyOrder([...existingIssues, newIssue]);
|
|
606
|
+
await writeFile(join(getIssuesDir(), filename), content);
|
|
607
|
+
return newIssue;
|
|
581
608
|
}
|
|
582
609
|
async function updateIssue(id, input) {
|
|
583
610
|
const issue = await getIssue(id);
|
|
@@ -607,12 +634,16 @@ ${input.body}
|
|
|
607
634
|
` : issue.content;
|
|
608
635
|
const content = `${generateFrontmatter(updatedFrontmatter)}
|
|
609
636
|
${updatedContent}`;
|
|
610
|
-
|
|
611
|
-
return {
|
|
637
|
+
const updatedIssue = {
|
|
612
638
|
...issue,
|
|
613
639
|
frontmatter: updatedFrontmatter,
|
|
614
640
|
content: updatedContent
|
|
615
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;
|
|
616
647
|
}
|
|
617
648
|
async function closeIssue(id) {
|
|
618
649
|
return updateIssue(id, { status: "closed" });
|
|
@@ -2013,17 +2044,7 @@ function createSearchIndex(issues) {
|
|
|
2013
2044
|
function sortIssues(issues, sortBy) {
|
|
2014
2045
|
const sortOption = sortBy.toLowerCase();
|
|
2015
2046
|
if (sortOption === "roadmap") {
|
|
2016
|
-
issues.sort(
|
|
2017
|
-
const orderA = a.frontmatter.order;
|
|
2018
|
-
const orderB = b.frontmatter.order;
|
|
2019
|
-
if (orderA && orderB)
|
|
2020
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
2021
|
-
if (orderA && !orderB)
|
|
2022
|
-
return -1;
|
|
2023
|
-
if (!orderA && orderB)
|
|
2024
|
-
return 1;
|
|
2025
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2026
|
-
});
|
|
2047
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
2027
2048
|
} else if (sortOption === "priority") {
|
|
2028
2049
|
const priorityOrder = {
|
|
2029
2050
|
high: 0,
|
|
@@ -2077,17 +2098,7 @@ function sortIssues(issues, sortBy) {
|
|
|
2077
2098
|
} else if (sortOption === "id") {
|
|
2078
2099
|
issues.sort((a, b) => b.id.localeCompare(a.id));
|
|
2079
2100
|
} else {
|
|
2080
|
-
issues.sort(
|
|
2081
|
-
const orderA = a.frontmatter.order;
|
|
2082
|
-
const orderB = b.frontmatter.order;
|
|
2083
|
-
if (orderA && orderB)
|
|
2084
|
-
return orderA < orderB ? -1 : orderA > orderB ? 1 : 0;
|
|
2085
|
-
if (orderA && !orderB)
|
|
2086
|
-
return -1;
|
|
2087
|
-
if (!orderA && orderB)
|
|
2088
|
-
return 1;
|
|
2089
|
-
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
2090
|
-
});
|
|
2101
|
+
issues.sort(compareIssuesByRoadmapOrder);
|
|
2091
2102
|
}
|
|
2092
2103
|
}
|
|
2093
2104
|
function filterByQuery(issues, query) {
|
|
@@ -2218,6 +2229,7 @@ var compact = markdown([
|
|
|
2218
2229
|
"- When reopening an issue and other open issues exist, include exactly one position flag.",
|
|
2219
2230
|
"- Use dependency order: prerequisites first, dependent/user-facing work later. Use `--last` when placement is unclear.",
|
|
2220
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.",
|
|
2221
2233
|
"- `issy list` shows a `Blk` column with open blocker counts; `-` means unblocked.",
|
|
2222
2234
|
"- `issy list --unblocked` shows only open issues with no open blockers.",
|
|
2223
2235
|
"",
|
|
@@ -2310,6 +2322,7 @@ var topics = [
|
|
|
2310
2322
|
"- Replace blockers with `issy update <id> --depends-on 0001,0003`.",
|
|
2311
2323
|
'- Clear blockers with `issy update <id> --depends-on ""`.',
|
|
2312
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.",
|
|
2313
2326
|
"- `issy list` shows a compact `Blk` column. `-` means unblocked; otherwise the value is the number of open blockers.",
|
|
2314
2327
|
"- `issy list --unblocked` shows only open issues with no open blockers.",
|
|
2315
2328
|
"",
|
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
|
}
|