dev-loops 0.7.2 → 0.8.0

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.
Files changed (71) hide show
  1. package/.claude/.claude-plugin/plugin.json +1 -1
  2. package/.claude/agents/dev-loop.md +1 -1
  3. package/.claude/agents/refiner.md +2 -2
  4. package/.claude/agents/review.md +9 -12
  5. package/.claude/commands/loop-continue.md +2 -1
  6. package/.claude/commands/loop-enqueue.md +8 -1
  7. package/.claude/commands/loop-info.md +1 -1
  8. package/.claude/hooks/_bash-command-classify.mjs +7 -6
  9. package/.claude/hooks/_hook-decisions.mjs +5 -4
  10. package/.claude/skills/copilot-pr-followup/SKILL.md +5 -5
  11. package/.claude/skills/dev-loop/SKILL.md +7 -7
  12. package/.claude/skills/docs/acceptance-criteria-verification.md +12 -4
  13. package/.claude/skills/docs/anti-patterns.md +3 -3
  14. package/.claude/skills/docs/artifact-authority-contract.md +6 -4
  15. package/.claude/skills/docs/confirmation-rules.md +1 -1
  16. package/.claude/skills/docs/copilot-loop-operations.md +4 -2
  17. package/.claude/skills/docs/issue-intake-procedure.md +12 -3
  18. package/.claude/skills/docs/merge-preconditions.md +5 -3
  19. package/.claude/skills/docs/pr-lifecycle-contract.md +1 -1
  20. package/.claude/skills/docs/public-dev-loop-contract.md +2 -1
  21. package/.claude/skills/docs/retrospective-checkpoint-contract.md +9 -6
  22. package/.claude/skills/docs/validation-policy.md +1 -1
  23. package/.claude/skills/local-implementation/SKILL.md +3 -3
  24. package/.claude/skills/loop-grill/SKILL.md +34 -14
  25. package/AGENTS.md +1 -1
  26. package/CHANGELOG.md +30 -0
  27. package/README.md +95 -189
  28. package/agents/refiner.agent.md +2 -2
  29. package/agents/review.agent.md +9 -12
  30. package/extension/README.md +5 -4
  31. package/package.json +5 -4
  32. package/scripts/docs/validate-state-machine-conformance.mjs +78 -1
  33. package/scripts/github/_gate-names.mjs +5 -0
  34. package/scripts/github/capture-review-threads.mjs +2 -2
  35. package/scripts/github/detect-checkpoint-evidence.mjs +7 -7
  36. package/scripts/github/edit-issue.mjs +259 -0
  37. package/scripts/github/probe-ci-status.mjs +18 -0
  38. package/scripts/github/probe-copilot-review.mjs +24 -3
  39. package/scripts/github/reconcile-draft-gate.mjs +13 -13
  40. package/scripts/github/request-copilot-review.mjs +17 -16
  41. package/scripts/github/upsert-checkpoint-verdict.mjs +25 -23
  42. package/scripts/github/verify-briefing-prefixes.mjs +224 -33
  43. package/scripts/github/write-gate-context.mjs +8 -4
  44. package/scripts/loop/_post-convergence-change.mjs +2 -2
  45. package/scripts/loop/_pr-runner-coordination.mjs +112 -13
  46. package/scripts/loop/check-retro-tooling.mjs +14 -9
  47. package/scripts/loop/copilot-pr-handoff.mjs +18 -14
  48. package/scripts/loop/detect-copilot-loop-state.mjs +11 -11
  49. package/scripts/loop/detect-internal-only-pr.mjs +6 -6
  50. package/scripts/loop/detect-pr-gate-coordination-state.mjs +117 -15
  51. package/scripts/loop/detect-refinement-grill-state.mjs +136 -0
  52. package/scripts/loop/run-watch-cycle.mjs +42 -7
  53. package/scripts/loop/sanctioned-commands.mjs +1 -0
  54. package/scripts/pages/build-state-atlas.mjs +15 -0
  55. package/scripts/projects/add-queue-item.mjs +87 -4
  56. package/skills/copilot-pr-followup/SKILL.md +5 -5
  57. package/skills/dev-loop/SKILL.md +2 -2
  58. package/skills/docs/acceptance-criteria-verification.md +12 -4
  59. package/skills/docs/anti-patterns.md +3 -3
  60. package/skills/docs/artifact-authority-contract.md +6 -4
  61. package/skills/docs/confirmation-rules.md +1 -1
  62. package/skills/docs/copilot-loop-operations.md +4 -2
  63. package/skills/docs/issue-intake-procedure.md +12 -3
  64. package/skills/docs/merge-preconditions.md +5 -3
  65. package/skills/docs/pr-lifecycle-contract.md +1 -1
  66. package/skills/docs/public-dev-loop-contract.md +2 -1
  67. package/skills/docs/required-rules.json +17 -1
  68. package/skills/docs/retrospective-checkpoint-contract.md +9 -6
  69. package/skills/docs/validation-policy.md +1 -1
  70. package/skills/local-implementation/SKILL.md +3 -3
  71. package/skills/loop-grill/SKILL.md +38 -17
@@ -10,6 +10,9 @@ import { runHandoff } from "./copilot-pr-handoff.mjs";
10
10
  import { STATE } from "@dev-loops/core/loop/copilot-loop-state";
11
11
  import { DEFAULT_POLL_INTERVAL_MS } from "@dev-loops/core/loop/policy-constants";
12
12
  import { detectCopilotSessionActivity } from "./detect-copilot-session-activity.mjs";
13
+ import { ensureAsyncRunnerOwnership } from "./_pr-runner-coordination.mjs";
14
+ import { resolveRepoRoot } from "./_repo-root-resolver.mjs";
15
+ import { resolveStaleRunnerMaxAgeMs } from "./_stale-runner-detection.mjs";
13
16
  import { parseArgs } from "node:util";
14
17
  import {
15
18
  EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY,
@@ -205,6 +208,33 @@ function buildWatchCycleContractTrace({
205
208
  },
206
209
  };
207
210
  }
