@afokapu/atdd-flow 0.3.24 → 0.3.26
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/README.md +34 -24
- package/package.json +1 -1
- package/src/cli.ts +4 -2
- package/src/multiplexer.ts +43 -9
- package/src/seats.ts +31 -2
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Desk (private Git repository) Code repositories / worktrees
|
|
|
16
16
|
- A **seat** owns responsibility, branch, worktree, checkpoint, and host addresses; a **task** owns its brief, ownership, dependencies, criteria, and proof.
|
|
17
17
|
- A **thread** owns messages, receipts, results, and decisions; a **host** owns panes and notifications—never durable state.
|
|
18
18
|
|
|
19
|
-
A stable address, such as `driver.runtime@
|
|
19
|
+
A stable address, such as `driver.runtime@example-app`, survives a different pane, host, model, or replacement agent. A replacement reads its durable seat, task, checkpoint, and threads rather than predecessor-private context.
|
|
20
20
|
|
|
21
21
|
Workflow is a YAML protocol and CLI—not a database, daemon, agent runtime, or task-management SaaS. Git supplies history and replication; a multiplexer may wake an agent, but never owns state.
|
|
22
22
|
|
|
@@ -76,15 +76,25 @@ model. Final behavioral review has its own bounded routing step described below.
|
|
|
76
76
|
|
|
77
77
|
## Configure worktrees and seats
|
|
78
78
|
|
|
79
|
-
Create a project, then
|
|
79
|
+
Create a project, declare its primary checkout and linked-worktree root, then adjust its policy in `work/<project>/project.yaml`:
|
|
80
80
|
|
|
81
81
|
```sh
|
|
82
|
-
atdd-flow project init
|
|
82
|
+
atdd-flow project init example-app
|
|
83
|
+
atdd-flow project configure example-app --repository /Users/you/Github/example-app --worktree-root /Users/you/Github/worktrees/example-app
|
|
83
84
|
```
|
|
84
85
|
|
|
86
|
+
`project configure` sets only `repository` and `worktree_root`. It fails closed, leaving the project
|
|
87
|
+
unchanged, when `--repository` is not the top level of a Git repository or a flag is unknown, repeated,
|
|
88
|
+
or missing its value. It never changes seats, runtimes, tasks, branches, worktrees, or repository
|
|
89
|
+
content; it lists each seat whose recorded placement violates the topology as `unprojected`.
|
|
90
|
+
|
|
91
|
+
A misplaced seat is left unprojected: `multiplexer status` reports it under `unprojected`, and
|
|
92
|
+
`multiplexer apply` and launch projection of other seats continue. Projecting that seat explicitly
|
|
93
|
+
still rejects until its placement is corrected.
|
|
94
|
+
|
|
85
95
|
```yaml
|
|
86
|
-
repository: /Users/you/Github/
|
|
87
|
-
worktree_root: /Users/you/Github/worktrees/
|
|
96
|
+
repository: /Users/you/Github/example-app
|
|
97
|
+
worktree_root: /Users/you/Github/worktrees/example-app
|
|
88
98
|
roles:
|
|
89
99
|
coordinator: { address: coordinator@{project}, branch: main, worktree: '{repository}' }
|
|
90
100
|
driver: { address: driver.{name}@{project}, branch: delivery/{name}, base: main, worktree: '{worktree_root}/{name}' }
|
|
@@ -93,11 +103,11 @@ roles:
|
|
|
93
103
|
The operator or coordinator creates seats; drivers do not choose their policy:
|
|
94
104
|
|
|
95
105
|
```sh
|
|
96
|
-
atdd-flow spawn
|
|
97
|
-
atdd-flow spawn
|
|
106
|
+
atdd-flow spawn example-app coordinator main --worktree /Users/you/Github/example-app
|
|
107
|
+
atdd-flow spawn example-app driver runtime
|
|
98
108
|
```
|
|
99
109
|
|
|
100
|
-
`spawn` creates missing driver worktrees through Git: this example creates `/Users/you/Github/worktrees/
|
|
110
|
+
`spawn` creates missing driver worktrees through Git: this example creates `/Users/you/Github/worktrees/example-app/runtime` on `delivery/runtime`. ATDD Bun owns safe retirement, not creation. A seat can own several tasks.
|
|
101
111
|
|
|
102
112
|
## Deliver work
|
|
103
113
|
|
|
@@ -108,14 +118,14 @@ todo → in_progress → review → done
|
|
|
108
118
|
The coordinator assigns; the driver implements, proves each criterion, and submits for review; only the coordinator marks the task done.
|
|
109
119
|
|
|
110
120
|
```sh
|
|
111
|
-
atdd-flow task add
|
|
112
|
-
--coordinator coordinator@
|
|
121
|
+
atdd-flow task add example-app runtime-rollout --title 'Complete runtime rollout' \
|
|
122
|
+
--coordinator coordinator@example-app --assignee driver.runtime@example-app \
|
|
113
123
|
--done-when 'Checks pass'
|
|
114
|
-
atdd-flow task start
|
|
115
|
-
atdd-flow task prove
|
|
116
|
-
atdd-flow task review
|
|
124
|
+
atdd-flow task start example-app runtime-rollout --by driver.runtime@example-app
|
|
125
|
+
atdd-flow task prove example-app runtime-rollout --by driver.runtime@example-app --item 1 --proof 'CI run 42'
|
|
126
|
+
atdd-flow task review example-app runtime-rollout --by driver.runtime@example-app
|
|
117
127
|
# An independently attached reviewer persists APPROVE, RETURN, or ESCALATE through behavioral-review record
|
|
118
|
-
atdd-flow task done
|
|
128
|
+
atdd-flow task done example-app runtime-rollout --by coordinator@example-app
|
|
119
129
|
```
|
|
120
130
|
|
|
121
131
|
Proof is a compact PR, CI run, report, commit range, deployment, or thread reference. Dependencies gate prerequisites; independent tasks are parallel-ready. A coordinator can staff an unassigned ready task with `atdd-flow task assign <project> <task-id> --assignee <address> --by <coordinator-address>`; assignment is allowed only once while the task is `todo`. Use `task block` only for a real external blocker, then checkpoint exact state and next action. Once the blocker is resolved, only that coordinator can clear it with `atdd-flow task unblock <project> <task-id> --by <coordinator-address>` while the task remains `in_progress`. For deliveries explicitly governed by the `workflow` ATDD Bun profile, `review → done` additionally requires a durable final behavioral-review result with decision `APPROVE` for the current clean delivery commit. For an idle driver’s final task, `task done ... --retire-assignee` delegates clean-and-merged worktree retirement to ATDD Bun.
|
|
@@ -127,11 +137,11 @@ For topology normalization, transfer an active task through the narrow durable c
|
|
|
127
137
|
Threads are the durable inbox/outbox. Workflow persists a message before a best-effort host notification, so a closed pane, rate limit, or missed prompt cannot lose it.
|
|
128
138
|
|
|
129
139
|
```sh
|
|
130
|
-
atdd-flow thread start --with coordinator@
|
|
131
|
-
--subject 'Runtime rollout' --task
|
|
132
|
-
atdd-flow post T-... --from coordinator@
|
|
140
|
+
atdd-flow thread start --with coordinator@example-app,driver.runtime@example-app \
|
|
141
|
+
--subject 'Runtime rollout' --task example-app/runtime-rollout
|
|
142
|
+
atdd-flow post T-... --from coordinator@example-app --to driver.runtime@example-app \
|
|
133
143
|
--label 'implementation request' --expects-result --body 'Implement the task and return proof references.'
|
|
134
|
-
atdd-flow result T-... M-... --from driver.runtime@
|
|
144
|
+
atdd-flow result T-... M-... --from driver.runtime@example-app --label 'proof returned' --body 'CI run 42; PR #81.'
|
|
135
145
|
```
|
|
136
146
|
|
|
137
147
|
New durable thread and message IDs are human-readable: `T-` or `M-`, a UTC-second
|
|
@@ -149,14 +159,14 @@ For a shared boundary: driver → coordinator → affected coordinator(s) → mi
|
|
|
149
159
|
|
|
150
160
|
```sh
|
|
151
161
|
atdd-flow status
|
|
152
|
-
atdd-flow status seat driver.runtime@
|
|
153
|
-
atdd-flow open driver.runtime@
|
|
162
|
+
atdd-flow status seat driver.runtime@example-app
|
|
163
|
+
atdd-flow open driver.runtime@example-app
|
|
154
164
|
```
|
|
155
165
|
|
|
156
166
|
Herdr and tmux can notify an already attached host pane. Flow does not create host panes: the operator starts the agent in its declared worktree with `ATDD_WORKFLOW_ROOT` and `ATDD_WORKFLOW_SEAT`, then attaches that pane deterministically.
|
|
157
167
|
|
|
158
168
|
```sh
|
|
159
|
-
atdd-flow attach driver.runtime@
|
|
169
|
+
atdd-flow attach driver.runtime@example-app --application herdr
|
|
160
170
|
```
|
|
161
171
|
|
|
162
172
|
### Optional Herdr worktree projection
|
|
@@ -213,10 +223,10 @@ To host Pi in Herdr, create a pane with the durable Desk and seat identity, then
|
|
|
213
223
|
PI_EXTENSION="$(atdd-flow pi extension-path)"
|
|
214
224
|
herdr pane split --current --direction right --cwd /path/to/worktree --no-focus \
|
|
215
225
|
--env ATDD_WORKFLOW_ROOT=/path/to/desk \
|
|
216
|
-
--env ATDD_WORKFLOW_SEAT=driver.runtime@
|
|
226
|
+
--env ATDD_WORKFLOW_SEAT=driver.runtime@example-app
|
|
217
227
|
# Use the pane id returned above.
|
|
218
228
|
herdr agent start pi-runtime --kind pi --pane <pane-id> -- --extension "$PI_EXTENSION"
|
|
219
|
-
atdd-flow --root /path/to/desk attach driver.runtime@
|
|
229
|
+
atdd-flow --root /path/to/desk attach driver.runtime@example-app --application herdr --wake native
|
|
220
230
|
```
|
|
221
231
|
|
|
222
232
|
Pi receives its identity and startup task from the extension itself. Its normal TUI remains visible and manually usable; incoming Desk mail wakes it through Pi's native message API. The runtime lifecycle has one internal bounded inbox transport; it does not add a second watcher, registry, mailbox, reservation system, or routing path.
|
|
@@ -265,7 +275,7 @@ Jev is read-only; it cannot mutate state, approve proof, or override ATDD Bun.
|
|
|
265
275
|
|
|
266
276
|
```sh
|
|
267
277
|
atdd-flow scout --goal 'Fix payment retry behavior' --path src/payment/retry.ts --path src/profile/avatar.ts
|
|
268
|
-
atdd-flow focus-check
|
|
278
|
+
atdd-flow focus-check example-app runtime-rollout --action 'Add a generic retry orchestration service'
|
|
269
279
|
```
|
|
270
280
|
|
|
271
281
|
`scout` selects likely files. `focus-check` returns `REQUIRED`, `USEFUL_BUT_NOT_REQUIRED`, or `SPECULATIVE`.
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { basename, resolve } from "node:path";
|
|
4
|
-
import { init, initProject, spawn, bind, useApplication, attach, describe, checkpoint, launchPiRuntime, migrate, openSeat, piExtensionPath } from "./seats";
|
|
4
|
+
import { init, initProject, configureProject, spawn, bind, useApplication, attach, describe, checkpoint, launchPiRuntime, migrate, openSeat, piExtensionPath } from "./seats";
|
|
5
5
|
import { createExactSessionAdoptionAuthorization } from "./adoption-authorizations";
|
|
6
6
|
import { addParticipant, openThread, post, readMessage, receipt, result, startThread } from "./threads";
|
|
7
7
|
import * as tasks from "./tasks";
|
|
@@ -23,6 +23,7 @@ Usage:
|
|
|
23
23
|
atdd-flow init <desk-directory> [--git]
|
|
24
24
|
atdd-flow desk migrate
|
|
25
25
|
atdd-flow project init <project>
|
|
26
|
+
atdd-flow project configure <project> [--repository <primary-checkout>] [--worktree-root <directory>]
|
|
26
27
|
atdd-flow spawn <project> <role> <name> [--worktree <path>] [--branch <branch>] [--agent <legacy-executable>]
|
|
27
28
|
atdd-flow bind <address> [--application <application>] --address <native-address> [--session <name>] [--agent <executable>] [--worktree <path>] [--wake host|native]
|
|
28
29
|
atdd-flow attach <address> [--application <application>] [--wake host|native]
|
|
@@ -87,7 +88,8 @@ async function main() {
|
|
|
87
88
|
},
|
|
88
89
|
project: async () => {
|
|
89
90
|
if (rest[0] === "init") return initProject(root, required(rest[1], "project"));
|
|
90
|
-
|
|
91
|
+
if (rest[0] === "configure") return configureProject(root, required(rest[1], "project"), rest.slice(2));
|
|
92
|
+
throw new Error("Use `atdd-flow project init <project>` or `atdd-flow project configure <project> [--repository <primary-checkout>] [--worktree-root <directory>]`.");
|
|
91
93
|
},
|
|
92
94
|
spawn: () => spawn(root, required(rest[0], "project"), required(rest[1], "role"), required(rest[2], "name"), rest.slice(3)),
|
|
93
95
|
bind: () => bind(root, required(rest[0], "address"), rest.slice(1)),
|
package/src/multiplexer.ts
CHANGED
|
@@ -89,6 +89,21 @@ async function projectSeats(root: string, projectName: string) {
|
|
|
89
89
|
return seats.filter((entry): entry is Seat => Boolean(entry && entry.schema === "atdd-workflow/seat/v2" && !entry.retired));
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
export type UnprojectedSeat = { address: string; reason: string };
|
|
93
|
+
|
|
94
|
+
/** Seats whose recorded placement violates the project topology; they are never projected. */
|
|
95
|
+
export async function misplacedSeats(root: string, projectName: string): Promise<UnprojectedSeat[]> {
|
|
96
|
+
const config = await project(root, projectName);
|
|
97
|
+
if (!config.repository) return [];
|
|
98
|
+
const primary = resolve(config.repository);
|
|
99
|
+
const result: UnprojectedSeat[] = [];
|
|
100
|
+
for (const entry of await projectSeats(root, projectName)) {
|
|
101
|
+
try { assertNewTopologyPlacement(config, entry, primary); }
|
|
102
|
+
catch (error) { result.push({ address: entry.address, reason: (error as Error).message }); }
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
function assertNewTopologyPlacement(config: Project, entry: Seat, primary: string) {
|
|
93
108
|
const coordinator = config.roles.coordinator;
|
|
94
109
|
const namedCoordinatorDefaults = coordinator?.address === "coordinator.{name}@{project}" && coordinator.branch === "integration/{name}";
|
|
@@ -111,7 +126,11 @@ function assertNewTopologyPlacement(config: Project, entry: Seat, primary: strin
|
|
|
111
126
|
}
|
|
112
127
|
}
|
|
113
128
|
|
|
114
|
-
|
|
129
|
+
/**
|
|
130
|
+
* A misplaced seat is skipped and recorded in `unprojected` so it never aborts projection of other
|
|
131
|
+
* seats; requesting that seat explicitly (`strict`) still rejects.
|
|
132
|
+
*/
|
|
133
|
+
async function targets(root: string, options: { strict?: string; unprojected?: UnprojectedSeat[] } = {}): Promise<Target[]> {
|
|
115
134
|
let projects: string[];
|
|
116
135
|
try { projects = await readdir(paths(root).work); }
|
|
117
136
|
catch { return []; }
|
|
@@ -124,8 +143,13 @@ async function targets(root: string): Promise<Target[]> {
|
|
|
124
143
|
const primary = resolve(config.repository);
|
|
125
144
|
result.push({ project: name, worktree: primary, workspaceLabel: name });
|
|
126
145
|
for (const entry of await projectSeats(root, name)) {
|
|
127
|
-
assertNewTopologyPlacement(config, entry, primary);
|
|
128
|
-
|
|
146
|
+
try { assertNewTopologyPlacement(config, entry, primary); }
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (entry.address === options.strict) throw error;
|
|
149
|
+
options.unprojected?.push({ address: entry.address, reason: (error as Error).message });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if ((entry.role === "main" || entry.role === "coordinator" || entry.role === "operator") && resolve(entry.worktree) === primary) {
|
|
129
153
|
result.push({ project: name, worktree: primary, workspaceLabel: name, seat: entry });
|
|
130
154
|
continue;
|
|
131
155
|
}
|
|
@@ -166,8 +190,16 @@ async function liveWorkspaces(session: string, command: HerdrCommand) {
|
|
|
166
190
|
return value.workspaces as Workspace[];
|
|
167
191
|
}
|
|
168
192
|
|
|
193
|
+
/** A live workspace without a checkout path never matches: resolve("") would be the process cwd. */
|
|
194
|
+
function matchWorkspace(workspaces: Workspace[], worktree: string) {
|
|
195
|
+
return workspaces.find((entry) => {
|
|
196
|
+
const checkout = entry.worktree?.checkout_path;
|
|
197
|
+
return typeof checkout === "string" && checkout !== "" && resolve(checkout) === worktree;
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
169
201
|
async function ensureWorkspace(session: string, target: Target, workspaces: Workspace[], command: HerdrCommand, primary?: WorkspaceState) {
|
|
170
|
-
let current = workspaces
|
|
202
|
+
let current = matchWorkspace(workspaces, target.worktree);
|
|
171
203
|
if (!current) {
|
|
172
204
|
const args = target.workspaceLabel === target.project
|
|
173
205
|
? ["workspace", "create", "--cwd", target.worktree, "--label", target.workspaceLabel, "--no-focus"]
|
|
@@ -229,7 +261,7 @@ async function ensureTarget(session: string, target: Target, workspaces: Workspa
|
|
|
229
261
|
/** Reconciles exactly one Desk seat into a labeled Herdr workspace/tab/pane. */
|
|
230
262
|
export async function projectHerdrSeat(root: string, address: string, session: string, command: HerdrCommand = herdr): Promise<HerdrSeatProjection> {
|
|
231
263
|
await herdrPolicy();
|
|
232
|
-
const desired = await targets(root);
|
|
264
|
+
const desired = await targets(root, { strict: address });
|
|
233
265
|
const target = desired.find((entry) => entry.seat?.address === address);
|
|
234
266
|
if (!target?.seat) throw new Error(`No active Desk projection target exists for ${address}.`);
|
|
235
267
|
const primaryTarget = desired.find((entry) => entry.project === target.project && entry.workspaceLabel === target.project && !entry.seat);
|
|
@@ -260,16 +292,17 @@ export async function multiplexer(root: string, args: string[]) {
|
|
|
260
292
|
console.log("No Herdr session selected; projection was not run.");
|
|
261
293
|
return;
|
|
262
294
|
}
|
|
263
|
-
const
|
|
295
|
+
const unprojected: UnprojectedSeat[] = [];
|
|
296
|
+
const desired = await targets(root, { unprojected });
|
|
264
297
|
const command: HerdrCommand = herdr;
|
|
265
298
|
const workspaces = await liveWorkspaces(session, command);
|
|
266
299
|
if (action === "status") {
|
|
267
|
-
const present = desired.filter((target) => workspaces
|
|
300
|
+
const present = desired.filter((target) => matchWorkspace(workspaces, target.worktree));
|
|
268
301
|
const topology = await Promise.all(desired.map(async (target) => {
|
|
269
|
-
const workspace = workspaces
|
|
302
|
+
const workspace = matchWorkspace(workspaces, target.worktree);
|
|
270
303
|
return workspace ? topologyCompliant(session, target, workspace, command) : false;
|
|
271
304
|
}));
|
|
272
|
-
console.log(JSON.stringify({ schema: "atdd-workflow/multiplexer-status/v1", application: "herdr", session, desired: desired.length, present: present.length, topology_compliant: topology.filter(Boolean).length }, null, 2));
|
|
305
|
+
console.log(JSON.stringify({ schema: "atdd-workflow/multiplexer-status/v1", application: "herdr", session, desired: desired.length, present: present.length, topology_compliant: topology.filter(Boolean).length, unprojected }, null, 2));
|
|
273
306
|
return;
|
|
274
307
|
}
|
|
275
308
|
const primary = new Map<string, WorkspaceState>();
|
|
@@ -283,5 +316,6 @@ export async function multiplexer(root: string, args: string[]) {
|
|
|
283
316
|
}
|
|
284
317
|
await ensureTarget(session, target, workspaces, command, projectPrimary);
|
|
285
318
|
}
|
|
319
|
+
for (const entry of unprojected) console.log(`unprojected ${entry.address}: ${entry.reason}`);
|
|
286
320
|
console.log(`Projected ${desired.length} Desk seat worktree(s) into Herdr session ${session}.`);
|
|
287
321
|
}
|
package/src/seats.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
2
|
import { mkdir, readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from "./core";
|
|
11
11
|
import { type ModelSelectionInput, type ModelSelectionResponse, selectModel } from "./judgment";
|
|
12
12
|
import { seatTasks } from "./tasks";
|
|
13
|
-
import { projectHerdrSeat } from "./multiplexer";
|
|
13
|
+
import { misplacedSeats, projectHerdrSeat } from "./multiplexer";
|
|
14
14
|
import { isRuntimeStateStale, readRuntimeState } from "./runtime-state";
|
|
15
15
|
import { resolveExactSessionAdoptionAuthorization } from "./adoption-authorizations";
|
|
16
16
|
|
|
@@ -68,6 +68,35 @@ export async function initProject(root: string, name: string) {
|
|
|
68
68
|
console.log(`Initialized project ${name}`);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
const configureUsage = "Use `atdd-flow project configure <project> [--repository <primary-checkout>] [--worktree-root <directory>]`.";
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The supported way to declare an existing project's primary checkout and linked-worktree root.
|
|
75
|
+
* Everything is validated before one atomic write; only those two project fields change.
|
|
76
|
+
*/
|
|
77
|
+
export async function configureProject(root: string, name: string, args: string[]) {
|
|
78
|
+
const config = await project(root, name);
|
|
79
|
+
const values = new Map<string, string>();
|
|
80
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
81
|
+
const flag = args[index], value = args[index + 1];
|
|
82
|
+
if (!["--repository", "--worktree-root"].includes(flag) || values.has(flag) || !value || value.startsWith("--")) throw new Error(configureUsage);
|
|
83
|
+
values.set(flag, value);
|
|
84
|
+
}
|
|
85
|
+
if (!values.size) throw new Error(configureUsage);
|
|
86
|
+
const repository = values.get("--repository"), worktreeRoot = values.get("--worktree-root");
|
|
87
|
+
if (repository) {
|
|
88
|
+
const path = resolve(repository);
|
|
89
|
+
const real = (value: string) => { try { return realpathSync(value); } catch { return value; } };
|
|
90
|
+
const top = existsSync(path) ? Bun.spawnSync(["git", "-C", path, "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "pipe" }) : undefined;
|
|
91
|
+
if (!top || top.exitCode !== 0 || real(top.stdout.toString().trim()) !== real(path)) throw new Error(`${path} is not the top level of a Git repository.`);
|
|
92
|
+
config.repository = path;
|
|
93
|
+
}
|
|
94
|
+
if (worktreeRoot) config.worktree_root = resolve(worktreeRoot);
|
|
95
|
+
await atomicYaml(paths(root).projectFile(name), config);
|
|
96
|
+
for (const entry of await misplacedSeats(root, name)) console.log(`unprojected ${entry.address}: ${entry.reason}`);
|
|
97
|
+
console.log(`${name} configured`);
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
async function ensureWorktree(config: Project, role: Role, worktree: string, branch: string) {
|
|
72
101
|
if (!config.repository || worktree === resolve(config.repository) || await exists(worktree)) return;
|
|
73
102
|
await mkdir(dirname(worktree), { recursive: true });
|