@cmmd-center/forge 0.13.63 → 0.13.64

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.64";
66261
66261
  //#endregion
66262
66262
  //#region src/sentry.ts
66263
66263
  const SERVER_APP_NAME = "forge-server";
@@ -89347,6 +89347,28 @@ 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). CMMD
89353
+ * delivers the grant proactively on thread.create and the kick, so a miss
89354
+ * on a woken runtime is a cache or ticket TTL problem, not a missing
89355
+ * request: this closes the gap where the ticket lands in the broker
89356
+ * authority cache mid-request, and logs the outcome either way so the
89357
+ * cause is visible instead of a silent refusal.
89358
+ */
89359
+ async requestRuntimeGithubGrant() {
89360
+ const cached = this.runtimeGrantAuthContext("github");
89361
+ if (cached) return cached;
89362
+ const runtimeId = process.env.FORGE_CMMD_RUNTIME_ID?.trim();
89363
+ if (!runtimeId) return null;
89364
+ const result = await this.status({
89365
+ sessionId: AuthSessionId.make(`cmmd-runtime:${runtimeId}`),
89366
+ subject: `runtime:${runtimeId}`
89367
+ });
89368
+ const grant = this.runtimeGrantAuthContext("github");
89369
+ console.warn(`[github-broker] re-requested the github scope grant for runtime ${runtimeId}: ${grant ? "cache populated" : `still missing (broker answered ${result.ok ? "ok" : result.code ?? "no grant"})`}`);
89370
+ return grant;
89371
+ }
89350
89372
  async workspaceStatus(input) {
89351
89373
  const cwdValidation = validateCwd(input.cwd);
89352
89374
  if (cwdValidation) return cwdValidation;
@@ -97853,6 +97875,235 @@ function serializeProviderCatalog(providers) {
97853
97875
  return catalog;
97854
97876
  }
97855
97877
  //#endregion
97878
+ //#region src/provider/personalClaudeInVm.ts
97879
+ /**
97880
+ * CMMD writes this marker when it activates a personal Claude subscription
97881
+ * inside a running sprite. Activation always happens after boot, so a
97882
+ * boot-time env var alone cannot reflect it (FOR-36249).
97883
+ *
97884
+ * CMMD names the file in the Service env (FORGE_PERSONAL_CLAUDE_MARKER_PATH).
97885
+ * Without that, the marker sits directly under FORGE_HOME, which is the same
97886
+ * guest home CMMD derives every sprite path from. A server with neither has no
97887
+ * sprite home to look in, and a desktop never has a marker, so it reads none.
97888
+ */
97889
+ const PERSONAL_CLAUDE_IN_VM_MARKER_NAME = ".forge-personal-claude-in-vm";
97890
+ const PERSONAL_CLAUDE_GENERATION_FILE_NAME = ".forge-personal-claude-generation";
97891
+ function markerPath(env) {
97892
+ if (env.FORGE_PERSONAL_CLAUDE_MARKER_PATH) return env.FORGE_PERSONAL_CLAUDE_MARKER_PATH;
97893
+ if (env.FORGE_HOME) return (0, node_path.join)(env.FORGE_HOME, PERSONAL_CLAUDE_IN_VM_MARKER_NAME);
97894
+ return null;
97895
+ }
97896
+ /**
97897
+ * True when this runtime should offer the personal Claude subscription lane.
97898
+ *
97899
+ * Accepts either signal: the boot-time env var (backward compatible) or the
97900
+ * activation marker on disk. Reading the marker at call time lets the provider
97901
+ * catalog and the per-turn readiness guard self-heal once CMMD delivers,
97902
+ * without a sprite reboot. Callers still verify the credential is non-empty
97903
+ * before treating it as usable; this only decides whether the lane is active.
97904
+ */
97905
+ function personalClaudeInVmEnabled(env = process.env) {
97906
+ if (env.FORGE_PERSONAL_CLAUDE_IN_VM === "true") return true;
97907
+ const marker = markerPath(env);
97908
+ if (!marker) return false;
97909
+ try {
97910
+ return (0, node_fs.existsSync)(marker);
97911
+ } catch {
97912
+ return false;
97913
+ }
97914
+ }
97915
+ /** Exported at boot. This is the restart proof CMMD waits on, so it must never read the file. */
97916
+ function bootExportedPersonalClaudeGenerationHash(env = process.env) {
97917
+ return env.FORGE_PERSONAL_CLAUDE_GENERATION_HASH?.trim() ?? "";
97918
+ }
97919
+ /**
97920
+ * The sha256 of the Claude credential generation this runtime holds, or "" when
97921
+ * it holds none. The readiness guard compares it with the generation CMMD
97922
+ * applied. The restart proof must not use it; it uses the boot-exported hash.
97923
+ *
97924
+ * The boot preload exports FORGE_PERSONAL_CLAUDE_GENERATION_HASH, but only when
97925
+ * the marker already exists at boot. Activation in place writes the generation
97926
+ * file and touches the marker without setting any env, so an empty variable
97927
+ * falls back to that file, hashed the way the preload hashes it. CMMD puts the
97928
+ * file beside the marker in the guest home and names no path for it. A marker
97929
+ * with no generation file is a legacy runtime, and gets no hash here either.
97930
+ */
97931
+ function personalClaudeGenerationHash(env = process.env) {
97932
+ const exported = bootExportedPersonalClaudeGenerationHash(env);
97933
+ if (exported) return exported;
97934
+ const marker = markerPath(env);
97935
+ if (!marker) return "";
97936
+ try {
97937
+ if (!(0, node_fs.existsSync)(marker)) return "";
97938
+ const generation = (0, node_fs.readFileSync)((0, node_path.join)((0, node_path.dirname)(marker), PERSONAL_CLAUDE_GENERATION_FILE_NAME), "utf8").trim();
97939
+ return generation ? (0, node_crypto.createHash)("sha256").update(generation).digest("hex") : "";
97940
+ } catch {
97941
+ return "";
97942
+ }
97943
+ }
97944
+ //#endregion
97945
+ //#region src/provider/providerAuthFailureCopy.ts
97946
+ /**
97947
+ * Rewrite provider auth failures into an action the user can actually take.
97948
+ *
97949
+ * The provider SDKs assume they are running under a human at a terminal, so
97950
+ * their auth errors name CLI verbs: Claude's is literally
97951
+ * `Not logged in · Please run /login`. On Forge Cloud that reaches a browser
97952
+ * user who has no terminal, no shell, and no way to run `/login` — the turn
97953
+ * dies in ~3 s naming an action that does not exist for them (observed live
97954
+ * 2026-08-05).
97955
+ *
97956
+ * CMMD already writes the right shape for the Codex egress path — "Connect
97957
+ * ChatGPT in Settings > Providers, then retry" — which names a destination the
97958
+ * user can click. This gives the provider-SDK failures the same treatment.
97959
+ *
97960
+ * Scope is deliberately narrow. Only messages that clearly identify a
97961
+ * *credential* problem are rewritten; everything else passes through verbatim,
97962
+ * because a rewritten unknown error loses detail an operator needs and risks
97963
+ * telling the user to reconnect an account that was never the problem.
97964
+ */
97965
+ /**
97966
+ * Signatures of "you have no usable credential", matched case-insensitively.
97967
+ *
97968
+ * Kept as explicit patterns rather than a broad /auth/i so that genuine
97969
+ * mid-turn failures mentioning authorization (a tool hitting a 403 on some
97970
+ * unrelated API, say) are not misreported as "your subscription is
97971
+ * disconnected".
97972
+ */
97973
+ const CREDENTIAL_FAILURE_PATTERNS = [
97974
+ /\bnot logged in\b/i,
97975
+ /\bplease run\s*\/login\b/i,
97976
+ /\brun\s*\/login\b/i,
97977
+ /\blogin required\b/i,
97978
+ /\bnot authenticated\b/i,
97979
+ /\bno credentials? found\b/i,
97980
+ /\binvalid api key\b/i,
97981
+ /\bauthentication_error\b/i
97982
+ ];
97983
+ /**
97984
+ * Claude's words for a delivered credential whose refresh chain is dead.
97985
+ *
97986
+ * Deliberately NOT in CREDENTIAL_FAILURE_PATTERNS. On a runtime whose Claude
97987
+ * credential CMMD delivers, this is not "you are signed out" and reconnecting
97988
+ * in Settings is not the fix: CMMD's authority rotates the refresh token on
97989
+ * every refresh, the snapshot in the guest dies at the next rotation, and the
97990
+ * repair is a re-delivery the next send triggers (FOR-36425, CMMD-36441).
97991
+ * Anywhere else the credential is the user's own local login, so the CLI text
97992
+ * still passes through untouched.
97993
+ */
97994
+ const DELIVERED_CREDENTIAL_EXPIRED_PATTERN = /\boauth session expired\b/i;
97995
+ const DELIVERED_CLAUDE_CREDENTIAL_EXPIRED_COPY = "The delivered Claude credential expired. CMMD refreshes it on the next send, so retry this turn.";
97996
+ /** True when the message is about a missing/unusable credential, not a tool failure. */
97997
+ function isProviderCredentialFailure(message) {
97998
+ return CREDENTIAL_FAILURE_PATTERNS.some((pattern) => pattern.test(message));
97999
+ }
98000
+ /**
98001
+ * Map a ProviderKind to the account name a user would recognise.
98002
+ *
98003
+ * NOTE the literal is `claudeAgent`, not `claude`. This read `"claude"` and so
98004
+ * never matched, silently degrading every Claude auth failure to the generic
98005
+ * fallback — "Forge Cloud has no **this provider** subscription connected."
98006
+ * Same provider-name drift class as the model-mapping fix (#2103/#2105).
98007
+ *
98008
+ * The names come from the shared PROVIDER_ACCOUNT_NAMES map rather than a local
98009
+ * switch: this file's copy was the ONLY correct provider→label mapping in the
98010
+ * codebase while four client call sites reimplemented it as binary ternaries.
98011
+ * One Record over ProviderKind means a new provider is a compile error in one
98012
+ * place instead of five sentences that read wrong in production.
98013
+ *
98014
+ * Providers that do not sell a personal subscription stay generic on purpose —
98015
+ * "Forge Cloud has no Ollama subscription connected" names something that does
98016
+ * not exist.
98017
+ */
98018
+ function providerLabel(provider) {
98019
+ return supportsPersonalSubscription(provider) ? PROVIDER_ACCOUNT_NAMES[provider] : "this provider";
98020
+ }
98021
+ /**
98022
+ * Return the message to show for a failed turn.
98023
+ *
98024
+ * `surface` decides the destination: a cloud runtime has no terminal, so it is
98025
+ * pointed at Settings; a local/desktop runtime CAN run the CLI login, so its
98026
+ * copy still offers that as the concrete fix rather than sending someone to a
98027
+ * settings page that is not how they authenticate.
98028
+ */
98029
+ function rewriteProviderAuthFailure(input) {
98030
+ const original = input.message ?? "";
98031
+ if (original.length === 0) return {
98032
+ message: original,
98033
+ rewritten: false
98034
+ };
98035
+ if (input.deliveredClaudeCredential === true && DELIVERED_CREDENTIAL_EXPIRED_PATTERN.test(original)) return {
98036
+ message: DELIVERED_CLAUDE_CREDENTIAL_EXPIRED_COPY,
98037
+ rewritten: true
98038
+ };
98039
+ if (!isProviderCredentialFailure(original)) return {
98040
+ message: original,
98041
+ rewritten: false
98042
+ };
98043
+ const who = providerLabel(input.provider);
98044
+ return {
98045
+ 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.`,
98046
+ rewritten: true
98047
+ };
98048
+ }
98049
+ /**
98050
+ * Convenience for call sites that just want the string to show.
98051
+ *
98052
+ * Exists so adapters do not each re-derive the surface and re-handle the
98053
+ * undefined case inline — that inlining is what pushed
98054
+ * ClaudeAdapter.handleResultMessage over the cyclomatic gate. Returns
98055
+ * `undefined` unchanged so "no error" stays distinguishable from "empty error".
98056
+ */
98057
+ function providerAuthFailureMessage(message, provider, env = process.env) {
98058
+ if (message === void 0) return void 0;
98059
+ return rewriteProviderAuthFailure({
98060
+ message,
98061
+ provider,
98062
+ surface: env.FORGE_CMMD_RUNTIME_MODE === "true" ? "cloud" : "local",
98063
+ deliveredClaudeCredential: provider === "claudeAgent" && personalClaudeInVmEnabled(env)
98064
+ }).message;
98065
+ }
98066
+ //#endregion
98067
+ //#region src/orchestration/turnFailureReason.ts
98068
+ /**
98069
+ * Machine-readable reasons for a turn that failed before it produced any
98070
+ * assistant output, replicated to CMMD on the `turn_status` event
98071
+ * (CMMD-Center/forge#3496, CMMD-Center/CMMD#5272). Codes match the catalog
98072
+ * CMMD renders actions from (CMMD-Center/CMMD#5289): a code CMMD does not
98073
+ * recognize degrades to its own generic fallback, so a new Forge failure
98074
+ * class is safe to ship before CMMD's catalog knows about it.
98075
+ */
98076
+ const GITHUB_GRANT_DETAIL_PATTERN = /exact scoped runtime GitHub grant/i;
98077
+ /**
98078
+ * The exact-machine broker's own wording (`providerEgressToken.ts`) for all
98079
+ * three reasons it could not produce a provider token: an incomplete runtime
98080
+ * identity, no owner session bound to the request, or no live broker
98081
+ * authority. Every one of those is a real authentication refusal, not a
98082
+ * workspace or adapter failure, and the prefix is unique to that code path.
98083
+ */
98084
+ const EXACT_MACHINE_BROKER_AUTHORITY_PATTERN = /provider token issuance requires a live exact-machine broker authority/i;
98085
+ /**
98086
+ * Classifies a `provider.turn.start.failed` activity's detail. This activity
98087
+ * kind also covers non-auth causes (a draining runtime, a missing user
98088
+ * message, a rejected workspace, a start-timeout cleanup failure), so only
98089
+ * text that names a known authentication refusal is reported as one; every
98090
+ * other detail degrades to `unspecified_failure` rather than telling CMMD to
98091
+ * offer a reconnect action credentials cannot fix.
98092
+ */
98093
+ function classifyProviderTurnStartFailureReason(detail) {
98094
+ const message = detail.trim().length > 0 ? detail : "The provider could not start a session for this run.";
98095
+ return {
98096
+ 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",
98097
+ message
98098
+ };
98099
+ }
98100
+ function checkpointTimeoutReason(detail) {
98101
+ return {
98102
+ code: "checkpoint_timeout",
98103
+ message: detail.trim().length > 0 ? detail : "The workspace checkpoint timed out before the turn could start."
98104
+ };
98105
+ }
98106
+ //#endregion
97856
98107
  //#region src/runtimeReplication/replicaSerializer.ts
97857
98108
  const replicaTurnStatuses = new Set([
97858
98109
  "pending",
@@ -98193,6 +98444,53 @@ function serializeFailureActivity(activity) {
98193
98444
  }
98194
98445
  };
98195
98446
  }
98447
+ /** Only the pre-turn baseline capture sets this; a post-turn capture failure
98448
+ * never blocked the turn from starting and must not fail it retroactively. */
98449
+ function blocksTurnStart(activity) {
98450
+ const payload = activity.payload;
98451
+ if (!payload || typeof payload !== "object") return false;
98452
+ return payload.blocksTurnStart === true;
98453
+ }
98454
+ function isCheckpointTimeoutDetail(detail) {
98455
+ return /timed out/i.test(detail);
98456
+ }
98457
+ /**
98458
+ * The structured reason a turn never started, when this activity is one of
98459
+ * the two known causes (CMMD-Center/forge#3496): the provider refused to
98460
+ * start a session, or the pre-turn checkpoint that must precede it timed out.
98461
+ * `null` for every other activity, including a checkpoint failure that did
98462
+ * not block turn start or was not a timeout.
98463
+ */
98464
+ function turnStartFailureReason(activity) {
98465
+ const detail = failureDetail(activity);
98466
+ if (activity.kind === "provider.turn.start.failed") return classifyProviderTurnStartFailureReason(detail ?? "");
98467
+ if (activity.kind === "checkpoint.capture.failed" && blocksTurnStart(activity) && detail !== null && isCheckpointTimeoutDetail(detail)) return checkpointTimeoutReason(detail);
98468
+ return null;
98469
+ }
98470
+ /**
98471
+ * Replicates a turn-start failure as `turn_status: failed` with a structured
98472
+ * `reason`, alongside (not instead of) the free-text `error_summary` above:
98473
+ * CMMD fails the owning Work Session from this event
98474
+ * (CMMD-Center/CMMD#5272/#5286), which `error_summary` alone cannot drive.
98475
+ */
98476
+ function serializeTurnStartFailureStatus(activity) {
98477
+ const reason = turnStartFailureReason(activity);
98478
+ if (reason === null) return null;
98479
+ return {
98480
+ sourceSequence: null,
98481
+ event: {
98482
+ eventId: `activity:${activity.id}:turn-status`,
98483
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
98484
+ ...eventTurn(activity.turnId),
98485
+ kind: "turn_status",
98486
+ status: "failed",
98487
+ reason: {
98488
+ code: reason.code,
98489
+ message: text(reason.message, 2e3) ?? reason.message
98490
+ }
98491
+ }
98492
+ };
98493
+ }
98196
98494
  const turnUsageKeys = [
98197
98495
  "uncachedInputTokens",
98198
98496
  "cachedInputTokens",
@@ -98541,7 +98839,7 @@ function serializeRuntimeThreadEvents(thread) {
98541
98839
  });
98542
98840
  const recordedTurns = serializeRecordedTurns(thread);
98543
98841
  const textGenerations = serializeTextGenerations(thread);
98544
- return [
98842
+ const priorEvents = [
98545
98843
  ...messages,
98546
98844
  ...replicaActivities,
98547
98845
  ...toolActivities,
@@ -98550,7 +98848,15 @@ function serializeRuntimeThreadEvents(thread) {
98550
98848
  ...failureActivities,
98551
98849
  ...recordedTurns,
98552
98850
  ...textGenerations
98553
- ].toSorted((left, right) => {
98851
+ ];
98852
+ const latestPriorOccurredAt = priorEvents.reduce((latest, entry) => latest === null || entry.event.occurredAt > latest ? entry.event.occurredAt : latest, null);
98853
+ const turnStartFailureStatuses = thread.activities.flatMap((activity) => {
98854
+ const event = serializeTurnStartFailureStatus(activity);
98855
+ if (event === null) return [];
98856
+ if (latestPriorOccurredAt !== null && event.event.occurredAt < latestPriorOccurredAt) return [];
98857
+ return [event];
98858
+ });
98859
+ return [...priorEvents, ...turnStartFailureStatuses].toSorted((left, right) => {
98554
98860
  if (left.sourceSequence !== null && right.sourceSequence !== null) return left.sourceSequence - right.sourceSequence;
98555
98861
  if (left.sourceSequence !== null) return -1;
98556
98862
  if (right.sourceSequence !== null) return 1;
@@ -152498,7 +152804,7 @@ async function prepareWorkspaceRepositoryDependencies(input) {
152498
152804
  const dependencies = resolveWorkspaceEnvironmentConfig(input.cwd).repositoryDependencies;
152499
152805
  if (dependencies.length === 0) return [];
152500
152806
  const broker = input.broker ?? new GitHubBroker();
152501
- const auth = broker.runtimeGrantAuthContext("github");
152807
+ const auth = broker.runtimeGrantAuthContext("github") ?? await broker.requestRuntimeGithubGrant?.() ?? null;
152502
152808
  if (auth === null) throw new Error("Repository dependencies require an exact scoped runtime GitHub grant.");
152503
152809
  const readOrigin = input.readOrigin ?? readOriginFromGit;
152504
152810
  const resolved = resolveRepositoryDependencies(dependencies, input.cloudProjectsRoot);
@@ -213506,73 +213812,6 @@ function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
213506
213812
  }
213507
213813
  }
213508
213814
  //#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
213815
  //#region src/provider/claudeCredentialContainment.ts
213577
213816
  /**
213578
213817
  * Containment for provider credentials inherited by the Claude Agent SDK child.
@@ -215656,128 +215895,6 @@ function createGitHubMcpServer(runBrokeredTool, threadId, settings) {
215656
215895
  });
215657
215896
  }
215658
215897
  //#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
215898
  //#region src/provider/missingWorkspaceCopy.ts
215782
215899
  /**
215783
215900
  * Refuse a turn whose checkout folder is gone, and say so.
@@ -234138,6 +234255,8 @@ const CheckpointDiffQueryLive = require_Schema$1.effect(CheckpointDiffQuery, mak
234138
234255
  * @module CheckpointStoreLive
234139
234256
  */
234140
234257
  const CHECKPOINT_PATH_OUTPUT_MAX_BYTES = 16 * 1024 * 1024;
234258
+ const CheckpointCaptureTimeoutConfig = require_Config.number("FORGE_CHECKPOINT_CAPTURE_TIMEOUT_MS").pipe(require_Config.withDefault(6e4));
234259
+ const EFFECT_TIMER_MAX_MS = 2147483647;
234141
234260
  function parseNullSeparatedPaths(output) {
234142
234261
  return output.split("\0").filter((entry) => entry.length > 0);
234143
234262
  }
@@ -234145,7 +234264,7 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234145
234264
  const fs = yield* require_Path.FileSystem;
234146
234265
  const path = yield* require_Path.Path;
234147
234266
  const git = yield* GitCore;
234148
- const resolveHeadCommit = (cwd) => git.execute({
234267
+ const resolveHeadCommit = (cwd, timeoutMs) => git.execute({
234149
234268
  operation: "CheckpointStore.resolveHeadCommit",
234150
234269
  cwd,
234151
234270
  args: [
@@ -234154,7 +234273,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234154
234273
  "--quiet",
234155
234274
  "HEAD^{commit}"
234156
234275
  ],
234157
- allowNonZeroExit: true
234276
+ allowNonZeroExit: true,
234277
+ ...timeoutMs !== void 0 ? { timeoutMs } : {}
234158
234278
  }).pipe(require_Schema$1.map((result) => {
234159
234279
  if (result.code !== 0) return null;
234160
234280
  const commit = result.stdout.trim();
@@ -234193,7 +234313,9 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234193
234313
  }).pipe(require_Schema$1.map((result) => result.code === 0 && result.stdout.trim() === "true"), require_Schema$1.catch_(() => require_Schema$1.succeed(false)));
234194
234314
  const captureCheckpoint = require_Schema$1.fn("captureCheckpoint")(function* (input) {
234195
234315
  const operation = "CheckpointStore.captureCheckpoint";
234196
- const headCommit = yield* resolveHeadCommit(input.cwd);
234316
+ const captureTimeoutMs = yield* CheckpointCaptureTimeoutConfig.asEffect().pipe(require_Schema$1.orDie);
234317
+ 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}.`));
234318
+ const headCommit = yield* resolveHeadCommit(input.cwd, captureTimeoutMs);
234197
234319
  const [trackedPathResult, untrackedPathResult] = yield* require_Schema$1.all([git.execute({
234198
234320
  operation,
234199
234321
  cwd: input.cwd,
@@ -234210,7 +234332,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234210
234332
  "-z",
234211
234333
  "--"
234212
234334
  ],
234213
- maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES
234335
+ maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES,
234336
+ timeoutMs: captureTimeoutMs
234214
234337
  }), git.execute({
234215
234338
  operation,
234216
234339
  cwd: input.cwd,
@@ -234221,7 +234344,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234221
234344
  "-z",
234222
234345
  "--"
234223
234346
  ],
234224
- maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES
234347
+ maxOutputBytes: CHECKPOINT_PATH_OUTPUT_MAX_BYTES,
234348
+ timeoutMs: captureTimeoutMs
234225
234349
  })], { concurrency: 2 });
