@gajae-code/coding-agent 0.12.5 → 0.12.6

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 (127) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +4 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +10 -8
  5. package/dist/types/extensibility/extensions/runner.d.ts +9 -7
  6. package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
  7. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
  8. package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
  9. package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
  10. package/dist/types/gjc-runtime/team-store.d.ts +0 -4
  11. package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
  12. package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
  13. package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
  14. package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
  15. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
  16. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
  17. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
  18. package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
  19. package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
  20. package/dist/types/modes/components/plugin-settings.d.ts +2 -0
  21. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  22. package/dist/types/sdk/broker/broker.d.ts +18 -0
  23. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
  24. package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
  25. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -3
  26. package/dist/types/sdk/bus/index.d.ts +4 -0
  27. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -2
  28. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
  29. package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
  30. package/dist/types/sdk/host/host.d.ts +23 -2
  31. package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
  32. package/dist/types/session/agent-session.d.ts +5 -4
  33. package/dist/types/session/artifacts.d.ts +8 -5
  34. package/dist/types/session/attempt-record-store.d.ts +19 -0
  35. package/dist/types/session/client-bridge.d.ts +14 -0
  36. package/dist/types/session/internal/managed-session-storage.d.ts +5 -2
  37. package/dist/types/session/messages.d.ts +4 -1
  38. package/dist/types/session/session-manager.d.ts +6 -4
  39. package/dist/types/session/session-storage.d.ts +8 -0
  40. package/dist/types/session/streaming-output.d.ts +32 -0
  41. package/dist/types/task/receipt.d.ts +2 -0
  42. package/dist/types/task/types.d.ts +178 -0
  43. package/dist/types/tools/bash.d.ts +1 -1
  44. package/dist/types/tools/fetch.d.ts +3 -0
  45. package/dist/types/tools/output-meta.d.ts +37 -5
  46. package/dist/types/tools/tool-result.d.ts +1 -0
  47. package/dist/types/utils/artifact-id.d.ts +2 -0
  48. package/package.json +8 -8
  49. package/scripts/capture-sticky-viewport-showcase.ts +131 -5
  50. package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
  51. package/scripts/verify-sticky-viewport-showcase.ts +620 -93
  52. package/src/config/model-profiles.ts +8 -0
  53. package/src/config/settings-schema.ts +17 -3
  54. package/src/cursor.ts +20 -13
  55. package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
  56. package/src/eval/backend.ts +2 -2
  57. package/src/exec/bash-executor.ts +425 -151
  58. package/src/extensibility/extensions/runner.ts +80 -6
  59. package/src/extensibility/extensions/wrapper.ts +22 -15
  60. package/src/extensibility/runtime-skill-discovery.ts +4 -6
  61. package/src/gjc-runtime/launch-tmux.ts +309 -293
  62. package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
  63. package/src/gjc-runtime/psmux-detect.ts +52 -6
  64. package/src/gjc-runtime/team-launch.ts +50 -2
  65. package/src/gjc-runtime/team-runtime.ts +716 -251
  66. package/src/gjc-runtime/team-store.ts +25 -7
  67. package/src/gjc-runtime/team-workers.ts +161 -1
  68. package/src/gjc-runtime/tmux-common.ts +16 -4
  69. package/src/gjc-runtime/tmux-gc.ts +37 -5
  70. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
  71. package/src/gjc-runtime/tmux-provider-context.ts +451 -0
  72. package/src/gjc-runtime/tmux-sessions.ts +409 -156
  73. package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
  74. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
  75. package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
  76. package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
  77. package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
  78. package/src/gjc-runtime/workflow-manifest.ts +12 -0
  79. package/src/internal-urls/agent-protocol.ts +98 -38
  80. package/src/internal-urls/artifact-protocol.ts +28 -17
  81. package/src/internal-urls/docs-index.generated.ts +5 -5
  82. package/src/internal-urls/local-protocol.ts +39 -18
  83. package/src/internal-urls/registry-helpers.ts +62 -0
  84. package/src/modes/acp/acp-agent.ts +51 -13
  85. package/src/modes/acp/acp-client-bridge.ts +154 -11
  86. package/src/modes/components/gjc-bundle-settings.ts +2 -0
  87. package/src/modes/components/plugin-settings.ts +5 -0
  88. package/src/modes/components/settings-selector.ts +4 -0
  89. package/src/modes/controllers/selector-controller.ts +1 -0
  90. package/src/prompts/tools/bash.md +2 -2
  91. package/src/prompts/tools/monitor.md +4 -4
  92. package/src/sdk/acp/adapter.ts +200 -9
  93. package/src/sdk/broker/broker.ts +93 -10
  94. package/src/sdk/broker/lifecycle-ledger.ts +227 -11
  95. package/src/sdk/broker/lifecycle.ts +691 -92
  96. package/src/sdk/bus/chat-daemon-control.ts +32 -6
  97. package/src/sdk/bus/index.ts +347 -14
  98. package/src/sdk/bus/telegram-daemon-contract.ts +4 -2
  99. package/src/sdk/bus/telegram-daemon.ts +283 -44
  100. package/src/sdk/bus/topic-registry.ts +133 -10
  101. package/src/sdk/host/host.ts +187 -9
  102. package/src/sdk/host/reverse-leases.ts +249 -42
  103. package/src/sdk/session.ts +7 -6
  104. package/src/session/agent-session.ts +930 -354
  105. package/src/session/artifacts.ts +211 -74
  106. package/src/session/attempt-record-store.ts +106 -0
  107. package/src/session/client-bridge.ts +15 -0
  108. package/src/session/internal/managed-session-scope.ts +252 -67
  109. package/src/session/internal/managed-session-storage.ts +105 -5
  110. package/src/session/messages.ts +32 -7
  111. package/src/session/session-manager.ts +19 -21
  112. package/src/session/session-storage.ts +146 -24
  113. package/src/session/streaming-output.ts +198 -40
  114. package/src/task/index.ts +45 -0
  115. package/src/task/provider-retry-status.ts +4 -1
  116. package/src/task/receipt.ts +4 -0
  117. package/src/task/render.ts +30 -0
  118. package/src/task/types.ts +34 -0
  119. package/src/tools/bash.ts +818 -239
  120. package/src/tools/eval.ts +99 -4
  121. package/src/tools/fetch.ts +77 -15
  122. package/src/tools/find.ts +7 -3
  123. package/src/tools/monitor.ts +7 -1
  124. package/src/tools/output-meta.ts +706 -116
  125. package/src/tools/search.ts +9 -4
  126. package/src/tools/tool-result.ts +1 -0
  127. package/src/utils/artifact-id.ts +6 -0
