@afokapu/atdd-flow 0.3.46 → 0.3.47
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/package.json +1 -1
- package/src/seats.ts +49 -9
package/package.json
CHANGED
package/src/seats.ts
CHANGED
|
@@ -213,7 +213,7 @@ async function noBranch(config: Project, branch: string) {
|
|
|
213
213
|
if (await probe.exited === 0) throw new Error(`Driver branch ${branch} already exists; refusing to reuse it.`);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
-
async function taskCoordinatorBase(root: string, config: Project, task: Task) {
|
|
216
|
+
async function taskCoordinatorBase(root: string, config: Project, task: Task, requireCleanCheckout = true) {
|
|
217
217
|
const coordinatorAddress = await canonicalAddress(root, task.coordinator);
|
|
218
218
|
const suffix = `@${config.project}`;
|
|
219
219
|
if (!coordinatorAddress.endsWith(suffix)) throw new Error(`Task coordinator ${coordinatorAddress} is outside project ${config.project}.`);
|
|
@@ -235,15 +235,17 @@ async function taskCoordinatorBase(root: string, config: Project, task: Task) {
|
|
|
235
235
|
throw new Error(`Task coordinator ${coordinator.address} must be main or a named coordinator.`);
|
|
236
236
|
}
|
|
237
237
|
if (coordinator.project !== config.project) throw new Error(`Task coordinator ${coordinator.address} is outside project ${config.project}.`);
|
|
238
|
+
const repository = required(config.repository, "project repository");
|
|
239
|
+
const branchHead = await runOutput(["git", "-C", repository, "rev-parse", branch]);
|
|
240
|
+
if (!requireCleanCheckout) return { coordinator, branch, commit: branchHead };
|
|
238
241
|
if (await runOutput(["git", "-C", coordinator.worktree, "status", "--porcelain"])) {
|
|
239
242
|
throw new Error(`Coordinator ${coordinator.address} worktree is dirty.`);
|
|
240
243
|
}
|
|
241
244
|
const checkedOut = await runOutput(["git", "-C", coordinator.worktree, "branch", "--show-current"]);
|
|
242
245
|
if (checkedOut !== branch) throw new Error(`Coordinator ${coordinator.address} is not checked out at ${branch}.`);
|
|
243
246
|
const head = await runOutput(["git", "-C", coordinator.worktree, "rev-parse", "HEAD"]);
|
|
244
|
-
const branchHead = await runOutput(["git", "-C", required(config.repository, "project repository"), "rev-parse", branch]);
|
|
245
247
|
if (head !== branchHead) throw new Error(`Coordinator ${coordinator.address} head is stale relative to ${branch}.`);
|
|
246
|
-
return { coordinator, branch, commit:
|
|
248
|
+
return { coordinator, branch, commit: branchHead };
|
|
247
249
|
}
|
|
248
250
|
|
|
249
251
|
/** Creates a fresh driver and assigns its existing todo task from the task coordinator's exact head. */
|
|
@@ -252,6 +254,22 @@ async function spawnTaskDriver(root: string, config: Project, role: Role, name:
|
|
|
252
254
|
throw new Error("Task-aware driver spawn requires the canonical driver role topology.");
|
|
253
255
|
}
|
|
254
256
|
if (has(args, "--worktree") || has(args, "--branch")) throw new Error("Task-aware driver spawn derives its worktree and branch; do not provide --worktree or --branch.");
|
|
257
|
+
const baseArgument = words(args, "--base");
|
|
258
|
+
const dryRun = has(args, "--dry-run");
|
|
259
|
+
const explicit = args.includes("--base") || dryRun;
|
|
260
|
+
if (explicit) {
|
|
261
|
+
const allowed = new Set(["--task", "--base", "--dry-run"]);
|
|
262
|
+
const taskArgument = words(args, "--task");
|
|
263
|
+
const onlyExpectedValues = args.every((arg, index) => allowed.has(arg)
|
|
264
|
+
|| (index > 0 && ["--task", "--base"].includes(args[index - 1]!)));
|
|
265
|
+
if (!onlyExpectedValues || args.filter((arg) => arg === "--task").length !== 1
|
|
266
|
+
|| args.filter((arg) => arg === "--base").length !== 1
|
|
267
|
+
|| args.filter((arg) => arg === "--dry-run").length > 1
|
|
268
|
+
|| !taskArgument || taskArgument.startsWith("--")
|
|
269
|
+
|| !baseArgument || !/^[0-9a-f]{40}$/.test(baseArgument)) {
|
|
270
|
+
throw new Error("Task-aware explicit base requires one --task <task-id> and one --base <full-40-lowercase-commit-sha>.");
|
|
271
|
+
}
|
|
272
|
+
}
|
|
255
273
|
const id = taskId(required(words(args, "--task"), "--task"));
|
|
256
274
|
const task = await readYaml<Task>(paths(root).taskFile(config.project, id));
|
|
257
275
|
if (task.schema !== "atdd-workflow/task/v1") throw new Error(`Unsupported task schema: ${id}`);
|
|
@@ -263,7 +281,23 @@ async function spawnTaskDriver(root: string, config: Project, role: Role, name:
|
|
|
263
281
|
if (await exists(paths(root).seatFile(address))) throw new Error(`Driver seat ${address} already exists; refusing to reuse it.`);
|
|
264
282
|
if (await exists(worktree)) throw new Error(`Driver worktree ${worktree} already exists; refusing to reuse it.`);
|
|
265
283
|
await noBranch(config, branch);
|
|
266
|
-
|
|
284
|
+
// An explicit baseline is governed by the repository ref, never by a
|
|
285
|
+
// potentially dirty or stale coordinator checkout. Legacy spawn retains its
|
|
286
|
+
// existing clean-checkout guard and derives exactly as before.
|
|
287
|
+
const coordinatorBase = await taskCoordinatorBase(root, config, task, !baseArgument);
|
|
288
|
+
let base = coordinatorBase;
|
|
289
|
+
if (baseArgument) {
|
|
290
|
+
try { await runOutput(["git", "-C", required(config.repository, "project repository"), "cat-file", "-e", `${baseArgument}^{commit}`]); }
|
|
291
|
+
catch { throw new Error(`Explicit base ${baseArgument} does not exist locally in the configured repository.`); }
|
|
292
|
+
try { await run(["git", "-C", required(config.repository, "project repository"), "merge-base", "--is-ancestor", coordinatorBase.commit, baseArgument], true); }
|
|
293
|
+
catch { throw new Error(`Explicit base ${baseArgument} does not descend ${coordinatorBase.branch}.`); }
|
|
294
|
+
base = { ...coordinatorBase, commit: baseArgument };
|
|
295
|
+
}
|
|
296
|
+
if (dryRun) {
|
|
297
|
+
if (!baseArgument) throw new Error("--dry-run is supported only with explicit --base task-aware spawn.");
|
|
298
|
+
console.log(`${address} dry-run ${id} base ${base.commit} branch ${branch} worktree ${worktree}`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
267
301
|
const portfolio = await modelPortfolio(root);
|
|
268
302
|
const requestedAgent = words(args, "--agent");
|
|
269
303
|
if (portfolio && requestedAgent) throw new Error("--agent is a legacy pin and cannot be used when models.yaml owns model allocation.");
|
|
@@ -280,17 +314,23 @@ async function spawnTaskDriver(root: string, config: Project, role: Role, name:
|
|
|
280
314
|
await mkdir(dirname(worktree), { recursive: true });
|
|
281
315
|
await run(["git", "-C", required(config.repository, "project repository"), "worktree", "add", "-b", branch, worktree, base.commit], true);
|
|
282
316
|
created = true;
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
if (
|
|
317
|
+
const currentBranch = await runOutput(["git", "-C", required(config.repository, "project repository"), "rev-parse", coordinatorBase.branch]);
|
|
318
|
+
if (currentBranch !== coordinatorBase.commit) throw new Error(`Coordinator ${base.coordinator.address} advanced during driver provisioning.`);
|
|
319
|
+
if (!baseArgument) {
|
|
320
|
+
const current = await runOutput(["git", "-C", base.coordinator.worktree, "rev-parse", "HEAD"]);
|
|
321
|
+
if (current !== coordinatorBase.commit) throw new Error(`Coordinator ${base.coordinator.address} advanced during driver provisioning.`);
|
|
322
|
+
}
|
|
286
323
|
await atomicYaml(paths(root).seatFile(address), record);
|
|
287
324
|
await atomicYaml(paths(root).taskFile(config.project, id), task);
|
|
288
325
|
} catch (error) {
|
|
289
|
-
if (created)
|
|
326
|
+
if (created) {
|
|
327
|
+
await run(["git", "-C", required(config.repository, "project repository"), "worktree", "remove", "--force", worktree], true).catch(() => undefined);
|
|
328
|
+
await run(["git", "-C", required(config.repository, "project repository"), "branch", "-D", branch], true).catch(() => undefined);
|
|
329
|
+
}
|
|
290
330
|
await rm(paths(root).seat(address), { recursive: true, force: true });
|
|
291
331
|
throw error;
|
|
292
332
|
}
|
|
293
|
-
console.log(`${address} assigned ${id} return ${base.branch}`);
|
|
333
|
+
console.log(`${address} assigned ${id} return ${base.branch}${baseArgument ? ` base ${base.commit}` : ""}`);
|
|
294
334
|
}
|
|
295
335
|
|
|
296
336
|
export async function spawn(root: string, projectName: string, roleName: string, name: string, args: string[]) {
|