234226
234350
  const changedPaths = [...new Set([...parseNullSeparatedPaths(trackedPathResult.stdout), ...parseNullSeparatedPaths(untrackedPathResult.stdout)])];
234227
234351
  if (headCommit && changedPaths.length === 0) {
@@ -234232,7 +234356,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234232
234356
  "update-ref",
234233
234357
  input.checkpointRef,
234234
234358
  headCommit
234235
- ]
234359
+ ],
234360
+ timeoutMs: captureTimeoutMs
234236
234361
  });
234237
234362
  return;
234238
234363
  }
@@ -234250,13 +234375,15 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234250
234375
  operation,
234251
234376
  cwd: input.cwd,
234252
234377
  args: ["read-tree", headCommit],
234253
- env: commitEnv
234378
+ env: commitEnv,
234379
+ timeoutMs: captureTimeoutMs
234254
234380
  });
234255
234381
  else yield* git.execute({
234256
234382
  operation,
234257
234383
  cwd: input.cwd,
234258
234384
  args: ["read-tree", "--empty"],
234259
- env: commitEnv
234385
+ env: commitEnv,
234386
+ timeoutMs: captureTimeoutMs
234260
234387
  });
234261
234388
  if (changedPaths.length > 0) yield* git.execute({
234262
234389
  operation,
@@ -234269,13 +234396,15 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234269
234396
  "--pathspec-file-nul"
234270
234397
  ],
234271
234398
  stdin: changedPaths.map((changedPath) => `${changedPath}\0`).join(""),
234272
- env: commitEnv
234399
+ env: commitEnv,
234400
+ timeoutMs: captureTimeoutMs
234273
234401
  });
