speccle 0.11.0 → 0.15.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.
Files changed (41) hide show
  1. package/README.md +290 -3
  2. package/dist/calibration.js +156 -0
  3. package/dist/changeset.js +141 -0
  4. package/dist/claims.js +5 -1
  5. package/dist/cli.js +596 -4
  6. package/dist/config.js +117 -0
  7. package/dist/doctor.js +141 -0
  8. package/dist/init.js +5 -4
  9. package/dist/lenses.js +68 -0
  10. package/dist/remedy.js +133 -0
  11. package/dist/render.js +323 -2
  12. package/dist/reviewinit.js +139 -0
  13. package/dist/reviewrun.js +516 -0
  14. package/dist/risk.js +228 -0
  15. package/dist/skills.js +63 -0
  16. package/dist/update.js +48 -0
  17. package/dist/verify.js +92 -0
  18. package/lenses/accessibility.md +39 -0
  19. package/lenses/architecture.md +38 -0
  20. package/lenses/correctness.md +36 -0
  21. package/lenses/house-conventions.md +37 -0
  22. package/lenses/performance.md +40 -0
  23. package/lenses/risk.md +45 -0
  24. package/lenses/security.md +42 -0
  25. package/lenses/test-quality.md +39 -0
  26. package/package.json +7 -5
  27. package/skills/carve-feature/SKILL.md +183 -0
  28. package/skills/carve-feature/references/convention.md +239 -0
  29. package/skills/conform/SKILL.md +129 -0
  30. package/skills/conform/references/convention.md +239 -0
  31. package/skills/feature/SKILL.md +103 -0
  32. package/skills/implement-feature/SKILL.md +152 -0
  33. package/skills/implement-feature/references/convention.md +239 -0
  34. package/skills/plan-feature/SKILL.md +125 -0
  35. package/skills/plan-feature/references/convention.md +239 -0
  36. package/skills/review/SKILL.md +190 -0
  37. package/skills/spec-feature/SKILL.md +163 -0
  38. package/skills/spec-feature/references/convention.md +239 -0
  39. package/skills/strengthen/SKILL.md +184 -0
  40. package/skills/strengthen/references/heatmap.md +39 -0
  41. package/skills/strengthen/references/stack.md +23 -0
package/dist/cli.js CHANGED
@@ -1,22 +1,82 @@
1
1
  #!/usr/bin/env node
2
+ import { calibrationReport, recordCalibration } from "./calibration.js";
2
3
  import { check } from "./check.js";
3
4
  import { claims } from "./claims.js";
5
+ import { initConfig } from "./config.js";
4
6
  import { DEFAULT_DIALECT, DIALECT_NAMES } from "./dialects.js";
5
- import { init } from "./init.js";
7
+ import { doctor } from "./doctor.js";
8
+ import { init, ownVersion } from "./init.js";
9
+ import { materializeLenses } from "./lenses.js";
6
10
  import { lint } from "./lint.js";
7
- import { renderCheck, renderClaims, renderHuman, renderInit, renderStrength } from "./render.js";
11
+ import { recallRemedy, recordRemedy, REMEDY_ROUTES } from "./remedy.js";
12
+ import { renderCalibrateRecord, renderCalibrateReport, renderCheck, renderClaims, renderConfigInit, renderDoctor, renderHuman, renderInit, renderLensesInit, renderRemedyRecall, renderRemedyRecord, renderReviewInit, renderReviewRun, renderRisk, renderSkillsInit, renderStrength, renderUpdate, renderVerify, } from "./render.js";
13
+ import { scaffoldReviewWorkflow } from "./reviewinit.js";
14
+ import { reviewRun } from "./reviewrun.js";
15
+ import { risk } from "./risk.js";
16
+ import { materializeSkills } from "./skills.js";
8
17
  import { DEFAULT_COVERAGE_SUMMARY, DEFAULT_MUTATION_REPORT, strength } from "./strength.js";
