@timurproko/a1 0.1.8-dev.0b6d8cc → 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.
package/README.md CHANGED
@@ -10,13 +10,14 @@ npm install --global @timurproko/a1@latest
10
10
 
11
11
  ```sh
12
12
  a1 # A1-owned UI and profile: ~/.a1/agent
13
- a1 version # show Installed, Release (latest), and Next versions
14
- a1 update # update to npm latest
15
- a1 update:next # update to npm next (or a1 update:<commit> for a specific preview)
13
+ a1 version # show Installed, Release, and Develop versions
14
+ a1 update # update to the current release
15
+ a1 update:develop # update to the current development preview
16
+ a1 update:107 # install numbered development preview 107
16
17
  a1 update --models # refresh A1's model catalogs
17
18
  ```
18
19
 
19
- Prerelease builds — what `a1 update:next` installs — add two development profiles
20
+ Prerelease builds — what `a1 update:develop` installs — add two development profiles
20
21
  for comparing against pinned Pi and for experimenting against an isolated profile.
21
22
  A release build does not carry them.
22
23
 
@@ -63,24 +64,30 @@ npm run test:full # complete non-physical suite
63
64
  Two channels, both published by CI from the exact bytes it validated. Nothing is
64
65
  ever published from a workstation.
65
66
 
66
- ### Previews — npm `next`
67
+ ### Development previews
67
68
 
68
- Automatic. Every push to `develop` publishes one, versioned
69
- `<major.minor.patch>-dev.<short commit>` the base from whatever `package.json`
70
- declares, the suffix naming the commit it was built from. It is stamped at publish
71
- time and never committed, so previews cost no commits and need no command, and an
72
- installed preview says exactly which source produced it.
69
+ Ordinary pushes to `develop` do not publish. The nightly run at `03:17 UTC` always
70
+ performs complete verification of current `origin/develop`; it publishes only when
71
+ that source's preview is absent. A maintainer can request the same authoritative
72
+ GitHub Actions path and wait for its result:
73
73
 
74
74
  ```sh
75
- a1 update:next # install the newest preview
76
- a1 update:7eabe9e # install the preview built from that commit
75
+ npm run develop
77
76
  ```
78
77
 
79
- Naming a commit is what the version suffix is for: read it off `a1 version`, a
80
- pull request, or a commit list, and install exactly that build you never need to
81
- know which version it went out under. A commit that was never published is refused
82
- rather than guessed at. A full preview version works in the same place, so a string
83
- pasted back from `a1 version` is understood too.
78
+ A preview is `<major.minor.patch>-dev.<pull-request number>`, stamped only into the
79
+ package. For example, the source GitHub presents as `develop (#107)` produces
80
+ `0.1.8-dev.107`. Repeating a request for immutable version 107 succeeds without
81
+ building or publishing again.
82
+
83
+ ```sh
84
+ a1 update:develop # install the current development preview
85
+ a1 update:107 # install preview 107
86
+ a1 update:0.1.8-dev.107 # install that exact full preview version
87
+ ```
88
+
89
+ The published version list is authoritative: an unpublished number is refused
90
+ rather than guessed. npm's `next` dist-tag remains an internal registry detail.
84
91
 
85
92
  ### Stable — npm `latest`
86
93
 
@@ -93,12 +100,11 @@ npm run release -- major # 0.1.1 -> 1.0.0
93
100
  npm run release -- 0.4.0 # an exact version
94
101
  ```
95
102
 
96
- It lands the version on `develop` through a pull request that merges itself.
97
- **Landing it is what publishes** the workflow builds, validates the packed release
98
- on Windows, Linux, and macOS, publishes to npm `latest` with provenance, and only
99
- then writes the `v<version>` tag and records the GitHub Release. The command waits
100
- for that to succeed, then opens the next `-dev` line so previews resume
101
- immediately.
103
+ It lands the version on `develop` through a pull request that merges itself, then
104
+ explicitly dispatches publication for that exact commit. GitHub Actions builds,
105
+ validates the packed release on Windows, Linux, and macOS, publishes to npm
106
+ `latest` with provenance, and only then writes the `v<version>` tag and records the
107
+ GitHub Release. The command waits for success before opening the next `-dev` line.
102
108
 
103
109
  ```sh
