@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.
- package/cli/src/commands/complete.ts +42 -2
- package/cli/src/commands/init.ts +1 -1
- package/cli/src/observer/data.ts +49 -15
- package/cli/src/observer/renderer.ts +11 -0
- package/cli/src/observer/watcher.ts +10 -5
- package/package.json +1 -1
- package/skills/deploy/SKILL.md +1 -1
- package/skills/deploy/stages/merge.md +23 -13
- package/skills/wrap-up/SKILL.md +9 -34
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import
|
|
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
|
+
}
|
package/cli/src/commands/init.ts
CHANGED
|
@@ -154,7 +154,7 @@ export function initCommand(options: {
|
|
|
154
154
|
version: 1,
|
|
155
155
|
slug,
|
|
156
156
|
branch,
|
|
157
|
-
started: new Date().toISOString()
|
|
157
|
+
started: new Date().toISOString(),
|
|
158
158
|
mode: modeLabel,
|
|
159
159
|
...(classification && { classification }),
|
|
160
160
|
status: "in-progress",
|
package/cli/src/observer/data.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
|
180
|
-
const
|
|
181
|
-
if (
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
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:
|
|
187
|
-
completedAt:
|
|
188
|
-
phases:
|
|
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
|
|
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
|
-
|
|
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
|
|
31
|
-
if (!fs.existsSync(
|
|
30
|
+
const stateDir = path.join(worktreeRoot, ".work-kit");
|
|
31
|
+
if (!fs.existsSync(stateDir)) return;
|
|
32
32
|
|
|
33
33
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
//
|
|
48
|
+
// Directory might not exist yet
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
|
package/package.json
CHANGED
package/skills/deploy/SKILL.md
CHANGED
|
@@ -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
|
|
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.
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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)
|
package/skills/wrap-up/SKILL.md
CHANGED
|
@@ -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. **
|
|
17
|
-
3. **
|
|
18
|
-
4. **
|
|
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
|
|
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
|
|
82
|
+
5. Report: summary written, worktree removed, branch deleted, done.
|