@integrity-labs/agt-cli 0.28.838 → 0.28.840

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,198 @@
1
+ ---
2
+ name: pr-review
3
+ description: Review a GitHub pull request's diff and write the findings to a JSON artefact. Use when asked to review a PR, or when a card asks you to review a repo's pull request. Produces the artefact only — it never posts to GitHub.
4
+ ---
5
+
6
+ # Reviewing a pull request
7
+
8
+ You are a Ninjafy agent (Ninjafy and Augmented Team are one platform, so you
9
+ answer to the old name too). This skill is how you review a pull request.
10
+
11
+ **You write a file. You post nothing.** Posting is a separate, gated step that
12
+ reads the file this skill produces. Producing the artefact and publishing it are
13
+ deliberately different actions, so a review can be made and read before anybody
14
+ decides it should appear on the PR.
15
+
16
+ You do **not** need a checkout. Everything below comes from the GitHub API via
17
+ `gh`, which is already on your PATH and already authenticated.
18
+
19
+ ---
20
+
21
+ ## 1. Resolve the PR
22
+
23
+ You need the repository as `owner/name` and the PR number. If you were given
24
+ only a URL, read them off it.
25
+
26
+ ```bash
27
+ gh api repos/OWNER/NAME/pulls/NUMBER
28
+ ```
29
+
30
+ From the response take `number`, `title`, `state`, `head.sha` and `base.sha`.
31
+
32
+ - **`state` is not `open`** — say so and stop. Reviewing a closed chapter helps
33
+ nobody.
34
+ - **The call fails** — say so and stop. Do not review from memory of the repo.
35
+
36
+ `head.sha` is the commit you are reviewing. Every later step is pinned to it,
37
+ and it is part of the artefact's filename, because a review of one commit says
38
+ nothing about another.
39
+
40
+ ## 2. Get the diff — from the API, not from a working tree
41
+
42
+ ```bash
43
+ gh api repos/OWNER/NAME/pulls/NUMBER/files --paginate --slurp
44
+ ```
45
+
46
+ Each entry has `filename`, `status`, `patch` (a unified diff hunk) and the
47
+ change counts. Read the patches; that is the change.
48
+
49
+ **Use this endpoint and no other.** The step that posts findings anchors each
50
+ one to a line it computes from *these same patches* — the right-hand side of
51
+ each hunk, which is the only place GitHub accepts an inline comment. A line
52
+ number taken from anywhere else (a local `git diff`, a whole-file read, a
53
+ `compare` against a different base) can be perfectly correct about the code and
54
+ still unanchorable, and an unanchorable finding is quietly demoted out of the
55
+ place a reader would look for it.
56
+
57
+ Some entries have **no `patch`** — a binary file, or one too large for GitHub to
58
+ render. Do not guess at their contents. Either fetch the file if you need it, or
59
+ leave it unreviewed and say which files you skipped.
60
+
61
+ ### Then check the PR has not moved under you
62
+
63
+ This endpoint returns the diff **as it is now**, not the diff at the `head.sha`
64
+ you read in step 1. If someone pushed while you were reading, you now hold one
65
+ commit's diff about to be filed under another commit's name.
66
+
67
+ So read the head again and compare:
68
+
69
+ ```bash
70
+ gh api repos/OWNER/NAME/pulls/NUMBER --jq .head.sha
71
+ ```
72
+
73
+ - **Same as step 1** — carry on.
74
+ - **Different** — start again from step 1 against the new head. If it moves a
75
+ second time, stop and write an `incomplete` artefact saying the PR is moving
76
+ faster than you can read it.
77
+
78
+ Do not simply relabel what you already read with the newer sha. That is the
79
+ failure this check exists to prevent, not a way around it: the artefact would be
80
+ a review of code nobody can see any more, indistinguishable from a review of the
81
+ code that is there.
82
+
83
+ ## 3. Load the repo's own rules
84
+
85
+ ```bash
86
+ gh api repos/OWNER/NAME/contents/CLAUDE.md?ref=HEAD_SHA --jq .content
87
+ ```
88
+
89
+ ```bash
90
+ gh api repos/OWNER/NAME/contents/docs/reference/review-rules.md?ref=HEAD_SHA --jq .content
91
+ ```
92
+
93
+ Both are base64; decode them. Both may 404, which is fine and normal — most
94
+ repos have neither.
95
+
96
+ Where `review-rules.md` exists it **outranks your priors**: a rule saying "this
97
+ is deliberate here, do not flag it" exists because a reviewer already raised
98
+ that exact thing and a human said no. Where it does not exist, review anyway and
99
+ say so in your summary — a repo without one gets a generic review, which is
100
+ worth having and worth labelling as such.
101
+
102
+ Pin both reads to `HEAD_SHA`. Reading the default branch's rules against a
103
+ branch's code compares the change to something it was never written against.
104
+
105
+ ## 4. Review
106
+
107
+ Prioritise, in this order:
108
+
109
+ 1. **Correctness.** Does it do what it claims? What input makes it wrong?
110
+
111
+ 2. **Claims that outrun the code.** A comment, commit message or PR body
112
+ asserting a guard, a threshold or a behaviour the code does not implement.
113
+ These are worse than a missing guard, because they stop the next person
114
+ looking. Read what the change *says about itself*, and check it.
115
+
116
+ 3. **Silent failure.** A path where something goes wrong and nothing reports it:
117
+ a swallowed error, a fallback that renders a failed measurement as a
118
+ plausible normal value, an empty result indistinguishable from "nothing to
119
+ report". Ask of every failure branch: *if this fired right now, how would
120
+ anyone know?*
121
+
122
+ 4. **Tests that cannot fail.** A test that looks correct and is structurally
123
+ incapable of failing reports coverage it does not provide, and CI will never
124
+ reveal it, because CI only ever runs it green.
125
+
126
+ 5. **Security visible in the diff** — a new unauthenticated path, a secret on a
127
+ command line or in an environment variable, an unparameterised query, a taint
128
+ flow from request input to a sink.
129
+
130
+ **Do not spend the review on formatting, naming, or suggesting more cases for
131
+ behaviour already covered.** A review that is 80% nits gets skimmed, and then
132
+ the one real finding in it gets skimmed too.
133
+
134
+ ## 5. Say which of the three outcomes you reached
135
+
136
+ They are different states and reporting them identically is the failure this
137
+ skill exists to find in other people's code:
138
+
139
+ - You reviewed the diff and **found nothing**. A legitimate, useful result.
140
+ - You reviewed the diff and **found things**.
141
+ - You **could not finish** — the diff was too large to hold, a file had no
142
+ patch and you needed it, an API call failed. Say that. Never present a partial
143
+ read as a clean one.
144
+
145
+ ## 6. Write the artefact
146
+
147
+ One file per PR head, under `.smithers/review/` relative to your working
148
+ directory (create it if absent):
149
+
150
+ ```text
151
+ .smithers/review/<pr>-<first 12 chars of head sha>.code.json
152
+ ```
153
+
154
+ ```json
155
+ {
156
+ "pr": 1234,
157
+ "head": "abc123def456",
158
+ "reviewedAt": "2026-09-07T00:00:00Z",
159
+ "rulesFile": "docs/reference/review-rules.md",
160
+ "status": "reviewed",
161
+ "findings": [
162
+ {
163
+ "file": "packages/api/src/routes/thing.ts",
164
+ "line": 42,
165
+ "severity": "major",
166
+ "category": "correctness",
167
+ "summary": "one sentence stating the defect",
168
+ "why": "the failing case, concretely: these inputs produce this wrong output"
169
+ }
170
+ ]
171
+ }
172
+ ```
173
+
174
+ **`severity` is `major` or `minor`. Nothing else.** Not `high`, not `critical`,
175
+ not `P1`. The step that posts these filters by severity, and it compares within
176
+ a known vocabulary: a token it does not recognise is kept rather than dropped,
177
+ so an invented level does not vanish — but it does defeat the filter, silently,
178
+ while the filter carries on printing as though it were working. `P1`/`P2`/`P3`
179
+ belong to the separate security review and are not yours to emit here.
180
+
181
+ `category` is one of `correctness`, `claim`, `silent-failure`, `test`,
182
+ `security`.
183
+
184
+ **`status` is `reviewed` or `incomplete`.** `incomplete` takes a `reason`. An
185
+ empty `findings` array on an `incomplete` review must never be read as clean,
186
+ and the posting step refuses to post from one at all — which is the whole point
187
+ of the field. Use it rather than shrinking the review to fit.
188
+
189
+ `line` is a right-hand line number from the patches you read in step 2. Omit it
190
+ if the finding is about the change as a whole rather than one line.
191
+
192
+ ## 7. Report, and stop
193
+
194
+ Print counts by severity and category, the artefact path, and whether a rules
195
+ file was found. Then **stop**.
196
+
197
+ Do not post the findings. Do not open a review. Do not comment on the PR. If you
198
+ were asked to review, you have done what was asked.
@@ -103,7 +103,11 @@ export function buildBody(f, id) {
103
103
  '',
104
104
  f.why ?? f.attack ?? '',
105
105
  '',
106
- '<sub>Posted by `/s:code-review`. This reviewer never submits a blocking verdict.',
106
+ // NOT `/s:code-review`. That is an operator slash command in a Claude Code
107
+ // plugin the managed fleet never installs, so from the moment the `pr-review`
108
+ // skill became a second producer the footer named a thing most readers of it
109
+ // cannot run — a claim outrunning the code, on every comment this bot posts.
110
+ '<sub>Posted by the Obi Wan reviewer. This reviewer never submits a blocking verdict.',
107
111
  'Reply if this is deliberate and it becomes a rule in `docs/reference/review-rules.md`.</sub>',
108
112
  marker(id),
109
113
  ].join('\n');
