@opengsd/gsd-core 1.19.0 → 1.20.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-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/agents/gsd-code-fixer.compact.md +5 -5
- package/agents/gsd-code-fixer.md +9 -4
- package/agents/gsd-code-reviewer.compact.md +49 -8
- package/agents/gsd-code-reviewer.md +53 -14
- package/agents/gsd-executor.md +8 -4
- package/commands/gsd/update.md +5 -2
- package/gsd-core/bin/gsd-tools.cjs +73 -23
- package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
- package/gsd-core/bin/lib/capability-registry.cjs +284 -93
- package/gsd-core/bin/lib/capability-validator.cjs +15 -0
- package/gsd-core/bin/lib/commands.cjs +27 -17
- package/gsd-core/bin/lib/config-loader.cjs +3 -3
- package/gsd-core/bin/lib/core-utils.cjs +11 -0
- package/gsd-core/bin/lib/frontmatter.cjs +21 -3
- package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
- package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
- package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
- package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
- package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
- package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
- package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
- package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
- package/gsd-core/bin/lib/init.cjs +180 -67
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
- package/gsd-core/bin/lib/markdown-table.cjs +23 -0
- package/gsd-core/bin/lib/mcp-server.cjs +8 -5
- package/gsd-core/bin/lib/milestone.cjs +18 -50
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
- package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
- package/gsd-core/bin/lib/phase-id.cjs +609 -35
- package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
- package/gsd-core/bin/lib/phase-locator.cjs +328 -19
- package/gsd-core/bin/lib/phase.cjs +2990 -666
- package/gsd-core/bin/lib/planning-document.cjs +26 -5
- package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
- package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
- package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
- package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
- package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
- package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
- package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
- package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
- package/gsd-core/bin/lib/roadmap-parser.cjs +730 -144
- package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
- package/gsd-core/bin/lib/roadmap.cjs +358 -106
- package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
- package/gsd-core/bin/lib/smart-entry.cjs +22 -11
- package/gsd-core/bin/lib/state-contract.cjs +11 -7
- package/gsd-core/bin/lib/state-document.cjs +332 -23
- package/gsd-core/bin/lib/state-transition.cjs +39 -10
- package/gsd-core/bin/lib/state.cjs +498 -179
- package/gsd-core/bin/lib/template.cjs +2 -2
- package/gsd-core/bin/lib/validate.cjs +3 -2
- package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
- package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
- package/gsd-core/bin/lib/verify.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +26 -20
- package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
- package/gsd-core/references/cicd-review-patterns.md +45 -0
- package/gsd-core/references/necessity-review.md +48 -0
- package/gsd-core/workflows/code-review-fix.md +46 -5
- package/gsd-core/workflows/code-review.md +3 -0
- package/gsd-core/workflows/execute-phase.md +1 -1
- package/gsd-core/workflows/execute-plan.md +1 -1
- package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
- package/gsd-core/workflows/plan-phase.md +1 -1
- package/gsd-core/workflows/pr-branch.md +2 -0
- package/gsd-core/workflows/reapply-patches.md +104 -0
- package/gsd-core/workflows/review.md +54 -141
- package/gsd-core/workflows/secure-phase.md +1 -1
- package/gsd-core/workflows/settings-advanced.md +57 -9
- package/gsd-core/workflows/ship.md +12 -0
- package/hooks/dist/gsd-graphify-update.sh +6 -0
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/gsd-worktree-path-guard.js +4 -4
- package/hooks/dist/lib/git-probe.js +16 -0
- package/hooks/gsd-graphify-update.sh +6 -0
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-windsurf-pre-write.js +4 -4
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/gsd-worktree-path-guard.js +4 -4
- package/hooks/lib/git-probe.js +16 -0
- package/package.json +3 -3
- package/pi/package-agents/gsd-code-fixer.md +9 -4
- package/pi/package-agents/gsd-code-reviewer.md +53 -14
- package/pi/package-agents/gsd-executor.md +8 -4
- package/pi/package-agents/manifest.json +3 -3
- package/pi/package-skills/gsd-update/SKILL.md +4 -1
- package/scripts/ci-next-health.cjs +246 -23
- package/scripts/docs-guard-registry.cjs +2 -0
- package/scripts/gen-features.cjs +14 -2
- package/scripts/lib/drift-scan.cjs +27 -0
- package/scripts/lib/macos-conformance-tier.generated.cjs +6 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +11 -0
- package/scripts/lint-phase-enumeration-drift.cjs +8 -4
- package/scripts/lint-roadmap-progress-drift.cjs +421 -0
- package/scripts/mutation-matrix.cjs +23 -1
- package/scripts/sync-next-version.cjs +38 -6
- package/skills/gsd-update/SKILL.md +5 -2
- package/vscode/package.json +1 -1
|
@@ -59,14 +59,158 @@ function classifyRunConclusion(payload) {
|
|
|
59
59
|
return VERDICT.RED;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Run statuses that mean "an attempt is still going". A run object reports the
|
|
63
|
+
// state of its LATEST attempt only, so a re-run of a red run reads as
|
|
64
|
+
// in-flight with no trace of the failure it is re-running.
|
|
65
|
+
const IN_FLIGHT_STATUSES = new Set(['queued', 'in_progress', 'waiting', 'requested', 'pending']);
|
|
66
|
+
|
|
67
|
+
function isInFlight(run) {
|
|
68
|
+
return Boolean(run) && IN_FLIGHT_STATUSES.has(run.status);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Pure, total: the "list workflow runs" payload (newest first, ANY status) ->
|
|
73
|
+
* the verdict, the run it rests on, and why. Never throws.
|
|
74
|
+
*
|
|
75
|
+
* A completed newest run is judged by classifyRunConclusion. An in-flight
|
|
76
|
+
* newest run is never read as healthy on its own:
|
|
77
|
+
* - a re-run (`run_attempt` > 1) whose previous attempt did not succeed is RED
|
|
78
|
+
* — the fetcher attaches that attempt's conclusion as
|
|
79
|
+
* `previous_attempt_conclusion`, because the list endpoint cannot show it;
|
|
80
|
+
* - otherwise the newest SETTLED run decides (red stays RED, green is CLEAN).
|
|
81
|
+
* This is what stops a mid-flight re-run from hiding the failure it re-runs
|
|
82
|
+
* (2026-10-27: it let #5755 and #5750 through behind a red `next`).
|
|
83
|
+
*
|
|
84
|
+
* @returns {{verdict:string, run:(object|null), reason:string}}
|
|
85
|
+
*/
|
|
86
|
+
function classifyBaseRuns(payload) {
|
|
87
|
+
if (payload === null || typeof payload !== 'object' || !Array.isArray(payload.workflow_runs)) {
|
|
88
|
+
return { verdict: VERDICT.INDETERMINATE, run: null, reason: 'malformed-payload' };
|
|
89
|
+
}
|
|
90
|
+
const runs = payload.workflow_runs;
|
|
91
|
+
if (runs.length === 0) return { verdict: VERDICT.CLEAN, run: null, reason: 'resolved' };
|
|
92
|
+
|
|
93
|
+
const [newest] = runs;
|
|
94
|
+
if (!isInFlight(newest)) {
|
|
95
|
+
return {
|
|
96
|
+
verdict: classifyRunConclusion({ workflow_runs: [newest] }),
|
|
97
|
+
run: newest,
|
|
98
|
+
reason: 'resolved',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const prior = newest.previous_attempt_conclusion;
|
|
103
|
+
if (typeof prior === 'string' && prior !== '' && prior !== 'success') {
|
|
104
|
+
return { verdict: VERDICT.RED, run: newest, reason: 'rerun-in-flight-after-failure' };
|
|
105
|
+
}
|
|
106
|
+
// A re-run of an attempt that succeeded: the newest evidence is green, so an
|
|
107
|
+
// older red run must not turn the gate red.
|
|
108
|
+
if (prior === 'success') {
|
|
109
|
+
return { verdict: VERDICT.CLEAN, run: newest, reason: 'rerun-in-flight-after-success' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const settled = runs.find((run) => !isInFlight(run));
|
|
113
|
+
if (!settled) return { verdict: VERDICT.CLEAN, run: null, reason: 'newest-in-flight-no-history' };
|
|
114
|
+
return {
|
|
115
|
+
verdict: classifyRunConclusion({ workflow_runs: [settled] }),
|
|
116
|
+
run: settled,
|
|
117
|
+
reason: 'newest-in-flight',
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// merge_group heads are `gh-readonly-queue/<base>/pr-<N>-<40-hex sha>`; the
|
|
122
|
+
// name carries only the LAST PR of the group.
|
|
123
|
+
const QUEUE_REF_PATTERN = /^(?:refs\/heads\/)?gh-readonly-queue\/(.+)\/pr-(\d+)-[0-9a-f]{40}$/;
|
|
124
|
+
|
|
125
|
+
/** @returns {{base:string, prNumber:number}|null} */
|
|
126
|
+
function parseQueueRef(ref) {
|
|
127
|
+
if (typeof ref !== 'string') return null;
|
|
128
|
+
const match = QUEUE_REF_PATTERN.exec(ref.trim());
|
|
129
|
+
if (!match) return null;
|
|
130
|
+
return { base: match[1], prNumber: Number(match[2]) };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Repository permissions that may apply the bypass label. `maintain` is
|
|
134
|
+
// reported as `write` by this endpoint's legacy `permission` field; both are
|
|
135
|
+
// accepted so a role-name change cannot silently lock the bypass out.
|
|
136
|
+
const BYPASS_PERMISSIONS = new Set(['admin', 'maintain', 'write']);
|
|
137
|
+
const EVENT_PAGES_MAX = 10;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* merge_group has no label surface, so read it back from the API. The bypass
|
|
141
|
+
* is honoured only when ALL hold, and every failure is "not authorized" (RED),
|
|
142
|
+
* never fail-open:
|
|
143
|
+
* 1. the ref names a PR and the group holds exactly that one commit — a group
|
|
144
|
+
* of several PRs would let one labelled PR carry unlabelled ones;
|
|
145
|
+
* 2. the PR currently carries the label;
|
|
146
|
+
* 3. the actor of the label's latest `labeled` event has write or admin
|
|
147
|
+
* permission (triage-role users can apply labels).
|
|
148
|
+
*
|
|
149
|
+
* `ghGet(path)` returns parsed JSON or throws.
|
|
150
|
+
*
|
|
151
|
+
* @returns {Promise<{authorized:boolean, reason:string, actor?:string, prNumber?:number}>}
|
|
152
|
+
*/
|
|
153
|
+
async function evaluateQueueBypass({ headRef, baseRef, baseSha, headSha, repo, ghGet } = {}) {
|
|
154
|
+
const parsed = parseQueueRef(headRef);
|
|
155
|
+
if (!parsed) return { authorized: false, reason: 'queue-ref-unparseable' };
|
|
156
|
+
// The queue branch must be for the branch whose health is being judged.
|
|
157
|
+
if (typeof baseRef === 'string' && baseRef !== '' && parsed.base !== baseRef) {
|
|
158
|
+
return { authorized: false, reason: 'queue-base-mismatch', prNumber: parsed.prNumber };
|
|
159
|
+
}
|
|
160
|
+
const { prNumber } = parsed;
|
|
161
|
+
if (!baseSha || !headSha) return { authorized: false, reason: 'no-group-shas', prNumber };
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const compare = await ghGet(`/repos/${repo}/compare/${baseSha}...${headSha}`);
|
|
165
|
+
if (!compare || compare.total_commits !== 1) {
|
|
166
|
+
return { authorized: false, reason: 'multi-pr-group', prNumber };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const labels = await ghGet(`/repos/${repo}/issues/${prNumber}/labels?per_page=100`);
|
|
170
|
+
const hasLabel = Array.isArray(labels) && labels.some((l) => l && l.name === BYPASS_LABEL);
|
|
171
|
+
if (!hasLabel) return { authorized: false, reason: 'no-label', prNumber };
|
|
172
|
+
|
|
173
|
+
let actor = null;
|
|
174
|
+
let exhausted = false;
|
|
175
|
+
for (let page = 1; page <= EVENT_PAGES_MAX; page += 1) {
|
|
176
|
+
const events = await ghGet(`/repos/${repo}/issues/${prNumber}/events?per_page=100&page=${page}`);
|
|
177
|
+
if (!Array.isArray(events)) break;
|
|
178
|
+
for (const event of events) {
|
|
179
|
+
if (event && event.event === 'labeled' && event.label && event.label.name === BYPASS_LABEL) {
|
|
180
|
+
actor = event.actor && typeof event.actor.login === 'string' ? event.actor.login : null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (events.length < 100) break;
|
|
184
|
+
if (page === EVENT_PAGES_MAX) exhausted = true;
|
|
185
|
+
}
|
|
186
|
+
// More events than we read: the latest `labeled` event may be unseen, so
|
|
187
|
+
// the applier is unknown. Fail closed.
|
|
188
|
+
if (exhausted) return { authorized: false, reason: 'label-applier-unknown', prNumber };
|
|
189
|
+
if (!actor) return { authorized: false, reason: 'label-applier-unknown', prNumber };
|
|
190
|
+
|
|
191
|
+
const perm = await ghGet(`/repos/${repo}/collaborators/${encodeURIComponent(actor)}/permission`);
|
|
192
|
+
const level = perm && typeof perm.permission === 'string' ? perm.permission : '';
|
|
193
|
+
if (!BYPASS_PERMISSIONS.has(level)) {
|
|
194
|
+
return { authorized: false, reason: 'label-applier-lacks-write', actor, prNumber };
|
|
195
|
+
}
|
|
196
|
+
return { authorized: true, reason: 'queue-label', actor, prNumber };
|
|
197
|
+
} catch (err) {
|
|
198
|
+
return { authorized: false, reason: `bypass-check-failed: ${err.message}`, prNumber };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
62
202
|
/**
|
|
63
203
|
* Dependency-injected orchestrator. `fetchLatestRun` is an async function
|
|
64
204
|
* taking the resolved base branch name and returning the parsed API payload
|
|
65
205
|
* (or throwing) — injected so tests never touch the network.
|
|
206
|
+
* `evaluateQueueBypass` (merge_group only) is an async function returning
|
|
207
|
+
* `{authorized, reason, actor}`; it is how a queued fix-forward is recognised.
|
|
66
208
|
*
|
|
67
209
|
* @returns {Promise<{verdict:string, payload:*, reason:string}>}
|
|
68
210
|
*/
|
|
69
|
-
async function resolveNextHealth({
|
|
211
|
+
async function resolveNextHealth({
|
|
212
|
+
fetchLatestRun, eventName, baseRef, labels, evaluateQueueBypass: queueBypass,
|
|
213
|
+
} = {}) {
|
|
70
214
|
if (!APPLICABLE_EVENTS.has(eventName)) {
|
|
71
215
|
return { verdict: VERDICT.SKIPPED_NOT_APPLICABLE, payload: null, reason: 'not-applicable-event' };
|
|
72
216
|
}
|
|
@@ -82,23 +226,46 @@ async function resolveNextHealth({ fetchLatestRun, eventName, baseRef, labels }
|
|
|
82
226
|
return { verdict: VERDICT.INDETERMINATE, payload: null, reason: `fetch-failed: ${err.message}` };
|
|
83
227
|
}
|
|
84
228
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
229
|
+
const assessed = classifyBaseRuns(payload);
|
|
230
|
+
// The run the verdict rests on, so annotations link the red run and not the
|
|
231
|
+
// in-flight re-run that sits in front of it.
|
|
232
|
+
const evidence = assessed.run ? { workflow_runs: [assessed.run] } : payload;
|
|
233
|
+
if (assessed.verdict !== VERDICT.RED) {
|
|
234
|
+
const reason = assessed.reason === 'resolved' ? 'resolved' : assessed.reason;
|
|
235
|
+
return { verdict: assessed.verdict, payload: evidence, reason };
|
|
88
236
|
}
|
|
89
237
|
|
|
90
|
-
// Escape hatch
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// fix-forward as a direct pull_request merge (where the label IS readable)
|
|
94
|
-
// rather than through the merge queue, until GitHub exposes an equivalent
|
|
95
|
-
// signal for merge_group.
|
|
238
|
+
// Escape hatch for the PR that is itself the fix-forward. pull_request runs
|
|
239
|
+
// read the label from the event (`labels`). merge_group events carry no
|
|
240
|
+
// labels, so the queue run asks the API instead (evaluateQueueBypass).
|
|
96
241
|
const labelList = Array.isArray(labels) ? labels : [];
|
|
97
242
|
if (labelList.includes(BYPASS_LABEL)) {
|
|
98
|
-
return { verdict: VERDICT.BYPASSED, payload, reason: 'bypass-label' };
|
|
243
|
+
return { verdict: VERDICT.BYPASSED, payload: evidence, reason: 'bypass-label' };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (eventName === 'merge_group' && typeof queueBypass === 'function') {
|
|
247
|
+
let decision;
|
|
248
|
+
try {
|
|
249
|
+
decision = await queueBypass();
|
|
250
|
+
} catch (err) {
|
|
251
|
+
decision = { authorized: false, reason: `bypass-check-failed: ${err.message}` };
|
|
252
|
+
}
|
|
253
|
+
if (decision && decision.authorized) {
|
|
254
|
+
return {
|
|
255
|
+
verdict: VERDICT.BYPASSED,
|
|
256
|
+
payload: evidence,
|
|
257
|
+
reason: decision.reason || 'queue-label',
|
|
258
|
+
actor: decision.actor,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
verdict: VERDICT.RED,
|
|
263
|
+
payload: evidence,
|
|
264
|
+
reason: `${assessed.reason === 'resolved' ? 'red' : assessed.reason}; bypass: ${decision && decision.reason}`,
|
|
265
|
+
};
|
|
99
266
|
}
|
|
100
267
|
|
|
101
|
-
return { verdict: VERDICT.RED, payload, reason: 'red' };
|
|
268
|
+
return { verdict: VERDICT.RED, payload: evidence, reason: assessed.reason === 'resolved' ? 'red' : assessed.reason };
|
|
102
269
|
}
|
|
103
270
|
|
|
104
271
|
function usage() {
|
|
@@ -108,9 +275,15 @@ function usage() {
|
|
|
108
275
|
'',
|
|
109
276
|
'CI-only gate: checks whether the PR/merge-group\'s base branch\'s own last',
|
|
110
277
|
'push-triggered Tests run is red, and fails the job (exit 1) when it is —',
|
|
111
|
-
'unless the PR carries the "fix-next" bypass label.
|
|
112
|
-
'
|
|
113
|
-
'
|
|
278
|
+
'unless the PR carries the "fix-next" bypass label. A push run that is',
|
|
279
|
+
'still in flight (including a re-run of a failed run) never counts as',
|
|
280
|
+
'healthy: a re-run whose previous attempt failed is RED, otherwise the',
|
|
281
|
+
'newest settled run decides. Every other case (wrong event, no',
|
|
282
|
+
'resolvable base ref, an unreadable API read) fails open (exit 0).',
|
|
283
|
+
'',
|
|
284
|
+
'On merge_group the label is read back from the API: the queue ref must',
|
|
285
|
+
'name a PR, the group must hold exactly one commit, the PR must carry the',
|
|
286
|
+
'label, and whoever applied it must have write or admin permission.',
|
|
114
287
|
'',
|
|
115
288
|
'Environment variables read:',
|
|
116
289
|
' GITHUB_EVENT_NAME workflow trigger event; only "pull_request" and',
|
|
@@ -128,6 +301,12 @@ function usage() {
|
|
|
128
301
|
' the caller workflow must populate this from',
|
|
129
302
|
' github.event.pull_request.labels.*.name); checked',
|
|
130
303
|
' for the literal "fix-next" bypass label',
|
|
304
|
+
' MERGE_GROUP_HEAD_REF merge-group queue branch',
|
|
305
|
+
' (github.event.merge_group.head_ref; falls back to',
|
|
306
|
+
' GITHUB_REF)',
|
|
307
|
+
' MERGE_GROUP_BASE_SHA merge-group base sha (github.event.merge_group.base_sha)',
|
|
308
|
+
' MERGE_GROUP_HEAD_SHA merge-group head sha (github.event.merge_group.head_sha;',
|
|
309
|
+
' falls back to GITHUB_SHA)',
|
|
131
310
|
' GITHUB_OUTPUT path to append verdict= step output to',
|
|
132
311
|
' GITHUB_STEP_SUMMARY path to append a human-readable summary to',
|
|
133
312
|
].join('\n');
|
|
@@ -200,9 +379,10 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
200
379
|
const baseRef = resolveBaseRef(eventName);
|
|
201
380
|
const labels = parseLabels(process.env.PR_LABELS);
|
|
202
381
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
382
|
+
// JSON GET against the API. `path` is either absolute-from-root (starts with
|
|
383
|
+
// `/`) or a full URL. Returns parsed JSON (null if the body is not JSON).
|
|
384
|
+
const ghGet = async (pathOrUrl) => {
|
|
385
|
+
const url = pathOrUrl.startsWith('http') ? pathOrUrl : `${apiBase}${pathOrUrl}`;
|
|
206
386
|
const headers = {
|
|
207
387
|
accept: 'application/vnd.github+json',
|
|
208
388
|
'x-github-api-version': '2022-11-28',
|
|
@@ -221,26 +401,66 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
221
401
|
}
|
|
222
402
|
};
|
|
223
403
|
|
|
224
|
-
|
|
404
|
+
// No `status=completed` filter: an in-flight re-run must be visible so it
|
|
405
|
+
// cannot hide the failure it is re-running (see classifyBaseRuns).
|
|
406
|
+
const fetchLatestRun = async (branch) => {
|
|
407
|
+
const payload = await ghGet(
|
|
408
|
+
`/repos/${repo}/actions/workflows/${TESTS_WORKFLOW_FILE}/runs`
|
|
409
|
+
+ `?branch=${encodeURIComponent(branch)}&event=push&per_page=5`,
|
|
410
|
+
);
|
|
411
|
+
const newest = payload && Array.isArray(payload.workflow_runs) ? payload.workflow_runs[0] : undefined;
|
|
412
|
+
if (isInFlight(newest) && Number(newest.run_attempt) > 1) {
|
|
413
|
+
// The list shows only the latest attempt; the previous one holds the
|
|
414
|
+
// failure being re-run. An API error here propagates -> INDETERMINATE.
|
|
415
|
+
// If this read fails we cannot show the re-run is not hiding a failure,
|
|
416
|
+
// and failing open here would reopen the hole this closes: treat an
|
|
417
|
+
// unreadable previous attempt as not-success (RED).
|
|
418
|
+
try {
|
|
419
|
+
const previous = await ghGet(
|
|
420
|
+
`/repos/${repo}/actions/runs/${newest.id}/attempts/${Number(newest.run_attempt) - 1}`,
|
|
421
|
+
);
|
|
422
|
+
newest.previous_attempt_conclusion = previous && typeof previous.conclusion === 'string'
|
|
423
|
+
? previous.conclusion : 'unknown';
|
|
424
|
+
} catch {
|
|
425
|
+
newest.previous_attempt_conclusion = 'unknown';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return payload;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
const evaluateQueue = () => evaluateQueueBypass({
|
|
432
|
+
headRef: process.env.MERGE_GROUP_HEAD_REF || process.env.GITHUB_REF || '',
|
|
433
|
+
baseRef,
|
|
434
|
+
baseSha: process.env.MERGE_GROUP_BASE_SHA || '',
|
|
435
|
+
headSha: process.env.MERGE_GROUP_HEAD_SHA || process.env.GITHUB_SHA || '',
|
|
436
|
+
repo,
|
|
437
|
+
ghGet,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const result = await resolveNextHealth({
|
|
441
|
+
fetchLatestRun, eventName, baseRef, labels, evaluateQueueBypass: evaluateQueue,
|
|
442
|
+
});
|
|
225
443
|
|
|
226
444
|
writeOutput([`verdict=${result.verdict}`]);
|
|
227
|
-
writeSummary(`Base branch health: ${result.verdict}`);
|
|
445
|
+
writeSummary(`Base branch health: ${result.verdict}${result.reason ? ` (${result.reason})` : ''}`);
|
|
228
446
|
|
|
229
447
|
if (result.verdict === VERDICT.RED) {
|
|
230
448
|
const runUrl = runUrlOf(result.payload);
|
|
231
449
|
process.stderr.write(
|
|
232
|
-
`::error::the base branch "${baseRef}"'s own last Tests run is red: ${runUrl} — `
|
|
450
|
+
`::error::the base branch "${baseRef}"'s own last Tests run is red or still re-running after a failure: ${runUrl} (${result.reason}) — `
|
|
233
451
|
+ 'wait for a fix-forward merge, or if THIS pull request is the fix, ask a '
|
|
234
|
-
+ `maintainer to apply the "${BYPASS_LABEL}" label
|
|
452
|
+
+ `maintainer with write access to apply the "${BYPASS_LABEL}" label and re-queue it `
|
|
453
|
+
+ '(the queue run reads the label from the API; it must be the only PR in its merge group).\n',
|
|
235
454
|
);
|
|
236
455
|
return 1;
|
|
237
456
|
}
|
|
238
457
|
|
|
239
458
|
if (result.verdict === VERDICT.BYPASSED) {
|
|
240
459
|
const runUrl = runUrlOf(result.payload);
|
|
460
|
+
const by = result.actor ? ` (label applied by ${result.actor})` : '';
|
|
241
461
|
process.stderr.write(
|
|
242
462
|
`::warning::the base branch "${baseRef}"'s own last Tests run is red: ${runUrl} — `
|
|
243
|
-
+ `a human applied the "${BYPASS_LABEL}" label to explicitly override this gate.\n`,
|
|
463
|
+
+ `a human applied the "${BYPASS_LABEL}" label to explicitly override this gate${by}.\n`,
|
|
244
464
|
);
|
|
245
465
|
return 0;
|
|
246
466
|
}
|
|
@@ -266,6 +486,9 @@ module.exports = {
|
|
|
266
486
|
BYPASS_LABEL,
|
|
267
487
|
TESTS_WORKFLOW_FILE,
|
|
268
488
|
classifyRunConclusion,
|
|
489
|
+
classifyBaseRuns,
|
|
490
|
+
parseQueueRef,
|
|
491
|
+
evaluateQueueBypass,
|
|
269
492
|
resolveNextHealth,
|
|
270
493
|
main,
|
|
271
494
|
};
|
|
@@ -163,6 +163,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
163
163
|
'docs/COMMANDS.md',
|
|
164
164
|
'docs/how-to/run-phases-autonomously.md',
|
|
165
165
|
],
|
|
166
|
+
'tests/adr-612-bracket-new-project-config.test.cjs': ['docs/CONFIGURATION.md'],
|
|
166
167
|
// Walks docs/adr/ as a directory (builds/reads docs/adr/README.md and
|
|
167
168
|
// fixture ADRs throughout) and separately reads docs/contributor-standards.md.
|
|
168
169
|
'tests/adr-index-gate.test.cjs': ['docs/adr/', 'docs/contributor-standards.md'],
|
|
@@ -319,6 +320,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
319
320
|
'tests/host-integration-descriptors.test.cjs': [
|
|
320
321
|
'docs/reference/host-integration-capability-matrix.md',
|
|
321
322
|
],
|
|
323
|
+
'tests/inline-plan-threshold-default.test.cjs': ['docs/CONFIGURATION.md', 'docs/zh-CN/CONFIGURATION.md', 'docs/pt-BR/CONFIGURATION.md'],
|
|
322
324
|
'tests/install.test.cjs': ['docs/reference/agents.md', 'docs/INVENTORY.md', 'docs/INVENTORY-MANIFEST.json'],
|
|
323
325
|
// SOURCE_DIRS includes 'docs' and walks it recursively for every .md file
|
|
324
326
|
// (intel.test.cjs:1223-1228) — a generic tree walk.
|
package/scripts/gen-features.cjs
CHANGED
|
@@ -666,6 +666,17 @@ function describeViolation(v) {
|
|
|
666
666
|
}
|
|
667
667
|
}
|
|
668
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Fragment bodies live in `docs/features/` and are emitted into `docs/FEATURES.md`, one
|
|
671
|
+
* directory up, so a relative markdown link target written for the fragment must lose
|
|
672
|
+
* `depth` leading `../` to resolve from the index (1 for a section; 2 for a group note from
|
|
673
|
+
* `docs/features/_groups/`). Only relative targets are touched:
|
|
674
|
+
* `http(s):`, `#anchor` and absolute `/` targets (and anything not starting `../`) pass through.
|
|
675
|
+
*/
|
|
676
|
+
function rewriteRelativeLinks(text, depth = 1) {
|
|
677
|
+
return text.replace(new RegExp(`\\]\\((?:\\.\\./){${depth}}`, 'g'), '](');
|
|
678
|
+
}
|
|
679
|
+
|
|
669
680
|
/** Render the generated region: the table of contents, then every section. */
|
|
670
681
|
function renderFeatures(corpus) {
|
|
671
682
|
const out = [START_MARKER, '', '## Table of Contents', ''];
|
|
@@ -677,10 +688,10 @@ function renderFeatures(corpus) {
|
|
|
677
688
|
|
|
678
689
|
for (const g of corpus.groups) {
|
|
679
690
|
out.push('', '---', '', `## ${g.title}`, '');
|
|
680
|
-
if (g.note) out.push(g.note, '');
|
|
691
|
+
if (g.note) out.push(rewriteRelativeLinks(g.note, 2), '');
|
|
681
692
|
g.sections.forEach((s, i) => {
|
|
682
693
|
if (i > 0) out.push('---', '');
|
|
683
|
-
out.push(`### ${s.id}. ${s.title}`, '', s.body, '');
|
|
694
|
+
out.push(`### ${s.id}. ${s.title}`, '', rewriteRelativeLinks(s.body), '');
|
|
684
695
|
});
|
|
685
696
|
}
|
|
686
697
|
|
|
@@ -836,6 +847,7 @@ module.exports = {
|
|
|
836
847
|
checkInboundAnchors,
|
|
837
848
|
buildCorpus,
|
|
838
849
|
renderFeatures,
|
|
850
|
+
rewriteRelativeLinks,
|
|
839
851
|
spliceIntoFeatures,
|
|
840
852
|
describeViolation,
|
|
841
853
|
};
|
|
@@ -294,12 +294,39 @@ function sanitizeForReport(text) {
|
|
|
294
294
|
.replace(/[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2066-\u2069]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
|
|
295
295
|
}
|
|
296
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Read the quoted or backtick-delimited string literal starting at `line[start]`
|
|
299
|
+
* (which must be `'`, `"`, or a backtick). Returns `{ text, end }` (`text`
|
|
300
|
+
* includes both delimiters, `end` is the index one past the literal) or null when
|
|
301
|
+
* no close quote is found within MAX_REGEX_LITERAL_LEN characters. Same single
|
|
302
|
+
* left-to-right, no-backtracking, escape-aware pass as `readRegexLiteralAt` (a
|
|
303
|
+
* `\x` escape consumes both characters, so an escaped quote never ends the
|
|
304
|
+
* literal early). A literal cannot span lines in a per-line scan. The older drift
|
|
305
|
+
* guards each carry their own byte-identical copy; new guards import this one.
|
|
306
|
+
*/
|
|
307
|
+
function readStringLiteralAt(line, start) {
|
|
308
|
+
const quote = line[start];
|
|
309
|
+
if (quote !== "'" && quote !== '"' && quote !== '`') return null;
|
|
310
|
+
const limit = Math.min(line.length, start + MAX_REGEX_LITERAL_LEN);
|
|
311
|
+
for (let i = start + 1; i < limit; i++) {
|
|
312
|
+
const ch = line[i];
|
|
313
|
+
if (ch === '\\') {
|
|
314
|
+
i++;
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (ch === '\r' || ch === '\n') return null;
|
|
318
|
+
if (ch === quote) return { text: line.slice(start, i + 1), end: i + 1 };
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
|
|
297
323
|
module.exports = {
|
|
298
324
|
SKIP_DIR_NAMES,
|
|
299
325
|
isInsideRoot,
|
|
300
326
|
isUnderSkippedDir,
|
|
301
327
|
walk,
|
|
302
328
|
readRegexLiteralAt,
|
|
329
|
+
readStringLiteralAt,
|
|
303
330
|
MAX_REGEX_LITERAL_LEN,
|
|
304
331
|
sanitizeForReport,
|
|
305
332
|
scanTree,
|
|
@@ -9,6 +9,8 @@ module.exports = {
|
|
|
9
9
|
"tests/5100-win32-bare-bash-sentinel.property.test.cjs",
|
|
10
10
|
"tests/active-workstream-store.unit.test.cjs",
|
|
11
11
|
"tests/adr-612-bracket-phase-counting.test.cjs",
|
|
12
|
+
"tests/adr-612-bracket-phase-remove.test.cjs",
|
|
13
|
+
"tests/adr-612-bracket-write-path.test.cjs",
|
|
12
14
|
"tests/adr-index-gate.test.cjs",
|
|
13
15
|
"tests/adr-parser.property.test.cjs",
|
|
14
16
|
"tests/adr-parser.unit.test.cjs",
|
|
@@ -63,6 +65,7 @@ module.exports = {
|
|
|
63
65
|
"tests/contributor-standards.test.cjs",
|
|
64
66
|
"tests/core-utils.test.cjs",
|
|
65
67
|
"tests/cursor-subagent-isolation.test.cjs",
|
|
68
|
+
"tests/custom-phase-readers.test.cjs",
|
|
66
69
|
"tests/debugger-semantic-recall.test.cjs",
|
|
67
70
|
"tests/default-flip-documentation-lint.test.cjs",
|
|
68
71
|
"tests/discuss-phase-power.test.cjs",
|
|
@@ -94,6 +97,7 @@ module.exports = {
|
|
|
94
97
|
"tests/gen-context-index.test.cjs",
|
|
95
98
|
"tests/gen-section-manifest.test.cjs",
|
|
96
99
|
"tests/git-base-branch.test.cjs",
|
|
100
|
+
"tests/git-location-env-scrub.test.cjs",
|
|
97
101
|
"tests/graphify-query.test.cjs",
|
|
98
102
|
"tests/graphify-visualization.test.cjs",
|
|
99
103
|
"tests/gsd-agent-isolation-guard.test.cjs",
|
|
@@ -202,6 +206,7 @@ module.exports = {
|
|
|
202
206
|
"tests/retired-artifact-cleanup.test.cjs",
|
|
203
207
|
"tests/review-build-prompt-optional-sections.test.cjs",
|
|
204
208
|
"tests/review-lane-invocation.test.cjs",
|
|
209
|
+
"tests/review-lane-prompt-cap.test.cjs",
|
|
205
210
|
"tests/review-lane-runner.test.cjs",
|
|
206
211
|
"tests/review-lane-windows-spawn-resolution.test.cjs",
|
|
207
212
|
"tests/review-parallel-lanes.test.cjs",
|
|
@@ -229,6 +234,7 @@ module.exports = {
|
|
|
229
234
|
"tests/shell-command-projection-md-normalize.test.cjs",
|
|
230
235
|
"tests/shipped-reference-cites.test.cjs",
|
|
231
236
|
"tests/skill-manifest.test.cjs",
|
|
237
|
+
"tests/smart-entry.unit.test.cjs",
|
|
232
238
|
"tests/spawn-liveness-banner.test.cjs",
|
|
233
239
|
"tests/state-document.test.cjs",
|
|
234
240
|
"tests/state-transition.test.cjs",
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
CONFORMANCE_TIER_FILES: [
|
|
6
6
|
"tests/5100-win32-bare-bash-sentinel.property.test.cjs",
|
|
7
|
+
"tests/adr-612-bracket-new-project-config.test.cjs",
|
|
8
|
+
"tests/adr-612-bracket-phase-remove.test.cjs",
|
|
9
|
+
"tests/adr-612-bracket-write-path.test.cjs",
|
|
7
10
|
"tests/adr-index-gate.test.cjs",
|
|
8
11
|
"tests/adr857-core-without-capabilities.test.cjs",
|
|
9
12
|
"tests/agent-install-check.test.cjs",
|
|
@@ -52,7 +55,9 @@ module.exports = {
|
|
|
52
55
|
"tests/claude-md.test.cjs",
|
|
53
56
|
"tests/cline-install.test.cjs",
|
|
54
57
|
"tests/close-phase-todos-padded-resolves.test.cjs",
|
|
58
|
+
"tests/code-review-cicd.test.cjs",
|
|
55
59
|
"tests/code-review-fix-pipeline-regression.test.cjs",
|
|
60
|
+
"tests/code-review-necessity.test.cjs",
|
|
56
61
|
"tests/code-review-pipeline-regression.test.cjs",
|
|
57
62
|
"tests/code-review-tier3-files-override-scoping.test.cjs",
|
|
58
63
|
"tests/code-review.test.cjs",
|
|
@@ -86,6 +91,7 @@ module.exports = {
|
|
|
86
91
|
"tests/cursor-hook-workspace-roots.test.cjs",
|
|
87
92
|
"tests/cursor-hooks.test.cjs",
|
|
88
93
|
"tests/cursor-subagent-isolation.test.cjs",
|
|
94
|
+
"tests/custom-phase-readers.test.cjs",
|
|
89
95
|
"tests/debug-session-management.test.cjs",
|
|
90
96
|
"tests/defaults-json-fallback.test.cjs",
|
|
91
97
|
"tests/diagnose-windows-spawn-watcher.test.cjs",
|
|
@@ -134,6 +140,7 @@ module.exports = {
|
|
|
134
140
|
"tests/gen-health-docs.test.cjs",
|
|
135
141
|
"tests/gen-section-manifest.test.cjs",
|
|
136
142
|
"tests/git-base-branch.test.cjs",
|
|
143
|
+
"tests/git-location-env-scrub.test.cjs",
|
|
137
144
|
"tests/git-optional-locks-parity.test.cjs",
|
|
138
145
|
"tests/golden-install-tree.test.cjs",
|
|
139
146
|
"tests/graphify-graph-path.test.cjs",
|
|
@@ -143,6 +150,7 @@ module.exports = {
|
|
|
143
150
|
"tests/gsd-check-update-worker-atomic-cache.test.cjs",
|
|
144
151
|
"tests/gsd-check-update-worker-platform-gate.test.cjs",
|
|
145
152
|
"tests/gsd-mcp-server-bin.test.cjs",
|
|
153
|
+
"tests/gsd-mcp-server.test.cjs",
|
|
146
154
|
"tests/gsd-path-export-hook.test.cjs",
|
|
147
155
|
"tests/gsd-statusline.test.cjs",
|
|
148
156
|
"tests/gsd-validate-commit-crash-policy.test.cjs",
|
|
@@ -268,6 +276,7 @@ module.exports = {
|
|
|
268
276
|
"tests/process-seam.test.cjs",
|
|
269
277
|
"tests/profile-output.test.cjs",
|
|
270
278
|
"tests/profile-pipeline.test.cjs",
|
|
279
|
+
"tests/progress-drift-guard.test.cjs",
|
|
271
280
|
"tests/prohibition-enforcement.test.cjs",
|
|
272
281
|
"tests/project-root.test.cjs",
|
|
273
282
|
"tests/quick-batch-command-router.stdin.test.cjs",
|
|
@@ -290,6 +299,7 @@ module.exports = {
|
|
|
290
299
|
"tests/review-lane-availability.test.cjs",
|
|
291
300
|
"tests/review-lane-invocation.test.cjs",
|
|
292
301
|
"tests/review-lane-no-redirect-5433.test.cjs",
|
|
302
|
+
"tests/review-lane-prompt-cap.test.cjs",
|
|
293
303
|
"tests/review-lane-runner.test.cjs",
|
|
294
304
|
"tests/review-lane-windows-spawn-resolution.test.cjs",
|
|
295
305
|
"tests/review-model-config.test.cjs",
|
|
@@ -327,6 +337,7 @@ module.exports = {
|
|
|
327
337
|
"tests/shipped-reference-cites.test.cjs",
|
|
328
338
|
"tests/skill-manifest.test.cjs",
|
|
329
339
|
"tests/slug-derivation-drift-guard.test.cjs",
|
|
340
|
+
"tests/smart-entry.unit.test.cjs",
|
|
330
341
|
"tests/state-todos-render.test.cjs",
|
|
331
342
|
"tests/state.platform.test.cjs",
|
|
332
343
|
"tests/subject-first-line.platform.test.cjs",
|
|
@@ -191,6 +191,10 @@
|
|
|
191
191
|
* WHOLE ROADMAP (not milestone-scoped) for existing `2.N` ids — an id
|
|
192
192
|
* collision it must avoid can come from any milestone, so it needs the
|
|
193
193
|
* physical set, matching the CREATE-adjacent exemption category.
|
|
194
|
+
* - `src/phase.cts` `collectPhaseDirNums` (#5528): the allocators' one on-disk scan,
|
|
195
|
+
* run over the cwd's phases dir (cmdPhaseAdd/cmdPhaseAddBatch) and a sibling worktree's
|
|
196
|
+
* (collectSiblingWorktreePhaseNums). It builds the set of USED phase numbers across
|
|
197
|
+
* checkouts, not a milestone-scoped list, so listMilestonePhaseDirs cannot serve it.
|
|
194
198
|
* - `src/phase.cts` `cmdPhasePlanIndex`: resolves ONE caller-supplied
|
|
195
199
|
* `phase` id to its directory — a single-phase LOCATION lookup, not an
|
|
196
200
|
* enumeration of the current milestone's phase set.
|
|
@@ -316,10 +320,10 @@ const FUNCTION_SCOPED_EXEMPTIONS = new Map([
|
|
|
316
320
|
[path.join('src', 'gate-phase-context.cts'), new Set(['resolvePhaseDirByToken'])],
|
|
317
321
|
[path.join('src', 'init.cts'), new Set(['detectHasPriorPhases', 'detectUiPhaseActive'])],
|
|
318
322
|
[path.join('src', 'milestone.cts'), new Set(['archivePhaseDirectories', 'cmdMilestoneComplete', 'cmdPhasesClear'])],
|
|
319
|
-
// #3849:
|
|
320
|
-
// a
|
|
321
|
-
//
|
|
322
|
-
[path.join('src', 'phase.cts'), new Set(['cmdPhasesList', 'cmdPhaseNextDecimal', 'cmdPhasePlanIndex', 'cmdPhaseInsert', 'renameDecimalPhases', 'renameIntegerPhases', '
|
|
323
|
+
// #3849/#5528: collectPhaseDirNums is the allocators' one on-disk scan (the cwd's phases dir for
|
|
324
|
+
// cmdPhaseAdd/cmdPhaseAddBatch, a sibling worktree's for collectSiblingWorktreePhaseNums). It is a
|
|
325
|
+
// used-NUMBER roster across checkouts, not the milestone-scoped list listMilestonePhaseDirs serves.
|
|
326
|
+
[path.join('src', 'phase.cts'), new Set(['cmdPhasesList', 'cmdPhaseNextDecimal', 'cmdPhasePlanIndex', 'cmdPhaseInsert', 'renameDecimalPhases', 'renameIntegerPhases', 'collectPhaseDirNums'])],
|
|
323
327
|
[path.join('src', 'audit.cts'), new Set(['listAuditPhaseTargets'])],
|
|
324
328
|
[path.join('src', 'commands.cts'), new Set(['cmdHistoryDigest'])],
|
|
325
329
|
[path.join('src', 'state.cts'), new Set(['cmdStateValidate', 'cmdStateSync', 'cmdStateRebuild', 'countRoadmapPhaseHeadings'])],
|