@cmmd-center/forge 0.13.63 → 0.13.65

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/dist/bin.cjs CHANGED
@@ -66257,7 +66257,7 @@ function normalizeNumberish(value) {
66257
66257
  }
66258
66258
  //#endregion
66259
66259
  //#region package.json
66260
- var version$1 = "0.13.63";
66260
+ var version$1 = "0.13.65";
66261
66261
  //#endregion
66262
66262
  //#region src/sentry.ts
66263
66263
  const SERVER_APP_NAME = "forge-server";
@@ -89347,6 +89347,62 @@ var GitHubBroker = class {
89347
89347
  runtimeResource: "status"
89348
89348
  });
89349
89349
  }
89350
+ /**
89351
+ * On a "github" scope cache miss, re-requests it once through the existing
89352
+ * CMMD client before the caller refuses (CMMD-Center/forge#3496,
89353
+ * CMMD-Center/forge#3498). CMMD delivers the grant proactively on
89354
+ * thread.create and the kick, so a miss on a woken runtime is a cache or
89355
+ * ticket TTL problem, not a missing request: this closes the gap where the
89356
+ * ticket lands in the broker authority cache mid-request, and logs the
89357
+ * outcome on failure so the cause is visible instead of a silent refusal.
89358
+ *
89359
+ * A CMMD cloud runtime (every sprite) must still make this round trip
89360
+ * itself: `status()`'s own grant gate fails closed before ever reaching
89361
+ * the network for that runtime class (ADR-015 — it must never fall
89362
+ * through to the human JWT), so this uses `probeRuntimeGithubGrantDelivery`
89363
+ * instead, which makes the identical round trip without that JWT
89364
+ * fallback.
89365
+ */
89366
+ async requestRuntimeGithubGrant() {
89367
+ const cached = this.runtimeGrantAuthContext("github");
89368
+ if (cached) return cached;
89369
+ const runtimeId = process.env.FORGE_CMMD_RUNTIME_ID?.trim();
89370
+ if (!runtimeId) return null;
89371
+ if (this.isCmmdCloudRuntimeMode) await this.probeRuntimeGithubGrantDelivery(runtimeId);
89372
+ else await this.status({
89373
+ sessionId: AuthSessionId.make(`cmmd-runtime:${runtimeId}`),
89374
+ subject: `runtime:${runtimeId}`
89375
+ });
89376
+ const grant = this.runtimeGrantAuthContext("github");
89377
+ if (!grant) console.warn(`[github-broker] re-requested the github scope grant for runtime ${runtimeId}: still missing after one network round trip`);
89378
+ return grant;
89379
+ }
89380
+ /**
89381
+ * The cloud-runtime half of `requestRuntimeGithubGrant`'s re-request.
89382
+ *
89383
+ * CMMD's status route always requires a Bearer that already carries the
89384
+ * "github" scope (verified server-side against the presented ticket's own
89385
+ * claims — see `forge-runtime-grants.routes.ts` on CMMD main), and there is
89386
+ * no separate CMMD route that mints a "github"-scoped grant on demand. So
89387
+ * this call's own response can never carry a grant to install; its purpose
89388
+ * is the wait. CMMD delivers a fresh grant on this same runtime's inbound
89389
+ * thread.create/kick requests (`ServerAuth.authenticateHttpRequest` /
89390
+ * `authenticateWebSocketUpgrade`, which install it into the broker
89391
+ * authority cache as a side effect of authenticating each request), and a
89392
+ * delivery already in flight when the caller checked the cache gets this
89393
+ * round trip's worth of extra time to land before the caller gives up.
89394
+ * Bounded by the same timeout as every other runtime-grant call, and
89395
+ * deliberately unauthenticated: sending no Bearer is exactly as
89396
+ * informative to CMMD as sending one lacking the required scope, and
89397
+ * skips resolving one for a request whose own response is never used.
89398
+ */
89399
+ async probeRuntimeGithubGrantDelivery(runtimeId) {
89400
+ const response = await this.fetchImpl(makeUrl(this.commandAppUrl, `/api/forge/runtimes/${encodeURIComponent(runtimeId)}/github/status`), {
89401
+ method: "GET",
89402
+ signal: AbortSignal.timeout(GITHUB_BROKER_REQUEST_TIMEOUT_MS)
89403
+ }).catch(() => null);
89404
+ if (response !== null) await parseResponseBody(response);
89405
+ }
89350
89406
  async workspaceStatus(input) {
89351
89407
  const cwdValidation = validateCwd(input.cwd);
89352
89408
  if (cwdValidation) return cwdValidation;
@@ -97853,6 +97909,235 @@ function serializeProviderCatalog(providers) {
97853
97909
  return catalog;
97854
97910
  }
97855
97911
  //#endregion
97912
+ //#region src/provider/personalClaudeInVm.ts
97913
+ /**
97914
+ * CMMD writes this marker when it activates a personal Claude subscription
97915
+ * inside a running sprite. Activation always happens after boot, so a
97916
+ * boot-time env var alone cannot reflect it (FOR-36249).
97917
+ *
97918
+ * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
97919
+ * Without that, the marker sits directly under FORGE_HOME, which is the same
97920
+ * guest home CMMD derives every sprite path from. A server with neither has no
97921
+ * sprite home to look in, and a desktop never has a marker, so it reads none.
97922
+ */
97923
+ const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
97924
+ const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
97925
+ function markerPath(env) {
97926
+ if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
97927
+ if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
97928
+ return null;
97929
+ }
97930
+ /**
97931
+ * True when this runtime should offer the personal Claude subscription lane.
97932
+ *
97933
+ * Accepts either signal: the boot-time env var (backward compatible) or the
97934
+ * activation marker on disk. Reading the marker at call time lets the provider
97935
+ * catalog and the per-turn readiness guard self-heal once CMMD delivers,
97936
+ * without a sprite reboot. Callers still verify the credential is non-empty
97937
+ * before treating it as usable; this only decides whether the lane is active.
97938
+ */
97939
+ function personalClaudeInVmEnabled(env = process.env) {
97940
+ if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
97941
+ const marker = markerPath(env);
97942
+ if (!marker) return false;
97943
+ try {
97944
+ return (0, node_fs.existsSync)(marker);
97945
+ } catch {
97946
+ return false;
97947
+ }
97948
+ }
97949
+ /** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
97950
+ function bootExportedPersonalClaudeGenerationHash(env = process.env) {
97951
+ return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
97952
+ }
97953
+ /**
97954
+ * The sha256 of the Claude credential generation this runtime holds, or "" when
97955
+ * it holds none. The readiness guard compares it with the generation CMMD
97956
+ * applied. The restart proof must not use it; it uses the boot-exported hash.
97957
+ *
97958
+ * The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
97959
+ * the marker already exists at boot. Activation in place writes the generation
97960
+ * file and touches the marker without setting any env, so an empty variable
97961
+ * falls back to that file, hashed the way the preload hashes it. CMMD puts the
97962
+ * file beside the marker in the guest home and names no path for it. A marker
97963
+ * with no generation file is a legacy runtime, and gets no hash here either.
97964
+ */
97965
+ function personalClaudeGenerationHash(env = process.env) {
97966
+ const exported = bootExportedPersonalClaudeGenerationHash(env);
97967
+ if (exported) return exported;
97968
+ const marker = markerPath(env);
97969
+ if (!marker) return "";
97970
+ try {
97971
+ if (!(0, node_fs.existsSync)(marker)) return "";
97972
+ const generation = (0, node_fs.readFileSync)((0, node_path.join)((0, node_path.dirname)(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
97973
+ return generation ? (0, node_crypto.createHash)("sha256").update(generation).digest("hex") : "";
97974
+ } catch {
97975
+ return "";
97976
+ }
97977
+ }
97978
+ //#endregion
97979
+ //#region src/provider/providerAuthFailureCopy.ts
97980
+ /**
97981
+ * Rewrite provider auth failures into an action the user can actually take.
97982
+ *
97983
+ * The provider SDKs assume they are running under a human at a terminal, so
97984
+ * their auth errors name CLI verbs: Claude's is literally
97985
+ * `Not logged in · Please run /login`. On Forge Cloud that reaches a browser
97986
+ * user who has no terminal, no shell, and no way to run `/login` — the turn
97987
+ * dies in ~3 s naming an action that does not exist for them (observed live
97988
+ * 2026-08-05).
97989
+ *
97990
+ * CMMD already writes the right shape for the Codex egress path — "Connect
97991
+ * ChatGPT in Settings > Providers, then retry" — which names a destination the
97992
+ * user can click. This gives the provider-SDK failures the same treatment.
97993
+ *
97994
+ * Scope is deliberately narrow. Only messages that clearly identify a
97995
+ * *credential* problem are rewritten; everything else passes through verbatim,
97996
+ * because a rewritten unknown error loses detail an operator needs and risks
97997
+ * telling the user to reconnect an account that was never the problem.
97998
+ */
97999
+ /**
98000
+ * Signatures of "you have no usable credential", matched case-insensitively.
98001
+ *
98002
+ * Kept as explicit patterns rather than a broad /auth/i so that genuine
98003
+ * mid-turn failures mentioning authorization (a tool hitting a 403 on some
98004
+ * unrelated API, say) are not misreported as "your subscription is
98005
+ * disconnected".
98006
+ */
98007
+ const CREDENTIAL_FAILURE_PATTERNS = [
98008
+ /\bnot logged in\b/i,
98009
+ /\bplease run\s*\/login\b/i,
98010
+ /\brun\s*\/login\b/i,
98011
+ /\blogin required\b/i,
98012
+ /\bnot authenticated\b/i,
98013
+ /\bno credentials? found\b/i,
98014
+ /\binvalid api key\b/i,
98015
+ /\bauthentication_error\b/i
98016
+ ];
98017
+ /**
98018
+ * Claude's words for a delivered credential whose refresh chain is dead.
98019
+ *
98020
+ * Deliberately NOT in CREDENTIAL_FAILURE_PATTERNS. On a runtime whose Claude
98021
+ * credential CMMD delivers, this is not "you are signed out" and reconnecting
98022
+ * in Settings is not the fix: CMMD's authority rotates the refresh token on
98023
+ * every refresh, the snapshot in the guest dies at the next rotation, and the
98024
+ * repair is a re-delivery the next send triggers (FOR-36425, CMMD-36441).
98025
+ * Anywhere else the credential is the user's own local login, so the CLI text
98026
+ * still passes through untouched.
98027
+ */
98028
+ const DELIVERED_CREDENTIAL_EXPIRED_PATTERN = /\boauth session expired\b/i;
98029
+ const DELIVERED_CLAUDE_CREDENTIAL_EXPIRED_COPY = "The delivered Claude credential expired. CMMD refreshes it on the next send, so retry this turn.";
98030
+ /** True when the message is about a missing/unusable credential, not a tool failure. */
98031
+ function isProviderCredentialFailure(message) {
98032
+ return CREDENTIAL_FAILURE_PATTERNS.some((pattern) => pattern.test(message));
98033
+ }
98034
+ /**
98035
+ * Map a ProviderKind to the account name a user would recognise.
98036
+ *
98037
+ * NOTE the literal is `claudeAgent`, not `claude`. This read `"claude"` and so
98038
+ * never matched, silently degrading every Claude auth failure to the generic
98039
+ * fallback — "Forge Cloud has no **this provider** subscription connected."
98040
+ * Same provider-name drift class as the model-mapping fix (#2103/#2105).
98041
+ *
98042
+ * The names come from the shared PROVIDER_ACCOUNT_NAMES map rather than a local
98043
+ * switch: this file's copy was the ONLY correct provider→label mapping in the
98044
+ * codebase while four client call sites reimplemented it as binary ternaries.
98045
+ * One Record over ProviderKind means a new provider is a compile error in one
98046
+ * place instead of five sentences that read wrong in production.
98047
+ *
98048
+ * Providers that do not sell a personal subscription stay generic on purpose —
98049
+ * "Forge Cloud has no Ollama subscription connected" names something that does
98050
+ * not exist.
98051
+ */
98052
+ function providerLabel(provider) {
98053
+ return supportsPersonalSubscription(provider) ? PROVIDER_ACCOUNT_NAMES[provider] : "this provider";
98054
+ }
98055
+ /**
98056
+ * Return the message to show for a failed turn.
98057
+ *
98058
+ * `surface` decides the destination: a cloud runtime has no terminal, so it is
98059
+ * pointed at Settings; a local/desktop runtime CAN run the CLI login, so its
98060
+ * copy still offers that as the concrete fix rather than sending someone to a
98061
+ * settings page that is not how they authenticate.
98062
+ */
98063
+ function rewriteProviderAuthFailure(input) {
98064
+ const original = input.message ?? "";
98065
+ if (original.length === 0) return {
98066
+ message: original,
98067
+ rewritten: false
98068
+ };
98069
+ if (input.deliveredClaudeCredential === true && DELIVERED_CREDENTIAL_EXPIRED_PATTERN.test(original)) return {
98070
+ message: DELIVERED_CLAUDE_CREDENTIAL_EXPIRED_COPY,
98071
+ rewritten: true
98072
+ };
98073
+ if (!isProviderCredentialFailure(original)) return {
98074
+ message: original,
98075
+ rewritten: false
98076
+ };
98077
+ const who = providerLabel(input.provider);
98078
+ return {
98079
+ message: input.surface === "cloud" ? `Forge Cloud has no ${who} subscription connected for your account. Open Settings > Providers to connect one, then retry this turn.` : `This runtime is not signed in to ${who}. Connect it from Settings > Providers, then retry this turn.`,
98080
+ rewritten: true
98081
+ };
98082
+ }
98083
+ /**
98084
+ * Convenience for call sites that just want the string to show.
98085
+ *
98086
+ * Exists so adapters do not each re-derive the surface and re-handle the
98087
+ * undefined case inline — that inlining is what pushed
98088
+ * ClaudeAdapter.handleResultMessage over the cyclomatic gate. Returns
98089
+ * `undefined` unchanged so "no error" stays distinguishable from "empty error".
98090
+ */
98091
+ function providerAuthFailureMessage(message, provider, env = process.env) {
98092
+ if (message === void 0) return void 0;
98093
+ return rewriteProviderAuthFailure({
98094
+ message,
98095
+ provider,
98096
+ surface: env.FORGE_CMMD_RUNTIME_MODE === "true" ? "cloud" : "local",
98097
+ deliveredClaudeCredential: provider === "claudeAgent" && personalClaudeInVmEnabled(env)
98098
+ }).message;
98099
+ }
98100
+ //#endregion
98101
+ //#region src/orchestration/turnFailureReason.ts
98102
+ /**
98103
+ * Machine-readable reasons for a turn that failed before it produced any
98104
+ * assistant output, replicated to CMMD on the `turn_status` event
98105
+ * (CMMD-Center/forge#3496, CMMD-Center/CMMD#5272). Codes match the catalog
98106
+ * CMMD renders actions from (CMMD-Center/CMMD#5289): a code CMMD does not
98107
+ * recognize degrades to its own generic fallback, so a new Forge failure
98108
+ * class is safe to ship before CMMD's catalog knows about it.
98109
+ */
98110
+ const GITHUB_GRANT_DETAIL_PATTERN = /exact scoped runtime GitHub grant/i;
98111
+ /**
98112
+ * The exact-machine broker's own wording (`providerEgressToken.ts`) for all
98113
+ * three reasons it could not produce a provider token: an incomplete runtime
98114
+ * identity, no owner session bound to the request, or no live broker
98115
+ * authority. Every one of those is a real authentication refusal, not a
98116
+ * workspace or adapter failure, and the prefix is unique to that code path.
98117
+ */
98118
+ const EXACT_MACHINE_BROKER_AUTHORITY_PATTERN = /provider token issuance requires a live exact-machine broker authority/i;
98119
+ /**
98120
+ * Classifies a `provider.turn.start.failed` activity's detail. This activity
98121
+ * kind also covers non-auth causes (a draining runtime, a missing user
98122
+ * message, a rejected workspace, a start-timeout cleanup failure), so only
98123
+ * text that names a known authentication refusal is reported as one; every
98124
+ * other detail degrades to `unspecified_failure` rather than telling CMMD to
98125
+ * offer a reconnect action credentials cannot fix.
98126
+ */
98127
+ function classifyProviderTurnStartFailureReason(detail) {
98128
+ const message = detail.trim().length > 0 ? detail : "The provider could not start a session for this run.";
98129
+ return {
98130
+ code: GITHUB_GRANT_DETAIL_PATTERN.test(detail) ? "provider_start_refused_github_grant" : EXACT_MACHINE_BROKER_AUTHORITY_PATTERN.test(detail) || isProviderCredentialFailure(detail) ? "provider_start_refused_auth" : "unspecified_failure",
98131
+ message
98132
+ };
98133
+ }
98134
+ function checkpointTimeoutReason(detail) {
98135
+ return {
98136
+ code: "checkpoint_timeout",
98137
+ message: detail.trim().length > 0 ? detail : "The workspace checkpoint timed out before the turn could start."
98138
+ };
98139
+ }
98140
+ //#endregion
97856
98141
  //#region src/runtimeReplication/replicaSerializer.ts
97857
98142
  const replicaTurnStatuses = new Set([
97858
98143
  "pending",
@@ -98193,6 +98478,53 @@ function serializeFailureActivity(activity) {
98193
98478
  }
98194
98479
  };
98195
98480
  }
98481
+ /** Only the pre-turn baseline capture sets this; a post-turn capture failure
98482
+ * never blocked the turn from starting and must not fail it retroactively. */
98483
+ function blocksTurnStart(activity) {
98484
+ const payload = activity.payload;
98485
+ if (!payload || typeof payload !== "object") return false;
98486
+ return payload.blocksTurnStart === true;
98487
+ }
98488
+ function isCheckpointTimeoutDetail(detail) {
98489
+ return /timed out/i.test(detail);
98490
+ }
98491
+ /**
98492
+ * The structured reason a turn never started, when this activity is one of
98493
+ * the two known causes (CMMD-Center/forge#3496): the provider refused to
98494
+ * start a session, or the pre-turn checkpoint that must precede it timed out.
98495
+ * `null` for every other activity, including a checkpoint failure that did
98496
+ * not block turn start or was not a timeout.
98497
+ */
98498
+ function turnStartFailureReason(activity) {
98499
+ const detail = failureDetail(activity);
98500
+ if (activity.kind === "provider.turn.start.failed") return classifyProviderTurnStartFailureReason(detail ?? "");
98501
+ if (activity.kind === "checkpoint.capture.failed" && blocksTurnStart(activity) && detail !== null && isCheckpointTimeoutDetail(detail)) return checkpointTimeoutReason(detail);
98502
+ return null;
98503
+ }
98504
+ /**
98505
+ * Replicates a turn-start failure as `turn_status: failed` with a structured
98506
+ * `reason`, alongside (not instead of) the free-text `error_summary` above:
98507
+ * CMMD fails the owning Work Session from this event
98508
+ * (CMMD-Center/CMMD#5272/#5286), which `error_summary` alone cannot drive.
98509
+ */
98510
+ function serializeTurnStartFailureStatus(activity) {
98511
+ const reason = turnStartFailureReason(activity);
98512
+ if (reason === null) return null;
98513
+ return {
98514
+ sourceSequence: null,
98515
+ event: {
98516
+ eventId: `activity:${activity.id}:turn-status`,
98517
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
98518
+ ...eventTurn(activity.turnId),
98519
+ kind: "turn_status",
98520
+ status: "failed",
98521
+ reason: {
98522
+ code: reason.code,
98523
+ message: text(reason.message, 2e3) ?? reason.message
98524
+ }
98525
+ }
98526
+ };
98527
+ }
98196
98528
  const turnUsageKeys = [
98197
98529
  "uncachedInputTokens",
98198
98530
  "cachedInputTokens",
@@ -98541,7 +98873,7 @@ function serializeRuntimeThreadEvents(thread) {
98541
98873
  });
98542
98874
  const recordedTurns = serializeRecordedTurns(thread);
98543
98875
  const textGenerations = serializeTextGenerations(thread);
98544
- return [
98876
+ const priorEvents = [
98545
98877
  ...messages,
98546
98878
  ...replicaActivities,
98547
98879
  ...toolActivities,
@@ -98550,7 +98882,15 @@ function serializeRuntimeThreadEvents(thread) {
98550
98882
  ...failureActivities,
98551
98883
  ...recordedTurns,
98552
98884
  ...textGenerations
98553
- ].toSorted((left, right) => {
98885
+ ];
98886
+ const latestPriorOccurredAt = priorEvents.reduce((latest, entry) => latest === null || entry.event.occurredAt > latest ? entry.event.occurredAt : latest, null);
98887
+ const turnStartFailureStatuses = thread.activities.flatMap((activity) => {
98888
+ const event = serializeTurnStartFailureStatus(activity);
98889
+ if (event === null) return [];
98890
+ if (latestPriorOccurredAt !== null && event.event.occurredAt < latestPriorOccurredAt) return [];
98891
+ return [event];
98892
+ });
98893
+ return [...priorEvents, ...turnStartFailureStatuses].toSorted((left, right) => {
98554
98894
  if (left.sourceSequence !== null && right.sourceSequence !== null) return left.sourceSequence - right.sourceSequence;
98555
98895
  if (left.sourceSequence !== null) return -1;
98556
98896
  if (right.sourceSequence !== null) return 1;
@@ -152498,7 +152838,7 @@ async function prepareWorkspaceRepositoryDependencies(input) {
152498
152838
  const dependencies = resolveWorkspaceEnvironmentConfig(input.cwd).repositoryDependencies;
152499
152839
  if (dependencies.length === 0) return [];
152500
152840
  const broker = input.broker ?? new GitHubBroker();
152501
- const auth = broker.runtimeGrantAuthContext("github");
152841
+ const auth = broker.runtimeGrantAuthContext("github") ?? await broker.requestRuntimeGithubGrant?.() ?? null;
152502
152842
  if (auth === null) throw new Error("Repository dependencies require an exact scoped runtime GitHub grant.");
152503
152843
  const readOrigin = input.readOrigin ?? readOriginFromGit;
152504
152844
  const resolved = resolveRepositoryDependencies(dependencies, input.cloudProjectsRoot);
@@ -213506,73 +213846,6 @@ function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
213506
213846
  }
213507
213847
  }
213508
213848
  //#endregion
213509
- //#region src/provider/personalClaudeInVm.ts
213510
- /**
213511
- * CMMD writes this marker when it activates a personal Claude subscription
213512
- * inside a running sprite. Activation always happens after boot, so a
213513
- * boot-time env var alone cannot reflect it (FOR-36249).
213514
- *
213515
- * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
213516
- * Without that, the marker sits directly under FORGE_HOME, which is the same
213517
- * guest home CMMD derives every sprite path from. A server with neither has no
213518
- * sprite home to look in, and a desktop never has a marker, so it reads none.
213519
- */
213520
- const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
213521
- const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
213522
- function markerPath(env) {
213523
- if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
213524
- if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
213525
- return null;
213526
- }
213527
- /**
213528
- * True when this runtime should offer the personal Claude subscription lane.
213529
- *
213530
- * Accepts either signal: the boot-time env var (backward compatible) or the
213531
- * activation marker on disk. Reading the marker at call time lets the provider
213532
- * catalog and the per-turn readiness guard self-heal once CMMD delivers,
213533
- * without a sprite reboot. Callers still verify the credential is non-empty
213534
- * before treating it as usable; this only decides whether the lane is active.
213535
- */
213536
- function personalClaudeInVmEnabled(env = process.env) {
213537
- if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
213538
- const marker = markerPath(env);
213539
- if (!marker) return false;
213540
- try {
213541
- return (0, node_fs.existsSync)(marker);
213542
- } catch {
213543
- return false;
213544
- }
213545
- }
213546
- /** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
213547
- function bootExportedPersonalClaudeGenerationHash(env = process.env) {
213548
- return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
213549
- }
213550
- /**
213551
- * The sha256 of the Claude credential generation this runtime holds, or "" when
213552
- * it holds none. The readiness guard compares it with the generation CMMD
213553
- * applied. The restart proof must not use it; it uses the boot-exported hash.
213554
- *
213555
- * The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
213556
- * the marker already exists at boot. Activation in place writes the generation
213557
- * file and touches the marker without setting any env, so an empty variable
213558
- * falls back to that file, hashed the way the preload hashes it. CMMD puts the
213559
- * file beside the marker in the guest home and names no path for it. A marker
213560
- * with no generation file is a legacy runtime, and gets no hash here either.
213561
- */
213562
- function personalClaudeGenerationHash(env = process.env) {
213563
- const exported = bootExportedPersonalClaudeGenerationHash(env);
213564
- if (exported) return exported;
213565
- const marker = markerPath(env);
213566
- if (!marker) return "";
213567
- try {
213568
- if (!(0, node_fs.existsSync)(marker)) return "";
213569
- const generation = (0, node_fs.readFileSync)((0, node_path.join)((0, node_path.dirname)(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
213570
- return generation ? (0, node_crypto.createHash)("sha256").update(generation).digest("hex") : "";
213571
- } catch {
213572
- return "";
213573
- }
213574
- }
213575
- //#endregion
213576
213849
  //#region src/provider/claudeCredentialContainment.ts
213577
213850
  /**
213578
213851
  * Containment for provider credentials inherited by the Claude Agent SDK child.
@@ -215656,128 +215929,6 @@ function createGitHubMcpServer(runBrokeredTool, threadId, settings) {
215656
215929
  });
215657
215930
  }
215658
215931
  //#endregion
215659
- //#region src/provider/providerAuthFailureCopy.ts
215660
- /**
215661
- * Rewrite provider auth failures into an action the user can actually take.
215662
- *
215663
- * The provider SDKs assume they are running under a human at a terminal, so
215664
- * their auth errors name CLI verbs: Claude's is literally
215665
- * `Not logged in · Please run /login`. On Forge Cloud that reaches a browser
215666
- * user who has no terminal, no shell, and no way to run `/login` — the turn
215667
- * dies in ~3 s naming an action that does not exist for them (observed live
215668
- * 2026-08-05).
215669
- *
215670
- * CMMD already writes the right shape for the Codex egress path — "Connect
215671
- * ChatGPT in Settings > Providers, then retry" — which names a destination the
215672
- * user can click. This gives the provider-SDK failures the same treatment.
215673
- *
215674
- * Scope is deliberately narrow. Only messages that clearly identify a
215675
- * *credential* problem are rewritten; everything else passes through verbatim,
215676
- * because a rewritten unknown error loses detail an operator needs and risks
215677
- * telling the user to reconnect an account that was never the problem.
215678
- */
215679
- /**
215680
- * Signatures of "you have no usable credential", matched case-insensitively.
215681
- *
215682
- * Kept as explicit patterns rather than a broad /auth/i so that genuine
215683
- * mid-turn failures mentioning authorization (a tool hitting a 403 on some
215684
- * unrelated API, say) are not misreported as "your subscription is
215685
- * disconnected".
215686
- */
215687
- const CREDENTIAL_FAILURE_PATTERNS = [
215688
- /\bnot logged in\b/i,
215689
- /\bplease run\s*\/login\b/i,
215690
- /\brun\s*\/login\b/i,
215691
- /\blogin required\b/i,
215692
- /\bnot authenticated\b/i,
215693
- /\bno credentials? found\b/i,
215694
- /\binvalid api key\b/i,
215695
- /\bauthentication_error\b/i
215696
- ];
215697
- /**
215698
- * Claude's words for a delivered credential whose refresh chain is dead.
215699
- *
215700
- * Deliberately NOT in CREDENTIAL_FAILURE_PATTERNS. On a runtime whose Claude
215701
- * credential CMMD delivers, this is not "you are signed out" and reconnecting
215702
- * in Settings is not the fix: CMMD's authority rotates the refresh token on
215703
- * every refresh, the snapshot in the guest dies at the next rotation, and the
215704
- * repair is a re-delivery the next send triggers (FOR-36425, CMMD-36441).
215705
- * Anywhere else the credential is the user's own local login, so the CLI text
215706
- * still passes through untouched.
215707
- */
215708
- const DELIVERED_CREDENTIAL_EXPIRED_PATTERN = /\boauth session expired\b/i;
215709
- const DELIVERED_CLAUDE_CREDENTIAL_EXPIRED_COPY = "The delivered Claude credential expired. CMMD refreshes it on the next send, so retry this turn.";
215710
- /** True when the message is about a missing/unusable credential, not a tool failure. */
215711
- function isProviderCredentialFailure(message) {
215712
- return CREDENTIAL_FAILURE_PATTERNS.some((pattern) => pattern.test(message));
215713
- }
215714
- /**
215715
- * Map a ProviderKind to the account name a user would recognise.
215716
- *
215717
- * NOTE the literal is `claudeAgent`, not `claude`. This read `"claude"` and so
215718
- * never matched, silently degrading every Claude auth failure to the generic
215719
- * fallback — "Forge Cloud has no **this provider** subscription connected."
215720
- * Same provider-name drift class as the model-mapping fix (#2103/#2105).
215721
- *
215722
- * The names come from the shared PROVIDER_ACCOUNT_NAMES map rather than a local
215723
- * switch: this file's copy was the ONLY correct provider→label mapping in the
215724
- * codebase while four client call sites reimplemented it as binary ternaries.
215725
- * One Record over ProviderKind means a new provider is a compile error in one
215726
- * place instead of five sentences that read wrong in production.
215727
- *
215728
- * Providers that do not sell a personal subscription stay generic on purpose —
215729
- * "Forge Cloud has no Ollama subscription connected" names something that does
215730
- * not exist.
215731
- */
215732
- function providerLabel(provider) {
215733
- return supportsPersonalSubscription(provider) ? PROVIDER_ACCOUNT_NAMES[provider] : "this provider";
215734
- }
215735
- /**
215736
- * Return the message to show for a failed turn.
215737
- *
215738
- * `surface` decides the destination: a cloud runtime has no terminal, so it is
215739
- * pointed at Settings; a local/desktop runtime CAN run the CLI login, so its
215740
- * copy still offers that as the concrete fix rather than sending someone to a
215741
- * settings page that is not how they authenticate.
215742
- */
215743
- function rewriteProviderAuthFailure(input) {
215744
- const original = input.message ?? "";
215745
- if (original.length === 0) return {
215746
- message: original,
215747
- rewritten: false
215748
- };
215749
- if (input.deliveredClaudeCredential === true && DELIVERED_CREDENTIAL_EXPIRED_PATTERN.test(original)) return {
215750
- message: DELIVERED_CLAUDE_CREDENTIAL_EXPIRED_COPY,
215751
- rewritten: true
215752
- };
215753
- if (!isProviderCredentialFailure(original)) return {
215754
- message: original,
215755
- rewritten: false
215756
- };
215757
- const who = providerLabel(input.provider);
215758
- return {
215759
- message: input.surface === "cloud" ? `Forge Cloud has no ${who} subscription connected for your account. Open Settings > Providers to connect one, then retry this turn.` : `This runtime is not signed in to ${who}. Connect it from Settings > Providers, then retry this turn.`,
215760
- rewritten: true
215761
- };
215762
- }
215763
- /**
215764
- * Convenience for call sites that just want the string to show.
215765
- *
215766
- * Exists so adapters do not each re-derive the surface and re-handle the
215767
- * undefined case inline — that inlining is what pushed
215768
- * ClaudeAdapter.handleResultMessage over the cyclomatic gate. Returns
215769
- * `undefined` unchanged so "no error" stays distinguishable from "empty error".
215770
- */
215771
- function providerAuthFailureMessage(message, provider, env = process.env) {
215772
- if (message === void 0) return void 0;
215773
- return rewriteProviderAuthFailure({
215774
- message,
215775
- provider,
215776
- surface: env.FORGE_CMMD_RUNTIME_MODE === "true" ? "cloud" : "local",
215777
- deliveredClaudeCredential: provider === "claudeAgent" && personalClaudeInVmEnabled(env)
215778
- }).message;
215779
- }
215780
- //#endregion
215781
215932
  //#region src/provider/missingWorkspaceCopy.ts
215782
215933
  /**
215783
215934
  * Refuse a turn whose checkout folder is gone, and say so.
@@ -234138,6 +234289,8 @@ const CheckpointDiffQueryLive = require_Schema$1.effect(CheckpointDiffQuery, mak
234138
234289
  * @module CheckpointStoreLive
234139
234290
  */
234140
234291
  const CHECKPOINT_PATH_OUTPUT_MAX_BYTES = 16 * 1024 * 1024;
234292
+ const CheckpointCaptureTimeoutConfig = require_Config.number("FORGE_CHECKPOINT_CAPTURE_TIMEOUT_MS").pipe(require_Config.withDefault(6e4));
234293
+ const EFFECT_TIMER_MAX_MS = 2147483647;
234141
234294
  function parseNullSeparatedPaths(output) {
234142
234295
  return output.split("\0").filter((entry) => entry.length > 0);
234143
234296
  }
@@ -234145,7 +234298,7 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234145
234298
  const fs = yield* require_Path.FileSystem;
234146
234299
  const path = yield* require_Path.Path;
234147
234300
  const git = yield* GitCore;
234148
- const resolveHeadCommit = (cwd) => git.execute({
234301
+ const resolveHeadCommit = (cwd, timeoutMs) => git.execute({
234149
234302
  operation: "CheckpointStore.resolveHeadCommit",
234150
234303
  cwd,
234151
234304
  args: [
@@ -234154,7 +234307,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234154
234307
  "--quiet",
234155
234308
  "HEAD^{commit}"
234156
234309
  ],
234157
- allowNonZeroExit: true
234310
+ allowNonZeroExit: true,
234311
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
234158
234312
  }).pipe(require_Schema$1.map((result) => {
234159
234313
  if (result.code !== 0) return null;
234160
234314
  const commit = result.stdout.trim();
@@ -234193,7 +234347,9 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234193
234347
  }).pipe(require_Schema$1.map((result) => result.code === 0 && result.stdout.trim() === "true"), require_Schema$1.catch_(() => require_Schema$1.succeed(false)));
234194
234348
  const captureCheckpoint = require_Schema$1.fn("captureCheckpoint")(function* (input) {
234195
234349
  const operation = "CheckpointStore.captureCheckpoint";
234196
- const headCommit = yield* resolveHeadCommit(input.cwd);
234350
+ const captureTimeoutMs = yield* CheckpointCaptureTimeoutConfig.asEffect().pipe(require_Schema$1.orDie);
234351
+ if (!Number.isFinite(captureTimeoutMs) || captureTimeoutMs <= 0 || captureTimeoutMs > EFFECT_TIMER_MAX_MS) return yield* require_Schema$1.die(/* @__PURE__ */ new Error(`FORGE_CHECKPOINT_CAPTURE_TIMEOUT_MS must be a finite number greater than zero and at most ${EFFECT_TIMER_MAX_MS}, got ${captureTimeoutMs}.`));
234352
+ const headCommit = yield* resolveHeadCommit(input.cwd, captureTimeoutMs);
234197
234353
  const [trackedPathResult, untrackedPathResult] = yield* require_Schema$1.all([git.execute({
234198
234354
  operation,
234199
234355
  cwd: input.cwd,
@@ -234210,7 +234366,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234210
234366
  "-z",
234211
234367
  "--"
234212
234368
  ],
234213
- maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES
234369
+ maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES,
234370
+ timeoutMs: captureTimeoutMs
234214
234371
  }), git.execute({
234215
234372
  operation,
234216
234373
  cwd: input.cwd,
@@ -234221,7 +234378,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234221
234378
  "-z",
234222
234379
  "--"
234223
234380
  ],
234224
- maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES
234381
+ maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES,
234382
+ timeoutMs: captureTimeoutMs
234225
234383
  })], { concurrency: 2 });
234226
234384
  const changedPaths = [...new Set([...parseNullSeparatedPaths(trackedPathResult.stdout), ...parseNullSeparatedPaths(untrackedPathResult.stdout)])];
234227
234385
  if (headCommit && changedPaths.length === 0) {
@@ -234232,7 +234390,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234232
234390
  "update-ref",
234233
234391
  input.checkpointRef,
234234
234392
  headCommit
234235
- ]
234393
+ ],
234394
+ timeoutMs: captureTimeoutMs
234236
234395
  });
234237
234396
  return;
234238
234397
  }
@@ -234250,13 +234409,15 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234250
234409
  operation,
234251
234410
  cwd: input.cwd,
234252
234411
  args: ["read-tree", headCommit],
234253
- env: commitEnv
234412
+ env: commitEnv,
234413
+ timeoutMs: captureTimeoutMs
234254
234414
  });
234255
234415
  else yield* git.execute({
234256
234416
  operation,
234257
234417
  cwd: input.cwd,
234258
234418
  args: ["read-tree", "--empty"],
234259
- env: commitEnv
234419
+ env: commitEnv,
234420
+ timeoutMs: captureTimeoutMs
234260
234421
  });
234261
234422
  if (changedPaths.length > 0) yield* git.execute({
234262
234423
  operation,
@@ -234269,13 +234430,15 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234269
234430
  "--pathspec-file-nul"
234270
234431
  ],
234271
234432
  stdin: changedPaths.map((changedPath) => `${changedPath}\0`).join(""),
234272
- env: commitEnv
234433
+ env: commitEnv,
234434
+ timeoutMs: captureTimeoutMs
234273
234435
  });
234274
234436
  const treeOid = (yield* git.execute({
234275
234437
  operation,
234276
234438
  cwd: input.cwd,
234277
234439
  args: ["write-tree"],
234278
- env: commitEnv
234440
+ env: commitEnv,
234441
+ timeoutMs: captureTimeoutMs
234279
234442
  })).stdout.trim();
234280
234443
  if (treeOid.length === 0) return yield* new GitCommandError({
234281
234444
  operation,
@@ -234293,7 +234456,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234293
234456
  "-m",
234294
234457
  message
234295
234458
  ],
234296
- env: commitEnv
234459
+ env: commitEnv,
234460
+ timeoutMs: captureTimeoutMs
234297
234461
  })).stdout.trim();
234298
234462
  if (commitOid.length === 0) return yield* new GitCommandError({
234299
234463
  operation,
@@ -234308,7 +234472,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234308
234472
  "update-ref",
234309
234473
  input.checkpointRef,
234310
234474
  commitOid
234311
- ]
234475
+ ],
234476
+ timeoutMs: captureTimeoutMs
234312
234477
  });
234313
234478
  }), (tempDir) => fs.remove(tempDir, { recursive: true })).pipe(require_Schema$1.catchTags({ PlatformError: (error) => require_Schema$1.fail(new CheckpointInvariantError({
234314
234479
  operation: "CheckpointStore.captureCheckpoint",
@@ -292201,7 +292366,7 @@ const make$8 = require_Schema$1.gen(function* () {
292201
292366
  summary: "Agent turn blocked",
292202
292367
  detail: "Forge runtime is updating after the current turn. New agent turns are paused until the runtime finishes draining.",
292203
292368
  turnId: null,
292204
- createdAt: input.createdAt
292369
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
292205
292370
  });
292206
292371
  return;
292207
292372
  }
@@ -292436,7 +292601,7 @@ const make$8 = require_Schema$1.gen(function* () {
292436
292601
  summary: "Agent turn blocked",
292437
292602
  detail: workspaceError,
292438
292603
  turnId: null,
292439
- createdAt: event.payload.createdAt
292604
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
292440
292605
  });
292441
292606
  return;
292442
292607
  }
@@ -293528,7 +293693,10 @@ const make$7 = require_Schema$1.gen(function* () {
293528
293693
  tone: "error",
293529
293694
  kind: "checkpoint.capture.failed",
293530
293695
  summary: "Checkpoint capture failed",
293531
- payload: { detail: input.detail },
293696
+ payload: {
293697
+ detail: input.detail,
293698
+ ...input.blocksTurnStart ? { blocksTurnStart: true } : {}
293699
+ },
293532
293700
  turnId: input.turnId,
293533
293701
  createdAt: input.createdAt
293534
293702
  },
@@ -293814,7 +293982,13 @@ const make$7 = require_Schema$1.gen(function* () {
293814
293982
  yield* checkpointStore.captureCheckpoint({
293815
293983
  cwd: checkpointCwd,
293816
293984
  checkpointRef: baselineCheckpointRef
293817
- });
293985
+ }).pipe(require_Schema$1.catch_((error) => appendCaptureFailureActivity({
293986
+ threadId,
293987
+ turnId: null,
293988
+ detail: error.message,
293989
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
293990
+ blocksTurnStart: true
293991
+ }).pipe(require_Schema$1.catch_(() => require_Schema$1.void_))));
293818
293992
  yield* receiptBus.publish({
293819
293993
  type: "checkpoint.baseline.captured",
293820
293994
  threadId,
@@ -298206,7 +298380,7 @@ function resolveBuildCommitFromEnv(env) {
298206
298380
  * environment descriptor down instead of reporting an honest "unknown".
298207
298381
  */
298208
298382
  function readBakedBuildCommit() {
298209
- return "63701057dccde6ea0a120ebb0a1e1ae8c461b056";
298383
+ return "06c11aad2e4673f13061c7bed6d0a4cb63ff8122";
298210
298384
  }
298211
298385
  async function resolveServerBuildCommit(input) {
298212
298386
  if (isFullCommitSha(input.baked)) return input.baked;