@withtruing/companion 0.14.0 → 0.14.2

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/cli.js CHANGED
@@ -4,7 +4,7 @@ import { existsSync } from 'node:fs';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { addConnection, cleanupMigratedManagedRepositories, configPath, enableWorkspacePolling, hasTemporaryManagedRepository, loadConfig, managedReposDir, migrateManagedRepositoriesToTemporaryStorage, normalizeConsoleUrl, recordWorkspaceId, removePairing, } from './config.js';
7
- import { CompanionClient, CompanionHttpError, CLAUDE_CLI_FAST_MODEL, CODEX_FAST_MODEL, PairingHttpError, } from './http-client.js';
7
+ import { CompanionClient, CompanionHttpError, CLAUDE_CLI_FAST_MODEL, wholeReadBackendName as sharedWholeReadBackendName, CODEX_FAST_MODEL, PairingHttpError, } from './http-client.js';
8
8
  import { assertIsRepo, currentCommit, GIT_UNAVAILABLE_MESSAGE, REMOTE_TROUBLESHOOTING_HINT, repositoryIdentity, RepositoryIdentityError, repositoryTopLevel, } from './local-scan.js';
9
9
  import { runAnalyseShipmentTask, runDesignPromptTask, runGenerateSpecBatchTask, runGenericTask, runProposeNewCapabilitiesTask, runProposeSkeletonTask, runStructuredModelCall, runVerifyModuleTask } from './api-key-caller.js';
10
10
  import { runAnalyseShipmentTaskViaCli, runDesignPromptTaskViaCli, runGenerateSpecBatchTaskViaCli, runGenericTaskViaCli, runProposeNewCapabilitiesTaskViaCli, runProposeSkeletonTaskViaCli, runStructuredModelCallViaCli, runVerifyModuleTaskViaCli, runWholeReadModelCallViaCli } from './claude-cli-caller.js';
@@ -21,7 +21,7 @@ import { AttemptSourceCleanupError, createCompanionAttemptSourceManager, startAt
21
21
  import { createWholeReadAssignmentPool, } from './whole-read-pool.js';
22
22
  import { createWholeReadSharedSourceManager, } from './whole-read-shared-source.js';
23
23
  import { checkConnectionRepositoryReadiness, createRepositoryProvider, proveManagedRepositoryAccess, RepositoryProviderError, } from './repository-provider.js';
24
- import { WholeReadGitError } from './whole-read-git.js';
24
+ import { WholeReadGitError, resolveLocalBuildCommit } from './whole-read-git.js';
25
25
  import { acquireGithubCompanionSource, acquireAssignedSource, assignmentRouteReadinessCode, acquireLocalSource, dispatchWholeReadClaim, } from './whole-read-runner.js';
26
26
  import { WHOLE_READ_PHASE_POLICY } from './whole-read/phase-policy.js';
27
27
  import { acquireInstanceLock, installService, isNpxCachePath, releaseInstanceLock, runDaemon, servicePaths, serviceStatus, startService, stopService, uninstallService, } from './service.js';
@@ -1180,11 +1180,75 @@ function wholeReadTransport(client, onUsageLimit) {
1180
1180
  };
1181
1181
  }
