schub 0.1.2 → 0.1.4

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.
Files changed (80) hide show
  1. package/README.md +27 -0
  2. package/dist/index.js +12830 -3057
  3. package/package.json +5 -2
  4. package/skills/create-proposal/SKILL.md +5 -1
  5. package/skills/create-tasks/SKILL.md +5 -4
  6. package/skills/implement-task/SKILL.md +6 -1
  7. package/skills/review-proposal/SKILL.md +3 -2
  8. package/skills/update-roadmap/SKILL.md +23 -0
  9. package/src/changes.test.ts +166 -0
  10. package/src/changes.ts +159 -54
  11. package/src/commands/adr.test.ts +6 -5
  12. package/src/commands/changes.test.ts +136 -14
  13. package/src/commands/changes.ts +102 -1
  14. package/src/commands/cookbook.test.ts +6 -5
  15. package/src/commands/init.test.ts +69 -2
  16. package/src/commands/init.ts +48 -5
  17. package/src/commands/review.test.ts +7 -6
  18. package/src/commands/review.ts +1 -1
  19. package/src/commands/roadmap.test.ts +84 -0
  20. package/src/commands/roadmap.ts +84 -0
  21. package/src/commands/tasks-create.test.ts +22 -22
  22. package/src/commands/tasks-implement.test.ts +253 -0
  23. package/src/commands/tasks-implement.ts +121 -0
  24. package/src/commands/tasks-list.test.ts +27 -27
  25. package/src/commands/tasks-update.test.ts +92 -0
  26. package/src/commands/tasks.ts +98 -1
  27. package/src/features/roadmap/index.ts +230 -0
  28. package/src/features/roadmap/roadmap.test.ts +77 -0
  29. package/src/features/tasks/constants.ts +1 -0
  30. package/src/features/tasks/create.ts +10 -8
  31. package/src/features/tasks/filesystem.test.ts +285 -18
  32. package/src/features/tasks/filesystem.ts +152 -39
  33. package/src/features/tasks/graph.ts +18 -3
  34. package/src/features/tasks/index.ts +10 -1
  35. package/src/features/tasks/worktree.ts +48 -0
  36. package/src/frontmatter.ts +115 -0
  37. package/src/index.test.ts +42 -6
  38. package/src/index.ts +226 -109
  39. package/src/opencode.test.ts +53 -0
  40. package/src/opencode.ts +74 -0
  41. package/src/tasks.ts +2 -0
  42. package/src/tui/App.test.tsx +418 -0
  43. package/src/tui/App.tsx +343 -0
  44. package/src/tui/components/PlanView.test.tsx +101 -0
  45. package/src/tui/components/PlanView.tsx +89 -0
  46. package/src/tui/components/PreviewPage.test.tsx +69 -0
  47. package/src/tui/components/PreviewPage.tsx +87 -0
  48. package/src/tui/components/ProposalDetailView.test.tsx +169 -0
  49. package/src/tui/components/ProposalDetailView.tsx +166 -0
  50. package/src/tui/components/RoadmapView.test.tsx +85 -0
  51. package/src/tui/components/RoadmapView.tsx +369 -0
  52. package/src/tui/components/StatusView.test.tsx +1351 -0
  53. package/src/tui/components/StatusView.tsx +519 -0
  54. package/src/tui/components/markdown-renderer.test.ts +46 -0
  55. package/src/tui/components/markdown-renderer.ts +89 -0
  56. package/src/tui/components/status-view-data.ts +322 -0
  57. package/src/tui/components/status-view-render.tsx +329 -0
  58. package/src/tui/index.ts +16 -0
  59. package/templates/create-proposal/adr-template.md +6 -4
  60. package/templates/create-proposal/cookbook-template.md +5 -3
  61. package/templates/create-proposal/proposal-template.md +8 -6
  62. package/templates/create-roadmap/roadmap.md +5 -0
  63. package/templates/create-tasks/task-template.md +9 -4
  64. package/templates/review-proposal/q&a-template.md +8 -3
  65. package/templates/review-proposal/review-me-template.md +6 -4
  66. package/templates/setup-project/project-overview-template.md +5 -0
  67. package/templates/setup-project/project-setup-template.md +5 -0
  68. package/templates/setup-project/project-wow-template.md +5 -0
  69. package/src/App.test.tsx +0 -93
  70. package/src/App.tsx +0 -155
  71. package/src/components/PlanView.test.tsx +0 -113
  72. package/src/components/PlanView.tsx +0 -160
  73. package/src/components/StatusView.test.tsx +0 -380
  74. package/src/components/StatusView.tsx +0 -367
  75. package/src/ide.ts +0 -7
  76. package/templates/templates-parity.test.ts +0 -45
  77. /package/src/{clipboard.ts → tui/clipboard.ts} +0 -0
  78. /package/src/{components → tui/components}/statusColor.ts +0 -0
  79. /package/src/{terminal.test.ts → tui/terminal.test.ts} +0 -0
  80. /package/src/{terminal.ts → tui/terminal.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schub",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "schub": "./src/index.ts"
@@ -20,15 +20,18 @@
20
20
  "test": "bun test"
21
21
  },
