dev-loops 0.2.7 → 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/agents/developer.md +1 -0
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/review.md +30 -0
- package/.claude/hooks/_run-context.mjs +9 -16
- package/.claude/skills/copilot-pr-followup/SKILL.md +62 -6
- package/.claude/skills/dev-loop/SKILL.md +6 -6
- package/.claude/skills/docs/anti-patterns.md +2 -0
- package/.claude/skills/docs/copilot-loop-operations.md +3 -3
- 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 +25 -5
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +69 -0
- package/README.md +8 -2
- package/agents/developer.agent.md +1 -0
- package/agents/fixer.agent.md +1 -0
- package/agents/review.agent.md +30 -0
- package/cli/index.mjs +60 -8
- package/extension/README.md +1 -1
- package/package.json +3 -3
- package/scripts/README.md +8 -7
- package/scripts/_core-helpers.mjs +1 -0
- package/scripts/claude/headless-dev-loop.mjs +53 -13
- package/scripts/claude/headless-info-smoke.mjs +45 -11
- package/scripts/github/build-adjacent-bundle.mjs +448 -0
- package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
- package/scripts/github/detect-checkpoint-evidence.mjs +95 -4
- package/scripts/github/offer-human-handoff.mjs +147 -0
- package/scripts/github/post-gate-findings.mjs +392 -0
- package/scripts/github/probe-ci-status.mjs +468 -0
- package/scripts/github/reconcile-draft-gate.mjs +2 -2
- package/scripts/github/request-copilot-review.mjs +72 -11
- package/scripts/github/resolve-handoff-candidates.mjs +412 -0
- package/scripts/github/upsert-checkpoint-verdict.mjs +599 -17
- package/scripts/github/verify-fresh-review-context.mjs +12 -1
- package/scripts/github/write-gate-context.mjs +634 -0
- package/scripts/github/write-gate-findings-log.mjs +1 -1
- 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-change-scope.mjs +36 -11
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +30 -18
- package/scripts/loop/detect-stale-runner.mjs +3 -4
- package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
- 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 +112 -16
- package/scripts/loop/run-watch-cycle.mjs +75 -22
- package/scripts/projects/add-queue-item.mjs +80 -48
- package/scripts/projects/archive-done-items.mjs +136 -39
- package/scripts/projects/ensure-queue-board.mjs +67 -65
- package/scripts/projects/list-queue-items.mjs +59 -57
- package/scripts/projects/move-queue-item.mjs +125 -125
- package/scripts/projects/reorder-queue-item.mjs +67 -48
- package/scripts/projects/sync-item-status.mjs +199 -0
- package/skills/copilot-pr-followup/SKILL.md +62 -6
- package/skills/dev-loop/SKILL.md +2 -2
- package/skills/dev-loop/scripts/log-bash-exit-1.mjs +2 -2
- package/skills/dev-loop/scripts/phase-files.mjs +2 -2
- package/skills/docs/anti-patterns.md +2 -0
- package/skills/docs/copilot-loop-operations.md +3 -3
- 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 +25 -5
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { formatCliError, isDirectCliRun } from "../_core-helpers.mjs";
|
|
3
|
+
import { runChild as _runChild } from "../_cli-primitives.mjs";
|
|
4
|
+
import { syncBoardStatus } from "@dev-loops/core/loop/queue-board-sync";
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
|
|
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)
|
|
9
|
+
|
|
10
|
+
Sync a queued issue/PR's board Status column on a dev-loop transition (e.g.
|
|
11
|
+
PR opened → "In Progress", merged → "Done"). Resolves the board from .devloops
|
|
12
|
+
and uses local gh auth.
|
|
13
|
+
|
|
14
|
+
This command is BEST-EFFORT and NON-FATAL: a board that is not configured, an
|
|
15
|
+
item that is not on the board, or any GitHub API failure exits 0 with a JSON
|
|
16
|
+
result describing the skip/failure. It never fails the caller.
|
|
17
|
+
|
|
18
|
+
Options:
|
|
19
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
20
|
+
--item <number> Required. Linked issue/PR number (positive integer).
|
|
21
|
+
--to-column <name> Required. Target Status column (e.g. "In Progress", "Done").
|
|
22
|
+
--help, -h Show this help.
|
|
23
|
+
|
|
24
|
+
Output (stdout):
|
|
25
|
+
JSON: the syncBoardStatus result, e.g.
|
|
26
|
+
{ ok: true, skipped: false, result: { item: { newColumn } } }
|
|
27
|
+
{ ok: true, skipped: true, reason: "board not configured" }
|
|
28
|
+
|
|
29
|
+
Exit codes:
|
|
30
|
+
0 — always on a parsed command (best-effort sync; skips/failures are reported in JSON)
|
|
31
|
+
1 — usage or argument error
|
|
32
|
+
`.trim();
|
|
33
|
+
|
|
34
|
+
function parseCliArgs(argv) {
|
|
35
|
+
const requireValue = (token, message, code) => {
|
|
36
|
+
const v = token.value;
|
|
37
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
|
|
38
|
+
throw Object.assign(new Error(message), { code, usage: USAGE });
|
|
39
|
+
}
|
|
40
|
+
return v;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const args = {};
|
|
44
|
+
const { tokens } = parseArgs({
|
|
45
|
+
args: [...argv],
|
|
46
|
+
options: {
|
|
47
|
+
repo: { type: "string" },
|
|
48
|
+
item: { type: "string" },
|
|
49
|
+
"to-column": { type: "string" },
|
|
50
|
+
help: { type: "boolean", short: "h" },
|
|
51
|
+
},
|
|
52
|
+
allowPositionals: true,
|
|
53
|
+
strict: false,
|
|
54
|
+
tokens: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
for (const token of tokens) {
|
|
58
|
+
if (token.kind === "positional") {
|
|
59
|
+
throw Object.assign(new Error(`Unexpected argument: ${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
60
|
+
}
|
|
61
|
+
if (token.kind !== "option") {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
switch (token.name) {
|
|
65
|
+
case "help":
|
|
66
|
+
if (token.value !== undefined) {
|
|
67
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
68
|
+
}
|
|
69
|
+
args.help = true;
|
|
70
|
+
break;
|
|
71
|
+
case "repo":
|
|
72
|
+
args.repo = requireValue(token, "--repo requires a value (owner/name)", "INVALID_REPO");
|
|
73
|
+
break;
|
|
74
|
+
case "item":
|
|
75
|
+
args.item = requireValue(token, "--item requires a value (positive integer)", "INVALID_ITEM");
|
|
76
|
+
break;
|
|
77
|
+
case "to-column":
|
|
78
|
+
args.toColumn = requireValue(token, "--to-column requires a value", "INVALID_COLUMN");
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return args;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Validation ───────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
90
|
+
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
91
|
+
|
|
92
|
+
function validateRepo(repo) {
|
|
93
|
+
if (!repo || typeof repo !== "string") {
|
|
94
|
+
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
95
|
+
}
|
|
96
|
+
const trimmed = repo.trim();
|
|
97
|
+
if (trimmed !== repo) {
|
|
98
|
+
throw Object.assign(
|
|
99
|
+
new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
|
|
100
|
+
{ code: "INVALID_REPO" },
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const slashIdx = repo.indexOf("/");
|
|
104
|
+
if (slashIdx === -1) {
|
|
105
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
106
|
+
}
|
|
107
|
+
const owner = repo.slice(0, slashIdx);
|
|
108
|
+
const name = repo.slice(slashIdx + 1);
|
|
109
|
+
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
110
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
111
|
+
}
|
|
112
|
+
return repo;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseItemNumber(raw) {
|
|
116
|
+
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
117
|
+
throw Object.assign(new Error("--item is required"), { code: "INVALID_ITEM" });
|
|
118
|
+
}
|
|
119
|
+
const trimmed = raw.trim();
|
|
120
|
+
const asNum = Number(trimmed);
|
|
121
|
+
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
122
|
+
return asNum;
|
|
123
|
+
}
|
|
124
|
+
throw Object.assign(new Error(`--item must be a positive integer, got "${raw}"`), { code: "INVALID_ITEM" });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── Main logic ──────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function main(args, { env = process.env, runChild, cwd = process.cwd() } = {}) {
|
|
130
|
+
const child = runChild ?? _runChild;
|
|
131
|
+
const repo = validateRepo(args.repo);
|
|
132
|
+
const item = parseItemNumber(args.item);
|
|
133
|
+
const toColumn = (args.toColumn ?? "").trim();
|
|
134
|
+
if (!toColumn) {
|
|
135
|
+
throw Object.assign(new Error("--to-column is required"), { code: "INVALID_COLUMN" });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// syncBoardStatus owns the fail-open contract: a not-configured board, an
|
|
139
|
+
// item not on the board, or any gh/API failure resolves to a skipped/failure
|
|
140
|
+
// result rather than throwing. We surface that result verbatim and exit 0.
|
|
141
|
+
return syncBoardStatus(repo, cwd, item, toColumn, env, { runChild: child });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
147
|
+
let args;
|
|
148
|
+
try {
|
|
149
|
+
args = parseCliArgs(argv);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
stderr.write(`${formatCliError(err)}\n`);
|
|
152
|
+
process.exitCode = 1;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (args.help) {
|
|
156
|
+
stdout.write(USAGE);
|
|
157
|
+
// Help is a success path: clear any pre-existing non-zero process.exitCode
|
|
158
|
+
// so help output can't inherit a leaked failure code.
|
|
159
|
+
process.exitCode = 0;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Argument validation errors (bad --repo / --item / missing --to-column) are
|
|
164
|
+
// genuine usage errors → exit 1. Everything past validation is best-effort:
|
|
165
|
+
// syncBoardStatus never throws for board/API conditions, so a parsed command
|
|
166
|
+
// always reports its result on stdout and exits 0.
|
|
167
|
+
let result;
|
|
168
|
+
try {
|
|
169
|
+
result = await main(args, { env, cwd });
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (err.code === "INVALID_REPO" || err.code === "INVALID_ITEM" || err.code === "INVALID_COLUMN" || err.code === "INVALID_ARGS") {
|
|
172
|
+
stderr.write(`${formatCliError(err)}\n`);
|
|
173
|
+
process.exitCode = 1;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Defensive: any unexpected error stays best-effort/non-fatal — report it
|
|
177
|
+
// as a skip and keep exit 0 so it never blocks the PR/merge caller.
|
|
178
|
+
stdout.write(JSON.stringify({ ok: true, skipped: true, reason: err.message ?? "board sync failed" }) + "\n");
|
|
179
|
+
// Explicitly assert success: a pre-existing non-zero process.exitCode from a
|
|
180
|
+
// long-lived caller must not leak through this best-effort path.
|
|
181
|
+
process.exitCode = 0;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
stdout.write(JSON.stringify(result) + "\n");
|
|
185
|
+
// Best-effort contract: a parsed command always reports success. Explicitly
|
|
186
|
+
// clear any pre-existing non-zero process.exitCode so it cannot leak.
|
|
187
|
+
process.exitCode = 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
191
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
192
|
+
// Best-effort: even an unexpected failure must not fail the caller.
|
|
193
|
+
process.stdout.write(JSON.stringify({ ok: true, skipped: true, reason: error.message ?? "board sync failed" }) + "\n");
|
|
194
|
+
// Force the documented exit-0 contract even on this last-resort path.
|
|
195
|
+
process.exitCode = 0;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { main, parseCliArgs, parseItemNumber, runCli };
|
|
@@ -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`
|
|
@@ -264,6 +264,24 @@ Do not treat `fix applied locally` as the end of the loop when the workflow also
|
|
|
264
264
|
|
|
265
265
|
For every `draft_gate` or `pre_approval_gate` comment, you MUST run:
|
|
266
266
|
|
|
267
|
+
For a gate that ran via the fan-out/fan-in sub-loop, pass the structured per-angle review results via `--findings-json` (NOT the wall-of-text `--findings-summary`). The operator/loop writes that JSON file from the collected per-angle results — the same per-angle `{angle, verdict, findings}` objects the fan-out reviewers wrote to `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json` that feed `consolidateFanin`. The helper renders a readable per-angle breakdown and derives the single-line `**Findings summary:**` digest itself:
|
|
268
|
+
|
|
269
|
+
```sh
|
|
270
|
+
node <resolved-skill-scripts>/github/upsert-checkpoint-verdict.mjs \
|
|
271
|
+
--repo <owner/name> \
|
|
272
|
+
--pr <number> \
|
|
273
|
+
--gate <draft_gate|pre_approval_gate> \
|
|
274
|
+
--head-sha <current_head_sha> \
|
|
275
|
+
--verdict <clean|findings_present|blocked> \
|
|
276
|
+
--findings-json <path-to-per-angle-results.json> \
|
|
277
|
+
--next-action "<next action>" --findings-severity-counts '{"must-fix":0,"worth-fixing-now":0,"defer":0}' \
|
|
278
|
+
--execution-mode fanout_fanin
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`--findings-json` accepts the per-angle review-results array (`[{ angle, verdict?, findings:[{severity, summary, file?, line?, disposition?}] }]`, the primary shape that feeds `consolidateFanin`); it also accepts the flat per-finding array that `consolidateFanin`/`toFindingsLogShape` produce (`[{ severity, summary, angle?, ... }]`), grouping it by each finding's `.angle`. A non-empty input matching neither shape is rejected rather than silently rendering all-clean. If the structured results are not available, fall back to `--findings-summary "<summary>"` (the inline_single_agent fallback path).
|
|
282
|
+
|
|
283
|
+
For a gate that ran inline (single agent, not via the sub-loop):
|
|
284
|
+
|
|
267
285
|
```sh
|
|
268
286
|
node <resolved-skill-scripts>/github/upsert-checkpoint-verdict.mjs \
|
|
269
287
|
--repo <owner/name> \
|
|
@@ -272,13 +290,26 @@ node <resolved-skill-scripts>/github/upsert-checkpoint-verdict.mjs \
|
|
|
272
290
|
--head-sha <current_head_sha> \
|
|
273
291
|
--verdict <clean|findings_present|blocked> \
|
|
274
292
|
--findings-summary "<summary>" \
|
|
275
|
-
--next-action "<next action>" --findings-severity-counts '{"must-fix":0,"worth-fixing-now":0,"defer":0}'
|
|
293
|
+
--next-action "<next action>" --findings-severity-counts '{"must-fix":0,"worth-fixing-now":0,"defer":0}' \
|
|
294
|
+
--execution-mode inline_single_agent --inline-reason "<why>"
|
|
276
295
|
```
|
|
277
296
|
|
|
297
|
+
`--execution-mode <fanout_fanin|inline_single_agent>` records how the gate review ran (default `inline_single_agent`). When the gate did not run via the fan-out/fan-in sub-loop ([Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md)), you MUST pass `--execution-mode inline_single_agent --inline-reason "<why>"` — silent inline runs are no longer allowed: inline mode requires a non-empty `--inline-reason` and emits a stderr warning. Because inline is the default mode, a bare call with neither flag now fails with an argument error, so always pass `--execution-mode` explicitly (and `--inline-reason` for inline). The recorded `executionMode` is surfaced by `detect-checkpoint-evidence.mjs` and gated by `gates.requireFanoutEvidence`.
|
|
298
|
+
|
|
278
299
|
Do NOT use `gh pr comment`, `gh api`, or `gh pr review` for gate comments.
|
|
279
300
|
|
|
280
301
|
`--force --force-reason` on `upsert-checkpoint-verdict.mjs` is a narrow operator-authorized CI override for the helper itself, not the default gate path. Use it only when the helper refuses gate entry solely because the current head is `blocked_needs_user_decision` with `ciStatus="failure"`, and only after the user explicitly authorizes ignoring that current-head CI failure for this one gate-comment upsert. It does **not** bypass stale-head checks, unresolved-thread / unsettled-review refusal, non-draft `draft_gate` refusal, merge conflicts, or other legality checks.
|
|
281
302
|
|
|
303
|
+
### Gate fan-out/fan-in procedure (agent-orchestrated)
|
|
304
|
+
|
|
305
|
+
Both gates run this same checkpoint review chain. It is an **agent-orchestrated skill procedure** — a node script cannot spawn the per-angle reviewers, so the conductor agent drives the fan-out and uses the pure `@dev-loops/core/loop/gate-fanin` helpers only for consolidation, batching, and ledger mapping. The cost model is **build once, seed many**: the context-builder script builds ONE deterministic, neutral context bundle (diff + adjacent code) and each independent reviewer is seeded with that identical bundle verbatim instead of re-deriving it (work-dedup; no fork primitive, no Workflow tool). See [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md).
|
|
306
|
+
|
|
307
|
+
1. **Context (Phase 1) — build the neutral bundle ONCE:** build/read the gate-context artifact via `scripts/github/write-gate-context.mjs` (`buildGateContext` resolves the angle set through `resolveGateAnglesDynamic`; mandatory angles always survive). The artifact at `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.json` carries the resolved angles + change scope + acceptance-criteria pointer. When the resolver was handed a diff with `diffOutput`, `buildGateContext` also persists the FULL diff to `tmp/gate-context/<repo-slug>/pr-<N>/<gate>-<headSha>.diff` and records `scope.diffPath` (relative) plus `scope.changedFiles` (full repo-relative paths). It ADDITIONALLY builds a deterministic, neutral `adjacentCode` bundle — each changed file plus its 1-hop import in/out-edges (callers/callees/imports), with size guards (skip lockfiles/generated/binary/minified; cap per-file bytes; truncate the long tail) recorded in a `stripped`/`truncated`/`missing` manifest. This single bundle is the build-once seed; reviewers do not re-derive the diff + adjacent code.
|
|
308
|
+
2. **Fan-out (Phase 2) — seed each reviewer with the bundle verbatim:** plan batches with `planFanoutBatches(angles, resolveMaxFanoutReviewers(config))` (default cap 8). Spawn one scoped `review` agent per resolved angle (plain Agent tool), parallel up to the cap, batching any overflow sequentially. When `degraded` is true, record the sequential degradation in the gate evidence. Reviewers are independent: each is seeded with the IDENTICAL neutral bundle (the gate-context artifact's `scope.diffPath` + `adjacentCode`) verbatim plus its single angle, and never inherits the conductor agent's conversation or opinions. Each reviewer follows the [review agent scoped angle-review mode](../../agents/review.agent.md): fresh context, single angle, read-only, writing its per-angle findings artifact to `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json`. Brief each reviewer to use the provided bundle as its base and to review like an external code reviewer hunting real bugs: read the FULL diff (from `scope.diffPath`, or `git diff` when null) and the bundled adjacent code (callers, callees, imports) rather than re-deriving them, then review adversarially for concrete defects (edge cases, input validation, numeric coercion incl. NaN/Infinity/floats/negatives, null/undefined, boundary conditions, mismatched caller/callee contracts, dedup/identity bugs) with `file:line` + the failing scenario — not process nits like "no test exists". Reviewers are read-only and MAY widen scope (open adjacent repo files beyond the bundle) only when their angle genuinely needs more; that widening is recorded in the optional `contextWidened` field on each per-angle findings artifact.
|
|
309
|
+
3. **Fan-in (Phase 3):** consolidate the per-angle artifacts via `consolidateFanin({ angleResults, blockCleanOnFindingSeverities })` (blocking severities from `resolveGateConfig(config, <configKey>).blockCleanOnFindingSeverities`, where `<configKey>` is the config key for the gate — `draft` for `draft_gate`, `preApproval` for `pre_approval_gate` — not the `draft_gate|pre_approval_gate` artifact name). It yields the gate `verdict` (`clean` when no blocking-severity finding remains, `findings_present` when one does, `blocked` when any per-angle artifact is malformed/missing). Map consolidated findings with `toFindingsLogShape(...)` and write the disposition ledger via `write-gate-findings-log.mjs` **before** the visible comment. The ledger is written regardless of any comment opt-out. Then, when `resolveGatePostFindingsComments(config)` is true (default; opt-out via `gates.postFindingsComments: false`), post the consolidated findings as a visible, marker-tagged PR comment via `node <resolved-skill-scripts>/github/post-gate-findings.mjs --repo <owner/name> --pr <number> --gate <gate> --head-sha <current_head_sha> --findings '<toFindingsLogShape JSON>'` so the findings are auditable and Copilot/humans are aware of them. The helper is idempotent per gate (exactly one comment per gate, updated in place on each run; the reviewed head is shown in the body) and posts a brief "no findings" note when the consolidated set is empty. When `postFindingsComments` is false the helper no-ops with a `skipped` result and you skip this post step; the ledger still records the findings.
|
|
310
|
+
4. **Verdict (Phase 4):** post the verdict with the [Gate comment command](#mandatory-gate-comment-command-contract) using `--execution-mode fanout_fanin` and `--findings-json <path>`. Write that JSON from the collected per-angle results (the same per-angle `{angle, verdict, findings}` artifacts at `tmp/gate-reviews/<repo-slug>/pr-<N>/<gate>-<headSha>/<angle>.json` that fed `consolidateFanin` in Phase 3; the flat `toFindingsLogShape` output is also accepted and grouped by `.angle`) so the verdict comment renders the structured per-angle breakdown instead of a wall-of-text summary. Do NOT use `--findings-summary` for a fan-out gate — that is the inline_single_agent fallback.
|
|
311
|
+
5. **Retry (Phase 5):** on blocking findings, drive each internal fan-out finding through the SAME fix → reply-with-resolving-commit → resolve loop used for external Copilot review comments (Step 7 above): fix on the branch, push the resolving commit, then re-run only the `findings_present` angles from the previous pass (the context-builder and fan-in always re-run); repeat until the consolidated verdict is `clean` for the current head SHA. Internal fan-out findings and external review comments are handled identically by that loop.
|
|
312
|
+
|
|
282
313
|
### Draft gate contract (before marking PR ready for review)
|
|
283
314
|
|
|
284
315
|
The canonical checkpoint verdict comment contract is [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). This section summarizes the procedural integration only.
|
|
@@ -286,11 +317,16 @@ The canonical checkpoint verdict comment contract is [Gate Review Comment Contra
|
|
|
286
317
|
- **Gate name:** Draft gate
|
|
287
318
|
- **Trigger / boundary:** right before running `gh pr ready` (draft → ready for review)
|
|
288
319
|
- **Skip rule:** before entering the draft gate, run `detect-pr-gate-coordination-state.mjs` and check `draftGateAlreadySatisfied`. If `true`, skip the draft gate entirely — the draft→ready transition was already recorded. `draft_gate` is a one-time gate; do not re-post on new heads once clean draft-gate evidence exists for the transition record. (While the PR is still draft, advancing the head SHA does require a new draft-gate comment for the new head.) This skip rule applies only to the draft boundary.
|
|
289
|
-
- **Execution directive:** run the
|
|
320
|
+
- **Execution directive:** run the [Gate fan-out/fan-in procedure](#gate-fan-outfan-in-procedure-agent-orchestrated) (the agent-orchestrated chain defined in [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md)) with the draft gate inspection angles resolved from config.
|
|
290
321
|
- **Review angles:** resolved at runtime from config via `resolveGateAngles(config, "draft")` from `@dev-loops/core/config`. Default config enables all configured draft gate angle families; consumer repos may opt out individual angles via `excludeAngles`. Do **not** apply angles from the other gate; each gate owns its own angle list from config.
|
|
291
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.
|
|
292
323
|
- **Pass criteria:** all configured draft gate angles pass; all findings at severities in `blockCleanOnFindingSeverities` are addressed; validation passes; no unrelated files are included.
|
|
293
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 `dev-loops queue sync-status --to-column <col>`:
|
|
326
|
+
```sh
|
|
327
|
+
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "In Progress"
|
|
328
|
+
```
|
|
329
|
+
Best-effort and NON-FATAL: it uses local `gh` auth (no CI/PAT), exits 0 when the board is not configured / the item is not on the board / the API fails, and never blocks marking the PR ready.
|
|
294
330
|
- **Non-substitution rule:** a clean `draft_gate` comment only authorizes the draft → ready-for-review transition for that head SHA. It does **not** satisfy `pre_approval_gate`, final-approval readiness, or merge-ready requirements.
|
|
295
331
|
- **Required PR comment:** post a visible checkpoint verdict comment using the mandatory [Gate comment command](#mandatory-gate-comment-command-contract). Keep validation reporting concise: include command names with pass/fail status. Do **not** paste raw passing test output into the visible gate comment. If you include a failing validation excerpt, keep it focused and truncate it to a deterministic retained-prefix length before posting the comment. See [Gate Review Comment Contract](../../docs/gate-review-comment-contract.md). Do not run `gh pr ready` unless a visible `clean` `draft_gate` checkpoint verdict comment exists for the current head SHA. A checkpoint verdict comment for an older head SHA does not satisfy this requirement for the current head. If findings exist, the PR stays draft and needs fixes before retry. If fixes advance the head SHA while still draft, post a new checkpoint verdict comment for the new head. If the checkpoint verdict comment cannot be posted, fail closed and do not run `gh pr ready`.
|
|
296
332
|
|
|
@@ -300,7 +336,7 @@ This is the default pre-approval gate for this workflow boundary. The canonical
|
|
|
300
336
|
|
|
301
337
|
- **Gate name:** Pre-approval gate
|
|
302
338
|
- **Trigger / boundary:** right before calling a PR/branch review-complete, approval-ready, merge-ready, or ready for final handoff
|
|
303
|
-
- **Execution directive:** run the
|
|
339
|
+
- **Execution directive:** run the [Gate fan-out/fan-in procedure](#gate-fan-outfan-in-procedure-agent-orchestrated) (the agent-orchestrated chain defined in [Gate Review Sub-Loop Contract](../../docs/gate-review-sub-loop-contract.md)) with the pre-approval gate inspection angles resolved from config. The `acceptance-criteria` angle is mandatory for this gate (see `.devloops` `gates.preApproval.mandatoryAngles`) and always survives dynamic resolution. Retry rule: in subsequent cycles, only re-run reviewers that produced `findings_present` in the previous pass.
|
|
304
340
|
- **Review angles:** resolved at runtime from config via `resolveGateAngles(config, "preApproval")` from `@dev-loops/core/config`. Default config enables all configured pre-approval gate angle families; consumer repos may opt out individual angles via `excludeAngles`.
|
|
305
341
|
- **Persona mapping:** each angle resolves to a reviewer persona via `resolveReviewerRole(config, angle)` from `@dev-loops/core/config`. Include this prompt in each reviewer's briefing so the reviewer knows exactly what to look for.
|
|
306
342
|
- **Pass criteria:** the sub-loop completes with verdict `clean`; all configured angles pass; if parallel execution is impractical, still run all configured lenses and explicitly record the limitation.
|
|
@@ -334,6 +370,8 @@ See [Merge Preconditions](../docs/merge-preconditions.md). Verify: zero unresolv
|
|
|
334
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).
|
|
335
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`.
|
|
336
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
|
+
|
|
337
375
|
### Mechanical pre-merge gate evidence check
|
|
338
376
|
|
|
339
377
|
Immediately before any `gh pr merge`, run:
|
|
@@ -362,6 +400,24 @@ node <resolved-skill-scripts>/loop/checkpoint-contract.mjs --state skipped --rea
|
|
|
362
400
|
|
|
363
401
|
Do not report completion or advance to the next PR queue item until `.pi/dev-loop-retrospective-checkpoint.json` is updated to `complete` or `skipped`.
|
|
364
402
|
|
|
403
|
+
### Post-merge board archive (best-effort)
|
|
404
|
+
|
|
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"):
|
|
406
|
+
|
|
407
|
+
```sh
|
|
408
|
+
node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name> || true
|
|
409
|
+
```
|
|
410
|
+
|
|
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.
|
|
412
|
+
|
|
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>`:
|
|
414
|
+
|
|
415
|
+
```sh
|
|
416
|
+
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Same best-effort/NON-FATAL contract as the archive step: local `gh` auth (no CI/PAT), exits 0 when the board is not configured / the item is not on the board / the API fails, and never blocks the merge or the retrospective.
|
|
420
|
+
|
|
365
421
|
## Validation policy
|
|
366
422
|
|
|
367
423
|
Follow [Validation Policy](../docs/validation-policy.md). Default: `npm run verify` before PR creation, gate entry, and merge. For repo-local examples: `npm run test:dev-loop` for skill scripts, contract tests for templates, `git diff --check` for docs. When CI runs exist, use `gh run watch` or `detect-copilot-loop-state.mjs` instead of `sleep`-based polling. Distinguish: locally validated, full PR-equivalent checks, awaiting CI.
|
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
|
|
@@ -9,9 +9,9 @@ export {
|
|
|
9
9
|
readRecordFromStdin,
|
|
10
10
|
runCli,
|
|
11
11
|
truncateText,
|
|
12
|
-
} from "
|
|
12
|
+
} from "@dev-loops/core/bash-exit-one";
|
|
13
13
|
|
|
14
|
-
import { runCli } from "
|
|
14
|
+
import { runCli } from "@dev-loops/core/bash-exit-one";
|
|
15
15
|
|
|
16
16
|
const invokedAsScript = process.argv[1]
|
|
17
17
|
? import.meta.url === pathToFileURL(process.argv[1]).href
|
|
@@ -13,9 +13,9 @@ export {
|
|
|
13
13
|
uniqueSortedStrings,
|
|
14
14
|
upsertPhaseIndex,
|
|
15
15
|
writeJson,
|
|
16
|
-
} from "
|
|
16
|
+
} from "@dev-loops/core/loop/phase-files";
|
|
17
17
|
|
|
18
|
-
import { runCli } from "
|
|
18
|
+
import { runCli } from "@dev-loops/core/loop/phase-files";
|
|
19
19
|
|
|
20
20
|
const invokedAsScript = process.argv[1]
|
|
21
21
|
? import.meta.url === pathToFileURL(process.argv[1]).href
|
|
@@ -10,6 +10,8 @@ Canonical owner for anti-pattern guidance across all workflow families.
|
|
|
10
10
|
4. **Merging directly to main without PR**: Use PR-based remote loop when practical.
|
|
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
|
+
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 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.
|
|
13
15
|
|
|
14
16
|
## Light mode exception
|
|
15
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>]`.
|
|
@@ -172,9 +172,9 @@ Checkbox rule: acceptance criteria, definition-of-done items, and any task list
|
|
|
172
172
|
|
|
173
173
|
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. Do 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.
|
|
174
174
|
|
|
175
|
-
Only use `node <resolved-skill-scripts>/github/create-
|
|
175
|
+
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`).
|
|
176
176
|
|
|
177
|
-
MUST use `node <resolved-skill-scripts>/github/create-
|
|
177
|
+
MUST use `node <resolved-skill-scripts>/github/create-pr.mjs --repo <owner/name> --assignee @me --base <base> --head <head> --title "..." --body-file <body-file>` (always draft, self-assigned by default; `--assignee @me` is the default).
|
|
178
178
|
|
|
179
179
|
## Timeout and watch policy
|
|
180
180
|
|
|
@@ -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
|
|