taskplane 0.28.4 → 0.28.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -215
  3. package/bin/gitignore-patterns.mjs +79 -79
  4. package/bin/rpc-wrapper.mjs +1086 -1086
  5. package/bin/taskplane.mjs +3254 -3254
  6. package/dashboard/public/app.js +2573 -2573
  7. package/dashboard/public/index.html +139 -139
  8. package/dashboard/public/style.css +1882 -1882
  9. package/dashboard/public/taskplane-word-color.svg +18 -18
  10. package/dashboard/public/taskplane-word-white.svg +18 -18
  11. package/dashboard/server.cjs +1666 -1666
  12. package/extensions/reviewer-extension.ts +119 -119
  13. package/extensions/task-orchestrator.ts +28 -28
  14. package/extensions/taskplane/abort.ts +502 -502
  15. package/extensions/taskplane/agent-bridge-extension.ts +838 -765
  16. package/extensions/taskplane/agent-host.ts +833 -745
  17. package/extensions/taskplane/cleanup.ts +747 -747
  18. package/extensions/taskplane/config-loader.ts +1328 -1322
  19. package/extensions/taskplane/config-schema.ts +692 -682
  20. package/extensions/taskplane/config.ts +73 -73
  21. package/extensions/taskplane/context-window.ts +66 -66
  22. package/extensions/taskplane/diagnostic-reports.ts +463 -463
  23. package/extensions/taskplane/diagnostics.ts +385 -385
  24. package/extensions/taskplane/engine-worker-entry.mjs +34 -34
  25. package/extensions/taskplane/engine-worker.ts +381 -381
  26. package/extensions/taskplane/engine.ts +4539 -4527
  27. package/extensions/taskplane/execution.ts +2733 -2708
  28. package/extensions/taskplane/extension.ts +30 -9
  29. package/extensions/taskplane/formatting.ts +773 -773
  30. package/extensions/taskplane/git.ts +90 -90
  31. package/extensions/taskplane/index.ts +28 -28
  32. package/extensions/taskplane/lane-runner.ts +1383 -1360
  33. package/extensions/taskplane/mailbox.ts +689 -689
  34. package/extensions/taskplane/merge.ts +3135 -3135
  35. package/extensions/taskplane/messages.ts +985 -985
  36. package/extensions/taskplane/migrations.ts +278 -278
  37. package/extensions/taskplane/naming.ts +117 -117
  38. package/extensions/taskplane/path-resolver.ts +237 -237
  39. package/extensions/taskplane/persistence.ts +2087 -2087
  40. package/extensions/taskplane/process-registry.ts +416 -416
  41. package/extensions/taskplane/quality-gate.ts +1033 -1033
  42. package/extensions/taskplane/resume.ts +2879 -2878
  43. package/extensions/taskplane/sessions.ts +57 -57
  44. package/extensions/taskplane/settings-loader.ts +136 -136
  45. package/extensions/taskplane/settings-tui.ts +1867 -1867
  46. package/extensions/taskplane/sidecar-telemetry.ts +252 -252
  47. package/extensions/taskplane/supervisor-primer.md +1694 -1694
  48. package/extensions/taskplane/supervisor.ts +4341 -4341
  49. package/extensions/taskplane/task-executor-core.ts +550 -550
  50. package/extensions/taskplane/tmux-compat.ts +37 -37
  51. package/extensions/taskplane/types.ts +4297 -4278
  52. package/extensions/taskplane/verification.ts +542 -542
  53. package/extensions/taskplane/waves.ts +1548 -1548
  54. package/extensions/taskplane/workspace.ts +705 -705
  55. package/extensions/taskplane/worktree.ts +2604 -2505
  56. package/package.json +57 -57
  57. package/skills/create-taskplane-task/SKILL.md +465 -465
  58. package/skills/create-taskplane-task/references/prompt-template.md +285 -285
  59. package/templates/agents/local/supervisor.md +33 -33
  60. package/templates/agents/local/task-merger.md +27 -27
  61. package/templates/agents/local/task-reviewer.md +30 -30
  62. package/templates/agents/local/task-worker.md +34 -34
  63. package/templates/agents/supervisor-routing.md +92 -92
  64. package/templates/agents/supervisor.md +168 -168
  65. package/templates/agents/task-merger.md +214 -214
  66. package/templates/agents/task-reviewer.md +192 -192
  67. package/templates/agents/task-worker.md +505 -429
  68. package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -98
  69. package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -73
  70. package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -97
  71. package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -73