1182
1182
  function wholeReadBackendName(runtime) {
1183
- if (runtime.backend === 'api-key')
1184
- return 'anthropic_api';
1185
- if (runtime.backend === 'cli')
1186
- return 'claude_cli';
1187
- return 'codex';
1183
+ return sharedWholeReadBackendName(runtime.backend);
1184
+ }
1185
+ export function assignmentSourceProblem(reason) {
1186
+ if (!(reason instanceof WholeReadGitError))
1187
+ return 'git_failed';
1188
+ if (reason.code === 'snapshot_unavailable')
1189
+ return 'snapshot_unavailable';
1190
+ if (reason.code === 'branch_unavailable')
1191
+ return 'branch_missing';
1192
+ if (reason.code === 'repository_unavailable')
1193
+ return 'folder_missing';
1194
+ return 'git_failed';
1195
+ }
1196
+ function safeReason(reason) {
1197
+ const text = reason instanceof Error ? reason.message : String(reason);
1198
+ return text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+@/gi, '$1').replace(/\s+/g, ' ').slice(0, 200);
1199
+ }
1200
+ /** Runs the offered assignment's source check; says why on failure instead of swallowing it. */
1201
+ export async function verifyAssignmentSource(input) {
1202
+ const { assignment, local } = input;
1203
+ try {
1204
+ const acquired = await input.sharedSources.acquire(wholeReadSharedSourceKey(input.pairing, assignment), async (lease, signal) => {
1205
+ const source = await acquireAssignedSource({
1206
+ route: assignment.route,
1207
+ acquireLocal: () => local?.branch
1208
+ ? acquireLocalSource({ repoPath: local.repoPath, branch: local.branch,
1209
+ snapshotSha: assignment.snapshotSha, signal })
1210
+ : Promise.reject(new WholeReadGitError(local ? 'branch_unavailable' : 'repository_unavailable')),
1211
+ acquireGithubCompanion: () => {
1212
+ const repositoryUrl = `https://github.com/${githubLabel(parseGitHubRepo(assignment.canonicalRepository))}.git`;
1213
+ return acquireGithubCompanionSource({ lease, repositoryUrl,
1214
+ snapshotSha: assignment.snapshotSha, signal });
1215
+ },
1216
+ });
1217
+ if (!source)
1218
+ throw new WholeReadGitError('snapshot_unavailable');
1219
+ return source;
1220
+ });
1221
+ await acquired.release();
1222
+ input.reportedFailures?.delete(`${assignment.id}:${assignment.assignmentRevision}`);
1223
+ return { verified: true };
1224
+ }
1225
+ catch (reason) {
1226
+ const sourceProblem = assignmentSourceProblem(reason);
1227
+ const failureKey = `${assignment.id}:${assignment.assignmentRevision}`;
1228
+ if (input.reportedFailures?.get(failureKey) === sourceProblem)
1229
+ return { verified: false, sourceProblem };
1230
+ input.reportedFailures?.set(failureKey, sourceProblem);
1231
+ const name = assignment.canonicalRepository.split('/').filter(Boolean).pop() ?? 'the repository';
1232
+ const prefix = `[${new Date().toISOString()}] Couldn't prepare ${name} for the build`;
1233
+ if (sourceProblem === 'snapshot_unavailable' && assignment.route === 'local' && local?.branch) {
1234
+ let folderCommit = 'unknown';
1235
+ try {
1236
+ folderCommit = (await resolveLocalBuildCommit(local.repoPath, local.branch, new AbortController().signal)).slice(0, 7);
1237
+ }
1238
+ catch { /* the reason below still names the code */ }
1239
+ input.log(`${prefix}: the folder's ${local.branch} branch is at ${folderCommit}, but Truing asked for ${assignment.snapshotSha.slice(0, 7)} (snapshot_unavailable). Bring the folder level with GitHub and start the map again.`);
1240
+ }
1241
+ else if (reason instanceof WholeReadGitError) {
1242
+ input.log(`${prefix}: ${sourceProblem === 'branch_missing' ? "the folder's branch isn't set"
1243
+ : sourceProblem === 'folder_missing' ? "the folder isn't where Truing expects it"
1244
+ : sourceProblem === 'snapshot_unavailable' ? 'the code Truing asked for could not be fetched'
1245
+ : 'git could not read the code'} (${reason.code}).`);
1246
+ }
1247
+ else {
1248
+ input.log(`${prefix}: ${safeReason(reason)} (${sourceProblem}).`);
1249
+ }
1250
+ return { verified: false, sourceProblem };
1251
+ }
1188
1252
  }
1189
1253
  async function runWholeReadAssignment(input) {
1190
1254
  const connection = input.connections.find((candidate) => candidate.connectionId === input.assignment.connectionId);
@@ -1207,35 +1271,14 @@ async function runWholeReadAssignment(input) {
1207
1271
  return { status: 'not_claimed' };
1208
1272
  }
1209
1273
  if (input.assignment.state === 'offered') {
1210
- let verified = false;
1211
- try {
1212
- const acquired = await input.sharedSources.acquire(wholeReadSharedSourceKey(input.pairing, input.assignment), async (lease, signal) => {
1213
- const source = await acquireAssignedSource({
1214
- route: input.assignment.route,
1215
- acquireLocal: () => local?.branch
1216
- ? acquireLocalSource({ repoPath: local.repoPath, branch: local.branch,
1217
- snapshotSha: input.assignment.snapshotSha, signal })
1218
- : Promise.reject(new WholeReadGitError('snapshot_unavailable')),
1219
- acquireGithubCompanion: () => {
1220
- const repositoryUrl = `https://github.com/${githubLabel(parseGitHubRepo(input.assignment.canonicalRepository))}.git`;
1221
- return acquireGithubCompanionSource({ lease, repositoryUrl,
1222
- snapshotSha: input.assignment.snapshotSha, signal });
1223
- },
1224
- });
1225
- if (!source)
1226
- throw new WholeReadGitError('snapshot_unavailable');
1227
- return source;
1228
- });
1229
- await acquired.release();
1230
- verified = true;
1231
- }
1232
- catch {
1233
- verified = false;
1234
- }
1274
+ const check = await verifyAssignmentSource({ pairing: input.pairing, assignment: input.assignment,
1275
+ local, sharedSources: input.sharedSources, log: input.log, reportedFailures: input.reportedFailures });
1276
+ const verified = check.verified;
1235
1277
  const acceptance = await input.client.acceptBuildAssignment(input.assignment, {
1236
1278
  lsRemoteVerified: verified, exactCommitFetched: verified,
1237
1279
  backend: input.assignment.frozenBackend, backendReady,
1238
1280
  routeSupported: routeReadiness === 'ready',
1281
+ ...(check.verified ? {} : { sourceProblem: check.sourceProblem }),
1239
1282
  });
1240
1283
  if (acceptance.status !== 'ready')
1241
1284
  return { status: 'not_claimed' };
@@ -1368,7 +1411,8 @@ async function runPairing(pairing, options) {
1368
1411
  return [[connection.connectionId, CODEX_FAST_MODEL]];
1369
1412
  return [];
1370
1413
  }));
