@event4u/agent-config 13.0.0 → 14.1.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/CHANGELOG.md +195 -315
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +336 -112
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +2128 -1326
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1764 -899
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +5 -5
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gate_ledger.ts +5 -1
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +80 -143
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/build_archive_index.ts +62 -3
- package/src/scripts/capture_skill_catalogue.ts +264 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_gate_coverage.ts +93 -0
- package/src/scripts/check_pr_ci_current.ts +492 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/git_authorization_hook.ts +186 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/block_unauthorized_git.ts +62 -6
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence artifacts declare what kind of evidence they are
|
|
3
|
+
* (road-to-release-review-p0 Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* The contract is `docs/contracts/evidence-artifact-types.md`; this is its
|
|
6
|
+
* check. Read that document first — in particular its scope section, because
|
|
7
|
+
* the single most important property of this linter is what it deliberately
|
|
8
|
+
* does NOT do.
|
|
9
|
+
*
|
|
10
|
+
* **It is forward-looking, and there is no baseline file.** Measured by
|
|
11
|
+
* `--all` at the time of writing: 332 tracked markdown artifacts under
|
|
12
|
+
* `agents/evidence/`, 188 already resolving a type and 144 not.
|
|
13
|
+
* `check_completion_review` already refused to make one marker field required
|
|
14
|
+
* on the stated ground that it "would have been a migration event for the
|
|
15
|
+
* whole evidence corpus", and that reasoning is adopted rather than
|
|
16
|
+
* overturned. So the gate fires only on files ADDED relative to a base ref. A
|
|
17
|
+
* ratchet baseline was the obvious alternative and was rejected: it would mean
|
|
18
|
+
* committing a 144-entry suppression list, and a new baseline file in this
|
|
19
|
+
* repo trips two suppression gates of its own — paying a governance cost to
|
|
20
|
+
* enforce a rule the scope decision says should not be enforced retroactively.
|
|
21
|
+
*
|
|
22
|
+
* **Not registered in `gate-coverage.yml`, and the reason is the manifest's
|
|
23
|
+
* own.** A diff-scoped gate legitimately scans 0 on most runs, so it can carry
|
|
24
|
+
* no honest `min_scanned` floor, and `min_scanned: 0` is precisely the
|
|
25
|
+
* false-count shape that file rejects. `check_rule_projection_integrity` is
|
|
26
|
+
* the worked precedent: NOT registered, with the reason, ledger half
|
|
27
|
+
* discharged. This gate follows it — it adopts `_lib/gate_ledger.ts` below, so
|
|
28
|
+
* `check_gate_completeness` counts it as an adopter rather than a gap.
|
|
29
|
+
*
|
|
30
|
+
* **A declaration is read wherever the author already wrote one.** Three of
|
|
31
|
+
* the five types are pre-existing line grammars owned by
|
|
32
|
+
* `check_completion_review.ts`, and this linter imports those parsers rather
|
|
33
|
+
* than re-implementing them. Re-declaring an artifact that already carries
|
|
34
|
+
* `<!-- completion-review: v1 … -->` under a second `evidence-type:` marker
|
|
35
|
+
* would be the ambiguity the contract exists to remove.
|
|
36
|
+
*
|
|
37
|
+
* Modes:
|
|
38
|
+
* --new-only <base-ref> gate: fail on an ADDED artifact with no type
|
|
39
|
+
* --all report: corpus census, always exit 0
|
|
40
|
+
* --quiet verdict line only
|
|
41
|
+
*
|
|
42
|
+
* Exit 0 = clean (or report mode). Exit 1 = an added artifact carries no
|
|
43
|
+
* resolvable type. Exit 2 = the linter could not run (bad ref, no git).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { execFileSync } from 'node:child_process';
|
|
47
|
+
import fs from 'node:fs';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
50
|
+
|
|
51
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
52
|
+
import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
|
|
53
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
54
|
+
import { parseHonestNull, parseMarkerLine, parseSkipDeclaration } from './check_completion_review.js';
|
|
55
|
+
|
|
56
|
+
export const EVIDENCE_ROOT = path.join('agents', 'evidence');
|
|
57
|
+
|
|
58
|
+
/** The five types the contract defines. Anything else is an error, not a warning. */
|
|
59
|
+
export const EVIDENCE_TYPES = [
|
|
60
|
+
'original-review',
|
|
61
|
+
'current-binding',
|
|
62
|
+
'declared-skip',
|
|
63
|
+
'honest-null',
|
|
64
|
+
'analysis',
|
|
65
|
+
] as const;
|
|
66
|
+
|
|
67
|
+
export type EvidenceType = (typeof EVIDENCE_TYPES)[number];
|
|
68
|
+
|
|
69
|
+
/** The explicit marker, for the two types with no pre-existing grammar. */
|
|
70
|
+
const TYPE_MARKER_RE = /^<!--\s*evidence-type:\s*([a-z-]+)\s*-->$/;
|
|
71
|
+
|
|
72
|
+
/** How many leading lines are scanned for the marker. A type belongs at the top. */
|
|
73
|
+
const MARKER_SCAN_LINES = 40;
|
|
74
|
+
|
|
75
|
+
export interface TypeResolution {
|
|
76
|
+
readonly type: EvidenceType | null;
|
|
77
|
+
/** Which declaration produced it — named so a reader can check the claim. */
|
|
78
|
+
readonly via: 'marker' | 'completion-review' | 'honest-null' | 'skip' | 'review-input-path' | null;
|
|
79
|
+
/** Set when a marker was present but named something outside the set. */
|
|
80
|
+
readonly invalidMarker: string | null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Resolve one artifact's declared type.
|
|
85
|
+
*
|
|
86
|
+
* Order matters and encodes the contract: the pre-existing grammars are
|
|
87
|
+
* checked FIRST, so an artifact that already declares itself through
|
|
88
|
+
* `check_completion_review`'s vocabulary is never asked for a second marker.
|
|
89
|
+
* The path rule is last and is the contract's one acknowledged exception.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveEvidenceType(relPath: string, contents: string): TypeResolution {
|
|
92
|
+
// BOTH scans share one window. They did not: the grammar scan walked the
|
|
93
|
+
// whole file while the marker scan stopped at 40 lines, so a quoted
|
|
94
|
+
// `completion-review` line deep in a prose artifact silently overrode an
|
|
95
|
+
// explicit `evidence-type:` marker its author had written at the top
|
|
96
|
+
// (R2 finding 8). A declaration belongs near the top of the file either
|
|
97
|
+
// way, and one window makes the precedence readable instead of positional.
|
|
98
|
+
const lines = contents.split('\n').slice(0, MARKER_SCAN_LINES);
|
|
99
|
+
|
|
100
|
+
for (const raw of lines) {
|
|
101
|
+
const line = raw.trim();
|
|
102
|
+
if (line === '') {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (parseMarkerLine(line) !== null) {
|
|
106
|
+
return { type: 'current-binding', via: 'completion-review', invalidMarker: null };
|
|
107
|
+
}
|
|
108
|
+
if (parseHonestNull(line) !== null) {
|
|
109
|
+
return { type: 'honest-null', via: 'honest-null', invalidMarker: null };
|
|
110
|
+
}
|
|
111
|
+
if (parseSkipDeclaration(line) !== null) {
|
|
112
|
+
return { type: 'declared-skip', via: 'skip', invalidMarker: null };
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const raw of lines) {
|
|
117
|
+
const m = TYPE_MARKER_RE.exec(raw.trim());
|
|
118
|
+
if (m === null) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const value = m[1] as string;
|
|
122
|
+
if ((EVIDENCE_TYPES as readonly string[]).includes(value)) {
|
|
123
|
+
return { type: value as EvidenceType, via: 'marker', invalidMarker: null };
|
|
124
|
+
}
|
|
125
|
+
// A misspelled type reads as untyped to every consumer, which is the
|
|
126
|
+
// state the contract exists to end — so it is louder than absence,
|
|
127
|
+
// not quieter.
|
|
128
|
+
return { type: null, via: null, invalidMarker: value };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// The contract's single filename-derived case. Safe because the directory
|
|
132
|
+
// is written by `dispatch_r2_reviewer.ts` and by nothing else, so the path
|
|
133
|
+
// IS the declaration rather than a heuristic about one.
|
|
134
|
+
// Split on `/`, never `path.sep`. Every caller feeds paths from
|
|
135
|
+
// `git diff --name-only` / `git ls-files`, and git emits `/`-separated
|
|
136
|
+
// paths on every platform — so on Windows `path.sep` (`\`) made the whole
|
|
137
|
+
// path one segment and this branch could never match (R2 finding 10).
|
|
138
|
+
// `\` is accepted too so a hand-built Windows path still resolves.
|
|
139
|
+
if (relPath.split(/[/\\]/).some((seg) => seg.endsWith('.review-input'))) {
|
|
140
|
+
return { type: 'original-review', via: 'review-input-path', invalidMarker: null };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { type: null, via: null, invalidMarker: null };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _git(root: string, args: readonly string[]): string {
|
|
147
|
+
return execFileSync('git', [...args], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Markdown files ADDED under `agents/evidence/` relative to `baseRef`. */
|
|
151
|
+
export function addedEvidenceFiles(root: string, baseRef: string): string[] {
|
|
152
|
+
const out = _git(root, [
|
|
153
|
+
'diff',
|
|
154
|
+
'--name-only',
|
|
155
|
+
'--diff-filter=A',
|
|
156
|
+
`${baseRef}...HEAD`,
|
|
157
|
+
'--',
|
|
158
|
+
`${EVIDENCE_ROOT}/`,
|
|
159
|
+
]);
|
|
160
|
+
return out
|
|
161
|
+
.split('\n')
|
|
162
|
+
.map((s) => s.trim())
|
|
163
|
+
.filter((s) => s !== '' && s.endsWith('.md'));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Every markdown artifact in the corpus, for the census mode. */
|
|
167
|
+
export function allEvidenceFiles(root: string): string[] {
|
|
168
|
+
const out = _git(root, ['ls-files', '--', `${EVIDENCE_ROOT}/`]);
|
|
169
|
+
return out
|
|
170
|
+
.split('\n')
|
|
171
|
+
.map((s) => s.trim())
|
|
172
|
+
.filter((s) => s !== '' && s.endsWith('.md'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface Finding {
|
|
176
|
+
readonly file: string;
|
|
177
|
+
readonly reason: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function checkFiles(root: string, files: readonly string[], ledger?: GateLedger): Finding[] {
|
|
181
|
+
const findings: Finding[] = [];
|
|
182
|
+
ledger?.plan([...files]);
|
|
183
|
+
for (const rel of files) {
|
|
184
|
+
let contents: string;
|
|
185
|
+
try {
|
|
186
|
+
contents = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
187
|
+
} catch {
|
|
188
|
+
// A file listed by git but unreadable on disk is not this
|
|
189
|
+
// linter's finding to report — a deleted-then-re-added path in a
|
|
190
|
+
// dirty tree hits this and is somebody else's gate.
|
|
191
|
+
ledger?.outOfScope(rel, 'no_applicable_files');
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const res = resolveEvidenceType(rel, contents);
|
|
195
|
+
if (res.invalidMarker !== null) {
|
|
196
|
+
ledger?.fail(rel, `invalid evidence-type: ${res.invalidMarker}`);
|
|
197
|
+
findings.push({
|
|
198
|
+
file: rel,
|
|
199
|
+
reason:
|
|
200
|
+
`evidence-type: ${res.invalidMarker} is not one of ` +
|
|
201
|
+
`${EVIDENCE_TYPES.join(' | ')}`,
|
|
202
|
+
});
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (res.type === null) {
|
|
206
|
+
ledger?.fail(rel, 'no evidence type declared');
|
|
207
|
+
findings.push({
|
|
208
|
+
file: rel,
|
|
209
|
+
reason:
|
|
210
|
+
'no evidence type declared — add `<!-- evidence-type: analysis -->` ' +
|
|
211
|
+
'(or the type that fits) per docs/contracts/evidence-artifact-types.md',
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
ledger?.complete(rel);
|
|
216
|
+
}
|
|
217
|
+
return findings;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _argValue(flag: string): string | null {
|
|
221
|
+
const i = process.argv.indexOf(flag);
|
|
222
|
+
if (i === -1) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
const v = process.argv[i + 1];
|
|
226
|
+
return v === undefined || v.startsWith('--') ? null : v;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function main(): number {
|
|
230
|
+
const quiet = process.argv.includes('--quiet');
|
|
231
|
+
const root = process.cwd();
|
|
232
|
+
const out = (s: string): void => {
|
|
233
|
+
if (!quiet) {
|
|
234
|
+
process.stdout.write(`${s}\n`);
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
if (process.argv.includes('--all')) {
|
|
239
|
+
let files: string[];
|
|
240
|
+
try {
|
|
241
|
+
files = allEvidenceFiles(root);
|
|
242
|
+
} catch (exc) {
|
|
243
|
+
process.stdout.write(`lint_evidence_artifacts: cannot list ${EVIDENCE_ROOT} — ${String(exc)}\n`);
|
|
244
|
+
return 2;
|
|
245
|
+
}
|
|
246
|
+
const ledger = new GateLedger('lint_evidence_artifacts');
|
|
247
|
+
const findings = checkFiles(root, files, ledger);
|
|
248
|
+
ledger.report();
|
|
249
|
+
// Census mode reads the whole corpus, so a zero here IS blindness —
|
|
250
|
+
// no `allowEmpty`, deliberately. The gate mode below is the opposite
|
|
251
|
+
// case and says so on its own line.
|
|
252
|
+
reportScanned({
|
|
253
|
+
gate: 'lint_evidence_artifacts --all',
|
|
254
|
+
scanned: files.length,
|
|
255
|
+
units: 'evidence artifacts',
|
|
256
|
+
roots: [EVIDENCE_ROOT],
|
|
257
|
+
});
|
|
258
|
+
const typed = files.length - findings.length;
|
|
259
|
+
out(`lint_evidence_artifacts — census over ${String(files.length)} artifact(s)`);
|
|
260
|
+
out(` typed ${String(typed)}`);
|
|
261
|
+
out(` untyped ${String(findings.length)}`);
|
|
262
|
+
// Report mode, never a gate: the pre-existing untyped corpus is a
|
|
263
|
+
// deliberate scope exclusion, so publishing the number is the point
|
|
264
|
+
// and failing on it would contradict the contract.
|
|
265
|
+
process.stdout.write(
|
|
266
|
+
`lint_evidence_artifacts: census — ${String(typed)} typed, ${String(findings.length)} untyped (report only)\n`,
|
|
267
|
+
);
|
|
268
|
+
return 0;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// A bare invocation IS the gate, resolving its own base. Requiring an
|
|
272
|
+
// explicit ref would have made the roadmap's own `verify:` annotation exit
|
|
273
|
+
// 2 on a usage error, and a check whose default invocation is a usage
|
|
274
|
+
// error is a check nobody runs. `--new-only <ref>` stays as the override.
|
|
275
|
+
//
|
|
276
|
+
// `resolveBaseRef` rather than a hardcoded `origin/main`: `actions/checkout`
|
|
277
|
+
// performs a shallow PR-merge fetch, so a PR build frequently has no
|
|
278
|
+
// `origin/main` remote-tracking ref at all. That ladder already handles it
|
|
279
|
+
// and is the repo's one answer to this question.
|
|
280
|
+
const baseRef = _argValue('--new-only') ?? resolveBaseRef(root);
|
|
281
|
+
if (baseRef === null) {
|
|
282
|
+
// Fail rather than compare against an assumed-empty base — the same
|
|
283
|
+
// default `resolveBaseRef`'s own docstring names. An unresolvable base
|
|
284
|
+
// would otherwise make every artifact look un-added and the gate would
|
|
285
|
+
// pass by scanning nothing.
|
|
286
|
+
process.stdout.write(
|
|
287
|
+
'lint_evidence_artifacts: no base ref resolved — pass --new-only <ref>, ' +
|
|
288
|
+
'set RATCHET_BASE_REF, or run --all for the census\n',
|
|
289
|
+
);
|
|
290
|
+
return 2;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let files: string[];
|
|
294
|
+
try {
|
|
295
|
+
files = addedEvidenceFiles(root, baseRef);
|
|
296
|
+
} catch (exc) {
|
|
297
|
+
process.stdout.write(`lint_evidence_artifacts: cannot diff against ${baseRef} — ${String(exc)}\n`);
|
|
298
|
+
return 2;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const ledger = new GateLedger('lint_evidence_artifacts');
|
|
302
|
+
const findings = checkFiles(root, files, ledger);
|
|
303
|
+
ledger.report();
|
|
304
|
+
// A run that scans nothing exits green, and SAYS SO — silent green over an
|
|
305
|
+
// empty set is how a gate stops measuring anything without anyone
|
|
306
|
+
// noticing. The `allowEmpty` reason is `EMPTY_VALID` rather than
|
|
307
|
+
// `OPTIONAL_INPUT`: this gate measures a DIFF, not a corpus, so "no
|
|
308
|
+
// evidence artifact was added on this branch" genuinely is the success
|
|
309
|
+
// state and stays true even if `agents/evidence/` were deleted entirely.
|
|
310
|
+
reportScanned({
|
|
311
|
+
gate: 'lint_evidence_artifacts',
|
|
312
|
+
scanned: files.length,
|
|
313
|
+
units: 'added evidence artifacts',
|
|
314
|
+
roots: [EVIDENCE_ROOT],
|
|
315
|
+
allowEmpty:
|
|
316
|
+
'EMPTY_VALID: the scope is the set of artifacts ADDED since the base ref; ' +
|
|
317
|
+
'a branch that adds none has nothing to type, and zero is the pass.',
|
|
318
|
+
});
|
|
319
|
+
if (files.length === 0) {
|
|
320
|
+
process.stdout.write(
|
|
321
|
+
`lint_evidence_artifacts: ✅ no evidence artifact added since ${baseRef} — nothing to check\n`,
|
|
322
|
+
);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
out(`lint_evidence_artifacts — ${String(files.length)} added artifact(s) since ${baseRef}`);
|
|
326
|
+
if (findings.length === 0) {
|
|
327
|
+
process.stdout.write(
|
|
328
|
+
`lint_evidence_artifacts: ✅ all ${String(files.length)} added artifact(s) declare a type\n`,
|
|
329
|
+
);
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
process.stdout.write(`lint_evidence_artifacts: ❌ ${String(findings.length)} untyped artifact(s)\n`);
|
|
333
|
+
for (const f of findings) {
|
|
334
|
+
process.stdout.write(` ${f.file}\n ${f.reason}\n`);
|
|
335
|
+
}
|
|
336
|
+
return 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
340
|
+
function _isCliEntry(): boolean {
|
|
341
|
+
if (process.argv[1] === undefined) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
345
|
+
if (import.meta.url === argvUrl) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
350
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
351
|
+
return here === argv;
|
|
352
|
+
} catch {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
358
|
+
process.exit(main());
|
|
359
|
+
}
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* (Status, Owner, Blocks, What to do, Resolved when).
|
|
10
10
|
* 2. Every inline `<!-- blocked-by: <id> -->` annotation resolves to a
|
|
11
11
|
* `### blocker: <id>` entry declared in the SAME file.
|
|
12
|
+
* 3. An entry that declares `- **Class:**` declares a class the taxonomy
|
|
13
|
+
* knows (0-3), and a class-0 or class-1 entry says HOW it runs
|
|
14
|
+
* (`- **Run:**`). A gate that claims to be executable without naming the
|
|
15
|
+
* command is the same defect as a decision with no option set: it reads
|
|
16
|
+
* as actionable and is not.
|
|
12
17
|
*
|
|
13
18
|
* Fenced code blocks are stripped before scanning so a roadmap that shows
|
|
14
19
|
* the `## Blockers` shape as a documentation example is not flagged.
|
|
@@ -63,6 +68,36 @@ const DECIDABILITY_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
|
|
|
63
68
|
['If you do nothing', /^-[ \t]*\*\*If you do nothing:\*\*[ \t]*\S/im],
|
|
64
69
|
];
|
|
65
70
|
|
|
71
|
+
/**
|
|
72
|
+
* The gate taxonomy's four classes, as authored in the blocker entry.
|
|
73
|
+
*
|
|
74
|
+
* `Class` is deliberately OPTIONAL and its absence means class 3 — human-only.
|
|
75
|
+
* Making the safe class the default is what keeps a misclassification a
|
|
76
|
+
* reviewed edit rather than a runtime judgment: nothing an author forgets to
|
|
77
|
+
* write can become executable by omission.
|
|
78
|
+
*/
|
|
79
|
+
const CLASS_FIELD_RE = /^-[ \t]*\*\*Class:\*\*[ \t]*(.*)$/im;
|
|
80
|
+
const RUN_FIELD_RE = /^-[ \t]*\*\*Run:\*\*[ \t]*(\S.*)$/im;
|
|
81
|
+
const KNOWN_CLASSES: ReadonlySet<string> = new Set(['0', '1', '2', '3']);
|
|
82
|
+
/** The classes whose whole claim is that an agent can execute them. */
|
|
83
|
+
const RUNNABLE_CLASSES: ReadonlySet<string> = new Set(['0', '1']);
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The authored class, or `''` when the entry declares none.
|
|
87
|
+
*
|
|
88
|
+
* Only the leading token is read, so `- **Class:** 1 — budget-preauthorized`
|
|
89
|
+
* parses as `1`: the taxonomy name is documentation for the reader and must
|
|
90
|
+
* not have to be spelled identically in every entry to stay valid.
|
|
91
|
+
*/
|
|
92
|
+
function _blockerClass(body: string): string {
|
|
93
|
+
const m = CLASS_FIELD_RE.exec(body);
|
|
94
|
+
if (!m) {
|
|
95
|
+
return '';
|
|
96
|
+
}
|
|
97
|
+
const raw = ((m[1] as string) ?? '').trim();
|
|
98
|
+
return raw === '' ? '' : (raw.split(/[\s—–-]/)[0] as string);
|
|
99
|
+
}
|
|
100
|
+
|
|
66
101
|
/** `What to do:` body — everything up to the next field marker. */
|
|
67
102
|
const WHAT_TO_DO_SLICE_RE =
|
|
68
103
|
/^-[ \t]*\*\*What to do:\*\*([\s\S]*?)(?=^-[ \t]*\*\*[A-Z]|^###[ \t]|$(?![\s\S]))/im;
|
|
@@ -157,6 +192,33 @@ function _scanBoth(rawText: string): ScanResult {
|
|
|
157
192
|
// made would be churn, and the ratchet would never reach zero.
|
|
158
193
|
const isResolved = /^-[ \t]*\*\*Status:\*\*[ \t]*resolved/im.test(body);
|
|
159
194
|
if (!isResolved) {
|
|
195
|
+
// The class/run contract is HARD rather than ratcheted, and it
|
|
196
|
+
// can be: `Class:` is a new opt-in field, so on the day it
|
|
197
|
+
// ships no entry in the tree declares one and the rule fires on
|
|
198
|
+
// nothing. There is no backlog to grandfather — the "strict
|
|
199
|
+
// gate reds ~283 files" failure the decidability half had to
|
|
200
|
+
// avoid simply does not arise here.
|
|
201
|
+
const cls = _blockerClass(body);
|
|
202
|
+
if (cls !== '') {
|
|
203
|
+
const at = _lineAt(text, sectionStart + cur.start);
|
|
204
|
+
if (!KNOWN_CLASSES.has(cls)) {
|
|
205
|
+
violations.push({
|
|
206
|
+
line: at,
|
|
207
|
+
message:
|
|
208
|
+
`blocker '${cur.id}' declares unknown class '${cls}' ` +
|
|
209
|
+
'(expected 0 auto-run, 1 budget-preauthorized, ' +
|
|
210
|
+
'2 consent-once, or 3 human-only)',
|
|
211
|
+
});
|
|
212
|
+
} else if (RUNNABLE_CLASSES.has(cls) && !RUN_FIELD_RE.test(body)) {
|
|
213
|
+
violations.push({
|
|
214
|
+
line: at,
|
|
215
|
+
message:
|
|
216
|
+
`blocker '${cur.id}' is class ${cls} but declares no ` +
|
|
217
|
+
'**Run:** command — a gate that claims to be runnable ' +
|
|
218
|
+
'must say how',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
160
222
|
const gaps = DECIDABILITY_FIELDS.filter(([, re]) => !re.test(body)).map(
|
|
161
223
|
([name]) => name,
|
|
162
224
|
);
|
|
@@ -358,6 +420,9 @@ export {
|
|
|
358
420
|
ROADMAP_GLOB,
|
|
359
421
|
REQUIRED_FIELDS,
|
|
360
422
|
DECIDABILITY_FIELDS,
|
|
423
|
+
KNOWN_CLASSES,
|
|
424
|
+
RUNNABLE_CLASSES,
|
|
425
|
+
_blockerClass,
|
|
361
426
|
_hasExecutableSubstance,
|
|
362
427
|
_scan,
|
|
363
428
|
_scanBoth,
|
|
@@ -445,7 +445,13 @@ function _check_external_population_gates(text: string, warnings: string[]): voi
|
|
|
445
445
|
|
|
446
446
|
function _check_execution_mode(fm: string, problems: string[]): void {
|
|
447
447
|
if (!fm || !/^execution:/m.test(fm)) {
|
|
448
|
-
|
|
448
|
+
// Absent is VALID and stays valid — but it no longer means "interactive".
|
|
449
|
+
// Since road-to-user-out-of-the-loop Phase 1, an absent field is DERIVED
|
|
450
|
+
// by the `roadmap-process-loop § 3a` ladder from the invocation form, so
|
|
451
|
+
// the old comment here named a default that no longer exists. Nothing to
|
|
452
|
+
// check either way: this linter validates a declared value, and there is
|
|
453
|
+
// no declaration to validate.
|
|
454
|
+
return;
|
|
449
455
|
}
|
|
450
456
|
const m = EXEC_MODE_PAT.exec(fm);
|
|
451
457
|
if (m === null) {
|
package/src/scripts/release.ts
CHANGED
|
@@ -124,7 +124,9 @@ import {
|
|
|
124
124
|
derive_category_hits,
|
|
125
125
|
render_derived_head_values,
|
|
126
126
|
} from './_lib/release_highlights.js';
|
|
127
|
+
import { gh_argv_label, gh_retry } from './_lib/gh_transient.js';
|
|
127
128
|
import {
|
|
129
|
+
NEXT_SECTION_RE,
|
|
128
130
|
extract_changelog_section,
|
|
129
131
|
pr_body_from_section,
|
|
130
132
|
release_notes_from_section,
|
|
@@ -616,6 +618,31 @@ function git(args: readonly string[], opts: { capture?: boolean } = {}): string
|
|
|
616
618
|
return capture ? r.stdout.trim() : '';
|
|
617
619
|
}
|
|
618
620
|
|
|
621
|
+
function _sleep_ms(ms: number): void {
|
|
622
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* `gh` with transient-failure retry and a readable death. Always captures, so
|
|
627
|
+
* the classifier can see stderr; `check: false` keeps the caller's fallback.
|
|
628
|
+
*/
|
|
629
|
+
function gh(args: readonly string[], opts: { check?: boolean } = {}): RunResult {
|
|
630
|
+
const check = opts.check ?? true;
|
|
631
|
+
const r = gh_retry(args, (argv) => run(argv, { check: false, capture: true }), {
|
|
632
|
+
sleep: (ms) => (_exec_override ? undefined : _sleep_ms(ms)),
|
|
633
|
+
notify: (m) => process.stdout.write(`${m}\n`),
|
|
634
|
+
});
|
|
635
|
+
if (check && r.returncode !== 0) {
|
|
636
|
+
die(`${gh_argv_label(args)} failed (${r.returncode}):\n${(r.stderr || r.stdout).trim()}`);
|
|
637
|
+
}
|
|
638
|
+
// Mutating calls used to inherit stdio — echo, or the PR / release URL
|
|
639
|
+
// vanishes. Probes (`check: false`) stay quiet; their stdout is JSON.
|
|
640
|
+
if (check && r.stdout.trim()) {
|
|
641
|
+
process.stdout.write(`${r.stdout.trim()}\n`);
|
|
642
|
+
}
|
|
643
|
+
return r;
|
|
644
|
+
}
|
|
645
|
+
|
|
619
646
|
/**
|
|
620
647
|
* One name per failing check, from `gh pr checks --json name,bucket` output.
|
|
621
648
|
*
|
|
@@ -923,13 +950,9 @@ function push_release_branch(branch: string): void {
|
|
|
923
950
|
|
|
924
951
|
/** mergeStateStatus of the branch's PR ('' when the probe fails). */
|
|
925
952
|
function _pr_merge_state(branch: string): string {
|
|
926
|
-
const r =
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
check: false,
|
|
930
|
-
capture: true,
|
|
931
|
-
},
|
|
932
|
-
);
|
|
953
|
+
const r = gh(['pr', 'view', branch, '--json', 'mergeStateStatus', '--jq', '.mergeStateStatus'], {
|
|
954
|
+
check: false,
|
|
955
|
+
});
|
|
933
956
|
return r.returncode === 0 ? r.stdout.trim() : '';
|
|
934
957
|
}
|
|
935
958
|
|
|
@@ -973,14 +996,11 @@ function _refresh_pr_body_from_head(branch: string, target: string): void {
|
|
|
973
996
|
}
|
|
974
997
|
const capped = _cap_body(section!.body, GH_PR_BODY_LIMIT - 200, '`CHANGELOG.md` in this PR');
|
|
975
998
|
const body = pr_body_from_section(capped, target);
|
|
976
|
-
const live =
|
|
977
|
-
check: false,
|
|
978
|
-
capture: true,
|
|
979
|
-
});
|
|
999
|
+
const live = gh(['pr', 'view', branch, '--json', 'body', '-q', '.body'], { check: false });
|
|
980
1000
|
if (live.returncode === 0 && live.stdout.replace(/\r\n/gu, '\n').trim() === body.trim()) {
|
|
981
1001
|
return;
|
|
982
1002
|
}
|
|
983
|
-
|
|
1003
|
+
gh(['pr', 'edit', branch, '--body', body]);
|
|
984
1004
|
}
|
|
985
1005
|
|
|
986
1006
|
function merge_release_pr(branch: string, wait_for_checks: boolean): void {
|
|
@@ -1147,22 +1167,8 @@ function _push_tag(tag: string): void {
|
|
|
1147
1167
|
|
|
1148
1168
|
/** Most recent PR (any state) with `release/X.Y.Z` as head, or null. */
|
|
1149
1169
|
function _pr_for_branch(branch: string): Record<string, unknown> | null {
|
|
1150
|
-
const
|
|
1151
|
-
|
|
1152
|
-
'gh',
|
|
1153
|
-
'pr',
|
|
1154
|
-
'list',
|
|
1155
|
-
'--head',
|
|
1156
|
-
branch,
|
|
1157
|
-
'--state',
|
|
1158
|
-
'all',
|
|
1159
|
-
'--json',
|
|
1160
|
-
'number,state,url',
|
|
1161
|
-
'--limit',
|
|
1162
|
-
'1',
|
|
1163
|
-
],
|
|
1164
|
-
{ check: false, capture: true },
|
|
1165
|
-
);
|
|
1170
|
+
const argv = ['pr', 'list', '--head', branch, '--state', 'all', '--json', 'number,state,url'];
|
|
1171
|
+
const r = gh([...argv, '--limit', '1'], { check: false });
|
|
1166
1172
|
if (r.returncode !== 0) {
|
|
1167
1173
|
return null;
|
|
1168
1174
|
}
|
|
@@ -1476,8 +1482,7 @@ function _previous_test_count_from_changelog(prev_tag: string | null): number |
|
|
|
1476
1482
|
}
|
|
1477
1483
|
const headEnd = m.index + m[0].length;
|
|
1478
1484
|
const rest = text.slice(headEnd);
|
|
1479
|
-
const
|
|
1480
|
-
const next_heading = next_re.exec(rest);
|
|
1485
|
+
const next_heading = NEXT_SECTION_RE.exec(rest);
|
|
1481
1486
|
const sectionEnd = headEnd + (next_heading ? next_heading.index : rest.length);
|
|
1482
1487
|
const section = text.slice(headEnd, sectionEnd);
|
|
1483
1488
|
const count_match = _TEST_COUNT_LINE_RE.exec(section);
|
|
@@ -2141,19 +2146,8 @@ function execute(
|
|
|
2141
2146
|
'`CHANGELOG.md` in this PR',
|
|
2142
2147
|
);
|
|
2143
2148
|
const pr_body = pr_body_from_section(pr_changelog, plan.target);
|
|
2144
|
-
|
|
2145
|
-
'
|
|
2146
|
-
'pr',
|
|
2147
|
-
'create',
|
|
2148
|
-
'--base',
|
|
2149
|
-
MAIN_BRANCH,
|
|
2150
|
-
'--head',
|
|
2151
|
-
branch,
|
|
2152
|
-
'--title',
|
|
2153
|
-
`release: ${plan.target}`,
|
|
2154
|
-
'--body',
|
|
2155
|
-
pr_body,
|
|
2156
|
-
]);
|
|
2149
|
+
gh(['pr', 'create', '--base', MAIN_BRANCH, '--head', branch,
|
|
2150
|
+
'--title', `release: ${plan.target}`, '--body', pr_body]);
|
|
2157
2151
|
}
|
|
2158
2152
|
|
|
2159
2153
|
// ─── 6. wait for checks ─────────────────────────────────────────────────
|
|
@@ -2236,16 +2230,7 @@ function execute(
|
|
|
2236
2230
|
GH_RELEASE_NOTES_LIMIT,
|
|
2237
2231
|
'`CHANGELOG.md`',
|
|
2238
2232
|
);
|
|
2239
|
-
|
|
2240
|
-
'gh',
|
|
2241
|
-
'release',
|
|
2242
|
-
'create',
|
|
2243
|
-
plan.target,
|
|
2244
|
-
'--title',
|
|
2245
|
-
plan.target,
|
|
2246
|
-
'--notes',
|
|
2247
|
-
notes,
|
|
2248
|
-
]);
|
|
2233
|
+
gh(['release', 'create', plan.target, '--title', plan.target, '--notes', notes]);
|
|
2249
2234
|
|
|
2250
2235
|
// ─── 9b. dispatch-chain the tag-triggered workflows (--ci only) ──────
|
|
2251
2236
|
// release-guard.yml, publish-npm.yml, and cloud-release.yml all
|