@timurproko/a1 0.1.8-dev.0aceebe → 0.1.8-dev.112

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 (27) hide show
  1. package/README.md +41 -31
  2. package/dist/native/darwin-arm64/manifest.json +1 -1
  3. package/dist/native/linux-x64/manifest.json +1 -1
  4. package/dist/native/win32-x64/manifest.json +2 -2
  5. package/dist/native/win32-x64/process-guardian.exe +0 -0
  6. package/dist/src/cli/dispatch.js +67 -30
  7. package/dist/src/cli/packages.d.ts +9 -1
  8. package/dist/src/cli/packages.js +24 -20
  9. package/dist/src/cli/version-stats.js +4 -4
  10. package/dist/src/foundation/pi-component-adapter/index.d.ts +1 -1
  11. package/dist/src/foundation/pi-component-adapter/index.js +1 -1
  12. package/dist/src/foundation/pi-component-adapter/shell-presenters-transcript.js +2 -1
  13. package/dist/src/foundation/pi-engine-adapter/adapter.d.ts +7 -0
  14. package/dist/src/foundation/pi-engine-adapter/adapter.js +193 -39
  15. package/dist/src/foundation/pi-owned-ui-integration/session-shell.d.ts +6 -0
  16. package/dist/src/foundation/pi-owned-ui-integration/session-shell.js +223 -13
  17. package/dist/src/foundation/release/index.d.ts +0 -1
  18. package/dist/src/foundation/release/index.js +0 -1
  19. package/dist/src/foundation/release/update.d.ts +1 -1
  20. package/dist/src/foundation/release/update.js +13 -15
  21. package/docs/architecture/toolchain.md +1 -1
  22. package/docs/ci-release-runbook.md +76 -79
  23. package/docs/features/launch-profiles.md +1 -1
  24. package/docs/manual-transparent-checkpoint.md +1 -1
  25. package/package.json +3 -1
  26. package/dist/src/foundation/release/development-preview-release.d.ts +0 -49
  27. package/dist/src/foundation/release/development-preview-release.js +0 -114
@@ -1,6 +1,6 @@
1
1
  import { MOUSE_TRACKING_OFF, MOUSE_TRACKING_ON, parseMouseInput } from "../ui-components/index.js";
2
2
  import { PINNED_PI_HIDDEN_COMMAND_NAMES, PINNED_PI_WORKFLOW_COMMAND_NAMES, } from "../pi-engine-adapter/index.js";
3
- import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
3
+ import { createPiExtensionUiBridge, createPiQueuedInputStatus, createPiShellArmin, createPiShellAuthProviderSelector, createPiShellChangelog, createPiShellDaxnuts, createPiShellDialog, createPiShellEarendilAnnouncement, createPiShellEditor, createPiShellExtensionSelector, createPiShellFooter, createPiShellHeader, createPiShellHotkeys, createPiShellLoadedResources, createPiShellLoginDialog, createPiShellModelSelector, createPiShellOperationLoader, createPiShellReloadBox, createPiShellScopedModelsSelector, createPiShellSelector, createPiShellSessionInfo, createPiShellSessionSelector, createPiShellSettingsSelector, createPiShellStatus, createPiShellTranscriptComponent, createPiShellTreeSelector, createPiShellTrustSelector, createPiShellUserMessageSelector, onPiThemeChange, piTheme, renderPiShellPackageUpdateNotice, renderPiShellStartupDiagnostic, renderPiShellStatusText, renderPiShellTranscriptBlock, } from "../pi-component-adapter/index.js";
4
4
  import { PiTuiRuntimeAdapter, } from "../pi-tui-runtime-adapter/index.js";