@@ -2,6 +2,7 @@ import type { AgentToolUpdateCallback } from "@gajae-code/agent-core";
2
2
 
3
3
  import { sanitizeText } from "@gajae-code/utils";
4
4
  import { formatBytes } from "../tools/render-utils";
5
+ import { isValidArtifactId } from "../utils/artifact-id";
5
6
  import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
6
7
 
7
8
  function sanitizeOutputChunk(rawChunk: string): string {
@@ -49,6 +50,47 @@ export type TerminalArtifactPublisher = (
49
50
  info: { totalBytes: number; omittedBytes: number },
50
51
  ) => Promise<TerminalArtifactPublishResult>;
51
52
 
53
+ const TERMINAL_ARTIFACT_PUBLISH_WAIT_MS = 500;
54
+ const TERMINAL_ARTIFACT_PENDING_LIMIT = 64;
55
+ const pendingTerminalArtifactPublishesByOwner = new WeakMap<object, Set<Promise<TerminalArtifactPublishResult>>>();
56
+
57
+ function pendingTerminalArtifactPublishesForOwner(owner: object): Set<Promise<TerminalArtifactPublishResult>> {
58
+ let pending = pendingTerminalArtifactPublishesByOwner.get(owner);
59
+ if (!pending) {
60
+ pending = new Set();
61
+ pendingTerminalArtifactPublishesByOwner.set(owner, pending);
62
+ }
63
+ return pending;
64
+ }
65
+
66
+ function terminalPublisherOwner(publisher: TerminalArtifactPublisher): object {
67
+ return (publisher as TerminalArtifactPublisher & { owner?: object }).owner ?? publisher;
68
+ }
69
+
70
+ async function publishTerminalArtifactBounded(
71
+ publisher: TerminalArtifactPublisher,
72
+ content: string,
73
+ info: { totalBytes: number; omittedBytes: number },
74
+ ): Promise<TerminalArtifactPublishResult> {
75
+ const pendingTerminalArtifactPublishes = pendingTerminalArtifactPublishesForOwner(terminalPublisherOwner(publisher));
76
+ if (pendingTerminalArtifactPublishes.size >= TERMINAL_ARTIFACT_PENDING_LIMIT) {
77
+ return { status: "failed", diagnostic: "pending publisher limit reached for this session" };
78
+ }
79
+ const promise = Promise.resolve().then(() => publisher(content, info));
80
+ pendingTerminalArtifactPublishes.add(promise);
81
+ void promise.then(
82
+ () => pendingTerminalArtifactPublishes.delete(promise),
83
+ () => pendingTerminalArtifactPublishes.delete(promise),
84
+ );
85
+ return Promise.race([
86
+ promise,
87
+ Bun.sleep(TERMINAL_ARTIFACT_PUBLISH_WAIT_MS).then(() => ({
88
+ status: "failed" as const,
89
+ diagnostic: `did not settle within ${TERMINAL_ARTIFACT_PUBLISH_WAIT_MS}ms`,
90
+ })),
91
+ ]);
92
+ }
93
+
52
94
  // =============================================================================
53
95
  // Interfaces
54
96
  // =============================================================================
@@ -64,18 +106,60 @@ export interface OutputSummary {
64
106
  elidedBytes?: number;
65
107
  /** Lines elided from the middle when head-retain mode is active. */
66
108
  elidedLines?: number;
109
+ /** Exact source ranges retained by head/tail middle-elision windows. */
110
+ headRange?: { start: number; end: number };
111
+ tailRange?: { start: number; end: number };
112
+ /** The retained tail ends with only a byte fragment of its source line. */
113
+ lastLinePartial?: boolean;
114
+ /** The retained head ends with only a byte fragment of its source line. */
115
+ firstLinePartial?: boolean;
67
116
  /** Bytes dropped by the per-line column cap (sum across all lines). */
68
117
  columnDroppedBytes?: number;
69
118
  /** Number of distinct lines that hit the per-line column cap. */
70
119
  columnTruncatedLines?: number;
120
+ /** Configured per-line byte/column cap that caused visible omission. */
121
+ columnMax?: number;
71
122
  /** Artifact ID for internal URL access (artifact://<id>) when output was persisted. */
72
123
  artifactId?: string;
124
+ /** Artifact existence was proven by successful publication or file finalization. */
125
+ artifactVerified?: boolean;
73
126
  /** Bytes omitted from artifact storage after the artifact hard cap was reached. */
74
127
  artifactTruncatedBytes?: number;
128
+ /** Bytes dropped before the Bash executor received the native output stream. */
129
+ sourceTruncatedBytes?: number;
130
+ /** Exact source capture completeness could not be proven. */
131
+ sourceCaptureIncomplete?: boolean;
75
132
  /** Bounded diagnostic when artifact writer or terminal publisher creation, write, finalization, or publication failed. */
76
133
  artifactFailureDiagnostic?: string;
77
134
  }
78
135
 
136
+ const kCurrentExecutionArtifactProof = Symbol("OutputSink.CurrentExecutionArtifactProof");
137
+ interface CurrentExecutionArtifactProof {
138
+ artifactId: string;
139
+ payload: string;
140
+ consumed: boolean;
141
+ }
142
+
143
+ /** Verify that this exact OutputSink summary came from a successful current-execution publication. */
144
+ export function hasCurrentExecutionArtifactProof(
145
+ summary: OutputSummary,
146
+ artifactId: string,
147
+ payload?: string,
148
+ ): boolean {
149
+ const proof = (summary as OutputSummary & { [kCurrentExecutionArtifactProof]?: CurrentExecutionArtifactProof })[
150
+ kCurrentExecutionArtifactProof
151
+ ];
152
+ if (
153
+ !proof ||
154
+ proof.artifactId !== artifactId ||
155
+ proof.consumed ||
156
+ (payload !== undefined && proof.payload !== payload)
157
+ )
158
+ return false;
159
+ proof.consumed = true;
160
+ return true;
161
+ }
162
+
79
163
  export interface OutputSinkOptions {
80
164
  /**
81
165
  * Deprecated managed artifact pathname. Bare paths are deliberately ignored:
@@ -141,7 +225,10 @@ export interface TruncationResult {
141
225
  elidedBytes?: number;
142
226
  /** Lines elided from the middle (truncateMiddle only). */
143
227
  elidedLines?: number;
228
+ headRange?: { start: number; end: number };
229
+ tailRange?: { start: number; end: number };
144
230
  lastLinePartial?: boolean;
231
+ firstLinePartial?: boolean;
145
232
  firstLineExceedsLimit?: boolean;
146
233
  lastLineExceedsLimit?: boolean;
147
234
  }
@@ -793,7 +880,7 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
793
880
  const head = windows.head;
794
881
  const tail = windows.tail;
795
882
  if (!head || !tail) return noTruncResult(content, windows.totalLines, windows.totalBytes);
796
- if (head.lines + tail.lines >= windows.totalLines)
883
+ if (head.lines + tail.lines >= windows.totalLines && tail.kind !== "partial-line")
797
884
  return noTruncResult(content, windows.totalLines, windows.totalBytes);
798
885
  const legacyElidedLines = Math.max(0, windows.totalLines - head.lines - tail.lines);
799
886
  const legacyElidedBytes =
@@ -802,7 +889,7 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
802
889
  : windows.elidedBytes;
803
890
  const marker = formatMiddleElisionMarker(legacyElidedLines, legacyElidedBytes);
804
891
  const markerBytes = Buffer.byteLength(marker, "utf-8");
805
- return {
892
+ const result: TruncationResult = {
806
893
  content: `${head.content}\n${marker}\n${tail.content}`,
807
894
  truncated: true,
808
895
  truncatedBy: "middle",
@@ -816,6 +903,11 @@ export function truncateMiddle(content: string, options: TruncationOptions = {})
816
903
  firstLineExceedsLimit: false,
817
904
  ...(tail.lastLinePartial ? { lastLineExceedsLimit: true } : {}),
818
905
  };
906
+ Object.defineProperties(result, {
907
+ headRange: { value: { start: head.origin.startLine, end: head.origin.endLine } },
908
+ tailRange: { value: { start: tail.origin.startLine, end: tail.origin.endLine } },
909
+ });
910
+ return result;
819
911
  }
820
912
  }
