@dezkareid/osddt 1.10.1 → 1.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/AGENTS.md CHANGED
@@ -82,7 +82,6 @@ Always prefer use exact versions for dependencies. Do not use `^` or `~`.
82
82
 
83
83
  #### Linting & Formatting
84
84
  - **ESLint**: `9.39.2`
85
- - **Prettier**: `3.8.1`
86
85
 
87
86
  #### Type Definitions
88
87
  - **@types/node**: `25.0.10`
@@ -155,10 +154,20 @@ The selected agents are saved in `.osddtrc` alongside `repoType`. When `osddt up
155
154
  | `npx @dezkareid/osddt setup --agents <list> --repo-type <type>` | External | Non-interactive setup (for CI/scripted environments) |
156
155
  | `osddt meta-info` | Local dev | Output current branch and date as JSON |
157
156
  | `npx @dezkareid/osddt meta-info` | External | Output current branch and date as JSON |
158
- | `osddt done <feature-name> --dir <project-path>` | Local dev | Move `working-on/<feature>` to `done/<feature>` |
159
- | `npx @dezkareid/osddt done <feature-name> --dir <project-path>` | External | Move `working-on/<feature>` to `done/<feature>` |
160
- | `osddt update` | Local dev | Regenerate agent command files from the existing `.osddtrc` |
161
- | `npx @dezkareid/osddt update` | External | Regenerate agent command files from the existing `.osddtrc` |
157
+ | `osddt done <feature-name> --dir <project-path>` | Local dev | Move `working-on/<feature>` to `done/<feature>` |
158
+ | `npx @dezkareid/osddt done <feature-name> --dir <project-path>` | External | Move `working-on/<feature>` to `done/<feature>` |
159
+ | `osddt done <feature-name> --dir <project-path> --worktree` | Local dev | Archive feature, remove git worktree, and clean state file |
160
+ | `npx @dezkareid/osddt done <feature-name> --dir <project-path> --worktree` | External | Archive feature, remove git worktree, and clean state file |
161
+ | `osddt update` | Local dev | Regenerate agent command files from the existing `.osddtrc` |
162
+ | `npx @dezkareid/osddt update` | External | Regenerate agent command files from the existing `.osddtrc` |
163
+ | `osddt start-worktree <feature-name>` | Local dev | Create a git worktree for a feature and scaffold working-on/ |
164
+ | `npx @dezkareid/osddt start-worktree <feature-name>` | External | Create a git worktree for a feature and scaffold working-on/ |
165
+ | `osddt start-worktree <feature-name> --dir <package-path>` | Local dev | Same, specifying the package path in a monorepo |
166
+ | `npx @dezkareid/osddt start-worktree <feature-name> --dir <package-path>` | External | Same, specifying the package path in a monorepo |
167
+ | `osddt worktree-info <feature-name>` | Local dev | Look up worktree paths for a feature (JSON output) |
168
+ | `npx @dezkareid/osddt worktree-info <feature-name>` | External | Look up worktree paths for a feature (JSON output) |
169
+ | `osddt setup-worktree` | Local dev | Validate environment for git worktree usage |
170
+ | `npx @dezkareid/osddt setup-worktree` | External | Validate environment for git worktree usage |
162
171
 
163
172
  #### `osddt setup` options
164
173
 
@@ -178,8 +187,9 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
178
187
  | ------------------ | ------------------------------------------------------------------ |
179
188
  | `osddt.continue` | Detect the current workflow phase and prompt the next command |
180
189
  | `osddt.research` | Research a topic and write a research file to inform the spec |
181
- | `osddt.start` | Start a new feature by creating a branch and working-on folder |
182
- | `osddt.spec` | Analyze requirements and write a feature specification |
190
+ | `osddt.start` | Start a new feature by creating a branch and working-on folder |
191
+ | `osddt.start-worktree` | Start a new feature using a git worktree for parallel development |
192
+ | `osddt.spec` | Analyze requirements and write a feature specification |
183
193
  | `osddt.clarify` | Resolve open questions in the spec and record decisions (optional) |
184
194
  | `osddt.plan` | Create a technical implementation plan from a specification |
185
195
  | `osddt.tasks` | Generate actionable tasks from an implementation plan |
@@ -249,6 +259,35 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
249
259
  /osddt.done
250
260
  ```
251
261
 
262
+ ##### Example D — Parallel feature workflow (git worktree)
263
+
264
+ Use `osddt.start-worktree` to work on multiple features simultaneously. Each feature gets its own isolated directory (a git worktree), so you can keep multiple editor windows open without switching branches.
265
+
266
+ Before using worktrees for the first time, validate your environment:
267
+
268
+ ```bash
269
+ npx @dezkareid/osddt setup-worktree
270
+ ```
271
+
272
+ Then start a feature in its own worktree:
273
+
274
+ ```
275
+ /osddt.start-worktree add-payment-gateway
276
+ /osddt.spec
277
+ /osddt.plan use Stripe SDK, REST endpoints
278
+ /osddt.tasks
279
+ /osddt.implement
280
+ /osddt.done
281
+ ```
282
+
283
+ The worktree is created as a sibling of the repo root (e.g. `../my-repo-add-payment-gateway`). Active worktrees are tracked in `.osddt-worktrees` (sibling to the repo root) — a JSON array with entries containing `featureName`, `branch`, `worktreePath`, `workingDir`, and `repoRoot`. `osddt.continue` and `osddt.done` read this file to resolve the correct paths. When `osddt.done` runs, it archives the planning folder **and** removes the worktree automatically.
284
+
285
+ You can customise the worktree base directory via `worktreeBase` in `.osddtrc`:
286
+
287
+ ```json
288
+ { "repoType": "single", "worktreeBase": "/Users/me/worktrees" }
289
+ ```
290
+
252
291
  ---
