@yaag/extension 0.5.2 → 0.6.1

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 (59) hide show
  1. package/package.json +5 -5
  2. package/src/index.ts +26 -20
  3. package/src/process/index.ts +16 -0
  4. package/src/record/index.ts +24 -0
  5. package/src/{run-program-param.ts → record/run-program-param.ts} +1 -1
  6. package/src/{run-record.ts → record/run-record.ts} +10 -1
  7. package/src/{run-registry.ts → record/run-registry.ts} +71 -13
  8. package/src/{run-restore.ts → record/run-restore.ts} +1 -1
  9. package/src/{run-settlement.ts → record/run-settlement.ts} +1 -1
  10. package/src/{fake-run-start.ts → testing/fake-run-start.ts} +1 -1
  11. package/src/testing/index.ts +19 -0
  12. package/src/{run-call-render.ts → testing/run-call-render.ts} +3 -4
  13. package/src/{run-tool-test-support.ts → testing/run-tool-test-support.ts} +11 -10
  14. package/src/{describe-tool.ts → tool/describe-tool.ts} +1 -2
  15. package/src/tool/index.ts +9 -0
  16. package/src/{run-tool.ts → tool/run-tool.ts} +32 -17
  17. package/src/{status-tool.ts → tool/status-tool.ts} +53 -67
  18. package/src/{stop-tool.ts → tool/stop-tool.ts} +5 -5
  19. package/src/{background-status.ts → view/background-status.ts} +1 -1
  20. package/src/view/index.ts +25 -0
  21. package/src/view/restored-run-text.ts +77 -0
  22. package/src/{run-call-body.ts → view/run-call-body.ts} +1 -1
  23. package/src/{run-foreground.ts → view/run-foreground.ts} +2 -2
  24. package/src/view/run-listing.ts +33 -0
  25. package/src/{run-picker.ts → view/run-picker.ts} +11 -4
  26. package/src/{yaag-command.ts → view/yaag-command.ts} +20 -7
  27. package/src/workspace/index.ts +8 -0
  28. package/src/{setup-workspace.ts → workspace/setup-workspace.ts} +6 -2
  29. /package/src/{cli-child.ts → process/cli-child.ts} +0 -0
  30. /package/src/{eval-pipe.ts → process/eval-pipe.ts} +0 -0
  31. /package/src/{event-reader.ts → process/event-reader.ts} +0 -0
  32. /package/src/{process-liveness.ts → process/process-liveness.ts} +0 -0
  33. /package/src/{resolve-bun.ts → process/resolve-bun.ts} +0 -0
  34. /package/src/{resolve-cli.ts → process/resolve-cli.ts} +0 -0
  35. /package/src/{run-argv.ts → process/run-argv.ts} +0 -0
  36. /package/src/{spawn-run.ts → process/spawn-run.ts} +0 -0
  37. /package/src/{status.ts → process/status.ts} +0 -0
  38. /package/src/{stderr-buffer.ts → process/stderr-buffer.ts} +0 -0
  39. /package/src/{resume-source.ts → record/resume-source.ts} +0 -0
  40. /package/src/{run-id.ts → record/run-id.ts} +0 -0
  41. /package/src/{run-store.ts → record/run-store.ts} +0 -0
  42. /package/src/{run-summary-parse.ts → record/run-summary-parse.ts} +0 -0
  43. /package/src/{runs-dir.ts → record/runs-dir.ts} +0 -0
  44. /package/src/{fake-extension-ui.ts → testing/fake-extension-ui.ts} +0 -0
  45. /package/src/{fake-theme.ts → testing/fake-theme.ts} +0 -0
  46. /package/src/{fake-tui.ts → testing/fake-tui.ts} +0 -0
  47. /package/src/{pi-keybindings.ts → testing/pi-keybindings.ts} +0 -0
  48. /package/src/{test-extension-context.ts → testing/test-extension-context.ts} +0 -0
  49. /package/src/{test-tui-context.ts → testing/test-tui-context.ts} +0 -0
  50. /package/src/{system-prompt-append.ts → tool/system-prompt-append.ts} +0 -0
  51. /package/src/{run-complete-renderer.ts → view/run-complete-renderer.ts} +0 -0
  52. /package/src/{run-details.ts → view/run-details.ts} +0 -0
  53. /package/src/{run-tree-component.ts → view/run-tree-component.ts} +0 -0
  54. /package/src/{run-tree-host.ts → view/run-tree-host.ts} +0 -0
  55. /package/src/{run-trees.ts → view/run-trees.ts} +0 -0
  56. /package/src/{usage.ts → view/usage.ts} +0 -0
  57. /package/src/{program-directories.ts → workspace/program-directories.ts} +0 -0
  58. /package/src/{setup-workspace-command.ts → workspace/setup-workspace-command.ts} +0 -0
  59. /package/src/{setup-workspace-tool.ts → workspace/setup-workspace-tool.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yaag/extension",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,14 +19,14 @@
19
19
  },
20
20
  "scripts": {
21
21
  "typecheck": "tsc --noEmit",
22
- "test": "for file in src/*.test.ts; do bun test --timeout 30000 \"$file\" || exit 1; done",
22
+ "test": "for file in src/*/*.test.ts; do bun test --timeout 30000 \"$file\" || exit 1; done",
23
23
  "test:e2e": "bun test --timeout 30000 e2e"
24
24
  },
25
25
  "dependencies": {
26
26
  "@earendil-works/pi-tui": "^0.84.0",
27
- "@yaag/cli": "0.5.2",
28
- "@yaag/runtime": "0.5.2",
29
- "@yaag/tui": "0.5.2",
27
+ "@yaag/cli": "0.6.1",
28
+ "@yaag/runtime": "0.6.1",
29
+ "@yaag/tui": "0.6.1",
30
30
  "nanoid": "^6.0.1"
31
31
  },
