@drisp/cli 0.7.7 → 0.7.8

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.
@@ -8,7 +8,7 @@ import {
8
8
  installWorkflowPlugins,
9
9
  resolveWorkflow,
10
10
  writeGlobalConfig
11
- } from "./chunk-HZ6GRLOM.js";
11
+ } from "./chunk-OYUBT6RS.js";
12
12
  import "./chunk-G3PMV62Z.js";
13
13
 
14
14
  // src/setup/steps/WorkflowInstallWizard.tsx
@@ -93,4 +93,4 @@ function WorkflowInstallWizard({ source, onDone }) {
93
93
  export {
94
94
  WorkflowInstallWizard as default
95
95
  };
96
- //# sourceMappingURL=WorkflowInstallWizard-VVEEJW25.js.map
96
+ //# sourceMappingURL=WorkflowInstallWizard-IOJD3YOA.js.map
@@ -21,6 +21,7 @@ import {
21
21
  LAYOUT_MARKERS,
22
22
  ORCHESTRATOR_LOOP,
23
23
  WAITING_ON_CHILDREN_STOP_REASON,
24
+ budgetTokens,
24
25
  compileWorkflowPlan,
25
26
  createSteerQueue,
26
27
  createWorkflowRunner,
@@ -54,7 +55,7 @@ import {
54
55
  verifyRendered,
55
56
  wakesFreshAfterHandover,
56
57
  workflowRegistryDir
57
- } from "./chunk-HZ6GRLOM.js";
58
+ } from "./chunk-OYUBT6RS.js";
58
59
  import {
59
60
  __commonJS,
60
61
  __toESM
@@ -12688,7 +12689,7 @@ var RUN_EXIT_CODE = {
12688
12689
  import { existsSync as existsSync4 } from "fs";
12689
12690
  import { link, mkdir as mkdir6, readdir as readdir3, readFile as readFile5, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
12690
12691
  import { homedir as homedir5 } from "os";
12691
- import { dirname as dirname5, join as join10 } from "path";
12692
+ import { dirname as dirname5, join as join11 } from "path";
12692
12693
 
12693
12694
  // ../../modules/runtime/src/runner-service.ts
12694
12695
  import { execFile } from "child_process";
@@ -12880,7 +12881,7 @@ WantedBy=default.target
12880
12881
  // ../../modules/runtime/src/service.ts
12881
12882
  import { mkdir as mkdir5, readFile as readFile4, unlink as unlink2 } from "fs/promises";
12882
12883
  import { homedir as homedir4 } from "os";
12883
- import { join as join9, resolve as resolve6 } from "path";
12884
+ import { join as join10, resolve as resolve6 } from "path";
12884
12885
 
12885
12886
  // ../../packages/protocol/src/domain.ts
12886
12887
  var RunStatusSchema = external_exports.enum([
@@ -14892,7 +14893,7 @@ async function restoreCheckpoint(store, input) {
14892
14893
 
14893
14894
  // ../../modules/runtime/src/software-version.ts
14894
14895
  import "fs";
14895
- var runnerSoftwareVersion = true ? "0.7.7" : JSON.parse(readFileSync2(new URL("../../../apps/cli/package.json", import.meta.url), "utf8")).version;
14896
+ var runnerSoftwareVersion = true ? "0.7.8" : JSON.parse(readFileSync2(new URL("../../../apps/cli/package.json", import.meta.url), "utf8")).version;
14896
14897
 
14897
14898
  // ../../modules/runtime/src/connection.ts
14898
14899
  var RunnerDisconnectedError = class extends Error {
@@ -20149,8 +20150,8 @@ function sessionStore(store, runId, options = {}) {
20149
20150
 
20150
20151
  // ../../modules/runtime/src/workflow-executor.ts
20151
20152
  import { execFileSync as execFileSync9 } from "child_process";
20152
- import { statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
20153
- import { join as join8 } from "path";
20153
+ import { statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
20154
+ import { join as join9 } from "path";
20154
20155
 
20155
20156
  // src/core/build/workspace.ts
20156
20157
  import { execFileSync as execFileSync5 } from "child_process";
@@ -20204,6 +20205,11 @@ var fillSchema = external_exports.strictObject({
20204
20205
  )
20205
20206
  ).min(1).optional(),
20206
20207
  sizeClass: external_exports.enum(["S", "M", "L"]),
20208
+ /**
20209
+ * Who merges the pull request (root ADR-0009): `agent`, the default when absent, or `you`, which makes the Merge
20210
+ * step a Sign-off the member answers in the child's Thread. Filled from the member's words before Start.
20211
+ */
20212
+ merge: external_exports.enum(["agent", "you"]).optional(),
20207
20213
  instructions: external_exports.string().min(1).max(6e3).refine((text) => !TEMPLATE_STRUCTURE.test(text), {
20208
20214
  error: INSTRUCTIONS_MESSAGE
20209
20215
  }),
@@ -20293,6 +20299,8 @@ var issueEntrySchema = external_exports.strictObject({
20293
20299
  mcpServers: external_exports.array(external_exports.string()),
20294
20300
  instructionsSummary: external_exports.string(),
20295
20301
  gates: external_exports.array(external_exports.string()),
20302
+ /** Who merges, from the fill; absent means agents merge (root ADR-0009). */
20303
+ merge: external_exports.enum(["agent", "you"]).optional(),
20296
20304
  workflowName: external_exports.string().max(128)
20297
20305
  });
20298
20306
  var buildPlanSchema = external_exports.strictObject({
@@ -20504,6 +20512,7 @@ function scopeFingerprint(issue) {
20504
20512
  }
20505
20513
 
20506
20514
  // src/core/build/planner.ts
20515
+ var MIXED_MERGE = "The fills disagree on merge. Write the same value in every fill: you when the member asked to merge themselves, else agent.";
20507
20516
  function skipReason(issue, pullRequests) {
20508
20517
  const prs = issue.branchName ? pullRequests[issue.branchName] : void 0;
20509
20518
  if (issue.labels.includes("ready-for-human")) return "ready-for-human";
@@ -20563,7 +20572,8 @@ function shownFromFill(fill) {
20563
20572
  skills: renderedSkills(fill),
20564
20573
  mcpServers: renderedMcpServers(fill),
20565
20574
  instructionsSummary: summarizeInstructions(fill.instructions),
20566
- gates: gateTitles(fill)
20575
+ gates: gateTitles(fill),
20576
+ ...fill.merge ? { merge: fill.merge } : {}
20567
20577
  };
20568
20578
  }
20569
20579
  function blockingGraph(entries) {
@@ -20629,6 +20639,16 @@ function planBuild(context) {
20629
20639
  });
20630
20640
  }
20631
20641
  if (missing.length > 0) return { status: "needs_fills", fills: missing };
20642
+ if (new Set([...fills.values()].map((fill) => fill.merge ?? "agent")).size > 1)
20643
+ return {
20644
+ status: "needs_fills",
20645
+ fills: entries.map(({ identifier, title, url }) => ({
20646
+ identifier,
20647
+ title,
20648
+ url
20649
+ })),
20650
+ reason: MIXED_MERGE
20651
+ };
20632
20652
  const hasSubIssues = new Set(input.issues.map((issue) => issue.parentId));
20633
20653
  const parentOnly = new Set(
20634
20654
  entries.filter(
@@ -20942,7 +20962,8 @@ function validateChildRun(grant, store, paths) {
20942
20962
  if (!verified.ok) throw mismatch(`its ${verified.file} does not re-render from its fill`);
20943
20963
  const shown = approval.plan.entries?.[name];
20944
20964
  const derived = shownFromFill(fill.data);
20945
- if (!shown || !Object.entries(derived).every(([key, value]) => isDeepStrictEqual2(shown[key], value)))
20965
+ const sameMerge = (shown?.merge ?? "agent") === (fill.data.merge ?? "agent");
20966
+ if (!shown || !sameMerge || !Object.entries(derived).every(([key, value]) => isDeepStrictEqual2(shown[key], value)))
20946
20967
  throw mismatch("the plan the member approved showed something else for it");
20947
20968
  return { permissions: renderedPermissions(fill.data) };
20948
20969
  }
@@ -21337,6 +21358,50 @@ function runArtifactEmitter(directory, record, midTurnMinGapMs = MID_TURN_MIN_GA
21337
21358
  };
21338
21359
  }
21339
21360
 
21361
+ // ../../modules/runtime/src/run-usage.ts
21362
+ import { readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync4 } from "fs";
21363
+ import { join as join8 } from "path";
21364
+ var isMark = (value) => {
21365
+ const mark = value;
21366
+ return !!mark && Number.isInteger(mark.index) && typeof mark.title === "string" && typeof mark.tokens === "number" && Number.isFinite(mark.tokens) && mark.tokens >= 0;
21367
+ };
21368
+ var read2 = (path23) => {
21369
+ try {
21370
+ return JSON.parse(readFileSync5(path23, "utf8"));
21371
+ } catch {
21372
+ return void 0;
21373
+ }
21374
+ };
21375
+ function stepUsageRecorder(directory, counted) {
21376
+ const path23 = join8(directory, "usage.json");
21377
+ const stored = read2(path23);
21378
+ const marks = Array.isArray(stored) ? stored.filter(isMark) : [];
21379
+ return ({ closing = false } = {}) => {
21380
+ const step = read2(join8(directory, "step.json"));
21381
+ if (!step || !Number.isInteger(step.index) || typeof step.title !== "string") return;
21382
+ const figure = counted();
21383
+ if (figure === null) return;
21384
+ const last = marks.at(-1);
21385
+ const tokens = Math.max(figure, last?.tokens ?? 0);
21386
+ const sameStep = last?.index === step.index && last?.title === step.title;
21387
+ if (sameStep && !(closing && tokens > last.tokens)) return;
21388
+ marks.push({ index: step.index, title: step.title, tokens });
21389
+ let json = JSON.stringify(marks);
21390
+ if (Buffer.byteLength(json) > ARTIFACT_MAX_BYTES) {
21391
+ for (let i = marks.length - 2; i > 0; i--) {
21392
+ if (marks[i].index === marks[i - 1].index && marks[i].title === marks[i - 1].title) marks.splice(i, 1);
21393
+ }
21394
+ json = JSON.stringify(marks);
21395
+ if (Buffer.byteLength(json) > ARTIFACT_MAX_BYTES) {
21396
+ marks.pop();
21397
+ return;
21398
+ }
21399
+ }
21400
+ writeFileSync4(`${path23}.tmp`, json);
21401
+ renameSync(`${path23}.tmp`, path23);
21402
+ };
21403
+ }
21404
+
21340
21405
  // ../../modules/runtime/src/workflow-executor.ts
21341
21406
  var isBuildCloseReason = (code) => buildCloseReasonSchema.safeParse(code).success;
21342
21407
  var continuationSchema = external_exports.discriminatedUnion("kind", [
@@ -21458,7 +21523,7 @@ function createWorkflowExecutor(dependencies = {}) {
21458
21523
  dependencies.dataDirectory ?? runnerDataDirectory(store.sql.database.name),
21459
21524
  grant.runId
21460
21525
  );
21461
- const emitArtifacts2 = runArtifactEmitter(artifacts, record, (dependencies.artifactIntervalMs ?? 2e3) * 2.5);
21526
+ const emitArtifacts = runArtifactEmitter(artifacts, record, (dependencies.artifactIntervalMs ?? 2e3) * 2.5);
21462
21527
  const loop = bootstrapped.workflow?.loop;
21463
21528
  const childLoop = grant.parent && loop ? { ...loop, maxRunTokens: Math.min(loop.maxRunTokens ?? Infinity, grant.parent.tokenCap) } : loop;
21464
21529
  const config2 = {
@@ -21491,6 +21556,17 @@ function createWorkflowExecutor(dependencies = {}) {
21491
21556
  }
21492
21557
  } : {}
21493
21558
  });
21559
+ const recordUsage = stepUsageRecorder(artifacts, () => {
21560
+ const memory = deserializeRunMemory(view2.getLatestRun()?.runMemoryJson);
21561
+ return memory ? budgetTokens(memory) : null;
21562
+ });
21563
+ const sweepArtifacts2 = (midTurn = false, closing = false) => {
21564
+ try {
21565
+ recordUsage({ closing });
21566
+ } catch {
21567
+ }
21568
+ emitArtifacts(midTurn);
21569
+ };
21494
21570
  const previous = view2.getLatestRun();
21495
21571
  const predecessor = grant.previousRun ? store.sessionValues(grant.sessionId, "workflow").find((snapshot) => snapshot.id === grant.previousRun.runId) : void 0;
21496
21572
  const adapterResumeSessionId2 = grant.purpose === "resume" ? previous?.adapterSessionId : predecessor?.adapterSessionId;
@@ -21498,12 +21574,12 @@ function createWorkflowExecutor(dependencies = {}) {
21498
21574
  if (continuation?.kind === "answer") {
21499
21575
  const pending2 = pendingApproval(store, grant.runId);
21500
21576
  if (pending2) {
21501
- writeFileSync4(join8(artifacts, "answer.json"), JSON.stringify(continuation.answer));
21577
+ writeFileSync5(join9(artifacts, "answer.json"), JSON.stringify(continuation.answer));
21502
21578
  record(APPROVAL_ANSWERED, { answer: continuation.answer, plan: pending2.plan });
21503
21579
  }
21504
21580
  }
21505
21581
  const guidedPlan = continuation?.kind === "resume" && continuation.guidance?.trim() && pendingApproval(store, grant.runId) ? continuation.guidance : void 0;
21506
- if (guidedPlan !== void 0) writeFileSync4(join8(artifacts, "answer.json"), JSON.stringify(guidedPlan));
21582
+ if (guidedPlan !== void 0) writeFileSync5(join9(artifacts, "answer.json"), JSON.stringify(guidedPlan));
21507
21583
  let storedAnswer2;
21508
21584
  if (continuation?.kind === "answer") {
21509
21585
  const answer = permissionAnswerSchema.safeParse(continuation.answer);
@@ -21534,7 +21610,7 @@ ${JSON.stringify(continuation.answer)}` : grant.spec.prompt;
21534
21610
  Current human request:
21535
21611
  ${prompt2}`;
21536
21612
  }
21537
- return { workingDirectory: workingDirectory2, config: config2, view: view2, adapterResumeSessionId: adapterResumeSessionId2, storedAnswer: storedAnswer2, prompt: prompt2, displayText: displayText2, emitArtifacts: emitArtifacts2 };
21613
+ return { workingDirectory: workingDirectory2, config: config2, view: view2, adapterResumeSessionId: adapterResumeSessionId2, storedAnswer: storedAnswer2, prompt: prompt2, displayText: displayText2, sweepArtifacts: sweepArtifacts2 };
21538
21614
  };
21539
21615
  let setup2;
21540
21616
  try {
@@ -21546,12 +21622,12 @@ ${prompt2}`;
21546
21622
  if (error instanceof RunSetupError && isBuildCloseReason(error.code)) return { status: "failed", reason: error.code };
21547
21623
  return { status: "failed" };
21548
21624
  }
21549
- const { workingDirectory, config, view, adapterResumeSessionId, storedAnswer, prompt, displayText, emitArtifacts } = setup2;
21625
+ const { workingDirectory, config, view, adapterResumeSessionId, storedAnswer, prompt, displayText, sweepArtifacts } = setup2;
21550
21626
  const queue = createSteerQueue();
21551
21627
  queues.set(grant.runId, queue);
21552
21628
  const sweep = setInterval(() => {
21553
21629
  try {
21554
- emitArtifacts(true);
21630
+ sweepArtifacts(true);
21555
21631
  } catch {
21556
21632
  }
21557
21633
  }, dependencies.artifactIntervalMs ?? 2e3);
@@ -21587,14 +21663,14 @@ ${prompt2}`;
21587
21663
  }
21588
21664
  });
21589
21665
  },
21590
- onTurnComplete: emitArtifacts,
21666
+ onTurnComplete: () => sweepArtifacts(false, true),
21591
21667
  stdout: { write: () => true },
21592
21668
  stderr: { write: (text) => process.stderr.write(text) },
21593
21669
  json: false
21594
21670
  })).finally(() => {
21595
21671
  queues.delete(grant.runId);
21596
21672
  clearInterval(sweep);
21597
- emitArtifacts();
21673
+ sweepArtifacts(false, true);
21598
21674
  });
21599
21675
  if (!result.workflowOutcome)
21600
21676
  throw new Error(result.failure?.message ?? "Workflow returned no authoritative outcome");
@@ -21730,8 +21806,8 @@ var runnerConfigSchema = external_exports.object({
21730
21806
  /** The Runner's name in the Team, as Connect recorded it (hostname by default). */
21731
21807
  name: external_exports.string().optional()
21732
21808
  });
21733
- var defaultConfigPath = () => process.env.DRISP_RUNNER_CONFIG ?? join9(homedir4(), ".config", "drisp", "runner-v2.json");
21734
- var expandHome = (value) => resolve6(value.startsWith("~/") ? join9(homedir4(), value.slice(2)) : value);
21809
+ var defaultConfigPath = () => process.env.DRISP_RUNNER_CONFIG ?? join10(homedir4(), ".config", "drisp", "runner-v2.json");
21810
+ var expandHome = (value) => resolve6(value.startsWith("~/") ? join10(homedir4(), value.slice(2)) : value);
21735
21811
  async function readRunnerConfig(path23 = defaultConfigPath()) {
21736
21812
  const config = runnerConfigSchema.parse(JSON.parse(await readFile4(expandHome(path23), "utf8")));
21737
21813
  return { ...config, dataDirectory: expandHome(config.dataDirectory) };
@@ -21740,7 +21816,7 @@ async function startConfiguredRunner(path23 = defaultConfigPath()) {
21740
21816
  const config = await readRunnerConfig(path23);
21741
21817
  await mkdir5(config.dataDirectory, { recursive: true, mode: 448 });
21742
21818
  const store = new ExecutionStore(
21743
- join9(config.dataDirectory, "execution.sqlite"),
21819
+ join10(config.dataDirectory, "execution.sqlite"),
21744
21820
  {
21745
21821
  orgId: config.orgId,
21746
21822
  runnerId: config.runnerId,
@@ -21829,17 +21905,17 @@ async function runConfiguredRunners(paths, abort, { whenAllDisconnected } = {})
21829
21905
  }
21830
21906
 
21831
21907
  // ../../modules/runtime/src/identities.ts
21832
- var runnerConfigDirectory = () => join10(homedir5(), ".config", "drisp");
21833
- var teamIdentityPath = (hub, orgId, directory = runnerConfigDirectory()) => join10(directory, "runners", encodeURIComponent(hub.host), `${encodeURIComponent(orgId)}.json`);
21908
+ var runnerConfigDirectory = () => join11(homedir5(), ".config", "drisp");
21909
+ var teamIdentityPath = (hub, orgId, directory = runnerConfigDirectory()) => join11(directory, "runners", encodeURIComponent(hub.host), `${encodeURIComponent(orgId)}.json`);
21834
21910
  function identityHubOrigin({ endpoint }) {
21835
21911
  const url = new URL(endpoint);
21836
21912
  url.protocol = url.protocol === "wss:" ? "https:" : "http:";
21837
21913
  return url.origin;
21838
21914
  }
21839
- var legacyIdentityPath = (directory) => join10(directory, "runner-v2.json");
21915
+ var legacyIdentityPath = (directory) => join11(directory, "runner-v2.json");
21840
21916
  var overriddenIdentityPath = () => process.env.DRISP_RUNNER_CONFIG;
21841
21917
  async function machineInstallationId(directory = runnerConfigDirectory()) {
21842
- const path23 = join10(directory, "installation-id");
21918
+ const path23 = join11(directory, "installation-id");
21843
21919
  const existing = await readFile5(path23, "utf8").then((text) => text.trim(), () => "");
21844
21920
  if (existing) return existing;
21845
21921
  let installationId = crypto.randomUUID();
@@ -21859,13 +21935,13 @@ async function machineInstallationId(directory = runnerConfigDirectory()) {
21859
21935
  }
21860
21936
  async function runnerIdentities(directory = runnerConfigDirectory()) {
21861
21937
  const override = overriddenIdentityPath();
21862
- const runners = join10(directory, "runners");
21863
- const identityFiles = async (folder) => (await readdir3(folder).catch(() => [])).filter((name) => name.endsWith(".json")).sort().map((name) => join10(folder, name));
21938
+ const runners = join11(directory, "runners");
21939
+ const identityFiles = async (folder) => (await readdir3(folder).catch(() => [])).filter((name) => name.endsWith(".json")).sort().map((name) => join11(folder, name));
21864
21940
  const hubs = (await readdir3(runners, { withFileTypes: true }).catch(() => [])).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
21865
21941
  const paths = override ? [expandHome(override)] : [
21866
21942
  legacyIdentityPath(directory),
21867
21943
  ...await identityFiles(runners),
21868
- ...(await Promise.all(hubs.map((hub) => identityFiles(join10(runners, hub))))).flat()
21944
+ ...(await Promise.all(hubs.map((hub) => identityFiles(join11(runners, hub))))).flat()
21869
21945
  ];
21870
21946
  const present = paths.filter((path23) => existsSync4(path23));
21871
21947
  return Promise.all(present.map(async (path23) => ({ path: path23, config: await readRunnerConfig(path23) })));
@@ -22045,4 +22121,4 @@ export {
22045
22121
  runMachineRunners,
22046
22122
  writeIdentityOnce
22047
22123
  };
22048
- //# sourceMappingURL=chunk-D246FR4I.js.map
22124
+ //# sourceMappingURL=chunk-HGMJ3YL2.js.map
@@ -21227,6 +21227,11 @@ var BUILD_STRINGS = {
21227
21227
  templatePark: "Drisp was updated during this Build. Build again to re-plan.",
21228
21228
  notStarted: "Not started",
21229
21229
  waiting: "Waiting for child Runs.",
21230
+ /**
21231
+ * The Merge step's Sign-off when the member merges (`merge: you`, root ADR-0009): the `NEEDS_HUMAN` text a child
21232
+ * parks with, `{sha}` being the full head SHA it asks about. The app reads it as "Blocked · Your merge".
21233
+ */
21234
+ mergeSignOff: "Your merge: this pull request is ready to merge at {sha}. Reply in this Thread to sign off on that commit, or say what to change.",
21230
21235
  notAGitHubRepository: "The project folder is not a git repository with a GitHub origin.",
21231
21236
  cycle: (identifiers) => `These issues block each other in Linear: ${identifiers.join(", ")}.`,
21232
21237
  /** The comment the Orchestrator posts with its one Linear write, moving a merged issue to Done. */
@@ -21255,7 +21260,7 @@ var GATES = [
21255
21260
  },
21256
21261
  {
21257
21262
  title: "Checks",
21258
- passesWhen: "{install}{checks} pass",
21263
+ passesWhen: "{install}{checks} pass, each check run through `drisp build check <name> -- <command>` (for example `drisp build check typecheck -- make typecheck`), which records its outcome for the Build page",
21259
21264
  cannotPass: "This issue's checks still fail after repeated fixes. Open the Thread to see the failing output."
21260
21265
  },
21261
21266
  {
@@ -21281,7 +21286,7 @@ var GATES = [
21281
21286
  ];
21282
21287
 
21283
21288
  // src/core/workflows/builtins/orchestrator/workflow.md
21284
- var workflow_default = '# Orchestrator: plan and run a Build\n\nYou plan and run a Drisp Build of one Plan: the open issues of one Linear project. The Run\'s prompt names the\nLinear project, the Plan key and the project folder, which is your working directory. You never write code here:\neach issue is built by its own child Run, which `drisp build tick` starts.\n\nThe Runner sets these for you:\n\n- `DRISP_RUN_ARTIFACTS`: this Run\'s artifact directory. Files you write there are shown to the member.\n- `DRISP_RUN_ID`, `DRISP_RUNNER_ID`, `DRISP_RUN_TOKEN`, `DRISP_HUB_URL`: this Run, its Runner and its right to start\n child Runs; `drisp build` reads them itself.\n\n## 1. Read Linear (first Turn)\n\nWith the `linear` MCP server, read the project and every issue in it, then write\n`$DRISP_RUN_ARTIFACTS/input/plan-input.json` (create the `input` directory) in exactly this shape. Every issue carries\nevery field, empty when it has none. `readAt` is the output of\n`node -e "console.log(new Date().toISOString())"` run just before you read Linear; never write a time yourself:\n\n```json\n{\n "version": 1,\n "readAt": "<the command\'s output, e.g. 2026-10-11T09:30:00.000Z>",\n "project": {\n "id": "<the project\'s `uuid`, not its `id`>",\n "name": "<the project\'s `name`>",\n "slugId": "<the last `-`-separated part of the project\'s `url`>"\n },\n "issues": [\n {\n "id": "<the issue\'s `uuid`, not its `id`>",\n "identifier": "<the issue\'s `id`, e.g. DRI-12>",\n "title": "<the issue\'s `title`>",\n "description": "<full current description>",\n "acceptanceCriteria": "<acceptance criteria, empty when included in description>",\n "external": false,\n "url": "<the issue\'s `url`>",\n "branchName": "<the issue\'s `gitBranchName`, or null>",\n "state": {\n "type": "<the issue\'s `statusType`: backlog|unstarted|started|completed|canceled|triage>",\n "name": "<the issue\'s `status`>"\n },\n "labels": ["<each of the issue\'s `labels`>"],\n "parentId": "<the parent issue\'s `uuid`, or null>",\n "blockedBy": ["<the `uuid` of each issue in `relations.blockedBy`>"],\n "prioritySortOrder": 0\n }\n ]\n}\n```\n\nLinear\'s MCP names fields differently from this file, and the difference matters: the app finds each issue\'s row by\nits UUID, so a plan that carries identifiers shows no Build state on any row.\n\n- The MCP\'s `id` is the readable identifier (`DRI-12`, or `P-DRI-7` for a project); the UUID is in `uuid`. Every\n `id`, `parentId` and `blockedBy` value in `plan-input.json` is a UUID (`7f7b03f7-accf-46b2-98fb-abadfed76594`).\n- An issue\'s `parentId` and `relations.blockedBy` from the MCP name issues by identifier. Look each one up: use the\n `uuid` of the project\'s issue with that identifier, or `get_issue` it when it is outside the project.\n- Read relations with `get_issue` and `includeRelations: true`; read the project\'s issues with `list_issues` and\n `fields` including `uuid`.\n- The project\'s `url` ends in its `slugId` (`\u2026/project/build-sandbox-b7640301cd64` \u2192 `b7640301cd64`).\n\n`drisp build plan` rejects a plan with an identifier where a UUID belongs and names the field to fix.\n\nAlso include every referenced blocker outside the project, with `external: true`, its current status and UUID. Follow blocker relations recursively; deduplicate by UUID. Never mark a project issue external. External issues are read only and never scheduled. Preserve actual prioritySortOrder when available.\n\nOnly ready-for-agent issues without conflicting ready-for-human, needs-info, needs-triage or wontfix labels are eligible. Do not invent or change labels to make an issue eligible.\n\nInclude every issue of the project, done and canceled ones too: the plan decides what to skip. Then run:\n\n```sh\ndrisp build plan --input "$DRISP_RUN_ARTIFACTS/input/plan-input.json"\n```\n\nIt prints one JSON object. Act on its `status`.\n\n## 2. `needs_fills`\n\n`fills` lists issues that need a `fill.json` (`identifier`, `title`, `url`, `path`) and `templateVersion` is the value\neach fill must carry. For each listed issue: read it through `linear` (description, acceptance criteria, comments) and\nthe repo\'s ADRs and CONTEXT files it touches, then write its `fill.json` at `path`:\n\n```json\n{\n "version": 1,\n "templateVersion": "<templateVersion, copied exactly>",\n "areas": ["macos|web|cli|hub|protocol|docs", "\u2026"],\n "qa": "none|web|macos|both",\n "profile": "implementation|documentation|investigation",\n "rationale": "<why these steps are appropriate>",\n "acceptanceEvidence": [\n "<each acceptance criterion and the evidence that demonstrates it>"\n ],\n "sizeClass": "S|M|L",\n "instructions": "<what the child needs beyond the issue>",\n "ownWork": true\n}\n```\n\n- `areas`: the parts of the repo the change touches, each once.\n- `profile`: implementation for behavior changes, documentation for prose-only repository changes, investigation for findings with no repository changes. Implementation defaults to TDD and two-reviewer code review; documentation and investigation omit these by default. Choose the profile from the actual deliverable, not the issue size.\n- Optional `testFirst` and `review`: `{ "enabled": true|false, "reason": "<issue-specific justification>" }`. Retain tests and review for behavioral changes; omit only when not applicable and explain why. These selections also control required skills.\n- Optional `checks`: a nonempty list of actual validation commands from the repo, using the supported npm, vitest, make or git diff command families. Read repository tooling and add the relevant test/build checks: typechecking alone does not prove behavior. If a necessary command is unsupported, state that limitation in instructions and require human attention, never claim validation passed.\n- `acceptanceEvidence`: cover every criterion, including the method and expected outcome.\n- `qa`: `both` when both interfaces change, `web` when the change shows in a web page, `macos` when it shows in the macOS app, else `none`.\n- `sizeClass`: `S` for a change of a file or two, `M` for a feature across a few modules, `L` for a large or risky\n change.\n- `instructions` (at most 6000 characters): start with one short paragraph saying what to build, then restate the\n acceptance criteria, name the ADRs it touches and the parts of the codebase involved. Plain prose and `-` lists\n only: no headings, code fences or HTML comments (`drisp build plan` rejects them).\n- `ownWork`: `false` only for an issue with sub-issues whose own description asks for nothing beyond them, such as\n a PRD above its slices; the plan then skips it and waits for its sub-issues instead. Otherwise `true`.\n\nThen run `drisp build plan` again with the same `--input` path.\n\n## 3. `planned` or `failed`\n\nOn `planned` the Build plan is written and shown to the member, who decides whether to start it: end the Turn with\n`<!-- AWAITING_APPROVAL -->` as the Journal\'s last line. On `failed` the reason is shown instead: end with\n`<!-- WORKFLOW_COMPLETE -->`.\n\n## 4. Every later wake: tick\n\nYou are woken when the member answers the approval question (with Start, or with words of their own before it) and whenever one of your child Runs closes or parks.\nEach time: read Linear again into `$DRISP_RUN_ARTIFACTS/input/plan-input.json` as in step 1 (with a new `readAt`),\nincluding full descriptions, acceptance criteria, labels and external blocker status. A scope change holds the affected issue for a fresh Build and approval. Then run:\n\n```sh\ndrisp build tick --input "$DRISP_RUN_ARTIFACTS/input/plan-input.json"\n```\n\nIt prints one JSON object. Act on its `action`, and repeat until an action ends the Turn:\n\n- `refresh_input`: read Linear into the input again (a newer `readAt`) and tick again.\n- `replan`: the member wrote words before starting, and `guidance` lists every round of them, oldest first. Plan again\n with them as the member\'s wishes: read the issues they concern (and the repo) as in step 2, rewrite the `fill.json`\n of each issue the guidance changes (`instructions`, `areas`, `qa`, `sizeClass`, or `ownWork` to leave an issue out),\n and run `drisp build plan` again with the same `--input` path. Then end as in step 3: with\n `<!-- AWAITING_APPROVAL -->` on `planned`, `<!-- WORKFLOW_COMPLETE -->` on `failed`. Write nothing but those\n `fill.json` files.\n- `repair`: for each of `issues`, read it in Linear first and skip an issue Linear now shows Done or Canceled (its\n child got there first); otherwise move it to Done and post its `comment` verbatim through `linear`. Then read\n Linear into the input again and tick again.\n- `rebase`: for each of `items`, in its `worktree`: `git rebase origin/main`, resolve the conflicts, then\n `git push --force-with-lease origin <branch>` (the item\'s `branch`) if the branch is on the remote. Never push any\n other branch. If a rebase cannot be resolved, `git rebase --abort`.\n Then read Linear into the input again and tick again, adding `--rebase-failed <identifier>` for each that failed.\n- `wait`: end the Turn with `<!-- WAITING_ON_CHILDREN -->` as the Journal\'s last line.\n- `park`: end the Turn with `<!-- NEEDS_HUMAN: <message> -->` as the Journal\'s last line, copying `message`\n verbatim.\n- `complete`: end the Turn with `<!-- WORKFLOW_COMPLETE -->` as the Journal\'s last line.\n\n## Rules\n\n- Never start, cancel or edit Runs, Workflows, worktrees or branches except as an action says; `drisp build` does\n what is needed.\n- Never write to Linear except a `repair`; never open a pull request.\n- Write only `plan-input.json` and the `fill.json` files `drisp build plan` asks for. Never write `answer.json`.\n';
21289
+ var workflow_default = '# Orchestrator: plan and run a Build\n\nYou plan and run a Drisp Build of one Plan: the open issues of one Linear project. The Run\'s prompt names the\nLinear project, the Plan key and the project folder, which is your working directory. You never write code here:\neach issue is built by its own child Run, which `drisp build tick` starts.\n\nThe Runner sets these for you:\n\n- `DRISP_RUN_ARTIFACTS`: this Run\'s artifact directory. Files you write there are shown to the member.\n- `DRISP_RUN_ID`, `DRISP_RUNNER_ID`, `DRISP_RUN_TOKEN`, `DRISP_HUB_URL`: this Run, its Runner and its right to start\n child Runs; `drisp build` reads them itself.\n\n## 1. Read Linear (first Turn)\n\nWith the `linear` MCP server, read the project and every issue in it, then write\n`$DRISP_RUN_ARTIFACTS/input/plan-input.json` (create the `input` directory) in exactly this shape. Every issue carries\nevery field, empty when it has none. `readAt` is the output of\n`node -e "console.log(new Date().toISOString())"` run just before you read Linear; never write a time yourself:\n\n```json\n{\n "version": 1,\n "readAt": "<the command\'s output, e.g. 2026-10-11T09:30:00.000Z>",\n "project": {\n "id": "<the project\'s `uuid`, not its `id`>",\n "name": "<the project\'s `name`>",\n "slugId": "<the last `-`-separated part of the project\'s `url`>"\n },\n "issues": [\n {\n "id": "<the issue\'s `uuid`, not its `id`>",\n "identifier": "<the issue\'s `id`, e.g. DRI-12>",\n "title": "<the issue\'s `title`>",\n "description": "<full current description>",\n "acceptanceCriteria": "<acceptance criteria, empty when included in description>",\n "external": false,\n "url": "<the issue\'s `url`>",\n "branchName": "<the issue\'s `gitBranchName`, or null>",\n "state": {\n "type": "<the issue\'s `statusType`: backlog|unstarted|started|completed|canceled|triage>",\n "name": "<the issue\'s `status`>"\n },\n "labels": ["<each of the issue\'s `labels`>"],\n "parentId": "<the parent issue\'s `uuid`, or null>",\n "blockedBy": ["<the `uuid` of each issue in `relations.blockedBy`>"],\n "prioritySortOrder": 0\n }\n ]\n}\n```\n\nLinear\'s MCP names fields differently from this file, and the difference matters: the app finds each issue\'s row by\nits UUID, so a plan that carries identifiers shows no Build state on any row.\n\n- The MCP\'s `id` is the readable identifier (`DRI-12`, or `P-DRI-7` for a project); the UUID is in `uuid`. Every\n `id`, `parentId` and `blockedBy` value in `plan-input.json` is a UUID (`7f7b03f7-accf-46b2-98fb-abadfed76594`).\n- An issue\'s `parentId` and `relations.blockedBy` from the MCP name issues by identifier. Look each one up: use the\n `uuid` of the project\'s issue with that identifier, or `get_issue` it when it is outside the project.\n- Read relations with `get_issue` and `includeRelations: true`; read the project\'s issues with `list_issues` and\n `fields` including `uuid`.\n- The project\'s `url` ends in its `slugId` (`\u2026/project/build-sandbox-b7640301cd64` \u2192 `b7640301cd64`).\n\n`drisp build plan` rejects a plan with an identifier where a UUID belongs and names the field to fix.\n\nAlso include every referenced blocker outside the project, with `external: true`, its current status and UUID. Follow blocker relations recursively; deduplicate by UUID. Never mark a project issue external. External issues are read only and never scheduled. Preserve actual prioritySortOrder when available.\n\nOnly ready-for-agent issues without conflicting ready-for-human, needs-info, needs-triage or wontfix labels are eligible. Do not invent or change labels to make an issue eligible.\n\nInclude every issue of the project, done and canceled ones too: the plan decides what to skip. Then run:\n\n```sh\ndrisp build plan --input "$DRISP_RUN_ARTIFACTS/input/plan-input.json"\n```\n\nIt prints one JSON object. Act on its `status`.\n\n## 2. `needs_fills`\n\n`fills` lists issues that need a `fill.json` (`identifier`, `title`, `url`, `path`) and `templateVersion` is the value\neach fill must carry. A `reason`, when present, says what to correct in the listed fills (fills that disagree on\n`merge`, say); fix that and keep the rest of each fill. For each listed issue: read it through `linear` (description,\nacceptance criteria, comments) and the repo\'s ADRs and CONTEXT files it touches, then write its `fill.json` at `path`:\n\n```json\n{\n "version": 1,\n "templateVersion": "<templateVersion, copied exactly>",\n "areas": ["macos|web|cli|hub|protocol|docs", "\u2026"],\n "qa": "none|web|macos|both",\n "profile": "implementation|documentation|investigation",\n "rationale": "<why these steps are appropriate>",\n "acceptanceEvidence": [\n "<each acceptance criterion and the evidence that demonstrates it>"\n ],\n "sizeClass": "S|M|L",\n "merge": "agent|you",\n "instructions": "<what the child needs beyond the issue>",\n "ownWork": true\n}\n```\n\n- `areas`: the parts of the repo the change touches, each once.\n- `profile`: implementation for behavior changes, documentation for prose-only repository changes, investigation for findings with no repository changes. Implementation defaults to TDD and two-reviewer code review; documentation and investigation omit these by default. Choose the profile from the actual deliverable, not the issue size.\n- Optional `testFirst` and `review`: `{ "enabled": true|false, "reason": "<issue-specific justification>" }`. Retain tests and review for behavioral changes; omit only when not applicable and explain why. These selections also control required skills.\n- Optional `checks`: a nonempty list of actual validation commands from the repo, using the supported npm, vitest, make or git diff command families. Read repository tooling and add the relevant test/build checks: typechecking alone does not prove behavior. If a necessary command is unsupported, state that limitation in instructions and require human attention, never claim validation passed.\n- `acceptanceEvidence`: cover every criterion, including the method and expected outcome.\n- `qa`: `both` when both interfaces change, `web` when the change shows in a web page, `macos` when it shows in the macOS app, else `none`.\n- `sizeClass`: `S` for a change of a file or two, `M` for a feature across a few modules, `L` for a large or risky\n change.\n- `merge`: who merges each pull request. `agent` (the default) unless the member\'s own words in this chat, in the\n Run\'s prompt or a guidance reply, ask to merge themselves ("ask me before merging", "I\'ll merge"): then `you`, and\n the child\'s Merge step waits for the member\'s Sign-off in its Thread. It is one choice for the whole Build: write the\n same value in every fill. It is fixed once the member presses Start: if they ask to change it after Start, tell\n them to Cancel the Build and Build again.\n- `instructions` (at most 6000 characters): start with one short paragraph saying what to build, then restate the\n acceptance criteria, name the ADRs it touches and the parts of the codebase involved. Plain prose and `-` lists\n only: no headings, code fences or HTML comments (`drisp build plan` rejects them).\n- `ownWork`: `false` only for an issue with sub-issues whose own description asks for nothing beyond them, such as\n a PRD above its slices; the plan then skips it and waits for its sub-issues instead. Otherwise `true`.\n\nThen run `drisp build plan` again with the same `--input` path.\n\n## 3. `planned` or `failed`\n\nOn `planned` the Build plan is written and shown to the member, who decides whether to start it: end the Turn with\n`<!-- AWAITING_APPROVAL -->` as the Journal\'s last line. On `failed` the reason is shown instead: end with\n`<!-- WORKFLOW_COMPLETE -->`.\n\n## 4. Every later wake: tick\n\nYou are woken when the member answers the approval question (with Start, or with words of their own before it) and whenever one of your child Runs closes or parks.\nEach time: read Linear again into `$DRISP_RUN_ARTIFACTS/input/plan-input.json` as in step 1 (with a new `readAt`),\nincluding full descriptions, acceptance criteria, labels and external blocker status. A scope change holds the affected issue for a fresh Build and approval. Then run:\n\n```sh\ndrisp build tick --input "$DRISP_RUN_ARTIFACTS/input/plan-input.json"\n```\n\nIt prints one JSON object. Act on its `action`, and repeat until an action ends the Turn:\n\n- `refresh_input`: read Linear into the input again (a newer `readAt`) and tick again.\n- `replan`: the member wrote words before starting, and `guidance` lists every round of them, oldest first. Plan again\n with them as the member\'s wishes: read the issues they concern (and the repo) as in step 2, rewrite the `fill.json`\n of each issue the guidance changes (`instructions`, `areas`, `qa`, `sizeClass`, `merge` in every fill when the\n member says who merges, or `ownWork` to leave an issue out),\n and run `drisp build plan` again with the same `--input` path. Then end as in step 3: with\n `<!-- AWAITING_APPROVAL -->` on `planned`, `<!-- WORKFLOW_COMPLETE -->` on `failed`. Write nothing but those\n `fill.json` files.\n- `repair`: for each of `issues`, read it in Linear first and skip an issue Linear now shows Done or Canceled (its\n child got there first); otherwise move it to Done and post its `comment` verbatim through `linear`. Then read\n Linear into the input again and tick again.\n- `rebase`: for each of `items`, in its `worktree`: `git rebase origin/main`, resolve the conflicts, then\n `git push --force-with-lease origin <branch>` (the item\'s `branch`) if the branch is on the remote. Never push any\n other branch. If a rebase cannot be resolved, `git rebase --abort`.\n Then read Linear into the input again and tick again, adding `--rebase-failed <identifier>` for each that failed.\n- `wait`: end the Turn with `<!-- WAITING_ON_CHILDREN -->` as the Journal\'s last line.\n- `park`: end the Turn with `<!-- NEEDS_HUMAN: <message> -->` as the Journal\'s last line, copying `message`\n verbatim.\n- `complete`: end the Turn with `<!-- WORKFLOW_COMPLETE -->` as the Journal\'s last line.\n\n## Rules\n\n- Never start, cancel or edit Runs, Workflows, worktrees or branches except as an action says; `drisp build` does\n what is needed.\n- Never write to Linear except a `repair`; never open a pull request.\n- Write only `plan-input.json` and the `fill.json` files `drisp build plan` asks for. Never write `answer.json`.\n';
21285
21290
 
21286
21291
  // src/core/workflows/builtins/orchestrator/index.ts
21287
21292
  var ORCHESTRATOR_WORKFLOW_NAME = "orchestrator";
@@ -21472,7 +21477,7 @@ is in \`ignored\`: it does not block the merge and its name goes in the evidence
21472
21477
  empty, wait a minute and run it again. When \`failed\` is not empty, fix the change; if it still fails, the Merge gate
21473
21478
  cannot pass. Never judge a check by its timing.
21474
21479
 
21475
- Immediately before merging, verify the current revision again: {mergeVerify}. If evidence is stale, rerun the
21480
+ {signOff}Immediately before merging, verify the current revision again: {mergeVerify}. If evidence is stale, rerun the
21476
21481
  selected validation steps and record fresh receipts before continuing.
21477
21482
 
21478
21483
  Merge with \`gh pr merge --squash --delete-branch --match-head-commit <sha>\`, where \`<sha>\` is HEAD after the last
@@ -21503,6 +21508,18 @@ Evidence. Never create issues and never edit an issue's title or description.
21503
21508
 
21504
21509
  When every gate has passed, end with \`<!-- WORKFLOW_COMPLETE -->\` as the Journal's last line.
21505
21510
  `;
21511
+ var MERGE_SIGN_OFF = {
21512
+ passesWhen: "the merge gate below holds, with the member\u2019s Sign-off on HEAD",
21513
+ section: `Sign-off: this Build's member merges, so the merge waits for their Sign-off on the exact head commit. When
21514
+ everything above holds and HEAD is pushed, end the Turn with \`<!-- NEEDS_HUMAN: {signOffText} -->\` as the Journal's
21515
+ last line, where \`<sha>\` is the full \`git rev-parse HEAD\`. The member answers in this Thread. When their reply signs
21516
+ off on that commit, record it with \`{signOffCommand}\`, which refuses any SHA but HEAD; it is your attestation of
21517
+ their reply, so never record one they did not give. Any other reply is guidance: act on it, then ask again. A
21518
+ Sign-off names one commit: after a rebase or any new commit, ask again before merging.
21519
+ When \`gh pr merge\` fails because the branch is protected, the Merge gate cannot pass.
21520
+
21521
+ `
21522
+ };
21506
21523
  var STAGE_TEMPLATE = `### {n}. {title}
21507
21524
 
21508
21525
  Start: run \`{stepCommand}\`.
@@ -21584,6 +21601,7 @@ var RENDERER_RULES = {
21584
21601
  permissions: {
21585
21602
  edit: ["Edit(./**)"],
21586
21603
  shell: [
21604
+ "drisp build check",
21587
21605
  "drisp build checks",
21588
21606
  "drisp build step",
21589
21607
  "drisp build merged",
@@ -21721,11 +21739,11 @@ function selectedGates(fill) {
21721
21739
  ).map(
21722
21740
  (gate) => fill.profile === "investigation" && gate.title === "Checks" ? {
21723
21741
  ...gate,
21724
- passesWhen: "{install}{checks} pass; each finding is supported by a source or reproducible observation and each acceptance criterion has evidence; unresolved questions are stated explicitly"
21742
+ passesWhen: "{install}{checks} pass, each check run through `drisp build check <name> -- <command>`; each finding is supported by a source or reproducible observation and each acceptance criterion has evidence; unresolved questions are stated explicitly"
21725
21743
  } : fill.profile === "investigation" && gate.title === "Evidence" ? {
21726
21744
  ...gate,
21727
21745
  passesWhen: "findings and evidence for each acceptance criterion are posted on the Linear issue, with unresolved questions explicitly stated; the issue is moved to Done only when all acceptance criteria are satisfied"
21728
- } : gate
21746
+ } : fill.merge === "you" && gate.title === "Merge" ? { ...gate, passesWhen: MERGE_SIGN_OFF.passesWhen } : gate
21729
21747
  );
21730
21748
  if (!usesTestFirst(fill))
21731
21749
  gates.splice(1, 0, {
@@ -21753,6 +21771,7 @@ var fillSlots = (template, slots) => template.replace(/\{(\w+)\}/g, (whole, key)
21753
21771
  function selectionFlags(fill) {
21754
21772
  return `${fill.profile ? ` --profile ${fill.profile}` : ""}${fill.testFirst ? ` --test-first ${fill.testFirst.enabled}` : ""}${fill.review ? ` --review ${fill.review.enabled}` : ""}`;
21755
21773
  }
21774
+ var mergeFlag = (fill) => fill.merge === "you" ? " --merge you" : "";
21756
21775
  function renderStages(fill, checks, install) {
21757
21776
  const gates = selectedGates(fill);
21758
21777
  return gates.map((gate, index) => {
@@ -21768,11 +21787,12 @@ function renderStages(fill, checks, install) {
21768
21787
  stepCommand: command,
21769
21788
  passesWhen
21770
21789
  });
21790
+ const finish = `${command}${gate.title === "Merge" ? mergeFlag(fill) : ""}`;
21771
21791
  return stage + (["Merge", "Evidence"].includes(gate.title) ? `
21772
21792
 
21773
21793
  Before doing this step: run \`${command} --verify\`. Missing or stale evidence must be refreshed first.` : "") + `
21774
21794
 
21775
- Finish: write the actual result and supporting paths to \`.athena/evidence/step-${index + 1}.md\`, then run \`${command} --complete --evidence .athena/evidence/step-${index + 1}.md\`. This records an agent attestation, not independent verification.` + (gate.cannotPass ? fillSlots(CANNOT_PASS_TEMPLATE, { cannotPass: gate.cannotPass }) : NO_GATE_TEXT_TEMPLATE);
21795
+ Finish: write the actual result and supporting paths to \`.athena/evidence/step-${index + 1}.md\`, then run \`${finish} --complete --evidence .athena/evidence/step-${index + 1}.md\`. This records an agent attestation, not independent verification.` + (gate.cannotPass ? fillSlots(CANNOT_PASS_TEMPLATE, { cannotPass: gate.cannotPass }) : NO_GATE_TEXT_TEMPLATE);
21776
21796
  }).join("\n\n");
21777
21797
  }
21778
21798
  function render(fill, workflowName, layout) {
@@ -21818,6 +21838,7 @@ function render(fill, workflowName, layout) {
21818
21838
  "Your Linear writes are In Progress in Explore, findings and evidence, and Done in Evidence."
21819
21839
  );
21820
21840
  }
21841
+ const mergeCommand = `drisp build step ${gateTitles(fill).indexOf("Merge") + 1} --qa ${fill.qa}${selectionFlags(fill)}`;
21821
21842
  const workflowMd = fillSlots(template, {
21822
21843
  identifier: parsed.identifier,
21823
21844
  instructions: [
@@ -21830,7 +21851,11 @@ function render(fill, workflowName, layout) {
21830
21851
  )
21831
21852
  ].filter(Boolean).join("\n\n"),
21832
21853
  gates: renderStages(fill, checks, installCommand(fill, layout)),
21833
- mergeVerify: `\`drisp build step ${gateTitles(fill).indexOf("Merge") + 1} --qa ${fill.qa}${selectionFlags(fill)} --verify\``,
21854
+ signOff: fill.merge === "you" ? fillSlots(MERGE_SIGN_OFF.section, {
21855
+ signOffText: BUILD_STRINGS.mergeSignOff.replace("{sha}", "<sha>"),
21856
+ signOffCommand: `${mergeCommand} --merge you --sign-off <sha>`
21857
+ }) : "",
21858
+ mergeVerify: `\`${mergeCommand}${mergeFlag(fill)} --verify\``,
21834
21859
  checksEvidence: checks.map((command) => `${command} \u2713`).join(" \xB7 "),
21835
21860
  shellCommands: checksText(shellCommands(fill))
21836
21861
  });
@@ -24282,6 +24307,7 @@ function collectMcpServersWithOptions(pluginDirs) {
24282
24307
  export {
24283
24308
  WAITING_ON_CHILDREN_STOP_REASON,
24284
24309
  AWAITING_APPROVAL_STOP_REASON,
24310
+ budgetTokens,
24285
24311
  createSteerQueue,
24286
24312
  DEFAULT_PERMISSION_GRACE_MS,
24287
24313
  serializeRunMemory,
@@ -24321,6 +24347,7 @@ export {
24321
24347
  DESCRIPTION_TEMPLATE,
24322
24348
  QA_SECTIONS,
24323
24349
  WORKFLOW_MD_TEMPLATE,
24350
+ MERGE_SIGN_OFF,
24324
24351
  STAGE_TEMPLATE,
24325
24352
  CANNOT_PASS_TEMPLATE,
24326
24353
  NO_GATE_TEXT_TEMPLATE,
@@ -24347,4 +24374,4 @@ export {
24347
24374
  compileWorkflowPlan,
24348
24375
  collectMcpServersWithOptions
24349
24376
  };
24350
- //# sourceMappingURL=chunk-HZ6GRLOM.js.map
24377
+ //# sourceMappingURL=chunk-OYUBT6RS.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runMachineRunners
3
- } from "./chunk-D246FR4I.js";
3
+ } from "./chunk-HGMJ3YL2.js";
4
4
 
5
5
  // src/app/entry/runnerDaemon.ts
6
6
  if (process.argv.includes("--help")) {
@@ -13,4 +13,4 @@ if (process.argv.includes("--help")) {
13
13
  process.exitCode = 1;
14
14
  });
15
15
  }
16
- //# sourceMappingURL=chunk-D5VZT7M3.js.map
16
+ //# sourceMappingURL=chunk-TBUUNMC6.js.map
package/dist/cli.js CHANGED
@@ -142,7 +142,7 @@ import {
142
142
  visualPositionToOffset,
143
143
  wrapText,
144
144
  writeIdentityOnce
145
- } from "./chunk-D246FR4I.js";
145
+ } from "./chunk-HGMJ3YL2.js";
146
146
  import {
147
147
  generateId as generateId2
148
148
  } from "./chunk-PQIB66SG.js";
@@ -161,6 +161,7 @@ import {
161
161
  DESCRIPTION_TEMPLATE,
162
162
  GATES,
163
163
  LAYOUT_MARKERS,
164
+ MERGE_SIGN_OFF,
164
165
  NO_GATE_TEXT_TEMPLATE,
165
166
  ORCHESTRATOR_LIMITS,
166
167
  QA_SECTIONS,
@@ -200,7 +201,7 @@ import {
200
201
  workflowRegistryDir,
201
202
  writeGlobalConfig,
202
203
  writeProjectConfig
203
- } from "./chunk-HZ6GRLOM.js";
204
+ } from "./chunk-OYUBT6RS.js";
204
205
  import {
205
206
  __toESM
206
207
  } from "./chunk-G3PMV62Z.js";
@@ -208,10 +209,10 @@ import {
208
209
  // src/app/entry/cli.tsx
209
210
  import { render } from "ink";
210
211
  import meow from "meow";
211
- import fs11 from "fs";
212
+ import fs12 from "fs";
212
213
  import { createRequire } from "module";
213
214
  import os5 from "os";
214
- import path11 from "path";
215
+ import path12 from "path";
215
216
  import { fileURLToPath } from "url";
216
217
 
217
218
  // src/app/shell/sessionUiState.ts
@@ -5330,14 +5331,14 @@ function extractWebSearch(_input, response) {
5330
5331
  return { type: "text", content: extractTextContent(response), maxLines: 10 };
5331
5332
  }
5332
5333
  function extractNotebookEdit(input, _response) {
5333
- const path12 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5334
+ const path13 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5334
5335
  const mode = typeof input["edit_mode"] === "string" ? input["edit_mode"] : "replace";
5335
5336
  const source = typeof input["new_source"] === "string" ? input["new_source"] : "";
5336
- if (!source) return { type: "text", content: `${mode} cell in ${path12}` };
5337
+ if (!source) return { type: "text", content: `${mode} cell in ${path13}` };
5337
5338
  return {
5338
5339
  type: "code",
5339
5340
  content: source,
5340
- language: detectLanguage(path12),
5341
+ language: detectLanguage(path13),
5341
5342
  maxLines: 10
5342
5343
  };
5343
5344
  }
@@ -5863,10 +5864,10 @@ function extractToolSubject(toolName, toolInput) {
5863
5864
  const parts = [];
5864
5865
  const pattern = str(input, "pattern");
5865
5866
  const glob = str(input, "glob");
5866
- const path12 = str(input, "path");
5867
+ const path13 = str(input, "path");
5867
5868
  if (pattern) parts.push(`"${pattern}"`);
5868
5869
  if (glob) parts.push(glob);
5869
- if (path12) parts.push(`in ${path12}`);
5870
+ if (path13) parts.push(`in ${path13}`);
5870
5871
  return parts.length > 0 ? parts.join(" ") : void 0;
5871
5872
  }
5872
5873
  case "Skill":
@@ -13097,7 +13098,7 @@ var thisMachine = (installationId) => ({
13097
13098
  os: `${platform()} ${release()}`.slice(0, 64),
13098
13099
  softwareVersion: runnerSoftwareVersion
13099
13100
  });
13100
- var post = (origin, path12, body) => fetch(new URL(path12, origin), {
13101
+ var post = (origin, path13, body) => fetch(new URL(path13, origin), {
13101
13102
  method: "POST",
13102
13103
  headers: { "Content-Type": "application/json" },
13103
13104
  body: JSON.stringify(body)
@@ -13116,8 +13117,8 @@ async function alreadyConnected(existing, { forTeam, ownIdentity }) {
13116
13117
  const problem = own ? await credentialAccepted(own.config) ? void 0 : "lapsed" : forTeam(existing.orgId).length ? "other_runner" : "missing";
13117
13118
  const runner = { orgId: existing.orgId, runnerId: existing.runnerId, name: existing.name };
13118
13119
  if (own && problem === void 0) return { status: "already_connected", ...runner, path: own.path };
13119
- const path12 = (own ?? forTeam(existing.orgId)[0])?.path;
13120
- return { status: "identity_unusable", ...runner, problem, ...path12 ? { path: path12 } : {} };
13120
+ const path13 = (own ?? forTeam(existing.orgId)[0])?.path;
13121
+ return { status: "identity_unusable", ...runner, problem, ...path13 ? { path: path13 } : {} };
13121
13122
  }
13122
13123
  async function connectWithCode({
13123
13124
  code,
@@ -13189,20 +13190,20 @@ async function saveIdentity(origin, credential, { single, forTeam }, directory)
13189
13190
  name: credential.name
13190
13191
  });
13191
13192
  const replaced = [];
13192
- for (const { path: path13 } of forTeam(credential.orgId)) {
13193
- const aside = `${path13}.replaced-${Date.now()}`;
13194
- await rename(path13, aside);
13193
+ for (const { path: path14 } of forTeam(credential.orgId)) {
13194
+ const aside = `${path14}.replaced-${Date.now()}`;
13195
+ await rename(path14, aside);
13195
13196
  replaced.push(aside);
13196
13197
  }
13197
- const path12 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13198
+ const path13 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13198
13199
  try {
13199
- await writeIdentityOnce(path12, JSON.stringify(config, null, 2) + "\n");
13200
+ await writeIdentityOnce(path13, JSON.stringify(config, null, 2) + "\n");
13200
13201
  } catch (error) {
13201
13202
  if (error.code === "EEXIST")
13202
- throw new Error(`Another Connect wrote a Runner identity for this Team at ${path12} meanwhile; it was left unchanged.`);
13203
+ throw new Error(`Another Connect wrote a Runner identity for this Team at ${path13} meanwhile; it was left unchanged.`);
13203
13204
  throw error;
13204
13205
  }
13205
- return { status: "connected", config, path: path12, replaced };
13206
+ return { status: "connected", config, path: path13, replaced };
13206
13207
  }
13207
13208
  function pathSegment(value) {
13208
13209
  const segment = encodeURIComponent(value);
@@ -13242,7 +13243,7 @@ async function refuseExisting(target) {
13242
13243
  throw new Error(`This machine already has a Runner${name ? ` (${name})` : ""} configured at ${target}.`);
13243
13244
  }
13244
13245
  async function disconnectRunner({
13245
- identity: { path: path12, config },
13246
+ identity: { path: path13, config },
13246
13247
  serviceManager,
13247
13248
  othersRemain
13248
13249
  }) {
@@ -13275,7 +13276,7 @@ async function disconnectRunner({
13275
13276
  await restore();
13276
13277
  throw new Error(`The Hub refused to disconnect ${name} (${body.error ?? response.status}); it is still connected.`);
13277
13278
  }
13278
- await forgetRunner(path12);
13279
+ await forgetRunner(path13);
13279
13280
  if (othersRemain) await restore();
13280
13281
  else await serviceManager?.remove();
13281
13282
  return result;
@@ -13428,18 +13429,18 @@ async function runRunnerCommandV2(input, options = {}) {
13428
13429
  `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.`
13429
13430
  );
13430
13431
  }
13431
- const { config, path: path12, replaced } = outcome;
13432
+ const { config, path: path13, replaced } = outcome;
13432
13433
  for (const aside of replaced)
13433
13434
  process.stdout.write(
13434
13435
  `The Hub no longer recognised this machine's earlier identity for this Team; it was moved aside to ${aside}.
13435
13436
  `
13436
13437
  );
13437
13438
  process.stdout.write(
13438
- `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path12}.
13439
+ `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path13}.
13439
13440
  `
13440
13441
  );
13441
13442
  if (services) {
13442
- const service = await ensureService(services, config, path12);
13443
+ const service = await ensureService(services, config, path13);
13443
13444
  process.stdout.write(`${serviceStarted(service)}
13444
13445
  `);
13445
13446
  return 0;
@@ -13448,7 +13449,7 @@ async function runRunnerCommandV2(input, options = {}) {
13448
13449
  process.stdout.write(
13449
13450
  "No launchd or systemd here to run the Runner as a background service; running it in the foreground instead.\n"
13450
13451
  );
13451
- await runConfiguredRunner(path12, options.signal);
13452
+ await runConfiguredRunner(path13, options.signal);
13452
13453
  return 0;
13453
13454
  }
13454
13455
  case "disconnect": {
@@ -13519,9 +13520,9 @@ async function runRunnerCommandV2(input, options = {}) {
13519
13520
  configured: true,
13520
13521
  // One entry per Team this machine serves; credentials are never printed.
13521
13522
  teams: identities.map(
13522
- ({ path: path12, config: { token: _token, ...config } }) => ({
13523
+ ({ path: path13, config: { token: _token, ...config } }) => ({
13523
13524
  ...config,
13524
- path: path12
13525
+ path: path13
13525
13526
  })
13526
13527
  ),
13527
13528
  service: service ? {
@@ -13677,7 +13678,9 @@ function computeTemplateVersion() {
13677
13678
  JSON.stringify(QA_SECTIONS),
13678
13679
  JSON.stringify(GATES),
13679
13680
  JSON.stringify(RENDERER_RULES),
13680
- JSON.stringify(LAYOUT_MARKERS)
13681
+ JSON.stringify(LAYOUT_MARKERS),
13682
+ JSON.stringify(MERGE_SIGN_OFF),
13683
+ BUILD_STRINGS.mergeSignOff
13681
13684
  ];
13682
13685
  const hash = createHash("sha256");
13683
13686
  for (const part of parts) hash.update(`${part.length}:${part}`);
@@ -13782,7 +13785,8 @@ function runBuildPlan(deps) {
13782
13785
  fills: result.fills.map((fill) => ({
13783
13786
  ...fill,
13784
13787
  path: fillPath(fill.identifier)
13785
- }))
13788
+ })),
13789
+ ...result.reason ? { reason: result.reason } : {}
13786
13790
  };
13787
13791
  const host = deps.hostCapabilities();
13788
13792
  for (const workflow of result.workflows) {
@@ -13866,9 +13870,9 @@ var DrispClient = class _DrispClient {
13866
13870
  if (!match) throw new Error("invalid_hub_url");
13867
13871
  return new _DrispClient(url.origin, decodeURIComponent(match[1]), async () => token);
13868
13872
  }
13869
- async request(path12, body) {
13873
+ async request(path13, body) {
13870
13874
  const response = await fetch(
13871
- `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}${path12}`,
13875
+ `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}${path13}`,
13872
13876
  {
13873
13877
  method: body === void 0 ? "GET" : "POST",
13874
13878
  headers: {
@@ -14776,6 +14780,15 @@ var receiptSchema = external_exports.strictObject({
14776
14780
  evidence: external_exports.string().min(1).max(16e3),
14777
14781
  kind: external_exports.literal("agent-attestation")
14778
14782
  });
14783
+ var COMMIT_SHA = /^[0-9a-f]{40}$/;
14784
+ var gateEvidenceSchema = external_exports.strictObject({
14785
+ receipts: external_exports.array(receiptSchema).max(100),
14786
+ /**
14787
+ * The Sign-off on the Merge step (`merge: you`): the head commit the child attests the member signed off on in
14788
+ * its Thread. Like the receipts, an agent attestation.
14789
+ */
14790
+ signOff: external_exports.strictObject({ sha: external_exports.string().regex(COMMIT_SHA) }).optional()
14791
+ });
14779
14792
  var QA_KINDS = fillSchema.shape.qa.options;
14780
14793
  function runBuildStep(input) {
14781
14794
  const directory = requireEnv(input.env, "DRISP_RUN_ARTIFACTS");
@@ -14805,19 +14818,57 @@ function runBuildStep(input) {
14805
14818
  throw new BuildUsageError(
14806
14819
  `Step must be a whole number between 1 and ${titles.length}, not "${input.n ?? ""}".`
14807
14820
  );
14821
+ if (input.merge !== void 0 && input.merge !== "agent" && input.merge !== "you")
14822
+ throw new BuildUsageError("--merge must be agent or you.");
14823
+ const mergeStep = titles.indexOf("Merge") + 1;
14824
+ const signsOff = input.merge === "you" && index === mergeStep;
14825
+ if (input.signOff !== void 0) {
14826
+ if (input.merge !== "you")
14827
+ throw new BuildUsageError(
14828
+ "--sign-off needs --merge you: agents merge this pull request."
14829
+ );
14830
+ if (mergeStep === 0)
14831
+ throw new BuildUsageError(
14832
+ "--sign-off is for the Merge step, and this workflow has none."
14833
+ );
14834
+ if (!signsOff)
14835
+ throw new BuildUsageError(
14836
+ `--sign-off is for the Merge step (${mergeStep}), not step ${index}.`
14837
+ );
14838
+ if (!COMMIT_SHA.test(input.signOff))
14839
+ throw new BuildUsageError(
14840
+ "--sign-off needs the full 40-character commit SHA the member signed off on."
14841
+ );
14842
+ }
14808
14843
  if (!fs8.statSync(directory, { throwIfNoEntry: false })?.isDirectory())
14809
14844
  throw new BuildUsageError(
14810
14845
  `DRISP_RUN_ARTIFACTS (${directory}) is not a directory.`
14811
14846
  );
14812
14847
  const receiptsPath = path8.join(directory, "gate_evidence.json");
14813
14848
  let receipts = [];
14849
+ let signOff;
14814
14850
  if (fs8.existsSync(receiptsPath)) {
14815
14851
  try {
14816
- receipts = external_exports.strictObject({ receipts: external_exports.array(receiptSchema).max(100) }).parse(JSON.parse(fs8.readFileSync(receiptsPath, "utf8"))).receipts;
14852
+ ({ receipts, signOff } = gateEvidenceSchema.parse(
14853
+ JSON.parse(fs8.readFileSync(receiptsPath, "utf8"))
14854
+ ));
14817
14855
  } catch {
14818
14856
  throw new BuildUsageError("Gate evidence is invalid.");
14819
14857
  }
14820
14858
  }
14859
+ const save = () => fs8.writeFileSync(
14860
+ receiptsPath,
14861
+ JSON.stringify({ receipts, ...signOff ? { signOff } : {} }) + "\n"
14862
+ );
14863
+ const head = signsOff && (input.signOff || input.verify) ? headCommit(input.cwd ?? process.cwd()) : "";
14864
+ if (input.signOff !== void 0) {
14865
+ if (input.signOff !== head)
14866
+ throw new BuildUsageError(
14867
+ `The Sign-off names ${input.signOff}, but HEAD is ${head}. ${askForSignOff(head)}`
14868
+ );
14869
+ signOff = { sha: head };
14870
+ save();
14871
+ }
14821
14872
  const revision = receipts.length || input.complete || input.verify ? workRevision(input.cwd ?? process.cwd()) : "";
14822
14873
  const fresh = (receipt) => ["Explore", "Pull request", "Evidence", "Merge"].includes(receipt.title) || receipt.revision === revision;
14823
14874
  if (input.verify || input.complete && titles[index - 1] === "Evidence") {
@@ -14827,6 +14878,20 @@ function runBuildStep(input) {
14827
14878
  `Missing or stale evidence: ${missing.join(", ")}. Repeat those steps on the current revision.`
14828
14879
  );
14829
14880
  }
14881
+ if (signsOff && input.verify) {
14882
+ if (!signOff)
14883
+ throw new BuildUsageError(
14884
+ `No Sign-off on HEAD (${head}) yet. ${askForSignOff(head)}`
14885
+ );
14886
+ if (signOff.sha !== head)
14887
+ throw new BuildUsageError(
14888
+ `The Sign-off names ${signOff.sha}, but HEAD is now ${head}. ${askForSignOff(head)}`
14889
+ );
14890
+ }
14891
+ if (signsOff && input.complete && !signOff)
14892
+ throw new BuildUsageError(
14893
+ "No Sign-off was recorded: verify and merge only after the member signs off on HEAD."
14894
+ );
14830
14895
  if (input.complete) {
14831
14896
  if (!input.evidence)
14832
14897
  throw new BuildUsageError(
@@ -14850,7 +14915,7 @@ function runBuildStep(input) {
14850
14915
  evidence,
14851
14916
  kind: "agent-attestation"
14852
14917
  });
14853
- fs8.writeFileSync(receiptsPath, JSON.stringify({ receipts }) + "\n");
14918
+ save();
14854
14919
  }
14855
14920
  const artifact = {
14856
14921
  index,
@@ -14867,12 +14932,23 @@ function runBuildStep(input) {
14867
14932
  );
14868
14933
  return artifact;
14869
14934
  }
14935
+ function askForSignOff(sha) {
14936
+ return `Ask the member: end the Turn with <!-- NEEDS_HUMAN: ${BUILD_STRINGS.mergeSignOff.replace("{sha}", sha)} -->`;
14937
+ }
14938
+ var gitIn = (cwd) => (...args) => execFileSync("git", args, {
14939
+ cwd,
14940
+ encoding: "utf8",
14941
+ stdio: ["ignore", "pipe", "pipe"]
14942
+ });
14943
+ function headCommit(cwd) {
14944
+ try {
14945
+ return gitIn(cwd)("rev-parse", "HEAD").trim();
14946
+ } catch {
14947
+ throw new BuildUsageError("A Sign-off must be recorded in a git worktree.");
14948
+ }
14949
+ }
14870
14950
  function workRevision(cwd) {
14871
- const git2 = (...args) => execFileSync("git", args, {
14872
- cwd,
14873
- encoding: "utf8",
14874
- stdio: ["ignore", "pipe", "pipe"]
14875
- });
14951
+ const git2 = gitIn(cwd);
14876
14952
  try {
14877
14953
  const hash = createHash2("sha256").update(git2("rev-parse", "HEAD")).update(git2("diff", "HEAD", "--", ".", ":!.athena/evidence"));
14878
14954
  for (const name of git2("ls-files", "--others", "--exclude-standard", "-z").split("\0").filter(Boolean).sort()) {
@@ -14939,22 +15015,87 @@ function runBuildMerged(input) {
14939
15015
  return parsed.data;
14940
15016
  }
14941
15017
 
15018
+ // src/core/build/check.ts
15019
+ import { spawnSync } from "child_process";
15020
+ import fs10 from "fs";
15021
+ import path10 from "path";
15022
+ var checkRecordSchema = external_exports.strictObject({
15023
+ name: external_exports.string().trim().min(1).max(64),
15024
+ outcome: external_exports.enum(["passed", "failed"])
15025
+ });
15026
+ var NAME_PATTERN = /^[^\p{Cc}]+$/u;
15027
+ var MAX_RECORDS = 50;
15028
+ function spawnCheck(command, cwd, onOutput) {
15029
+ const [program, ...args] = command;
15030
+ const result = spawnSync(program, args, {
15031
+ cwd,
15032
+ stdio: onOutput ? ["inherit", "pipe", "inherit"] : "inherit",
15033
+ maxBuffer: 64 * 1024 * 1024
15034
+ });
15035
+ if (onOutput) onOutput(result.stdout);
15036
+ if (result.error) return 127;
15037
+ return result.status ?? 1;
15038
+ }
15039
+ function readRecords(file) {
15040
+ try {
15041
+ const parsed = external_exports.array(checkRecordSchema).safeParse(JSON.parse(fs10.readFileSync(file, "utf8")));
15042
+ return parsed.success ? parsed.data : [];
15043
+ } catch {
15044
+ return [];
15045
+ }
15046
+ }
15047
+ function runBuildCheck(input) {
15048
+ const directory = requireEnv(input.env, "DRISP_RUN_ARTIFACTS");
15049
+ const name = (input.name ?? "").trim();
15050
+ if (!name || name.length > 64 || !NAME_PATTERN.test(name))
15051
+ throw new BuildUsageError(
15052
+ "A check needs a name of 1 to 64 characters on one line."
15053
+ );
15054
+ if (input.command.length === 0 || input.command.some((token) => !token))
15055
+ throw new BuildUsageError("A check needs a command after `--`.");
15056
+ if (!fs10.statSync(directory, { throwIfNoEntry: false })?.isDirectory())
15057
+ throw new BuildUsageError(
15058
+ `DRISP_RUN_ARTIFACTS (${directory}) is not a directory.`
15059
+ );
15060
+ const code = input.run(input.command, input.cwd);
15061
+ const file = path10.join(directory, "checks.json");
15062
+ const record = { name, outcome: code === 0 ? "passed" : "failed" };
15063
+ const records = readRecords(file);
15064
+ const at = records.findIndex((existing) => existing.name === name);
15065
+ if (at >= 0) records[at] = record;
15066
+ else records.push(record);
15067
+ const temporary = `${file}.${process.pid}.tmp`;
15068
+ fs10.writeFileSync(
15069
+ temporary,
15070
+ `${JSON.stringify(records.slice(-MAX_RECORDS))}
15071
+ `
15072
+ );
15073
+ fs10.renameSync(temporary, file);
15074
+ return code;
15075
+ }
15076
+
14942
15077
  // src/app/entry/buildCommand.ts
14943
15078
  var USAGE3 = `Usage: drisp build plan --input <path>
14944
15079
  drisp build tick --input <path> [--rebase-failed <IDENTIFIER>]...
14945
15080
  drisp build checks
14946
15081
  drisp build step <n> [--qa none|web|macos|both] [--profile implementation|documentation|investigation]
14947
- [--test-first true|false] [--review true|false] [--complete --evidence <file>] [--verify]
15082
+ [--test-first true|false] [--review true|false] [--merge agent|you] [--sign-off <sha>]
15083
+ [--complete --evidence <file>] [--verify]
14948
15084
  drisp build merged [<pr url or number>]
15085
+ drisp build check <name> -- <command>
14949
15086
  plan and tick run inside an Orchestrator Run (DRISP_RUN_ID, DRISP_RUNNER_ID and DRISP_RUN_ARTIFACTS set; tick also
14950
15087
  DRISP_RUN_TOKEN and DRISP_HUB_URL). DRISP_BUILD_PLAN_INPUT=<path> stands in for plan's --input, with
14951
15088
  fills/<IDENTIFIER>.json beside it. checks runs in a child's worktree: the merge gate's verdict on HEAD's GitHub runs.
14952
15089
  step runs in any Run with DRISP_RUN_ARTIFACTS set: it writes the step artifact {index, title, total} for gate <n> of the
14953
15090
  selected build-child profile (QA is a gate unless --qa none) and refuses an index outside the list.
14954
15091
  --complete --evidence <file> records an agent attestation bound to the source revision; --verify requires fresh
14955
- receipts for all preceding steps. Keep evidence files under .athena/evidence/.
15092
+ receipts for all preceding steps. Keep evidence files under .athena/evidence/. With --merge you, the Merge step's
15093
+ --verify also needs the member's Sign-off on HEAD (--complete one at all), which --sign-off <sha> records for that full
15094
+ SHA only.
14956
15095
  merged runs in a child's worktree after its pull request merged: it writes the pull_request artifact
14957
- {url, number, sha, mergedAt} and refuses a pull request that is not merged.`;
15096
+ {url, number, sha, mergedAt} and refuses a pull request that is not merged.
15097
+ check runs <command>, passes its output through, exits with its code and records {name, outcome} in the checks run artifact
15098
+ (DRISP_RUN_ARTIFACTS set); it refuses a missing name or command before running anything.`;
14958
15099
  function githubPullRequests(folder, branch) {
14959
15100
  const output = execFileSync2(
14960
15101
  "gh",
@@ -15072,6 +15213,16 @@ async function runBuildCommand(input) {
15072
15213
  case "checks":
15073
15214
  output = runBuildChecks(input.cwd, input.checks ?? defaultChecks);
15074
15215
  break;
15216
+ case "check": {
15217
+ const code = runBuildCheck({
15218
+ env: env2,
15219
+ cwd: input.cwd,
15220
+ name: input.positional?.[0],
15221
+ command: input.command ?? [],
15222
+ run: input.checkRunner ?? spawnCheck
15223
+ });
15224
+ return code;
15225
+ }
15075
15226
  case "merged":
15076
15227
  output = runBuildMerged({
15077
15228
  env: env2,
@@ -15088,6 +15239,8 @@ async function runBuildCommand(input) {
15088
15239
  profile: input.flags.profile,
15089
15240
  testFirst: input.flags.testFirst,
15090
15241
  review: input.flags.review,
15242
+ merge: input.flags.merge,
15243
+ signOff: input.flags.signOff,
15091
15244
  complete: input.flags.complete,
15092
15245
  verify: input.flags.verify,
15093
15246
  evidence: input.flags.evidence,
@@ -15116,9 +15269,9 @@ import process7 from "process";
15116
15269
 
15117
15270
  // src/harnesses/claude/system/doctorEnvironment.ts
15118
15271
  import { execFileSync as execFileSync3 } from "child_process";
15119
- import * as fs10 from "fs";
15272
+ import * as fs11 from "fs";
15120
15273
  import * as os4 from "os";
15121
- import * as path10 from "path";
15274
+ import * as path11 from "path";
15122
15275
  var MANAGED_POLICY_KEYS = [
15123
15276
  "forceLoginMethod",
15124
15277
  "forceLoginOrgUUID",
@@ -15222,8 +15375,8 @@ function collectEnvironment(options = {}) {
15222
15375
  const detectVersion = options.detectClaudeVersionFn ?? detectClaudeVersion;
15223
15376
  const runAuthStatus = options.runClaudeAuthStatusFn ?? runClaudeAuthStatus;
15224
15377
  const resolveForwarder = options.resolveHookForwarderCommandFn ?? resolveHookForwarderCommand;
15225
- const statFn = options.statFn ?? ((p) => fs10.statSync(p));
15226
- const readFileFn = options.readFileFn ?? ((p) => fs10.readFileSync(p, "utf8"));
15378
+ const statFn = options.statFn ?? ((p) => fs11.statSync(p));
15379
+ const readFileFn = options.readFileFn ?? ((p) => fs11.readFileSync(p, "utf8"));
15227
15380
  const settingsPaths = resolveClaudeSettingsSurfacePaths(
15228
15381
  homeDir,
15229
15382
  cwd,
@@ -15268,7 +15421,7 @@ function collectEnvironment(options = {}) {
15268
15421
  });
15269
15422
  let globalConfigPresent = false;
15270
15423
  try {
15271
- statFn(path10.join(homeDir, ".claude.json"));
15424
+ statFn(path11.join(homeDir, ".claude.json"));
15272
15425
  globalConfigPresent = true;
15273
15426
  } catch {
15274
15427
  globalConfigPresent = false;
@@ -15748,13 +15901,13 @@ function runRunsCommand(input) {
15748
15901
  // src/app/entry/cli.tsx
15749
15902
  import { jsx as jsx24 } from "react/jsx-runtime";
15750
15903
  function resolvePackageJsonPath(entryUrl) {
15751
- let currentDir = path11.dirname(fileURLToPath(entryUrl));
15904
+ let currentDir = path12.dirname(fileURLToPath(entryUrl));
15752
15905
  for (; ; ) {
15753
- const candidatePath = path11.join(currentDir, "package.json");
15754
- if (fs11.existsSync(candidatePath)) {
15906
+ const candidatePath = path12.join(currentDir, "package.json");
15907
+ if (fs12.existsSync(candidatePath)) {
15755
15908
  return candidatePath;
15756
15909
  }
15757
- const parentDir = path11.dirname(currentDir);
15910
+ const parentDir = path12.dirname(currentDir);
15758
15911
  if (parentDir === currentDir) {
15759
15912
  throw new Error("Could not locate package.json for CLI metadata.");
15760
15913
  }
@@ -15926,7 +16079,8 @@ var cli = meow(
15926
16079
  build plan --input <path> Plan a Build inside an Orchestrator Run (prints one JSON object)
15927
16080
  build tick --input <path> Run one step of a started Build (prints the next action; [--rebase-failed <ID>]...)
15928
16081
  build checks The merge gate's verdict on HEAD's GitHub runs (in a child's worktree)
15929
- build step <n> Report the Run's current step: writes the step run artifact ([--qa none|web|macos|both], --profile, --complete --evidence, --verify)
16082
+ build step <n> Report the Run's current step: writes the step run artifact ([--qa none|web|macos|both], --profile, --merge you, --sign-off <sha>, --complete --evidence, --verify)
16083
+ build check <name> -- <cmd> Run a command and record its outcome: writes the checks run artifact
15930
16084
  build merged Record the merged pull request: writes the pull_request run artifact
15931
16085
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
15932
16086
  doctor Diagnose Claude headless setup (use with --harness=claude)
@@ -16068,6 +16222,8 @@ var cli = meow(
16068
16222
  profile: { type: "string" },
16069
16223
  testFirst: { type: "string" },
16070
16224
  review: { type: "string" },
16225
+ merge: { type: "string" },
16226
+ signOff: { type: "string" },
16071
16227
  complete: { type: "boolean" },
16072
16228
  verify: { type: "boolean" },
16073
16229
  evidence: { type: "string" },
@@ -16113,7 +16269,7 @@ var cli = meow(
16113
16269
  }
16114
16270
  );
16115
16271
  async function main() {
16116
- const projectDir = path11.resolve(cli.flags.projectDir);
16272
+ const projectDir = path12.resolve(cli.flags.projectDir);
16117
16273
  const [command, ...commandArgs] = cli.input;
16118
16274
  const isRunCommand = command === "run" || command === "exec";
16119
16275
  if (command && !KNOWN_COMMANDS.has(command)) {
@@ -16167,7 +16323,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16167
16323
  await exitWith(1);
16168
16324
  return;
16169
16325
  }
16170
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-VVEEJW25.js");
16326
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-IOJD3YOA.js");
16171
16327
  const { waitUntilExit } = render(
16172
16328
  /* @__PURE__ */ jsx24(
16173
16329
  WorkflowInstallWizard,
@@ -16246,16 +16402,23 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16246
16402
  return;
16247
16403
  }
16248
16404
  if (command === "build") {
16249
- const [subcommand = "", ...buildArgs] = commandArgs;
16405
+ const rawArgs = process.argv.slice(2);
16406
+ const dashIdx = rawArgs.indexOf("--");
16407
+ const checkTokens = dashIdx >= 0 ? rawArgs.slice(dashIdx + 1) : [];
16408
+ const cleanArgs = checkTokens.length > 0 ? commandArgs.slice(0, commandArgs.length - checkTokens.length) : commandArgs;
16409
+ const [subcommand = "", ...buildArgs] = cleanArgs;
16250
16410
  await exitWith(
16251
16411
  await runBuildCommand({
16252
16412
  subcommand,
16253
16413
  positional: buildArgs,
16414
+ command: checkTokens,
16254
16415
  flags: {
16255
16416
  ...typeof cli.flags.qa === "string" ? { qa: cli.flags.qa } : {},
16256
16417
  profile: cli.flags.profile,
16257
16418
  testFirst: cli.flags.testFirst,
16258
16419
  review: cli.flags.review,
16420
+ merge: cli.flags.merge,
16421
+ signOff: cli.flags.signOff,
16259
16422
  complete: cli.flags.complete,
16260
16423
  verify: cli.flags.verify,
16261
16424
  evidence: cli.flags.evidence,
@@ -16340,8 +16503,8 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16340
16503
  const showSetup = isRunCommand ? false : shouldShowSetup({
16341
16504
  cliInput: cli.input,
16342
16505
  setupComplete: globalConfig.setupComplete,
16343
- globalConfigExists: fs11.existsSync(
16344
- path11.join(os5.homedir(), ".config", "athena", "config.json")
16506
+ globalConfigExists: fs12.existsSync(
16507
+ path12.join(os5.homedir(), ".config", "athena", "config.json")
16345
16508
  )
16346
16509
  });
16347
16510
  let runtimeConfig;
@@ -1,6 +1,6 @@
1
- import "./chunk-D5VZT7M3.js";
2
- import "./chunk-D246FR4I.js";
1
+ import "./chunk-TBUUNMC6.js";
2
+ import "./chunk-HGMJ3YL2.js";
3
3
  import "./chunk-PQIB66SG.js";
4
- import "./chunk-HZ6GRLOM.js";
4
+ import "./chunk-OYUBT6RS.js";
5
5
  import "./chunk-G3PMV62Z.js";
6
6
  //# sourceMappingURL=dashboard-daemon.js.map
package/dist/runner.js CHANGED
@@ -1,6 +1,6 @@
1
- import "./chunk-D5VZT7M3.js";
2
- import "./chunk-D246FR4I.js";
1
+ import "./chunk-TBUUNMC6.js";
2
+ import "./chunk-HGMJ3YL2.js";
3
3
  import "./chunk-PQIB66SG.js";
4
- import "./chunk-HZ6GRLOM.js";
4
+ import "./chunk-OYUBT6RS.js";
5
5
  import "./chunk-G3PMV62Z.js";
6
6
  //# sourceMappingURL=runner.js.map
package/package.json CHANGED
@@ -17,7 +17,7 @@
17
17
  "hooks",
18
18
  "dashboard"
19
19
  ],
20
- "version": "0.7.7",
20
+ "version": "0.7.8",
21
21
  "license": "MIT",
22
22
  "bin": {
23
23
  "athena": "dist/cli.js",