@gajae-code/coding-agent 0.4.4 → 0.5.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.
- package/CHANGELOG.md +83 -0
- package/dist/types/cli/fast-help.d.ts +1 -0
- package/dist/types/cli/setup-cli.d.ts +2 -0
- package/dist/types/commands/harness.d.ts +6 -0
- package/dist/types/commands/setup.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +11 -2
- package/dist/types/config/model-profiles.d.ts +7 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +35 -0
- package/dist/types/config/settings-schema.d.ts +4 -3
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +8 -2
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +3 -0
- package/dist/types/harness-control-plane/finalize.d.ts +5 -0
- package/dist/types/harness-control-plane/owner.d.ts +1 -1
- package/dist/types/harness-control-plane/phase-rollup.d.ts +23 -0
- package/dist/types/harness-control-plane/receipt-ingest.d.ts +19 -0
- package/dist/types/harness-control-plane/receipt-spool.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +46 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +6 -1
- package/dist/types/harness-control-plane/types.d.ts +13 -1
- package/dist/types/hindsight/mental-models.d.ts +5 -5
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/model-selector.d.ts +1 -12
- package/dist/types/modes/rpc/rpc-client.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-types.d.ts +4 -1
- package/dist/types/modes/utils/abort-message.d.ts +4 -0
- package/dist/types/sdk.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/blob-store.d.ts +20 -1
- package/dist/types/session/session-manager.d.ts +32 -6
- package/dist/types/session/streaming-output.d.ts +3 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -0
- package/dist/types/setup/hermes-setup.d.ts +7 -0
- package/dist/types/task/fork-context-advisory.d.ts +13 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/roi-reconciliation.d.ts +27 -0
- package/dist/types/task/types.d.ts +17 -0
- package/dist/types/thinking-metadata.d.ts +16 -0
- package/dist/types/thinking.d.ts +3 -12
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resolve.d.ts +0 -10
- package/dist/types/utils/tool-choice.d.ts +14 -1
- package/package.json +8 -7
- package/scripts/build-binary.ts +4 -0
- package/src/cli/fast-help.ts +80 -0
- package/src/cli/setup-cli.ts +12 -3
- package/src/cli.ts +112 -17
- package/src/commands/coordinator.ts +44 -1
- package/src/commands/harness.ts +128 -11
- package/src/commands/launch.ts +2 -2
- package/src/commands/mcp-serve.ts +3 -2
- package/src/commands/session.ts +3 -1
- package/src/commands/setup.ts +4 -0
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +255 -56
- package/src/config/model-resolver.ts +9 -6
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -3
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +427 -193
- package/src/cursor.ts +46 -4
- package/src/defaults/gjc/skills/team/SKILL.md +3 -2
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +8 -2
- package/src/export/html/index.ts +13 -9
- package/src/gjc-runtime/launch-worktree.ts +12 -1
- package/src/gjc-runtime/session-state-sidecar.ts +38 -0
- package/src/gjc-runtime/team-runtime.ts +33 -7
- package/src/gjc-runtime/tmux-common.ts +15 -0
- package/src/gjc-runtime/tmux-sessions.ts +19 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +505 -41
- package/src/gjc-runtime/workflow-manifest.generated.json +27 -1
- package/src/gjc-runtime/workflow-manifest.ts +16 -1
- package/src/harness-control-plane/finalize.ts +39 -5
- package/src/harness-control-plane/owner.ts +87 -28
- package/src/harness-control-plane/phase-rollup.ts +96 -0
- package/src/harness-control-plane/receipt-ingest.ts +127 -0
- package/src/harness-control-plane/receipt-spool.ts +128 -0
- package/src/harness-control-plane/receipts.ts +229 -1
- package/src/harness-control-plane/rpc-adapter.ts +8 -0
- package/src/harness-control-plane/state-machine.ts +27 -6
- package/src/harness-control-plane/storage.ts +23 -0
- package/src/harness-control-plane/types.ts +33 -1
- package/src/hindsight/mental-models.ts +17 -16
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/main.ts +7 -3
- package/src/modes/components/assistant-message.ts +26 -14
- package/src/modes/components/diff.ts +97 -0
- package/src/modes/components/model-selector.ts +353 -181
- package/src/modes/components/status-line.ts +6 -6
- package/src/modes/components/tool-execution.ts +30 -13
- package/src/modes/controllers/event-controller.ts +5 -4
- package/src/modes/controllers/selector-controller.ts +33 -42
- package/src/modes/interactive-mode.ts +4 -5
- package/src/modes/print-mode.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +3 -2
- package/src/modes/rpc/rpc-mode.ts +44 -14
- package/src/modes/rpc/rpc-types.ts +5 -2
- package/src/modes/shared/agent-wire/command-dispatch.ts +10 -5
- package/src/modes/shared/agent-wire/command-validation.ts +11 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/utils/abort-message.ts +41 -0
- package/src/modes/utils/context-usage.ts +15 -8
- package/src/modes/utils/ui-helpers.ts +5 -6
- package/src/sdk.ts +38 -6
- package/src/secrets/obfuscator.ts +102 -27
- package/src/session/agent-session.ts +121 -25
- package/src/session/blob-store.ts +89 -3
- package/src/session/session-manager.ts +328 -57
- package/src/session/streaming-output.ts +185 -122
- package/src/session/tool-choice-queue.ts +23 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +3 -2
- package/src/setup/hermes-setup.ts +63 -8
- package/src/task/executor.ts +69 -6
- package/src/task/fork-context-advisory.ts +99 -0
- package/src/task/index.ts +31 -2
- package/src/task/receipt.ts +7 -0
- package/src/task/render.ts +21 -1
- package/src/task/roi-reconciliation.ts +90 -0
- package/src/task/types.ts +15 -0
- package/src/thinking-metadata.ts +51 -0
- package/src/thinking.ts +26 -46
- package/src/tools/bash.ts +1 -1
- package/src/tools/index.ts +4 -2
- package/src/tools/resolve.ts +93 -18
- package/src/tools/subagent-render.ts +10 -1
- package/src/utils/edit-mode.ts +1 -1
- package/src/utils/title-generator.ts +16 -2
- package/src/utils/tool-choice.ts +45 -16
|
@@ -19,11 +19,12 @@ import {
|
|
|
19
19
|
type ReceiptSubject,
|
|
20
20
|
type ReviewFailureEvidence,
|
|
21
21
|
type ReviewVerdictEvidence,
|
|
22
|
+
sha256Hex,
|
|
22
23
|
type ValidationEvidence,
|
|
23
24
|
validateReceipt,
|
|
24
25
|
} from "./receipts";
|
|
25
26
|
import { readReceiptIndex, writeReceiptImmutable } from "./storage";
|
|
26
|
-
import { isReviewVerdict, type ReviewVerdict } from "./types";
|
|
27
|
+
import { extractReviewVerdict, isReviewVerdict, type ReviewVerdict } from "./types";
|
|
27
28
|
|
|
28
29
|
export interface ValidationCommandSpec {
|
|
29
30
|
name: string;
|
|
@@ -56,6 +57,11 @@ export interface FinalizeOptions {
|
|
|
56
57
|
reviewOnly?: boolean;
|
|
57
58
|
/** Operator/loop-supplied terminal review verdict (closed vocabulary). */
|
|
58
59
|
verdict?: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Final assistant text from the live RPC owner, used to extract a closed-vocabulary verdict
|
|
62
|
+
* for review-only sessions when no explicit {@link verdict} is supplied. Never persisted raw.
|
|
63
|
+
*/
|
|
64
|
+
assistantText?: string | null;
|
|
59
65
|
/** Bounded PR/issue reference for the review target (e.g. "PR-414"). Never resolved from the live repo. */
|
|
60
66
|
prTarget?: string | null;
|
|
61
67
|
validationCommands?: ValidationCommandSpec[];
|
|
@@ -78,6 +84,14 @@ function receiptId(prefix: string): string {
|
|
|
78
84
|
return `${prefix}-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
/** Bound + whitespace-collapse assistant text into a redaction-safe digest summary (never a raw dump). */
|
|
88
|
+
function boundedAssistantSummary(text: string | null): string | null {
|
|
89
|
+
if (!text) return null;
|
|
90
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
91
|
+
if (collapsed.length === 0) return null;
|
|
92
|
+
return collapsed.length > 280 ? `${collapsed.slice(0, 280)}…` : collapsed;
|
|
93
|
+
}
|
|
94
|
+
|
|
81
95
|
export async function runFinalize(opts: FinalizeOptions): Promise<FinalizeResult> {
|
|
82
96
|
if (opts.reviewOnly) return runReviewFinalize(opts);
|
|
83
97
|
|
|
@@ -214,9 +228,27 @@ async function runReviewFinalize(opts: FinalizeOptions): Promise<FinalizeResult>
|
|
|
214
228
|
issueArtifact: null,
|
|
215
229
|
};
|
|
216
230
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
231
|
+
// Explicit operator/loop verdict always wins. Only when none is supplied do we fall back to
|
|
232
|
+
// extracting a closed-vocabulary verdict from the live RPC owner's final assistant text.
|
|
233
|
+
const explicitProvided = opts.verdict != null;
|
|
234
|
+
const explicitValid = isReviewVerdict(opts.verdict);
|
|
235
|
+
const assistantText = typeof opts.assistantText === "string" ? opts.assistantText : null;
|
|
236
|
+
const extracted = explicitProvided ? null : extractReviewVerdict(assistantText);
|
|
237
|
+
const verdict: ReviewVerdict | null = explicitValid ? (opts.verdict as ReviewVerdict) : extracted;
|
|
238
|
+
const verdictSource: "input" | "assistant" = explicitValid ? "input" : "assistant";
|
|
239
|
+
|
|
240
|
+
if (!verdict) {
|
|
241
|
+
const reason = explicitProvided ? "review-verdict-invalid" : "review-verdict-missing";
|
|
242
|
+
const assistantDigest = assistantText ? sha256Hex(assistantText) : null;
|
|
243
|
+
const assistantSummary = boundedAssistantSummary(assistantText);
|
|
244
|
+
const failure: ReviewFailureEvidence = {
|
|
245
|
+
reason,
|
|
246
|
+
prTarget,
|
|
247
|
+
failedAt: now(),
|
|
248
|
+
fallback: "operator-or-omx-review",
|
|
249
|
+
...(assistantDigest ? { assistantDigest } : {}),
|
|
250
|
+
...(assistantSummary ? { assistantSummary } : {}),
|
|
251
|
+
};
|
|
220
252
|
const receipt = buildReceipt<ReviewFailureEvidence>({
|
|
221
253
|
receiptId: receiptId("revfail"),
|
|
222
254
|
sessionId: opts.sessionId,
|
|
@@ -238,12 +270,14 @@ async function runReviewFinalize(opts: FinalizeOptions): Promise<FinalizeResult>
|
|
|
238
270
|
return { ...baseResult, completed: false, receiptPath: entry.path, verdict: null, blockers };
|
|
239
271
|
}
|
|
240
272
|
|
|
241
|
-
const
|
|
273
|
+
const assistantDigest = verdictSource === "assistant" && assistantText ? sha256Hex(assistantText) : null;
|
|
242
274
|
const evidence: ReviewVerdictEvidence = {
|
|
243
275
|
verdict,
|
|
244
276
|
prTarget,
|
|
245
277
|
finalizedAt: now(),
|
|
246
278
|
summaryRef: typeof opts.prTarget === "string" ? `verdict:${verdict}@${opts.prTarget}` : `verdict:${verdict}`,
|
|
279
|
+
verdictSource,
|
|
280
|
+
...(assistantDigest ? { assistantDigest } : {}),
|
|
247
281
|
};
|
|
248
282
|
const receipt = buildReceipt<ReviewVerdictEvidence>({
|
|
249
283
|
receiptId: receiptId("verdict"),
|
|
@@ -20,6 +20,7 @@ import { ControlServer, type EndpointRequest } from "./control-endpoint";
|
|
|
20
20
|
import { defaultFinalizeChecks, type FinalizeChecks, runFinalize, type ValidationCommandSpec } from "./finalize";
|
|
21
21
|
import { type OperateResult, operate } from "./operate";
|
|
22
22
|
import { preserveDirtyWorktree } from "./preserve";
|
|
23
|
+
import { RECEIPT_SPOOL_DIR_ENV, withReceiptSpoolDir } from "./receipt-spool";
|
|
23
24
|
import {
|
|
24
25
|
buildReceipt,
|
|
25
26
|
type ReceiptSubject,
|
|
@@ -28,8 +29,7 @@ import {
|
|
|
28
29
|
type VanishEvidence,
|
|
29
30
|
validateReceipt,
|
|
30
31
|
} from "./receipts";
|
|
31
|
-
import type
|
|
32
|
-
import { singleFlightAccept } from "./rpc-adapter";
|
|
32
|
+
import { type HarnessRpc, type RpcStateSnapshot, singleFlightAccept } from "./rpc-adapter";
|
|
33
33
|
import {
|
|
34
34
|
acquireLease,
|
|
35
35
|
canWriteEvents,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
releaseLease,
|
|
40
40
|
type SessionLease,
|
|
41
41
|
} from "./session-lease";
|
|
42
|
-
import { buildStateView, nextAllowedActions } from "./state-machine";
|
|
42
|
+
import { buildStateView, nextAllowedActions, submitUnavailableReason } from "./state-machine";
|
|
43
43
|
import {
|
|
44
44
|
appendEvent,
|
|
45
45
|
controlSocketPath,
|
|
@@ -200,11 +200,6 @@ export class RuntimeOwner {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
async #emitMapped(mapped: NonNullable<ReturnType<typeof observeRpcOutboundFrame>>): Promise<void> {
|
|
203
|
-
await this.#emit(
|
|
204
|
-
mapped.severity,
|
|
205
|
-
mapped.kind,
|
|
206
|
-
mapped.signal ? { ...mapped.evidence, signal: mapped.signal } : mapped.evidence,
|
|
207
|
-
);
|
|
208
203
|
if (mapped.kind === "rpc_agent_completed") {
|
|
209
204
|
const state = await readSessionState(this.#opts.root, this.#opts.sessionId);
|
|
210
205
|
if (
|
|
@@ -218,6 +213,11 @@ export class RuntimeOwner {
|
|
|
218
213
|
await writeSessionState(this.#opts.root, state);
|
|
219
214
|
}
|
|
220
215
|
}
|
|
216
|
+
await this.#emit(
|
|
217
|
+
mapped.severity,
|
|
218
|
+
mapped.kind,
|
|
219
|
+
mapped.signal ? { ...mapped.evidence, signal: mapped.signal } : mapped.evidence,
|
|
220
|
+
);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
#aggregateSignals(events: EventEnvelope[]): string[] {
|
|
@@ -233,6 +233,20 @@ export class RuntimeOwner {
|
|
|
233
233
|
return out;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
#eventSubmitGateReason(kind: string, evidence: Record<string, unknown>): string | null {
|
|
237
|
+
const reason = typeof evidence.reason === "string" ? evidence.reason : null;
|
|
238
|
+
const signal = typeof evidence.signal === "string" ? evidence.signal : null;
|
|
239
|
+
const rpcActive =
|
|
240
|
+
kind === "prompt_accepted" ||
|
|
241
|
+
reason === "pre-state-not-idle" ||
|
|
242
|
+
kind.startsWith("rpc_") ||
|
|
243
|
+
signal === "prompt-accepted" ||
|
|
244
|
+
signal === "streaming" ||
|
|
245
|
+
signal === "tool-call" ||
|
|
246
|
+
signal === "test-running";
|
|
247
|
+
return rpcActive ? "rpc-not-idle" : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
236
250
|
async #emit(severity: Severity, kind: string, evidence: Record<string, unknown>): Promise<void> {
|
|
237
251
|
const lease = await readLease(this.#opts.root, this.#opts.sessionId);
|
|
238
252
|
// Single-writer guard: only emit while we still hold a live lease.
|
|
@@ -247,6 +261,7 @@ export class RuntimeOwner {
|
|
|
247
261
|
ownerLive: true,
|
|
248
262
|
blockers: [],
|
|
249
263
|
};
|
|
264
|
+
const submitGateReason = this.#eventSubmitGateReason(kind, evidence);
|
|
250
265
|
const envelope: EventEnvelope = {
|
|
251
266
|
eventId: randomUUID(),
|
|
252
267
|
cursor: ++this.#cursor,
|
|
@@ -255,21 +270,41 @@ export class RuntimeOwner {
|
|
|
255
270
|
kind,
|
|
256
271
|
state: view,
|
|
257
272
|
evidence,
|
|
258
|
-
nextAllowedActions: nextAllowedActions(view.lifecycle, true),
|
|
273
|
+
nextAllowedActions: nextAllowedActions(view.lifecycle, true, { submitUnavailableReason: submitGateReason }),
|
|
259
274
|
writer: { ownerId: this.ownerId, leaseEpoch: this.#leaseEpoch },
|
|
260
275
|
};
|
|
261
276
|
await appendEvent(this.#opts.root, this.#opts.sessionId, envelope);
|
|
262
277
|
}
|
|
263
278
|
|
|
264
|
-
#response(
|
|
279
|
+
#response(
|
|
280
|
+
state: SessionState,
|
|
281
|
+
evidence: Record<string, unknown>,
|
|
282
|
+
ok = true,
|
|
283
|
+
submitGateReason: string | null = null,
|
|
284
|
+
): PrimitiveResponse {
|
|
265
285
|
return {
|
|
266
286
|
ok,
|
|
267
287
|
state: buildStateView(state, true),
|
|
268
288
|
evidence,
|
|
269
|
-
nextAllowedActions: nextAllowedActions(state.lifecycle, true),
|
|
289
|
+
nextAllowedActions: nextAllowedActions(state.lifecycle, true, { submitUnavailableReason: submitGateReason }),
|
|
270
290
|
};
|
|
271
291
|
}
|
|
272
292
|
|
|
293
|
+
#submitGateReason(state: SessionState, rpcState: RpcStateSnapshot | null): string | null {
|
|
294
|
+
const rpcReason = rpcState
|
|
295
|
+
? rpcState.isStreaming || rpcState.steeringQueueDepth > 0 || rpcState.followupQueueDepth > 0
|
|
296
|
+
? "rpc-not-idle"
|
|
297
|
+
: null
|
|
298
|
+
: "rpc-not-live";
|
|
299
|
+
return submitUnavailableReason(state.lifecycle, true, rpcReason);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async #withReceiptSpoolFromInput<T>(input: Record<string, unknown>, fn: () => Promise<T>): Promise<T> {
|
|
303
|
+
const requested = input[RECEIPT_SPOOL_DIR_ENV];
|
|
304
|
+
if (typeof requested === "string" && requested.trim()) return withReceiptSpoolDir(requested, fn);
|
|
305
|
+
return fn();
|
|
306
|
+
}
|
|
307
|
+
|
|
273
308
|
async #handle(req: EndpointRequest): Promise<unknown> {
|
|
274
309
|
switch (req.verb) {
|
|
275
310
|
case "ping":
|
|
@@ -281,13 +316,13 @@ export class RuntimeOwner {
|
|
|
281
316
|
case "retire":
|
|
282
317
|
return this.#retire();
|
|
283
318
|
case "finalize":
|
|
284
|
-
return this.#finalize(req.input);
|
|
319
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#finalize(req.input));
|
|
285
320
|
case "recover":
|
|
286
|
-
return this.#recover();
|
|
321
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#recover());
|
|
287
322
|
case "validate":
|
|
288
|
-
return this.#validate();
|
|
323
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#validate());
|
|
289
324
|
case "operate":
|
|
290
|
-
return this.#operate(req.input);
|
|
325
|
+
return this.#withReceiptSpoolFromInput(req.input, () => this.#operate(req.input));
|
|
291
326
|
default:
|
|
292
327
|
return { ok: false, error: `owner_unsupported_verb:${req.verb}` };
|
|
293
328
|
}
|
|
@@ -297,8 +332,10 @@ export class RuntimeOwner {
|
|
|
297
332
|
const state = await this.#loadState();
|
|
298
333
|
const workspace = state.handle.workspace;
|
|
299
334
|
let streaming = false;
|
|
335
|
+
let rpcState: RpcStateSnapshot | null = null;
|
|
300
336
|
try {
|
|
301
|
-
|
|
337
|
+
rpcState = await this.#opts.rpc.getState();
|
|
338
|
+
streaming = rpcState.isStreaming;
|
|
302
339
|
} catch {
|
|
303
340
|
streaming = false;
|
|
304
341
|
}
|
|
@@ -328,12 +365,7 @@ export class RuntimeOwner {
|
|
|
328
365
|
gitDelta = "unknown";
|
|
329
366
|
}
|
|
330
367
|
}
|
|
331
|
-
const rpcLive = this.#opts.rpc.isLive
|
|
332
|
-
? this.#opts.rpc.isLive()
|
|
333
|
-
: await this.#opts.rpc
|
|
334
|
-
.getState()
|
|
335
|
-
.then(() => true)
|
|
336
|
-
.catch(() => false);
|
|
368
|
+
const rpcLive = this.#opts.rpc.isLive ? this.#opts.rpc.isLive() : rpcState !== null;
|
|
337
369
|
const rpcLastFrameAt = this.#opts.rpc.lastFrameAt ? this.#opts.rpc.lastFrameAt() : null;
|
|
338
370
|
// Sticky semantic signals come from the persisted owner event log -> survive polling gaps.
|
|
339
371
|
const recent = (await readEvents(this.#opts.root, this.#opts.sessionId, 0)).slice(-200);
|
|
@@ -343,6 +375,7 @@ export class RuntimeOwner {
|
|
|
343
375
|
(t): t is string => typeof t === "string",
|
|
344
376
|
);
|
|
345
377
|
const lastActivityAt = stamps.length > 0 ? (stamps.sort().at(-1) ?? state.updatedAt) : state.updatedAt;
|
|
378
|
+
const submitGateReason = this.#submitGateReason(state, rpcState);
|
|
346
379
|
return {
|
|
347
380
|
lifecycle: state.lifecycle,
|
|
348
381
|
ownerLive: true,
|
|
@@ -354,6 +387,8 @@ export class RuntimeOwner {
|
|
|
354
387
|
risk: deleted ? "deleted-worktree" : "normal",
|
|
355
388
|
rpcLive,
|
|
356
389
|
rpcLastFrameAt,
|
|
390
|
+
readyForSubmit: submitGateReason === null,
|
|
391
|
+
submitUnavailableReason: submitGateReason,
|
|
357
392
|
};
|
|
358
393
|
}
|
|
359
394
|
|
|
@@ -504,13 +539,21 @@ export class RuntimeOwner {
|
|
|
504
539
|
const workspace = state.handle.workspace;
|
|
505
540
|
const checks = this.#finalizeChecks ?? defaultFinalizeChecks(workspace);
|
|
506
541
|
const reviewOnly = state.handle.mode === "review";
|
|
542
|
+
const inputVerdict = reviewOnly ? (typeof input.verdict === "string" ? input.verdict : null) : undefined;
|
|
543
|
+
// Review-only finalize with no explicit verdict pulls the final assistant text from the live
|
|
544
|
+
// RPC owner so the verdict can be extracted deterministically instead of demanded from the operator.
|
|
545
|
+
let assistantText: string | null = null;
|
|
546
|
+
if (reviewOnly && inputVerdict == null && this.#opts.rpc.getLastAssistantText) {
|
|
547
|
+
assistantText = await this.#opts.rpc.getLastAssistantText().catch(() => null);
|
|
548
|
+
}
|
|
507
549
|
const fin = await runFinalize({
|
|
508
550
|
root: this.#opts.root,
|
|
509
551
|
sessionId: this.#opts.sessionId,
|
|
510
552
|
workspace,
|
|
511
553
|
branch: state.handle.branch ?? "",
|
|
512
554
|
reviewOnly,
|
|
513
|
-
verdict:
|
|
555
|
+
verdict: inputVerdict,
|
|
556
|
+
assistantText: reviewOnly ? assistantText : undefined,
|
|
514
557
|
prTarget: reviewOnly ? state.handle.issueOrPr : undefined,
|
|
515
558
|
requireTests: input.requireTests !== false,
|
|
516
559
|
requireCommit: input.requireCommit !== false,
|
|
@@ -535,10 +578,21 @@ export class RuntimeOwner {
|
|
|
535
578
|
const prompt = typeof input.prompt === "string" ? input.prompt : "";
|
|
536
579
|
const state = await this.#loadState();
|
|
537
580
|
if (!prompt) {
|
|
538
|
-
return this.#response(
|
|
581
|
+
return this.#response(
|
|
582
|
+
state,
|
|
583
|
+
{ accepted: false, submitted: false, reason: "empty-prompt" },
|
|
584
|
+
false,
|
|
585
|
+
"empty-prompt",
|
|
586
|
+
);
|
|
539
587
|
}
|
|
540
|
-
|
|
541
|
-
|
|
588
|
+
const lifecycleGate = submitUnavailableReason(state.lifecycle, true);
|
|
589
|
+
if (lifecycleGate) {
|
|
590
|
+
return this.#response(
|
|
591
|
+
state,
|
|
592
|
+
{ accepted: false, submitted: false, reason: lifecycleGate },
|
|
593
|
+
false,
|
|
594
|
+
lifecycleGate,
|
|
595
|
+
);
|
|
542
596
|
}
|
|
543
597
|
const result = await singleFlightAccept(this.#opts.rpc, prompt, this.#opts.acceptanceTimeoutMs);
|
|
544
598
|
if (result.accepted) {
|
|
@@ -552,11 +606,12 @@ export class RuntimeOwner {
|
|
|
552
606
|
} else {
|
|
553
607
|
await this.#emit("warn", "prompt_not_accepted", { reason: result.reason });
|
|
554
608
|
}
|
|
609
|
+
const submitGateReason = result.accepted ? null : result.reason === "pre-state-not-idle" ? "rpc-not-idle" : null;
|
|
555
610
|
return this.#response(
|
|
556
611
|
state,
|
|
557
612
|
{
|
|
558
613
|
accepted: result.accepted,
|
|
559
|
-
submitted:
|
|
614
|
+
submitted: result.commandId !== null,
|
|
560
615
|
reason: result.reason,
|
|
561
616
|
commandId: result.commandId,
|
|
562
617
|
preSubmitCursor: result.preSubmitCursor,
|
|
@@ -564,12 +619,16 @@ export class RuntimeOwner {
|
|
|
564
619
|
acceptanceEvidence: result.preSubmitState,
|
|
565
620
|
},
|
|
566
621
|
result.accepted,
|
|
622
|
+
submitGateReason,
|
|
567
623
|
);
|
|
568
624
|
}
|
|
569
625
|
|
|
570
626
|
async #observe(): Promise<PrimitiveResponse> {
|
|
571
627
|
const state = await this.#loadState();
|
|
572
|
-
|
|
628
|
+
const observation = await this.#observeGit();
|
|
629
|
+
const submitGateReason =
|
|
630
|
+
typeof observation.submitUnavailableReason === "string" ? observation.submitUnavailableReason : null;
|
|
631
|
+
return this.#response(state, { observation, ownerRouted: true }, true, submitGateReason);
|
|
573
632
|
}
|
|
574
633
|
|
|
575
634
|
async #retire(): Promise<PrimitiveResponse> {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase-boundary receipt rollup builder (receipt-of-receipts).
|
|
3
|
+
*
|
|
4
|
+
* At a harness lifecycle boundary, N child task receipts can be superseded by a
|
|
5
|
+
* single `phase-rollup` receipt that preserves per-child pointers (id, status,
|
|
6
|
+
* outputRef, sha256) plus aggregate ROI totals. The rollup is hash-sealed via
|
|
7
|
+
* the standard receipt envelope and validated fail-closed like every other
|
|
8
|
+
* family (see `validatePhaseRollup` in receipts.ts). Pure builder — no runtime
|
|
9
|
+
* injection behavior is changed here.
|
|
10
|
+
*/
|
|
11
|
+
import type { TaskResultReceipt } from "../task/receipt";
|
|
12
|
+
import {
|
|
13
|
+
type BuildReceiptInput,
|
|
14
|
+
buildReceipt,
|
|
15
|
+
canonicalJson,
|
|
16
|
+
type PhaseRollupChildPointer,
|
|
17
|
+
type PhaseRollupEvidence,
|
|
18
|
+
type ReceiptEnvelope,
|
|
19
|
+
sha256Hex,
|
|
20
|
+
} from "./receipts";
|
|
21
|
+
|
|
22
|
+
function childPointer(receipt: TaskResultReceipt): PhaseRollupChildPointer {
|
|
23
|
+
const ref = receipt.outputRef;
|
|
24
|
+
// Receipt-of-receipts integrity requires BOTH a pointer URI and its content
|
|
25
|
+
// hash. A URI without a verifiable hash cannot be integrity-checked, so we
|
|
26
|
+
// drop the (one-sided) pointer entirely rather than emit an unverifiable ref
|
|
27
|
+
// that the fail-closed validator would reject.
|
|
28
|
+
const hasVerifiableRef = Boolean(ref?.uri) && Boolean(ref?.sha256);
|
|
29
|
+
return {
|
|
30
|
+
id: receipt.id,
|
|
31
|
+
status: receipt.status,
|
|
32
|
+
outputUri: hasVerifiableRef ? (ref?.uri ?? null) : null,
|
|
33
|
+
outputSha256: hasVerifiableRef ? (ref?.sha256 ?? null) : null,
|
|
34
|
+
// Normalize through JSON first: in-memory task receipts carry optional
|
|
35
|
+
// fields with value `undefined`, which canonicalJson would hash as
|
|
36
|
+
// `null` while persisted/parsed receipts omit those keys entirely.
|
|
37
|
+
// JSON round-tripping drops undefined-valued keys so the hash is
|
|
38
|
+
// identical for in-memory and rehydrated copies of the same receipt.
|
|
39
|
+
receiptSha256: sha256Hex(canonicalJson(JSON.parse(JSON.stringify(receipt)))),
|
|
40
|
+
// Per-child ROI accounting so the rollup aggregate is recomputable from
|
|
41
|
+
// child evidence (see validatePhaseRollup). `tokens` falls back to the
|
|
42
|
+
// receipt's raw token count when no ROI proxy is present.
|
|
43
|
+
tokens: receipt.roi?.tokens ?? receipt.tokens,
|
|
44
|
+
costTotal: receipt.roi?.costTotal ?? null,
|
|
45
|
+
clonedTokens: receipt.roi?.clonedTokens ?? null,
|
|
46
|
+
lowRoi: receipt.roi?.lowRoi ?? false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface BuildPhaseRollupInput {
|
|
51
|
+
receiptId: string;
|
|
52
|
+
sessionId: string;
|
|
53
|
+
source: string;
|
|
54
|
+
subject: BuildReceiptInput<PhaseRollupEvidence>["subject"];
|
|
55
|
+
phase: string;
|
|
56
|
+
children: readonly TaskResultReceipt[];
|
|
57
|
+
/** Supply for deterministic output; defaults to now. */
|
|
58
|
+
createdAt?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildPhaseRollupReceipt(input: BuildPhaseRollupInput): ReceiptEnvelope<PhaseRollupEvidence> {
|
|
62
|
+
// `null` means "no child reported this metric" — the canonical value the
|
|
63
|
+
// fail-closed validator reconciles against. Decide presence from whether a
|
|
64
|
+
// child carried the field at all (not from a >0 sum), so a legitimate
|
|
65
|
+
// all-zero total reconciles instead of collapsing to null and mismatching.
|
|
66
|
+
const anyCost = input.children.some(child => (child.roi?.costTotal ?? null) !== null);
|
|
67
|
+
const anyCloned = input.children.some(child => (child.roi?.clonedTokens ?? null) !== null);
|
|
68
|
+
const totalCostTotal = anyCost
|
|
69
|
+
? input.children.reduce((total, child) => total + (child.roi?.costTotal ?? 0), 0)
|
|
70
|
+
: null;
|
|
71
|
+
const totalClonedTokens = anyCloned
|
|
72
|
+
? input.children.reduce((total, child) => total + (child.roi?.clonedTokens ?? 0), 0)
|
|
73
|
+
: null;
|
|
74
|
+
const evidence: PhaseRollupEvidence = {
|
|
75
|
+
phase: input.phase,
|
|
76
|
+
children: input.children.map(childPointer),
|
|
77
|
+
aggregate: {
|
|
78
|
+
childCount: input.children.length,
|
|
79
|
+
completed: input.children.filter(child => child.status === "completed").length,
|
|
80
|
+
failed: input.children.filter(child => child.status === "failed" || child.status === "merge_failed").length,
|
|
81
|
+
totalTokens: input.children.reduce((total, child) => total + (child.roi?.tokens ?? child.tokens), 0),
|
|
82
|
+
totalCostTotal,
|
|
83
|
+
totalClonedTokens,
|
|
84
|
+
lowRoiChildIds: input.children.filter(child => child.roi?.lowRoi).map(child => child.id),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
return buildReceipt({
|
|
88
|
+
receiptId: input.receiptId,
|
|
89
|
+
sessionId: input.sessionId,
|
|
90
|
+
family: "phase-rollup",
|
|
91
|
+
source: input.source,
|
|
92
|
+
subject: input.subject,
|
|
93
|
+
evidence,
|
|
94
|
+
createdAt: input.createdAt,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { CompletionEvidence, ReceiptEnvelope, ReviewVerdictEvidence } from "./receipts";
|
|
2
|
+
import { validateReceipt } from "./receipts";
|
|
3
|
+
import { canTransition } from "./state-machine";
|
|
4
|
+
import type { HarnessLifecycle, ReceiptFamily, SessionState } from "./types";
|
|
5
|
+
|
|
6
|
+
export const RECEIPT_DIGEST_MAX_CHARS = 280;
|
|
7
|
+
|
|
8
|
+
export const RECEIPT_FAMILY_LIFECYCLE_TARGETS: Partial<Record<ReceiptFamily, HarnessLifecycle>> = {
|
|
9
|
+
completion: "completed",
|
|
10
|
+
// Review-only sessions terminate on a valid review verdict rather than a
|
|
11
|
+
// completion receipt; the finalizer treats valid verdicts as terminal.
|
|
12
|
+
"review-verdict": "completed",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Family-specific evidence consistency: the lifecycle target must agree with
|
|
17
|
+
* what the evidence itself claims. Hash validity alone is not enough — a
|
|
18
|
+
* semantically contradictory receipt must not drive the lifecycle.
|
|
19
|
+
*/
|
|
20
|
+
function evidenceContradiction(receipt: ReceiptEnvelope<unknown>, target: HarnessLifecycle): string | undefined {
|
|
21
|
+
if (receipt.family === "completion") {
|
|
22
|
+
const evidence = receipt.evidence as CompletionEvidence;
|
|
23
|
+
if (evidence.finalLifecycle !== target) {
|
|
24
|
+
return `evidence-lifecycle-mismatch:${evidence.finalLifecycle}`;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (receipt.family === "review-verdict") {
|
|
28
|
+
const evidence = receipt.evidence as ReviewVerdictEvidence;
|
|
29
|
+
// Owner confirmation is not a terminal success verdict.
|
|
30
|
+
if (evidence.verdict === "OWNER_CONFIRMATION_REQUIRED") {
|
|
31
|
+
return "review-verdict-not-terminal";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ReceiptIngestResult {
|
|
38
|
+
accepted: ReceiptEnvelope<unknown>[];
|
|
39
|
+
rejected: { receipt: ReceiptEnvelope<unknown>; reasons: string[] }[];
|
|
40
|
+
transitions: { from: HarnessLifecycle; to: HarnessLifecycle; receiptId: string }[];
|
|
41
|
+
finalLifecycle: HarnessLifecycle;
|
|
42
|
+
digest: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function ingestReceipts(
|
|
46
|
+
state: SessionState,
|
|
47
|
+
receipts: readonly ReceiptEnvelope<unknown>[],
|
|
48
|
+
): ReceiptIngestResult {
|
|
49
|
+
let lifecycle = state.lifecycle;
|
|
50
|
+
const accepted: ReceiptEnvelope<unknown>[] = [];
|
|
51
|
+
const rejected: { receipt: ReceiptEnvelope<unknown>; reasons: string[] }[] = [];
|
|
52
|
+
const transitions: { from: HarnessLifecycle; to: HarnessLifecycle; receiptId: string }[] = [];
|
|
53
|
+
|
|
54
|
+
for (const receipt of receipts) {
|
|
55
|
+
const validation = validateReceipt(receipt);
|
|
56
|
+
if (!validation.valid) {
|
|
57
|
+
rejected.push({ receipt, reasons: validation.reasons });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Fail closed on receipts the envelope itself marks invalid: the hash
|
|
62
|
+
// can be self-consistent while the issuer recorded the receipt as not
|
|
63
|
+
// proving its claim.
|
|
64
|
+
if (receipt.valid !== true) {
|
|
65
|
+
rejected.push({ receipt, reasons: ["receipt-marked-invalid"] });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Fail closed on cross-session receipts: a self-consistent receipt from
|
|
70
|
+
// another session must never drive this session's lifecycle.
|
|
71
|
+
if (receipt.sessionId !== state.sessionId) {
|
|
72
|
+
rejected.push({ receipt, reasons: [`session-mismatch:${receipt.sessionId}`] });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const target = RECEIPT_FAMILY_LIFECYCLE_TARGETS[receipt.family];
|
|
77
|
+
if (target) {
|
|
78
|
+
// Non-terminal review verdicts (OWNER_CONFIRMATION_REQUIRED) are
|
|
79
|
+
// valid receipts but do not complete the session: accept, no move.
|
|
80
|
+
if (receipt.family === "review-verdict" && evidenceContradiction(receipt, target) !== undefined) {
|
|
81
|
+
accepted.push(receipt);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// Other contradictions (e.g. completion evidence whose
|
|
85
|
+
// finalLifecycle disagrees with the target) reject fail-closed.
|
|
86
|
+
const contradiction = evidenceContradiction(receipt, target);
|
|
87
|
+
if (contradiction !== undefined) {
|
|
88
|
+
rejected.push({ receipt, reasons: [contradiction] });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (!canTransition(lifecycle, target)) {
|
|
92
|
+
rejected.push({ receipt, reasons: [`illegal-transition:${lifecycle}->${target}`] });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
transitions.push({ from: lifecycle, to: target, receiptId: receipt.receiptId });
|
|
97
|
+
lifecycle = target;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
accepted.push(receipt);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
accepted,
|
|
105
|
+
rejected,
|
|
106
|
+
transitions,
|
|
107
|
+
finalLifecycle: lifecycle,
|
|
108
|
+
digest: buildReceiptIngestDigest(receipts.length, accepted.length, rejected, state.lifecycle, lifecycle),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function buildReceiptIngestDigest(
|
|
113
|
+
total: number,
|
|
114
|
+
acceptedCount: number,
|
|
115
|
+
rejected: readonly { receipt: ReceiptEnvelope<unknown>; reasons: readonly string[] }[],
|
|
116
|
+
initialLifecycle: HarnessLifecycle,
|
|
117
|
+
finalLifecycle: HarnessLifecycle,
|
|
118
|
+
): string {
|
|
119
|
+
let digest = `ingested ${total} receipts: ${acceptedCount} accepted, ${rejected.length} rejected; lifecycle ${initialLifecycle}->${finalLifecycle}`;
|
|
120
|
+
if (rejected.length > 0) {
|
|
121
|
+
const rejectedSummary = rejected
|
|
122
|
+
.map(item => `${item.receipt?.receiptId ?? "<malformed>"}(${item.reasons.join("|")})`)
|
|
123
|
+
.join(",");
|
|
124
|
+
digest += `; rejected: ${rejectedSummary}`;
|
|
125
|
+
}
|
|
126
|
+
return digest.slice(0, RECEIPT_DIGEST_MAX_CHARS);
|
|
127
|
+
}
|