@crvouga/personal 0.1.3 → 0.1.4

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/commands.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "version": 1,
3
3
  "commands": [
4
+ {
5
+ "id": "fleet-sync",
6
+ "title": "Synchronize the enrolled Ubuntu fleet",
7
+ "description": "Controller push to all enrolled Ubuntu nodes at one approved revision; interactive confirmation or --apply --yes. Readiness is verified separately.",
8
+ "group": "Fleet",
9
+ "keywords": [
10
+ "ubuntu",
11
+ "enrollment",
12
+ "sync"
13
+ ],
14
+ "args": [
15
+ "fleet-sync"
16
+ ],
17
+ "examples": [
18
+ "personal fleet-sync --dry-run --json"
19
+ ],
20
+ "mutation": true,
21
+ "help": "personal fleet-sync --help",
22
+ "execution": {
23
+ "default": "interactive-confirmation-or-action-required",
24
+ "authorization": "interactive-confirmation or --apply --yes",
25
+ "preview": "personal fleet-sync --dry-run --json",
26
+ "scope": "enrolled Ubuntu fleet over verified SSH; controller is not an implicit target",
27
+ "note": "JSON/noninteractive never prompt. Legacy fleet sync stays plan-only. Full Ubuntu acceptance is not yet implemented."
28
+ }
29
+ },
30
+ {
31
+ "id": "fleet converge",
32
+ "title": "Synchronize the enrolled Ubuntu fleet",
33
+ "description": "Controller push to all enrolled Ubuntu nodes at one approved revision; interactive confirmation or --apply --yes. Readiness is verified separately.",
34
+ "group": "Fleet",
35
+ "keywords": [
36
+ "ubuntu",
37
+ "enrollment",
38
+ "sync"
39
+ ],
40
+ "args": [
41
+ "fleet",
42
+ "converge"
43
+ ],
44
+ "examples": [
45
+ "personal fleet converge --dry-run --json"
46
+ ],
47
+ "mutation": true,
48
+ "help": "personal fleet converge --help",
49
+ "execution": {
50
+ "default": "interactive-confirmation-or-action-required",
51
+ "authorization": "interactive-confirmation or --apply --yes",
52
+ "preview": "personal fleet converge --dry-run --json",
53
+ "scope": "enrolled Ubuntu fleet over verified SSH; controller is not an implicit target",
54
+ "note": "JSON/noninteractive never prompt. Legacy fleet sync stays plan-only. Full Ubuntu acceptance is not yet implemented."
55
+ }
56
+ },
4
57
  {
5
58
  "id": "t3-plan",
6
59
  "title": "Preview creates, updates and owned removals",
@@ -18,7 +71,12 @@
18
71
  "examples": [
19
72
  "personal t3-plan --json"
20
73
  ],
21
- "mutation": false
74
+ "mutation": false,
75
+ "help": "personal t3-plan --help",
76
+ "execution": {
77
+ "default": "read-only",
78
+ "authorization": "none"
79
+ }
22
80
  },
23
81
  {
24
82
  "id": "t3-apply",
@@ -37,7 +95,15 @@
37
95
  "examples": [
38
96
  "personal t3-apply --json"
39
97
  ],
40
- "mutation": true
98
+ "mutation": true,
99
+ "help": "personal t3-apply --help",
100
+ "execution": {
101
+ "default": "apply",
102
+ "authorization": "command-invocation",
103
+ "preview": "personal t3-apply --dry-run --json",
104
+ "requiredOptions": [],
105
+ "note": "JSON selects output format; it does not prevent writes."
106
+ }
41
107
  },
42
108
  {
43
109
  "id": "t3-status",
@@ -56,7 +122,12 @@
56
122
  "examples": [
57
123
  "personal t3-status --json"
58
124
  ],
59
- "mutation": false
125
+ "mutation": false,
126
+ "help": "personal t3-status --help",
127
+ "execution": {
128
+ "default": "read-only",
129
+ "authorization": "none"
130
+ }
60
131
  },
61
132
  {
62
133
  "id": "t3-cleanup",
@@ -75,7 +146,15 @@
75
146
  "examples": [
76
147
  "personal t3-cleanup --json"
77
148
  ],
78
- "mutation": true
149
+ "mutation": true,
150
+ "help": "personal t3-cleanup --help",
151
+ "execution": {
152
+ "default": "apply",
153
+ "authorization": "command-invocation",
154
+ "preview": "personal t3-cleanup --dry-run --json",
155
+ "requiredOptions": [],
156
+ "note": "JSON selects output format; it does not prevent writes."
157
+ }
79
158
  },
80
159
  {
81
160
  "id": "t3-import",
@@ -92,9 +171,19 @@
92
171
  "t3-import"
93
172
  ],
94
173
  "examples": [
95
- "personal t3-import --json"
96
- ],
97
- "mutation": true
174
+ "personal t3-import --instance INSTANCE_ID --json"
175
+ ],
176
+ "mutation": true,
177
+ "help": "personal t3-import --help",
178
+ "execution": {
179
+ "default": "apply",
180
+ "authorization": "command-invocation",
181
+ "preview": "personal t3-plan --json",
182
+ "requiredOptions": [
183
+ "--instance ID"
184
+ ],
185
+ "note": "JSON selects output format; it does not prevent writes."
186
+ }
98
187
  },