18
+ import { update } from "./update.js";
19
+ import { verify } from "./verify.js";
9
20
  const USAGE = `Usage: speccle <command> [options]
10
21
 
11
22
  Commands:
23
+ init [path] [--json] Record repo facts in .speccle/config.json, materialize the
24
+ skills into .claude/skills/ and the lenses into .speccle/lenses/
25
+ doctor [path] [--json] Report staleness across the CLI, skills, lenses, and strength stack
26
+ update [path] [--json] Refresh the skills and lenses as a diff; report stack and binary fixes
12
27
  lint [path] [--json] Lint every SPEC.md under path (default: current directory)
13
28
  claims [path] [--json] Join criteria to the test names that claim them — no reports needed
29
+ verify [path] [--json] Run .speccle/checks/ against the change set: cross-file invariants
30
+ risk [path] [--json] Score the change set from spec-aware signals; gate on the review threshold
31
+ calibrate record [path] Append a calibration entry: the risk floor + your honest verdict
32
+ calibrate report [path] Read the calibration record: signal reliability + the supported threshold
33
+ remedy record [path] Record a remedy: the finding, the fix, and the prevention artefact
34
+ remedy recall [path] Recall the known remedy for a finding's class — fix consistently
35
+ review init [path] [--json] Scaffold the opt-in CI driver: a GitHub Actions workflow, pinned
36
+ review run [path] Review a pull request with the lens panel and post one review.
37
+ The one command here that calls a model — needs a metered key
14
38
  strength [path] [--json] Oracle-strength heatmap: per-criterion killed ÷ covered
15
39
  strength init [path] [--json] Provision the strength stack: devDependencies + configs
40
+ --version, -v Print the installed CLI version
16
41
 
17
- claims options:
42
+ claims / risk options:
18
43
  --dialect <name> Test dialect: ${DIALECT_NAMES.join(", ")} (default: ${DEFAULT_DIALECT})
19
44
 
45
+ verify / risk options:
46
+ --base <ref> Read the change set from the commits between <ref> and HEAD, instead of the
47
+ working tree's pending change — what a CI driver needs, where the tree is
48
+ clean. Measured from the merge base, so it needs their shared history
49
+
50
+ risk exit codes: 0 below the review threshold (review may fix), 1 at or above it (human required)
51
+
52
+ calibrate record options:
53
+ --needed-human <true|false> Did this change actually need a human? (required — the honest verdict)
54
+ --found-real <true|false> Did the review find something real? (required)
55
+ --escalated A risk lens escalated beyond the deterministic floor
56
+ --note <text> Free-text context for the entry
57
+ --dialect <name> Test dialect: ${DIALECT_NAMES.join(", ")} (default: ${DEFAULT_DIALECT})
58
+
59
+ remedy record options:
60
+ --class <handle> Short, stable handle for the finding's class (required — the recall key)
61
+ --finding <text> What the finding is (required)
62
+ --fix <text> The fix applied to the code (required)
63
+ --route <route> Prevention route: ${REMEDY_ROUTES.join(", ")} (required)
64
+ --artefact <ref> The prevention artefact: a .speccle/checks|lenses path or a SPEC
65
+ criterion id (required for every route but none)
66
+ --note <text> Free-text context for the entry
67
+
68
+ remedy recall options:
69
+ --class <handle> The finding's class to look up (required)
70
+
71
+ review run options:
72
+ --pr <number> The pull request to review (required)
73
+ --repo <owner/name> Defaults to GITHUB_REPOSITORY
74
+ --base <ref> Base ref for the risk verdict (default: origin/<the PR's base>)
75
+ --force Review again even if this driver already reviewed the PR
76
+ --model <id> Overrides SPECCLE_REVIEW_MODEL
77
+ --dry-run Report what would be posted, and post nothing
78
+ Reads ANTHROPIC_API_KEY and GITHUB_TOKEN from the environment
79
+
20
80
  strength options:
21
81
  --check Report whether the reports are fresh, stale, or missing — never runs them
22
82
  --mutation <file> Stryker JSON report (default: ${DEFAULT_MUTATION_REPORT})
@@ -30,12 +90,50 @@ strength init options:
30
90
  Exit codes: 0 clean, 1 violations, 2 usage error`;