104
110
  a1 update # install the newest stable release
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-25T08:02:28.988Z",
8
+ "builtAt": "2026-08-25T09:42:56.359Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-25T08:02:31.153Z",
8
+ "builtAt": "2026-08-25T09:43:23.793Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-25T08:02:58.479Z",
8
+ "builtAt": "2026-08-25T09:44:35.937Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "fdbde2408643ffb897f5107d96ed40e8c8467505ef7daa64f86f9c2497c304ca",
11
+ "sha256": "0e2919843f1cdc824ce48203dace68df43f8ebb4b24b190ff7baf6c13d4846b1",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
@@ -6,8 +6,8 @@ export function cliUsage(capabilities) {
6
6
  ...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
7
7
  "version",
8
8
  "update [self|--models]",
9
- "update:next",
10
- "update:<commit>",
9
+ "update:develop",
10
+ "update:<number>",
11
11
  "pi install <source>",
12
12
  "pi remove <source>",
13
13
  "pi list",
@@ -58,20 +58,22 @@ export function parseCliCommand(arguments_, capabilities) {
58
58
  return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown command: ${command ?? ""}`) };
59
59
  }
60
60
  /**
61
- * What follows the colon says which build to move to. `next` is the newest
62
- * preview; anything else names one outright, by the commit it was built from or by
63
- * its full version — a preview is published as `<version>-dev.<commit>`, so the
64
- * commit alone is enough to find it.
61
+ * What follows the colon says which development build to move to. `develop`
62
+ * selects the channel head; a positive decimal or a full numbered preview names
63
+ * one immutable publication.
65
64
  */
66
65
  function parseColonUpdate(suffix, rest) {
67
66
  if (rest.length > 0)
68
67
  return { kind: "error", message: PRODUCT_TEXT.diagnostic("update takes what to move to after the colon, and nothing else.") };
69
- if (suffix === "next")
68
+ if (suffix === "next") {
69
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`renamed its development channel; run ${PRODUCT_TEXT.commandName} update:develop.`) };
70
+ }
71
+ if (suffix === "develop")
70
72
  return { kind: "update", channel: "next" };
71
73
  if (suffix.length === 0)
72
- return { kind: "error", message: PRODUCT_TEXT.diagnostic(`update: needs a preview after the colon, as in ${PRODUCT_TEXT.commandName} update:next.`) };
73
- if (!/^[0-9a-z][0-9a-z.+-]*$/i.test(suffix)) {
74
- return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unusable preview: ${suffix}`) };
74
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`update: needs a preview after the colon, as in ${PRODUCT_TEXT.commandName} update:develop.`) };
75
+ if (!/^[1-9]\d*$/.test(suffix) && !/^\d+\.\d+\.\d+-dev\.[1-9]\d*$/.test(suffix)) {
76
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unusable numbered preview: ${suffix}`) };
75
77
  }
76
78
  return { kind: "update", channel: "next", target: suffix };
77
79
  }
@@ -94,8 +96,8 @@ function parseUpdate(rest) {
94
96
  message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
95
97
  };
96
98
  }
97
- if (target === "next" || target === "stable") {
98
- const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
99
+ if (target === "next" || target === "develop" || target === "stable") {
100
+ const form = target === "stable" ? `${PRODUCT_TEXT.commandName} update` : `${PRODUCT_TEXT.commandName} update:develop`;
99
101
  return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
100
102
  }
101
103
  if (target === "--models")
@@ -11,6 +11,12 @@ export interface PackageCommandRequest {
11
11
  readonly verb: PackageCommandVerb;
12
12
  readonly source: string | null;
13
13
  }
14
+ export interface PackageCommandStyle {
15
+ readonly dim: (message: string) => string;
16
+ readonly bold: (message: string) => string;
17
+ readonly green: (message: string) => string;
18
+ readonly red: (message: string) => string;
19
+ }
14
20
  export interface PackageCommandEnvironment {
15
21
  readonly createPort: (input: AgentPackagesPortInput) => AgentPackagesPort;
16
22
  readonly cwd?: string;
@@ -18,6 +24,8 @@ export interface PackageCommandEnvironment {
18
24
  readonly stdout?: (message: string) => void;
19
25
  readonly stderr?: (message: string) => void;
20
26
  readonly initializeProfile?: typeof initializeProductProfile;
27
+ /** Defaults to Chalk's terminal-aware styles; injectable for transcript tests. */
28
+ readonly style?: PackageCommandStyle;
21
29
  }
22
30
  export declare function runPackageCommand(request: PackageCommandRequest, environment: PackageCommandEnvironment): Promise<number>;
23
- export declare function renderPackageOutcome(outcome: AgentPackageOutcome, profileRoot: string): string;
31
+ export declare function renderPackageOutcome(outcome: AgentPackageOutcome, profileRoot: string, style?: PackageCommandStyle): string;
@@ -1,3 +1,4 @@
1
+ import chalk from "chalk";
1
2
  import { configurationRootForProfile, initializeProductProfile, resolveLaunchProfilePaths } from "../features/launch/index.js";
2
3
  import { PRODUCT_TEXT } from "../product-identity.js";
3
4
  export async function runPackageCommand(request, environment) {
@@ -5,6 +6,7 @@ export async function runPackageCommand(request, environment) {
5
6
  const stderr = environment.stderr ?? (message => process.stderr.write(message));
6
7
  const cwd = environment.cwd ?? process.cwd();
7
8
  const processEnvironment = environment.environment ?? process.env;
9
+ const style = environment.style ?? chalk;
8
10
  const paths = resolveLaunchProfilePaths({ environment: processEnvironment });
9
11
  const profileRoot = configurationRootForProfile("a1", paths);
10
12
  if (profileRoot === null)
@@ -19,10 +21,10 @@ export async function runPackageCommand(request, environment) {
19
21
  const port = environment.createPort({
20
22
  profileRoot,
21
23
  cwd,
22
- onProgress: progress => stdout(`${progress.message}\n`),
24
+ onProgress: progress => stdout(`${style.dim(progress.message)}\n`),
23
25
  });
24
26
  const outcome = await runVerb(port, request);
25
- const rendered = renderPackageOutcome(outcome, profileRoot);
27
+ const rendered = renderPackageOutcome(outcome, profileRoot, style);
26
28
  (outcome.status === "completed" ? stdout : stderr)(rendered);
27
29
  return outcome.status === "completed" ? 0 : 1;
28
30
  }
@@ -37,38 +39,40 @@ async function runVerb(port, request) {
37
39
  throw new Error(PRODUCT_TEXT.diagnostic(`requires a source for ${request.verb}`));
38
40
  return request.verb === "install" ? await port.install(request.source) : await port.remove(request.source);
39
41
  }
40
- export function renderPackageOutcome(outcome, profileRoot) {
41
- const name = PRODUCT_TEXT.displayName;
42
+ export function renderPackageOutcome(outcome, profileRoot, style = chalk) {
43
+ // Model refresh remains an A1 top-level command. The `a1 pi` compatibility
44
+ // transcript applies to package operations only.
45
+ if (outcome.operation === "refresh-models") {
46
+ if (outcome.status === "failed") {
47
+ return `${PRODUCT_TEXT.diagnostic(`could not ${describeOperation(outcome.operation)}: ${outcome.detail ?? "unknown failure"}`)}\n`;
48
+ }
49
+ return `${PRODUCT_TEXT.displayName} refreshed the model catalogs in ${profileRoot}.\n`;
50
+ }
42
51
  if (outcome.status === "failed") {
43
- return `${PRODUCT_TEXT.diagnostic(`could not ${describeOperation(outcome.operation)}: ${outcome.detail ?? "unknown failure"}`)}\n`;
52
+ return `${style.red(`Error: ${outcome.detail ?? "Unknown package command error"}`)}\n`;
44
53
  }
45
54
  if (outcome.status === "not-found") {
46
- return `${PRODUCT_TEXT.diagnostic(`found no package matching ${outcome.source ?? "that source"} in ${profileRoot}.`)}\n`;
55
+ return `${style.red(`No matching package found for ${outcome.source ?? "that source"}`)}\n`;
47
56
  }
48
57
  switch (outcome.operation) {
49
58
  case "install":
50
- return `${name} installed ${outcome.source} into ${profileRoot}.\n`
51
- + `Restart ${PRODUCT_TEXT.commandName} for a running session to load it.\n`;
59
+ return `${style.green(`Installed ${outcome.source}`)}\n`;
52
60
  case "remove":
53
- return `${name} removed ${outcome.source} from ${profileRoot}.\n`;
61
+ return `${style.green(`Removed ${outcome.source}`)}\n`;
54
62
  case "update":
55
- return outcome.source === null
56
- ? `${name} updated the packages in ${profileRoot}.\n`
57
- : `${name} updated ${outcome.source}.\n`;
58
- case "refresh-models":
59
- return `${name} refreshed the model catalogs in ${profileRoot}.\n`;
63
+ return `${style.green(outcome.source === null ? "Updated packages" : `Updated ${outcome.source}`)}\n`;
60
64
  case "list":
61
- return renderPackageList(outcome, profileRoot);
65
+ return renderPackageList(outcome, style);
62
66
  }
63
67
  }
64
- function renderPackageList(outcome, profileRoot) {
68
+ function renderPackageList(outcome, style) {
65
69
  if (outcome.packages.length === 0)
66
- return `${PRODUCT_TEXT.displayName} has no packages installed in ${profileRoot}.\n`;
67
- const lines = [`Packages installed for ${PRODUCT_TEXT.commandName} in ${profileRoot}:`];
70
+ return `${style.dim("No packages installed.")}\n`;
71
+ const lines = [style.bold("User packages:")];
68
72
  for (const entry of outcome.packages) {
69
- lines.push(` ${entry.source}${entry.filtered ? " (partly enabled)" : ""}`);
73
+ lines.push(` ${entry.source}${entry.filtered ? " (filtered)" : ""}`);
70
74
  if (entry.installedPath !== null)
71
- lines.push(` ${entry.installedPath}`);
75
+ lines.push(style.dim(` ${entry.installedPath}`));
72
76
  }
73
77
  return `${lines.join("\n")}\n`;
74
78
  }
@@ -21,7 +21,7 @@ export async function runVersionStats(options) {
21
21
  return 1;
22
22
  }
23
23
  const remote = await queryDistTags(runner);
24
- output.stdout(`Installed: ${installed}\nRelease: ${remote.release ?? "unavailable"}\nNext: ${remote.next ?? "unavailable"}\n`);
24
+ output.stdout(`Installed: ${installed}\nRelease: ${remote.release ?? "unavailable"}\nDevelop: ${remote.develop ?? "unavailable"}\n`);
25
25
  if (remote.error)
26
26
  output.stderr(`${PRODUCT_TEXT.diagnostic(`could not resolve npm dist-tags: ${remote.error}`)}\n`);
27
27
  return 0;
@@ -42,15 +42,15 @@ async function queryDistTags(runner) {
42
42
  throw new TypeError("npm returned a non-object dist-tags value");
43
43
  const tags = metadata;
44
44
  const release = parseVersion(tags.latest, "npm latest");
45
- const next = tags.next === undefined ? null : parseVersion(tags.next, "npm next");
46
- return { release, next, error: null };
45
+ const develop = tags.next === undefined ? null : parseVersion(tags.next, "npm development channel");
46
+ return { release, develop, error: null };
47
47
  }
48
48
  catch (error) {
49
49
  return unavailable(message(error));
50
50
  }
51
51
  }
52
52
  function unavailable(error) {
53
- return { release: null, next: null, error };
53
+ return { release: null, develop: null, error };
54
54
  }
55
55
  function createVersionProcessRunner() {
56
56
  return async (command, arguments_) => await new Promise((resolvePromise, rejectPromise) => {
@@ -1476,14 +1476,16 @@ export class PiEngineAdapter {
1476
1476
  this.#upsertMessageBlock(event.message, "live");
1477
1477
  return;
1478
1478
  case "message_update": {
1479
- const block = this.#upsertMessageBlock(event.message, "live");
1480
- if (block && isRecord(event.assistantMessageEvent) && typeof event.assistantMessageEvent.delta === "string") {
1481
- this.#upsertTranscriptBlock({
1482
- ...block,
1483
- text: block.text.endsWith(event.assistantMessageEvent.delta)
1484
- ? block.text
1485
- : `${block.text}${event.assistantMessageEvent.delta}`,
1486
- });
1479
+ const delta = isRecord(event.assistantMessageEvent) && typeof event.assistantMessageEvent.delta === "string"
1480
+ ? event.assistantMessageEvent.delta
1481
+ : undefined;
1482
+ // The delta is folded in before the block is stored, so a chunk is one update to
1483
+ // one block rather than a store without the delta followed by a store with it.
1484
+ const blocks = this.#messageBlocks(event.message, "live", this.#transcript.length);
1485
+ for (const [index, block] of blocks.entries()) {
1486
+ this.#upsertTranscriptBlock(index === 0 && delta !== undefined && !block.text.endsWith(delta)
1487
+ ? { ...block, text: `${block.text}${delta}` }
1488
+ : block);
1487
1489
  }
1488
1490
  return;
1489
1491
  }
@@ -1638,7 +1640,13 @@ export class PiEngineAdapter {
1638
1640
  }
1639
1641
  }
1640
1642
  }
1641
- this.#setTranscript(blocks);
1643
+ // An authoritative rebuild restates most of what is already there. Reusing the block
1644
+ // that already says it keeps its revision, and with it the rows the shell rendered for
1645
+ // it — otherwise every turn that ends re-renders the whole session.
1646
+ this.#setTranscript(blocks.map(block => {
1647
+ const existing = this.#transcriptBlock(block.id);
1648
+ return existing !== undefined && sameBlockContent(existing, block) ? existing : block;
1649
+ }));
1642
1650
  }
1643
1651
  #upsertMessageBlock(message, status) {
1644
1652
  const blocks = this.#messageBlocks(message, status, this.#transcript.length);
@@ -1815,8 +1823,14 @@ export class PiEngineAdapter {
1815
1823
  }
1816
1824
  #upsertTranscriptBlock(block) {
1817
1825
  const index = this.#transcriptIndex.get(block.id);
1818
- if (index !== undefined)
1826
+ if (index !== undefined) {
1827
+ const existing = this.#transcript[index];
1828
+ // Nothing to tell the shell about a block that repeats itself, and keeping the
1829
+ // revision keeps the rows it already rendered.
1830
+ if (existing !== undefined && sameBlockContent(existing, block))
1831
+ return;
1819
1832
  this.#transcript[index] = block;
1833
+ }
1820
1834
  else {
1821
1835
  this.#transcriptIndex.set(block.id, this.#transcript.length);
1822
1836
  this.#transcript.push(block);
@@ -2388,6 +2402,36 @@ function compactResourceLabel(path) {
2388
2402
  const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
2389
2403
  return segments.at(-1) ?? path;
2390
2404
  }
2405
+ /**
2406
+ * Whether two blocks say the same thing. A block that says what it already said is not a
2407
+ * new revision: the shell renders a block once per revision, so bumping one it did not
2408
+ * need re-renders it for nothing.
2409
+ */
2410
+ function sameBlockContent(left, right) {
2411
+ return left.kind === right.kind
2412
+ && left.status === right.status
2413
+ && left.title === right.title
2414
+ && left.text === right.text
2415
+ && sameValue(left.payload, right.payload);
2416
+ }
2417
+ function sameValue(left, right) {
2418
+ if (left === right)
2419
+ return true;
2420
+ if (typeof left !== typeof right || left === null || right === null)
2421
+ return false;
2422
+ if (Array.isArray(left) || Array.isArray(right)) {
2423
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
2424
+ return false;
2425
+ return left.every((value, index) => sameValue(value, right[index]));
2426
+ }
2427
+ if (typeof left !== "object")
2428
+ return false;
2429
+ const leftKeys = Object.keys(left);
2430
+ const rightRecord = right;
2431
+ if (leftKeys.length !== Object.keys(rightRecord).length)
2432
+ return false;
2433
+ return leftKeys.every(key => Object.hasOwn(rightRecord, key) && sameValue(left[key], rightRecord[key]));
2434
+ }
2391
2435
  function textFromContent(content) {
2392
2436
  if (typeof content === "string")
2393
2437
  return content;
@@ -39,6 +39,12 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
39
39
  readonly onDequeue?: () => void;
40
40
  }, startup?: PiShellHeaderOptions, agentDir?: string, extensionRenderers?: PiShellExtensionRendererResolver);
41
41
  update(view: OwnedUiSessionViewModel): void;
42
+ /**
43
+ * Applies one block: its component is created or updated in place and the order grows
44
+ * only when the block is new. A streamed chunk costs one component update rather than a
45
+ * walk of the whole transcript.
46
+ */
47
+ applyTranscriptBlock(block: OwnedUiSessionViewModel["transcript"][number]): void;
42
48
  render(width: number): readonly string[];
43
49
  layoutRoot(): PiTuiLayoutNode;
44
50
  transcriptComponent(id: string): PiShellTranscriptComponentPort | undefined;
@@ -71,6 +71,25 @@ export class OwnedUiSessionShellRoot {
71
71
  this.editor.setThinkingLevel(view.thinkingLevel);
72
72
  this.invalidate();
73
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
+ }
74
93
  render(width) {
75
94
  const queued = this.#view.editor.queuedSubmissions.length === 0 ? [] : this.#queued.render(width);
76
95
  return [
@@ -794,9 +813,12 @@ export class OwnedUiSessionShell {
794
813
  });
795
814
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
796
815
  this.#unsubscribe = this.backend.onEvent(event => {
797
- // One view read per event: the model is built by the backend, and building it
798
- // twice per streamed chunk is what made a long session cost more per chunk.
799
- const view = this.#syncView();
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();
800
822
  if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
801
823
  void this.#flushCompactionQueue();
802
824
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
@@ -1415,6 +1437,18 @@ export class OwnedUiSessionShell {
1415
1437
  this.#extensionBridge.dispose();
1416
1438
  await this.runtime.dispose();
1417
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
+ }
1418
1452
  #syncView() {
1419
1453
  const view = this.view();
1420
1454
  if (this.backend.sessionGeneration !== this.#sessionGeneration) {
@@ -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 };
@@ -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
 
@@ -5,107 +5,104 @@ GitHub Actions is the only automation platform. Three refs are protected:
5
5
  | Ref | Protection | Produced by |
6
6
  | --- | --- | --- |
7
7
  | `develop` | `Development validation required` on every pull request | `.github/workflows/ci.yml` |
8
- | `master` | cannot be deleted or force-updated | ruleset only |
9
- | `refs/tags/v*` | cannot be deleted or moved | ruleset only |
8
+ | `master` | cannot be deleted or force-updated | stable publication |
9
+ | `refs/tags/v*` | cannot be deleted or moved | stable publication |
10
10
 
11
- Publishing is one workflow, `.github/workflows/release.yml`, triggered by pushes
12
- rather than dispatched by hand.
11
+ `.github/workflows/release.yml` is the only publisher. A push publishes neither
12
+ channel. The workflow starts at `03:17 UTC` for nightly development verification,
13
+ or by explicit dispatch from `npm run develop` or `npm run release`.
13
14
 
14
- `develop` is where work lands. `master` records what the npm `latest` tag serves:
15
- the release fast-forwards it to the commit it published, and nothing else ever
16
- writes it. There is no promotion to arrange and nothing to keep in sync — `master`
17
- is an effect of publishing, not a step before it.
15
+ `develop` is where work lands. `master` records what npm `latest` serves and is an
16
+ effect of stable publication, not a trigger.
18
17
 
19
- ## How much validation runs when
18
+ ## Validation by trigger
20
19
 
21
- | Change | Validation |
20
+ | Trigger | Validation and outcome |
22
21
  | --- | --- |
23
- | Docs or specs only | OpenSpec strict lint, nothing else |
24
- | Any code PR into `develop` | Fast tier: typecheck, architecture checks, unit/contract tests |
25
- | Preview publish (`next`) | Fast tier + package gates on Windows, Linux, and macOS |
26
- | Stable publish (`latest`) | Complete suite on Windows, Linux, and macOS |
22
+ | Pull request into `develop` | Fast required validation; docs/spec-only changes use strict OpenSpec validation |
23
+ | `npm run develop` | Preview package gates on Windows, Linux, and macOS; an existing numbered preview is an early successful no-op |
24
+ | Nightly at `03:17 UTC` | Complete non-physical suite on Windows, Linux, and macOS, every night |
25
+ | `npm run release -- ...` | Complete exact-byte stable gates, then npm `latest`, tag, GitHub Release, and `master` |
26
+ | `.github/workflows/full-regression.yml` | Additional on-demand complete regression without publication authority |
27
27
 
28
- Need more coverage for something risky? Run the **Full regression** workflow on
29
- demand.
28
+ ## Numbered development previews
30
29
 
31
- ## Previews publish themselves
30
+ A merge or push to `develop` does not publish by itself. To request a deliberate
31
+ preview from any authorized checkout:
32
32
 
33
- Every push to `develop` publishes a preview to the npm `next` tag. Nothing to
34
- dispatch and nothing to approve.
33
+ ```sh
34
+ npm run develop
35
+ ```
36
+
37
+ The command fetches authoritative `origin/develop`, resolves the unique merged pull
38
+ request associated with that exact commit through GitHub, and derives
39
+ `<major.minor.patch>-dev.<pull-request number>`. Thus GitHub's `develop (#107)`
40
+ source produces `0.1.8-dev.107`. It first checks npm; if the immutable version
41
+ already exists it reports that version without dispatching package work. Otherwise
42
+ it dispatches GitHub Actions, waits, and reports the published version. It never
43
+ builds or uploads npm bytes from the workstation.
44
+
45
+ Nightly resolves the same current `origin/develop` source. It always runs complete
46
+ verification even if source has not changed. For a new number it packs once and
47
+ runs the suite against that final-version tarball before publication. For an
48
+ existing number it downloads the exact npm tarball and runs package/update gates
49
+ against those registry bytes; publication is then a successful no-op.
50
+
51
+ Manual and nightly runs share one non-cancelling concurrency group. Their final
52
+ registry check is serialized, so overlapping requests can produce only one publish
53
+ and one successful existing-version no-op. A development publication moves npm's
54
+ internal `next` dist-tag and never moves `latest`.
55
+
56
+ Users install previews with public `develop` terminology:
35
57
 
36
- The version is stamped at publish time — `<major.minor.patch>-dev.<short commit>`,
37
- the base taken from whatever `package.json` declares and the suffix from the commit
38
- being published and is never written back to the repository. An installed preview
39
- therefore names the exact source it came from, and rebuilding a commit produces the
40
- same version rather than a new one. That suffix is also how a specific preview is
41
- installed: `a1 update:<commit>` resolves it against the published list and
42
- refuses a commit that was never published. `develop` therefore carries one open prerelease version between
43
- releases, and no commit is ever spent on a preview.
58
+ ```sh
59
+ a1 update:develop # current development channel
60
+ a1 update:107 # numbered preview
61
+ a1 update:0.1.8-dev.107 # exact full preview version
62
+ ```
44
63
 
45
- One consequence worth knowing: a push that would republish an existing version
46
- fails early, before anything is packed.
64
+ `a1 update:next` is removed and redirects to `a1 update:develop` without registry
65
+ or runtime work.
47
66
 
48
67
  ## Cutting a stable release
49
68
 
50
- One command, from a clean `develop` that matches its remote:
69
+ From a clean `develop` matching its remote:
51
70
 
52
71
  ```sh
53
72
  npm run release -- patch # or minor, major, or an exact x.y.z
54
73
  ```
55
74
 
56
- It lands `x.y.z` on `develop` through a pull request that merges itself, waits for
57
- that publication to succeed, and then lands `x.y.(z+1)-dev` so previews resume
58
- immediately. It publishes nothing itself and creates no tag.
59
-
60
- Landing the stable version is what publishes. The same pipeline sees a commit
61
- declaring a stable version and runs its stable form: build the process guardian on
62
- all three platforms, pack once, run the complete suite against those exact bytes on
63
- all three platforms, publish to npm `latest` with provenance from the
64
- `npm-publish` environment, and only then write the tag `vx.y.z`, record the GitHub
65
- Release, and fast-forward `master`.
75
+ The command lands `x.y.z` through its version pull request, explicitly dispatches
76
+ stable publication for that exact current `origin/develop` commit, and waits. Only
77
+ after success does it land `x.y.(z+1)-dev`.
66
78
 
67
- That order is the point. A release that fails leaves no tag, no release, and no
68
- moved branch only a red run. Nothing ever advertises a version that does not
69
- exist on the registry.
79
+ Stable publication builds the process guardian on all supported platforms, packs
80
+ once, runs the complete suite against those exact bytes on Windows, Linux, and
81
+ macOS, publishes to npm `latest` with provenance from the `npm-publish`
82
+ environment, and then writes `vx.y.z`, records the GitHub Release, and fast-forwards
83
+ `master`. A push of the stable version does not publish it.
70
84
 
71
85
  Rules that do not bend:
72
86
 
73
- - **Never upload locally rebuilt bytes.** The publisher uploads the artifact the
74
- validation ran against, and re-checks its digest before and after.
75
- - **Never route around validation by rebuilding inside a publisher.** The publish
76
- job has no checkout of dependencies, no build, and no pack step.
77
- - **Never move a release tag.** A wrong tag is superseded by the next version, not
78
- repointed. The tag is written by the publication itself, so it exists only for
79
- versions that shipped.
87
+ - **Never upload locally rebuilt bytes.** The publisher uploads the artifact the validation ran against and checks its digest before and after.
88
+ - **Never route around validation by rebuilding inside a publisher.** The publish job receives the packed artifact and does not install dependencies, build, or pack.
89
+ - **Never move a release tag.** A wrong tag is superseded by the next version, not repointed.
80
90
 
81
91
  ## When something fails
82
92
 
83
- - **PR validation fails:** fix the code and push. Do not mark a failed tier optional.
84
- - **Preview publish fails:** fix and push again; the next push publishes the next
85
- run number. Nothing needs cleaning up.
86
- - **Stable publish fails:** nothing was recorded no tag, no release, no moved
87
- branch. Fix the cause and release the next version. The failed version number is
88
- spent, because `develop` has already moved past it.
89
- - **Stable publish is uncertain after npm accepted the bytes:** stop. Check the
90
- registry for the version, tag, and digest. Repair a dist-tag only as a separate
91
- reviewed operation never republish.
92
-
93
- ## Why the ruleset looks the way it does
94
-
95
- One person maintains this repository, and GitHub does not let a PR author approve
96
- their own PR. Requiring even one approval would deadlock the authorized
97
- solo-maintainer path no PR could ever merge. So the `develop` ruleset requires a
98
- pull request, a green required check, and resolved review threads, but sets required
99
- approving reviews to zero. It does not require the branch to be up to date: once a
100
- PR is green it merges even if unrelated work landed first, with no re-validation
101
- loop.
102
-
103
- The `master` and tag rulesets carry no checks at all. Both only ever receive a commit
104
- that has already been validated and published, so what matters about them is that
105
- neither can be rewritten. Requiring a pull request on `master` would stop the release
106
- from recording itself there.
107
-
108
- Do not add a direct-push bypass as a shortcut, and never weaken the force-push or
109
- deletion protection. Ruleset mutation is a separate administrative operation: run
110
- `node scripts/check-github-rulesets.mjs` to see the diff, and apply only when a
111
- maintainer explicitly confirms with `--apply --confirm apply-a1-ci-rulesets`.
93
+ - **PR validation fails:** fix the code and push; do not mark a failed tier optional.
94
+ - **Development publication fails:** fix the cause and rerun `npm run develop`; an npm version that already exists is never overwritten.
95
+ - **Stable publication fails before npm accepts bytes:** no tag, release, or moved branch exists. Fix the cause and release the next version.
96
+ - **Stable publication is uncertain after npm accepted bytes:** stop and inspect registry version, digest, tag, and release. Never republish immutable bytes.
97
+
98
+ ## Branch protection rationale
99
+
100
+ One person maintains this repository, and a PR author cannot approve their own PR.
101
+ The `develop` ruleset therefore requires a pull request, a green required check,
102
+ and resolved review threads, but zero approving reviews. `master` and release tags
103
+ are written only after publication and are protected from force-push, movement, and
104
+ deletion.
105
+
106
+ Do not add direct-push bypasses. Ruleset mutation remains a separate administrative
107
+ operation: inspect with `node scripts/check-github-rulesets.mjs`, and apply only
108
+ with explicit maintainer confirmation.
@@ -8,7 +8,7 @@
8
8
  | `a1 pi` | Untouched vanilla Pi fallback and comparison oracle | ordinary `~/.pi/agent` |
9
9
  | `a1 sandbox` | Unchanged isolated vanilla Pi profile for experiments | `~/.a1/sandbox` |
10
10
 
11
- `a1 pi` and `a1 sandbox` are development instruments: one compares A1 against pinned Pi, the other tries resources against an isolated profile. Prerelease builds — what `a1 update:next` installs — expose them. A release build does not, and does not recognize the words: what it exposes is bare `a1` plus the maintenance and package commands. Working in this repository is unaffected, because `npm start:pi` and `npm run start:sandbox` prepare the profile and launch directly rather than through the command line.
11
+ `a1 pi` and `a1 sandbox` are development instruments: one compares A1 against pinned Pi, the other tries resources against an isolated profile. Prerelease builds — what `a1 update:develop` or `a1 update:<number>` installs — expose them. A release build does not, and does not recognize the words: what it exposes is bare `a1` plus the maintenance and package commands. Working in this repository is unaffected, because `npm start:pi` and `npm run start:sandbox` prepare the profile and launch directly rather than through the command line.
12
12
 
13
13
  There is no `a1 agent` command. The former `a1 ui` subcommand is removed. Bare `a1` is the owned agent product surface and remains the entry point when multi-agent UX is introduced.
14
14
 
@@ -50,4 +50,4 @@ Report failures with direct-versus-A1 behavior, platform/terminal versions, exac
50
50
 
51
51
  For recovery, use `a1 pi`; do not use the removed `a1 ui` command. `a1 sandbox` remains profile isolation rather than a security boundary.
52
52
 
53
- Manual acceptance can authorize an exact uncertified npm `next` preview after non-desktop gates pass. It does not certify stable terminal parity or platform support.
53
+ Manual acceptance can authorize an exact uncertified development preview after non-desktop gates pass. It does not certify stable terminal parity or platform support.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.0b6d8cc",
3
+ "version": "0.1.8-dev.112",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",
@@ -33,6 +33,7 @@
33
33
  "check:deprecated": "node scripts/check-deprecated-dependencies.mjs",
34
34
  "branches:prune": "node scripts/prune-merged-branches.mjs",
35
35
  "check": "npm run test:fast",
36
+ "develop": "node scripts/develop.mjs",
36
37
  "release": "node scripts/release.mjs",
37
38
  "test": "npm run test:fast",
38
39
  "test:fast": "node scripts/run-validation-tier.mjs typecheck fast",
@@ -57,6 +58,7 @@
57
58
  "dependencies": {
58
59
  "@earendil-works/pi-coding-agent": "0.84.2",
59
60
  "@earendil-works/pi-tui": "0.84.2",
61
+ "chalk": "5.6.2",
60
62
  "cross-spawn": "7.0.6",
61
63
  "grok-mermaid": "0.2.2",
62
64
  "semver": "7.8.5"