99
188
  {
100
189
  "id": "workstation",
@@ -113,12 +202,21 @@
113
202
  "examples": [
114
203
  "personal workstation --help"
115
204
  ],
116
- "mutation": false
205
+ "mutation": false,
206
+ "help": "personal workstation --help",
207
+ "execution": {
208
+ "default": "help",
209
+ "mutation": "depends-on-subcommand",
210
+ "requirements": [
211
+ "Personal checkout and Bun for workstation operations"
212
+ ],
213
+ "note": "Inspect leaf --help; use --json --non-interactive for diagnostics. Native agent launches stream native output."
214
+ }
117
215
  },
118
216
  {
119
217
  "id": "fleet-node-sync",
120
218
  "title": "Sync this node",
121
- "description": "Upsert and reconcile locked tools and the Codex proxy profile on this machine. Applies by default; --dry-run previews.",
219
+ "description": "Reconcile locked tools, the personal launcher, Codex proxy profile and owned T3 instances on this machine. Applies by default; --dry-run previews.",
122
220
  "group": "Fleet",
123
221
  "keywords": [
124
222
  "node",
@@ -132,7 +230,15 @@
132
230
  "examples": [
133
231
  "personal fleet-node-sync --json"
134
232
  ],
135
- "mutation": true
233
+ "mutation": true,
234
+ "help": "personal fleet-node-sync --help",
235
+ "execution": {
236
+ "default": "apply",
237
+ "authorization": "command-invocation",
238
+ "preview": "personal fleet-node-sync --dry-run --json",
239
+ "requiredOptions": [],
240
+ "note": "JSON selects output format; it does not prevent writes."
241
+ }
136
242
  },
137
243
  {
138
244
  "id": "fleet-status",
@@ -151,7 +257,12 @@
151
257
  "examples": [
152
258
  "personal fleet-status --json"
153
259
  ],
154
- "mutation": false
260
+ "mutation": false,
261
+ "help": "personal fleet-status --help",
262
+ "execution": {
263
+ "default": "read-only",
264
+ "authorization": "none"
265
+ }
155
266
  },
156
267
  {
157
268
  "id": "fleet-discover",
@@ -170,7 +281,12 @@
170
281
  "examples": [
171
282
  "personal fleet-discover --json"
172
283
  ],
173
- "mutation": false
284
+ "mutation": false,
285
+ "help": "personal fleet-discover --help",
286
+ "execution": {
287
+ "default": "read-only",
288
+ "authorization": "none"
289
+ }
174
290
  },
175
291
  {
176
292
  "id": "fleet repairs",
@@ -193,7 +309,12 @@
193
309
  "examples": [
194
310
  "personal fleet repairs --json"
195
311
  ],
196
- "mutation": false
312
+ "mutation": false,
313
+ "help": "personal fleet repairs --help",
314
+ "execution": {
315
+ "default": "read-only",
316
+ "authorization": "none"
317
+ }
197
318
  },
198
319
  {
199
320
  "id": "fleet repair",
@@ -217,7 +338,13 @@
217
338
  "examples": [
218
339
  "personal fleet repair --json"
219
340
  ],
220
- "mutation": true
341
+ "mutation": true,
342
+ "help": "personal fleet repair --help",
343
+ "execution": {
344
+ "default": "preview",
345
+ "authorization": "--apply --yes",
346
+ "note": "Supply required options shown by command --help. A preview does not apply changes."
347
+ }
221
348
  },
222
349
  {
223
350
  "id": "fleet health",
@@ -242,7 +369,12 @@
242
369
  "examples": [
243
370
  "personal fleet health --json"
244
371
  ],
245
- "mutation": false
372
+ "mutation": false,
373
+ "help": "personal fleet health --help",
374
+ "execution": {
375
+ "default": "read-only",
376
+ "authorization": "none"
377
+ }
246
378
  },
247
379
  {
248
380
  "id": "fleet discover",
@@ -263,7 +395,12 @@
263
395
  "examples": [
264
396
  "personal fleet discover --json"
265
397
  ],
266
- "mutation": false
398
+ "mutation": false,
399
+ "help": "personal fleet discover --help",
400
+ "execution": {
401
+ "default": "read-only",
402
+ "authorization": "none"
403
+ }
267
404
  },
268
405
  {
269
406
  "id": "fleet doctor",
@@ -287,7 +424,12 @@
287
424
  "examples": [
288
425
  "personal fleet doctor --json"
289
426
  ],
290
- "mutation": false
427
+ "mutation": false,
428
+ "help": "personal fleet doctor --help",
429
+ "execution": {
430
+ "default": "read-only",
431
+ "authorization": "none"
432
+ }
291
433
  },
292
434
  {
293
435
  "id": "fleet doctor local",
@@ -307,7 +449,12 @@
307
449
  "examples": [
308
450
  "personal fleet doctor --local --json"
309
451
  ],
310
- "mutation": false
452
+ "mutation": false,
453
+ "help": "personal fleet doctor --local --help",
454
+ "execution": {
455
+ "default": "read-only",
456
+ "authorization": "none"
457
+ }
311
458
  },
312
459
  {
313
460
  "id": "fleet nodes",
@@ -327,7 +474,12 @@
327
474
  "examples": [
328
475
  "personal fleet nodes --json"
329
476
  ],
330
- "mutation": false
477
+ "mutation": false,
478
+ "help": "personal fleet nodes --help",
479
+ "execution": {
480
+ "default": "read-only",
481
+ "authorization": "none"
482
+ }
331
483
  },
332
484
  {
333
485
  "id": "fleet add",
@@ -347,7 +499,13 @@
347
499
  "examples": [
348
500
  "personal fleet add --json"
349
501
  ],
350
- "mutation": true
502
+ "mutation": true,
503
+ "help": "personal fleet add --help",
504
+ "execution": {
505
+ "default": "preview",
506
+ "authorization": "--apply --yes",
507
+ "note": "Supply required options shown by command --help. A preview does not apply changes."
508
+ }
351
509
  },
352
510
  {
353
511
  "id": "fleet validate",
@@ -366,7 +524,12 @@
366
524
  "examples": [
367
525
  "personal fleet validate --json"
368
526
  ],
369
- "mutation": false
527
+ "mutation": false,
528
+ "help": "personal fleet validate --help",
529
+ "execution": {
530
+ "default": "read-only",
531
+ "authorization": "none"
532
+ }
370
533
  },
371
534
  {
372
535
  "id": "fleet onboard",
@@ -387,7 +550,12 @@
387
550
  "examples": [
388
551
  "personal fleet onboard --json"
389
552
  ],
390
- "mutation": false
553
+ "mutation": false,
554
+ "help": "personal fleet onboard --help",
555
+ "execution": {
556
+ "default": "read-only",
557
+ "authorization": "none"
558
+ }
391
559
  },
392
560
  {
393
561
  "id": "fleet sync",
@@ -408,7 +576,13 @@
408
576
  "examples": [
409
577
  "personal fleet sync --json"
410
578
  ],
411
- "mutation": true
579
+ "mutation": true,
580
+ "help": "personal fleet sync --help",
581
+ "execution": {
582
+ "default": "preview",
583
+ "authorization": "--apply --yes",
584
+ "note": "Supply required options shown by command --help. A preview does not apply changes."
585
+ }
412
586
  },
413
587
  {
414
588
  "id": "fleet init",
@@ -427,7 +601,13 @@
427
601
  "examples": [
428
602
  "personal fleet init --json"
429
603
  ],
430
- "mutation": true
604
+ "mutation": true,
605
+ "help": "personal fleet init --help",
606
+ "execution": {
607
+ "default": "preview",
608
+ "authorization": "--apply --yes",
609
+ "note": "Supply required options shown by command --help. A preview does not apply changes."
610
+ }
431
611
  },
432
612
  {
433
613
  "id": "fleet schema",
@@ -446,12 +626,17 @@
446
626
  "examples": [
447
627
  "personal fleet schema --json"
448
628
  ],
449
- "mutation": false
629
+ "mutation": false,
630
+ "help": "personal fleet schema --help",
631
+ "execution": {
632
+ "default": "read-only",
633
+ "authorization": "none"
634
+ }
450
635
  },
451
636
  {
452
637
  "id": "commands",
453
638
  "title": "Explore the command catalog",
454
- "description": "Search command names, descriptions and keywords; emit the complete machine-readable interface.",
639
+ "description": "Search the command index (also available as personal index); emit help links and mutation semantics.",
455
640
  "group": "Reference",
456
641
  "keywords": [
457
642
  "search",
@@ -464,9 +649,15 @@
464
649
  "commands"
465
650
  ],
466
651
  "examples": [
652
+ "personal index --json",
467
653
  "personal commands --json --search ssh"
468
654
  ],
469
- "mutation": false
655
+ "mutation": false,
656
+ "help": "personal commands --help",
657
+ "execution": {
658
+ "default": "read-only",
659
+ "authorization": "none"
660
+ }
470
661
  },
471
662
  {
472
663
  "id": "agent",
@@ -485,7 +676,12 @@
485
676
  "examples": [
486
677
  "personal agent --json"
487
678
  ],
488
- "mutation": false
679
+ "mutation": false,
680
+ "help": "personal agent --help",
681
+ "execution": {
682
+ "default": "read-only",
683
+ "authorization": "none"
684
+ }
489
685
  }
490
686
  ]