234274
234402
  const treeOid = (yield* git.execute({
234275
234403
  operation,
234276
234404
  cwd: input.cwd,
234277
234405
  args: ["write-tree"],
234278
- env: commitEnv
234406
+ env: commitEnv,
234407
+ timeoutMs: captureTimeoutMs
234279
234408
  })).stdout.trim();
234280
234409
  if (treeOid.length === 0) return yield* new GitCommandError({
234281
234410
  operation,
@@ -234293,7 +234422,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234293
234422
  "-m",
234294
234423
  message
234295
234424
  ],
234296
- env: commitEnv
234425
+ env: commitEnv,
234426
+ timeoutMs: captureTimeoutMs
234297
234427
  })).stdout.trim();
234298
234428
  if (commitOid.length === 0) return yield* new GitCommandError({
234299
234429
  operation,
@@ -234308,7 +234438,8 @@ const makeCheckpointStore = require_Schema$1.gen(function* () {
234308
234438
  "update-ref",
234309
234439
  input.checkpointRef,
234310
234440
  commitOid
234311
- ]
234441
+ ],
234442
+ timeoutMs: captureTimeoutMs
234312
234443
  });
234313
234444
  }), (tempDir) => fs.remove(tempDir, { recursive: true })).pipe(require_Schema$1.catchTags({ PlatformError: (error) => require_Schema$1.fail(new CheckpointInvariantError({
234314
234445
  operation: "CheckpointStore.captureCheckpoint",
@@ -292201,7 +292332,7 @@ const make$8 = require_Schema$1.gen(function* () {
292201
292332
  summary: "Agent turn blocked",
292202
292333
  detail: "Forge runtime is updating after the current turn. New agent turns are paused until the runtime finishes draining.",
292203
292334
  turnId: null,
292204
- createdAt: input.createdAt
292335
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
292205
292336
  });
292206
292337
  return;
292207
292338
  }
@@ -292436,7 +292567,7 @@ const make$8 = require_Schema$1.gen(function* () {
292436
292567
  summary: "Agent turn blocked",
292437
292568
  detail: workspaceError,
292438
292569
  turnId: null,
292439
- createdAt: event.payload.createdAt
292570
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
292440
292571
  });
292441
292572
  return;
292442
292573
  }
@@ -293528,7 +293659,10 @@ const make$7 = require_Schema$1.gen(function* () {
293528
293659
  tone: "error",
293529
293660
  kind: "checkpoint.capture.failed",
293530
293661
  summary: "Checkpoint capture failed",
293531
- payload: { detail: input.detail },
293662
+ payload: {
293663
+ detail: input.detail,
293664
+ ...input.blocksTurnStart ? { blocksTurnStart: true } : {}
293665
+ },
293532
293666
  turnId: input.turnId,
293533
293667
  createdAt: input.createdAt
293534
293668
  },
@@ -293814,7 +293948,13 @@ const make$7 = require_Schema$1.gen(function* () {
293814
293948
  yield* checkpointStore.captureCheckpoint({
293815
293949
  cwd: checkpointCwd,
293816
293950
  checkpointRef: baselineCheckpointRef
293817
- });
293951
+ }).pipe(require_Schema$1.catch_((error) => appendCaptureFailureActivity({
293952
+ threadId,
293953
+ turnId: null,
293954
+ detail: error.message,
293955
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
293956
+ blocksTurnStart: true
293957
+ }).pipe(require_Schema$1.catch_(() => require_Schema$1.void_))));
293818
293958
  yield* receiptBus.publish({
293819
293959
  type: "checkpoint.baseline.captured",
293820
293960
  threadId,
@@ -298206,7 +298346,7 @@ function resolveBuildCommitFromEnv(env) {
298206
298346
  * environment descriptor down instead of reporting an honest "unknown".
298207
298347
  */
298208
298348
  function readBakedBuildCommit() {
298209
- return "63701057dccde6ea0a120ebb0a1e1ae8c461b056";
298349
+ return "589c5f6a52f5cd82c598399da0ece13acd43d50d";
298210
298350
  }
298211
298351
  async function resolveServerBuildCommit(input) {
298212
298352
  if (isFullCommitSha(input.baked)) return input.baked;