253
292
 
254
293
  - Use **`osddt.research`** when you want to explore the codebase and gather findings before writing the spec. It creates the `working-on/<feature-name>/` folder and writes `osddt.research.md`.
@@ -304,6 +343,17 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
304
343
  4. Records all new answers in a `## Decisions` section of `osddt.spec.md` and removes each answered question from the **Open Questions** section (removing the section heading too if it becomes empty).
305
344
  5. Always prompts the user to run (or re-run) `osddt.plan` to reflect the updated decisions.
306
345
 
346
+ #### osddt.start-worktree behaviour
347
+
348
+ - **Input**: Either a human-readable feature description or an existing branch name.
349
+ - **Branch name resolution**: same rules as `osddt.start`.
350
+ - **Actions performed by the agent**:
351
+ 1. Derives the branch and feature name.
352
+ 2. Runs `npx @dezkareid/osddt start-worktree <feature-name> [--dir <package-path>]`.
353
+ 3. The CLI creates the git worktree as a sibling of the repo root (`../<repo-name>-<feature-name>`, overridable via `worktreeBase` in `.osddtrc`), scaffolds `working-on/<feature-name>/` inside the worktree, and writes an entry to `.osddt-worktrees` (sibling to repo root).
354
+ 4. Reports the branch, worktree path, and working directory.
355
+ 5. Next step: `/osddt.spec`.
356
+
307
357
  #### osddt.done behaviour
308
358
 
309
359
  - **Input**: None — the feature is identified automatically.
@@ -311,7 +361,9 @@ Templates are generated by `npx @dezkareid/osddt setup` and placed in each agent
311
361
  1. Reads `.osddtrc` to resolve the project path (single vs monorepo). For monorepos, asks the user which package.
312
362
  2. Lists all folders under `working-on/`. If there is only one, uses it automatically; if there are multiple, asks the user to pick one.
313
363
  3. Confirms all tasks in `osddt.tasks.md` are checked off (`- [x]`).
314
- 4. Runs `npx @dezkareid/osddt done <feature-name> --dir <project-path>` to move the folder.
364
+ 4. Runs `npx @dezkareid/osddt worktree-info <feature-name>` to check if this is a worktree feature.
365
+ - If found: runs `npx @dezkareid/osddt done <feature-name> --dir <project-path> --worktree` (archives folder, removes worktree, cleans state file).
366
+ - If not found: runs `npx @dezkareid/osddt done <feature-name> --dir <project-path>` (existing behaviour).
315
367
 
316
368
  Note: the `osddt done` CLI command does **not** read `.osddtrc` — project path resolution is the agent's responsibility, handled in step 1 above.
317
369
 
package/README.md CHANGED
@@ -3,13 +3,18 @@ Other spec driven development tool but for monorepo
3
3
 
4
4
  ## CLI Commands
5
5
 
6
- | Command | Description |
7
- | -------------------------------------------------------------------- | ------------------------------------------------------------- |
8
- | `@dezkareid/osddt setup` | Generate agent command files for Claude and Gemini |
9
- | `@dezkareid/osddt setup --agents <list> --repo-type <type>` | Non-interactive setup (for CI/scripted environments) |
10
- | `@dezkareid/osddt meta-info` | Output current branch and date as JSON |
11
- | `@dezkareid/osddt done <feature-name> --dir <project-path>` | Move `working-on/<feature>` to `done/<feature>` |
12
- | `@dezkareid/osddt update` | Regenerate agent command files from the existing `.osddtrc` |
6
+ | Command | Description |
7
+ | -------------------------------------------------------------------------------- | ------------------------------------------------------------- |
8
+ | `@dezkareid/osddt setup` | Generate agent command files for Claude and Gemini |
9
+ | `@dezkareid/osddt setup --agents <list> --repo-type <type>` | Non-interactive setup (for CI/scripted environments) |
10
+ | `@dezkareid/osddt meta-info` | Output current branch and date as JSON |
11
+ | `@dezkareid/osddt done <feature-name> --dir <project-path>` | Move `working-on/<feature>` to `done/<feature>` |
12
+ | `@dezkareid/osddt done <feature-name> --dir <project-path> --worktree` | Archive feature, remove git worktree, and clean state file |
13
+ | `@dezkareid/osddt update` | Regenerate agent command files from the existing `.osddtrc` |
14
+ | `@dezkareid/osddt start-worktree <feature-name>` | Create a git worktree for a feature and scaffold working-on/ |
15
+ | `@dezkareid/osddt start-worktree <feature-name> --dir <package-path>` | Same, specifying the package path in a monorepo |
16
+ | `@dezkareid/osddt worktree-info <feature-name>` | Look up worktree paths for a feature (JSON output) |
17
+ | `@dezkareid/osddt setup-worktree` | Validate environment for git worktree usage |
13
18
 