22
22
  "dependencies": {
23
+ "@inkjs/ui": "^2.0.0",
23
24
  "chalk": "^5.6.2",
24
25
  "ink": "^6.6.0",
25
26
  "react": "^19.2.3",
26
- "react-devtools-core": "^6.1.2"
27
+ "react-devtools-core": "^6.1.2",
28
+ "yargs": "^18.0.0"
27
29
  },
28
30
  "devDependencies": {
29
31
  "@types/bun": "latest",
30
32
  "@types/node": "^24.3.0",
31
33
  "@types/react": "^19.2.8",
34
+ "@types/yargs": "^17.0.35",
32
35
  "ink-testing-library": "^4.0.0"
33
36
  }
34
37
  }
@@ -11,7 +11,7 @@ $ARGUMENTS
11
11
 
12
12
  ## Workflow
13
13
 
14
- 1. Derive a concise, verb-led `change-id` suffix from the request (kebab-case: `add-`, `update-`, `remove-`, `refactor-`, `fix-`) and prefix it with `C` + the next 3-digit sequence + `_` (e.g., `C001_add-user-auth`).
14
+ 1. Derive a concise, verb-led `change-id` suffix from the request (kebab-case: `add-`, `update-`, `remove-`, `refactor-`, `fix-`) and prefix it with `C` + the next 4-digit sequence + `_` (e.g., `C0001_add-user-auth`).
15
15
  2. Run `npx schub changes create --change-id "<change-id>" --title "<title>" --input "<user prompt verbatim>"` to scaffold the proposal template.
16
16
  - Supported flags: `--change-id`, `--title`, `--input`, `--overwrite`.
17
17
  3. Update the generated proposal with concrete, testable statements.
@@ -31,3 +31,7 @@ $ARGUMENTS
31
31
  - (OPTIONAL) Contracts: `.schub/changes/<change-id>/contracts.md`
32
32
  - (OPTIONAL) Research: `.schub/changes/<change-id>/research.md`
33
33
  - (OPTIONAL) ADR: `.schub/changes/<change-id>/adr.md`
34
+
35
+ ## Notes
36
+
37
+ - **Do not start the implementation** only edit the `changes` folder.
@@ -3,7 +3,7 @@ name: create-tasks
3
3
  description: "Break an accepted proposal into actionable task files under `.schub/tasks/` using the task template. Use when asked to create tasks for a change-id."
4
4
  ---
5
5
 
6
- ## User Input (should contain change-id or shorthand `C###`)
6
+ ## User Input (should contain change-id or shorthand `C####`)
7
7
 
8
8
  ```text
9
9
  $ARGUMENTS
@@ -11,13 +11,13 @@ $ARGUMENTS
11
11
 
12
12
  ## Workflow
13
13
 
14
- 1. If the user provides shorthand `C###`, resolve it to the matching `C###_<suffix>` change folder (error if ambiguous). Confirm `.schub/changes/<change-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.
14
+ 1. If the user provides shorthand `C####`, resolve it to the matching `C####_<suffix>` change folder (error if ambiguous). Confirm `.schub/changes/<change-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.
15
15
  2. Read the proposal and other files in the <change-id> folder.
16
16
  3. Derive tasks, each task should be:
17
17
  - Small enough to implement in one sitting.
18
18
  - Independently testable.
19
19
  - Explicit about file paths and affected components.
20
- 4. Assign a unique task id using `T###` (ex: `T001`) and avoid collisions with existing tasks.
20
+ 4. Assign a unique task id using `T####` (ex: `T0001`) and avoid collisions with existing tasks.
21
21
  5. Run `npx schub tasks create --change-id "<change-id>" --title "<task title>"` to scaffold tasks.
22
22
  - Repeat `--title` for multiple tasks.
