@cat-factory/server 0.313.0 → 0.314.1

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.
@@ -1,9 +1,10 @@
1
- import type { BootstrapJobHandle, BootstrapJobRepository, BootstrapJobUpdate, BootstrapRepoOutcome, BootstrapRepoRequest, GitHubClient, MonorepoTargetRepo, GitHubInstallationRepository, GitHubRepo, GroupCacheHandle, ModelRef, RepoBootstrapper, RepoProjectionRepository } from '@cat-factory/kernel';
1
+ import type { AgentContextRecorder, BootstrapJobHandle, BootstrapJobRepository, BootstrapJobUpdate, BootstrapRepoOutcome, BootstrapRepoRequest, GitHubClient, MonorepoTargetRepo, ReferenceRepoAccess, GitHubInstallationRepository, GitHubRepo, GroupCacheHandle, ModelRef, RepoBootstrapper, RepoProjectionRepository, VcsProvider } from '@cat-factory/kernel';
2
2
  import type { ContainerSessionService } from '../containers/ContainerSessionService.js';
3
+ import { type ToolTrajectoryDeps } from './toolTrajectory.js';
3
4
  import type { JobPackageRegistrySpec } from './ContainerAgentExecutor.js';
4
- import type { MintInstallationToken } from './repoTargeting.js';
5
+ import { type MintInstallationToken } from './repoTargeting.js';
5
6
  import { type ResolveRunnerTransport } from './RunnerJobClient.js';