14
19
  ### `osddt setup` options
15
20
 
@@ -106,6 +111,40 @@ flowchart LR
106
111
 
107
112
  > Review the `## Assumptions` section of `osddt.plan.md` before implementing. Run `/osddt.clarify` if any Open Questions in the spec need resolving first.
108
113
 
114
+ #### Parallel feature workflow (git worktree)
115
+
116
+ Use `osddt.start-worktree` when you want to work on multiple features simultaneously. Each feature gets its own isolated directory so you can keep multiple editor windows open without switching branches.
117
+
118
+ Before using worktrees for the first time, validate your environment:
119
+
120
+ ```bash
121
+ npx @dezkareid/osddt setup-worktree
122
+ ```
123
+
124
+ Then start a feature in its own worktree:
125
+
126
+ ```
127
+ /osddt.start-worktree add-payment-gateway
128
+ /osddt.spec
129
+ /osddt.plan use Stripe SDK, REST endpoints
130
+ /osddt.tasks
131
+ /osddt.implement
132
+ /osddt.done
133
+ ```
134
+
135
+ The worktree is created as a sibling of your repo (e.g. `../my-repo-add-payment-gateway`). The `working-on/` planning folder lives inside the worktree alongside the code. When `osddt.done` runs, it archives the planning folder **and** removes the worktree automatically.
136
+
137
+ You can customise the worktree base directory by adding `worktreeBase` to `.osddtrc`:
138
+
139
+ ```json
140
+ {
141
+ "repoType": "single",
142
+ "worktreeBase": "/Users/me/worktrees"
143
+ }
144
+ ```
145
+
146
+ Active worktrees are tracked in `.osddt-worktrees` (sibling to the repo root), a JSON array with entries containing `featureName`, `branch`, `worktreePath`, `workingDir`, and `repoRoot`. This file is the single source of truth used by `osddt.continue` and `osddt.done` to resolve the correct paths.
147
+
109
148
  #### Resuming after closing a session
110
149
 
111
150
  ```
@@ -118,18 +157,19 @@ flowchart LR
118
157
  /osddt.done
119
158
  ```
120
159
 
121
- | Template | Description |
122
- | ------------------ | ------------------------------------------------------------------ |
123
- | `osddt.continue` | Detect the current workflow phase and prompt the next command |
124
- | `osddt.research` | Research a topic and write a research file to inform the spec |
125
- | `osddt.start` | Start a new feature by creating a branch and working-on folder |
126
- | `osddt.spec` | Analyze requirements and write a feature specification |
127
- | `osddt.clarify` | Resolve open questions in the spec and record decisions (optional) |
128
- | `osddt.plan` | Create a technical implementation plan from a specification |
129
- | `osddt.tasks` | Generate actionable tasks from an implementation plan |
130
- | `osddt.implement` | Execute tasks from the task list one by one |
131
- | `osddt.fast` | Bootstrap all planning artifacts (spec, plan, tasks) in one shot |
132
- | `osddt.done` | Resolve project path, verify tasks, and move the feature to done |
160
+ | Template | Description |
161
+ | ----------------------- | ------------------------------------------------------------------ |
162
+ | `osddt.continue` | Detect the current workflow phase and prompt the next command |
163
+ | `osddt.research` | Research a topic and write a research file to inform the spec |
164
+ | `osddt.start` | Start a new feature by creating a branch and working-on folder |
165
+ | `osddt.start-worktree` | Start a new feature using a git worktree for parallel development |
166
+ | `osddt.spec` | Analyze requirements and write a feature specification |
167
+ | `osddt.clarify` | Resolve open questions in the spec and record decisions (optional) |
168
+ | `osddt.plan` | Create a technical implementation plan from a specification |
169
+ | `osddt.tasks` | Generate actionable tasks from an implementation plan |
170
+ | `osddt.implement` | Execute tasks from the task list one by one |
171
+ | `osddt.fast` | Bootstrap all planning artifacts (spec, plan, tasks) in one shot |
172
+ | `osddt.done` | Resolve project path, verify tasks, and move the feature to done |
133
173
 
134
174
  Generated files are placed in:
135
175
 
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function setupWorktreeCommand(): Command;
@@ -0,0 +1,9 @@
1
+ import { Command } from 'commander';
2
+ export interface WorktreeEntry {
3
+ featureName: string;
4
+ branch: string;
5
+ worktreePath: string;
6
+ workingDir: string;
7
+ repoRoot: string;
8
+ }
9
+ export declare function startWorktreeCommand(): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function worktreeInfoCommand(): Command;
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'url';
6
6
  import select from '@inquirer/select';
7
7
  import checkbox from '@inquirer/checkbox';
8
8
  import { execSync } from 'child_process';
9
+ import readline from 'readline';
9
10
 