23
23
  - Optional flags: `--status "<status>"` (default: `backlog`), `--overwrite`.
@@ -25,8 +25,9 @@ $ARGUMENTS
25
25
  - Priority (P1/P2/P3)
26
26
  - Parallelizable (yes/no)
27
27
  - Goal, scope, steps, acceptance, and evidence
28
+ - If the task is blocked, set `blocked_reason` in the task frontmatter
28
29
  7. When defining the acceptance, include relevant tests, e2e commands and documentation updates.
29
- 8. Resolve blockers by check all existing tasks that are not done. If another task is a blocker, add it to **Depends on**.
30
+ 8. Resolve blockers by check all existing tasks that are not done. If another task is a blocker, add it to `depends_on` in frontmatter.
30
31
  9. Stop after the task files are created. Do not implement code or modify the plan or supporting artifacts.
31
32
 
32
33
  ## Output Locations
@@ -9,6 +9,11 @@ description: "Implement a single task end-to-end: locate the task file by id, mo
9
9
  $ARGUMENTS
10
10
  ```
11
11
 
12
+ ## Task Implement Modes
13
+
14
+ `schub tasks implement` defaults to `worktree` mode. It creates `.schub/worktrees/<task-id>` on branch `task/<task-id>` and launches Opencode from that worktree.
15
+ Use `--mode none` to skip worktree creation and run from the repo root. Use `--worktree-root` to override the base worktree path.
16
+
12
17
  ## Workflow
13
18
 
14
19
  1. **Identify the task**
@@ -23,7 +28,7 @@ $ARGUMENTS
23
28
 
24
29
  3. **Move the task before work**
25
30
  - If starting work: move to `.schub/tasks/wip/` (create folder if needed).
26
- - If blocked: move to `.schub/tasks/blocked/` and record the blocker in the task file.
31
+ - If blocked: move to `.schub/tasks/blocked/` and set `blocked_reason` in the task frontmatter.
27
32
  - If complete: move to `.schub/tasks/done/` after confirming all checklists are checked.
28
33
  - Keep the task in **exactly one** status folder.
29
34
 
@@ -3,7 +3,7 @@ name: review-proposal
3
3
  description: "Run a review session for a proposal by creating a list of open questions, updating the proposal, adding a Q&A section, and deleting REVIEW_ME.md on completion. Use when asked to review proposals."
4
4
  ---
5
5
 
6
- ## User Input (change-id or shorthand `C###`)
6
+ ## User Input (change-id or shorthand `C####`)
7
7
 
8
8
  ```text
9
9
  $ARGUMENTS
@@ -11,7 +11,7 @@ $ARGUMENTS
11
11
 
12
12
  ## Workflow
13
13
 
14
- 0. If the user provides shorthand `C###`, resolve it to the matching `C###_<suffix>` change folder (error if ambiguous). Mark the proposal Status as "In Review"
14
+ 0. If the user provides shorthand `C####`, resolve it to the matching `C####_<suffix>` change folder (error if ambiguous). Mark the proposal Status as "In Review"
15
15
  1. Review the proposal by checking the [MISSING INFORMATION] tags and the potential issues listed.
16
16
  2. If there are issues, or missing information, run `npx schub review create --change-id "<change-id>"` to scaffold `.schub/changes/<change-id>/REVIEW_ME.md`.
17
17
  3. Triage each item:
@@ -35,3 +35,4 @@ $ARGUMENTS
35
35
 
36
36
  - Avoid expanding scope beyond missing information in the proposal.
37
37
  - Focus review on high-stakes issues; place details into assumptions instead of asking.