32
32
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -1,22 +1,25 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { createBackgroundStatus } from "./background-status.ts";
3
- import { createDescribeTool } from "./describe-tool.ts";
4
- import { findProgramDirectories } from "./program-directories.ts";
5
- import { resolveBun } from "./resolve-bun.ts";
6
- import { resolveCliEntry } from "./resolve-cli.ts";
7
- import { createRunCompleteRenderer } from "./run-complete-renderer.ts";
8
- import { RunRegistry } from "./run-registry.ts";
9
- import { RunStore } from "./run-store.ts";
10
- import { createRunTool } from "./run-tool.ts";
11
- import { RunTreeStore } from "./run-trees.ts";
12
- import { createSetupWorkspaceExecutor } from "./setup-workspace.ts";
13
- import { createSetupWorkspaceCommand } from "./setup-workspace-command.ts";
14
- import { createSetupWorkspaceTool } from "./setup-workspace-tool.ts";
15
- import { statusReport } from "./status.ts";
16
- import { createStatusTool } from "./status-tool.ts";
17
- import { createStopTool } from "./stop-tool.ts";
18
- import { createSystemPromptAppender } from "./system-prompt-append.ts";
19
- import { createYaagCommand } from "./yaag-command.ts";
2
+ import { resolveBun, resolveCliEntry, statusReport } from "./process/index.ts";
3
+ import { RunRegistry, RunStore } from "./record/index.ts";
4
+ import {
5
+ createDescribeTool,
6
+ createRunTool,
7
+ createStatusTool,
8
+ createStopTool,
9
+ createSystemPromptAppender,
10
+ } from "./tool/index.ts";
11
+ import {
12
+ createBackgroundStatus,
13
+ createRunCompleteRenderer,
14
+ createYaagCommand,
15
+ RunTreeStore,
16
+ } from "./view/index.ts";
17
+ import {
18
+ createSetupWorkspaceCommand,
19
+ createSetupWorkspaceExecutor,
20
+ createSetupWorkspaceTool,
21
+ findProgramDirectories,
22
+ } from "./workspace/index.ts";
20
23
 
21
24
  /**
22
25
  * The yaag pi extension. Loaded by pi through jiti, in Node — nothing here may
@@ -35,7 +38,6 @@ export default async function (pi: ExtensionAPI): Promise<void> {
35
38
  // One registry per session, mirrored to a durable store so a restart of this
36
39
  // Host Session keeps every Run id addressable (ticket 02).
37
40
  const registry = new RunRegistry({ store: new RunStore() });
38
- await registry.restore();
39
41
  // The renderer projections and the background footer segment both live for
40
42
  // the session, beside the registry, so a background Run keeps a tree.
41
43
  const store = new RunTreeStore();
@@ -56,9 +58,13 @@ export default async function (pi: ExtensionAPI): Promise<void> {
56
58
  const appendYaagPrompt = createSystemPromptAppender(() => findProgramDirectories(process.cwd()));
57
59
  pi.on("before_agent_start", (event) => appendYaagPrompt(event));
58
60
 
59
- pi.on("session_start", (_event, ctx) => {
61
+ pi.on("session_start", async (_event, ctx) => {
60
62
  if (bun === null) ctx.ui.notify(report, "error");
61
63
  if (ctx.hasUI) status.bind(ctx.ui);
64
+ // The pi session id is knowable only here, and `/resume` rebinds this
65
+ // extension and fires the same id, which is what re-adopts this session's
66
+ // Runs. A fork arrives with a new id and adopts nothing (ADR-0036).
67
+ await registry.restore(ctx.sessionManager.getSessionId());
62
68
  });
63
69
 
64
70
  // Commands and tools have separate namespaces: this CLI-resolution command
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Public surface of the `process/` module: child-process plumbing and the Bun-CLI bridge.
3
+ * Files inside this directory import each other directly.
4
+ */
5
+ export {
6
+ type CliChild,
7
+ type CliChildOptions,
8
+ type CliChildOutcome,
9
+ startCliChild,
10
+ } from "./cli-child.ts";
11
+ export { isProcessAlive, type ProcessIdentity, readProcessStart } from "./process-liveness.ts";
12
+ export { resolveBun } from "./resolve-bun.ts";
13
+ export { resolveCliEntry } from "./resolve-cli.ts";
14
+ export type { ProgramTarget } from "./run-argv.ts";
15
+ export { type RunHandle, type RunOutcome, type StartRunOptions, startRun } from "./spawn-run.ts";
16
+ export { statusReport } from "./status.ts";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Public surface of the `record/` module: the Run record registry, store, restore, and resume.
3
+ * Files inside this directory import each other directly.
4
+ */
5
+ export { resolveProgramParams } from "./resume-source.ts";
6
+ export { mintRunId } from "./run-id.ts";
7
+ export {
8
+ confirmProgramTarget,
9
+ type ProgramParams,
10
+ programExpansion,
11
+ programLabel,
12
+ programTarget,
13
+ } from "./run-program-param.ts";
14
+ export type { RunLaunch, RunRecord } from "./run-record.ts";
15
+ export {
16
+ type DatedRun,
17
+ type LiveRun,
18
+ type RegisteredRun,
19
+ type RestoredRun,
20
+ RunRegistry,
21
+ type RunSettlement,
22
+ } from "./run-registry.ts";
23
+ export { errorText, failure, observedSettlement, toError, viewResult } from "./run-settlement.ts";
24
+ export { RunStore } from "./run-store.ts";
@@ -1,7 +1,7 @@
1
1
  import { access } from "node:fs/promises";
2
2
  import { resolve } from "node:path";
3
3
  import { clampToWidth, sanitizeTerminalLine, sanitizeTerminalText } from "@yaag/tui";
4
- import type { ProgramTarget } from "./run-argv.ts";
4
+ import type { ProgramTarget } from "../process/index.ts";
5
5
 
6
6
  /**
7
7
  * The `yaag_run` parameters that name the program; exactly one is given.
@@ -1,5 +1,5 @@
1
1
  import { initialSummary, type RunSummary } from "@yaag/runtime";
2
- import type { ProcessIdentity } from "./process-liveness.ts";
2
+ import type { ProcessIdentity } from "../process/index.ts";
3
3
  import { parseRunSummary } from "./run-summary-parse.ts";
4
4
 
5
5
  /**
@@ -40,6 +40,12 @@ export type RunLaunch =
40
40
  /** One Run as stored on disk, at `<runs dir>/<id>.json`. */
