33` ado-script ` is the umbrella name for the TypeScript workspace at
44[ ` scripts/ado-script/ ` ] ( ../scripts/ado-script/ ) . It produces small,
55ncc-bundled Node programs that the ** compiler injects into every emitted
6- pipeline** as runtime helpers. Today it produces ` gate.js ` , the
7- trigger-filter gate evaluator, and ` import.js ` , the runtime prompt
8- resolver described in [ ` runtime-imports.md ` ] ( runtime-imports.md ) .
6+ pipeline** as runtime helpers. Today it produces three bundles:
7+
8+ - ` gate.js ` — trigger-filter gate evaluator (Setup job).
9+ - ` import.js ` — runtime prompt resolver described in
10+ [ ` runtime-imports.md ` ] ( runtime-imports.md ) (Agent job).
11+ - ` exec-context-pr.js ` — PR-context precompute that resolves the
12+ merge-base, writes ` aw-context/pr/{base,head}.sha ` , and appends a
13+ prompt fragment to the agent prompt (Agent job, before the agent
14+ runs). See [ ` execution-context.md ` ] ( execution-context.md ) .
915
1016> ** Internal-only.** ` ado-script ` is not a user-facing front-matter
1117> feature. Authors never write an ` ado-script: ` block in their agent
@@ -52,17 +58,76 @@ because the compiler always embeds an absolute marker path and
5258not re-expanded).
5359
5460The bundle lives at ` import.js ` and ships in the same
55- ` ado-script.zip ` release asset as ` gate.js ` , so pipelines download it
56- through the same Setup -job asset flow. ` import.js ` uses only the Node
57- standard library, so the ncc bundle is small ( ~ 1.5 KB) and carries no
58- SDK dependency.
61+ ` ado-script.zip ` release asset as ` gate.js ` and ` exec-context-pr.js ` ,
62+ so pipelines download it through the same Agent -job asset flow.
63+ ` import.js ` uses only the Node standard library, so the ncc bundle is
64+ small ( ~ 1.5 KB) and carries no SDK dependency.
5965
6066The Stage-2 threat-analysis prompt is ** not** runtime-imported.
6167` src/data/threat-analysis.md ` is ` include_str! ` 'd into the ` ado-aw `
6268binary and inlined into the emitted YAML at compile time, matching
6369gh-aw's pattern (their ` threat_detection.md ` ships with the setup
6470action and is read directly from disk — no marker, no resolver).
6571
72+ ## What ` exec-context-pr.js ` does
73+
74+ ` exec-context-pr.js ` is a single-shot Node program that runs as the
75+ ** precompute step** of the PR contributor of the execution-context
76+ extension. It runs in the Agent job * before* the agent step, inside
77+ the AWF network-isolated sandbox's prepare phase.
78+
79+ It performs the work that used to live as ~ 190 lines of bash heredoc
80+ inside ` src/compile/extensions/exec_context/pr.rs ` :
81+
82+ 1 . ** Validate identifiers** — ` PR_ID ` , ` SYSTEM_TEAMPROJECT ` ,
83+ ` BUILD_REPOSITORY_NAME ` , and ` SYSTEM_PULLREQUEST_TARGETBRANCH ` are
84+ each matched against a strict allowlist regex (` validate.ts ` )
85+ before any of them are interpolated into a git refspec or the
86+ agent prompt. On any failure the program writes
87+ ` aw-context/pr/error.txt ` and a ` ### PR context (unavailable) `
88+ fragment to the agent prompt, then exits 0 (soft fail: the agent
89+ still runs, but is told the context is missing).
90+ 2 . ** Resolve merge-base** — if the checkout is a synthetic
91+ merge-commit (parent count ≥ 3 per ADO's PR-validation flow),
92+ ` merge-base.ts::resolveMergeBase ` computes ` git merge-base ` over
93+ the two parents. Otherwise it fetches the target branch with
94+ progressive deepening (` --depth=200/500/2000/--unshallow ` ) and
95+ then ` git merge-base ` against ` HEAD ` . Same ` BASE_SHA ` semantics
96+ in both paths (git's true common ancestor).
97+ 3 . ** Stage artefacts** — writes ` aw-context/pr/base.sha ` and
98+ ` aw-context/pr/head.sha ` so the agent can `git diff $(cat
99+ .../base.sha)..$(cat .../head.sha)` itself.
100+ 4 . ** Append prompt fragment** — appends a ` ## PR context ` section to
101+ ` /tmp/awf-tools/agent-prompt.md ` (path overridable via
102+ ` AW_AGENT_PROMPT_FILE ` for tests).
103+
104+ ### Trust boundary
105+
106+ The bearer (` SYSTEM_ACCESSTOKEN ` ) is mapped into the Node process's
107+ env by the wrapper bash step, but is ** only** propagated into the
108+ spawned ` git ` child process via `GIT_CONFIG_COUNT=1 / KEY_0 /
109+ VALUE_0` env vars (see ` git.ts::bearerEnv` + ` runGit` in
110+ ` merge-base.ts ` ). It never appears in argv, is never written to
111+ ` .git/config ` , and is never visible to the agent process (which is
112+ spawned later, in a separate AWF child). The
113+ ` test_execution_context_pr_does_not_leak_system_accesstoken ` Rust
114+ test walks the emitted YAML and asserts this scoping.
115+
116+ ### Env-var contract
117+
118+ | Env var | Source | Purpose |
119+ | ---| ---| ---|
120+ | ` SYSTEM_ACCESSTOKEN ` | ` $(System.AccessToken) ` | ADO REST / git fetch bearer |
121+ | ` SYSTEM_PULLREQUEST_PULLREQUESTID ` | ` $(System.PullRequest.PullRequestId) ` | PR identifier (validated numeric) |
122+ | ` SYSTEM_PULLREQUEST_TARGETBRANCH ` | ` $(System.PullRequest.TargetBranch) ` | PR target branch for the fetch |
123+ | ` SYSTEM_TEAMPROJECT ` | ` $(System.TeamProject) ` | ADO project name (validated) |
124+ | ` BUILD_REPOSITORY_NAME ` | ` $(Build.Repository.Name) ` | Repository name (validated) |
125+ | ` BUILD_SOURCESDIRECTORY ` | ` $(Build.SourcesDirectory) ` | Workspace root for ` aw-context/ ` |
126+ | ` AW_AGENT_PROMPT_FILE ` | (test override) | Override default ` /tmp/awf-tools/agent-prompt.md ` |
127+
128+ The bundle uses only ` node:child_process ` / ` node:fs ` / ` node:path `
129+ — no ` azure-devops-node-api ` , no ` fetch ` . The ncc'd bundle is ~ 8 KB.
130+
66131## End-to-end data flow
67132
68133```
@@ -183,20 +248,29 @@ scripts/ado-script/
183248│ │ ├── facts.ts # fact acquisition (env + REST)
184249│ │ ├── predicates.ts # 11 predicate evaluators + validatePredicateTree + glob ReDoS hardening
185250│ │ └── selfcancel.ts # best-effort build cancellation
186- │ └── import/ # import.js entry point + runtime prompt resolver
187- │ ├── index.ts # main(): expand runtime-import markers in place
188- │ └── __tests__/ # marker, path-resolution, and single-pass coverage
189- ├── test/ # End-to-end smoke tests
251+ │ ├── import/ # import.js entry point + runtime prompt resolver
252+ │ │ ├── index.ts # main(): expand runtime-import markers in place
253+ │ │ └── __tests__/ # marker, path-resolution, and single-pass coverage
254+ │ └── exec-context-pr/ # exec-context-pr.js entry point + PR precompute
255+ │ ├── index.ts # main(): validate → resolve merge-base → stage SHAs → append prompt
256+ │ ├── validate.ts # identifier regex guards
257+ │ ├── git.ts # execFile wrappers + bearerEnv helper
258+ │ ├── merge-base.ts # synthetic-merge detection + progressive-deepening fetch
259+ │ ├── prompt.ts # success / failure prompt-fragment writers
260+ │ └── __tests__/ # 32 unit tests across the four modules
261+ ├── test/ # End-to-end smoke tests (gate, import, exec-context-pr)
190262├── gate.js # ncc bundle output (gitignored)
191- └── import.js # ncc bundle output (gitignored)
263+ ├── import.js # ncc bundle output (gitignored)
264+ └── exec-context-pr.js # ncc bundle output (gitignored)
192265```
193266
194267The release workflow (` .github/workflows/release.yml ` ) runs
195- ` npm ci && npm run build ` , then zips ` scripts/ado-script/gate.js ` and
196- ` scripts/ado-script/import.js ` into
197- the ` ado-script.zip ` release asset. Pipelines download that asset at
198- runtime by URL pinned to the compiler's ` CARGO_PKG_VERSION ` , verify
199- its SHA-256 against the ` checksums.txt ` asset, then extract.
268+ ` npm ci && npm run build ` , then zips ` scripts/ado-script/gate.js ` ,
269+ ` scripts/ado-script/import.js ` , and
270+ ` scripts/ado-script/exec-context-pr.js ` into the ` ado-script.zip `
271+ release asset. Pipelines download that asset at runtime by URL pinned
272+ to the compiler's ` CARGO_PKG_VERSION ` , verify its SHA-256 against the
273+ ` checksums.txt ` asset, then extract.
200274
201275## Schema codegen
202276
@@ -254,11 +328,12 @@ three step strings into the Setup job:
254328 runs the gate with ` GATE_SPEC ` and the env-var contract documented
255329 above.
256330
257- ### Agent job (runtime-import resolver)
331+ ### Agent job (runtime-import resolver + PR-context precompute )
258332
259- When ` inlined-imports: false ` (the default), ` prepare_steps() ` returns
260- the same install + download pair plus the resolver invocation, into
261- the Agent job's existing ` {{ prepare_steps }} ` block:
333+ When ` inlined-imports: false ` (the default) OR the execution-context
334+ PR contributor activates (` on.pr ` configured and not disabled),
335+ ` prepare_steps() ` returns the install + download pair into the Agent
336+ job's existing ` {{ prepare_steps }} ` block:
262337
2633381 . ** ` NodeTool@0 ` ** — same shape as above.
2643392 . ** ` curl ` download + verify + extract** — same artefact, same
@@ -267,24 +342,40 @@ the Agent job's existing `{{ prepare_steps }}` block:
267342 expands ` {{#runtime-import …}} ` markers in
268343 ` /tmp/awf-tools/agent-prompt.md ` in place. See
269344 [ ` runtime-imports.md ` ] ( runtime-imports.md ) for marker syntax.
345+ ** Only emitted when ` inlined-imports: false ` .**
346+
347+ The PR-context precompute step (` node exec-context-pr.js ` ) is owned
348+ by ` ExecContextExtension ` (not ` AdoScriptExtension ` ) and emitted in
349+ its own ` Tool ` -phase ` prepare_steps() ` . Phase ordering
350+ (` AdoScriptExtension::phase() == System ` < ` ExecContextExtension::phase() == Tool ` )
351+ guarantees the bundle is installed and on disk before the
352+ exec-context invocation runs.
270353
271354### Per-job download (NOT a duplication bug)
272355
273356ADO jobs use ** isolated VMs** — ` /tmp ` is not shared between jobs.
274357The ` ado-script.zip ` bundle therefore has to be downloaded once per
275- job that consumes it. When both features are active (a pipeline with
276- both ` filters: ` and ` inlined-imports: false ` ), install + download
277- steps appear in ** both** Setup and Agent. That's correct architecture
278- given ADO's topology, not waste.
358+ job that consumes it. When both Setup and Agent need it, install +
359+ download steps appear in ** both** . That's correct architecture given
360+ ADO's topology, not waste.
279361
280362### What gets emitted, by case
281363
282- | ` filters: ` | ` inlined-imports ` | Setup-job steps | Agent-job extra steps |
364+ | Setup consumer | Agent consumer | Setup-job steps | Agent-job extra steps |
283365| ---| ---| ---| ---|
284- | inactive | ` true ` | (none) | (none) |
285- | inactive | ` false ` | (no Setup job) | install + download + resolver |
286- | active | ` true ` | install + download + gate | (none) |
287- | active | ` false ` | install + download + gate | install + download + resolver |
366+ | no gate | none | (none) | (none) |
367+ | no gate | ` inlined-imports: false ` only | (no Setup job) | install + download + resolver |
368+ | no gate | ` on.pr ` execution-context only | (no Setup job) | install + download + exec-context-pr |
369+ | no gate | both | (no Setup job) | install + download + resolver + exec-context-pr |
370+ | gate | none | install + download + gate | (none) |
371+ | gate | any combination of resolver / exec-pr | install + download + gate | install + download + (resolver?) + (exec-context-pr?) |
372+
373+ The "Setup consumer" column is gated on ` filters: ` lowering to non-empty
374+ checks. The "Agent consumer" columns are gated on
375+ ` inlined-imports: false ` (resolver) and the PR contributor's
376+ activation predicate (exec-context-pr; see
377+ ` pr_contributor_will_activate ` in
378+ ` src/compile/extensions/exec_context/mod.rs ` ).
288379
289380The IR-to-bash codegen that produces the gate step is
290381` compile_gate_step_external ` in ` src/compile/filter_ir.rs ` .
0 commit comments