@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,492 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Is the open PR's CI verdict about the code that is actually on the branch?
|
|
4
|
+
*
|
|
5
|
+
* "Fix the CI" names a remote state, and three different things can make a
|
|
6
|
+
* locally-verified fix look delivered when it is not. All three were measured on
|
|
7
|
+
* one run (2026-08-17, PR #1391) and none of them is visible from a green local
|
|
8
|
+
* gate:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Unpushed work.** The fix is committed locally, the PR still carries the
|
|
11
|
+
* failure. A pre-push hook refusing is enough to produce this silently.
|
|
12
|
+
* 2. **Stale green.** `gh pr checks` reports the last run it has, which may
|
|
13
|
+
* have run against the pre-fix commit. Reading that green as a verdict on
|
|
14
|
+
* the current head is the sharpest of the three, because the output says
|
|
15
|
+
* SUCCESS.
|
|
16
|
+
* 3. **A foreign head.** Someone pressed *Update branch* on the PR, so the
|
|
17
|
+
* branch is behind its OWN remote counterpart and the PR head is a commit
|
|
18
|
+
* this session never saw.
|
|
19
|
+
*
|
|
20
|
+
* Prose cannot enforce any of that — hence a gate. It answers one question:
|
|
21
|
+
* does a settled, successful check set exist whose head SHA is the PR head, and
|
|
22
|
+
* is that PR head what the local branch says it is?
|
|
23
|
+
*
|
|
24
|
+
* NOT a merge-readiness check and deliberately not a base-freshness check —
|
|
25
|
+
* `check_branch_freshness` owns "is the branch behind its base" and duplicating
|
|
26
|
+
* it here would mean two gates owning one verdict.
|
|
27
|
+
*
|
|
28
|
+
* Degrades rather than blocks on anything it cannot observe: no `gh`, no auth,
|
|
29
|
+
* an unresponsive endpoint, or no open PR all exit 0 with the reason named. A
|
|
30
|
+
* gate that blocks a push because the network is down is a gate that gets
|
|
31
|
+
* switched off. What it BLOCKS is the case where the remote answered and the
|
|
32
|
+
* answer is "your fix is not in this PR" or "this green is not about your code".
|
|
33
|
+
*
|
|
34
|
+
* Exit codes (contract §6): 0 = current, or unobservable-and-said-so ·
|
|
35
|
+
* 1 = the remote answered and the verdict is not about the branch head ·
|
|
36
|
+
* 2 = internal error. `scanned:` is emitted on EVERY exit path.
|
|
37
|
+
*
|
|
38
|
+
* ## The per-target ledger, and the exemption that was withdrawn
|
|
39
|
+
*
|
|
40
|
+
* This gate first shipped with a `// ledger-exempt:` marker arguing it had "no
|
|
41
|
+
* target population to account for — a short-circuiting chain of 0-3 remote
|
|
42
|
+
* facts". Its completion review refuted that from the file itself: the check
|
|
43
|
+
* rows ARE a population, and the parse below **drops** every row whose `state`
|
|
44
|
+
* is not a string. A dropped row could be the failing one, so the gate could
|
|
45
|
+
* print *"green on its own head"* over a check it never read — the precise
|
|
46
|
+
* "absence of scanning looks like absence of findings" shape the ledger exists
|
|
47
|
+
* to count.
|
|
48
|
+
*
|
|
49
|
+
* The exemption's secondary claim was accurate — every `main()` exit already
|
|
50
|
+
* self-reports through `scanReport` with an explicit `allowEmpty` reason — and
|
|
51
|
+
* that is exactly why the first argument was easy to believe. It was still
|
|
52
|
+
* resting on the false half.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import * as path from 'node:path';
|
|
56
|
+
import { spawnSync } from 'node:child_process';
|
|
57
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
58
|
+
|
|
59
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
60
|
+
import { reportScanned } from './_lib/scan_scope.js';
|
|
61
|
+
|
|
62
|
+
const NETWORK_TIMEOUT_MS = 8_000;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Emit the count through the shared helper, so the published number is by
|
|
66
|
+
* construction the one the scope assertion just accepted.
|
|
67
|
+
*
|
|
68
|
+
* `allowEmpty` is the load-bearing parameter, and it is also why this gate does
|
|
69
|
+
* NOT carry a `min_scanned` floor in `src/config/gate-coverage.yml`. The number
|
|
70
|
+
* of facts compared here is 0–3 depending on remote state, and zero is a
|
|
71
|
+
* legitimate pass on two paths (no open PR, nothing observable). A floor cannot
|
|
72
|
+
* express that, and the manifest's own header says padding it to raise a
|
|
73
|
+
* coverage figure is the opposite of gaming. Same reasoning, same shape, as the
|
|
74
|
+
* sibling network probe `check_branch_freshness` — which is likewise unregistered
|
|
75
|
+
* for exactly this reason. "There was nothing to check" and "I checked and it was
|
|
76
|
+
* fine" must not print the same thing, which `allowEmpty` is what enforces.
|
|
77
|
+
*/
|
|
78
|
+
function scanReport(scanned: number, allowEmpty?: string): void {
|
|
79
|
+
reportScanned({
|
|
80
|
+
gate: 'check_pr_ci_current',
|
|
81
|
+
scanned,
|
|
82
|
+
units: 'remote fact(s) compared',
|
|
83
|
+
roots: ['origin'],
|
|
84
|
+
...(allowEmpty === undefined ? {} : { allowEmpty }),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface Probe {
|
|
89
|
+
ok: boolean;
|
|
90
|
+
stdout: string;
|
|
91
|
+
reason?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function run(cmd: string, args: readonly string[], cwd: string): Probe {
|
|
95
|
+
// Bounds `git` as well as `gh`: the sibling gate records that an unresponsive
|
|
96
|
+
// SSH remote reached through an UNTIMED git call is how a stated worst case
|
|
97
|
+
// stops being one.
|
|
98
|
+
const res = spawnSync(cmd, [...args], {
|
|
99
|
+
cwd,
|
|
100
|
+
encoding: 'utf-8',
|
|
101
|
+
timeout: NETWORK_TIMEOUT_MS,
|
|
102
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
103
|
+
});
|
|
104
|
+
if (res.error !== undefined || res.status === null) {
|
|
105
|
+
return { ok: false, stdout: '', reason: `${cmd} did not answer within ${String(NETWORK_TIMEOUT_MS / 1000)}s` };
|
|
106
|
+
}
|
|
107
|
+
if (res.status !== 0) {
|
|
108
|
+
return { ok: false, stdout: res.stdout ?? '', reason: (res.stderr ?? '').trim().split('\n')[0] ?? 'non-zero exit' };
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, stdout: res.stdout ?? '' };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface CheckRow {
|
|
114
|
+
state: string;
|
|
115
|
+
name: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface Verdict {
|
|
119
|
+
/** 0 = current or unobservable · 1 = the remote answered and it is stale. */
|
|
120
|
+
exit: 0 | 1;
|
|
121
|
+
/** One line, always printed — the reason is the whole product of this gate. */
|
|
122
|
+
message: string;
|
|
123
|
+
/** How many facts the gate actually compared. */
|
|
124
|
+
scanned: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface Facts {
|
|
128
|
+
/** null when there is no open PR for this branch — nothing to verify. */
|
|
129
|
+
pr: number | null;
|
|
130
|
+
/** The PR head SHA the FORGE reports. */
|
|
131
|
+
prHead: string | null;
|
|
132
|
+
/** Local branch tip. */
|
|
133
|
+
localHead: string | null;
|
|
134
|
+
/** The head SHA the reported checks ran against. */
|
|
135
|
+
checksHead: string | null;
|
|
136
|
+
rows: readonly CheckRow[];
|
|
137
|
+
/**
|
|
138
|
+
* Names of check rows the parse DROPPED because their `state` was not a
|
|
139
|
+
* string. Carried out rather than swallowed: a dropped row could be the
|
|
140
|
+
* failing one, so it is a skip the ledger must count, not a non-event.
|
|
141
|
+
*/
|
|
142
|
+
droppedRows?: readonly string[];
|
|
143
|
+
/** Set when something could not be observed; forces the degrade path. */
|
|
144
|
+
unobservable: string | null;
|
|
145
|
+
/**
|
|
146
|
+
* How the local tip relates to the PR head — ancestry, not equality, because
|
|
147
|
+
* the two disagreements mean opposite things.
|
|
148
|
+
*
|
|
149
|
+
* `ahead` local is a descendant: normal state before a push, and the
|
|
150
|
+
* "fix never pushed" defect after one.
|
|
151
|
+
* `behind` local is an ancestor: the PR head is a commit this session never
|
|
152
|
+
* made — someone pressed Update branch. NEVER ok, in either mode:
|
|
153
|
+
* a push would be rejected, and forcing over it is what
|
|
154
|
+
* git-history-discipline gates.
|
|
155
|
+
* `diverged` neither contains the other. Never ok either.
|
|
156
|
+
*/
|
|
157
|
+
relation: 'equal' | 'ahead' | 'behind' | 'diverged' | 'unknown';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface DecideOptions {
|
|
161
|
+
/**
|
|
162
|
+
* True when the gate runs BEFORE a push (the pre-push chain).
|
|
163
|
+
*
|
|
164
|
+
* Load-bearing, and the reason this flag exists at all: before a push, local
|
|
165
|
+
* being ahead of the PR head is the normal state of every branch with work on
|
|
166
|
+
* it. A gate that treated it as a defect would refuse every push there is —
|
|
167
|
+
* which is the chicken-and-egg this flag resolves rather than papers over.
|
|
168
|
+
* `behind` and `diverged` still block in both modes.
|
|
169
|
+
*/
|
|
170
|
+
prePush?: boolean;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Pure decision over already-gathered facts, so the whole verdict table is
|
|
175
|
+
* testable without a network or a forge.
|
|
176
|
+
*/
|
|
177
|
+
export function decide(f: Facts, opts: DecideOptions = {}): Verdict {
|
|
178
|
+
if (f.unobservable !== null) {
|
|
179
|
+
return { exit: 0, message: `unverified — ${f.unobservable}. Not a pass; nothing was observed.`, scanned: 0 };
|
|
180
|
+
}
|
|
181
|
+
if (f.pr === null) {
|
|
182
|
+
return { exit: 0, message: 'no open PR for this branch — no remote verdict to be current with.', scanned: 0 };
|
|
183
|
+
}
|
|
184
|
+
if (f.relation === 'behind' || f.relation === 'diverged') {
|
|
185
|
+
return {
|
|
186
|
+
exit: 1,
|
|
187
|
+
message:
|
|
188
|
+
`PR #${String(f.pr)} head ${f.prHead?.slice(0, 9) ?? '?'} is not reachable from the local ` +
|
|
189
|
+
`branch (${f.relation}) — the PR carries a commit this checkout does not have, which is what ` +
|
|
190
|
+
'a GitHub *Update branch* press looks like. Merge it in; never force over it.',
|
|
191
|
+
scanned: 2,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (f.relation === 'ahead') {
|
|
195
|
+
if (opts.prePush === true) {
|
|
196
|
+
return {
|
|
197
|
+
exit: 0,
|
|
198
|
+
message:
|
|
199
|
+
`PR #${String(f.pr)} is behind the local branch, which is the normal state before a ` +
|
|
200
|
+
'push. Re-run after pushing to verify the remote verdict.',
|
|
201
|
+
scanned: 2,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
exit: 1,
|
|
206
|
+
message:
|
|
207
|
+
`PR #${String(f.pr)} head is ${f.prHead?.slice(0, 9) ?? '?'} but the local branch is at ` +
|
|
208
|
+
`${f.localHead?.slice(0, 9) ?? '?'} — the PR does NOT contain what is committed here. ` +
|
|
209
|
+
'A local green says nothing about a fix that was never pushed.',
|
|
210
|
+
scanned: 2,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
if (f.rows.length === 0) {
|
|
214
|
+
return {
|
|
215
|
+
exit: 1,
|
|
216
|
+
message:
|
|
217
|
+
`PR #${String(f.pr)} reports no checks at all. Absence of a red is not a green — ` +
|
|
218
|
+
'either the run never registered, or the workflow paths do not match this diff.',
|
|
219
|
+
scanned: 2,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (f.checksHead !== null && f.prHead !== null && f.checksHead !== f.prHead) {
|
|
223
|
+
return {
|
|
224
|
+
exit: 1,
|
|
225
|
+
message:
|
|
226
|
+
`STALE VERDICT — PR #${String(f.pr)} checks ran against ${f.checksHead.slice(0, 9)}, ` +
|
|
227
|
+
`head is ${f.prHead.slice(0, 9)}. Whatever those checks say, they do not say it about ` +
|
|
228
|
+
'the current code. This is the failure mode that reads as SUCCESS.',
|
|
229
|
+
scanned: 3,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
const unsettled = f.rows.filter((r) => ['PENDING', 'QUEUED', 'IN_PROGRESS'].includes(r.state));
|
|
233
|
+
if (unsettled.length > 0) {
|
|
234
|
+
return {
|
|
235
|
+
exit: 1,
|
|
236
|
+
message:
|
|
237
|
+
`PR #${String(f.pr)} has ${String(unsettled.length)} check(s) still running on ` +
|
|
238
|
+
`${f.prHead?.slice(0, 9) ?? 'head'} — pending is a reason to WAIT, never a verdict.`,
|
|
239
|
+
scanned: f.rows.length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
const bad = f.rows.filter((r) => !['SUCCESS', 'SKIPPED', 'NEUTRAL'].includes(r.state));
|
|
243
|
+
if (bad.length > 0) {
|
|
244
|
+
return {
|
|
245
|
+
exit: 1,
|
|
246
|
+
message:
|
|
247
|
+
`PR #${String(f.pr)} is RED on its current head: ` +
|
|
248
|
+
bad.map((r) => `${r.state} ${r.name}`).join(', '),
|
|
249
|
+
scanned: f.rows.length,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
exit: 0,
|
|
254
|
+
message:
|
|
255
|
+
`PR #${String(f.pr)} is green on its own head ${f.prHead?.slice(0, 9) ?? '?'}, ` +
|
|
256
|
+
'and the local branch matches it.',
|
|
257
|
+
scanned: f.rows.length,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether {@link decide} reached the check rows at all.
|
|
263
|
+
*
|
|
264
|
+
* Every branch above the row logic — unobservable, no open PR, behind/diverged,
|
|
265
|
+
* ahead — settles the verdict WITHOUT reading a single row state. Recording
|
|
266
|
+
* those rows as `complete` would publish `scanned=N` for a run that inspected
|
|
267
|
+
* none of them: the absence-of-scanning-reads-as-absence-of-findings inflation
|
|
268
|
+
* the ledger exists to count, in the gate that adopted it. Under `--pre-push`,
|
|
269
|
+
* `ahead` is the NORMAL state, so this is the common path, not the exotic one.
|
|
270
|
+
*/
|
|
271
|
+
export function rowsWereEvaluated(f: Facts): boolean {
|
|
272
|
+
if (f.unobservable !== null) return false;
|
|
273
|
+
if (f.pr === null) return false;
|
|
274
|
+
return f.relation !== 'behind' && f.relation !== 'diverged' && f.relation !== 'ahead';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Publish the row-level accounting for one run.
|
|
279
|
+
*
|
|
280
|
+
* Three populations, three outcomes: rows the parse dropped are skips (a dropped
|
|
281
|
+
* row could be the failing one), rows a row-reading verdict covered are
|
|
282
|
+
* completions, and rows the verdict never reached are out of scope for THIS run.
|
|
283
|
+
*
|
|
284
|
+
* The line is emitted even under `--quiet` — {@link GateLedger.report} writes
|
|
285
|
+
* unconditionally by design, so a `--quiet` run of this gate is one line louder
|
|
286
|
+
* than before. Stated here because it is a change to this gate's own contract.
|
|
287
|
+
*/
|
|
288
|
+
function reportRowLedger(f: Facts): void {
|
|
289
|
+
const ledger = new GateLedger('check_pr_ci_current');
|
|
290
|
+
const dropped = new Set(f.droppedRows ?? []);
|
|
291
|
+
// Duplicate check names are a normal remote condition — a cancelled re-run
|
|
292
|
+
// beside a live one, two workflows sharing a job name, or a dropped row
|
|
293
|
+
// whose name equals a kept row's. `plan` throws on a repeat, and this gate
|
|
294
|
+
// promises to degrade rather than block, so the plan is deduplicated.
|
|
295
|
+
const planned = [...new Set([...f.rows.map((r) => r.name), ...dropped])];
|
|
296
|
+
ledger.plan(planned);
|
|
297
|
+
const evaluated = rowsWereEvaluated(f);
|
|
298
|
+
for (const name of planned) {
|
|
299
|
+
if (dropped.has(name)) ledger.skip(name, 'not_applicable_kind');
|
|
300
|
+
else if (evaluated) ledger.complete(name);
|
|
301
|
+
else ledger.outOfScope(name, 'precondition_unmet');
|
|
302
|
+
}
|
|
303
|
+
ledger.report();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Ancestry between the local tip and the PR head.
|
|
308
|
+
*
|
|
309
|
+
* Equality is not enough: "local is ahead" and "local is behind" are opposite
|
|
310
|
+
* situations with opposite correct actions (push vs merge), and an equality test
|
|
311
|
+
* collapses them into one indistinguishable mismatch. A SHA the checkout does not
|
|
312
|
+
* have is `unknown` rather than a guess — the fetch is the caller's business.
|
|
313
|
+
*/
|
|
314
|
+
export function relate(
|
|
315
|
+
repo: string,
|
|
316
|
+
local: string | null,
|
|
317
|
+
pr: string | null,
|
|
318
|
+
): Facts['relation'] {
|
|
319
|
+
if (local === null || pr === null) return 'unknown';
|
|
320
|
+
if (local === pr) return 'equal';
|
|
321
|
+
if (!run('git', ['cat-file', '-e', `${pr}^{commit}`], repo).ok) return 'unknown';
|
|
322
|
+
const prReachable = run('git', ['merge-base', '--is-ancestor', pr, local], repo).ok;
|
|
323
|
+
const localReachable = run('git', ['merge-base', '--is-ancestor', local, pr], repo).ok;
|
|
324
|
+
if (prReachable) return 'ahead';
|
|
325
|
+
if (localReachable) return 'behind';
|
|
326
|
+
return 'diverged';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Gather the facts `decide` consumes. Every failure becomes `unobservable`. */
|
|
330
|
+
export function gather(repo: string): Facts {
|
|
331
|
+
const empty: Facts = {
|
|
332
|
+
pr: null, prHead: null, localHead: null, checksHead: null, rows: [], droppedRows: [], unobservable: null, relation: 'unknown',
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const branch = run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], repo);
|
|
336
|
+
if (!branch.ok) return { ...empty, unobservable: `cannot read the current branch (${branch.reason ?? '?'})` };
|
|
337
|
+
const branchName = branch.stdout.trim();
|
|
338
|
+
if (branchName === 'HEAD' || branchName === '') {
|
|
339
|
+
return { ...empty, unobservable: 'detached HEAD — no branch to resolve a PR for' };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const local = run('git', ['rev-parse', 'HEAD'], repo);
|
|
343
|
+
if (!local.ok) return { ...empty, unobservable: `cannot read the local head (${local.reason ?? '?'})` };
|
|
344
|
+
|
|
345
|
+
const pr = run(
|
|
346
|
+
'gh',
|
|
347
|
+
['pr', 'list', '--head', branchName, '--state', 'open', '--json', 'number,headRefOid', '--limit', '1'],
|
|
348
|
+
repo,
|
|
349
|
+
);
|
|
350
|
+
if (!pr.ok) return { ...empty, unobservable: `gh could not be reached (${pr.reason ?? '?'})` };
|
|
351
|
+
let parsed: Array<{ number?: number; headRefOid?: string }>;
|
|
352
|
+
try {
|
|
353
|
+
parsed = JSON.parse(pr.stdout || '[]') as Array<{ number?: number; headRefOid?: string }>;
|
|
354
|
+
} catch {
|
|
355
|
+
return { ...empty, unobservable: 'gh returned output this gate could not parse' };
|
|
356
|
+
}
|
|
357
|
+
const first = parsed[0];
|
|
358
|
+
if (first?.number === undefined) {
|
|
359
|
+
return { ...empty, localHead: local.stdout.trim(), unobservable: null };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const checks = run('gh', ['pr', 'checks', String(first.number), '--json', 'state,name'], repo);
|
|
363
|
+
// `gh pr checks` exits non-zero when any check is failing, so a non-zero exit
|
|
364
|
+
// with parseable stdout is DATA, not an error. Treating it as unobservable
|
|
365
|
+
// would make the gate blind in exactly the red case it exists for.
|
|
366
|
+
let rows: CheckRow[] = [];
|
|
367
|
+
// A row whose `state` is not a string is DROPPED, and that drop is the one
|
|
368
|
+
// thing in this gate worth counting: a dropped row could be the failing one,
|
|
369
|
+
// and without accounting the gate prints "green on its own head" over a
|
|
370
|
+
// check it never read. `dropped` is surfaced by the caller through the
|
|
371
|
+
// ledger rather than swallowed here.
|
|
372
|
+
let dropped: string[] = [];
|
|
373
|
+
if (checks.stdout.trim() !== '') {
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(checks.stdout) as CheckRow[];
|
|
376
|
+
rows = parsed.filter((r) => typeof r.state === 'string');
|
|
377
|
+
dropped = parsed
|
|
378
|
+
.filter((r) => typeof r.state !== 'string')
|
|
379
|
+
.map((r, i) => (typeof r.name === 'string' ? r.name : `row-${String(i)}`));
|
|
380
|
+
} catch {
|
|
381
|
+
rows = [];
|
|
382
|
+
}
|
|
383
|
+
} else if (!checks.ok) {
|
|
384
|
+
return {
|
|
385
|
+
...empty,
|
|
386
|
+
pr: first.number,
|
|
387
|
+
prHead: first.headRefOid ?? null,
|
|
388
|
+
localHead: local.stdout.trim(),
|
|
389
|
+
unobservable: `gh pr checks gave no readable output (${checks.reason ?? '?'})`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// The SHA the checks ran against. `gh pr checks` does not expose it, so it
|
|
394
|
+
// comes from the check runs on the PR head — if the forge reports runs for a
|
|
395
|
+
// different SHA, that is the stale-green case.
|
|
396
|
+
const runsHead = run(
|
|
397
|
+
'gh',
|
|
398
|
+
['api', `repos/{owner}/{repo}/commits/${first.headRefOid ?? 'HEAD'}/check-runs`, '--jq', '.check_runs[0].head_sha'],
|
|
399
|
+
repo,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
const localSha = local.stdout.trim();
|
|
403
|
+
const prSha = first.headRefOid ?? null;
|
|
404
|
+
return {
|
|
405
|
+
pr: first.number,
|
|
406
|
+
prHead: prSha,
|
|
407
|
+
localHead: localSha,
|
|
408
|
+
checksHead: runsHead.ok && runsHead.stdout.trim() !== '' ? runsHead.stdout.trim() : null,
|
|
409
|
+
rows,
|
|
410
|
+
droppedRows: dropped,
|
|
411
|
+
unobservable: null,
|
|
412
|
+
relation: relate(repo, localSha, prSha),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function main(argv?: readonly string[]): number {
|
|
417
|
+
const args = argv ?? process.argv.slice(2);
|
|
418
|
+
let repo = process.cwd();
|
|
419
|
+
let quiet = false;
|
|
420
|
+
let prePush = false;
|
|
421
|
+
for (let i = 0; i < args.length; i++) {
|
|
422
|
+
const a = args[i] as string;
|
|
423
|
+
if (a === '--pre-push') {
|
|
424
|
+
prePush = true;
|
|
425
|
+
} else if (a === '--repo') {
|
|
426
|
+
const v = args[++i];
|
|
427
|
+
if (v === undefined || v.startsWith('--')) {
|
|
428
|
+
process.stderr.write('❌ check_pr_ci_current: --repo requires a value\n');
|
|
429
|
+
scanReport(0, 'argument error — nothing was compared');
|
|
430
|
+
return 1;
|
|
431
|
+
}
|
|
432
|
+
repo = v;
|
|
433
|
+
} else if (a === '--quiet') {
|
|
434
|
+
quiet = true;
|
|
435
|
+
} else if (a === '-h' || a === '--help') {
|
|
436
|
+
process.stdout.write(
|
|
437
|
+
'usage: check_pr_ci_current [--repo PATH] [--pre-push] [--quiet]\n' +
|
|
438
|
+
' --pre-push tolerates the PR being behind the local branch, which is\n' +
|
|
439
|
+
' the normal state before a push; it still blocks a PR head the local\n' +
|
|
440
|
+
' branch cannot reach.\n' +
|
|
441
|
+
' Verifies the open PR carries the local commits AND that its check\n' +
|
|
442
|
+
' verdict ran against the PR head. Degrades to exit 0 when the remote\n' +
|
|
443
|
+
' cannot be observed, naming what is unverified.\n',
|
|
444
|
+
);
|
|
445
|
+
scanReport(0, 'help output — nothing was compared');
|
|
446
|
+
return 0;
|
|
447
|
+
} else {
|
|
448
|
+
process.stderr.write(`❌ check_pr_ci_current: unknown argument \`${a}\`\n`);
|
|
449
|
+
scanReport(0, 'argument error — nothing was compared');
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
let verdict: Verdict;
|
|
455
|
+
try {
|
|
456
|
+
const facts = gather(repo);
|
|
457
|
+
// The verdict comes FIRST: whether the rows were inspected at all is a
|
|
458
|
+
// property of the verdict's path, not of the parse (see reportRowLedger).
|
|
459
|
+
verdict = decide(facts, { prePush });
|
|
460
|
+
reportRowLedger(facts);
|
|
461
|
+
} catch (exc) {
|
|
462
|
+
scanReport(0, 'internal error — nothing was compared');
|
|
463
|
+
process.stderr.write(
|
|
464
|
+
`❌ check_pr_ci_current: internal error: ${exc instanceof Error ? exc.message : String(exc)}\n`,
|
|
465
|
+
);
|
|
466
|
+
return 2;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// The degrade path stays LOUD even under --quiet: "unverified" reported
|
|
470
|
+
// silently is indistinguishable from "verified", which is the whole defect.
|
|
471
|
+
if (verdict.exit === 1) {
|
|
472
|
+
process.stdout.write(`❌ ${verdict.message}\n`);
|
|
473
|
+
} else if (verdict.message.startsWith('unverified')) {
|
|
474
|
+
process.stdout.write(`⚠️ check_pr_ci_current: ${verdict.message}\n`);
|
|
475
|
+
} else if (!quiet) {
|
|
476
|
+
process.stdout.write(`✅ ${verdict.message}\n`);
|
|
477
|
+
}
|
|
478
|
+
scanReport(
|
|
479
|
+
verdict.scanned,
|
|
480
|
+
verdict.scanned === 0 ? 'no open PR, or the remote could not be observed — stated above' : undefined,
|
|
481
|
+
);
|
|
482
|
+
return verdict.exit;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
486
|
+
function _isCliEntry(): boolean {
|
|
487
|
+
if (process.argv[1] === undefined) return false;
|
|
488
|
+
return import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href || process.argv[1] === _HERE;
|
|
489
|
+
}
|
|
490
|
+
if (_isCliEntry()) {
|
|
491
|
+
process.exit(main());
|
|
492
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Gate — every committed review-findings ledger conforms to its schema AND
|
|
4
|
+
* declares its own independence consistently.
|
|
5
|
+
*
|
|
6
|
+
* The defect this closes is not a malformed file. It is a well-formed one that
|
|
7
|
+
* SAYS NOTHING: a ledger produced by a single-model pass was byte-
|
|
8
|
+
* indistinguishable from one backed by independent models, so a consumer read
|
|
9
|
+
* "no findings" as cross-model acceptance. Silence defaulted to the strongest
|
|
10
|
+
* possible reading, which is the wrong direction for an integrity field.
|
|
11
|
+
*
|
|
12
|
+
* Two checks, and the second is the one with teeth:
|
|
13
|
+
* 1. schema conformance against review-findings.schema.json;
|
|
14
|
+
* 2. `acceptance_status` and `assurance` must be the values
|
|
15
|
+
* `review_independence` derives — so a hand-set `accepted` beside a
|
|
16
|
+
* same-family reviewer set is caught rather than trusted.
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 — every ledger conforms and declares consistently
|
|
20
|
+
* 1 — one or more violations
|
|
21
|
+
* 2 — the gate could not run (no ledger directory)
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as os from 'node:os';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { GateLedger } from './_lib/gate_ledger.js';
|
|
29
|
+
import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
|
|
30
|
+
import { independenceViolations } from './_lib/review_independence.js';
|
|
31
|
+
import { load_schema, validate, type YamlValue } from './validate_frontmatter.js';
|
|
32
|
+
|
|
33
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
34
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
35
|
+
const LEDGER_DIR = path.join(process.cwd(), 'agents', 'evidence', 'release-findings');
|
|
36
|
+
|
|
37
|
+
function selfTest(): number {
|
|
38
|
+
const mk = (name: string, body: unknown): string => {
|
|
39
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'crs-selftest-'));
|
|
40
|
+
const dir = path.join(root, 'agents', 'evidence', 'release-findings');
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
fs.writeFileSync(path.join(dir, name), JSON.stringify(body, null, 2) + '\n', 'utf-8');
|
|
43
|
+
return root;
|
|
44
|
+
};
|
|
45
|
+
const run = (root: string): number =>
|
|
46
|
+
runGateCli(REPO_ROOT, 'src/scripts/check_review_schema.ts', ['--quiet'], root);
|
|
47
|
+
|
|
48
|
+
const base = {
|
|
49
|
+
schema_version: 1,
|
|
50
|
+
release: '1.0.0',
|
|
51
|
+
reviewers: ['anthropic', 'openai'],
|
|
52
|
+
findings: [],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return runSelfTest({
|
|
56
|
+
gate: 'check_review_schema',
|
|
57
|
+
minCases: 4,
|
|
58
|
+
minRejectCases: 3,
|
|
59
|
+
cases: [
|
|
60
|
+
{
|
|
61
|
+
name: 'a cross-family ledger declaring accepted passes',
|
|
62
|
+
expect: 'accept',
|
|
63
|
+
run: () =>
|
|
64
|
+
run(
|
|
65
|
+
mk('1.0.0.json', {
|
|
66
|
+
...base,
|
|
67
|
+
review_independence: 'cross-family',
|
|
68
|
+
acceptance_status: 'accepted',
|
|
69
|
+
assurance: 'independent',
|
|
70
|
+
}),
|
|
71
|
+
),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'a same-family ledger claiming accepted is rejected — the whole point of the pair',
|
|
75
|
+
expect: 'reject',
|
|
76
|
+
run: () =>
|
|
77
|
+
run(
|
|
78
|
+
mk('1.0.0.json', {
|
|
79
|
+
...base,
|
|
80
|
+
reviewers: ['anthropic', 'anthropic'],
|
|
81
|
+
review_independence: 'same-family',
|
|
82
|
+
acceptance_status: 'accepted',
|
|
83
|
+
assurance: 'independent',
|
|
84
|
+
}),
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'a ledger that declares no independence at all is rejected — silence read as acceptance is the defect',
|
|
89
|
+
expect: 'reject',
|
|
90
|
+
run: () => run(mk('1.0.0.json', base)),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'a ledger whose assurance contradicts its independence is rejected',
|
|
94
|
+
expect: 'reject',
|
|
95
|
+
run: () =>
|
|
96
|
+
run(
|
|
97
|
+
mk('1.0.0.json', {
|
|
98
|
+
...base,
|
|
99
|
+
reviewers: ['anthropic'],
|
|
100
|
+
review_independence: 'single-member',
|
|
101
|
+
acceptance_status: 'provisional',
|
|
102
|
+
assurance: 'independent',
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function main(argv?: readonly string[]): number {
|
|
111
|
+
const args = argv ?? process.argv.slice(2);
|
|
112
|
+
if (args.includes('--self-test')) return selfTest();
|
|
113
|
+
const quiet = args.includes('--quiet');
|
|
114
|
+
|
|
115
|
+
if (!fs.existsSync(LEDGER_DIR)) {
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`❌ check_review_schema: ledger directory absent (${LEDGER_DIR}) — the gate cannot run\n`,
|
|
118
|
+
);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const files = fs.readdirSync(LEDGER_DIR).filter((n) => n.endsWith('.json')).sort();
|
|
123
|
+
if (files.length === 0) {
|
|
124
|
+
process.stderr.write(
|
|
125
|
+
`❌ check_review_schema: 0 ledgers under ${LEDGER_DIR} — a gate that scanned nothing has verified nothing\n`,
|
|
126
|
+
);
|
|
127
|
+
return 2;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ledger = new GateLedger('check_review_schema');
|
|
131
|
+
ledger.plan(files);
|
|
132
|
+
const schema = load_schema('review-findings');
|
|
133
|
+
const problems: string[] = [];
|
|
134
|
+
|
|
135
|
+
for (const name of files) {
|
|
136
|
+
const before = problems.length;
|
|
137
|
+
let doc: Record<string, unknown>;
|
|
138
|
+
try {
|
|
139
|
+
doc = JSON.parse(fs.readFileSync(path.join(LEDGER_DIR, name), 'utf-8')) as Record<string, unknown>;
|
|
140
|
+
} catch (exc) {
|
|
141
|
+
problems.push(`${name}: unparseable JSON — ${String(exc)}`);
|
|
142
|
+
ledger.fail(name, 'unparseable');
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
for (const e of validate(doc as YamlValue, schema).filter((x) => x.severity !== 'warning')) {
|
|
146
|
+
problems.push(`${name}: schema — ${e.format()}`);
|
|
147
|
+
}
|
|
148
|
+
for (const v of independenceViolations(doc)) {
|
|
149
|
+
problems.push(`${name}: independence — ${v}`);
|
|
150
|
+
}
|
|
151
|
+
if (problems.length > before) ledger.fail(name, `${String(problems.length - before)} violation(s)`);
|
|
152
|
+
else ledger.complete(name);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
ledger.finalize();
|
|
156
|
+
process.stderr.write(`scanned: ${files.length}\n`);
|
|
157
|
+
|
|
158
|
+
if (problems.length > 0) {
|
|
159
|
+
process.stdout.write(
|
|
160
|
+
`❌ check_review_schema: ${problems.length} violation(s) across ${files.length} ledger(s)\n\n`,
|
|
161
|
+
);
|
|
162
|
+
for (const p of problems) process.stdout.write(` ${p}\n`);
|
|
163
|
+
process.stdout.write(
|
|
164
|
+
`\nA review artifact that does not declare its independence reads as cross-model\n` +
|
|
165
|
+
`acceptance to every consumer. Derive the fields with _lib/review_independence.ts.\n`,
|
|
166
|
+
);
|
|
167
|
+
return 1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!quiet) {
|
|
171
|
+
process.stdout.write(
|
|
172
|
+
`✅ check_review_schema: ${files.length} ledger(s) conform and declare independence consistently.\n`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _isCliEntry(): boolean {
|
|
179
|
+
if (process.argv[1] === undefined) return false;
|
|
180
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
181
|
+
try {
|
|
182
|
+
return (
|
|
183
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) ===
|
|
184
|
+
fs.realpathSync(path.resolve(process.argv[1]))
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
192
|
+
process.exit(main());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export { main };
|