@afokapu/atdd-flow 0.3.61 → 0.3.62

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/tasks.ts +27 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@afokapu/atdd-flow",
3
- "version": "0.3.61",
3
+ "version": "0.3.62",
4
4
  "description": "Filesystem-first Desk registry for projects, seats, tasks, threads, and handoffs.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/tasks.ts CHANGED
@@ -13,6 +13,7 @@ export type HandoffState = "executing" | "awaiting_coordinator" | "awaiting_assi
13
13
  export type TaskHandoff = { state: HandoffState; evidence: string; updated_at: string; submitted_phase?: TaskPhase };
14
14
  export type DoneWhen = { text: string; proof?: string };
15
15
  export type GovernedBase = { coordinator: string; branch: string; commit: string };
16
+ export type LineageRef = { ref: string; commit: string; reason?: string };
16
17
  /** An append-only accountability decision; prior coordinator evidence remains untouched. */
17
18
  export type CoordinatorTransfer = {
18
19
  from: string;
@@ -21,6 +22,8 @@ export type CoordinatorTransfer = {
21
22
  /** Required when main executes the transfer; references an operator@desk message. */
22
23
  authorization?: string;
23
24
  exact_head: { branch: string; commit: string };
25
+ /** The driver ref used for the lineage check; HEAD means the recorded branch no longer resolved. */
26
+ lineage_ref?: LineageRef;
24
27
  effective_at: string;
25
28
  };
26
29
  export type TerminalDisposition = { kind: "superseded" | "rejected"; by: string; at: string; reason: string };
@@ -117,6 +120,23 @@ async function writeTask(root: string, projectName: string, id: string, task: Ta
117
120
 
118
121
  /** Records the exact parent head for new bounded topology without rewriting legacy tasks. */
119
122
  async function governedBase(root: string, projectName: string, coordinatorAddress: string, assignee?: string): Promise<GovernedBase | undefined> {
123
+ return (await governedLineage(root, projectName, coordinatorAddress, assignee))?.base;
124
+ }
125
+
126
+ /** The recorded branch when it resolves; otherwise the driver worktree HEAD; otherwise fail closed. */
127
+ async function driverLineageRef(driver: Seat): Promise<LineageRef> {
128
+ const resolveRef = (ref: string) => runOutput(["git", "-C", driver.worktree, "rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
129
+ try { return { ref: driver.branch, commit: await resolveRef(driver.branch) }; }
130
+ catch {
131
+ if (!await exists(driver.worktree)) {
132
+ throw new Error(`Driver ${driver.address} branch ${driver.branch} no longer resolves and its worktree ${driver.worktree} is missing; cannot verify lineage.`);
133
+ }
134
+ try { return { ref: "HEAD", commit: await resolveRef("HEAD"), reason: `recorded branch ${driver.branch} no longer resolves; used worktree HEAD` }; }
135
+ catch { throw new Error(`Driver ${driver.address} branch ${driver.branch} no longer resolves and worktree ${driver.worktree} has no valid HEAD.`); }
136
+ }
137
+ }
138
+
139
+ async function governedLineage(root: string, projectName: string, coordinatorAddress: string, assignee?: string): Promise<{ base: GovernedBase; lineage: LineageRef } | undefined> {
120
140
  if (!assignee) return undefined;
121
141
  const config = await project(root, projectName);
122
142
  if (!config.repository) return undefined;
@@ -146,9 +166,10 @@ async function governedBase(root: string, projectName: string, coordinatorAddres
146
166
  throw new Error(`Task coordinator ${coordinator.address} must be main or a named coordinator.`);
147
167
  }
148
168
  const commit = await runOutput(["git", "-C", coordinator.worktree, "rev-parse", branch]);
149
- const common = await runOutput(["git", "-C", driver.worktree, "merge-base", commit, driver.branch]);
169
+ const lineage = await driverLineageRef(driver);
170
+ const common = await runOutput(["git", "-C", driver.worktree, "merge-base", commit, lineage.commit]);
150
171
  if (common !== commit) throw new Error(`Driver ${driver.address} is not based on the exact coordinator head ${commit}.`);
151
- return { coordinator: coordinator.address, branch, commit };
172
+ return { base: { coordinator: coordinator.address, branch, commit }, lineage };
152
173
  }
153
174
 
154
175
  /**
@@ -390,8 +411,9 @@ export async function transfer(root: string, projectName: string, id: string, ar
390
411
 
391
412
  // governedBase confirms main/named-coordinator identity, exact branch head,
392
413
  // and the driver's descendant relationship before accountability changes.
393
- const base = await governedBase(root, projectName, target, task.assignee);
394
- if (!base) throw new Error(`Task ${id} needs an assigned driver and governed repository before transfer.`);
414
+ const governed = await governedLineage(root, projectName, target, task.assignee);
415
+ const base = governed?.base;
416
+ if (!governed || !base) throw new Error(`Task ${id} needs an assigned driver and governed repository before transfer.`);
395
417
  const authorization = actor === main ? required(words(args, "--authorization"), "an owner-authorized topology plan") : undefined;
396
418
  if (authorization) await operatorAuthorization(root, authorization, main, {
397
419
  schema: "atdd-workflow/task-transfer-authorization/v1",
@@ -408,6 +430,7 @@ export async function transfer(root: string, projectName: string, id: string, ar
408
430
  reason: required(words(args, "--reason"), "--reason"),
409
431
  ...(authorization ? { authorization } : {}),
410
432
  exact_head: { branch: base.branch, commit: base.commit },
433
+ lineage_ref: governed.lineage,
411
434
  effective_at: now(),
412
435
  };
413
436
  task.coordinator = target;