@gajae-code/coding-agent 0.12.11 → 0.12.13
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 +68 -0
- package/dist/types/cli/marketplace-hint.d.ts +28 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock-gc.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/models-config-schema.d.ts +99 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/acp/acp-agent.d.ts +24 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/html-format.d.ts +10 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
- package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +40 -6
- package/dist/types/tools/index.d.ts +15 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/marketplace-hint.ts +39 -0
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/plugin-cli.ts +10 -0
- package/src/cli/update-cli.ts +90 -23
- package/src/cli.ts +23 -0
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +65 -26
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +46 -30
- package/src/config/model-registry.ts +17 -5
- package/src/config/models-config-schema.ts +9 -4
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
- package/src/gjc-runtime/deep-interview-state.ts +27 -3
- package/src/gjc-runtime/gc-render.ts +8 -1
- package/src/gjc-runtime/gc-runtime.ts +15 -1
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/main.ts +214 -70
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/acp/acp-agent.ts +123 -7
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/model-selector.ts +52 -5
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/print-mode.ts +10 -0
- package/src/modes/utils/host-status.ts +38 -0
- package/src/prompts/system/project-prompt.md +19 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/html-format.ts +19 -3
- package/src/sdk/bus/index.ts +91 -12
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
- package/src/sdk/bus/telegram-daemon.ts +2007 -509
- package/src/sdk/bus/telegram-reference.ts +114 -15
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +144 -29
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -13
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +228 -7
- package/src/session/session-manager.ts +225 -60
- package/src/setup/provider-presets.json +3 -3
- package/src/task/index.ts +92 -44
- package/src/thinking-metadata.ts +2 -1
- package/src/tools/ask.ts +10 -0
- package/src/tools/index.ts +15 -0
- package/src/tools/todo-write.ts +4 -4
- package/src/utils/npm-registry.ts +660 -0
|
@@ -18,6 +18,11 @@ import { type EnsureBrokerSettings, ensureBroker } from "../sdk/broker/ensure";
|
|
|
18
18
|
import { UnsupportedStateVersionError } from "../sdk/broker/state-version";
|
|
19
19
|
import { SdkClient, SdkClientError } from "../sdk/client/client";
|
|
20
20
|
import { readSdkBrokerDiscovery } from "../sdk/client/discovery";
|
|
21
|
+
import {
|
|
22
|
+
type ActivatedPreparedSession,
|
|
23
|
+
requestPreparedSessionActivation,
|
|
24
|
+
SessionActivationError,
|
|
25
|
+
} from "../sdk/session-activation";
|
|
21
26
|
import {
|
|
22
27
|
type CoordinatorModelProfileLoader,
|
|
23
28
|
loadCoordinatorModelProfiles,
|
|
@@ -222,6 +227,8 @@ interface TurnRecord {
|
|
|
222
227
|
|
|
223
228
|
type CoordinatorSessionStateValue =
|
|
224
229
|
| "booting"
|
|
230
|
+
/** Live and endpoint-addressable, but withholding readiness until activation. */
|
|
231
|
+
| "prepared"
|
|
225
232
|
| "ready_for_input"
|
|
226
233
|
| "running"
|
|
227
234
|
| "needs_user_input"
|
|
@@ -290,6 +297,8 @@ interface CoordinatorEventInput {
|
|
|
290
297
|
metadata?: Record<string, string | number | boolean | null>;
|
|
291
298
|
}
|
|
292
299
|
|
|
300
|
+
const UNOBSERVED_COMPENSATION_CODE = "broker_compensation_unobserved";
|
|
301
|
+
|
|
293
302
|
const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
|
|
294
303
|
const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
|
|
295
304
|
const DEFAULT_RUNTIME_PROMPT_ACK_TIMEOUT_MS = 10_000;
|
|
@@ -364,12 +373,18 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
364
373
|
if (name === "gjc_coordinator_start_session") {
|
|
365
374
|
return {
|
|
366
375
|
name,
|
|
367
|
-
description:
|
|
376
|
+
description:
|
|
377
|
+
"Start a broker-managed GJC session through canonical SDK lifecycle control. Set prepare_existing_thread to hold the session at prepared (endpoint-addressable, readiness withheld) so an existing chat thread can be bound before activation.",
|
|
368
378
|
inputSchema: {
|
|
369
379
|
type: "object",
|
|
370
380
|
properties: {
|
|
371
381
|
cwd,
|
|
372
382
|
prompt: { type: "string" },
|
|
383
|
+
prepare_existing_thread: {
|
|
384
|
+
type: "boolean",
|
|
385
|
+
description:
|
|
386
|
+
"Create the session prepared instead of ready: no readiness is published and no initial prompt is accepted until gjc_coordinator_activate_session proves activation.",
|
|
387
|
+
},
|
|
373
388
|
mpreset,
|
|
374
389
|
idempotency_key: idempotencyKey,
|
|
375
390
|
allow_mutation: allowMutation,
|
|
@@ -378,6 +393,22 @@ function toolSchema(name: CoordinatorToolName): {
|
|
|
378
393
|
},
|
|
379
394
|
};
|
|
380
395
|
}
|
|
396
|
+
if (name === "gjc_coordinator_activate_session") {
|
|
397
|
+
return {
|
|
398
|
+
name,
|
|
399
|
+
description:
|
|
400
|
+
"Activate a prepared session so it publishes the readiness it withheld. Requires the session's own proof at the exact endpoint generation, so it fails closed while no existing-thread binding has been applied.",
|
|
401
|
+
inputSchema: {
|
|
402
|
+
type: "object",
|
|
403
|
+
properties: {
|
|
404
|
+
session_id: sessionId,
|
|
405
|
+
idempotency_key: idempotencyKey,
|
|
406
|
+
allow_mutation: allowMutation,
|
|
407
|
+
},
|
|
408
|
+
required: ["session_id", "idempotency_key", "allow_mutation"],
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
381
412
|
if (name === "gjc_coordinator_stop_session") {
|
|
382
413
|
return {
|
|
383
414
|
name,
|
|
@@ -853,6 +884,38 @@ function boundedPublicResponse(response: Record<string, unknown>): Record<string
|
|
|
853
884
|
return asRecord(value) ?? { ok: false, error: { code: "unavailable", message: "Invalid coordinator response." } };
|
|
854
885
|
}
|
|
855
886
|
|
|
887
|
+
/**
|
|
888
|
+
* `activation_outcome_unknown` states only that the activation's outcome could
|
|
889
|
+
* not be observed: the session may already have published the readiness the
|
|
890
|
+
* request asked for. It is the one activation answer that proves nothing, so it
|
|
891
|
+
* is returned to the caller without being sealed as a settled receipt.
|
|
892
|
+
*/
|
|
893
|
+
function isUnknownActivationOutcome(response: Record<string, unknown>): boolean {
|
|
894
|
+
if (response.ok !== false) return false;
|
|
895
|
+
return asRecord(response.error)?.code === "activation_outcome_unknown";
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Which durable states may reach the activation proof, and which of them is
|
|
900
|
+
* already settled.
|
|
901
|
+
*
|
|
902
|
+
* Durable state is a record of what was proved, never a proof on its own. Only
|
|
903
|
+
* a `prepared` session (readiness still withheld) and a `ready_for_input` one
|
|
904
|
+
* (readiness already proved once) are activatable, and both are answered by the
|
|
905
|
+
* live session at the exact endpoint, generation, incarnation, and binding.
|
|
906
|
+
* Every other observed state — stale, booting, running, needs_user_input,
|
|
907
|
+
* completed, errored, unknown, or a missing state file — is not activatable and
|
|
908
|
+
* fails closed before any frame is sent.
|
|
909
|
+
*/
|
|
910
|
+
function classifyCoordinatorActivation(
|
|
911
|
+
state: CoordinatorSessionState | null,
|
|
912
|
+
): { activatable: true; settled: boolean; observed: string } | { activatable: false; observed: string } {
|
|
913
|
+
const observed = state?.state ?? "unknown";
|
|
914
|
+
if (observed === "prepared") return { activatable: true, settled: false, observed };
|
|
915
|
+
if (observed === "ready_for_input") return { activatable: true, settled: true, observed };
|
|
916
|
+
return { activatable: false, observed };
|
|
917
|
+
}
|
|
918
|
+
|
|
856
919
|
interface RuntimePromptAcknowledgement {
|
|
857
920
|
accepted: true;
|
|
858
921
|
command_id: string;
|
|
@@ -2509,6 +2572,16 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2509
2572
|
return normalizeRuntimePromptAcknowledgement(result);
|
|
2510
2573
|
}
|
|
2511
2574
|
|
|
2575
|
+
/**
|
|
2576
|
+
* The outcome of a failed compensating close is unobserved, not decided: the
|
|
2577
|
+
* session may still be running. Sealing it under the idempotency key would
|
|
2578
|
+
* answer that uncertainty forever, so the key stays open for a real retry.
|
|
2579
|
+
*/
|
|
2580
|
+
function isUnobservedCompensation(response: Record<string, unknown>): boolean {
|
|
2581
|
+
const error = asRecord(response.error);
|
|
2582
|
+
return error?.code === UNOBSERVED_COMPENSATION_CODE;
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2512
2585
|
function sdkError(error: unknown): Record<string, unknown> {
|
|
2513
2586
|
if (error instanceof SdkClientError) return { ok: false, error: { code: error.code, message: error.message } };
|
|
2514
2587
|
return {
|
|
@@ -2528,12 +2601,26 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2528
2601
|
return path.join(namespaceDir, "idempotency", `${keyDigest}.json`);
|
|
2529
2602
|
}
|
|
2530
2603
|
|
|
2604
|
+
/**
|
|
2605
|
+
* Run one mutation under its idempotency key, then seal its response as the
|
|
2606
|
+
* key's terminal replay.
|
|
2607
|
+
*
|
|
2608
|
+
* `isNonterminal` is the one exception. Sealing is correct for a decided
|
|
2609
|
+
* outcome, and wrong for a response that states only that the outcome could
|
|
2610
|
+
* not be observed: the key would answer that uncertainty forever, even once
|
|
2611
|
+
* the remote effect settled. A tool may declare such a response nonterminal,
|
|
2612
|
+
* which returns it to this caller while the receipt stays `in_progress`, so
|
|
2613
|
+
* an exact same-key retry re-runs the observation under the same request
|
|
2614
|
+
* digest. The default declares nothing nonterminal, so every other tool keeps
|
|
2615
|
+
* its existing caching, conflict, and replay behaviour unchanged.
|
|
2616
|
+
*/
|
|
2531
2617
|
async function withToolIdempotency(
|
|
2532
2618
|
tool: string,
|
|
2533
2619
|
idempotencyKey: string,
|
|
2534
2620
|
canonicalArgs: Record<string, unknown>,
|
|
2535
2621
|
operation: () => Promise<Record<string, unknown>>,
|
|
2536
2622
|
recoverInProgress = false,
|
|
2623
|
+
isNonterminal: (response: Record<string, unknown>) => boolean = () => false,
|
|
2537
2624
|
): Promise<Record<string, unknown>> {
|
|
2538
2625
|
const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
|
|
2539
2626
|
const requestDigest = createHash("sha256")
|
|
@@ -2584,6 +2671,9 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2584
2671
|
};
|
|
2585
2672
|
if (existing.state === "in_progress") {
|
|
2586
2673
|
const response = boundedPublicResponse(await operation().catch(error => sdkError(error)));
|
|
2674
|
+
// The receipt keeps its original key and request digests, so a
|
|
2675
|
+
// reused key still conflicts and a later settled answer still seals.
|
|
2676
|
+
if (isNonterminal(response)) return response;
|
|
2587
2677
|
await writeCoordinatorIdempotencyFile(file, {
|
|
2588
2678
|
...existing,
|
|
2589
2679
|
state: "completed",
|
|
@@ -2610,6 +2700,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2610
2700
|
};
|
|
2611
2701
|
await writeCoordinatorIdempotencyFile(file, started);
|
|
2612
2702
|
const response = boundedPublicResponse(await operation().catch(error => sdkError(error)));
|
|
2703
|
+
if (isNonterminal(response)) return response;
|
|
2613
2704
|
await writeCoordinatorIdempotencyFile(file, {
|
|
2614
2705
|
...started,
|
|
2615
2706
|
state: "completed",
|
|
@@ -2803,6 +2894,75 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
2803
2894
|
return binding.endpoint;
|
|
2804
2895
|
}
|
|
2805
2896
|
|
|
2897
|
+
/**
|
|
2898
|
+
* The same incarnation-bound authority `resolveSessionEndpoint` requires,
|
|
2899
|
+
* plus the exact endpoint generation the activation frame has to name. A
|
|
2900
|
+
* session whose endpoint rolled or whose workspace binding drifted is refused
|
|
2901
|
+
* here, before any activation is attempted.
|
|
2902
|
+
*/
|
|
2903
|
+
async function resolveSessionActivationTarget(
|
|
2904
|
+
session: Record<string, unknown>,
|
|
2905
|
+
idempotencyKey?: string,
|
|
2906
|
+
): Promise<{ endpoint: { url: string; token: string }; endpointGeneration: number }> {
|
|
2907
|
+
const sessionId = optionalString(session.session_id) ?? optionalString(session.sessionId);
|
|
2908
|
+
const cwd = optionalString(session.cwd);
|
|
2909
|
+
const persistedWorkspace = optionalString(session.broker_workspace);
|
|
2910
|
+
const persistedGeneration =
|
|
2911
|
+
typeof session.endpoint_generation === "number" &&
|
|
2912
|
+
Number.isSafeInteger(session.endpoint_generation) &&
|
|
2913
|
+
session.endpoint_generation > 0
|
|
2914
|
+
? session.endpoint_generation
|
|
2915
|
+
: null;
|
|
2916
|
+
const persistedIncarnation = optionalString(session.endpoint_incarnation);
|
|
2917
|
+
if (!sessionId || !cwd || !persistedWorkspace || persistedGeneration === null || !persistedIncarnation)
|
|
2918
|
+
throw new SdkClientError("not_found", "Coordinator session has no incarnation-bound broker identity.");
|
|
2919
|
+
const workspace = await canonicalBrokerWorkspace(cwd);
|
|
2920
|
+
if (!sameCanonicalPath(workspace, persistedWorkspace, platform))
|
|
2921
|
+
throw new SdkClientError("endpoint_stale", "Coordinator session workspace binding is stale.");
|
|
2922
|
+
const binding = await exactBrokerSessionBinding(sessionId, workspace, idempotencyKey);
|
|
2923
|
+
if (binding.endpointGeneration !== persistedGeneration || binding.endpointIncarnation !== persistedIncarnation)
|
|
2924
|
+
throw new SdkClientError("endpoint_stale", "Coordinator session endpoint incarnation is stale.");
|
|
2925
|
+
return { endpoint: binding.endpoint, endpointGeneration: binding.endpointGeneration };
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2928
|
+
/**
|
|
2929
|
+
* Ask a prepared session to publish its withheld readiness.
|
|
2930
|
+
*
|
|
2931
|
+
* The Coordinator never writes a chat mapping and never fakes a readiness
|
|
2932
|
+
* signal: it proves exact endpoint authority, then delegates to the same
|
|
2933
|
+
* activation exchange the `gjc notify activate-thread` CLI uses. The session's
|
|
2934
|
+
* own activation gate remains the authority on whether a binding exists.
|
|
2935
|
+
*/
|
|
2936
|
+
async function activatePreparedCoordinatorSession(
|
|
2937
|
+
session: Record<string, unknown>,
|
|
2938
|
+
sessionId: string,
|
|
2939
|
+
idempotencyKey: string,
|
|
2940
|
+
): Promise<ActivatedPreparedSession> {
|
|
2941
|
+
const target = await resolveSessionActivationTarget(session, idempotencyKey);
|
|
2942
|
+
let client: SdkClient;
|
|
2943
|
+
try {
|
|
2944
|
+
client = await (services.connectSdk ?? ((url, token) => SdkClient.connect(url, token)))(
|
|
2945
|
+
target.endpoint.url,
|
|
2946
|
+
target.endpoint.token,
|
|
2947
|
+
);
|
|
2948
|
+
} catch {
|
|
2949
|
+
// Nothing was sent, so no activation can have been applied.
|
|
2950
|
+
throw new SessionActivationError("activation_unavailable", "The session endpoint could not be reached.");
|
|
2951
|
+
}
|
|
2952
|
+
try {
|
|
2953
|
+
return await requestPreparedSessionActivation(
|
|
2954
|
+
{
|
|
2955
|
+
request: async frame => (await client.request(frame)) as Record<string, unknown>,
|
|
2956
|
+
close: async () => await client.close(),
|
|
2957
|
+
},
|
|
2958
|
+
sessionId,
|
|
2959
|
+
target.endpointGeneration,
|
|
2960
|
+
);
|
|
2961
|
+
} finally {
|
|
2962
|
+
await client.close().catch(() => undefined);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2806
2966
|
async function listSessions(cwd?: string): Promise<Array<Record<string, unknown>>> {
|
|
2807
2967
|
const roots = cwd ? [cwd] : config.allowedRoots;
|
|
2808
2968
|
const listings = await Promise.all(
|
|
@@ -3606,6 +3766,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
3606
3766
|
return response;
|
|
3607
3767
|
},
|
|
3608
3768
|
true,
|
|
3769
|
+
isUnobservedCompensation,
|
|
3609
3770
|
);
|
|
3610
3771
|
}
|
|
3611
3772
|
if (name === "gjc_coordinator_read_status") {
|
|
@@ -4026,10 +4187,41 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4026
4187
|
};
|
|
4027
4188
|
}
|
|
4028
4189
|
const prompt = typeof args.prompt === "string" && args.prompt.length > 0 ? args.prompt : null;
|
|
4190
|
+
/**
|
|
4191
|
+
* A prepared session is deliberately not ready for input: its readiness
|
|
4192
|
+
* is withheld until an operator-supplied thread is bound and activation
|
|
4193
|
+
* is proven. Accepting an initial prompt here would either be silently
|
|
4194
|
+
* dropped or delivered to a session no consumer has been told is live,
|
|
4195
|
+
* so it is refused before any broker mutation or idempotency record.
|
|
4196
|
+
*/
|
|
4197
|
+
// Runtime dispatch hands `params.arguments` through unvalidated, so a
|
|
4198
|
+
// client sending the string "true" would coerce to false here and start
|
|
4199
|
+
// an ordinary ready session that accepts the prompt — the opposite of
|
|
4200
|
+
// what the schema promises. Reject a non-boolean before any mutation.
|
|
4201
|
+
const requestedPrepare = (args as Record<string, unknown>).prepare_existing_thread;
|
|
4202
|
+
if (requestedPrepare !== undefined && typeof requestedPrepare !== "boolean")
|
|
4203
|
+
return {
|
|
4204
|
+
ok: false,
|
|
4205
|
+
error: {
|
|
4206
|
+
code: "invalid_input",
|
|
4207
|
+
message: `prepare_existing_thread must be a boolean; received ${typeof requestedPrepare}.`,
|
|
4208
|
+
},
|
|
4209
|
+
};
|
|
4210
|
+
const preparesExistingThread = requestedPrepare === true;
|
|
4211
|
+
if (preparesExistingThread && prompt)
|
|
4212
|
+
return {
|
|
4213
|
+
ok: false,
|
|
4214
|
+
error: {
|
|
4215
|
+
code: "invalid_input",
|
|
4216
|
+
message:
|
|
4217
|
+
"prepare_existing_thread cannot carry an initial prompt; activate the session first, then send_prompt.",
|
|
4218
|
+
},
|
|
4219
|
+
};
|
|
4029
4220
|
const canonicalArgs = {
|
|
4030
4221
|
cwd,
|
|
4031
4222
|
mpreset: mpresetResolution.mpreset,
|
|
4032
4223
|
...(prompt ? { prompt } : {}),
|
|
4224
|
+
...(preparesExistingThread ? { prepare_existing_thread: true } : {}),
|
|
4033
4225
|
allow_mutation: true,
|
|
4034
4226
|
};
|
|
4035
4227
|
return await withToolIdempotency(
|
|
@@ -4056,10 +4248,45 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4056
4248
|
cwd,
|
|
4057
4249
|
target: coordinatorLifecycleTarget(config.sessionCommand, cwd),
|
|
4058
4250
|
...(mpresetResolution.mpreset ? { modelPreset: mpresetResolution.mpreset } : {}),
|
|
4251
|
+
...(preparesExistingThread ? { readiness: "deferred" } : {}),
|
|
4059
4252
|
},
|
|
4060
4253
|
idempotencyKey,
|
|
4061
4254
|
),
|
|
4062
4255
|
);
|
|
4256
|
+
/**
|
|
4257
|
+
* Preparation is only real when the broker proves it. A create that
|
|
4258
|
+
* silently published readiness would leave a live session whose root
|
|
4259
|
+
* is already claimed, so the session is closed rather than reported
|
|
4260
|
+
* as prepared.
|
|
4261
|
+
*/
|
|
4262
|
+
if (preparesExistingThread && created.readiness !== "prepared") {
|
|
4263
|
+
const unpreparedId = optionalString(created.sessionId ?? created.session_id);
|
|
4264
|
+
let compensated = true;
|
|
4265
|
+
if (unpreparedId) {
|
|
4266
|
+
compensated = await brokerSession(
|
|
4267
|
+
cwd,
|
|
4268
|
+
"session.close",
|
|
4269
|
+
{ sessionId: unpreparedId },
|
|
4270
|
+
`${idempotencyKey}:unprepared-close`,
|
|
4271
|
+
).then(
|
|
4272
|
+
() => true,
|
|
4273
|
+
() => false,
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
// A swallowed compensation leaves a live, untracked session while
|
|
4277
|
+
// idempotency seals the failure, so exact retries only replay the
|
|
4278
|
+
// cached error and never reach the session again. Name the session
|
|
4279
|
+
// and mark the outcome unobserved so the key is not sealed.
|
|
4280
|
+
if (!compensated)
|
|
4281
|
+
throw new SdkClientError(
|
|
4282
|
+
UNOBSERVED_COMPENSATION_CODE,
|
|
4283
|
+
`SDK broker did not prepare the requested session, and closing the unprepared session ${unpreparedId} failed; it may still be running.`,
|
|
4284
|
+
);
|
|
4285
|
+
throw new SdkClientError(
|
|
4286
|
+
"broker_request_unavailable",
|
|
4287
|
+
"SDK broker did not prepare the requested session.",
|
|
4288
|
+
);
|
|
4289
|
+
}
|
|
4063
4290
|
sessionId = safeExternalId("session", created.sessionId ?? created.session_id);
|
|
4064
4291
|
const sessionCwd = await canonicalBrokerWorkspace(optionalString(created.cwd) ?? cwd);
|
|
4065
4292
|
const binding = await exactBrokerSessionBinding(sessionId, sessionCwd, idempotencyKey);
|
|
@@ -4076,7 +4303,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4076
4303
|
kind: "start",
|
|
4077
4304
|
session: canonicalCreationSnapshot(session),
|
|
4078
4305
|
remote_create_key: creation.request.remote_create_key,
|
|
4079
|
-
initial_state: prompt ? "running" : "ready_for_input",
|
|
4306
|
+
initial_state: prompt ? "running" : preparesExistingThread ? "prepared" : "ready_for_input",
|
|
4080
4307
|
initial_prompt: prompt
|
|
4081
4308
|
? { text: prompt, caller_key_digest: createHash("sha256").update(idempotencyKey).digest("hex") }
|
|
4082
4309
|
: null,
|
|
@@ -4117,14 +4344,18 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4117
4344
|
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
4118
4345
|
return response;
|
|
4119
4346
|
}
|
|
4120
|
-
const sessionState = await writeSessionState(
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4347
|
+
const sessionState = await writeSessionState(
|
|
4348
|
+
namespaceDir,
|
|
4349
|
+
sessionId,
|
|
4350
|
+
preparesExistingThread ? "prepared" : "ready_for_input",
|
|
4351
|
+
{ live: null, reason: null },
|
|
4352
|
+
);
|
|
4124
4353
|
await appendCoordinatorEvent(namespaceDir, {
|
|
4125
4354
|
kind: "session.started",
|
|
4126
4355
|
sessionId,
|
|
4127
|
-
summary:
|
|
4356
|
+
summary: preparesExistingThread
|
|
4357
|
+
? `Session ${sessionId} prepared through SDK lifecycle control`
|
|
4358
|
+
: `Session ${sessionId} started through SDK lifecycle control`,
|
|
4128
4359
|
payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
|
|
4129
4360
|
});
|
|
4130
4361
|
const response = {
|
|
@@ -4132,12 +4363,113 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4132
4363
|
session: publicCoordinatorSession(session),
|
|
4133
4364
|
session_state: publicCoordinatorSessionState(sessionState),
|
|
4134
4365
|
lifecycle,
|
|
4366
|
+
...(preparesExistingThread ? { session_id: sessionId, state: "prepared" as const } : {}),
|
|
4135
4367
|
};
|
|
4136
4368
|
await advanceCreationReceipt(questionPaths, creation.keyDigest, "projected", response);
|
|
4137
4369
|
await advanceCreationReceipt(questionPaths, creation.keyDigest, "completed", response);
|
|
4138
4370
|
return response;
|
|
4139
4371
|
},
|
|
4140
4372
|
true,
|
|
4373
|
+
isUnobservedCompensation,
|
|
4374
|
+
);
|
|
4375
|
+
}
|
|
4376
|
+
if (name === "gjc_coordinator_activate_session") {
|
|
4377
|
+
requireCoordinatorMutation(config, "sessions", args);
|
|
4378
|
+
const idempotencyKey = requiredIdempotencyKey(args);
|
|
4379
|
+
const sessionId = safeExternalId("session", args.session_id);
|
|
4380
|
+
return await withToolIdempotency(
|
|
4381
|
+
name,
|
|
4382
|
+
idempotencyKey,
|
|
4383
|
+
{ session_id: sessionId, allow_mutation: true },
|
|
4384
|
+
async () =>
|
|
4385
|
+
await withSessionTransition(sessionId, async () => {
|
|
4386
|
+
const currentSession = asRecord(await readJsonFile(sessionFile(sessionId)));
|
|
4387
|
+
if (!currentSession)
|
|
4388
|
+
return {
|
|
4389
|
+
ok: false,
|
|
4390
|
+
error: { code: "not_found", message: `Coordinator session not found: ${sessionId}` },
|
|
4391
|
+
};
|
|
4392
|
+
const before = await readSessionState(namespaceDir, sessionId);
|
|
4393
|
+
/**
|
|
4394
|
+
* A settled activation is not reported from durable state alone: a
|
|
4395
|
+
* session that went stale, errored, completed, or never recorded a
|
|
4396
|
+
* state cannot be answered `already`, and even a recorded
|
|
4397
|
+
* `ready_for_input` has to be re-proved against the live session
|
|
4398
|
+
* below before it may be.
|
|
4399
|
+
*/
|
|
4400
|
+
const eligibility = classifyCoordinatorActivation(before);
|
|
4401
|
+
if (!eligibility.activatable)
|
|
4402
|
+
return {
|
|
4403
|
+
ok: false,
|
|
4404
|
+
session_id: sessionId,
|
|
4405
|
+
state: eligibility.observed,
|
|
4406
|
+
session_state: publicCoordinatorSessionState(before),
|
|
4407
|
+
error: {
|
|
4408
|
+
code: "session_not_activatable",
|
|
4409
|
+
message: `Coordinator session is not activatable in state ${eligibility.observed}.`,
|
|
4410
|
+
},
|
|
4411
|
+
};
|
|
4412
|
+
let activated: ActivatedPreparedSession;
|
|
4413
|
+
try {
|
|
4414
|
+
activated = await activatePreparedCoordinatorSession(currentSession, sessionId, idempotencyKey);
|
|
4415
|
+
} catch (error) {
|
|
4416
|
+
if (!(error instanceof SessionActivationError)) throw error;
|
|
4417
|
+
return {
|
|
4418
|
+
ok: false,
|
|
4419
|
+
session_id: sessionId,
|
|
4420
|
+
state: before?.state ?? "unknown",
|
|
4421
|
+
session_state: publicCoordinatorSessionState(before),
|
|
4422
|
+
error: { code: error.code, message: error.message },
|
|
4423
|
+
};
|
|
4424
|
+
}
|
|
4425
|
+
/**
|
|
4426
|
+
* An already-ready session transitions nothing: the answer above is
|
|
4427
|
+
* the live session's own, proved at the exact endpoint generation
|
|
4428
|
+
* this call resolved, so durable state is neither rewritten nor
|
|
4429
|
+
* given a second readiness event.
|
|
4430
|
+
*/
|
|
4431
|
+
if (eligibility.settled)
|
|
4432
|
+
return {
|
|
4433
|
+
ok: true,
|
|
4434
|
+
session_id: sessionId,
|
|
4435
|
+
status: activated.status,
|
|
4436
|
+
state: "ready_for_input" as const,
|
|
4437
|
+
endpoint_generation: activated.endpointGeneration,
|
|
4438
|
+
session_state: publicCoordinatorSessionState(before),
|
|
4439
|
+
};
|
|
4440
|
+
// Only a proven `activated`/`already` moves durable state to ready.
|
|
4441
|
+
const sessionState = await writeSessionState(namespaceDir, sessionId, "ready_for_input", {
|
|
4442
|
+
live: true,
|
|
4443
|
+
reason: null,
|
|
4444
|
+
});
|
|
4445
|
+
await appendCoordinatorEvent(namespaceDir, {
|
|
4446
|
+
kind: "session.started",
|
|
4447
|
+
sessionId,
|
|
4448
|
+
summary: `Session ${sessionId} activated its withheld readiness`,
|
|
4449
|
+
payloadRef: path.relative(namespaceDir, sessionFile(sessionId)),
|
|
4450
|
+
metadata: {
|
|
4451
|
+
status: activated.status,
|
|
4452
|
+
endpoint_generation: activated.endpointGeneration,
|
|
4453
|
+
},
|
|
4454
|
+
});
|
|
4455
|
+
return {
|
|
4456
|
+
ok: true,
|
|
4457
|
+
session_id: sessionId,
|
|
4458
|
+
status: activated.status,
|
|
4459
|
+
state: "ready_for_input" as const,
|
|
4460
|
+
endpoint_generation: activated.endpointGeneration,
|
|
4461
|
+
session_state: publicCoordinatorSessionState(sessionState),
|
|
4462
|
+
};
|
|
4463
|
+
}),
|
|
4464
|
+
/**
|
|
4465
|
+
* A crash between writing the receipt and settling it leaves an
|
|
4466
|
+
* in-progress activation. Recovering it is safe because every retry
|
|
4467
|
+
* re-proves the workspace, generation, and incarnation before it
|
|
4468
|
+
* sends anything, and the session answers a repeated activation
|
|
4469
|
+
* `already` rather than publishing readiness twice.
|
|
4470
|
+
*/
|
|
4471
|
+
true,
|
|
4472
|
+
isUnknownActivationOutcome,
|
|
4141
4473
|
);
|
|
4142
4474
|
}
|
|
4143
4475
|
if (name === "gjc_coordinator_send_prompt") {
|
|
@@ -4166,6 +4498,25 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
|
|
|
4166
4498
|
error: { code: "not_found", message: `Coordinator session not found: ${sessionId}` },
|
|
4167
4499
|
};
|
|
4168
4500
|
}
|
|
4501
|
+
/**
|
|
4502
|
+
* A prepared session is not ready for input. Its readiness is still
|
|
4503
|
+
* withheld, so a prompt here would be delivered to a session no
|
|
4504
|
+
* consumer has been told is live, and (for an existing-thread
|
|
4505
|
+
* preparation) before its root binding could be applied.
|
|
4506
|
+
*/
|
|
4507
|
+
const preparedState = await readSessionState(namespaceDir, sessionId);
|
|
4508
|
+
if (preparedState?.state === "prepared") {
|
|
4509
|
+
return {
|
|
4510
|
+
ok: false,
|
|
4511
|
+
session_id: sessionId,
|
|
4512
|
+
state: "prepared" as const,
|
|
4513
|
+
error: {
|
|
4514
|
+
code: "session_not_activated",
|
|
4515
|
+
message: `Session ${sessionId} is prepared; activate it before sending a prompt.`,
|
|
4516
|
+
},
|
|
4517
|
+
session_state: publicCoordinatorSessionState(preparedState),
|
|
4518
|
+
};
|
|
4519
|
+
}
|
|
4169
4520
|
const previousActiveTurn = await readActiveTurn(namespaceDir, sessionId);
|
|
4170
4521
|
if (previousActiveTurn && args.queue !== true && args.force !== true) {
|
|
4171
4522
|
return {
|
|
@@ -122,7 +122,7 @@ Deep Interview threshold: <resolvedThresholdPercent> (source: <resolvedThreshold
|
|
|
122
122
|
- Substitute `<resolvedThreshold>`, `<resolvedThresholdPercent>`, and `<resolvedThresholdSource>` throughout the remaining instructions before continuing.
|
|
123
123
|
- Include `threshold_source` in the first `gjc deep-interview write` payload and preserve it on later state updates; do not edit `.gjc/_session-{sessionid}/state` files directly unless an explicit force override is active.
|
|
124
124
|
- Include both threshold and source in the final spec metadata.
|
|
125
|
-
- Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless `
|
|
125
|
+
- Read any `language` object from active deep-interview state and carry `language.instruction` forward mechanically. If absent, default to English unless the appended `User:` request makes another user/session language obvious or the user explicitly requests another language. Do not add language-specific special cases.
|
|
126
126
|
|
|
127
127
|
## Phase 0.5: Suitability Gate
|
|
128
128
|
|
|
@@ -308,6 +308,8 @@ One generation freezes the change set and reviews it exactly once:
|
|
|
308
308
|
- Native/desktop/tui surfaces require a structurally valid screenshot, PTY capture with terminal control codes, or app-automation transcript.
|
|
309
309
|
- API/package surfaces require a real artifact file or typed receipt whose artifact `kind` contains one of `api`, `package`, `consumer`, `black-box`, or `test-report`; examples: `api-package-test-report`, `package-consumer-report`, `black-box-api-receipt`. Algorithm/math surfaces require a real artifact file or typed receipt whose artifact `kind` contains one of `property`, `boundary`, `edge`, `adversarial`, `failure`, `math`, `algorithm`, or `test-report`; examples: `property-test-report`, `algorithm-boundary-report`. Bare `inlineEvidence` text alone is not sufficient for any surface.
|
|
310
310
|
- The mandatory **computer-use** red-team suite (`kill-switch-bypass`, `suspended-enforcement`, `permission-revoked`, …) is conditional, not universal: require it only when computer/desktop control is genuinely part of the product surface being dogfooded. For every other product type, prove the change through the matching live surface instead — browser-use automation for web/GUI, bash/CLI live invocation or argv replay for CLI, and real artifacts or typed receipts for API/package/algorithm/math. Editing docs, prompts, or skills that merely mention computer-use does not by itself make the computer-use suite applicable; pick the red-team surface that matches what the change actually ships.
|
|
311
|
+
- **The runtime decides applicability from the change set, and it fails closed.** Judgement about "what the change actually ships" does not override it, so check the paths before assuming the suite is skippable. `gjc ultragoal checkpoint --status complete` requires the suite whenever the computed change set touches computer source (`crates/pi-natives/src/computer/**`), the computer tool (`packages/coding-agent/src/tools/computer.ts`, `packages/coding-agent/src/tools/computer/**`), or a **shared behavior registry** — `packages/coding-agent/src/config/settings-schema.ts`, `packages/coding-agent/src/tools/index.ts`, `packages/coding-agent/src/tools/renderers.ts`. The registries are deliberately unconditional: they mix computer and non-computer entries, and a path-only or uninspectable change cannot prove computer controls were untouched, so *any* edit to them demands the suite even when the diff contains nothing computer-related. The suite is also required whenever change-set capture was incomplete. Generated bindings (`packages/natives/native/index.{d.ts,js}`), prompt/skill/doc files, and every other path do not trigger it on their own.
|
|
312
|
+
- Practical consequence: a change that is not about computer-use at all — say a new settings key in `settings-schema.ts` — will still be gated on the seven mandatory cases. Do **not** fabricate them to get past the gate, and do not weaken the gate. Either supply a genuine suite, or treat it as a blocker and escalate to the operator (`gjc ultragoal record-critic-gate-override` exists for an authorized override).
|
|
311
313
|
7. The executor QA/red-team lane must report a matrix using `executorQa.contractCoverage`, `executorQa.surfaceEvidence`, `executorQa.adversarialCases`, and `executorQa.artifactRefs`. Not-applicable rows are allowed only in `contractCoverage` and `surfaceEvidence`; each `status: "not_applicable"` row requires `contractRef` plus `reason`. `adversarialCases` rows cannot be not-applicable.
|
|
312
314
|
8. **Join before repairing.** Fold all three lane verdicts and the final code review into the strict gate under `iteration.reviewCohort` (`reviewGeneration`, `sourceHash`, `joined: true`, and the three `lanes`). No lane may checkpoint on its own, and no fix work starts until the findings are joined. Clean means `architectReview.architectureStatus`, `architectReview.productStatus`, and `architectReview.codeStatus` are all `"CLEAR"`, `architectReview.recommendation` is `"APPROVE"`, executor QA statuses are `"passed"`, iteration is `"passed"` with `fullRerun: true`, the cohort is joined with every lane clean and hash-bound, every evidence field is non-empty, every required matrix row is present, and every blockers array is empty. `COMMENT`, `WATCH`, `REQUEST CHANGES`, `BLOCK`, missing evidence, missing or shallow matrix rows, plan/code mismatches, or non-empty blockers are non-clean.
|
|
313
315
|
9. If the joined findings contain any blocker, do **not** checkpoint `complete` and do **not** call `goal({"op":"complete"})`. Record **one consolidated blocker batch** for all findings from the whole cohort instead of one story per lane:
|