821
913
  }
@@ -932,6 +1024,8 @@ export class OutputSink {
932
1024
  #head = "";
933
1025
  #headBytes = 0;
934
1026
  #headRetentionDisabled = false;
1027
+ #headEndsMidLine = false;
1028
+ #tailStartsMidLine = false;
935
1029
  #totalLines = 0; // newline count
936
1030
  #totalBytes = 0;
937
1031
  #processedBytes = 0;
@@ -967,6 +1061,8 @@ export class OutputSink {
967
1061
  #pendingArtifactContent?: string[];
968
1062
  #artifactPublishAttempted = false;
969
1063
  #finalized = false;
1064
+ #finalizing = false;
1065
+ #artifactFinalization?: Promise<string | undefined>;
970
1066
 
971
1067
  #fileReady = false;
972
1068
 
@@ -1020,9 +1116,10 @@ export class OutputSink {
1020
1116
  return this.#buffer;
1021
1117
  }
1022
1118
 
1023
- #setTail(text: string, bytes = Buffer.byteLength(text, "utf-8")): void {
1119
+ #setTail(text: string, bytes = Buffer.byteLength(text, "utf-8"), startsMidLine = false): void {
1024
1120
  this.#buffer = text;
1025
1121
  this.#bufferBytes = bytes;
1122
+ this.#tailStartsMidLine = startsMidLine;
1026
1123
  }
1027
1124
 
1028
1125
  #appendTail(text: string, bytes: number): void {
@@ -1037,9 +1134,12 @@ export class OutputSink {
1037
1134
 
1038
1135
  #trimTailTo(maxBytes: number): void {
1039
1136
  if (this.#bufferBytes <= maxBytes) return;
1040
- const { text, bytes } = truncateTailBytes(this.#buffer, maxBytes);
1137
+ const previous = this.#buffer;
1138
+ const { text, bytes } = truncateTailBytes(previous, maxBytes);
1139
+ const removed = previous.slice(0, previous.length - text.length);
1041
1140
  this.#buffer = text;
1042
1141
  this.#bufferBytes = bytes;
1142
+ this.#tailStartsMidLine = removed.length > 0 && !removed.endsWith("\n");
1043
1143
  }
1044
1144
 
1045
1145
  /**
@@ -1050,6 +1150,7 @@ export class OutputSink {
1050
1150
  // F21: with coalescing enabled, accumulate raw chunks and process them in batches; the default
1051
1151
  // (disabled) path calls #ingest directly and is byte-identical to the historical per-chunk path.
1052
1152
  push(chunk: string): void {
1153
+ if (this.#finalized || this.#finalizing) return;
1053
1154
  if (!this.#coalesceSanitize) {
1054
1155
  this.#ingest(chunk);
1055
1156
  return;
@@ -1132,6 +1233,10 @@ export class OutputSink {
1132
1233
  }
1133
1234
  }
1134
1235
 
1236
+ if (this.#headBytes > 0 && this.#bufferBytes === 0 && tailBytes > 0) {
1237
+ this.#headEndsMidLine = !this.#head.endsWith("\n") && !tailChunk.startsWith("\n");
1238
+ this.#tailStartsMidLine = this.#headEndsMidLine;
1239
+ }
1135
1240
  this.#pushTail(tailChunk, tailBytes);
1136
1241
  }
1137
1242
 
@@ -1216,7 +1321,8 @@ export class OutputSink {
1216
1321
  // Avoid creating a giant intermediate string when chunk alone dominates.
1217
1322
  if (dataBytes >= threshold) {
1218
1323
  const { text, bytes } = truncateTailBytes(chunk, threshold);
1219
- this.#setTail(text, bytes);
1324
+ const removed = chunk.slice(0, chunk.length - text.length);
1325
+ this.#setTail(text, bytes, removed.length > 0 && !removed.endsWith("\n"));
1220
1326
  } else {
1221
1327
  // Intermediate size is bounded (<= threshold + dataBytes), safe to concat.
1222
1328
  this.#appendTail(chunk, dataBytes);
@@ -1390,13 +1496,19 @@ export class OutputSink {
1390
1496
  return this.#publishedArtifactId;
1391
1497
  }
1392
1498
  this.#artifactPublishAttempted = true;
1499
+ this.#finalizing = true;
1393
1500
  const content = this.#pendingArtifactContent?.join("") ?? "";
1394
1501
  const omittedBytes = this.#artifactTruncatedBytes;
1395
1502
  try {
1396
- const published = await this.#artifactPublisher!(content, {
1503
+ const published = await publishTerminalArtifactBounded(this.#artifactPublisher!, content, {
1397
1504
  totalBytes: this.#totalBytes,
1398
1505
  omittedBytes,
1399
1506
  });
1507
+ if (typeof published !== "object" || published === null || !("status" in published)) {
1508
+ this.#recordTerminalArtifactDiagnostic("failed", "publisher returned an invalid result");
1509
+ this.#artifactFinalizationFailed = true;
1510
+ return undefined;
1511
+ }
1400
1512
  if (published.status === "unavailable") {
1401
1513
  this.#recordTerminalArtifactDiagnostic("unavailable");
1402
1514
  this.#artifactFinalizationFailed = true;
@@ -1407,8 +1519,13 @@ export class OutputSink {
1407
1519
  this.#artifactFinalizationFailed = true;
1408
1520
  return undefined;
1409
1521
  }
1410
- if (published.artifactId.length === 0) {
1411
- this.#recordTerminalArtifactDiagnostic("failed", "storage returned no artifact id");
1522
+ if (published.status !== "published") {
1523
+ this.#recordTerminalArtifactDiagnostic("failed", "publisher returned an invalid result");
1524
+ this.#artifactFinalizationFailed = true;
1525
+ return undefined;
1526
+ }
1527
+ if (!isValidArtifactId(published.artifactId)) {
1528
+ this.#recordTerminalArtifactDiagnostic("failed", "storage returned an invalid artifact id");
1412
1529
  this.#artifactFinalizationFailed = true;
1413
1530
  return undefined;
1414
1531
  }
@@ -1432,6 +1549,48 @@ export class OutputSink {
1432
1549
  }
1433
1550
  }
1434
1551
 
1552
+ #shouldFinalizeArtifact(): boolean {
1553
+ return (
1554
+ this.#finalized ||
1555
+ this.#artifactFinalizationFailed ||
1556
+ this.#shouldPublishTerminalArtifact() ||
1557
+ this.#file !== undefined ||
1558
+ (this.#artifactPath !== undefined && this.#artifactTruncatedBytes > 0)
1559
+ );
1560
+ }
1561
+
1562
+ async #finalizeArtifact(): Promise<string | undefined> {
1563
+ let artifactId: string | undefined;
1564
+ if (this.#finalized) artifactId = this.#publishedArtifactId ?? this.#artifactId;
1565
+ else if (!this.#artifactFinalizationFailed) {
1566
+ if (this.#artifactPublisher && !this.#artifactPath) artifactId = await this.#publishTerminalArtifact();
1567
+ if (!this.#artifactFinalizationFailed && this.#artifactPath) {
1568
+ if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1569
+ if (this.#file) {
1570
+ this.#finalizing = true;
1571
+ await this.#writeArtifactTruncationNotice();
1572
+ try {
1573
+ await this.#file.sink.end();
1574
+ artifactId = this.#file.artifactId;
1575
+ this.#finalized = true;
1576
+ } catch (error) {
1577
+ this.#recordArtifactFailure("end", error);
1578
+ this.#artifactFinalizationFailed = true;
1579
+ this.#file = undefined;
1580
+ this.#fileReady = false;
1581
+ }
1582
+ }
1583
+ }
1584
+ }
1585
+ if (this.#finalized) {
1586
+ this.#pendingFileWrites = undefined;
1587
+ this.#pendingFileWriteBytes = 0;
1588
+ this.#fileReady = false;
1589
+ this.#pendingArtifactContent = undefined;
1590
+ }
1591
+ return artifactId;
1592
+ }
1593
+
1435
1594
  createInput(): WritableStream<Uint8Array | string> {
1436
1595
  const dec = new TextDecoder("utf-8", { ignoreBOM: true });
1437
1596
  const finalize = () => {
@@ -1457,17 +1616,20 @@ export class OutputSink {
1457
1616
  * append directly to the tail without reordering the replacement.
1458
1617
  */
1459
1618
  replace(text: string): void {
1619
+ if (this.#finalized || this.#finalizing) return;
1460
1620
  this.#coalesceBuf = "";
1461
1621
  const replacementBytes = Buffer.byteLength(text, "utf-8");
1462
1622
  this.#head = "";
1463
1623
  this.#headBytes = 0;
1464
1624
  this.#setTail("");
1625
+ this.#headEndsMidLine = false;
1465
1626
 
1466
1627
  if (this.#headLimit > 0) {
1467
1628
  const head = truncateHeadBytes(text, this.#headLimit);
1468
1629
  this.#appendHead(head.text, head.bytes);
1469
1630
  const tailText = text.substring(head.text.length);
1470
- this.#setTail(tailText, replacementBytes - head.bytes);
1631
+ this.#headEndsMidLine = tailText.length > 0 && !head.text.endsWith("\n") && !tailText.startsWith("\n");
1632
+ this.#setTail(tailText, replacementBytes - head.bytes, this.#headEndsMidLine);
1471
1633
  } else {
1472
1634
  this.#setTail(text, replacementBytes);
1473
1635
  }
@@ -1491,35 +1653,9 @@ export class OutputSink {
1491
1653
  const totalLines = this.#sawData ? this.#totalLines + 1 : 0;
1492
1654
 
1493
1655
  let artifactId: string | undefined;
1494
- if (this.#finalized) {
1495
- artifactId = this.#publishedArtifactId ?? this.#artifactId;
1496
- } else if (!this.#artifactFinalizationFailed) {
1497
- if (this.#artifactPublisher && !this.#artifactPath) {
1498
- artifactId = await this.#publishTerminalArtifact();
1499
- }
1500
- if (!this.#artifactFinalizationFailed && this.#artifactPath) {
1501
- if (this.#artifactTruncatedBytes > 0) this.#createFileSink();
1502
- await this.#writeArtifactTruncationNotice();
1503
- if (this.#file) {
1504
- try {
1505
- await this.#file.sink.end();
1506
- artifactId = this.#file.artifactId;
1507
- this.#finalized = true;
1508
- } catch (error) {
1509
- this.#recordArtifactFailure("end", error);
1510
- this.#artifactFinalizationFailed = true;
1511
- this.#file = undefined;
1512
- this.#fileReady = false;
1513
- }
1514
- }
1515
- }
1516
- }
1517
- if (this.#finalized) {
1518
- // Terminal: the artifact is closed; replay state is no longer needed.
1519
- this.#pendingFileWrites = undefined;
1520
- this.#pendingFileWriteBytes = 0;
1521
- this.#fileReady = false;
1522
- this.#pendingArtifactContent = undefined;
1656
+ if (this.#shouldFinalizeArtifact()) {
1657
+ this.#artifactFinalization ??= this.#finalizeArtifact();
1658
+ artifactId = await this.#artifactFinalization;
1523
1659
  }
1524
1660
  // Non-finalized dumps (no artifact sink ever opened) keep the raw replay
1525
1661
  // queue so a later post-dump push that spills produces a CUMULATIVE
@@ -1543,6 +1679,8 @@ export class OutputSink {
1543
1679
  let outputLines: number;
1544
1680
  let elidedBytes: number | undefined;
1545
1681
  let elidedLines: number | undefined;
1682
+ let headRange: { start: number; end: number } | undefined;
1683
+ let tailRange: { start: number; end: number } | undefined;
1546
1684
 
1547
1685
  if (headBytes > 0 && effectiveTotalBytes > headBytes + tailBytes) {
1548
1686
  // Middle was elided. Emit both explicit windows, including when they are
@@ -1555,7 +1693,12 @@ export class OutputSink {
1555
1693
  const tailSep = tailBuf.startsWith("\n") ? "" : "\n";
1556
1694
  body = `${headText}${headSep}${marker}${tailSep}${tailBuf}`;
1557
1695
  outputBytes = headBytes + markerBytes + tailBytes + headSep.length + tailSep.length;
1558
- outputLines = headLines + 1 + tailLines;
1696
+ outputLines = body.length > 0 ? countNewlines(body) + 1 : 0;
1697
+ headRange = headLines > 0 && !this.#headEndsMidLine ? { start: 1, end: headLines } : undefined;
1698
+ tailRange =
1699
+ tailLines > 0 && !this.#tailStartsMidLine
1700
+ ? { start: processedTotalLines - tailLines + 1, end: processedTotalLines }
1701
+ : undefined;
1559
1702
  this.#truncated = true;
1560
1703
  } else if (headBytes > 0) {
1561
1704
  // Head + tail combine into the full buffered output (no overlap or elision).
@@ -1569,7 +1712,7 @@ export class OutputSink {
1569
1712
  }
1570
1713
 
1571
1714
  if (this.#columnDroppedBytes > 0) this.#truncated = true;
1572
- return {
1715
+ const summary: OutputSummary = {
1573
1716
  output: `${noticeLine}${body}`,
1574
1717
  truncated: this.#truncated,
1575
1718
  totalLines,
@@ -1578,12 +1721,27 @@ export class OutputSink {
1578
1721
  outputBytes,
1579
1722
  elidedBytes,
1580
1723
  elidedLines,
1724
+ headRange,
1725
+ tailRange,
1726
+ firstLinePartial: this.#headEndsMidLine || undefined,
1727
+ lastLinePartial: this.#tailStartsMidLine || undefined,
1581
1728
  columnDroppedBytes: this.#columnDroppedBytes > 0 ? this.#columnDroppedBytes : undefined,
1582
1729
  columnTruncatedLines: this.#columnTruncatedLines > 0 ? this.#columnTruncatedLines : undefined,
1730
+ columnMax: this.#columnDroppedBytes > 0 ? this.#maxColumns : undefined,
1583
1731
  artifactTruncatedBytes: this.#artifactTruncatedBytes > 0 ? this.#artifactTruncatedBytes : undefined,
1584
1732
  artifactFailureDiagnostic: this.#artifactFailureDiagnostic,
1585
1733
  artifactId,
1734
+ artifactVerified: artifactId !== undefined && this.#publishedArtifactId === artifactId,
1586
1735
  };
1736
+ if (summary.artifactVerified === true && artifactId !== undefined) {
1737
+ Object.defineProperty(summary, kCurrentExecutionArtifactProof, {
1738
+ value: { artifactId, payload: summary.output, consumed: false } satisfies CurrentExecutionArtifactProof,
1739
+ enumerable: true,
1740
+ configurable: false,
1741
+ writable: false,
1742
+ });
1743
+ }
1744
+ return summary;
1587
1745
  }
1588
1746
  }
1589
1747
 
package/src/task/index.ts CHANGED
@@ -53,6 +53,9 @@ import {
53
53
  RepositoryBindingError,
54
54
  resolveTaskRepositoryBinding,
55
55
  } from "../gjc-runtime/repository-binding";
56
+ import { reconcileReviewSourceDelivery } from "../gjc-runtime/ultragoal-review-source";
57
+ import { classifyUltragoalReviewDelivery, validateUltragoalReviewDispatch } from "../gjc-runtime/ultragoal-runtime";
58
+
56
59
  import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
57
60
  import { ArtifactManager } from "../session/artifacts";
58
61
  import { generateCommitMessage } from "../utils/commit-message-generator";
@@ -1702,6 +1705,27 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1702
1705
  if (taskRepositoryBinding.relativeSubdir) {
1703
1706
  assertPathUnderRepositoryBinding(taskRepositoryBinding, ".");
1704
1707
  }
1708
+ if (task.reviewSource) {
1709
+ await validateUltragoalReviewDispatch({
1710
+ cwd: this.session.cwd,
1711
+ dispatchId: task.reviewSource.dispatchId,
1712
+ cohortId: task.reviewSource.cohortId,
1713
+ taskId: task.id,
1714
+ lane: task.reviewSource.lane,
1715
+ snapshotId: task.reviewSource.snapshotId,
1716
+ generation: task.reviewSource.generation,
1717
+ repositoryBindingDigest: task.reviewSource.repositoryBindingDigest,
1718
+ stateRevision: task.reviewSource.stateRevision,
1719
+ rerunCommand: task.reviewSource.rerunCommand,
1720
+ taskSourceTaskId: task.reviewSource.taskId,
1721
+ createdAt: task.reviewSource.createdAt,
1722
+ });
1723
+ }
1724
+ if (isIsolated && task.reviewSource) {
1725
+ throw new Error(
1726
+ "source-aware review tasks cannot use generic task isolation; dispatch them against the coordinator-owned source snapshot",
1727
+ );
1728
+ }
1705
1729
  if (!isIsolated) {
1706
1730
  await assertExecutionRootMatchesRepositoryBinding(this.session.cwd, taskRepositoryBinding);
1707
1731
  const result = await runSubprocess({
@@ -1756,11 +1780,32 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1756
1780
  parentTelemetry: this.session.getTelemetry?.(),
1757
1781
  forkContextSeed,
1758
1782
  });
1783
+ const reviewSourceDisposition = task.reviewSource
1784
+ ? await reconcileReviewSourceDelivery({
1785
+ cwd: this.session.cwd,
1786
+ repositoryBinding: taskRepositoryBinding,
1787
+ reviewSource: task.reviewSource,
1788
+ })
1789
+ : undefined;
1790
+ const reviewSource =
1791
+ task.reviewSource && reviewSourceDisposition
1792
+ ? {
1793
+ ...task.reviewSource,
1794
+ ...reviewSourceDisposition,
1795
+ ...(await classifyUltragoalReviewDelivery({
1796
+ cwd: this.session.cwd,
1797
+ cohortId: task.reviewSource.cohortId,
1798
+ dispatchId: task.reviewSource.dispatchId,
1799
+ observedDisposition: reviewSourceDisposition.disposition,
1800
+ })),
1801
+ }
1802
+ : undefined;
1759
1803
  return {
1760
1804
  ...result,
1761
1805
  ...(forkContext ? { forkContext } : {}),
1762
1806
  forkContextAdvisory,
1763
1807
  repositoryBinding: publicRepositoryBinding(taskRepositoryBinding),
1808
+ ...(reviewSource ? { reviewSource } : {}),
1764
1809
  };
1765
1810
  }
1766
1811
 
@@ -3,6 +3,7 @@ import type { AgentProgress, TaskToolDetails } from "./types";
3
3
  export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"];
4
4
 
5
5
  const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
6
+ const FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_MESSAGE = "Provider stream timed out while waiting for first event";
6
7
  const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
7
8
  const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
8
9
 
@@ -22,7 +23,9 @@ export function classifyProviderRetryFromTransport(facts: {
22
23
  return "first_event_timeout";
23
24
  }
24
25
  const errorMessage = facts.errorMessage ?? "";
25
- if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
26
+ if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage) || errorMessage === FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_MESSAGE) {
27
+ return "first_event_timeout";
28
+ }
26
29
  if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
27
30
  return "provider_error";
28
31
  }
@@ -67,6 +67,9 @@ export interface TaskResultReceipt {
67
67
  forkContextAdvisory?: SingleResult["forkContextAdvisory"];
68
68
  /** Resolved repository identity for this delegated lane (#2901). */
69
69
  repositoryBinding?: SingleResult["repositoryBinding"];
70
+ /** Runtime-derived review source disposition and rerun guidance (#3469). */
71
+ reviewSource?: SingleResult["reviewSource"];
72
+
70
73
  roi?: TaskRoi;
71
74
  }
72
75
 
@@ -311,6 +314,7 @@ export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
311
314
  forkContext: raw.forkContext,
312
315
  forkContextAdvisory: raw.forkContextAdvisory,
313
316
  repositoryBinding: raw.repositoryBinding,
317
+ reviewSource: raw.reviewSource,
314
318
  roi: buildTaskRoi(raw),
315
319
  };
316
320
  }
@@ -916,6 +916,36 @@ function renderAgentResult(result: TaskResultReceipt, isLast: boolean, expanded:
916
916
  if (result.roi?.lowRoi) {
917
917
  lines.push(`${continuePrefix}${theme.fg("warning", "low ROI: produced no material contribution")}`);
918
918
  }
919
+ if (result.reviewSource?.disposition === "stale_review_delivery") {
920
+ lines.push(
921
+ `${continuePrefix}${theme.fg(
922
+ "warning",
923
+ truncateToWidth(
924
+ replaceTabs(
925
+ sanitizeText(
926
+ `STALE REVIEW: source advanced after dispatch; excluded from review gates. Rerun: ${result.reviewSource.rerunCommand}`,
927
+ ),
928
+ ),
929
+
930
+ 100,
931
+ ),
932
+ )}`,
933
+ );
934
+ } else if (result.reviewSource?.disposition === "invalid_provenance") {
935
+ lines.push(
936
+ `${continuePrefix}${theme.fg(
937
+ "error",
938
+ truncateToWidth(
939
+ replaceTabs(
940
+ sanitizeText(
941
+ `INVALID REVIEW PROVENANCE: receipt cannot satisfy review gates. Rerun: ${result.reviewSource.rerunCommand}`,
942
+ ),
943
+ ),
944
+ 100,
945
+ ),
946
+ )}`,
947
+ );
948
+ }
919
949
 
920
950
  if (result.persistence?.recoveryRef) {
921
951
  const label = result.persistence.ownerWorktreeApplied ? "Recovery patch" : "Unapplied recovery patch";
package/src/task/types.ts CHANGED
@@ -2,6 +2,7 @@ import type { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Usage } from "@gajae-code/ai";
3
3
  import { $env } from "@gajae-code/utils";
4
4
  import * as z from "zod/v4";
5
+ import type { ReviewDeliveryDisposition, ReviewSourceLane } from "../gjc-runtime/ultragoal-review-source";
5
6
  import { isValidTaskId, TASK_ID_DESCRIPTION } from "./id";
6
7
  import type { TaskResultReceipt } from "./receipt";
7
8
  import type { SpawnRoiReconciliation } from "./roi-reconciliation";
@@ -114,8 +115,26 @@ const createTaskItemSchema = (_contextEnabled: boolean) =>
114
115
  .describe(
115
116
  "authoritative repository identity; omitted items are stamped from session cwd before discovery/spawn and still fail closed on sibling drift",
116
117
  ),
118
+ reviewSource: reviewSourceSchema.optional(),
117
119
  });
118
120
 
121
+ const reviewSourceSchema = z
122
+ .object({
123
+ schema: z.literal("gjc.review_source_dispatch.v1"),
124
+ cohortId: z.string().min(1),
125
+ generation: z.number().int().min(1),
126
+ lane: z.enum(["cleaner", "architect", "qa", "critic"]),
127
+ snapshotId: z.string().startsWith("sha256:"),
128
+ repositoryBindingDigest: z.string().startsWith("sha256:"),
129
+ stateRevision: z.number().int().min(0),
130
+ dispatchId: z.string().min(1),
131
+ rerunCommand: z.string().min(1),
132
+ taskId: z.string().min(1),
133
+ createdAt: z.string().min(1),
134
+ })
135
+ .strict()
136
+ .describe("leader-issued immutable review source identity; review lanes only");
137
+
119
138
  /** Single task item for parallel execution (default shape with context enabled). */
120
139
  export const taskItemSchema = createTaskItemSchema(true);
121
140
  export type TaskItem = z.infer<typeof taskItemSchema>;
@@ -524,6 +543,21 @@ export interface SingleResult {
524
543
  head?: string;
525
544
  branch?: string;
526
545
  };
546
+ /** Leader-issued immutable source identity for source-aware review lanes (#3469). */
547
+ reviewSource?: {
548
+ schema: "gjc.review_source_dispatch.v1";
549
+ cohortId: string;
550
+ generation: number;
551
+ lane: ReviewSourceLane;
552
+ snapshotId: string;
553
+ repositoryBindingDigest: string;
554
+ stateRevision: number;
555
+ dispatchId: string;
556
+ rerunCommand: string;
557
+ disposition: ReviewDeliveryDisposition;
558
+ currentSnapshotId?: string;
559
+ deliveryId?: string;
560
+ };
527
561
  }
528
562
 
529
563
  /** True only for complete, factual five-bucket cost accounting. */