10
11
  function getRepoPreamble(npxCommand) {
11
12
  return `## Context
@@ -121,6 +122,15 @@ const COMMAND_DEFINITIONS = [
121
122
 
122
123
  ## Instructions
123
124
 
125
+ Before checking the working directory, run the following command to check whether this feature uses a git worktree:
126
+
127
+ \`\`\`
128
+ ${npxCommand} worktree-info <feature-name>
129
+ \`\`\`
130
+
131
+ - If it exits with code **0**: parse the JSON output and use the returned \`workingDir\` as \`<project-path>/working-on/<feature-name>\`. Skip the main-tree scan below.
132
+ - If it exits with code **1**: the feature is not a worktree feature. Use the standard project path from \`.osddtrc\` and scan the main tree as usual.
133
+
124
134
  Check the working directory \`<project-path>/working-on/<feature-name>\` for the files listed below **in order** to determine the current phase. Use the first matching condition:
125
135
 
126
136
  | Condition | Current phase | Run next |
@@ -222,6 +232,45 @@ ${getCustomContextStep(npxCommand, 'start')}## Arguments
222
232
 
223
233
  ${args}
224
234
 
235
+ ${getNextStepToSpec(args)}
236
+ `,
237
+ },
238
+ {
239
+ name: 'osddt.start-worktree',
240
+ description: 'Start a new feature using a git worktree for parallel development',
241
+ body: ({ args, npxCommand }) => `${getRepoPreamble(npxCommand)}## Instructions
242
+
243
+ The argument provided is: ${args}
244
+
245
+ Determine the branch name using the following logic:
246
+
247
+ 1. If ${args} looks like a branch name (e.g. \`feat/my-feature\`, \`fix/some-bug\`, \`my-feature-branch\` — no spaces, kebab-case or slash-separated), use it as-is.
248
+ 2. Otherwise treat ${args} as a human-readable feature description, convert it to a feature name, and use the format \`feat/<derived-name>\` as the branch name.
249
+
250
+ Apply the constraints below to the feature name (the segment after the last \`/\`) before using it:
251
+
252
+ ${FEATURE_NAME_RULES}
253
+
254
+ Once the branch name is determined:
255
+
256
+ 3. Run the following command to create the git worktree, scaffold the working directory, and register the feature in the state file:
257
+
258
+ \`\`\`
259
+ ${npxCommand} start-worktree <feature-name>
260
+ \`\`\`
261
+
262
+ For monorepos, pass the package path:
263
+
264
+ \`\`\`
265
+ ${npxCommand} start-worktree <feature-name> --dir <package-path>
266
+ \`\`\`
267
+
268
+ 4. Report the branch name, worktree path, and working directory shown in the command output.
269
+
270
+ ${getCustomContextStep(npxCommand, 'start')}## Arguments
271
+
272
+ ${args}
273
+
225
274
  ${getNextStepToSpec(args)}
226
275
  `,
227
276
  },
@@ -502,16 +551,53 @@ ${args}
502
551
  body: ({ npxCommand }) => `## Instructions
503
552
 
504
553
  1. Confirm all tasks in \`osddt.tasks.md\` are checked off (\`- [x]\`)
505
- 2. Run the following command to move the feature folder from \`working-on\` to \`done\`:
554
+ 2. Run the following command to check whether this feature uses a git worktree:
506
555
 
507
556
  \`\`\`
508
- ${npxCommand} done <feature-name> --dir <project-path>
557
+ ${npxCommand} worktree-info <feature-name>
509
558
  \`\`\`
510
559
 
511
- The command will automatically prefix the destination folder name with today's date in \`YYYY-MM-DD\` format.
560
+ 3. Based on the result:
561
+
562
+ **If it exits with code 1 (standard feature):** use the project path from \`.osddtrc\`, then run:
563
+ \`\`\`
564
+ ${npxCommand} done <feature-name> --dir <project-path>
565
+ \`\`\`
566
+ Skip to step 8.
567
+
568
+ **If it exits with code 0 (worktree feature):** parse the JSON to get \`worktreePath\` and \`branch\`, derive \`<project-path>\` from \`workingDir\`, then continue below.
569
+
570
+ 4. Check for uncommitted changes inside the worktree:
571
+
572
+ \`\`\`
573
+ git -C <worktreePath> status --porcelain
574
+ \`\`\`
575
+
576
+ 5. If there are **uncommitted changes**:
577
+ 1. Run \`git -C <worktreePath> diff\` to inspect them.
578
+ 2. Derive a concise commit message in **conventional commit** format (e.g. \`feat: add payment gateway integration\`) based on the diff.
579
+ 3. Present the proposed message to the user: _"Use this commit message, or provide your own?"_
580
+ 4. Once confirmed, commit:
581
+ \`\`\`
582
+ git -C <worktreePath> add -A
583
+ git -C <worktreePath> commit -m "<confirmed-message>"
584
+ \`\`\`
585
+
586
+ 6. Push the branch to remote (covers both first push and subsequent pushes):
587
+
588
+ \`\`\`
589
+ git -C <worktreePath> push --set-upstream origin <branch>
590
+ \`\`\`
591
+
592
+ 7. Run the done command with the \`--worktree\` flag:
593
+ \`\`\`
594
+ ${npxCommand} done <feature-name> --dir <project-path> --worktree
595
+ \`\`\`
596
+
597
+ 8. The command will automatically prefix the destination folder name with today's date in \`YYYY-MM-DD\` format.
512
598
  For example, \`working-on/feature-a\` will be moved to \`done/YYYY-MM-DD-feature-a\`.
513
599
 
514
- 3. Report the result of the command, including the full destination path
600
+ 9. Report the result of the command, including the full destination path
515
601
 
516
602
  ${getCustomContextStep(npxCommand, 'done')}`,