31
91
  async function main(argv) {
32
92
  const [command, ...rest] = argv;
93
+ if (command === "--version" || command === "-v") {
94
+ console.log(await ownVersion());
95
+ return 0;
96
+ }
97
+ if (command === "init")
98
+ return runInit(rest);
99
+ if (command === "doctor")
100
+ return runDoctor(rest);
101
+ if (command === "update")
102
+ return runUpdate(rest);
33
103
  if (command === "lint")
34
104
  return runLint(rest);
35
105
  if (command === "claims")
36
106
  return runClaims(rest);
107
+ if (command === "verify")
108
+ return runVerify(rest);
109
+ if (command === "risk")
110
+ return runRisk(rest);
111
+ if (command === "calibrate" && rest[0] === "record")
112
+ return runCalibrateRecord(rest.slice(1));
113
+ if (command === "calibrate" && rest[0] === "report")
114
+ return runCalibrateReport(rest.slice(1));
115
+ if (command === "calibrate") {
116
+ console.error(`calibrate needs a subcommand: record or report\n\n${USAGE}`);
117
+ return 2;
118
+ }
119
+ if (command === "remedy" && rest[0] === "record")
120
+ return runRemedyRecord(rest.slice(1));
121
+ if (command === "remedy" && rest[0] === "recall")
122
+ return runRemedyRecall(rest.slice(1));
123
+ if (command === "remedy") {
124
+ console.error(`remedy needs a subcommand: record or recall\n\n${USAGE}`);
125
+ return 2;
126
+ }
127
+ if (command === "review" && rest[0] === "init")
128
+ return runReviewInit(rest.slice(1));
129
+ if (command === "review" && rest[0] === "run")
130
+ return runReviewRun(rest.slice(1));
131
+ if (command === "review") {
132
+ console.error(`review needs a subcommand: init or run\n\n${USAGE}`);
133
+ return 2;
134
+ }
37
135
  if (command === "strength" && rest[0] === "init")
38
- return runInit(rest.slice(1));
136
+ return runStrengthInit(rest.slice(1));
39
137
  if (command === "strength")
40
138
  return runStrength(rest);
41
139
  console.error(USAGE);
@@ -79,6 +177,356 @@ async function runClaims(args) {
79
177
  console.log(json ? JSON.stringify(report, null, 2) : renderClaims(report));
80
178
  return report.clean ? 0 : 1;
81
179
  }
180
+ async function runVerify(args) {
181
+ let json = false;
182
+ let base;
183
+ const positional = [];
184
+ for (let i = 0; i < args.length; i++) {
185
+ const arg = args[i];
186
+ if (arg === "--json")
187
+ json = true;
188
+ else if (arg === "--base") {
189
+ const value = args[++i];
190
+ if (value === undefined) {
191
+ console.error(`--base needs a git ref\n\n${USAGE}`);
192
+ return 2;
193
+ }
194
+ base = value;
195
+ }
196
+ else if (arg.startsWith("-")) {
197
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
198
+ return 2;
199
+ }
200
+ else
201
+ positional.push(arg);
202
+ }
203
+ if (positional.length > 1) {
204
+ console.error(`verify takes at most one path\n\n${USAGE}`);
205
+ return 2;
206
+ }
207
+ let report;
208
+ try {
209
+ report = await verify(positional[0] ?? ".", { ...(base !== undefined && { base }) });
210
+ }
211
+ catch (err) {
212
+ console.error(message(err));
213
+ return 2;
214
+ }
215
+ console.log(json ? JSON.stringify(report, null, 2) : renderVerify(report));
216
+ return report.clean ? 0 : 1;
217
+ }
218
+ async function runRisk(args) {
219
+ let json = false;
220
+ let dialect;
221
+ let base;
222
+ const positional = [];
223
+ for (let i = 0; i < args.length; i++) {
224
+ const arg = args[i];
225
+ if (arg === "--json")
226
+ json = true;
227
+ else if (arg === "--dialect" || arg === "--base") {
228
+ const value = args[++i];
229
+ if (value === undefined) {
230
+ console.error(`${arg} needs ${arg === "--base" ? "a git ref" : "a dialect name"}\n\n${USAGE}`);
231
+ return 2;
232
+ }
233
+ if (arg === "--dialect")
234
+ dialect = value;
235
+ else
236
+ base = value;
237
+ }
238
+ else if (arg.startsWith("-")) {
239
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
240
+ return 2;
241
+ }
242
+ else
243
+ positional.push(arg);
244
+ }
245
+ if (positional.length > 1) {
246
+ console.error(`risk takes at most one path\n\n${USAGE}`);
247
+ return 2;
248
+ }
249
+ let report;
250
+ try {
251
+ report = await risk(positional[0] ?? ".", {
252
+ ...(dialect !== undefined && { dialect }),
253
+ ...(base !== undefined && { base }),
254
+ });
255
+ }
256
+ catch (err) {
257
+ console.error(message(err));
258
+ return 2;
259
+ }
260
+ console.log(json ? JSON.stringify(report, null, 2) : renderRisk(report));
261
+ return report.humanRequired ? 1 : 0;
262
+ }
263
+ async function runCalibrateRecord(args) {
264
+ let json = false;
265
+ let dialect;
266
+ let neededHuman;
267
+ let foundReal;
268
+ let escalated = false;
269
+ let note;
270
+ const positional = [];
271
+ for (let i = 0; i < args.length; i++) {
272
+ const arg = args[i];
273
+ if (arg === "--json")
274
+ json = true;
275
+ else if (arg === "--escalated")
276
+ escalated = true;
277
+ else if (arg === "--needed-human" || arg === "--found-real") {
278
+ const value = args[++i];
279
+ if (value !== "true" && value !== "false") {
280
+ console.error(`${arg} needs true or false\n\n${USAGE}`);
281
+ return 2;
282
+ }
283
+ if (arg === "--needed-human")
284
+ neededHuman = value === "true";
285
+ else
286
+ foundReal = value === "true";
287
+ }
288
+ else if (arg === "--note" || arg === "--dialect") {
289
+ const value = args[++i];
290
+ if (value === undefined) {
291
+ console.error(`${arg} needs a value\n\n${USAGE}`);
292
+ return 2;
293
+ }
294
+ if (arg === "--note")
295
+ note = value;
296
+ else
297
+ dialect = value;
298
+ }
299
+ else if (arg.startsWith("-")) {
300
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
301
+ return 2;
302
+ }
303
+ else
304
+ positional.push(arg);
305
+ }
306
+ if (positional.length > 1) {
307
+ console.error(`calibrate record takes at most one path\n\n${USAGE}`);
308
+ return 2;
309
+ }
310
+ // The honest verdict is required, never defaulted — a fabricated verdict is the dishonest
311
+ // calibration data ADR-0042 exists to keep out of the record.
312
+ if (neededHuman === undefined || foundReal === undefined) {
313
+ console.error(`calibrate record needs --needed-human and --found-real\n\n${USAGE}`);
314
+ return 2;
315
+ }
316
+ let report;
317
+ try {
318
+ report = await recordCalibration(positional[0] ?? ".", { neededHuman, foundReal, escalated, ...(note !== undefined && { note }) }, { ...(dialect !== undefined && { dialect }) });
319
+ }
320
+ catch (err) {
321
+ console.error(message(err));
322
+ return 2;
323
+ }
324
+ console.log(json ? JSON.stringify(report, null, 2) : renderCalibrateRecord(report));
325
+ return 0;
326
+ }
327
+ async function runCalibrateReport(args) {
328
+ let json = false;
329
+ const positional = [];
330
+ for (const arg of args) {
331
+ if (arg === "--json")
332
+ json = true;
333
+ else if (arg.startsWith("-")) {
334
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
335
+ return 2;
336
+ }
337
+ else
338
+ positional.push(arg);
339
+ }
340
+ if (positional.length > 1) {
341
+ console.error(`calibrate report takes at most one path\n\n${USAGE}`);
342
+ return 2;
343
+ }
344
+ let report;
345
+ try {
346
+ report = await calibrationReport(positional[0] ?? ".");
347
+ }
348
+ catch (err) {
349
+ console.error(message(err));
350
+ return 2;
351
+ }
352
+ console.log(json ? JSON.stringify(report, null, 2) : renderCalibrateReport(report));
353
+ return 0;
354
+ }
355
+ async function runRemedyRecord(args) {
356
+ let json = false;
357
+ let classHandle;
358
+ let finding;
359
+ let fix;
360
+ let route;
361
+ let artefact;
362
+ let note;
363
+ const positional = [];
364
+ for (let i = 0; i < args.length; i++) {
365
+ const arg = args[i];
366
+ if (arg === "--json")
367
+ json = true;
368
+ else if (arg === "--class" ||
369
+ arg === "--finding" ||
370
+ arg === "--fix" ||
371
+ arg === "--route" ||
372
+ arg === "--artefact" ||
373
+ arg === "--note") {
374
+ const value = args[++i];
375
+ if (value === undefined) {
376
+ console.error(`${arg} needs a value\n\n${USAGE}`);
377
+ return 2;
378
+ }
379
+ if (arg === "--class")
380
+ classHandle = value;
381
+ else if (arg === "--finding")
382
+ finding = value;
383
+ else if (arg === "--fix")
384
+ fix = value;
385
+ else if (arg === "--route")
386
+ route = value;
387
+ else if (arg === "--artefact")
388
+ artefact = value;
389
+ else
390
+ note = value;
391
+ }
392
+ else if (arg.startsWith("-")) {
393
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
394
+ return 2;
395
+ }
396
+ else
397
+ positional.push(arg);
398
+ }
399
+ if (positional.length > 1) {
400
+ console.error(`remedy record takes at most one path\n\n${USAGE}`);
401
+ return 2;
402
+ }
403
+ if (classHandle === undefined ||
404
+ finding === undefined ||
405
+ fix === undefined ||
406
+ route === undefined) {
407
+ console.error(`remedy record needs --class, --finding, --fix, and --route\n\n${USAGE}`);
408
+ return 2;
409
+ }
410
+ if (!REMEDY_ROUTES.includes(route)) {
411
+ console.error(`--route must be one of ${REMEDY_ROUTES.join(", ")}\n\n${USAGE}`);
412
+ return 2;
413
+ }
414
+ let report;
415
+ try {
416
+ report = await recordRemedy(positional[0] ?? ".", {
417
+ class: classHandle,
418
+ finding,
419
+ fix,
420
+ route: route,
421
+ ...(artefact !== undefined && { artefact }),
422
+ ...(note !== undefined && { note }),
423
+ });
424
+ }
425
+ catch (err) {
426
+ console.error(message(err));
427
+ return 2;
428
+ }
429
+ console.log(json ? JSON.stringify(report, null, 2) : renderRemedyRecord(report));
430
+ return 0;
431
+ }
432
+ async function runRemedyRecall(args) {
433
+ let json = false;
434
+ let classHandle;
435
+ const positional = [];
436
+ for (let i = 0; i < args.length; i++) {
437
+ const arg = args[i];
438
+ if (arg === "--json")
439
+ json = true;
440
+ else if (arg === "--class") {
441
+ const value = args[++i];
442
+ if (value === undefined) {
443
+ console.error(`--class needs a value\n\n${USAGE}`);
444
+ return 2;
445
+ }
446
+ classHandle = value;
447
+ }
448
+ else if (arg.startsWith("-")) {
449
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
450
+ return 2;
451
+ }
452
+ else
453
+ positional.push(arg);
454
+ }
455
+ if (positional.length > 1) {
456
+ console.error(`remedy recall takes at most one path\n\n${USAGE}`);
457
+ return 2;
458
+ }
459
+ if (classHandle === undefined) {
460
+ console.error(`remedy recall needs --class\n\n${USAGE}`);
461
+ return 2;
462
+ }
463
+ let report;
464
+ try {
465
+ report = await recallRemedy(positional[0] ?? ".", classHandle);
466
+ }
467
+ catch (err) {
468
+ console.error(message(err));
469
+ return 2;
470
+ }
471
+ console.log(json ? JSON.stringify(report, null, 2) : renderRemedyRecall(report));
472
+ return 0;
473
+ }
474
+ async function runDoctor(args) {
475
+ let json = false;
476
+ const positional = [];
477
+ for (const arg of args) {
478
+ if (arg === "--json")
479
+ json = true;
480
+ else if (arg.startsWith("-")) {
481
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
482
+ return 2;
483
+ }
484
+ else
485
+ positional.push(arg);
486
+ }
487
+ if (positional.length > 1) {
488
+ console.error(`doctor takes at most one path\n\n${USAGE}`);
489
+ return 2;
490
+ }
491
+ let report;
492
+ try {
493
+ report = await doctor(positional[0] ?? ".");
494
+ }
495
+ catch (err) {
496
+ console.error(message(err));
497
+ return 2;
498
+ }
499
+ console.log(json ? JSON.stringify(report, null, 2) : renderDoctor(report));
500
+ return report.ok ? 0 : 1;
501
+ }
502
+ async function runUpdate(args) {
503
+ let json = false;
504
+ const positional = [];
505
+ for (const arg of args) {
506
+ if (arg === "--json")
507
+ json = true;
508
+ else if (arg.startsWith("-")) {
509
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
510
+ return 2;
511
+ }
512
+ else
513
+ positional.push(arg);
514
+ }
515
+ if (positional.length > 1) {
516
+ console.error(`update takes at most one path\n\n${USAGE}`);
517
+ return 2;
518
+ }
519
+ let report;
520
+ try {
521
+ report = await update(positional[0] ?? ".");
522
+ }
523
+ catch (err) {
524
+ console.error(message(err));
525
+ return 2;
526
+ }
527
+ console.log(json ? JSON.stringify(report, null, 2) : renderUpdate(report));
528
+ return 0;
529
+ }
82
530
  async function runLint(args) {
83
531
  let json = false;
84
532
  const positional = [];
@@ -172,6 +620,150 @@ async function runStrength(args) {
172
620
  return 0;
173
621
  }
174
622
  async function runInit(args) {
623
+ let json = false;
624
+ const positional = [];
625
+ for (const arg of args) {
626
+ if (arg === "--json")
627
+ json = true;
628
+ else if (arg.startsWith("-")) {
629
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
630
+ return 2;
631
+ }
632
+ else
633
+ positional.push(arg);
634
+ }
635
+ if (positional.length > 1) {
636
+ console.error(`init takes at most one path\n\n${USAGE}`);
637
+ return 2;
638
+ }
639
+ const root = positional[0] ?? ".";
640
+ let config;
641
+ let skills;
642
+ let lenses;
643
+ try {
644
+ // Materialize first, then stamp the version onto the config — so the recorded anchors
645
+ // only ever name the skills and lenses that actually landed on disk.
646
+ skills = await materializeSkills(root);
647
+ lenses = await materializeLenses(root);
648
+ config = await initConfig(root, await ownVersion());
649
+ }
650
+ catch (err) {
651
+ console.error(message(err));
652
+ return 2;
653
+ }
654
+ if (json) {
655
+ console.log(JSON.stringify({ config, skills, lenses }, null, 2));
656
+ }
657
+ else {
658
+ console.log(renderConfigInit(config));
659
+ console.log("");
660
+ console.log(renderSkillsInit(skills));
661
+ console.log("");
662
+ console.log(renderLensesInit(lenses));
663
+ }
664
+ return 0;
665
+ }
666
+ async function runReviewInit(args) {
667
+ let json = false;
668
+ const positional = [];
669
+ for (const arg of args) {
670
+ if (arg === "--json")
671
+ json = true;
672
+ else if (arg.startsWith("-")) {
673
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
674
+ return 2;
675
+ }
676
+ else
677
+ positional.push(arg);
678
+ }
679
+ if (positional.length > 1) {
680
+ console.error(`review init takes at most one path\n\n${USAGE}`);
681
+ return 2;
682
+ }
683
+ let report;
684
+ try {
685
+ report = await scaffoldReviewWorkflow(positional[0] ?? ".");
686
+ }
687
+ catch (err) {
688
+ console.error(message(err));
689
+ return 2;
690
+ }
691
+ console.log(json ? JSON.stringify(report, null, 2) : renderReviewInit(report));
692
+ return 0;
693
+ }
694
+ async function runReviewRun(args) {
695
+ let json = false;
696
+ let force = false;
697
+ let dryRun = false;
698
+ let pr;
699
+ let repo;
700
+ let base;
701
+ let model;
702
+ const positional = [];
703
+ for (let i = 0; i < args.length; i++) {
704
+ const arg = args[i];
705
+ if (arg === "--json")
706
+ json = true;
707
+ else if (arg === "--force")
708
+ force = true;
709
+ else if (arg === "--dry-run")
710
+ dryRun = true;
711
+ else if (arg === "--pr" || arg === "--repo" || arg === "--base" || arg === "--model") {
712
+ const value = args[++i];
713
+ if (value === undefined) {
714
+ console.error(`${arg} needs a value\n\n${USAGE}`);
715
+ return 2;
716
+ }
717
+ if (arg === "--pr") {
718
+ const number = Number(value);
719
+ if (!Number.isInteger(number) || number <= 0) {
720
+ console.error(`--pr needs a pull request number\n\n${USAGE}`);
721
+ return 2;
722
+ }
723
+ pr = number;
724
+ }
725
+ else if (arg === "--repo")
726
+ repo = value;
727
+ else if (arg === "--base")
728
+ base = value;
729
+ else
730
+ model = value;
731
+ }
732
+ else if (arg.startsWith("-")) {
733
+ console.error(`Unknown option: ${arg}\n\n${USAGE}`);
734
+ return 2;
735
+ }
736
+ else
737
+ positional.push(arg);
738
+ }
739
+ if (positional.length > 1) {
740
+ console.error(`review run takes at most one path\n\n${USAGE}`);
741
+ return 2;
742
+ }
743
+ if (pr === undefined) {
744
+ console.error(`review run needs --pr\n\n${USAGE}`);
745
+ return 2;
746
+ }
747
+ let report;
748
+ try {
749
+ report = await reviewRun(positional[0] ?? ".", {
750
+ pr,
751
+ force,
752
+ dryRun,
753
+ ...(repo !== undefined && { repo }),
754
+ ...(base !== undefined && { base }),
755
+ ...(model !== undefined && { model }),
756
+ });
757
+ }
758
+ catch (err) {
759
+ console.error(message(err));
760
+ return 2;
761
+ }
762
+ console.log(json ? JSON.stringify(report, null, 2) : renderReviewRun(report));
763
+ // Posting findings is not a failure: the risk gate is the check, and it runs as its own step.
764
+ return 0;
765
+ }
766
+ async function runStrengthInit(args) {
175
767
  let json = false;
176
768
  let skipInstall = false;
177
769
  const mutate = [];