@abdullahsahmad/work-kit 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.
@@ -1,9 +1,11 @@
1
- import { readState, writeState, findWorktreeRoot } from "../state/store.js";
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { readState, writeState, findWorktreeRoot, readStateMd } from "../state/store.js";
2
4
  import { isPhaseComplete } from "../engine/transitions.js";
3
5
  import { checkLoopback } from "../engine/loopbacks.js";
4
6
  import { PHASE_ORDER } from "../config/phases.js";
5
7
  import { parseLocation, resetToLocation } from "../state/helpers.js";
6
- import type { Action, PhaseName } from "../state/schema.js";
8
+ import type { Action, PhaseName, WorkKitState } from "../state/schema.js";
7
9
 
8
10
  export function completeCommand(target: string, outcome?: string, worktreeRoot?: string): Action {
9
11
  const root = worktreeRoot || findWorktreeRoot();
@@ -101,6 +103,7 @@ export function completeCommand(target: string, outcome?: string, worktreeRoot?:
101
103
  state.currentPhase = null;
102
104
  state.currentSubStage = null;
103
105
  writeState(root, state);
106
+ archiveCompleted(root, state);
104
107
  return { action: "complete", message: "All phases complete. Work-kit finished." };
105
108
  }
106
109
 
@@ -121,3 +124,40 @@ export function completeCommand(target: string, outcome?: string, worktreeRoot?:
121
124
  message: `${phase}/${subStage} complete${outcome ? ` (outcome: ${outcome})` : ""}. Run \`npx work-kit next\` to continue.`,
122
125
  };
123
126
  }
