@opengsd/gsd-core 1.20.1 → 1.21.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/.opencode/plugins/gsd-core.js +33 -2
- package/agents/gsd-executor.md +1 -1
- package/agents/gsd-plan-checker.md +1 -1
- package/agents/gsd-planner.md +3 -3
- package/agents/gsd-verifier.md +2 -2
- package/bin/install.js +234 -126
- package/commands/gsd/autonomous.md +1 -1
- package/gsd-core/bin/gsd-tools.cjs +450 -753
- package/gsd-core/bin/lib/capability-activation.cjs +8 -2
- package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
- package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
- package/gsd-core/bin/lib/capability-lock.cjs +19 -27
- package/gsd-core/bin/lib/capability-registry.cjs +74 -71
- package/gsd-core/bin/lib/capability-validator.cjs +1 -0
- package/gsd-core/bin/lib/capability-writer.cjs +7 -4
- package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
- package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
- package/gsd-core/bin/lib/cli-surface.cjs +545 -0
- package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
- package/gsd-core/bin/lib/commands.cjs +80 -2
- package/gsd-core/bin/lib/config-file-read.cjs +85 -0
- package/gsd-core/bin/lib/config-loader.cjs +23 -15
- package/gsd-core/bin/lib/config.cjs +139 -101
- package/gsd-core/bin/lib/configuration.cjs +41 -2
- package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
- package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
- package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
- package/gsd-core/bin/lib/gap-checker.cjs +12 -1
- package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
- package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
- package/gsd-core/bin/lib/graphify.cjs +19 -2
- package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
- package/gsd-core/bin/lib/init.cjs +13 -11
- package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
- package/gsd-core/bin/lib/install-engine.cjs +11 -19
- package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
- package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
- package/gsd-core/bin/lib/io.cjs +6 -0
- package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
- package/gsd-core/bin/lib/markdown-table.cjs +39 -1
- package/gsd-core/bin/lib/milestone.cjs +8 -6
- package/gsd-core/bin/lib/model-resolver.cjs +8 -4
- package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
- package/gsd-core/bin/lib/phase-locator.cjs +8 -3
- package/gsd-core/bin/lib/phase.cjs +107 -31
- package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
- package/gsd-core/bin/lib/planning-snapshot.cjs +8 -4
- package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
- package/gsd-core/bin/lib/profile-output.cjs +9 -6
- package/gsd-core/bin/lib/project-context.cjs +439 -0
- package/gsd-core/bin/lib/project-root.cjs +9 -230
- package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
- package/gsd-core/bin/lib/report-parser.cjs +16 -0
- package/gsd-core/bin/lib/roadmap-parser.cjs +328 -50
- package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
- package/gsd-core/bin/lib/roadmap.cjs +2 -2
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +7 -4
- package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
- package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
- package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
- package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
- package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
- package/gsd-core/bin/lib/state-transition.cjs +91 -41
- package/gsd-core/bin/lib/state.cjs +446 -61
- package/gsd-core/bin/lib/surface.cjs +4 -7
- package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
- package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
- package/gsd-core/bin/lib/verify.cjs +49 -6
- package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
- package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
- package/gsd-core/references/autonomous-smart-discuss.md +99 -18
- package/gsd-core/references/checkpoints.md +1 -1
- package/gsd-core/references/planner-guidance.md +11 -0
- package/gsd-core/references/planner-human-verify-mode.md +1 -1
- package/gsd-core/references/planner-reversibility.md +2 -0
- package/gsd-core/references/planner-verify-command-grounding.md +22 -6
- package/gsd-core/workflows/autonomous.md +4 -4
- package/gsd-core/workflows/code-review-fix.md +5 -7
- package/gsd-core/workflows/execute-phase.md +21 -7
- package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
- package/gsd-core/workflows/plan-phase.md +18 -19
- package/hooks/dist/gsd-agent-isolation-guard.js +3 -2
- package/hooks/dist/gsd-config-reload.js +2 -2
- package/hooks/dist/gsd-context-monitor.js +2 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +8 -5
- package/hooks/dist/gsd-read-injection-scanner.js +2 -2
- package/hooks/dist/gsd-secret-read-guard.js +141 -6
- package/hooks/dist/gsd-statusline.js +3 -2
- package/hooks/dist/gsd-workflow-guard.js +2 -1
- package/hooks/dist/lib/config-read.js +49 -0
- package/hooks/dist/lib/git-cmd.js +3 -1
- package/hooks/dist/lib/git-head-tree.js +474 -0
- package/hooks/dist/lib/isolation-sentinel.js +97 -17
- package/hooks/dist/lib/secret-read-exemptions.js +161 -0
- package/hooks/gsd-agent-isolation-guard.js +3 -2
- package/hooks/gsd-config-reload.js +2 -2
- package/hooks/gsd-context-monitor.js +2 -1
- package/hooks/gsd-cursor-subagent-start.js +8 -5
- package/hooks/gsd-read-injection-scanner.js +2 -2
- package/hooks/gsd-secret-read-guard.js +141 -6
- package/hooks/gsd-statusline.js +3 -2
- package/hooks/gsd-workflow-guard.js +2 -1
- package/hooks/lib/config-read.js +49 -0
- package/hooks/lib/git-cmd.js +3 -1
- package/hooks/lib/git-head-tree.js +474 -0
- package/hooks/lib/isolation-sentinel.js +97 -17
- package/hooks/lib/secret-read-exemptions.js +161 -0
- package/package.json +7 -2
- package/pi/package-agents/gsd-executor.md +1 -1
- package/pi/package-agents/gsd-plan-checker.md +1 -1
- package/pi/package-agents/gsd-planner.md +3 -3
- package/pi/package-agents/gsd-verifier.md +2 -2
- package/pi/package-agents/manifest.json +4 -4
- package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
- package/scripts/changeset/cli.cjs +2 -2
- package/scripts/changeset/serialize.cjs +12 -2
- package/scripts/ci-compact-v8-coverage.cjs +204 -0
- package/scripts/ci-fix-next-rerun.cjs +149 -0
- package/scripts/ci-next-health.cjs +52 -9
- package/scripts/ci-test-scope.cjs +77 -71
- package/scripts/command-contract-helpers.cjs +56 -7
- package/scripts/docs-guard-registry.cjs +3 -0
- package/scripts/gen-cli-surface-manifest.cjs +287 -0
- package/scripts/gen-platform-conformance-tier.cjs +258 -19
- package/scripts/lib/macos-conformance-tier.generated.cjs +55 -47
- package/scripts/lib/platform-conformance-tier.generated.cjs +19 -64
- package/scripts/lib/shellcheck-fetch.cjs +71 -4
- package/scripts/lint-removed-but-needed.cjs +562 -50
- package/scripts/release-notes/changelog-release-notes.cjs +345 -0
- package/scripts/release-notes/discord-release-summary.cjs +82 -1
- package/scripts/timeout-evidence-from-ci.cjs +964 -0
- package/skills/gsd-autonomous/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// ci-fix-next-rerun.cjs — #6076: re-run ONLY the `Base branch health` job of a
|
|
5
|
+
// PR's latest Tests run after the `fix-next` label was added or removed.
|
|
6
|
+
//
|
|
7
|
+
// Runs from .github/workflows/fix-next-rerun.yml (pull_request_target) against
|
|
8
|
+
// the BASE sha's copy of this file, never the PR's. The core is dependency
|
|
9
|
+
// injected (`gh`, `now`, `sleep`) so every branch is tested without a network.
|
|
10
|
+
|
|
11
|
+
const { execFile } = require('node:child_process');
|
|
12
|
+
const { runMain } = require('./lib/cli-exit.cjs');
|
|
13
|
+
|
|
14
|
+
const BASE_HEALTH_JOB_NAME = 'Base branch health';
|
|
15
|
+
const TESTS_WORKFLOW_FILE = 'test.yml';
|
|
16
|
+
const GH_TIMEOUT_MS = 30000;
|
|
17
|
+
const DEFAULT_DEADLINE_MS = 85 * 60 * 1000;
|
|
18
|
+
const DEFAULT_POLL_MS = 30 * 1000;
|
|
19
|
+
const PR_NUMBER_PATTERN = /^[1-9]\d*$/;
|
|
20
|
+
const HEAD_SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {(args:string[]) => Promise<string>} opts.gh stdout of `gh <args>`; throws on failure
|
|
25
|
+
* @param {string} opts.repo owner/repo
|
|
26
|
+
* @param {string} opts.headSha head sha from the label event payload
|
|
27
|
+
* @param {string} opts.prNumber PR number
|
|
28
|
+
* @param {() => number} opts.now ms clock (read once at start, once per incomplete poll)
|
|
29
|
+
* @param {(ms:number) => Promise<void>} opts.sleep
|
|
30
|
+
* @param {number} opts.deadlineMs how long to wait for the run to complete
|
|
31
|
+
* @param {number} opts.pollMs
|
|
32
|
+
* @returns {Promise<{action:'rerun'|'skip'|'error', reason:string, runId?:string, jobId?:string}>}
|
|
33
|
+
*/
|
|
34
|
+
async function rerunBaseHealth({
|
|
35
|
+
gh, repo, headSha, prNumber, now, sleep, deadlineMs = DEFAULT_DEADLINE_MS, pollMs = DEFAULT_POLL_MS,
|
|
36
|
+
}) {
|
|
37
|
+
try {
|
|
38
|
+
const deadline = now() + deadlineMs;
|
|
39
|
+
let run;
|
|
40
|
+
for (;;) {
|
|
41
|
+
const listed = JSON.parse(await gh([
|
|
42
|
+
'run', 'list', '--workflow', TESTS_WORKFLOW_FILE, '--commit', headSha,
|
|
43
|
+
'--event', 'pull_request', '--limit', '1', '--json', 'databaseId,status,conclusion',
|
|
44
|
+
]));
|
|
45
|
+
if (!Array.isArray(listed) || listed.length === 0) return { action: 'skip', reason: 'no-run' };
|
|
46
|
+
[run] = listed;
|
|
47
|
+
if (run.status === 'completed') break;
|
|
48
|
+
if (now() >= deadline) return { action: 'error', reason: 'timeout', runId: String(run.databaseId) };
|
|
49
|
+
await sleep(pollMs);
|
|
50
|
+
}
|
|
51
|
+
const runId = String(run.databaseId);
|
|
52
|
+
|
|
53
|
+
// A cancelled run was superseded by a newer one, which reads the label from
|
|
54
|
+
// the API; re-running a job here would rejoin the Tests concurrency group
|
|
55
|
+
// and could cancel that newer run.
|
|
56
|
+
if (run.conclusion === 'cancelled') return { action: 'skip', reason: 'run-cancelled', runId };
|
|
57
|
+
|
|
58
|
+
// A run waiting for approval has not executed; there is nothing to re-run.
|
|
59
|
+
if (run.conclusion === 'action_required') return { action: 'skip', reason: 'run-awaiting-approval', runId };
|
|
60
|
+
|
|
61
|
+
const readHead = async () => (await gh(['api', `repos/${repo}/pulls/${prNumber}`, '--jq', '.head.sha'])).trim();
|
|
62
|
+
if ((await readHead()) !== headSha) return { action: 'skip', reason: 'head-moved', runId };
|
|
63
|
+
|
|
64
|
+
const view = JSON.parse(await gh(['run', 'view', runId, '--json', 'jobs']));
|
|
65
|
+
const jobs = view && Array.isArray(view.jobs) ? view.jobs : [];
|
|
66
|
+
const job = jobs.find((j) => j && j.name === BASE_HEALTH_JOB_NAME);
|
|
67
|
+
if (!job) return { action: 'error', reason: 'no-job', runId };
|
|
68
|
+
const jobId = String(job.databaseId);
|
|
69
|
+
|
|
70
|
+
// Second read immediately before the re-run, to shrink the stale-run window.
|
|
71
|
+
if ((await readHead()) !== headSha) return { action: 'skip', reason: 'head-moved', runId };
|
|
72
|
+
|
|
73
|
+
await gh(['run', 'rerun', runId, '--job', jobId]);
|
|
74
|
+
return { action: 'rerun', reason: 'rerun', runId, jobId };
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return { action: 'error', reason: `gh-failed: ${err && err.message ? err.message : String(err)}` };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function realGh(args) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
execFile('gh', args, { timeout: GH_TIMEOUT_MS, maxBuffer: 8 * 1024 * 1024, encoding: 'utf8' }, (err, stdout) => {
|
|
83
|
+
if (err) reject(err);
|
|
84
|
+
else resolve(stdout);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {object} [deps] test seams: env, gh, now, sleep, stdout, stderr, deadlineMs, pollMs
|
|
91
|
+
* @returns {Promise<number>} exit code
|
|
92
|
+
*/
|
|
93
|
+
async function main(deps = {}) {
|
|
94
|
+
const env = deps.env || process.env;
|
|
95
|
+
const out = deps.stdout || ((s) => process.stdout.write(s));
|
|
96
|
+
const errOut = deps.stderr || ((s) => process.stderr.write(s));
|
|
97
|
+
|
|
98
|
+
const prNumber = (env.PR_NUMBER || '').trim();
|
|
99
|
+
if (!PR_NUMBER_PATTERN.test(prNumber)) {
|
|
100
|
+
errOut(`::error::PR_NUMBER must be a positive integer, got ${JSON.stringify(prNumber)}\n`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const headSha = (env.HEAD_SHA || '').trim();
|
|
104
|
+
if (!HEAD_SHA_PATTERN.test(headSha)) {
|
|
105
|
+
errOut('::error::HEAD_SHA must be a 40-character hex sha\n');
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
const repo = (env.GH_REPO || '').trim();
|
|
109
|
+
if (!/^[\w.-]+\/[\w.-]+$/.test(repo)) {
|
|
110
|
+
errOut('::error::GH_REPO must be owner/repo\n');
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await rerunBaseHealth({
|
|
115
|
+
gh: deps.gh || realGh,
|
|
116
|
+
repo,
|
|
117
|
+
headSha,
|
|
118
|
+
prNumber,
|
|
119
|
+
now: deps.now || Date.now,
|
|
120
|
+
sleep: deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
|
|
121
|
+
deadlineMs: deps.deadlineMs,
|
|
122
|
+
pollMs: deps.pollMs,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (result.action === 'error') {
|
|
126
|
+
errOut(`::error::fix-next re-run failed (${result.reason})${result.runId ? ` on Tests run ${result.runId}` : ''}; re-run "${BASE_HEALTH_JOB_NAME}" by hand.\n`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
if (result.action === 'skip') {
|
|
130
|
+
out(`::notice::fix-next re-run skipped (${result.reason}); the newer or upcoming run reads the label from the API.\n`);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
out(`Re-ran "${BASE_HEALTH_JOB_NAME}" (job ${result.jobId}) on Tests run ${result.runId}.\n`);
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (require.main === module) {
|
|
138
|
+
runMain(main);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
BASE_HEALTH_JOB_NAME,
|
|
143
|
+
TESTS_WORKFLOW_FILE,
|
|
144
|
+
DEFAULT_DEADLINE_MS,
|
|
145
|
+
DEFAULT_POLL_MS,
|
|
146
|
+
GH_TIMEOUT_MS,
|
|
147
|
+
rerunBaseHealth,
|
|
148
|
+
main,
|
|
149
|
+
};
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// with a non-success conclusion for the base branch's last push-triggered Tests
|
|
18
18
|
// run, that is treated as authoritative and blocks — with one explicit, visible,
|
|
19
19
|
// human-operated escape hatch (the `fix-next` label) for the PR that is itself
|
|
20
|
-
// the fix-forward.
|
|
20
|
+
// the fix-forward. On a pull request the label is read from the API (#6076), so
|
|
21
|
+
// after it is added the fix-next-rerun workflow re-runs this check and sees it.
|
|
21
22
|
//
|
|
22
23
|
// Every non-success conclusion (`failure`, `cancelled`, `timed_out`,
|
|
23
24
|
// `action_required`, ...) is treated as RED, not just `failure` — a cancelled or
|
|
@@ -205,11 +206,14 @@ async function evaluateQueueBypass({ headRef, baseRef, baseSha, headSha, repo, g
|
|
|
205
206
|
* (or throwing) — injected so tests never touch the network.
|
|
206
207
|
* `evaluateQueueBypass` (merge_group only) is an async function returning
|
|
207
208
|
* `{authorized, reason, actor}`; it is how a queued fix-forward is recognised.
|
|
209
|
+
* `fetchPrLabels` (pull_request only, RED base only) is an async function
|
|
210
|
+
* returning the PR's current label names (or throwing, which keeps the gate
|
|
211
|
+
* RED with reason `label-read-failed`); when absent, `labels` decides (#6076).
|
|
208
212
|
*
|
|
209
213
|
* @returns {Promise<{verdict:string, payload:*, reason:string}>}
|
|
210
214
|
*/
|
|
211
215
|
async function resolveNextHealth({
|
|
212
|
-
fetchLatestRun, eventName, baseRef, labels, evaluateQueueBypass: queueBypass,
|
|
216
|
+
fetchLatestRun, eventName, baseRef, labels, fetchPrLabels, evaluateQueueBypass: queueBypass,
|
|
213
217
|
} = {}) {
|
|
214
218
|
if (!APPLICABLE_EVENTS.has(eventName)) {
|
|
215
219
|
return { verdict: VERDICT.SKIPPED_NOT_APPLICABLE, payload: null, reason: 'not-applicable-event' };
|
|
@@ -235,10 +239,25 @@ async function resolveNextHealth({
|
|
|
235
239
|
return { verdict: assessed.verdict, payload: evidence, reason };
|
|
236
240
|
}
|
|
237
241
|
|
|
238
|
-
// Escape hatch for the PR that is itself the fix-forward. pull_request
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
+
// Escape hatch for the PR that is itself the fix-forward. A pull_request run
|
|
243
|
+
// with `fetchPrLabels` reads the PR's CURRENT labels from the API (#6076), so
|
|
244
|
+
// a re-run sees a label added or removed after the run started; a failed read
|
|
245
|
+
// keeps the gate RED. Without it, the event's `labels` decide (bootstrap).
|
|
246
|
+
// merge_group events carry no labels, so the queue run asks the API instead
|
|
247
|
+
// (evaluateQueueBypass).
|
|
248
|
+
let labelList = Array.isArray(labels) ? labels : [];
|
|
249
|
+
if (eventName === 'pull_request' && typeof fetchPrLabels === 'function') {
|
|
250
|
+
try {
|
|
251
|
+
const current = await fetchPrLabels();
|
|
252
|
+
labelList = Array.isArray(current) ? current : [];
|
|
253
|
+
} catch (err) {
|
|
254
|
+
return {
|
|
255
|
+
verdict: VERDICT.RED,
|
|
256
|
+
payload: evidence,
|
|
257
|
+
reason: `${assessed.reason === 'resolved' ? 'red' : assessed.reason}; label-read-failed: ${err.message}`,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
242
261
|
if (labelList.includes(BYPASS_LABEL)) {
|
|
243
262
|
return { verdict: VERDICT.BYPASSED, payload: evidence, reason: 'bypass-label' };
|
|
244
263
|
}
|
|
@@ -281,6 +300,9 @@ function usage() {
|
|
|
281
300
|
'newest settled run decides. Every other case (wrong event, no',
|
|
282
301
|
'resolvable base ref, an unreadable API read) fails open (exit 0).',
|
|
283
302
|
'',
|
|
303
|
+
'On pull_request with PR_NUMBER set, the label is read from the API, so a',
|
|
304
|
+
're-run sees the PR\'s current labels (#6076); a failed read keeps the gate RED.',
|
|
305
|
+
'',
|
|
284
306
|
'On merge_group the label is read back from the API: the queue ref must',
|
|
285
307
|
'name a PR, the group must hold exactly one commit, the PR must carry the',
|
|
286
308
|
'label, and whoever applied it must have write or admin permission.',
|
|
@@ -300,7 +322,15 @@ function usage() {
|
|
|
300
322
|
' PR_LABELS comma-separated PR label names (pull_request events;',
|
|
301
323
|
' the caller workflow must populate this from',
|
|
302
324
|
' github.event.pull_request.labels.*.name); checked',
|
|
303
|
-
' for the literal "fix-next" bypass label',
|
|
325
|
+
' for the literal "fix-next" bypass label; the fallback',
|
|
326
|
+
' when PR_NUMBER is unset or malformed',
|
|
327
|
+
' PR_NUMBER PR number (pull_request events; from',
|
|
328
|
+
' github.event.pull_request.number); trimmed, then it',
|
|
329
|
+
' must match /^[1-9]\\d*$/. When valid, the label is',
|
|
330
|
+
' read from the API so a re-run sees the current',
|
|
331
|
+
' label; a failed read keeps the gate RED. A',
|
|
332
|
+
' malformed value (0, 07, -1, abc, empty) falls back',
|
|
333
|
+
' to PR_LABELS',
|
|
304
334
|
' MERGE_GROUP_HEAD_REF merge-group queue branch',
|
|
305
335
|
' (github.event.merge_group.head_ref; falls back to',
|
|
306
336
|
' GITHUB_REF)',
|
|
@@ -437,8 +467,19 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
437
467
|
ghGet,
|
|
438
468
|
});
|
|
439
469
|
|
|
470
|
+
// #6076: with a valid PR number, read the PR's current labels from the API.
|
|
471
|
+
const prNumber = (process.env.PR_NUMBER || '').trim();
|
|
472
|
+
const fetchPrLabels = /^[1-9]\d*$/.test(prNumber)
|
|
473
|
+
? async () => {
|
|
474
|
+
const body = await ghGet(`/repos/${repo}/issues/${prNumber}/labels?per_page=100`);
|
|
475
|
+
return Array.isArray(body)
|
|
476
|
+
? body.map((l) => l && l.name).filter((name) => typeof name === 'string')
|
|
477
|
+
: [];
|
|
478
|
+
}
|
|
479
|
+
: undefined;
|
|
480
|
+
|
|
440
481
|
const result = await resolveNextHealth({
|
|
441
|
-
fetchLatestRun, eventName, baseRef, labels, evaluateQueueBypass: evaluateQueue,
|
|
482
|
+
fetchLatestRun, eventName, baseRef, labels, fetchPrLabels, evaluateQueueBypass: evaluateQueue,
|
|
442
483
|
});
|
|
443
484
|
|
|
444
485
|
writeOutput([`verdict=${result.verdict}`]);
|
|
@@ -450,7 +491,9 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
450
491
|
`::error::the base branch "${baseRef}"'s own last Tests run is red or still re-running after a failure: ${runUrl} (${result.reason}) — `
|
|
451
492
|
+ 'wait for a fix-forward merge, or if THIS pull request is the fix, ask a '
|
|
452
493
|
+ `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)
|
|
494
|
+
+ '(the queue run reads the label from the API; it must be the only PR in its merge group). '
|
|
495
|
+
+ 'On a pull request the label is read from the API too, so after it is added the '
|
|
496
|
+
+ 'fix-next-rerun workflow re-runs this check.\n',
|
|
454
497
|
);
|
|
455
498
|
return 1;
|
|
456
499
|
}
|
|
@@ -6,34 +6,68 @@ const { execFileSync } = require('child_process');
|
|
|
6
6
|
const { existsSync, readdirSync, appendFileSync, readFileSync } = require('fs');
|
|
7
7
|
|
|
8
8
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
'
|
|
25
|
-
'pr-
|
|
26
|
-
'pr-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
9
|
+
const { classifyUnstrippedContent, NOISY_FOR_SOURCE_REACHABILITY } = require('./gen-platform-conformance-tier.cjs');
|
|
10
|
+
|
|
11
|
+
// #6075: CI YAML (any `.github/**/*.yml|*.yaml`: workflows, composite actions,
|
|
12
|
+
// dependabot/labeler/issue-form config) is "inert" CI. A PR that changes only CI
|
|
13
|
+
// YAML never runs the matrix: it reports code_changed=true, product_changed=false,
|
|
14
|
+
// full_matrix=false and targets only the workflow-lint tests (the `test-inert`
|
|
15
|
+
// lane). Mixed with product code, the product files decide the scope.
|
|
16
|
+
// Anchored at the repo root: a nested `.github/` under a product prefix is not CI YAML.
|
|
17
|
+
const CI_YAML_RE = /^\.github\/.+\.ya?ml$/;
|
|
18
|
+
function isCiYaml(filePath) {
|
|
19
|
+
return CI_YAML_RE.test(filePath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Tests shared by both the 'workflow automation' and 'inert CI' rules.
|
|
23
|
+
const WORKFLOW_LINT_TESTS = [
|
|
24
|
+
'tests/workflow-shell-pinning.test.cjs',
|
|
25
|
+
'tests/pr-template-policy.test.cjs',
|
|
26
|
+
'tests/lint-pr-check-project-dir.test.cjs',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// #6075: tests that read `.github/**` YAML content (workflow contracts). A CI
|
|
30
|
+
// YAML edit re-runs them in the targeted lane (never full_matrix). Several are
|
|
31
|
+
// conformance-tier members; that does not flip full_matrix for a CI-YAML-only
|
|
32
|
+
// change because classify() exempts isCiYaml(file) from the windows-hint
|
|
33
|
+
// escalation and forces full_matrix=false when only CI YAML changed.
|
|
34
|
+
const WORKFLOW_CONTRACT_TESTS = [
|
|
35
|
+
'tests/adr-218-release-version-validation.test.cjs',
|
|
36
|
+
'tests/adr-230-pr-target-policy.test.cjs',
|
|
37
|
+
'tests/ci-conformance-pr-gating.test.cjs',
|
|
38
|
+
'tests/ci-docs-guard-registry.test.cjs',
|
|
39
|
+
'tests/ci-durations-upload-nonfatal.test.cjs',
|
|
40
|
+
'tests/ci-full-lane-sharding.test.cjs',
|
|
41
|
+
'tests/ci-merge-group-wiring.test.cjs',
|
|
42
|
+
'tests/ci-pr-mergeability.test.cjs',
|
|
43
|
+
'tests/ci-test-job-timeout-budget.test.cjs',
|
|
44
|
+
'tests/ci-test-scope.test.cjs',
|
|
45
|
+
'tests/merge-commit-pr.test.cjs',
|
|
46
|
+
'tests/mutation-tap-runner-wiring.test.cjs',
|
|
47
|
+
'tests/mutation-workflow-base-ref.test.cjs',
|
|
48
|
+
'tests/policy-release-no-npm-self-upgrade.test.cjs',
|
|
49
|
+
'tests/policy-shell-pinning.test.cjs',
|
|
50
|
+
'tests/release-backmerge-invariants.test.cjs',
|
|
51
|
+
'tests/release-coverage-scope.test.cjs',
|
|
52
|
+
'tests/release-finalize-notes-5922.test.cjs',
|
|
53
|
+
'tests/release-finalize-syncs-next-version.test.cjs',
|
|
54
|
+
'tests/release-hotfix-empty-cherry-pick.test.cjs',
|
|
55
|
+
'tests/release-notes-real-os-5922.test.cjs',
|
|
56
|
+
'tests/release-shard-lane-sharding.test.cjs',
|
|
57
|
+
'tests/release-tarball-smoke-workflow.test.cjs',
|
|
58
|
+
'tests/require-issue-link-policy.test.cjs',
|
|
59
|
+
'tests/reviewed-or-maintainer-workflow.test.cjs',
|
|
60
|
+
'tests/security-scan.security.test.cjs',
|
|
61
|
+
'tests/self-test-report-workflow.test.cjs',
|
|
62
|
+
'tests/workflow-token-permissions.test.cjs',
|
|
63
|
+
];
|
|
32
64
|
|
|
33
65
|
// Workflows that gate merges, ship the product, or run security/cross-platform
|
|
34
|
-
// suites
|
|
35
|
-
//
|
|
36
|
-
//
|
|
66
|
+
// suites. Kept as a registry of gating workflows. Enforced by the test
|
|
67
|
+
// 'every protected workflow name refers to a real file' in
|
|
68
|
+
// tests/ci-pr-mergeability.test.cjs (existence parity) and, at module load, by
|
|
69
|
+
// missingProtectedWorkflows() below. Since #6075 a change to any of them is CI
|
|
70
|
+
// YAML and, alone, runs only the workflow-lint and workflow-contract tests.
|
|
37
71
|
const PROTECTED_WORKFLOWS = new Set([
|
|
38
72
|
'test.yml',
|
|
39
73
|
'install-smoke.yml',
|
|
@@ -41,41 +75,16 @@ const PROTECTED_WORKFLOWS = new Set([
|
|
|
41
75
|
'security-scan.yml',
|
|
42
76
|
'release.yml',
|
|
43
77
|
// #3833: the reusable gate every `pull_request` compute lane depends on —
|
|
44
|
-
//
|
|
45
|
-
// running the full matrix it is responsible for enforcing.
|
|
78
|
+
// the gate it defines is exercised by tests/ci-pr-mergeability.test.cjs.
|
|
46
79
|
'pr-mergeable-preflight.yml',
|
|
47
80
|
]);
|
|
48
|
-
for (const wf of PROTECTED_WORKFLOWS) {
|
|
49
|
-
if (INERT_WORKFLOWS.has(wf)) {
|
|
50
|
-
throw new Error(`ci-test-scope: protected workflow "${wf}" must not be in INERT_WORKFLOWS (it requires the full test matrix).`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Returns true if the path is an inert (non-pipeline) workflow file.
|
|
56
|
-
* Only `.github/workflows/<name>` where <name> is in INERT_WORKFLOWS qualifies.
|
|
57
|
-
*/
|
|
58
|
-
function isInertCi(filePath) {
|
|
59
|
-
if (!filePath.startsWith('.github/workflows/')) return false;
|
|
60
|
-
const name = filePath.slice('.github/workflows/'.length);
|
|
61
|
-
// Must be a direct child (no further slashes) and in the allowlist.
|
|
62
|
-
return !name.includes('/') && INERT_WORKFLOWS.has(name);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Tests shared by both the 'workflow automation' and 'inert CI' rules.
|
|
66
|
-
const WORKFLOW_LINT_TESTS = [
|
|
67
|
-
'tests/workflow-shell-pinning.test.cjs',
|
|
68
|
-
'tests/pr-template-policy.test.cjs',
|
|
69
|
-
'tests/lint-pr-check-project-dir.test.cjs',
|
|
70
|
-
];
|
|
71
81
|
|
|
72
82
|
const RULES = [
|
|
73
83
|
{
|
|
74
84
|
name: 'workflow automation',
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
match: filePath =>
|
|
78
|
-
filePath.startsWith('.github/rulesets/'),
|
|
85
|
+
// .github/rulesets/* (JSON branch protection) trigger the full matrix.
|
|
86
|
+
// CI YAML (.github/**/*.y{a,}ml) is the 'inert CI' rule below (#6075).
|
|
87
|
+
match: filePath => filePath.startsWith('.github/rulesets/'),
|
|
79
88
|
fullMatrix: true,
|
|
80
89
|
tests: [
|
|
81
90
|
...WORKFLOW_LINT_TESTS,
|
|
@@ -84,11 +93,12 @@ const RULES = [
|
|
|
84
93
|
},
|
|
85
94
|
{
|
|
86
95
|
name: 'inert CI',
|
|
87
|
-
match: filePath =>
|
|
96
|
+
match: filePath => isCiYaml(filePath),
|
|
88
97
|
fullMatrix: false,
|
|
89
98
|
tests: [
|
|
90
99
|
...WORKFLOW_LINT_TESTS,
|
|
91
100
|
'tests/policy-lint-shallow-checkout.test.cjs',
|
|
101
|
+
...WORKFLOW_CONTRACT_TESTS,
|
|
92
102
|
],
|
|
93
103
|
},
|
|
94
104
|
{
|
|
@@ -530,7 +540,7 @@ function reachesConformanceTierOrSeam(file, deps = {}) {
|
|
|
530
540
|
|
|
531
541
|
if (file.startsWith('src/')) {
|
|
532
542
|
const content = readFileSync(file, 'utf8');
|
|
533
|
-
const { signals } =
|
|
543
|
+
const { signals } = classifyUnstrippedContent(content);
|
|
534
544
|
const narrowSignals = signals.filter(signal => !NOISY_FOR_SOURCE_REACHABILITY.has(signal));
|
|
535
545
|
return narrowSignals.length > 0;
|
|
536
546
|
}
|
|
@@ -549,7 +559,7 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
549
559
|
const targeted = new Set();
|
|
550
560
|
const reasons = [];
|
|
551
561
|
let productOrPipelineChanged = false; // product/pipeline code (excludes docs)
|
|
552
|
-
let inertCiChanged = false; //
|
|
562
|
+
let inertCiChanged = false; // CI YAML files (#6075)
|
|
553
563
|
let fullMatrix = false;
|
|
554
564
|
|
|
555
565
|
for (const file of files) {
|
|
@@ -571,13 +581,8 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
571
581
|
productOrPipelineChanged = true;
|
|
572
582
|
}
|
|
573
583
|
|
|
574
|
-
//
|
|
575
|
-
if (
|
|
576
|
-
productOrPipelineChanged = true;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
// Inert workflow files set a lightweight signal.
|
|
580
|
-
if (isInertCi(file)) {
|
|
584
|
+
// CI YAML sets a lightweight signal; it is never product/pipeline code.
|
|
585
|
+
if (isCiYaml(file)) {
|
|
581
586
|
inertCiChanged = true;
|
|
582
587
|
}
|
|
583
588
|
|
|
@@ -637,11 +642,13 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
637
642
|
// 14 of 16 rules, newly escalating most ordinary product-code PRs
|
|
638
643
|
// (src/, agents/, commands/, hooks/, skills/, config paths). Only one
|
|
639
644
|
// rule's escalation is live today: 'portability lint rules (ADR-1703)'.
|
|
640
|
-
// Four others already had fullMatrix: true (no-op here), and
|
|
641
|
-
// CI'
|
|
645
|
+
// Four others already had fullMatrix: true (no-op here), and CI YAML
|
|
646
|
+
// (the 'inert CI' rule) is excluded from this escalation entirely.
|
|
642
647
|
// Distinct, greppable reason so the conformance-lane coverage for it
|
|
643
648
|
// is traceable per rule.
|
|
644
|
-
|
|
649
|
+
// #6075: CI YAML never escalates (it is lint-only scope, also when mixed
|
|
650
|
+
// with product code, so the product files alone decide full_matrix).
|
|
651
|
+
if (!isCiYaml(file) && rule.tests.some(t => isWindowsHint(t) && reachesConformanceTierOrSeam(t, reachabilityDeps))) {
|
|
645
652
|
fullMatrix = true;
|
|
646
653
|
reasons.push(`${file}: ${rule.name} (windows-hint rule test, #4641)`);
|
|
647
654
|
}
|
|
@@ -660,7 +667,7 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
660
667
|
]);
|
|
661
668
|
for (const f of SCOPED_LANE_EXCLUDE) { targeted.delete(f); }
|
|
662
669
|
|
|
663
|
-
// code_changed: true when product/pipeline OR
|
|
670
|
+
// code_changed: true when product/pipeline code OR CI YAML changed.
|
|
664
671
|
// Docs-only PRs (neither flag set) get code_changed=false → full matrix skip.
|
|
665
672
|
const codeChanged = productOrPipelineChanged || inertCiChanged;
|
|
666
673
|
|
|
@@ -672,7 +679,7 @@ function classify(files, reachabilityDeps = {}) {
|
|
|
672
679
|
targetedTests.push('unit');
|
|
673
680
|
}
|
|
674
681
|
|
|
675
|
-
//
|
|
682
|
+
// CI-YAML-only: full_matrix must be false (override any RULES that fired).
|
|
676
683
|
if (inertCiChanged && !productOrPipelineChanged) {
|
|
677
684
|
fullMatrix = false;
|
|
678
685
|
}
|
|
@@ -732,7 +739,6 @@ module.exports = {
|
|
|
732
739
|
RULES,
|
|
733
740
|
missingRuleTestFiles,
|
|
734
741
|
PROTECTED_WORKFLOWS,
|
|
735
|
-
INERT_WORKFLOWS,
|
|
736
742
|
missingProtectedWorkflows,
|
|
737
743
|
classify,
|
|
738
744
|
reachesConformanceTierOrSeam,
|
|
@@ -939,21 +939,70 @@ const REMEDIES = Object.freeze({
|
|
|
939
939
|
*
|
|
940
940
|
* File-derived strings (registry cells, marker tokens, workflow text) are
|
|
941
941
|
* echoed into lint output that AI agents read as trusted instructions —
|
|
942
|
-
* every interpolated field
|
|
943
|
-
* before it is written, so a malicious repo file cannot make
|
|
944
|
-
* instruction-shaped text
|
|
945
|
-
*
|
|
942
|
+
* every interpolated field has its hostile code points ESCAPED and is
|
|
943
|
+
* length-capped before it is written, so a malicious repo file cannot make
|
|
944
|
+
* the lint emit instruction-shaped text, and the reader can still trace the
|
|
945
|
+
* spelling as written (a dropped character would show text that does not
|
|
946
|
+
* occur in the file).
|
|
947
|
+
*
|
|
948
|
+
* Escape format: each hostile code point (see isEchoHostile) is rendered as
|
|
949
|
+
* `\u{X}` with UPPERCASE hex and no padding, e.g. U+0085 -> `\u{85}`,
|
|
950
|
+
* U+200B -> `\u{200B}`, tag characters (U+E0000-U+E007F) -> `\u{E0041}`. The
|
|
951
|
+
* escape introducer itself, a literal backslash U+005C, is escaped as `\u{5C}`
|
|
952
|
+
* so a spelled-out `\u{202E}` in the input can never print identically to a
|
|
953
|
+
* real U+202E: whenever nothing was dropped (guaranteed when the output is at
|
|
954
|
+
* most 190 code points, since the longest escape `\u{10FFFF}` is 10), the
|
|
955
|
+
* output decodes back to exactly the input, so distinct inputs have distinct
|
|
956
|
+
* output. Once the cap truncates, the tail is dropped and inputs differing
|
|
957
|
+
* only beyond it can share one output (e.g. 195 `a` with and without a
|
|
958
|
+
* trailing U+0085). The output is free of every hostile character.
|
|
959
|
+
*
|
|
960
|
+
* Cap: 200 code points of OUTPUT (an escape counts as its own length, never
|
|
961
|
+
* split), so the result is bounded however hostile the input; scanning stops
|
|
962
|
+
* as soon as the next unit would exceed the cap, costing O(200) matches on a
|
|
963
|
+
* huge input. A surrogate pair is never split.
|
|
964
|
+
*
|
|
965
|
+
* Implemented as a code-point filter rather than a regex so no
|
|
966
|
+
* control-character regex literal exists to lint.
|
|
946
967
|
*/
|
|
947
968
|
function sanitizeEcho(text) {
|
|
969
|
+
const CAP = 200;
|
|
948
970
|
let out = '';
|
|
971
|
+
let used = 0;
|
|
949
972
|
for (const ch of String(text)) {
|
|
950
973
|
const code = ch.codePointAt(0);
|
|
951
|
-
|
|
952
|
-
|
|
974
|
+
const hostile = code === 0x5c || isEchoHostile(code);
|
|
975
|
+
const unit = hostile ? `\\u{${code.toString(16).toUpperCase()}}` : ch;
|
|
976
|
+
const len = hostile ? unit.length : 1; // escapes are ASCII: one code point per char
|
|
977
|
+
if (used + len > CAP) break;
|
|
978
|
+
out += unit;
|
|
979
|
+
used += len;
|
|
953
980
|
}
|
|
954
|
-
return out
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Code points sanitizeEcho escapes: C0 + DEL, C1 (incl. 8-bit CSI U+009B),
|
|
986
|
+
* line/paragraph separators, zero-width + LRM/RLM, bidi embeddings/overrides
|
|
987
|
+
* and isolates, the Arabic letter mark, word joiner/invisible operators, the
|
|
988
|
+
* Mongolian vowel separator, the soft hyphen and the BOM; all able to forge
|
|
989
|
+
* or hide output a reader (human or agent) trusts. Escaping U+200D makes
|
|
990
|
+
* joined emoji sequences visible as `\u{200D}`, which is acceptable for echoed paths.
|
|
991
|
+
* @param {number} code
|
|
992
|
+
* @returns {boolean}
|
|
993
|
+
*/
|
|
994
|
+
function isEchoHostile(code) {
|
|
995
|
+
return ECHO_HOSTILE_RE.test(String.fromCodePoint(code));
|
|
955
996
|
}
|
|
956
997
|
|
|
998
|
+
// Category-based: Cc controls (C0, DEL, C1), Cf format chars (zero-width, bidi,
|
|
999
|
+
// soft hyphen, BOM, tags U+E0000-E007F, interlinear annotation, musical
|
|
1000
|
+
// format), Zl/Zp separators, Cs surrogates, Co private use, Cn unassigned;
|
|
1001
|
+
// plus variation selectors and the invisible filler characters that are not Cf.
|
|
1002
|
+
// Each combining/variation/filler member is its own alternative so no
|
|
1003
|
+
// character class holds a combining sequence (no-misleading-character-class).
|
|
1004
|
+
const ECHO_HOSTILE_RE = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}\p{Co}\p{Cn}]|[\u{FE00}-\u{FE0F}]|[\u{E0100}-\u{E01EF}]|\u{180B}|\u{180C}|\u{180D}|\u{180F}|\u{34F}|\u{115F}|\u{1160}|\u{3164}|\u{FFA0}|\u{17B4}|\u{17B5}|\u{2800}/u;
|
|
1005
|
+
|
|
957
1006
|
module.exports = {
|
|
958
1007
|
CANONICAL_TOOLS,
|
|
959
1008
|
parseFrontmatter,
|
|
@@ -180,6 +180,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
180
180
|
// against the milestone-release-map module's exported enums.
|
|
181
181
|
'tests/milestone-release-map.test.cjs': ['docs/CONFIGURATION.md'],
|
|
182
182
|
'tests/analyze-dependencies.test.cjs': ['docs/COMMANDS.md'],
|
|
183
|
+
'tests/smart-discuss-classification.test.cjs': ['docs/COMMANDS.md'],
|
|
183
184
|
'tests/auto-select-attribute.test.cjs': ['docs/reference/plan-md.md'],
|
|
184
185
|
'tests/autonomous-converge.test.cjs': [
|
|
185
186
|
'docs/COMMANDS.md',
|
|
@@ -192,6 +193,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
192
193
|
'docs/reference/capability-manifest.md',
|
|
193
194
|
],
|
|
194
195
|
'tests/claude-md.test.cjs': ['docs/COMMANDS.md'],
|
|
196
|
+
'tests/claude-orchestration-4747.test.cjs': ['docs/how-to/enable-claude-orchestration-workflow-backend.md'],
|
|
195
197
|
'tests/claude-orchestration.test.cjs': ['docs/explanation/claude-orchestration-capability.md'],
|
|
196
198
|
'tests/command-contract.test.cjs': [
|
|
197
199
|
'docs/INVENTORY.md',
|
|
@@ -309,6 +311,7 @@ const DOCS_GUARD_TESTS = {
|
|
|
309
311
|
'docs/pt-BR/context-monitor.md',
|
|
310
312
|
'docs/zh-CN/context-monitor.md',
|
|
311
313
|
],
|
|
314
|
+
'tests/gen-cli-surface-manifest.test.cjs': ['docs/CLI-SURFACE-MANIFEST.json'],
|
|
312
315
|
'tests/gen-state-md-docs.test.cjs': [
|
|
313
316
|
'docs/reference/state-md.md',
|
|
314
317
|
'docs/ja-JP/reference/state-md.md',
|