@staff0rd/assist 0.786.1 → 0.788.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.
@@ -30,19 +30,38 @@ Never overwrite a differing target without the user's confirmation.
30
30
 
31
31
  ## 3. Run init
32
32
 
33
- `init.mjs` asks for the provider, base URL, Claude model, Codex model and API key, checks both models can be reached, then sets the repo variables `ASSIST_REVIEW_PROVIDER`, `ASSIST_REVIEW_BASE_URL`, `ASSIST_REVIEW_CLAUDE_MODEL`, `ASSIST_REVIEW_CODEX_MODEL` and the secret `ASSIST_REVIEW_API_KEY` with `gh`. It needs a terminal for its prompts, and the API key must never pass through this conversation.
33
+ `init.mjs` asks for the config, checks every model can be reached, then sets the repo variables (and the secret, when an API key is used) with `gh`. It needs a terminal for its prompts, and the API key must never pass through this conversation.
34
34
 
35
- 1. Ask the user for the provider (`litellm` or `foundry`), the base URL, the Claude model and the Codex model, as the provider's model names. Do not ask for the API key. The base URL is:
36
- - `litellm`: the proxy root (e.g. `https://litellm.example.com`); Claude uses it directly and Codex uses `<url>/v1`.
37
- - `foundry`: the Azure AI Foundry resource root, `https://<resource>.services.ai.azure.com`, with no path; Claude uses `<url>/anthropic` and Codex uses `<url>/openai/v1`. The models are the resource's deployment names, and the API key is the resource's key.
38
- 2. Tell the user to run this in their own terminal from the repo root, where it prompts for the API key with masked input:
35
+ The review has three slots: two reviewers and the synthesis. Each slot is `claude:<model>` (run by Claude Code) or `codex:<model>` (run by Codex), so a provider with only GPT deployments can run every slot on Codex.
36
+
37
+ 1. Ask the user for:
38
+ - the provider: `litellm` or `foundry`.
39
+ - the base URL:
40
+ - `litellm`: the proxy root (e.g. `https://litellm.example.com`); Claude uses it directly and Codex uses `<url>/v1`.
41
+ - `foundry`: the Azure AI Foundry resource root, `https://<resource>.services.ai.azure.com`, with no path; Claude uses `<url>/anthropic` and Codex uses `<url>/openai/v1`. The models are the resource's deployment names.
42
+ - each slot as `claude:<model>` or `codex:<model>`: reviewer 1, reviewer 2 and the synthesis.
43
+ - for `foundry`, the auth: an API key, or Entra. Entra is required when the resource has local auth disabled (`az cognitiveservices account show ... --query properties.disableLocalAuth`). For Entra, ask for the client ID and tenant ID of the identity with `Cognitive Services OpenAI User` on the resource, and optionally a GitHub environment to scope its federated credential to.
44
+
45
+ Do not ask for the API key.
46
+
47
+ 2. Tell the user to run this in their own terminal from the repo root. Add `--azure-client-id <id> --azure-tenant-id <id> [--environment <name>]` for Entra. Without them, it prompts for the API key with masked input.
39
48
 
40
49
  ```
41
- node .github/review-ci/init.mjs --provider <provider> --base-url <url> --claude-model <model> --codex-model <model>
50
+ node .github/review-ci/init.mjs --provider <provider> --base-url <url> --reviewer-1 <harness:model> --reviewer-2 <harness:model> --synthesis <harness:model>
42
51
  ```
43
52
 
44
- 3. Once they report it finished, confirm with `gh variable list` and `gh secret list` that the four variables and the secret are set. If init reported an unreachable model, relay its error and let the user re-run it.
53
+ With Entra, init checks the models with the user's own `az login` token. Add `--skip-check` if the user's identity cannot reach the models but the CI identity can; the workflow's check step still verifies them.
54
+
55
+ 3. For Entra, the identity needs a federated credential with issuer `https://token.actions.githubusercontent.com`, audience `api://AzureADTokenExchange`, and subject `repo:<owner>/<repo>:environment:<environment>` when an environment was given, otherwise `repo:<owner>/<repo>:pull_request`. Show the user the command, and do not run it yourself:
56
+
57
+ ```
58
+ az identity federated-credential create --name review-ci-<repo> --identity-name <identity> --resource-group <rg> --issuer https://token.actions.githubusercontent.com --subject <subject> --audiences api://AzureADTokenExchange
59
+ ```
60
+
61
+ (`az ad app federated-credential create` for an app registration.) When an environment was given, it must exist in the repo (`gh api -X PUT repos/<owner>/<repo>/environments/<environment>`).
62
+
63
+ 4. Once they report it finished, confirm with `gh variable list` (and `gh secret list` for an API key) that the variables are set. If init reported an unreachable model, relay its error and let the user re-run it.
45
64
 
