@gajae-code/coding-agent 0.11.11 → 0.12.0

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 (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -0,0 +1,239 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { Container, renderMetrics, Text, TUI } from "@gajae-code/tui";
4
+ import { VirtualTerminal } from "../../tui/test/virtual-terminal";
5
+ import {
6
+ __ircSidebarPerfCounters,
7
+ type IrcSidebarTheme,
8
+ IrcSplitViewComponent,
9
+ } from "../src/modes/components/irc-sidebar";
10
+ import { __ircLedgerPerfCounters, IrcObservationLedger } from "../src/modes/irc-observation-ledger";
11
+
12
+ const SCHEMA_VERSION = 1;
13
+ const WIDTH = 80;
14
+ const HEIGHTS = [1, 3, 10] as const;
15
+ const TRANSCRIPT_ROWS = [10_000, 100_000] as const;
16
+ const STICKY_SUFFIX_WORKLOAD_NAMES = [
17
+ "sticky-suffix-10000-rows-height-1",
18
+ "sticky-suffix-10000-rows-height-3",
19
+ "sticky-suffix-10000-rows-height-10",
20
+ "sticky-suffix-100000-rows-height-1",
21
+ "sticky-suffix-100000-rows-height-3",
22
+ "sticky-suffix-100000-rows-height-10",
23
+ ] as const;
24
+
25
+ const stickySuffixMatrix = TRANSCRIPT_ROWS.flatMap(transcriptRows =>
26
+ HEIGHTS.map(height => ({
27
+ transcriptRows,
28
+ height,
29
+ name: `sticky-suffix-${transcriptRows}-rows-height-${height}`,
30
+ })),
31
+ );
32
+
33
+ function assertStickySuffixMatrix(): void {
34
+ const names = stickySuffixMatrix.map(workload => workload.name);
35
+ if (JSON.stringify(names) !== JSON.stringify(STICKY_SUFFIX_WORKLOAD_NAMES))
36
+ throw new Error("sticky suffix workload matrix changed");
37
+ }
38
+
39
+ const sidebarTheme = {
40
+ fg: (_color: "dim" | "accent", text: string) => text,
41
+ bold: (text: string) => text,
42
+ boxSharp: { vertical: "|" },
43
+ } satisfies IrcSidebarTheme;
44
+
45
+ type HardCounters = Readonly<Record<string, number | boolean>>;
46
+ type WorkloadResult = Readonly<{ name: string; hard: HardCounters; advisory: Record<string, number> }>;
47
+ export type StickyViewportPr1Benchmark = Readonly<{
48
+ schemaVersion: number;
49
+ workloads: readonly WorkloadResult[];
50
+ advisory: {
51
+ readonly timingAndMemoryOnly: true;
52
+ readonly wallMs: number;
53
+ readonly cpuUserMicros: number;
54
+ readonly cpuSystemMicros: number;
55
+ readonly rssBytes: number;
56
+ };
57
+ }>;
58
+
59
+ function rows(count: number, prefix: string): Text[] {
60
+ return Array.from({ length: count }, (_, index) => new Text(`${prefix}-${index}`, 0, 0));
61
+ }
62
+
63
+ async function settle(terminal: VirtualTerminal): Promise<void> {
64
+ await terminal.waitForRender();
65
+ await Promise.resolve();
66
+ }
67
+
68
+ async function stickySuffixWorkload(transcriptRows: number, height: number): Promise<WorkloadResult> {
69
+ renderMetrics.reset();
70
+ const terminal = new VirtualTerminal(WIDTH, height, { isProcessTerminal: true });
71
+ const tui = new TUI(terminal);
72
+ const transcript = new Container();
73
+ for (const row of rows(transcriptRows, "transcript")) transcript.addChild(row);
74
+ const status = new Text("status: pinned", 0, 0);
75
+ const suffix = Array.from({ length: height + 3 }, (_value, index) => new Text(`suffix-${index}`, 0, 0));
76
+ tui.addChild(transcript);
77
+ tui.addChild(status);
78
+ for (const row of suffix) tui.addChild(row);
79
+ tui.setBottomPinnedComponent(status);
80
+ const flatFrameRows = transcriptRows + suffix.length + 1;
81
+ const originalSlice = Array.prototype.slice;
82
+ let flatFrameSlices = 0;
83
+ let negativeControlSlices = 0;
84
+ const isLargeFlatFrame = (receiver: unknown[]): boolean =>
85
+ receiver.length === flatFrameRows &&
86
+ receiver[0] === "transcript-0" &&
87
+ receiver[transcriptRows - 1] === `transcript-${transcriptRows - 1}`;
88
+ try {
89
+ Array.prototype.slice = function <T>(this: T[], start?: number, end?: number): T[] {
90
+ if (isLargeFlatFrame(this)) flatFrameSlices++;
91
+ return originalSlice.call(this, start, end);
92
+ };
93
+ const negativeControl = Array.from({ length: flatFrameRows }, (_value, index) =>
94
+ index < transcriptRows ? `transcript-${index}` : "suffix",
95
+ );
96
+ negativeControl.slice(0, 1);
97
+ negativeControlSlices = flatFrameSlices;
98
+ flatFrameSlices = 0;
99
+ tui.start();
100
+ await settle(terminal);
101
+ const structural = renderMetrics.snapshot().structuralCounters;
102
+ const selected = structural.pinnedSuffixSelectedRows ?? 0;
103
+ const overflowFrames = structural.pinnedSuffixOverflowFrames ?? 0;
104
+ if (flatFrameSlices !== 0) throw new Error("pinned suffix sliced the large transcript frame");
105
+ if (negativeControlSlices !== 1) throw new Error("large-frame slice detector did not count its negative control");
106
+ if (selected > height) throw new Error("pinned suffix selected rows exceed terminal height");
107
+ if (overflowFrames !== 1 || selected === 0) throw new Error("pinned suffix overflow workload did not execute");
108
+ return {
109
+ name: `sticky-suffix-${transcriptRows}-rows-height-${height}`,
110
+ hard: {
111
+ largeFlatFrameSliceCalls: flatFrameSlices,
112
+ pinnedSuffixOverflowFrames: overflowFrames,
113
+ pinnedSuffixSelectedRows: selected,
114
+ },
115
+ advisory: { renderCount: renderMetrics.snapshot().renderCount },
116
+ };
117
+ } finally {
118
+ Array.prototype.slice = originalSlice;
119
+ tui.stop();
120
+ }
121
+ }
122
+
123
+ async function equalOutputSourceWorkload(): Promise<WorkloadResult> {
124
+ renderMetrics.reset();
125
+ const terminal = new VirtualTerminal(WIDTH, 10);
126
+ const tui = new TUI(terminal);
127
+ try {
128
+ tui.addChild(new Text("output", 0, 0));
129
+ tui.setViewportOutputSource({ identity: "pr1-equal", revision: 1n });
130
+ tui.start();
131
+ await settle(terminal);
132
+ const before = renderMetrics.snapshot().renderCount;
133
+ for (let index = 0; index < 1_000; index++) tui.setViewportOutputSource({ identity: "pr1-equal", revision: 1n });
134
+ await Promise.resolve();
135
+ const snapshot = renderMetrics.snapshot();
136
+ const equalNoops = snapshot.structuralCounters.viewportOutputSourceEqualNoops ?? 0;
137
+ if (snapshot.renderCount !== before || equalNoops !== 1_000)
138
+ throw new Error("equal output source requested a render");
139
+ return {
140
+ name: "equal-output-source-1000",
141
+ hard: { equalNoops, renderRequests: snapshot.renderCount - before },
142
+ advisory: {},
143
+ };
144
+ } finally {
145
+ tui.stop();
146
+ }
147
+ }
148
+
149
+ function addObservation(ledger: IrcObservationLedger, id: string): void {
150
+ ledger.observe(
151
+ { observationId: id, kind: "incoming", from: "peer", to: "you", text: `observation ${id}`, timestamp: 0 },
152
+ false,
153
+ );
154
+ }
155
+
156
+ function sidebarCacheWorkload(): WorkloadResult {
157
+ __ircSidebarPerfCounters.reset();
158
+ __ircSidebarPerfCounters.enable();
159
+ __ircLedgerPerfCounters.reset();
160
+ __ircLedgerPerfCounters.enable();
161
+ const ledger = new IrcObservationLedger();
162
+ for (let index = 0; index < 9_999; index++) addObservation(ledger, `history-${index}`);
163
+ const split = new IrcSplitViewComponent(new Text("left pane", 0, 0), ledger, sidebarTheme);
164
+ split.setVisible(true);
165
+ for (let index = 0; index < 100; index++) split.render(WIDTH);
166
+ const stable = __ircSidebarPerfCounters.snapshot();
167
+ addObservation(ledger, "mutation");
168
+ split.render(WIDTH);
169
+ const final = __ircSidebarPerfCounters.snapshot();
170
+ const ledgerCounters = __ircLedgerPerfCounters.snapshot();
171
+ if (stable.projectionMemoMisses !== 1 || stable.styledCacheMisses !== 1 || stable.wrapCalls === 0)
172
+ throw new Error("unchanged sidebar did not reuse caches");
173
+ if (stable.projectionMemoHits !== 99 || stable.styledCacheHits !== 99)
174
+ throw new Error("unchanged sidebar cache counts changed");
175
+ if (final.wrapCalls <= stable.wrapCalls || final.projectionMemoMisses !== 2 || final.styledCacheMisses !== 2)
176
+ throw new Error("sidebar mutation did not reproject once");
177
+ return {
178
+ name: "irc-sidebar-near-cap-cache",
179
+ hard: {
180
+ ledgerEpochAdvances: ledgerCounters.epochAdvances,
181
+ unchangedProjectionMisses: stable.projectionMemoMisses,
182
+ unchangedProjectionHits: stable.projectionMemoHits,
183
+ unchangedStyledMisses: stable.styledCacheMisses,
184
+ unchangedStyledHits: stable.styledCacheHits,
185
+ unchangedWrapCalls: stable.wrapCalls,
186
+ mutationProjectionMisses: final.projectionMemoMisses,
187
+ mutationStyledMisses: final.styledCacheMisses,
188
+ mutationWrapCalls: final.wrapCalls,
189
+ },
190
+ advisory: {},
191
+ };
192
+ }
193
+
194
+ export async function runStickyViewportPr1Benchmark(): Promise<StickyViewportPr1Benchmark> {
195
+ const cpu = process.cpuUsage();
196
+ const started = performance.now();
197
+ const wasEnabled = renderMetrics.enabled;
198
+ renderMetrics.enable();
199
+ try {
200
+ assertStickySuffixMatrix();
201
+ const workloads: WorkloadResult[] = [];
202
+ for (const workload of stickySuffixMatrix)
203
+ workloads.push(await stickySuffixWorkload(workload.transcriptRows, workload.height));
204
+ workloads.push(await equalOutputSourceWorkload(), sidebarCacheWorkload());
205
+ const usage = process.cpuUsage(cpu);
206
+ return {
207
+ schemaVersion: SCHEMA_VERSION,
208
+ workloads,
209
+ advisory: {
210
+ timingAndMemoryOnly: true,
211
+ wallMs: performance.now() - started,
212
+ cpuUserMicros: usage.user,
213
+ cpuSystemMicros: usage.system,
214
+ rssBytes: process.memoryUsage().rss,
215
+ },
216
+ };
217
+ } finally {
218
+ renderMetrics.reset();
219
+ __ircSidebarPerfCounters.disable();
220
+ __ircLedgerPerfCounters.disable();
221
+ if (!wasEnabled) renderMetrics.disable();
222
+ }
223
+ }
224
+
225
+ function outputPath(args: readonly string[]): string | undefined {
226
+ const index = args.indexOf("--out");
227
+ if (index < 0 || !args[index + 1]) return undefined;
228
+ return args[index + 1];
229
+ }
230
+
231
+ if (import.meta.main) {
232
+ const benchmark = await runStickyViewportPr1Benchmark();
233
+ const output = `${JSON.stringify(benchmark, null, 2)}\n`;
234
+ const destination = outputPath(process.argv.slice(2));
235
+ if (destination) {
236
+ await fs.mkdir(path.dirname(destination), { recursive: true });
237
+ await Bun.write(destination, output);
238
+ } else process.stdout.write(output);
239
+ }
@@ -0,0 +1,292 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import {
4
+ renderStickyViewportShowcase,
5
+ STICKY_VIEWPORT_SHOWCASE_ENTRIES,
6
+ STICKY_VIEWPORT_SHOWCASE_KEYS,
7
+ type StickyViewportShowcaseEntry,
8
+ } from "../test/fixtures/tui/sticky-viewport-showcase";
9
+
10
+ const COMMAND =
11
+ "bun packages/coding-agent/scripts/capture-sticky-viewport-showcase.ts --out .gjc/qa/sticky-viewport-<run>";
12
+ const REVISION = "sticky-viewport-showcase-v2";
13
+ const TIMESTAMP = "1970-01-01T00:00:00.000Z";
14
+ const PAYLOADS = ["terminal.txt", "terminal-ansi.txt", "terminal.html", "metadata.json"] as const;
15
+ const FONT_RENDERING_ASSUMPTIONS =
16
+ "Embedded red-claw theme at deterministic truecolor; HTML uses a monospace terminal fallback stack.";
17
+ const WRAPPING_TRUNCATION_POLICY =
18
+ "ANSI-aware terminal-cell wrapping preserves semantic CJK phrase boundaries; constrained height drops the notice, decorative pet, then low-priority hooks without truncating pinned status or the focused composer.";
19
+ const ACCEPTANCE_VERSION = "sticky-viewport-stage-03";
20
+ const DESIGN_VERSION = "modes-design-sticky-viewport-v3";
21
+ const CJK_PHRASE_BOUNDARIES = ["의미 있는 문장 경계", "意味のある文の境界", "保留语义短语边界"] as const;
22
+ const HOST_MATRIX = {
23
+ capture_host: "VirtualTerminal",
24
+ live_pty: false,
25
+ network: false,
26
+ } as const;
27
+ const escapeHtml = (value: string) =>
28
+ value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
29
+ type AnsiStyle = {
30
+ foreground?: string;
31
+ background?: string;
32
+ bold?: boolean;
33
+ dim?: boolean;
34
+ italic?: boolean;
35
+ underline?: boolean;
36
+ blink?: boolean;
37
+ inverse?: boolean;
38
+ invisible?: boolean;
39
+ strikethrough?: boolean;
40
+ overline?: boolean;
41
+ };
42
+ const TERMINAL_DEFAULT_FOREGROUND = "#ffe7dc";
43
+ const TERMINAL_DEFAULT_BACKGROUND = "#110b0b";
44
+ const ANSI_COLORS: Record<number, string> = {
45
+ 30: "#000000",
46
+ 31: "#cc0000",
47
+ 32: "#4e9a06",
48
+ 33: "#c4a000",
49
+ 34: "#3465a4",
50
+ 35: "#75507b",
51
+ 36: "#06989a",
52
+ 37: "#d3d7cf",
53
+ 90: "#555753",
54
+ 91: "#ef2929",
55
+ 92: "#8ae234",
56
+ 93: "#fce94f",
57
+ 94: "#729fcf",
58
+ 95: "#ad7fa8",
59
+ 96: "#34e2e2",
60
+ 97: "#eeeeec",
61
+ };
62
+ /** Converts an xterm 256-color palette index into its canonical CSS color. */
63
+ export const xterm256Color = (index: number): string => {
64
+ if (!Number.isInteger(index) || index < 0 || index > 255) throw new RangeError(`invalid xterm color index ${index}`);
65
+ if (index < 16) return ANSI_COLORS[index < 8 ? index + 30 : index + 82]!;
66
+ if (index >= 232) {
67
+ const value = (index - 232) * 10 + 8;
68
+ return `rgb(${value},${value},${value})`;
69
+ }
70
+ const value = index - 16,
71
+ channel = (component: number) => (component === 0 ? 0 : component * 40 + 55);
72
+ return `rgb(${channel(Math.floor(value / 36))},${channel(Math.floor((value % 36) / 6))},${channel(value % 6)})`;
73
+ };
74
+ const styleAttribute = (style: AnsiStyle): string => {
75
+ const foreground = style.inverse ? (style.background ?? TERMINAL_DEFAULT_BACKGROUND) : style.foreground;
76
+ const background = style.inverse ? (style.foreground ?? TERMINAL_DEFAULT_FOREGROUND) : style.background;
77
+ const decorations = [
78
+ style.underline && "underline",
79
+ style.strikethrough && "line-through",
80
+ style.overline && "overline",
81
+ ]
82
+ .filter(Boolean)
83
+ .join(" ");
84
+ return [
85
+ foreground && `color:${foreground}`,
86
+ background && `background-color:${background}`,
87
+ style.bold && "font-weight:700",
88
+ style.dim && "opacity:.65",
89
+ style.italic && "font-style:italic",
90
+ style.blink && "animation:blink 1s step-end infinite",
91
+ style.invisible && "visibility:hidden",
92
+ decorations && `text-decoration:${decorations}`,
93
+ ]
94
+ .filter(Boolean)
95
+ .join(";");
96
+ };
97
+ const NON_VISUAL_TERMINAL_CONTROL = /\x1b_[^\x1b\x07]*(?:\x07|\x1b\\)/g;
98
+ /** Stateful SGR conversion with closing/reopening spans and partial/full resets. */
99
+ export function ansiToHtml(value: string): string {
100
+ const visibleText = value.replace(NON_VISUAL_TERMINAL_CONTROL, "");
101
+ let body = "",
102
+ offset = 0,
103
+ spanOpen = false,
104
+ style: AnsiStyle = {};
105
+ const close = () => {
106
+ if (spanOpen) {
107
+ body += "</span>";
108
+ spanOpen = false;
109
+ }
110
+ };
111
+ const open = () => {
112
+ const attribute = styleAttribute(style);
113
+ if (attribute) {
114
+ body += `<span style="${attribute}">`;
115
+ spanOpen = true;
116
+ }
117
+ };
118
+ for (const match of visibleText.matchAll(/\x1b\[([0-9;]*)m/g)) {
119
+ body += escapeHtml(visibleText.slice(offset, match.index));
120
+ offset = (match.index ?? 0) + match[0].length;
121
+ close();
122
+ const codes = (match[1] || "0").split(";").map(Number);
123
+ for (let index = 0; index < codes.length; index += 1) {
124
+ const code = codes[index];
125
+ if (code === 0) style = {};
126
+ else if (code === 1) style.bold = true;
127
+ else if (code === 2) style.dim = true;
128
+ else if (code === 3) style.italic = true;
129
+ else if (code === 4) style.underline = true;
130
+ else if (code === 5) style.blink = true;
131
+ else if (code === 7) style.inverse = true;
132
+ else if (code === 8) style.invisible = true;
133
+ else if (code === 9) style.strikethrough = true;
134
+ else if (code === 22) {
135
+ style.bold = false;
136
+ style.dim = false;
137
+ } else if (code === 23) style.italic = false;
138
+ else if (code === 24) style.underline = false;
139
+ else if (code === 25) style.blink = false;
140
+ else if (code === 27) style.inverse = false;
141
+ else if (code === 28) style.invisible = false;
142
+ else if (code === 29) style.strikethrough = false;
143
+ else if (code === 53) style.overline = true;
144
+ else if (code === 55) style.overline = false;
145
+ else if (code === 39) style.foreground = undefined;
146
+ else if (code === 49) style.background = undefined;
147
+ else if (code in ANSI_COLORS) style.foreground = ANSI_COLORS[code];
148
+ else if (code >= 40 && code <= 47) style.background = ANSI_COLORS[code - 10];
149
+ else if (code >= 100 && code <= 107) style.background = ANSI_COLORS[code - 10];
150
+ else if (code === 38 || code === 48) {
151
+ const mode = codes[index + 1];
152
+ if (mode === 2 && [codes[index + 2], codes[index + 3], codes[index + 4]].every(Number.isInteger)) {
153
+ const color = `rgb(${codes[index + 2]},${codes[index + 3]},${codes[index + 4]})`;
154
+ if (code === 38) style.foreground = color;
155
+ else style.background = color;
156
+ index += 4;
157
+ } else if (mode === 5 && Number.isInteger(codes[index + 2])) {
158
+ if (code === 38) style.foreground = xterm256Color(codes[index + 2]!);
159
+ else style.background = xterm256Color(codes[index + 2]!);
160
+ index += 2;
161
+ }
162
+ }
163
+ }
164
+ open();
165
+ }
166
+ body += escapeHtml(visibleText.slice(offset));
167
+ close();
168
+ return `<!doctype html><html lang="en"><meta charset="utf-8"><title>Sticky viewport showcase</title><style>body{margin:0;background:#110b0b;color:#ffe7dc}pre{white-space:pre;font-family:ui-monospace,monospace}@keyframes blink{50%{visibility:hidden}}</style><pre>${body}</pre></html>\n`;
169
+ }
170
+ const json = (value: unknown) => `${JSON.stringify(value, null, 2)}\n`;
171
+ const hash = (value: string) => new Bun.CryptoHasher("sha256").update(value).digest("hex");
172
+ function out(args: string[]): string {
173
+ if (args.length !== 2 || args[0] !== "--out" || !args[1]) throw new Error(`Usage: ${COMMAND}`);
174
+ return args[1];
175
+ }
176
+ async function capture(entry: StickyViewportShowcaseEntry, root: string) {
177
+ const rendered = await renderStickyViewportShowcase(entry);
178
+ if (!rendered.state.composer_visible)
179
+ throw new Error(`${entry.key}: focused composer was not visible in production frame`);
180
+ if (entry.stateId === "manual-new-output" && rendered.state.notice !== true)
181
+ throw new Error(`${entry.key}: manual output notice precondition failed`);
182
+ if (
183
+ JSON.stringify(rendered.cjkPhraseBoundaries) !==
184
+ JSON.stringify(entry.stateId === "narrow-cjk" ? CJK_PHRASE_BOUNDARIES : [])
185
+ )
186
+ throw new Error(`${entry.key}: CJK phrase boundary metadata precondition failed`);
187
+ const directory = path.join(root, ...entry.key.split("/"));
188
+ await fs.mkdir(directory, { recursive: true });
189
+ const metadata = json({
190
+ schema_version: 2,
191
+ entry_key: entry.key,
192
+ fixture_revision: REVISION,
193
+ capture_timestamp: TIMESTAMP,
194
+ command_or_replay_source: COMMAND,
195
+ fixture_source: "packages/coding-agent/test/fixtures/tui/sticky-viewport-showcase.ts",
196
+ terminal: {
197
+ ...entry.viewport,
198
+ font_rendering_assumptions: FONT_RENDERING_ASSUMPTIONS,
199
+ wrapping_truncation_policy: WRAPPING_TRUNCATION_POLICY,
200
+ },
201
+ render_mode: entry.renderMode,
202
+ ansi_mode: entry.renderMode === "unicode-color",
203
+ source_revision: rendered.sourceRevision,
204
+ output_revision: rendered.outputRevision,
205
+ state: rendered.state,
206
+ provenance: {
207
+ capture_mode: "production-tui-virtual-terminal",
208
+ live_pty: false,
209
+ network: false,
210
+ fixed_clock: true,
211
+ author_identity: "capture-sticky-viewport-showcase",
212
+ executor_identity: "capture-sticky-viewport-showcase",
213
+ },
214
+ cjk_phrase_boundaries: rendered.cjkPhraseBoundaries,
215
+ });
216
+ const contents = {
217
+ "terminal.txt": rendered.terminalText,
218
+ "terminal-ansi.txt": rendered.terminalAnsiText,
219
+ "terminal.html": ansiToHtml(rendered.terminalAnsiText),
220
+ "metadata.json": metadata,
221
+ };
222
+ const files = await Promise.all(
223
+ PAYLOADS.map(async name => {
224
+ const content = contents[name];
225
+ await Bun.write(path.join(directory, name), content);
226
+ return {
227
+ path: `${entry.key}/${name}`,
228
+ sha256: hash(content),
229
+ byte_length: Buffer.byteLength(content),
230
+ };
231
+ }),
232
+ );
233
+ return {
234
+ key: entry.key,
235
+ state_id: entry.stateId,
236
+ viewport: entry.viewport,
237
+ render_mode: entry.renderMode,
238
+ files,
239
+ };
240
+ }
241
+ async function main() {
242
+ const root = path.resolve(out(process.argv.slice(2)));
243
+ await fs.mkdir(root, { recursive: true });
244
+ const entries = [];
245
+ for (const entry of STICKY_VIEWPORT_SHOWCASE_ENTRIES) entries.push(await capture(entry, root));
246
+ const manifest = json({
247
+ schema_version: 2,
248
+ fixture_revision: REVISION,
249
+ command: COMMAND,
250
+ capture_timestamp: TIMESTAMP,
251
+ expected_entry_count: 20,
252
+ entry_count: 20,
253
+ ordered_keys: STICKY_VIEWPORT_SHOWCASE_KEYS,
254
+ provenance: {
255
+ capture_mode: "production-tui-virtual-terminal",
256
+ live_pty: false,
257
+ network: false,
258
+ fixed_clock: true,
259
+ author_identity: "capture-sticky-viewport-showcase",
260
+ executor_identity: "capture-sticky-viewport-showcase",
261
+ },
262
+ review_input_file: "review-input.json",
263
+ entries,
264
+ });
265
+ await Bun.write(path.join(root, "manifest.json"), manifest);
266
+ await Bun.write(
267
+ path.join(root, "review-input.json"),
268
+ json({
269
+ schema_version: 2,
270
+ manifest_sha256: hash(manifest),
271
+ command_or_replay_source: COMMAND,
272
+ capture_timestamp: TIMESTAMP,
273
+ fixture_source: "packages/coding-agent/test/fixtures/tui/sticky-viewport-showcase.ts",
274
+ fixed_clock: true,
275
+ live_pty: false,
276
+ network: false,
277
+ expected_keys: STICKY_VIEWPORT_SHOWCASE_KEYS,
278
+ author_identity: "capture-sticky-viewport-showcase",
279
+ executor_identity: "capture-sticky-viewport-showcase",
280
+ required_artifacts: PAYLOADS,
281
+ acceptance_version: ACCEPTANCE_VERSION,
282
+ design_version: DESIGN_VERSION,
283
+ host_matrix: HOST_MATRIX,
284
+ narrow_cjk: {
285
+ entry_key: "narrow-cjk/48x10/unicode-color",
286
+ phrase_boundaries: ["의미 있는 문장 경계", "意味のある文の境界", "保留语义短语边界"],
287
+ },
288
+ }),
289
+ );
290
+ process.stdout.write(`Captured 20 production TUI sticky viewport entries to ${root}\n`);
291
+ }
292
+ if (import.meta.main) await main();
@@ -119,6 +119,8 @@ const LOCKED_EXCLUSIONS: Readonly<Record<string, string>> = {
119
119
  "agent_session:continuePersistedHistory": "internal startup lifecycle plumbing, not a user-facing control seam",
120
120
  "agent_session:promoteRecoveryHydrationAfterOwnershipReadyFence":
121
121
  "internal owner-recovery authority transition after a durable writer fence, never a user-facing SDK operation",
122
+ "agent_session:restoreFromMemoryGuardCheckpoint":
123
+ "internal owner-recovery staged restore builder after durable claims/fencing, never a user-facing SDK operation",
122
124
  "agent_session:setActiveModelProfile": "internal accessor/plumbing, not a user-facing control seam",
123
125
  "agent_session:getActiveModelProfile": "internal accessor/plumbing, not a user-facing control seam",
124
126
  "agent_session:getSessionDefaultModelSelector": "internal accessor/plumbing, not a user-facing control seam",