211
+ // A blocking watch can run for the full external-healthy-wait timeout (30 min for
212
+ // Copilot/CI), which equals the runner-coordination stale window. Without a
213
+ // heartbeat the claim ages to stale across a single watch and the next step is
214
+ // refused. Refresh the lease on entry, on a periodic interval below
215
+ // the stale window, and once more on return so the resuming runner has a fresh claim.
216
+ async function runWatchHoldingLease(watchFactory, { repo, pr, env, cwd, ensureOwnershipImpl }) {
217
+ const heartbeat = async () => {
218
+ try {
219
+ // claimIfMissing:true self-heals a missing record but never stomps a live
220
+ // competitor (assertRunnerOwnership refuses to write on OWNERSHIP_LOST), so
221
+ // fail-closed competitor semantics are preserved.
222
+ await ensureOwnershipImpl({ repo, pr, env, cwd, claimIfMissing: true, requireExisting: false });
223
+ } catch {
224
+ // best-effort: a heartbeat failure must never affect the watch
225
+ }
226
+ };
227
+ const intervalMs = Math.max(1, Math.floor(resolveStaleRunnerMaxAgeMs({}, env) / 2));
228
+ await heartbeat();
229
+ const timer = setInterval(() => { void heartbeat(); }, intervalMs);
230
+ if (typeof timer.unref === "function") timer.unref();
231
+ try {
232
+ return await watchFactory();
233
+ } finally {
234
+ clearInterval(timer);
235
+ await heartbeat();
236
+ }
237
+ }
208
238
  export function parseWatchCycleCliArgs(argv) {
209
239
  const options = {
210
240
  help: false,
@@ -278,9 +308,11 @@ export async function runWatchCycle(
278
308
  detectCopilotSessionActivityImpl = detectCopilotSessionActivity,
279
309
  fetchPrHeadBranchImpl = fetchPrHeadBranch,
280
310
  watchWorkflowRunImpl = watchWorkflowRun,
311
+ ensureOwnershipImpl = ensureAsyncRunnerOwnership,
281
312
  detectSessionActivity = false,
282
313
  } = {},
283
314
  ) {
315
+ const leaseCwd = resolveRepoRoot(process.cwd());
284
316
  const handoff = await runHandoffImpl(options, { env, ghCommand });
285
317
  const result = {
286
318
  ok: true,
@@ -370,13 +402,16 @@ export async function runWatchCycle(
370
402
  session.activity === "active"
371
403
  && Number.isInteger(session.runId)
372
404
  ) {
373
- const workflowWatchResult = await watchWorkflowRunImpl(
374
- {
375
- repo: options.repo,
376
- runId: session.runId,
377
- timeoutMs: persistentWatchTimeoutMs,
378
- },
379
- { env, ghCommand },
405
+ const workflowWatchResult = await runWatchHoldingLease(
406
+ () => watchWorkflowRunImpl(
407
+ {
408
+ repo: options.repo,
409
+ runId: session.runId,
410
+ timeoutMs: persistentWatchTimeoutMs,
411
+ },
412
+ { env, ghCommand },
413
+ ),
414
+ { repo: options.repo, pr: options.pr, env, cwd: leaseCwd, ensureOwnershipImpl },
380
415
  );
381
416
  workflowRunWatch = {
382
417
  attempted: true,
@@ -37,6 +37,7 @@ export const SANCTIONED_COMMANDS = Object.freeze({
37
37
  // Metadata edits.
38
38
  edits: Object.freeze({
39
39
  "pr-body-title-assignee-milestone": "scripts/github/edit-pr.mjs",
40
+ "issue-body-title-assignee-milestone": "scripts/github/edit-issue.mjs",
40
41
  "issue-comment": "scripts/github/comment-issue.mjs",
41
42
  }),
42
43
 
@@ -13,6 +13,7 @@
13
13
  // (NAV_CSS references --heading/--kicker/--accent-soft, all declared here).
14
14
  import { STATE, TRANSITIONS } from '../../packages/core/src/loop/copilot-loop-state.mjs';
15
15
  import { REVIEWER_STATE, REVIEWER_TRANSITIONS } from '../../packages/core/src/loop/reviewer-loop-state.mjs';
16
+ import { GRILL_STATE, GRILL_TRANSITIONS } from '../../packages/core/src/loop/refinement-grill-state.mjs';
16
17
  import { OUTER_STATE, OUTER_TRANSITIONS } from '../../packages/core/src/loop/conductor-routing.mjs';
17
18
  import { PUBLIC_DEV_LOOP_GATE_CONTRACT } from '../../packages/core/src/loop/public-dev-loop-routing-contract.mjs';
18
19
  import { PR_LIFECYCLE_STATES, PR_LIFECYCLE_TRANSITIONS } from '../../packages/core/src/loop/pr-lifecycle.mjs';
@@ -100,6 +101,10 @@ const reviewerDiagram = renderStateDiagram(
100
101
  edgesFromTransitions(Object.values(REVIEWER_STATE), REVIEWER_TRANSITIONS),
101
102
  Object.values(REVIEWER_STATE),
102
103
  );
104
+ const refinementGrillDiagram = renderStateDiagram(
105
+ edgesFromTransitions(Object.values(GRILL_STATE), GRILL_TRANSITIONS),
106
+ Object.values(GRILL_STATE),
107
+ );
103
108
  const outerDiagram = renderStateDiagram(
104
109
  edgesFromTransitions(Object.values(OUTER_STATE), OUTER_TRANSITIONS),
105
110
  Object.values(OUTER_STATE),
@@ -214,6 +219,16 @@ const SECTIONS = [
214
219
  ],
215
220
  diagram: reviewerDiagram,
216
221
  },
222
+ {
223
+ id: 'refinement-grill',
224
+ title: 'Refinement / grill sub-loop',
225
+ source: 'packages/core/src/loop/refinement-grill-state.mjs — GRILL_STATE + GRILL_TRANSITIONS',
226
+ prose: [
227
+ 'Before a slice enters the loop, the refinement grill runs as a closed, deterministic sub-loop over the issue, PR body, or plan file. It loads the target, detects spec gaps, and iterates detect-gaps to answer to synthesize to re-grill until it reaches a fixed point. The iteration lives entirely in the transition graph; the LLM answer and synthesis enter only as a bounded input consumed at the await_answers state, never as hidden orchestration in a coordinator script.',
228
+ 'Write-back synthesizes only sharpened Acceptance criteria, Definition of done, and Non-goals into the body — the raw Q&A transcript never lands in the artifact. An uncitable gap stops honestly at needs_human_handoff naming the question rather than fabricating an answer to force convergence, and any load or parse failure fails closed to blocked_needs_user_decision.',
229
+ ],
230
+ diagram: refinementGrillDiagram,
231
+ },
217
232
  {
218
233
  id: 'release-pipeline',
219
234
  title: 'Release pipeline',
@@ -4,7 +4,8 @@ import { runChild as _runChild } from "../_cli-primitives.mjs";
4
4
  import { resolveProjectSelector, findProject, applyDevloopsBoard } from "./_resolve-project.mjs";
5
5
  import { parseArgs } from "node:util";
6
6
  import { JQ_OUTPUT_PARSE_OPTIONS, JQ_OUTPUT_USAGE, emitResult, matchJqOutputToken } from "../lib/jq-output.mjs";
7
- import { loadStateColumnMap, LOGICAL_COLUMN } from "@dev-loops/core/loop/queue-board-sync";
7
+ import { loadStateColumnMap, LOGICAL_COLUMN, nonSuccessBoardColumn } from "@dev-loops/core/loop/queue-board-sync";
8
+ import { decideEnqueueRefinementGate, detectIssueRefinementArtifact } from "@dev-loops/core/loop/issue-refinement-artifact";
8
9
 
9
10
  const USAGE = `Usage: dev-loops queue add --repo <owner/name> [--project <number|id>] --item <number>
10
11
  dev-loops project add … (back-compat alias for "queue add")
@@ -24,10 +25,20 @@ Options:
24
25
  default, honors queue.statusColumns.next_up). Sugar
25
26
  for --column <that column>; cannot be combined with
26
27
  a conflicting --column/--status.
28
+ --auto Headless mode. When an issue targets the pickup
29
+ (next_up) column without a refinement artifact
30
+ (Acceptance criteria/DoD/linked doc), divert it
31
+ to the non-pickup park column instead of failing.
32
+ Without --auto, the same case throws
33
+ MISSING_REFINEMENT_ARTIFACT.
27
34
  --help, -h Show this help.
28
35
 
29
36
  Output (stdout):
30
- JSON: { ok: true, item: { itemId, issueNumber, prNumber, status, alreadyPresent } }
37
+ JSON: { ok: true, item: { itemId, issueNumber, prNumber, status, alreadyPresent },
38
+ refinement? }
39
+ refinement (present only when the pickup-column gate ran): either
40
+ { refined: true } or, on a headless --auto divert,
41
+ { refined: false, diverted: true, requestedColumn, parkedColumn, reason, missing }.
31
42
 
32
43
  ${JQ_OUTPUT_USAGE}
33
44
 
@@ -36,6 +47,8 @@ Exit codes:
36
47
  1 — usage or argument error
37
48
  2 — GitHub API error / invalid --jq filter
38
49
  3 — project, field, column, or issue/PR not found
50
+ 4 — issue targets the pickup column without a refinement artifact
51
+ (MISSING_REFINEMENT_ARTIFACT; interactive only, --auto diverts instead)
39
52
  `.trim();
40
53
 
41
54
  function parseCliArgs(argv) {
@@ -58,6 +71,7 @@ function parseCliArgs(argv) {
58
71
  column: { type: "string" },
59
72
  status: { type: "string" },
60
73
  "next-up": { type: "boolean" },
74
+ auto: { type: "boolean" },
61
75
  help: { type: "boolean", short: "h" },
62
76
  ...JQ_OUTPUT_PARSE_OPTIONS,
63
77
  },
@@ -113,6 +127,12 @@ function parseCliArgs(argv) {
113
127
  }
114
128
  args.nextUp = true;
115
129
  break;
130
+ case "auto":
131
+ if (token.value !== undefined) {
132
+ throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
133
+ }
134
+ args.auto = true;
135
+ break;
116
136
  default: {
117
137
  if (matchJqOutputToken(token, args, (t) => requireValue(t, "--jq requires a filter"))) break;
118
138
  throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
@@ -367,6 +387,7 @@ function classifyExitCode(err) {
367
387
  err.code === "INVALID_STATUS" || err.code === "INVALID_ARGS") return 1;
368
388
  if (err.code === "PROJECT_NOT_FOUND" || err.code === "FIELD_NOT_FOUND" || err.code === "COLUMN_NOT_FOUND" ||
369
389
  err.code === "ITEM_NOT_FOUND" || err.code === "CONTENT_NOT_FOUND") return 3;
390
+ if (err.code === "MISSING_REFINEMENT_ARTIFACT") return 4;
370
391
  return 2;
371
392
  }
372
393
 
@@ -502,6 +523,67 @@ async function main(args, { env = process.env, runChild, cwd = process.cwd() } =
502
523
  let issueNumber = fullResult.__typename === "Issue" ? itemNumber : null;
503
524
  let prNumber = fullResult.__typename === "PullRequest" ? itemNumber : null;
504
525
 
526
+ // 5b. Refinement-artifact gate: shift the draft-gate check
527
+ // LEFT to enqueue time so an un-refined issue never lands in the Next Up
528
+ // pickup column. Scoped to issues (not PRs) targeting the pickup column —
529
+ // create-pr.mjs auto-enqueues PRs into In Progress, which never trips this.
530
+ let refinement;
531
+ let effectiveTargetOption = targetOption;
532
+ let effectiveTargetStatus = targetStatus;
533
+ if (issueNumber !== null && targetStatus === nextUpColumn) {
534
+ const bodyResult = await child(
535
+ "gh",
536
+ ["issue", "view", String(issueNumber), "--repo", repo, "--json", "body"],
537
+ env,
538
+ );
539
+ if (bodyResult.code !== 0) {
540
+ const detail = bodyResult.stderr.trim() || `exit code ${bodyResult.code}`;
541
+ throw Object.assign(new Error(`gh issue view failed: ${detail}`), { code: "GH_API_ERROR" });
542
+ }
543
+ const bodyPayload = parseJsonText(bodyResult.stdout, { label: "gh issue view" });
544
+ const body = typeof bodyPayload?.body === "string" ? bodyPayload.body : "";
545
+ const artifact = detectIssueRefinementArtifact({ body, issueNumber });
546
+ const decision = decideEnqueueRefinementGate({ artifact, targetIsPickup: true, auto: !!args.auto });
547
+ if (decision.action === "block") {
548
+ throw Object.assign(new Error(decision.reason), {
549
+ code: "MISSING_REFINEMENT_ARTIFACT",
550
+ missing: decision.missing,
551
+ });
552
+ }
553
+ if (decision.action === "divert") {
554
+ const parkedColumn = nonSuccessBoardColumn(cwd);
555
+ if (parkedColumn === nextUpColumn) {
556
+ // A non-success park column equal to the pickup column would divert the
557
+ // un-refined item straight back into Next Up, defeating the gate. Fail
558
+ // closed on that misconfiguration rather than parking into pickup.
559
+ throw Object.assign(
560
+ new Error(`Park column "${parkedColumn}" (queue.nonSuccessStatus) is the pickup column; cannot divert an un-refined item into Next Up.`),
561
+ { code: "INVALID_STATUS" },
562
+ );
563
+ }
564
+ const parkedOption = statusField.options.find((o) => o.name === parkedColumn);
565
+ if (!parkedOption) {
566
+ const available = statusField.options.map((o) => o.name).join(", ");
567
+ throw Object.assign(
568
+ new Error(`Park column "${parkedColumn}" (queue.nonSuccessStatus) not found in Status field. Available: ${available}`),
569
+ { code: "COLUMN_NOT_FOUND" },
570
+ );
571
+ }
572
+ effectiveTargetOption = parkedOption;
573
+ effectiveTargetStatus = parkedColumn;
574
+ refinement = {
575
+ refined: false,
576
+ diverted: true,
577
+ requestedColumn: nextUpColumn,
578
+ parkedColumn,
579
+ reason: decision.reason,
580
+ missing: decision.missing,
581
+ };
582
+ } else {
583
+ refinement = { refined: true };
584
+ }
585
+ }
586
+
505
587
  // 6. Add item to project
506
588
  const addPayload = await ghGraphql(ADD_PROJECT_ITEM, {
507
589
  projectId: project.id,
@@ -518,7 +600,7 @@ async function main(args, { env = process.env, runChild, cwd = process.cwd() } =
518
600
  projectId: project.id,
519
601
  itemId: newItem.id,
520
602
  fieldId: statusField.id,
521
- optionId: targetOption.id,
603
+ optionId: effectiveTargetOption.id,
522
604
  }, env, child);
523
605
 
524
606
  const updated = updatePayload?.data?.updateProjectV2ItemFieldValue?.projectV2Item;
@@ -532,9 +614,10 @@ async function main(args, { env = process.env, runChild, cwd = process.cwd() } =
532
614
  itemId: newItem.id,
533
615
  issueNumber,
534
616
  prNumber,
535
- status: targetStatus,
617
+ status: effectiveTargetStatus,
536
618
  alreadyPresent: false,
537
619
  },
620
+ ...(refinement ? { refinement } : {}),
538
621
  };
539
622
  }
540
623
 
@@ -13,7 +13,7 @@ user-invocable: false
13
13
 
14
14
  # Copilot PR Follow-up
15
15
 
16
- This skill is the canonical internal `copilot_pr_followup` route behind the public `dev-loop` façade.
16
+ Canonical owner for the internal `copilot_pr_followup` route behind the public `dev-loop` façade.
17
17
 
18
18
  It is also the canonical internal owner of the shared post-PR mechanics used by this repo:
19
19
  PR discovery and interpretation, async watch behavior, fix / reply-resolve / re-request flow,
@@ -238,12 +238,12 @@ When unresolved feedback exists, use a narrow follow-up loop:
238
238
  - when the intent is GitHub linkability, keep commit SHAs and issue/PR refs as plain text (for example 3ee82fc and owner/repo#70) and do not wrap them in backticks
239
239
  - keep backticks for actual code/path/CLI literals only
240
240
  - if either helper was newly added or recently changed, smoke-check it against one real thread before assuming the rest of the loop can rely on it
241
- 9. before resolving an addressed review thread, run a post-fix verification checkpoint
241
+ 9. <!-- rule: COPILOT-FOLLOWUP-VERIFY-BEFORE-RESOLVE --> `COPILOT-FOLLOWUP-VERIFY-BEFORE-RESOLVE`: before resolving an addressed review thread, run a post-fix verification checkpoint
242
242
  - confirm the GitHub reply actually exists on the intended thread/comment, not only in local notes or helper stdout
243
243
  - confirm the pushed current-head diff genuinely addresses the reviewer concern on the flagged lines or pattern; if the concern is only partially addressed, leave the thread open and explain what remains
244
244
  - refresh the API-backed thread snapshot via `dev-loops gate capture-threads` and use that refreshed data — including the unresolved thread count — for follow-up decisions rather than prose assumptions
245
245
  - if any verification check fails, do **not** resolve the thread; leave it open, add a short explanation when needed, and re-enter the fix/reply loop
246
- 10. resolve the addressed review thread only after the reply is attached successfully, the verification checkpoint passes, and the concern is genuinely addressed
246
+ 10. <!-- rule: COPILOT-FOLLOWUP-RESOLVE-AFTER-REPLY --> `COPILOT-FOLLOWUP-RESOLVE-AFTER-REPLY`: resolve the addressed review thread only after the reply is attached successfully, the verification checkpoint passes, and the concern is genuinely addressed
247
247
  - do not stop at a local fix if GitHub-side reply/resolve is authorized
248
248
  11. after completing reply/resolve for a pass, verify zero unresolved threads remain via `dev-loops gate capture-threads` before proceeding
249
249
  - if the refreshed snapshot reports unresolved threads, re-enter the reply/resolve loop for the missed threads
@@ -252,14 +252,14 @@ When unresolved feedback exists, use a narrow follow-up loop:
252
252
  - for a light-dispatched PR, pass `--lightweight` on every round-cap-consuming helper invocation — `detect-copilot-loop-state.mjs`, `copilot-pr-handoff.mjs`, `detect-pr-gate-coordination-state.mjs`, `request-copilot-review.mjs`, and `upsert-checkpoint-verdict.mjs` — otherwise those tools resolve the full-PR cap and the composed lightweight cap is never enforced
253
253
  - **Opt out entirely:** `maxCopilotRounds: 0` disables the external Copilot review gate for the repo — the loop runs `draft_gate → pre_approval_gate` with the local harness only, never requesting or waiting on Copilot. Use this when the repo has no Copilot reviewer configured or prefers local-harness-only review.
254
254
  - use the completed Copilot review-round count from `detect-copilot-loop-state.mjs` / `copilot-pr-handoff.mjs` as the current PR's review-round count
255
- - if completed review rounds have reached the maximum (default: 5), do **not** re-request Copilot review within that concluded cycle
255
+ - if completed review rounds have reached the resolved round cap above, do **not** re-request Copilot review within that concluded cycle
256
256
  - if the loop already converged and then significant post-convergence changes land on a newer head (new/changed product or test logic, not doc/message/comment-only edits), treat that as a NEW cycle and re-request Copilot review when regular rounds are already > 0 (the prior cycle's cap does not suppress this new-cycle request)
257
257
  - when the round limit is reached **and** the refreshed thread snapshot proves zero unresolved threads **and** current-head CI is green or credibly green, treat that clean state as eligible for `pre_approval_gate` fallback instead of deadlocking on another Copilot rerequest
258
258
  - when using that fallback, add a short round-exhaustion note to the visible `pre_approval_gate` gate evidence so the PR records why no further Copilot rerequest occurred
259
259
  - if the round cap is reached before the PR is thread-clean or before CI is green/credibly green, reply-resolve any remaining intentionally deferred threads with a short `deferred to follow-up` note, then stop and report that the Copilot round limit was reached
260
260
  - **Signal-gated re-request suppression:** the `detect-copilot-loop-state.mjs` state machine classifies review-thread comments by signal level (High/Mid/Low). High-signal (bugs, security, contract violations) always re-requests; Low-signal (cosmetic nits) never re-requests. When low-signal detection is enabled and thresholds are met, the machine returns a low-signal-converged terminal state routing to `pre_approval_gate` without further re-requests. See [Copilot Loop Operations](../docs/copilot-loop-operations.md) for full signal-level semantics.
261
261
  - if that local validation is still known red, continue remediation instead of re-requesting Copilot
262
- - after a fix push advances the PR head SHA, re-run `detect-copilot-loop-state.mjs` for the new head and apply the [Copilot CI Status Contract](../docs/copilot-ci-status-contract.md). Previous-head CI is stale; only current-head results unblock CI-dependent steps. if GitHub CI/checks for the updated head are known red for a fixable issue, continue remediation instead of re-requesting Copilot. only once the updated head is green or credibly green, explicitly re-request Copilot review for the new head. Always use `request-copilot-review.mjs` — never `gh api POST repos/.../requested_reviewers` directly.
262
+ - after a fix push advances the PR head SHA, re-run `detect-copilot-loop-state.mjs` for the new head and apply the [Copilot CI Status Contract](../docs/copilot-ci-status-contract.md). Previous-head CI is stale; only current-head results unblock CI-dependent steps. if GitHub CI/checks for the updated head are known red for a fixable issue, continue remediation instead of re-requesting Copilot. <!-- rule: COPILOT-FOLLOWUP-REREQUEST-GREEN-GATE --> `COPILOT-FOLLOWUP-REREQUEST-GREEN-GATE`: only once the updated head is green or credibly green, explicitly re-request Copilot review for the new head. Always use `request-copilot-review.mjs` — never `gh api POST repos/.../requested_reviewers` directly.
263
263
  - only enter a wait/watch loop if the request result is confirmed as `requested` or `already-requested`
264
264
  - for `requested` / `already-requested`, immediately re-baseline with `detect-copilot-loop-state.mjs`; if the returned state is `waiting_for_copilot_review`, use `dev-loops loop watch-cycle` or stop/resume later, and if the returned state is `waiting_for_ci`, use `dev-loops loop watch-ci` (provider-agnostic CI wait; `gh run watch` is an Actions-only fallback) or stop/resume later after that single detector refresh
265
265
  - if the request result is `unavailable`, report that limitation and stop unless the user explicitly wants passive waiting anyway
@@ -151,9 +151,9 @@ When you need a fact from a dev-loops JSON-emitting script, climb this ladder an
151
151
 
152
152
  **Inline-first rule:** Prefer inline commands over nested async delegation when managing a single PR. Use nested delegation only for parallel fan-out or when the parent needs to continue other work.
153
153
 
154
- **Bounded async task contract:** Break work into discrete tasks with clear inputs, explicit outputs, bounded scope. No shell polling — use `run-watch-cycle.mjs` or `gh run watch`. For fan-out reviewer waits (gate sub-loops or any Agent-tool fan-out), await completion via the harness completion notification or the reviewer's findings artifact at its deterministic path, then join via `consolidateFanin` — never tail/parse a subagent transcript, never `node -e`/`python3`-parse tool JSON, never `sleep`-poll (see the [fan-in wait anti-pattern](../docs/anti-patterns.md) and [Checkpoint Review Chain Contract](../../docs/gate-review-sub-loop-contract.md) Phase 2).
154
+ **Bounded async task contract:** Break work into discrete tasks with clear inputs, explicit outputs, bounded scope. No shell polling — use `run-watch-cycle.mjs` or `gh run watch`. Fan-out reviewer waits (gate sub-loops or any Agent-tool fan-out) follow `ANTIPATTERN-FANIN-WAIT` in [Anti-patterns](../docs/anti-patterns.md): await completion via the harness notification or the reviewer's findings artifact at its deterministic path and join via `consolidateFanin` — never transcript-tail, `node -e`/`python3`-parse tool JSON, or `sleep`-poll.
155
155
 
156
- **Round-cap budget check (enforced):** After every watch cycle, fix pass, or reply-resolve, check whether completed Copilot review rounds have reached the maximum (default: 5). Stop re-requesting Copilot review when the limit is reached **within that review cycle**. Exception: if the loop already converged and then significant post-convergence changes land on a newer head (new/changed product or test logic, not doc/message/comment-only edits), open a new Copilot review cycle and re-request review when regular rounds are already > 0, even if the previous cycle hit the cap. Read these gate-cadence facts via the token-economical convention above (`run-watch-cycle.mjs --concise`, or `--jq`/`--silent` for a single field/predicate) — never `| python3` or `node -e`.
156
+ **Round-cap budget check (enforced):** After every watch cycle, fix pass, or reply-resolve, check whether completed Copilot review rounds have reached the resolved round cap (`refinement.maxCopilotRounds`, default 5; light-dispatched PRs resolve the lower `resolveEffectiveCopilotRoundCap`, default 1 — owned by `COPILOT-FOLLOWUP-ROUND-CAP`). Stop re-requesting Copilot review when the limit is reached **within that review cycle**. Exception: the post-convergence new-cycle re-request carve-out (a converged loop that later takes significant post-convergence changes on a newer head opens a new cycle even if the previous one hit the cap) is owned by `COPILOT-FOLLOWUP-ROUND-CAP`. Read these gate-cadence facts via the token-economical convention above (`run-watch-cycle.mjs --concise`, or `--jq`/`--silent` for a single field/predicate) — never `| python3` or `node -e`.
157
157
 
158
158
  ## Shorthand issue-based auto trigger contract
159
159
 
@@ -5,9 +5,17 @@ Canonical owner for the acceptance-criteria verification procedure run during th
5
5
  ## Procedure
6
6
 
7
7
  <!-- rule: ACCEPT-CRITERIA-VERIFY-AND-REFLECT -->
8
- `ACCEPT-CRITERIA-VERIFY-AND-REFLECT`: Before posting the `pre_approval_gate` comment, the agent MUST verify every acceptance criteria checklist item in the issue linked to this PR, and MUST reflect the verified items back into both the linked issue body and the PR body (exception: under the lightweight path `specSource: pr_body`the step 5 issue-body mirroring is skipped, since the PR body, not the issue body, is the canonical spec surface; see the lightweight fork below):
9
-
10
- > **Lightweight (PR-body-as-spec) fork:** when the session is lightweight — the resolver output / handoff envelope carries `specSource: pr_body` (`canonicalSpecSource: pr_body`) — a linked issue still exists (`--lightweight` runs on the `--issue` path, so keep the `Closes #N` linkage and all issue-level tracking), but the **PR body**, not the issue body, is the canonical spec-of-record read for AC/DoD. In that case, still resolve the linked issue for tracking, but read the AC/DoD/invariants from the PR body rather than the issue body (skip step 2's issue-body read for spec purposes). Run `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <pr-number> --expected-issue <issue-number>` (reuses `@dev-loops/core/loop/issue-refinement-artifact`) to confirm the body carries the required invariants — it fails closed with a per-section `missing_*` reason if any is absent, in which case post the gate comment with verdict `blocked`. The PR body MUST carry the `Closes #N` linkage (or GitHub's other closing-keyword forms): `validate-pr-body-spec` fails closed with `missing_closing_issue_reference` without it, and with `closes_wrong_issue` when `--expected-issue` doesn't match. Then continue at step 3, extracting the **Acceptance criteria** checklist items from the PR body itself; step 5's issue-body AC-tick update is unnecessary for lightweight since the issue body is not the spec surface (the PR body is ticked in step 6 as usual). The default issue-backed procedure below is unchanged.
8
+ `ACCEPT-CRITERIA-VERIFY-AND-REFLECT`: Before posting the `pre_approval_gate` comment, the agent MUST verify every acceptance criteria checklist item in the issue linked to this PR, and MUST reflect the verified items back into both the linked issue body and the PR body (exception: when the linked issue body is not the spec-of-record under a lightweight session (tracker-backed or issue-less; the PR body is the spec) or a plan-file promotion session (the committed plan doc the PR body points to is the spec) — the step 5 issue-body mirroring is skipped; see the fork below):
9
+
10
+ > **Non-tracker spec-source fork:** when the linked issue body is not the spec-of-record, the fork covers three arms lightweight (the PR body itself is the spec; splits on whether a backing issue exists) and plan-file promotion (a distinct local-planning origin whose committed plan doc, pointed to from the PR body, is the spec). Handle each arm as below.
11
+ >
12
+ > **Tracker-backed lightweight** (`--issue --lightweight`): a linked issue exists, so keep the `Closes #N` linkage and all issue-level tracking, but the **PR body**, not the issue body, is the canonical spec-of-record read for AC/DoD. Still resolve the linked issue for tracking, but read the AC/DoD/invariants from the PR body rather than the issue body (skip step 2's issue-body read for spec purposes). Run `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <pr-number> --expected-issue <issue-number>` (reuses `@dev-loops/core/loop/issue-refinement-artifact`) to confirm the body carries the required invariants — it fails closed with a per-section `missing_*` reason if any is absent, in which case post the gate comment with verdict `blocked`. The PR body MUST carry the `Closes #N` linkage (or GitHub's other closing-keyword forms): `validate-pr-body-spec` fails closed with `missing_closing_issue_reference` without it, and with `closes_wrong_issue` when `--expected-issue` doesn't match. Then continue at step 3, extracting the **Acceptance criteria** checklist items from the PR body itself; step 5's issue-body AC-tick update is unnecessary since the issue body is not the spec surface (the PR body is ticked in step 6 as usual).
13
+ >
14
+ > **Issue-less lightweight** (`--lightweight` alone): no linked issue exists — by design, not a gap. There is nothing to resolve at step 1 and nothing to read or mirror at steps 2 and 5; skip all three. Read AC/DoD/invariants directly from the PR body and run `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <pr-number> --no-issue`, which fails closed with a per-section `missing_*` reason for any absent invariant and with `unexpected_closing_issue_reference` if the body carries a closing reference to an issue that doesn't back it (a closing reference MUST NOT be present). On any validation failure, post the gate comment with verdict `blocked`. Step 1's "zero candidates → blocked" rule is scoped to tracker-backed sessions; for a deliberately issue-less session, zero closing references is the expected state, not a blocked condition. Continue at step 3 against the PR body, then step 4 and 6 as usual.
15
+ >
16
+ > **Plan-file promotion (P4) — a distinct local-planning origin, not a lightweight sub-arm:** no linked issue exists — similar to the issue-less lightweight arm, there is nothing to resolve at step 1 and nothing to read or mirror at steps 2 and 5; skip all three. AC/DoD live in the PR body, copied from the committed plan doc that is the spec-of-record; read them from the PR body as usual.
17
+ >
18
+ > The default issue-backed procedure below is unchanged.
11
19
 
12
20
  1. **Resolve the linked issue number deterministically:** use `gh pr view <pr-number> --repo <owner/name> --json closingIssuesReferences,body` and apply this decision tree: if there is exactly one closing issue reference, use it; else if there is exactly one PR-body `Closes #N` / `Fixes #N` pattern, use it; otherwise (zero or multiple candidates), post the gate comment with verdict `blocked` (gate cannot complete deterministically) rather than guessing.
13
21
 
@@ -17,7 +25,7 @@ Canonical owner for the acceptance-criteria verification procedure run during th
17
25
 
18
26
  4. **Verify each AC item** against the proposed changes on the current PR head.
19
27
 
20
- 5. **Update the issue body once:** compute the fully-updated issue body by replacing each verified item's `- [ ]` with `- [x]`, write it to a temporary file, and perform a single `gh issue edit <issue-number> --body-file <tmp-file> --repo <owner/name>`. Do not issue one edit per item; prefer `--body-file` over inline `--body` to avoid shell quoting/escaping hazards.
28
+ 5. **Update the issue body once:** compute the fully-updated issue body by replacing each verified item's `- [ ]` with `- [x]`, write it to a temporary file, and perform a single `node scripts/github/edit-issue.mjs --repo <owner/name> --issue <issue-number> --body-file <tmp-file>`. Do not issue one edit per item; prefer `--body-file` over inline `--body` to avoid shell quoting/escaping hazards.
21
29
 
22
30
  6. **Tick the PR body once:** after the verification is clean, flip each verified item's `- [ ]` to `- [x]` in the PR body via a single `gh pr edit --body-file` update, so the merged PR shows checked AC/DoD. Run `node scripts/github/tick-verified-checkboxes.mjs --repo <owner/name> --pr <pr-number> --verified <exact label>...` (one edit; exact-label match; only items actually verified are ticked; unverified or deferred items stay `- [ ]`; fail closed). This runs automatically as part of the `pre_approval_gate`, not by memory.
23
31
 
@@ -6,18 +6,18 @@ Canonical owner for anti-pattern guidance across all workflow families.
6
6
 
7
7
  1. **Skipping fan-out/fan-in for non-trivial changes**: Do not implement directly without refinement when scope exceeds light-mode thresholds.
8
8
  2. **Routing review-only work through local_implementation**: Review-only comparison, synthesis, or consolidation must use `refiner` agent, not `dev-loop` + `local_implementation`.
9
- 3. **Thin placeholder PR descriptions**: Always include change summary, scope/context, acceptance criteria, definition of done, non-goals, and `Closes #N`.
9
+ 3. **Thin placeholder PR descriptions**: Always include change summary, scope/context, acceptance criteria, definition of done, non-goals, and `Closes #N` (tracker-backed; issue-less lightweight and plan-file promotion PRs instead follow [merge precondition 7](merge-preconditions.md#required-before-merge)).
10
10
  4. **Merging directly to main without PR**: Use PR-based remote loop when practical.
11
11
  5. **Duplicate worktree paths**: See `WORKTREE-DEDUPE` in [Worktree usage guidance](../../docs/worktree-guidance.md#coordination-and-collision-checks).
12
12
  6. **Main-checkout mutation**: See `WORKTREE-DEFAULT-USE` in [Worktree usage guidance](../../docs/worktree-guidance.md#default-rule-use-a-worktree-for-mutating-local-work).
13
13
  7. **Spelunking tooling internals instead of using the public surface**: Do not read installed package internals, scan tooling source, or run ad-hoc scripts to understand a tool's behavior. Use the CLI, its `--help` subcommands, and `skills/docs/`. Read tool source only when the task is to inspect or change that tool, or when a concrete failure path is inside it and no public CLI/docs path exists. Once a failure is concrete, search changed files for the exact pattern — don't run duplicate broad searches.
14
14
  8. **Hand-editing the queue file when a board is configured**: When a GitHub Projects board is configured (`queue.projectNumber`/`queue.boardTitle` in `.devloops`), the board is the authoritative queue MEMBERSHIP and ordering source — not just status. Add work via the board (`dev-loops queue add ... --next-up` to land directly in the normative `Next Up` pickup queue; default lands in Backlog, which is unprioritized intake and never auto-picked), not by hand-editing `.pi/dev-loop-queue.json`; the queue runner reconciles board `Next Up` items into queue entries before each run. See the operator guides under `docs/` (queue board usage/setup) for the workflow.
15
- 9. **Hand-rolling a fan-in wait by polling a subagent transcript or parsing tool JSON with `node -e`/`python3`**: When awaiting fan-out reviewers (gate `draft_gate`/`pre_approval_gate` sub-loops, or any Agent-tool fan-out), NEVER tail/parse another agent's JSONL transcript, NEVER use `node -e`/`python3` to parse tool/subagent JSON, and NEVER `sleep`-poll in a shell loop for completion. These improvisations breach the [internal-tooling-only rule (issue #982)](retrospective-checkpoint-contract.md#internal-tooling-only-rule-issue-982--now-advisory) (no `node -e`/`python3` parsing of tool JSON) and the no-shell-polling rule, and a single improvised wait adds a non-empty entry to the advisory `retrospectiveFindings.rawCallViolations` array (issue #1077: reported to the conductor, **never blocking**). **Sanctioned wait:** rely on the harness completion notification, or read each reviewer's findings artifact at its deterministic output path (see [Checkpoint Review Chain Contract](../../docs/gate-review-sub-loop-contract.md) Phase 2), then join via `consolidateFanin` from `@dev-loops/core/loop/gate-fanin` (Phase 3). See also the dev-loop SKILL "Bounded async task contract".
15
+ 9. <!-- rule: ANTIPATTERN-FANIN-WAIT --> `ANTIPATTERN-FANIN-WAIT` — **Hand-rolling a fan-in wait by polling a subagent transcript or parsing tool JSON with `node -e`/`python3`**: When awaiting fan-out reviewers (gate `draft_gate`/`pre_approval_gate` sub-loops, or any Agent-tool fan-out), NEVER tail/parse another agent's JSONL transcript, NEVER use `node -e`/`python3` to parse tool/subagent JSON, and NEVER `sleep`-poll in a shell loop for completion. These improvisations breach the [internal-tooling-only rule (issue #982)](retrospective-checkpoint-contract.md#internal-tooling-only-rule-issue-982--now-advisory) (no `node -e`/`python3` parsing of tool JSON) and the no-shell-polling rule, and a single improvised wait adds a non-empty entry to the advisory `retrospectiveFindings.rawCallViolations` array (issue #1077: reported to the conductor, **never blocking**). **Sanctioned wait:** rely on the harness completion notification, or read each reviewer's findings artifact at its deterministic output path (see [Checkpoint Review Chain Contract](../../docs/gate-review-sub-loop-contract.md) Phase 2), then join via `consolidateFanin` from `@dev-loops/core/loop/gate-fanin` (Phase 3). See also the dev-loop SKILL "Bounded async task contract".
16
16
  10. **Ad hoc `gh api graphql` review-thread queries or `gh pr checks`/shell-pipe CI polling**: Do not hand-write a GraphQL query to enumerate review threads, and do not wrap `gh pr checks` in an `awk`/`grep`/`until`-based shell loop to wait for CI. Enumerate threads (with the ids `reply-resolve-review-thread.mjs` needs) via `scripts/github/list-review-threads.mjs`; block on current-head CI settling via `scripts/github/wait-pr-checks.mjs` (or `dev-loops loop watch-ci` for the JSON-status variant); read the aggregate loop/CI/thread state via `detect-copilot-loop-state.mjs` instead of re-deriving it from raw `gh` output.
17
17
 
18
18
  ## Light mode exception
19
19
 
20
- Small scoped changes (≤3 files, ≤200 lines) may skip fan-out/fan-in but must still run validation and a single review pass. See [Local Implementation](../local-implementation/SKILL.md) for details.
20
+ Small scoped changes under the configured `localImplementation.lightMode` thresholds (owned by `LOCAL-LIGHT-MODE-CONFIG-SURFACE` in [Local Implementation](../local-implementation/SKILL.md)) may skip fan-out/fan-in but must still run validation and a single review pass.
21
21
 
22
22
  ## Cross-references
23
23
 
@@ -1,17 +1,19 @@
1
1
  # Artifact authority contract
2
2
 
3
- This document is the canonical authority for the artifact-selection model: whether a work item originates from a GitHub issue (tracker-first) or from a persisted markdown plan file (local-planning).
3
+ Canonical owner for the artifact-selection model: whether a work item originates from a GitHub issue (tracker-first), a persisted markdown plan file (local-planning), or the sanctioned lightweight PR-body-as-spec path.
4
4
 
5
5
  This canonical owner lives in the shipped `skills/docs/` surface because installed skill/runtime consumers reliably own the skills subtree. In installed layouts, read the same contract via [Artifact Authority Contract](../docs/artifact-authority-contract.md) from the installed skill directory.
6
6
 
7
7
  Other repo docs may summarize or link this contract, but they should not redefine it.
8
8
 
9
- ## Two-tier model
9
+ ## Three-origin model
10
10
 
11
11
  <!-- rule: ARTIFACT-TWO-TIER-EXCLUSIVE -->
12
- dev-loops supports two mutually exclusive artifact authority modes. Every work item MUST originate from exactly one authoritative artifact: a GitHub issue or a persisted markdown plan file. Work MUST NOT originate from a PR or a direct local change unless explicitly requested.
12
+ <!-- The rule ID predates the three-origin wording below (it was written when the
13
+ model was two-tier); retained as-is for ID stability, not renamed. -->
14
+ dev-loops supports three mutually exclusive artifact authority origins. Every work item MUST originate from exactly one authoritative artifact: a GitHub issue, a persisted markdown plan file, or — on the sanctioned lightweight path — the PR description itself as the spec-of-record (no committed plan artifact). Work MUST NOT originate from a PR (other than the sanctioned lightweight PR-body-as-spec path) or a direct local change unless explicitly requested.
13
15
 
14
- The shipped extension default selects local-planning; see [Shipped default posture](#shipped-default-posture) below. The mode names that follow describe the two tiers; "default" in their headings refers to the local-first code-level default in `BUILT_IN_DEFAULTS`.
16
+ The shipped extension default selects local-planning; see [Shipped default posture](#shipped-default-posture) below. The mode names that follow describe the origins; "default" in their headings refers to the local-first code-level default in `BUILT_IN_DEFAULTS`.
15
17
 
16
18
  ### Tracker-first
17
19
 
@@ -5,7 +5,7 @@ Canonical owner for agent confirmation / authorization rules across all workflow
5
5
  ## Core rule
6
6
 
7
7
  <!-- rule: CONFIRM-CORE-EXPLICIT -->
8
- Before any state-changing action, get explicit confirmation unless the latest user message already clearly authorizes that exact action.
8
+ `CONFIRM-CORE-EXPLICIT`: Before any state-changing action, the agent MUST obtain explicit confirmation unless the latest user message already clearly authorizes that exact action.
9
9
 
10
10
  ## What counts as confirmation
11
11
 
@@ -1,6 +1,6 @@
1
1
  # Copilot loop operations
2
2
 
3
- This document is the canonical operational reference for the deterministic Copilot PR follow-up state machine used by the routed `copilot_pr_followup`, `wait_watch`, `reviewer_fixer`, and `final_approval` paths behind `dev-loop`.
3
+ Canonical owner for operating the deterministic Copilot PR follow-up state machine the operational reference for the routed `copilot_pr_followup`, `wait_watch`, `reviewer_fixer`, and `final_approval` paths behind `dev-loop`. The machine's states and transitions are defined by [Copilot Loop State Graph](../../docs/copilot-loop-state-graph.md).
4
4
 
5
5
  Use it together with:
6
6
  - [Copilot PR Follow-up Skill](../copilot-pr-followup/SKILL.md)
@@ -162,12 +162,14 @@ When you do hand work to Copilot:
162
162
 
163
163
  ## PR description contract
164
164
 
165
+ This section governs tracker-backed (Copilot-handoff) PRs; issue-less/plan-file-backed PRs are governed by merge precondition 7 in merge-preconditions.md instead.
166
+
165
167
  Follow the PR description contract (see [Agent Instructions](../../AGENTS.md) if present; otherwise use the structure below): detailed structured descriptions, not thin placeholders. At minimum include change summary, scope/context, explicit acceptance criteria, explicit definition of done, and explicit non-goals, and `Closes #N` (or `Fixes #N`) for the linked issue so GitHub auto-closes it on merge.
166
168
 
167
169
  Checkbox rule: acceptance criteria, definition-of-done items, and any task list must be rendered as real GitHub markdown checkboxes inside list items (`- [ ]` / `- [x]`, also `* [ ]` / `* [x]`). Do not wrap checkbox markers (e.g. `[x]`) in backticks. Do not place checkbox markers inside table cells — task lists are not interactive there even with a leading `- `.
168
170
 
169
171
  <!-- rule: OPS-DRAFT-FIRST-PR -->
170
- `OPS-DRAFT-FIRST-PR`: New PRs in this workflow MUST be opened as **draft** PRs first when the repository enables `.devloops` at repo root `workflow.requireDraftFirst` (the built-in shipped default remains permissive; this repo opts in) mechanically enforced by the `create-pr.mjs` wrapper below. Agents MUST NOT create a fresh PR directly in ready-for-review state unless the user explicitly overrides that policy for the current PR scope. The draft gate inspection is a real workflow boundary, so a new PR must exist in draft before `gh pr ready` is even eligible.
172
+ `OPS-DRAFT-FIRST-PR`: New PRs in this workflow MUST always be opened as **draft** PRs mechanically enforced by the `create-pr.mjs` wrapper below, which is unconditionally draft-only and rejects `--ready`. Agents MUST NOT create a fresh PR directly in ready-for-review state; `gh pr ready` (via the `ready-for-review.mjs` wrapper) is the only path out of draft, gated on clean draft-gate evidence. This rule owns the draft-first mechanism itself; [TRACKER-PROJECTION-REQUIRED-METADATA](./tracker-first-loop-state.md#31-required-pr-metadata) owns the tracker-facing "PR MUST start as a draft" metadata expectation. The draft gate inspection is a real workflow boundary, so a new PR must exist in draft before `gh pr ready` is even eligible.
171
173
 
172
174
  Only use `node <resolved-skill-scripts>/github/create-pr.mjs` when authoritative issue↔PR resolution says there is no already-open linked PR. If a PR already exists, reuse/update that canonical PR instead of opening another one. This wrapper preserves the underlying `gh pr create` output contract while enforcing draft-first mechanically and self-assigning the PR by default (`--assignee @me`).
173
175
 
@@ -26,6 +26,15 @@ For **all new ideas** that are not already anchored to an existing issue (includ
26
26
  - after approval, run a second async mutation pass (dispatched via the procedure) instead of mutating directly from inherited context
27
27
  - verify post-mutation artifact state and record what actually changed
28
28
 
29
+ **Quick-capture exemption:** board quick-capture enqueue (the `/loop-enqueue` freeform
30
+ path — freeform text turned directly into a minimal issue, gated on explicit human
31
+ approval, then grilled inline and added to the board) MAY defer the up-front proposal
32
+ artifact, the async classification pass, the async fan-out/fan-in proposal generation, and
33
+ the second async mutation pass; its inline confirm→create→grill steps stand in for them,
34
+ with grilling supplying classification/refinement after creation. The human-gated-mutation
35
+ and create-new-only safeties still apply — it always creates a new issue and never
36
+ repurposes/overwrites an existing one.
37
+
29
38
  Deterministic intake + mutation-gate state machine:
30
39
 
31
40
  ```text
@@ -127,15 +136,15 @@ Preflight verdicts:
127
136
  - `ready_for_followup`: linked PR has become substantive; resume from that PR
128
137
  - `timed_out`: observational first; refresh authoritative state
129
138
  - if refreshed state is still `waiting_for_initial_copilot_implementation`, remain attached to the same durable wait seam and continue waiting
130
- - if the refreshed state exits this seam, route based on that refreshed state instead of surfacing timeout attention
139
+ - <!-- rule: INTAKE-TIMEOUT-ROUTE-ON-SEAM-EXIT --> `INTAKE-TIMEOUT-ROUTE-ON-SEAM-EXIT`: if the refreshed state exits this seam, route based on that refreshed state instead of surfacing timeout attention
131
140
  - when the refreshed state is `linked_pr_ready_for_followup`, re-enter normal PR follow-up per [FACADE-BOOTSTRAP-ISOLATED-WORKTREE-CONTINUATION](public-dev-loop-contract.md): if the follow-up handoff carries `conductorRouting.handoffEnvelope.requiresLocalIsolation=true`, perform the expected isolated-checkout/worktree handoff and continue
132
- - only surface timeout attention when the seam's durable watch budget is actually exhausted
141
+ - <!-- rule: INTAKE-TIMEOUT-SURFACE-BUDGET-EXHAUSTED --> `INTAKE-TIMEOUT-SURFACE-BUDGET-EXHAUSTED`: only surface timeout attention when the seam's durable watch budget is actually exhausted
133
142
  - for explicit inspect/status requests, report the still-waiting state and exit normally
134
143
  - carry that resolved repo slug through every later GitHub issue/PR command
135
144
 
136
145
  ### From a plan-doc path
137
146
 
138
- - Resolve the target repository slug for this work item before any GitHub search or mutation
147
+ - <!-- rule: INTAKE-REPO-SLUG-RESOLVE-FIRST --> `INTAKE-REPO-SLUG-RESOLVE-FIRST`: Resolve the target repository slug for this work item before any GitHub search or mutation
139
148
  - default to the current repository slug
140
149
  - if the plan-doc reference explicitly points at another GitHub repository, resolve `<resolved-repo>` first
141
150
  - search existing issues with:
@@ -39,13 +39,15 @@ doing manually; it is not a general conflict-resolution engine.
39
39
  ## Required before merge
40
40
 
41
41
  <!-- rule: MERGE-PRECOND-REQUIRED -->
42
+ Before merge, ALL of the following MUST hold:
43
+
42
44
  1. ✅ Conflict-free with base (`mergeable: MERGEABLE`; not `CONFLICTING`/`DIRTY`/`BEHIND`/`UNKNOWN`)
43
45
  2. ✅ CI green on current head (or crediblyGreen via `--local-validation-head-sha`)
44
46
  3. ✅ Draft gate satisfied — clean `draft_gate` verdict per `GATE-COMMENT-VERDICT-VALUES` ([Checkpoint Verdict Comment Contract](../../docs/gate-review-comment-contract.md))
45
47
  4. ✅ Pre-approval gate satisfied — clean `pre_approval_gate` verdict on the current head, same rule
46
48
  5. ✅ All review threads resolved
47
49
  6. ✅ Explicit merge authorization from operator
48
- 7. ✅ Issue-backed work: PR body contains `Closes #N` or `Fixes #N`; for an issue-less lightweight PR the closing reference is instead absent by design (owned by `ARTIFACT-LIGHTWEIGHT-BODY-INVARIANTS` in [Artifact Authority Contract](artifact-authority-contract.md))
50
+ 7. ✅ Closing-reference state matches artifact backing, each arm owned by a different contract: tracker-backed work PR body contains `Closes #N` or `Fixes #N` (owned by the PR description contract in [copilot-loop-operations.md](copilot-loop-operations.md)); issue-less lightweight (PR-body-as-spec, no backing issue) — the closing reference is absent by design and `node scripts/loop/validate-pr-body-spec.mjs --repo <owner/name> --pr <number> --no-issue` passes clean (owned by `ARTIFACT-LIGHTWEIGHT-BODY-INVARIANTS` in [Artifact Authority Contract](artifact-authority-contract.md)); plan-file promotion (P4) — the PR body carries the committed plan-doc path as the spec-of-record and, being issue-less by design (`buildPromotionPrBody` neutralizes closing keywords), the closing reference MUST NOT be present
49
51
  8. ✅ PR **title** free of merge-blocking markers — `WIP`, `[WIP]`, `DRAFT`, `DO NOT MERGE`, `🚧` (case-insensitive)
50
52
 
51
53
  > Runner-coordination lock: the pre-merge evidence check fails closed on a stale/foreign runner claim for the PR. A completing run releases its claim best-effort at every terminal stop (including the human approval checkpoint), so a merge re-dispatch normally proceeds. If a lock held by a completed/dead run still blocks the merge, take it over explicitly with `node <resolved-skill-scripts>/loop/pr-runner-coordination.mjs takeover --repo <owner/name> --pr <number>`. Never take over a genuinely active (non-stale) run — that fail-closed block is intentional.
@@ -80,9 +82,9 @@ A marker is allowed only while the PR is still in draft; it must be removed befo
80
82
 
81
83
  ## Merge authorization
82
84
 
83
- - Must be explicit for the active issue/PR scope
85
+ - Merge authorization MUST be explicit for the active issue/PR scope
84
86
  - `"Merge authorized if gates green"` is valid explicit authorization
85
- - Implied approval from prior turns is not sufficient
87
+ - Implied approval from prior turns MUST NOT be treated as sufficient
86
88
 
87
89
  ### `autonomy.humanMergeOnly` — fixed human-only merge (non-overridable)
88
90
 
@@ -80,7 +80,7 @@ This gate uses review angles resolved from config (`resolveGateAngles(config, "p
80
80
  Boundary note:
81
81
  - `pre_approval_gate` governs only final approval readiness for the reviewed head
82
82
  - a clean verdict requires no findings at any severity in the gate's `blockCleanOnFindingSeverities` (resolved from config via `resolveGateConfig(config, "preApproval").blockCleanOnFindingSeverities`)
83
- - non-draft PRs do not need visible `draft_gate` evidence to enter the post-draft review / `pre_approval_gate` lifecycle; only the draft -> ready transition depends on `draft_gate`
83
+ - non-draft PRs do not need *per-head* `draft_gate` evidence to enter the post-draft review / `pre_approval_gate` lifecycle the one-time draft -> ready transition record still applies; a non-draft PR with no clean `draft_gate` evidence at all fails closed and must reconcile that missing evidence first (see the current-head `draft_gate` evidence bullet under [Fail-closed rules](#fail-closed-rules))
84
84
 
85
85
  ## Lifecycle states
86
86
 
@@ -72,7 +72,8 @@ Use this taxonomy consistently across docs, discovery surfaces, and tests:
72
72
  | Internal routed strategy modules | `issue-intake`, `copilot-pr-followup`, `local-implementation`, `final-approval` | keep internal-only behind `dev-loop`; do not expose as executable peer workflow entrypoints |
73
73
  | Reusable role agents | `developer`, `docs`, `review`, `fixer`, `quality`, `refiner` | keep framed as reusable building blocks, not peer public workflow entrypoints |
74
74
 
75
- Any remaining specialized Copilot behavior stays internal-only behind `dev-loop`.
75
+ <!-- rule: FACADE-COPILOT-INTERNAL-ONLY -->
76
+ `FACADE-COPILOT-INTERNAL-ONLY`: Any remaining specialized Copilot behavior stays internal-only behind `dev-loop`.
76
77
 
77
78
  <!-- rule: FACADE-TAXONOMY-DRIFT-TEST -->
78
79
  `FACADE-TAXONOMY-DRIFT-TEST`: Regression tests MUST fail if this taxonomy drifts in wording or surfaced entrypoint assets.