127
+
128
+ // ── Archive on completion ──────────────────────────────────────────
129
+
130
+ function archiveCompleted(worktreeRoot: string, state: WorkKitState): void {
131
+ const mainRoot = state.metadata.mainRepoRoot || worktreeRoot;
132
+ const date = new Date().toISOString().split("T")[0];
133
+ const slug = state.slug;
134
+ const wkDir = path.join(mainRoot, ".claude", "work-kit");
135
+ const archiveDir = path.join(wkDir, "archive");
136
+
137
+ // Ensure directories exist
138
+ fs.mkdirSync(archiveDir, { recursive: true });
139
+
140
+ // Archive state.md
141
+ const stateMd = readStateMd(worktreeRoot);
142
+ if (stateMd) {
143
+ const archivePath = path.join(archiveDir, `${date}-${slug}.md`);
144
+ fs.writeFileSync(archivePath, stateMd, "utf-8");
145
+ }
146
+
147
+ // Compute completed phases
148
+ const completedPhases = PHASE_ORDER
149
+ .filter(p => state.phases[p].status === "completed")
150
+ .join("→");
151
+
152
+ // Append to index.md
153
+ const indexPath = path.join(wkDir, "index.md");
154
+ let indexContent = "";
155
+ if (fs.existsSync(indexPath)) {
156
+ indexContent = fs.readFileSync(indexPath, "utf-8");
157
+ }
158
+ if (!indexContent.includes("| Date ")) {
159
+ indexContent = "| Date | Slug | PR | Status | Phases |\n| --- | --- | --- | --- | --- |\n";
160
+ }
161
+ indexContent += `| ${date} | ${slug} | n/a | completed | ${completedPhases} |\n`;
162
+ fs.writeFileSync(indexPath, indexContent, "utf-8");
163
+ }
@@ -154,7 +154,7 @@ export function initCommand(options: {
154
154
  version: 1,
155
155
  slug,
156
156
  branch,
157
- started: new Date().toISOString().split("T")[0],
157
+ started: new Date().toISOString(),
158
158
  mode: modeLabel,
159
159
  ...(classification && { classification }),
160
160
  status: "in-progress",
@@ -101,16 +101,18 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
101
101
 
102
102
  const subStageKeys = Object.keys(phase.subStages);
103
103
  if (subStageKeys.length === 0) {
104
- // Use default substages
104
+ // Use default substages — skip entirely skipped phases
105
+ if (phase.status === "skipped") continue;
105
106
  const defaults = SUBSTAGES_BY_PHASE[phaseName] || [];
106
107
  total += defaults.length;
107
108
  if (phase.status === "completed") completed += defaults.length;
108
- else if (phase.status === "skipped") completed += defaults.length;
109
109
  } else {
110
110
  for (const key of subStageKeys) {
111
- total++;
112
111
  const sub = phase.subStages[key];
113
- if (sub.status === "completed" || sub.status === "skipped") {
112
+ // Skip excluded substages they shouldn't affect progress
113
+ if (sub.status === "skipped") continue;
114
+ total++;
115
+ if (sub.status === "completed") {
114
116
  completed++;
115
117
  }
116
118
  }
@@ -172,20 +174,34 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
172
174
 
173
175
  const items: CompletedItemView[] = [];
174
176
  // Parse markdown table or list entries
175
- // Expected format: | slug | PR | date | phases |
177
+ // Format: | Date | Slug | PR | Status | Phases |
176
178
  // or list format: - slug (#PR) - date - phases
177
179
  const lines = content.split("\n");
178
180
  for (const line of lines) {
179
- // Try table format: | slug | #PR | date | phases |
180
- const tableMatch = line.match(/^\|\s*(.+?)\s*\|\s*(#?\d+)?\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
181
- if (tableMatch) {
182
- const slug = tableMatch[1].trim();
183
- if (slug === "Slug" || slug === "---" || slug.startsWith("-")) continue; // skip header
181
+ // Try 5-column table: | Date | Slug | PR | Status | Phases |
182
+ const table5Match = line.match(/^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
183
+ if (table5Match) {
184
+ const col1 = table5Match[1].trim();
185
+ if (col1 === "Date" || col1 === "---" || col1.startsWith("-")) continue; // skip header
186
+ items.push({
187
+ slug: table5Match[2].trim(),
188
+ pr: table5Match[3].trim() !== "n/a" ? table5Match[3].trim() : undefined,
189
+ completedAt: col1,
190
+ phases: table5Match[5].trim(),
191
+ });
192
+ continue;
193
+ }
194
+
195
+ // Try 4-column table: | slug | PR | date | phases |
196
+ const table4Match = line.match(/^\|\s*(.+?)\s*\|\s*(#?\d+)?\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
197
+ if (table4Match) {
198
+ const slug = table4Match[1].trim();
199
+ if (slug === "Slug" || slug === "---" || slug.startsWith("-")) continue;
184
200
  items.push({
185
201
  slug,
186
- pr: tableMatch[2]?.trim() || undefined,
187
- completedAt: tableMatch[3].trim(),
188
- phases: tableMatch[4].trim(),
202
+ pr: table4Match[2]?.trim() || undefined,
203
+ completedAt: table4Match[3].trim(),
204
+ phases: table4Match[4].trim(),
189
205
  });
190
206
  continue;
191
207
  }
@@ -210,10 +226,22 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
210
226
  export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: string[]): DashboardData {
211
227
  const worktrees = cachedWorktrees ?? discoverWorktrees(mainRepoRoot);
212
228
  const activeItems: WorkItemView[] = [];
229
+ const completedFromWorktrees: CompletedItemView[] = [];
213
230
 
214
231
  for (const wt of worktrees) {
215
232
  const item = collectWorkItem(wt);
216
- if (item && item.status !== "completed") {
233
+ if (!item) continue;
234
+ if (item.status === "completed") {
235
+ const phaseNames = item.phases
236
+ .filter(p => p.status === "completed")
237
+ .map(p => p.name)
238
+ .join("→");
239
+ completedFromWorktrees.push({
240
+ slug: item.slug,
241
+ completedAt: item.startedAt,
242
+ phases: phaseNames,
243
+ });
244
+ } else {
217
245
  activeItems.push(item);
218
246
  }
219
247
  }
@@ -227,7 +255,13 @@ export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: str
227
255
  return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
228
256
  });
229
257
 
230
- const completedItems = collectCompletedItems(mainRepoRoot);
258
+ // Merge completed items from worktrees and index file, dedup by slug
259
+ const indexItems = collectCompletedItems(mainRepoRoot);
260
+ const seen = new Set(completedFromWorktrees.map(i => i.slug));
261
+ const completedItems = [
262
+ ...completedFromWorktrees,
263
+ ...indexItems.filter(i => !seen.has(i.slug)),
264
+ ];
231
265
 
232
266
  return {
233
267
  activeItems,
@@ -8,12 +8,23 @@ function formatTimeAgo(dateStr: string): string {
8
8
  const then = new Date(dateStr).getTime();
9
9
  if (isNaN(then)) return "unknown";
10
10
 
11
+ // If only a date (no time component), show the date string as-is
12
+ // to avoid misleading hour-level precision
13
+ const isDateOnly = /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
14
+
11
15
  const diffMs = now - then;
12
16
  const minutes = Math.floor(diffMs / 60000);
13
17
  const hours = Math.floor(diffMs / 3600000);
14
18
  const days = Math.floor(diffMs / 86400000);
15
19
  const weeks = Math.floor(days / 7);
16
20
 
21
+ if (isDateOnly) {
22
+ if (days < 1) return "today";
23
+ if (days === 1) return "yesterday";
24
+ if (days < 7) return `${days}d ago`;
25
+ return `${weeks}w ago`;
26
+ }
27
+
17
28
  if (minutes < 1) return "just now";
18
29
  if (minutes < 60) return `${minutes}m ago`;
19
30
  if (hours < 24) {
@@ -27,12 +27,17 @@ export function startWatching(
27
27
 
28
28
  function watchStateFile(worktreeRoot: string): void {
29
29
  if (watchers.has(worktreeRoot)) return;
30
- const stateFile = path.join(worktreeRoot, ".work-kit", "state.json");
31
- if (!fs.existsSync(stateFile)) return;
30
+ const stateDir = path.join(worktreeRoot, ".work-kit");
31
+ if (!fs.existsSync(stateDir)) return;
32
32
 
33
33
  try {
34
- const watcher = fs.watch(stateFile, { persistent: false }, () => {
35
- debouncedUpdate();
34
+ // Watch the directory, not the file writeState uses atomic
35
+ // rename (write tmp + rename), which replaces the inode and
36
+ // breaks fs.watch on the file on Linux.
37
+ const watcher = fs.watch(stateDir, { persistent: false }, (_event, filename) => {
38
+ if (filename === "state.json") {
39
+ debouncedUpdate();
40
+ }
36
41
  });
37
42
  watcher.on("error", () => {
38
43
  watcher.close();
@@ -40,7 +45,7 @@ export function startWatching(
40
45
  });
41
46
  watchers.set(worktreeRoot, watcher);
42
47
  } catch {
43
- // File might not exist yet
48
+ // Directory might not exist yet
44
49
  }
45
50
  }
46
51
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abdullahsahmad/work-kit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Structured development workflow for Claude Code. Two modes, 6 phases, 27 sub-stages.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@ For each sub-stage:
23
23
 
24
24
  ## Key Principle
25
25
 
26
- **Verify before acting, monitor after acting.** Never merge without checking CI. Never walk away after merge without checking deployment.
26
+ **Verify before acting, monitor after acting.** Never merge with failing CI. Merge is automatic after review approval — no user confirmation needed. Never walk away after merge without checking deployment.
27
27
 
28
28
  ## Recording
29
29
 
@@ -9,16 +9,26 @@ description: "Deploy sub-stage: Get the PR merged safely."
9
9
 
10
10
  ## Instructions
11
11
 
12
- 1. Check CI status on the PR all checks must pass
13
- 2. Check for merge conflicts — resolve if any
14
- 3. Rebase on main if the branch is behind
15
- 4. Post a readiness summary as a PR comment:
16
- - Tests: pass/fail
17
- - Review: approved
18
- - Conflicts: none/resolved
19
- - Risk: low/medium/high
20
- 5. **Prepare a merge readiness summary** and return it to the orchestrator for user approval — don't auto-merge
21
- 6. Once the orchestrator confirms user approval, merge using the project's preferred method (check CONTRIBUTING.md or README for merge preferences; default to squash if unspecified)
12
+ 1. Determine the default branch (`main` or `master`):
13
+ ```bash
14
+ git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main"
15
+ ```
16
+ 2. Sync the feature branch with the default branch:
17
+ ```bash
18
+ git fetch origin
19
+ git rebase origin/<default-branch>
20
+ ```
21
+ If rebase conflicts occur, resolve them. If they're non-trivial, report to the user and abort.
22
+ 3. If a PR exists, check CI status — all checks must pass. If CI fails, diagnose and fix (loop back to Build if needed).
23
+ 4. Merge automatically using the project's preferred method (check CONTRIBUTING.md or README; default to squash):
24
+ - If a PR exists: merge via `gh pr merge --squash --delete-branch`
25
+ - If no PR: merge locally:
26
+ ```bash
27
+ git checkout <default-branch>
28
+ git merge --squash feature/<slug>
29
+ git commit -m "feat: <slug>"
30
+ git branch -d feature/<slug>
31
+ ```
22
32
 
23
33
  ## Output (append to state.md)
24
34
 
@@ -40,8 +50,8 @@ description: "Deploy sub-stage: Get the PR merged safely."
40
50
 
41
51
  ## Rules
42
52
 
43
- - NEVER force push to main
53
+ - NEVER force push to main/master
44
54
  - NEVER merge with failing CI
45
- - ALWAYS return to the orchestrator for user approval before merging
46
55
  - If CI fails, diagnose the issue — don't just retry
47
- - If conflicts are non-trivial, explain them to the user before resolving
56
+ - If rebase conflicts are non-trivial, explain them to the user before resolving
57
+ - Merge is automatic — do NOT ask the user for permission (review phase already approved it)
@@ -12,12 +12,12 @@ allowed-tools: Bash, Read, Write, Edit, Glob, Grep
12
12
 
13
13
  ## Instructions
14
14
 
15
+ > **Note:** Archiving state.md and appending to `.claude/work-kit/index.md` are handled automatically by the CLI when you run `work-kit complete` on the last sub-stage. You do NOT need to do these manually.
16
+
15
17
  1. **Read the full `.work-kit/state.md`** — every phase output from Plan through the last completed phase
16
- 2. **Archive the full state** — copy `.work-kit/state.md` to `.claude/work-kit/archive/<YYYY-MM-DD>-<slug>.md` on the **main branch** (this is the unedited, complete record)
17
- 3. **Synthesize the work-kit log entry** not a copy-paste of state, but a distilled record that a future developer (or agent) would find useful
18
- 4. **Write the summary file** to `.claude/work-kit/<date>-<slug>.md` on the **main branch** (not the worktree)
19
- 5. **Append to the index** in `.claude/work-kit/index.md`
20
- 6. **Ask the user** if they want the worktree removed
18
+ 2. **Synthesize the work-kit log entry** — not a copy-paste of state, but a distilled record that a future developer (or agent) would find useful
19
+ 3. **Write the summary file** to `.claude/work-kit/<date>-<slug>.md` on the **main branch** (not the worktree)
20
+ 4. **Ask the user** if they want the worktree and branch removed
21
21
 
22
22
  ## Work-Kit Log Entry Format
23
23
 
@@ -48,14 +48,6 @@ status: <completed | partial | rolled-back>
48
48
  - <what changed and why>
49
49
  ```
50
50
 
51
- ## Index Entry
52
-
53
- Append one row to `.claude/work-kit/index.md`:
54
-
55
- ```
56
- | <YYYY-MM-DD> | <slug> | <#PR or n/a> | <completed/partial/rolled-back> | <phases completed, e.g. "all 6" or "plan→review (no deploy)"> |
57
- ```
58
-
59
51
  ## What to Include vs. Exclude
60
52
 
61
53
  **Include:**
@@ -71,27 +63,9 @@ Append one row to `.claude/work-kit/index.md`:
71
63
  - Internal process notes ("ran tests 3 times before they passed")
72
64
  - Anything derivable from the git diff or PR description
73
65
 
74
- ## Archive
75
-
76
- The archive is the full, unedited copy of `.work-kit/state.md` — every phase output, every decision, every deviation. It exists so you can always go back to the complete record.
77
-
78
- When archiving, prepend a reference header to the copy so the archive knows where its summary lives:
79
-
80
- ```markdown
81
- > **Summary:** [<YYYY-MM-DD>-<slug>](../<YYYY-MM-DD>-<slug>.md)
82
-
83
- <rest of state.md content>
84
- ```
85
-
86
- ```bash
87
- # From the worktree, copy state to the archive on main
88
- cp .work-kit/state.md <main-repo-root>/.claude/work-kit/archive/<YYYY-MM-DD>-<slug>.md
89
- # Then prepend the summary reference to the archive file
90
- ```
91
-
92
66
  ## Cleanup
93
67
 
94
- After writing the archive, summary, and index:
68
+ After writing the summary:
95
69
 
96
70
  1. Switch to main branch: `cd` back to the main repo root
97
71
  2. Stage and commit all work-kit log files:
@@ -99,9 +73,10 @@ After writing the archive, summary, and index:
99
73
  git add .claude/work-kit/
100
74
  git commit -m "work-kit: <slug>"
101
75
  ```
102
- 3. Ask the user: "Remove the worktree `worktrees/<slug>`?"
76
+ 3. Ask the user: "Remove the worktree `worktrees/<slug>` and delete branch `feature/<slug>`?"
103
77
  4. If yes:
104
78
  ```bash
105
79
  git worktree remove worktrees/<slug>
80
+ git branch -d feature/<slug>
106
81
  ```
107
- 5. Report: summary written, worktree status, done.
82
+ 5. Report: summary written, worktree removed, branch deleted, done.