@integrity-labs/agt-cli 0.28.836 → 0.28.838

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.
@@ -16,8 +16,9 @@
16
16
  * That copy is now DOWNSTREAM of this one. Do NOT add an equality sync test:
17
17
  * this copy is expected to diverge, and already has — `refuseIdentity()` below
18
18
  * makes a write without the VERIFIED review-bot identity impossible here, while
19
- * the plugin copy stays permissive for the human who runs it. A `--min-severity`
20
- * floor is the remaining planned divergence (ENG-10001).
19
+ * the plugin copy stays permissive for the human who runs it. `--min-severity`
20
+ * (below) is the second such divergence, and for the same reason: a human reads
21
+ * every finding and decides; an unattended agent posts what it is handed.
21
22
  */
22
23
  /**
23
24
  * post-review-findings — deliver `/s:code-review` and `/s:security-review`
@@ -48,6 +49,7 @@
48
49
  * post-review-findings.mjs --pr 5409 --post
49
50
  * post-review-findings.mjs --pr 5409 --status
50
51
  * post-review-findings.mjs --pr 5409 --resolve <id> --reason "..."
52
+ * post-review-findings.mjs --pr 5409 --post --min-severity major --min-severity P2
51
53
  */
52
54
 
53
55
  import { execFileSync } from 'node:child_process';
@@ -91,7 +93,10 @@ export function marker(id) {
91
93
  }
92
94
 