517
603
  },
@@ -527,7 +613,7 @@ ${body}`;
527
613
  }
528
614
  function getClaudeTemplates(cwd, npxCommand) {
529
615
  const dir = path.join(cwd, CLAUDE_COMMANDS_DIR);
530
- return COMMAND_DEFINITIONS.map((cmd) => ({
616
+ return COMMAND_DEFINITIONS.map(cmd => ({
531
617
  filePath: path.join(dir, `${cmd.name}.md`),
532
618
  content: formatClaudeCommand(cmd.description, cmd.body({ args: '$ARGUMENTS', npxCommand })),
533
619
  }));
@@ -543,7 +629,7 @@ ${body}"""
543
629
  }
544
630
  function getGeminiTemplates(cwd, npxCommand) {
545
631
  const dir = path.join(cwd, GEMINI_COMMANDS_DIR);
546
- return COMMAND_DEFINITIONS.map((cmd) => ({
632
+ return COMMAND_DEFINITIONS.map(cmd => ({
547
633
  filePath: path.join(dir, `${cmd.name}.toml`),
548
634
  content: formatGeminiCommand(cmd.description, cmd.body({ args: '{{args}}', npxCommand })),
549
635
  }));
@@ -608,12 +694,12 @@ async function resolveNpxCommand(cwd) {
608
694
  const VALID_AGENTS = ['claude', 'gemini'];
609
695
  const VALID_REPO_TYPES = ['single', 'monorepo'];
610
696
  function parseAgents(raw) {
611
- const values = raw.split(',').map((s) => s.trim());
697
+ const values = raw.split(',').map(s => s.trim());
612
698
  if (values.length === 0) {
613
699
  console.error('Error: --agents requires at least one value.');
614
700
  process.exit(1);
615
701
  }
616
- const invalid = values.filter((v) => !VALID_AGENTS.includes(v));
702
+ const invalid = values.filter(v => !VALID_AGENTS.includes(v));
617
703
  if (invalid.length > 0) {
618
704
  console.error(`Error: Invalid agent(s): ${invalid.join(', ')}. Valid values: ${VALID_AGENTS.join(', ')}.`);
619
705
  process.exit(1);
@@ -712,7 +798,13 @@ function todayPrefix() {
712
798
  const dd = String(now.getDate()).padStart(2, '0');
713
799
  return `${yyyy}-${mm}-${dd}`;
714
800
  }
715
- async function runDone(featureName, cwd) {
801
+ function resolveRepoRoot$2(cwd) {
802
+ return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
803
+ }
804
+ function stateFilePath$2(repoRoot) {
805
+ return path.join(path.dirname(repoRoot), '.osddt-worktrees');
806
+ }
807
+ async function runDone(featureName, cwd, worktree) {
716
808
  const src = path.join(cwd, 'working-on', featureName);
717
809
  const destName = `${todayPrefix()}-${featureName}`;
718
810
  const dest = path.join(cwd, 'done', destName);
@@ -723,6 +815,30 @@ async function runDone(featureName, cwd) {
723
815
  await fs.ensureDir(path.dirname(dest));
724
816
  await fs.move(src, dest);
725
817
  console.log(`Moved: working-on/${featureName} → done/${destName}`);
818
+ if (!worktree)
819
+ return;
820
+ const repoRoot = resolveRepoRoot$2(process.cwd());
821
+ const stateFile = stateFilePath$2(repoRoot);
822
+ if (!(await fs.pathExists(stateFile))) {
823
+ console.error(`Warning: .osddt-worktrees not found at ${stateFile}. Skipping worktree cleanup.`);
824
+ return;
825
+ }
826
+ const entries = await fs.readJson(stateFile);
827
+ const entry = entries.find(e => e.featureName === featureName);
828
+ if (!entry) {
829
+ console.error(`Warning: No worktree entry found for "${featureName}". Skipping worktree cleanup.`);
830
+ return;
831
+ }
832
+ if (await fs.pathExists(entry.worktreePath)) {
833
+ execSync(`git worktree remove "${entry.worktreePath}" --force`, { cwd: repoRoot, stdio: 'inherit' });
834
+ console.log(`Removed worktree: ${entry.worktreePath}`);
835
+ }
836
+ else {
837
+ console.log(`Worktree path not found on filesystem, skipping git worktree remove.`);
838
+ }
839
+ const updated = entries.filter(e => e.featureName !== featureName);
840
+ await fs.writeJson(stateFile, updated, { spaces: 2 });
841
+ console.log(`Removed state entry for "${featureName}" from .osddt-worktrees`);
726
842
  }
727
843
  function doneCommand() {
728
844
  const cmd = new Command('done');
@@ -730,9 +846,10 @@ function doneCommand() {
730
846
  .description('Move a feature from working-on/<feature-name> to done/<feature-name>')
731
847
  .argument('<feature-name>', 'name of the feature to mark as done')
732
848
  .option('-d, --dir <directory>', 'project directory', process.cwd())
849
+ .option('--worktree', 'also remove the git worktree and clean up the state file')
733
850
  .action(async (featureName, options) => {
734
851
  const targetDir = path.resolve(options.dir);
735
- await runDone(featureName, targetDir);
852
+ await runDone(featureName, targetDir, options.worktree ?? false);
736
853
  });
737
854
  return cmd;
738
855
  }
@@ -745,7 +862,7 @@ async function hasOsddtCommandFile(dir, pattern) {
745
862
  if (!(await fs.pathExists(dir)))
746
863
  return false;
747
864
  const entries = await fs.readdir(dir);
748
- return entries.some((f) => pattern.test(f));
865
+ return entries.some(f => pattern.test(f));
749
866
  }
750
867
  async function inferAgents(cwd) {
751
868
  const detected = [];
@@ -868,6 +985,269 @@ function contextCommand() {
868
985
  return cmd;
869
986
  }
870
987
 
988
+ function resolveRepoRoot$1(cwd) {
989
+ return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
990
+ }
991
+ function repoName(repoRoot) {
992
+ return path.basename(repoRoot);
993
+ }
994
+ function stateFilePath$1(repoRoot) {
995
+ return path.join(path.dirname(repoRoot), '.osddt-worktrees');
996
+ }
997
+ async function readStateFile(stateFile) {
998
+ if (!(await fs.pathExists(stateFile)))
999
+ return [];
1000
+ return fs.readJson(stateFile);
1001
+ }
1002
+ async function writeStateFile(stateFile, entries) {
1003
+ await fs.writeJson(stateFile, entries, { spaces: 2 });
1004
+ }
1005
+ function branchExists(branch, cwd) {
1006
+ try {
1007
+ execSync(`git rev-parse --verify ${branch}`, { cwd, stdio: 'ignore' });
1008
+ return true;
1009
+ }
1010
+ catch {
1011
+ return false;
1012
+ }
1013
+ }
1014
+ function remoteBranchExists(branch, cwd) {
1015
+ try {
1016
+ execSync(`git ls-remote --exit-code --heads origin ${branch}`, { cwd, stdio: 'ignore' });
1017
+ return true;
1018
+ }
1019
+ catch {
1020
+ return false;
1021
+ }
1022
+ }
1023
+ async function prompt(question) {
1024
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1025
+ return new Promise((resolve) => {
1026
+ rl.question(question, (answer) => {
1027
+ rl.close();
1028
+ resolve(answer.trim());
1029
+ });
1030
+ });
1031
+ }
1032
+ async function createWorktree(branch, worktreePath, repoRoot) {
1033
+ if (await fs.pathExists(worktreePath)) {
1034
+ console.log(`\nDirectory already exists at: ${worktreePath}`);
1035
+ const answer = await prompt('Resume or Abort? [R/a] ');
1036
+ if (answer.toLowerCase() === 'a') {
1037
+ console.log('Aborted.');
1038
+ process.exit(0);
1039
+ }
1040
+ return;
1041
+ }
1042
+ const localExists = branchExists(branch, repoRoot);
1043
+ const remoteExists = !localExists && remoteBranchExists(branch, repoRoot);
1044
+ if (localExists || remoteExists) {
1045
+ console.log(`\nBranch "${branch}" already exists ${localExists ? 'locally' : 'on remote'}.`);
1046
+ const answer = await prompt('Resume or Abort? [R/a] ');
1047
+ if (answer.toLowerCase() === 'a') {
1048
+ console.log('Aborted.');
1049
+ process.exit(0);
1050
+ }
1051
+ execSync(`git worktree add "${worktreePath}" ${branch}`, { cwd: repoRoot, stdio: 'inherit' });
1052
+ }
1053
+ else {
1054
+ execSync(`git worktree add "${worktreePath}" -b ${branch}`, { cwd: repoRoot, stdio: 'inherit' });
1055
+ }
1056
+ }
1057
+ async function runStartWorktree(featureName, options) {
1058
+ const cwd = process.cwd();
1059
+ const repoRoot = resolveRepoRoot$1(cwd);
1060
+ const branch = `feat/${featureName}`;
1061
+ // Read .osddtrc
1062
+ const rcPath = path.join(repoRoot, '.osddtrc');
1063
+ let rc = { repoType: 'single' };
1064
+ if (await fs.pathExists(rcPath)) {
1065
+ rc = await fs.readJson(rcPath);
1066
+ }
1067
+ // Resolve worktree path
1068
+ const base = rc.worktreeBase ?? path.dirname(repoRoot);
1069
+ const worktreePath = path.join(base, `${repoName(repoRoot)}-${featureName}`);
1070
+ // Check state file for existing entry
1071
+ const stateFile = stateFilePath$1(repoRoot);
1072
+ const entries = await readStateFile(stateFile);
1073
+ const existing = entries.find(e => e.featureName === featureName);
1074
+ if (existing) {
1075
+ console.log(`\nWorktree for "${featureName}" already exists at: ${existing.worktreePath}`);
1076
+ const answer = await prompt('Resume or Abort? [R/a] ');
1077
+ if (answer.toLowerCase() === 'a') {
1078
+ console.log('Aborted.');
1079
+ process.exit(0);
1080
+ }
1081
+ console.log(`\nResuming existing worktree.`);
1082
+ console.log(` Branch: ${existing.branch}`);
1083
+ console.log(` Worktree path: ${existing.worktreePath}`);
1084
+ console.log(` Working dir: ${existing.workingDir}`);
1085
+ return;
1086
+ }
1087
+ await createWorktree(branch, worktreePath, repoRoot);
1088
+ // Resolve working dir
1089
+ let projectPath;
1090
+ if (rc.repoType === 'monorepo') {
1091
+ const pkg = options.dir ?? await prompt('Package path (e.g. packages/my-package): ');
1092
+ projectPath = path.join(worktreePath, pkg);
1093
+ }
1094
+ else {
1095
+ projectPath = worktreePath;
1096
+ }
1097
+ const workingDir = path.join(projectPath, 'working-on', featureName);
1098
+ await fs.ensureDir(workingDir);
1099
+ // Write state file entry
1100
+ entries.push({ featureName, branch, worktreePath, workingDir, repoRoot });
1101
+ await writeStateFile(stateFile, entries);
1102
+ console.log(`\nWorktree feature started:`);
1103
+ console.log(` Branch: ${branch}`);
1104
+ console.log(` Worktree path: ${worktreePath}`);
1105
+ console.log(` Working dir: ${workingDir}`);
1106
+ }
1107
+ function startWorktreeCommand() {
1108
+ const cmd = new Command('start-worktree');
1109
+ cmd
1110
+ .description('Start a new feature using a git worktree')
1111
+ .argument('<feature-name>', 'feature name (kebab-case, max 30 chars)')
1112
+ .option('-d, --dir <package>', 'package path within monorepo (skips prompt)')
1113
+ .action(async (featureName, options) => {
1114
+ await runStartWorktree(featureName, options);
1115
+ });
1116
+ return cmd;
1117
+ }
1118
+
1119
+ function resolveRepoRoot(cwd) {
1120
+ return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
1121
+ }
1122
+ function stateFilePath(repoRoot) {
1123
+ return path.join(path.dirname(repoRoot), '.osddt-worktrees');
1124
+ }
1125
+ async function runWorktreeInfo(featureName) {
1126
+ const repoRoot = resolveRepoRoot(process.cwd());
1127
+ const stateFile = stateFilePath(repoRoot);
1128
+ if (!(await fs.pathExists(stateFile))) {
1129
+ console.error(`No .osddt-worktrees file found at: ${stateFile}`);
1130
+ process.exit(1);
1131
+ }
1132
+ const entries = await fs.readJson(stateFile);
1133
+ const entry = entries.find(e => e.featureName === featureName);
1134
+ if (!entry) {
1135
+ console.error(`No worktree entry found for feature: ${featureName}`);
1136
+ process.exit(1);
1137
+ }
1138
+ console.log(JSON.stringify({ worktreePath: entry.worktreePath, workingDir: entry.workingDir, branch: entry.branch }));
1139
+ }
1140
+ function worktreeInfoCommand() {
1141
+ const cmd = new Command('worktree-info');
1142
+ cmd
1143
+ .description('Look up worktree paths for a feature from the state file')
1144
+ .argument('<feature-name>', 'feature name to look up')
1145
+ .action(async (featureName) => {
1146
+ await runWorktreeInfo(featureName);
1147
+ });
1148
+ return cmd;
1149
+ }
1150
+
1151
+ function checkGitVersion() {
1152
+ try {
1153
+ const output = execSync('git --version', { encoding: 'utf-8' }).trim();
1154
+ // e.g. "git version 2.39.3"
1155
+ const match = output.match(/git version (\d+)\.(\d+)/);
1156
+ if (!match) {
1157
+ return { label: 'Git version >= 2.5', passed: false, detail: `Could not parse git version: ${output}` };
1158
+ }
1159
+ const major = parseInt(match[1], 10);
1160
+ const minor = parseInt(match[2], 10);
1161
+ const ok = major > 2 || (major === 2 && minor >= 5);
1162
+ return {
1163
+ label: 'Git version >= 2.5',
1164
+ passed: ok,
1165
+ detail: ok ? output : `Found ${output} — git worktree requires >= 2.5`,
1166
+ };
1167
+ }
1168
+ catch {
1169
+ return { label: 'Git version >= 2.5', passed: false, detail: 'git not found or not executable' };
1170
+ }
1171
+ }
1172
+ function checkNotAWorktree(cwd) {
1173
+ try {
1174
+ const gitCommonDir = execSync('git rev-parse --git-common-dir', { cwd, encoding: 'utf-8' }).trim();
1175
+ const gitDir = execSync('git rev-parse --git-dir', { cwd, encoding: 'utf-8' }).trim();
1176
+ const isWorktree = gitDir !== gitCommonDir && gitDir !== '.git';
1177
+ return {
1178
+ label: 'Current directory is not a worktree',
1179
+ passed: !isWorktree,
1180
+ detail: isWorktree
1181
+ ? `This directory is itself a worktree (git-dir: ${gitDir}). Run setup-worktree from the main repository.`
1182
+ : 'OK',
1183
+ };
1184
+ }
1185
+ catch {
1186
+ return { label: 'Current directory is not a worktree', passed: false, detail: 'Not inside a git repository' };
1187
+ }
1188
+ }
1189
+ async function checkTargetWritable(cwd) {
1190
+ let targetBase;
1191
+ try {
1192
+ const repoRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim();
1193
+ const rcPath = path.join(repoRoot, '.osddtrc');
1194
+ if (await fs.pathExists(rcPath)) {
1195
+ const rc = await fs.readJson(rcPath);
1196
+ targetBase = rc.worktreeBase ?? path.dirname(repoRoot);
1197
+ }
1198
+ else {
1199
+ targetBase = path.dirname(repoRoot);
1200
+ }
1201
+ }
1202
+ catch {
1203
+ return { label: 'Worktree target directory is writable', passed: false, detail: 'Could not resolve repo root' };
1204
+ }
1205
+ try {
1206
+ await fs.access(targetBase, fs.constants.W_OK);
1207
+ return { label: 'Worktree target directory is writable', passed: true, detail: `${targetBase} is writable` };
1208
+ }
1209
+ catch {
1210
+ return { label: 'Worktree target directory is writable', passed: false, detail: `${targetBase} is not writable` };
1211
+ }
1212
+ }
1213
+ function printResult(result) {
1214
+ const icon = result.passed ? '✓' : '✗';
1215
+ console.log(` ${icon} ${result.label}`);
1216
+ if (!result.passed) {
1217
+ console.log(` → ${result.detail}`);
1218
+ }
1219
+ }
1220
+ async function runSetupWorktree(cwd) {
1221
+ console.log('Checking environment for git worktree support...\n');
1222
+ const results = [
1223
+ checkGitVersion(),
1224
+ checkNotAWorktree(cwd),
1225
+ await checkTargetWritable(cwd),
1226
+ ];
1227
+ for (const result of results) {
1228
+ printResult(result);
1229
+ }
1230
+ const allPassed = results.every(r => r.passed);
1231
+ console.log('');
1232
+ if (allPassed) {
1233
+ console.log('All checks passed. You can use the worktree workflow.');
1234
+ }
1235
+ else {
1236
+ console.log('Some checks failed. Resolve the issues above before using the worktree workflow.');
1237
+ process.exit(1);
1238
+ }
1239
+ }
1240
+ function setupWorktreeCommand() {
1241
+ const cmd = new Command('setup-worktree');
1242
+ cmd
1243
+ .description('Validate the environment for git worktree usage')
1244
+ .option('-d, --dir <directory>', 'directory to check', process.cwd())
1245
+ .action(async (options) => {
1246
+ await runSetupWorktree(path.resolve(options.dir));
1247
+ });
1248
+ return cmd;
1249
+ }
1250
+
871
1251
  const __filename$1 = fileURLToPath(import.meta.url);
872
1252
  const __dirname$1 = path.dirname(__filename$1);
873
1253
  const pkgPath = path.join(__dirname$1, '..', 'package.json');
@@ -882,4 +1262,7 @@ program.addCommand(metaInfoCommand());
882
1262
  program.addCommand(doneCommand());
883
1263
  program.addCommand(updateCommand());
884
1264
  program.addCommand(contextCommand());
1265
+ program.addCommand(startWorktreeCommand());
1266
+ program.addCommand(worktreeInfoCommand());
1267
+ program.addCommand(setupWorktreeCommand());
885
1268
  program.parse(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dezkareid/osddt",
3
- "version": "1.10.1",
3
+ "version": "1.11.0",
4
4
  "description": "Package for Spec-Driven Development workflow",
5
5
  "keywords": [
6
6
  "spec-driven",
@@ -42,13 +42,12 @@
42
42
  "devDependencies": {
43
43
  "@commitlint/cli": "20.5.0",
44
44
  "@commitlint/config-conventional": "20.5.0",
45
- "@dezkareid/eslint-config-ts-base": "1.0.0",
45
+ "@dezkareid/eslint-plugin-web": "1.0.0",
46
46
  "@rollup/plugin-typescript": "12.1.4",
47
47
  "@types/fs-extra": "11.0.4",
48
48
  "@types/node": "25.0.10",
49
49
  "eslint": "9.39.2",
50
50
  "husky": "9.1.7",
51
- "prettier": "3.8.1",
52
51
  "rollup": "4.56.0",
53
52
  "typescript": "5.9.3",
54
53
  "vitest": "4.0.18"
@@ -59,7 +58,7 @@
59
58
  "test": "vitest run",
60
59
  "test:watch": "vitest",
61
60
  "lint": "eslint src",
62
- "format": "prettier --write src",
61
+ "lint:fix": "eslint src --fix",
63
62
  "setup-local": "pnpm run build && npx osddt setup --agents claude --repo-type single",
64
63
  "update-local": "pnpm run build && npx osddt update"
65
64
  }
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};