46
65
  ## 4. Report
47
66
 
48
- List the files written or skipped, and remind the user to commit `.github/review-ci/` and `.github/workflows/review-ci.yml`. The workflow runs on `pull_request: opened` and fails at its check step, naming the problem, when a variable or the secret is unset or a model cannot be reached. Its review step then posts the findings as a `COMMENT` review on the PR.
67
+ List the files written or skipped, and remind the user to commit `.github/review-ci/` and `.github/workflows/review-ci.yml`. The workflow runs on `pull_request: opened` and fails at its check step, naming the problem, when a variable or the secret is unset, the Entra token cannot be obtained, or a model cannot be reached. Its review step then posts the findings as a `COMMENT` review on the PR.
@@ -7,15 +7,20 @@ on:
7
7
  permissions:
8
8
  contents: read
9
9
  pull-requests: write
10
+ id-token: write
10
11
 
11
12
  jobs:
12
13
  review:
13
14
  runs-on: ubuntu-latest
15
+ environment: ${{ vars.ASSIST_REVIEW_ENVIRONMENT }}
14
16
  env:
15
17
  ASSIST_REVIEW_PROVIDER: ${{ vars.ASSIST_REVIEW_PROVIDER }}
16
18
  ASSIST_REVIEW_BASE_URL: ${{ vars.ASSIST_REVIEW_BASE_URL }}
17
- ASSIST_REVIEW_CLAUDE_MODEL: ${{ vars.ASSIST_REVIEW_CLAUDE_MODEL }}
18
- ASSIST_REVIEW_CODEX_MODEL: ${{ vars.ASSIST_REVIEW_CODEX_MODEL }}
19
+ ASSIST_REVIEW_REVIEWER_1: ${{ vars.ASSIST_REVIEW_REVIEWER_1 }}
20
+ ASSIST_REVIEW_REVIEWER_2: ${{ vars.ASSIST_REVIEW_REVIEWER_2 }}
21
+ ASSIST_REVIEW_SYNTHESIS: ${{ vars.ASSIST_REVIEW_SYNTHESIS }}
22
+ ASSIST_REVIEW_AZURE_CLIENT_ID: ${{ vars.ASSIST_REVIEW_AZURE_CLIENT_ID }}
23
+ ASSIST_REVIEW_AZURE_TENANT_ID: ${{ vars.ASSIST_REVIEW_AZURE_TENANT_ID }}
19
24
  ASSIST_REVIEW_API_KEY: ${{ secrets.ASSIST_REVIEW_API_KEY }}
20
25
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21
26
  steps:
@@ -30,8 +35,13 @@ jobs:
30
35
  with:
31
36
  node-version: "lts/*"
32
37
 
33
- - name: Install Claude Code and Codex
34
- run: npm install -g @anthropic-ai/claude-code @openai/codex
38
+ - name: Install Claude Code
39
+ if: startsWith(vars.ASSIST_REVIEW_REVIEWER_1, 'claude:') || startsWith(vars.ASSIST_REVIEW_REVIEWER_2, 'claude:') || startsWith(vars.ASSIST_REVIEW_SYNTHESIS, 'claude:')
40
+ run: npm install -g @anthropic-ai/claude-code
41
+
42
+ - name: Install Codex
43
+ if: startsWith(vars.ASSIST_REVIEW_REVIEWER_1, 'codex:') || startsWith(vars.ASSIST_REVIEW_REVIEWER_2, 'codex:') || startsWith(vars.ASSIST_REVIEW_SYNTHESIS, 'codex:')
44
+ run: npm install -g @openai/codex
35
45
 
36
46
  - name: Check review config
37
47
  run: node .github/review-ci/check.mjs
@@ -1,2 +1,2 @@
1
1
  import { createRequire } from "node:module"; const require = createRequire(import.meta.url);
