taskplane 0.15.0 → 0.16.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.
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
import { buildExecutionContext } from "./workspace.ts";
|
|
47
47
|
import { openSettingsTui } from "./settings-tui.ts";
|
|
48
48
|
import { loadProjectConfig } from "./config-loader.ts";
|
|
49
|
+
import { runMigrations } from "./migrations.ts";
|
|
49
50
|
import {
|
|
50
51
|
activateSupervisor,
|
|
51
52
|
deactivateSupervisor,
|
|
@@ -1493,6 +1494,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1493
1494
|
};
|
|
1494
1495
|
}
|
|
1495
1496
|
|
|
1497
|
+
// TP-063: Run additive migrations before batch start (primary trigger).
|
|
1498
|
+
// Non-fatal — failures warn but never block batch execution.
|
|
1499
|
+
try {
|
|
1500
|
+
const migrationResult = runMigrations(execCtx.repoRoot);
|
|
1501
|
+
if (migrationResult.messages.length > 0) {
|
|
1502
|
+
ctx.ui.notify(migrationResult.messages.join("\n"), "info");
|
|
1503
|
+
}
|
|
1504
|
+
if (migrationResult.errors.length > 0) {
|
|
1505
|
+
ctx.ui.notify(
|
|
1506
|
+
`⚠️ Migration warnings:\n${migrationResult.errors.map(e => ` ⚠ ${e.id}: ${e.error}`).join("\n")}`,
|
|
1507
|
+
"warning",
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
} catch {
|
|
1511
|
+
// Swallow — migrations must never block /orch
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1496
1514
|
// TP-128: Transition from routing-mode supervisor to batch execution
|
|
1497
1515
|
if (supervisorState.active && supervisorState.routingContext) {
|
|
1498
1516
|
await deactivateSupervisor(pi, supervisorState);
|
|
@@ -2853,6 +2871,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
2853
2871
|
supervisorConfig = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
2854
2872
|
}
|
|
2855
2873
|
|
|
2874
|
+
// TP-063: Run additive migrations on session start (safety net trigger).
|
|
2875
|
+
// This ensures migrations run even if the user doesn't invoke /orch.
|
|
2876
|
+
// Non-fatal — failures are silently swallowed so startup is never blocked.
|
|
2877
|
+
try {
|
|
2878
|
+
const migrationResult = runMigrations(execCtx.repoRoot);
|
|
2879
|
+
if (migrationResult.messages.length > 0) {
|
|
2880
|
+
ctx.ui.notify(migrationResult.messages.join("\n"), "info");
|
|
2881
|
+
}
|
|
2882
|
+
// Errors on session_start are silent — avoid noisy warnings at startup
|
|
2883
|
+
} catch {
|
|
2884
|
+
// Swallow — migrations must never block session startup
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2856
2887
|
// Set status line
|
|
2857
2888
|
const areaCount = Object.keys(runnerConfig.task_areas).length;
|
|
2858
2889
|
const modeLabel = execCtx.mode === "workspace" ? "workspace" : "repo";
|
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
* @returns A short message describing what was created, or null if skipped (already exists)
|
|
35
|
+
* @throws If the migration cannot complete (e.g., missing template source)
|
|
36
|
+
*/
|
|
37
|
+
run(projectRoot: string, packageRoot: string): string | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Record of a single applied migration in `.pi/taskplane.json`.
|
|
42
|
+
*/
|
|
43
|
+
export interface AppliedMigration {
|
|
44
|
+
/** ISO timestamp when the migration was applied */
|
|
45
|
+
appliedAt: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The `migrations` section within `.pi/taskplane.json`.
|
|
50
|
+
*/
|
|
51
|
+
export interface MigrationState {
|
|
52
|
+
applied: Record<string, AppliedMigration>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Shape of `.pi/taskplane.json` (partial — only fields we read/write).
|
|
57
|
+
* Other fields (version, installedAt, lastUpgraded, components) are
|
|
58
|
+
* preserved as-is during read-modify-write.
|
|
59
|
+
*/
|
|
60
|
+
export interface TaskplaneMeta {
|
|
61
|
+
[key: string]: unknown;
|
|
62
|
+
migrations?: MigrationState;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result of running migrations.
|
|
67
|
+
*/
|
|
68
|
+
export interface MigrationRunResult {
|
|
69
|
+
/** Migration IDs that were applied in this run */
|
|
70
|
+
applied: string[];
|
|
71
|
+
/** Migration IDs that were skipped (already applied or target exists) */
|
|
72
|
+
skipped: string[];
|
|
73
|
+
/** Migrations that failed with errors (non-fatal — logged and skipped) */
|
|
74
|
+
errors: Array<{ id: string; error: string }>;
|
|
75
|
+
/** Human-readable messages for each applied migration */
|
|
76
|
+
messages: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Meta File Helpers ────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const TASKPLANE_META_FILENAME = "taskplane.json";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Load `.pi/taskplane.json`, returning its content or an empty object
|
|
85
|
+
* if the file doesn't exist or is malformed.
|
|
86
|
+
*
|
|
87
|
+
* Never throws — returns `{}` for any read/parse error.
|
|
88
|
+
*/
|
|
89
|
+
export function loadTaskplaneMeta(projectRoot: string): TaskplaneMeta {
|
|
90
|
+
const metaPath = join(projectRoot, ".pi", TASKPLANE_META_FILENAME);
|
|
91
|
+
try {
|
|
92
|
+
if (!existsSync(metaPath)) return {};
|
|
93
|
+
const raw = readFileSync(metaPath, "utf-8");
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
96
|
+
return parsed as TaskplaneMeta;
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Save `.pi/taskplane.json`, merging the provided meta with any
|
|
104
|
+
* existing content. Creates the `.pi/` directory if needed.
|
|
105
|
+
*
|
|
106
|
+
* Performs a shallow merge at the top level — existing keys not in
|
|
107
|
+
* `meta` are preserved. The `migrations` key is always taken from
|
|
108
|
+
* the provided `meta` object (deep replacement).
|
|
109
|
+
*/
|
|
110
|
+
export function saveTaskplaneMeta(projectRoot: string, meta: TaskplaneMeta): void {
|
|
111
|
+
const piDir = join(projectRoot, ".pi");
|
|
112
|
+
mkdirSync(piDir, { recursive: true });
|
|
113
|
+
|
|
114
|
+
const metaPath = join(piDir, TASKPLANE_META_FILENAME);
|
|
115
|
+
|
|
116
|
+
// Read existing content to preserve version-tracker fields
|
|
117
|
+
let existing: TaskplaneMeta = {};
|
|
118
|
+
try {
|
|
119
|
+
if (existsSync(metaPath)) {
|
|
120
|
+
const raw = readFileSync(metaPath, "utf-8");
|
|
121
|
+
const parsed = JSON.parse(raw);
|
|
122
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
123
|
+
existing = parsed as TaskplaneMeta;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Existing file unreadable — start fresh but we'll overwrite only our keys
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Merge: existing fields preserved, our fields override
|
|
131
|
+
const merged = { ...existing, ...meta };
|
|
132
|
+
writeFileSync(metaPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Package Root Resolution ──────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve the taskplane package root directory.
|
|
139
|
+
*
|
|
140
|
+
* Uses ESM `import.meta.url` to compute the path deterministically.
|
|
141
|
+
* The package root is two levels up from this file:
|
|
142
|
+
* `<package-root>/extensions/taskplane/migrations.ts`
|
|
143
|
+
*
|
|
144
|
+
* @param importMetaUrl - Pass `import.meta.url` from the calling module
|
|
145
|
+
* @returns Absolute path to the package root
|
|
146
|
+
*/
|
|
147
|
+
export function resolvePackageRoot(importMetaUrl?: string): string {
|
|
148
|
+
const url = importMetaUrl ?? import.meta.url;
|
|
149
|
+
const thisDir = dirname(fileURLToPath(url));
|
|
150
|
+
// extensions/taskplane/ → extensions/ → package root
|
|
151
|
+
return join(thisDir, "..", "..");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Migration Registry ──────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Registry of all additive migrations, ordered by creation date.
|
|
158
|
+
*
|
|
159
|
+
* New migrations are appended to this array. Each migration must:
|
|
160
|
+
* - Have a unique, stable `id` (never renamed after release)
|
|
161
|
+
* - Only create files that don't exist (additive-only)
|
|
162
|
+
* - Throw on unrecoverable errors (e.g., missing template source)
|
|
163
|
+
* - Return null if the target already exists (skip)
|
|
164
|
+
*/
|
|
165
|
+
export const MIGRATION_REGISTRY: Migration[] = [
|
|
166
|
+
{
|
|
167
|
+
id: "add-supervisor-local-template-v1",
|
|
168
|
+
description: "Create .pi/agents/supervisor.md from template if missing",
|
|
169
|
+
run(projectRoot: string, packageRoot: string): string | null {
|
|
170
|
+
const targetPath = join(projectRoot, ".pi", "agents", "supervisor.md");
|
|
171
|
+
|
|
172
|
+
// Skip if file already exists — never overwrite
|
|
173
|
+
if (existsSync(targetPath)) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Resolve template source
|
|
178
|
+
const templatePath = join(packageRoot, "templates", "agents", "local", "supervisor.md");
|
|
179
|
+
if (!existsSync(templatePath)) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Migration template not found: ${templatePath}. ` +
|
|
182
|
+
`This may indicate a packaging issue with the taskplane package.`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Create target directory and copy template
|
|
187
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
188
|
+
copyFileSync(templatePath, targetPath);
|
|
189
|
+
|
|
190
|
+
return "Created .pi/agents/supervisor.md from template";
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
// ── Migration Runner ─────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Run all pending additive migrations.
|
|
199
|
+
*
|
|
200
|
+
* Loads migration state from `.pi/taskplane.json`, runs only unapplied
|
|
201
|
+
* migrations from the registry, and persists applied IDs + timestamps.
|
|
202
|
+
*
|
|
203
|
+
* Each migration is individually try/caught:
|
|
204
|
+
* - Success → recorded as applied, message logged
|
|
205
|
+
* - Skip (returns null) → recorded as applied (target already exists)
|
|
206
|
+
* - Error → logged and skipped (NOT recorded — will be retried next time)
|
|
207
|
+
*
|
|
208
|
+
* @param projectRoot - Project root directory
|
|
209
|
+
* @param packageRoot - Taskplane package root (for template resolution).
|
|
210
|
+
* If omitted, resolved from import.meta.url.
|
|
211
|
+
* @returns Migration run result with applied/skipped/error details
|
|
212
|
+
*/
|
|
213
|
+
export function runMigrations(
|
|
214
|
+
projectRoot: string,
|
|
215
|
+
packageRoot?: string,
|
|
216
|
+
): MigrationRunResult {
|
|
217
|
+
const pkgRoot = packageRoot ?? resolvePackageRoot();
|
|
218
|
+
const result: MigrationRunResult = {
|
|
219
|
+
applied: [],
|
|
220
|
+
skipped: [],
|
|
221
|
+
errors: [],
|
|
222
|
+
messages: [],
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Load current state
|
|
226
|
+
const meta = loadTaskplaneMeta(projectRoot);
|
|
227
|
+
const migrationState: MigrationState = meta.migrations ?? { applied: {} };
|
|
228
|
+
|
|
229
|
+
let stateChanged = false;
|
|
230
|
+
|
|
231
|
+
for (const migration of MIGRATION_REGISTRY) {
|
|
232
|
+
// Skip already-applied migrations
|
|
233
|
+
if (migrationState.applied[migration.id]) {
|
|
234
|
+
result.skipped.push(migration.id);
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const message = migration.run(projectRoot, pkgRoot);
|
|
240
|
+
|
|
241
|
+
// Record as applied (whether it created something or skipped)
|
|
242
|
+
migrationState.applied[migration.id] = {
|
|
243
|
+
appliedAt: new Date().toISOString(),
|
|
244
|
+
};
|
|
245
|
+
stateChanged = true;
|
|
246
|
+
|
|
247
|
+
if (message) {
|
|
248
|
+
result.applied.push(migration.id);
|
|
249
|
+
result.messages.push(`📦 Migration: ${message}`);
|
|
250
|
+
} else {
|
|
251
|
+
// Target already existed — still mark as applied so we don't recheck
|
|
252
|
+
result.skipped.push(migration.id);
|
|
253
|
+
}
|
|
254
|
+
} catch (err: unknown) {
|
|
255
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
256
|
+
result.errors.push({ id: migration.id, error: errMsg });
|
|
257
|
+
// NOT recorded as applied — will be retried next time
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Persist state if anything changed
|
|
262
|
+
if (stateChanged) {
|
|
263
|
+
try {
|
|
264
|
+
saveTaskplaneMeta(projectRoot, { ...meta, migrations: migrationState });
|
|
265
|
+
} catch (err: unknown) {
|
|
266
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
267
|
+
result.errors.push({
|
|
268
|
+
id: "__state_save",
|
|
269
|
+
error: `Failed to persist migration state: ${errMsg}`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return result;
|
|
275
|
+
}
|