41
41
  export interface RunRecord {
42
42
  readonly id: string;
43
+ /**
44
+ * The pi session that started the Run. `null` for a record an older yaag
45
+ * wrote, or an unbound registry; such a record belongs to no session
46
+ * (ADR-0036).
47
+ */
48
+ readonly sessionId: string | null;
43
49
  readonly launch: RunLaunch;
44
50
  readonly process: ProcessIdentity | null;
45
51
  readonly startedAt: string;
@@ -81,6 +87,7 @@ export function parseRunRecord(value: unknown): RunRecord | null {
81
87
  // field a consumer reads has the type this parser proved (rule 0001 §3).
82
88
  return {
83
89
  id: candidate["id"],
90
+ sessionId: typeof candidate["sessionId"] === "string" ? candidate["sessionId"] : null,
84
91
  launch,
85
92
  process: identity,
86
93
  startedAt: candidate["startedAt"],
@@ -97,9 +104,11 @@ export function startedRecord(options: {
97
104
  readonly launch: RunLaunch;
98
105
  readonly process: ProcessIdentity | null;
99
106
  readonly now: string;
107
+ readonly sessionId?: string | null;
100
108
  }): RunRecord {
101
109
  return {
102
110
  id: options.id,
111
+ sessionId: options.sessionId ?? null,
103
112
  launch: options.launch,
104
113
  process: options.process,
105
114
  startedAt: options.now,
@@ -1,10 +1,9 @@
1
1
  import type { EndedRunSummary, RunSummary } from "@yaag/runtime";
2
- import type { ProcessIdentity } from "./process-liveness.ts";
2
+ import type { ProcessIdentity, RunOutcome } from "../process/index.ts";
3
3
  import { pickInlineResumeSource } from "./resume-source.ts";
4
4
  import { type RunLaunch, type RunRecord, startedRecord } from "./run-record.ts";
5
5
  import { restoreRecords } from "./run-restore.ts";
6
6
  import type { RunStore } from "./run-store.ts";
7
- import type { RunOutcome } from "./spawn-run.ts";
8
7
 
9
8
  export type { RunLaunch } from "./run-record.ts";
10
9
 
@@ -44,6 +43,12 @@ export type RegisteredRun =
44
43
  /** What the registry knows about an id. */
45
44
  export type RunStatus = RegisteredRun | { readonly state: "unknown" };
46
45
 
46
+ /** A registered Run plus the start instant that orders a bounded listing. */
47
+ export interface DatedRun {
48
+ readonly startedAt: string;
49
+ readonly entry: RegisteredRun;
50
+ }
51
+
47
52
  /** How often a Run's latest Summary is persisted while it runs. */
48
53
  export const SUMMARY_WRITE_INTERVAL_MS = 2_000;
49
54
 
@@ -67,8 +72,11 @@ export class RunRegistry {
67
72
  readonly #records = new Map<string, RunRecord>();
68
73
  readonly #lastWrite = new Map<string, number>();
69
74
  readonly #trailing = new Map<string, ReturnType<typeof setTimeout>>();
75
+ readonly #startedAt = new Map<string, string>();
70
76
  readonly #store: RunStore | undefined;
71
77
  readonly #now: () => Date;
78
+ #sessionId: string | null = null;
79
+ #restoredFor: string | null | undefined = undefined;
72
80
 
73
81
  constructor(options: RunRegistryOptions = {}) {
74
82
  this.#store = options.store;
@@ -90,25 +98,72 @@ export class RunRegistry {
90
98
  return [...this.#runs.keys()];
91
99
  }
92
100
 
101
+ /** Every addressable Run with its start instant, in registration order. */
102
+ get datedRuns(): readonly DatedRun[] {
103
+ return [...this.#runs.entries()].map(([id, entry]) => ({
104
+ startedAt: this.#startedAt.get(id) ?? "",
105
+ entry,
106
+ }));
107
+ }
108
+
93
109
  /**
94
- * Loads persisted Runs, settles the ones an earlier session left `live`, and
95
- * seeds the still-unaccounted ones. Finished Runs of earlier sessions stay on
96
- * disk and are reachable through {@link recall} by id.
110
+ * Binds this Host Session's pi session id, settles the Runs an earlier
111
+ * session left `live`, and adopts the ones this session owns (ADR-0036).
112
+ *
113
+ * Idempotent per session id: a second `session_start` with the same id (a
114
+ * `/reload`) adopts nothing again. A fork carries a new id and therefore
115
+ * adopts nothing; the parent's Runs stay reachable through {@link recall}.
97
116
  */
98
- async restore(): Promise<void> {
117
+ async restore(sessionId?: string): Promise<void> {
118
+ const bound = sessionId ?? null;
119
+ if (this.#restoredFor === bound) return;
120
+ this.#sessionId = bound;
121
+ this.#restoredFor = bound;
99
122
  if (this.#store === undefined) return;
100
123
  const records = await restoreRecords({ store: this.#store, now: this.#now });
101
124
  for (const record of records) {
102
- if (record.state !== "orphaned" && record.state !== "interrupted") continue;
125
+ // A Run of this process is never shadowed by its own persisted record.
126
+ if (this.#runs.has(record.id)) continue;
127
+ if (!this.#adoptable(record)) continue;
103
128
  this.#runs.set(record.id, { state: "restored", run: restored(record) });
129
+ this.#startedAt.set(record.id, record.startedAt);
104
130
  }
105
131
  }
106
132
 
133
+ /** Persisted Runs this overview does not list; addressable by id only. */
134
+ async unlistedRunCount(listed: readonly string[]): Promise<number> {
135
+ if (this.#store === undefined) return 0;
136
+ const shown = new Set(listed);
137
+ return (await this.#store.load()).filter((record) => !shown.has(record.id)).length;
138
+ }
139
+
140
+ /** Persisted finished Runs that another pi session owns. */
141
+ async finishedElsewhereCount(): Promise<number> {
142
+ if (this.#store === undefined) return 0;
143
+ return (await this.#store.load()).filter(
144
+ (record) =>
145
+ record.state === "finished" &&
146
+ (this.#sessionId === null || record.sessionId !== this.#sessionId),
147
+ ).length;
148
+ }
149
+
150
+ /**
151
+ * `restoreRecords` already settled every stale `live` record, so this check
152
+ * can never adopt a Run as live (ADR-0036).
153
+ */
154
+ #adoptable(record: RunRecord): boolean {
155
+ if (record.state === "orphaned" || record.state === "interrupted") return true;
156
+ return this.#sessionId !== null && record.sessionId === this.#sessionId;
157
+ }
158
+
107
159
  /** Registers a newly-started Run and synchronously observes its settlement. */
108
160
  add(run: LiveRun, launch?: RunLaunch, identity?: ProcessIdentity | null): void {
161
+ const startedAt = this.#now().toISOString();
162
+ this.#startedAt.set(run.id, startedAt);
109
163
  this.#live.set(run.id, run);
110
164
  this.#runs.set(run.id, { state: "live", run });
111
- if (launch !== undefined) this.#persistStart(run.id, launch, identity ?? null);
165
+ if (launch !== undefined)
166
+ this.#persistStart({ id: run.id, launch, process: identity ?? null, now: startedAt });
112
167
  void run.outcome.then(
113
168
  (outcome) => this.finish(run.id, outcome),
114
169
  (reason: unknown) => this.reject(run.id, reason),
@@ -203,13 +258,16 @@ export class RunRegistry {
203
258
  this.#persistEnd(id, summary, outcome);
204
259
  }
205
260
 
206
- #persistStart(id: string, launch: RunLaunch, identity: ProcessIdentity | null): void {
261
+ #persistStart(options: {
262
+ readonly id: string;
263
+ readonly launch: RunLaunch;
264
+ readonly process: ProcessIdentity | null;
265
+ readonly now: string;
266
+ }): void {
207
267
  if (this.#store === undefined) return;
208
268
  const store = this.#store;
209
- this.#records.set(
210
- id,
211
- startedRecord({ id, launch, process: identity, now: this.#now().toISOString() }),
212
- );
269
+ const id = options.id;
270
+ this.#records.set(id, startedRecord({ ...options, sessionId: this.#sessionId }));
213
271
  this.#persist(id);
214
272
  void store.prune();
215
273
  }
@@ -1,4 +1,4 @@
1
- import { isProcessAlive, type ProcessIdentity } from "./process-liveness.ts";
1
+ import { isProcessAlive, type ProcessIdentity } from "../process/index.ts";
2
2
  import type { RunRecord } from "./run-record.ts";
3
3
  import type { RunStore } from "./run-store.ts";
4
4
 
@@ -3,8 +3,8 @@
3
3
  * the blocking, interactive, and background paths of `yaag_run`.
4
4
  */
5
5
  import type { RunViewResult } from "@yaag/tui";
6
+ import type { RunOutcome } from "../process/index.ts";
6
7
  import type { RunRegistry, RunSettlement } from "./run-registry.ts";
7
- import type { RunOutcome } from "./spawn-run.ts";
8
8
 
9
9
  /**
10
10
  * The settlement the registry retained for a Run.
@@ -1,5 +1,5 @@
1
1
  import { initialSummary } from "@yaag/runtime";
2
- import type { RunHandle, StartRunOptions } from "./spawn-run.ts";
2
+ import type { RunHandle, StartRunOptions } from "../process/index.ts";
3
3
 
4
4
  /** A start seam that never spawns: it captures its options and ends the Run at once. */
5
5
  export function fakeStart(stdout = "{}"): {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Public surface of the `testing/` module: package-spanning test helpers and fakes.
3
+ * Files inside this directory import each other directly.
4
+ */
5
+ export { fakeStart } from "./fake-run-start.ts";
6
+ export { loadPiKeybindings } from "./pi-keybindings.ts";
7
+ export { renderCallLabel, renderCallLines } from "./run-call-render.ts";
8
+ export {
9
+ asError,
10
+ fixture,
11
+ run,
12
+ running,
13
+ type Sent,
14
+ stop,
15
+ toolDescription,
16
+ until,
17
+ } from "./run-tool-test-support.ts";
18
+ export { TestExtensionContext } from "./test-extension-context.ts";
19
+ export { TestTuiContext } from "./test-tui-context.ts";
@@ -1,9 +1,8 @@
1
1
  import { stripTerminalSequences } from "@earendil-works/pi-tui";
2
+ import { resolveBun, resolveCliEntry } from "../process/index.ts";
3
+ import { RunRegistry } from "../record/index.ts";
4
+ import { createRunTool } from "../tool/index.ts";
2
5
  import { fakeTheme } from "./fake-theme.ts";
3
- import { resolveBun } from "./resolve-bun.ts";
4
- import { resolveCliEntry } from "./resolve-cli.ts";
5
- import { RunRegistry } from "./run-registry.ts";
6
- import { createRunTool } from "./run-tool.ts";
7
6
 
8
7
  const cli = resolveCliEntry();
9
8
  const bun = await resolveBun();
@@ -1,15 +1,16 @@
1
1
  import { dirname, join } from "node:path";
2
2
  import type { AgentToolResult, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import type { BackgroundStatus } from "./background-status.ts";
4
- import { resolveBun } from "./resolve-bun.ts";
5
- import { resolveCliEntry } from "./resolve-cli.ts";
3
+ import type { RunHandle } from "../process/index.ts";
4
+ import { resolveBun, resolveCliEntry } from "../process/index.ts";
5
+ import { RunRegistry } from "../record/index.ts";
6
+ import {
7
+ createRunTool,
8
+ createStopTool,
9
+ type SendMessage,
10
+ type StopDetails,
11
+ } from "../tool/index.ts";
12
+ import type { BackgroundStatus, RunDetails, RunTreeStore } from "../view/index.ts";
6
13
  import type { RunParams } from "./run-call-render.ts";
7
- import type { RunDetails } from "./run-details.ts";
8
- import { RunRegistry } from "./run-registry.ts";
9
- import { createRunTool, type SendMessage } from "./run-tool.ts";
10
- import type { RunTreeStore } from "./run-trees.ts";
11
- import type { RunHandle } from "./spawn-run.ts";
12
- import { createStopTool, type StopDetails } from "./stop-tool.ts";
13
14
  import { TestExtensionContext } from "./test-extension-context.ts";
14
15
 
15
16
  const cli = resolveCliEntry();
@@ -34,7 +35,7 @@ export async function run(
34
35
  readonly onUpdate?: (partial: AgentToolResult<RunDetails>) => void;
35
36
  readonly registry?: RunRegistry;
36
37
  readonly sent?: Sent[];
37
- readonly start?: (options: import("./spawn-run.ts").StartRunOptions) => RunHandle;
38
+ readonly start?: (options: import("../process/index.ts").StartRunOptions) => RunHandle;
38
39
  /** A tui-mode context, for the inline foreground path. */
39
40
  readonly ctx?: ExtensionContext;
40
41
  readonly store?: RunTreeStore;
@@ -1,8 +1,7 @@
1
1
  import { resolve } from "node:path";
2
2
  import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
- import { startCliChild } from "./cli-child.ts";
5
- import { statusReport } from "./status.ts";
4
+ import { startCliChild, statusReport } from "../process/index.ts";
6
5
 
7
6
  const parameters = Type.Object({
8
7
  file: Type.String({ description: "Path to the Orchestration Program file" }),
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Public surface of the `tool/` module: the pi tools the extension registers.
3
+ * Files inside this directory import each other directly.
4
+ */
5
+ export { createDescribeTool } from "./describe-tool.ts";
6
+ export { createRunTool, type RunDetails, type SendMessage } from "./run-tool.ts";
7
+ export { createStatusTool } from "./status-tool.ts";
8
+ export { createStopTool, type StopDetails } from "./stop-tool.ts";
9
+ export { createSystemPromptAppender } from "./system-prompt-append.ts";
@@ -3,25 +3,40 @@ import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-age
3
3
  import { Text } from "@earendil-works/pi-tui";
4
4
  import { initialSummary } from "@yaag/runtime";
5
5
  import { Type } from "typebox";
6
- import { type BackgroundStatus, createBackgroundStatus } from "./background-status.ts";
7
- import { callBody } from "./run-call-body.ts";
8
- import type { RunDetails } from "./run-details.ts";
9
- import { foregroundInline, foregroundResult, inlineAvailable } from "./run-foreground.ts";
10
- import { RunTreeComponent } from "./run-tree-component.ts";
11
- import { RunTreeStore } from "./run-trees.ts";
6
+ import type { RunDetails } from "../view/index.ts";
7
+ import {
8
+ type BackgroundStatus,
9
+ callBody,
10
+ createBackgroundStatus,
11
+ foregroundInline,
12
+ foregroundResult,
13
+ inlineAvailable,
14
+ RunTreeComponent,
15
+ RunTreeStore,
16
+ } from "../view/index.ts";
12
17
 
13
- export type { RunDetails } from "./run-details.ts";
18
+ export type { RunDetails } from "../view/index.ts";
14
19
 
15
- import { type ProcessIdentity, readProcessStart } from "./process-liveness.ts";
16
- import { resolveProgramParams } from "./resume-source.ts";
17
- import type { ProgramTarget } from "./run-argv.ts";
18
- import { mintRunId } from "./run-id.ts";
19
- import { confirmProgramTarget, programTarget } from "./run-program-param.ts";
20
- import type { RunLaunch } from "./run-record.ts";
21
- import type { LiveRun, RunRegistry, RunSettlement } from "./run-registry.ts";
22
- import { errorText, failure, observedSettlement } from "./run-settlement.ts";
23
- import { type RunHandle, type RunOutcome, type StartRunOptions, startRun } from "./spawn-run.ts";
24
- import { statusReport } from "./status.ts";
20
+ import type { ProgramTarget } from "../process/index.ts";
21
+ import {
22
+ type ProcessIdentity,
23
+ type RunHandle,
24
+ type RunOutcome,
25
+ readProcessStart,
26
+ type StartRunOptions,
27
+ startRun,
28
+ statusReport,
29
+ } from "../process/index.ts";
30
+ import type { LiveRun, RunLaunch, RunRegistry, RunSettlement } from "../record/index.ts";
31
+ import {
32
+ confirmProgramTarget,
33
+ errorText,
34
+ failure,
35
+ mintRunId,
36
+ observedSettlement,
37
+ programTarget,
38
+ resolveProgramParams,
39
+ } from "../record/index.ts";
25
40
 
26
41
  const parameters = Type.Object({
27
42
  file: Type.Optional(
@@ -2,9 +2,8 @@ import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-
2
2
  import type { RunSummary } from "@yaag/runtime";
3
3
  import { renderSnapshot, TreeState } from "@yaag/tui";
4
4
  import { Type } from "typebox";
5
- import type { RunRecord } from "./run-record.ts";
6
- import type { RegisteredRun, RestoredRun, RunRegistry } from "./run-registry.ts";
7
- import { toUsage } from "./usage.ts";
5
+ import type { RegisteredRun, RestoredRun, RunRegistry } from "../record/index.ts";
6
+ import { boundedRuns, RUN_LISTING_CAP, restoredStateText, toUsage } from "../view/index.ts";
8
7
 
9
8
  const parameters = Type.Object({
10
9
  id: Type.Optional(Type.String({ description: "The Run id returned by yaag_run (e.g. r1)" })),
@@ -33,18 +32,22 @@ const DESCRIPTION = [
33
32
  "resolves: a Run whose process died is reported as interrupted, and a Run whose",
34
33
  "process is still alive is reported as orphaned.",
35
34
  "",
36
- "Without an id, lists this session's Runs plus every orphaned or interrupted",
37
- "Run left by an earlier session.",
35
+ "Without an id, this tool lists the Runs of this session, and the orphaned or",
36
+ "interrupted Runs another session left. The list is bounded: it holds every",
37
+ `live Run, and then the newest Runs up to a total of ${RUN_LISTING_CAP}. An older Run stays`,
38
+ "on disk, and the last line of the list counts the Runs on disk. Give an id to",
39
+ "see one of them.",
38
40
  "",
39
41
  "An interrupted inline Run resumes with `resume` alone: yaag reads the program",
40
42
  "source from its own Run record.",
41
43
  ].join("\n");
42
44
 
43
45
  /**
44
- * The `yaag_status` tool renders session-local Run snapshots for the model.
46
+ * The `yaag_status` tool renders Run snapshots for the model.
45
47
  *
46
- * Throws if an id was not started in this session; finished Runs remain
47
- * queryable and are never restarted or controlled by this tool.
48
+ * An id resolves through {@link RunRegistry.recall}, so a Run of any session is
49
+ * queryable while its record is on disk; only an id no record holds throws.
50
+ * The tool never restarts or controls a Run.
48
51
  */
49
52
  export function createStatusTool(
50
53
  registry: RunRegistry,
@@ -58,7 +61,7 @@ export function createStatusTool(
58
61
  description: DESCRIPTION,
59
62
  parameters,
60
63
  async execute(_id, params) {
61
- if (params.id === undefined) return overview(registry.runs, now, width);
64
+ if (params.id === undefined) return await overview(registry, now, width);
62
65
  const found = await registry.recall(params.id);
63
66
  if (found.state === "unknown") throw unknownId(params.id, registry.knownIds);
64
67
  const details = detailsFor(found);
@@ -71,25 +74,40 @@ export function createStatusTool(
71
74
  };
72
75
  }
73
76
 
74
- function overview(
75
- runs: readonly RegisteredRun[],
77
+ /**
78
+ * Lists the Runs of this session, bounded by {@link RUN_LISTING_CAP}.
79
+ *
80
+ * The disk counts are read on each call: the store holds dozens of small files
81
+ * and a stale count would name Runs that no longer exist (ADR-0036).
82
+ */
83
+ async function overview(
84
+ registry: RunRegistry,
76
85
  now: () => number,
77
86
  width: number,
78
- ): AgentToolResult<StatusDetails> {
79
- if (runs.length === 0)
80
- return {
81
- content: [{ type: "text", text: "No Runs started this session." }],
82
- details: { summary: undefined },
83
- };
84
- return {
85
- content: [
86
- {
87
- type: "text",
88
- text: runs.map((run) => snapshot(detailsFor(run), now, width, true)).join("\n"),
89
- },
90
- ],
91
- details: { summary: undefined },
92
- };
87
+ ): Promise<AgentToolResult<StatusDetails>> {
88
+ const shown = boundedRuns(registry.datedRuns);
89
+ if (shown.length === 0) return text(emptyOverview(await registry.finishedElsewhereCount()));
90
+ const lines = shown.map((run) => snapshot(detailsFor(run), now, width, true));
91
+ const unlisted = await registry.unlistedRunCount(shown.map((run) => run.run.id));
92
+ if (unlisted > 0) lines.push(overflowLine(shown.length, unlisted));
93
+ return text(lines.join("\n"));
94
+ }
95
+
96
+ function text(value: string): AgentToolResult<StatusDetails> {
97
+ return { content: [{ type: "text", text: value }], details: { summary: undefined } };
98
+ }
99
+
100
+ /** The confirmed empty-overview copy of the specification, with its count. */
101
+ function emptyOverview(elsewhere: number): string {
102
+ if (elsewhere === 0) return "No Runs in this session.";
103
+ const runs = elsewhere === 1 ? "Run" : "Runs";
104
+ const verb = elsewhere === 1 ? "is" : "are";
105
+ return `No Runs in this session. ${elsewhere} finished ${runs} from other sessions ${verb} on disk; query one with yaag_status({ id }).`;
106
+ }
107
+
108
+ /** The confirmed overflow copy of the specification, with its two counts. */
109
+ function overflowLine(shown: number, unlisted: number): string {
110
+ return `${shown} shown; ${unlisted} more on disk \u2014 query one with yaag_status({ id })`;
93
111
  }
94
112
 
95
113
  function detailsFor(run: RegisteredRun): SnapshotDetails {
@@ -106,51 +124,19 @@ function detailsFor(run: RegisteredRun): SnapshotDetails {
106
124
  }
107
125
 
108
126
  /**
109
- * A Run of an earlier Host Session. Its persisted state is stated in the
110
- * Result region, because the folded Summary alone cannot say that nobody owns
111
- * the Run any more (ticket 02).
127
+ * A Run read back from its record: this session may own it again, but nothing
128
+ * is attached to it. Its persisted state is stated in the Result region,
129
+ * because the folded Summary alone cannot say that the snapshot is final and
130
+ * that no process feeds it (ticket 02, ADR-0036).
112
131
  */
113
132
  function restoredDetails(run: RestoredRun): SnapshotDetails {
114
133
  const { record } = run;
115
- const state = `Run ${record.id} is ${record.state} (started ${record.startedAt}).`;
116
- const alive =
117
- record.state === "orphaned" && record.process !== null
118
- ? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
119
- : "";
120
- const detail = `${alive}${resumeHint(record)}`;
121
134
  const result = record.outcome?.kind === "fulfilled" ? `\n${record.outcome.result}` : "";
122
- return { id: record.id, summary: run.summary, result: `${state}${detail}${result}` };
123
- }
124
-
125
- /**
126
- * Names the Checkpoint an unowned Run left behind (ADR-0031), and the call that
127
- * resumes it. The Run republishes the Checkpoint at each Ask boundary, so it
128
- * exists whenever the Run reached one, for a dead Run and for a still-running
129
- * orphan alike.
130
- *
131
- * The hint names the Checkpoint path and never the stored program source: the
132
- * path is enough, because yaag reads the source back from the Run record
133
- * (ADR-0033). A whole source here would flood the transcript and repeat a
134
- * private text the record already holds.
135
- */
136
- function resumeHint(record: RunRecord): string {
137
- const artifact = record.summary.artifact;
138
- if (artifact === null || artifact === undefined) return "";
139
- if (record.state !== "interrupted" && record.state !== "orphaned") return "";
140
- // Each path becomes a JSON string literal: a path can hold a quote, a
141
- // backslash, or a newline, and the hint must stay a call the model can copy.
142
- const resume = JSON.stringify(artifact);
143
- const { launch } = record;
144
- switch (launch.kind) {
145
- case "inline":
146
- return ` Resume it with yaag_run({ resume: ${resume} }) and no file and no script; yaag reuses the program source it stored.`;
147
- case "file":
148
- return ` Resume it with yaag_run({ file: ${JSON.stringify(launch.file)}, resume: ${resume} }).`;
149
- default: {
150
- const never: never = launch;
151
- return never;
152
- }
153
- }
135
+ return {
136
+ id: record.id,
137
+ summary: run.summary,
138
+ result: `${restoredStateText(record)}${result}`,
139
+ };
154
140
  }
155
141
 
156
142
  function snapshot(
@@ -1,9 +1,9 @@
1
1
  import type { AgentToolResult, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import type { RunSummary } from "@yaag/runtime";
3
3
  import { Type } from "typebox";
4
- import { isProcessAlive } from "./process-liveness.ts";
5
- import type { RestoredRun, RunRegistry } from "./run-registry.ts";
6
- import { toUsage } from "./usage.ts";
4
+ import { isProcessAlive } from "../process/index.ts";
5
+ import type { RestoredRun, RunRegistry } from "../record/index.ts";
6
+ import { toUsage } from "../view/index.ts";
7
7
 
8
8
  /** What the stop result carries for rendering — never shown to the model. */
9
9
  export interface StopDetails {
@@ -67,8 +67,8 @@ function agentStateReport(agent: RunSummary["agents"][string]): string {
67
67
  }
68
68
 
69
69
  /**
70
- * Ends a Run nobody owns any more. There is no reap ladder to run — its pipes
71
- * belong to a dead Host Session — so the process group is killed outright
70
+ * Ends a Run no process is attached to. There is no reap ladder to run — its
71
+ * pipes belong to a dead Host Session — so the process group is killed outright
72
72
  * (ADR-0008). The pid is re-probed first, because a record can name a pid that
73
73
  * a different process has since inherited (ticket 02).
74
74
  */
@@ -7,7 +7,7 @@
7
7
  * as the last background Run ends.
8
8
  */
9
9
  import { costText } from "@yaag/tui";
10
- import type { RunRegistry } from "./run-registry.ts";
10
+ import type { RunRegistry } from "../record/index.ts";
11
11
  import type { RunTreeStore } from "./run-trees.ts";
12
12
 
13
13
  /** The one footer key this extension owns. */
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Public surface of the `view/` module: TUI views, renderers, and the /yaag command.
3
+ * Files inside this directory import each other directly.
4
+ */
5
+ export {
6
+ type BackgroundStatus,
7
+ createBackgroundStatus,
8
+ STATUS_KEY,
9
+ type StatusSurface,
10
+ } from "./background-status.ts";
11
+ export { restoredStateText } from "./restored-run-text.ts";
12
+ export { callBody } from "./run-call-body.ts";
13
+ export { createRunCompleteRenderer } from "./run-complete-renderer.ts";
14
+ export type { RunDetails } from "./run-details.ts";
15
+ export {
16
+ foregroundInline,
17
+ foregroundResult,
18
+ foregroundWidgetKey,
19
+ inlineAvailable,
20
+ } from "./run-foreground.ts";
21
+ export { boundedRuns, RUN_LISTING_CAP } from "./run-listing.ts";
22
+ export { RunTreeComponent } from "./run-tree-component.ts";
23
+ export { RunTreeStore } from "./run-trees.ts";
24
+ export { toUsage } from "./usage.ts";
25
+ export { createYaagCommand } from "./yaag-command.ts";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The words a Run read back from its record gets, shared by `yaag_status` and
3
+ * the `/yaag` snapshot view (ADR-0036). A resumed session owns such a Run again
4
+ * when the record names it, but no session is attached to it: there is no event
5
+ * stream and no renderer projection, only the persisted Summary. Pure text, no
6
+ * I/O.
7
+ */
8
+ import type { RunViewResult } from "@yaag/tui";
9
+ import type { RunRecord } from "../record/index.ts";
10
+
11
+ /**
12
+ * States the persisted verdict of a Run that no process is attached to.
13
+ *
14
+ * The folded Summary alone cannot say that the Run stopped without an owner
15
+ * process, or that a Checkpoint of it can be resumed (ticket 02), so the state,
16
+ * the surviving process, and the resume call are stated here.
17
+ */
18
+ export function restoredStateText(record: RunRecord): string {
19
+ const state = `Run ${record.id} is ${record.state} (started ${record.startedAt}).`;
20
+ const alive =
21
+ record.state === "orphaned" && record.process !== null
22
+ ? ` Its process ${record.process.pid} is still running; stop it with yaag_stop.`
23
+ : "";
24
+ return `${state}${alive}${resumeHint(record)}`;
25
+ }
26
+
27
+ /**
28
+ * The frozen Result region of a restored Run, for the read-only `/yaag` view.
29
+ *
30
+ * Every restored Run states its persisted verdict first, whatever its outcome:
31
+ * the reader opened a snapshot of a record, which no live Run feeds any more,
32
+ * and the value or the error alone does not say so (ADR-0036).
33
+ */
34
+ export function restoredViewResult(record: RunRecord): RunViewResult {
35
+ const state = restoredStateText(record);
36
+ const { outcome } = record;
37
+ if (outcome?.kind === "fulfilled")
38
+ return { kind: "fulfilled", result: joinText(state, outcome.result) };
39
+ if (outcome?.kind === "rejected")
40
+ return { kind: "failed", error: joinText(state, outcome.reason) };
41
+ return { kind: "failed", error: state };
42
+ }
43
+
44
+ function joinText(state: string, detail: string): string {
45
+ return detail.trim() === "" ? state : `${state}\n${detail}`;
46
+ }
47
+
48
+ /**
49
+ * Names the Checkpoint an unowned Run left behind (ADR-0031), and the call that
50
+ * resumes it. The Run republishes the Checkpoint at each Ask boundary, so it
51
+ * exists whenever the Run reached one, for a dead Run and for a still-running
52
+ * orphan alike.
53
+ *
54
+ * The hint names the Checkpoint path and never the stored program source: the
55
+ * path is enough, because yaag reads the source back from the Run record
56
+ * (ADR-0033). A whole source here would flood the transcript and repeat a
57
+ * private text the record already holds.
58
+ */
59
+ function resumeHint(record: RunRecord): string {
60
+ const artifact = record.summary.artifact;
61
+ if (artifact === null || artifact === undefined) return "";
62
+ if (record.state !== "interrupted" && record.state !== "orphaned") return "";
63
+ // Each path becomes a JSON string literal: a path can hold a quote, a
64
+ // backslash, or a newline, and the hint must stay a call the model can copy.
65
+ const resume = JSON.stringify(artifact);
66
+ const { launch } = record;
67
+ switch (launch.kind) {
68
+ case "inline":
69
+ return ` Resume it with yaag_run({ resume: ${resume} }) and no file and no script; yaag reuses the program source it stored.`;
70
+ case "file":
71
+ return ` Resume it with yaag_run({ file: ${JSON.stringify(launch.file)}, resume: ${resume} }).`;
72
+ default: {
73
+ const never: never = launch;
74
+ return never;
75
+ }
76
+ }
77
+ }
@@ -4,7 +4,7 @@
4
4
  * which shows that source.
5
5
  */
6
6
  import { keyText, type Theme } from "@earendil-works/pi-coding-agent";
7
- import { type ProgramParams, programExpansion, programLabel } from "./run-program-param.ts";
7
+ import { type ProgramParams, programExpansion, programLabel } from "../record/index.ts";
8
8
 
9
9
  /** What the call render needs: the call itself, its styling, and its state. */
10
10
  export interface CallBodyOptions {
@@ -8,9 +8,9 @@
8
8
  */
9
9
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
10
10
  import { renderInlineRun, type TreeState } from "@yaag/tui";
11
+ import type { LiveRun, RunRegistry, RunSettlement } from "../record/index.ts";
12
+ import { failure, observedSettlement, toError } from "../record/index.ts";
11
13
  import type { RunDetails } from "./run-details.ts";
12
- import type { LiveRun, RunRegistry, RunSettlement } from "./run-registry.ts";
13
- import { failure, observedSettlement, toError } from "./run-settlement.ts";
14
14
  import type { RunTreeStore } from "./run-trees.ts";
15
15
  import { toUsage } from "./usage.ts";
16
16
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The one bound both Run listings share: the `/yaag` picker and the
3
+ * `yaag_status` overview must agree on which Runs exist (ADR-0036).
4
+ */
5
+ import type { DatedRun, RegisteredRun } from "../record/index.ts";
6
+
7
+ /** How many Runs the `/yaag` picker and the `yaag_status` overview list. */
8
+ export const RUN_LISTING_CAP = 5;
9
+
10
+ /**
11
+ * The newest `cap` Runs, newest first, plus every live Run whatever its age.
12
+ *
13
+ * A live Run is exempt from the cap: the session must always see and stop what
14
+ * it runs (ADR-0036). Runs that start at the same instant keep registration
15
+ * order.
16
+ */
17
+ export function boundedRuns(
18
+ runs: readonly DatedRun[],
19
+ cap: number = RUN_LISTING_CAP,
20
+ ): readonly RegisteredRun[] {
21
+ const newestFirst = [...runs.entries()].sort(compareNewestFirst).map(([, run]) => run);
22
+ return newestFirst
23
+ .filter((run, rank) => rank < cap || run.entry.state === "live")
24
+ .map((run) => run.entry);
25
+ }
26
+
27
+ function compareNewestFirst(
28
+ left: readonly [number, DatedRun],
29
+ right: readonly [number, DatedRun],
30
+ ): number {
31
+ if (left[1].startedAt === right[1].startedAt) return left[0] - right[0];
32
+ return left[1].startedAt < right[1].startedAt ? 1 : -1;
33
+ }
@@ -3,21 +3,28 @@
3
3
  * the list can be tested without a live UI.
4
4
  */
5
5
  import type { RunSummary } from "@yaag/runtime";
6
- import type { RegisteredRun } from "./run-registry.ts";
6
+ import type { DatedRun } from "../record/index.ts";
7
+ import { boundedRuns } from "./run-listing.ts";
7
8
 
8
9
  /** One Run the picker can open: its id, latest Summary fold, and liveness. */
9
10
  export interface PickableRun {
10
11
  readonly id: string;
11
12
  readonly summary: RunSummary;
12
13
  readonly live: boolean;
14
+ /** A Run an earlier session started; the view of it is a frozen snapshot. */
15
+ readonly restored: boolean;
13
16
  }
14
17
 
15
- /** Every registered Run, live and finished, in registration order. */
16
- export function pickableRuns(runs: readonly RegisteredRun[]): readonly PickableRun[] {
17
- return runs.map((run) => ({
18
+ /**
19
+ * The Runs the picker lists: the bounded listing the `yaag_status` overview
20
+ * uses, so both surfaces agree on which Runs exist (ADR-0036).
21
+ */
22
+ export function pickableRuns(runs: readonly DatedRun[]): readonly PickableRun[] {
23
+ return boundedRuns(runs).map((run) => ({
18
24
  id: run.run.id,
19
25
  summary: run.run.summary,
20
26
  live: run.state === "live",
27
+ restored: run.state === "restored",
21
28
  }));
22
29
  }
23
30
 
@@ -14,9 +14,10 @@ import type {
14
14
  RegisteredCommand,
15
15
  } from "@earendil-works/pi-coding-agent";
16
16
  import { createRunTreeView, type RunTreeViewHost, type RunViewResult, TreeState } from "@yaag/tui";
17
+ import type { RunRegistry, RunSettlement } from "../record/index.ts";
18
+ import { observedSettlement, viewResult } from "../record/index.ts";
19
+ import { restoredViewResult } from "./restored-run-text.ts";
17
20
  import { type PickableRun, pickableRuns, runFromLabel, runPickerLabel } from "./run-picker.ts";
18
- import type { RunRegistry, RunSettlement } from "./run-registry.ts";
19
- import { observedSettlement, viewResult } from "./run-settlement.ts";
20
21
  import { createRunTreeHost } from "./run-tree-host.ts";
21
22
  import type { RunTreeStore } from "./run-trees.ts";
22
23
 
@@ -53,9 +54,9 @@ export function createYaagCommand(options: YaagCommandOptions): CommandOptions {
53
54
  * not the TUI, or when the reader cancels the picker.
54
55
  */
55
56
  export async function runYaagCommand(ctx: YaagContext, options: YaagCommandOptions): Promise<void> {
56
- const runs = pickableRuns(options.registry.runs);
57
+ const runs = pickableRuns(options.registry.datedRuns);
57
58
  if (runs.length === 0) {
58
- ctx.ui.notify("yaag: no Runs started this session.", "info");
59
+ ctx.ui.notify("yaag: no Runs in this session.", "info");
59
60
  return;
60
61
  }
61
62
  if (ctx.mode !== "tui") {
@@ -87,8 +88,13 @@ async function openRun(
87
88
  run: PickableRun,
88
89
  options: YaagCommandOptions,
89
90
  ): Promise<void> {
90
- const state = options.store.get(run.id) ?? TreeState.fromSummary(run.summary);
91
91
  const found = options.registry.lookup(run.id);
92
+ // A Run of an earlier session has no projection here, so its view is built
93
+ // from the persisted Summary alone (ADR-0036).
94
+ const state =
95
+ found.state === "restored"
96
+ ? TreeState.fromSummary(run.summary)
97
+ : (options.store.get(run.id) ?? TreeState.fromSummary(run.summary));
92
98
  let unsubscribe: (() => void) | undefined;
93
99
  // The observer below outlives no more than the `ctx.ui.custom()` call: once
94
100
  // the view exits or the UI call rejects, a later settlement must touch
@@ -98,6 +104,8 @@ async function openRun(
98
104
  await ctx.ui.custom<void>((tui, _theme, keybindings, done) => {
99
105
  const host: RunTreeViewHost = {
100
106
  ...createRunTreeHost({ ui: ctx.ui, tui, keybindings, state }),
107
+ // A restored orphan is never re-attached, so there is nothing to stop
108
+ // from here; `yaag_stop` ends it (ADR-0036).
101
109
  stop: () => {
102
110
  if (found.state === "live") found.run.stop();
103
111
  },
@@ -105,8 +113,13 @@ async function openRun(
105
113
  // hand over, because a Run of this session is already addressable.
106
114
  done: () => done(),
107
115
  };
108
- const view = createRunTreeView({ state, host, label: run.id });
109
- if (found.state === "finished")
116
+ const view = createRunTreeView({
117
+ state,
118
+ host,
119
+ label: found.state === "restored" ? `${run.id} (restored)` : run.id,
120
+ });
121
+ if (found.state === "restored") view.settle(restoredViewResult(found.run.record));
122
+ else if (found.state === "finished")
110
123
  settle(state, view, found.run.outcome, options.registry, run.id);
111
124
  else if (found.state === "live") {
112
125
  unsubscribe = options.store.subscribe((id) => {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Public surface of the `workspace/` module: workspace setup and Program Directory discovery.
3
+ * Files inside this directory import each other directly.
4
+ */
5
+ export { findProgramDirectories } from "./program-directories.ts";
6
+ export { createSetupWorkspaceExecutor } from "./setup-workspace.ts";
7
+ export { createSetupWorkspaceCommand } from "./setup-workspace-command.ts";
8
+ export { createSetupWorkspaceTool } from "./setup-workspace-tool.ts";
@@ -1,5 +1,9 @@
1
- import { type CliChild, type CliChildOptions, startCliChild } from "./cli-child.ts";
2
- import { statusReport } from "./status.ts";
1
+ import {
2
+ type CliChild,
3
+ type CliChildOptions,
4
+ startCliChild,
5
+ statusReport,
6
+ } from "../process/index.ts";
3
7
 
4
8
  /** Injectable boundary for starting the Bun CLI child. */
5
9
  export type StartCliChild = (options: CliChildOptions) => CliChild;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes