@drisp/cli 0.7.6 → 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.
package/dist/cli.js CHANGED
@@ -37,6 +37,7 @@ import {
37
37
  cursorToVisualPosition,
38
38
  defaultWorktreesRoot,
39
39
  detectClaudeVersion,
40
+ eligibilityReason,
40
41
  ensureBuildWorktree,
41
42
  ensureChildWorktree,
42
43
  expandHome,
@@ -126,6 +127,7 @@ import {
126
127
  runnerServiceUnit,
127
128
  runnerSoftwareVersion,
128
129
  runnerUpdateRequestSchema,
130
+ scopeFingerprint,
129
131
  serviceInstallationId,
130
132
  sessionsDir,
131
133
  sliceAnsi,
@@ -140,7 +142,7 @@ import {
140
142
  visualPositionToOffset,
141
143
  wrapText,
142
144
  writeIdentityOnce
143
- } from "./chunk-2N7IBKJF.js";
145
+ } from "./chunk-HGMJ3YL2.js";
144
146
  import {
145
147
  generateId as generateId2
146
148
  } from "./chunk-PQIB66SG.js";
@@ -159,6 +161,7 @@ import {
159
161
  DESCRIPTION_TEMPLATE,
160
162
  GATES,
161
163
  LAYOUT_MARKERS,
164
+ MERGE_SIGN_OFF,
162
165
  NO_GATE_TEXT_TEMPLATE,
163
166
  ORCHESTRATOR_LIMITS,
164
167
  QA_SECTIONS,
@@ -198,7 +201,7 @@ import {
198
201
  workflowRegistryDir,
199
202
  writeGlobalConfig,
200
203
  writeProjectConfig
201
- } from "./chunk-W57EQYCM.js";
204
+ } from "./chunk-OYUBT6RS.js";
202
205
  import {
203
206
  __toESM
204
207
  } from "./chunk-G3PMV62Z.js";
@@ -206,10 +209,10 @@ import {
206
209
  // src/app/entry/cli.tsx
207
210
  import { render } from "ink";
208
211
  import meow from "meow";
209
- import fs11 from "fs";
212
+ import fs12 from "fs";
210
213
  import { createRequire } from "module";
211
214
  import os5 from "os";
212
- import path11 from "path";
215
+ import path12 from "path";
213
216
  import { fileURLToPath } from "url";
214
217
 
215
218
  // src/app/shell/sessionUiState.ts
@@ -5328,14 +5331,14 @@ function extractWebSearch(_input, response) {
5328
5331
  return { type: "text", content: extractTextContent(response), maxLines: 10 };
5329
5332
  }
5330
5333
  function extractNotebookEdit(input, _response) {
5331
- const path12 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5334
+ const path13 = typeof input["notebook_path"] === "string" ? input["notebook_path"] : "";
5332
5335
  const mode = typeof input["edit_mode"] === "string" ? input["edit_mode"] : "replace";
5333
5336
  const source = typeof input["new_source"] === "string" ? input["new_source"] : "";
5334
- if (!source) return { type: "text", content: `${mode} cell in ${path12}` };
5337
+ if (!source) return { type: "text", content: `${mode} cell in ${path13}` };
5335
5338
  return {
5336
5339
  type: "code",
5337
5340
  content: source,
5338
- language: detectLanguage(path12),
5341
+ language: detectLanguage(path13),
5339
5342
  maxLines: 10
5340
5343
  };
5341
5344
  }
@@ -5861,10 +5864,10 @@ function extractToolSubject(toolName, toolInput) {
5861
5864
  const parts = [];
5862
5865
  const pattern = str(input, "pattern");
5863
5866
  const glob = str(input, "glob");
5864
- const path12 = str(input, "path");
5867
+ const path13 = str(input, "path");
5865
5868
  if (pattern) parts.push(`"${pattern}"`);
5866
5869
  if (glob) parts.push(glob);
5867
- if (path12) parts.push(`in ${path12}`);
5870
+ if (path13) parts.push(`in ${path13}`);
5868
5871
  return parts.length > 0 ? parts.join(" ") : void 0;
5869
5872
  }
5870
5873
  case "Skill":
@@ -13095,7 +13098,7 @@ var thisMachine = (installationId) => ({
13095
13098
  os: `${platform()} ${release()}`.slice(0, 64),
13096
13099
  softwareVersion: runnerSoftwareVersion
13097
13100
  });
13098
- var post = (origin, path12, body) => fetch(new URL(path12, origin), {
13101
+ var post = (origin, path13, body) => fetch(new URL(path13, origin), {
13099
13102
  method: "POST",
13100
13103
  headers: { "Content-Type": "application/json" },
13101
13104
  body: JSON.stringify(body)
@@ -13114,8 +13117,8 @@ async function alreadyConnected(existing, { forTeam, ownIdentity }) {
13114
13117
  const problem = own ? await credentialAccepted(own.config) ? void 0 : "lapsed" : forTeam(existing.orgId).length ? "other_runner" : "missing";
13115
13118
  const runner = { orgId: existing.orgId, runnerId: existing.runnerId, name: existing.name };
13116
13119
  if (own && problem === void 0) return { status: "already_connected", ...runner, path: own.path };
13117
- const path12 = (own ?? forTeam(existing.orgId)[0])?.path;
13118
- 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 } : {} };
13119
13122
  }
13120
13123
  async function connectWithCode({
13121
13124
  code,
@@ -13187,20 +13190,20 @@ async function saveIdentity(origin, credential, { single, forTeam }, directory)
13187
13190
  name: credential.name
13188
13191
  });
13189
13192
  const replaced = [];
13190
- for (const { path: path13 } of forTeam(credential.orgId)) {
13191
- const aside = `${path13}.replaced-${Date.now()}`;
13192
- 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);
13193
13196
  replaced.push(aside);
13194
13197
  }
13195
- const path12 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13198
+ const path13 = single ?? teamIdentityPath(origin, credential.orgId, directory);
13196
13199
  try {
13197
- await writeIdentityOnce(path12, JSON.stringify(config, null, 2) + "\n");
13200
+ await writeIdentityOnce(path13, JSON.stringify(config, null, 2) + "\n");
13198
13201
  } catch (error) {
13199
13202
  if (error.code === "EEXIST")
13200
- 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.`);
13201
13204
  throw error;
13202
13205
  }
13203
- return { status: "connected", config, path: path12, replaced };
13206
+ return { status: "connected", config, path: path13, replaced };
13204
13207
  }
13205
13208
  function pathSegment(value) {
13206
13209
  const segment = encodeURIComponent(value);
@@ -13240,7 +13243,7 @@ async function refuseExisting(target) {
13240
13243
  throw new Error(`This machine already has a Runner${name ? ` (${name})` : ""} configured at ${target}.`);
13241
13244
  }
13242
13245
  async function disconnectRunner({
13243
- identity: { path: path12, config },
13246
+ identity: { path: path13, config },
13244
13247
  serviceManager,
13245
13248
  othersRemain
13246
13249
  }) {
@@ -13273,7 +13276,7 @@ async function disconnectRunner({
13273
13276
  await restore();
13274
13277
  throw new Error(`The Hub refused to disconnect ${name} (${body.error ?? response.status}); it is still connected.`);
13275
13278
  }
13276
- await forgetRunner(path12);
13279
+ await forgetRunner(path13);
13277
13280
  if (othersRemain) await restore();
13278
13281
  else await serviceManager?.remove();
13279
13282
  return result;
@@ -13426,18 +13429,18 @@ async function runRunnerCommandV2(input, options = {}) {
13426
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.`
13427
13430
  );
13428
13431
  }
13429
- const { config, path: path12, replaced } = outcome;
13432
+ const { config, path: path13, replaced } = outcome;
13430
13433
  for (const aside of replaced)
13431
13434
  process.stdout.write(
13432
13435
  `The Hub no longer recognised this machine's earlier identity for this Team; it was moved aside to ${aside}.
13433
13436
  `
13434
13437
  );
13435
13438
  process.stdout.write(
13436
- `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path12}.
13439
+ `Connected as ${config.name ?? config.runnerId}. Identity saved to ${path13}.
13437
13440
  `
13438
13441
  );
13439
13442
  if (services) {
13440
- const service = await ensureService(services, config, path12);
13443
+ const service = await ensureService(services, config, path13);
13441
13444
  process.stdout.write(`${serviceStarted(service)}
13442
13445
  `);
13443
13446
  return 0;
@@ -13446,7 +13449,7 @@ async function runRunnerCommandV2(input, options = {}) {
13446
13449
  process.stdout.write(
13447
13450
  "No launchd or systemd here to run the Runner as a background service; running it in the foreground instead.\n"
13448
13451
  );
13449
- await runConfiguredRunner(path12, options.signal);
13452
+ await runConfiguredRunner(path13, options.signal);
13450
13453
  return 0;
13451
13454
  }
13452
13455
  case "disconnect": {
@@ -13517,9 +13520,9 @@ async function runRunnerCommandV2(input, options = {}) {
13517
13520
  configured: true,
13518
13521
  // One entry per Team this machine serves; credentials are never printed.
13519
13522
  teams: identities.map(
13520
- ({ path: path12, config: { token: _token, ...config } }) => ({
13523
+ ({ path: path13, config: { token: _token, ...config } }) => ({
13521
13524
  ...config,
13522
- path: path12
13525
+ path: path13
13523
13526
  })
13524
13527
  ),
13525
13528
  service: service ? {
@@ -13615,7 +13618,7 @@ async function recoveryIdentity(runId) {
13615
13618
  }
13616
13619
 
13617
13620
  // src/app/entry/buildCommand.ts
13618
- import { execFileSync } from "child_process";
13621
+ import { execFileSync as execFileSync2 } from "child_process";
13619
13622
 
13620
13623
  // src/core/build/planCommand.ts
13621
13624
  import fs6 from "fs";
@@ -13675,7 +13678,9 @@ function computeTemplateVersion() {
13675
13678
  JSON.stringify(QA_SECTIONS),
13676
13679
  JSON.stringify(GATES),
13677
13680
  JSON.stringify(RENDERER_RULES),
13678
- JSON.stringify(LAYOUT_MARKERS)
13681
+ JSON.stringify(LAYOUT_MARKERS),
13682
+ JSON.stringify(MERGE_SIGN_OFF),
13683
+ BUILD_STRINGS.mergeSignOff
13679
13684
  ];
13680
13685
  const hash = createHash("sha256");
13681
13686
  for (const part of parts) hash.update(`${part.length}:${part}`);
@@ -13755,7 +13760,7 @@ function runBuildPlan(deps) {
13755
13760
  const fixturePullRequests = fixtureDir ? readJson(path6.join(fixtureDir, "pull-requests.json")) : void 0;
13756
13761
  const pullRequests = fixturePullRequests ?? Object.fromEntries(
13757
13762
  input.issues.filter(
13758
- (issue) => issue.branchName && issue.state.type !== "canceled" && !issue.labels.includes("ready-for-human")
13763
+ (issue) => issue.branchName && issue.state.type !== "canceled" && !eligibilityReason(issue)
13759
13764
  ).map((issue) => [
13760
13765
  issue.branchName,
13761
13766
  deps.pullRequests(deps.folder, issue.branchName)
@@ -13780,7 +13785,8 @@ function runBuildPlan(deps) {
13780
13785
  fills: result.fills.map((fill) => ({
13781
13786
  ...fill,
13782
13787
  path: fillPath(fill.identifier)
13783
- }))
13788
+ })),
13789
+ ...result.reason ? { reason: result.reason } : {}
13784
13790
  };
13785
13791
  const host = deps.hostCapabilities();
13786
13792
  for (const workflow of result.workflows) {
@@ -13864,9 +13870,9 @@ var DrispClient = class _DrispClient {
13864
13870
  if (!match) throw new Error("invalid_hub_url");
13865
13871
  return new _DrispClient(url.origin, decodeURIComponent(match[1]), async () => token);
13866
13872
  }
13867
- async request(path12, body) {
13873
+ async request(path13, body) {
13868
13874
  const response = await fetch(
13869
- `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}${path12}`,
13875
+ `${this.baseUrl}/v2/orgs/${encodeURIComponent(this.orgId)}${path13}`,
13870
13876
  {
13871
13877
  method: body === void 0 ? "GET" : "POST",
13872
13878
  headers: {
@@ -14266,8 +14272,22 @@ function occupies(view) {
14266
14272
  function assessTick(facts) {
14267
14273
  const { entries, orchestratorRunId } = facts;
14268
14274
  const state = structuredClone(facts.state);
14275
+ for (const [id, reason] of Object.entries(state.needsPerson))
14276
+ if (reason.startsWith("Issue is no longer eligible") || reason.startsWith("Waiting for prerequisite"))
14277
+ delete state.needsPerson[id];
14269
14278
  const views = new Map(facts.children.map((view) => [view.runId, view]));
14270
14279
  const issues = new Map(facts.issues.map((issue) => [issue.id, issue]));
14280
+ const parentOnly = new Set(
14281
+ facts.skipped?.filter((issue) => issue.reason === "parent-only").map((issue) => issue.id) ?? []
14282
+ );
14283
+ const expandBlocker = (id, seen = /* @__PURE__ */ new Set()) => {
14284
+ if (!parentOnly.has(id) || seen.has(id)) return [id];
14285
+ const children = facts.issues.filter((issue) => issue.parentId === id);
14286
+ if (!children.length) return [id];
14287
+ return children.flatMap(
14288
+ (issue) => expandBlocker(issue.id, /* @__PURE__ */ new Set([...seen, id]))
14289
+ );
14290
+ };
14271
14291
  const attemptsOf = (id) => state.attempts[id] ?? [];
14272
14292
  for (const entry of entries) {
14273
14293
  const prefix = `${orchestratorRunId}:${entry.identifier}:a`;
@@ -14310,7 +14330,7 @@ function assessTick(facts) {
14310
14330
  const status = /* @__PURE__ */ new Map();
14311
14331
  const skippedBlocker = (id) => {
14312
14332
  const linear = issues.get(id)?.state.type;
14313
- return linear === "completed" || linear === "canceled" ? "done" : "blocked";
14333
+ return linear === "completed" ? "done" : "blocked";
14314
14334
  };
14315
14335
  const repair = [];
14316
14336
  const repaired = new Set(state.repaired ?? []);
@@ -14321,7 +14341,7 @@ function assessTick(facts) {
14321
14341
  const linear = issues.get(entry.id)?.state.type;
14322
14342
  const statusOf = () => {
14323
14343
  if (linear === "completed" || linear === "canceled") return "done";
14324
- const prs = prsOf(entry);
14344
+ const prs = entry.profile === "investigation" ? void 0 : prsOf(entry);
14325
14345
  if (prs?.merged) {
14326
14346
  if (!repaired.has(entry.id) && !(newest && occupies(view))) {
14327
14347
  const prUrl = prs.mergedUrl ?? branchOf(entry);
@@ -14336,18 +14356,36 @@ function assessTick(facts) {
14336
14356
  return "done";
14337
14357
  }
14338
14358
  if (newest && (!view || !view.closed)) return "live";
14359
+ const current = issues.get(entry.id);
14360
+ if (!current || eligibilityReason(current)) {
14361
+ state.needsPerson[entry.id] = `Issue is no longer eligible (${current ? eligibilityReason(current) : "missing from Linear"}). Update Linear, then Build again.`;
14362
+ return "needs_person";
14363
+ }
14364
+ if (entry.scopeFingerprint && entry.scopeFingerprint !== scopeFingerprint(current)) {
14365
+ state.needsPerson[entry.id] = "Issue scope changed after approval. Build again to review the revised plan.";
14366
+ return "needs_person";
14367
+ }
14339
14368
  if (newest?.outcome === "template_mismatch") return "replan";
14340
14369
  if (newest?.outcome === "completed")
14341
- state.needsPerson[entry.id] ??= BUILD_STRINGS.completedUnmerged;
14370
+ state.needsPerson[entry.id] ??= entry.profile === "investigation" ? "The investigation ended without marking its accepted findings Done in Linear. Review its evidence." : BUILD_STRINGS.completedUnmerged;
14342
14371
  if (state.needsPerson[entry.id] || newest && !mayRetry(attempts))
14343
14372
  return "needs_person";
14344
- const blockers = entry.blockedBy.map(
14345
- (id) => status.get(id) ?? skippedBlocker(id)
14346
- );
14373
+ const blockers = [
14374
+ .../* @__PURE__ */ new Set([
14375
+ ...entry.blockedBy,
14376
+ ...current.blockedBy.flatMap((id) => expandBlocker(id))
14377
+ ])
14378
+ ].map((id) => {
14379
+ if (issues.get(id)?.state.type === "canceled") return "blocked";
14380
+ return status.get(id) ?? (entries.some((each) => each.id === id) && issues.get(id)?.state.type !== "completed" ? "pending" : skippedBlocker(id));
14381
+ });
14347
14382
  if (blockers.some(
14348
14383
  (b) => b === "needs_person" || b === "replan" || b === "blocked"
14349
- ))
14384
+ )) {
14385
+ if (current.blockedBy.some((id) => !entry.blockedBy.includes(id)))
14386
+ state.needsPerson[entry.id] = "Waiting for prerequisite work. Resolve its readiness, completion or cancellation in Linear, then resume.";
14350
14387
  return "blocked";
14388
+ }
14351
14389
  return blockers.every((b) => b === "done") ? "ready" : "pending";
14352
14390
  };
14353
14391
  status.set(entry.id, statusOf());
@@ -14399,6 +14437,12 @@ function tickAction(input) {
14399
14437
  const done = input.entries.filter(
14400
14438
  (entry) => input.status.get(entry.id) === "done"
14401
14439
  ).length;
14440
+ if (done !== input.entries.length)
14441
+ return {
14442
+ action: "park",
14443
+ reason: "attention",
14444
+ message: `Built ${done} of ${input.entries.length}. Remaining issues are blocked or need attention. Resolve the issue or dependency, then resume; Build again if the approved scope changed.`
14445
+ };
14402
14446
  return {
14403
14447
  action: "complete",
14404
14448
  message: `Built ${done} of ${input.entries.length}`
@@ -14523,6 +14567,7 @@ async function runBuildTick(deps) {
14523
14567
  const entries = plan.groups.flatMap((group) => group.issues).filter((entry) => approved.has(entry.id));
14524
14568
  const issues = new Map(input.issues.map((issue) => [issue.id, issue]));
14525
14569
  const assessment = assessTick({
14570
+ skipped: plan.skipped,
14526
14571
  orchestratorRunId,
14527
14572
  entries,
14528
14573
  issues: input.issues,
@@ -14725,8 +14770,25 @@ function evaluateRemoteChecks(runs) {
14725
14770
  }
14726
14771
 
14727
14772
  // src/core/build/step.ts
14773
+ import { execFileSync } from "child_process";
14774
+ import { createHash as createHash2 } from "crypto";
14728
14775
  import fs8 from "fs";
14729
14776
  import path8 from "path";
14777
+ var receiptSchema = external_exports.strictObject({
14778
+ title: external_exports.string().min(1),
14779
+ revision: external_exports.string().regex(/^[0-9a-f]{64}$/),
14780
+ evidence: external_exports.string().min(1).max(16e3),
14781
+ kind: external_exports.literal("agent-attestation")
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
+ });
14730
14792
  var QA_KINDS = fillSchema.shape.qa.options;
14731
14793
  function runBuildStep(input) {
14732
14794
  const directory = requireEnv(input.env, "DRISP_RUN_ARTIFACTS");
@@ -14735,20 +14797,133 @@ function runBuildStep(input) {
14735
14797
  throw new BuildUsageError(
14736
14798
  `--qa must be one of ${QA_KINDS.join(", ")}, not "${qa}".`
14737
14799
  );
14738
- const titles = gateTitles({ qa });
14800
+ const parsedProfile = fillSchema.shape.profile.safeParse(input.profile);
14801
+ if (!parsedProfile.success)
14802
+ throw new BuildUsageError("Unknown workflow profile.");
14803
+ const profile = parsedProfile.data;
14804
+ const selection = (value) => {
14805
+ if (value === void 0) return void 0;
14806
+ if (value !== "true" && value !== "false")
14807
+ throw new BuildUsageError("Step selection must be true or false.");
14808
+ return { enabled: value === "true", reason: "Approved workflow" };
14809
+ };
14810
+ const titles = gateTitles({
14811
+ qa,
14812
+ profile,
14813
+ testFirst: selection(input.testFirst),
14814
+ review: selection(input.review)
14815
+ });
14739
14816
  const index = /^[0-9]+$/.test(input.n ?? "") ? Number(input.n) : 0;
14740
14817
  if (index < 1 || index > titles.length)
14741
14818
  throw new BuildUsageError(
14742
14819
  `Step must be a whole number between 1 and ${titles.length}, not "${input.n ?? ""}".`
14743
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
+ }
14744
14843
  if (!fs8.statSync(directory, { throwIfNoEntry: false })?.isDirectory())
14745
14844
  throw new BuildUsageError(
14746
14845
  `DRISP_RUN_ARTIFACTS (${directory}) is not a directory.`
14747
14846
  );
14847
+ const receiptsPath = path8.join(directory, "gate_evidence.json");
14848
+ let receipts = [];
14849
+ let signOff;
14850
+ if (fs8.existsSync(receiptsPath)) {
14851
+ try {
14852
+ ({ receipts, signOff } = gateEvidenceSchema.parse(
14853
+ JSON.parse(fs8.readFileSync(receiptsPath, "utf8"))
14854
+ ));
14855
+ } catch {
14856
+ throw new BuildUsageError("Gate evidence is invalid.");
14857
+ }
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
+ }
14872
+ const revision = receipts.length || input.complete || input.verify ? workRevision(input.cwd ?? process.cwd()) : "";
14873
+ const fresh = (receipt) => ["Explore", "Pull request", "Evidence", "Merge"].includes(receipt.title) || receipt.revision === revision;
14874
+ if (input.verify || input.complete && titles[index - 1] === "Evidence") {
14875
+ const missing = titles.slice(0, index - 1).filter((title) => !receipts.some((r) => r.title === title && fresh(r)));
14876
+ if (missing.length)
14877
+ throw new BuildUsageError(
14878
+ `Missing or stale evidence: ${missing.join(", ")}. Repeat those steps on the current revision.`
14879
+ );
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
+ );
14895
+ if (input.complete) {
14896
+ if (!input.evidence)
14897
+ throw new BuildUsageError(
14898
+ "--complete requires --evidence <file> containing the result and supporting paths."
14899
+ );
14900
+ let evidence;
14901
+ try {
14902
+ evidence = fs8.readFileSync(
14903
+ path8.resolve(input.cwd ?? process.cwd(), input.evidence),
14904
+ "utf8"
14905
+ ).trim();
14906
+ } catch {
14907
+ throw new BuildUsageError("Cannot read the evidence file.");
14908
+ }
14909
+ if (!evidence || evidence.length > 16e3)
14910
+ throw new BuildUsageError("Evidence must contain 1\u201316000 characters.");
14911
+ receipts = receipts.filter((r) => r.title !== titles[index - 1]);
14912
+ receipts.push({
14913
+ title: titles[index - 1],
14914
+ revision,
14915
+ evidence,
14916
+ kind: "agent-attestation"
14917
+ });
14918
+ save();
14919
+ }
14748
14920
  const artifact = {
14749
14921
  index,
14750
14922
  title: titles[index - 1],
14751
- total: titles.length
14923
+ total: titles.length,
14924
+ ...receipts.length ? {
14925
+ completed: receipts.filter(fresh).map((r) => titles.indexOf(r.title) + 1).filter((n) => n > 0)
14926
+ } : {}
14752
14927
  };
14753
14928
  fs8.writeFileSync(
14754
14929
  path8.join(directory, "step.json"),
@@ -14757,6 +14932,34 @@ function runBuildStep(input) {
14757
14932
  );
14758
14933
  return artifact;
14759
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
+ }
14950
+ function workRevision(cwd) {
14951
+ const git2 = gitIn(cwd);
14952
+ try {
14953
+ const hash = createHash2("sha256").update(git2("rev-parse", "HEAD")).update(git2("diff", "HEAD", "--", ".", ":!.athena/evidence"));
14954
+ for (const name of git2("ls-files", "--others", "--exclude-standard", "-z").split("\0").filter(Boolean).sort()) {
14955
+ if (name.startsWith(".athena/evidence/")) continue;
14956
+ hash.update(name).update(fs8.readFileSync(path8.join(cwd, name)));
14957
+ }
14958
+ return hash.digest("hex");
14959
+ } catch {
14960
+ throw new BuildUsageError("Evidence must be recorded in a git worktree.");
14961
+ }
14962
+ }
14760
14963
 
14761
14964
  // src/core/build/merged.ts
14762
14965
  import fs9 from "fs";
@@ -14812,21 +15015,89 @@ function runBuildMerged(input) {
14812
15015
  return parsed.data;
14813
15016
  }
14814
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
+
14815
15077
  // src/app/entry/buildCommand.ts
14816
15078
  var USAGE3 = `Usage: drisp build plan --input <path>
14817
15079
  drisp build tick --input <path> [--rebase-failed <IDENTIFIER>]...
14818
15080
  drisp build checks
14819
- drisp build step <n> [--qa none|web|macos]
15081
+ drisp build step <n> [--qa none|web|macos|both] [--profile implementation|documentation|investigation]
15082
+ [--test-first true|false] [--review true|false] [--merge agent|you] [--sign-off <sha>]
15083
+ [--complete --evidence <file>] [--verify]
14820
15084
  drisp build merged [<pr url or number>]
15085
+ drisp build check <name> -- <command>
14821
15086
  plan and tick run inside an Orchestrator Run (DRISP_RUN_ID, DRISP_RUNNER_ID and DRISP_RUN_ARTIFACTS set; tick also
14822
15087
  DRISP_RUN_TOKEN and DRISP_HUB_URL). DRISP_BUILD_PLAN_INPUT=<path> stands in for plan's --input, with
14823
15088
  fills/<IDENTIFIER>.json beside it. checks runs in a child's worktree: the merge gate's verdict on HEAD's GitHub runs.
14824
15089
  step runs in any Run with DRISP_RUN_ARTIFACTS set: it writes the step artifact {index, title, total} for gate <n> of the
14825
- build-child template (QA is a gate unless --qa none) and refuses an index outside the list.
15090
+ selected build-child profile (QA is a gate unless --qa none) and refuses an index outside the list.
15091
+ --complete --evidence <file> records an agent attestation bound to the source revision; --verify requires fresh
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.
14826
15095
  merged runs in a child's worktree after its pull request merged: it writes the pull_request artifact
14827
- {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.`;
14828
15099
  function githubPullRequests(folder, branch) {
14829
- const output = execFileSync(
15100
+ const output = execFileSync2(
14830
15101
  "gh",
14831
15102
  [
14832
15103
  "pr",
@@ -14865,7 +15136,7 @@ function hostCapabilities(folder) {
14865
15136
  }
14866
15137
  var defaultChecks = {
14867
15138
  head: (cwd) => git(cwd, ["rev-parse", "HEAD"]),
14868
- gh: (args, cwd) => execFileSync("gh", args, {
15139
+ gh: (args, cwd) => execFileSync2("gh", args, {
14869
15140
  cwd,
14870
15141
  encoding: "utf8",
14871
15142
  stdio: ["ignore", "pipe", "pipe"]
@@ -14942,6 +15213,16 @@ async function runBuildCommand(input) {
14942
15213
  case "checks":
14943
15214
  output = runBuildChecks(input.cwd, input.checks ?? defaultChecks);
14944
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
+ }
14945
15226
  case "merged":
14946
15227
  output = runBuildMerged({
14947
15228
  env: env2,
@@ -14954,6 +15235,15 @@ async function runBuildCommand(input) {
14954
15235
  output = runBuildStep({
14955
15236
  env: env2,
14956
15237
  n: input.positional?.[0],
15238
+ cwd: input.cwd,
15239
+ profile: input.flags.profile,
15240
+ testFirst: input.flags.testFirst,
15241
+ review: input.flags.review,
15242
+ merge: input.flags.merge,
15243
+ signOff: input.flags.signOff,
15244
+ complete: input.flags.complete,
15245
+ verify: input.flags.verify,
15246
+ evidence: input.flags.evidence,
14957
15247
  ...input.flags.qa ? { qa: input.flags.qa } : {}
14958
15248
  });
14959
15249
  break;
@@ -14978,10 +15268,10 @@ ${USAGE3}
14978
15268
  import process7 from "process";
14979
15269
 
14980
15270
  // src/harnesses/claude/system/doctorEnvironment.ts
14981
- import { execFileSync as execFileSync2 } from "child_process";
14982
- import * as fs10 from "fs";
15271
+ import { execFileSync as execFileSync3 } from "child_process";
15272
+ import * as fs11 from "fs";
14983
15273
  import * as os4 from "os";
14984
- import * as path10 from "path";
15274
+ import * as path11 from "path";
14985
15275
  var MANAGED_POLICY_KEYS = [
14986
15276
  "forceLoginMethod",
14987
15277
  "forceLoginOrgUUID",
@@ -15056,7 +15346,7 @@ function readEnforcement(scopes) {
15056
15346
  }
15057
15347
  function tryRichAuth(claudeBinary) {
15058
15348
  try {
15059
- const output = execFileSync2(claudeBinary, ["auth", "status"], {
15349
+ const output = execFileSync3(claudeBinary, ["auth", "status"], {
15060
15350
  timeout: 1e4,
15061
15351
  encoding: "utf-8",
15062
15352
  stdio: ["ignore", "pipe", "pipe"]
@@ -15085,8 +15375,8 @@ function collectEnvironment(options = {}) {
15085
15375
  const detectVersion = options.detectClaudeVersionFn ?? detectClaudeVersion;
15086
15376
  const runAuthStatus = options.runClaudeAuthStatusFn ?? runClaudeAuthStatus;
15087
15377
  const resolveForwarder = options.resolveHookForwarderCommandFn ?? resolveHookForwarderCommand;
15088
- const statFn = options.statFn ?? ((p) => fs10.statSync(p));
15089
- 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"));
15090
15380
  const settingsPaths = resolveClaudeSettingsSurfacePaths(
15091
15381
  homeDir,
15092
15382
  cwd,
@@ -15131,7 +15421,7 @@ function collectEnvironment(options = {}) {
15131
15421
  });
15132
15422
  let globalConfigPresent = false;
15133
15423
  try {
15134
- statFn(path10.join(homeDir, ".claude.json"));
15424
+ statFn(path11.join(homeDir, ".claude.json"));
15135
15425
  globalConfigPresent = true;
15136
15426
  } catch {
15137
15427
  globalConfigPresent = false;
@@ -15611,13 +15901,13 @@ function runRunsCommand(input) {
15611
15901
  // src/app/entry/cli.tsx
15612
15902
  import { jsx as jsx24 } from "react/jsx-runtime";
15613
15903
  function resolvePackageJsonPath(entryUrl) {
15614
- let currentDir = path11.dirname(fileURLToPath(entryUrl));
15904
+ let currentDir = path12.dirname(fileURLToPath(entryUrl));
15615
15905
  for (; ; ) {
15616
- const candidatePath = path11.join(currentDir, "package.json");
15617
- if (fs11.existsSync(candidatePath)) {
15906
+ const candidatePath = path12.join(currentDir, "package.json");
15907
+ if (fs12.existsSync(candidatePath)) {
15618
15908
  return candidatePath;
15619
15909
  }
15620
- const parentDir = path11.dirname(currentDir);
15910
+ const parentDir = path12.dirname(currentDir);
15621
15911
  if (parentDir === currentDir) {
15622
15912
  throw new Error("Could not locate package.json for CLI metadata.");
15623
15913
  }
@@ -15789,7 +16079,8 @@ var cli = meow(
15789
16079
  build plan --input <path> Plan a Build inside an Orchestrator Run (prints one JSON object)
15790
16080
  build tick --input <path> Run one step of a started Build (prints the next action; [--rebase-failed <ID>]...)
15791
16081
  build checks The merge gate's verdict on HEAD's GitHub runs (in a child's worktree)
15792
- build step <n> Report the Run's current step: writes the step run artifact ([--qa none|web|macos])
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
15793
16084
  build merged Record the merged pull request: writes the pull_request run artifact
15794
16085
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
15795
16086
  doctor Diagnose Claude headless setup (use with --harness=claude)
@@ -15928,6 +16219,14 @@ var cli = meow(
15928
16219
  type: "string",
15929
16220
  isMultiple: true
15930
16221
  },
16222
+ profile: { type: "string" },
16223
+ testFirst: { type: "string" },
16224
+ review: { type: "string" },
16225
+ merge: { type: "string" },
16226
+ signOff: { type: "string" },
16227
+ complete: { type: "boolean" },
16228
+ verify: { type: "boolean" },
16229
+ evidence: { type: "string" },
15931
16230
  qa: {
15932
16231
  type: "string"
15933
16232
  },
@@ -15970,7 +16269,7 @@ var cli = meow(
15970
16269
  }
15971
16270
  );
15972
16271
  async function main() {
15973
- const projectDir = path11.resolve(cli.flags.projectDir);
16272
+ const projectDir = path12.resolve(cli.flags.projectDir);
15974
16273
  const [command, ...commandArgs] = cli.input;
15975
16274
  const isRunCommand = command === "run" || command === "exec";
15976
16275
  if (command && !KNOWN_COMMANDS.has(command)) {
@@ -16024,7 +16323,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16024
16323
  await exitWith(1);
16025
16324
  return;
16026
16325
  }
16027
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-YANLNMTP.js");
16326
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-IOJD3YOA.js");
16028
16327
  const { waitUntilExit } = render(
16029
16328
  /* @__PURE__ */ jsx24(
16030
16329
  WorkflowInstallWizard,
@@ -16103,13 +16402,26 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16103
16402
  return;
16104
16403
  }
16105
16404
  if (command === "build") {
16106
- 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;
16107
16410
  await exitWith(
16108
16411
  await runBuildCommand({
16109
16412
  subcommand,
16110
16413
  positional: buildArgs,
16414
+ command: checkTokens,
16111
16415
  flags: {
16112
16416
  ...typeof cli.flags.qa === "string" ? { qa: cli.flags.qa } : {},
16417
+ profile: cli.flags.profile,
16418
+ testFirst: cli.flags.testFirst,
16419
+ review: cli.flags.review,
16420
+ merge: cli.flags.merge,
16421
+ signOff: cli.flags.signOff,
16422
+ complete: cli.flags.complete,
16423
+ verify: cli.flags.verify,
16424
+ evidence: cli.flags.evidence,
16113
16425
  ...typeof cli.flags.input === "string" ? { input: cli.flags.input } : {},
16114
16426
  ...cli.flags.rebaseFailed?.length ? { rebaseFailed: cli.flags.rebaseFailed } : {}
16115
16427
  },
@@ -16191,8 +16503,8 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16191
16503
  const showSetup = isRunCommand ? false : shouldShowSetup({
16192
16504
  cliInput: cli.input,
16193
16505
  setupComplete: globalConfig.setupComplete,
16194
- globalConfigExists: fs11.existsSync(
16195
- path11.join(os5.homedir(), ".config", "athena", "config.json")
16506
+ globalConfigExists: fs12.existsSync(
16507
+ path12.join(os5.homedir(), ".config", "athena", "config.json")
16196
16508
  )
16197
16509
  });
16198
16510
  let runtimeConfig;