38
+ - **Do not start the implementation** only edit the `changes` folder.
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: update-roadmap
3
+ description: "Add, list, or propose items from the project roadmap in .schub/roadmap.md. Use when asked to update or promote roadmap user stories."
4
+ ---
5
+
6
+ ## User Input
7
+
8
+ ```text
9
+ $ARGUMENTS
10
+ ```
11
+
12
+ ## Workflow
13
+
14
+ 1. Ensure `.schub/roadmap.md` exists. If missing, ask the user to run `npx schub init` first.
15
+ 2. To view items, run `npx schub roadmap list` and confirm the index of the target story.
16
+ 3. To add a story, run `npx schub roadmap add "<story>"`.
17
+ 4. To create a proposal from a story, run `npx schub roadmap propose <index>`.
18
+ 5. Confirm the roadmap entry now includes the `C####` proposal prefix.
19
+
20
+ ## Output Locations
21
+
22
+ - `.schub/roadmap.md`
23
+ - `.schub/changes/<change-id>/proposal.md`
@@ -0,0 +1,166 @@
1
+ import { expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { archiveChange, createChange, listChangeOverview, readChangeDetail } from "./changes";
6
+
7
+ const createSchubRoot = () => {
8
+ const base = mkdtempSync(join(tmpdir(), "schub-change-archive-"));
9
+ const schubDir = join(base, ".schub");
10
+ mkdirSync(schubDir, { recursive: true });
11
+ return { schubDir };
12
+ };
13
+
14
+ const seedProposal = (schubDir: string, changeId: string, status: string) => {
15
+ const changeDir = join(schubDir, "changes", changeId);
16
+ mkdirSync(changeDir, { recursive: true });
17
+ const proposal = `---\nstatus: ${status}\n---\n# Proposal - Seed\n`;
18
+ writeFileSync(join(changeDir, "proposal.md"), proposal, "utf8");
19
+ };
20
+
21
+ const seedArchivedProposal = (schubDir: string, changeId: string, status: string) => {
22
+ const changeDir = join(schubDir, "archive", "changes", changeId);
23
+ mkdirSync(changeDir, { recursive: true });
24
+ const proposal = `---\nstatus: ${status}\n---\n# Proposal - Seed\n`;
25
+ writeFileSync(join(changeDir, "proposal.md"), proposal, "utf8");
26
+ };
27
+
28
+ const writeProposalContent = (schubDir: string, changeId: string, proposal: string) => {
29
+ const changeDir = join(schubDir, "changes", changeId);
30
+ mkdirSync(changeDir, { recursive: true });
31
+ writeFileSync(join(changeDir, "proposal.md"), proposal, "utf8");
32
+ };
33
+
34
+ test("createChange rejects duplicate prefixes", () => {
35
+ const { schubDir } = createSchubRoot();
36
+ seedProposal(schubDir, "C0001_existing-change", "Draft");
37
+
38
+ expect(() =>
39
+ createChange(schubDir, {
40
+ changeId: "C0001_new-change",
41
+ title: "New Change",
42
+ }),
43
+ ).toThrow("Change prefix 'C0001' already exists");
44
+ });
45
+
46
+ test("archiveChange updates proposal and moves directory", () => {
47
+ const { schubDir } = createSchubRoot();
48
+ const changeId = "C0001_archive-change";
49
+ seedProposal(schubDir, changeId, "Accepted");
50
+
51
+ archiveChange(schubDir, changeId);
52
+
53
+ const archivePath = join(schubDir, "archive", "changes", changeId);
54
+ expect(existsSync(join(schubDir, "changes", changeId))).toBe(false);
55
+ expect(existsSync(archivePath)).toBe(true);
56
+
57
+ const proposalPath = join(archivePath, "proposal.md");
58
+ const updated = readFileSync(proposalPath, "utf8");
59
+ expect(updated).toContain("status: Archived");
60
+ });
61
+
62
+ test("archiveChange rejects archive collisions", () => {
63
+ const { schubDir } = createSchubRoot();
64
+ const changeId = "C0001_archive-change";
65
+ seedProposal(schubDir, changeId, "Accepted");
66
+
67
+ const archivePath = join(schubDir, "archive", "changes", changeId);
68
+ mkdirSync(archivePath, { recursive: true });
69
+ const archivedProposal = join(archivePath, "proposal.md");
70
+ writeFileSync(archivedProposal, "---\nstatus: Archived\n---\n# Proposal - Existing\n", "utf8");
71
+
72
+ expect(() => archiveChange(schubDir, changeId)).toThrow(`Archive already exists: ${archivePath}`);
73
+
74
+ const activeProposal = readFileSync(join(schubDir, "changes", changeId, "proposal.md"), "utf8");
75
+ expect(activeProposal).toContain("status: Accepted");
76
+ expect(readFileSync(archivedProposal, "utf8")).toContain("# Proposal - Existing");
77
+ });
78
+
79
+ test("listChangeOverview includes archived proposals with normalized ordering", () => {
80
+ const { schubDir } = createSchubRoot();
81
+ seedProposal(schubDir, "C0001_draft-change", "draft");
82
+ seedProposal(schubDir, "C0002_review-change", "Pending review");
83
+ seedProposal(schubDir, "C0003_accepted-change", "ACCEPTED");
84
+ seedArchivedProposal(schubDir, "C0004_implementing-change", "Implementing");
85
+ seedArchivedProposal(schubDir, "C0005_done-change", "Done");
86
+ seedArchivedProposal(schubDir, "C0006_archived-change", "Archived");
87
+ seedProposal(schubDir, "C0007_custom-change", "needs input");
88
+
89
+ const changes = listChangeOverview(schubDir);
90
+ const changeMap = new Map(changes.map((change) => [change.id, change]));
91
+
92
+ expect(changeMap.get("C0006_archived-change")?.statusLabel).toBe("Archived");
93
+ expect(changeMap.get("C0001_draft-change")?.statusLabel).toBe("Draft");
94
+ expect(changeMap.get("C0007_custom-change")?.statusLabel).toBe("Needs Input");
95
+ expect(changeMap.get("C0003_accepted-change")?.statusOrder).toBe(2);
96
+
97
+ const groupedStatuses = new Map<string, number>();
98
+ for (const change of changes) {
99
+ groupedStatuses.set(change.statusLabel, change.statusOrder);
100
+ }
101
+
102
+ const orderedLabels = Array.from(groupedStatuses.entries())
103
+ .map(([label, order]) => ({ label, order }))
104
+ .sort((left, right) => left.order - right.order || left.label.localeCompare(right.label))
105
+ .map((group) => group.label);
106
+
107
+ expect(orderedLabels).toEqual([
108
+ "Draft",
109
+ "Pending Review",
110
+ "Accepted",
111
+ "Implementing",
112
+ "Done",
113
+ "Archived",
114
+ "Needs Input",
115
+ ]);
116
+ });
117
+
118
+ test("readChangeDetail parses proposal metadata and summary", () => {
119
+ const { schubDir } = createSchubRoot();
120
+ const changeId = "C0100_detail-view";
121
+ const proposal = `---\nchange_id: ${changeId}\ncreated: 2026-01-10\nstatus: Draft\ninput: TUI detail view\n---\n# Proposal - Detail View\n\n## Summary\n\nAdd a detail view in the TUI.\n\n## Why\n\nBecause.\n`;
122
+
123
+ writeProposalContent(schubDir, changeId, proposal);
124
+
125
+ const detail = readChangeDetail(schubDir, changeId);
126
+
127
+ expect(detail.changeId).toBe(changeId);
128
+ expect(detail.created).toBe("2026-01-10");
129
+ expect(detail.status).toBe("Draft");
130
+ expect(detail.input).toBe("TUI detail view");
131
+ expect(detail.summary).toBe("Add a detail view in the TUI.");
132
+ });
133
+
134
+ test("readChangeDetail accepts variable-length prefixes", () => {
135
+ const { schubDir } = createSchubRoot();
136
+ const shortId = "C1_short-change";
137
+ const longId = "C12345_long-change";
138
+ const proposal = (changeId: string) =>
139
+ `---\nchange_id: ${changeId}\ncreated: 2026-01-12\nstatus: Draft\ninput: None\n---\n# Proposal - Prefix\n\n## Summary\n\nPrefix test.\n`;
140
+
141
+ writeProposalContent(schubDir, shortId, proposal(shortId));
142
+ writeProposalContent(schubDir, longId, proposal(longId));
143
+
144
+ expect(readChangeDetail(schubDir, shortId).changeId).toBe(shortId);
145
+ expect(readChangeDetail(schubDir, longId).changeId).toBe(longId);
146
+ });
147
+
148
+ test("readChangeDetail falls back when summary is blank or missing", () => {
149
+ const { schubDir } = createSchubRoot();
150
+ const blankId = "C0101_blank-summary";
151
+ const missingId = "C0102_missing-summary";
152
+
153
+ writeProposalContent(
154
+ schubDir,
155
+ blankId,
156
+ `---\nchange_id: ${blankId}\ncreated: 2026-01-11\nstatus: Draft\ninput: None\n---\n# Proposal - Blank Summary\n\n## Summary\n\n \n\n## Why\n\nBecause.\n`,
157
+ );
158
+ writeProposalContent(
159
+ schubDir,
160
+ missingId,
161
+ `---\nchange_id: ${missingId}\ncreated: 2026-01-11\nstatus: Draft\ninput: None\n---\n# Proposal - Missing Summary\n\n## Why\n\nBecause.\n`,
162
+ );
163
+
164
+ expect(readChangeDetail(schubDir, blankId).summary).toBe("No summary provided.");
165
+ expect(readChangeDetail(schubDir, missingId).summary).toBe("No summary provided.");
166
+ });