@drisp/cli 0.6.8 → 0.6.10

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
@@ -7,17 +7,25 @@ import {
7
7
  IndexedTimeline,
8
8
  PORTABLE_PROVIDER_ENV_VARS,
9
9
  RUN_EXIT_CODE,
10
+ ServiceNotEnabledError,
10
11
  attachRuntimeEventLoop,
12
+ behindMain,
11
13
  bootstrapRuntimeConfig,
12
14
  buildApiKeyHelperSettings,
13
15
  buildPhaseFeedEvent,
16
+ buildPlanSchema,
14
17
  buildPostByToolUseId,
15
18
  buildProbeConfigs,
16
19
  centerAnsi,
20
+ childWorktreePath,
17
21
  classifyFailure,
22
+ commandSchema,
23
+ commitBuildFiles,
18
24
  compactText,
19
25
  computeInputRows,
20
26
  connectAlreadyConnectedSchema,
27
+ connectCodeResponseSchema,
28
+ connectPendingResponseSchema,
21
29
  connectRedeemResponseSchema,
22
30
  connectStartResponseSchema,
23
31
  createFeedMapper,
@@ -26,10 +34,15 @@ import {
26
34
  createSessionStore,
27
35
  credentialHelperKey,
28
36
  cursorToVisualPosition,
37
+ defaultWorktreesRoot,
29
38
  detectClaudeVersion,
39
+ ensureBuildWorktree,
40
+ ensureChildWorktree,
30
41
  expandHome,
31
42
  extractFriendlyServerName,
32
43
  extractPermissionSnapshot,
44
+ fallbackBranch,
45
+ finalizePlan,
33
46
  fit,
34
47
  fitAnsi,
35
48
  forgetRunner,
@@ -46,12 +59,18 @@ import {
46
59
  getLatestRunForSession,
47
60
  getMostRecentAthenaSession,
48
61
  getSessionMeta,
62
+ git,
63
+ hasUncommittedWork,
49
64
  hintGlyphs,
65
+ historyRecordSchema,
50
66
  hubHttpUrl,
51
67
  identityHubOrigin,
52
68
  isBashToolResponse,
53
69
  isCommandPrefix,
70
+ isGitHubRepository,
54
71
  isScopedPermissionsRequest,
72
+ isStartAnswer,
73
+ lastCommitTouching,
55
74
  listAwaitingAttentionRuns,
56
75
  listHarnessAdapters,
57
76
  listHarnessCapabilities,
@@ -66,6 +85,8 @@ import {
66
85
  overriddenIdentityPath,
67
86
  parseFrontmatter,
68
87
  parseToolName,
88
+ planBuild,
89
+ planInputSchema,
69
90
  platformRunnerServices,
70
91
  probeSkipReason,
71
92
  processRegistry,
@@ -74,6 +95,8 @@ import {
74
95
  registerCleanupOnExit,
75
96
  releaseMcpAsset,
76
97
  renderInputLines,
98
+ repoLayoutAt,
99
+ repositoryBindingSchema,
77
100
  require_string_width,
78
101
  resolveClaudeBinary,
79
102
  resolveClaudeSettingsSurfacePaths,
@@ -88,12 +111,18 @@ import {
88
111
  runExec,
89
112
  runMachineRunners,
90
113
  runProbe,
114
+ runViewSchema,
115
+ runnerAdoptRequestSchema,
91
116
  runnerConfigDirectory,
92
117
  runnerConfigSchema,
118
+ runnerDisconnectPreviewSchema,
93
119
  runnerDisconnectResponseSchema,
94
120
  runnerIdentities,
121
+ runnerListEntrySchema,
122
+ runnerListSchema,
95
123
  runnerServiceUnit,
96
124
  runnerSoftwareVersion,
125
+ runnerUpdateRequestSchema,
97
126
  serviceInstallationId,
98
127
  sessionsDir,
99
128
  sliceAnsi,
@@ -101,12 +130,14 @@ import {
101
130
  startDashboardDecisionDrain,
102
131
  startWorkflowExecution,
103
132
  supportsSessionApproval,
133
+ teamDefaultRequestSchema,
134
+ teamDefaultSchema,
104
135
  teamIdentityPath,
105
136
  todoGlyphSet,
106
137
  visualPositionToOffset,
107
138
  wrapText,
108
139
  writeIdentityOnce
109
- } from "./chunk-HN3WNQVY.js";
140
+ } from "./chunk-BTHLAJV7.js";
110
141
  import {
111
142
  generateId as generateId2
112
143
  } from "./chunk-PQIB66SG.js";
@@ -120,9 +151,21 @@ import {
120
151
  useTheme
121
152
  } from "./chunk-KPDCVR6Q.js";
122
153
  import {
154
+ BUILD_STRINGS,
155
+ CANNOT_PASS_TEMPLATE,
156
+ DESCRIPTION_TEMPLATE,
157
+ GATES,
158
+ LAYOUT_MARKERS,
159
+ NO_GATE_TEXT_TEMPLATE,
160
+ ORCHESTRATOR_LIMITS,
161
+ QA_SECTIONS,
162
+ RENDERER_RULES,
163
+ STAGE_TEMPLATE,
164
+ WORKFLOW_MD_TEMPLATE,
123
165
  collectMcpServersWithOptions,
124
166
  createSteerQueue,
125
167
  deserializeRunMemory,
168
+ external_exports,
126
169
  findMarketplaceRepoDir,
127
170
  gatherMarketplaceWorkflowSources,
128
171
  hasProjectWorkflow,
@@ -133,6 +176,7 @@ import {
133
176
  listMarketplaceWorkflows,
134
177
  listMarketplaceWorkflowsFromRepo,
135
178
  listWorkflows,
179
+ parseWorkflowConfig,
136
180
  projectConfigPath,
137
181
  pullMarketplaceRepo,
138
182
  readConfig,
@@ -145,10 +189,12 @@ import {
145
189
  resolveWorkflowMarketplaceSource,
146
190
  updateWorkflow,
147
191
  updateWorkflows,
192
+ verifyRendered,
148
193
  wakesFreshAfterHandover,
194
+ workflowRegistryDir,
149
195
  writeGlobalConfig,
150
196
  writeProjectConfig
151
- } from "./chunk-CIPO2P2Q.js";
197
+ } from "./chunk-YUVMVYUF.js";
152
198
  import {
153
199
  __toESM
154
200
  } from "./chunk-G3PMV62Z.js";
@@ -156,10 +202,10 @@ import {
156
202
  // src/app/entry/cli.tsx
157
203
  import { render } from "ink";
158
204
  import meow from "meow";
159
- import fs6 from "fs";
205
+ import fs9 from "fs";
160
206
  import { createRequire } from "module";
161
207
  import os5 from "os";
162
- import path6 from "path";
208
+ import path9 from "path";
163
209
  import { fileURLToPath } from "url";
164
210
 
165
211
  // src/app/shell/sessionUiState.ts
@@ -5278,14 +5324,14 @@ function extractWebSearch(_input, response) {
5278
5324
  return { type: "text", content: extractTextContent(response), maxLines: 10 };
5279
5325
  }
5280
5326
  function extractNotebookEdit(input, _response) {
5281
- const path7 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5327
+ const path10 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5282
5328
  const mode = typeof input["edit_mode"] === "string" ? input["edit_mode"] : "replace";
5283
5329
  const source = typeof input["new_source"] === "string" ? input["new_source"] : "";
5284
- if (!source) return { type: "text", content: `${mode} cell in ${path7}` };
5330
+ if (!source) return { type: "text", content: `${mode} cell in ${path10}` };
5285
5331
  return {
5286
5332
  type: "code",
5287
5333
  content: source,
5288
- language: detectLanguage(path7),
5334
+ language: detectLanguage(path10),
5289
5335
  maxLines: 10
5290
5336
  };
5291
5337
  }
@@ -5811,10 +5857,10 @@ function extractToolSubject(toolName, toolInput) {
5811
5857
  const parts = [];
5812
5858
  const pattern = str(input, "pattern");
5813
5859
  const glob = str(input, "glob");
5814
- const path7 = str(input, "path");
5860
+ const path10 = str(input, "path");
5815
5861
  if (pattern) parts.push(`"${pattern}"`);
5816
5862
  if (glob) parts.push(glob);
5817
- if (path7) parts.push(`in ${path7}`);
5863
+ if (path10) parts.push(`in ${path10}`);
5818
5864
  return parts.length > 0 ? parts.join(" ") : void 0;
5819
5865
  }
5820
5866
  case "Skill":
@@ -5916,6 +5962,14 @@ function eventLabel(event) {
5916
5962
  return event.data.task_subject || "Task Completed";
5917
5963
  case "config.change":
5918
5964
  return "Config Change";
5965
+ case "instructions.loaded":
5966
+ return "Instructions Loaded";
5967
+ case "worktree.create":
5968
+ return "Worktree Created";
5969
+ case "worktree.remove":
5970
+ return "Worktree Removed";
5971
+ case "artifacts.manifest":
5972
+ return "Artifacts Manifest";
5919
5973
  default:
5920
5974
  return "Event";
5921
5975
  }
@@ -5970,8 +6024,8 @@ function renderDetailLines(event, width, pairedPostEvent, theme = darkTheme) {
5970
6024
  event.data.tool_input
5971
6025
  );
5972
6026
  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");
6027
+ const json2 = JSON.stringify(event.data.tool_input, null, 2);
6028
+ const requestLines = highlightCode(json2, cw, "json");
5975
6029
  return buildResult(header, requestLines, true);
5976
6030
  }
5977
6031
  case "subagent.start": {
@@ -5994,8 +6048,8 @@ function renderDetailLines(event, width, pairedPostEvent, theme = darkTheme) {
5994
6048
  } else if (prompt) {
5995
6049
  content = renderMarkdownToLines(prompt, cw);
5996
6050
  } else {
5997
- const json = JSON.stringify(event.data, null, 2);
5998
- content = highlightCode(json, cw, "json");
6051
+ const json2 = JSON.stringify(event.data, null, 2);
6052
+ content = highlightCode(json2, cw, "json");
5999
6053
  }
6000
6054
  return buildResult(header, content, !prompt && !response);
6001
6055
  }
@@ -6104,8 +6158,8 @@ Named by Turn ${event.data.turn} of Workflow Run \`${event.data.runId}\`.`,
6104
6158
  case "elicitation.result":
6105
6159
  case "artifacts.manifest": {
6106
6160
  const header = buildCompactHeader(event, width, { theme });
6107
- const json = JSON.stringify(event.raw ?? event.data, null, 2);
6108
- const content = highlightCode(json, cw, "json");
6161
+ const json2 = JSON.stringify(event.raw ?? event.data, null, 2);
6162
+ const content = highlightCode(json2, cw, "json");
6109
6163
  return buildResult(header, content, true);
6110
6164
  }
6111
6165
  default: {
@@ -8411,8 +8465,8 @@ function renderHeaderLines(model, width, hasColor, theme) {
8411
8465
  const harnessText = `${style("Harness: ", palette.label)}${style(model.harness, palette.value)}`;
8412
8466
  const effortSuffix = model.effort_level ? style(` with ${model.effort_level} effort`, palette.label) : "";
8413
8467
  const modelText = `${style("Model: ", palette.label)}${style(formatModelName(model.model_name), palette.value)}${effortSuffix}`;
8414
- const tokenLabel = model.token_label ?? "Tokens";
8415
- const runLabel = model.run_label ?? "Runs";
8468
+ const tokenLabel = model.token_label;
8469
+ const runLabel = model.run_label;
8416
8470
  const leftTokens = [
8417
8471
  { text: athena, priority: 100 },
8418
8472
  { text: divider, priority: 5 },
@@ -12561,11 +12615,10 @@ function parseArgs(args, context) {
12561
12615
  } else if (arg === "--global") {
12562
12616
  global = true;
12563
12617
  } else if (arg === "--env") {
12564
- const value = args[i + 1];
12565
- if (value === void 0) {
12618
+ if (i + 1 >= args.length) {
12566
12619
  return { error: `mcp ${context}: --env requires a KEY=VALUE argument` };
12567
12620
  }
12568
- env2.push(value);
12621
+ env2.push(args[i + 1]);
12569
12622
  i++;
12570
12623
  } else if (arg.startsWith("--")) {
12571
12624
  return { error: `Unknown flag for mcp ${context}: ${arg}` };
@@ -13038,7 +13091,7 @@ var thisMachine = (installationId) => ({
13038
13091
  os: `${platform()} ${release()}`.slice(0, 64),
13039
13092
  softwareVersion: runnerSoftwareVersion
13040
13093
  });
13041
- var post = (origin, path7, body) => fetch(new URL(path7, origin), {
13094
+ var post = (origin, path10, body) => fetch(new URL(path10, origin), {
13042
13095
  method: "POST",
13043
13096
  headers: { "Content-Type": "application/json" },
13044
13097
  body: JSON.stringify(body)
@@ -13057,8 +13110,8 @@ async function alreadyConnected(existing, { forTeam, ownIdentity }) {
13057
13110
  const problem = own ? await credentialAccepted(own.config) ? void 0 : "lapsed" : forTeam(existing.orgId).length ? "other_runner" : "missing";
13058
13111
  const runner = { orgId: existing.orgId, runnerId: existing.runnerId, name: existing.name };
13059
13112
  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 } : {} };
13113
+ const path10 = (own ?? forTeam(existing.orgId)[0])?.path;
13114
+ return { status: "identity_unusable", ...runner, problem, ...path10 ? { path: path10 } : {} };
13062
13115
  }
13063
13116
  async function connectWithCode({
13064
13117
  code,
@@ -13130,20 +13183,20 @@ async function saveIdentity(origin, credential, { single, forTeam }, directory)
13130
13183
  name: credential.name
13131
13184
  });
13132
13185
  const replaced = [];
13133
- for (const { path: path8 } of forTeam(credential.orgId)) {
13134
- const aside = `${path8}.replaced-${Date.now()}`;
13135
- await rename(path8, aside);
13186
+ for (const { path: path11 } of forTeam(credential.orgId)) {
13187
+ const aside = `${path11}.replaced-${Date.now()}`;
13188
+ await rename(path11, aside);
13136
13189
  replaced.push(aside);
13137
13190
  }
13138
- const path7 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13191
+ const path10 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13139
13192
  try {
13140
- await writeIdentityOnce(path7, JSON.stringify(config, null, 2) + "\n");
13193
+ await writeIdentityOnce(path10, JSON.stringify(config, null, 2) + "\n");
13141
13194
  } catch (error) {
13142
13195
  if (error.code === "EEXIST")
13143
- throw new Error(`Another Connect wrote a Runner identity for this Team at ${path7} meanwhile; it was left unchanged.`);
13196
+ throw new Error(`Another Connect wrote a Runner identity for this Team at ${path10} meanwhile; it was left unchanged.`);
13144
13197
  throw error;
13145
13198
  }
13146
- return { status: "connected", config, path: path7, replaced };
13199
+ return { status: "connected", config, path: path10, replaced };
13147
13200
  }
13148
13201
  function pathSegment(value) {
13149
13202
  const segment = encodeURIComponent(value);
@@ -13183,7 +13236,7 @@ async function refuseExisting(target) {
13183
13236
  throw new Error(`This machine already has a Runner${name ? ` (${name})` : ""} configured at ${target}.`);
13184
13237
  }
13185
13238
  async function disconnectRunner({
13186
- identity: { path: path7, config },
13239
+ identity: { path: path10, config },
13187
13240
  serviceManager,
13188
13241
  othersRemain
13189
13242
  }) {
@@ -13216,14 +13269,14 @@ async function disconnectRunner({
13216
13269
  await restore();
13217
13270
  throw new Error(`The Hub refused to disconnect ${name} (${body.error ?? response.status}); it is still connected.`);
13218
13271
  }
13219
- await forgetRunner(path7);
13272
+ await forgetRunner(path10);
13220
13273
  if (othersRemain) await restore();
13221
13274
  else await serviceManager?.remove();
13222
13275
  return result;
13223
13276
  }
13224
13277
 
13225
13278
  // src/app/entry/runnerCommandV2.ts
13226
- var usage = `Usage: drisp runner [connect [<code>] --hub <url> [--foreground] [--replace] | disconnect [<team>] | status | recover | help]
13279
+ var usage = `Usage: drisp runner [connect [<code>] --hub <url> [--foreground] [--replace] | disconnect [<team>] | status | restart | recover | help]
13227
13280
 
13228
13281
  Connect this machine from Settings \u2192 Runners in the web console, which shows the
13229
13282
  command to paste here:
@@ -13239,8 +13292,11 @@ that runs this machine's Runner for every Team it serves, at login. Connecting a
13239
13292
  the same Team reports the Runner the machine already is and changes nothing. When this
13240
13293
  machine can no longer serve that Runner (its credential is gone or has lapsed), Connect
13241
13294
  says so without using the code, and --replace connects a new Runner in its place.
13242
- drisp runner status lists the Teams and the service's state. --foreground skips the
13243
- service and runs the Runner in this terminal, for containers and CI; drisp runner runs
13295
+ drisp runner status lists the Teams and the service's state. drisp runner restart
13296
+ rewrites the service definition for the CLI installed now and restarts the service
13297
+ (on Linux it also turns start-at-login back on), so run it after
13298
+ npm install -g @drisp/cli@latest; it never installs one, and it fails any Run in
13299
+ progress, so restart when no Run is executing. --foreground skips the service and runs the Runner in this terminal, for containers and CI; drisp runner runs
13244
13300
  every Team's Runner there again later. DRISP_HUB_URL may stand in for --hub, and
13245
13301
  DRISP_RUNNER_CONFIG selects one identity file of a separate installation instead, which
13246
13302
  gets its own service.
@@ -13267,6 +13323,7 @@ The code works once and expires in ${minutes} minutes. Waiting for approval\u202
13267
13323
  );
13268
13324
  }
13269
13325
  var serviceStarted = (service) => `The Runner runs as a background service (${service.kind} ${service.name}) and starts again at login.`;
13326
+ var notConnected = "This machine is not connected to a Team. Settings \u2192 Runners shows the command to connect it.";
13270
13327
  var servicesFor = (options) => options.services === void 0 ? platformRunnerServices() : options.services;
13271
13328
  async function serviceInstallation(config) {
13272
13329
  const single = overriddenIdentityPath();
@@ -13275,25 +13332,35 @@ async function serviceInstallation(config) {
13275
13332
  ...single ? { configPath: single } : {}
13276
13333
  };
13277
13334
  }
13335
+ async function installAndRestart(service, configPath, onInstalled = () => {
13336
+ }) {
13337
+ await service.install(runnerServiceUnit(configPath));
13338
+ onInstalled();
13339
+ await service.restart();
13340
+ }
13278
13341
  async function ensureService(services, config, identityPath) {
13279
13342
  let service;
13280
- let installed = false;
13343
+ const progress = { installed: false };
13281
13344
  try {
13282
13345
  const { installationId, configPath } = await serviceInstallation(config);
13283
13346
  service = services(installationId);
13284
- await service.install(runnerServiceUnit(configPath));
13285
- installed = true;
13286
- if ((await service.status()).running) await service.stop();
13287
- await service.start();
13347
+ await installAndRestart(service, configPath, () => {
13348
+ progress.installed = true;
13349
+ });
13288
13350
  return service;
13289
13351
  } catch (error) {
13352
+ if (error instanceof ServiceNotEnabledError && service)
13353
+ throw new Error(
13354
+ `Connected, and the Runner runs as a background service (${service.kind} ${service.name}). ${error.message}
13355
+ Run drisp runner restart to enable it again.`
13356
+ );
13290
13357
  throw new Error(
13291
13358
  [
13292
13359
  `Connected, but the background service did not start: ${error instanceof Error ? error.message : String(error)}`,
13293
13360
  `The identity is saved to ${identityPath}: run drisp runner to run the Runner in the foreground.`,
13294
13361
  // launchd loads an installed LaunchAgent at the next login and RunAtLoad starts it. A systemd unit
13295
- // is enabled only by the start that just failed, so it has no such fallback.
13296
- ...installed && service?.kind === "launchd" ? [
13362
+ // is enabled only by the restart that just failed, so it has no such fallback.
13363
+ ...progress.installed && service?.kind === "launchd" ? [
13297
13364
  `The LaunchAgent ${service.name} is installed, so launchd starts the Runner at your next login.`
13298
13365
  ] : []
13299
13366
  ].join("\n")
@@ -13301,6 +13368,10 @@ async function ensureService(services, config, identityPath) {
13301
13368
  }
13302
13369
  }
13303
13370
  async function runRunnerCommandV2(input, options = {}) {
13371
+ if (input.flags.help || input.subcommand === "help") {
13372
+ process.stdout.write(usage);
13373
+ return 0;
13374
+ }
13304
13375
  try {
13305
13376
  if (input.flags.detach)
13306
13377
  throw new Error(
@@ -13308,7 +13379,8 @@ async function runRunnerCommandV2(input, options = {}) {
13308
13379
  );
13309
13380
  switch (input.subcommand) {
13310
13381
  case "connect": {
13311
- const [code, extra] = input.subcommandArgs;
13382
+ const code = input.subcommandArgs.at(0);
13383
+ const extra = input.subcommandArgs.at(1);
13312
13384
  if (extra) throw new Error(`Unexpected argument ${extra}.`);
13313
13385
  const hub = input.flags.hub ?? process.env["DRISP_HUB_URL"];
13314
13386
  if (!hub)
@@ -13350,18 +13422,18 @@ async function runRunnerCommandV2(input, options = {}) {
13350
13422
  `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
13423
  );
13352
13424
  }
13353
- const { config, path: path7, replaced } = outcome;
13425
+ const { config, path: path10, replaced } = outcome;
13354
13426
  for (const aside of replaced)
13355
13427
  process.stdout.write(
13356
13428
  `The Hub no longer recognised this machine's earlier identity for this Team; it was moved aside to ${aside}.
13357
13429
  `
13358
13430
  );
13359
13431
  process.stdout.write(
13360
- `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path7}.
13432
+ `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path10}.
13361
13433
  `
13362
13434
  );
13363
13435
  if (services) {
13364
- const service = await ensureService(services, config, path7);
13436
+ const service = await ensureService(services, config, path10);
13365
13437
  process.stdout.write(`${serviceStarted(service)}
13366
13438
  `);
13367
13439
  return 0;
@@ -13370,7 +13442,7 @@ async function runRunnerCommandV2(input, options = {}) {
13370
13442
  process.stdout.write(
13371
13443
  "No launchd or systemd here to run the Runner as a background service; running it in the foreground instead.\n"
13372
13444
  );
13373
- await runConfiguredRunner(path7, options.signal);
13445
+ await runConfiguredRunner(path10, options.signal);
13374
13446
  return 0;
13375
13447
  }
13376
13448
  case "disconnect": {
@@ -13430,10 +13502,7 @@ async function runRunnerCommandV2(input, options = {}) {
13430
13502
  }
13431
13503
  case "status": {
13432
13504
  const identities = await runnerIdentities();
13433
- if (!identities.length)
13434
- throw new Error(
13435
- "This machine is not connected to a Team. Settings \u2192 Runners shows the command to connect it."
13436
- );
13505
+ if (!identities.length) throw new Error(notConnected);
13437
13506
  const services = servicesFor(options);
13438
13507
  const service = services ? services(
13439
13508
  (await serviceInstallation(identities[0].config)).installationId
@@ -13444,9 +13513,9 @@ async function runRunnerCommandV2(input, options = {}) {
13444
13513
  configured: true,
13445
13514
  // One entry per Team this machine serves; credentials are never printed.
13446
13515
  teams: identities.map(
13447
- ({ path: path7, config: { token: _token, ...config } }) => ({
13516
+ ({ path: path10, config: { token: _token, ...config } }) => ({
13448
13517
  ...config,
13449
- path: path7
13518
+ path: path10
13450
13519
  })
13451
13520
  ),
13452
13521
  service: service ? {
@@ -13462,15 +13531,24 @@ async function runRunnerCommandV2(input, options = {}) {
13462
13531
  );
13463
13532
  return 0;
13464
13533
  }
13534
+ case "restart": {
13535
+ const [extra] = input.subcommandArgs;
13536
+ if (extra) throw new Error(`Unexpected argument ${extra}.`);
13537
+ const { service, configPath } = await installedService(
13538
+ servicesFor(options)
13539
+ );
13540
+ await installAndRestart(service, configPath);
13541
+ process.stdout.write(
13542
+ `Restarted the background service ${service.kind} ${service.name}.
13543
+ `
13544
+ );
13545
+ return 0;
13546
+ }
13465
13547
  case "":
13466
13548
  await runMachineRunners(options.signal, {
13467
13549
  services: servicesFor(options)
13468
13550
  });
13469
13551
  return 0;
13470
- case "help":
13471
- case "--help":
13472
- process.stdout.write(usage);
13473
- return 0;
13474
13552
  default:
13475
13553
  throw new Error(
13476
13554
  `Unsupported v2 runner command: ${input.subcommand}.
@@ -13485,6 +13563,25 @@ ${usage}`
13485
13563
  return 1;
13486
13564
  }
13487
13565
  }
13566
+ async function installedService(services) {
13567
+ const identities = await runnerIdentities();
13568
+ if (!identities.length) throw new Error(notConnected);
13569
+ const whose = overriddenIdentityPath() ? "the Runner installation DRISP_RUNNER_CONFIG selects" : "this machine's Runner";
13570
+ const foreground = "drisp runner runs it in the foreground meanwhile.";
13571
+ if (!services)
13572
+ throw new Error(
13573
+ `No background service is installed for ${whose}: there is no launchd or systemd --user here. ${foreground}`
13574
+ );
13575
+ const { installationId, configPath } = await serviceInstallation(
13576
+ identities[0].config
13577
+ );
13578
+ const service = services(installationId);
13579
+ if (!(await service.status()).installed)
13580
+ throw new Error(
13581
+ `No background service is installed for ${whose} (${service.kind} ${service.name}). drisp runner connect installs one when it connects to a Team (Settings \u2192 Runners shows the command); restart never installs one. ${foreground}`
13582
+ );
13583
+ return { service, ...configPath ? { configPath } : {} };
13584
+ }
13488
13585
  async function recoveryIdentity(runId) {
13489
13586
  const identities = await runnerIdentities();
13490
13587
  if (!identities.length)
@@ -13513,14 +13610,1248 @@ async function recoveryIdentity(runId) {
13513
13610
  throw new Error(`No Runner on this machine holds Run ${runId}.`);
13514
13611
  }
13515
13612
 
13613
+ // src/app/entry/buildCommand.ts
13614
+ import { execFileSync } from "child_process";
13615
+
13616
+ // src/core/build/planCommand.ts
13617
+ import fs6 from "fs";
13618
+ import path6 from "path";
13619
+
13620
+ // src/core/build/io.ts
13621
+ import fs5 from "fs";
13622
+ import path5 from "path";
13623
+ var BuildUsageError = class extends Error {
13624
+ };
13625
+ var PlanFailure = class extends Error {
13626
+ };
13627
+ function requireEnv(env2, name) {
13628
+ const value = env2[name];
13629
+ if (!value)
13630
+ throw new BuildUsageError(
13631
+ `${name} is not set: drisp build runs inside an Orchestrator Run.`
13632
+ );
13633
+ return value;
13634
+ }
13635
+ function readJson(file) {
13636
+ try {
13637
+ return JSON.parse(fs5.readFileSync(file, "utf-8"));
13638
+ } catch {
13639
+ return void 0;
13640
+ }
13641
+ }
13642
+ function writeFile(file, content) {
13643
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
13644
+ fs5.writeFileSync(file, content, "utf-8");
13645
+ }
13646
+ var json = (value) => `${JSON.stringify(value, null, 2)}
13647
+ `;
13648
+ function readInput(file) {
13649
+ const raw = readJson(file);
13650
+ if (raw === void 0)
13651
+ throw new PlanFailure(`Could not read plan-input.json at ${file}.`);
13652
+ const parsed = planInputSchema.safeParse(raw);
13653
+ if (!parsed.success) {
13654
+ const issue = parsed.error.issues[0];
13655
+ throw new PlanFailure(
13656
+ `plan-input.json is invalid at ${issue.path.join(".") || "(root)"}: ${issue.message}`
13657
+ );
13658
+ }
13659
+ return parsed.data;
13660
+ }
13661
+
13662
+ // src/core/workflows/builtins/build-child/version.ts
13663
+ import { createHash } from "crypto";
13664
+ function computeTemplateVersion() {
13665
+ const parts = [
13666
+ WORKFLOW_MD_TEMPLATE,
13667
+ STAGE_TEMPLATE,
13668
+ CANNOT_PASS_TEMPLATE,
13669
+ NO_GATE_TEXT_TEMPLATE,
13670
+ DESCRIPTION_TEMPLATE,
13671
+ JSON.stringify(QA_SECTIONS),
13672
+ JSON.stringify(GATES),
13673
+ JSON.stringify(RENDERER_RULES),
13674
+ JSON.stringify(LAYOUT_MARKERS)
13675
+ ];
13676
+ const hash = createHash("sha256");
13677
+ for (const part of parts) hash.update(`${part.length}:${part}`);
13678
+ return hash.digest("hex");
13679
+ }
13680
+ var templateVersion = computeTemplateVersion();
13681
+
13682
+ // src/core/build/planCommand.ts
13683
+ function checkGenerated(directory, workflow, host, layout) {
13684
+ const workflowJson = fs6.readFileSync(
13685
+ path6.join(directory, "workflow.json"),
13686
+ "utf-8"
13687
+ );
13688
+ const workflowMd = fs6.readFileSync(
13689
+ path6.join(directory, "workflow.md"),
13690
+ "utf-8"
13691
+ );
13692
+ try {
13693
+ parseWorkflowConfig(JSON.parse(workflowJson), {
13694
+ instructions: workflowMd,
13695
+ host
13696
+ });
13697
+ } catch (error) {
13698
+ throw new PlanFailure(
13699
+ `${workflow.identifier}: ${error instanceof Error ? error.message : String(error)}`
13700
+ );
13701
+ }
13702
+ const verified = verifyRendered({
13703
+ fill: workflow.fill,
13704
+ layout,
13705
+ workflowName: workflow.workflowName,
13706
+ workflowJson,
13707
+ workflowMd
13708
+ });
13709
+ if (!verified.ok)
13710
+ throw new PlanFailure(
13711
+ `${workflow.identifier}: the written ${verified.file} does not match what its fill renders.`
13712
+ );
13713
+ }
13714
+ function runBuildPlan(deps) {
13715
+ const orchestratorRunId = requireEnv(deps.env, "DRISP_RUN_ID");
13716
+ const runnerId = requireEnv(deps.env, "DRISP_RUNNER_ID");
13717
+ const artifacts = requireEnv(deps.env, "DRISP_RUN_ARTIFACTS");
13718
+ const fixtureInput = deps.env["DRISP_BUILD_PLAN_INPUT"];
13719
+ const inputPath = fixtureInput ?? deps.inputPath;
13720
+ if (!inputPath)
13721
+ throw new BuildUsageError("Usage: drisp build plan --input <path>");
13722
+ const fail = (reason) => {
13723
+ writeFile(path6.join(artifacts, "build_plan_error.json"), json({ reason }));
13724
+ return { status: "failed", reason };
13725
+ };
13726
+ try {
13727
+ if (!isGitHubRepository(deps.folder))
13728
+ throw new PlanFailure(BUILD_STRINGS.notAGitHubRepository);
13729
+ const input = readInput(path6.resolve(deps.folder, inputPath));
13730
+ const worktree = ensureBuildWorktree({
13731
+ folder: deps.folder,
13732
+ slugId: input.project.slugId,
13733
+ worktreesRoot: deps.worktreesRoot
13734
+ });
13735
+ const designDir = path6.join(
13736
+ worktree.path,
13737
+ ".athena",
13738
+ "workflows",
13739
+ input.project.slugId
13740
+ );
13741
+ writeFile(path6.join(designDir, "plan-input.json"), json(input));
13742
+ const fixtureDir = fixtureInput ? path6.dirname(path6.resolve(fixtureInput)) : void 0;
13743
+ const fillPath = (identifier) => path6.join(designDir, identifier, "fill.json");
13744
+ const fills = Object.fromEntries(
13745
+ input.issues.map((issue) => {
13746
+ const fixture = fixtureDir ? path6.join(fixtureDir, "fills", `${issue.identifier}.json`) : void 0;
13747
+ const file = fixture && fs6.existsSync(fixture) ? fixture : fillPath(issue.identifier);
13748
+ return [issue.identifier, readJson(file)];
13749
+ })
13750
+ );
13751
+ const fixturePullRequests = fixtureDir ? readJson(path6.join(fixtureDir, "pull-requests.json")) : void 0;
13752
+ const pullRequests = fixturePullRequests ?? Object.fromEntries(
13753
+ input.issues.filter(
13754
+ (issue) => issue.branchName && issue.state.type !== "canceled" && !issue.labels.includes("ready-for-human")
13755
+ ).map((issue) => [
13756
+ issue.branchName,
13757
+ deps.pullRequests(deps.folder, issue.branchName)
13758
+ ])
13759
+ );
13760
+ const layout = repoLayoutAt(worktree.path, "HEAD");
13761
+ const result = planBuild({
13762
+ input,
13763
+ fills,
13764
+ pullRequests,
13765
+ orchestratorRunId,
13766
+ folder: deps.folder,
13767
+ runnerId,
13768
+ templateVersion,
13769
+ layout
13770
+ });
13771
+ if (result.status === "failed") return fail(result.reason);
13772
+ if (result.status === "needs_fills")
13773
+ return {
13774
+ status: "needs_fills",
13775
+ templateVersion,
13776
+ fills: result.fills.map((fill) => ({
13777
+ ...fill,
13778
+ path: fillPath(fill.identifier)
13779
+ }))
13780
+ };
13781
+ const host = deps.hostCapabilities();
13782
+ for (const workflow of result.workflows) {
13783
+ const directory = path6.join(designDir, workflow.identifier);
13784
+ writeFile(path6.join(directory, "fill.json"), json(workflow.fill));
13785
+ writeFile(path6.join(directory, "workflow.json"), workflow.workflowJson);
13786
+ writeFile(path6.join(directory, "workflow.md"), workflow.workflowMd);
13787
+ checkGenerated(directory, workflow, host, layout);
13788
+ }
13789
+ const relativeDesignDir = path6.relative(worktree.path, designDir);
13790
+ commitBuildFiles(
13791
+ worktree.path,
13792
+ [relativeDesignDir],
13793
+ `drisp build: Generated Workflows for ${input.project.slugId}`
13794
+ );
13795
+ const commit = lastCommitTouching(worktree.path, relativeDesignDir, [
13796
+ path6.join(relativeDesignDir, "plan.json")
13797
+ ]);
13798
+ const plan = finalizePlan(result.plan, commit);
13799
+ writeFile(path6.join(designDir, "plan.json"), json(plan));
13800
+ commitBuildFiles(
13801
+ worktree.path,
13802
+ [path6.join(relativeDesignDir, "plan.json")],
13803
+ `drisp build: Run plan for ${input.project.slugId}`
13804
+ );
13805
+ writeFile(path6.join(artifacts, "build_plan.json"), json(plan));
13806
+ return {
13807
+ status: "planned",
13808
+ entries: plan.groups.reduce((sum, group) => sum + group.issues.length, 0),
13809
+ skipped: plan.skipped.length
13810
+ };
13811
+ } catch (error) {
13812
+ if (error instanceof BuildUsageError) throw error;
13813
+ return fail(error instanceof Error ? error.message : String(error));
13814
+ }
13815
+ }
13816
+
13817
+ // src/core/build/tick.ts
13818
+ import { randomUUID as randomUUID2 } from "crypto";
13819
+ import fs7 from "fs";
13820
+ import path7 from "path";
13821
+
13822
+ // ../../packages/client/src/index.ts
13823
+ var runArtifactSchema = external_exports.object({ name: external_exports.string(), seq: external_exports.number().int(), json: external_exports.json() });
13824
+ var githubRepositorySchema = external_exports.object({
13825
+ installationId: external_exports.number().int().positive(),
13826
+ repositoryId: external_exports.number().int().positive(),
13827
+ owner: external_exports.string(),
13828
+ name: external_exports.string(),
13829
+ defaultBranch: external_exports.string()
13830
+ });
13831
+ var ClientError = class extends Error {
13832
+ constructor(code, status) {
13833
+ super(code);
13834
+ this.code = code;
13835
+ this.status = status;
13836
+ }
13837
+ code;
13838
+ status;
13839
+ };
13840
+ var DrispClient = class _DrispClient {
13841
+ constructor(baseUrl, orgId, token) {
13842
+ this.baseUrl = baseUrl;
13843
+ this.orgId = orgId;
13844
+ this.token = token;
13845
+ const url = new URL(baseUrl);
13846
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
13847
+ throw new Error("TLS_required");
13848
+ }
13849
+ baseUrl;
13850
+ orgId;
13851
+ token;
13852
+ /**
13853
+ * A client that authenticates with a Run's delegation token (root ADR-0008), from the agent's environment:
13854
+ * `hubUrl` is `DRISP_HUB_URL`, the Team-scoped base `https://<hub>/v2/orgs/<orgId>`, and `token` is
13855
+ * `DRISP_RUN_TOKEN`. The token starts, cancels and reads only its own Run's children.
13856
+ */
13857
+ static delegated(hubUrl, token) {
13858
+ const url = new URL(hubUrl);
13859
+ const match = /^\/v2\/orgs\/([^/]+)\/?$/.exec(url.pathname);
13860
+ if (!match) throw new Error("invalid_hub_url");
13861
+ return new _DrispClient(url.origin, decodeURIComponent(match[1]), async () => token);
13862
+ }
13863
+ async request(path10, body) {
13864
+ const response = await fetch(
13865
+ `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}${path10}`,
13866
+ {
13867
+ method: body === void 0 ? "GET" : "POST",
13868
+ headers: {
13869
+ Authorization: `Bearer ${await this.token()}`,
13870
+ "Content-Type": "application/json"
13871
+ },
13872
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
13873
+ }
13874
+ );
13875
+ const result = await response.json();
13876
+ if (!response.ok)
13877
+ throw new ClientError(result.error ?? `HTTP ${response.status}`, response.status);
13878
+ return result;
13879
+ }
13880
+ submit(command) {
13881
+ return this.request("/commands", commandSchema.parse(command));
13882
+ }
13883
+ /** The recent Runs; with a filter, every Run carrying that key or every child of that Run, newest first. */
13884
+ async runs(filter) {
13885
+ const query = !filter ? "" : "key" in filter ? `?key=${encodeURIComponent(filter.key)}` : `?parent=${encodeURIComponent(filter.parent)}`;
13886
+ return external_exports.object({ hubEpoch: external_exports.string(), runs: external_exports.array(runViewSchema) }).parse(await this.request(`/runs${query}`));
13887
+ }
13888
+ /** The newest `run.artifact` named `name` that the Run reported; `not_found` when it reported none. */
13889
+ async artifact(runId, name) {
13890
+ return runArtifactSchema.parse(
13891
+ await this.request(`/runs/${encodeURIComponent(runId)}/artifacts/${encodeURIComponent(name)}`)
13892
+ );
13893
+ }
13894
+ async run(runId) {
13895
+ return runViewSchema.parse(await this.request(`/runs/${encodeURIComponent(runId)}`));
13896
+ }
13897
+ history(runId, after = 0) {
13898
+ return this.request(`/runs/${encodeURIComponent(runId)}/history?after=${after}`);
13899
+ }
13900
+ async runners() {
13901
+ return runnerListSchema.parse(await this.request("/runners"));
13902
+ }
13903
+ /** A one-use Connect code bound to the signed-in member, for `drisp runner connect <code>` (root ADR-0005). */
13904
+ async createConnectCode(hubEpoch) {
13905
+ return connectCodeResponseSchema.parse(await this.request("/runners/connect-codes", { hubEpoch }));
13906
+ }
13907
+ /** Rename a Runner, set its capacity, or change its Audience (root ADR-0005). */
13908
+ async updateRunner(hubEpoch, runnerId, update) {
13909
+ const body = runnerUpdateRequestSchema.parse({ hubEpoch, ...update });
13910
+ return runnerListEntrySchema.parse(await this.request(`/runners/${encodeURIComponent(runnerId)}`, body));
13911
+ }
13912
+ /** Admin only: where a member's Runs go when they have no ready Personal Runner; `null` clears it. */
13913
+ async setTeamDefault(hubEpoch, teamDefault) {
13914
+ const body = teamDefaultRequestSchema.parse({ hubEpoch, teamDefault });
13915
+ return external_exports.object({ teamDefault: teamDefaultSchema.nullable() }).parse(await this.request("/team-default", body));
13916
+ }
13917
+ /** The Runs a Disconnect of `runnerId` would stop, for its confirmation. */
13918
+ async disconnectPreview(runnerId) {
13919
+ return runnerDisconnectPreviewSchema.parse(
13920
+ await this.request(`/runners/${encodeURIComponent(runnerId)}/disconnect`)
13921
+ );
13922
+ }
13923
+ /** Disconnect (root ADR-0005): the Runner leaves the Team and its open Runs are Stopped. */
13924
+ async disconnectRunner(hubEpoch, runnerId) {
13925
+ return runnerDisconnectResponseSchema.parse(
13926
+ await this.request(`/runners/${encodeURIComponent(runnerId)}/disconnect`, { hubEpoch })
13927
+ );
13928
+ }
13929
+ /** Admin only: take over a Runner paused as "Needs an owner" because its member left, so it serves again. */
13930
+ async adoptRunner(hubEpoch, runnerId) {
13931
+ const body = runnerAdoptRequestSchema.parse({ hubEpoch });
13932
+ return runnerListEntrySchema.parse(await this.request(`/runners/${encodeURIComponent(runnerId)}/adopt`, body));
13933
+ }
13934
+ submitManaged(profileId, command, repository) {
13935
+ return this.request("/managed-runs", {
13936
+ profileId,
13937
+ ...repository,
13938
+ command: commandSchema.parse(command)
13939
+ });
13940
+ }
13941
+ async repositories() {
13942
+ const schema = external_exports.object({
13943
+ configured: external_exports.boolean(),
13944
+ installationUrl: external_exports.string().url().optional(),
13945
+ repositories: external_exports.array(repositoryBindingSchema)
13946
+ });
13947
+ try {
13948
+ return schema.parse(await this.request("/repositories"));
13949
+ } catch (error) {
13950
+ if (error instanceof ClientError && error.status === 404 && error.code === "not_found")
13951
+ return schema.parse({ configured: false, repositories: [] });
13952
+ throw error;
13953
+ }
13954
+ }
13955
+ async connectGitHub(hubEpoch) {
13956
+ return external_exports.object({
13957
+ connectionId: external_exports.string(),
13958
+ userCode: external_exports.string(),
13959
+ verificationUri: external_exports.string().url(),
13960
+ expiresAt: external_exports.number(),
13961
+ interval: external_exports.number(),
13962
+ installationUrl: external_exports.string().url()
13963
+ }).parse(await this.request("/github/device", { hubEpoch }));
13964
+ }
13965
+ async pollGitHub(hubEpoch, connectionId) {
13966
+ return external_exports.discriminatedUnion("state", [
13967
+ external_exports.object({ state: external_exports.literal("pending"), interval: external_exports.number() }),
13968
+ external_exports.object({
13969
+ state: external_exports.literal("authorized"),
13970
+ authorizationId: external_exports.string(),
13971
+ expiresAt: external_exports.number(),
13972
+ repositories: external_exports.array(githubRepositorySchema)
13973
+ })
13974
+ ]).parse(await this.request("/github/device/poll", { hubEpoch, connectionId }));
13975
+ }
13976
+ async bindRepository(hubEpoch, authorizationId, repository, settings) {
13977
+ return repositoryBindingSchema.parse(
13978
+ await this.request("/repositories", {
13979
+ hubEpoch,
13980
+ authorizationId,
13981
+ installationId: repository.installationId,
13982
+ repositoryId: repository.repositoryId,
13983
+ ...settings
13984
+ })
13985
+ );
13986
+ }
13987
+ async updateRepository(hubEpoch, id, settings) {
13988
+ return repositoryBindingSchema.parse(
13989
+ await this.request(`/repositories/${encodeURIComponent(id)}`, {
13990
+ hubEpoch,
13991
+ ...settings
13992
+ })
13993
+ );
13994
+ }
13995
+ disconnectRepository(hubEpoch, id) {
13996
+ return this.request(`/repositories/${encodeURIComponent(id)}/disconnect`, { hubEpoch });
13997
+ }
13998
+ receipt(commandId) {
13999
+ return this.request(`/commands/${encodeURIComponent(commandId)}`);
14000
+ }
14001
+ deleteSession(hubEpoch, sessionId) {
14002
+ return this.request("/sessions/delete", { hubEpoch, sessionId });
14003
+ }
14004
+ async pendingDevice(code) {
14005
+ return external_exports.object({ name: external_exports.string() }).parse(await this.request(`/auth/device?code=${encodeURIComponent(code)}`));
14006
+ }
14007
+ /** Approves the macOS app's sign-in for this client's Team: the token is bound to it. */
14008
+ approveDevice(userCode) {
14009
+ return this.request("/auth/device/approve", { userCode });
14010
+ }
14011
+ /** The machine asking to Connect with a code it printed (`drisp runner connect` with no code; root ADR-0005). */
14012
+ async pendingConnect(code) {
14013
+ return connectPendingResponseSchema.parse(await this.request(`/runners/connect/pending?code=${encodeURIComponent(code)}`));
14014
+ }
14015
+ /** Connects the machine that printed `code` to this client's Team, as a Runner with `audience`. Team needs an admin. */
14016
+ approveConnect(code, audience, hubEpoch) {
14017
+ return this.request("/runners/connect/approve", { code, audience, ...hubEpoch === void 0 ? {} : { hubEpoch } });
14018
+ }
14019
+ /** Refuses the machine that printed `code`; its wait ends without a credential. */
14020
+ denyConnect(code) {
14021
+ return this.request("/runners/connect/deny", { code });
14022
+ }
14023
+ async historyPage(runId, after = 0) {
14024
+ const page = external_exports.object({
14025
+ through: external_exports.number(),
14026
+ archives: external_exports.array(
14027
+ external_exports.object({
14028
+ id: external_exports.string(),
14029
+ checksum: external_exports.string(),
14030
+ first_seq: external_exports.number(),
14031
+ last_seq: external_exports.number()
14032
+ })
14033
+ ),
14034
+ batches: external_exports.array(external_exports.object({ firstSeq: external_exports.number(), records: external_exports.array(historyRecordSchema) }))
14035
+ }).parse(await this.history(runId, after));
14036
+ const records = [];
14037
+ const archive = page.archives[0];
14038
+ if (archive) {
14039
+ const response = await fetch(
14040
+ `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}/runs/${encodeURIComponent(runId)}/objects/${encodeURIComponent(archive.id)}`,
14041
+ { headers: { Authorization: `Bearer ${await this.token()}` } }
14042
+ );
14043
+ if (!response.ok) throw new Error(`history_download_${response.status}`);
14044
+ const bytes = await response.arrayBuffer();
14045
+ const checksum = [...new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
14046
+ if (checksum !== archive.checksum) throw new Error("history_integrity_failure");
14047
+ const text = await new Response(
14048
+ new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
14049
+ ).text();
14050
+ for (const line of text.trim().split("\n"))
14051
+ records.push(
14052
+ external_exports.object({ seq: external_exports.number(), record: historyRecordSchema }).parse(JSON.parse(line))
14053
+ );
14054
+ }
14055
+ for (const batch of page.batches)
14056
+ batch.records.forEach(
14057
+ (record, index) => records.push({ seq: batch.firstSeq + index, record })
14058
+ );
14059
+ records.sort((a, b) => a.seq - b.seq);
14060
+ let through = after;
14061
+ const contiguous = [];
14062
+ for (const row of records) {
14063
+ if (row.seq <= through) continue;
14064
+ if (row.seq !== through + 1) break;
14065
+ contiguous.push(row);
14066
+ through = row.seq;
14067
+ }
14068
+ if (through === after && page.through > after) throw new Error("history_gap");
14069
+ return { through, availableThrough: page.through, records: contiguous };
14070
+ }
14071
+ watch(hubEpoch, runIds, update, error) {
14072
+ let stopped = false;
14073
+ let socket;
14074
+ let timer;
14075
+ let delay = 250;
14076
+ const runs = /* @__PURE__ */ new Map();
14077
+ const connect = async () => {
14078
+ try {
14079
+ const { ticket } = external_exports.object({ ticket: external_exports.string() }).parse(await this.request("/connect-ticket", { hubEpoch }));
14080
+ if (stopped) return;
14081
+ const endpoint = new URL(
14082
+ `/v2/orgs/${encodeURIComponent(this.orgId)}/connect`,
14083
+ this.baseUrl
14084
+ );
14085
+ endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:";
14086
+ const ws = new WebSocket(endpoint, ["drisp-v2", `ticket.${ticket}`]);
14087
+ socket = ws;
14088
+ ws.onopen = () => ws.send(JSON.stringify({ type: "watch", hubEpoch, runIds }));
14089
+ ws.onmessage = (event) => {
14090
+ try {
14091
+ if (typeof event.data !== "string" || new TextEncoder().encode(event.data).length > 8 * 1024 * 1024)
14092
+ throw new Error("frame_too_large");
14093
+ const message = JSON.parse(String(event.data));
14094
+ if (message.type === "error") throw new Error(message.code);
14095
+ if (message.hubEpoch !== hubEpoch) throw new Error("recovery_required");
14096
+ const header = external_exports.object({ subscriptionId: external_exports.string(), type: external_exports.enum(["snapshot", "change"]) }).parse(message);
14097
+ let through;
14098
+ if (header.type === "snapshot") {
14099
+ const snapshot = external_exports.object({
14100
+ controlThrough: external_exports.number().int().nonnegative(),
14101
+ runs: external_exports.array(runViewSchema)
14102
+ }).parse(message);
14103
+ runs.clear();
14104
+ for (const run of snapshot.runs) runs.set(run.runId, run);
14105
+ through = snapshot.controlThrough;
14106
+ } else {
14107
+ const change = external_exports.object({
14108
+ through: external_exports.number().int().nonnegative(),
14109
+ runs: external_exports.array(external_exports.object({ seq: external_exports.number(), run: runViewSchema }))
14110
+ }).parse(message);
14111
+ for (const { run } of change.runs)
14112
+ if ((runs.get(run.runId)?.recordRevision ?? 0) < run.recordRevision)
14113
+ runs.set(run.runId, run);
14114
+ through = change.through;
14115
+ }
14116
+ update([...runs.values()]);
14117
+ delay = 250;
14118
+ ws.send(
14119
+ JSON.stringify({ type: "watch.ack", subscriptionId: header.subscriptionId, through })
14120
+ );
14121
+ } catch (cause) {
14122
+ if (cause instanceof Error && cause.message === "reset_required") {
14123
+ ws.close();
14124
+ return;
14125
+ }
14126
+ stopped = true;
14127
+ error(cause);
14128
+ ws.close();
14129
+ }
14130
+ };
14131
+ ws.onclose = () => {
14132
+ if (!stopped) {
14133
+ timer = setTimeout(() => {
14134
+ void connect();
14135
+ }, delay);
14136
+ delay = Math.min(delay * 2, 3e4);
14137
+ }
14138
+ };
14139
+ } catch (cause) {
14140
+ if (stopped) return;
14141
+ error(cause);
14142
+ if (cause instanceof ClientError && cause.status < 500 && cause.status !== 429) return;
14143
+ timer = setTimeout(() => {
14144
+ void connect();
14145
+ }, delay);
14146
+ delay = Math.min(delay * 2, 3e4);
14147
+ }
14148
+ };
14149
+ void connect();
14150
+ return () => {
14151
+ stopped = true;
14152
+ if (timer) clearTimeout(timer);
14153
+ socket?.close();
14154
+ };
14155
+ }
14156
+ };
14157
+
14158
+ // src/core/build/state.ts
14159
+ var ATTEMPT_OUTCOMES = [
14160
+ "completed",
14161
+ "failed",
14162
+ "cancelled",
14163
+ "template_mismatch"
14164
+ ];
14165
+ var attemptSchema = external_exports.strictObject({
14166
+ n: external_exports.number().int().min(1).max(2),
14167
+ runId: external_exports.string().min(1),
14168
+ tokenCap: external_exports.number().int().positive(),
14169
+ startedAt: external_exports.string(),
14170
+ /** Where the attempt worked (chosen while building slice 2): later ticks check its PR and remove its worktree. */
14171
+ branch: external_exports.string().min(1),
14172
+ worktree: external_exports.string().min(1),
14173
+ outcome: external_exports.enum(ATTEMPT_OUTCOMES).optional()
14174
+ });
14175
+ var buildStateSchema = external_exports.strictObject({
14176
+ version: external_exports.literal(1),
14177
+ orchestratorRunId: external_exports.string().min(1),
14178
+ projectId: external_exports.string().min(1),
14179
+ slugId: external_exports.string().regex(/^[a-z0-9]+$/),
14180
+ templateVersion: external_exports.string().regex(/^[0-9a-f]{64}$/),
14181
+ approvedAt: external_exports.string(),
14182
+ approved: external_exports.strictObject({ groups: external_exports.array(external_exports.array(external_exports.string())) }),
14183
+ attempts: external_exports.record(external_exports.string(), external_exports.array(attemptSchema)),
14184
+ needsPerson: external_exports.record(external_exports.string(), external_exports.string()),
14185
+ tickCount: external_exports.number().int().nonnegative(),
14186
+ lastTickAt: external_exports.string(),
14187
+ /**
14188
+ * Additive (chosen while building slice 3): the entries a tick had seen done, for rebasing parked children's
14189
+ * branches after a merge. No longer written since slice 4 (a parked child rebases itself), still read so a state
14190
+ * written before parses.
14191
+ */
14192
+ done: external_exports.array(external_exports.string()).optional(),
14193
+ /** The merged issues the Orchestrator was already asked to repair in Linear, so it is asked once. */
14194
+ repaired: external_exports.array(external_exports.string()).optional()
14195
+ });
14196
+
14197
+ // src/core/build/childStart.ts
14198
+ function childRunId(orchestratorRunId, identifier, attempt) {
14199
+ return `${orchestratorRunId}:${identifier}:a${attempt}`;
14200
+ }
14201
+ function childPrompt(child) {
14202
+ return [
14203
+ `${child.identifier} ${child.title}`,
14204
+ `Linear: ${child.url}`,
14205
+ `Branch: ${child.branch}`,
14206
+ `Worktree: ${child.worktree}`,
14207
+ `Build: ${child.orchestratorRunId}`
14208
+ ].join("\n");
14209
+ }
14210
+ function childStartIntent(child, parent) {
14211
+ const runId = childRunId(
14212
+ child.orchestratorRunId,
14213
+ child.identifier,
14214
+ child.attempt
14215
+ );
14216
+ return {
14217
+ runId,
14218
+ intent: {
14219
+ kind: "start",
14220
+ sessionId: runId,
14221
+ runnerId: parent.runnerId,
14222
+ scope: parent.scope,
14223
+ key: `issue:${child.issueId}`,
14224
+ parent: { runId: child.orchestratorRunId, tokenCap: child.tokenCap },
14225
+ spec: {
14226
+ prompt: childPrompt(child),
14227
+ projectDir: child.worktree,
14228
+ harness: "claude-code",
14229
+ workflow: child.workflowName
14230
+ }
14231
+ }
14232
+ };
14233
+ }
14234
+
14235
+ // src/core/build/tickDecision.ts
14236
+ var MAX_ATTEMPTS = 2;
14237
+ function mayRetry(attempts) {
14238
+ return attempts.length < MAX_ATTEMPTS && attempts.at(-1)?.outcome === "failed";
14239
+ }
14240
+ var STOPPED_CLOSE_REASONS = /* @__PURE__ */ new Set(["cancelled_before_start", "abandoned"]);
14241
+ function outcomeOf(view) {
14242
+ if (view.closeReason === "workflow_template_mismatch")
14243
+ return "template_mismatch";
14244
+ const status = view.execution?.status;
14245
+ if (status === "completed" || status === "failed" || status === "cancelled")
14246
+ return status;
14247
+ if (view.cancellation || STOPPED_CLOSE_REASONS.has(view.closeReason ?? ""))
14248
+ return "cancelled";
14249
+ return "failed";
14250
+ }
14251
+ function buildLimits(own, delegation) {
14252
+ if (!delegation) return own;
14253
+ return {
14254
+ ...own,
14255
+ maxChildren: Math.min(own.maxChildren, delegation.maxChildren),
14256
+ tokenBudget: Math.min(own.tokenBudget, delegation.tokenBudget)
14257
+ };
14258
+ }
14259
+ function occupies(view) {
14260
+ return !view || !view.closed && view.execution?.status !== "awaiting_attention";
14261
+ }
14262
+ function assessTick(facts) {
14263
+ const { entries, orchestratorRunId } = facts;
14264
+ const state = structuredClone(facts.state);
14265
+ const views = new Map(facts.children.map((view) => [view.runId, view]));
14266
+ const issues = new Map(facts.issues.map((issue) => [issue.id, issue]));
14267
+ const attemptsOf = (id) => state.attempts[id] ?? [];
14268
+ for (const entry of entries) {
14269
+ const prefix = `${orchestratorRunId}:${entry.identifier}:a`;
14270
+ for (const view of facts.children) {
14271
+ if (!view.runId.startsWith(prefix)) continue;
14272
+ const n = Number(view.runId.slice(prefix.length));
14273
+ if (!(n === 1 || n === 2)) continue;
14274
+ if (attemptsOf(entry.id).some((a) => a.runId === view.runId)) continue;
14275
+ state.attempts[entry.id] = [
14276
+ ...attemptsOf(entry.id),
14277
+ {
14278
+ n,
14279
+ runId: view.runId,
14280
+ tokenCap: view.parent?.tokenCap ?? entry.tokenCap,
14281
+ startedAt: facts.now,
14282
+ ...facts.locate(entry)
14283
+ }
14284
+ ].sort((a, b) => a.n - b.n);
14285
+ }
14286
+ }
14287
+ for (const attempts of Object.values(state.attempts))
14288
+ for (const attempt of attempts) {
14289
+ const view = views.get(attempt.runId);
14290
+ if (view?.closed && !attempt.outcome) attempt.outcome = outcomeOf(view);
14291
+ }
14292
+ const branchOf = (entry) => attemptsOf(entry.id).at(-1)?.branch ?? facts.locate(entry).branch;
14293
+ for (const identifier of facts.rebaseFailed) {
14294
+ const entry = entries.find((each) => each.identifier === identifier);
14295
+ if (entry)
14296
+ state.needsPerson[entry.id] = BUILD_STRINGS.rebaseFailed(branchOf(entry));
14297
+ }
14298
+ const pullRequests = /* @__PURE__ */ new Map();
14299
+ const prsOf = (entry) => {
14300
+ const branch = attemptsOf(entry.id).at(-1)?.branch ?? issues.get(entry.id)?.branchName;
14301
+ if (!branch) return void 0;
14302
+ if (!pullRequests.has(branch))
14303
+ pullRequests.set(branch, facts.pullRequests(branch));
14304
+ return pullRequests.get(branch);
14305
+ };
14306
+ const status = /* @__PURE__ */ new Map();
14307
+ const skippedBlocker = (id) => {
14308
+ const linear = issues.get(id)?.state.type;
14309
+ return linear === "completed" || linear === "canceled" ? "done" : "blocked";
14310
+ };
14311
+ const repair = [];
14312
+ const repaired = new Set(state.repaired ?? []);
14313
+ for (const entry of entries) {
14314
+ const attempts = attemptsOf(entry.id);
14315
+ const newest = attempts.at(-1);
14316
+ const view = newest && views.get(newest.runId);
14317
+ const linear = issues.get(entry.id)?.state.type;
14318
+ const statusOf = () => {
14319
+ if (linear === "completed" || linear === "canceled") return "done";
14320
+ const prs = prsOf(entry);
14321
+ if (prs?.merged) {
14322
+ if (!repaired.has(entry.id) && !(newest && occupies(view))) {
14323
+ const prUrl = prs.mergedUrl ?? branchOf(entry);
14324
+ repair.push({
14325
+ id: entry.id,
14326
+ identifier: entry.identifier,
14327
+ prUrl,
14328
+ comment: BUILD_STRINGS.repairComment(prUrl, orchestratorRunId)
14329
+ });
14330
+ repaired.add(entry.id);
14331
+ }
14332
+ return "done";
14333
+ }
14334
+ if (newest && (!view || !view.closed)) return "live";
14335
+ if (newest?.outcome === "template_mismatch") return "replan";
14336
+ if (newest?.outcome === "completed")
14337
+ state.needsPerson[entry.id] ??= BUILD_STRINGS.completedUnmerged;
14338
+ if (state.needsPerson[entry.id] || newest && !mayRetry(attempts))
14339
+ return "needs_person";
14340
+ const blockers = entry.blockedBy.map(
14341
+ (id) => status.get(id) ?? skippedBlocker(id)
14342
+ );
14343
+ if (blockers.some(
14344
+ (b) => b === "needs_person" || b === "replan" || b === "blocked"
14345
+ ))
14346
+ return "blocked";
14347
+ return blockers.every((b) => b === "done") ? "ready" : "pending";
14348
+ };
14349
+ status.set(entry.id, statusOf());
14350
+ if (status.get(entry.id) === "done" && state.needsPerson[entry.id] === BUILD_STRINGS.completedUnmerged)
14351
+ delete state.needsPerson[entry.id];
14352
+ }
14353
+ if (repaired.size > 0) state.repaired = [...repaired];
14354
+ let parkReason = facts.templateVersion !== state.templateVersion ? "template" : state.tickCount >= facts.limits.tickAllowance ? "budget" : void 0;
14355
+ const occupying = entries.filter((entry) => {
14356
+ const newest = attemptsOf(entry.id).at(-1);
14357
+ return newest !== void 0 && occupies(views.get(newest.runId));
14358
+ }).length;
14359
+ const queue = [];
14360
+ if (!parkReason) {
14361
+ let capped = Object.values(state.attempts).flat().reduce((sum, attempt) => sum + attempt.tokenCap, 0);
14362
+ for (const entry of entries) {
14363
+ if (status.get(entry.id) !== "ready") continue;
14364
+ if (capped + entry.tokenCap > facts.limits.tokenBudget) {
14365
+ parkReason = "budget";
14366
+ break;
14367
+ }
14368
+ queue.push(entry);
14369
+ capped += entry.tokenCap;
14370
+ }
14371
+ }
14372
+ return {
14373
+ state,
14374
+ status,
14375
+ repair,
14376
+ queue,
14377
+ slots: Math.max(0, facts.limits.maxChildren - occupying),
14378
+ ...parkReason ? { parkReason } : {}
14379
+ };
14380
+ }
14381
+ function tickAction(input) {
14382
+ const live = input.children.filter((view) => !view.closed);
14383
+ if (input.started > 0 || live.some((view) => occupies(view)))
14384
+ return { action: "wait" };
14385
+ const remaining = input.entries.some(
14386
+ (entry) => ["ready", "pending"].includes(input.status.get(entry.id))
14387
+ );
14388
+ if (input.parkReason && remaining)
14389
+ return {
14390
+ action: "park",
14391
+ reason: input.parkReason,
14392
+ message: input.parkReason === "budget" ? BUILD_STRINGS.budgetPark : BUILD_STRINGS.templatePark
14393
+ };
14394
+ if (live.length > 0) return { action: "wait" };
14395
+ const done = input.entries.filter(
14396
+ (entry) => input.status.get(entry.id) === "done"
14397
+ ).length;
14398
+ return {
14399
+ action: "complete",
14400
+ message: `Built ${done} of ${input.entries.length}`
14401
+ };
14402
+ }
14403
+
14404
+ // src/core/build/tick.ts
14405
+ var UNAVAILABLE_RETRY_MS = 2e3;
14406
+ var UNAVAILABLE_RETRY_FOR_MS = 3e4;
14407
+ var NEVER = (/* @__PURE__ */ new Date(0)).toISOString();
14408
+ var notStarted = () => ({
14409
+ action: "complete",
14410
+ message: BUILD_STRINGS.notStarted
14411
+ });
14412
+ function pushBranch(worktree, branch) {
14413
+ git(worktree, ["push", "--quiet", "origin", `${branch}:${branch}`]);
14414
+ }
14415
+ async function startChild(deps, hubEpoch, start) {
14416
+ for (let waited = 0; ; waited += UNAVAILABLE_RETRY_MS) {
14417
+ try {
14418
+ await deps.hub.submit({
14419
+ commandId: randomUUID2(),
14420
+ hubEpoch,
14421
+ runId: start.runId,
14422
+ intent: start.intent
14423
+ });
14424
+ return { kind: "started" };
14425
+ } catch (error) {
14426
+ if (!(error instanceof ClientError) || error.status >= 500)
14427
+ return { kind: "unreachable" };
14428
+ if (error.code === "key_busy") return { kind: "key_busy" };
14429
+ if (error.code === "delegation_budget_exhausted") return { kind: "budget" };
14430
+ if (error.code !== "workflow_unavailable")
14431
+ return { kind: "refused", code: error.code };
14432
+ if (waited >= UNAVAILABLE_RETRY_FOR_MS) return { kind: "unavailable" };
14433
+ await deps.sleep(UNAVAILABLE_RETRY_MS);
14434
+ }
14435
+ }
14436
+ }
14437
+ function currentPlan(plan, state) {
14438
+ const { needsPerson: _previous, ...approved } = plan;
14439
+ const entries = plan.groups.flatMap((group) => group.issues);
14440
+ const needsPerson = entries.filter((entry) => state.needsPerson[entry.id]).map((entry) => ({ id: entry.id, reason: state.needsPerson[entry.id] }));
14441
+ return needsPerson.length > 0 ? { ...approved, needsPerson } : approved;
14442
+ }
14443
+ async function runBuildTick(deps) {
14444
+ const orchestratorRunId = requireEnv(deps.env, "DRISP_RUN_ID");
14445
+ const runnerId = requireEnv(deps.env, "DRISP_RUNNER_ID");
14446
+ const artifacts = requireEnv(deps.env, "DRISP_RUN_ARTIFACTS");
14447
+ if (!deps.inputPath)
14448
+ throw new BuildUsageError("Usage: drisp build tick --input <path>");
14449
+ let input;
14450
+ try {
14451
+ input = readInput(path7.resolve(deps.folder, deps.inputPath));
14452
+ } catch (error) {
14453
+ if (error instanceof PlanFailure) throw new BuildUsageError(error.message);
14454
+ throw error;
14455
+ }
14456
+ const worktree = ensureBuildWorktree({
14457
+ folder: deps.folder,
14458
+ slugId: input.project.slugId,
14459
+ worktreesRoot: deps.worktreesRoot
14460
+ });
14461
+ const designDir = path7.join(
14462
+ worktree.path,
14463
+ ".athena",
14464
+ "workflows",
14465
+ input.project.slugId
14466
+ );
14467
+ const relativeDesignDir = path7.relative(worktree.path, designDir);
14468
+ const statePath = path7.join(designDir, "build-state.json");
14469
+ const planPath = path7.join(designDir, "plan.json");
14470
+ const parsedPlan = buildPlanSchema.safeParse(readJson(planPath));
14471
+ if (!parsedPlan.success || parsedPlan.data.orchestratorRunId !== orchestratorRunId)
14472
+ return notStarted();
14473
+ const plan = parsedPlan.data;
14474
+ const push = deps.push ?? pushBranch;
14475
+ let stored;
14476
+ const parsedState = buildStateSchema.safeParse(readJson(statePath));
14477
+ if (parsedState.success && parsedState.data.orchestratorRunId === orchestratorRunId) {
14478
+ stored = parsedState.data;
14479
+ } else {
14480
+ if (!isStartAnswer(readJson(path7.join(artifacts, "answer.json"))))
14481
+ return notStarted();
14482
+ stored = {
14483
+ version: 1,
14484
+ orchestratorRunId,
14485
+ projectId: plan.projectId,
14486
+ slugId: input.project.slugId,
14487
+ templateVersion: plan.templateVersion,
14488
+ approvedAt: deps.now().toISOString(),
14489
+ approved: {
14490
+ groups: plan.groups.map((group) => group.issues.map((issue) => issue.id))
14491
+ },
14492
+ attempts: {},
14493
+ needsPerson: {},
14494
+ tickCount: 0,
14495
+ lastTickAt: NEVER
14496
+ };
14497
+ push(worktree.path, worktree.branch);
14498
+ }
14499
+ if (!(Date.parse(input.readAt) > Date.parse(stored.lastTickAt)))
14500
+ return { action: "refresh_input" };
14501
+ stored.tickCount += 1;
14502
+ stored.lastTickAt = deps.now().toISOString();
14503
+ const parent = await deps.hub.run(orchestratorRunId);
14504
+ const children = await deps.hub.runs({ parent: orchestratorRunId });
14505
+ const approved = new Set(stored.approved.groups.flat());
14506
+ const entries = plan.groups.flatMap((group) => group.issues).filter((entry) => approved.has(entry.id));
14507
+ const issues = new Map(input.issues.map((issue) => [issue.id, issue]));
14508
+ const assessment = assessTick({
14509
+ orchestratorRunId,
14510
+ entries,
14511
+ issues: input.issues,
14512
+ state: stored,
14513
+ children: children.runs,
14514
+ templateVersion,
14515
+ limits: buildLimits(ORCHESTRATOR_LIMITS, parent.delegation),
14516
+ pullRequests: (branch) => deps.pullRequests(deps.folder, branch),
14517
+ locate: (entry) => locate(deps, entry, issues.get(entry.id)?.branchName),
14518
+ rebaseFailed: deps.rebaseFailed ?? [],
14519
+ now: deps.now().toISOString()
14520
+ });
14521
+ const { state, status } = assessment;
14522
+ let parkReason = assessment.parkReason;
14523
+ const record = () => {
14524
+ writeFile(statePath, json(state));
14525
+ const files = [path7.join(relativeDesignDir, "build-state.json")];
14526
+ const emitted = json(currentPlan(plan, state));
14527
+ if (emitted !== fs7.readFileSync(planPath, "utf-8")) {
14528
+ writeFile(planPath, emitted);
14529
+ writeFile(path7.join(artifacts, "build_plan.json"), emitted);
14530
+ files.push(path7.join(relativeDesignDir, "plan.json"));
14531
+ }
14532
+ if (commitBuildFiles(
14533
+ worktree.path,
14534
+ files,
14535
+ `drisp build: tick ${state.tickCount} for ${state.slugId}`
14536
+ ))
14537
+ push(worktree.path, worktree.branch);
14538
+ };
14539
+ if (assessment.repair.length > 0) {
14540
+ record();
14541
+ return { action: "repair", issues: assessment.repair };
14542
+ }
14543
+ const deferred = [];
14544
+ const behind = [];
14545
+ let started = 0;
14546
+ if (assessment.queue.length > 0 && assessment.slots > 0) {
14547
+ for (const entry of assessment.queue) {
14548
+ if (started >= assessment.slots) break;
14549
+ const issue = issues.get(entry.id);
14550
+ if (!issue) continue;
14551
+ const child = ensureChildWorktree({
14552
+ folder: deps.folder,
14553
+ worktreesRoot: deps.worktreesRoot,
14554
+ issue
14555
+ });
14556
+ if (child.status === "needs_person") {
14557
+ state.needsPerson[entry.id] = child.reason;
14558
+ status.set(entry.id, "needs_person");
14559
+ continue;
14560
+ }
14561
+ if (child.existed && !hasUncommittedWork(child.path) && behindMain(deps.folder, child.path)) {
14562
+ behind.push({
14563
+ identifier: entry.identifier,
14564
+ worktree: child.path,
14565
+ branch: child.branch
14566
+ });
14567
+ continue;
14568
+ }
14569
+ const entryDir = path7.join(relativeDesignDir, entry.identifier);
14570
+ git(worktree.path, ["checkout", plan.commit, "--", entryDir]);
14571
+ installWorkflowFromSource(
14572
+ {
14573
+ kind: "filesystem",
14574
+ workflowPath: path7.join(worktree.path, entryDir, "workflow.json"),
14575
+ version: plan.commit
14576
+ },
14577
+ entry.workflowName
14578
+ );
14579
+ const attempt = (state.attempts[entry.id]?.length ?? 0) + 1;
14580
+ const start = childStartIntent(
14581
+ {
14582
+ orchestratorRunId,
14583
+ identifier: entry.identifier,
14584
+ issueId: entry.id,
14585
+ title: issue.title,
14586
+ url: issue.url,
14587
+ branch: child.branch,
14588
+ worktree: child.path,
14589
+ workflowName: entry.workflowName,
14590
+ tokenCap: entry.tokenCap,
14591
+ attempt
14592
+ },
14593
+ { runnerId, scope: parent.scope }
14594
+ );
14595
+ const result = await startChild(deps, children.hubEpoch, start);
14596
+ if (result.kind === "started") {
14597
+ (state.attempts[entry.id] ??= []).push({
14598
+ n: attempt,
14599
+ runId: start.runId,
14600
+ tokenCap: entry.tokenCap,
14601
+ startedAt: deps.now().toISOString(),
14602
+ branch: child.branch,
14603
+ worktree: child.path
14604
+ });
14605
+ status.set(entry.id, "live");
14606
+ started += 1;
14607
+ } else if (result.kind === "key_busy")
14608
+ deferred.push({ entry, reason: BUILD_STRINGS.keyBusy(entry.identifier) });
14609
+ else if (result.kind === "unavailable")
14610
+ deferred.push({
14611
+ entry,
14612
+ reason: BUILD_STRINGS.workflowUnavailable(entry.identifier)
14613
+ });
14614
+ else if (result.kind === "unreachable")
14615
+ deferred.push({
14616
+ entry,
14617
+ reason: BUILD_STRINGS.hubUnreachable(entry.identifier)
14618
+ });
14619
+ else if (result.kind === "budget") {
14620
+ parkReason = "budget";
14621
+ break;
14622
+ } else {
14623
+ state.needsPerson[entry.id] = BUILD_STRINGS.startRefused(result.code);
14624
+ status.set(entry.id, "needs_person");
14625
+ }
14626
+ }
14627
+ }
14628
+ const anyLive = started > 0 || children.runs.some((view) => !view.closed) || behind.length > 0;
14629
+ if (!anyLive)
14630
+ for (const { entry, reason } of deferred) {
14631
+ state.needsPerson[entry.id] = reason;
14632
+ status.set(entry.id, "needs_person");
14633
+ }
14634
+ const views = new Map(children.runs.map((view) => [view.runId, view]));
14635
+ for (const entry of entries) {
14636
+ const newest = state.attempts[entry.id]?.at(-1);
14637
+ if (!newest || status.get(entry.id) !== "done") continue;
14638
+ if (!views.get(newest.runId)?.closed) continue;
14639
+ const installed = fs7.existsSync(
14640
+ path7.join(workflowRegistryDir(), entry.workflowName)
14641
+ );
14642
+ if (!installed && !fs7.existsSync(newest.worktree)) continue;
14643
+ if (!deps.pullRequests(deps.folder, newest.branch).merged) continue;
14644
+ if (fs7.existsSync(newest.worktree)) {
14645
+ try {
14646
+ git(deps.folder, ["worktree", "remove", newest.worktree]);
14647
+ } catch {
14648
+ }
14649
+ }
14650
+ if (installed) removeWorkflow(entry.workflowName);
14651
+ }
14652
+ record();
14653
+ if (behind.length > 0) return { action: "rebase", items: behind };
14654
+ return tickAction({
14655
+ entries,
14656
+ status,
14657
+ children: children.runs,
14658
+ started,
14659
+ ...parkReason ? { parkReason } : {}
14660
+ });
14661
+ }
14662
+ function locate(deps, entry, branchName) {
14663
+ const worktree = childWorktreePath(
14664
+ deps.worktreesRoot,
14665
+ deps.folder,
14666
+ entry.identifier
14667
+ );
14668
+ if (fs7.existsSync(path7.join(worktree, ".git")))
14669
+ return {
14670
+ worktree,
14671
+ branch: git(worktree, ["rev-parse", "--abbrev-ref", "HEAD"])
14672
+ };
14673
+ return {
14674
+ worktree,
14675
+ branch: branchName ?? fallbackBranch(entry.identifier, entry.title)
14676
+ };
14677
+ }
14678
+
14679
+ // src/core/build/mergeGate.ts
14680
+ var PASSED = /* @__PURE__ */ new Set([
14681
+ "success",
14682
+ "skipped",
14683
+ "neutral"
14684
+ ]);
14685
+ function zeroStepFailure(run) {
14686
+ const failedJobs = run.jobs.filter((job) => !PASSED.has(job.conclusion));
14687
+ return failedJobs.length > 0 && failedJobs.every((job) => Array.isArray(job.steps) && job.steps.length === 0);
14688
+ }
14689
+ function evaluateRemoteChecks(runs) {
14690
+ const verdict = {
14691
+ ok: true,
14692
+ failed: [],
14693
+ ignored: [],
14694
+ pending: []
14695
+ };
14696
+ for (const run of runs) {
14697
+ if (run.status !== "completed") verdict.pending.push(run.name);
14698
+ else if (PASSED.has(run.conclusion)) continue;
14699
+ else if (zeroStepFailure(run)) verdict.ignored.push(run.name);
14700
+ else verdict.failed.push(run.name);
14701
+ }
14702
+ verdict.ok = verdict.failed.length === 0 && verdict.pending.length === 0;
14703
+ return verdict;
14704
+ }
14705
+
14706
+ // src/app/entry/buildCommand.ts
14707
+ var USAGE3 = `Usage: drisp build plan --input <path>
14708
+ drisp build tick --input <path> [--rebase-failed <IDENTIFIER>]...
14709
+ drisp build checks
14710
+ plan and tick run inside an Orchestrator Run (DRISP_RUN_ID, DRISP_RUNNER_ID and DRISP_RUN_ARTIFACTS set; tick also
14711
+ DRISP_RUN_TOKEN and DRISP_HUB_URL). DRISP_BUILD_PLAN_INPUT=<path> stands in for plan's --input, with
14712
+ fills/<IDENTIFIER>.json beside it. checks runs in a child's worktree: the merge gate's verdict on HEAD's GitHub runs.`;
14713
+ function githubPullRequests(folder, branch) {
14714
+ const output = execFileSync(
14715
+ "gh",
14716
+ [
14717
+ "pr",
14718
+ "list",
14719
+ "--head",
14720
+ branch,
14721
+ "--state",
14722
+ "all",
14723
+ "--json",
14724
+ "state,url",
14725
+ "--limit",
14726
+ "50"
14727
+ ],
14728
+ { cwd: folder, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
14729
+ );
14730
+ return parsePullRequests(output);
14731
+ }
14732
+ function parsePullRequests(output) {
14733
+ const prs = JSON.parse(output);
14734
+ const merged = prs.find((pr) => pr.state === "MERGED");
14735
+ return {
14736
+ open: prs.some((pr) => pr.state === "OPEN"),
14737
+ merged: merged !== void 0,
14738
+ ...merged ? { mergedUrl: merged.url } : {}
14739
+ };
14740
+ }
14741
+ function hostCapabilities(folder) {
14742
+ const effective = resolveEffectiveCapabilities({
14743
+ globalConfig: readGlobalConfig(),
14744
+ projectConfig: readConfig(folder)
14745
+ });
14746
+ return {
14747
+ skills: effective.skills.map((skill) => skill.name),
14748
+ mcpServers: effective.mcpServers.map((server) => server.name)
14749
+ };
14750
+ }
14751
+ var defaultChecks = {
14752
+ head: (cwd) => git(cwd, ["rev-parse", "HEAD"]),
14753
+ gh: (args, cwd) => execFileSync("gh", args, {
14754
+ cwd,
14755
+ encoding: "utf8",
14756
+ stdio: ["ignore", "pipe", "pipe"]
14757
+ })
14758
+ };
14759
+ function runBuildChecks(cwd, deps) {
14760
+ const head = deps.head(cwd);
14761
+ const runs = JSON.parse(
14762
+ deps.gh(
14763
+ [
14764
+ "run",
14765
+ "list",
14766
+ "--commit",
14767
+ head,
14768
+ "--json",
14769
+ "databaseId,name,status,conclusion",
14770
+ "--limit",
14771
+ "100"
14772
+ ],
14773
+ cwd
14774
+ )
14775
+ );
14776
+ const withJobs = runs.map((run) => ({
14777
+ name: run.name,
14778
+ status: run.status,
14779
+ conclusion: run.conclusion,
14780
+ jobs: run.status === "completed" && !PASSED.has(run.conclusion) ? JSON.parse(
14781
+ deps.gh(
14782
+ ["run", "view", String(run.databaseId), "--json", "jobs"],
14783
+ cwd
14784
+ )
14785
+ ).jobs : []
14786
+ }));
14787
+ return evaluateRemoteChecks(withJobs);
14788
+ }
14789
+ async function runBuildCommand(input) {
14790
+ const stdout = input.stdout ?? process.stdout;
14791
+ const stderr = input.stderr ?? process.stderr;
14792
+ const env2 = input.env ?? process.env;
14793
+ try {
14794
+ let output;
14795
+ switch (input.subcommand) {
14796
+ case "plan":
14797
+ output = runBuildPlan({
14798
+ env: env2,
14799
+ folder: input.cwd,
14800
+ ...input.flags.input ? { inputPath: input.flags.input } : {},
14801
+ worktreesRoot: defaultWorktreesRoot(),
14802
+ hostCapabilities: () => hostCapabilities(input.cwd),
14803
+ pullRequests: githubPullRequests,
14804
+ ...input.dependencies
14805
+ });
14806
+ break;
14807
+ case "tick": {
14808
+ requireEnv(env2, "DRISP_RUN_ID");
14809
+ requireEnv(env2, "DRISP_RUNNER_ID");
14810
+ requireEnv(env2, "DRISP_RUN_ARTIFACTS");
14811
+ const token = requireEnv(env2, "DRISP_RUN_TOKEN");
14812
+ const hubUrl = requireEnv(env2, "DRISP_HUB_URL");
14813
+ output = await runBuildTick({
14814
+ env: env2,
14815
+ folder: input.cwd,
14816
+ ...input.flags.input ? { inputPath: input.flags.input } : {},
14817
+ ...input.flags.rebaseFailed?.length ? { rebaseFailed: input.flags.rebaseFailed } : {},
14818
+ worktreesRoot: defaultWorktreesRoot(),
14819
+ hub: DrispClient.delegated(hubUrl, token),
14820
+ pullRequests: githubPullRequests,
14821
+ now: () => /* @__PURE__ */ new Date(),
14822
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
14823
+ ...input.tick
14824
+ });
14825
+ break;
14826
+ }
14827
+ case "checks":
14828
+ output = runBuildChecks(input.cwd, input.checks ?? defaultChecks);
14829
+ break;
14830
+ default:
14831
+ stderr.write(`${USAGE3}
14832
+ `);
14833
+ return 2;
14834
+ }
14835
+ stdout.write(`${JSON.stringify(output)}
14836
+ `);
14837
+ return 0;
14838
+ } catch (error) {
14839
+ if (!(error instanceof BuildUsageError)) throw error;
14840
+ stderr.write(`${error.message}
14841
+ ${USAGE3}
14842
+ `);
14843
+ return 2;
14844
+ }
14845
+ }
14846
+
13516
14847
  // src/app/entry/doctorCommand.ts
13517
14848
  import process7 from "process";
13518
14849
 
13519
14850
  // src/harnesses/claude/system/doctorEnvironment.ts
13520
- import { execFileSync } from "child_process";
13521
- import * as fs5 from "fs";
14851
+ import { execFileSync as execFileSync2 } from "child_process";
14852
+ import * as fs8 from "fs";
13522
14853
  import * as os4 from "os";
13523
- import * as path5 from "path";
14854
+ import * as path8 from "path";
13524
14855
  var MANAGED_POLICY_KEYS = [
13525
14856
  "forceLoginMethod",
13526
14857
  "forceLoginOrgUUID",
@@ -13595,7 +14926,7 @@ function readEnforcement(scopes) {
13595
14926
  }
13596
14927
  function tryRichAuth(claudeBinary) {
13597
14928
  try {
13598
- const output = execFileSync(claudeBinary, ["auth", "status"], {
14929
+ const output = execFileSync2(claudeBinary, ["auth", "status"], {
13599
14930
  timeout: 1e4,
13600
14931
  encoding: "utf-8",
13601
14932
  stdio: ["ignore", "pipe", "pipe"]
@@ -13624,8 +14955,8 @@ function collectEnvironment(options = {}) {
13624
14955
  const detectVersion = options.detectClaudeVersionFn ?? detectClaudeVersion;
13625
14956
  const runAuthStatus = options.runClaudeAuthStatusFn ?? runClaudeAuthStatus;
13626
14957
  const resolveForwarder = options.resolveHookForwarderCommandFn ?? resolveHookForwarderCommand;
13627
- const statFn = options.statFn ?? ((p) => fs5.statSync(p));
13628
- const readFileFn = options.readFileFn ?? ((p) => fs5.readFileSync(p, "utf8"));
14958
+ const statFn = options.statFn ?? ((p) => fs8.statSync(p));
14959
+ const readFileFn = options.readFileFn ?? ((p) => fs8.readFileSync(p, "utf8"));
13629
14960
  const settingsPaths = resolveClaudeSettingsSurfacePaths(
13630
14961
  homeDir,
13631
14962
  cwd,
@@ -13670,7 +15001,7 @@ function collectEnvironment(options = {}) {
13670
15001
  });
13671
15002
  let globalConfigPresent = false;
13672
15003
  try {
13673
- statFn(path5.join(homeDir, ".claude.json"));
15004
+ statFn(path8.join(homeDir, ".claude.json"));
13674
15005
  globalConfigPresent = true;
13675
15006
  } catch {
13676
15007
  globalConfigPresent = false;
@@ -14150,13 +15481,13 @@ function runRunsCommand(input) {
14150
15481
  // src/app/entry/cli.tsx
14151
15482
  import { jsx as jsx24 } from "react/jsx-runtime";
14152
15483
  function resolvePackageJsonPath(entryUrl) {
14153
- let currentDir = path6.dirname(fileURLToPath(entryUrl));
15484
+ let currentDir = path9.dirname(fileURLToPath(entryUrl));
14154
15485
  for (; ; ) {
14155
- const candidatePath = path6.join(currentDir, "package.json");
14156
- if (fs6.existsSync(candidatePath)) {
15486
+ const candidatePath = path9.join(currentDir, "package.json");
15487
+ if (fs9.existsSync(candidatePath)) {
14157
15488
  return candidatePath;
14158
15489
  }
14159
- const parentDir = path6.dirname(currentDir);
15490
+ const parentDir = path9.dirname(currentDir);
14160
15491
  if (parentDir === currentDir) {
14161
15492
  throw new Error("Could not locate package.json for CLI metadata.");
14162
15493
  }
@@ -14178,6 +15509,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
14178
15509
  "skill",
14179
15510
  "marketplace",
14180
15511
  "runner",
15512
+ "build",
14181
15513
  // Deprecated alias of `runner` (#188); removed in 0.7.0.
14182
15514
  "dashboard",
14183
15515
  "telemetry",
@@ -14323,7 +15655,10 @@ var cli = meow(
14323
15655
  skill <sub> Manage personal skills (install, remove, list)
14324
15656
  marketplace <sub> Manage marketplace sources (add, refresh, remove, list)
14325
15657
  runner [sub] Run the runner that connects this machine to a Team and executes its Runs
14326
- (no subcommand: foreground; connect [code] --hub <url> [--replace], disconnect [team], status, recover, help)
15658
+ (no subcommand: foreground; connect [code] --hub <url> [--replace], disconnect [team], status, restart, recover, help)
15659
+ build plan --input <path> Plan a Build inside an Orchestrator Run (prints one JSON object)
15660
+ build tick --input <path> Run one step of a started Build (prints the next action; [--rebase-failed <ID>]...)
15661
+ build checks The merge gate's verdict on HEAD's GitHub runs (in a child's worktree)
14327
15662
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
14328
15663
  doctor Diagnose Claude headless setup (use with --harness=claude)
14329
15664
 
@@ -14354,7 +15689,7 @@ var cli = meow(
14354
15689
  --dry-run Print resolved bootstrap (workflow, isolation, plugins, harness) and exit (run mode)
14355
15690
  --project Scope workflow command to project config (workflow use)
14356
15691
  --global Scope workflow command to global config (workflow use, default)
14357
- --help Show command help
15692
+ --help, -h Show command help (runner shows its own)
14358
15693
  --version Show CLI version
14359
15694
 
14360
15695
  Note: All isolation modes use --setting-sources "" to completely isolate
@@ -14389,6 +15724,12 @@ var cli = meow(
14389
15724
  importMeta: import.meta,
14390
15725
  allowUnknownFlags: false,
14391
15726
  flags: {
15727
+ // Declared so -h works too; meow alone handles only a bare --help (DRI-193).
15728
+ help: {
15729
+ type: "boolean",
15730
+ shortFlag: "h",
15731
+ default: false
15732
+ },
14392
15733
  projectDir: {
14393
15734
  type: "string",
14394
15735
  default: process.cwd()
@@ -14448,6 +15789,13 @@ var cli = meow(
14448
15789
  hub: {
14449
15790
  type: "string"
14450
15791
  },
15792
+ input: {
15793
+ type: "string"
15794
+ },
15795
+ rebaseFailed: {
15796
+ type: "string",
15797
+ isMultiple: true
15798
+ },
14451
15799
  foreground: {
14452
15800
  type: "boolean",
14453
15801
  default: false
@@ -14487,7 +15835,7 @@ var cli = meow(
14487
15835
  }
14488
15836
  );
14489
15837
  async function main() {
14490
- const projectDir = path6.resolve(cli.flags.projectDir);
15838
+ const projectDir = path9.resolve(cli.flags.projectDir);
14491
15839
  const [command, ...commandArgs] = cli.input;
14492
15840
  const isRunCommand = command === "run" || command === "exec";
14493
15841
  if (command && !KNOWN_COMMANDS.has(command)) {
@@ -14498,6 +15846,10 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14498
15846
  await exitWith(1);
14499
15847
  return;
14500
15848
  }
15849
+ if (cli.flags.help && command !== "runner" && command !== "dashboard") {
15850
+ cli.showHelp(0);
15851
+ return;
15852
+ }
14501
15853
  if (command === "exec") {
14502
15854
  console.error(
14503
15855
  "drisp exec is deprecated and is removed in 0.7.0; use drisp run instead."
@@ -14537,7 +15889,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14537
15889
  await exitWith(1);
14538
15890
  return;
14539
15891
  }
14540
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-TQ4SF5KB.js");
15892
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-NZFBSBLY.js");
14541
15893
  const { waitUntilExit } = render(
14542
15894
  /* @__PURE__ */ jsx24(
14543
15895
  WorkflowInstallWizard,
@@ -14608,12 +15960,27 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14608
15960
  ...cli.flags.detach ? { detach: true } : {},
14609
15961
  ...typeof cli.flags.hub === "string" ? { hub: cli.flags.hub } : {},
14610
15962
  ...cli.flags.foreground ? { foreground: true } : {},
14611
- ...cli.flags.replace ? { replace: true } : {}
15963
+ ...cli.flags.replace ? { replace: true } : {},
15964
+ ...cli.flags.help ? { help: true } : {}
14612
15965
  }
14613
15966
  })
14614
15967
  );
14615
15968
  return;
14616
15969
  }
15970
+ if (command === "build") {
15971
+ const [subcommand = ""] = commandArgs;
15972
+ await exitWith(
15973
+ await runBuildCommand({
15974
+ subcommand,
15975
+ flags: {
15976
+ ...typeof cli.flags.input === "string" ? { input: cli.flags.input } : {},
15977
+ ...cli.flags.rebaseFailed?.length ? { rebaseFailed: cli.flags.rebaseFailed } : {}
15978
+ },
15979
+ cwd: process.cwd()
15980
+ })
15981
+ );
15982
+ return;
15983
+ }
14617
15984
  if (command === "runs") {
14618
15985
  await exitWith(
14619
15986
  runRunsCommand({
@@ -14687,8 +16054,8 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
14687
16054
  const showSetup = isRunCommand ? false : shouldShowSetup({
14688
16055
  cliInput: cli.input,
14689
16056
  setupComplete: globalConfig.setupComplete,
14690
- globalConfigExists: fs6.existsSync(
14691
- path6.join(os5.homedir(), ".config", "athena", "config.json")
16057
+ globalConfigExists: fs9.existsSync(
16058
+ path9.join(os5.homedir(), ".config", "athena", "config.json")
14692
16059
  )
14693
16060
  });
14694
16061
  let runtimeConfig;