491
687
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crvouga/personal",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Interactive, agent-friendly developer fleet command center",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,8 +37,8 @@
37
37
  "registry": "https://registry.npmjs.org/"
38
38
  },
39
39
  "personalCli": {
40
- "contentHash": "1ac1f6314fac17a14b6d62ca5f7ef6d567813c1fbe1e5e9b53a7580790a9b076",
41
- "sourceCommit": "a241493b1e75735369bab9ca2bbcd575f80c5053",
40
+ "contentHash": "8554fd297a91a36218ea3e0736bd25fbd8043a68ceab25e8b32b452a8353c755",
41
+ "sourceCommit": "30085cc27cc2fd79da4b7b21ae1df5dffd30c525",
42
42
  "dirty": false
43
43
  }
44
44
  }
package/probe.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // packages/fleet/src/configure.ts
2
2
  import { join } from "node:path";
3
- import { mkdir, writeFile, rename } from "node:fs/promises";
3
+ import { mkdir, writeFile, rename, readFile } from "node:fs/promises";
4
4
  import { randomUUID } from "node:crypto";
5
5
 
6
6
  // packages/fleet/src/errors.ts
@@ -33,7 +33,15 @@ function absolutePath(value) {
33
33
  function parseNode(value) {
34
34
  if (!record(value))
35
35
  throw new FleetUsageError("Each node must be an object.");
36
- fields(value, ["id", "ssh", "workspace", "runtime", "required"]);
36
+ fields(value, [
37
+ "id",
38
+ "ssh",
39
+ "workspace",
40
+ "runtime",
41
+ "required",
42
+ "tailscaleId",
43
+ "bootstrapViaSsh"
44
+ ]);
37
45
  const names = nodeNames(value);
38
46
  if (!absolutePath(value["workspace"]))
39
47
  throw new FleetUsageError("workspace must be an absolute POSIX path.");
@@ -42,11 +50,22 @@ function parseNode(value) {
42
50
  }
43
51
  return {
44
52
  ...names,
53
+ ...enrollmentOptions(value),
45
54
  workspace: value["workspace"],
46
55
  ...typeof value["runtime"] === "string" ? { runtime: value["runtime"] } : {},
47
56
  required: requiredCapabilities(value["required"])
48
57
  };
49
58
  }
59
+ function enrollmentOptions(value) {
60
+ if (value["bootstrapViaSsh"] !== undefined && typeof value["bootstrapViaSsh"] !== "boolean")
61
+ throw new FleetUsageError("bootstrapViaSsh must explicitly authorize an existing trusted SSH bootstrap route.");
62
+ if (value["tailscaleId"] !== undefined && (typeof value["tailscaleId"] !== "string" || !/^[a-zA-Z0-9:_-]{1,128}$/.test(value["tailscaleId"])))
63
+ throw new FleetUsageError("tailscaleId must be the explicitly enrolled stable Tailscale device ID.");
64
+ return {
65
+ ...value["bootstrapViaSsh"] === true ? { bootstrapViaSsh: true } : {},
66
+ ...typeof value["tailscaleId"] === "string" ? { tailscaleId: value["tailscaleId"] } : {}
67
+ };
68
+ }
50
69
  function nodeNames(value) {
51
70
  if (typeof value["id"] !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(value["id"])) {
52
71
  throw new FleetUsageError("Node IDs must be lowercase names (1–63 characters).");
@@ -88,16 +107,33 @@ function parseConfig(value) {
88
107
  function validConfiguration(value) {
89
108
  return record(value) && typeof value["revision"] === "string" && /^[a-f0-9]{40}$/.test(value["revision"]) && ["codex", "claude", "opencode", "gemini"].includes(String(value["tool"])) && ["direct", "proxy"].includes(String(value["profile"])) && typeof value["node"] === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value["node"]);
90
109
  }
91
- async function configureWorkspace(root, request, run) {
110
+ async function configureWorkspace(root, request, run, options = {
111
+ additionalTools: [],
112
+ bootstrappedDeployment: false
113
+ }) {
114
+ if (options.additionalTools.some((tool) => !validConfiguration({ ...request, tool })))
115
+ return failure("Invalid additional profile.");
92
116
  if (!validConfiguration(request))
93
117
  return failure("Invalid reviewed revision or configuration selection.");
94
- const directory = join(root, "node_modules", ".fleet-revisions", request.revision);
118
+ const directory = deploymentDirectory(root, request.revision, options.bootstrappedDeployment);
95
119
  const head = await run(["git", "rev-parse", "--verify", `${request.revision}^{commit}`], { cwd: root });
96
120
  if (!head.ok || head.stdout.trim() !== request.revision)
97
121
  return failure("Reviewed commit is unavailable locally. Fetch it explicitly and retry.");
98
122
  const prepared = await prepareRevision(root, directory, request.revision, run);
99
123
  if (prepared.length)
100
124
  return prepared;
125
+ if (!options.bootstrappedDeployment) {
126
+ const setup = await setupRevision(directory, run);
127
+ if (setup.length)
128
+ return setup;
129
+ }
130
+ const checks = [];
131
+ for (const tool of [request.tool, ...options.additionalTools]) {
132
+ checks.push(...await applyProfile(root, directory, { ...request, tool }, run));
133
+ }
134
+ return checks;
135
+ }
136
+ async function setupRevision(directory, run) {
101
137
  const setup = await run([
102
138
  "mise",
103
139
  "exec",
@@ -110,6 +146,9 @@ async function configureWorkspace(root, request, run) {
110
146
  ], { cwd: directory, timeoutMs: 1200000 });
111
147
  if (!setup.ok)
112
148
  return failure("Existing setup owner failed. Partial tool/dependency installation is possible; no configuration receipt was advanced.");
149
+ return [];
150
+ }
151
+ async function applyProfile(root, directory, request, run) {
113
152
  const applied = await applyOwner(directory, request, run);
114
153
  if (!applied.ok)
115
154
  return failure("personal workstation configuration failed. Inspect preview on this node; no rollout receipt was advanced.");
@@ -137,6 +176,8 @@ function failure(summary) {
137
176
  return [{ id: "configuration:apply", status: "fail", summary }];
138
177
  }
139
178
  async function prepareRevision(root, directory, revision, run) {
179
+ if (root === directory)
180
+ return verifyRevision(directory, revision, run);
140
181
  await mkdir(join(root, "node_modules", ".fleet-revisions"), {
141
182
  recursive: true
142
183
  });
@@ -147,6 +188,10 @@ async function prepareRevision(root, directory, revision, run) {
147
188
  return failure("Cannot create pinned deployment worktree. Existing data was preserved.");
148
189
  deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
149
190
  }
191
+ return verifyRevision(directory, revision, run);
192
+ }
193
+ async function verifyRevision(directory, revision, run) {
194
+ const deployed = await run(["git", "rev-parse", "HEAD"], { cwd: directory });
150
195
  const clean = await run(["git", "status", "--porcelain"], { cwd: directory });
151
196
  if (!deployed.ok || deployed.stdout.trim() !== revision || !clean.ok || clean.stdout.trim())
152
197
  return failure("Pinned deployment checkout differs or is dirty. Inspect it; fleet never resets it.");
@@ -161,9 +206,17 @@ async function recordConfiguration(root, request, receipt) {
161
206
  const receipts = join(root, "node_modules", ".fleet-configuration");
162
207
  await mkdir(receipts, { recursive: true, mode: 448 });
163
208
  const path = join(receipts, `${request.node}-${request.tool}-${request.profile}.json`);
209
+ const contents = JSON.stringify({ version: 1, ...request, fingerprint: receipt }) + `
210
+ `;
211
+ try {
212
+ if (await readFile(path, "utf8") === contents)
213
+ return;
214
+ } catch (error) {
215
+ if (error.code !== "ENOENT")
216
+ throw error;
217
+ }
164
218
  const temp = `${path}.${randomUUID()}.tmp`;
165
- await writeFile(temp, JSON.stringify({ version: 1, ...request, fingerprint: receipt }) + `
166
- `, { flag: "wx", mode: 384 });
219
+ await writeFile(temp, contents, { flag: "wx", mode: 384 });
167
220
  await rename(temp, path);
168
221
  }
169
222
  async function applyOwner(directory, request, run) {
@@ -188,14 +241,17 @@ async function applyOwner(directory, request, run) {
188
241
  request.profile
189
242
  ], { cwd: directory, timeoutMs: 120000 });
190
243
  }
244
+ function deploymentDirectory(root, revision, bootstrapped) {
245
+ return bootstrapped ? root : join(root, "node_modules", ".fleet-revisions", revision);
246
+ }
191
247
 
192
248
  // packages/fleet/src/fingerprint.ts
193
- import { readFile as readFile2, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
249
+ import { readFile as readFile3, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
194
250
  import { join as join3 } from "node:path";
195
251
 
196
252
  // packages/fleet/src/input-hashes.ts
197
253
  import { createHash } from "node:crypto";
198
- import { readFile, readdir } from "node:fs/promises";
254
+ import { readFile as readFile2, readdir } from "node:fs/promises";
199
255
  import { join as join2 } from "node:path";
200
256
  var INPUT_FILES = [
201
257
  "package.json",
@@ -210,7 +266,7 @@ var FINGERPRINT_FILES = [
210
266
  ];
211
267
  async function fileHash(path) {
212
268
  try {
213
- return digest(await readFile(path));
269
+ return digest(await readFile2(path));
214
270
  } catch {
215
271
  return null;
216
272
  }
@@ -276,7 +332,7 @@ async function dependencyCheck(root, current) {
276
332
  action: "Review fleet sync, then explicitly apply a frozen toolchain/dependency sync."
277
333
  };
278
334
  try {
279
- const receipt = JSON.parse(await readFile2(join3(root, RECEIPT), "utf8"));
335
+ const receipt = JSON.parse(await readFile3(join3(root, RECEIPT), "utf8"));
280
336
  const matches = record(receipt) && receipt["version"] === 1 && JSON.stringify(receipt["files"]) === JSON.stringify(current.files);
281
337
  return {
282
338
  id: "dependencies",
@@ -626,7 +682,7 @@ function runCommand(command, options = {}) {
626
682
  GIT_TERMINAL_PROMPT: "0",
627
683
  CI: "1"
628
684
  },
629
- stdio: "pipe",
685
+ stdio: childStdio(options.preserveOperationLock),
630
686
  windowsHide: true,
631
687
  detached: process.platform !== "win32"
632
688
  });
@@ -680,6 +736,20 @@ function runCommand(command, options = {}) {
680
736
  child.stdin.end(options.input);
681
737
  });
682
738
  }
739
+ function childStdio(preserveLock) {
740
+ return preserveLock ? [
741
+ "pipe",
742
+ "pipe",
743
+ "pipe",
744
+ "ignore",
745
+ "ignore",
746
+ "ignore",
747
+ "ignore",
748
+ "ignore",
749
+ "ignore",
750
+ 9
751
+ ] : ["pipe", "pipe", "pipe"];
752
+ }
683
753
 
684
754
  // packages/fleet/src/sync.ts
685
755
  import { unlink } from "node:fs/promises";
@@ -943,6 +1013,8 @@ var STATUSES = ["pass", "warn", "fail", "skip"];
943
1013
  function validCheck(value) {
944
1014
  if (!record(value) || typeof value["id"] !== "string" || !/^[a-z0-9:._-]{1,100}$/.test(value["id"]) || !STATUSES.includes(String(value["status"])) || typeof value["summary"] !== "string" || value["summary"].length > 2000)
945
1015
  return false;
1016
+ if (!validDuration(value["phaseDurationMs"]))
1017
+ return false;
946
1018
  const remedy = value["remedy"];
947
1019
  return remedy === undefined || validRemedy(remedy);
948
1020
  }
@@ -965,6 +1037,9 @@ function validFingerprint(value) {
965
1037
  function validHash(value) {
966
1038
  return value === null || typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
967
1039
  }
1040
+ function validDuration(value) {
1041
+ return value === undefined || typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 86400000;
1042
+ }
968
1043
 
969
1044
  // packages/fleet/src/remote-entry.ts
970
1045
  async function main() {
package/releasing.md CHANGED
@@ -165,3 +165,12 @@ npx --yes @crvouga/personal@latest fleet doctor --all --json
165
165
  Do not claim release or green CI based on local checks. Deployment of the CI
166
166
  workflow requires these source changes to land on main, and remote fleet coverage
167
167
  requires actual locally configured SSH aliases and checkout paths.
168
+
169
+ ## Fleet-sync release gate
170
+
171
+ The Ubuntu fleet-sync flow is not yet a complete machine-convergence release.
172
+ Read `../fleet/docs/ubuntu-sync.md` before publication. The package build embeds
173
+ an automatically approvable source SHA only for a clean checkout; dirty builds
174
+ retain provenance metadata but require an explicit reviewed revision. The anonymous
175
+ consumer smoke covers both `fleet-sync` and `fleet converge` previews without a
176
+ checkout. Passing those checks is not Ubuntu/service/provider acceptance.