dev-loops 0.2.6 → 0.3.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 +2 -1
- package/.claude/agents/developer.md +1 -0
- package/.claude/agents/fixer.md +1 -0
- package/.claude/agents/review.md +30 -0
- package/.claude/skills/copilot-pr-followup/SKILL.md +57 -3
- package/.claude/skills/dev-loop/SKILL.md +5 -5
- package/.claude/skills/docs/anti-patterns.md +2 -0
- package/.claude/skills/docs/copilot-loop-operations.md +2 -2
- package/.claude/skills/local-implementation/SKILL.md +17 -3
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +60 -0
- package/agents/dev-loop.agent.md +5 -1
- 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 +42 -7
- package/package.json +2 -2
- package/scripts/README.md +6 -5
- package/scripts/_cli-primitives.mjs +2 -0
- package/scripts/_core-helpers.mjs +1 -0
- package/scripts/claude/generate-claude-assets.mjs +12 -2
- package/scripts/claude/headless-dev-loop.mjs +53 -13
- package/scripts/claude/headless-info-smoke.mjs +45 -11
- package/scripts/docs/validate-links.mjs +20 -11
- package/scripts/github/build-adjacent-bundle.mjs +448 -0
- package/scripts/github/capture-review-threads.mjs +32 -14
- package/scripts/github/{create-draft-pr.mjs → create-pr.mjs} +28 -12
- package/scripts/github/detect-checkpoint-evidence.mjs +123 -15
- package/scripts/github/detect-linked-issue-pr.mjs +22 -10
- package/scripts/github/manage-sub-issues.mjs +37 -16
- package/scripts/github/post-gate-findings.mjs +392 -0
- package/scripts/github/probe-copilot-review.mjs +24 -12
- package/scripts/github/ready-for-review.mjs +17 -8
- package/scripts/github/reconcile-draft-gate.mjs +24 -12
- package/scripts/github/reply-resolve-review-threads.mjs +34 -15
- package/scripts/github/request-copilot-review.mjs +97 -19
- package/scripts/github/resolve-tracker-local-spec.mjs +29 -12
- package/scripts/github/stage-reviewer-draft.mjs +32 -14
- package/scripts/github/upsert-checkpoint-verdict.mjs +646 -41
- 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 +42 -21
- package/scripts/loop/build-handoff-envelope.mjs +32 -14
- package/scripts/loop/conductor-monitor.mjs +25 -9
- package/scripts/loop/conductor.mjs +31 -12
- package/scripts/loop/copilot-pr-handoff.mjs +31 -14
- package/scripts/loop/debt-remediate.mjs +28 -11
- package/scripts/loop/detect-change-scope.mjs +36 -11
- package/scripts/loop/detect-copilot-loop-state.mjs +29 -12
- package/scripts/loop/detect-copilot-session-activity.mjs +29 -12
- package/scripts/loop/detect-initial-copilot-pr-state.mjs +26 -10
- package/scripts/loop/detect-internal-only-pr.mjs +31 -13
- package/scripts/loop/detect-issue-refinement-artifact.mjs +29 -12
- package/scripts/loop/detect-pr-gate-coordination-state.mjs +56 -28
- package/scripts/loop/detect-reviewer-loop-state.mjs +35 -16
- package/scripts/loop/detect-stale-runner.mjs +32 -14
- package/scripts/loop/detect-tracker-first-loop-state.mjs +38 -11
- package/scripts/loop/detect-tracker-pr-state.mjs +23 -8
- package/scripts/loop/info.mjs +28 -10
- package/scripts/loop/inspect-run-viewer/cli.mjs +44 -21
- package/scripts/loop/inspect-run.mjs +35 -16
- package/scripts/loop/outer-loop.mjs +35 -16
- package/scripts/loop/pr-runner-coordination.mjs +31 -12
- package/scripts/loop/pre-commit-branch-guard.mjs +26 -9
- package/scripts/loop/pre-flight-gate.mjs +25 -9
- package/scripts/loop/pre-pr-ready-gate.mjs +24 -8
- package/scripts/loop/pre-push-main-guard.mjs +19 -5
- package/scripts/loop/pre-write-remote-freshness-guard.mjs +23 -7
- package/scripts/loop/resolve-dev-loop-startup.mjs +29 -12
- package/scripts/loop/run-conductor-cycle.mjs +23 -8
- package/scripts/loop/run-queue.mjs +87 -15
- package/scripts/loop/run-refinement-audit.mjs +44 -22
- package/scripts/loop/run-watch-cycle.mjs +28 -12
- package/scripts/loop/steer-loop.mjs +122 -62
- package/scripts/loop/watch-initial-copilot-pr.mjs +28 -12
- package/scripts/projects/add-queue-item.mjs +60 -43
- package/scripts/projects/archive-done-items.mjs +506 -0
- package/scripts/projects/ensure-queue-board.mjs +65 -64
- package/scripts/projects/list-queue-items.mjs +57 -56
- package/scripts/projects/move-queue-item.mjs +123 -124
- package/scripts/projects/reorder-queue-item.mjs +384 -108
- package/scripts/projects/sync-item-status.mjs +198 -0
- package/scripts/refine/_refine-helpers.mjs +21 -9
- package/scripts/refine/verify.mjs +31 -13
- package/skills/copilot-pr-followup/SKILL.md +57 -3
- package/skills/dev-loop/SKILL.md +9 -5
- 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 +2 -2
- package/skills/local-implementation/SKILL.md +17 -3
|
@@ -0,0 +1,198 @@
|
|
|
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 project sync-status --repo <owner/name> --item <number> --to-column <name>
|
|
8
|
+
|
|
9
|
+
Sync a queued issue/PR's board Status column on a dev-loop transition (e.g.
|
|
10
|
+
PR opened → "In Progress", merged → "Done"). Resolves the board from .devloops
|
|
11
|
+
and uses local gh auth.
|
|
12
|
+
|
|
13
|
+
This command is BEST-EFFORT and NON-FATAL: a board that is not configured, an
|
|
14
|
+
item that is not on the board, or any GitHub API failure exits 0 with a JSON
|
|
15
|
+
result describing the skip/failure. It never fails the caller.
|
|
16
|
+
|
|
17
|
+
Options:
|
|
18
|
+
--repo <owner/name> Required. Repository to scope the project search.
|
|
19
|
+
--item <number> Required. Linked issue/PR number (positive integer).
|
|
20
|
+
--to-column <name> Required. Target Status column (e.g. "In Progress", "Done").
|
|
21
|
+
--help, -h Show this help.
|
|
22
|
+
|
|
23
|
+
Output (stdout):
|
|
24
|
+
JSON: the syncBoardStatus result, e.g.
|
|
25
|
+
{ ok: true, skipped: false, result: { item: { newColumn } } }
|
|
26
|
+
{ ok: true, skipped: true, reason: "board not configured" }
|
|
27
|
+
|
|
28
|
+
Exit codes:
|
|
29
|
+
0 — always on a parsed command (best-effort sync; skips/failures are reported in JSON)
|
|
30
|
+
1 — usage or argument error
|
|
31
|
+
`.trim();
|
|
32
|
+
|
|
33
|
+
function parseCliArgs(argv) {
|
|
34
|
+
const requireValue = (token, message, code) => {
|
|
35
|
+
const v = token.value;
|
|
36
|
+
if (typeof v !== "string" || v.length === 0 || v.startsWith("-")) {
|
|
37
|
+
throw Object.assign(new Error(message), { code, usage: USAGE });
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const args = {};
|
|
43
|
+
const { tokens } = parseArgs({
|
|
44
|
+
args: [...argv],
|
|
45
|
+
options: {
|
|
46
|
+
repo: { type: "string" },
|
|
47
|
+
item: { type: "string" },
|
|
48
|
+
"to-column": { type: "string" },
|
|
49
|
+
help: { type: "boolean", short: "h" },
|
|
50
|
+
},
|
|
51
|
+
allowPositionals: true,
|
|
52
|
+
strict: false,
|
|
53
|
+
tokens: true,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (token.kind === "positional") {
|
|
58
|
+
throw Object.assign(new Error(`Unexpected argument: ${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
59
|
+
}
|
|
60
|
+
if (token.kind !== "option") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
switch (token.name) {
|
|
64
|
+
case "help":
|
|
65
|
+
if (token.value !== undefined) {
|
|
66
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}=${token.value}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
67
|
+
}
|
|
68
|
+
args.help = true;
|
|
69
|
+
break;
|
|
70
|
+
case "repo":
|
|
71
|
+
args.repo = requireValue(token, "--repo requires a value (owner/name)", "INVALID_REPO");
|
|
72
|
+
break;
|
|
73
|
+
case "item":
|
|
74
|
+
args.item = requireValue(token, "--item requires a value (positive integer)", "INVALID_ITEM");
|
|
75
|
+
break;
|
|
76
|
+
case "to-column":
|
|
77
|
+
args.toColumn = requireValue(token, "--to-column requires a value", "INVALID_COLUMN");
|
|
78
|
+
break;
|
|
79
|
+
default:
|
|
80
|
+
throw Object.assign(new Error(`Unknown flag: ${token.rawName}`), { code: "INVALID_ARGS", usage: USAGE });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return args;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Validation ───────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
const OWNER_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/;
|
|
89
|
+
const REPO_NAME_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9])?$/;
|
|
90
|
+
|
|
91
|
+
function validateRepo(repo) {
|
|
92
|
+
if (!repo || typeof repo !== "string") {
|
|
93
|
+
throw Object.assign(new Error("--repo is required"), { code: "INVALID_REPO" });
|
|
94
|
+
}
|
|
95
|
+
const trimmed = repo.trim();
|
|
96
|
+
if (trimmed !== repo) {
|
|
97
|
+
throw Object.assign(
|
|
98
|
+
new Error(`--repo must not have leading/trailing whitespace, got "${repo}"`),
|
|
99
|
+
{ code: "INVALID_REPO" },
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const slashIdx = repo.indexOf("/");
|
|
103
|
+
if (slashIdx === -1) {
|
|
104
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
105
|
+
}
|
|
106
|
+
const owner = repo.slice(0, slashIdx);
|
|
107
|
+
const name = repo.slice(slashIdx + 1);
|
|
108
|
+
if (!owner || !name || !OWNER_RE.test(owner) || !REPO_NAME_RE.test(name)) {
|
|
109
|
+
throw Object.assign(new Error(`--repo must be exactly owner/name, got "${repo}"`), { code: "INVALID_REPO" });
|
|
110
|
+
}
|
|
111
|
+
return repo;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function parseItemNumber(raw) {
|
|
115
|
+
if (!raw || typeof raw !== "string" || raw.trim().length === 0) {
|
|
116
|
+
throw Object.assign(new Error("--item is required"), { code: "INVALID_ITEM" });
|
|
117
|
+
}
|
|
118
|
+
const trimmed = raw.trim();
|
|
119
|
+
const asNum = Number(trimmed);
|
|
120
|
+
if (Number.isInteger(asNum) && asNum > 0 && String(asNum) === trimmed) {
|
|
121
|
+
return asNum;
|
|
122
|
+
}
|
|
123
|
+
throw Object.assign(new Error(`--item must be a positive integer, got "${raw}"`), { code: "INVALID_ITEM" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Main logic ──────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
async function main(args, { env = process.env, runChild, cwd = process.cwd() } = {}) {
|
|
129
|
+
const child = runChild ?? _runChild;
|
|
130
|
+
const repo = validateRepo(args.repo);
|
|
131
|
+
const item = parseItemNumber(args.item);
|
|
132
|
+
const toColumn = (args.toColumn ?? "").trim();
|
|
133
|
+
if (!toColumn) {
|
|
134
|
+
throw Object.assign(new Error("--to-column is required"), { code: "INVALID_COLUMN" });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// syncBoardStatus owns the fail-open contract: a not-configured board, an
|
|
138
|
+
// item not on the board, or any gh/API failure resolves to a skipped/failure
|
|
139
|
+
// result rather than throwing. We surface that result verbatim and exit 0.
|
|
140
|
+
return syncBoardStatus(repo, cwd, item, toColumn, env, { runChild: child });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── CLI entrypoint ──────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
async function runCli(argv, { stdout = process.stdout, stderr = process.stderr, env = process.env, cwd = process.cwd() } = {}) {
|
|
146
|
+
let args;
|
|
147
|
+
try {
|
|
148
|
+
args = parseCliArgs(argv);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
stderr.write(`${formatCliError(err)}\n`);
|
|
151
|
+
process.exitCode = 1;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (args.help) {
|
|
155
|
+
stdout.write(USAGE);
|
|
156
|
+
// Help is a success path: clear any pre-existing non-zero process.exitCode
|
|
157
|
+
// so help output can't inherit a leaked failure code.
|
|
158
|
+
process.exitCode = 0;
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Argument validation errors (bad --repo / --item / missing --to-column) are
|
|
163
|
+
// genuine usage errors → exit 1. Everything past validation is best-effort:
|
|
164
|
+
// syncBoardStatus never throws for board/API conditions, so a parsed command
|
|
165
|
+
// always reports its result on stdout and exits 0.
|
|
166
|
+
let result;
|
|
167
|
+
try {
|
|
168
|
+
result = await main(args, { env, cwd });
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.code === "INVALID_REPO" || err.code === "INVALID_ITEM" || err.code === "INVALID_COLUMN" || err.code === "INVALID_ARGS") {
|
|
171
|
+
stderr.write(`${formatCliError(err)}\n`);
|
|
172
|
+
process.exitCode = 1;
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Defensive: any unexpected error stays best-effort/non-fatal — report it
|
|
176
|
+
// as a skip and keep exit 0 so it never blocks the PR/merge caller.
|
|
177
|
+
stdout.write(JSON.stringify({ ok: true, skipped: true, reason: err.message ?? "board sync failed" }) + "\n");
|
|
178
|
+
// Explicitly assert success: a pre-existing non-zero process.exitCode from a
|
|
179
|
+
// long-lived caller must not leak through this best-effort path.
|
|
180
|
+
process.exitCode = 0;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
stdout.write(JSON.stringify(result) + "\n");
|
|
184
|
+
// Best-effort contract: a parsed command always reports success. Explicitly
|
|
185
|
+
// clear any pre-existing non-zero process.exitCode so it cannot leak.
|
|
186
|
+
process.exitCode = 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (isDirectCliRun(import.meta.url)) {
|
|
190
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
191
|
+
// Best-effort: even an unexpected failure must not fail the caller.
|
|
192
|
+
process.stdout.write(JSON.stringify({ ok: true, skipped: true, reason: error.message ?? "board sync failed" }) + "\n");
|
|
193
|
+
// Force the documented exit-0 contract even on this last-resort path.
|
|
194
|
+
process.exitCode = 0;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export { main, parseCliArgs, parseItemNumber, runCli };
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
|
|
4
4
|
import { buildParseError, formatCliError, isDirectCliRun, parseJsonText } from "../_core-helpers.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { parsePositiveInteger, requireTokenValue, runChild } from "../_cli-primitives.mjs";
|
|
6
7
|
import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
7
8
|
|
|
8
9
|
export const FORBIDDEN_PROSE_PATTERNS = [
|
|
@@ -23,23 +24,34 @@ export function normalizeIssueNumber(value, label, parseError) {
|
|
|
23
24
|
|
|
24
25
|
export function parseCheckerCliArgs(argv, usage, checkerName) {
|
|
25
26
|
const parseError = buildParseError(usage);
|
|
26
|
-
const
|
|
27
|
+
const { tokens } = parseArgs({
|
|
28
|
+
args: [...argv],
|
|
29
|
+
options: { help: { type: "boolean", short: "h" }, input: { type: "string" }, json: { type: "boolean" } },
|
|
30
|
+
allowPositionals: true,
|
|
31
|
+
strict: false,
|
|
32
|
+
tokens: true,
|
|
33
|
+
});
|
|
27
34
|
const options = { help: false, input: undefined, json: false };
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
for (const token of tokens) {
|
|
36
|
+
if (token.kind === "positional") {
|
|
37
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
38
|
+
}
|
|
39
|
+
if (token.kind !== "option") {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (token.name === "help") {
|
|
31
43
|
options.help = true;
|
|
32
44
|
return options;
|
|
33
45
|
}
|
|
34
|
-
if (token === "
|
|
35
|
-
options.input =
|
|
46
|
+
if (token.name === "input") {
|
|
47
|
+
options.input = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
36
48
|
continue;
|
|
37
49
|
}
|
|
38
|
-
if (token === "
|
|
50
|
+
if (token.name === "json") {
|
|
39
51
|
options.json = true;
|
|
40
52
|
continue;
|
|
41
53
|
}
|
|
42
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
54
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
43
55
|
}
|
|
44
56
|
if (typeof options.input !== "string" || options.input.trim().length === 0) {
|
|
45
57
|
throw parseError(`${checkerName} requires --input <path>`);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { buildParseError, formatCliError } from "../_core-helpers.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { parsePositiveInteger, requireTokenValue } from "../_cli-primitives.mjs";
|
|
4
5
|
import { detectRepoSlug, parseRepoSlug } from "@dev-loops/core/github/repo-slug";
|
|
5
6
|
|
|
6
7
|
import { isDirectCliRun, loadTreeFromInput, loadTreeOnline } from "./_refine-helpers.mjs";
|
|
@@ -30,7 +31,19 @@ Optional:
|
|
|
30
31
|
const parseError = buildParseError(USAGE);
|
|
31
32
|
|
|
32
33
|
export function parseRefineVerifyCliArgs(argv) {
|
|
33
|
-
const
|
|
34
|
+
const { tokens } = parseArgs({
|
|
35
|
+
args: [...argv],
|
|
36
|
+
options: {
|
|
37
|
+
help: { type: "boolean", short: "h" },
|
|
38
|
+
issue: { type: "string" },
|
|
39
|
+
repo: { type: "string" },
|
|
40
|
+
input: { type: "string" },
|
|
41
|
+
json: { type: "boolean" },
|
|
42
|
+
},
|
|
43
|
+
allowPositionals: true,
|
|
44
|
+
strict: false,
|
|
45
|
+
tokens: true,
|
|
46
|
+
});
|
|
34
47
|
const options = {
|
|
35
48
|
help: false,
|
|
36
49
|
issue: undefined,
|
|
@@ -39,29 +52,34 @@ export function parseRefineVerifyCliArgs(argv) {
|
|
|
39
52
|
json: false,
|
|
40
53
|
};
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
55
|
+
for (const token of tokens) {
|
|
56
|
+
if (token.kind === "positional") {
|
|
57
|
+
throw parseError(`Unknown argument: ${token.value}`);
|
|
58
|
+
}
|
|
59
|
+
if (token.kind !== "option") {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (token.name === "help") {
|
|
45
63
|
options.help = true;
|
|
46
64
|
return options;
|
|
47
65
|
}
|
|
48
|
-
if (token === "
|
|
49
|
-
options.issue = parsePositiveInteger(
|
|
66
|
+
if (token.name === "issue") {
|
|
67
|
+
options.issue = parsePositiveInteger(requireTokenValue(token, parseError), "Issue number", parseError);
|
|
50
68
|
continue;
|
|
51
69
|
}
|
|
52
|
-
if (token === "
|
|
53
|
-
options.repo =
|
|
70
|
+
if (token.name === "repo") {
|
|
71
|
+
options.repo = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
54
72
|
continue;
|
|
55
73
|
}
|
|
56
|
-
if (token === "
|
|
57
|
-
options.input =
|
|
74
|
+
if (token.name === "input") {
|
|
75
|
+
options.input = requireTokenValue(token, parseError, { flagPattern: /^-/u });
|
|
58
76
|
continue;
|
|
59
77
|
}
|
|
60
|
-
if (token === "
|
|
78
|
+
if (token.name === "json") {
|
|
61
79
|
options.json = true;
|
|
62
80
|
continue;
|
|
63
81
|
}
|
|
64
|
-
throw parseError(`Unknown argument: ${token}`);
|
|
82
|
+
throw parseError(`Unknown argument: ${token.rawName}`);
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
const hasIssueMode = options.issue !== undefined;
|
|
@@ -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 `add-queue-item --status`:
|
|
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.
|
|
@@ -362,6 +398,24 @@ node <resolved-skill-scripts>/loop/checkpoint-contract.mjs --state skipped --rea
|
|
|
362
398
|
|
|
363
399
|
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
400
|
|
|
401
|
+
### Post-merge board archive (best-effort)
|
|
402
|
+
|
|
403
|
+
After the retrospective checkpoint write, optionally tidy the queue board locally:
|
|
404
|
+
|
|
405
|
+
```sh
|
|
406
|
+
node <resolved-skill-scripts>/projects/archive-done-items.mjs --repo <owner/name>
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
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
|
+
|
|
411
|
+
Alongside the archive step, sync the linked issue's board item to "Done" so the queue board reflects the merge without a manual `add-queue-item --status`:
|
|
412
|
+
|
|
413
|
+
```sh
|
|
414
|
+
node <resolved-skill-scripts>/projects/sync-item-status.mjs --repo <owner/name> --item <linked-issue> --to-column "Done"
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
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.
|
|
418
|
+
|
|
365
419
|
## Validation policy
|
|
366
420
|
|
|
367
421
|
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
|
@@ -34,7 +34,11 @@ For async-required routes (config `workflow.asyncStartMode`, default `required`)
|
|
|
34
34
|
|
|
35
35
|
> Under the Claude Code harness the dev-loop runs as a single agent: run these steps directly — no read-only boundary and no separate async-subagent dispatch. See [Main Agent Contract](../docs/main-agent-contract.md).
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
<!-- pi-only -->
|
|
38
|
+
**CLI invocation (`<dev-loops-package-root>`):** dev-loop CLI commands below are invoked as `node <dev-loops-package-root>/cli/index.mjs <verb...>` using the package-local CLI rather than `npx`, so they resolve unambiguously from the installed package without a global install. Resolve `<dev-loops-package-root>` from this skill's own installed path: this skill is installed at `<package-root>/.pi/skills/dev-loop/SKILL.md`, so the package root is `../../..` from this skill's directory. (The `dev-loop` agent resolves it analogously from its own installed path.)
|
|
39
|
+
<!-- /pi-only -->
|
|
40
|
+
|
|
41
|
+
Resolve authoritative state via the startup resolver (`node <dev-loops-package-root>/cli/index.mjs loop startup --issue <n>` for issues, `node <dev-loops-package-root>/cli/index.mjs loop startup --pr <n>` for PRs), then immediately build the handoff envelope via `node <dev-loops-package-root>/cli/index.mjs loop build-envelope --input <resolver-output.json>`. The envelope determines `requiredReads`, `nextAction`, `stopRules`, and `acceptance` — load only those files, execute only that bounded task. It is the first handoff artifact consumed before loading any route pack. See [Workflow Handoff Contract](../docs/workflow-handoff-contract.md) for the derivation contract.
|
|
38
42
|
|
|
39
43
|
**Retrospective checkpoint gate:** the resolver reads `.pi/dev-loop-retrospective-checkpoint.json` and injects the state. When the checkpoint is `missing` and the repo config `workflow.requireRetrospective` (set via `.devloops` at repo root) is `true`, the resolver returns `needs_reconcile`. Complete or explicitly skip the retrospective before starting.
|
|
40
44
|
|
|
@@ -109,10 +113,10 @@ When `@dev-loops/core` is available again, switch back to the full helper. The f
|
|
|
109
113
|
|
|
110
114
|
## Read-only info shortcut
|
|
111
115
|
|
|
112
|
-
Info/handoff requests can be served directly via `
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
115
|
-
- `
|
|
116
|
+
Info/handoff requests can be served directly via `node <dev-loops-package-root>/cli/index.mjs loop info` (read-only; no full dev-loop run required):
|
|
117
|
+
- `node <dev-loops-package-root>/cli/index.mjs loop info --issue <n>` — human-readable issue state summary (strategy, route, linked PR, next action)
|
|
118
|
+
- `node <dev-loops-package-root>/cli/index.mjs loop info --pr <n>` — human-readable PR state summary (branch, CI, threads, rounds, action)
|
|
119
|
+
- `node <dev-loops-package-root>/cli/index.mjs loop info --issue <n> --json` — machine-readable JSON output
|
|
116
120
|
|
|
117
121
|
## Guard rules
|
|
118
122
|
|
|
@@ -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 project add ... --status "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
|
|
|
@@ -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
|
|
|
@@ -59,7 +59,7 @@ When the local spec already lives in a tracker issue:
|
|
|
59
59
|
- sync durable scope / acceptance / status changes back to the tracker issue rather than maintaining a duplicate local phase doc
|
|
60
60
|
- keep `tmp/` as temporary local execution state only; it does not become a second durable spec surface
|
|
61
61
|
- for tracker-backed sessions, the handoff path is always: push the working branch → open a PR → merge via GitHub
|
|
62
|
-
- for tracker-backed sessions,
|
|
62
|
+
- for tracker-backed sessions, always open PRs through the canonical `dev-loops pr create` path, which is ALWAYS draft and always assigned — self-assigned by default (`--assignee @me` when none is given; honors an explicit `--assignee <login>` / `-a <login>`), never via raw `gh pr create`. The PR body must contain `Closes #N` (or `Fixes #N`) for the linked issue so GitHub auto-closes it on merge. When `.devloops` sets `workflow.requireDraftFirst` to true, use `dev-loops pr create --assignee @me ...`. 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.
|
|
63
63
|
- do not suggest a direct local-main merge for tracker-backed sessions; do not merge the working branch into local `main` at phase completion
|
|
64
64
|
|
|
65
65
|
## Primary execution rules
|
|
@@ -87,6 +87,20 @@ This gate does **not** apply to other routed strategies (`copilot_pr_followup`,
|
|
|
87
87
|
|
|
88
88
|
Development-only bypass (`PI_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
89
|
|
|
90
|
+
## Narrow failure-triage fast path
|
|
91
|
+
|
|
92
|
+
When resuming local implementation with dirty work or an observed failing command, use this path before broad discovery:
|
|
93
|
+
|
|
94
|
+
1. run startup once from the relevant worktree/context
|
|
95
|
+
2. inspect current state with `git status` and the changed files
|
|
96
|
+
3. reproduce or identify the failing command
|
|
97
|
+
4. run an exact-pattern search in changed files for the suspect call site
|
|
98
|
+
5. patch the minimum call sites
|
|
99
|
+
6. run focused smoke checks for that failure
|
|
100
|
+
7. run default verification
|
|
101
|
+
|
|
102
|
+
Follow [Anti-patterns](../docs/anti-patterns.md) for the general tooling-internals and duplicate-broad-search prohibition; this local fast path owns only the ordered failure-triage procedure above.
|
|
103
|
+
|
|
90
104
|
### Step 1
|
|
91
105
|
|
|
92
106
|
- Implement **one phase at a time**.
|
|
@@ -513,7 +527,7 @@ After the phase plan passes review:
|
|
|
513
527
|
10. Update `tmp/phases/phase-x/manifest.json` and `tmp/phases/index.json`.
|
|
514
528
|
11. Update [Implementation State](../../docs/IMPLEMENTATION_STATE.md).
|
|
515
529
|
12. **Exit validation gate — no uncommitted changes.** Before the subagent session terminates, run `git status --porcelain` and verify the output is empty. If uncommitted changes exist, first determine whether they are intended implementation changes or unintended/post-validation deltas. Revert any unintended or speculative changes. For intended changes, rerun the narrowest justified validation (`npm run verify` or equivalent) before staging and committing with an appropriate message; for tracker-backed sessions, also push the branch. A subagent session that exits with uncommitted changes in the worktree is a workflow defect and must not be treated as a clean completion. After committing, verify `git status --porcelain` is empty before declaring phase completion.
|
|
516
|
-
13. For tracker-backed sessions, create
|
|
530
|
+
13. For tracker-backed sessions, create the PR from the working branch using the canonical `dev-loops pr create --assignee @me --repo <owner/name> --base main --head <branch> --title "..." --body-file <body-file>` (always draft, self-assigned by default; `--assignee @me` is the default and is shown here for clarity). When the `dev-loops` CLI helper is unavailable, use the equivalent `create-pr.mjs` script directly: `node <resolved-skill-scripts>/github/create-pr.mjs --repo <owner/name> --assignee @me --base main --head <branch> --title "..." --body-file <body-file>`. Never open a PR with raw `gh pr create`. The PR body must contain `Closes #N` (or `Fixes #N`) for the linked issue. Do not create a fresh PR directly in ready-for-review state unless the user explicitly overrides that policy.
|
|
517
531
|
14. If authorized, merge the fully reviewed, locally validated phase branch back into local `main` (phase-doc-backed sessions) or proceed through the PR gate pipeline (tracker-backed sessions).
|
|
518
532
|
15. If authorization for PR creation or merge is still pending (commit authorization is already enforced by the exit validation gate in step 12), mark the phase as `awaiting-finalization` rather than `completed`, and record exactly which finalization step is pending.
|
|
519
533
|
|
|
@@ -620,7 +634,7 @@ See [Stop Conditions](../docs/stop-conditions.md). Local-specific stops: phase c
|
|
|
620
634
|
- Rerun validation after review-driven fixes.
|
|
621
635
|
- A phase is not operationally closed until its branch state is captured in commit history and the reviewed branch has been finalized according to session type (merged into local `main` for phase-doc-backed sessions; merged via GitHub PR for tracker-backed sessions), unless authorization for that finalization is still pending.
|
|
622
636
|
- For tracker-backed sessions, the handoff path is always: push the working branch → open a PR → merge via GitHub; never merge the working branch into local `main`.
|
|
623
|
-
- PR creation
|
|
637
|
+
- PR creation always goes through `dev-loops pr create`, which is ALWAYS draft and always assigned — self-assigned by default (`--assignee @me` when none is given; honors an explicit `--assignee <login>` / `-a <login>`), never via raw `gh pr create`, and the PR body must contain `Closes #N` (or `Fixes #N`) for the linked issue so GitHub auto-closes it on merge. When `.devloops` sets `workflow.requireDraftFirst` to true, use `dev-loops pr create --assignee @me ...`. 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 eligible.
|
|
624
638
|
- When authorization is pending, record the phase as `awaiting-finalization` and describe the exact missing step.
|
|
625
639
|
- For phase-doc-backed sessions, merge the fully reviewed, locally validated branch back into local `main` when authorized.
|
|
626
640
|
|