schub 0.1.2 → 0.1.3

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.
@@ -1,4 +1,5 @@
1
- import { createChange, resolveChangeRoot, updateChangeStatus } from "../changes";
1
+ import { archiveChange, createChange, listChanges, resolveChangeRoot, updateChangeStatus } from "../changes";
2
+ import { archiveTasksForChange } from "../tasks";
2
3
 
3
4
  type ChangeCreateOptions = {
4
5
  changeId?: string;
@@ -12,6 +13,11 @@ type ChangeStatusOptions = {
12
13
  status: string;
13
14
  };
14
15
 
16
+ type ChangeArchiveOptions = {
17
+ changeId: string;
18
+ skipTasks: boolean;
19
+ };
20
+
15
21
  const parseChangeCreateOptions = (args: string[]) => {
16
22
  let changeId: string | undefined;
17
23
  let title: string | undefined;
@@ -148,6 +154,82 @@ const parseChangeStatusOptions = (args: string[]) => {
148
154
  return options;
149
155
  };
150
156
 
157
+ const parseChangeArchiveOptions = (args: string[]) => {
158
+ let changeId: string | undefined;
159
+ let skipTasks = false;
160
+ const unknown: string[] = [];
161
+
162
+ const rejectUnsupported = (flag: string) => {
163
+ throw new Error(`Unsupported option: ${flag}.`);
164
+ };
165
+
166
+ for (let index = 0; index < args.length; index += 1) {
167
+ const arg = args[index];
168
+ if (arg === "--skip-tasks") {
169
+ skipTasks = true;
170
+ continue;
171
+ }
172
+ if (arg === "--change-id") {
173
+ changeId = args[index + 1];
174
+ if (changeId === undefined) {
175
+ throw new Error("Missing value for --change-id.");
176
+ }
177
+ index += 1;
178
+ continue;
179
+ }
180
+ if (arg.startsWith("--change-id=")) {
181
+ changeId = arg.slice("--change-id=".length);
182
+ continue;
183
+ }
184
+ if (arg === "--schub-root" || arg === "--agent-root") {
185
+ rejectUnsupported(arg);
186
+ }
187
+ if (arg.startsWith("--schub-root=")) {
188
+ rejectUnsupported("--schub-root");
189
+ }
190
+ if (arg.startsWith("--agent-root=")) {
191
+ rejectUnsupported("--agent-root");
192
+ }
193
+ unknown.push(arg);
194
+ }
195
+
196
+ if (unknown.length > 0) {
197
+ throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
198
+ }
199
+
200
+ if (!changeId) {
201
+ throw new Error("Provide --change-id.");
202
+ }
203
+
204
+ const options: ChangeArchiveOptions = { changeId, skipTasks };
205
+ return options;
206
+ };
207
+
208
+ const parseChangeListOptions = (args: string[]) => {
209
+ const unknown: string[] = [];
210
+
211
+ const rejectUnsupported = (flag: string) => {
212
+ throw new Error(`Unsupported option: ${flag}.`);
213
+ };
214
+
215
+ for (const arg of args) {
216
+ if (arg === "--schub-root" || arg === "--agent-root") {
217
+ rejectUnsupported(arg);
218
+ }
219
+ if (arg.startsWith("--schub-root=")) {
220
+ rejectUnsupported("--schub-root");
221
+ }
222
+ if (arg.startsWith("--agent-root=")) {
223
+ rejectUnsupported("--agent-root");
224
+ }
225
+ unknown.push(arg);
226
+ }
227
+
228
+ if (unknown.length > 0) {
229
+ throw new Error(`Unknown option(s): ${unknown.join(", ")}`);
230
+ }
231
+ };
232
+
151
233
  export const runChangesCreate = (args: string[], startDir: string) => {
152
234
  const options = parseChangeCreateOptions(args);
153
235
  const schubDir = resolveChangeRoot(startDir);
@@ -161,3 +243,22 @@ export const runChangesStatus = (args: string[], startDir: string) => {
161
243
  const updated = updateChangeStatus(schubDir, options.changeId, options.status);
162
244
  process.stdout.write(`[OK] Updated status for ${updated.changeId}: ${updated.previousStatus} -> ${updated.status}\n`);
163
245
  };
246
+
247
+ export const runChangesArchive = (args: string[], startDir: string) => {
248
+ const options = parseChangeArchiveOptions(args);
249
+ const schubDir = resolveChangeRoot(startDir);
250
+ const archived = archiveChange(schubDir, options.changeId);
251
+ const archivedTasks = options.skipTasks ? [] : archiveTasksForChange(schubDir, archived.changeId);
252
+ const taskLabel = options.skipTasks
253
+ ? "tasks kept"
254
+ : `${archivedTasks.length} task${archivedTasks.length === 1 ? "" : "s"} archived`;
255
+ process.stdout.write(`[OK] Archived change ${archived.changeId} (${taskLabel})\n`);
256
+ };
257
+
258
+ export const runChangesList = (args: string[], startDir: string) => {
259
+ parseChangeListOptions(args);
260
+ const schubDir = resolveChangeRoot(startDir);
261
+ const changes = listChanges(schubDir);
262
+ const lines = changes.map((change) => `${change.id} ${change.title} (${change.status})`);
263
+ process.stdout.write(`${lines.join("\n")}\n`);
264
+ };
@@ -49,7 +49,7 @@ const seedChange = (schubRoot: string, changeId: string, title: string) => {
49
49
 
50
50
  test("cookbook create scaffolds cookbook file", () => {
51
51
  const { cwd, schubRoot } = createRepo();
52
- const changeId = "C004_new-cookbook";
52
+ const changeId = "C0004_new-cookbook";
53
53
  const changeTitle = "New Cookbook";
54
54
  seedChange(schubRoot, changeId, changeTitle);
55
55
 
@@ -3,7 +3,13 @@ import { spawnSync } from "node:child_process";
3
3
  import { existsSync, mkdirSync, mkdtempSync, realpathSync, writeFileSync } from "node:fs";
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
- import { installCodexSkills, resolveCodexSkillsRoot, selectProviders } from "./init";
6
+ import {
7
+ installCodexSkills,
8
+ installOpencodeSkills,
9
+ resolveCodexSkillsRoot,
10
+ resolveOpencodeSkillsRoot,
11
+ selectProviders,
12
+ } from "./init";
7
13
 
8
14
  const createRepoFixture = () => {
9
15
  const base = mkdtempSync(join(tmpdir(), "schub-init-codex-"));
@@ -15,7 +21,9 @@ const createRepoFixture = () => {
15
21
 
16
22
  test("selectProviders handles numeric and named inputs", () => {
17
23
  expect(selectProviders("1")).toEqual(["codex"]);
18
- expect(selectProviders("codex, 1")).toEqual(["codex"]);
24
+ expect(selectProviders("2")).toEqual(["opencode"]);
25
+ expect(selectProviders("codex, opencode")).toEqual(["codex", "opencode"]);
26
+ expect(selectProviders("Opencode, 1")).toEqual(["opencode", "codex"]);
19
27
  });
20
28
 
21
29
  test("resolveCodexSkillsRoot prefers worktree .codex when present", () => {
@@ -63,6 +71,51 @@ test("resolveCodexSkillsRoot uses cwd when git is unavailable", () => {
63
71
  }
64
72
  });
65
73
 
74
+ test("resolveOpencodeSkillsRoot prefers worktree .opencode when present", () => {
75
+ const { repoRoot, startDir } = createRepoFixture();
76
+ const gitInit = spawnSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
77
+
78
+ expect(gitInit.status).toBe(0);
79
+
80
+ mkdirSync(join(repoRoot, ".opencode"), { recursive: true });
81
+
82
+ const destination = resolveOpencodeSkillsRoot(startDir);
83
+ const expectedRoot = realpathSync(repoRoot);
84
+
85
+ expect(destination).toBe(join(expectedRoot, ".opencode", "skills"));
86
+ });
87
+
88
+ test("resolveOpencodeSkillsRoot falls back to home when worktree .opencode is missing", () => {
89
+ const { repoRoot, startDir } = createRepoFixture();
90
+ const gitInit = spawnSync("git", ["init"], { cwd: repoRoot, stdio: "ignore" });
91
+
92
+ expect(gitInit.status).toBe(0);
93
+
94
+ const originalHome = process.env.HOME;
95
+ const homeDir = mkdtempSync(join(tmpdir(), "schub-home-"));
96
+ process.env.HOME = homeDir;
97
+
98
+ try {
99
+ const destination = resolveOpencodeSkillsRoot(startDir);
100
+ expect(destination).toBe(join(homeDir, ".opencode", "skills"));
101
+ } finally {
102
+ process.env.HOME = originalHome;
103
+ }
104
+ });
105
+
106
+ test("resolveOpencodeSkillsRoot uses cwd when git is unavailable", () => {
107
+ const { startDir } = createRepoFixture();
108
+ const originalPath = process.env.PATH;
109
+ process.env.PATH = "";
110
+
111
+ try {
112
+ const destination = resolveOpencodeSkillsRoot(startDir);
113
+ expect(destination).toBe(join(startDir, ".opencode", "skills"));
114
+ } finally {
115
+ process.env.PATH = originalPath;
116
+ }
117
+ });
118
+
66
119
  test("installCodexSkills skips existing skill directories", () => {
67
120
  const base = mkdtempSync(join(tmpdir(), "schub-init-install-"));
68
121
  const destination = join(base, "skills");
@@ -76,3 +129,17 @@ test("installCodexSkills skips existing skill directories", () => {
76
129
  expect(installed.length).toBeGreaterThan(0);
77
130
  expect(existsSync(join(destination, "create-tasks", "SKILL.md"))).toBe(true);
78
131
  });
132
+
133
+ test("installOpencodeSkills skips existing skill directories", () => {
134
+ const base = mkdtempSync(join(tmpdir(), "schub-init-opencode-install-"));
135
+ const destination = join(base, "skills");
136
+ const existingSkill = join(destination, "review-proposal");
137
+ mkdirSync(existingSkill, { recursive: true });
138
+ writeFileSync(join(existingSkill, "SKILL.md"), "existing", "utf8");
139
+
140
+ const { installed, skipped } = installOpencodeSkills(destination);
141
+
142
+ expect(skipped).toContain("review-proposal");
143
+ expect(installed.length).toBeGreaterThan(0);
144
+ expect(existsSync(join(destination, "create-tasks", "SKILL.md"))).toBe(true);
145
+ });
@@ -5,14 +5,17 @@ import { createInterface } from "node:readline";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { initSchubRoot, resolveGitRoot } from "../init";
7
7
 
8
- type ProviderId = "codex";
8
+ type ProviderId = "codex" | "opencode";
9
9
 
10
10
  type ProviderOption = {
11
11
  id: ProviderId;
12
12
  label: string;
13
13
  };
14
14
 
15
- const PROVIDERS: ProviderOption[] = [{ id: "codex", label: "Codex" }];
15
+ const PROVIDERS: ProviderOption[] = [
16
+ { id: "codex", label: "Codex" },
17
+ { id: "opencode", label: "Opencode" },
18
+ ];
16
19
  const BUNDLED_SKILLS_ROOT = fileURLToPath(new URL("../../skills", import.meta.url));
17
20
 
18
21
  const isDirectory = (path: string) => {
@@ -82,7 +85,24 @@ export const resolveCodexSkillsRoot = (startDir: string) => {
82
85
  return join(home, ".codex", "skills");
83
86
  };
84
87
 
85
- export const installCodexSkills = (destination: string) => {
88
+ export const resolveOpencodeSkillsRoot = (startDir: string) => {
89
+ const resolvedStart = resolve(startDir);
90
+ const gitRoot = resolveGitRoot(resolvedStart);
91
+
92
+ if (!gitRoot) {
93
+ return join(resolvedStart, ".opencode", "skills");
94
+ }
95
+
96
+ const localOpencodeRoot = join(gitRoot, ".opencode");
97
+ if (isDirectory(localOpencodeRoot)) {
98
+ return join(localOpencodeRoot, "skills");
99
+ }
100
+
101
+ const home = process.env.HOME ?? homedir();
102
+ return join(home, ".opencode", "skills");
103
+ };
104
+
105
+ const installBundledSkills = (destination: string) => {
86
106
  mkdirSync(destination, { recursive: true });
87
107
 
88
108
  const entries = readdirSync(BUNDLED_SKILLS_ROOT, { withFileTypes: true });
@@ -109,8 +129,12 @@ export const installCodexSkills = (destination: string) => {
109
129
  return { installed, skipped };
110
130
  };
111
131
 
112
- const reportCodexInstall = (destination: string, installed: string[], skipped: string[]) => {
113
- process.stdout.write(`[OK] Codex skills: ${destination}\n`);
132
+ export const installCodexSkills = (destination: string) => installBundledSkills(destination);
133
+
134
+ export const installOpencodeSkills = (destination: string) => installBundledSkills(destination);
135
+
136
+ const reportSkillsInstall = (label: string, destination: string, installed: string[], skipped: string[]) => {
137
+ process.stdout.write(`[OK] ${label} skills: ${destination}\n`);
114
138
 
115
139
  for (const skill of installed) {
116
140
  process.stdout.write(`[OK] Installed ${skill}\n`);
@@ -121,6 +145,14 @@ const reportCodexInstall = (destination: string, installed: string[], skipped: s
121
145
  }
122
146
  };
123
147
 
148
+ const reportCodexInstall = (destination: string, installed: string[], skipped: string[]) => {
149
+ reportSkillsInstall("Codex", destination, installed, skipped);
150
+ };
151
+
152
+ const reportOpencodeInstall = (destination: string, installed: string[], skipped: string[]) => {
153
+ reportSkillsInstall("Opencode", destination, installed, skipped);
154
+ };
155
+
124
156
  const parseInitOptions = (args: string[]) => {
125
157
  if (args.length === 0) {
126
158
  return;
@@ -141,4 +173,10 @@ export const runInit = async (args: string[], startDir: string) => {
141
173
  const { installed, skipped } = installCodexSkills(destination);
142
174
  reportCodexInstall(destination, installed, skipped);
143
175
  }
176
+
177
+ if (providers.includes("opencode")) {
178
+ const destination = resolveOpencodeSkillsRoot(startDir);
179
+ const { installed, skipped } = installOpencodeSkills(destination);
180
+ reportOpencodeInstall(destination, installed, skipped);
181
+ }
144
182
  };
@@ -49,7 +49,7 @@ const seedChange = (schubRoot: string, changeId: string, title: string) => {
49
49
 
50
50
  test("review create scaffolds REVIEW_ME from template", () => {
51
51
  const { cwd, schubRoot } = createRepo();
52
- const changeId = "C001_sample-change";
52
+ const changeId = "C0001_sample-change";
53
53
  const changeTitle = "Sample Change";
54
54
  seedChange(schubRoot, changeId, changeTitle);
55
55
 
@@ -72,7 +72,7 @@ test("review create scaffolds REVIEW_ME from template", () => {
72
72
 
73
73
  test("review complete creates Q&A with review content", () => {
74
74
  const { cwd, schubRoot } = createRepo();
75
- const changeId = "C002_review-complete";
75
+ const changeId = "C0002_review-complete";
76
76
  const changeTitle = "Review Complete";
77
77
  seedChange(schubRoot, changeId, changeTitle);
78
78
 
@@ -171,7 +171,7 @@ export const runReviewCreate = (args: string[], startDir: string) => {
171
171
 
172
172
  if (!isValidChangeId(trimmedId)) {
173
173
  throw new Error(
174
- `Invalid change-id '${options.changeId}'. Use kebab-case or a C-prefixed id (e.g., C001_add-user-auth).`,
174
+ `Invalid change-id '${options.changeId}'. Use kebab-case or a C-prefixed id (e.g., C0001_add-user-auth).`,
175
175
  );
176
176
  }
177
177
 
@@ -49,12 +49,12 @@ const writeExistingTask = (schubRoot: string, status: string, id: string, slug:
49
49
  test("tasks create scaffolds multiple task files", () => {
50
50
  const { repoRoot, cwd } = createRepo();
51
51
  const schubRoot = join(repoRoot, ".schub");
52
- seedProposal(schubRoot, "C001_update-cli");
53
- writeExistingTask(schubRoot, "backlog", "T003", "existing-task");
52
+ seedProposal(schubRoot, "C0001_update-cli");
53
+ writeExistingTask(schubRoot, "backlog", "T0003", "existing-task");
54
54
 
55
55
  const { result, stdout } = runTasksCreate(cwd, [
56
56
  "--change-id",
57
- "C001_update-cli",
57
+ "C0001_update-cli",
58
58
  "--status",
59
59
  "ready",
60
60
  "--title",
@@ -65,8 +65,8 @@ test("tasks create scaffolds multiple task files", () => {
65
65
 
66
66
  expect(result.exitCode).toBe(0);
67
67
 
68
- const firstPath = join(schubRoot, "tasks", "ready", "T004_first-task.md");
69
- const secondPath = join(schubRoot, "tasks", "ready", "T005_second-task.md");
68
+ const firstPath = join(schubRoot, "tasks", "ready", "T0004_first-task.md");
69
+ const secondPath = join(schubRoot, "tasks", "ready", "T0005_second-task.md");
70
70
 
71
71
  expect(existsSync(firstPath)).toBe(true);
72
72
  expect(existsSync(secondPath)).toBe(true);
@@ -74,13 +74,13 @@ test("tasks create scaffolds multiple task files", () => {
74
74
  const firstContent = readFileSync(firstPath, "utf8");
75
75
  const template = readFileSync(taskTemplatePath, "utf8");
76
76
  const expectedFirst = template
77
- .replace("{{TASK_ID}}", "T004")
77
+ .replace("{{TASK_ID}}", "T0004")
78
78
  .replace("{{TASK_TITLE}}", "First Task")
79
- .replace("{{CHANGE_ID}}", "C001_update-cli");
79
+ .replace("{{CHANGE_ID}}", "C0001_update-cli");
80
80
  const expectedSecond = template
81
- .replace("{{TASK_ID}}", "T005")
81
+ .replace("{{TASK_ID}}", "T0005")
82
82
  .replace("{{TASK_TITLE}}", "Second: Task!")
83
- .replace("{{CHANGE_ID}}", "C001_update-cli");
83
+ .replace("{{CHANGE_ID}}", "C0001_update-cli");
84
84
 
85
85
  expect(firstContent).toBe(expectedFirst);
86
86
  expect(readFileSync(secondPath, "utf8")).toBe(expectedSecond);
@@ -92,7 +92,7 @@ test("tasks create scaffolds multiple task files", () => {
92
92
  test("tasks create prefers local template overrides", () => {
93
93
  const { repoRoot, cwd } = createRepo();
94
94
  const schubRoot = join(repoRoot, ".schub");
95
- seedProposal(schubRoot, "C001_update-cli");
95
+ seedProposal(schubRoot, "C0001_update-cli");
96
96
 
97
97
  const localTemplateDir = join(schubRoot, "templates", "create-tasks");
98
98
  mkdirSync(localTemplateDir, { recursive: true });
@@ -101,7 +101,7 @@ test("tasks create prefers local template overrides", () => {
101
101
 
102
102
  const { result } = runTasksCreate(cwd, [
103
103
  "--change-id",
104
- "C001_update-cli",
104
+ "C0001_update-cli",
105
105
  "--status",
106
106
  "ready",
107
107
  "--title",
@@ -110,12 +110,12 @@ test("tasks create prefers local template overrides", () => {
110
110
 
111
111
  expect(result.exitCode).toBe(0);
112
112
 
113
- const taskPath = join(schubRoot, "tasks", "ready", "T001_local-task.md");
113
+ const taskPath = join(schubRoot, "tasks", "ready", "T0001_local-task.md");
114
114
  const content = readFileSync(taskPath, "utf8");
115
115
  const expected = localTemplate
116
- .replace("{{TASK_ID}}", "T001")
116
+ .replace("{{TASK_ID}}", "T0001")
117
117
  .replace("{{TASK_TITLE}}", "Local Task")
118
- .replace("{{CHANGE_ID}}", "C001_update-cli");
118
+ .replace("{{CHANGE_ID}}", "C0001_update-cli");
119
119
 
120
120
  expect(content).toBe(expected);
121
121
  });
@@ -123,9 +123,9 @@ test("tasks create prefers local template overrides", () => {
123
123
  test("tasks create rejects invalid status", () => {
124
124
  const { repoRoot, cwd } = createRepo();
125
125
  const schubRoot = join(repoRoot, ".schub");
126
- seedProposal(schubRoot, "C001_update-cli");
126
+ seedProposal(schubRoot, "C0001_update-cli");
127
127
 
128
- const { result, stderr } = runTasksCreate(cwd, ["--change-id", "C001_update-cli", "--status", "bad status"]);
128
+ const { result, stderr } = runTasksCreate(cwd, ["--change-id", "C0001_update-cli", "--status", "bad status"]);
129
129
 
130
130
  expect(result.exitCode).not.toBe(0);
131
131
  expect(stderr).toContain("Invalid status");
@@ -136,7 +136,7 @@ test("tasks create rejects missing proposal", () => {
136
136
  const schubRoot = join(repoRoot, ".schub");
137
137
  mkdirSync(join(schubRoot, "changes"), { recursive: true });
138
138
 
139
- const { result, stderr } = runTasksCreate(cwd, ["--change-id", "C001_missing", "--title", "New Task"]);
139
+ const { result, stderr } = runTasksCreate(cwd, ["--change-id", "C0001_missing", "--title", "New Task"]);
140
140
 
141
141
  expect(result.exitCode).not.toBe(0);
142
142
  expect(stderr).toContain("Required change files missing");
@@ -145,9 +145,9 @@ test("tasks create rejects missing proposal", () => {
145
145
  test("tasks create rejects non-accepted proposals", () => {
146
146
  const { repoRoot, cwd } = createRepo();
147
147
  const schubRoot = join(repoRoot, ".schub");
148
- seedProposal(schubRoot, "C001_update-cli", "Draft");
148
+ seedProposal(schubRoot, "C0001_update-cli", "Draft");
149
149
 
150
- const { result, stderr } = runTasksCreate(cwd, ["--change-id", "C001_update-cli", "--title", "New Task"]);
150
+ const { result, stderr } = runTasksCreate(cwd, ["--change-id", "C0001_update-cli", "--title", "New Task"]);
151
151
 
152
152
  expect(result.exitCode).not.toBe(0);
153
153
  expect(stderr).toContain("Mark the proposal as Accepted");
@@ -156,11 +156,11 @@ test("tasks create rejects non-accepted proposals", () => {
156
156
  test("tasks create rejects schub root flags", () => {
157
157
  const { repoRoot, cwd } = createRepo();
158
158
  const schubRoot = join(repoRoot, ".schub");
159
- seedProposal(schubRoot, "C001_update-cli");
159
+ seedProposal(schubRoot, "C0001_update-cli");
160
160
 
161
161
  const { result, stderr } = runTasksCreate(cwd, [
162
162
  "--change-id",
163
- "C001_update-cli",
163
+ "C0001_update-cli",
164
164
  "--title",
165
165
  "New Task",
166
166
  "--schub-root",
@@ -48,44 +48,44 @@ const writeTask = (tasksRoot: string, status: string, id: string, slug: string,
48
48
 
49
49
  test("tasks list shows tasks sorted by id", () => {
50
50
  const { cwd, tasksRoot } = createTaskRepo();
51
- writeTask(tasksRoot, "ready", "T010", "later-task");
52
- writeTask(tasksRoot, "backlog", "T002", "middle-task");
53
- writeTask(tasksRoot, "archived", "T003", "archived-task");
54
- writeTask(tasksRoot, "wip", "T001", "first-task");
51
+ writeTask(tasksRoot, "ready", "T0010", "later-task");
52
+ writeTask(tasksRoot, "backlog", "T0002", "middle-task");
53
+ writeTask(tasksRoot, "archived", "T0003", "archived-task");
54
+ writeTask(tasksRoot, "wip", "T0001", "first-task");
55
55
 
56
56
  const { result, stdout } = runCli(cwd);
57
57
 
58
58
  expect(result.exitCode).toBe(0);
59
59
  const lines = stdout.trim().split("\n").filter(Boolean);
60
60
  expect(lines).toHaveLength(4);
61
- expect(lines[0]).toContain("T001");
61
+ expect(lines[0]).toContain("T0001");
62
62
  expect(lines[0]).toContain("first task");
63
63
  expect(lines[0]).toContain("(wip)");
64
- expect(lines[1]).toContain("T002");
65
- expect(lines[2]).toContain("T003");
66
- expect(lines[3]).toContain("T010");
64
+ expect(lines[1]).toContain("T0002");
65
+ expect(lines[2]).toContain("T0003");
66
+ expect(lines[3]).toContain("T0010");
67
67
  });
68
68
 
69
69
  test("tasks list filters by status", () => {
70
70
  const { cwd, tasksRoot } = createTaskRepo();
71
- writeTask(tasksRoot, "ready", "T010", "later-task");
72
- writeTask(tasksRoot, "backlog", "T002", "middle-task");
73
- writeTask(tasksRoot, "wip", "T001", "first-task");
71
+ writeTask(tasksRoot, "ready", "T0010", "later-task");
72
+ writeTask(tasksRoot, "backlog", "T0002", "middle-task");
73
+ writeTask(tasksRoot, "wip", "T0001", "first-task");
74
74
 
75
75
  const { result, stdout } = runCli(cwd, ["--status", "ready,wip"]);
76
76
 
77
77
  expect(result.exitCode).toBe(0);
78
78
  const lines = stdout.trim().split("\n").filter(Boolean);
79
79
  expect(lines).toHaveLength(2);
80
- expect(lines.join("\n")).toContain("T010");
81
- expect(lines.join("\n")).toContain("T001");
82
- expect(lines.join("\n")).not.toContain("T002");
80
+ expect(lines.join("\n")).toContain("T0010");
81
+ expect(lines.join("\n")).toContain("T0001");
82
+ expect(lines.join("\n")).not.toContain("T0002");
83
83
  });
84
84
 
85
85
  test("tasks list supports json output", () => {
86
86
  const { cwd, tasksRoot } = createTaskRepo();
87
- writeTask(tasksRoot, "ready", "T010", "later-task");
88
- writeTask(tasksRoot, "wip", "T001", "first-task");
87
+ writeTask(tasksRoot, "ready", "T0010", "later-task");
88
+ writeTask(tasksRoot, "wip", "T0001", "first-task");
89
89
 
90
90
  const { result, stdout } = runCli(cwd, ["--json"]);
91
91
 
@@ -97,11 +97,11 @@ test("tasks list supports json output", () => {
97
97
  path: string;
98
98
  }>;
99
99
  expect(tasks[0]).toMatchObject({
100
- id: "T001",
100
+ id: "T0001",
101
101
  title: "first task",
102
102
  status: "wip",
103
103
  });
104
- expect(tasks[0].path).toContain(".schub/tasks/wip/T001_first-task.md");
104
+ expect(tasks[0].path).toContain(".schub/tasks/wip/T0001_first-task.md");
105
105
  });
106
106
 
107
107
  test("tasks list shows checklist counts in text output", () => {
@@ -109,10 +109,10 @@ test("tasks list shows checklist counts in text output", () => {
109
109
  writeTask(
110
110
  tasksRoot,
111
111
  "wip",
112
- "T001",
112
+ "T0001",
113
113
  "first-task",
114
114
  [
115
- "# Task: T001 First task",
115
+ "# Task: T0001 First task",
116
116
  "",
117
117
  "## Steps",
118
118
  "- [ ] Draft outline",
@@ -123,17 +123,17 @@ test("tasks list shows checklist counts in text output", () => {
123
123
  "",
124
124
  ].join("\n"),
125
125
  );
126
- writeTask(tasksRoot, "backlog", "T002", "second-task");
126
+ writeTask(tasksRoot, "backlog", "T0002", "second-task");
127
127
 
128
128
  const { result, stdout } = runCli(cwd);
129
129
 
130
130
  expect(result.exitCode).toBe(0);
131
131
  const lines = stdout.trim().split("\n").filter(Boolean);
132
132
  expect(lines).toHaveLength(2);
133
- expect(lines[0]).toContain("T001");
133
+ expect(lines[0]).toContain("T0001");
134
134
  expect(lines[0]).toContain("(wip)");
135
135
  expect(lines[0]).toContain("(1/2)");
136
- expect(lines[1]).toContain("T002");
136
+ expect(lines[1]).toContain("T0002");
137
137
  expect(lines[1]).not.toMatch(/\(\d+\/\d+\)/);
138
138
  });
139
139
 
@@ -142,11 +142,11 @@ test("tasks list json includes checklist counts", () => {
142
142
  writeTask(
143
143
  tasksRoot,
144
144
  "ready",
145
- "T001",
145
+ "T0001",
146
146
  "first-task",
147
- ["# Task: T001 First task", "", "## Steps", "- [ ] Draft outline", "- [x] Review outline", ""].join("\n"),
147
+ ["# Task: T0001 First task", "", "## Steps", "- [ ] Draft outline", "- [x] Review outline", ""].join("\n"),
148
148
  );
149
- writeTask(tasksRoot, "ready", "T002", "second-task");
149
+ writeTask(tasksRoot, "ready", "T0002", "second-task");
150
150
 
151
151
  const { result, stdout } = runCli(cwd, ["--json"]);
152
152
 
@@ -160,7 +160,7 @@ test("tasks list json includes checklist counts", () => {
160
160
  checklistTotal?: number;
161
161
  }>;
162
162
  expect(tasks[0]).toMatchObject({
163
- id: "T001",
163
+ id: "T0001",
164
164
  checklistRemaining: 1,
165
165
  checklistTotal: 2,
166
166
  });