@credda/cli 0.1.5 → 1.0.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.
@@ -0,0 +1,1192 @@
1
+ /**
2
+ * The command table. This is the single source of truth for what the CLI
3
+ * accepts: the parser validates against it and `--help` is rendered from it,
4
+ * so documented flags and accepted flags cannot drift apart.
5
+ *
6
+ * ## What this CLI claims, and what it does not
7
+ *
8
+ * A run prepares an environment, reproduces the reported failure, captures its
9
+ * signature as evidence, diagnoses a cause where the evidence supports one,
10
+ * writes the patch and proves it with a test that fails before and passes
11
+ * after. It ends at a diff for a person to review. **It proposes and never
12
+ * merges**, and nothing here takes write access to a repository to do it.
13
+ *
14
+ * **How far a run goes is decided by the provider, not by a flag.** The fix
15
+ * stage is on the investigation path (ADR 0019) and is entered only when the
16
+ * configured provider can author code. Under `CREDDA_PROVIDER=auto` with no
17
+ * key the engine degrades to rule-based reasoning and stops after the
18
+ * diagnosis, because a rule-based patch is worse than none. `PATCH_PATH_STATES`
19
+ * in `packages/shared/src/states.ts` carries that gate and the evidence behind
20
+ * it. There is deliberately no switch here that overrides it: a flag would put
21
+ * an unevidenced claim one environment variable away from a customer.
22
+ *
23
+ * Two consequences this file carries: the old command names keep working and
24
+ * stop describing an output (see {@link INVESTIGATE}), and exit code 3 is
25
+ * reserved rather than reused (see {@link RESERVED_EXIT_CODES}).
26
+ */
27
+ import { GLOBAL_FLAGS, own } from './args.js';
28
+ /**
29
+ * Exit codes. Documented here, in `--help`, and in docs/cli.md.
30
+ *
31
+ * An investigation that abstains is a success: NO_CHANGE_REQUIRED and
32
+ * INCONCLUSIVE both exit 0. Every non-zero code an investigation can return is
33
+ * a genuine failure.
34
+ *
35
+ * `credda triage` is the one command with two successful codes, and the second of
36
+ * them is non-zero. It is not an investigation and has no Outcome: it executes
37
+ * nothing, so "did it reach a verdict" is not a question about it. What a caller
38
+ * needs from it is which of two correct answers it gave, and 0 is the silent
39
+ * one. See {@link EXIT.COMMENT_READY} for why that way round.
40
+ *
41
+ * The report record (ADR 0012) adds no code, and the omission is a decision. Its
42
+ * confidence class is the obvious candidate -- something like "8:
43
+ * NOT_ESTABLISHED", the next free number -- and it is the wrong thing to encode.
44
+ * `NOT_ESTABLISHED` is the *correct* class for an abstention, which is the
45
+ * outcome this table already insists is a success: code 0 covers it for an
46
+ * investigation and for triage alike, and it is the only one of these eight
47
+ * codes that does. Giving it a non-zero code
48
+ * would make every CI that treats non-zero as failure fail on exactly the runs
49
+ * Credda gets right, and would create a second, contradictory answer to a
50
+ * question `outcome` already answers. The confidence class is a property of the
51
+ * record, readable with `credda report <id> --json`, and the exit code stays a
52
+ * statement about whether the run reached a verdict.
53
+ *
54
+ * ## Code 3 was held open, and is returned again
55
+ *
56
+ * `PATCH_REJECTED` is the exit code of a run that produced a change and then
57
+ * threw it away. ADR 0015 stopped anything from producing changes and held the
58
+ * code open rather than renumbering the table; ADR 0019 put the fix stage back
59
+ * on the path, so runs return 3 again and it means exactly what the old scripts
60
+ * were written against. {@link RESERVED_EXIT_CODES} is empty as a result, and
61
+ * kept, because a test reads it and a code moving between reserved and returned
62
+ * should move its reason with it.
63
+ */
64
+ export const EXIT = {
65
+ /** Success, including NO_CHANGE_REQUIRED and INCONCLUSIVE. */
66
+ SUCCESS: 0,
67
+ /** Credda itself failed: internal error, unreadable database, crash. */
68
+ INTERNAL_ERROR: 1,
69
+ /** The command line or its inputs were wrong. Nothing was run. */
70
+ USAGE_ERROR: 2,
71
+ /** A change was produced and independent verification rejected it. */
72
+ PATCH_REJECTED: 3,
73
+ /** The run was cancelled (Ctrl-C). */
74
+ CANCELLED: 4,
75
+ /**
76
+ * NO_RUNNABLE_CHECK: nothing runnable could be derived from the report, so
77
+ * nothing was executed against the repository. Not a success and not a crash.
78
+ * See `exitCodeFor` for why it is neither 0 nor 1.
79
+ */
80
+ NO_RUNNABLE_CHECK: 5,
81
+ /**
82
+ * `credda triage` produced a comment, and it is on stdout. Nothing failed.
83
+ *
84
+ * ## Why the comment is the non-zero side and silence is 0
85
+ *
86
+ * Silence is the common case, not the exceptional one: half of real inbound
87
+ * produces nothing worth saying (`bench/harvest`, 50.6% of 729 issues). A
88
+ * code that turned every second opened issue into a red job would be switched
89
+ * off inside a week, and this repository's standing rule is already that
90
+ * abstention is a success. So silence exits 0, and it is 0 for the same
91
+ * reason NO_CHANGE_REQUIRED is.
92
+ *
93
+ * That leaves the comment needing a code of its own, because "post this" and
94
+ * "post nothing" are the two answers a caller has to tell apart and stdout
95
+ * being empty is a weaker signal than a number. Giving it a non-zero one is
96
+ * deliberate rather than reluctant: **every way of misreading this code then
97
+ * fails towards not posting.** A shell under `set -e` stops before the
98
+ * posting step; a caller that ignores the code and pipes stdout gets an empty
99
+ * document on the silent path; a caller that tests for 0 posts only silence,
100
+ * which posts nothing. The failure this product cannot afford is a
101
+ * confidently wrong refusal on a stranger's issue -- the dominant rule is
102
+ * still wrong 8.7% of the times it fires (Credda-io/core#7) -- so the
103
+ * direction of every mistake here has to be silence.
104
+ *
105
+ * 6 rather than reusing 5: NO_RUNNABLE_CHECK is a statement that nothing was
106
+ * executed against the repository, which is true of *every* triage run by
107
+ * design, so the two would stop meaning different things.
108
+ */
109
+ COMMENT_READY: 6,
110
+ /**
111
+ * `credda cancel` reached a run that is still executing and asked it to stop.
112
+ * The request is delivered; the run has not stopped yet.
113
+ *
114
+ * ## Why this is not 0, and not 4
115
+ *
116
+ * `apps/api/src/routes/investigations.ts` answers the same question with two
117
+ * different HTTP statuses -- 200 CANCELLED when the run is genuinely over, 202
118
+ * CANCELLATION_REQUESTED when a process is still inside it holding a sandbox
119
+ * and a model budget. A shell has no status line to read. It has this number,
120
+ * and if both answers were 0 then `credda cancel $id && echo stopped` would
121
+ * print "stopped" over a container that is still running and still spending.
122
+ * That is the one false claim this whole route was written to avoid, so the
123
+ * two answers get two codes.
124
+ *
125
+ * 4 is the run's own code, returned by `credda investigate` when the run it
126
+ * was executing was cancelled. It is a statement that a run ended. This is a
127
+ * statement that one was asked to, made by a different process that cannot
128
+ * see whether it did. Reusing 4 would collapse exactly the distinction.
129
+ *
130
+ * Every way of misreading 7 fails towards waiting rather than towards
131
+ * assuming: `set -e` stops, a test for 0 does not proceed. `credda events
132
+ * <id> --follow` is how a caller learns the run actually ended.
133
+ */
134
+ CANCELLATION_REQUESTED: 7,
135
+ };
136
+ /**
137
+ * Codes no run of this version can return, and the reason each is held open.
138
+ *
139
+ * A test reads this, so a code cannot quietly move between "reserved" and
140
+ * "returned" without the reason moving with it.
141
+ */
142
+ export const RESERVED_EXIT_CODES = {};
143
+ export const EXIT_CODE_HELP = [
144
+ ' 0 Credda reached the answer it was asked for and nothing failed. For a run',
145
+ ' that executed something against this repository, that means its finding is',
146
+ ' on record: REPRODUCED_AND_DIAGNOSED, REPRODUCED_NOT_DIAGNOSED,',
147
+ ' NO_CHANGE_REQUIRED or INCONCLUSIVE. Abstention is a success here, and',
148
+ ' Credda declining to allege a defect it did not demonstrate is a feature.',
149
+ ' For `credda triage`, which executes nothing at all, 0 means it correctly had',
150
+ ' nothing to say -- see 6.',
151
+ ' 1 Internal error. Credda failed; the investigation did not reach a verdict.',
152
+ ' 2 Usage error. A bad flag, a missing value, or an input that could not be read.',
153
+ ' 3 PATCH_REJECTED. Credda wrote a change and independent verification rejected it,',
154
+ ' so it was discarded and the workspace restored. No change is on offer. The',
155
+ ' diagnosis still stands and is worth reading.',
156
+ ' 4 Cancelled by the operator (Ctrl-C).',
157
+ ' 5 NO_RUNNABLE_CHECK. Nothing runnable could be derived from the report, so',
158
+ ' nothing was executed. This is a fact about the report, not about your code,',
159
+ ' and it is separated from 0 so `credda ... && deploy` cannot read it as a pass.',
160
+ ' 6 COMMENT_READY, from `credda triage` only: there is a comment to post and it is',
161
+ ' on stdout. Nothing failed. Triage exits 0 when it correctly has nothing to',
162
+ ' say, which is about half of real issues, so 0 there is silence and 6 is the',
163
+ ' one that means speak.',
164
+ ' 7 CANCELLATION_REQUESTED, from `credda cancel` only: a run is still executing',
165
+ ' and has been asked to stop. It has NOT stopped. The process tears its sandbox',
166
+ ' down and writes its own terminal state when it reaches its next checkpoint;',
167
+ ' follow it with `credda events <id> --follow`. 0 from `credda cancel` means',
168
+ ' nothing is running, which is a different and stronger claim.',
169
+ ];
170
+ /**
171
+ * `investigate`, with `resolve` and `fix` as permanent aliases for it.
172
+ *
173
+ * ## Why the name moved twice
174
+ *
175
+ * `fix` named a stage. `resolve` named the whole workflow. Both were accurate
176
+ * about the destination and wrong about the guarantee: a run reproduces,
177
+ * diagnoses, and then patches and verifies only where the evidence and the
178
+ * provider let it, and a name that promises a fix promises an outcome no run
179
+ * can commit to in advance. `investigate` names what every run does; how far
180
+ * it gets is reported rather than asserted by the verb.
181
+ *
182
+ * ## Why both old names still work
183
+ *
184
+ * Neither is deprecated and neither will be removed. `fix` and `resolve` appear
185
+ * in docs/cli.md, README.md, docs/setup.md and in bench/external's harness
186
+ * invocation, and they are in people's fingers. A command name that silently
187
+ * stops working is a worse failure than an inconsistent one. All three
188
+ * spellings parse identically, print their own name in usage and errors, and
189
+ * dispatch through {@link canonicalCommand}.
190
+ *
191
+ * What the old names must NOT do is carry their old promise. `credda fix --help`
192
+ * prints this command's summary, which says what the run produces, so nobody
193
+ * reads the name as a description of the output.
194
+ */
195
+ const INVESTIGATE = {
196
+ name: 'investigate',
197
+ summary: 'Reproduce a reported failure, diagnose it, and fix it where the provider allows',
198
+ args: '<repo-path> <description | @file | -> [options]',
199
+ flags: {
200
+ sandbox: {
201
+ kind: 'string',
202
+ choices: ['local', 'native', 'docker'],
203
+ valueName: '<local|native|docker>',
204
+ description: 'Execution plane. local and native both run repository code directly on\n' +
205
+ ' this host and only a local credda invocation may select them;\n' +
206
+ ' docker isolates it. Credda never falls back silently',
207
+ defaultNote: 'local',
208
+ },
209
+ provider: {
210
+ kind: 'string',
211
+ choices: ['auto', 'heuristic', 'openai-compatible'],
212
+ valueName: '<auto|heuristic|openai-compatible>',
213
+ description: 'auto uses ANTHROPIC_API_KEY then CREDDA_OPENAI_API_KEY when present;\n' +
214
+ ' heuristic forces rule-based reasoning; openai-compatible\n' +
215
+ ' targets an OpenAI-compatible endpoint (NVIDIA NIM by default)',
216
+ defaultNote: 'auto',
217
+ },
218
+ 'budget-minutes': {
219
+ kind: 'number',
220
+ valueName: '<n>',
221
+ description: 'Wall-clock budget for the investigation',
222
+ defaultNote: '20',
223
+ },
224
+ 'max-turns': {
225
+ kind: 'number',
226
+ valueName: '<n>',
227
+ description: 'Maximum model calls across all agent roles',
228
+ defaultNote: '120',
229
+ },
230
+ out: {
231
+ kind: 'string',
232
+ valueName: '<file>',
233
+ description: 'Also write the machine-readable result of this run to <file> as JSON',
234
+ },
235
+ /*
236
+ * Where the report came from, recorded on the run.
237
+ *
238
+ * The engine API's create route has accepted `issueRef` since it existed;
239
+ * a terminal had no way to set it, so every locally started run recorded
240
+ * nothing about its own origin. That was tolerable while every local run
241
+ * was a person pasting a sentence they had written. It stopped being
242
+ * tolerable when `credda discover` began writing reports, because a run
243
+ * started from a report Credda wrote itself is a different claim from one
244
+ * a person filed and a reader has to be able to tell which.
245
+ *
246
+ * It is a general flag and not a discovery flag on purpose: nothing
247
+ * downstream branches on the value, and a person pasting a tracker URL
248
+ * here is using it exactly as intended. ADR 0024 is explicit that
249
+ * discovery adds no stage, no terminal and no refusal, so its only trace
250
+ * in the pipeline is this string on the record.
251
+ */
252
+ ref: {
253
+ kind: 'string',
254
+ valueName: '<ref>',
255
+ description: 'Record where this report came from -- an issue reference, a URL, or the\n' +
256
+ ' ref `credda discover` prints. Stored on the run and shown by\n' +
257
+ ' `credda report`. Nothing branches on it',
258
+ },
259
+ },
260
+ details: [
261
+ 'What a run does, and where it stops:',
262
+ ' prepare an environment, reproduce the reported failure, capture its',
263
+ ' failure signature, and diagnose a cause where the evidence supports one.',
264
+ ' With a model-backed provider it then attempts a fix and verifies it, and',
265
+ ' it reports all of it. Every stage runs in a disposable copy, so this',
266
+ ' command changes nothing in your working tree.',
267
+ '',
268
+ 'Description sources:',
269
+ ' "text" a short description given inline',
270
+ ' @file read the report from a file (shells truncate multi-line arguments)',
271
+ ' - read the report from stdin',
272
+ '',
273
+ 'Recording where the report came from:',
274
+ ' --ref <ref> is written to the run and printed by `credda report`. Use it',
275
+ ' for the issue this came from, or paste the ref that',
276
+ ' `credda discover` prints beside a candidate it wrote.',
277
+ '',
278
+ 'Configuration precedence, highest first:',
279
+ ' 1. the CLI flag on this command line',
280
+ ' 2. the environment variable (CREDDA_SANDBOX, CREDDA_PROVIDER)',
281
+ ' 3. credda.config.json, searched upward from the working directory,',
282
+ ' then $CREDDA_HOME/credda.config.json',
283
+ ' 4. the built-in default',
284
+ '',
285
+ 'Create a config file with: credda init',
286
+ '',
287
+ 'The report this produces: credda report <id>',
288
+ ],
289
+ };
290
+ /**
291
+ * `report`, with `resolution` as a permanent alias for it.
292
+ *
293
+ * ADR 0012 named this record a *resolution* when the pipeline ended in a patch
294
+ * and a pull request. ADR 0015 then took the Fix and Verify stages off the V1
295
+ * path, and for that stretch a record produced today could carry neither.
296
+ * ADR 0019 (2026-08-27) put both stages back: a run with a model-backed
297
+ * provider writes a patch and verifies it, so Change and Verification are
298
+ * filled in again from that run's own records.
299
+ *
300
+ * `report` is still the better name. It is what the command does -- show what
301
+ * the run established -- whether or not the run reached the fix stage, and it
302
+ * does not promise a resolution to a run that stopped at the diagnosis.
303
+ *
304
+ * `resolution` keeps working, for the same reason `fix` does.
305
+ */
306
+ const REPORT = {
307
+ name: 'report',
308
+ summary: 'Show what an investigation established, and what it did not',
309
+ args: '<investigation-id-or-prefix> [--json] [--markdown] [--patch]',
310
+ flags: {
311
+ markdown: {
312
+ kind: 'boolean',
313
+ description: 'Emit the report as Markdown: the same document Credda posts to a\n' +
314
+ ' pull request. Pipe it, paste it, or commit it',
315
+ },
316
+ patch: {
317
+ kind: 'boolean',
318
+ description: 'Emit the recorded unified diff and nothing else. Exits non-zero\n' +
319
+ ' when the run recorded no patch, so a script cannot\n' +
320
+ ' mistake an empty document for an empty change',
321
+ },
322
+ },
323
+ details: [
324
+ 'The record (ADR 0012): Bug, Evidence, Reproduction, Root Cause and',
325
+ 'Confidence, and, when the run reached the fix stage, Change and',
326
+ 'Verification.',
327
+ '',
328
+ 'What the run behind this record does: Credda is handed a LABELLED bug',
329
+ 'report. It reproduces the failure, diagnoses the cause, writes a patch, and',
330
+ 'proves the patch with a test that fails before it and passes after. It',
331
+ 'NEVER merges, and it does not scan a codebase looking for unknown bugs.',
332
+ '',
333
+ 'Change and Verification are printed from the run\'s own records. Reaching',
334
+ 'them depends on the provider (ADR 0019): a run with no model-backed',
335
+ 'provider stops at the diagnosis and records neither, and both sections say',
336
+ 'that rather than going quiet.',
337
+ '',
338
+ 'Every section is derived from something that was executed and recorded, or',
339
+ 'it is absent. A section with nothing behind it is not filled in -- the hole',
340
+ 'is named under Confidence instead.',
341
+ '',
342
+ 'Confidence is an ordinal class -- ESTABLISHED, PARTIALLY_ESTABLISHED or',
343
+ 'NOT_ESTABLISHED -- and the list of what this record does not establish. It',
344
+ 'is never a percentage: Credda has no calibrated probability model, and the',
345
+ 'field a reviewer reads to decide how much to trust a finding is the worst',
346
+ 'place to invent a number.',
347
+ '',
348
+ 'Any unambiguous prefix of an investigation id is accepted.',
349
+ '',
350
+ '--markdown emits the same document the forge delivery posts, which until now',
351
+ 'was reachable only from a webhook. It leads with what the investigation did',
352
+ 'NOT establish, and its "What was not done" section states, from the record,',
353
+ 'whether code was written and what has not been shown. That section is the',
354
+ 'point; do not strip it before sharing the rest.',
355
+ '',
356
+ 'Under --json neither document is emitted: a JSONL stream may carry nothing',
357
+ 'but objects. The suppressed flag is named on stderr rather than dropped in',
358
+ 'silence, and the document is one command away without --json.',
359
+ '',
360
+ '--patch writes the unified diff this run recorded, on stdout, with nothing',
361
+ 'around it. It exists so a delivery surface can commit what the run actually',
362
+ 'produced instead of re-deriving a change from prose; whether that diff may',
363
+ 'be PROPOSED to anyone is a separate question, answered by the delivery',
364
+ 'block in the result file that `credda investigate --out` writes.',
365
+ ],
366
+ };
367
+ /**
368
+ * `credda triage`: read one report, say what Credda could not use in it, or say
369
+ * nothing at all.
370
+ *
371
+ * ## Why it is called triage, and what the name must never come to mean
372
+ *
373
+ * Every other name considered here promised something the command does not do,
374
+ * which is the mistake {@link INVESTIGATE} spent two renames undoing. `decline`
375
+ * and `reply` both take the issue as their object -- "decline this issue" reads
376
+ * as a verdict on the reporter, and the copy this command prints is built
377
+ * around never being one. `decline-reply`, after the package it renders
378
+ * through, names the artefact exactly but is the only hyphenated command in a
379
+ * table of single words.
380
+ *
381
+ * `triage` is the maintainer's own word for the thing this does: look at an
382
+ * inbound report without doing the work, and say what would be needed. It is
383
+ * accurate today and it has one way to go wrong, so it is written down -- **this
384
+ * command must never label, close, assign, prioritise or otherwise decide
385
+ * anything about an issue.** It reads a file and prints a comment or nothing.
386
+ * The day it does more than that, the name is a promise again and has to move.
387
+ *
388
+ * ## Why it takes a file and never a string
389
+ *
390
+ * The body is text a stranger typed. The launcher's `run.mjs`
391
+ * (Credda-io/action) documents at length why
392
+ * it may never reach a shell, and the same reasoning applies one layer up: an
393
+ * argument goes through a shell, a file name does not. {@link INVESTIGATE}
394
+ * accepts `@file` alongside inline text because a person at a terminal has a
395
+ * sentence in their head; this command exists to be invoked by a workflow on
396
+ * text nobody has read, so the inline form is not offered at all.
397
+ */
398
+ const TRIAGE = {
399
+ name: 'triage',
400
+ summary: 'Say what Credda could not use in a report, or say nothing',
401
+ args: '<issue-file> [--repo <path>]',
402
+ flags: {
403
+ repo: {
404
+ kind: 'string',
405
+ valueName: '<path>',
406
+ description: 'A checkout of the repository the report was filed against. Without it\n' +
407
+ ' Credda assumes it knows nothing about the repository, which is\n' +
408
+ ' the reading that invents the least',
409
+ },
410
+ },
411
+ details: [
412
+ 'What a run does, and what it costs:',
413
+ ' it reads the report, mines it for a runnable reproduction exactly as an',
414
+ ' investigation would, and renders the refusals into one short comment. No',
415
+ ' sandbox, no container, no install, no network, no model call and no API',
416
+ ' key -- nothing is executed and nothing is written. It is cheap enough to',
417
+ ' run on every issue the moment it is opened.',
418
+ '',
419
+ 'What it prints:',
420
+ ' the comment on stdout, or nothing on stdout. There is no third form.',
421
+ ' Diagnostics go to stderr, so `credda triage issue.md > comment.md` yields',
422
+ ' either the comment or an empty file.',
423
+ '',
424
+ 'Silence is the common outcome and it is a correct one. Measured over 729',
425
+ 'real inbound issues, about half contain nothing Credda could ask for and a',
426
+ 'quarter produce a specific request (bench/harvest). A comment that names',
427
+ 'nothing the reporter could act on is not written, because a bot that posts',
428
+ 'generic advice on every issue is a bot that gets muted.',
429
+ '',
430
+ 'Exit code, not stdout, is what says which happened:',
431
+ ' 6 there is a comment, and it is on stdout',
432
+ ' 0 there is correctly nothing to say',
433
+ 'Do NOT write `credda triage issue.md > c.md && post c.md`: that posts on the',
434
+ 'silent path and stays quiet on the speaking one, which is the wrong way',
435
+ 'round twice.',
436
+ '',
437
+ 'This is not an investigation and makes no claim about the repository. It',
438
+ 'never says a bug is absent, because it never ran anything. The full',
439
+ 'reproduce-and-report run is: credda investigate <repo-path> @<issue-file>',
440
+ ],
441
+ };
442
+ /**
443
+ * The vocabularies the validation flags accept, written out here rather than
444
+ * imported from `@credda/shared`.
445
+ *
446
+ * This file is mirrored byte-for-byte into the public `@credda/cli` package,
447
+ * which depends on nothing and builds outside this monorepo. An import of
448
+ * `@credda/shared` here would compile in `core` and break the mirror, so the
449
+ * only import this file may ever take is `./args.js`, which is mirrored
450
+ * alongside it.
451
+ *
452
+ * The copies are held to their originals by a test
453
+ * (`apps/cli/test/commands.test.ts`) that compares each list to the shared
454
+ * constant it duplicates. A vocabulary that drifts fails there rather than
455
+ * turning into a flag the API rejects.
456
+ */
457
+ /**
458
+ * `INVESTIGATION_STATES` and `OUTCOMES` from `packages/shared/src/states.ts`,
459
+ * written out for the same reason the validation vocabularies below are: the
460
+ * mirror package depends on nothing and may not import `@credda/shared`.
461
+ * `apps/cli/test/commands.test.ts` holds both lists to their originals.
462
+ */
463
+ const INVESTIGATION_STATE_CHOICES = [
464
+ 'CREATED',
465
+ 'PREPARING_ENVIRONMENT',
466
+ 'ANALYZING_REPOSITORY',
467
+ 'UNDERSTANDING_ISSUE',
468
+ 'INVESTIGATING',
469
+ 'ATTEMPTING_REPRODUCTION',
470
+ 'REPRODUCED',
471
+ 'DIAGNOSING',
472
+ 'ROOT_CAUSE_IDENTIFIED',
473
+ 'REPRODUCED_AND_DIAGNOSED',
474
+ 'REPRODUCED_NOT_DIAGNOSED',
475
+ 'CONTRADICTS_SPECIFICATION',
476
+ 'ISSUE_ALREADY_RESOLVED',
477
+ 'REPORT_REFUTED',
478
+ 'NO_CHANGE_REQUIRED',
479
+ 'NO_RUNNABLE_CHECK',
480
+ 'REPRODUCTION_FAILED',
481
+ 'INSUFFICIENT_EVIDENCE',
482
+ 'GENERATING_PATCH',
483
+ 'TESTING_PATCH',
484
+ 'VERIFYING',
485
+ 'VERIFIED',
486
+ 'READY_FOR_REVIEW',
487
+ 'VERIFICATION_FAILED',
488
+ 'PATCH_REJECTED',
489
+ 'NEEDS_HUMAN_INPUT',
490
+ 'CANCELLED',
491
+ 'FAILED',
492
+ ];
493
+ const OUTCOME_CHOICES = [
494
+ 'REPRODUCED_AND_DIAGNOSED',
495
+ 'REPRODUCED_NOT_DIAGNOSED',
496
+ 'CONTRADICTS_SPECIFICATION',
497
+ 'NO_CHANGE_REQUIRED',
498
+ 'NO_RUNNABLE_CHECK',
499
+ 'INCONCLUSIVE',
500
+ 'VERIFIED',
501
+ 'PARTIALLY_VERIFIED',
502
+ 'PATCH_REJECTED',
503
+ 'CANCELLED',
504
+ 'ERRORED',
505
+ ];
506
+ const VALIDATION_STATE_CHOICES = [
507
+ 'CREATED',
508
+ 'ANALYZING_CHANGE',
509
+ 'UNDERSTANDING_INTENT',
510
+ 'PLANNING',
511
+ 'PREPARING_ENVIRONMENT',
512
+ 'RUNNING',
513
+ 'CONFIRMING_FINDINGS',
514
+ 'INVESTIGATING_FINDING',
515
+ 'COMPLETED',
516
+ 'CANCELLED',
517
+ 'FAILED',
518
+ ];
519
+ const VALIDATION_OUTCOME_CHOICES = [
520
+ 'VERIFIED',
521
+ 'FAILED',
522
+ 'BLOCKED',
523
+ 'INCONCLUSIVE',
524
+ 'NO_CHANGE_REQUIRED',
525
+ 'CANCELLED',
526
+ 'ERRORED',
527
+ ];
528
+ const FINDING_SEVERITY_CHOICES = ['HIGH', 'MEDIUM', 'LOW'];
529
+ const FINDING_STATUS_CHOICES = ['OPEN', 'DISMISSED', 'ENVIRONMENT_RELATED', 'RESOLVED'];
530
+ /**
531
+ * `credda validations` and `credda validation`: the change-scoped run, read
532
+ * from a terminal.
533
+ *
534
+ * ## Why the object is separate from an investigation, and the commands with it
535
+ *
536
+ * An investigation asks whether one reported defect is fixed and answers with
537
+ * one Outcome. A validation asks whether a change works, which does not
538
+ * decompose into one question -- it decomposes into n checks that pass, fail,
539
+ * or turn out to be impossible to run, independently of one another (ADR 0010,
540
+ * and `packages/shared/src/validation.ts`). `status` and `inspect` cannot be
541
+ * widened to cover both without one of the two objects reading as the other,
542
+ * so the pair below mirrors them rather than absorbing them: `validations`
543
+ * lists, `validation` reads one in full.
544
+ *
545
+ * ## What these two commands may never come to mean
546
+ *
547
+ * They READ. Nothing here starts a validation, and nothing here writes,
548
+ * merges, closes or comments. They are the terminal's view of records the
549
+ * engine already wrote, and a validation is scoped to a change somebody
550
+ * proposed -- Credda does not go looking through a repository for defects
551
+ * nobody reported.
552
+ *
553
+ * The filters are exactly the ones `apps/api/src/routes/validations.ts`
554
+ * accepts, under the same names and the same vocabularies, because a filter
555
+ * that means something different on two surfaces is worse than a missing one.
556
+ */
557
+ const VALIDATIONS = {
558
+ name: 'validations',
559
+ summary: 'List change-scoped validation runs',
560
+ args: '[--repository <path-or-id>] [--state <state>] [--outcome <outcome>] [--limit <n>] [--offset <n>]',
561
+ flags: {
562
+ repository: {
563
+ kind: 'string',
564
+ valueName: '<path-or-id>',
565
+ description: 'Only validations of one repository. A path to a checkout or the\n' +
566
+ ' repository id; an unknown one is refused rather than answered\n' +
567
+ ' with an empty list',
568
+ },
569
+ state: {
570
+ kind: 'string',
571
+ choices: VALIDATION_STATE_CHOICES,
572
+ valueName: '<state>',
573
+ description: 'Only validations in this state',
574
+ },
575
+ outcome: {
576
+ kind: 'string',
577
+ choices: VALIDATION_OUTCOME_CHOICES,
578
+ valueName: '<outcome>',
579
+ description: 'Only validations that concluded this',
580
+ },
581
+ limit: { kind: 'number', valueName: '<n>', description: 'How many to list', defaultNote: '50' },
582
+ offset: { kind: 'number', valueName: '<n>', description: 'Skip this many first', defaultNote: '0' },
583
+ },
584
+ details: [
585
+ 'A validation is the change-scoped run: it takes a change somebody proposed',
586
+ 'and asks, check by check, whether it works. It is a different object from an',
587
+ 'investigation, which takes one reported defect and asks whether it is fixed.',
588
+ ' credda status lists investigations instead',
589
+ '',
590
+ 'STATE is where the run got to. OUTCOME is what it concluded, and only the',
591
+ 'outcome is a verdict: a run that finished and found two failures is as',
592
+ 'COMPLETED as one that found none.',
593
+ '',
594
+ 'VERIFIED requires at least one check to have actually passed. A run with no',
595
+ 'passing check is INCONCLUSIVE, never a clean bill of health, and BLOCKED',
596
+ 'means the environment would not come up so nothing was asked of the change',
597
+ 'at all.',
598
+ '',
599
+ 'Read one of them in full with: credda validation <id>',
600
+ ],
601
+ };
602
+ const VALIDATION = {
603
+ name: 'validation',
604
+ summary: 'Show one validation: its checks, and the findings they raised',
605
+ args: '<validation-id-or-prefix> [--severity <s>] [--status <s>] [--limit <n>] [--offset <n>]',
606
+ flags: {
607
+ severity: {
608
+ kind: 'string',
609
+ choices: FINDING_SEVERITY_CHOICES,
610
+ valueName: '<severity>',
611
+ description: 'Only findings of this severity',
612
+ },
613
+ status: {
614
+ kind: 'string',
615
+ choices: FINDING_STATUS_CHOICES,
616
+ valueName: '<status>',
617
+ description: 'Only findings with this status',
618
+ },
619
+ limit: {
620
+ kind: 'number',
621
+ valueName: '<n>',
622
+ description: 'How many findings to show',
623
+ defaultNote: '50',
624
+ },
625
+ offset: {
626
+ kind: 'number',
627
+ valueName: '<n>',
628
+ description: 'Skip this many findings first',
629
+ defaultNote: '0',
630
+ },
631
+ },
632
+ details: [
633
+ 'Any unambiguous prefix of a validation id is accepted.',
634
+ ' credda validations lists recent validations',
635
+ '',
636
+ 'The plan is printed whole, in the order it was executed, and every check is',
637
+ 'shown with the status it reached. A check that was never run is printed as',
638
+ 'PENDING rather than omitted, because a silently missing check reads as a',
639
+ 'passing one.',
640
+ '',
641
+ 'Check statuses that are not failures, and are not successes either:',
642
+ ' PRE_EXISTING_FAILURE it fails on this change and fails identically on the',
643
+ ' base commit, so this change did not cause it. Shown',
644
+ ' as context and never raised as a finding.',
645
+ ' BLOCKED it could not be executed at all, so nothing was',
646
+ ' observed about the change in either direction.',
647
+ '',
648
+ 'A finding is narrower than a failure: a check reaches FAILED only after the',
649
+ 'base commit was re-run and passed there, so every finding below carries the',
650
+ 'fact that this change caused it.',
651
+ '',
652
+ 'This command reads records. It starts nothing, writes nothing, and Credda',
653
+ 'never merges a change.',
654
+ '',
655
+ 'The findings filters narrow the findings only; the plan above them is always',
656
+ 'printed whole, because a plan cut to a filter is a plan a reader cannot',
657
+ 'check the outcome against.',
658
+ ],
659
+ };
660
+ /**
661
+ * Stopping a run that is already going.
662
+ *
663
+ * ## Why this command exists at all
664
+ *
665
+ * Ctrl-C stops a run from the terminal that started it. That covers the case
666
+ * where the operator is still sitting there, and it is the only case Credda
667
+ * covered: a run started in a terminal that has since been closed, backgrounded,
668
+ * or left on another tab could not be stopped by anything short of `kill`, and
669
+ * `kill` leaves the sandbox container running -- which is why `credda reap`
670
+ * exists.
671
+ *
672
+ * `apps/api` has the same shape of problem from the other side and refuses to
673
+ * paper over it: `POST /api/investigations/:id/cancel` answers a CLI-started run
674
+ * with 409 NOT_CANCELLABLE, because the job queue never saw that run and the API
675
+ * cannot reach the process executing it. This command is the reach that answer
676
+ * says is missing, for the one machine where it is possible: `credda
677
+ * investigate` records its pid beside the store, and this reads it and sends the
678
+ * interrupt the running process already handles.
679
+ *
680
+ * ## What it may never say
681
+ *
682
+ * A cancel that reports success without stopping the run is worse than no
683
+ * cancel at all -- it tells an operator something false about their own machine
684
+ * and their own bill. So the two answers stay apart everywhere they are
685
+ * expressed: in the text, in the exit code (0 stopped, 7 asked), and in
686
+ * `CancelOutcome`, where `stopped: true` exists only on the outcomes for which
687
+ * it is true and a renderer that prints "Cancelled." on a request does not
688
+ * compile.
689
+ */
690
+ const CANCEL = {
691
+ name: 'cancel',
692
+ summary: 'Stop a running investigation, or say why it cannot be stopped',
693
+ args: '<investigation-id-or-prefix> [--reason <text>]',
694
+ flags: {
695
+ reason: {
696
+ kind: 'string',
697
+ valueName: '<text>',
698
+ description: 'Recorded on the investigation. Not required: a cancel with nothing\n' +
699
+ ' said is still a cancel',
700
+ },
701
+ },
702
+ details: [
703
+ 'Any unambiguous prefix of an investigation id is accepted.',
704
+ '',
705
+ 'There are two good answers and they are not the same answer:',
706
+ '',
707
+ ' stopped nothing is running. The run had not started, or its process is',
708
+ ' already gone. The record is CANCELLED. Exit code 0.',
709
+ ' asked a process on this machine is inside the run, holding a sandbox',
710
+ ' and possibly a model call. It was signalled. It has not stopped:',
711
+ ' it stops at its next checkpoint, tears the sandbox down, and',
712
+ ' writes its own terminal state. This command does not write that',
713
+ ' state and cannot say when it will be written. Exit code 7.',
714
+ '',
715
+ 'Follow the second one to its end with: credda events <id> --follow',
716
+ '',
717
+ 'A run that already finished cannot be stopped and cannot be undone, and a run',
718
+ 'this machine cannot reach is reported as unreachable rather than marked',
719
+ 'cancelled: marking it would be a state the still-running engine overwrites',
720
+ 'minutes later, having spent the whole budget you thought you had stopped.',
721
+ 'Both exit 2.',
722
+ '',
723
+ 'Cancelling a run that was killed rather than interrupted also leaves its',
724
+ 'sandbox container behind. Clean those up with: credda reap',
725
+ ],
726
+ };
727
+ /**
728
+ * `credda discover`: read a checkout and write the bug reports nobody filed.
729
+ *
730
+ * ## What it is, and the sentence it must never be read as
731
+ *
732
+ * ADR 0024 decides that discovery produces a REPORT and the existing pipeline
733
+ * decides what it is worth. That is the whole design and this command is the
734
+ * only thing that makes it reachable: `discoverFromRepository()` in
735
+ * `@credda/repository` walks a tree, runs both rule sets -- the security
736
+ * classes and the locally decidable defect classes -- and returns candidates in
737
+ * the same `{title, body}` slot a forge issue and a rendered signal fill. Until
738
+ * this existed nothing read them.
739
+ *
740
+ * "Credda finds bugs and security vulnerabilities" is the sentence this command
741
+ * must never be read as, wherever it is written. It finds SHAPES and writes
742
+ * reports about them, and measured against 160 real cases from 50 real
743
+ * repositories it confirmed none of them: 103 candidates, none of which fell
744
+ * silent at the maintainer's fix, and on no case did a finder name the defect
745
+ * the case pins (ADR 0024, amendment). A candidate is a report,
746
+ * not a finding and not a vulnerability disclosure, and every one of them is
747
+ * still owed a reproduction before it is anything at all -- which is why the
748
+ * output leads with the observation that would refute each one, and why it
749
+ * states no severity. Severity is a judgement about exposure and a single-file
750
+ * rule knows nothing about what a repository is exposed to.
751
+ *
752
+ * ## Why it does not start runs
753
+ *
754
+ * Discovery finding something is not consent to spend a model budget on it.
755
+ * This command writes reports and stops; the operator decides which of them is
756
+ * worth a sandbox, and starts it with `credda investigate` like any other
757
+ * report. That is the same discipline as `start: true` defaulting to false on
758
+ * the API's create route, and it is the reason this command may be run on
759
+ * anything without asking what it will cost.
760
+ *
761
+ * ## What it may never come to do
762
+ *
763
+ * It reads. It executes nothing -- not the repository's code and not the
764
+ * programs it emits, whose entire defect in the ReDoS case is that running them
765
+ * hangs a CPU (ADR 0005). The day this command runs one, it is an unsandboxed
766
+ * execution path opened by a scanner, and the name is a promise again.
767
+ */
768
+ const DISCOVER = {
769
+ name: 'discover',
770
+ summary: 'Read a checkout and write the bug reports nobody filed. Starts nothing',
771
+ args: '<repo-path> [--out <dir>] [--max-files <n>] [--json]',
772
+ flags: {
773
+ out: {
774
+ kind: 'string',
775
+ valueName: '<dir>',
776
+ description: 'Write each candidate report to a file in <dir>, and print the exact\n' +
777
+ ' investigate command for it. Without this, the candidates are\n' +
778
+ ' listed and nothing is written',
779
+ },
780
+ 'max-files': {
781
+ kind: 'number',
782
+ valueName: '<n>',
783
+ description: 'How many source files to read',
784
+ defaultNote: '400',
785
+ },
786
+ },
787
+ details: [
788
+ 'What a run does, and what it costs:',
789
+ ' it walks the checkout, reads its JavaScript and TypeScript source, runs',
790
+ ' both rule sets over it -- the security classes and the locally decidable',
791
+ ' defect classes -- and writes an ordinary bug report about each shape it',
792
+ ' saw. The listing counts the classes off those lists rather than naming a',
793
+ ' number here, which is a number that rots. No sandbox, no container, no',
794
+ ' install, no network, no model call and no API key. Nothing in the',
795
+ ' repository is executed and nothing in it is written to.',
796
+ '',
797
+ 'What a candidate is:',
798
+ ' a REPORT Credda wrote instead of waiting for somebody to write one. It is',
799
+ ' not a finding, it is not a vulnerability disclosure, and it states no',
800
+ ' severity -- severity is a judgement about exposure, and a rule reading one',
801
+ ' file knows nothing about what this repository is exposed to. Each report',
802
+ ' carries a program that decides the question by running, and that program',
803
+ ' is written so it can come back saying no. Most of them do.',
804
+ '',
805
+ 'Nothing is started:',
806
+ ' discovery finding something is not consent to spend a model budget on it.',
807
+ ' This command creates no investigation. You choose which candidate is worth',
808
+ ' one, and start it yourself:',
809
+ '',
810
+ ' credda discover ./my-app --out ./candidates',
811
+ ' credda investigate ./my-app @./candidates/01-redos-src-parse-ts-84.md \\',
812
+ ' --ref discovery:REDOS:src/parse.ts:84',
813
+ '',
814
+ ' The --ref is printed for you beside each candidate. It is what records',
815
+ ' that Credda wrote the report, so the run reads as its own claim rather',
816
+ ' than as somebody else\'s. Nothing downstream branches on it: a discovered',
817
+ ' report is put through the identical pipeline, with the identical stages',
818
+ ' and the identical refusals, and a candidate that cannot be reproduced',
819
+ ' produces nothing. That is the correct outcome, and the common one.',
820
+ '',
821
+ 'No candidates is not a clean bill of health. Two rule sets are looked for --',
822
+ 'the security shapes and the locally decidable defect shapes -- and the',
823
+ 'listing names which one spoke for each candidate. The output says how many',
824
+ 'files were read and whether the walk stopped short, because "we did not',
825
+ 'see it" and "it cannot happen" are different claims.',
826
+ '',
827
+ 'What this has been measured to do, so a candidate is read for what it is:',
828
+ ' against 160 cases from 50 real repositories -- each at a commit where a',
829
+ ' defect is present and again at the maintainer\'s fix -- the rules emitted',
830
+ ' 103 candidates, none of them fell silent at the fix, and on no case did a',
831
+ ' rule name the defect the case pins. Read a candidate as a report worth a',
832
+ ' reproduction, never as a defect this found.',
833
+ '',
834
+ 'Exit code is 0 whether or not anything was found. A list of reports is not',
835
+ 'a failed check.',
836
+ ],
837
+ };
838
+ export const COMMANDS = {
839
+ investigate: INVESTIGATE,
840
+ triage: TRIAGE,
841
+ discover: DISCOVER,
842
+ doctor: {
843
+ name: 'doctor',
844
+ summary: 'Check that this environment can reproduce a bug',
845
+ args: '[<repo-path>] [--deep]',
846
+ flags: {
847
+ deep: {
848
+ kind: 'boolean',
849
+ description: 'Also build or pull the sandbox image and probe its toolchain. Uses\n' +
850
+ ' the network and can take minutes, so it is opt-in',
851
+ },
852
+ },
853
+ details: [
854
+ 'Reports pass / warn / fail for eight checks that always run: the Node',
855
+ 'version, git, configuration, the active model provider, the selected',
856
+ 'sandbox, the container plane, CREDDA_HOME and the database. The sandbox',
857
+ 'image is a ninth, reported only when docker is the selected plane or',
858
+ '--deep was given. Exits non-zero only when something is genuinely broken;',
859
+ 'warnings alone exit 0.',
860
+ '',
861
+ 'With a <repo-path>, it also reports whether that repository could be',
862
+ 'prepared at all: the package manager and the exact install command a run',
863
+ 'would use, and the test, build and typecheck commands it would find. That',
864
+ 'is a plan read off the repository, not a run of it -- nothing is installed',
865
+ 'and nothing is executed.',
866
+ '',
867
+ '--deep additionally builds or pulls the docker sandbox image and probes its',
868
+ 'toolchain, which is the step that decides whether reproduction can happen',
869
+ 'at all. It uses the network and can take minutes, so it is opt-in.',
870
+ ],
871
+ },
872
+ reap: {
873
+ name: 'reap',
874
+ summary: 'Remove sandbox containers left behind by an interrupted run',
875
+ args: '[--dry-run] [--max-age-hours <n>]',
876
+ flags: {
877
+ 'dry-run': {
878
+ kind: 'boolean',
879
+ description: 'List what would be removed and remove nothing',
880
+ },
881
+ 'max-age-hours': {
882
+ kind: 'string',
883
+ description: 'Only reap containers older than this. Default 4, which is far above\n' +
884
+ ' any real investigation',
885
+ },
886
+ },
887
+ details: [
888
+ 'A sandbox container is removed when the investigation that made it finishes.',
889
+ 'If that process is killed instead -- SIGKILL, a crash, a closed terminal on a',
890
+ 'long benchmark -- nothing removes it, and it keeps its memory reservation and',
891
+ 'its volume until somebody notices. Three of them once ran for seventeen hours',
892
+ 'on this machine.',
893
+ '',
894
+ 'This removes sandbox containers older than --max-age-hours, then the sandbox',
895
+ 'volumes nothing references any more. Age is the test because it has no false',
896
+ 'positives above a threshold no real run reaches; the default of four hours is',
897
+ 'roughly twenty times the slowest case ever measured.',
898
+ '',
899
+ 'It acts on containers this process did not create, so on a shared docker',
900
+ 'daemon it can reach somebody else\'s run. Read --dry-run first.',
901
+ ],
902
+ },
903
+ init: {
904
+ name: 'init',
905
+ summary: 'Write a credda.config.json with documented defaults',
906
+ args: '[--global] [--force]',
907
+ flags: {
908
+ global: {
909
+ kind: 'boolean',
910
+ description: 'Write to $CREDDA_HOME/credda.config.json instead of the current directory',
911
+ },
912
+ force: { kind: 'boolean', description: 'Overwrite an existing config file' },
913
+ },
914
+ },
915
+ cancel: CANCEL,
916
+ /*
917
+ * The filters are the ones `apps/api/src/routes/investigations.ts` accepts,
918
+ * under the same names and the same vocabularies, for the reason
919
+ * {@link VALIDATIONS} gives: a filter that means something different on two
920
+ * surfaces is worse than a missing one.
921
+ *
922
+ * This command had only `--limit` while its younger sibling `validations`
923
+ * shipped with the full set, so the two questions most often asked of a queue
924
+ * -- whose repository, and how did it end -- could be asked of a validation
925
+ * from a terminal and not of an investigation. Every one of these is a filter
926
+ * the local store has always supported; nothing new is read.
927
+ *
928
+ * `--signal` and `--hasSignal` are the API filters deliberately absent. A
929
+ * signal is a row this CLI never writes: a run started from a terminal is
930
+ * started by the person at it, so `signalId` is null on every investigation
931
+ * in a local store, and `--signal` could only ever return nothing while
932
+ * `--hasSignal false` could only ever return everything.
933
+ *
934
+ * `--ref` is the opposite case and is here for it. Provenance is a column
935
+ * this CLI DOES write -- `credda investigate --ref` records it, and `credda
936
+ * discover` prints a ref for every candidate it writes precisely so the run
937
+ * started from one says Credda wrote the report -- so until this flag the
938
+ * terminal wrote a fact it could not then ask a question about.
939
+ */
940
+ status: {
941
+ name: 'status',
942
+ summary: 'List recent investigations',
943
+ args: '[--repository <path-or-id>] [--state <state>] [--outcome <outcome>] ' +
944
+ '[--ref <ref>] [--limit <n>] [--offset <n>] [--json]',
945
+ flags: {
946
+ repository: {
947
+ kind: 'string',
948
+ valueName: '<path-or-id>',
949
+ description: 'Only investigations of one repository. A path to a checkout or the\n' +
950
+ ' repository id; an unknown one is refused rather than answered\n' +
951
+ ' with an empty list',
952
+ },
953
+ state: {
954
+ kind: 'string',
955
+ choices: INVESTIGATION_STATE_CHOICES,
956
+ valueName: '<state>',
957
+ description: 'Only investigations in this state',
958
+ },
959
+ outcome: {
960
+ kind: 'string',
961
+ choices: OUTCOME_CHOICES,
962
+ valueName: '<outcome>',
963
+ description: 'Only investigations that concluded this',
964
+ },
965
+ ref: {
966
+ kind: 'string',
967
+ valueName: '<ref>',
968
+ description: 'Only investigations recorded as coming from this ref, matched whole.\n' +
969
+ ' The value `credda investigate --ref` stored, and the one\n' +
970
+ ' `credda discover` prints beside a candidate',
971
+ },
972
+ limit: { kind: 'number', valueName: '<n>', description: 'How many to list', defaultNote: '20' },
973
+ offset: { kind: 'number', valueName: '<n>', description: 'Skip this many first', defaultNote: '0' },
974
+ },
975
+ details: [
976
+ 'STATE is where the run got to, including the terminal it stopped on.',
977
+ 'OUTCOME is what it concluded, and a run still in flight has none -- so it',
978
+ 'matches no --outcome value, and --state is the way to ask for it.',
979
+ '',
980
+ 'Abstaining is a conclusion, not a gap: NO_CHANGE_REQUIRED means the',
981
+ 'reported thing did not happen, and INCONCLUSIVE means the run would not',
982
+ 'claim what it had not established. Both are successes and both exit 0.',
983
+ '',
984
+ '--ref asks where a run came from. It matches the whole string, which is',
985
+ 'the form both writers of it produce: an issue reference or URL you passed',
986
+ 'to `credda investigate --ref`, or the `discovery:<CLASS>:<file>:<line>`',
987
+ 'ref `credda discover` prints beside a candidate. A run started with no ref',
988
+ 'matches no value here.',
989
+ '',
990
+ ' credda validations lists validation runs instead',
991
+ ],
992
+ },
993
+ report: REPORT,
994
+ validations: VALIDATIONS,
995
+ validation: VALIDATION,
996
+ inspect: {
997
+ name: 'inspect',
998
+ summary: 'Show everything one run recorded, in full',
999
+ args: '<investigation-id-or-prefix>',
1000
+ flags: {},
1001
+ details: [
1002
+ 'Any unambiguous prefix of an investigation id is accepted.',
1003
+ '',
1004
+ 'This is the run: the reproduction, every hypothesis including the refuted',
1005
+ 'ones, and the evidence records. For what the run established and what it',
1006
+ 'did not, use: credda report <id>',
1007
+ '',
1008
+ 'What it spent is printed from the run\'s own cost record, and under --json',
1009
+ 'as `cost`, beside the ceiling that bounded it as `effectiveBudget`. Both',
1010
+ 'are absent rather than zero when nothing recorded them: a run still going,',
1011
+ 'and one that died before it could record, did not measure zero. A run',
1012
+ 'started from this terminal is bounded by the flags in its own banner, and',
1013
+ 'nothing writes that ceiling down, so `effectiveBudget` is null for it.',
1014
+ ],
1015
+ },
1016
+ events: {
1017
+ name: 'events',
1018
+ summary: 'Show the event timeline for an investigation',
1019
+ args: '<investigation-id-or-prefix> [--since <n>] [--follow] [--json]',
1020
+ flags: {
1021
+ since: {
1022
+ kind: 'number',
1023
+ valueName: '<n>',
1024
+ description: 'Only events with a sequence number greater than <n>',
1025
+ },
1026
+ follow: {
1027
+ kind: 'boolean',
1028
+ alias: 'f',
1029
+ description: 'Tail a running investigation until it reaches a terminal state',
1030
+ },
1031
+ },
1032
+ details: ['Any unambiguous prefix of an investigation id is accepted.'],
1033
+ },
1034
+ /*
1035
+ * The two former names. Kept permanently, and kept honest: each takes the
1036
+ * canonical command's flags, details and behaviour, and only the summary line
1037
+ * differs, so `credda fix --help` never reads as a promise to write one.
1038
+ */
1039
+ resolve: {
1040
+ ...INVESTIGATE,
1041
+ name: 'resolve',
1042
+ summary: "Alias for 'credda investigate', kept because scripts and docs use it",
1043
+ aliasOf: 'investigate',
1044
+ },
1045
+ fix: {
1046
+ ...INVESTIGATE,
1047
+ name: 'fix',
1048
+ summary: "Alias for 'credda investigate', kept because scripts and docs use it",
1049
+ aliasOf: 'investigate',
1050
+ },
1051
+ resolution: {
1052
+ ...REPORT,
1053
+ name: 'resolution',
1054
+ summary: "Alias for 'credda report', kept because scripts and docs use it",
1055
+ aliasOf: 'report',
1056
+ },
1057
+ };
1058
+ /**
1059
+ * The command an alias dispatches to. Unknown names are returned unchanged so
1060
+ * the caller's own "unknown command" path still owns that message.
1061
+ */
1062
+ export function canonicalCommand(name) {
1063
+ return own(COMMANDS, name)?.aliasOf ?? name;
1064
+ }
1065
+ /** Alias name to the command it stands for, for the root usage. */
1066
+ export function aliases() {
1067
+ return Object.entries(COMMANDS)
1068
+ .filter(([, spec]) => spec.aliasOf !== undefined)
1069
+ .map(([name, spec]) => [name, spec.aliasOf]);
1070
+ }
1071
+ const ENVIRONMENT = [
1072
+ ['CREDDA_HOME', 'Where Credda stores its database and evidence (default ./.credda)'],
1073
+ [
1074
+ 'ANTHROPIC_API_KEY',
1075
+ 'Enables the Anthropic provider. Without it reasoning is rule-based,\n' +
1076
+ ' which reaches a reproduction but rarely a diagnosis; every\n' +
1077
+ ' report says which provider produced it.',
1078
+ ],
1079
+ [
1080
+ 'CREDDA_PROVIDER',
1081
+ "'auto', 'heuristic' or 'openai-compatible'. 'heuristic' forces the\n" +
1082
+ ' deterministic provider',
1083
+ ],
1084
+ ['CREDDA_MODEL', 'Overrides the Anthropic model id'],
1085
+ [
1086
+ 'CREDDA_OPENAI_API_KEY',
1087
+ 'Enables the openai-compatible provider (NVIDIA NIM by default).\n' +
1088
+ ' NVIDIA_API_KEY is accepted as a second name.',
1089
+ ],
1090
+ ['CREDDA_OPENAI_BASE_URL', 'OpenAI-compatible base URL (default NVIDIA NIM)'],
1091
+ ['CREDDA_OPENAI_MODEL', 'Model id served by that endpoint'],
1092
+ [
1093
+ 'CREDDA_OPENAI_RPM',
1094
+ 'Client-side request pacing (default 40, NVIDIA free-tier limit)',
1095
+ ],
1096
+ [
1097
+ 'CREDDA_SANDBOX',
1098
+ "'local' (this command's default), 'native' or 'docker'. local and native\n" +
1099
+ ' run repository code directly on this host, and only a local credda\n' +
1100
+ ' invocation may select them: a repository arriving any other way\n' +
1101
+ ' is refused them and must use docker. Credda never falls back\n' +
1102
+ ' silently in either direction.',
1103
+ ],
1104
+ ['CREDDA_SANDBOX_IMAGE', 'Overrides the image the docker plane builds or pulls'],
1105
+ ['CREDDA_LOG_LEVEL', 'debug | info | warn | error (default warn)'],
1106
+ ['NO_COLOR', 'Set to any value to disable ANSI colour'],
1107
+ ['CREDDA_ASCII', 'Set to any value to draw with ASCII instead of box characters'],
1108
+ ['TERM', "'dumb' is treated the same as NO_COLOR"],
1109
+ ];
1110
+ export function rootUsage() {
1111
+ const lines = [
1112
+ 'credda - something broke, find out what',
1113
+ '',
1114
+ 'Usage: credda <command> [options]',
1115
+ '',
1116
+ 'Workflow: signal -> investigate -> reproduce -> diagnose -> report.',
1117
+ 'Credda reports what it found and stops there. It changes nothing in your',
1118
+ 'working tree: any fix it attempts is made in a disposable copy.',
1119
+ '',
1120
+ 'Commands:',
1121
+ ];
1122
+ const named = Object.values(COMMANDS).filter((command) => command.aliasOf === undefined);
1123
+ const width = Math.max(...named.map((c) => c.name.length));
1124
+ for (const command of named) {
1125
+ lines.push(` ${command.name.padEnd(width)} ${command.summary}`);
1126
+ }
1127
+ const aliased = aliases();
1128
+ if (aliased.length > 0) {
1129
+ lines.push('', 'Aliases:');
1130
+ for (const [alias, target] of aliased) {
1131
+ lines.push(` ${alias.padEnd(width)} ${target}, under its former name. Both are supported.`);
1132
+ }
1133
+ }
1134
+ lines.push('', 'Global options:');
1135
+ lines.push(...renderFlags(GLOBAL_FLAGS));
1136
+ lines.push('', 'Environment:');
1137
+ for (const [name, description] of ENVIRONMENT) {
1138
+ lines.push(` ${name.padEnd(19)} ${description}`);
1139
+ }
1140
+ lines.push('', 'Exit codes:');
1141
+ lines.push(...EXIT_CODE_HELP);
1142
+ lines.push('', "Run 'credda <command> --help' for options specific to a command.");
1143
+ return lines.join('\n');
1144
+ }
1145
+ export function commandUsage(name) {
1146
+ const spec = own(COMMANDS, name);
1147
+ if (spec === undefined)
1148
+ return rootUsage();
1149
+ const lines = [
1150
+ `credda ${spec.name} - ${spec.summary}`,
1151
+ '',
1152
+ `Usage: credda ${spec.name} ${spec.args}`.trimEnd(),
1153
+ ];
1154
+ if (spec.aliasOf !== undefined) {
1155
+ /*
1156
+ * What the command DOES, before the note about what it is called.
1157
+ *
1158
+ * An alias whose summary line reads "Alias for 'credda investigate'" tells a
1159
+ * reader nothing about the output, and this is the help a person reaches
1160
+ * when the name in their fingers is the one that used to promise a patch.
1161
+ * The canonical summary is restated here so the answer is on the screen
1162
+ * rather than one command away.
1163
+ */
1164
+ const target = own(COMMANDS, spec.aliasOf);
1165
+ if (target !== undefined)
1166
+ lines.push('', `What it does: ${target.summary.toLowerCase()}.`);
1167
+ lines.push('', `'credda ${spec.name}' and 'credda ${spec.aliasOf}' are the same command. ${spec.aliasOf} is the`, 'current name; this one is kept permanently, because docs, scripts and the', "external benchmark harness use it and a name in someone's fingers that stops", 'working is worse than one that is out of date. The name is the only thing', 'that is out of date.');
1168
+ }
1169
+ if (Object.keys(spec.flags).length > 0) {
1170
+ lines.push('', 'Options:');
1171
+ lines.push(...renderFlags(spec.flags));
1172
+ }
1173
+ lines.push('', 'Global options:');
1174
+ lines.push(...renderFlags(GLOBAL_FLAGS));
1175
+ if (spec.details !== undefined)
1176
+ lines.push('', ...spec.details);
1177
+ lines.push('', 'Exit codes:');
1178
+ lines.push(...EXIT_CODE_HELP);
1179
+ return lines.join('\n');
1180
+ }
1181
+ function renderFlags(flags) {
1182
+ const entries = Object.entries(flags).map(([name, spec]) => {
1183
+ const alias = spec.alias === undefined ? ' ' : `-${spec.alias}, `;
1184
+ const value = spec.kind === 'boolean' ? '' : ` ${spec.valueName ?? '<value>'}`;
1185
+ return [` ${alias}--${name}${value}`, spec, name];
1186
+ });
1187
+ const width = Math.max(...entries.map(([left]) => left.length));
1188
+ return entries.map(([left, spec]) => {
1189
+ const suffix = spec.defaultNote === undefined ? '' : ` (default: ${spec.defaultNote})`;
1190
+ return `${left.padEnd(width)} ${spec.description}${suffix}`;
1191
+ });
1192
+ }