@drisp/cli 0.6.8 → 0.6.9

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
@@ -8,16 +8,23 @@ import {
8
8
  PORTABLE_PROVIDER_ENV_VARS,
9
9
  RUN_EXIT_CODE,
10
10
  attachRuntimeEventLoop,
11
+ behindMain,
11
12
  bootstrapRuntimeConfig,
12
13
  buildApiKeyHelperSettings,
13
14
  buildPhaseFeedEvent,
15
+ buildPlanSchema,
14
16
  buildPostByToolUseId,
15
17
  buildProbeConfigs,
16
18
  centerAnsi,
19
+ childWorktreePath,
17
20
  classifyFailure,
21
+ commandSchema,
22
+ commitBuildFiles,
18
23
  compactText,
19
24
  computeInputRows,
20
25
  connectAlreadyConnectedSchema,
26
+ connectCodeResponseSchema,
27
+ connectPendingResponseSchema,
21
28
  connectRedeemResponseSchema,
22
29
  connectStartResponseSchema,
23
30
  createFeedMapper,
@@ -26,10 +33,15 @@ import {
26
33
  createSessionStore,
27
34
  credentialHelperKey,
28
35
  cursorToVisualPosition,
36
+ defaultWorktreesRoot,
29
37
  detectClaudeVersion,
38
+ ensureBuildWorktree,
39
+ ensureChildWorktree,
30
40
  expandHome,
31
41
  extractFriendlyServerName,
32
42
  extractPermissionSnapshot,
43
+ fallbackBranch,
44
+ finalizePlan,
33
45
  fit,
34
46
  fitAnsi,
35
47
  forgetRunner,
@@ -46,12 +58,18 @@ import {
46
58
  getLatestRunForSession,
47
59
  getMostRecentAthenaSession,
48
60
  getSessionMeta,
61
+ git,
62
+ hasUncommittedWork,
49
63
  hintGlyphs,
64
+ historyRecordSchema,
50
65
  hubHttpUrl,
51
66
  identityHubOrigin,
52
67
  isBashToolResponse,
53
68
  isCommandPrefix,
69
+ isGitHubRepository,
54
70
  isScopedPermissionsRequest,
71
+ isStartAnswer,
72
+ lastCommitTouching,
55
73
  listAwaitingAttentionRuns,
56
74
  listHarnessAdapters,
57
75
  listHarnessCapabilities,
@@ -66,6 +84,8 @@ import {
66
84
  overriddenIdentityPath,
67
85
  parseFrontmatter,
68
86
  parseToolName,
87
+ planBuild,
88
+ planInputSchema,
69
89
  platformRunnerServices,
70
90
  probeSkipReason,
71
91
  processRegistry,
@@ -74,6 +94,8 @@ import {
74
94
  registerCleanupOnExit,
75
95
  releaseMcpAsset,
76
96
  renderInputLines,
97
+ repoLayoutAt,
98
+ repositoryBindingSchema,
77
99
  require_string_width,
78
100
  resolveClaudeBinary,
79
101
  resolveClaudeSettingsSurfacePaths,
@@ -88,12 +110,18 @@ import {
88
110
  runExec,
89
111
  runMachineRunners,
90
112
  runProbe,
113
+ runViewSchema,
114
+ runnerAdoptRequestSchema,
91
115
  runnerConfigDirectory,
92
116
  runnerConfigSchema,
117
+ runnerDisconnectPreviewSchema,
93
118
  runnerDisconnectResponseSchema,
94
119
  runnerIdentities,
120
+ runnerListEntrySchema,
121
+ runnerListSchema,
95
122
  runnerServiceUnit,
96
123
  runnerSoftwareVersion,
124
+ runnerUpdateRequestSchema,
97
125
  serviceInstallationId,
98
126
  sessionsDir,
99
127
  sliceAnsi,
@@ -101,12 +129,14 @@ import {
101
129
  startDashboardDecisionDrain,
102
130
  startWorkflowExecution,
103
131
  supportsSessionApproval,
132
+ teamDefaultRequestSchema,
133
+ teamDefaultSchema,
104
134
  teamIdentityPath,
105
135
  todoGlyphSet,
106
136
  visualPositionToOffset,
107
137
  wrapText,
108
138
  writeIdentityOnce
109
- } from "./chunk-HN3WNQVY.js";
139
+ } from "./chunk-2JL4QT55.js";
110
140
  import {
111
141
  generateId as generateId2
112
142
  } from "./chunk-PQIB66SG.js";
@@ -120,9 +150,21 @@ import {
120
150
  useTheme
121
151
  } from "./chunk-KPDCVR6Q.js";
122
152
  import {
153
+ BUILD_STRINGS,
154
+ CANNOT_PASS_TEMPLATE,
155
+ DESCRIPTION_TEMPLATE,
156
+ GATES,
157
+ LAYOUT_MARKERS,
158
+ NO_GATE_TEXT_TEMPLATE,
159
+ ORCHESTRATOR_LIMITS,
160
+ QA_SECTIONS,
161
+ RENDERER_RULES,
162
+ STAGE_TEMPLATE,
163
+ WORKFLOW_MD_TEMPLATE,
123
164
  collectMcpServersWithOptions,
124
165
  createSteerQueue,
125
166
  deserializeRunMemory,
167
+ external_exports,
126
168
  findMarketplaceRepoDir,
127
169
  gatherMarketplaceWorkflowSources,
128
170
  hasProjectWorkflow,
@@ -133,6 +175,7 @@ import {
133
175
  listMarketplaceWorkflows,
134
176
  listMarketplaceWorkflowsFromRepo,
135
177
  listWorkflows,
178
+ parseWorkflowConfig,
136
179
  projectConfigPath,
137
180
  pullMarketplaceRepo,
138
181
  readConfig,
@@ -145,10 +188,12 @@ import {
145
188
  resolveWorkflowMarketplaceSource,
146
189
  updateWorkflow,
147
190
  updateWorkflows,
191
+ verifyRendered,
148
192
  wakesFreshAfterHandover,
193
+ workflowRegistryDir,
149
194
  writeGlobalConfig,
150
195
  writeProjectConfig
151
- } from "./chunk-CIPO2P2Q.js";
196
+ } from "./chunk-YUVMVYUF.js";
152
197
  import {
153
198
  __toESM
154
199
  } from "./chunk-G3PMV62Z.js";
@@ -156,10 +201,10 @@ import {
156
201
  // src/app/entry/cli.tsx
157
202
  import { render } from "ink";
158
203
  import meow from "meow";
159
- import fs6 from "fs";
204
+ import fs9 from "fs";
160
205
  import { createRequire } from "module";
161
206
  import os5 from "os";
162
- import path6 from "path";
207
+ import path9 from "path";
163
208
  import { fileURLToPath } from "url";
164
209
 
165
210
  // src/app/shell/sessionUiState.ts
@@ -5278,14 +5323,14 @@ function extractWebSearch(_input, response) {
5278
5323
  return { type: "text", content: extractTextContent(response), maxLines: 10 };
5279
5324
  }
5280
5325
  function extractNotebookEdit(input, _response) {
5281
- const path7 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5326
+ const path10 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5282
5327
  const mode = typeof input["edit_mode"] === "string" ? input["edit_mode"] : "replace";
5283
5328
  const source = typeof input["new_source"] === "string" ? input["new_source"] : "";
5284
- if (!source) return { type: "text", content: `${mode} cell in ${path7}` };
5329
+ if (!source) return { type: "text", content: `${mode} cell in ${path10}` };
5285
5330
  return {
5286
5331
  type: "code",
5287
5332
  content: source,
5288
- language: detectLanguage(path7),
5333
+ language: detectLanguage(path10),
5289
5334
  maxLines: 10
5290
5335
  };
5291
5336
  }
@@ -5811,10 +5856,10 @@ function extractToolSubject(toolName, toolInput) {
5811
5856
  const parts = [];
5812
5857
  const pattern = str(input, "pattern");
5813
5858
  const glob = str(input, "glob");
5814
- const path7 = str(input, "path");
5859
+ const path10 = str(input, "path");
5815
5860
  if (pattern) parts.push(`"${pattern}"`);
5816
5861
  if (glob) parts.push(glob);
5817
- if (path7) parts.push(`in ${path7}`);
5862
+ if (path10) parts.push(`in ${path10}`);
5818
5863
  return parts.length > 0 ? parts.join(" ") : void 0;
5819
5864
  }
5820
5865
  case "Skill":
@@ -5970,8 +6015,8 @@ function renderDetailLines(event, width, pairedPostEvent, theme = darkTheme) {
5970
6015
  event.data.tool_input
5971
6016
  );
5972
6017
  const header = buildCompactHeader(event, width, { subject, theme });
5973
- const json = JSON.stringify(event.data.tool_input, null, 2);
5974
- const requestLines = highlightCode(json, cw, "json");
6018
+ const json2 = JSON.stringify(event.data.tool_input, null, 2);
6019
+ const requestLines = highlightCode(json2, cw, "json");
5975
6020
  return buildResult(header, requestLines, true);
5976
6021
  }
5977
6022
  case "subagent.start": {
@@ -5994,8 +6039,8 @@ function renderDetailLines(event, width, pairedPostEvent, theme = darkTheme) {
5994
6039
  } else if (prompt) {
5995
6040
  content = renderMarkdownToLines(prompt, cw);
5996
6041
  } else {
5997
- const json = JSON.stringify(event.data, null, 2);
5998
- content = highlightCode(json, cw, "json");
6042
+ const json2 = JSON.stringify(event.data, null, 2);
6043
+ content = highlightCode(json2, cw, "json");
5999
6044
  }
6000
6045
  return buildResult(header, content, !prompt && !response);
6001
6046
  }
@@ -6104,8 +6149,8 @@ Named by Turn ${event.data.turn} of Workflow Run \`${event.data.runId}\`.`,
6104
6149
  case "elicitation.result":
6105
6150
  case "artifacts.manifest": {
6106
6151
  const header = buildCompactHeader(event, width, { theme });
6107
- const json = JSON.stringify(event.raw ?? event.data, null, 2);
6108
- const content = highlightCode(json, cw, "json");
6152
+ const json2 = JSON.stringify(event.raw ?? event.data, null, 2);
6153
+ const content = highlightCode(json2, cw, "json");
6109
6154
  return buildResult(header, content, true);
6110
6155
  }
6111
6156
  default: {
@@ -13038,7 +13083,7 @@ var thisMachine = (installationId) => ({
13038
13083
  os: `${platform()} ${release()}`.slice(0, 64),
13039
13084
  softwareVersion: runnerSoftwareVersion
13040
13085
  });
13041
- var post = (origin, path7, body) => fetch(new URL(path7, origin), {
13086
+ var post = (origin, path10, body) => fetch(new URL(path10, origin), {
13042
13087
  method: "POST",
13043
13088
  headers: { "Content-Type": "application/json" },
13044
13089
  body: JSON.stringify(body)
@@ -13057,8 +13102,8 @@ async function alreadyConnected(existing, { forTeam, ownIdentity }) {
13057
13102
  const problem = own ? await credentialAccepted(own.config) ? void 0 : "lapsed" : forTeam(existing.orgId).length ? "other_runner" : "missing";
13058
13103
  const runner = { orgId: existing.orgId, runnerId: existing.runnerId, name: existing.name };
13059
13104
  if (own && problem === void 0) return { status: "already_connected", ...runner, path: own.path };
13060
- const path7 = (own ?? forTeam(existing.orgId)[0])?.path;
13061
- return { status: "identity_unusable", ...runner, problem, ...path7 ? { path: path7 } : {} };
13105
+ const path10 = (own ?? forTeam(existing.orgId)[0])?.path;
13106
+ return { status: "identity_unusable", ...runner, problem, ...path10 ? { path: path10 } : {} };
13062
13107
  }
13063
13108
  async function connectWithCode({
13064
13109
  code,
@@ -13130,20 +13175,20 @@ async function saveIdentity(origin, credential, { single, forTeam }, directory)
13130
13175
  name: credential.name
13131
13176
  });
13132
13177
  const replaced = [];
13133
- for (const { path: path8 } of forTeam(credential.orgId)) {
13134
- const aside = `${path8}.replaced-${Date.now()}`;
13135
- await rename(path8, aside);
13178
+ for (const { path: path11 } of forTeam(credential.orgId)) {
13179
+ const aside = `${path11}.replaced-${Date.now()}`;
13180
+ await rename(path11, aside);
13136
13181
  replaced.push(aside);
13137
13182
  }
13138
- const path7 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13183
+ const path10 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13139
13184
  try {
13140
- await writeIdentityOnce(path7, JSON.stringify(config, null, 2) + "\n");
13185
+ await writeIdentityOnce(path10, JSON.stringify(config, null, 2) + "\n");
13141
13186
  } catch (error) {
13142
13187
  if (error.code === "EEXIST")
13143
- throw new Error(`Another Connect wrote a Runner identity for this Team at ${path7} meanwhile; it was left unchanged.`);
13188
+ throw new Error(`Another Connect wrote a Runner identity for this Team at ${path10} meanwhile; it was left unchanged.`);
13144
13189
  throw error;
13145
13190
  }
13146
- return { status: "connected", config, path: path7, replaced };
13191
+ return { status: "connected", config, path: path10, replaced };
13147
13192
  }
13148
13193
  function pathSegment(value) {
13149
13194
  const segment = encodeURIComponent(value);
@@ -13183,7 +13228,7 @@ async function refuseExisting(target) {
13183
13228
  throw new Error(`This machine already has a Runner${name ? ` (${name})` : ""} configured at ${target}.`);
13184
13229
  }
13185
13230
  async function disconnectRunner({
13186
- identity: { path: path7, config },
13231
+ identity: { path: path10, config },
13187
13232
  serviceManager,
13188
13233
  othersRemain
13189
13234
  }) {
@@ -13216,7 +13261,7 @@ async function disconnectRunner({
13216
13261
  await restore();
13217
13262
  throw new Error(`The Hub refused to disconnect ${name} (${body.error ?? response.status}); it is still connected.`);
13218
13263
  }
13219
- await forgetRunner(path7);
13264
+ await forgetRunner(path10);
13220
13265
  if (othersRemain) await restore();
13221
13266
  else await serviceManager?.remove();
13222
13267
  return result;
@@ -13350,18 +13395,18 @@ async function runRunnerCommandV2(input, options = {}) {
13350
13395
  `This machine is already connected to this Team as ${outcome.name}, but ${why}. ` + (code ? `The code was not used: connect a new Runner in its place with ${retry}. ` : `Connect a new Runner in its place with a Connect code from Settings \u2192 Runners: ${retry}. `) + `The old ${outcome.name} stays listed in Settings \u2192 Runners, offline.`
13351
13396
  );
13352
13397
  }
13353
- const { config, path: path7, replaced } = outcome;
13398
+ const { config, path: path10, replaced } = outcome;
13354
13399
  for (const aside of replaced)
13355
13400
  process.stdout.write(
13356
13401
  `The Hub no longer recognised this machine's earlier identity for this Team; it was moved aside to ${aside}.
13357
13402
  `
13358
13403
  );
13359
13404
  process.stdout.write(
13360
- `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path7}.
13405
+ `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path10}.
13361
13406
  `
13362
13407
  );
13363
13408
  if (services) {
13364
- const service = await ensureService(services, config, path7);
13409
+ const service = await ensureService(services, config, path10);
13365
13410
  process.stdout.write(`${serviceStarted(service)}
13366
13411
  `);
13367
13412
  return 0;
@@ -13370,7 +13415,7 @@ async function runRunnerCommandV2(input, options = {}) {
13370
13415
  process.stdout.write(
13371
13416
  "No launchd or systemd here to run the Runner as a background service; running it in the foreground instead.\n"
13372
13417
  );
13373
- await runConfiguredRunner(path7, options.signal);
13418
+ await runConfiguredRunner(path10, options.signal);
13374
13419
  return 0;
13375
13420
  }
13376
13421
  case "disconnect": {
@@ -13444,9 +13489,9 @@ async function runRunnerCommandV2(input, options = {}) {
13444
13489
  configured: true,
13445
13490
  // One entry per Team this machine serves; credentials are never printed.
13446
13491
  teams: identities.map(
13447
- ({ path: path7, config: { token: _token, ...config } }) => ({
13492
+ ({ path: path10, config: { token: _token, ...config } }) => ({
13448
13493
  ...config,
13449
- path: path7
13494
+ path: path10
13450
13495
  })
13451
13496
  ),
13452
13497
  service: service ? {
@@ -13513,14 +13558,1248 @@ async function recoveryIdentity(runId) {
13513
13558
  throw new Error(`No Runner on this machine holds Run ${runId}.`);
13514
13559
  }
13515
13560
 
13561
+ // src/app/entry/buildCommand.ts
13562
+ import { execFileSync } from "child_process";
13563
+
13564
+ // src/core/build/planCommand.ts
13565
+ import fs6 from "fs";
13566
+ import path6 from "path";
13567
+
13568
+ // src/core/build/io.ts
13569
+ import fs5 from "fs";
13570
+ import path5 from "path";
13571
+ var BuildUsageError = class extends Error {
13572
+ };
13573
+ var PlanFailure = class extends Error {
13574
+ };
13575
+ function requireEnv(env2, name) {
13576
+ const value = env2[name];
13577
+ if (!value)
13578
+ throw new BuildUsageError(
13579
+ `${name} is not set: drisp build runs inside an Orchestrator Run.`
13580
+ );
13581
+ return value;
13582
+ }
13583
+ function readJson(file) {
13584
+ try {
13585
+ return JSON.parse(fs5.readFileSync(file, "utf-8"));
13586
+ } catch {
13587
+ return void 0;
13588
+ }
13589
+ }
13590
+ function writeFile(file, content) {
13591
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
13592
+ fs5.writeFileSync(file, content, "utf-8");
13593
+ }
13594
+ var json = (value) => `${JSON.stringify(value, null, 2)}
13595
+ `;
13596
+ function readInput(file) {
13597
+ const raw = readJson(file);
13598
+ if (raw === void 0)
13599
+ throw new PlanFailure(`Could not read plan-input.json at ${file}.`);
13600
+ const parsed = planInputSchema.safeParse(raw);
13601
+ if (!parsed.success) {
13602
+ const issue = parsed.error.issues[0];
13603
+ throw new PlanFailure(
13604
+ `plan-input.json is invalid at ${issue.path.join(".") || "(root)"}: ${issue.message}`
13605
+ );
13606
+ }
13607
+ return parsed.data;
13608
+ }
13609
+
13610
+ // src/core/workflows/builtins/build-child/version.ts
13611
+ import { createHash } from "crypto";
13612
+ function computeTemplateVersion() {
13613
+ const parts = [
13614
+ WORKFLOW_MD_TEMPLATE,
13615
+ STAGE_TEMPLATE,
13616
+ CANNOT_PASS_TEMPLATE,
13617
+ NO_GATE_TEXT_TEMPLATE,
13618
+ DESCRIPTION_TEMPLATE,
13619
+ JSON.stringify(QA_SECTIONS),
13620
+ JSON.stringify(GATES),
13621
+ JSON.stringify(RENDERER_RULES),
13622
+ JSON.stringify(LAYOUT_MARKERS)
13623
+ ];
13624
+ const hash = createHash("sha256");
13625
+ for (const part of parts) hash.update(`${part.length}:${part}`);
13626
+ return hash.digest("hex");
13627
+ }
13628
+ var templateVersion = computeTemplateVersion();
13629
+
13630
+ // src/core/build/planCommand.ts
13631
+ function checkGenerated(directory, workflow, host, layout) {
13632
+ const workflowJson = fs6.readFileSync(
13633
+ path6.join(directory, "workflow.json"),
13634
+ "utf-8"
13635
+ );
13636
+ const workflowMd = fs6.readFileSync(
13637
+ path6.join(directory, "workflow.md"),
13638
+ "utf-8"
13639
+ );
13640
+ try {
13641
+ parseWorkflowConfig(JSON.parse(workflowJson), {
13642
+ instructions: workflowMd,
13643
+ host
13644
+ });
13645
+ } catch (error) {
13646
+ throw new PlanFailure(
13647
+ `${workflow.identifier}: ${error instanceof Error ? error.message : String(error)}`
13648
+ );
13649
+ }
13650
+ const verified = verifyRendered({
13651
+ fill: workflow.fill,
13652
+ layout,
13653
+ workflowName: workflow.workflowName,
13654
+ workflowJson,
13655
+ workflowMd
13656
+ });
13657
+ if (!verified.ok)
13658
+ throw new PlanFailure(
13659
+ `${workflow.identifier}: the written ${verified.file} does not match what its fill renders.`
13660
+ );
13661
+ }
13662
+ function runBuildPlan(deps) {
13663
+ const orchestratorRunId = requireEnv(deps.env, "DRISP_RUN_ID");
13664
+ const runnerId = requireEnv(deps.env, "DRISP_RUNNER_ID");
13665
+ const artifacts = requireEnv(deps.env, "DRISP_RUN_ARTIFACTS");
13666
+ const fixtureInput = deps.env["DRISP_BUILD_PLAN_INPUT"];
13667
+ const inputPath = fixtureInput ?? deps.inputPath;
13668
+ if (!inputPath)
13669
+ throw new BuildUsageError("Usage: drisp build plan --input <path>");
13670
+ const fail = (reason) => {
13671
+ writeFile(path6.join(artifacts, "build_plan_error.json"), json({ reason }));
13672
+ return { status: "failed", reason };
13673
+ };
13674
+ try {
13675
+ if (!isGitHubRepository(deps.folder))
13676
+ throw new PlanFailure(BUILD_STRINGS.notAGitHubRepository);
13677
+ const input = readInput(path6.resolve(deps.folder, inputPath));
13678
+ const worktree = ensureBuildWorktree({
13679
+ folder: deps.folder,
13680
+ slugId: input.project.slugId,
13681
+ worktreesRoot: deps.worktreesRoot
13682
+ });
13683
+ const designDir = path6.join(
13684
+ worktree.path,
13685
+ ".athena",
13686
+ "workflows",
13687
+ input.project.slugId
13688
+ );
13689
+ writeFile(path6.join(designDir, "plan-input.json"), json(input));
13690
+ const fixtureDir = fixtureInput ? path6.dirname(path6.resolve(fixtureInput)) : void 0;
13691
+ const fillPath = (identifier) => path6.join(designDir, identifier, "fill.json");
13692
+ const fills = Object.fromEntries(
13693
+ input.issues.map((issue) => {
13694
+ const fixture = fixtureDir ? path6.join(fixtureDir, "fills", `${issue.identifier}.json`) : void 0;
13695
+ const file = fixture && fs6.existsSync(fixture) ? fixture : fillPath(issue.identifier);
13696
+ return [issue.identifier, readJson(file)];
13697
+ })
13698
+ );
13699
+ const fixturePullRequests = fixtureDir ? readJson(path6.join(fixtureDir, "pull-requests.json")) : void 0;
13700
+ const pullRequests = fixturePullRequests ?? Object.fromEntries(
13701
+ input.issues.filter(
13702
+ (issue) => issue.branchName && issue.state.type !== "canceled" && !issue.labels.includes("ready-for-human")
13703
+ ).map((issue) => [
13704
+ issue.branchName,
13705
+ deps.pullRequests(deps.folder, issue.branchName)
13706
+ ])
13707
+ );
13708
+ const layout = repoLayoutAt(worktree.path, "HEAD");
13709
+ const result = planBuild({
13710
+ input,
13711
+ fills,
13712
+ pullRequests,
13713
+ orchestratorRunId,
13714
+ folder: deps.folder,
13715
+ runnerId,
13716
+ templateVersion,
13717
+ layout
13718
+ });
13719
+ if (result.status === "failed") return fail(result.reason);
13720
+ if (result.status === "needs_fills")
13721
+ return {
13722
+ status: "needs_fills",
13723
+ templateVersion,
13724
+ fills: result.fills.map((fill) => ({
13725
+ ...fill,
13726
+ path: fillPath(fill.identifier)
13727
+ }))
13728
+ };
13729
+ const host = deps.hostCapabilities();
13730
+ for (const workflow of result.workflows) {
13731
+ const directory = path6.join(designDir, workflow.identifier);
13732
+ writeFile(path6.join(directory, "fill.json"), json(workflow.fill));
13733
+ writeFile(path6.join(directory, "workflow.json"), workflow.workflowJson);
13734
+ writeFile(path6.join(directory, "workflow.md"), workflow.workflowMd);
13735
+ checkGenerated(directory, workflow, host, layout);
13736
+ }
13737
+ const relativeDesignDir = path6.relative(worktree.path, designDir);
13738
+ commitBuildFiles(
13739
+ worktree.path,
13740
+ [relativeDesignDir],
13741
+ `drisp build: Generated Workflows for ${input.project.slugId}`
13742
+ );
13743
+ const commit = lastCommitTouching(worktree.path, relativeDesignDir, [
13744
+ path6.join(relativeDesignDir, "plan.json")
13745
+ ]);
13746
+ const plan = finalizePlan(result.plan, commit);
13747
+ writeFile(path6.join(designDir, "plan.json"), json(plan));
13748
+ commitBuildFiles(
13749
+ worktree.path,
13750
+ [path6.join(relativeDesignDir, "plan.json")],
13751
+ `drisp build: Run plan for ${input.project.slugId}`
13752
+ );
13753
+ writeFile(path6.join(artifacts, "build_plan.json"), json(plan));
13754
+ return {
13755
+ status: "planned",
13756
+ entries: plan.groups.reduce((sum, group) => sum + group.issues.length, 0),
13757
+ skipped: plan.skipped.length
13758
+ };
13759
+ } catch (error) {
13760
+ if (error instanceof BuildUsageError) throw error;
13761
+ return fail(error instanceof Error ? error.message : String(error));
13762
+ }
13763
+ }
13764
+
13765
+ // src/core/build/tick.ts
13766
+ import { randomUUID as randomUUID2 } from "crypto";
13767
+ import fs7 from "fs";
13768
+ import path7 from "path";
13769
+
13770
+ // ../../packages/client/src/index.ts
13771
+ var runArtifactSchema = external_exports.object({ name: external_exports.string(), seq: external_exports.number().int(), json: external_exports.json() });
13772
+ var githubRepositorySchema = external_exports.object({
13773
+ installationId: external_exports.number().int().positive(),
13774
+ repositoryId: external_exports.number().int().positive(),
13775
+ owner: external_exports.string(),
13776
+ name: external_exports.string(),
13777
+ defaultBranch: external_exports.string()
13778
+ });
13779
+ var ClientError = class extends Error {
13780
+ constructor(code, status) {
13781
+ super(code);
13782
+ this.code = code;
13783
+ this.status = status;
13784
+ }
13785
+ code;
13786
+ status;
13787
+ };
13788
+ var DrispClient = class _DrispClient {
13789
+ constructor(baseUrl, orgId, token) {
13790
+ this.baseUrl = baseUrl;
13791
+ this.orgId = orgId;
13792
+ this.token = token;
13793
+ const url = new URL(baseUrl);
13794
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
13795
+ throw new Error("TLS_required");
13796
+ }
13797
+ baseUrl;
13798
+ orgId;
13799
+ token;
13800
+ /**
13801
+ * A client that authenticates with a Run's delegation token (root ADR-0008), from the agent's environment:
13802
+ * `hubUrl` is `DRISP_HUB_URL`, the Team-scoped base `https://<hub>/v2/orgs/<orgId>`, and `token` is
13803
+ * `DRISP_RUN_TOKEN`. The token starts, cancels and reads only its own Run's children.
13804
+ */
13805
+ static delegated(hubUrl, token) {
13806
+ const url = new URL(hubUrl);
13807
+ const match = /^\/v2\/orgs\/([^/]+)\/?$/.exec(url.pathname);
13808
+ if (!match) throw new Error("invalid_hub_url");
13809
+ return new _DrispClient(url.origin, decodeURIComponent(match[1]), async () => token);
13810
+ }
13811
+ async request(path10, body) {
13812
+ const response = await fetch(
13813
+ `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}${path10}`,
13814
+ {
13815
+ method: body === void 0 ? "GET" : "POST",
13816
+ headers: {
13817
+ Authorization: `Bearer ${await this.token()}`,
13818
+ "Content-Type": "application/json"
13819
+ },
13820
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
13821
+ }
13822
+ );
13823
+ const result = await response.json();
13824
+ if (!response.ok)
13825
+ throw new ClientError(result.error ?? `HTTP ${response.status}`, response.status);
13826
+ return result;
13827
+ }
13828
+ submit(command) {
13829
+ return this.request("/commands", commandSchema.parse(command));
13830
+ }
13831
+ /** The recent Runs; with a filter, every Run carrying that key or every child of that Run, newest first. */
13832
+ async runs(filter) {
13833
+ const query = !filter ? "" : "key" in filter ? `?key=${encodeURIComponent(filter.key)}` : `?parent=${encodeURIComponent(filter.parent)}`;
13834
+ return external_exports.object({ hubEpoch: external_exports.string(), runs: external_exports.array(runViewSchema) }).parse(await this.request(`/runs${query}`));
13835
+ }
13836
+ /** The newest `run.artifact` named `name` that the Run reported; `not_found` when it reported none. */
13837
+ async artifact(runId, name) {
13838
+ return runArtifactSchema.parse(
13839
+ await this.request(`/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(name)}`)
13840
+ );
13841
+ }
13842
+ async run(runId) {
13843
+ return runViewSchema.parse(await this.request(`/runs/${encodeURIComponent(runId)}`));
13844
+ }
13845
+ history(runId, after = 0) {
13846
+ return this.request(`/runs/${encodeURIComponent(runId)}/history?after=${after}`);
13847
+ }
13848
+ async runners() {
13849
+ return runnerListSchema.parse(await this.request("/runners"));
13850
+ }
13851
+ /** A one-use Connect code bound to the signed-in member, for `drisp runner connect <code>` (root ADR-0005). */
13852
+ async createConnectCode(hubEpoch) {
13853
+ return connectCodeResponseSchema.parse(await this.request("/runners/connect-codes", { hubEpoch }));
13854
+ }
13855
+ /** Rename a Runner, set its capacity, or change its Audience (root ADR-0005). */
13856
+ async updateRunner(hubEpoch, runnerId, update) {
13857
+ const body = runnerUpdateRequestSchema.parse({ hubEpoch, ...update });
13858
+ return runnerListEntrySchema.parse(await this.request(`/runners/${encodeURIComponent(runnerId)}`, body));
13859
+ }
13860
+ /** Admin only: where a member's Runs go when they have no ready Personal Runner; `null` clears it. */
13861
+ async setTeamDefault(hubEpoch, teamDefault) {
13862
+ const body = teamDefaultRequestSchema.parse({ hubEpoch, teamDefault });
13863
+ return external_exports.object({ teamDefault: teamDefaultSchema.nullable() }).parse(await this.request("/team-default", body));
13864
+ }
13865
+ /** The Runs a Disconnect of `runnerId` would stop, for its confirmation. */
13866
+ async disconnectPreview(runnerId) {
13867
+ return runnerDisconnectPreviewSchema.parse(
13868
+ await this.request(`/runners/${encodeURIComponent(runnerId)}/disconnect`)
13869
+ );
13870
+ }
13871
+ /** Disconnect (root ADR-0005): the Runner leaves the Team and its open Runs are Stopped. */
13872
+ async disconnectRunner(hubEpoch, runnerId) {
13873
+ return runnerDisconnectResponseSchema.parse(
13874
+ await this.request(`/runners/${encodeURIComponent(runnerId)}/disconnect`, { hubEpoch })
13875
+ );
13876
+ }
13877
+ /** Admin only: take over a Runner paused as "Needs an owner" because its member left, so it serves again. */
13878
+ async adoptRunner(hubEpoch, runnerId) {
13879
+ const body = runnerAdoptRequestSchema.parse({ hubEpoch });
13880
+ return runnerListEntrySchema.parse(await this.request(`/runners/${encodeURIComponent(runnerId)}/adopt`, body));
13881
+ }
13882
+ submitManaged(profileId, command, repository) {
13883
+ return this.request("/managed-runs", {
13884
+ profileId,
13885
+ ...repository,
13886
+ command: commandSchema.parse(command)
13887
+ });
13888
+ }
13889
+ async repositories() {
13890
+ const schema = external_exports.object({
13891
+ configured: external_exports.boolean(),
13892
+ installationUrl: external_exports.string().url().optional(),
13893
+ repositories: external_exports.array(repositoryBindingSchema)
13894
+ });
13895
+ try {
13896
+ return schema.parse(await this.request("/repositories"));
13897
+ } catch (error) {
13898
+ if (error instanceof ClientError && error.status === 404 && error.code === "not_found")
13899
+ return schema.parse({ configured: false, repositories: [] });
13900
+ throw error;
13901
+ }
13902
+ }
13903
+ async connectGitHub(hubEpoch) {
13904
+ return external_exports.object({
13905
+ connectionId: external_exports.string(),
13906
+ userCode: external_exports.string(),
13907
+ verificationUri: external_exports.string().url(),
13908
+ expiresAt: external_exports.number(),
13909
+ interval: external_exports.number(),
13910
+ installationUrl: external_exports.string().url()
13911
+ }).parse(await this.request("/github/device", { hubEpoch }));
13912
+ }
13913
+ async pollGitHub(hubEpoch, connectionId) {
13914
+ return external_exports.discriminatedUnion("state", [
13915
+ external_exports.object({ state: external_exports.literal("pending"), interval: external_exports.number() }),
13916
+ external_exports.object({
13917
+ state: external_exports.literal("authorized"),
13918
+ authorizationId: external_exports.string(),
13919
+ expiresAt: external_exports.number(),
13920
+ repositories: external_exports.array(githubRepositorySchema)
13921
+ })
13922
+ ]).parse(await this.request("/github/device/poll", { hubEpoch, connectionId }));
13923
+ }
13924
+ async bindRepository(hubEpoch, authorizationId, repository, settings) {
13925
+ return repositoryBindingSchema.parse(
13926
+ await this.request("/repositories", {
13927
+ hubEpoch,
13928
+ authorizationId,
13929
+ installationId: repository.installationId,
13930
+ repositoryId: repository.repositoryId,
13931
+ ...settings
13932
+ })
13933
+ );
13934
+ }
13935
+ async updateRepository(hubEpoch, id, settings) {
13936
+ return repositoryBindingSchema.parse(
13937
+ await this.request(`/repositories/${encodeURIComponent(id)}`, {
13938
+ hubEpoch,
13939
+ ...settings
13940
+ })
13941
+ );
13942
+ }
13943
+ disconnectRepository(hubEpoch, id) {
13944
+ return this.request(`/repositories/${encodeURIComponent(id)}/disconnect`, { hubEpoch });
13945
+ }
13946
+ receipt(commandId) {
13947
+ return this.request(`/commands/${encodeURIComponent(commandId)}`);
13948
+ }
13949
+ deleteSession(hubEpoch, sessionId) {
13950
+ return this.request("/sessions/delete", { hubEpoch, sessionId });
13951
+ }
13952
+ async pendingDevice(code) {
13953
+ return external_exports.object({ name: external_exports.string() }).parse(await this.request(`/auth/device?code=${encodeURIComponent(code)}`));
13954
+ }
13955
+ /** Approves the macOS app's sign-in for this client's Team: the token is bound to it. */
13956
+ approveDevice(userCode) {
13957
+ return this.request("/auth/device/approve", { userCode });
13958
+ }
13959
+ /** The machine asking to Connect with a code it printed (`drisp runner connect` with no code; root ADR-0005). */
13960
+ async pendingConnect(code) {
13961
+ return connectPendingResponseSchema.parse(await this.request(`/runners/connect/pending?code=${encodeURIComponent(code)}`));
13962
+ }
13963
+ /** Connects the machine that printed `code` to this client's Team, as a Runner with `audience`. Team needs an admin. */
13964
+ approveConnect(code, audience, hubEpoch) {
13965
+ return this.request("/runners/connect/approve", { code, audience, ...hubEpoch === void 0 ? {} : { hubEpoch } });
13966
+ }
13967
+ /** Refuses the machine that printed `code`; its wait ends without a credential. */
13968
+ denyConnect(code) {
13969
+ return this.request("/runners/connect/deny", { code });
13970
+ }
13971
+ async historyPage(runId, after = 0) {
13972
+ const page = external_exports.object({
13973
+ through: external_exports.number(),
13974
+ archives: external_exports.array(
13975
+ external_exports.object({
13976
+ id: external_exports.string(),
13977
+ checksum: external_exports.string(),
13978
+ first_seq: external_exports.number(),
13979
+ last_seq: external_exports.number()
13980
+ })
13981
+ ),
13982
+ batches: external_exports.array(external_exports.object({ firstSeq: external_exports.number(), records: external_exports.array(historyRecordSchema) }))
13983
+ }).parse(await this.history(runId, after));
13984
+ const records = [];
13985
+ const archive = page.archives[0];
13986
+ if (archive) {
13987
+ const response = await fetch(
13988
+ `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}/runs/${encodeURIComponent(runId)}/objects/${encodeURIComponent(archive.id)}`,
13989
+ { headers: { Authorization: `Bearer ${await this.token()}` } }
13990
+ );
13991
+ if (!response.ok) throw new Error(`history_download_${response.status}`);
13992
+ const bytes = await response.arrayBuffer();
13993
+ const checksum = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
13994
+ if (checksum !== archive.checksum) throw new Error("history_integrity_failure");
13995
+ const text = await new Response(
13996
+ new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
13997
+ ).text();
13998
+ for (const line of text.trim().split("\n"))
13999
+ records.push(
14000
+ external_exports.object({ seq: external_exports.number(), record: historyRecordSchema }).parse(JSON.parse(line))
14001
+ );
14002
+ }
14003
+ for (const batch of page.batches)
14004
+ batch.records.forEach(
14005
+ (record, index) => records.push({ seq: batch.firstSeq + index, record })
14006
+ );
14007
+ records.sort((a, b) => a.seq - b.seq);
14008
+ let through = after;
14009
+ const contiguous = [];
14010
+ for (const row of records) {
14011
+ if (row.seq <= through) continue;
14012
+ if (row.seq !== through + 1) break;
14013
+ contiguous.push(row);
14014
+ through = row.seq;
14015
+ }
14016
+ if (through === after && page.through > after) throw new Error("history_gap");
14017
+ return { through, availableThrough: page.through, records: contiguous };
14018
+ }
14019
+ watch(hubEpoch, runIds, update, error) {
14020
+ let stopped = false;
14021
+ let socket;
14022
+ let timer;
14023
+ let delay = 250;
14024
+ const runs = /* @__PURE__ */ new Map();
14025
+ const connect = async () => {
14026
+ try {
14027
+ const { ticket } = external_exports.object({ ticket: external_exports.string() }).parse(await this.request("/connect-ticket", { hubEpoch }));
14028
+ if (stopped) return;
14029
+ const endpoint = new URL(
14030
+ `/v2/orgs/${encodeURIComponent(this.orgId)}/connect`,
14031
+ this.baseUrl
14032
+ );
14033
+ endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:";
14034
+ const ws = new WebSocket(endpoint, ["drisp-v2", `ticket.${ticket}`]);
14035
+ socket = ws;
14036
+ ws.onopen = () => ws.send(JSON.stringify({ type: "watch", hubEpoch, runIds }));
14037
+ ws.onmessage = (event) => {
14038
+ try {
14039
+ if (typeof event.data !== "string" || new TextEncoder().encode(event.data).length > 8 * 1024 * 1024)
14040
+ throw new Error("frame_too_large");
14041
+ const message = JSON.parse(String(event.data));
14042
+ if (message.type === "error") throw new Error(message.code);
14043
+ if (message.hubEpoch !== hubEpoch) throw new Error("recovery_required");
14044
+ const header = external_exports.object({ subscriptionId: external_exports.string(), type: external_exports.enum(["snapshot", "change"]) }).parse(message);
14045
+ let through;
14046
+ if (header.type === "snapshot") {
14047
+ const snapshot = external_exports.object({
14048
+ controlThrough: external_exports.number().int().nonnegative(),
14049
+ runs: external_exports.array(runViewSchema)
14050
+ }).parse(message);
14051
+ runs.clear();
14052
+ for (const run of snapshot.runs) runs.set(run.runId, run);
14053
+ through = snapshot.controlThrough;
14054
+ } else {
14055
+ const change = external_exports.object({
14056
+ through: external_exports.number().int().nonnegative(),
14057
+ runs: external_exports.array(external_exports.object({ seq: external_exports.number(), run: runViewSchema }))
14058
+ }).parse(message);
14059
+ for (const { run } of change.runs)
14060
+ if ((runs.get(run.runId)?.recordRevision ?? 0) < run.recordRevision)
14061
+ runs.set(run.runId, run);
14062
+ through = change.through;
14063
+ }
14064
+ update([...runs.values()]);
14065
+ delay = 250;
14066
+ ws.send(
14067
+ JSON.stringify({ type: "watch.ack", subscriptionId: header.subscriptionId, through })
14068
+ );
14069
+ } catch (cause) {
14070
+ if (cause instanceof Error && cause.message === "reset_required") {
14071
+ ws.close();
14072
+ return;
14073
+ }
14074
+ stopped = true;
14075
+ error(cause);
14076
+ ws.close();
14077
+ }
14078
+ };
14079
+ ws.onclose = () => {
14080
+ if (!stopped) {
14081
+ timer = setTimeout(() => {
14082
+ void connect();
14083
+ }, delay);
14084
+ delay = Math.min(delay * 2, 3e4);
14085
+ }
14086
+ };
14087
+ } catch (cause) {
14088
+ if (stopped) return;
14089
+ error(cause);
14090
+ if (cause instanceof ClientError && cause.status < 500 && cause.status !== 429) return;
14091
+ timer = setTimeout(() => {
14092
+ void connect();
14093
+ }, delay);
14094
+ delay = Math.min(delay * 2, 3e4);
14095
+ }
14096
+ };
14097
+ void connect();
14098
+ return () => {
14099
+ stopped = true;
14100
+ if (timer) clearTimeout(timer);
14101
+ socket?.close();
14102
+ };
14103
+ }
14104
+ };
14105
+
14106
+ // src/core/build/state.ts
14107
+ var ATTEMPT_OUTCOMES = [
14108
+ "completed",
14109
+ "failed",
14110
+ "cancelled",
14111
+ "template_mismatch"
14112
+ ];
14113
+ var attemptSchema = external_exports.strictObject({
14114
+ n: external_exports.number().int().min(1).max(2),
14115
+ runId: external_exports.string().min(1),
14116
+ tokenCap: external_exports.number().int().positive(),
14117
+ startedAt: external_exports.string(),
14118
+ /** Where the attempt worked (chosen while building slice 2): later ticks check its PR and remove its worktree. */
14119
+ branch: external_exports.string().min(1),
14120
+ worktree: external_exports.string().min(1),
14121
+ outcome: external_exports.enum(ATTEMPT_OUTCOMES).optional()
14122
+ });
14123
+ var buildStateSchema = external_exports.strictObject({
14124
+ version: external_exports.literal(1),
14125
+ orchestratorRunId: external_exports.string().min(1),
14126
+ projectId: external_exports.string().min(1),
14127
+ slugId: external_exports.string().regex(/^[a-z0-9]+$/),
14128
+ templateVersion: external_exports.string().regex(/^[0-9a-f]{64}$/),
14129
+ approvedAt: external_exports.string(),
14130
+ approved: external_exports.strictObject({ groups: external_exports.array(external_exports.array(external_exports.string())) }),
14131
+ attempts: external_exports.record(external_exports.string(), external_exports.array(attemptSchema)),
14132
+ needsPerson: external_exports.record(external_exports.string(), external_exports.string()),
14133
+ tickCount: external_exports.number().int().nonnegative(),
14134
+ lastTickAt: external_exports.string(),
14135
+ /**
14136
+ * Additive (chosen while building slice 3): the entries a tick had seen done, for rebasing parked children's
14137
+ * branches after a merge. No longer written since slice 4 (a parked child rebases itself), still read so a state
14138
+ * written before parses.
14139
+ */
14140
+ done: external_exports.array(external_exports.string()).optional(),
14141
+ /** The merged issues the Orchestrator was already asked to repair in Linear, so it is asked once. */
14142
+ repaired: external_exports.array(external_exports.string()).optional()
14143
+ });
14144
+
14145
+ // src/core/build/childStart.ts
14146
+ function childRunId(orchestratorRunId, identifier, attempt) {
14147
+ return `${orchestratorRunId}:${identifier}:a${attempt}`;
14148
+ }
14149
+ function childPrompt(child) {
14150
+ return [
14151
+ `${child.identifier} ${child.title}`,
14152
+ `Linear: ${child.url}`,
14153
+ `Branch: ${child.branch}`,
14154
+ `Worktree: ${child.worktree}`,
14155
+ `Build: ${child.orchestratorRunId}`
14156
+ ].join("\n");
14157
+ }
14158
+ function childStartIntent(child, parent) {
14159
+ const runId = childRunId(
14160
+ child.orchestratorRunId,
14161
+ child.identifier,
14162
+ child.attempt
14163
+ );
14164
+ return {
14165
+ runId,
14166
+ intent: {
14167
+ kind: "start",
14168
+ sessionId: runId,
14169
+ runnerId: parent.runnerId,
14170
+ scope: parent.scope,
14171
+ key: `issue:${child.issueId}`,
14172
+ parent: { runId: child.orchestratorRunId, tokenCap: child.tokenCap },
14173
+ spec: {
14174
+ prompt: childPrompt(child),
14175
+ projectDir: child.worktree,
14176
+ harness: "claude-code",
14177
+ workflow: child.workflowName
14178
+ }
14179
+ }
14180
+ };
14181
+ }
14182
+
14183
+ // src/core/build/tickDecision.ts
14184
+ var MAX_ATTEMPTS = 2;
14185
+ function mayRetry(attempts) {
14186
+ return attempts.length < MAX_ATTEMPTS && attempts.at(-1)?.outcome === "failed";
14187
+ }
14188
+ var STOPPED_CLOSE_REASONS = /* @__PURE__ */ new Set(["cancelled_before_start", "abandoned"]);
14189
+ function outcomeOf(view) {
14190
+ if (view.closeReason === "workflow_template_mismatch")
14191
+ return "template_mismatch";
14192
+ const status = view.execution?.status;
14193
+ if (status === "completed" || status === "failed" || status === "cancelled")
14194
+ return status;
14195
+ if (view.cancellation || STOPPED_CLOSE_REASONS.has(view.closeReason ?? ""))
14196
+ return "cancelled";
14197
+ return "failed";
14198
+ }
14199
+ function buildLimits(own, delegation) {
14200
+ if (!delegation) return own;
14201
+ return {
14202
+ ...own,
14203
+ maxChildren: Math.min(own.maxChildren, delegation.maxChildren),
14204
+ tokenBudget: Math.min(own.tokenBudget, delegation.tokenBudget)
14205
+ };
14206
+ }
14207
+ function occupies(view) {
14208
+ return !view || !view.closed && view.execution?.status !== "awaiting_attention";
14209
+ }
14210
+ function assessTick(facts) {
14211
+ const { entries, orchestratorRunId } = facts;
14212
+ const state = structuredClone(facts.state);
14213
+ const views = new Map(facts.children.map((view) => [view.runId, view]));
14214
+ const issues = new Map(facts.issues.map((issue) => [issue.id, issue]));
14215
+ const attemptsOf = (id) => state.attempts[id] ?? [];
14216
+ for (const entry of entries) {
14217
+ const prefix = `${orchestratorRunId}:${entry.identifier}:a`;
14218
+ for (const view of facts.children) {
14219
+ if (!view.runId.startsWith(prefix)) continue;
14220
+ const n = Number(view.runId.slice(prefix.length));
14221
+ if (!(n === 1 || n === 2)) continue;
14222
+ if (attemptsOf(entry.id).some((a) => a.runId === view.runId)) continue;
14223
+ state.attempts[entry.id] = [
14224
+ ...attemptsOf(entry.id),
14225
+ {
14226
+ n,
14227
+ runId: view.runId,
14228
+ tokenCap: view.parent?.tokenCap ?? entry.tokenCap,
14229
+ startedAt: facts.now,
14230
+ ...facts.locate(entry)
14231
+ }
14232
+ ].sort((a, b) => a.n - b.n);
14233
+ }
14234
+ }
14235
+ for (const attempts of Object.values(state.attempts))
14236
+ for (const attempt of attempts) {
14237
+ const view = views.get(attempt.runId);
14238
+ if (view?.closed && !attempt.outcome) attempt.outcome = outcomeOf(view);
14239
+ }
14240
+ const branchOf = (entry) => attemptsOf(entry.id).at(-1)?.branch ?? facts.locate(entry).branch;
14241
+ for (const identifier of facts.rebaseFailed) {
14242
+ const entry = entries.find((each) => each.identifier === identifier);
14243
+ if (entry)
14244
+ state.needsPerson[entry.id] = BUILD_STRINGS.rebaseFailed(branchOf(entry));
14245
+ }
14246
+ const pullRequests = /* @__PURE__ */ new Map();
14247
+ const prsOf = (entry) => {
14248
+ const branch = attemptsOf(entry.id).at(-1)?.branch ?? issues.get(entry.id)?.branchName;
14249
+ if (!branch) return void 0;
14250
+ if (!pullRequests.has(branch))
14251
+ pullRequests.set(branch, facts.pullRequests(branch));
14252
+ return pullRequests.get(branch);
14253
+ };
14254
+ const status = /* @__PURE__ */ new Map();
14255
+ const skippedBlocker = (id) => {
14256
+ const linear = issues.get(id)?.state.type;
14257
+ return linear === "completed" || linear === "canceled" ? "done" : "blocked";
14258
+ };
14259
+ const repair = [];
14260
+ const repaired = new Set(state.repaired ?? []);
14261
+ for (const entry of entries) {
14262
+ const attempts = attemptsOf(entry.id);
14263
+ const newest = attempts.at(-1);
14264
+ const view = newest && views.get(newest.runId);
14265
+ const linear = issues.get(entry.id)?.state.type;
14266
+ const statusOf = () => {
14267
+ if (linear === "completed" || linear === "canceled") return "done";
14268
+ const prs = prsOf(entry);
14269
+ if (prs?.merged) {
14270
+ if (!repaired.has(entry.id) && !(newest && occupies(view))) {
14271
+ const prUrl = prs.mergedUrl ?? branchOf(entry);
14272
+ repair.push({
14273
+ id: entry.id,
14274
+ identifier: entry.identifier,
14275
+ prUrl,
14276
+ comment: BUILD_STRINGS.repairComment(prUrl, orchestratorRunId)
14277
+ });
14278
+ repaired.add(entry.id);
14279
+ }
14280
+ return "done";
14281
+ }
14282
+ if (newest && (!view || !view.closed)) return "live";
14283
+ if (newest?.outcome === "template_mismatch") return "replan";
14284
+ if (newest?.outcome === "completed")
14285
+ state.needsPerson[entry.id] ??= BUILD_STRINGS.completedUnmerged;
14286
+ if (state.needsPerson[entry.id] || newest && !mayRetry(attempts))
14287
+ return "needs_person";
14288
+ const blockers = entry.blockedBy.map(
14289
+ (id) => status.get(id) ?? skippedBlocker(id)
14290
+ );
14291
+ if (blockers.some(
14292
+ (b) => b === "needs_person" || b === "replan" || b === "blocked"
14293
+ ))
14294
+ return "blocked";
14295
+ return blockers.every((b) => b === "done") ? "ready" : "pending";
14296
+ };
14297
+ status.set(entry.id, statusOf());
14298
+ if (status.get(entry.id) === "done" && state.needsPerson[entry.id] === BUILD_STRINGS.completedUnmerged)
14299
+ delete state.needsPerson[entry.id];
14300
+ }
14301
+ if (repaired.size > 0) state.repaired = [...repaired];
14302
+ let parkReason = facts.templateVersion !== state.templateVersion ? "template" : state.tickCount >= facts.limits.tickAllowance ? "budget" : void 0;
14303
+ const occupying = entries.filter((entry) => {
14304
+ const newest = attemptsOf(entry.id).at(-1);
14305
+ return newest !== void 0 && occupies(views.get(newest.runId));
14306
+ }).length;
14307
+ const queue = [];
14308
+ if (!parkReason) {
14309
+ let capped = Object.values(state.attempts).flat().reduce((sum, attempt) => sum + attempt.tokenCap, 0);
14310
+ for (const entry of entries) {
14311
+ if (status.get(entry.id) !== "ready") continue;
14312
+ if (capped + entry.tokenCap > facts.limits.tokenBudget) {
14313
+ parkReason = "budget";
14314
+ break;
14315
+ }
14316
+ queue.push(entry);
14317
+ capped += entry.tokenCap;
14318
+ }
14319
+ }
14320
+ return {
14321
+ state,
14322
+ status,
14323
+ repair,
14324
+ queue,
14325
+ slots: Math.max(0, facts.limits.maxChildren - occupying),
14326
+ ...parkReason ? { parkReason } : {}
14327
+ };
14328
+ }
14329
+ function tickAction(input) {
14330
+ const live = input.children.filter((view) => !view.closed);
14331
+ if (input.started > 0 || live.some((view) => occupies(view)))
14332
+ return { action: "wait" };
14333
+ const remaining = input.entries.some(
14334
+ (entry) => ["ready", "pending"].includes(input.status.get(entry.id))
14335
+ );
14336
+ if (input.parkReason && remaining)
14337
+ return {
14338
+ action: "park",
14339
+ reason: input.parkReason,
14340
+ message: input.parkReason === "budget" ? BUILD_STRINGS.budgetPark : BUILD_STRINGS.templatePark
14341
+ };
14342
+ if (live.length > 0) return { action: "wait" };
14343
+ const done = input.entries.filter(
14344
+ (entry) => input.status.get(entry.id) === "done"
14345
+ ).length;
14346
+ return {
14347
+ action: "complete",
14348
+ message: `Built ${done} of ${input.entries.length}`
14349
+ };
14350
+ }
14351
+
14352
+ // src/core/build/tick.ts
14353
+ var UNAVAILABLE_RETRY_MS = 2e3;
14354
+ var UNAVAILABLE_RETRY_FOR_MS = 3e4;
14355
+ var NEVER = (/* @__PURE__ */ new Date(0)).toISOString();
14356
+ var notStarted = () => ({
14357
+ action: "complete",
14358
+ message: BUILD_STRINGS.notStarted
14359
+ });
14360
+ function pushBranch(worktree, branch) {
14361
+ git(worktree, ["push", "--quiet", "origin", `${branch}:${branch}`]);
14362
+ }
14363
+ async function startChild(deps, hubEpoch, start) {
14364
+ for (let waited = 0; ; waited += UNAVAILABLE_RETRY_MS) {
14365
+ try {
14366
+ await deps.hub.submit({
14367
+ commandId: randomUUID2(),
14368
+ hubEpoch,
14369
+ runId: start.runId,
14370
+ intent: start.intent
14371
+ });
14372
+ return { kind: "started" };
14373
+ } catch (error) {
14374
+ if (!(error instanceof ClientError) || error.status >= 500)
14375
+ return { kind: "unreachable" };
14376
+ if (error.code === "key_busy") return { kind: "key_busy" };
14377
+ if (error.code === "delegation_budget_exhausted") return { kind: "budget" };
14378
+ if (error.code !== "workflow_unavailable")
14379
+ return { kind: "refused", code: error.code };
14380
+ if (waited >= UNAVAILABLE_RETRY_FOR_MS) return { kind: "unavailable" };
14381
+ await deps.sleep(UNAVAILABLE_RETRY_MS);
14382
+ }
14383
+ }
14384
+ }
14385
+ function currentPlan(plan, state) {
14386
+ const { needsPerson: _previous, ...approved } = plan;
14387
+ const entries = plan.groups.flatMap((group) => group.issues);
14388
+ const needsPerson = entries.filter((entry) => state.needsPerson[entry.id]).map((entry) => ({ id: entry.id, reason: state.needsPerson[entry.id] }));
14389
+ return needsPerson.length > 0 ? { ...approved, needsPerson } : approved;
14390
+ }
14391
+ async function runBuildTick(deps) {
14392
+ const orchestratorRunId = requireEnv(deps.env, "DRISP_RUN_ID");
14393
+ const runnerId = requireEnv(deps.env, "DRISP_RUNNER_ID");
14394
+ const artifacts = requireEnv(deps.env, "DRISP_RUN_ARTIFACTS");
14395
+ if (!deps.inputPath)
14396
+ throw new BuildUsageError("Usage: drisp build tick --input <path>");
14397
+ let input;
14398
+ try {
14399
+ input = readInput(path7.resolve(deps.folder, deps.inputPath));
14400
+ } catch (error) {
14401
+ if (error instanceof PlanFailure) throw new BuildUsageError(error.message);
14402
+ throw error;
14403
+ }
14404
+ const worktree = ensureBuildWorktree({
14405
+ folder: deps.folder,
14406
+ slugId: input.project.slugId,
14407
+ worktreesRoot: deps.worktreesRoot
14408
+ });
14409
+ const designDir = path7.join(
14410
+ worktree.path,
14411
+ ".athena",
14412
+ "workflows",
14413
+ input.project.slugId
14414
+ );
14415
+ const relativeDesignDir = path7.relative(worktree.path, designDir);
14416
+ const statePath = path7.join(designDir, "build-state.json");
14417
+ const planPath = path7.join(designDir, "plan.json");
14418
+ const parsedPlan = buildPlanSchema.safeParse(readJson(planPath));
14419
+ if (!parsedPlan.success || parsedPlan.data.orchestratorRunId !== orchestratorRunId)
14420
+ return notStarted();
14421
+ const plan = parsedPlan.data;
14422
+ const push = deps.push ?? pushBranch;
14423
+ let stored;
14424
+ const parsedState = buildStateSchema.safeParse(readJson(statePath));
14425
+ if (parsedState.success && parsedState.data.orchestratorRunId === orchestratorRunId) {
14426
+ stored = parsedState.data;
14427
+ } else {
14428
+ if (!isStartAnswer(readJson(path7.join(artifacts, "answer.json"))))
14429
+ return notStarted();
14430
+ stored = {
14431
+ version: 1,
14432
+ orchestratorRunId,
14433
+ projectId: plan.projectId,
14434
+ slugId: input.project.slugId,
14435
+ templateVersion: plan.templateVersion,
14436
+ approvedAt: deps.now().toISOString(),
14437
+ approved: {
14438
+ groups: plan.groups.map((group) => group.issues.map((issue) => issue.id))
14439
+ },
14440
+ attempts: {},
14441
+ needsPerson: {},
14442
+ tickCount: 0,
14443
+ lastTickAt: NEVER
14444
+ };
14445
+ push(worktree.path, worktree.branch);
14446
+ }
14447
+ if (!(Date.parse(input.readAt) > Date.parse(stored.lastTickAt)))
14448
+ return { action: "refresh_input" };
14449
+ stored.tickCount += 1;
14450
+ stored.lastTickAt = deps.now().toISOString();
14451
+ const parent = await deps.hub.run(orchestratorRunId);
14452
+ const children = await deps.hub.runs({ parent: orchestratorRunId });
14453
+ const approved = new Set(stored.approved.groups.flat());
14454
+ const entries = plan.groups.flatMap((group) => group.issues).filter((entry) => approved.has(entry.id));
14455
+ const issues = new Map(input.issues.map((issue) => [issue.id, issue]));
14456
+ const assessment = assessTick({
14457
+ orchestratorRunId,
14458
+ entries,
14459
+ issues: input.issues,
14460
+ state: stored,
14461
+ children: children.runs,
14462
+ templateVersion,
14463
+ limits: buildLimits(ORCHESTRATOR_LIMITS, parent.delegation),
14464
+ pullRequests: (branch) => deps.pullRequests(deps.folder, branch),
14465
+ locate: (entry) => locate(deps, entry, issues.get(entry.id)?.branchName),
14466
+ rebaseFailed: deps.rebaseFailed ?? [],
14467
+ now: deps.now().toISOString()
14468
+ });
14469
+ const { state, status } = assessment;
14470
+ let parkReason = assessment.parkReason;
14471
+ const record = () => {
14472
+ writeFile(statePath, json(state));
14473
+ const files = [path7.join(relativeDesignDir, "build-state.json")];
14474
+ const emitted = json(currentPlan(plan, state));
14475
+ if (emitted !== fs7.readFileSync(planPath, "utf-8")) {
14476
+ writeFile(planPath, emitted);
14477
+ writeFile(path7.join(artifacts, "build_plan.json"), emitted);
14478
+ files.push(path7.join(relativeDesignDir, "plan.json"));
14479
+ }
14480
+ if (commitBuildFiles(
14481
+ worktree.path,
14482
+ files,
14483
+ `drisp build: tick ${state.tickCount} for ${state.slugId}`
14484
+ ))
14485
+ push(worktree.path, worktree.branch);
14486
+ };
14487
+ if (assessment.repair.length > 0) {
14488
+ record();
14489
+ return { action: "repair", issues: assessment.repair };
14490
+ }
14491
+ const deferred = [];
14492
+ const behind = [];
14493
+ let started = 0;
14494
+ if (assessment.queue.length > 0 && assessment.slots > 0) {
14495
+ for (const entry of assessment.queue) {
14496
+ if (started >= assessment.slots) break;
14497
+ const issue = issues.get(entry.id);
14498
+ if (!issue) continue;
14499
+ const child = ensureChildWorktree({
14500
+ folder: deps.folder,
14501
+ worktreesRoot: deps.worktreesRoot,
14502
+ issue
14503
+ });
14504
+ if (child.status === "needs_person") {
14505
+ state.needsPerson[entry.id] = child.reason;
14506
+ status.set(entry.id, "needs_person");
14507
+ continue;
14508
+ }
14509
+ if (child.existed && !hasUncommittedWork(child.path) && behindMain(deps.folder, child.path)) {
14510
+ behind.push({
14511
+ identifier: entry.identifier,
14512
+ worktree: child.path,
14513
+ branch: child.branch
14514
+ });
14515
+ continue;
14516
+ }
14517
+ const entryDir = path7.join(relativeDesignDir, entry.identifier);
14518
+ git(worktree.path, ["checkout", plan.commit, "--", entryDir]);
14519
+ installWorkflowFromSource(
14520
+ {
14521
+ kind: "filesystem",
14522
+ workflowPath: path7.join(worktree.path, entryDir, "workflow.json"),
14523
+ version: plan.commit
14524
+ },
14525
+ entry.workflowName
14526
+ );
14527
+ const attempt = (state.attempts[entry.id]?.length ?? 0) + 1;
14528
+ const start = childStartIntent(
14529
+ {
14530
+ orchestratorRunId,
14531
+ identifier: entry.identifier,
14532
+ issueId: entry.id,
14533
+ title: issue.title,
14534
+ url: issue.url,
14535
+ branch: child.branch,
14536
+ worktree: child.path,
14537
+ workflowName: entry.workflowName,
14538
+ tokenCap: entry.tokenCap,
14539
+ attempt
14540
+ },
14541
+ { runnerId, scope: parent.scope }
14542
+ );
14543
+ const result = await startChild(deps, children.hubEpoch, start);
14544
+ if (result.kind === "started") {
14545
+ (state.attempts[entry.id] ??= []).push({
14546
+ n: attempt,
14547
+ runId: start.runId,
14548
+ tokenCap: entry.tokenCap,
14549
+ startedAt: deps.now().toISOString(),
14550
+ branch: child.branch,
14551
+ worktree: child.path
14552
+ });
14553
+ status.set(entry.id, "live");
14554
+ started += 1;
14555
+ } else if (result.kind === "key_busy")
14556
+ deferred.push({ entry, reason: BUILD_STRINGS.keyBusy(entry.identifier) });
14557
+ else if (result.kind === "unavailable")
14558
+ deferred.push({
14559
+ entry,
14560
+ reason: BUILD_STRINGS.workflowUnavailable(entry.identifier)
14561
+ });
14562
+ else if (result.kind === "unreachable")
14563
+ deferred.push({
14564
+ entry,
14565
+ reason: BUILD_STRINGS.hubUnreachable(entry.identifier)
14566
+ });
14567
+ else if (result.kind === "budget") {
14568
+ parkReason = "budget";
14569
+ break;
14570
+ } else {
14571
+ state.needsPerson[entry.id] = BUILD_STRINGS.startRefused(result.code);
14572
+ status.set(entry.id, "needs_person");
14573
+ }
14574
+ }
14575
+ }
14576
+ const anyLive = started > 0 || children.runs.some((view) => !view.closed) || behind.length > 0;
14577
+ if (!anyLive)
14578
+ for (const { entry, reason } of deferred) {
14579
+ state.needsPerson[entry.id] = reason;
14580
+ status.set(entry.id, "needs_person");
14581
+ }
14582
+ const views = new Map(children.runs.map((view) => [view.runId, view]));
14583
+ for (const entry of entries) {
14584
+ const newest = state.attempts[entry.id]?.at(-1);
14585
+ if (!newest || status.get(entry.id) !== "done") continue;
14586
+ if (!views.get(newest.runId)?.closed) continue;
14587
+ const installed = fs7.existsSync(
14588
+ path7.join(workflowRegistryDir(), entry.workflowName)
14589
+ );
14590
+ if (!installed && !fs7.existsSync(newest.worktree)) continue;
14591
+ if (!deps.pullRequests(deps.folder, newest.branch).merged) continue;
14592
+ if (fs7.existsSync(newest.worktree)) {
14593
+ try {
14594
+ git(deps.folder, ["worktree", "remove", newest.worktree]);
14595
+ } catch {
14596
+ }
14597
+ }
14598
+ if (installed) removeWorkflow(entry.workflowName);
14599
+ }
14600
+ record();
14601
+ if (behind.length > 0) return { action: "rebase", items: behind };
14602
+ return tickAction({
14603
+ entries,
14604
+ status,
14605
+ children: children.runs,
14606
+ started,
14607
+ ...parkReason ? { parkReason } : {}
14608
+ });
14609
+ }
14610
+ function locate(deps, entry, branchName) {
14611
+ const worktree = childWorktreePath(
14612
+ deps.worktreesRoot,
14613
+ deps.folder,
14614
+ entry.identifier
14615
+ );
14616
+ if (fs7.existsSync(path7.join(worktree, ".git")))
14617
+ return {
14618
+ worktree,
14619
+ branch: git(worktree, ["rev-parse", "--abbrev-ref", "HEAD"])
14620
+ };
14621
+ return {
14622
+ worktree,
14623
+ branch: branchName ?? fallbackBranch(entry.identifier, entry.title)
14624
+ };
14625
+ }
14626
+
14627
+ // src/core/build/mergeGate.ts
14628
+ var PASSED = /* @__PURE__ */ new Set([
14629
+ "success",
14630
+ "skipped",
14631
+ "neutral"
14632
+ ]);
14633
+ function zeroStepFailure(run) {
14634
+ const failedJobs = run.jobs.filter((job) => !PASSED.has(job.conclusion));
14635
+ return failedJobs.length > 0 && failedJobs.every((job) => Array.isArray(job.steps) && job.steps.length === 0);
14636
+ }
14637
+ function evaluateRemoteChecks(runs) {
14638
+ const verdict = {
14639
+ ok: true,
14640
+ failed: [],
14641
+ ignored: [],
14642
+ pending: []
14643
+ };
14644
+ for (const run of runs) {
14645
+ if (run.status !== "completed") verdict.pending.push(run.name);
14646
+ else if (PASSED.has(run.conclusion)) continue;
14647
+ else if (zeroStepFailure(run)) verdict.ignored.push(run.name);
14648
+ else verdict.failed.push(run.name);
14649
+ }
14650
+ verdict.ok = verdict.failed.length === 0 && verdict.pending.length === 0;
14651
+ return verdict;
14652
+ }
14653
+
14654
+ // src/app/entry/buildCommand.ts
14655
+ var USAGE3 = `Usage: drisp build plan --input <path>
14656
+ drisp build tick --input <path> [--rebase-failed <IDENTIFIER>]...
14657
+ drisp build checks
14658
+ plan and tick run inside an Orchestrator Run (DRISP_RUN_ID, DRISP_RUNNER_ID and DRISP_RUN_ARTIFACTS set; tick also
14659
+ DRISP_RUN_TOKEN and DRISP_HUB_URL). DRISP_BUILD_PLAN_INPUT=<path> stands in for plan's --input, with
14660
+ fills/<IDENTIFIER>.json beside it. checks runs in a child's worktree: the merge gate's verdict on HEAD's GitHub runs.`;
14661
+ function githubPullRequests(folder, branch) {
14662
+ const output = execFileSync(
14663
+ "gh",
14664
+ [
14665
+ "pr",
14666
+ "list",
14667
+ "--head",
14668
+ branch,
14669
+ "--state",
14670
+ "all",
14671
+ "--json",
14672
+ "state,url",
14673
+ "--limit",
14674
+ "50"
14675
+ ],
14676
+ { cwd: folder, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
14677
+ );
14678
+ return parsePullRequests(output);
14679
+ }
14680
+ function parsePullRequests(output) {
14681
+ const prs = JSON.parse(output);
14682
+ const merged = prs.find((pr) => pr.state === "MERGED");
14683
+ return {
14684
+ open: prs.some((pr) => pr.state === "OPEN"),
14685
+ merged: merged !== void 0,
14686
+ ...merged ? { mergedUrl: merged.url } : {}
14687
+ };
14688
+ }
14689
+ function hostCapabilities(folder) {
14690
+ const effective = resolveEffectiveCapabilities({
14691
+ globalConfig: readGlobalConfig(),
14692
+ projectConfig: readConfig(folder)
14693
+ });
14694
+ return {
14695
+ skills: effective.skills.map((skill) => skill.name),
14696
+ mcpServers: effective.mcpServers.map((server) => server.name)
14697
+ };
14698
+ }
14699
+ var defaultChecks = {
14700
+ head: (cwd) => git(cwd, ["rev-parse", "HEAD"]),
14701
+ gh: (args, cwd) => execFileSync("gh", args, {
14702
+ cwd,
14703
+ encoding: "utf8",
14704
+ stdio: ["ignore", "pipe", "pipe"]
14705
+ })
14706
+ };
14707
+ function runBuildChecks(cwd, deps) {
14708
+ const head = deps.head(cwd);
14709
+ const runs = JSON.parse(
14710
+ deps.gh(
14711
+ [
14712
+ "run",
14713
+ "list",
14714
+ "--commit",
14715
+ head,
14716
+ "--json",
14717
+ "databaseId,name,status,conclusion",
14718
+ "--limit",
14719
+ "100"
14720
+ ],
14721
+ cwd
14722
+ )
14723
+ );
14724
+ const withJobs = runs.map((run) => ({
14725
+ name: run.name,
14726
+ status: run.status,
14727
+ conclusion: run.conclusion,
14728
+ jobs: run.status === "completed" && !PASSED.has(run.conclusion) ? JSON.parse(
14729
+ deps.gh(
14730
+ ["run", "view", String(run.databaseId), "--json", "jobs"],
14731
+ cwd
14732
+ )
14733
+ ).jobs : []
14734
+ }));
14735
+ return evaluateRemoteChecks(withJobs);
14736
+ }
14737
+ async function runBuildCommand(input) {
14738
+ const stdout = input.stdout ?? process.stdout;
14739
+ const stderr = input.stderr ?? process.stderr;
14740
+ const env2 = input.env ?? process.env;
14741
+ try {
14742
+ let output;
14743
+ switch (input.subcommand) {
14744
+ case "plan":
14745
+ output = runBuildPlan({
14746
+ env: env2,
14747
+ folder: input.cwd,
14748
+ ...input.flags.input ? { inputPath: input.flags.input } : {},
14749
+ worktreesRoot: defaultWorktreesRoot(),
14750
+ hostCapabilities: () => hostCapabilities(input.cwd),
14751
+ pullRequests: githubPullRequests,
14752
+ ...input.dependencies
14753
+ });
14754
+ break;
14755
+ case "tick": {
14756
+ requireEnv(env2, "DRISP_RUN_ID");
14757
+ requireEnv(env2, "DRISP_RUNNER_ID");
14758
+ requireEnv(env2, "DRISP_RUN_ARTIFACTS");
14759
+ const token = requireEnv(env2, "DRISP_RUN_TOKEN");
14760
+ const hubUrl = requireEnv(env2, "DRISP_HUB_URL");
14761
+ output = await runBuildTick({
14762
+ env: env2,
14763
+ folder: input.cwd,
14764
+ ...input.flags.input ? { inputPath: input.flags.input } : {},
14765
+ ...input.flags.rebaseFailed?.length ? { rebaseFailed: input.flags.rebaseFailed } : {},
14766
+ worktreesRoot: defaultWorktreesRoot(),
14767
+ hub: DrispClient.delegated(hubUrl, token),
14768
+ pullRequests: githubPullRequests,
14769
+ now: () => /* @__PURE__ */ new Date(),
14770
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
14771
+ ...input.tick
14772
+ });
14773
+ break;
14774
+ }
14775
+ case "checks":
14776
+ output = runBuildChecks(input.cwd, input.checks ?? defaultChecks);
14777
+ break;
14778
+ default:
14779
+ stderr.write(`${USAGE3}
14780
+ `);
14781
+ return 2;
14782
+ }
14783
+ stdout.write(`${JSON.stringify(output)}
14784
+ `);
14785
+ return 0;
14786
+ } catch (error) {
14787
+ if (!(error instanceof BuildUsageError)) throw error;
14788
+ stderr.write(`${error.message}
14789
+ ${USAGE3}
14790
+ `);
14791
+ return 2;
14792
+ }
14793
+ }
14794
+
13516
14795
  // src/app/entry/doctorCommand.ts
13517
14796
  import process7 from "process";
13518
14797
 
13519
14798
  // src/harnesses/claude/system/doctorEnvironment.ts
13520
- import { execFileSync } from "child_process";
13521
- import * as fs5 from "fs";
14799
+ import { execFileSync as execFileSync2 } from "child_process";
14800
+ import * as fs8 from "fs";
13522
14801
  import * as os4 from "os";
13523
- import * as path5 from "path";
14802
+ import * as path8 from "path";
13524
14803
  var MANAGED_POLICY_KEYS = [
13525
14804
  "forceLoginMethod",
13526
14805
  "forceLoginOrgUUID",
@@ -13595,7 +14874,7 @@ function readEnforcement(scopes) {
13595
14874
  }
13596
14875
  function tryRichAuth(claudeBinary) {
13597
14876
  try {
13598
- const output = execFileSync(claudeBinary, ["auth", "status"], {
14877
+ const output = execFileSync2(claudeBinary, ["auth", "status"], {
13599
14878
  timeout: 1e4,
13600
14879
  encoding: "utf-8",
13601
14880
  stdio: ["ignore", "pipe", "pipe"]
@@ -13624,8 +14903,8 @@ function collectEnvironment(options = {}) {
13624
14903
  const detectVersion = options.detectClaudeVersionFn ?? detectClaudeVersion;
13625
14904
  const runAuthStatus = options.runClaudeAuthStatusFn ?? runClaudeAuthStatus;
13626
14905
  const resolveForwarder = options.resolveHookForwarderCommandFn ?? resolveHookForwarderCommand;
13627
- const statFn = options.statFn ?? ((p) => fs5.statSync(p));
13628
- const readFileFn = options.readFileFn ?? ((p) => fs5.readFileSync(p, "utf8"));
14906
+ const statFn = options.statFn ?? ((p) => fs8.statSync(p));
14907
+ const readFileFn = options.readFileFn ?? ((p) => fs8.readFileSync(p, "utf8"));
13629
14908
  const settingsPaths = resolveClaudeSettingsSurfacePaths(
13630
14909
  homeDir,
13631
14910
  cwd,
@@ -13670,7 +14949,7 @@ function collectEnvironment(options = {}) {
13670
14949
  });
13671
14950
  let globalConfigPresent = false;
13672
14951
  try {
13673
- statFn(path5.join(homeDir, ".claude.json"));
14952
+ statFn(path8.join(homeDir, ".claude.json"));
13674
14953
  globalConfigPresent = true;
13675
14954
  } catch {
13676
14955
  globalConfigPresent = false;
@@ -14150,13 +15429,13 @@ function runRunsCommand(input) {
14150
15429
  // src/app/entry/cli.tsx
14151
15430
  import { jsx as jsx24 } from "react/jsx-runtime";
14152
15431
  function resolvePackageJsonPath(entryUrl) {
14153
- let currentDir = path6.dirname(fileURLToPath(entryUrl));
15432
+ let currentDir = path9.dirname(fileURLToPath(entryUrl));
14154
15433
  for (; ; ) {
14155
- const candidatePath = path6.join(currentDir, "package.json");
14156
- if (fs6.existsSync(candidatePath)) {
15434
+ const candidatePath = path9.join(currentDir, "package.json");
15435
+ if (fs9.existsSync(candidatePath)) {
14157
15436
  return candidatePath;
14158
15437
  }
14159
- const parentDir = path6.dirname(currentDir);
15438
+ const parentDir = path9.dirname(currentDir);
14160
15439
  if (parentDir === currentDir) {
14161
15440
  throw new Error("Could not locate package.json for CLI metadata.");
14162
15441
  }
@@ -14178,6 +15457,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
14178
15457
  "skill",
14179
15458
  "marketplace",
14180
15459
  "runner",
15460
+ "build",
14181
15461
  // Deprecated alias of `runner` (#188); removed in 0.7.0.
14182
15462
  "dashboard",
14183
15463
  "telemetry",
@@ -14324,6 +15604,9 @@ var cli = meow(
14324
15604
  marketplace <sub> Manage marketplace sources (add, refresh, remove, list)
14325
15605
  runner [sub] Run the runner that connects this machine to a Team and executes its Runs
14326
15606
  (no subcommand: foreground; connect [code] --hub <url> [--replace], disconnect [team], status, recover, help)
15607
+ build plan --input <path> Plan a Build inside an Orchestrator Run (prints one JSON object)
15608
+ build tick --input <path> Run one step of a started Build (prints the next action; [--rebase-failed <ID>]...)
15609
+ build checks The merge gate's verdict on HEAD's GitHub runs (in a child's worktree)
14327
15610
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
14328
15611
  doctor Diagnose Claude headless setup (use with --harness=claude)
14329
15612
 
@@ -14448,6 +15731,13 @@ var cli = meow(
14448
15731
  hub: {
14449
15732
  type: "string"
14450
15733
  },
15734
+ input: {
15735
+ type: "string"
15736
+ },
15737
+ rebaseFailed: {
15738
+ type: "string",
15739
+ isMultiple: true
15740
+ },
14451
15741
  foreground: {
14452
15742
  type: "boolean",
14453
15743
  default: false
@@ -14487,7 +15777,7 @@ var cli = meow(
14487
15777
  }
14488
15778
  );
14489
15779
  async function main() {
14490
- const projectDir = path6.resolve(cli.flags.projectDir);
15780
+ const projectDir = path9.resolve(cli.flags.projectDir);
14491
15781
  const [command, ...commandArgs] = cli.input;
14492
15782
  const isRunCommand = command === "run" || command === "exec";
14493
15783
  if (command && !KNOWN_COMMANDS.has(command)) {
@@ -14537,7 +15827,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14537
15827
  await exitWith(1);
14538
15828
  return;
14539
15829
  }
14540
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-TQ4SF5KB.js");
15830
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-NZFBSBLY.js");
14541
15831
  const { waitUntilExit } = render(
14542
15832
  /* @__PURE__ */ jsx24(
14543
15833
  WorkflowInstallWizard,
@@ -14614,6 +15904,20 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14614
15904
  );
14615
15905
  return;
14616
15906
  }
15907
+ if (command === "build") {
15908
+ const [subcommand = ""] = commandArgs;
15909
+ await exitWith(
15910
+ await runBuildCommand({
15911
+ subcommand,
15912
+ flags: {
15913
+ ...typeof cli.flags.input === "string" ? { input: cli.flags.input } : {},
15914
+ ...cli.flags.rebaseFailed?.length ? { rebaseFailed: cli.flags.rebaseFailed } : {}
15915
+ },
15916
+ cwd: process.cwd()
15917
+ })
15918
+ );
15919
+ return;
15920
+ }
14617
15921
  if (command === "runs") {
14618
15922
  await exitWith(
14619
15923
  runRunsCommand({
@@ -14687,8 +15991,8 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14687
15991
  const showSetup = isRunCommand ? false : shouldShowSetup({
14688
15992
  cliInput: cli.input,
14689
15993
  setupComplete: globalConfig.setupComplete,
14690
- globalConfigExists: fs6.existsSync(
14691
- path6.join(os5.homedir(), ".config", "athena", "config.json")
15994
+ globalConfigExists: fs9.existsSync(
15995
+ path9.join(os5.homedir(), ".config", "athena", "config.json")
14692
15996
  )
14693
15997
  });
14694
15998
  let runtimeConfig;