@drisp/cli 0.7.6 → 0.7.7

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-W57EQYCM.js";
11
+ } from "./chunk-HZ6GRLOM.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-YANLNMTP.js.map
96
+ //# sourceMappingURL=WorkflowInstallWizard-VVEEJW25.js.map
@@ -54,7 +54,7 @@ import {
54
54
  verifyRendered,
55
55
  wakesFreshAfterHandover,
56
56
  workflowRegistryDir
57
- } from "./chunk-W57EQYCM.js";
57
+ } from "./chunk-HZ6GRLOM.js";
58
58
  import {
59
59
  __commonJS,
60
60
  __toESM
@@ -14892,7 +14892,7 @@ async function restoreCheckpoint(store, input) {
14892
14892
 
14893
14893
  // ../../modules/runtime/src/software-version.ts
14894
14894
  import "fs";
14895
- var runnerSoftwareVersion = true ? "0.7.6" : JSON.parse(readFileSync2(new URL("../../../apps/cli/package.json", import.meta.url), "utf8")).version;
14895
+ var runnerSoftwareVersion = true ? "0.7.7" : JSON.parse(readFileSync2(new URL("../../../apps/cli/package.json", import.meta.url), "utf8")).version;
14896
14896
 
14897
14897
  // ../../modules/runtime/src/connection.ts
14898
14898
  var RunnerDisconnectedError = class extends Error {
@@ -20176,7 +20176,33 @@ var fillSchema = external_exports.strictObject({
20176
20176
  areas: external_exports.array(external_exports.enum(AREAS)).min(1).refine((areas) => new Set(areas).size === areas.length, {
20177
20177
  error: "areas must be unique"
20178
20178
  }),
20179
- qa: external_exports.enum(["none", "web", "macos"]),
20179
+ qa: external_exports.enum(["none", "web", "macos", "both"]),
20180
+ profile: external_exports.enum(["implementation", "documentation", "investigation"]).optional(),
20181
+ rationale: external_exports.string().min(1).max(2e3).refine((text) => !TEMPLATE_STRUCTURE.test(text), {
20182
+ error: INSTRUCTIONS_MESSAGE
20183
+ }).optional(),
20184
+ acceptanceEvidence: external_exports.array(
20185
+ external_exports.string().min(1).max(2e3).refine((text) => !TEMPLATE_STRUCTURE.test(text), {
20186
+ error: INSTRUCTIONS_MESSAGE
20187
+ })
20188
+ ).min(1).optional(),
20189
+ testFirst: external_exports.strictObject({
20190
+ enabled: external_exports.boolean(),
20191
+ reason: external_exports.string().min(1).max(1e3).refine((text) => !TEMPLATE_STRUCTURE.test(text), {
20192
+ error: INSTRUCTIONS_MESSAGE
20193
+ })
20194
+ }).optional(),
20195
+ review: external_exports.strictObject({
20196
+ enabled: external_exports.boolean(),
20197
+ reason: external_exports.string().min(1).max(1e3).refine((text) => !TEMPLATE_STRUCTURE.test(text), {
20198
+ error: INSTRUCTIONS_MESSAGE
20199
+ })
20200
+ }).optional(),
20201
+ checks: external_exports.array(
20202
+ external_exports.string().min(1).max(500).regex(
20203
+ /^(?:npm (?:run|test|--prefix apps\/cli run)|npx vitest run|make (?:-C apps\/macos|check|typecheck)|git diff --check)(?: [a-zA-Z0-9_./:=@-]+)*$/
20204
+ )
20205
+ ).min(1).optional(),
20180
20206
  sizeClass: external_exports.enum(["S", "M", "L"]),
20181
20207
  instructions: external_exports.string().min(1).max(6e3).refine((text) => !TEMPLATE_STRUCTURE.test(text), {
20182
20208
  error: INSTRUCTIONS_MESSAGE
@@ -20211,6 +20237,9 @@ var planInputSchema = external_exports.strictObject({
20211
20237
  // Names the issue's directory under `.athena/workflows/<slugId>/`, so only a Linear identifier.
20212
20238
  identifier: external_exports.string().regex(/^[A-Z][A-Z0-9]*-[0-9]+$/),
20213
20239
  title: external_exports.string(),
20240
+ description: external_exports.string().optional(),
20241
+ acceptanceCriteria: external_exports.string().optional(),
20242
+ external: external_exports.boolean().optional(),
20214
20243
  url: external_exports.string().url(),
20215
20244
  branchName: external_exports.string().nullable(),
20216
20245
  state: external_exports.strictObject({
@@ -20240,7 +20269,12 @@ var SKIP_REASONS = [
20240
20269
  "ready-for-human",
20241
20270
  "merged",
20242
20271
  "done-pr-open",
20243
- "parent-only"
20272
+ "parent-only",
20273
+ "external",
20274
+ "needs-info",
20275
+ "needs-triage",
20276
+ "wontfix",
20277
+ "not-ready-for-agent"
20244
20278
  ];
20245
20279
  var count = external_exports.number().int().nonnegative();
20246
20280
  var issueEntrySchema = external_exports.strictObject({
@@ -20249,7 +20283,9 @@ var issueEntrySchema = external_exports.strictObject({
20249
20283
  title: external_exports.string(),
20250
20284
  blockedBy: external_exports.array(external_exports.string()),
20251
20285
  areas: external_exports.array(external_exports.enum(AREAS)).min(1),
20252
- qa: external_exports.enum(["none", "web", "macos"]),
20286
+ qa: external_exports.enum(["none", "web", "macos", "both"]),
20287
+ profile: external_exports.enum(["implementation", "documentation", "investigation"]).optional(),
20288
+ scopeFingerprint: external_exports.string().optional(),
20253
20289
  sizeClass: external_exports.enum(["S", "M", "L"]),
20254
20290
  tokenCap: external_exports.number().int().positive(),
20255
20291
  model: external_exports.string(),
@@ -20441,6 +20477,32 @@ function mergeGuidance(earlier, guidance) {
20441
20477
  return rounds.slice(-GUIDANCE_ROUNDS);
20442
20478
  }
20443
20479
 
20480
+ // src/core/build/eligibility.ts
20481
+ import { createHash as createHash3 } from "crypto";
20482
+ function eligibilityReason(issue) {
20483
+ if (issue.external) return "external";
20484
+ for (const label of [
20485
+ "ready-for-human",
20486
+ "needs-info",
20487
+ "needs-triage",
20488
+ "wontfix"
20489
+ ])
20490
+ if (issue.labels.includes(label)) return label;
20491
+ if (issue.state.type === "triage") return "needs-triage";
20492
+ if (!issue.labels.includes("ready-for-agent")) return "not-ready-for-agent";
20493
+ return void 0;
20494
+ }
20495
+ function scopeFingerprint(issue) {
20496
+ return createHash3("sha256").update(
20497
+ JSON.stringify([
20498
+ issue.title,
20499
+ issue.description ?? "",
20500
+ issue.acceptanceCriteria ?? "",
20501
+ issue.parentId
20502
+ ])
20503
+ ).digest("hex");
20504
+ }
20505
+
20444
20506
  // src/core/build/planner.ts
20445
20507
  function skipReason(issue, pullRequests) {
20446
20508
  const prs = issue.branchName ? pullRequests[issue.branchName] : void 0;
@@ -20449,6 +20511,8 @@ function skipReason(issue, pullRequests) {
20449
20511
  if (issue.state.type === "completed") return "done";
20450
20512
  if (issue.state.type === "canceled") return "canceled";
20451
20513
  if (prs?.merged) return "merged";
20514
+ const eligibility = eligibilityReason(issue);
20515
+ if (eligibility) return eligibility;
20452
20516
  return void 0;
20453
20517
  }
20454
20518
  var byIdentifier = (a, b) => a.localeCompare(b, "en", { numeric: true });
@@ -20492,6 +20556,7 @@ function shownFromFill(fill) {
20492
20556
  return {
20493
20557
  areas: fill.areas,
20494
20558
  qa: fill.qa,
20559
+ ...fill.profile ? { profile: fill.profile } : {},
20495
20560
  sizeClass: fill.sizeClass,
20496
20561
  tokenCap: limits.maxRunTokens,
20497
20562
  model: limits.model,
@@ -20590,11 +20655,21 @@ function planBuild(context) {
20590
20655
  ({ children, blockers } = blockingGraph(entries));
20591
20656
  }
20592
20657
  const held = new Set(
20593
- skipped.filter((skip) => skip.reason === "ready-for-human").map((skip) => skip.id)
20658
+ skipped.filter(
20659
+ (skip) => !["done", "merged", "done-pr-open", "parent-only"].includes(
20660
+ skip.reason
20661
+ )
20662
+ ).map((skip) => skip.id)
20594
20663
  );
20595
20664
  const waitsFor = (issue) => [
20596
20665
  .../* @__PURE__ */ new Set([
20597
- ...issue.blockedBy.filter((id2) => blockers.has(id2) || held.has(id2)),
20666
+ ...issue.blockedBy.filter(
20667
+ (id2) => !input.issues.some(
20668
+ (each) => each.id === id2 && (each.state.type === "completed" || skipped.some(
20669
+ (skip) => skip.id === id2 && skip.reason === "merged"
20670
+ ))
20671
+ )
20672
+ ),
20598
20673
  ...children.get(issue.id) ?? [],
20599
20674
  ...input.issues.filter((each) => each.parentId === issue.id && held.has(each.id)).map((each) => each.id)
20600
20675
  ])
@@ -20624,6 +20699,7 @@ function planBuild(context) {
20624
20699
  title: issue.title,
20625
20700
  blockedBy: waitsFor(issue),
20626
20701
  ...shownFromFill(fill),
20702
+ scopeFingerprint: scopeFingerprint(issue),
20627
20703
  workflowName: workflowName2
20628
20704
  });
20629
20705
  workflows.push({
@@ -21195,7 +21271,7 @@ async function closeOrphanedRuns(store, {
21195
21271
  }
21196
21272
 
21197
21273
  // ../../modules/runtime/src/run-artifacts.ts
21198
- import { createHash as createHash3 } from "crypto";
21274
+ import { createHash as createHash4 } from "crypto";
21199
21275
  import { mkdirSync as mkdirSync2, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
21200
21276
  import { tmpdir as tmpdir5 } from "os";
21201
21277
  import { dirname as dirname4, join as join7 } from "path";
@@ -21238,7 +21314,7 @@ function runArtifactEmitter(directory, record, midTurnMinGapMs = MID_TURN_MIN_GA
21238
21314
  } catch {
21239
21315
  continue;
21240
21316
  }
21241
- const hash = createHash3("sha256").update(bytes).digest("hex");
21317
+ const hash = createHash4("sha256").update(bytes).digest("hex");
21242
21318
  if (emitted[name] === hash) continue;
21243
21319
  let json;
21244
21320
  try {
@@ -21943,6 +22019,8 @@ export {
21943
22019
  repoLayoutAt,
21944
22020
  approvalAnswer,
21945
22021
  mergeGuidance,
22022
+ eligibilityReason,
22023
+ scopeFingerprint,
21946
22024
  planBuild,
21947
22025
  finalizePlan,
21948
22026
  fallbackBranch,
@@ -21967,4 +22045,4 @@ export {
21967
22045
  runMachineRunners,
21968
22046
  writeIdentityOnce
21969
22047
  };
21970
- //# sourceMappingURL=chunk-2N7IBKJF.js.map
22048
+ //# sourceMappingURL=chunk-D246FR4I.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runMachineRunners
3
- } from "./chunk-2N7IBKJF.js";
3
+ } from "./chunk-D246FR4I.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-Y6XJPUR2.js.map
16
+ //# sourceMappingURL=chunk-D5VZT7M3.js.map
@@ -21281,7 +21281,7 @@ var GATES = [
21281
21281
  ];
21282
21282
 
21283
21283
  // 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 "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\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",\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- `qa`: `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`),\nthen 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';
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';
21285
21285
 
21286
21286
  // src/core/workflows/builtins/orchestrator/index.ts
21287
21287
  var ORCHESTRATOR_WORKFLOW_NAME = "orchestrator";
@@ -21440,6 +21440,14 @@ Journal's last line.
21440
21440
 
21441
21441
  {instructions}
21442
21442
 
21443
+ ## Evidence receipts
21444
+
21445
+ Progress is not proof that a step passed. Follow each Finish instruction only after its acceptance condition holds.
21446
+ Record actual results, commands, output paths and acceptance criteria, never a generic claim of success.
21447
+ Commit repository changes before recording validation evidence. Changes to HEAD or source invalidate validation
21448
+ receipts: rerun affected tests, review, checks and QA, including after a rebase, before verifying the Merge step.
21449
+ Keep evidence files under .athena/evidence/ and do not commit them after validation, since a new commit invalidates receipts.
21450
+
21443
21451
  ## Gates
21444
21452
 
21445
21453
  Work through the gates in this order. Start each gate by running the command its "Start" line gives, which reports the
@@ -21464,6 +21472,9 @@ is in \`ignored\`: it does not block the merge and its name goes in the evidence
21464
21472
  empty, wait a minute and run it again. When \`failed\` is not empty, fix the change; if it still fails, the Merge gate
21465
21473
  cannot pass. Never judge a check by its timing.
21466
21474
 
21475
+ Immediately before merging, verify the current revision again: {mergeVerify}. If evidence is stale, rerun the
21476
+ selected validation steps and record fresh receipts before continuing.
21477
+
21467
21478
  Merge with \`gh pr merge --squash --delete-branch --match-head-commit <sha>\`, where \`<sha>\` is HEAD after the last
21468
21479
  push. When it exits non-zero, \`gh pr view --json state\` decides: \`MERGED\` means the merge happened (a worktree
21469
21480
  cannot check out main afterwards); anything else means it did not.
@@ -21506,12 +21517,13 @@ No gate text: the loop's caps apply.`;
21506
21517
 
21507
21518
  // src/core/workflows/builtins/build-child/render.ts
21508
21519
  var RENDERER_RULES = {
21509
- alwaysSkills: ["tdd", "code-review"],
21520
+ alwaysSkills: [],
21521
+ profiles: ["implementation", "documentation", "investigation"],
21510
21522
  alwaysMcpServers: ["linear"],
21511
21523
  areaSkills: {
21512
21524
  macos: ["swiftui-expert-skill", "apple-docs", "apple-hig-components"]
21513
21525
  },
21514
- qaMcpServers: { web: ["drisp-browser"] },
21526
+ qaMcpServers: { web: ["drisp-browser"], both: ["drisp-browser"] },
21515
21527
  npmTestAreas: ["cli", "hub", "protocol"],
21516
21528
  /**
21517
21529
  * The Checks gate's commands, resolved from the fill's areas and the repo's layout at the plan's commit: a repo
@@ -21574,6 +21586,7 @@ var RENDERER_RULES = {
21574
21586
  shell: [
21575
21587
  "drisp build checks",
21576
21588
  "drisp build step",
21589
+ "drisp build merged",
21577
21590
  "gh pr checks",
21578
21591
  "gh pr create",
21579
21592
  "gh pr list",
@@ -21615,7 +21628,10 @@ var LAYOUT_MARKERS = [
21615
21628
  "apps/cli/package.json"
21616
21629
  ];
21617
21630
  function checkCommands(fill, layout) {
21631
+ if (fill.checks) return [...fill.checks];
21618
21632
  const rules = RENDERER_RULES.checks;
21633
+ if (fill.profile === "documentation" || fill.profile === "investigation")
21634
+ return [...rules.otherwise];
21619
21635
  if (layout.includes("Makefile"))
21620
21636
  return [
21621
21637
  ...rules.rootMakefile,
@@ -21634,7 +21650,11 @@ function checkCommands(fill, layout) {
21634
21650
  function installCommand(fill, layout) {
21635
21651
  const { install } = RENDERER_RULES.checks;
21636
21652
  if (!layout.includes(install.needs)) return void 0;
21637
- const needsNode = layout.includes("Makefile") || checkCommands(fill, layout).some((command) => /^np[mx] /.test(command));
21653
+ const needsNode = checkCommands(fill, layout).some(
21654
+ (command) => /^np[mx] /.test(command) || layout.includes("Makefile") && RENDERER_RULES.checks.rootMakefile.some(
21655
+ (rootCommand) => rootCommand === command
21656
+ )
21657
+ );
21638
21658
  return needsNode ? install.command : void 0;
21639
21659
  }
21640
21660
  var GENERATED_NAME = /^build--([a-z0-9]+)--([A-Z][A-Z0-9]*-[0-9]+)$/;
@@ -21652,6 +21672,8 @@ function renderedSkills(fill) {
21652
21672
  return [
21653
21673
  .../* @__PURE__ */ new Set([
21654
21674
  ...RENDERER_RULES.alwaysSkills,
21675
+ ...usesTestFirst(fill) ? ["tdd"] : [],
21676
+ ...usesReview(fill) ? ["code-review"] : [],
21655
21677
  ...fill.areas.flatMap((area) => RENDERER_RULES.areaSkills[area] ?? [])
21656
21678
  ])
21657
21679
  ].sort();
@@ -21664,8 +21686,24 @@ function renderedMcpServers(fill) {
21664
21686
  ])
21665
21687
  ].sort();
21666
21688
  }
21689
+ function shellCommands(fill) {
21690
+ const commands = [...RENDERER_RULES.permissions.shell];
21691
+ return fill.profile !== "investigation" ? commands : commands.filter(
21692
+ (command) => ![
21693
+ "drisp build merged",
21694
+ "gh pr create",
21695
+ "gh pr merge",
21696
+ "git add",
21697
+ "git commit",
21698
+ "git push",
21699
+ "git rebase",
21700
+ "git restore"
21701
+ ].includes(command)
21702
+ );
21703
+ }
21667
21704
  function renderedPermissions(fill) {
21668
- const { edit, shell, deny } = RENDERER_RULES.permissions;
21705
+ const { edit, deny } = RENDERER_RULES.permissions;
21706
+ const shell = shellCommands(fill);
21669
21707
  return {
21670
21708
  allow: [
21671
21709
  ...edit,
@@ -21675,10 +21713,29 @@ function renderedPermissions(fill) {
21675
21713
  deny: [...deny]
21676
21714
  };
21677
21715
  }
21678
- function gateTitles(fill) {
21679
- return GATES.filter((gate) => gate.title !== "QA" || fill.qa !== "none").map(
21680
- (gate) => gate.title
21716
+ var usesTestFirst = (fill) => fill.testFirst?.enabled ?? (!fill.profile || fill.profile === "implementation");
21717
+ var usesReview = (fill) => fill.review?.enabled ?? (!fill.profile || fill.profile === "implementation");
21718
+ function selectedGates(fill) {
21719
+ const gates = GATES.filter(
21720
+ (gate) => (gate.title !== "QA" || fill.qa !== "none") && (gate.title !== "Test first" || usesTestFirst(fill)) && (gate.title !== "Code review" || usesReview(fill)) && (fill.profile !== "investigation" || !["Pull request", "Merge"].includes(gate.title))
21721
+ ).map(
21722
+ (gate) => fill.profile === "investigation" && gate.title === "Checks" ? {
21723
+ ...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"
21725
+ } : fill.profile === "investigation" && gate.title === "Evidence" ? {
21726
+ ...gate,
21727
+ 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
21681
21729
  );
21730
+ if (!usesTestFirst(fill))
21731
+ gates.splice(1, 0, {
21732
+ title: fill.profile === "investigation" ? "Investigate" : "Implement",
21733
+ passesWhen: fill.profile === "investigation" ? "the question is investigated and findings address every acceptance criterion; no repository changes are made" : "the requested change is implemented and each acceptance criterion is checked"
21734
+ });
21735
+ return gates;
21736
+ }
21737
+ function gateTitles(fill) {
21738
+ return selectedGates(fill).map((gate) => gate.title);
21682
21739
  }
21683
21740
  function summarizeInstructions(instructions) {
21684
21741
  const paragraph = (instructions.trim().split(/\n\s*\n/)[0] ?? "").replace(/\s+/g, " ").trim();
@@ -21693,21 +21750,29 @@ function checksText(commands) {
21693
21750
  return quoted.length === 1 ? quoted[0] : `${quoted.slice(0, -1).join(", ")} and ${quoted.at(-1)}`;
21694
21751
  }
21695
21752
  var fillSlots = (template, slots) => template.replace(/\{(\w+)\}/g, (whole, key) => slots[key] ?? whole);
21753
+ function selectionFlags(fill) {
21754
+ return `${fill.profile ? ` --profile ${fill.profile}` : ""}${fill.testFirst ? ` --test-first ${fill.testFirst.enabled}` : ""}${fill.review ? ` --review ${fill.review.enabled}` : ""}`;
21755
+ }
21696
21756
  function renderStages(fill, checks, install) {
21697
- const gates = GATES.filter((gate) => gate.title !== "QA" || fill.qa !== "none");
21757
+ const gates = selectedGates(fill);
21698
21758
  return gates.map((gate, index) => {
21699
21759
  const passesWhen = fillSlots(gate.passesWhen, {
21700
21760
  install: install ? `after \`${install}\`, ` : "",
21701
21761
  checks: checksText(checks),
21702
- qa: fill.qa === "none" ? "" : QA_SECTIONS[fill.qa]
21762
+ qa: fill.qa === "none" ? "" : fill.qa === "both" ? `${QA_SECTIONS.web}; also ${QA_SECTIONS.macos}` : QA_SECTIONS[fill.qa]
21703
21763
  });
21764
+ const command = `drisp build step ${index + 1} --qa ${fill.qa}${selectionFlags(fill)}`;
21704
21765
  const stage = fillSlots(STAGE_TEMPLATE, {
21705
21766
  n: String(index + 1),
21706
21767
  title: gate.title,
21707
- stepCommand: `drisp build step ${index + 1} --qa ${fill.qa}`,
21768
+ stepCommand: command,
21708
21769
  passesWhen
21709
21770
  });
21710
- return stage + (gate.cannotPass ? fillSlots(CANNOT_PASS_TEMPLATE, { cannotPass: gate.cannotPass }) : NO_GATE_TEXT_TEMPLATE);
21771
+ return stage + (["Merge", "Evidence"].includes(gate.title) ? `
21772
+
21773
+ Before doing this step: run \`${command} --verify\`. Missing or stale evidence must be refreshed first.` : "") + `
21774
+
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);
21711
21776
  }).join("\n\n");
21712
21777
  }
21713
21778
  function render(fill, workflowName, layout) {
@@ -21717,7 +21782,7 @@ function render(fill, workflowName, layout) {
21717
21782
  const limits = sizeClassLimits(fill.sizeClass);
21718
21783
  const workflow = {
21719
21784
  name: workflowName,
21720
- description: fillSlots(DESCRIPTION_TEMPLATE, {
21785
+ description: fill.profile === "investigation" ? `Investigates ${parsed.identifier} and delivers findings with acceptance evidence.` : fillSlots(DESCRIPTION_TEMPLATE, {
21721
21786
  identifier: parsed.identifier
21722
21787
  }),
21723
21788
  promptTemplate: "{input}",
@@ -21734,12 +21799,40 @@ function render(fill, workflowName, layout) {
21734
21799
  permissions: renderedPermissions(fill)
21735
21800
  };
21736
21801
  const checks = checkCommands(fill, layout);
21737
- const workflowMd = fillSlots(WORKFLOW_MD_TEMPLATE, {
21802
+ let template = WORKFLOW_MD_TEMPLATE;
21803
+ if (!usesReview(fill))
21804
+ template = template.replaceAll(
21805
+ "both reviewers pass;",
21806
+ "review is omitted by the approved plan;"
21807
+ ).replaceAll(
21808
+ "Review: Standards \u2713 \xB7 Spec \u2713",
21809
+ "Review: omitted by the approved plan"
21810
+ );
21811
+ if (fill.profile === "investigation") {
21812
+ template = template.replace(
21813
+ /## Merge gate[\s\S]*?## Linear/,
21814
+ "## Findings\n\nPost findings and acceptance evidence on the Linear issue. No pull request or merge is required.\n\n## Linear"
21815
+ );
21816
+ template = template.replace(
21817
+ "Your Linear writes are exactly: In Progress in Explore, `PR: <url>` in Pull request, the evidence comment and Done in\nEvidence.",
21818
+ "Your Linear writes are In Progress in Explore, findings and evidence, and Done in Evidence."
21819
+ );
21820
+ }
21821
+ const workflowMd = fillSlots(template, {
21738
21822
  identifier: parsed.identifier,
21739
- instructions: fill.instructions.trim(),
21823
+ instructions: [
21824
+ fill.instructions.trim(),
21825
+ fill.rationale ? `Plan rationale: ${fill.rationale}` : "",
21826
+ fill.testFirst ? `Test first: ${fill.testFirst.enabled ? "included" : "omitted"} \u2014 ${fill.testFirst.reason}` : "",
21827
+ fill.review ? `Code review: ${fill.review.enabled ? "included" : "omitted"} \u2014 ${fill.review.reason}` : "",
21828
+ ...(fill.acceptanceEvidence ?? []).map(
21829
+ (item) => `Acceptance evidence: ${item}`
21830
+ )
21831
+ ].filter(Boolean).join("\n\n"),
21740
21832
  gates: renderStages(fill, checks, installCommand(fill, layout)),
21833
+ mergeVerify: `\`drisp build step ${gateTitles(fill).indexOf("Merge") + 1} --qa ${fill.qa}${selectionFlags(fill)} --verify\``,
21741
21834
  checksEvidence: checks.map((command) => `${command} \u2713`).join(" \xB7 "),
21742
- shellCommands: checksText([...RENDERER_RULES.permissions.shell])
21835
+ shellCommands: checksText(shellCommands(fill))
21743
21836
  });
21744
21837
  return { workflowJson: `${JSON.stringify(workflow, null, 2)}
21745
21838
  `, workflowMd };
@@ -24254,4 +24347,4 @@ export {
24254
24347
  compileWorkflowPlan,
24255
24348
  collectMcpServersWithOptions
24256
24349
  };
24257
- //# sourceMappingURL=chunk-W57EQYCM.js.map
24350
+ //# sourceMappingURL=chunk-HZ6GRLOM.js.map
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-D246FR4I.js";
144
146
  import {
145
147
  generateId as generateId2
146
148
  } from "./chunk-PQIB66SG.js";
@@ -198,7 +200,7 @@ import {
198
200
  workflowRegistryDir,
199
201
  writeGlobalConfig,
200
202
  writeProjectConfig
201
- } from "./chunk-W57EQYCM.js";
203
+ } from "./chunk-HZ6GRLOM.js";
202
204
  import {
203
205
  __toESM
204
206
  } from "./chunk-G3PMV62Z.js";
@@ -13615,7 +13617,7 @@ async function recoveryIdentity(runId) {
13615
13617
  }
13616
13618
 
13617
13619
  // src/app/entry/buildCommand.ts
13618
- import { execFileSync } from "child_process";
13620
+ import { execFileSync as execFileSync2 } from "child_process";
13619
13621
 
13620
13622
  // src/core/build/planCommand.ts
13621
13623
  import fs6 from "fs";
@@ -13755,7 +13757,7 @@ function runBuildPlan(deps) {
13755
13757
  const fixturePullRequests = fixtureDir ? readJson(path6.join(fixtureDir, "pull-requests.json")) : void 0;
13756
13758
  const pullRequests = fixturePullRequests ?? Object.fromEntries(
13757
13759
  input.issues.filter(
13758
- (issue) => issue.branchName && issue.state.type !== "canceled" && !issue.labels.includes("ready-for-human")
13760
+ (issue) => issue.branchName && issue.state.type !== "canceled" && !eligibilityReason(issue)
13759
13761
  ).map((issue) => [
13760
13762
  issue.branchName,
13761
13763
  deps.pullRequests(deps.folder, issue.branchName)
@@ -14266,8 +14268,22 @@ function occupies(view) {
14266
14268
  function assessTick(facts) {
14267
14269
  const { entries, orchestratorRunId } = facts;
14268
14270
  const state = structuredClone(facts.state);
14271
+ for (const [id, reason] of Object.entries(state.needsPerson))
14272
+ if (reason.startsWith("Issue is no longer eligible") || reason.startsWith("Waiting for prerequisite"))
14273
+ delete state.needsPerson[id];
14269
14274
  const views = new Map(facts.children.map((view) => [view.runId, view]));
14270
14275
  const issues = new Map(facts.issues.map((issue) => [issue.id, issue]));
14276
+ const parentOnly = new Set(
14277
+ facts.skipped?.filter((issue) => issue.reason === "parent-only").map((issue) => issue.id) ?? []
14278
+ );
14279
+ const expandBlocker = (id, seen = /* @__PURE__ */ new Set()) => {
14280
+ if (!parentOnly.has(id) || seen.has(id)) return [id];
14281
+ const children = facts.issues.filter((issue) => issue.parentId === id);
14282
+ if (!children.length) return [id];
14283
+ return children.flatMap(
14284
+ (issue) => expandBlocker(issue.id, /* @__PURE__ */ new Set([...seen, id]))
14285
+ );
14286
+ };
14271
14287
  const attemptsOf = (id) => state.attempts[id] ?? [];
14272
14288
  for (const entry of entries) {
14273
14289
  const prefix = `${orchestratorRunId}:${entry.identifier}:a`;
@@ -14310,7 +14326,7 @@ function assessTick(facts) {
14310
14326
  const status = /* @__PURE__ */ new Map();
14311
14327
  const skippedBlocker = (id) => {
14312
14328
  const linear = issues.get(id)?.state.type;
14313
- return linear === "completed" || linear === "canceled" ? "done" : "blocked";
14329
+ return linear === "completed" ? "done" : "blocked";
14314
14330
  };
14315
14331
  const repair = [];
14316
14332
  const repaired = new Set(state.repaired ?? []);
@@ -14321,7 +14337,7 @@ function assessTick(facts) {
14321
14337
  const linear = issues.get(entry.id)?.state.type;
14322
14338
  const statusOf = () => {
14323
14339
  if (linear === "completed" || linear === "canceled") return "done";
14324
- const prs = prsOf(entry);
14340
+ const prs = entry.profile === "investigation" ? void 0 : prsOf(entry);
14325
14341
  if (prs?.merged) {
14326
14342
  if (!repaired.has(entry.id) && !(newest && occupies(view))) {
14327
14343
  const prUrl = prs.mergedUrl ?? branchOf(entry);
@@ -14336,18 +14352,36 @@ function assessTick(facts) {
14336
14352
  return "done";
14337
14353
  }
14338
14354
  if (newest && (!view || !view.closed)) return "live";
14355
+ const current = issues.get(entry.id);
14356
+ if (!current || eligibilityReason(current)) {
14357
+ state.needsPerson[entry.id] = `Issue is no longer eligible (${current ? eligibilityReason(current) : "missing from Linear"}). Update Linear, then Build again.`;
14358
+ return "needs_person";
14359
+ }
14360
+ if (entry.scopeFingerprint && entry.scopeFingerprint !== scopeFingerprint(current)) {
14361
+ state.needsPerson[entry.id] = "Issue scope changed after approval. Build again to review the revised plan.";
14362
+ return "needs_person";
14363
+ }
14339
14364
  if (newest?.outcome === "template_mismatch") return "replan";
14340
14365
  if (newest?.outcome === "completed")
14341
- state.needsPerson[entry.id] ??= BUILD_STRINGS.completedUnmerged;
14366
+ 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
14367
  if (state.needsPerson[entry.id] || newest && !mayRetry(attempts))
14343
14368
  return "needs_person";
14344
- const blockers = entry.blockedBy.map(
14345
- (id) => status.get(id) ?? skippedBlocker(id)
14346
- );
14369
+ const blockers = [
14370
+ .../* @__PURE__ */ new Set([
14371
+ ...entry.blockedBy,
14372
+ ...current.blockedBy.flatMap((id) => expandBlocker(id))
14373
+ ])
14374
+ ].map((id) => {
14375
+ if (issues.get(id)?.state.type === "canceled") return "blocked";
14376
+ return status.get(id) ?? (entries.some((each) => each.id === id) && issues.get(id)?.state.type !== "completed" ? "pending" : skippedBlocker(id));
14377
+ });
14347
14378
  if (blockers.some(
14348
14379
  (b) => b === "needs_person" || b === "replan" || b === "blocked"
14349
- ))
14380
+ )) {
14381
+ if (current.blockedBy.some((id) => !entry.blockedBy.includes(id)))
14382
+ state.needsPerson[entry.id] = "Waiting for prerequisite work. Resolve its readiness, completion or cancellation in Linear, then resume.";
14350
14383
  return "blocked";
14384
+ }
14351
14385
  return blockers.every((b) => b === "done") ? "ready" : "pending";
14352
14386
  };
14353
14387
  status.set(entry.id, statusOf());
@@ -14399,6 +14433,12 @@ function tickAction(input) {
14399
14433
  const done = input.entries.filter(
14400
14434
  (entry) => input.status.get(entry.id) === "done"
14401
14435
  ).length;
14436
+ if (done !== input.entries.length)
14437
+ return {
14438
+ action: "park",
14439
+ reason: "attention",
14440
+ 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.`
14441
+ };
14402
14442
  return {
14403
14443
  action: "complete",
14404
14444
  message: `Built ${done} of ${input.entries.length}`
@@ -14523,6 +14563,7 @@ async function runBuildTick(deps) {
14523
14563
  const entries = plan.groups.flatMap((group) => group.issues).filter((entry) => approved.has(entry.id));
14524
14564
  const issues = new Map(input.issues.map((issue) => [issue.id, issue]));
14525
14565
  const assessment = assessTick({
14566
+ skipped: plan.skipped,
14526
14567
  orchestratorRunId,
14527
14568
  entries,
14528
14569
  issues: input.issues,
@@ -14725,8 +14766,16 @@ function evaluateRemoteChecks(runs) {
14725
14766
  }
14726
14767
 
14727
14768
  // src/core/build/step.ts
14769
+ import { execFileSync } from "child_process";
14770
+ import { createHash as createHash2 } from "crypto";
14728
14771
  import fs8 from "fs";
14729
14772
  import path8 from "path";
14773
+ var receiptSchema = external_exports.strictObject({
14774
+ title: external_exports.string().min(1),
14775
+ revision: external_exports.string().regex(/^[0-9a-f]{64}$/),
14776
+ evidence: external_exports.string().min(1).max(16e3),
14777
+ kind: external_exports.literal("agent-attestation")
14778
+ });
14730
14779
  var QA_KINDS = fillSchema.shape.qa.options;
14731
14780
  function runBuildStep(input) {
14732
14781
  const directory = requireEnv(input.env, "DRISP_RUN_ARTIFACTS");
@@ -14735,7 +14784,22 @@ function runBuildStep(input) {
14735
14784
  throw new BuildUsageError(
14736
14785
  `--qa must be one of ${QA_KINDS.join(", ")}, not "${qa}".`
14737
14786
  );
14738
- const titles = gateTitles({ qa });
14787
+ const parsedProfile = fillSchema.shape.profile.safeParse(input.profile);
14788
+ if (!parsedProfile.success)
14789
+ throw new BuildUsageError("Unknown workflow profile.");
14790
+ const profile = parsedProfile.data;
14791
+ const selection = (value) => {
14792
+ if (value === void 0) return void 0;
14793
+ if (value !== "true" && value !== "false")
14794
+ throw new BuildUsageError("Step selection must be true or false.");
14795
+ return { enabled: value === "true", reason: "Approved workflow" };
14796
+ };
14797
+ const titles = gateTitles({
14798
+ qa,
14799
+ profile,
14800
+ testFirst: selection(input.testFirst),
14801
+ review: selection(input.review)
14802
+ });
14739
14803
  const index = /^[0-9]+$/.test(input.n ?? "") ? Number(input.n) : 0;
14740
14804
  if (index < 1 || index > titles.length)
14741
14805
  throw new BuildUsageError(
@@ -14745,10 +14809,56 @@ function runBuildStep(input) {
14745
14809
  throw new BuildUsageError(
14746
14810
  `DRISP_RUN_ARTIFACTS (${directory}) is not a directory.`
14747
14811
  );
14812
+ const receiptsPath = path8.join(directory, "gate_evidence.json");
14813
+ let receipts = [];
14814
+ if (fs8.existsSync(receiptsPath)) {
14815
+ try {
14816
+ receipts = external_exports.strictObject({ receipts: external_exports.array(receiptSchema).max(100) }).parse(JSON.parse(fs8.readFileSync(receiptsPath, "utf8"))).receipts;
14817
+ } catch {
14818
+ throw new BuildUsageError("Gate evidence is invalid.");
14819
+ }
14820
+ }
14821
+ const revision = receipts.length || input.complete || input.verify ? workRevision(input.cwd ?? process.cwd()) : "";
14822
+ const fresh = (receipt) => ["Explore", "Pull request", "Evidence", "Merge"].includes(receipt.title) || receipt.revision === revision;
14823
+ if (input.verify || input.complete && titles[index - 1] === "Evidence") {
14824
+ const missing = titles.slice(0, index - 1).filter((title) => !receipts.some((r) => r.title === title && fresh(r)));
14825
+ if (missing.length)
14826
+ throw new BuildUsageError(
14827
+ `Missing or stale evidence: ${missing.join(", ")}. Repeat those steps on the current revision.`
14828
+ );
14829
+ }
14830
+ if (input.complete) {
14831
+ if (!input.evidence)
14832
+ throw new BuildUsageError(
14833
+ "--complete requires --evidence <file> containing the result and supporting paths."
14834
+ );
14835
+ let evidence;
14836
+ try {
14837
+ evidence = fs8.readFileSync(
14838
+ path8.resolve(input.cwd ?? process.cwd(), input.evidence),
14839
+ "utf8"
14840
+ ).trim();
14841
+ } catch {
14842
+ throw new BuildUsageError("Cannot read the evidence file.");
14843
+ }
14844
+ if (!evidence || evidence.length > 16e3)
14845
+ throw new BuildUsageError("Evidence must contain 1\u201316000 characters.");
14846
+ receipts = receipts.filter((r) => r.title !== titles[index - 1]);
14847
+ receipts.push({
14848
+ title: titles[index - 1],
14849
+ revision,
14850
+ evidence,
14851
+ kind: "agent-attestation"
14852
+ });
14853
+ fs8.writeFileSync(receiptsPath, JSON.stringify({ receipts }) + "\n");
14854
+ }
14748
14855
  const artifact = {
14749
14856
  index,
14750
14857
  title: titles[index - 1],
14751
- total: titles.length
14858
+ total: titles.length,
14859
+ ...receipts.length ? {
14860
+ completed: receipts.filter(fresh).map((r) => titles.indexOf(r.title) + 1).filter((n) => n > 0)
14861
+ } : {}
14752
14862
  };
14753
14863
  fs8.writeFileSync(
14754
14864
  path8.join(directory, "step.json"),
@@ -14757,6 +14867,23 @@ function runBuildStep(input) {
14757
14867
  );
14758
14868
  return artifact;
14759
14869
  }
14870
+ function workRevision(cwd) {
14871
+ const git2 = (...args) => execFileSync("git", args, {
14872
+ cwd,
14873
+ encoding: "utf8",
14874
+ stdio: ["ignore", "pipe", "pipe"]
14875
+ });
14876
+ try {
14877
+ const hash = createHash2("sha256").update(git2("rev-parse", "HEAD")).update(git2("diff", "HEAD", "--", ".", ":!.athena/evidence"));
14878
+ for (const name of git2("ls-files", "--others", "--exclude-standard", "-z").split("\0").filter(Boolean).sort()) {
14879
+ if (name.startsWith(".athena/evidence/")) continue;
14880
+ hash.update(name).update(fs8.readFileSync(path8.join(cwd, name)));
14881
+ }
14882
+ return hash.digest("hex");
14883
+ } catch {
14884
+ throw new BuildUsageError("Evidence must be recorded in a git worktree.");
14885
+ }
14886
+ }
14760
14887
 
14761
14888
  // src/core/build/merged.ts
14762
14889
  import fs9 from "fs";
@@ -14816,17 +14943,20 @@ function runBuildMerged(input) {
14816
14943
  var USAGE3 = `Usage: drisp build plan --input <path>
14817
14944
  drisp build tick --input <path> [--rebase-failed <IDENTIFIER>]...
14818
14945
  drisp build checks
14819
- drisp build step <n> [--qa none|web|macos]
14946
+ 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]
14820
14948
  drisp build merged [<pr url or number>]
14821
14949
  plan and tick run inside an Orchestrator Run (DRISP_RUN_ID, DRISP_RUNNER_ID and DRISP_RUN_ARTIFACTS set; tick also
14822
14950
  DRISP_RUN_TOKEN and DRISP_HUB_URL). DRISP_BUILD_PLAN_INPUT=<path> stands in for plan's --input, with
14823
14951
  fills/<IDENTIFIER>.json beside it. checks runs in a child's worktree: the merge gate's verdict on HEAD's GitHub runs.
14824
14952
  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.
14953
+ selected build-child profile (QA is a gate unless --qa none) and refuses an index outside the list.
14954
+ --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/.
14826
14956
  merged runs in a child's worktree after its pull request merged: it writes the pull_request artifact
14827
14957
  {url, number, sha, mergedAt} and refuses a pull request that is not merged.`;
14828
14958
  function githubPullRequests(folder, branch) {
14829
- const output = execFileSync(
14959
+ const output = execFileSync2(
14830
14960
  "gh",
14831
14961
  [
14832
14962
  "pr",
@@ -14865,7 +14995,7 @@ function hostCapabilities(folder) {
14865
14995
  }
14866
14996
  var defaultChecks = {
14867
14997
  head: (cwd) => git(cwd, ["rev-parse", "HEAD"]),
14868
- gh: (args, cwd) => execFileSync("gh", args, {
14998
+ gh: (args, cwd) => execFileSync2("gh", args, {
14869
14999
  cwd,
14870
15000
  encoding: "utf8",
14871
15001
  stdio: ["ignore", "pipe", "pipe"]
@@ -14954,6 +15084,13 @@ async function runBuildCommand(input) {
14954
15084
  output = runBuildStep({
14955
15085
  env: env2,
14956
15086
  n: input.positional?.[0],
15087
+ cwd: input.cwd,
15088
+ profile: input.flags.profile,
15089
+ testFirst: input.flags.testFirst,
15090
+ review: input.flags.review,
15091
+ complete: input.flags.complete,
15092
+ verify: input.flags.verify,
15093
+ evidence: input.flags.evidence,
14957
15094
  ...input.flags.qa ? { qa: input.flags.qa } : {}
14958
15095
  });
14959
15096
  break;
@@ -14978,7 +15115,7 @@ ${USAGE3}
14978
15115
  import process7 from "process";
14979
15116
 
14980
15117
  // src/harnesses/claude/system/doctorEnvironment.ts
14981
- import { execFileSync as execFileSync2 } from "child_process";
15118
+ import { execFileSync as execFileSync3 } from "child_process";
14982
15119
  import * as fs10 from "fs";
14983
15120
  import * as os4 from "os";
14984
15121
  import * as path10 from "path";
@@ -15056,7 +15193,7 @@ function readEnforcement(scopes) {
15056
15193
  }
15057
15194
  function tryRichAuth(claudeBinary) {
15058
15195
  try {
15059
- const output = execFileSync2(claudeBinary, ["auth", "status"], {
15196
+ const output = execFileSync3(claudeBinary, ["auth", "status"], {
15060
15197
  timeout: 1e4,
15061
15198
  encoding: "utf-8",
15062
15199
  stdio: ["ignore", "pipe", "pipe"]
@@ -15789,7 +15926,7 @@ var cli = meow(
15789
15926
  build plan --input <path> Plan a Build inside an Orchestrator Run (prints one JSON object)
15790
15927
  build tick --input <path> Run one step of a started Build (prints the next action; [--rebase-failed <ID>]...)
15791
15928
  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])
15929
+ build step <n> Report the Run's current step: writes the step run artifact ([--qa none|web|macos|both], --profile, --complete --evidence, --verify)
15793
15930
  build merged Record the merged pull request: writes the pull_request run artifact
15794
15931
  telemetry [action] Manage anonymous telemetry (enable/disable/status)
15795
15932
  doctor Diagnose Claude headless setup (use with --harness=claude)
@@ -15928,6 +16065,12 @@ var cli = meow(
15928
16065
  type: "string",
15929
16066
  isMultiple: true
15930
16067
  },
16068
+ profile: { type: "string" },
16069
+ testFirst: { type: "string" },
16070
+ review: { type: "string" },
16071
+ complete: { type: "boolean" },
16072
+ verify: { type: "boolean" },
16073
+ evidence: { type: "string" },
15931
16074
  qa: {
15932
16075
  type: "string"
15933
16076
  },
@@ -16024,7 +16167,7 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16024
16167
  await exitWith(1);
16025
16168
  return;
16026
16169
  }
16027
- const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-YANLNMTP.js");
16170
+ const { default: WorkflowInstallWizard } = await import("./WorkflowInstallWizard-VVEEJW25.js");
16028
16171
  const { waitUntilExit } = render(
16029
16172
  /* @__PURE__ */ jsx24(
16030
16173
  WorkflowInstallWizard,
@@ -16110,6 +16253,12 @@ Available commands: ${[...KNOWN_COMMANDS].join(", ")}`
16110
16253
  positional: buildArgs,
16111
16254
  flags: {
16112
16255
  ...typeof cli.flags.qa === "string" ? { qa: cli.flags.qa } : {},
16256
+ profile: cli.flags.profile,
16257
+ testFirst: cli.flags.testFirst,
16258
+ review: cli.flags.review,
16259
+ complete: cli.flags.complete,
16260
+ verify: cli.flags.verify,
16261
+ evidence: cli.flags.evidence,
16113
16262
  ...typeof cli.flags.input === "string" ? { input: cli.flags.input } : {},
16114
16263
  ...cli.flags.rebaseFailed?.length ? { rebaseFailed: cli.flags.rebaseFailed } : {}
16115
16264
  },
@@ -1,6 +1,6 @@
1
- import "./chunk-Y6XJPUR2.js";
2
- import "./chunk-2N7IBKJF.js";
1
+ import "./chunk-D5VZT7M3.js";
2
+ import "./chunk-D246FR4I.js";
3
3
  import "./chunk-PQIB66SG.js";
4
- import "./chunk-W57EQYCM.js";
4
+ import "./chunk-HZ6GRLOM.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-Y6XJPUR2.js";
2
- import "./chunk-2N7IBKJF.js";
1
+ import "./chunk-D5VZT7M3.js";
2
+ import "./chunk-D246FR4I.js";
3
3
  import "./chunk-PQIB66SG.js";
4
- import "./chunk-W57EQYCM.js";
4
+ import "./chunk-HZ6GRLOM.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.6",
20
+ "version": "0.7.7",
21
21
  "license": "MIT",
22
22
  "bin": {
23
23
  "athena": "dist/cli.js",