dev-loops 0.3.0 → 0.4.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.
- package/.claude/.claude-plugin/plugin.json +1 -1
- package/.claude/agents/dev-loop.md +1 -1
- package/.claude/hooks/_run-context.mjs +9 -16
- package/.claude/skills/copilot-pr-followup/SKILL.md +9 -7
- package/.claude/skills/dev-loop/SKILL.md +6 -6
- package/.claude/skills/docs/anti-patterns.md +1 -1
- package/.claude/skills/docs/copilot-loop-operations.md +1 -1
- package/.claude/skills/docs/issue-intake-procedure.md +4 -0
- package/.claude/skills/docs/merge-preconditions.md +65 -1
- package/.claude/skills/docs/stop-conditions.md +1 -0
- package/.claude/skills/docs/tracker-first-loop-state.md +6 -6
- package/.claude/skills/local-implementation/SKILL.md +8 -2
- package/CHANGELOG.md +26 -0
- package/README.md +8 -2
- package/cli/index.mjs +21 -2
- package/extension/README.md +1 -1
- package/package.json +3 -3
- package/scripts/README.md +2 -2
- package/scripts/github/offer-human-handoff.mjs +147 -0
- package/scripts/github/probe-ci-status.mjs +468 -0
- package/scripts/github/request-copilot-review.mjs +3 -3
- package/scripts/github/resolve-handoff-candidates.mjs +412 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +2 -2
- package/scripts/loop/_stale-runner-detection.mjs +1 -1
- package/scripts/loop/_worktree-path.mjs +27 -0
- package/scripts/loop/cleanup-worktree.mjs +175 -0
- package/scripts/loop/copilot-pr-handoff.mjs +1 -1
- package/scripts/loop/detect-stale-runner.mjs +3 -4
- package/scripts/loop/ensure-worktree.mjs +219 -0
- package/scripts/loop/outer-loop.mjs +1 -1
- package/scripts/loop/pr-runner-coordination.mjs +1 -1
- package/scripts/loop/pre-flight-gate.mjs +10 -7
- package/scripts/loop/pre-push-main-guard.mjs +4 -4
- package/scripts/loop/provision-worktree.mjs +243 -0
- package/scripts/loop/resolve-dev-loop-startup.mjs +5 -5
- package/scripts/loop/run-queue.mjs +25 -1
- package/scripts/loop/run-watch-cycle.mjs +75 -22
- package/scripts/projects/add-queue-item.mjs +20 -5
- package/scripts/projects/archive-done-items.mjs +2 -1
- package/scripts/projects/ensure-queue-board.mjs +2 -1
- package/scripts/projects/list-queue-items.mjs +2 -1
- package/scripts/projects/move-queue-item.mjs +2 -1
- package/scripts/projects/reorder-queue-item.mjs +5 -4
- package/scripts/projects/sync-item-status.mjs +2 -1
- package/skills/copilot-pr-followup/SKILL.md +9 -7
- package/skills/dev-loop/SKILL.md +2 -2
- package/skills/docs/anti-patterns.md +1 -1
- package/skills/docs/copilot-loop-operations.md +1 -1
- package/skills/docs/issue-intake-procedure.md +4 -0
- package/skills/docs/merge-preconditions.md +65 -1
- package/skills/docs/stop-conditions.md +1 -0
- package/skills/docs/tracker-first-loop-state.md +6 -6
- package/skills/local-implementation/SKILL.md +8 -2
|
@@ -5,7 +5,10 @@ import { buildParseError, formatCliError, isDirectCliRun } from "../_core-helper
|
|
|
5
5
|
import { parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
6
6
|
import { DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION } from "@dev-loops/core/loop/public-dev-loop-routing";
|
|
7
7
|
import { watchCopilotReview } from "../github/probe-copilot-review.mjs";
|
|
8
|
+
import { watchCiStatus } from "../github/probe-ci-status.mjs";
|
|
8
9
|
import { runHandoff } from "./copilot-pr-handoff.mjs";
|
|
10
|
+
import { STATE } from "@dev-loops/core/loop/copilot-loop-state";
|
|
11
|
+
import { DEFAULT_POLL_INTERVAL_MS } from "@dev-loops/core/loop/policy-constants";
|
|
9
12
|
import { detectCopilotSessionActivity } from "./detect-copilot-session-activity.mjs";
|
|
10
13
|
import { parseArgs } from "node:util";
|
|
11
14
|
import {
|
|
@@ -124,16 +127,33 @@ function buildWatchCycleContractTrace({
|
|
|
124
127
|
cycleDisposition,
|
|
125
128
|
sessionActivity = null,
|
|
126
129
|
workflowRunWatch = null,
|
|
130
|
+
ciWatchArgs = null,
|
|
131
|
+
ciWatchStatus = null,
|
|
127
132
|
}) {
|
|
128
|
-
|
|
129
|
-
|
|
133
|
+
// The CI-watch branch (waiting_for_ci) routes through probe-ci-status.mjs even
|
|
134
|
+
// though handoff.action !== "watch". It is an observational wait, so it mirrors
|
|
135
|
+
// the Copilot watch classification: a quiet timeout is a HEALTHY_WAIT, while
|
|
136
|
+
// success/failure/changed route a follow-up. Without this the boundary-action
|
|
137
|
+
// path below would misclassify a CI timeout as routed_followup.
|
|
138
|
+
const isCiWatch = ciWatchArgs !== null;
|
|
139
|
+
const isWatchBoundary = handoff.action === "watch" || isCiWatch;
|
|
140
|
+
const observedStatus = isCiWatch ? ciWatchStatus : watchStatus;
|
|
141
|
+
const boundaryClassification = isWatchBoundary
|
|
142
|
+
? (observedStatus === "timeout" || observedStatus === "idle"
|
|
143
|
+
? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.HEALTHY_WAIT
|
|
144
|
+
: DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.ROUTED_FOLLOWUP)
|
|
145
|
+
: handoff.loopDisposition === "blocked"
|
|
130
146
|
? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.BLOCKED
|
|
131
147
|
: handoff.terminal
|
|
132
148
|
? DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.TERMINAL
|
|
133
|
-
: DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.ROUTED_FOLLOWUP
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
: DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.ROUTED_FOLLOWUP;
|
|
150
|
+
const healthyWait = boundaryClassification === DEV_LOOP_CONTRACT_TRACE_CLASSIFICATION.HEALTHY_WAIT;
|
|
151
|
+
const helper = handoff.action === "watch"
|
|
152
|
+
? "scripts/github/probe-copilot-review.mjs"
|
|
153
|
+
: isCiWatch
|
|
154
|
+
? "scripts/github/probe-ci-status.mjs"
|
|
155
|
+
: null;
|
|
156
|
+
const effectiveArgs = isCiWatch ? ciWatchArgs : watchArgs;
|
|
137
157
|
return {
|
|
138
158
|
handoff: {
|
|
139
159
|
action: handoff.action,
|
|
@@ -142,38 +162,37 @@ function buildWatchCycleContractTrace({
|
|
|
142
162
|
terminal: Boolean(handoff.terminal),
|
|
143
163
|
},
|
|
144
164
|
waitStrategy: {
|
|
145
|
-
helper
|
|
146
|
-
mode:
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
effectiveTimeoutMs: watchArgs?.timeoutMs ?? null,
|
|
150
|
-
effectivePollIntervalMs: watchArgs?.pollIntervalMs ?? null,
|
|
165
|
+
helper,
|
|
166
|
+
mode: isWatchBoundary ? "persistent_watch" : "not_applicable",
|
|
167
|
+
effectiveTimeoutMs: effectiveArgs?.timeoutMs ?? null,
|
|
168
|
+
effectivePollIntervalMs: effectiveArgs?.pollIntervalMs ?? null,
|
|
151
169
|
timeoutPolicyClassification: watchTimeoutPolicy?.classification ?? null,
|
|
152
170
|
},
|
|
153
171
|
orchestration: {
|
|
154
172
|
emittedWatchArgs: handoff.watchArgs ?? null,
|
|
155
|
-
effectiveWatchArgs:
|
|
173
|
+
effectiveWatchArgs: effectiveArgs,
|
|
174
|
+
ciWatchArgs,
|
|
156
175
|
sessionActivity,
|
|
157
176
|
workflowRunWatch,
|
|
158
177
|
},
|
|
159
|
-
stateRefresh:
|
|
178
|
+
stateRefresh: isWatchBoundary
|
|
160
179
|
? {
|
|
161
180
|
boundaryKind: "post_watch_or_probe",
|
|
162
|
-
observedStatus
|
|
181
|
+
observedStatus,
|
|
163
182
|
refreshRequired: true,
|
|
164
|
-
refreshReason:
|
|
165
|
-
? "
|
|
166
|
-
: "
|
|
183
|
+
refreshReason: healthyWait
|
|
184
|
+
? "Healthy watch boundaries are observational only; refresh authoritative state before treating timeout/idle as stop or completion."
|
|
185
|
+
: "Watch boundaries with fresh activity require an authoritative state refresh before routing the follow-up path.",
|
|
167
186
|
}
|
|
168
187
|
: null,
|
|
169
188
|
stopReason: {
|
|
170
189
|
classification: boundaryClassification,
|
|
171
190
|
terminal: Boolean(handoff.terminal),
|
|
172
191
|
cycleDisposition,
|
|
173
|
-
reason:
|
|
174
|
-
? (
|
|
175
|
-
? "
|
|
176
|
-
: "
|
|
192
|
+
reason: isWatchBoundary
|
|
193
|
+
? (healthyWait
|
|
194
|
+
? "Quiet watcher boundaries remain healthy waits and must not be treated as terminal completion by themselves."
|
|
195
|
+
: "Fresh watcher activity requires follow-up instead of staying in a healthy wait boundary.")
|
|
177
196
|
: handoff.nextAction,
|
|
178
197
|
},
|
|
179
198
|
};
|
|
@@ -236,6 +255,7 @@ export async function runWatchCycle(
|
|
|
236
255
|
ghCommand = "gh",
|
|
237
256
|
runHandoffImpl = runHandoff,
|
|
238
257
|
watchCopilotReviewImpl = watchCopilotReview,
|
|
258
|
+
watchCiStatusImpl = watchCiStatus,
|
|
239
259
|
detectCopilotSessionActivityImpl = detectCopilotSessionActivity,
|
|
240
260
|
fetchPrHeadBranchImpl = fetchPrHeadBranch,
|
|
241
261
|
watchWorkflowRunImpl = watchWorkflowRun,
|
|
@@ -267,6 +287,39 @@ export async function runWatchCycle(
|
|
|
267
287
|
if (handoff.watchTimeoutPolicy !== undefined) {
|
|
268
288
|
result.watchTimeoutPolicy = handoff.watchTimeoutPolicy;
|
|
269
289
|
}
|
|
290
|
+
// Provider-agnostic CI wait (#917): a waiting_for_ci boundary would otherwise
|
|
291
|
+
// dead-end at action:"stop". Route it to the helper-owned CI watcher
|
|
292
|
+
// (CircleCI / GH Actions / external commit-status), not gh run watch.
|
|
293
|
+
if (handoff.action !== "watch" && handoff.state === STATE.WAITING_FOR_CI) {
|
|
294
|
+
// Mirror determineWatchTimeout but with a CI-specific context label so
|
|
295
|
+
// timeout diagnostics read "CI wait" instead of "Copilot review wait".
|
|
296
|
+
const ciTimeoutMs = enforceExternalHealthyWaitTimeout({
|
|
297
|
+
timeoutMs: EXTERNAL_HEALTHY_WAIT_TIMEOUT_POLICY.defaultTimeoutMs,
|
|
298
|
+
contextLabel: "CI wait",
|
|
299
|
+
});
|
|
300
|
+
const ciWatchArgs = {
|
|
301
|
+
repo: options.repo,
|
|
302
|
+
pr: options.pr,
|
|
303
|
+
pollIntervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
304
|
+
timeoutMs: ciTimeoutMs,
|
|
305
|
+
};
|
|
306
|
+
const ciWatch = await watchCiStatusImpl(ciWatchArgs, { env, ghCommand });
|
|
307
|
+
result.ciWatchArgs = ciWatchArgs;
|
|
308
|
+
result.ciWatch = ciWatch;
|
|
309
|
+
result.watchStatus = ciWatch.status;
|
|
310
|
+
// success/failure/changed all need authoritative re-detection (follow-up);
|
|
311
|
+
// a quiet timeout stays a healthy pending wait.
|
|
312
|
+
result.cycleDisposition = ciWatch.status === "timeout" ? "pending" : "needs_followup";
|
|
313
|
+
result.terminal = false;
|
|
314
|
+
result.contractTrace = buildWatchCycleContractTrace({
|
|
315
|
+
handoff,
|
|
316
|
+
watchTimeoutPolicy: result.watchTimeoutPolicy ?? null,
|
|
317
|
+
cycleDisposition: result.cycleDisposition,
|
|
318
|
+
ciWatchArgs,
|
|
319
|
+
ciWatchStatus: ciWatch.status,
|
|
320
|
+
});
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
270
323
|
if (handoff.action !== "watch") {
|
|
271
324
|
result.contractTrace = buildWatchCycleContractTrace({
|
|
272
325
|
handoff,
|
|
@@ -3,7 +3,8 @@ import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.
|
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
|
-
const USAGE = `Usage: dev-loops
|
|
6
|
+
const USAGE = `Usage: dev-loops queue add --repo <owner/name> --project <number|id> --item <number>
|
|
7
|
+
dev-loops project add … (back-compat alias for "queue add")
|
|
7
8
|
|
|
8
9
|
Add an existing issue or PR to a GitHub Projects V2 board.
|
|
9
10
|
|
|
@@ -11,7 +12,8 @@ Options:
|
|
|
11
12
|
--repo <owner/name> Required. Repository containing the issue/PR.
|
|
12
13
|
--project <number|id> Required. Project number (integer) or node ID.
|
|
13
14
|
--item <number> Required. Issue or PR number to add.
|
|
14
|
-
--
|
|
15
|
+
--column <name> Initial Status column (default: "Backlog").
|
|
16
|
+
--status <name> Back-compat alias for --column.
|
|
15
17
|
--help, -h Show this help.
|
|
16
18
|
|
|
17
19
|
Output (stdout):
|
|
@@ -41,6 +43,7 @@ function parseCliArgs(argv) {
|
|
|
41
43
|
repo: { type: "string" },
|
|
42
44
|
project: { type: "string" },
|
|
43
45
|
item: { type: "string" },
|
|
46
|
+
column: { type: "string" },
|
|
44
47
|
status: { type: "string" },
|
|
45
48
|
help: { type: "boolean", short: "h" },
|
|
46
49
|
},
|
|
@@ -81,7 +84,13 @@ function parseCliArgs(argv) {
|
|
|
81
84
|
args.item = val;
|
|
82
85
|
break;
|
|
83
86
|
}
|
|
87
|
+
case "column":
|
|
88
|
+
args.column = requireValue(token, "--column requires a value");
|
|
89
|
+
break;
|
|
84
90
|
case "status":
|
|
91
|
+
// Back-compat alias for --column (issue #912). Kept separate so a
|
|
92
|
+
// conflicting `--column X --status Y` is rejected rather than silently
|
|
93
|
+
// resolved by argv order.
|
|
85
94
|
args.status = requireValue(token, "--status requires a value");
|
|
86
95
|
break;
|
|
87
96
|
default:
|
|
@@ -375,9 +384,15 @@ async function main(args, { env = process.env, runChild } = {}) {
|
|
|
375
384
|
if (!Number.isInteger(itemNumber) || itemNumber < 1) {
|
|
376
385
|
throw Object.assign(new Error("--item is required and must be a positive integer"), { code: "INVALID_ITEM" });
|
|
377
386
|
}
|
|
378
|
-
|
|
387
|
+
if (args.column != null && args.status != null && args.column.trim() !== args.status.trim()) {
|
|
388
|
+
throw Object.assign(
|
|
389
|
+
new Error(`Conflicting --column ("${args.column}") and --status ("${args.status}") — pass only one (prefer --column).`),
|
|
390
|
+
{ code: "INVALID_ARGS", usage: USAGE },
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const targetStatus = (args.column ?? args.status ?? "Backlog").trim();
|
|
379
394
|
if (!targetStatus) {
|
|
380
|
-
throw Object.assign(new Error("--
|
|
395
|
+
throw Object.assign(new Error("--column must not be empty"), { code: "INVALID_STATUS" });
|
|
381
396
|
}
|
|
382
397
|
|
|
383
398
|
// 1. Resolve owner
|
|
@@ -542,4 +557,4 @@ if (isDirectCliRun(import.meta.url)) {
|
|
|
542
557
|
});
|
|
543
558
|
}
|
|
544
559
|
|
|
545
|
-
export { main };
|
|
560
|
+
export { main, parseCliArgs };
|
|
@@ -6,7 +6,8 @@ import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.
|
|
|
6
6
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
|
|
9
|
-
const USAGE = `Usage: dev-loops
|
|
9
|
+
const USAGE = `Usage: dev-loops queue archive-done --repo <owner/name> [--project <number|id>] [--older-than <duration>] [--dry-run]
|
|
10
|
+
(dev-loops project archive-done … is a back-compat alias)
|
|
10
11
|
|
|
11
12
|
Archive GitHub Projects V2 items whose issue/PR has been closed for at least the
|
|
12
13
|
given duration. Operator-triggered (no webhooks). Uses archiveProjectV2Item.
|
|
@@ -6,7 +6,8 @@ import { parse as parseYaml } from "yaml";
|
|
|
6
6
|
import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
7
7
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
8
8
|
|
|
9
|
-
const USAGE = `Usage: dev-loops
|
|
9
|
+
const USAGE = `Usage: dev-loops queue ensure --repo <owner/name> [--project <number>] [--title <title>] [--link-repo <owner/name>] [--repair-rename]
|
|
10
|
+
(dev-loops project ensure … is a back-compat alias)
|
|
10
11
|
|
|
11
12
|
--repair-rename Rename semantically equivalent Status columns to the standard names
|
|
12
13
|
(e.g. "Ready" -> "Next Up"). Without this flag the helper only
|
|
@@ -3,7 +3,8 @@ import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.
|
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
|
-
const USAGE = `Usage: dev-loops
|
|
6
|
+
const USAGE = `Usage: dev-loops queue list --repo <owner/name> --project <number|id> [--column <name>] [--limit <n>]
|
|
7
|
+
(dev-loops project list … is a back-compat alias)
|
|
7
8
|
|
|
8
9
|
List GitHub Projects V2 items filtered by Status column, ordered by position
|
|
9
10
|
ascending. Returns machine-readable JSON.
|
|
@@ -3,7 +3,8 @@ import { formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.
|
|
|
3
3
|
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
|
-
const USAGE = `Usage: dev-loops
|
|
6
|
+
const USAGE = `Usage: dev-loops queue move --repo <owner/name> --project <number|id> --item <number|node-id> --to-column <name>
|
|
7
|
+
(dev-loops project move … is a back-compat alias)
|
|
7
8
|
|
|
8
9
|
Move a GitHub Projects V2 item between Status columns.
|
|
9
10
|
|
|
@@ -4,10 +4,11 @@ import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
|
|
6
6
|
const USAGE = `Usage:
|
|
7
|
-
dev-loops
|
|
8
|
-
dev-loops
|
|
9
|
-
dev-loops
|
|
10
|
-
dev-loops
|
|
7
|
+
dev-loops queue reorder --repo <owner/name> --project <number|id> --item <number|node-id> [--after <number|node-id>]
|
|
8
|
+
dev-loops queue reorder move-to-top <ref> --repo <owner/name> --project <number|id>
|
|
9
|
+
dev-loops queue reorder move-after <ref> <after-ref> --repo <owner/name> --project <number|id>
|
|
10
|
+
dev-loops queue reorder order <ref1> <ref2> ... --repo <owner/name> --project <number|id>
|
|
11
|
+
(dev-loops project reorder … is a back-compat alias)
|
|
11
12
|
|
|
12
13
|
Reorder GitHub Projects V2 items by board position via updateProjectV2ItemPosition.
|
|
13
14
|
|
|
@@ -4,7 +4,8 @@ import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
|
4
4
|
import { syncBoardStatus } from "@dev-loops/core/loop/queue-board-sync";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
6
6
|
|
|
7
|
-
const USAGE = `Usage: dev-loops
|
|
7
|
+
const USAGE = `Usage: dev-loops queue sync-status --repo <owner/name> --item <number> --to-column <name>
|
|
8
|
+
(dev-loops project sync-status … is a back-compat alias)
|
|
8
9
|
|
|
9
10
|
Sync a queued issue/PR's board Status column on a dev-loop transition (e.g.
|
|
10
11
|
PR opened → "In Progress", merged → "Done"). Resolves the board from .devloops
|
|
@@ -166,9 +166,9 @@ The outer-loop checkpoint is the canonical re-attachment artifact.
|
|
|
166
166
|
|
|
167
167
|
Start every wait seam with a detector refresh: `detect-copilot-loop-state.mjs --repo <owner/name> --pr <number>`.
|
|
168
168
|
|
|
169
|
-
Allowed wait tools: `detect-copilot-loop-state.mjs` (one-shot), `dev-loops loop watch-cycle` (persistent), `copilot-pr-handoff.mjs --watch-status` (refresh after timeout/idle), `gh run watch <run-id> --repo <owner/name>` (
|
|
169
|
+
Allowed wait tools: `detect-copilot-loop-state.mjs` (one-shot), `dev-loops loop watch-cycle` (persistent), `copilot-pr-handoff.mjs --watch-status` (refresh after timeout/idle), `dev-loops loop watch-ci --repo <owner/name> --pr <number>` (provider-agnostic CI: CircleCI / Actions / external commit-status), `gh run watch <run-id> --repo <owner/name>` (Actions-only fallback when the run id is known). Otherwise exit and resume later from fresh detector call.
|
|
170
170
|
|
|
171
|
-
Practical rules: do not poll manually. `waiting_for_copilot_review` → `run-watch-cycle.mjs` or report-and-resume. `waiting_for_ci` with pending/none CI → `
|
|
171
|
+
Practical rules: do not poll manually. `waiting_for_copilot_review` → `run-watch-cycle.mjs` or report-and-resume. `waiting_for_ci` with pending/none CI → `dev-loops loop watch-ci --repo <owner/name> --pr <number>` (provider-agnostic; covers CircleCI / Actions / external commit-status) or report-and-resume; `gh run watch <run-id>` is an Actions-only fallback. `dev-loops loop watch-cycle` also auto-routes a `waiting_for_ci` boundary to this CI watcher. Bounded CI exception: zero current-head suites + previous-head green + local `npm run verify` passed → rerun detector with `--local-validation-head-sha` for `crediblyGreen` promotion. `ciStatus=failure` → stop/fix, never wait.
|
|
172
172
|
|
|
173
173
|
Preferred approach:
|
|
174
174
|
- route decisions through `copilot-pr-handoff.mjs` output; enter watcher only on `action: "watch"` with `watchEntryConfirmed=true`; prefer `dev-loops loop watch-cycle` for deterministic handoff → watch
|
|
@@ -251,7 +251,7 @@ When unresolved feedback exists, use a narrow follow-up loop:
|
|
|
251
251
|
- if that local validation is still known red, continue remediation instead of re-requesting Copilot
|
|
252
252
|
- 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.
|
|
253
253
|
- only enter a wait/watch loop if the request result is confirmed as `requested` or `already-requested`
|
|
254
|
-
- 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 `gh run watch`
|
|
254
|
+
- 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
|
|
255
255
|
- if the request result is `unavailable`, report that limitation and stop unless the user explicitly wants passive waiting anyway
|
|
256
256
|
- if the request command fails unexpectedly, stop and report the error rather than sleeping and hoping for a new review
|
|
257
257
|
13. after a confirmed re-requested Copilot pass, refresh PR thread state again before reporting completion; if fresh Copilot threads exist, return to this follow-up loop rather than stopping at `review requested`
|
|
@@ -322,7 +322,7 @@ The canonical checkpoint verdict comment contract is [Gate Review Comment Contra
|
|
|
322
322
|
- **CI prerequisite:** resolve the draft gate config first (`resolveGateConfig(config, "draft")`). When `requireCi=true` (default), wait for green current-head CI before entering `draft_gate`. When `requireCi=false`, the draft gate may proceed without green CI. This draft-only override does **not** relax `pre_approval_gate`; final approval and merge readiness still require green current-head CI.
|
|
323
323
|
- **Pass criteria:** all configured draft gate angles pass; all findings at severities in `blockCleanOnFindingSeverities` are addressed; validation passes; no unrelated files are included.
|
|
324
324
|
- **Next step after passing:** mark the PR ready for review.
|
|
325
|
-
- **Board status sync (best-effort, after ready-for-review):** once the PR is created/marked ready for review, sync the linked issue's board item to "In Progress" so the queue board reflects active work without a manual `
|
|
325
|
+
- **Board status sync (best-effort, after ready-for-review):** once the PR is created/marked ready for review, sync the linked issue's board item to "In Progress" so the queue board reflects active work without a manual `dev-loops queue sync-status --to-column <col>`:
|
|
326
326
|
```sh
|
|
327
327
|
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "In Progress"
|
|
328
328
|
```
|
|
@@ -370,6 +370,8 @@ See [Merge Preconditions](../docs/merge-preconditions.md). Verify: zero unresolv
|
|
|
370
370
|
After merge-ready preconditions pass, verify [Merge Preconditions](../docs/merge-preconditions.md) authoritatively before reporting merge-ready. Stop at the human approval checkpoint by default. Cross-check via `dev-loops gate capture-threads` (not prose assertion).
|
|
371
371
|
Follow [Merge Preconditions](../docs/merge-preconditions.md): stop at `waiting_for_merge_authorization` after approval unless merge explicitly authorized. Run pre-merge gate evidence check before any `gh pr merge`.
|
|
372
372
|
|
|
373
|
+
When `approval.humanHandoff.enabled` is set, don't just park silently at this stop: run `dev-loops gate offer-human-handoff --repo <owner/name> --pr <number>` to surface candidate reviewers/assignees, then **offer** them to the operator. Only on operator confirmation, route the PR with `--assign <login>` / `--request-review <login>`. This is OFFER-only — never auto-assign. See the `approval.humanHandoff` section in [Merge Preconditions](../docs/merge-preconditions.md); it pairs with `autonomy.humanMergeOnly`.
|
|
374
|
+
|
|
373
375
|
### Mechanical pre-merge gate evidence check
|
|
374
376
|
|
|
375
377
|
Immediately before any `gh pr merge`, run:
|
|
@@ -400,15 +402,15 @@ Do not report completion or advance to the next PR queue item until `.pi/dev-loo
|
|
|
400
402
|
|
|
401
403
|
### Post-merge board archive (best-effort)
|
|
402
404
|
|
|
403
|
-
After the retrospective checkpoint write,
|
|
405
|
+
After the retrospective checkpoint write, run the post-merge board archive as a standard step of the post-merge hook (see [Merge Preconditions](../docs/merge-preconditions.md) "Post-merge"):
|
|
404
406
|
|
|
405
407
|
```sh
|
|
406
|
-
node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name>
|
|
408
|
+
node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name> || true
|
|
407
409
|
```
|
|
408
410
|
|
|
409
411
|
Board and threshold resolve from `.devloops` (`queue.projectNumber`/`queue.boardTitle`, `queue.archiveOlderThanDays`, default 7d), using local `gh` auth — no CI, cron, or PAT. This step is best-effort and NON-FATAL: ignore any failure and never let it block the merge or the retrospective.
|
|
410
412
|
|
|
411
|
-
Alongside the archive step, sync the linked issue's board item to "Done" so the queue board reflects the merge without a manual `
|
|
413
|
+
Alongside the archive step, sync the linked issue's board item to "Done" so the queue board reflects the merge without a manual `dev-loops queue sync-status --to-column <col>`:
|
|
412
414
|
|
|
413
415
|
```sh
|
|
414
416
|
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
|
package/skills/dev-loop/SKILL.md
CHANGED
|
@@ -27,7 +27,7 @@ Required installed runtime contract docs are shared bundled copies under `../doc
|
|
|
27
27
|
|
|
28
28
|
The main agent must **always** dispatch the `dev-loop` async subagent for any dev-loop work.
|
|
29
29
|
Do not run `dev-loops loop startup` or any startup resolver in the main agent.
|
|
30
|
-
For async-required routes (config `workflow.asyncStartMode`, default `required`) the resolver needs an async run-id marker (`DEVLOOPS_RUN_ID
|
|
30
|
+
For async-required routes (config `workflow.asyncStartMode`, default `required`) the resolver needs an async run-id marker (`DEVLOOPS_RUN_ID`) that the harness injects when it dispatches the async subagent; under the Claude Code harness the requirement is relaxed automatically (no marker needed). The startup resolver also runs without a marker for non-async routes. Regardless, only the `dev-loop` subagent runs it — never the main agent.
|
|
31
31
|
<!-- /pi-only -->
|
|
32
32
|
|
|
33
33
|
### Resolve authoritative state
|
|
@@ -94,7 +94,7 @@ Do not preload route packs before the resolver selects the strategy.
|
|
|
94
94
|
|
|
95
95
|
## Async dispatch
|
|
96
96
|
|
|
97
|
-
**Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID
|
|
97
|
+
**Async dispatch rule (enforced):** the resolver fails closed for GitHub-first strategies when `canonicalStateSummary.requiresAsyncDispatch` is `true` (default `required` mode) — inline invocation without an async run-id marker (`DEVLOOPS_RUN_ID`) is rejected for those routes. Under the Claude Code harness this requirement is relaxed automatically. See [Startup procedure](#startup-procedure).
|
|
98
98
|
|
|
99
99
|
|
|
100
100
|
## Fallback gate-comment poster
|
|
@@ -11,7 +11,7 @@ Canonical owner for anti-pattern guidance across all workflow families.
|
|
|
11
11
|
5. **Duplicate worktree paths**: Check `git worktree list` before creating new worktrees; reuse existing matching worktrees.
|
|
12
12
|
6. **Main-checkout mutation**: Reserve main checkout for inspection/control; use `tmp/worktrees/` paths for mutation 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
|
-
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
|
|
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 ... --column "Next Up"`), 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
15
|
|
|
16
16
|
## Light mode exception
|
|
17
17
|
|
|
@@ -51,7 +51,7 @@ For tracker-first MVP `story -> PR -> tracker sync` work, the source-repo refere
|
|
|
51
51
|
|
|
52
52
|
4. Branch on the detector output instead of inventing a polling loop:
|
|
53
53
|
- `state=waiting_for_copilot_review` with `snapshot.copilotReviewOnCurrentHead=false`: do **not** poll manually; either run `node <resolved-skill-scripts>/loop/run-watch-cycle.mjs --repo <owner/name> --pr <number>` for persistent async waiting or report the wait state and resume later after the single detector call
|
|
54
|
-
- `state=waiting_for_ci` with `snapshot.ciStatus` in `{ "pending", "none" }`: do **not** poll manually by default; use `
|
|
54
|
+
- `state=waiting_for_ci` with `snapshot.ciStatus` in `{ "pending", "none" }`: do **not** poll manually by default; use `dev-loops loop watch-ci --repo <owner/name> --pr <number>` to block-wait on the combined check/status state (provider-agnostic — CircleCI, GitHub Actions, and any external commit-status / check-run). `gh run watch <run-id>` is an Actions-only fallback when the current-head run id is already known and you know the gating checks are GitHub Actions. Either way, on `timeout`/`changed` report pending CI and resume later after the single detector refresh. Bounded exception: if GitHub created zero current-head check suites/statuses, the previous head rollup was green, and local `npm run verify` already passed for the same current head, rerun `detect-copilot-loop-state.mjs` with `--local-validation-head-sha <current-head-sha>` so the detector can promote that exact zero-suite case to `snapshot.ciStatus="crediblyGreen"` instead of waiting forever on raw `none`.
|
|
55
55
|
- `snapshot.ciStatus="failure"` remains a stop/fix state, never a wait loop
|
|
56
56
|
|
|
57
57
|
5. For reviewer-side draft-review work, run `node <resolved-skill-scripts>/loop/detect-reviewer-loop-state.mjs --repo <owner/name> --pr <number> [--reviewer-login <login>] [--local-state <path>]`.
|
|
@@ -224,6 +224,10 @@ gh pr edit <pr-number> --repo <resolved-repo> --title "..." --body-file <body-fi
|
|
|
224
224
|
gh pr ready <pr-number> --repo <resolved-repo>
|
|
225
225
|
gh pr review <pr-number> --repo <resolved-repo> --approve --body "..."
|
|
226
226
|
node <resolved-skill-scripts>/github/detect-checkpoint-evidence.mjs --repo <resolved-repo> --pr <pr-number>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The merge itself is gated, not implied by the lines above. Before any `gh pr merge`, follow [Merge Preconditions](./merge-preconditions.md): all preconditions (green CI, clean `draft_gate` + current-head `pre_approval_gate`, zero unresolved threads, explicit merge authorization) must hold. Critically, when `autonomy.humanMergeOnly: true` is set, merge is a fixed human-only action — `resolveEffectiveMergeAuthorized` fails closed, the agent **never** runs `gh pr merge`, and instead reports merge-ready + gate evidence and hands off to a human. Only when **not** `humanMergeOnly` and merge is explicitly authorized may the agent run:
|
|
230
|
+
```sh
|
|
227
231
|
gh pr merge <pr-number> --repo <resolved-repo> --squash --delete-branch
|
|
228
232
|
```
|
|
229
233
|
|
|
@@ -32,9 +32,73 @@ A marker is allowed only while the PR is still in draft; it must be removed befo
|
|
|
32
32
|
- `"Merge authorized if gates green"` is valid explicit authorization
|
|
33
33
|
- Implied approval from prior turns is not sufficient
|
|
34
34
|
|
|
35
|
+
### `autonomy.humanMergeOnly` — fixed human-only merge (non-overridable)
|
|
36
|
+
|
|
37
|
+
When a repo sets `autonomy.humanMergeOnly: true` in `.devloops`, merge is a fixed
|
|
38
|
+
human action and this authorization step is **non-overridable**:
|
|
39
|
+
|
|
40
|
+
- `resolveAutonomyStopAt` always includes `merge`, even if `stopAt` is set to `[]`.
|
|
41
|
+
- The effective merge authorization fails closed: `resolveEffectiveMergeAuthorized`
|
|
42
|
+
returns `false` regardless of the `mergeAuthorized` envelope flag or an explicit
|
|
43
|
+
"merge" instruction. The lifecycle resolver therefore never advances to the merge
|
|
44
|
+
state and parks at the `pre_approval_gate` human-merge handoff.
|
|
45
|
+
- The agent still runs the full mechanical pre-merge evidence check and reports
|
|
46
|
+
merge-ready + gate evidence, then hands off to a human to perform `gh pr merge`.
|
|
47
|
+
The agent **never** runs `gh pr merge` itself.
|
|
48
|
+
|
|
49
|
+
This makes human-gated merge an enforced repo invariant, not a per-run default an
|
|
50
|
+
explicit instruction can unlock.
|
|
51
|
+
|
|
52
|
+
### `approval.humanHandoff` — offer to assign a human at the handoff (opt-in)
|
|
53
|
+
|
|
54
|
+
When a repo sets `approval.humanHandoff.enabled: true` in `.devloops`, the loop
|
|
55
|
+
does not just park silently at the human-merge stop — at the
|
|
56
|
+
`pre_approval_gate` / `waiting_for_merge_authorization` boundary it **offers**
|
|
57
|
+
to route the PR to a named human (pairs with `autonomy.humanMergeOnly`: when
|
|
58
|
+
human-merge is enforced, the handoff names who should take it). Disabled by
|
|
59
|
+
default; no candidate sourcing when disabled.
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
approval:
|
|
63
|
+
humanHandoff:
|
|
64
|
+
enabled: true
|
|
65
|
+
candidatesFrom: [codeowners, recent-committers]
|
|
66
|
+
assignees: [alice, bob]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
At the handoff boundary, resolve and surface candidates:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
dev-loops gate offer-human-handoff --repo <owner/name> --pr <number>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
This prints the deduped, ordered candidate list (priority:
|
|
76
|
+
`assignees` > `codeowners` for the touched paths (last-match-wins) >
|
|
77
|
+
`recent-committers` to those paths, PR author/bots excluded). It assigns no one.
|
|
78
|
+
|
|
79
|
+
**OFFER-only — the operator confirms the assignee** (auto-assigning without
|
|
80
|
+
confirmation is a non-goal). On confirmation, perform the action:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
dev-loops gate offer-human-handoff --repo <owner/name> --pr <number> \
|
|
84
|
+
--assign <login> --request-review <login>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
which runs `gh pr edit --add-assignee` / `--add-reviewer` for the confirmed
|
|
88
|
+
human(s).
|
|
89
|
+
|
|
35
90
|
## Post-merge
|
|
36
91
|
|
|
37
|
-
- Remove merged worktree: `
|
|
92
|
+
- Remove merged worktree (canonical): `node scripts/loop/cleanup-worktree.mjs --repo-root <main> (--issue <n> | --pr <n>)`.
|
|
93
|
+
It resolves the namespaced path, runs `git worktree remove --force <path> && git worktree prune` (the underlying
|
|
94
|
+
mechanism) from the main checkout, and refuses any path not under `tmp/worktrees/dev-loops/`. See
|
|
95
|
+
[worktree guidance](../../docs/worktree-guidance.md).
|
|
96
|
+
- Archive long-done queue items (operator-induced, NOT a cron): `node scripts/projects/archive-done-items.mjs --repo <owner/name> || true`.
|
|
97
|
+
Runs as part of this post-merge hook. It applies the configured `queue.archiveOlderThanDays` (default `7d`) and archives
|
|
98
|
+
board items whose issue/PR has been closed at least that long. Best-effort: run it as a standard post-merge step but ignore
|
|
99
|
+
any non-zero exit (a successful run that finds nothing to archive exits 0; a board/config-resolution/API error exits
|
|
100
|
+
non-zero, which the `|| true` masks) — a failure here must never block merge completion. See
|
|
101
|
+
[projects queue usage](../../docs/projects-queue-usage.md).
|
|
38
102
|
- Clean up stale branches
|
|
39
103
|
|
|
40
104
|
## Cross-references
|
|
@@ -10,6 +10,7 @@ Canonical owner for agent stop / wait / block conditions across all workflow fam
|
|
|
10
10
|
| `done` lifecycle state | all | Terminal stop |
|
|
11
11
|
| `approval_ready` without explicit merge auth | `final_approval` | Stop at approval gate |
|
|
12
12
|
| `merge_ready` without explicit merge auth | all | Stop at `waiting_for_merge_authorization` |
|
|
13
|
+
| `autonomy.humanMergeOnly: true` | all | Always stop at merge for human action, even with explicit merge auth — agent never runs `gh pr merge` (see [Merge preconditions](merge-preconditions.md)) |
|
|
13
14
|
| Ambiguous / contradictory state | all | Fail closed to `needs_reconcile` |
|
|
14
15
|
| Missing authoritative startup inputs | `dev-loop` | Fail closed |
|
|
15
16
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Tracker-First Story-to-PR Contract
|
|
2
2
|
|
|
3
3
|
This document defines the adapter-agnostic MVP contract for the tracker-first
|
|
4
|
-
story-to-PR workflow in `
|
|
4
|
+
story-to-PR workflow in `dev-loops`.
|
|
5
5
|
|
|
6
6
|
**MVP invariant: one tracker work item → one GitHub PR.**
|
|
7
7
|
|
|
@@ -43,9 +43,9 @@ are out of scope for this slice.
|
|
|
43
43
|
| PR lifecycle facts | GitHub | Draft / ready-for-review, open / merged / closed, branch, head SHA |
|
|
44
44
|
| PR review and CI facts | GitHub | Reviewer assignments, review states, check-run results, merge status |
|
|
45
45
|
| Decision content | ADR / RFC artifacts (when linked) | Architecture decisions and RFCs referenced from PR body |
|
|
46
|
-
| PR projection and reverse-sync logic | `
|
|
46
|
+
| PR projection and reverse-sync logic | `dev-loops` | Title/body/label generation rules, state mapping, sync triggers |
|
|
47
47
|
|
|
48
|
-
`
|
|
48
|
+
`dev-loops` **does not** become the canonical owner of any business fields.
|
|
49
49
|
It provides projection and sync logic only.
|
|
50
50
|
|
|
51
51
|
## 3. PR Projection Contract
|
|
@@ -234,9 +234,9 @@ This contract is intentionally narrower than the parent epics:
|
|
|
234
234
|
|
|
235
235
|
## 7. Related
|
|
236
236
|
|
|
237
|
-
- Parent workflow-family epic: [mfittko/
|
|
238
|
-
- Umbrella artifact model epic: [mfittko/
|
|
239
|
-
- This contract (first implementable slice): [mfittko/
|
|
237
|
+
- Parent workflow-family epic: [mfittko/dev-loops#17](https://github.com/mfittko/dev-loops/issues/17)
|
|
238
|
+
- Umbrella artifact model epic: [mfittko/dev-loops#19](https://github.com/mfittko/dev-loops/issues/19)
|
|
239
|
+
- This contract (first implementable slice): [mfittko/dev-loops#21](https://github.com/mfittko/dev-loops/issues/21)
|
|
240
240
|
- Copilot loop state graph: [Copilot Loop State Graph](../../docs/copilot-loop-state-graph.md)
|
|
241
241
|
- Reviewer loop state graph: [Reviewer Loop State Graph](../../docs/reviewer-loop-state-graph.md)
|
|
242
242
|
|
|
@@ -72,7 +72,13 @@ For the `local_implementation` strategy, before any planning or implementation m
|
|
|
72
72
|
node scripts/loop/pre-flight-gate.mjs --expected-branch <working-branch> --check-subagents
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Before creating or reusing a worktree for local implementation,
|
|
75
|
+
Before creating or reusing a worktree for local implementation, use the canonical lifecycle entrypoint, which fetches the base remote, then creates-or-reuses the worktree at its namespaced path and provisions it in one step:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
node scripts/loop/ensure-worktree.mjs --repo-root <main> --issue <n>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This creates (or reuses) the worktree at `tmp/worktrees/dev-loops/<kind>-<n>` from a freshly fetched `origin/main` (the authoritative base for all worktree creation) and provisions its gitignored runtime files. Raw `git worktree add -b <branch> tmp/worktrees/dev-loops/<kind>-<n> origin/main` is the underlying mechanism `ensure-worktree.mjs` runs for you. See [worktree guidance](../../docs/worktree-guidance.md).
|
|
76
82
|
|
|
77
83
|
This validates:
|
|
78
84
|
- Worktree isolation (current directory is under `tmp/worktrees/`)
|
|
@@ -85,7 +91,7 @@ Note: `--check-subagents` is advisory — it reports availability but does not b
|
|
|
85
91
|
|
|
86
92
|
This gate does **not** apply to other routed strategies (`copilot_pr_followup`, `external_pr_followup`, `reviewer_fixer`, `wait_watch`, `final_approval`, `issue_intake`). Those strategies have their own execution rules and may edit code from any checkout as needed.
|
|
87
93
|
|
|
88
|
-
Development-only bypass (`
|
|
94
|
+
Development-only bypass (`DEVLOOPS_PREFLIGHT_BYPASS=1`) exists for testing the gate itself but must not be used in production workflow runs. The bypass variable is a testing convenience, not an operational escape hatch.
|
|
89
95
|
|
|
90
96
|
## Narrow failure-triage fast path
|
|
91
97
|
|