@@ -1,278 +1,278 @@
1
- /**
2
- * Additive Upgrade Migrations for Taskplane
3
- *
4
- * Provides a lightweight migration runner that applies additive-only
5
- * changes (e.g., creating missing scaffold files) when extensions load
6
- * or `/orch` starts. Migrations never overwrite existing files.
7
- *
8
- * Migration state is tracked in `.pi/taskplane.json` under the
9
- * `migrations` key, preserving all existing version-tracker fields.
10
- *
11
- * @module migrations
12
- * @since TP-063
13
- */
14
-
15
- import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
16
- import { join, dirname } from "path";
17
- import { fileURLToPath } from "url";
18
-
19
- // ── Types ────────────────────────────────────────────────────────────
20
-
21
- /**
22
- * Metadata for a single additive migration.
23
- */
24
- export interface Migration {
25
- /** Unique, stable identifier (e.g., "add-supervisor-local-template-v1") */
26
- id: string;
27
- /** Human-readable description for logs */
28
- description: string;
29
- /**
30
- * Execute the migration. Should only create files that don't exist.
31
- *
32
- * @param projectRoot - Project root directory
33
- * @param packageRoot - Taskplane package root (for template resolution)
34
- * @param configRoot - Config root directory (e.g., ".pi" in repo mode, "shared-libs/.taskplane" in workspace mode)
35
- * @returns A short message describing what was created, or null if skipped (already exists)
36
- * @throws If the migration cannot complete (e.g., missing template source)
37
- */
38
- run(projectRoot: string, packageRoot: string, configRoot: string): string | null;
39
- }
40
-
41
- /**
42
- * Record of a single applied migration in `.pi/taskplane.json`.
43
- */
44
- export interface AppliedMigration {
45
- /** ISO timestamp when the migration was applied */
46
- appliedAt: string;
47
- }
48
-
49
- /**
50
- * The `migrations` section within `.pi/taskplane.json`.
51
- */
52
- export interface MigrationState {
53
- applied: Record<string, AppliedMigration>;
54
- }
55
-
56
- /**
57
- * Shape of `.pi/taskplane.json` (partial — only fields we read/write).
58
- * Other fields (version, installedAt, lastUpgraded, components) are
59
- * preserved as-is during read-modify-write.
60
- */
61
- export interface TaskplaneMeta {
62
- [key: string]: unknown;
63
- migrations?: MigrationState;
64
- }
65
-
66
- /**
67
- * Result of running migrations.
68
- */
69
- export interface MigrationRunResult {
70
- /** Migration IDs that were applied in this run */
71
- applied: string[];
72
- /** Migration IDs that were skipped (already applied or target exists) */
73
- skipped: string[];
74
- /** Migrations that failed with errors (non-fatal — logged and skipped) */
75
- errors: Array<{ id: string; error: string }>;
76
- /** Human-readable messages for each applied migration */
77
- messages: string[];
78
- }
79
-
80
- // ── Meta File Helpers ────────────────────────────────────────────────
81
-
82
- const TASKPLANE_META_FILENAME = "taskplane.json";
83
-
84
- /**
85
- * Load `.pi/taskplane.json`, returning its content or an empty object
86
- * if the file doesn't exist or is malformed.
87
- *
88
- * Never throws — returns `{}` for any read/parse error.
89
- */
90
- export function loadTaskplaneMeta(projectRoot: string): TaskplaneMeta {
91
- const metaPath = join(projectRoot, ".pi", TASKPLANE_META_FILENAME);
92
- try {
93
- if (!existsSync(metaPath)) return {};
94
- const raw = readFileSync(metaPath, "utf-8");
95
- const parsed = JSON.parse(raw);
96
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
97
- return parsed as TaskplaneMeta;
98
- } catch {
99
- return {};
100
- }
101
- }
102
-
103
- /**
104
- * Save `.pi/taskplane.json`, merging the provided meta with any
105
- * existing content. Creates the `.pi/` directory if needed.
106
- *
107
- * Performs a shallow merge at the top level — existing keys not in
108
- * `meta` are preserved. The `migrations` key is always taken from
109
- * the provided `meta` object (deep replacement).
110
- */
111
- export function saveTaskplaneMeta(projectRoot: string, meta: TaskplaneMeta): void {
112
- const piDir = join(projectRoot, ".pi");
113
- mkdirSync(piDir, { recursive: true });
114
-
115
- const metaPath = join(piDir, TASKPLANE_META_FILENAME);
116
-
117
- // Read existing content to preserve version-tracker fields
118
- let existing: TaskplaneMeta = {};
119
- try {
120
- if (existsSync(metaPath)) {
121
- const raw = readFileSync(metaPath, "utf-8");
122
- const parsed = JSON.parse(raw);
123
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
124
- existing = parsed as TaskplaneMeta;
125
- }
126
- }
127
- } catch {
128
- // Existing file unreadable — start fresh but we'll overwrite only our keys
129
- }
130
-
131
- // Merge: existing fields preserved, our fields override
132
- const merged = { ...existing, ...meta };
133
- writeFileSync(metaPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
134
- }
135
-
136
- // ── Package Root Resolution ──────────────────────────────────────────
137
-
138
- /**
139
- * Resolve the taskplane package root directory.
140
- *
141
- * Uses ESM `import.meta.url` to compute the path deterministically.
142
- * The package root is two levels up from this file:
143
- * `<package-root>/extensions/taskplane/migrations.ts`
144
- *
145
- * @param importMetaUrl - Pass `import.meta.url` from the calling module
146
- * @returns Absolute path to the package root
147
- */
148
- export function resolvePackageRoot(importMetaUrl?: string): string {
149
- const url = importMetaUrl ?? import.meta.url;
150
- const thisDir = dirname(fileURLToPath(url));
151
- // extensions/taskplane/ → extensions/ → package root
152
- return join(thisDir, "..", "..");
153
- }
154
-
155
- // ── Migration Registry ──────────────────────────────────────────────
156
-
157
- /**
158
- * Registry of all additive migrations, ordered by creation date.
159
- *
160
- * New migrations are appended to this array. Each migration must:
161
- * - Have a unique, stable `id` (never renamed after release)
162
- * - Only create files that don't exist (additive-only)
163
- * - Throw on unrecoverable errors (e.g., missing template source)
164
- * - Return null if the target already exists (skip)
165
- */
166
- export const MIGRATION_REGISTRY: Migration[] = [
167
- {
168
- id: "add-supervisor-local-template-v1",
169
- description: "Create agents/supervisor.md from template if missing",
170
- run(projectRoot: string, packageRoot: string, configRoot: string): string | null {
171
- const targetPath = join(configRoot, "agents", "supervisor.md");
172
-
173
- // Skip if file already exists — never overwrite
174
- if (existsSync(targetPath)) {
175
- return null;
176
- }
177
-
178
- // Resolve template source
179
- const templatePath = join(packageRoot, "templates", "agents", "local", "supervisor.md");
180
- if (!existsSync(templatePath)) {
181
- throw new Error(
182
- `Migration template not found: ${templatePath}. ` +
183
- `This may indicate a packaging issue with the taskplane package.`,
184
- );
185
- }
186
-
187
- // Create target directory and copy template
188
- mkdirSync(dirname(targetPath), { recursive: true });
189
- copyFileSync(templatePath, targetPath);
190
-
191
- return `Created ${targetPath} from template`;
192
- },
193
- },
194
- ];
195
-
196
- // ── Migration Runner ─────────────────────────────────────────────────
197
-
198
- /**
199
- * Run all pending additive migrations.
200
- *
201
- * Loads migration state from `.pi/taskplane.json`, runs only unapplied
202
- * migrations from the registry, and persists applied IDs + timestamps.
203
- *
204
- * Each migration is individually try/caught:
205
- * - Success → recorded as applied, message logged
206
- * - Skip (returns null) → recorded as applied (target already exists)
207
- * - Error → logged and skipped (NOT recorded — will be retried next time)
208
- *
209
- * @param projectRoot - Project root directory
210
- * @param packageRoot - Taskplane package root (for template resolution).
211
- * If omitted, resolved from import.meta.url.
212
- * @returns Migration run result with applied/skipped/error details
213
- */
214
- export function runMigrations(
215
- projectRoot: string,
216
- packageRoot?: string,
217
- configRoot?: string,
218
- ): MigrationRunResult {
219
- const pkgRoot = packageRoot ?? resolvePackageRoot();
220
- const cfgRoot = configRoot ?? join(projectRoot, ".pi");
221
- const result: MigrationRunResult = {
222
- applied: [],
223
- skipped: [],
224
- errors: [],
225
- messages: [],
226
- };
227
-
228
- // Load current state
229
- const meta = loadTaskplaneMeta(projectRoot);
230
- const migrationState: MigrationState = meta.migrations ?? { applied: {} };
231
-
232
- let stateChanged = false;
233
-
234
- for (const migration of MIGRATION_REGISTRY) {
235
- // Skip already-applied migrations
236
- if (migrationState.applied[migration.id]) {
237
- result.skipped.push(migration.id);
238
- continue;
239
- }
240
-
241
- try {
242
- const message = migration.run(projectRoot, pkgRoot, cfgRoot);
243
-
244
- // Record as applied (whether it created something or skipped)
245
- migrationState.applied[migration.id] = {
246
- appliedAt: new Date().toISOString(),
247
- };
248
- stateChanged = true;
249
-
250
- if (message) {
251
- result.applied.push(migration.id);
252
- result.messages.push(`📦 Migration: ${message}`);
253
- } else {
254
- // Target already existed — still mark as applied so we don't recheck
255
- result.skipped.push(migration.id);
256
- }
257
- } catch (err: unknown) {
258
- const errMsg = err instanceof Error ? err.message : String(err);
259
- result.errors.push({ id: migration.id, error: errMsg });
260
- // NOT recorded as applied — will be retried next time
261
- }
262
- }
263
-
264
- // Persist state if anything changed
265
- if (stateChanged) {
266
- try {
267
- saveTaskplaneMeta(projectRoot, { ...meta, migrations: migrationState });
268
- } catch (err: unknown) {
269
- const errMsg = err instanceof Error ? err.message : String(err);
270
- result.errors.push({
271
- id: "__state_save",
272
- error: `Failed to persist migration state: ${errMsg}`,
273
- });
274
- }
275
- }
276
-
277
- return result;
278
- }
1
+ /**
2
+ * Additive Upgrade Migrations for Taskplane
3
+ *
4
+ * Provides a lightweight migration runner that applies additive-only
5
+ * changes (e.g., creating missing scaffold files) when extensions load
6
+ * or `/orch` starts. Migrations never overwrite existing files.
7
+ *
8
+ * Migration state is tracked in `.pi/taskplane.json` under the
9
+ * `migrations` key, preserving all existing version-tracker fields.
10
+ *
11
+ * @module migrations
12
+ * @since TP-063
13
+ */
14
+
15
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from "fs";
16
+ import { join, dirname } from "path";
17
+ import { fileURLToPath } from "url";
18
+
19
+ // ── Types ────────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Metadata for a single additive migration.
23
+ */
24
+ export interface Migration {
25
+ /** Unique, stable identifier (e.g., "add-supervisor-local-template-v1") */
26
+ id: string;
27
+ /** Human-readable description for logs */
28
+ description: string;
29
+ /**
30
+ * Execute the migration. Should only create files that don't exist.
31
+ *
32
+ * @param projectRoot - Project root directory
33
+ * @param packageRoot - Taskplane package root (for template resolution)
34
+ * @param configRoot - Config root directory (e.g., ".pi" in repo mode, "shared-libs/.taskplane" in workspace mode)
35
+ * @returns A short message describing what was created, or null if skipped (already exists)
36
+ * @throws If the migration cannot complete (e.g., missing template source)
37
+ */
38
+ run(projectRoot: string, packageRoot: string, configRoot: string): string | null;
39
+ }
40
+
41
+ /**
42
+ * Record of a single applied migration in `.pi/taskplane.json`.
43
+ */
44
+ export interface AppliedMigration {
45
+ /** ISO timestamp when the migration was applied */
46
+ appliedAt: string;
47
+ }
48
+
49
+ /**
50
+ * The `migrations` section within `.pi/taskplane.json`.
51
+ */
52
+ export interface MigrationState {
53
+ applied: Record<string, AppliedMigration>;
54
+ }
55
+
56
+ /**
57
+ * Shape of `.pi/taskplane.json` (partial — only fields we read/write).
58
+ * Other fields (version, installedAt, lastUpgraded, components) are
59
+ * preserved as-is during read-modify-write.
60
+ */
61
+ export interface TaskplaneMeta {
62
+ [key: string]: unknown;
63
+ migrations?: MigrationState;
64
+ }
65
+
66
+ /**
67
+ * Result of running migrations.
68
+ */
69
+ export interface MigrationRunResult {
70
+ /** Migration IDs that were applied in this run */
71
+ applied: string[];
72
+ /** Migration IDs that were skipped (already applied or target exists) */
73
+ skipped: string[];
74
+ /** Migrations that failed with errors (non-fatal — logged and skipped) */
75
+ errors: Array<{ id: string; error: string }>;
76
+ /** Human-readable messages for each applied migration */
77
+ messages: string[];
78
+ }
79
+
80
+ // ── Meta File Helpers ────────────────────────────────────────────────
81
+
82
+ const TASKPLANE_META_FILENAME = "taskplane.json";
83
+
84
+ /**
85
+ * Load `.pi/taskplane.json`, returning its content or an empty object
86
+ * if the file doesn't exist or is malformed.
87
+ *
88
+ * Never throws — returns `{}` for any read/parse error.
89
+ */
90
+ export function loadTaskplaneMeta(projectRoot: string): TaskplaneMeta {
91
+ const metaPath = join(projectRoot, ".pi", TASKPLANE_META_FILENAME);
92
+ try {
93
+ if (!existsSync(metaPath)) return {};
94
+ const raw = readFileSync(metaPath, "utf-8");
95
+ const parsed = JSON.parse(raw);
96
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
97
+ return parsed as TaskplaneMeta;
98
+ } catch {
99
+ return {};
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Save `.pi/taskplane.json`, merging the provided meta with any
105
+ * existing content. Creates the `.pi/` directory if needed.
106
+ *
107
+ * Performs a shallow merge at the top level — existing keys not in
108
+ * `meta` are preserved. The `migrations` key is always taken from
109
+ * the provided `meta` object (deep replacement).
110
+ */
111
+ export function saveTaskplaneMeta(projectRoot: string, meta: TaskplaneMeta): void {
112
+ const piDir = join(projectRoot, ".pi");
113
+ mkdirSync(piDir, { recursive: true });
114
+
115
+ const metaPath = join(piDir, TASKPLANE_META_FILENAME);
116
+
117
+ // Read existing content to preserve version-tracker fields
118
+ let existing: TaskplaneMeta = {};
119
+ try {
120
+ if (existsSync(metaPath)) {
121
+ const raw = readFileSync(metaPath, "utf-8");
122
+ const parsed = JSON.parse(raw);
123
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
124
+ existing = parsed as TaskplaneMeta;
125
+ }
126
+ }
127
+ } catch {
128
+ // Existing file unreadable — start fresh but we'll overwrite only our keys
129
+ }
130
+
131
+ // Merge: existing fields preserved, our fields override
132
+ const merged = { ...existing, ...meta };
133
+ writeFileSync(metaPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
134
+ }
135
+
136
+ // ── Package Root Resolution ──────────────────────────────────────────
137
+
138
+ /**
139
+ * Resolve the taskplane package root directory.
140
+ *
141
+ * Uses ESM `import.meta.url` to compute the path deterministically.
142
+ * The package root is two levels up from this file:
143
+ * `<package-root>/extensions/taskplane/migrations.ts`
144
+ *
145
+ * @param importMetaUrl - Pass `import.meta.url` from the calling module
146
+ * @returns Absolute path to the package root
147
+ */
148
+ export function resolvePackageRoot(importMetaUrl?: string): string {
149
+ const url = importMetaUrl ?? import.meta.url;
150
+ const thisDir = dirname(fileURLToPath(url));
151
+ // extensions/taskplane/ → extensions/ → package root
152
+ return join(thisDir, "..", "..");
153
+ }
154
+
155
+ // ── Migration Registry ──────────────────────────────────────────────
156
+
157
+ /**
158
+ * Registry of all additive migrations, ordered by creation date.
159
+ *
160
+ * New migrations are appended to this array. Each migration must:
161
+ * - Have a unique, stable `id` (never renamed after release)
162
+ * - Only create files that don't exist (additive-only)
163
+ * - Throw on unrecoverable errors (e.g., missing template source)
164
+ * - Return null if the target already exists (skip)
165
+ */
166
+ export const MIGRATION_REGISTRY: Migration[] = [
167
+ {
168
+ id: "add-supervisor-local-template-v1",
169
+ description: "Create agents/supervisor.md from template if missing",
170
+ run(projectRoot: string, packageRoot: string, configRoot: string): string | null {
171
+ const targetPath = join(configRoot, "agents", "supervisor.md");
172
+
173
+ // Skip if file already exists — never overwrite
174
+ if (existsSync(targetPath)) {
175
+ return null;
176
+ }
177
+
178
+ // Resolve template source
179
+ const templatePath = join(packageRoot, "templates", "agents", "local", "supervisor.md");
180
+ if (!existsSync(templatePath)) {
181
+ throw new Error(
182
+ `Migration template not found: ${templatePath}. ` +
183
+ `This may indicate a packaging issue with the taskplane package.`,
184
+ );
185
+ }
186
+
187
+ // Create target directory and copy template
188
+ mkdirSync(dirname(targetPath), { recursive: true });
189
+ copyFileSync(templatePath, targetPath);
190
+
191
+ return `Created ${targetPath} from template`;
192
+ },
193
+ },
194
+ ];
195
+
196
+ // ── Migration Runner ─────────────────────────────────────────────────
197
+
198
+ /**
199
+ * Run all pending additive migrations.
200
+ *
201
+ * Loads migration state from `.pi/taskplane.json`, runs only unapplied
202
+ * migrations from the registry, and persists applied IDs + timestamps.
203
+ *
204
+ * Each migration is individually try/caught:
205
+ * - Success → recorded as applied, message logged
206
+ * - Skip (returns null) → recorded as applied (target already exists)
207
+ * - Error → logged and skipped (NOT recorded — will be retried next time)
208
+ *
209
+ * @param projectRoot - Project root directory
210
+ * @param packageRoot - Taskplane package root (for template resolution).
211
+ * If omitted, resolved from import.meta.url.
212
+ * @returns Migration run result with applied/skipped/error details
213
+ */
214
+ export function runMigrations(
215
+ projectRoot: string,
216
+ packageRoot?: string,
217
+ configRoot?: string,
218
+ ): MigrationRunResult {
219
+ const pkgRoot = packageRoot ?? resolvePackageRoot();
220
+ const cfgRoot = configRoot ?? join(projectRoot, ".pi");
221
+ const result: MigrationRunResult = {
222
+ applied: [],
223
+ skipped: [],
224
+ errors: [],
225
+ messages: [],
226
+ };
227
+
228
+ // Load current state
229
+ const meta = loadTaskplaneMeta(projectRoot);
230
+ const migrationState: MigrationState = meta.migrations ?? { applied: {} };
231
+
232
+ let stateChanged = false;
233
+
234
+ for (const migration of MIGRATION_REGISTRY) {
235
+ // Skip already-applied migrations
236
+ if (migrationState.applied[migration.id]) {
237
+ result.skipped.push(migration.id);
238
+ continue;
239
+ }
240
+
241
+ try {
242
+ const message = migration.run(projectRoot, pkgRoot, cfgRoot);
243
+
244
+ // Record as applied (whether it created something or skipped)
245
+ migrationState.applied[migration.id] = {
246
+ appliedAt: new Date().toISOString(),
247
+ };
248
+ stateChanged = true;
249
+
250
+ if (message) {
251
+ result.applied.push(migration.id);
252
+ result.messages.push(`📦 Migration: ${message}`);
253
+ } else {
254
+ // Target already existed — still mark as applied so we don't recheck
255
+ result.skipped.push(migration.id);
256
+ }
257
+ } catch (err: unknown) {
258
+ const errMsg = err instanceof Error ? err.message : String(err);
259
+ result.errors.push({ id: migration.id, error: errMsg });
260
+ // NOT recorded as applied — will be retried next time
261
+ }
262
+ }
263
+
264
+ // Persist state if anything changed
265
+ if (stateChanged) {
266
+ try {
267
+ saveTaskplaneMeta(projectRoot, { ...meta, migrations: migrationState });
268
+ } catch (err: unknown) {
269
+ const errMsg = err instanceof Error ? err.message : String(err);
270
+ result.errors.push({
271
+ id: "__state_save",
272
+ error: `Failed to persist migration state: ${errMsg}`,
273
+ });
274
+ }
275
+ }
276
+
277
+ return result;
278
+ }