93
95
  export function buildBody(f, id) {
94
- const sev = String(f.severity ?? 'minor').toUpperCase();
96
+ // NOT `?? 'minor'`. That printed a severity nobody assigned, and once a floor
97
+ // exists it would also make an unlabelled finding filterable — a silent drop
98
+ // caused entirely by the default. Unrated says what is true.
99
+ const sev = f.severity ? String(f.severity).toUpperCase() : 'UNRATED';
95
100
  const cat = f.category ? ` · ${f.category}` : '';
96
101
  return [
97
102
  `**${sev}${cat}** — ${f.summary}`,
@@ -104,6 +109,132 @@ export function buildBody(f, id) {
104
109
  ].join('\n');
105
110
  }
106
111
 
112
+ /**
113
+ * The severity floor, and why it does NOT merge the two scales into one order.
114
+ *
115
+ * The two producers declare two different vocabularies and neither defines what
116
+ * its levels mean:
117
+ * `/s:code-review` — "severity": "major|minor" (commands/code-review.md)
118
+ * `/s:security-review` — "severity": "P1|P2|P3" (commands/security-review.md)
119
+ *
120
+ * Measured over the 54 artefacts in `.smithers/review` on 2026-09-07 (39
121
+ * findings): the code lane emitted major x10 / minor x21, the security lane
122
+ * emitted P2 x1 / P3 x5, and P0 appears nowhere — in the artefacts, in either
123
+ * command, or in this tree. The lanes do not share a token.
124
+ *
125
+ * So a single merged rank would need a crosswalk (is `major` a P1 or a P2?)
126
+ * that NOTHING in this repo states. Inventing one is not a tidy-up, it decides
127
+ * something: place `major` above P2 and `--min-severity major` mutes 100% of
128
+ * the security lane's observed output — every finding it has ever produced —
129
+ * while reading like an ordinary noise filter. That is a suppression nobody
130
+ * chose, invisible on the wire, which is the failure this reviewer exists to
131
+ * find in other people's code.
132
+ *
133
+ * Comparison is therefore SCALE-LOCAL. A floor filters only findings written in
134
+ * its own vocabulary; anything on the other scale, or on no scale, is posted
135
+ * and reported. Concretely: `--min-severity major` drops `minor` and leaves
136
+ * every P-scale finding standing; `--min-severity P2` drops `P3` and leaves
137
+ * every code finding standing. If the producers ever agree a crosswalk, it
138
+ * belongs here as one table with that agreement cited — not guessed from the
139
+ * shape of the words.
140
+ *
141
+ * A consequence, and the reason `--min-severity` REPEATS: one value can never
142
+ * floor both lanes. "Post only the serious ones" against a two-lane producer is
143
+ * two floors — `--min-severity major --min-severity P2` — so the flag collects
144
+ * rather than overwrites. Repeating it for the SAME scale is refused instead of
145
+ * last-winning, on the rule this file already applies to `--status --resolve`:
146
+ * a flag that is accepted and then ignored is indistinguishable from one that
147
+ * was honoured.
148
+ */
149
+ export const SEVERITY_SCALES = {
150
+ code: { major: 2, minor: 1 },
151
+ security: { p1: 3, p2: 2, p3: 1 },
152
+ };
153
+
154
+ /** `{ scale, rank }` for a severity token, or null when it is on no known scale. */
155
+ export function severityRank(value) {
156
+ if (value === undefined || value === null) return null;
157
+ const k = String(value).trim().toLowerCase();
158
+ for (const [scale, table] of Object.entries(SEVERITY_SCALES)) {
159
+ if (Object.hasOwn(table, k)) return { scale, rank: table[k] };
160
+ }
161
+ return null;
162
+ }
163
+
164
+ /**
165
+ * Resolve the `--min-severity` argument, or throw.
166
+ *
167
+ * Fail-CLOSED here, unlike the fail-open treatment of an unknown finding
168
+ * severity below, because the two unknowns fail in opposite directions. An
169
+ * unrecognised FLOOR would compare against nothing and silently filter nothing
170
+ * — a run that reads exactly like a floor that was honoured and had no work to
171
+ * do. There is no output that distinguishes them, so it is refused instead.
172
+ */
173
+ export function parseMinSeverity(value) {
174
+ const r = severityRank(value);
175
+ if (r) return { ...r, token: String(value).trim() };
176
+ const known = Object.values(SEVERITY_SCALES).flatMap((t) => Object.keys(t)).join(', ');
177
+ throw new Error(
178
+ `--min-severity "${value}" is not a severity this reviewer knows. Use one of: ${known} ` +
179
+ '(case-insensitive). Floors are compared within one scale only — see SEVERITY_SCALES.',
180
+ );
181
+ }
182
+
183
+ /** Resolve every `--min-severity` into one floor per scale, or throw. */
184
+ export function parseMinSeverityList(values) {
185
+ const floors = {};
186
+ for (const v of values) {
187
+ const f = parseMinSeverity(v);
188
+ if (floors[f.scale]) {
189
+ throw new Error(
190
+ `--min-severity given twice for the ${f.scale} scale ("${floors[f.scale].token}" and "${f.token}"). ` +
191
+ 'Only one can apply, and silently keeping the last is indistinguishable from honouring both. ' +
192
+ 'Pass one floor per scale.',
193
+ );
194
+ }
195
+ floors[f.scale] = f;
196
+ }
197
+ return floors;
198
+ }
199
+
200
+ /**
201
+ * Split findings into those that clear the floor and those it withholds.
202
+ *
203
+ * Everything the floor cannot compare is KEPT, each carrying `_kept` saying why.
204
+ * Fail-open is the only safe direction: a finding that posts when it need not is
205
+ * noise a reader can skip, while one withheld by a comparison that never
206
+ * happened is invisible to everyone, including the agent that produced it.
207
+ *
208
+ * `blocking: true` also bypasses the floor. All four findings carrying that
209
+ * field in the measured set are `false`, so this rule has never yet fired — it
210
+ * is here because the alternative is a reviewer withholding a finding its own
211
+ * producer marked as blocking, and the cost of the rule while it lies dormant
212
+ * is nil.
213
+ */
214
+ export function applySeverityFloor(findings, floors) {
215
+ if (!floors || Object.keys(floors).length === 0) return { kept: findings, suppressed: [] };
216
+ const kept = [];
217
+ const suppressed = [];
218
+ for (const f of findings) {
219
+ if (f.blocking === true) {
220
+ kept.push({ ...f, _kept: 'marked blocking' });
221
+ continue;
222
+ }
223
+ const r = severityRank(f.severity);
224
+ const floor = r ? floors[r.scale] : undefined;
225
+ if (!r) {
226
+ kept.push({ ...f, _kept: f.severity ? `severity "${f.severity}" is on no known scale` : 'no severity' });
227
+ } else if (!floor) {
228
+ kept.push({ ...f, _kept: `no floor given for the ${r.scale} scale` });
229
+ } else if (r.rank >= floor.rank) {
230
+ kept.push(f);
231
+ } else {
232
+ suppressed.push(f);
233
+ }
234
+ }
235
+ return { kept, suppressed };
236
+ }
237
+
107
238
  /** Thin `gh` wrapper. `input` is piped to stdin, for endpoints taking a body. */
108
239
  let AUTH_TOKEN = null;
109
240
 
@@ -383,6 +514,9 @@ export function parseArgs(argv) {
383
514
  else if (k === '--dir') a.dir = argv[++i];
384
515
  else if (k === '--resolve') a.resolve = argv[++i];
385
516
  else if (k === '--reason') a.reason = argv[++i];
517
+ // Collects, rather than overwrites — see SEVERITY_SCALES: one value cannot
518
+ // floor both lanes, so repeating the flag is the intended way to floor both.
519
+ else if (k === '--min-severity') (a.minSeverity ??= []).push(argv[++i]);
386
520
  }
387
521
  return a;
388
522
  }