2
- var v=["ASSIST_REVIEW_PROVIDER","ASSIST_REVIEW_BASE_URL","ASSIST_REVIEW_CLAUDE_MODEL","ASSIST_REVIEW_CODEX_MODEL"],n="ASSIST_REVIEW_API_KEY",p=[...v,n],d=["litellm","foundry"];function E(i,e){if(i==="litellm")return{anthropic:e,responses:`${e}/v1`,anthropicKind:"gateway"};if(i==="foundry")return{anthropic:`${e}/anthropic`,responses:`${e}/openai/v1`,anthropicKind:"foundry"};throw new Error(`Unsupported ASSIST_REVIEW_PROVIDER "${i}" (expected one of: ${d.join(", ")})`)}async function s(i,e,t){try{let r=await fetch(i,{method:"POST",headers:{"Content-Type":"application/json",...e},body:JSON.stringify(t),signal:AbortSignal.timeout(6e4)});if(r.ok)return;let o=(await r.text()).trim().slice(0,300);return`HTTP ${r.status}${o?`: ${o}`:""}`}catch(r){if(!(r instanceof Error))return String(r);let o=r.cause instanceof Error?r.cause.message:"";return o?`${r.message} (${o})`:r.message}}async function l(i,e){let t={Authorization:`Bearer ${i.apiKey}`},r=`${e.anthropic}/v1/messages`,o=`${e.responses}/responses`,[a,c]=await Promise.all([s(r,{...e.anthropicKind==="gateway"?t:{},"x-api-key":i.apiKey,"anthropic-version":"2023-06-01"},{model:i.claudeModel,max_tokens:1,messages:[{role:"user",content:"ping"}]}),s(o,t,{model:i.codexModel,input:"ping",max_output_tokens:16})]);return[...a?[`Claude model "${i.claudeModel}" at ${r}: ${a}`]:[],...c?[`Codex model "${i.codexModel}" at ${o}: ${c}`]:[]]}function m(i){let e=r=>i[r]?.trim()??"",t=p.filter(r=>!e(r));return t.length>0?{config:null,missing:t}:{config:{provider:e("ASSIST_REVIEW_PROVIDER"),baseUrl:e("ASSIST_REVIEW_BASE_URL").replace(/\/+$/,""),claudeModel:e("ASSIST_REVIEW_CLAUDE_MODEL"),codexModel:e("ASSIST_REVIEW_CODEX_MODEL"),apiKey:e(n)},missing:t}}async function u(i){let{config:e,missing:t}=m(i);if(!e)return t.map(o=>`${o} is not set`);let r;try{r=E(e.provider,e.baseUrl)}catch(o){return[o instanceof Error?o.message:String(o)]}return l(e,r)}async function S(){let i=await u(process.env);if(i.length>0){for(let e of i)console.error(`review-ci: ${e}`);process.exit(1)}console.log("review-ci: config is set and both models are reachable")}await S();export{S as reviewCiCheck};
2
+ var f=["ASSIST_REVIEW_PROVIDER","ASSIST_REVIEW_BASE_URL","ASSIST_REVIEW_REVIEWER_1","ASSIST_REVIEW_REVIEWER_2","ASSIST_REVIEW_SYNTHESIS"];var c="ASSIST_REVIEW_API_KEY",m=["litellm","foundry"],d=["claude","codex"];function E(e,r){if(e==="litellm")return{anthropic:r,responses:`${r}/v1`,anthropicKind:"gateway"};if(e==="foundry")return{anthropic:`${r}/anthropic`,responses:`${r}/openai/v1`,anthropicKind:"foundry"};throw new Error(`Unsupported ASSIST_REVIEW_PROVIDER "${e}" (expected one of: ${m.join(", ")})`)}async function l(e,r,t){try{let i=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json",...r},body:JSON.stringify(t),signal:AbortSignal.timeout(6e4)});if(i.ok)return;let n=(await i.text()).trim().slice(0,300);return`HTTP ${i.status}${n?`: ${n}`:""}`}catch(i){if(!(i instanceof Error))return String(i);let n=i.cause instanceof Error?i.cause.message:"";return n?`${i.message} (${n})`:i.message}}function A(e,r,t){let i={Authorization:`Bearer ${t}`};return e.auth.kind==="entra"?i:r.anthropicKind==="foundry"?{"x-api-key":t}:{...i,"x-api-key":t}}async function k(e,r,t,i){if(e.harness==="codex"){let s=`${t.responses}/responses`,a=await l(s,{Authorization:`Bearer ${i}`},{model:e.model,input:"ping",max_output_tokens:16});return a&&`Codex model "${e.model}" at ${s}: ${a}`}let n=`${t.anthropic}/v1/messages`,o=await l(n,{...A(r,t,i),"anthropic-version":"2023-06-01"},{model:e.model,max_tokens:1,messages:[{role:"user",content:"ping"}]});return o&&`Claude model "${e.model}" at ${n}: ${o}`}function x(e){let r=new Map;for(let t of Object.values(e.slots))r.set(`${t.harness}:${t.model}`,t);return[...r.values()]}async function S(e,r,t){return(await Promise.all(x(e).map(n=>k(n,e,r,t)))).filter(n=>!!n)}function $(e){return d.includes(e)}function R(e,r){let t=r.indexOf(":"),i=r.slice(0,t).trim(),n=r.slice(t+1).trim();return t===-1||!$(i)||!n?`${e} "${r}" must be ${d.map(o=>`${o}:<model>`).join(" or ")}`:{harness:i,model:n}}function p(e,r){return e==="foundry"&&r("ASSIST_REVIEW_AZURE_CLIENT_ID")?["ASSIST_REVIEW_AZURE_CLIENT_ID","ASSIST_REVIEW_AZURE_TENANT_ID"]:[c]}function w(e,r){return p(e,r).includes(c)?{kind:"key",apiKey:r(c)}:{kind:"entra",clientId:r("ASSIST_REVIEW_AZURE_CLIENT_ID"),tenantId:r("ASSIST_REVIEW_AZURE_TENANT_ID")}}var b={claude:"ASSIST_REVIEW_REVIEWER_1",codex:"ASSIST_REVIEW_REVIEWER_2",synthesis:"ASSIST_REVIEW_SYNTHESIS"};function g(e){let r=s=>e[s]?.trim()??"",t=r("ASSIST_REVIEW_PROVIDER"),i=[...f,...p(t,r)].filter(s=>!r(s));if(i.length>0)return{config:null,errors:i.map(s=>`${s} is not set`)};let n=Object.entries(b).map(([s,a])=>[s,R(a,r(a))]),o=n.flatMap(([,s])=>typeof s=="string"?[s]:[]);return o.length>0?{config:null,errors:o}:{config:{provider:t,baseUrl:r("ASSIST_REVIEW_BASE_URL").replace(/\/+$/,""),slots:Object.fromEntries(n),auth:w(t,r)},errors:o}}var u="https://cognitiveservices.azure.com";var v=3e4;async function C(e,r){if(!e.ok){let t=(await e.text()).trim().slice(0,300);throw new Error(`${r}: HTTP ${e.status}${t?`: ${t}`:""}`)}return await e.json()}async function V(e,r){let t=new URL(e);t.searchParams.set("audience","api://AzureADTokenExchange");let i=await fetch(t,{headers:{Authorization:`bearer ${r}`},signal:AbortSignal.timeout(v)}),n=await C(i,"GitHub OIDC token request failed");if(!n.value)throw new Error("GitHub OIDC token response had no value");return n.value}async function _(e,r,t){let i=await V(t.requestUrl,t.requestToken),n=await fetch(`https://login.microsoftonline.com/${encodeURIComponent(r)}/oauth2/v2.0/token`,{method:"POST",body:new URLSearchParams({grant_type:"client_credentials",client_id:e,scope:`${u}/.default`,client_assertion_type:"urn:ietf:params:oauth:client-assertion-type:jwt-bearer",client_assertion:i}),signal:AbortSignal.timeout(v)}),o=await C(n,`Entra token exchange for client ${e} failed`);if(!o.access_token)throw new Error("Entra token response had no access_token");return o.access_token}import{execFileSync as N}from"node:child_process";function y(e){try{return N("az",["account","get-access-token","--resource",u,"--tenant",e,"--query","accessToken","-o","tsv"],{encoding:"utf8",stdio:["ignore","pipe","pipe"],shell:process.platform==="win32"}).trim()}catch(r){let t=r.stderr?.trim();throw new Error(`az account get-access-token failed for tenant ${e}${t?`: ${t}`:""}; run az login --tenant ${e}`)}}async function I(e,r){if(e.kind==="key")return e.apiKey;let t=r.ACTIONS_ID_TOKEN_REQUEST_URL,i=r.ACTIONS_ID_TOKEN_REQUEST_TOKEN;if(t&&i){let n=await _(e.clientId,e.tenantId,{requestUrl:t,requestToken:i});return console.log(`::add-mask::${n}`),n}if(r.GITHUB_ACTIONS)throw new Error("No GitHub OIDC token endpoint; give the job the id-token: write permission");return y(e.tenantId)}function h(e){return e instanceof Error?e.message:String(e)}async function T(e){let{config:r,errors:t}=g(e);if(!r)return t;let i;try{i=E(r.provider,r.baseUrl)}catch(o){return[h(o)]}let n;try{n=await I(r.auth,e)}catch(o){return[h(o)]}return S(r,i,n)}async function O(){let e=await T(process.env);if(e.length>0){for(let r of e)console.error(`review-ci: ${r}`);process.exit(1)}console.log("review-ci: config is set and every model is reachable")}await O();export{O as reviewCiCheck};