package/dist/bin/agt.js CHANGED
@@ -40,7 +40,7 @@ import {
40
40
  success,
41
41
  table,
42
42
  warn
43
- } from "../chunk-MZNTGZA7.js";
43
+ } from "../chunk-24AILXID.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.838" : "dev";
5470
+ var cliVersion = true ? "0.28.840" : "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.838" : "dev";
6661
+ var cliVersion2 = true ? "0.28.840" : "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) => {
@@ -6566,7 +6566,7 @@ function exchangeFailureKind(err) {
6566
6566
  }
6567
6567
 
6568
6568
  // src/lib/api-client.ts
6569
- var agtCliVersion = true ? "0.28.838" : "dev";
6569
+ var agtCliVersion = true ? "0.28.840" : "dev";
6570
6570
  var lastConfigHash = null;
6571
6571
  function setConfigHash(hash) {
6572
6572
  lastConfigHash = hash && hash.length > 0 ? hash : null;
@@ -10924,4 +10924,4 @@ export {
10924
10924
  managerInstallSystemUnitCommand,
10925
10925
  managerUninstallSystemUnitCommand
10926
10926
  };
10927
- //# sourceMappingURL=chunk-MZNTGZA7.js.map
10927
+ //# sourceMappingURL=chunk-24AILXID.js.map
@@ -60,7 +60,7 @@ import {
60
60
  safeWriteJsonAtomic,
61
61
  setConfigHash,
62
62
  tripClass
63
- } from "../chunk-MZNTGZA7.js";
63
+ } from "../chunk-24AILXID.js";
64
64
  import {
65
65
  getProjectDir as getProjectDir2,
66
66
  getReadyTasks,
@@ -2286,6 +2286,8 @@ var ASSET_FILES = [
2286
2286
  [AUTH_FILENAME, 420]
2287
2287
  ];
2288
2288
  var REVIEW_ASSET_DIRNAME = "review";
2289
+ var SKILL_FILENAME = "SKILL.md";
2290
+ var REVIEW_SKILL_ID = "pr-review";
2289
2291
  function resolveBundledReviewAssetDir() {
2290
2292
  const moduleDir = dirname5(fileURLToPath(import.meta.url));
2291
2293
  const candidates = [
@@ -2322,6 +2324,19 @@ function provisionReviewPoster(codeName, sourceDir = resolveBundledReviewAssetDi
2322
2324
  chmodSync(dest, mode);
2323
2325
  }
2324
2326
  }
2327
+ function provisionReviewSkill(codeName, sourceDir = resolveBundledReviewAssetDir()) {
2328
+ const src = join8(sourceDir, SKILL_FILENAME);
2329
+ if (!existsSync2(src)) {
2330
+ throw new Error(`[review-poster] bundled asset missing: ${src}`);
2331
+ }
2332
+ const adapter = getFramework("claude-code");
2333
+ if (!adapter.installSkillFiles) {
2334
+ throw new Error("[review-poster] claude-code adapter exposes no installSkillFiles");
2335
+ }
2336
+ adapter.installSkillFiles(codeName, REVIEW_SKILL_ID, [
2337
+ { relativePath: SKILL_FILENAME, content: readFileSync8(src, "utf-8") }
2338
+ ]);
2339
+ }
2325
2340
 
2326
2341
  // src/lib/id-keyed-migration.ts
2327
2342
  import { existsSync as existsSync3, lstatSync, readlinkSync, renameSync as renameSync2 } from "fs";
@@ -9598,6 +9613,15 @@ function decideSenderPolicyForward(evt, policy) {
9598
9613
 
9599
9614
  // ../../packages/core/dist/channels/governance/slack-peer-classifier.js
9600
9615
  var CODE_NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
9616
+ function findThirdPartyGrant(msg, cfg) {
9617
+ return cfg.third_party_bots?.find((b) => {
9618
+ if (msg.user && b.bot_user_id === msg.user)
9619
+ return true;
9620
+ if (!msg.bot_id)
9621
+ return false;
9622
+ return b.bot_id === msg.bot_id || b.bot_user_id === msg.bot_id;
9623
+ });
9624
+ }
9601
9625
  function classifyPeerMessage(msg, cfg, self) {
9602
9626
  if (!msg.bot_id)
9603
9627
  return { kind: "human" };
@@ -9607,6 +9631,18 @@ function classifyPeerMessage(msg, cfg, self) {
9607
9631
  if (cfg.peer_disabled_mode === "all") {
9608
9632
  return { kind: "drop", reason: "peer_disabled_all" };
9609
9633
  }
9634
+ if (!cfg.peers.some((p) => p.bot_user_id === msg.user)) {
9635
+ const grant = findThirdPartyGrant(msg, cfg);
9636
+ if (grant) {
9637
+ if (cfg.peer_disabled_mode === "cross_team_only") {
9638
+ return { kind: "drop", reason: "peer_disabled_cross_team" };
9639
+ }
9640
+ if (grant.channel_ids.includes(msg.channel)) {
9641
+ return { kind: "bot-ingress", bot: grant };
9642
+ }
9643
+ return { kind: "drop", reason: "third_party_channel_not_allowlisted" };
9644
+ }
9645
+ }
9610
9646
  if (cfg.peer_agent_mode === "off") {
9611
9647
  return { kind: "drop", reason: "mode_off" };
9612
9648
  }
@@ -9619,22 +9655,6 @@ function classifyPeerMessage(msg, cfg, self) {
9619
9655
  }
9620
9656
  const peer = cfg.peers.find((p) => p.bot_user_id === msg.user);
9621
9657
  if (!peer) {
9622
- const thirdParty = cfg.third_party_bots?.find((b) => {
9623
- if (msg.user && b.bot_user_id === msg.user)
9624
- return true;
9625
- if (!msg.bot_id)
9626
- return false;
9627
- return b.bot_id === msg.bot_id || b.bot_user_id === msg.bot_id;
9628
- });
9629
- if (thirdParty) {
9630
- if (cfg.peer_disabled_mode === "cross_team_only") {
9631
- return { kind: "drop", reason: "peer_disabled_cross_team" };
9632
- }
9633
- if (thirdParty.channel_ids.includes(msg.channel)) {
9634
- return { kind: "bot-ingress", bot: thirdParty };
9635
- }
9636
- return { kind: "drop", reason: "third_party_channel_not_allowlisted" };
9637
- }
9638
9658
  return { kind: "drop", reason: "unknown_peer" };
9639
9659
  }
9640
9660
  if (peer.gate_path === null) {
@@ -13669,7 +13689,7 @@ var agentRestartTimezoneInputs = /* @__PURE__ */ new Map();
13669
13689
  var lastVersionCheckAt = 0;
13670
13690
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
13671
13691
  var lastResponsivenessProbeAt = 0;
13672
- var agtCliVersion = true ? "0.28.838" : "dev";
13692
+ var agtCliVersion = true ? "0.28.840" : "dev";
13673
13693
  function resolveBrewPath(execFileSync2) {
13674
13694
  try {
13675
13695
  const out = execFileSync2("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -19526,6 +19546,11 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
19526
19546
  } catch (err) {
19527
19547
  log(`[persistent-session] Failed to provision review poster for '${codeName}': ${err.message}`);
19528
19548
  }
19549
+ try {
19550
+ provisionReviewSkill(codeName);
19551
+ } catch (err) {
19552
+ log(`[persistent-session] Failed to provision review skill for '${codeName}': ${err.message}`);
19553
+ }
19529
19554
  if (!isSessionHealthy(codeName)) {
19530
19555
  if (agentState.persistentSessionAgents.has(codeName)) {
19531
19556
  const ctx = getLastFailureContext(codeName);