1371
- const client = new CompanionClient(pairing.url, pairing.token, fastModelsByConnection);
1414
+ const backendsByConnection = Object.fromEntries(pairing.connections.map((connection) => [connection.connectionId, sharedWholeReadBackendName(connection.backend)]));
1415
+ const client = new CompanionClient(pairing.url, pairing.token, fastModelsByConnection, backendsByConnection);
1372
1416
  const tokenRejected = () => {
1373
1417
  error(`The Console at ${pairing.url} no longer accepts this computer's token — stopped working for it. Pair again from that Console to reconnect.`);
1374
1418
  return 'token_rejected';
@@ -1503,6 +1547,7 @@ async function runPairing(pairing, options) {
1503
1547
  const sharedSources = createWholeReadSharedSourceManager({
1504
1548
  onCleanupPending: () => { error('[whole-read-temp] cleanup_pending'); },
1505
1549
  });
1550
+ const reportedFailures = new Map();
1506
1551
  const wholeReadPool = createWholeReadAssignmentPool({
1507
1552
  key: (assignment) => assignment.id,
1508
1553
  mayClaim,
@@ -1511,6 +1556,7 @@ async function runPairing(pairing, options) {
1511
1556
  usageClaimLimit--; },
1512
1557
  run: (assignment) => runWholeReadAssignment({ pairing, client, assignment,
1513
1558
  connections: activeConnections, runtimes, workspaceRuntime, sharedSources,
1559
+ log: (message) => error(message), reportedFailures,
1514
1560
  onUsageLimit: () => recordCompletedClaim('usage limit reached'),
1515
1561
  shouldStop: options.shouldStop }),
1516
1562
  onError: (reason) => {
@@ -30,10 +30,20 @@ export const COMPANION_CAPABILITIES = [
30
30
  export const CLAUDE_CLI_FAST_MODEL = 'claude-haiku-5-5';
31
31
  export const CODEX_FAST_MODEL = 'gpt-5.6-luna';
32
32
  export const WHOLE_READ_FAST_MODEL_CAPABILITY = 'whole-read-fast-model@';
33
- const capabilityHeaders = (fastModel) => ({
33
+ export const WHOLE_READ_BACKEND_CAPABILITY = 'whole-read-backend@';
34
+ /** The one mapping from a connection's backend to the name the server freezes and the readiness report uses. */
35
+ export function wholeReadBackendName(backend) {
36
+ if (backend === 'api-key')
37
+ return 'anthropic_api';
38
+ if (backend === 'cli')
39
+ return 'claude_cli';
40
+ return 'codex';
41
+ }
42
+ export const capabilityHeaders = (fastModel, backend) => ({
34
43
  'X-Companion-Capabilities': [...COMPANION_CAPABILITIES,
35
44
  ...(fastModel && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(fastModel)
36
- ? [`${WHOLE_READ_FAST_MODEL_CAPABILITY}${fastModel}`] : [])].join(','),
45
+ ? [`${WHOLE_READ_FAST_MODEL_CAPABILITY}${fastModel}`] : []),
46
+ ...(backend && /^[a-z_]{1,32}$/.test(backend) ? [`${WHOLE_READ_BACKEND_CAPABILITY}${backend}`] : [])].join(','),
37
47
  });
38
48
  export class CompanionHttpError extends Error {
39
49
  status;
@@ -61,13 +71,15 @@ export class CompanionClient {
61
71
  url;
62
72
  token;
63
73
  fastModelsByConnection;
64
- constructor(url, token, fastModelsByConnection = {}) {
74
+ backendsByConnection;
75
+ constructor(url, token, fastModelsByConnection = {}, backendsByConnection = {}) {
65
76
  this.url = url;
66
77
  this.token = token;
67
78
  this.fastModelsByConnection = fastModelsByConnection;
79
+ this.backendsByConnection = backendsByConnection;
68
80
  }
69
81
  capabilityHeaders(connectionId) {
70
- return capabilityHeaders(connectionId ? this.fastModelsByConnection[connectionId] : undefined);
82
+ return capabilityHeaders(connectionId ? this.fastModelsByConnection[connectionId] : undefined, connectionId ? this.backendsByConnection[connectionId] : undefined);
71
83
  }
72
84
  request(path, timeoutMs, init) {
73
85
  return fetch(`${this.url}${path}`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@withtruing/companion",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "The Truing companion: reads your code and runs AI on your own computer, for Truing.",
5
5
  "type": "module",
6
6
  "license": "MIT",