5
5
  export class OwnedUiSessionShellRoot {
6
6
  editor;
@@ -8,6 +8,9 @@ export class OwnedUiSessionShellRoot {
8
8
  resources;
9
9
  #cwd;
10
10
  #transcript = new Map();
11
+ #blocksById = new Map();
12
+ #renderedRows = new Map();
13
+ #themeUnsubscribe;
11
14
  #transcriptOrder = [];
12
15
  #view;
13
16
  #status;
@@ -54,6 +57,9 @@ export class OwnedUiSessionShellRoot {
54
57
  this.editor.addToHistory(block.text);
55
58
  }
56
59
  this.#syncTranscript(view.transcript);
60
+ // Colours come from the active theme, so rendered rows outlive their revision only
61
+ // until the theme under them changes.
62
+ this.#themeUnsubscribe = onPiThemeChange(() => this.#renderedRows.clear());
57
63
  }
58
64
  update(view) {
59
65
  this.#view = view;
@@ -65,6 +71,25 @@ export class OwnedUiSessionShellRoot {
65
71
  this.editor.setThinkingLevel(view.thinkingLevel);
66
72
  this.invalidate();
67
73
  }
74
+ /**
75
+ * Applies one block: its component is created or updated in place and the order grows
76
+ * only when the block is new. A streamed chunk costs one component update rather than a
77
+ * walk of the whole transcript.
78
+ */
79
+ applyTranscriptBlock(block) {
80
+ this.#blocksById.set(block.id, block);
81
+ const component = this.#transcript.get(block.id);
82
+ if (component === undefined) {
83
+ const created = createPiShellTranscriptComponent(block, this.#cwd, this.#extensionRenderers);
84
+ created.setExpanded(this.#toolsExpanded);
85
+ this.#transcript.set(block.id, created);
86
+ this.#transcriptOrder.push(block.id);
87
+ }
88
+ else if (component.revision !== block.revision) {
89
+ component.update(block);
90
+ }
91
+ this.invalidate();
92
+ }
68
93
  render(width) {
69
94
  const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
70
95
  return [
@@ -128,10 +153,10 @@ export class OwnedUiSessionShellRoot {
128
153
  }
129
154
  #renderDocument(width) {
130
155
  const transcript = this.#transcriptOrder.flatMap((id, index) => {
131
- const block = this.#view.transcript.find(item => item.id === id);
156
+ const block = this.#blocksById.get(id);
132
157
  if (!this.#thinkingVisible && block?.kind === "thinking")
133
158
  return [];
134
- const rows = this.#transcript.get(id)?.render(width) ?? [];
159
+ const rows = this.#blockRows(id, block, width);
135
160
  if (index > 0 && block?.kind === "user")
136
161
  return ["", ...rows];
137
162
  return rows;
@@ -167,6 +192,25 @@ export class OwnedUiSessionShellRoot {
167
192
  ...diagnosticRows,
168
193
  ];
169
194
  }
195
+ /**
196
+ * Rows for one transcript block. A finalized block renders once for a given revision
197
+ * and width and is reused after that, so a frame costs what changed rather than what
198
+ * the session has accumulated. A live block, and anything the shell drives itself, is
199
+ * rendered every time because its content is still moving.
200
+ */
201
+ #blockRows(id, block, width) {
202
+ const component = this.#transcript.get(id);
203
+ if (component === undefined)
204
+ return [];
205
+ if (block === undefined || block.status !== "finalized")
206
+ return component.render(width);
207
+ const cached = this.#renderedRows.get(id);
208
+ if (cached && cached.width === width && cached.revision === block.revision)
209
+ return cached.rows;
210
+ const rows = component.render(width);
211
+ this.#renderedRows.set(id, { width, revision: block.revision, rows });
212
+ return rows;
213
+ }
170
214
  transcriptComponent(id) {
171
215
  return this.#transcript.get(id);
172
216
  }
@@ -334,6 +378,8 @@ export class OwnedUiSessionShellRoot {
334
378
  this.#extensionWorkingMessage = undefined;
335
379
  this.#status.setWorkingOverride(undefined);
336
380
  this.#footer.update(this.#viewWithExtensionStatuses(this.#view));
381
+ // An extension renderer may have drawn transcript blocks that are now unrendered by it.
382
+ this.#renderedRows.clear();
337
383
  this.invalidate();
338
384
  }
339
385
  addExtensionNotification(message, type) {
@@ -384,11 +430,13 @@ export class OwnedUiSessionShellRoot {
384
430
  this.#inputSurface.setFocused?.(focused);
385
431
  }
386
432
  dispose() {
433
+ this.#themeUnsubscribe();
387
434
  this.header.dispose?.();
388
435
  this.resources.dispose?.();
389
436
  for (const component of this.#transcript.values())
390
437
  component.dispose?.();
391
438
  this.#transcript.clear();
439
+ this.#renderedRows.clear();
392
440
  if (this.#inputSurface !== this.editor)
393
441
  this.#inputSurface.dispose?.();
394
442
  this.#extensionHeader?.dispose?.();
@@ -402,12 +450,16 @@ export class OwnedUiSessionShellRoot {
402
450
  this.#queued.dispose?.();
403
451
  }
404
452
  #syncTranscript(blocks) {
453
+ this.#blocksById.clear();
454
+ for (const block of blocks)
455
+ this.#blocksById.set(block.id, block);
405
456
  const nextIds = new Set(blocks.map(block => block.id));
406
457
  for (const [id, component] of this.#transcript) {
407
458
  if (id.startsWith("workflow-status-") || nextIds.has(id))
408
459
  continue;
409
460
  component.dispose?.();
410
461
  this.#transcript.delete(id);
462
+ this.#renderedRows.delete(id);
411
463
  }
412
464
  for (const block of blocks) {
413
465
  const component = this.#transcript.get(block.id);
@@ -431,8 +483,9 @@ export class OwnedUiSessionShellRoot {
431
483
  if (block !== undefined)
432
484
  order.push(block.id);
433
485
  }
486
+ const placed = new Set(order);
434
487
  for (const statusId of statusIds) {
435
- if (!order.includes(statusId))
488
+ if (!placed.has(statusId))
436
489
  order.push(statusId);
437
490
  }
438
491
  this.#transcriptOrder = order;
@@ -498,6 +551,8 @@ export class OwnedUiSessionShellRoot {
498
551
  this.resources.setExpanded(expanded);
499
552
  for (const component of this.#transcript.values())
500
553
  component.setExpanded(expanded);
554
+ // Expansion changes what a block draws without changing its revision.
555
+ this.#renderedRows.clear();
501
556
  this.invalidate();
502
557
  }
503
558
  }
@@ -525,12 +580,14 @@ function shellResourceEntries(backend) {
525
580
  sourcePath: resource.sourcePath,
526
581
  diagnostic: resource.diagnostic,
527
582
  }));
528
- for (const extension of backend.extensionResources()) {
529
- if (extension.hidden)
530
- continue;
583
+ const extensions = backend.extensionResources().filter(extension => !extension.hidden);
584
+ const loadedExtensions = extensions.filter(extension => extension.diagnostic === null);
585
+ const extensionLabels = compactExtensionLabels(loadedExtensions);
586
+ for (const extension of extensions) {
587
+ const labelIndex = loadedExtensions.indexOf(extension);
531
588
  resources.push({
532
589
  section: "Extensions",
533
- label: compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
590
+ label: extensionLabels[labelIndex] ?? compactResourceLabel(extension.sourcePath ?? extension.resolvedPath ?? "extension"),
534
591
  sourcePath: extension.sourcePath ?? extension.resolvedPath,
535
592
  diagnostic: extension.diagnostic,
536
593
  });
@@ -538,12 +595,129 @@ function shellResourceEntries(backend) {
538
595
  return resources;
539
596
  }
540
597
  function compactResourceLabel(path) {
541
- const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
598
+ const segments = compactPathSegments(path);
542
599
  const leaf = segments.at(-1) ?? path;
543
600
  if ((leaf === "index.ts" || leaf === "index.js") && segments.length > 1)
544
601
  return segments.at(-2) ?? leaf;
545
602
  return leaf;
546
603
  }
604
+ /**
605
+ * Pinned from InteractiveMode's compact extension-label helpers at Pi commit
606
+ * 914cf1472e715297caa30db4b9535d534a9eb718. The source metadata crosses an
607
+ * A1-owned boundary first; the owned shell never inspects Pi's private root.
608
+ */
609
+ function compactExtensionLabels(extensions) {
610
+ const localExtensions = extensions
611
+ .filter(extension => !isPackageExtensionSource(extension.sourceInfo))
612
+ .map(extension => {
613
+ const path = extension.sourcePath ?? extension.resolvedPath ?? "extension";
614
+ const segments = compactPathSegments(path);
615
+ if (segments.length > 1 && (segments.at(-1) === "index.ts" || segments.at(-1) === "index.js"))
616
+ segments.pop();
617
+ return { extension, segments };
618
+ });
619
+ return extensions.map(extension => {
620
+ const resourcePath = extension.sourcePath ?? extension.resolvedPath ?? "extension";
621
+ if (isPackageExtensionSource(extension.sourceInfo)) {
622
+ return compactPackageExtensionLabel(resourcePath, extension.sourceInfo);
623
+ }
624
+ const localIndex = localExtensions.findIndex(item => item.extension === extension);
625
+ const segments = localExtensions[localIndex]?.segments;
626
+ if (!segments || segments.length === 0)
627
+ return compactResourceLabel(resourcePath);
628
+ for (let count = 1; count <= segments.length; count += 1) {
629
+ const candidate = segments.slice(-count).join("/");
630
+ if (localExtensions.every((item, itemIndex) => itemIndex === localIndex || item.segments.slice(-count).join("/") !== candidate)) {
631
+ return candidate;
632
+ }
633
+ }
634
+ return segments.join("/");
635
+ });
636
+ }
637
+ function compactPackageExtensionLabel(resourcePath, sourceInfo) {
638
+ const sourceLabel = compactPackageSourceLabel(sourceInfo.source);
639
+ if (!sourceLabel)
640
+ return compactResourceLabel(resourcePath);
641
+ const shortPath = shortPackagePath(resourcePath, sourceInfo).replaceAll("\\", "/");
642
+ const packagePath = shortPath.startsWith("extensions/") ? shortPath.slice("extensions/".length) : shortPath;
643
+ const slash = packagePath.lastIndexOf("/");
644
+ const fileName = slash < 0 ? packagePath : packagePath.slice(slash + 1);
645
+ const directory = slash < 0 ? "" : packagePath.slice(0, slash);
646
+ const extension = fileName.lastIndexOf(".");
647
+ const name = extension <= 0 ? fileName : fileName.slice(0, extension);
648
+ if (name === "index")
649
+ return !directory || directory === "." ? sourceLabel : `${sourceLabel}:${directory}`;
650
+ return `${sourceLabel}:${packagePath}`;
651
+ }
652
+ function compactPackageSourceLabel(source) {
653
+ if (source.startsWith("npm:"))
654
+ return source.slice("npm:".length) || source;
655
+ if (!source.startsWith("git:"))
656
+ return source;
657
+ const gitSource = source.slice("git:".length).trim();
658
+ let repositoryPath;
659
+ const scpLike = gitSource.match(/^git@[^:]+:(.+)$/);
660
+ if (scpLike?.[1]) {
661
+ repositoryPath = scpLike[1];
662
+ }
663
+ else if (/^[a-z]+:\/\//i.test(gitSource)) {
664
+ try {
665
+ repositoryPath = new URL(gitSource).pathname.replace(/^\/+/, "");
666
+ }
667
+ catch {
668
+ return source;
669
+ }
670
+ }
671
+ else {
672
+ const slash = gitSource.indexOf("/");
673
+ if (slash >= 0)
674
+ repositoryPath = gitSource.slice(slash + 1);
675
+ }
676
+ if (!repositoryPath)
677
+ return source;
678
+ const ref = repositoryPath.indexOf("@");
679
+ const withoutRef = ref < 0 ? repositoryPath : repositoryPath.slice(0, ref);
680
+ return withoutRef.replace(/\.git$/, "") || source;
681
+ }
682
+ function shortPackagePath(resourcePath, sourceInfo) {
683
+ const fullPath = normalizeResourcePath(resourcePath);
684
+ const baseDir = sourceInfo.baseDir === null ? undefined : normalizeResourcePath(sourceInfo.baseDir).replace(/\/$/, "");
685
+ if (baseDir) {
686
+ const npmRoot = baseDir.match(/^(.*\/node_modules)\/(@?[^/]+(?:\/[^/]+)?)$/);
687
+ if (npmRoot?.[1] && fullPath.startsWith(`${npmRoot[1]}/`))
688
+ return relativeResourcePath(baseDir, fullPath);
689
+ if (fullPath === baseDir)
690
+ return ".";
691
+ if (fullPath.startsWith(`${baseDir}/`))
692
+ return fullPath.slice(baseDir.length + 1);
693
+ }
694
+ const npmMatch = fullPath.match(/node_modules\/(@?[^/]+(?:\/[^/]+)?)\/(.*)/);
695
+ if (npmMatch?.[2] && sourceInfo.source.startsWith("npm:"))
696
+ return npmMatch[2];
697
+ const gitMatch = fullPath.match(/git\/[^/]+\/[^/]+\/(.*)/);
698
+ if (gitMatch?.[1] && sourceInfo.source.startsWith("git:"))
699
+ return gitMatch[1];
700
+ return resourcePath;
701
+ }
702
+ function relativeResourcePath(from, to) {
703
+ const fromSegments = from.split("/").filter(Boolean);
704
+ const toSegments = to.split("/").filter(Boolean);
705
+ let common = 0;
706
+ while (common < fromSegments.length && common < toSegments.length
707
+ && fromSegments[common]?.toLowerCase() === toSegments[common]?.toLowerCase())
708
+ common += 1;
709
+ return [...fromSegments.slice(common).map(() => ".."), ...toSegments.slice(common)].join("/") || ".";
710
+ }
711
+ function compactPathSegments(path) {
712
+ return normalizeResourcePath(path).split("/").filter(segment => segment.length > 0 && segment !== "~");
713
+ }
714
+ function normalizeResourcePath(path) {
715
+ return path.replaceAll("\\", "/");
716
+ }
717
+ function isPackageExtensionSource(sourceInfo) {
718
+ const source = sourceInfo?.source ?? "";
719
+ return source.startsWith("npm:") || source.startsWith("git:");
720
+ }
547
721
  export class OwnedUiSessionShell {
548
722
  backend;
549
723
  root;
@@ -560,6 +734,7 @@ export class OwnedUiSessionShell {
560
734
  #sequence = 0;
561
735
  #started = false;
562
736
  #disposed = false;
737
+ #pointerReporting = false;
563
738
  #compactionQueue = [];
564
739
  #lastClearTime = 0;
565
740
  #activeLoginDialog;
@@ -638,8 +813,13 @@ export class OwnedUiSessionShell {
638
813
  });
639
814
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
640
815
  this.#unsubscribe = this.backend.onEvent(event => {
641
- this.#syncView();
642
- if (this.view().lifecycle === "ready" && this.#compactionQueue.length > 0)
816
+ // A streamed chunk names one block, and touching only that block is what keeps the
817
+ // cost of a chunk the same in a long session as in a new one. Everything else
818
+ // resynchronizes the view, which is cheap next to re-reading the transcript.
819
+ const view = event.type === "transcript-block" && this.#sessionGeneration === this.backend.sessionGeneration
820
+ ? this.#syncBlock(event.block)
821
+ : this.#syncView();
822
+ if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
643
823
  void this.#flushCompactionQueue();
644
824
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
645
825
  this.#resolveStopped?.();
@@ -1232,22 +1412,51 @@ export class OwnedUiSessionShell {
1232
1412
  this.root.appendDaxnuts();
1233
1413
  }
1234
1414
  }
1415
+ /**
1416
+ * Turns terminal pointer reporting on for a screen that reads the pointer, and off for
1417
+ * every path that ends it. While it is on the terminal hands A1 the wheel and the
1418
+ * button instead of scrolling and selecting itself, so leaving it on outlives the
1419
+ * screen that wanted it and takes the terminal's own scrolling and selection with it.
1420
+ */
1421
+ #setPointerReporting(enabled) {
1422
+ if (this.#pointerReporting === enabled)
1423
+ return;
1424
+ this.#pointerReporting = enabled;
1425
+ if (!this.runtime.active)
1426
+ return;
1427
+ this.runtime.writeControl(enabled ? MOUSE_TRACKING_ON : MOUSE_TRACKING_OFF);
1428
+ }
1235
1429
  async dispose() {
1236
1430
  if (this.#disposed)
1237
1431
  return;
1238
1432
  this.#disposed = true;
1433
+ this.#setPointerReporting(false);
1239
1434
  this.#unsubscribe();
1240
1435
  this.#dialogHandle?.hide();
1241
1436
  await this.backend.unbindExtensionUi();
1242
1437
  this.#extensionBridge.dispose();
1243
1438
  await this.runtime.dispose();
1244
1439
  }
1440
+ /**
1441
+ * Applies one transcript block without re-reading the session. The listeners still hear
1442
+ * the view they would have heard, so nothing downstream can tell the difference.
1443
+ */
1444
+ #syncBlock(block) {
1445
+ this.root.applyTranscriptBlock(block);
1446
+ this.runtime.requestRender();
1447
+ const view = this.view();
1448
+ for (const listener of this.#listeners)
1449
+ listener(view);
1450
+ return view;
1451
+ }
1245
1452
  #syncView() {
1246
1453
  const view = this.view();
1247
1454
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
1248
1455
  this.#sessionGeneration = this.backend.sessionGeneration;
1249
1456
  this.#activeLoginDialog = undefined;
1250
1457
  this.#extensionBridge.reset();
1458
+ // A replaced session takes its screens with it, pointer reporting included.
1459
+ this.#setPointerReporting(false);
1251
1460
  this.root.setInputSurface(null);
1252
1461
  this.root.resetExtensionUi();
1253
1462
  this.root.resetWorkflowPresentation();
@@ -1257,6 +1466,7 @@ export class OwnedUiSessionShell {
1257
1466
  this.runtime.requestRender();
1258
1467
  for (const listener of this.#listeners)
1259
1468
  listener(view);
1469
+ return view;
1260
1470
  }
1261
1471
  #openOwnedRoute(route) {
1262
1472
  const surface = this.#routeHost?.open(route) ?? null;
@@ -1267,7 +1477,7 @@ export class OwnedUiSessionShell {
1267
1477
  this.#dialogHandle?.hide();
1268
1478
  // Any-event reporting: hover and drag are what the screen is driven by, and
1269
1479
  // it also stops the terminal treating a drag as a text selection.
1270
- this.runtime.writeControl(MOUSE_TRACKING_ON);
1480
+ this.#setPointerReporting(true);
1271
1481
  // The interrupt chord is global, so it is watched on raw input rather than
1272
1482
  // through the overlay: the pinned shell handles that key before an overlay
1273
1483
  // ever sees it, which is why an owned screen must not rely on being asked.
@@ -1290,7 +1500,7 @@ export class OwnedUiSessionShell {
1290
1500
  });
1291
1501
  const closeSurface = () => {
1292
1502
  removeInterruptWatch();
1293
- this.runtime.writeControl(MOUSE_TRACKING_OFF);
1503
+ this.#setPointerReporting(false);
1294
1504
  this.#dialogHandle?.hide();
1295
1505
  this.#dialogHandle = undefined;
1296
1506
  this.#dialogId = undefined;
@@ -1,7 +1,6 @@
1
1
  export * from "./bootstrap.js";
2
2
  export * from "./cohort-selection.js";
3
3
  export * from "./cohort-state.js";
4
- export * from "./development-preview-release.js";
5
4
  export * from "./process-cleanup.js";
6
5
  export * from "./release.js";
7
6
  export * from "./release-gc.js";
@@ -1,7 +1,6 @@
1
1
  export * from "./bootstrap.js";
2
2
  export * from "./cohort-selection.js";
3
3
  export * from "./cohort-state.js";
4
- export * from "./development-preview-release.js";
5
4
  export * from "./process-cleanup.js";
6
5
  export * from "./release.js";
7
6
  export * from "./release-gc.js";
@@ -33,7 +33,7 @@ export interface UpdatePerformanceEvidence {
33
33
  export interface SelfUpdateOptions {
34
34
  packageRoot: string;
35
35
  channel?: UpdateChannel;
36
- /** A specific preview to install, named by its commit or its full version. */
36
+ /** A specific preview to install, named by its development number or full version. */
37
37
  target?: string;
38
38
  environment?: NodeJS.ProcessEnv;
39
39
  fileSystem?: UpdateFileSystem;
@@ -20,7 +20,7 @@ const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
20
20
  * a release, and that is the word the repository, its tags, and its GitHub
21
21
  * releases all use.
22
22
  */
23
- const UPDATE_CHANNEL_LABELS = { stable: "release", next: "next" };
23
+ const UPDATE_CHANNEL_LABELS = { stable: "release", next: "develop" };
24
24
  const defaultFileSystem = {
25
25
  async readFile(path) { return await readFile(path, "utf8"); },
26
26
  realpath,
@@ -223,12 +223,13 @@ function createUpdateProgress(output, enabled) {
223
223
  }
224
224
  /** The newest version the channel points at, which is what an unqualified update takes. */
225
225
  async function resolveChannelHead(runner, distTag, output) {
226
- const lookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${distTag} channel`);
226
+ const publicChannel = distTag === "next" ? "development" : "release";
227
+ const lookup = await runNpm(runner, ["view", `${PRODUCT_PACKAGE}@${distTag}`, "version"], true, output, `query the npm ${publicChannel} channel`);
227
228
  if (lookup.result === null)
228
229
  return { version: null, exitCode: lookup.exitCode };
229
230
  const version = validSemver(lookup.result.stdout.trim());
230
231
  if (version === null) {
231
- output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${distTag} version from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
232
+ output.stderr(`${PRODUCT_TEXT.diagnostic(`received a malformed ${publicChannel} channel version from npm: ${JSON.stringify(lookup.result.stdout.trim())}.`)}\n`);
232
233
  return { version: null, exitCode: 1 };
233
234
  }
234
235
  return { version, exitCode: 0 };
@@ -236,14 +237,10 @@ async function resolveChannelHead(runner, distTag, output) {
236
237
  /**
237
238
  * Resolve a preview the caller named.
238
239
  *
239
- * A preview is published as `<version>-dev.<commit>`, so its commit is enough to
240
- * say which one is wanted the version in front of it is not something anyone
241
- * should have to remember. A full version is accepted too, for anyone reading one
242
- * back from `a1 version` or a changelog.
243
- *
244
- * The published list is the authority: naming a commit that was never published,
245
- * or one published more than once under different versions, is an error rather
246
- * than a guess.
240
+ * A preview is published as `<version>-dev.<pull-request-number>`, so its decimal
241
+ * development number is enough to identify it. A full version is accepted too.
242
+ * The published list remains authoritative: nothing is constructed from the
243
+ * installed package's base version.
247
244
  */
248
245
  async function resolveRequestedPreview(runner, requested, output) {
249
246
  const lookup = await runNpm(runner, ["view", PRODUCT_PACKAGE, "versions", "--json"], true, output, "list the published versions");
@@ -261,8 +258,7 @@ async function resolveRequestedPreview(runner, requested, output) {
261
258
  const exact = versions.find(version => version === requested);
262
259
  if (exact !== undefined) {
263
260
  // Naming a release here would install it through the preview path, which is a
264
- // different command with a different meaning. The commit form cannot express
265
- // one, so only the fuller spelling of a preview reaches this.
261
+ // different command with a different meaning.
266
262
  if (!exact.includes("-dev.")) {
267
263
  output.stderr(`${PRODUCT_TEXT.diagnostic(`${exact} is a release, not a preview; run ${PRODUCT_TEXT.commandName} update to move to the current release.`)}\n`);
268
264
  return { version: null, exitCode: 1 };
@@ -314,7 +310,9 @@ export async function runSelfUpdate(options) {
314
310
  if (resolved.version === null)
315
311
  return resolved.exitCode;
316
312
  const targetVersion = resolved.version;
317
- output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} ${targetVersion}.\n`);
313
+ // No full stop after a version: it already ends in a dot-separated identifier,
314
+ // and a trailing one reads as part of the version rather than as punctuation.
315
+ output.stdout(`${PRODUCT_TEXT.commandName} update (${UPDATE_CHANNEL_LABELS[channel]}): ${runningVersion} → ${targetVersion}\n`);
318
316
  const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true));
319
317
  const rootLookup = await measure("global-root", async () => await runNpm(runner, ["root", "--global"], true, output, "resolve npm's global package root"));
320
318
  if (rootLookup.result === null)
@@ -403,7 +401,7 @@ export async function runSelfUpdate(options) {
403
401
  await transactionStore.clearCompleted();
404
402
  options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
405
403
  progress.finish();
406
- output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}.\n`);
404
+ output.stdout(`${PRODUCT_TEXT.commandName} updated successfully: ${targetVersion}\n`);
407
405
  return 0;
408
406
  }
409
407
  catch (error) {
@@ -71,7 +71,7 @@ Presentation acceptance is the reader comparing `a1 pi` with pinned Pi. `node sc
71
71
 
72
72
  ## Publication
73
73
 
74
- One workflow publishes both channels: `.github/workflows/release.yml`, triggered by every push to `develop`. What the pushed commit declares decides the channel: a prerelease version publishes a preview under npm `next` at a `-dev.<short commit>` version stamped at publish time, and a stable version publishes under npm `latest`. Both pack once, validate those exact bytes on Windows, Linux, and macOS, and publish with provenance from the `npm-publish` environment. The publisher never rebuilds, a preview never changes `latest`, and the release tag, GitHub Release, and `master` are written only after the registry has the package.
74
+ One workflow publishes both channels: `.github/workflows/release.yml`. Pushes do not publish. Nightly development verification runs at `03:17 UTC`; `npm run develop` explicitly requests a numbered preview and `npm run release` explicitly requests stable publication after its version pull request merges. A preview is stamped as `-dev.<merged pull-request number>` and uses npm `next` only as an internal dist-tag. New candidates pack once and are validated on Windows, Linux, and macOS; a repeated nightly verifies the exact immutable registry tarball. All publication uses provenance from the `npm-publish` environment, a preview never changes `latest`, and the stable tag, GitHub Release, and `master` are written only after npm has the package. One global non-cancelling concurrency group serializes the final registry check.
75
75
 
76
76
  `docs/ci-release-runbook.md` is the operational reference.
77
77