@ai-dossier/cli 0.13.0 → 0.18.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/README.md +236 -6
- package/dist/cache-resolver.d.ts.map +1 -1
- package/dist/cache-resolver.js +2 -11
- package/dist/cache-resolver.js.map +1 -1
- package/dist/cap-log.d.ts +31 -0
- package/dist/cap-log.d.ts.map +1 -0
- package/dist/cap-log.js +30 -0
- package/dist/cap-log.js.map +1 -0
- package/dist/capability.d.ts +108 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +407 -0
- package/dist/capability.js.map +1 -0
- package/dist/cli.js +6 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/cap.d.ts +13 -0
- package/dist/commands/cap.d.ts.map +1 -0
- package/dist/commands/cap.js +105 -0
- package/dist/commands/cap.js.map +1 -0
- package/dist/commands/plan.d.ts +20 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +323 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/runstate.d.ts.map +1 -1
- package/dist/commands/runstate.js +56 -292
- package/dist/commands/runstate.js.map +1 -1
- package/dist/commands/sched.d.ts +11 -0
- package/dist/commands/sched.d.ts.map +1 -0
- package/dist/commands/sched.js +402 -0
- package/dist/commands/sched.js.map +1 -0
- package/dist/gh.d.ts +155 -0
- package/dist/gh.d.ts.map +1 -0
- package/dist/gh.js +357 -0
- package/dist/gh.js.map +1 -0
- package/dist/helpers.d.ts +7 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +19 -0
- package/dist/helpers.js.map +1 -1
- package/dist/issue-selection.js +2 -2
- package/dist/issue-selection.js.map +1 -1
- package/dist/jsonl-log.d.ts +14 -0
- package/dist/jsonl-log.d.ts.map +1 -0
- package/dist/jsonl-log.js +32 -0
- package/dist/jsonl-log.js.map +1 -0
- package/dist/plan-artifact.d.ts +123 -0
- package/dist/plan-artifact.d.ts.map +1 -0
- package/dist/plan-artifact.js +213 -0
- package/dist/plan-artifact.js.map +1 -0
- package/dist/run-log.d.ts.map +1 -1
- package/dist/run-log.js +2 -9
- package/dist/run-log.js.map +1 -1
- package/dist/runstate-stats.d.ts +12 -3
- package/dist/runstate-stats.d.ts.map +1 -1
- package/dist/runstate-stats.js +33 -14
- package/dist/runstate-stats.js.map +1 -1
- package/dist/runstate.d.ts +87 -13
- package/dist/runstate.d.ts.map +1 -1
- package/dist/runstate.js +195 -25
- package/dist/runstate.js.map +1 -1
- package/dist/version.d.ts +10 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +23 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -2
|
@@ -12,211 +12,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
};
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
14
|
exports.registerRunstateCommand = registerRunstateCommand;
|
|
15
|
-
const node_child_process_1 = require("node:child_process");
|
|
16
15
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
17
16
|
const duration_1 = require("../duration");
|
|
17
|
+
const gh_1 = require("../gh");
|
|
18
18
|
const issue_selection_1 = require("../issue-selection");
|
|
19
19
|
const runstate_1 = require("../runstate");
|
|
20
20
|
const runstate_stats_1 = require("../runstate-stats");
|
|
21
21
|
const table_1 = require("../table");
|
|
22
|
-
/**
|
|
23
|
-
* Fail on stderr and exit 1, so a calling dossier can detect it.
|
|
24
|
-
*
|
|
25
|
-
* Each entry is one problem. An entry may span lines: the first gets the ❌, the rest are
|
|
26
|
-
* indented under it, which is how a cause line and its "Fix:" line stay visibly one item.
|
|
27
|
-
*/
|
|
28
|
-
function fail(lines) {
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
const [first, ...rest] = line.split('\n');
|
|
31
|
-
console.error(`❌ ${first}`);
|
|
32
|
-
for (const cont of rest)
|
|
33
|
-
console.error(` ${cont}`);
|
|
34
|
-
}
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Run a command, keeping WHY it failed.
|
|
39
|
-
*
|
|
40
|
-
* The whole point of this subcommand is that agents were silently getting things wrong,
|
|
41
|
-
* so a discarded stderr is the one thing we cannot afford: "gh is not installed", "gh is
|
|
42
|
-
* not logged in", and "issue 440 does not exist" need three different fixes and are
|
|
43
|
-
* indistinguishable without it.
|
|
44
|
-
*/
|
|
45
|
-
function exec(file, args) {
|
|
46
|
-
try {
|
|
47
|
-
const stdout = (0, node_child_process_1.execFileSync)(file, args, {
|
|
48
|
-
encoding: 'utf8',
|
|
49
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
|
-
});
|
|
51
|
-
return { ok: true, stdout: String(stdout).trim() };
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
const e = err;
|
|
55
|
-
const stderr = String(e?.stderr ?? '').trim() || String(e?.message ?? '').trim();
|
|
56
|
-
return {
|
|
57
|
-
ok: false,
|
|
58
|
-
error: {
|
|
59
|
-
notFound: e?.code === 'ENOENT',
|
|
60
|
-
status: typeof e?.status === 'number' ? e.status : null,
|
|
61
|
-
stderr,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/** Characters of command output kept in a top-level failure message. */
|
|
67
|
-
const SNIPPET_LENGTH = 300;
|
|
68
22
|
/** Characters kept in a `verify` warning, which is one line among several. */
|
|
69
23
|
const WARNING_SNIPPET_LENGTH = 120;
|
|
70
|
-
/** Collapse output to one bounded line so a huge or binary payload stays readable. */
|
|
71
|
-
function snippet(text, max = SNIPPET_LENGTH) {
|
|
72
|
-
const oneLine = text.replace(/\s+/g, ' ').trim();
|
|
73
|
-
if (oneLine === '')
|
|
74
|
-
return '(no output)';
|
|
75
|
-
return oneLine.length > max
|
|
76
|
-
? `${oneLine.slice(0, max)}… (${oneLine.length} characters total)`
|
|
77
|
-
: oneLine;
|
|
78
|
-
}
|
|
79
|
-
const GH_FAILURE_CASES = [
|
|
80
|
-
{
|
|
81
|
-
match: [
|
|
82
|
-
'gh auth login',
|
|
83
|
-
'not logged',
|
|
84
|
-
'authentication',
|
|
85
|
-
'unauthorized',
|
|
86
|
-
'http 401',
|
|
87
|
-
'bad credentials',
|
|
88
|
-
],
|
|
89
|
-
cause: () => 'gh is not authenticated.',
|
|
90
|
-
fix: `Fix: run 'gh auth login', confirm with 'gh auth status', then re-run.`,
|
|
91
|
-
},
|
|
92
|
-
{
|
|
93
|
-
match: ['could not resolve to', 'not found', 'http 404'],
|
|
94
|
-
cause: (where) => `GitHub could not find it in ${where}.`,
|
|
95
|
-
fix: `Fix: check the number is right and that the repository is the one you mean — pass --repo <owner/name> when running outside it.`,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
// Ahead of the 403 case on purpose: gh reports rate limiting AS an HTTP 403, so without
|
|
99
|
-
// this the operator is told to fix their permissions — which are fine. `stats --issues`
|
|
100
|
-
// can fire up to 200 sequential reads, which makes this a realistic failure.
|
|
101
|
-
match: ['rate limit', 'secondary rate', 'abuse detection', 'http 429'],
|
|
102
|
-
cause: () => 'GitHub rate-limited the request.',
|
|
103
|
-
fix: `Fix: wait for the window to reset ('gh api rate_limit' shows when), then re-run; narrow --issues to read fewer issues per run.`,
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
match: ['http 403', 'permission', 'write access', 'forbidden'],
|
|
107
|
-
cause: (where) => `GitHub refused the request — the authenticated account lacks access to ${where}.`,
|
|
108
|
-
fix: `Fix: check the account and its scopes with 'gh auth status'; reading an issue needs read access to the repository, and posting a milestone needs write.`,
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
match: ['dial tcp', 'no such host', 'timeout', 'connection refused', 'network is unreachable'],
|
|
112
|
-
cause: () => 'gh could not reach GitHub.',
|
|
113
|
-
fix: `Fix: check network/proxy access to github.com and re-run; this is transient, so a retry is usually enough.`,
|
|
114
|
-
},
|
|
115
|
-
];
|
|
116
|
-
/**
|
|
117
|
-
* `JSON.parse` for a `gh --json` payload: `null` instead of a throw, and the caller's
|
|
118
|
-
* declared shape instead of `any`. Every gh call here reads a `--json` payload, so all
|
|
119
|
-
* three call sites need the same "gh exited 0 but printed something that is not JSON"
|
|
120
|
-
* branch — they differ only in what they say about it. Fields stay `unknown` because gh's
|
|
121
|
-
* output is remote data — {@link asString} narrows each one at the point of use.
|
|
122
|
-
*/
|
|
123
|
-
function parseGhJson(raw) {
|
|
124
|
-
try {
|
|
125
|
-
return JSON.parse(raw);
|
|
126
|
-
}
|
|
127
|
-
catch {
|
|
128
|
-
return null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
/** A JSON field as a string, or `fallback` when gh omitted it or sent another type. */
|
|
132
|
-
function asString(value, fallback = '') {
|
|
133
|
-
return typeof value === 'string' ? value : fallback;
|
|
134
|
-
}
|
|
135
|
-
/**
|
|
136
|
-
* Turn a failed `gh` invocation into a message that names the cause and the fix.
|
|
137
|
-
*
|
|
138
|
-
* `action` is what we were trying to do, e.g. "Could not read issue #440".
|
|
139
|
-
*/
|
|
140
|
-
function ghFailure(action, err, repo) {
|
|
141
|
-
if (err.notFound) {
|
|
142
|
-
return [
|
|
143
|
-
`${action}: 'gh' is not installed, or is not on PATH.`,
|
|
144
|
-
`Fix: install the GitHub CLI (https://cli.github.com), then re-run. Check with: gh --version`,
|
|
145
|
-
].join('\n');
|
|
146
|
-
}
|
|
147
|
-
const hint = err.stderr.toLowerCase();
|
|
148
|
-
const where = repo ? `--repo ${repo}` : 'the current repository (no --repo was passed)';
|
|
149
|
-
const matched = GH_FAILURE_CASES.find((c) => c.match.some((needle) => hint.includes(needle)));
|
|
150
|
-
const cause = matched
|
|
151
|
-
? `${action}: ${matched.cause(where)}`
|
|
152
|
-
: `${action}: gh exited ${err.status === null ? 'abnormally' : `with status ${err.status}`}.`;
|
|
153
|
-
const fix = matched?.fix ?? `Fix: run the same gh command by hand to see the full error.`;
|
|
154
|
-
return [cause, fix, `gh said: ${snippet(err.stderr)}`].join('\n');
|
|
155
|
-
}
|
|
156
|
-
/** Reject a non-numeric `--issue` here rather than letting gh (or `mint`) fail obscurely. */
|
|
157
|
-
function requireIssueNumber(issue) {
|
|
158
|
-
if (!(0, runstate_1.isIssueNumber)(issue)) {
|
|
159
|
-
fail([
|
|
160
|
-
`Invalid --issue '${issue}' — expected a positive GitHub issue number.\nFix: pass the number only, e.g. --issue 440 (not a URL, a '#', or a branch name).`,
|
|
161
|
-
]);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* The `--issue`/`--repo` checks every issue-targeting subcommand runs before it starts.
|
|
166
|
-
* Kept together so a new subcommand cannot pick up one guard and miss the other.
|
|
167
|
-
*/
|
|
168
|
-
function requireIssueTarget(options) {
|
|
169
|
-
requireIssueNumber(options.issue);
|
|
170
|
-
requireRepoSlug(options.repo);
|
|
171
|
-
}
|
|
172
|
-
/** `--repo owner/name` appended to a gh invocation when the caller supplied one. */
|
|
173
|
-
function repoArgs(repo) {
|
|
174
|
-
return repo ? ['--repo', repo] : [];
|
|
175
|
-
}
|
|
176
|
-
/** `owner/name`, or `host/owner/name` for a GitHub Enterprise host. */
|
|
177
|
-
const REPO_SLUG_RE = /^(?:[A-Za-z0-9][A-Za-z0-9.-]*\/)?[A-Za-z0-9][A-Za-z0-9._-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
178
|
-
/** Reject a `--repo` that is not a repository slug before handing it to gh. */
|
|
179
|
-
function requireRepoSlug(repo) {
|
|
180
|
-
if (repo === undefined)
|
|
181
|
-
return;
|
|
182
|
-
if (!REPO_SLUG_RE.test(repo)) {
|
|
183
|
-
fail([
|
|
184
|
-
`Invalid --repo '${repo}' — expected owner/name.\nFix: pass the slug only, e.g. --repo imboard-ai/ai-dossier (not a URL, and not a shell expansion).`,
|
|
185
|
-
]);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
/** Whether `value` carries a control character, which has no place in a milestone value. */
|
|
189
|
-
function hasControlChar(value) {
|
|
190
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
191
|
-
const code = value.charCodeAt(i);
|
|
192
|
-
if (code < 0x20 || code === 0x7f)
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
return false;
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Whether a value read back OUT of a milestone comment may be passed to `git`/`gh`.
|
|
199
|
-
*
|
|
200
|
-
* These values are attacker-reachable: anyone who can comment on the issue can post a
|
|
201
|
-
* `<!-- runstate:v1 -->` body, so `branch=`, `worktree=` and `pr=` arrive from the network
|
|
202
|
-
* rather than from the caller. Every invocation here is `execFileSync` with an argument
|
|
203
|
-
* array and no shell, so there is no command injection — but a value that begins with `-`
|
|
204
|
-
* is still read as a FLAG rather than as data. `gh` (cobra) parses flags interspersed with
|
|
205
|
-
* positionals, so `pr=--repo=someone/else` or `pr=--web` would silently retarget, or add a
|
|
206
|
-
* side effect to, a subcommand documented as strictly read-only.
|
|
207
|
-
*/
|
|
208
|
-
function isSafeArg(value) {
|
|
209
|
-
return value !== '' && !value.startsWith('-') && !/\s/.test(value) && !hasControlChar(value);
|
|
210
|
-
}
|
|
211
|
-
/**
|
|
212
|
-
* Paths are only ever passed in value position (`git -C <path>`) or to `fs.statSync`, so
|
|
213
|
-
* they cannot be mistaken for a flag — but the protocol requires them absolute, and a
|
|
214
|
-
* relative path from a forged comment would resolve against whatever directory the agent
|
|
215
|
-
* happens to be in.
|
|
216
|
-
*/
|
|
217
|
-
function isSafePath(value) {
|
|
218
|
-
return value.startsWith('/') && !hasControlChar(value);
|
|
219
|
-
}
|
|
220
24
|
/** Parse `--kv k=v` occurrences into ordered pairs. */
|
|
221
25
|
function parseKvPairs(raw) {
|
|
222
26
|
const pairs = [];
|
|
@@ -240,43 +44,17 @@ function parseKvPairs(raw) {
|
|
|
240
44
|
* reports it.
|
|
241
45
|
*/
|
|
242
46
|
function tryFetchMilestones(issue, repo) {
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
return { ok: false, error:
|
|
246
|
-
|
|
247
|
-
const parsed = parseGhJson(res.stdout);
|
|
248
|
-
if (parsed === null) {
|
|
249
|
-
return {
|
|
250
|
-
ok: false,
|
|
251
|
-
error: [
|
|
252
|
-
`Could not read issue #${issue}: gh exited 0 but did not print JSON.`,
|
|
253
|
-
`Expected a {"comments":[…]} object from: gh issue view ${issue} --json comments${repo ? ` --repo ${repo}` : ''}`,
|
|
254
|
-
`Fix: run that command by hand — a gh older than 2.0 (no --json), or an interactive prompt landing on stdout, both look like this.`,
|
|
255
|
-
`gh printed: ${snippet(res.stdout)}`,
|
|
256
|
-
].join('\n'),
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
// A milestone-less issue and a shape we cannot read must not look the same: the first
|
|
260
|
-
// means "fresh run", the second means the tool is broken, and silently returning [] for
|
|
261
|
-
// both would make a resume start over and destroy work.
|
|
262
|
-
if (!Array.isArray(parsed?.comments)) {
|
|
263
|
-
return {
|
|
264
|
-
ok: false,
|
|
265
|
-
error: [
|
|
266
|
-
`Could not read issue #${issue}: gh's JSON has no "comments" array.`,
|
|
267
|
-
`Fix: confirm your gh supports 'gh issue view <n> --json comments' (gh --version); if it does, re-run — this is not an issue with no comments, it is an unreadable response.`,
|
|
268
|
-
`gh printed: ${snippet(res.stdout)}`,
|
|
269
|
-
].join('\n'),
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
const bodies = parsed.comments.map((c) => typeof c?.body === 'string' ? c.body : '');
|
|
47
|
+
const result = (0, gh_1.tryFetchComments)(issue, repo);
|
|
48
|
+
if (!result.ok)
|
|
49
|
+
return { ok: false, error: result.error };
|
|
50
|
+
const bodies = result.comments.map((c) => (typeof c?.body === 'string' ? c.body : ''));
|
|
273
51
|
return { ok: true, milestones: (0, runstate_1.parseMilestones)(bodies) };
|
|
274
52
|
}
|
|
275
53
|
/** Fetch every runstate milestone on an issue, oldest first, or exit 1 explaining why. */
|
|
276
54
|
function fetchMilestones(issue, repo) {
|
|
277
55
|
const result = tryFetchMilestones(issue, repo);
|
|
278
56
|
if (!result.ok)
|
|
279
|
-
fail([result.error]);
|
|
57
|
+
(0, gh_1.fail)([result.error]);
|
|
280
58
|
return result.milestones;
|
|
281
59
|
}
|
|
282
60
|
/** `git ls-remote --exit-code` exits 2 for "no such ref" — an answer, not a fault. */
|
|
@@ -298,18 +76,18 @@ const RUN_INCOMPLETE = 'treating the run as not yet complete';
|
|
|
298
76
|
* instead).
|
|
299
77
|
*/
|
|
300
78
|
function probeFailure(tool, subject, err, consequence) {
|
|
301
|
-
return `could not ${subject} (${tool} exited ${err.status ?? 'abnormally'}: ${snippet(err.stderr, WARNING_SNIPPET_LENGTH)}) — ${consequence}`;
|
|
79
|
+
return `could not ${subject} (${tool} exited ${err.status ?? 'abnormally'}: ${(0, gh_1.snippet)(err.stderr, WARNING_SNIPPET_LENGTH)}) — ${consequence}`;
|
|
302
80
|
}
|
|
303
81
|
/** The warning for a gh call that exited 0 but did not print JSON. */
|
|
304
82
|
function nonJsonFailure(subject, stdout, consequence) {
|
|
305
|
-
return `gh returned non-JSON for ${subject} (${snippet(stdout, WARNING_SNIPPET_LENGTH)}) — ${consequence}`;
|
|
83
|
+
return `gh returned non-JSON for ${subject} (${(0, gh_1.snippet)(stdout, WARNING_SNIPPET_LENGTH)}) — ${consequence}`;
|
|
306
84
|
}
|
|
307
85
|
function probeBranchOnRemote(branch, warn) {
|
|
308
|
-
if (!isSafeArg(branch)) {
|
|
86
|
+
if (!(0, gh_1.isSafeArg)(branch)) {
|
|
309
87
|
warn(`milestone branch '${branch}' is not a usable branch name — refusing to pass it to git, and ${TREATED_MISSING}`);
|
|
310
88
|
return false;
|
|
311
89
|
}
|
|
312
|
-
const res = exec('git', ['ls-remote', '--exit-code', 'origin', branch]);
|
|
90
|
+
const res = (0, gh_1.exec)('git', ['ls-remote', '--exit-code', 'origin', branch]);
|
|
313
91
|
if (res.ok)
|
|
314
92
|
return true;
|
|
315
93
|
if (res.error.notFound) {
|
|
@@ -326,7 +104,7 @@ function probeBranchOnRemote(branch, warn) {
|
|
|
326
104
|
* path came from (`label`) and the kind of entry they expect.
|
|
327
105
|
*/
|
|
328
106
|
function probePathExists(path, label, kind, warn) {
|
|
329
|
-
if (!isSafePath(path)) {
|
|
107
|
+
if (!(0, gh_1.isSafePath)(path)) {
|
|
330
108
|
warn(`milestone ${label} '${path}' is not an absolute path — ${TREATED_MISSING}`);
|
|
331
109
|
return false;
|
|
332
110
|
}
|
|
@@ -339,11 +117,11 @@ function probePathExists(path, label, kind, warn) {
|
|
|
339
117
|
}
|
|
340
118
|
}
|
|
341
119
|
function probeHeadOf(worktree, warn) {
|
|
342
|
-
if (!isSafePath(worktree)) {
|
|
120
|
+
if (!(0, gh_1.isSafePath)(worktree)) {
|
|
343
121
|
warn(`milestone worktree '${worktree}' is not an absolute path — refusing to run git in it, and ${IMPLEMENT_UNVERIFIED}`);
|
|
344
122
|
return null;
|
|
345
123
|
}
|
|
346
|
-
const res = exec('git', ['-C', worktree, 'rev-parse', '--short', 'HEAD']);
|
|
124
|
+
const res = (0, gh_1.exec)('git', ['-C', worktree, 'rev-parse', '--short', 'HEAD']);
|
|
347
125
|
if (res.ok)
|
|
348
126
|
return res.stdout;
|
|
349
127
|
// The worktree directory already passed dirExists, so a failure here is surprising.
|
|
@@ -351,45 +129,45 @@ function probeHeadOf(worktree, warn) {
|
|
|
351
129
|
return null;
|
|
352
130
|
}
|
|
353
131
|
function probePrState(pr, repo, warn) {
|
|
354
|
-
if (!isSafeArg(pr)) {
|
|
132
|
+
if (!(0, gh_1.isSafeArg)(pr)) {
|
|
355
133
|
warn(`milestone pr '${pr}' is not a usable PR reference — refusing to pass it to gh, and ${SHIP_UNVERIFIED}`);
|
|
356
134
|
return null;
|
|
357
135
|
}
|
|
358
|
-
const res = exec('gh', [
|
|
136
|
+
const res = (0, gh_1.exec)('gh', [
|
|
359
137
|
'pr',
|
|
360
138
|
'view',
|
|
361
139
|
pr,
|
|
362
140
|
'--json',
|
|
363
141
|
'state,mergedAt,mergeable',
|
|
364
|
-
...repoArgs(repo),
|
|
142
|
+
...(0, gh_1.repoArgs)(repo),
|
|
365
143
|
]);
|
|
366
144
|
if (!res.ok) {
|
|
367
145
|
warn(probeFailure('gh', `read PR ${pr}`, res.error, SHIP_UNVERIFIED));
|
|
368
146
|
return null;
|
|
369
147
|
}
|
|
370
|
-
const data = parseGhJson(res.stdout);
|
|
148
|
+
const data = (0, gh_1.parseGhJson)(res.stdout);
|
|
371
149
|
if (data === null) {
|
|
372
150
|
warn(nonJsonFailure(`PR ${pr}`, res.stdout, SHIP_UNVERIFIED));
|
|
373
151
|
return null;
|
|
374
152
|
}
|
|
375
153
|
return {
|
|
376
|
-
state: asString(data.state),
|
|
154
|
+
state: (0, gh_1.asString)(data.state),
|
|
377
155
|
mergedAt: typeof data.mergedAt === 'string' ? data.mergedAt : null,
|
|
378
|
-
mergeable: asString(data.mergeable),
|
|
156
|
+
mergeable: (0, gh_1.asString)(data.mergeable),
|
|
379
157
|
};
|
|
380
158
|
}
|
|
381
159
|
function probeIssueClosed(issue, repo, warn) {
|
|
382
|
-
const res = exec('gh', ['issue', 'view', issue, '--json', 'state', ...repoArgs(repo)]);
|
|
160
|
+
const res = (0, gh_1.exec)('gh', ['issue', 'view', issue, '--json', 'state', ...(0, gh_1.repoArgs)(repo)]);
|
|
383
161
|
if (!res.ok) {
|
|
384
162
|
warn(probeFailure('gh', `read the state of issue #${issue}`, res.error, RUN_INCOMPLETE));
|
|
385
163
|
return false;
|
|
386
164
|
}
|
|
387
|
-
const data = parseGhJson(res.stdout);
|
|
165
|
+
const data = (0, gh_1.parseGhJson)(res.stdout);
|
|
388
166
|
if (data === null) {
|
|
389
167
|
warn(nonJsonFailure(`the state of issue #${issue}`, res.stdout, RUN_INCOMPLETE));
|
|
390
168
|
return false;
|
|
391
169
|
}
|
|
392
|
-
return asString(data.state) === GH_STATE_CLOSED;
|
|
170
|
+
return (0, gh_1.asString)(data.state) === GH_STATE_CLOSED;
|
|
393
171
|
}
|
|
394
172
|
/**
|
|
395
173
|
* The real-world probe backing `runstate verify`. All reads — never needs push access.
|
|
@@ -412,39 +190,21 @@ function makeProbe(issue, repo, warnings = []) {
|
|
|
412
190
|
issueClosed: () => probeIssueClosed(issue, repo, warn),
|
|
413
191
|
};
|
|
414
192
|
}
|
|
415
|
-
/** `--dry-run
|
|
416
|
-
|
|
417
|
-
if (json) {
|
|
418
|
-
console.log(JSON.stringify({ posted: false, dryRun: true, body }, null, 2));
|
|
419
|
-
}
|
|
420
|
-
else {
|
|
421
|
-
process.stdout.write(body);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
/** Comment the built body onto the issue and report where it landed. */
|
|
193
|
+
/** `--dry-run` and posting are shared with the plan protocol — see `../gh`. */
|
|
194
|
+
/** Comment the built milestone body onto the issue and report where it landed. */
|
|
425
195
|
function postMilestone(body, options) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
options.issue,
|
|
430
|
-
'--body',
|
|
196
|
+
(0, gh_1.postIssueComment)({
|
|
197
|
+
issue: options.issue,
|
|
198
|
+
repo: options.repo,
|
|
431
199
|
body,
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
]);
|
|
441
|
-
}
|
|
442
|
-
if (options.json) {
|
|
443
|
-
console.log(JSON.stringify({ posted: true, url: res.stdout, body }, null, 2));
|
|
444
|
-
}
|
|
445
|
-
else {
|
|
446
|
-
console.log(`✅ ${options.phase} ${options.status} → ${res.stdout}`);
|
|
447
|
-
}
|
|
200
|
+
noun: 'milestone',
|
|
201
|
+
action: `Failed to post the ${options.phase}/${options.status} milestone to issue #${options.issue}`,
|
|
202
|
+
json: options.json,
|
|
203
|
+
// The milestone is the only durable record of the phase, so the body is handed back
|
|
204
|
+
// in the JSON result: re-running the phase is far more expensive than a retry.
|
|
205
|
+
jsonExtras: { body },
|
|
206
|
+
successLine: (url) => `✅ ${options.phase} ${options.status} → ${url}`,
|
|
207
|
+
});
|
|
448
208
|
}
|
|
449
209
|
/**
|
|
450
210
|
* `post` refuses an over-long body itself: GitHub answers a too-large comment with an
|
|
@@ -455,7 +215,7 @@ function requirePostableBody(body, pairs) {
|
|
|
455
215
|
if (body.length <= runstate_1.MAX_BODY_LENGTH)
|
|
456
216
|
return;
|
|
457
217
|
const biggest = [...pairs].sort((a, b) => b[1].length - a[1].length)[0];
|
|
458
|
-
fail([
|
|
218
|
+
(0, gh_1.fail)([
|
|
459
219
|
[
|
|
460
220
|
`Milestone is ${body.length} characters — GitHub rejects an issue comment over ${runstate_1.MAX_BODY_LENGTH}.`,
|
|
461
221
|
biggest
|
|
@@ -471,16 +231,16 @@ function registerPostSubcommand(cmd) {
|
|
|
471
231
|
.command('post')
|
|
472
232
|
.description('Build and post a runstate milestone comment (validates before posting)')
|
|
473
233
|
.requiredOption('--issue <number>', 'GitHub issue number')
|
|
474
|
-
.requiredOption('--phase <phase>',
|
|
234
|
+
.requiredOption('--phase <phase>', `classify, ${[...runstate_1.PHASES].join(', ')}, or a batch phase (${runstate_1.BATCH_PHASES.join(', ')})`)
|
|
475
235
|
.requiredOption('--status <status>', 'done, partial, blocked, or awaiting-merge')
|
|
476
|
-
.requiredOption('--run <id>', 'Run id
|
|
236
|
+
.requiredOption('--run <id>', 'Run id (r-<issue>-<hex>) — mint one with: runstate mint; full-cycle runs mint it at the gate phase')
|
|
477
237
|
.option('--kv <key=value...>', 'Phase-specific key=value pair (repeatable)')
|
|
478
238
|
.option('--next <phase>', 'Override the computed next= value')
|
|
479
239
|
.option('--repo <owner/name>', 'Target repository (defaults to the current one)')
|
|
480
240
|
.option('--dry-run', 'Print the comment body without posting it')
|
|
481
241
|
.option('--json', 'Output the result as JSON')
|
|
482
242
|
.action((options) => {
|
|
483
|
-
requireIssueTarget(options);
|
|
243
|
+
(0, gh_1.requireIssueTarget)(options);
|
|
484
244
|
const { pairs, errors: kvErrors } = parseKvPairs(options.kv ?? []);
|
|
485
245
|
const input = {
|
|
486
246
|
phase: options.phase,
|
|
@@ -491,11 +251,11 @@ function registerPostSubcommand(cmd) {
|
|
|
491
251
|
};
|
|
492
252
|
const errors = [...kvErrors, ...(0, runstate_1.validateMilestone)(input)];
|
|
493
253
|
if (errors.length > 0)
|
|
494
|
-
fail(errors);
|
|
254
|
+
(0, gh_1.fail)(errors);
|
|
495
255
|
const body = (0, runstate_1.buildMilestone)(input);
|
|
496
256
|
requirePostableBody(body, pairs);
|
|
497
257
|
if (options.dryRun) {
|
|
498
|
-
printDryRun(body, options.json);
|
|
258
|
+
(0, gh_1.printDryRun)(body, options.json);
|
|
499
259
|
return;
|
|
500
260
|
}
|
|
501
261
|
postMilestone(body, options);
|
|
@@ -510,7 +270,7 @@ function registerLastSubcommand(cmd) {
|
|
|
510
270
|
.option('--repo <owner/name>', 'Target repository (defaults to the current one)')
|
|
511
271
|
.option('--json', 'Output the parsed milestone as JSON')
|
|
512
272
|
.action((options) => {
|
|
513
|
-
requireIssueTarget(options);
|
|
273
|
+
(0, gh_1.requireIssueTarget)(options);
|
|
514
274
|
const milestones = fetchMilestones(options.issue, options.repo);
|
|
515
275
|
if (milestones.length === 0) {
|
|
516
276
|
if (options.json) {
|
|
@@ -540,7 +300,7 @@ function registerVerifySubcommand(cmd) {
|
|
|
540
300
|
.option('--repo <owner/name>', 'Target repository (defaults to the current one)')
|
|
541
301
|
.option('--json', 'Output the result as JSON')
|
|
542
302
|
.action((options) => {
|
|
543
|
-
requireIssueTarget(options);
|
|
303
|
+
(0, gh_1.requireIssueTarget)(options);
|
|
544
304
|
const milestones = fetchMilestones(options.issue, options.repo);
|
|
545
305
|
const warnings = [];
|
|
546
306
|
const result = (0, runstate_1.computeResume)(milestones, makeProbe(options.issue, options.repo, warnings));
|
|
@@ -550,6 +310,7 @@ function registerVerifySubcommand(cmd) {
|
|
|
550
310
|
run_id: result.run_id,
|
|
551
311
|
verified: result.verified,
|
|
552
312
|
resume_context: result.resume_context,
|
|
313
|
+
...(result.slot_trail ? { slot_trail: true } : {}),
|
|
553
314
|
...(result.hard_block ? { hard_block: result.hard_block } : {}),
|
|
554
315
|
...(result.note ? { note: result.note } : {}),
|
|
555
316
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
@@ -559,6 +320,8 @@ function registerVerifySubcommand(cmd) {
|
|
|
559
320
|
console.log(`resume_from=${result.resume_from}`);
|
|
560
321
|
console.log(`run_id=${result.run_id ?? 'none'}`);
|
|
561
322
|
console.log(`verified=${result.verified.length > 0 ? result.verified.join(',') : 'none'}`);
|
|
323
|
+
if (result.slot_trail)
|
|
324
|
+
console.log('slot_trail=present');
|
|
562
325
|
if (result.hard_block)
|
|
563
326
|
console.log(`hard_block=${result.hard_block}`);
|
|
564
327
|
if (result.note)
|
|
@@ -583,27 +346,27 @@ function registerVerifySubcommand(cmd) {
|
|
|
583
346
|
*/
|
|
584
347
|
function resolveStatsIssues(options) {
|
|
585
348
|
if (options.issue !== undefined && options.issues !== undefined) {
|
|
586
|
-
fail([
|
|
349
|
+
(0, gh_1.fail)([
|
|
587
350
|
`Pass --issue or --issues, not both.\nFix: --issue ${options.issue} for one issue, or --issues ${options.issues} for a set.`,
|
|
588
351
|
]);
|
|
589
352
|
}
|
|
590
353
|
if (options.issue !== undefined) {
|
|
591
|
-
requireIssueTarget({ issue: options.issue, repo: options.repo });
|
|
354
|
+
(0, gh_1.requireIssueTarget)({ issue: options.issue, repo: options.repo });
|
|
592
355
|
return [Number(options.issue)];
|
|
593
356
|
}
|
|
594
357
|
if (options.issues === undefined) {
|
|
595
|
-
fail([
|
|
358
|
+
(0, gh_1.fail)([
|
|
596
359
|
`Missing --issue or --issues.\nFix: 'runstate stats --issue 451', or a set: 'runstate stats --issues 440,448,451' (ranges too: 440..451).`,
|
|
597
360
|
]);
|
|
598
361
|
}
|
|
599
|
-
requireRepoSlug(options.repo);
|
|
362
|
+
(0, gh_1.requireRepoSlug)(options.repo);
|
|
600
363
|
try {
|
|
601
364
|
return (0, issue_selection_1.parseIssueSelection)(options.issues);
|
|
602
365
|
}
|
|
603
366
|
catch (err) {
|
|
604
367
|
// Selection errors already carry their own `Fix:` line — appending a generic one told
|
|
605
368
|
// the operator to do the very thing that just failed ("pass a range" on a cap error).
|
|
606
|
-
return fail([err instanceof Error ? err.message : String(err)]);
|
|
369
|
+
return (0, gh_1.fail)([err instanceof Error ? err.message : String(err)]);
|
|
607
370
|
}
|
|
608
371
|
}
|
|
609
372
|
/** Column layout of the per-run phase table. */
|
|
@@ -752,8 +515,9 @@ function readTrails(issues, repo) {
|
|
|
752
515
|
/**
|
|
753
516
|
* `runstate stats` — per-phase durations derived from a trail's `at=` stamps.
|
|
754
517
|
*
|
|
755
|
-
* Read-only by construction: the only
|
|
756
|
-
* {@link fetchMilestones}
|
|
518
|
+
* Read-only by construction: the only subprocesses it can reach are the `gh issue view`
|
|
519
|
+
* inside {@link fetchMilestones} and the verify probes, which apply the shared gh/git
|
|
520
|
+
* failure taxonomy from `src/gh.ts`.
|
|
757
521
|
*/
|
|
758
522
|
function registerStatsSubcommand(cmd) {
|
|
759
523
|
cmd
|
|
@@ -769,7 +533,7 @@ function registerStatsSubcommand(cmd) {
|
|
|
769
533
|
// Every issue unreadable is a genuine failure, not a degraded read — there is no
|
|
770
534
|
// report to hand back, so say why rather than printing an empty one and exiting 0.
|
|
771
535
|
if (trails.length === 0 && failed.length > 0) {
|
|
772
|
-
fail(failed.map((f) => f.error));
|
|
536
|
+
(0, gh_1.fail)(failed.map((f) => f.error));
|
|
773
537
|
}
|
|
774
538
|
const report = (0, runstate_stats_1.buildStatsReport)({ trails, failed, repo: options.repo });
|
|
775
539
|
if (options.json) {
|
|
@@ -801,7 +565,7 @@ function registerMintSubcommand(cmd) {
|
|
|
801
565
|
.action((options) => {
|
|
802
566
|
// Without this, `mint --issue not-a-number` happily prints `r-not-a-number-ab56`
|
|
803
567
|
// and the mistake only surfaces one phase later as "Invalid run id" from `post`.
|
|
804
|
-
requireIssueNumber(options.issue);
|
|
568
|
+
(0, gh_1.requireIssueNumber)(options.issue);
|
|
805
569
|
console.log((0, runstate_1.mintRunId)(options.issue));
|
|
806
570
|
});
|
|
807
571
|
}
|