6
- export interface ContainerRepoBootstrapperDependencies {
7
+ export interface ContainerRepoBootstrapperDependencies extends ToolTrajectoryDeps {
7
8
  /**
8
9
  * Resolve which runner backend (Cloudflare container or self-hosted pool) a
9
10
  * bootstrap job dispatches to — the same seam the implementation executor rides.
@@ -23,6 +24,18 @@ export interface ContainerRepoBootstrapperDependencies {
23
24
  repoProjectionCache?: GroupCacheHandle<GitHubRepo[]>;
24
25
  /** Resolves/validates the pre-created target repository (existence + emptiness). */
25
26
  githubClient: GitHubClient;
27
+ /**
28
+ * The provider {@link githubClient} speaks (`engineVcsProvider`), so a workspace whose
29
+ * connection is on ANOTHER one is refused rather than probed with the wrong client.
30
+ *
31
+ * Required rather than defaulted, for the reason `makeResolveRepoFilesForCoords` states it:
32
+ * a deployment serving a GitHub App beside per-workspace GitLab connections binds the App
33
+ * client here, and a GitLab workspace's installation id means nothing to it. Reading the
34
+ * template through it anyway answers 404, which this component would then report as "your
35
+ * reference architecture names the wrong repository" for an entry that is perfectly correct.
36
+ * A facade that forgets to state it must fail to typecheck rather than inherit a guess.
37
+ */
38
+ clientProvider: VcsProvider;
26
39
  /**
27
40
  * Mints a short-lived GitHub installation token for clone + push, scoped to the single repo
28
41
  * being bootstrapped. Bootstrap has no run initiator, so it names no `initiatedBy` and always
@@ -39,6 +52,12 @@ export interface ContainerRepoBootstrapperDependencies {
39
52
  githubApiBase?: string;
40
53
  /** Web base for building the created repo's URL (defaults to github.com). */
41
54
  webBaseUrl?: string;
55
+ /**
56
+ * Records the complete context each bootstrap dispatch handed its agent (best-effort, gated
57
+ * inside the recorder). Absent ⇒ a bootstrap's "Provided context" tab is empty, which is the
58
+ * shape every other agent run's would have if its executor skipped this call.
59
+ */
60
+ agentContextObservability?: AgentContextRecorder;
42
61
  /**
43
62
  * Resolve the workspace's private package-registry entries for the bootstrap
44
63
  * container (the scaffolder installs dependencies too). Same seam as
@@ -69,6 +88,43 @@ export declare class ContainerRepoBootstrapper implements RepoBootstrapper {
69
88
  constructor(deps: ContainerRepoBootstrapperDependencies);
70
89
  /** An active (non-soft-deleted) installation means the workspace is connected. */
71
90
  isWorkspaceConnected(workspaceId: string): Promise<boolean>;
91
+ /**
92
+ * Resolve the reference template through the workspace's INSTALLATION, which is the same reach
93
+ * the run's clone has.
94
+ *
95
+ * Deliberately NOT the workspace's repo projection (`resolveRepoFilesForCoords`), the way every
96
+ * other checkout-free read here is scoped. A reference architecture is an admin-managed entry
97
+ * naming `owner/name`, not a repository the board has linked, so the projection answers "no such
98
+ * repo" for a template the run then clones without trouble: the survey used to report the
99
+ * template as unsurveyed on every deployment whose template is not also a board service. What
100
+ * scopes this instead is the entry itself plus the installation's own grant, which is the pair
101
+ * that decides whether the clone works.
102
+ *
103
+ * A 404 is the provider's answer that this credential cannot see the repository, which on GitHub
104
+ * covers both "no such repo" and "the App was not granted it", and it is one verdict because
105
+ * they take the same fix. Any other failure is reported as UNREADABLE rather than as an absence,
106
+ * so an outage cannot be presented to an operator as a typo in their configuration.
107
+ *
108
+ * NEVER throws, which is a contract two callers depend on (the survey, whose whole phase is
109
+ * "park with what we know", and the pre-flight, which owes a 503). So the installation read sits
110
+ * INSIDE the try as well: on a mothership-mode node the repository read is an RPC to a process
111
+ * that can be a restart away, and a throw from there would arrive as a platform bug about
112
+ * somebody's own template.
113
+ */
114
+ resolveReferenceRepo(workspaceId: string, ref: {
115
+ owner: string;
116
+ name: string;
117
+ }): Promise<ReferenceRepoAccess>;
118
+ /**
119
+ * The one provider read behind every reference-template question: the pre-flight's verdict, the
120
+ * survey's reader and the dispatch's clone spec are all this, mapped three ways.
121
+ *
122
+ * Shared because the two callers used to ask it differently: one 404-aware and returning a
123
+ * verdict, the other collapsing every failure into one disposition. So the same rate limit was
124
+ * reported as the entry being wrong at one moment and as an outage at another. One probe, one
125
+ * set of causes, and each caller decides only what to DO about them.
126
+ */
127
+ private readTemplate;
72
128
  /**
73
129
  * Resolve a monorepo bootstrap target out of the WORKSPACE's own repo projection.
74
130
  *
@@ -153,23 +209,80 @@ export declare class ContainerRepoBootstrapper implements RepoBootstrapper {
153
209
  * Resolve the reference template a run reads from: its clone spec, plus the numeric id the
154
210
  * run's installation token has to be scoped to.
155
211
  *
156
- * ONE resolution for both dispatch shapes, because both CLONE the template and both mint the
212
+ * ONE resolution for every dispatch shape, because they all CLONE the template and all mint the
157
213
  * token that clone runs on. Two copies is what left the force-push path granting only its push
158
214
  * target while cloning the template with that same token, so a PRIVATE reference architecture
159
215
  * 404s on clone under one delivery and works under the other, and what left it assuming the
160
216
  * template's base branch was `main`.
161
217
  *
162
- * A template the installation cannot read resolves to no id and the conventional branch rather
163
- * than refusing the run: the clone then fails inside the harness naming the repository, which
164
- * is a better report than a pre-flight throw here, and the warning says which read failed.
218
+ * It THROWS on a template it cannot resolve, rather than dispatching with no id and the
219
+ * conventional branch: the container is about to clone it, so the run is over either way, and
220
+ * the difference is whether the report names the reference architecture and what to do about it
221
+ * or arrives as a git error from inside a container that had to be started first.
222
+ *
223
+ * The MESSAGE follows the cause the shared probe reported. "Point the reference architecture at
224
+ * a repository this workspace can reach" is the wrong instruction for a rate limit or a 500,
225
+ * and a monorepo apply dispatches days after a human settled its review, so the failure read
226
+ * here is as likely to be an outage as a typo. The provider error rides as `cause`, so a log
227
+ * describer walking the chain can still see which it was.
228
+ *
229
+ * Re-read rather than carried over from the run-start pre-flight, deliberately: this is the
230
+ * moment the clone happens, and on a monorepo run the pre-flight sits on the other side of a
231
+ * human review. An id plumbed through the request would be a claim about the repository as it
232
+ * was, dispatched against the repository as it is.
165
233
  */
166
234
  private resolveReferenceTemplate;
167
- /** The model-locked LLM-proxy session token a bootstrap container runs under. */
235
+ /**
236
+ * Mint the job's clone/push token, scoped to the repos this dispatch resolved.
237
+ *
238
+ * The scope goes through the SHARED `jobTokenRepoIds` rather than a hand-built array, for the
239
+ * case the hand-built one got wrong: a reference architecture naming the run's own target (or
240
+ * its monorepo) is one repository asked for twice, and a `repository_ids` list is a set.
241
+ *
242
+ * A refusal is re-thrown NAMING the template, because a scoped mint has one failure mode this
243
+ * component cannot pre-flight away. GitHub narrows a token only to repositories the
244
+ * installation was GRANTED, while a PUBLIC repository reads perfectly well through the API
245
+ * without being one, so a template outside the App's repository access resolves cleanly above
246
+ * and is refused here. There is no read on this port that separates the two cheaply and
247
+ * correctly on every provider (`getRepoById` answers from a bounded listing on the GitLab
248
+ * adapter, so a null there is not evidence of anything), and dropping the leg would only move
249
+ * the failure to the clone. What is left is to make the refusal say which repository to grant.
250
+ */
251
+ private mintDispatchToken;
252
+ /**
253
+ * The model-locked LLM-proxy session token a bootstrap container runs under.
254
+ *
255
+ * Keyed on the RUN, never on the drive: a monorepo run's apply phase is dispatched under its
256
+ * own container job id, so minting on that id files the apply's model calls under a key no
257
+ * run-scoped read asks for, and the run reports only what its survey spent.
258
+ */
168
259
  private mintSessionToken;
169
260
  /** The host's web base, trailing slashes stripped, for building clone URLs. */
170
261
  private webBase;
171
262
  /** Poll a dispatched bootstrap job, mapping the runner job view into an update. */
172
263
  pollBootstrap(handle: BootstrapJobHandle): Promise<BootstrapJobUpdate>;
264
+ /**
265
+ * The dispatched container job as an {@link AgentJobHandle}, the shape the shared trajectory
266
+ * drain speaks. `runId` is the bootstrap RUN and `jobId` the container job it dispatched,
267
+ * which is the same split every execution step's handle carries.
268
+ */
269
+ private jobHandle;
270
+ /**
271
+ * The resolved model as every OTHER producer writes it: `provider:model`, which is the format
272
+ * `AgentJobHandle.model` and `agentContextSnapshotSchema.model` both document. A bare model id
273
+ * renders beside prefixed ones on the same panel and gives nothing to a reader that splits the
274
+ * field to recover the provider.
275
+ */
276
+ private resolvedModel;
277
+ /**
278
+ * File what this dispatch handed the agent, so a bootstrap's Provided-context tab answers the
279
+ * same question every other agent run's does.
280
+ *
281
+ * AWAITED for the reason `recordAgentContextSnapshot` states: it runs after the container has
282
+ * already been accepted, so it delays nothing but the handle's return, and an un-awaited insert
283
+ * is dropped outright on the Worker, where this runs inside a Workflow step.
284
+ */
285
+ private recordDispatchContext;
173
286
  /**
174
287
  * Best-effort: reclaim the per-run container for a job. Releases through the same
175
288
  * transport the run dispatched to (keyed by job id) — for the Cloudflare backend
@@ -1 +1 @@
1
- {"version":3,"file":"ContainerRepoBootstrapper.d.ts","sourceRoot":"","sources":["../../src/agents/ContainerRepoBootstrapper.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ContainerRepoBootstrapper.d.ts","sourceRoot":"","sources":["../../src/agents/ContainerRepoBootstrapper.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,10 @@
1
- import { failureKindFromHarnessCause, runBestEffort } from '@cat-factory/kernel';
1
+ import { failureKindFromHarnessCause, getErrorMessage, runBestEffort, VcsApiError, } from '@cat-factory/kernel';
2
2
  import { isProxyableProvider } from '@cat-factory/agents';
3
+ import { bootstrapStepIds, REPO_BOOTSTRAP_AGENT_KIND } from '@cat-factory/contracts';
4
+ import { recordBootstrapContextSnapshot } from './agentContextRecord.js';
5
+ import { drainToolCalls } from './toolTrajectory.js';
6
+ import { makeRepoFiles } from './repoFiles.js';
7
+ import { jobTokenRepoIds } from './repoTargeting.js';
3
8
  import { RunnerJobClient } from './RunnerJobClient.js';
4
9
  import { logger } from '../observability/logger.js';
5
10
  /** The role prompt when adapting a cloned reference architecture. */
@@ -127,6 +132,87 @@ export class ContainerRepoBootstrapper {
127
132
  const installation = await this.deps.installationRepository.getByWorkspace(workspaceId);
128
133
  return !!installation && !installation.deletedAt;
129
134
  }
135
+ /**
136
+ * Resolve the reference template through the workspace's INSTALLATION, which is the same reach
137
+ * the run's clone has.
138
+ *
139
+ * Deliberately NOT the workspace's repo projection (`resolveRepoFilesForCoords`), the way every
140
+ * other checkout-free read here is scoped. A reference architecture is an admin-managed entry
141
+ * naming `owner/name`, not a repository the board has linked, so the projection answers "no such
142
+ * repo" for a template the run then clones without trouble: the survey used to report the
143
+ * template as unsurveyed on every deployment whose template is not also a board service. What
144
+ * scopes this instead is the entry itself plus the installation's own grant, which is the pair
145
+ * that decides whether the clone works.
146
+ *
147
+ * A 404 is the provider's answer that this credential cannot see the repository, which on GitHub
148
+ * covers both "no such repo" and "the App was not granted it", and it is one verdict because
149
+ * they take the same fix. Any other failure is reported as UNREADABLE rather than as an absence,
150
+ * so an outage cannot be presented to an operator as a typo in their configuration.
151
+ *
152
+ * NEVER throws, which is a contract two callers depend on (the survey, whose whole phase is
153
+ * "park with what we know", and the pre-flight, which owes a 503). So the installation read sits
154
+ * INSIDE the try as well: on a mothership-mode node the repository read is an RPC to a process
155
+ * that can be a restart away, and a throw from there would arrive as a platform bug about
156
+ * somebody's own template.
157
+ */
158
+ async resolveReferenceRepo(workspaceId, ref) {
159
+ let installation;
160
+ try {
161
+ installation = await this.deps.installationRepository.getByWorkspace(workspaceId);
162
+ }
163
+ catch (error) {
164
+ // A connection we cannot READ is not a connection we know to be absent: `not_connected`
165
+ // would tell the operator to install an App that may well already be installed.
166
+ return { status: 'unreadable', detail: getErrorMessage(error) };
167
+ }
168
+ if (!installation || installation.deletedAt)
169
+ return { status: 'not_connected' };
170
+ // The row's own provider, falling back to this client's for a row predating the column. A
171
+ // connection on another provider is not reachable from here whatever it holds, and reporting
172
+ // that as `not_found` is the misattribution the verdict split exists to prevent.
173
+ if ((installation.provider ?? this.deps.clientProvider) !== this.deps.clientProvider) {
174
+ return { status: 'not_connected' };
175
+ }
176
+ const read = await this.readTemplate(installation.installationId, ref);
177
+ if (read.status === 'not_found')
178
+ return { status: 'not_found' };
179
+ // Mapped field by field rather than passed through: the verdict crosses a port and becomes an
180
+ // HTTP `details.detail`, and the thrown value the probe kept for its own `cause` chain has no
181
+ // business on a wire shape.
182
+ if (read.status === 'unreadable')
183
+ return { status: 'unreadable', detail: read.detail };
184
+ return {
185
+ status: 'reachable',
186
+ files: makeRepoFiles(this.deps.githubClient, installation.installationId, {
187
+ owner: ref.owner,
188
+ repo: ref.name,
189
+ }),
190
+ defaultBranch: defaultBranchOf(read.repo),
191
+ };
192
+ }
193
+ /**
194
+ * The one provider read behind every reference-template question: the pre-flight's verdict, the
195
+ * survey's reader and the dispatch's clone spec are all this, mapped three ways.
196
+ *
197
+ * Shared because the two callers used to ask it differently: one 404-aware and returning a
198
+ * verdict, the other collapsing every failure into one disposition. So the same rate limit was
199
+ * reported as the entry being wrong at one moment and as an outage at another. One probe, one
200
+ * set of causes, and each caller decides only what to DO about them.
201
+ */
202
+ async readTemplate(installationId, ref) {
203
+ try {
204
+ const repo = await this.deps.githubClient.getRepo(installationId, {
205
+ owner: ref.owner,
206
+ repo: ref.name,
207
+ });
208
+ return { status: 'reachable', repo };
209
+ }
210
+ catch (error) {
211
+ if (error instanceof VcsApiError && error.status === 404)
212
+ return { status: 'not_found' };
213
+ return { status: 'unreadable', detail: getErrorMessage(error), cause: error };
214
+ }
215
+ }
130
216
  /**
131
217
  * Resolve a monorepo bootstrap target out of the WORKSPACE's own repo projection.
132
218
  *
@@ -190,23 +276,19 @@ export class ContainerRepoBootstrapper {
190
276
  // With a reference architecture the container clones + adapts it; without one
191
277
  // it scaffolds an empty repo from the freeform instructions alone.
192
278
  const reference = await this.resolveReferenceTemplate(request, installation.installationId, log);
279
+ const webBase = this.webBase();
280
+ const targetCloneUrl = `${webBase}/${owner}/${repoName}.git`;
281
+ const defaultBranch = defaultBranchOf(target);
193
282
  // Scoped to the repos this run touches: the target it force-pushes, and the template it
194
283
  // CLONES. The template belongs on the scope even though nothing pushes to it, because the
195
284
  // clone runs on this same token: leaving it off works only for a public reference
196
285
  // architecture and 404s on a private one, with nothing in the failure naming the cause.
197
286
  // `target` came from the pre-flight `getRepo` above, which is why it costs no extra read.
198
- const ghToken = await this.deps.mintInstallationToken(installation.installationId, {
199
- executionId: request.containerJobId,
200
- workspaceId: request.workspaceId,
201
- repoIds: [String(target.githubId), ...(reference?.githubId ? [reference.githubId] : [])],
202
- });
287
+ const ghToken = await this.mintDispatchToken(request, installation.installationId, { owner, name: repoName, repoId: String(target.githubId), baseBranch: defaultBranch }, reference);
203
288
  // Private-registry auth for the scaffolder's installs, exactly as the
204
289
  // implementation executor forwards it.
205
290
  const packageRegistries = (await this.deps.resolvePackageRegistries?.(request.workspaceId)) ?? [];
206
291
  const sessionToken = await this.mintSessionToken(request);
207
- const webBase = this.webBase();
208
- const targetCloneUrl = `${webBase}/${owner}/${repoName}.git`;
209
- const defaultBranch = defaultBranchOf(target);
210
292
  const targetSpec = { owner, name: repoName, cloneUrl: targetCloneUrl, defaultBranch };
211
293
  // The generic agent `repo` is the clone source: the reference when adapting one, or the
212
294
  // (uncloned) target placeholder when scaffolding from scratch. The real push destination
@@ -226,12 +308,12 @@ export class ContainerRepoBootstrapper {
226
308
  jobId: request.containerJobId,
227
309
  // The run's correlation ids, so the container's own lines join to this bootstrap in the
228
310
  // backend's logs: the same fields `buildCommonBody` puts on an execution job. A bootstrap
229
- // is a first-class agent run (one `agent_runs` table, one retry surface), so it must not be
230
- // the one agent-kind dispatch whose container logs cannot be joined to anything. Its run id
231
- // IS its job id: a bootstrap has no separate execution row, which is exactly what
232
- // `sessionService.mint` above is told.
311
+ // is a first-class agent run (one `agent_runs` table, one retry surface, one observability
312
+ // panel), so it must not be the one agent-kind dispatch whose container logs cannot be
313
+ // joined to anything. The id is the RUN's, matching the session token above: a bootstrap
314
+ // has no separate execution row, so its run id is what every run-scoped read is keyed by.
233
315
  workspaceId: request.workspaceId,
234
- executionId: request.containerJobId,
316
+ executionId: request.jobId,
235
317
  mode: 'coding',
236
318
  systemPrompt: reference ? ADAPT_SYSTEM_PROMPT : SCAFFOLD_SYSTEM_PROMPT,
237
319
  userPrompt: request.instructions ||
@@ -273,6 +355,7 @@ export class ContainerRepoBootstrapper {
273
355
  // equals the run id (no per-step fan-out into a shared container, and no second phase).
274
356
  await this.jobs.dispatch(request.workspaceId, { runId: request.jobId, jobId: request.containerJobId }, body, 'agent');
275
357
  log.info('bootstrap: container accepted job');
358
+ await this.recordDispatchContext(request, body, log);
276
359
  return {
277
360
  workspaceId: request.workspaceId,
278
361
  jobId: request.jobId,
@@ -443,14 +526,10 @@ export class ContainerRepoBootstrapper {
443
526
  * carrying a branch but no PR pushes work onto a branch nobody is told about.
444
527
  */
445
528
  async dispatchCodingShape(request, installation, log, spec) {
446
- // Scoped to the repos this run touches: the one it pushes to, and the template it reads. A
447
- // template outside the installation simply is not cloneable, which the harness reports as a
448
- // clone failure rather than silently running without it.
529
+ // Re-flighted here for the same reason the monorepo's target directory is: an apply dispatch
530
+ // can be days after the review, and a template that has since moved out of reach is a clone
531
+ // this run is about to fail.
449
532
  const reference = await this.resolveReferenceTemplate(request, installation.installationId, log);
450
- const repoIds = [
451
- String(spec.repoGithubId),
452
- ...(reference?.githubId ? [reference.githubId] : []),
453
- ];
454
533
  const referenceRepos = reference
455
534
  ? [
456
535
  {
@@ -463,11 +542,13 @@ export class ContainerRepoBootstrapper {
463
542
  },
464
543
  ]
465
544
  : [];
466
- const ghToken = await this.deps.mintInstallationToken(installation.installationId, {
467
- executionId: request.containerJobId,
468
- workspaceId: request.workspaceId,
469
- repoIds,
470
- });
545
+ // Scoped to the repos this run touches: the one it pushes to, and the template it reads.
546
+ const ghToken = await this.mintDispatchToken(request, installation.installationId, {
547
+ owner: spec.repo.owner,
548
+ name: spec.repo.name,
549
+ repoId: String(spec.repoGithubId),
550
+ baseBranch: spec.repo.baseBranch,
551
+ }, reference);
471
552
  const packageRegistries = (await this.deps.resolvePackageRegistries?.(request.workspaceId)) ?? [];
472
553
  const sessionToken = await this.mintSessionToken(request);
473
554
  // The delivery decides the two fields TOGETHER: with `newBranch` and `pr` the harness pushes
@@ -479,7 +560,8 @@ export class ContainerRepoBootstrapper {
479
560
  const body = {
480
561
  jobId: request.containerJobId,
481
562
  workspaceId: request.workspaceId,
482
- executionId: request.containerJobId,
563
+ // The RUN, not this phase's drive id: see the session mint above.
564
+ executionId: request.jobId,
483
565
  mode: 'coding',
484
566
  systemPrompt: spec.systemPrompt,
485
567
  userPrompt: spec.userPrompt,
@@ -504,6 +586,7 @@ export class ContainerRepoBootstrapper {
504
586
  branch: request.delivery.mode === 'pull_request' ? request.delivery.branch : spec.repo.baseBranch,
505
587
  reference: reference ? `${reference.owner}/${reference.name}` : null,
506
588
  });
589
+ await this.recordDispatchContext(request, body, log);
507
590
  return {
508
591
  workspaceId: request.workspaceId,
509
592
  jobId: request.jobId,
@@ -514,38 +597,106 @@ export class ContainerRepoBootstrapper {
514
597
  * Resolve the reference template a run reads from: its clone spec, plus the numeric id the
515
598
  * run's installation token has to be scoped to.
516
599
  *
517
- * ONE resolution for both dispatch shapes, because both CLONE the template and both mint the
600
+ * ONE resolution for every dispatch shape, because they all CLONE the template and all mint the
518
601
  * token that clone runs on. Two copies is what left the force-push path granting only its push
519
602
  * target while cloning the template with that same token, so a PRIVATE reference architecture
520
603
  * 404s on clone under one delivery and works under the other, and what left it assuming the
521
604
  * template's base branch was `main`.
522
605
  *
523
- * A template the installation cannot read resolves to no id and the conventional branch rather
524
- * than refusing the run: the clone then fails inside the harness naming the repository, which
525
- * is a better report than a pre-flight throw here, and the warning says which read failed.
606
+ * It THROWS on a template it cannot resolve, rather than dispatching with no id and the
607
+ * conventional branch: the container is about to clone it, so the run is over either way, and
608
+ * the difference is whether the report names the reference architecture and what to do about it
609
+ * or arrives as a git error from inside a container that had to be started first.
610
+ *
611
+ * The MESSAGE follows the cause the shared probe reported. "Point the reference architecture at
612
+ * a repository this workspace can reach" is the wrong instruction for a rate limit or a 500,
613
+ * and a monorepo apply dispatches days after a human settled its review, so the failure read
614
+ * here is as likely to be an outage as a typo. The provider error rides as `cause`, so a log
615
+ * describer walking the chain can still see which it was.
616
+ *
617
+ * Re-read rather than carried over from the run-start pre-flight, deliberately: this is the
618
+ * moment the clone happens, and on a monorepo run the pre-flight sits on the other side of a
619
+ * human review. An id plumbed through the request would be a claim about the repository as it
620
+ * was, dispatched against the repository as it is.
526
621
  */
527
622
  async resolveReferenceTemplate(request, installationId, log) {
528
623
  const reference = request.referenceRepo;
529
624
  if (!reference)
530
625
  return undefined;
531
- const template = await runBestEffort(log, 'bootstrap: read reference template repo', () => this.deps.githubClient.getRepo(installationId, {
532
- owner: reference.owner,
533
- repo: reference.name,
534
- }), { reference: `${reference.owner}/${reference.name}` });
535
- return {
536
- owner: reference.owner,
537
- name: reference.name,
538
- cloneUrl: `${this.webBase()}/${reference.owner}/${reference.name}.git`,
539
- baseBranch: template ? defaultBranchOf(template) : 'main',
540
- githubId: template ? String(template.githubId) : null,
541
- };
626
+ const repo = `${reference.owner}/${reference.name}`;
627
+ const read = await this.readTemplate(installationId, reference);
628
+ if (read.status === 'reachable') {
629
+ return {
630
+ owner: reference.owner,
631
+ name: reference.name,
632
+ repoId: String(read.repo.githubId),
633
+ baseBranch: defaultBranchOf(read.repo),
634
+ cloneUrl: `${this.webBase()}/${reference.owner}/${reference.name}.git`,
635
+ };
636
+ }
637
+ log.warn('bootstrap: reference template could not be resolved', {
638
+ reference: repo,
639
+ verdict: read.status,
640
+ });
641
+ if (read.status === 'not_found') {
642
+ throw new Error(`The reference template ${repo} cannot be seen through this workspace's source-control ` +
643
+ `connection, so it cannot be cloned. Point the reference architecture at a repository ` +
644
+ `this workspace can reach, or grant the App access to it, then retry.`);
645
+ }
646
+ throw new Error(`The reference template ${repo} could not be read just now, so this run was not dispatched ` +
647
+ `rather than dispatched against a template it may be unable to clone. Nothing here is ` +
648
+ `misconfigured: retry once the source-control connection recovers. Cause: ${read.detail}`, { cause: read.cause });
649
+ }
650
+ /**
651
+ * Mint the job's clone/push token, scoped to the repos this dispatch resolved.
652
+ *
653
+ * The scope goes through the SHARED `jobTokenRepoIds` rather than a hand-built array, for the
654
+ * case the hand-built one got wrong: a reference architecture naming the run's own target (or
655
+ * its monorepo) is one repository asked for twice, and a `repository_ids` list is a set.
656
+ *
657
+ * A refusal is re-thrown NAMING the template, because a scoped mint has one failure mode this
658
+ * component cannot pre-flight away. GitHub narrows a token only to repositories the
659
+ * installation was GRANTED, while a PUBLIC repository reads perfectly well through the API
660
+ * without being one, so a template outside the App's repository access resolves cleanly above
661
+ * and is refused here. There is no read on this port that separates the two cheaply and
662
+ * correctly on every provider (`getRepoById` answers from a bounded listing on the GitLab
663
+ * adapter, so a null there is not evidence of anything), and dropping the leg would only move
664
+ * the failure to the clone. What is left is to make the refusal say which repository to grant.
665
+ */
666
+ async mintDispatchToken(request, installationId, primary, template) {
667
+ const asTarget = (leg) => ({ installationId, ...leg });
668
+ try {
669
+ return await this.deps.mintInstallationToken(installationId, {
670
+ // The RUN, not this phase's drive id: see the session mint below.
671
+ executionId: request.jobId,
672
+ workspaceId: request.workspaceId,
673
+ repoIds: jobTokenRepoIds(asTarget(primary), template ? [asTarget(template)] : []),
674
+ });
675
+ }
676
+ catch (error) {
677
+ if (!template)
678
+ throw error;
679
+ throw new Error(`The source-control token for this run could not be issued for both ` +
680
+ `${primary.owner}/${primary.name} and the reference template ` +
681
+ `${template.owner}/${template.name}. A GitHub App token can only cover repositories ` +
682
+ `the installation has been GRANTED, and a public repository reads through the API ` +
683
+ `without being one. Grant the App access to ${template.owner}/${template.name}, or ` +
684
+ `point the reference architecture at a repository it already covers, then retry. ` +
685
+ `Cause: ${getErrorMessage(error)}`, { cause: error });
686
+ }
542
687
  }
543
- /** The model-locked LLM-proxy session token a bootstrap container runs under. */
688
+ /**
689
+ * The model-locked LLM-proxy session token a bootstrap container runs under.
690
+ *
691
+ * Keyed on the RUN, never on the drive: a monorepo run's apply phase is dispatched under its
692
+ * own container job id, so minting on that id files the apply's model calls under a key no
693
+ * run-scoped read asks for, and the run reports only what its survey spent.
694
+ */
544
695
  mintSessionToken(request) {
545
696
  return this.deps.sessionService.mint({
546
697
  workspaceId: request.workspaceId,
547
- executionId: request.containerJobId,
548
- agentKind: 'architect',
698
+ executionId: request.jobId,
699
+ agentKind: REPO_BOOTSTRAP_AGENT_KIND,
549
700
  provider: this.deps.model.provider,
550
701
  model: this.deps.model.model,
551
702
  });
@@ -560,6 +711,13 @@ export class ContainerRepoBootstrapper {
560
711
  runId: handle.jobId,
561
712
  jobId: handle.containerJobId,
562
713
  });
714
+ // The tool calls the harness drained on this poll, to the same two destinations an
715
+ // execution step's go to. Filed under the RUN (`runId`) and grouped by the container job,
716
+ // so a monorepo run's apply trajectory reads under the run a person opened. Isolated +
717
+ // best-effort inside `drainToolCalls`: it can never affect this poll's verdict.
718
+ // Correlated by the same two ids every other line about this run carries, so a drain that
719
+ // warns (an image too old to number its calls) names the run it was about.
720
+ await drainToolCalls(this.deps, this.jobHandle(handle), view.spans, logger.child({ jobId: handle.jobId, workspaceId: handle.workspaceId }));
563
721
  if (view.state === 'running') {
564
722
  return view.progress ? { state: 'running', subtasks: view.progress } : { state: 'running' };
565
723
  }
@@ -608,6 +766,48 @@ export class ContainerRepoBootstrapper {
608
766
  const outcome = await this.buildOutcome(handle, record.repoName, result.defaultBranch);
609
767
  return { state: 'done', outcome, ...pr };
610
768
  }
769
+ /**
770
+ * The dispatched container job as an {@link AgentJobHandle}, the shape the shared trajectory
771
+ * drain speaks. `runId` is the bootstrap RUN and `jobId` the container job it dispatched,
772
+ * which is the same split every execution step's handle carries.
773
+ */
774
+ jobHandle(handle) {
775
+ return {
776
+ jobId: handle.containerJobId,
777
+ runId: handle.jobId,
778
+ workspaceId: handle.workspaceId,
779
+ agentKind: REPO_BOOTSTRAP_AGENT_KIND,
780
+ model: this.resolvedModel(),
781
+ provider: this.deps.model.provider,
782
+ };
783
+ }
784
+ /**
785
+ * The resolved model as every OTHER producer writes it: `provider:model`, which is the format
786
+ * `AgentJobHandle.model` and `agentContextSnapshotSchema.model` both document. A bare model id
787
+ * renders beside prefixed ones on the same panel and gives nothing to a reader that splits the
788
+ * field to recover the provider.
789
+ */
790
+ resolvedModel() {
791
+ return `${this.deps.model.provider}:${this.deps.model.model}`;
792
+ }
793
+ /**
794
+ * File what this dispatch handed the agent, so a bootstrap's Provided-context tab answers the
795
+ * same question every other agent run's does.
796
+ *
797
+ * AWAITED for the reason `recordAgentContextSnapshot` states: it runs after the container has
798
+ * already been accepted, so it delays nothing but the handle's return, and an un-awaited insert
799
+ * is dropped outright on the Worker, where this runs inside a Workflow step.
800
+ */
801
+ async recordDispatchContext(request, body, log) {
802
+ await recordBootstrapContextSnapshot(this.deps.agentContextObservability, log, {
803
+ body,
804
+ model: this.resolvedModel(),
805
+ agentKind: REPO_BOOTSTRAP_AGENT_KIND,
806
+ workspaceId: request.workspaceId,
807
+ executionId: request.jobId,
808
+ stepIndex: dispatchStepIndex(request),
809
+ });
810
+ }
611
811
  /**
612
812
  * Best-effort: reclaim the per-run container for a job. Releases through the same
613
813
  * transport the run dispatched to (keyed by job id) — for the Cloudflare backend
@@ -690,6 +890,18 @@ export class ContainerRepoBootstrapper {
690
890
  return record;
691
891
  }
692
892
  }
893
+ /**
894
+ * Which of the RUN's own steps this dispatch is, numbered exactly as the board numbers them.
895
+ *
896
+ * The container dispatch is always the run's LAST move: a new-repo run is only `scaffold`, and a
897
+ * monorepo run's apply follows the survey and the human review. So it is read off the end of the
898
+ * shared `bootstrapStepIds` list rather than by searching it for a step NAME, which cannot
899
+ * answer `-1`. A snapshot filed at a step the run never had is a row every step-scoped read
900
+ * silently drops.
901
+ */
902
+ function dispatchStepIndex(request) {
903
+ return bootstrapStepIds({ monorepo: request.monorepo ?? null }).length - 1;
904
+ }
693
905
  /** A repo's default branch, or the conventional fallback when the provider reported none. */
694
906
  function defaultBranchOf(repo) {
695
907
  return repo.defaultBranch ?? 'main';