@@ -460,6 +594,9 @@ export function refuseIdentity({ kind, willWrite }) {
460
594
  async function main() {
461
595
  const a = parseArgs(process.argv.slice(2));
462
596
  if (!a.pr) throw new Error('--pr is required');
597
+ // Before the identity hop and before the PR is read: an unusable floor should
598
+ // cost one line of output, not a scan that then cannot be delivered as asked.
599
+ const floors = a.minSeverity === undefined ? null : parseMinSeverityList(a.minSeverity);
463
600
  const repo = a.repo ?? gh(['repo', 'view', '--json', 'nameWithOwner'], {}).nameWithOwner;
464
601
  const dir = a.dir ?? REVIEW_DIR;
465
602
 
@@ -520,7 +657,8 @@ async function main() {
520
657
  }
521
658
  // A merged or closed PR takes comments happily and shows them to nobody.
522
659
  const head = view.headRefOid;
523
- const findings = loadFindings(dir, a.pr, head);
660
+ const loaded = loadFindings(dir, a.pr, head);
661
+ const { kept: findings, suppressed } = applySeverityFloor(loaded, floors);
524
662
  // --slurp (gh >= 2.44) is REQUIRED with --paginate here: without it gh emits
525
663
  // one JSON array PER PAGE, and gh()'s JSON.parse of the concatenation throws
526
664
  // `Unexpected token [`. That makes --post fail on exactly the large PRs it is
@@ -541,7 +679,18 @@ async function main() {
541
679
  const newInline = inline.filter((f) => !already.has(findingId(f)));
542
680
  const newOutside = outside.filter((f) => !already.has(findingId(f)));
543
681
 
544
- console.log(`PR #${a.pr} @ ${head.slice(0, 9)} — ${findings.length} finding(s) loaded`);
682
+ console.log(`PR #${a.pr} @ ${head.slice(0, 9)} — ${loaded.length} finding(s) loaded`);
683
+ if (floors) {
684
+ const unfiltered = findings.filter((f) => f._kept);
685
+ const label = Object.values(floors).map((f) => `${f.token} (${f.scale})`).join(', ');
686
+ console.log(
687
+ ` severity floor: ${label} — ${suppressed.length} withheld, ${unfiltered.length} kept unfiltered`,
688
+ );
689
+ // A withheld finding exists nowhere else once this process exits, so it is
690
+ // named here rather than counted. A count alone cannot be checked.
691
+ for (const f of suppressed) console.log(` [withheld] ${f.severity} ${f.file} ${findingId(f)} ${f.summary.slice(0, 60)}`);
692
+ for (const f of unfiltered) console.log(` [kept] ${f.severity ?? '-'} ${f.file} ${findingId(f)} (${f._kept})`);
693
+ }
545
694
  console.log(` inline-anchorable: ${inline.length} outside-diff: ${outside.length}`);
546
695
  console.log(` already posted: ${inline.length + outside.length - newInline.length - newOutside.length}`);
547
696
  console.log(` would post now: ${newInline.length} inline + ${newOutside.length} in the body\n`);
package/dist/bin/agt.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-E6B43IJZ.js";
43
+ } from "../chunk-MZNTGZA7.js";
44
44
  import {
45
45
  getProjectDir,
46
46
  isSessionResumeDisabled,
@@ -5467,7 +5467,7 @@ import { execFileSync, execSync } from "child_process";
5467
5467
  import { existsSync as existsSync11, realpathSync as realpathSync2 } from "fs";
5468
5468
  import chalk18 from "chalk";
5469
5469
  import ora16 from "ora";
5470
- var cliVersion = true ? "0.28.836" : "dev";
5470
+ var cliVersion = true ? "0.28.838" : "dev";
5471
5471
  async function fetchLatestVersion() {
5472
5472
  const host2 = getHost();
5473
5473
  if (!host2) return null;
@@ -6658,7 +6658,7 @@ function handleError(err) {
6658
6658
  }
6659
6659
 
6660
6660
  // src/bin/agt.ts
6661
- var cliVersion2 = true ? "0.28.836" : "dev";
6661
+ var cliVersion2 = true ? "0.28.838" : "dev";
6662
6662
  var program = new Command();
6663
6663
  program.name("agt").description("Augmented CLI \u2014 agent provisioning and management").version(cliVersion2).option("--json", "Emit machine-readable JSON output (suppress spinners and colors)").option("--skip-update-check", "Skip the automatic update check on startup");
6664
6664
  program.hook("preAction", async (thisCommand, actionCommand) => {
@@ -451,7 +451,7 @@ function orderTitlesForDescription(entries) {
451
451
  // ../../packages/core/dist/provisioning/frameworks/claudecode/index.js
452
452
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, mkdirSync as mkdirSync3, existsSync as existsSync4, chmodSync as chmodSync4, readdirSync, rmSync as rmSync2, copyFileSync, lstatSync, realpathSync, symlinkSync, readlinkSync, renameSync as renameSync4, opendirSync } from "fs";
453
453
  import { join as join3, relative, dirname as dirname3 } from "path";
454
- import { homedir as homedir3 } from "os";
454
+ import { homedir as homedir3, tmpdir } from "os";
455
455
  import { execFile } from "child_process";
456
456
 
457
457
  // ../../packages/core/dist/integrations/xurl-config.js
@@ -1864,6 +1864,34 @@ function sweepScratchDir(codeName, now = Date.now()) {
1864
1864
  }
1865
1865
  return removed;
1866
1866
  }
1867
+ var STRANDED_ARTEFACT_TMP_PREFIXES = [
1868
+ "augmented-artefact-",
1869
+ "augmented-artefact-source-"
1870
+ ];
1871
+ function sweepStrandedArtefactTmpDirs(now = Date.now()) {
1872
+ const root = tmpdir();
1873
+ let entries;
1874
+ try {
1875
+ entries = readdirSync(root);
1876
+ } catch {
1877
+ return 0;
1878
+ }
1879
+ const cutoff = now - SCRATCH_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
1880
+ let removed = 0;
1881
+ for (const entry of entries) {
1882
+ if (!STRANDED_ARTEFACT_TMP_PREFIXES.some((prefix) => entry.startsWith(prefix)))
1883
+ continue;
1884
+ const full = join3(root, entry);
1885
+ try {
1886
+ if (isFreshWithin(full, cutoff))
1887
+ continue;
1888
+ rmSync2(full, { recursive: true, force: true });
1889
+ removed += 1;
1890
+ } catch {
1891
+ }
1892
+ }
1893
+ return removed;
1894
+ }
1867
1895
  var SCRATCH_SCAN_ENTRY_BUDGET = 5e3;
1868
1896
  function isFreshWithin(path, cutoff) {
1869
1897
  let budget = SCRATCH_SCAN_ENTRY_BUDGET;
@@ -2051,6 +2079,7 @@ function deployArtifactsToProject(codeName, provisionDir) {
2051
2079
  mkdirSync3(getScratchDir(codeName), { recursive: true });
2052
2080
  mkdirSync3(getAgentTmpDir(codeName), { recursive: true });
2053
2081
  sweepScratchDir(codeName);
2082
+ sweepStrandedArtefactTmpDirs();
2054
2083
  } catch (err) {
2055
2084
  process.stderr.write(`[scratch] [ensure-or-sweep-failed] agent=${codeName} error=${err.message}
2056
2085
  `);
@@ -6537,7 +6566,7 @@ function exchangeFailureKind(err) {
6537
6566
  }
6538
6567
 
6539
6568
  // src/lib/api-client.ts
6540
- var agtCliVersion = true ? "0.28.836" : "dev";
6569
+ var agtCliVersion = true ? "0.28.838" : "dev";
6541
6570
  var lastConfigHash = null;
6542
6571
  function setConfigHash(hash) {
6543
6572
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -10895,4 +10924,4 @@ export {
10895
10924
  managerInstallSystemUnitCommand,
10896
10925
  managerUninstallSystemUnitCommand
10897
10926
  };
10898
- //# sourceMappingURL=chunk-E6B43IJZ.js.map
10927
+ //# sourceMappingURL=chunk-MZNTGZA7.js.map