@prism-d1/cli 1.0.89 → 1.0.91

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.
@@ -2,56 +2,38 @@
2
2
  #
3
3
  # Installed by: prism-cli bootstrapper install-coding-agent
4
4
  #
5
- # This file deliberately does NOT live in bootstrapper/github-workflows/.
6
- # install-github-workflows copies every .yml in that directory, so putting it
7
- # there would hand an autonomous, Bedrock-billing, branch-pushing agent to every
8
- # repo that ran the workflow installer.
9
- #
10
- #
11
5
  # WHAT RUNS WHERE
12
6
  #
13
- # The agent runs on AgentCore, not here. This workflow is the thin client: it
14
- # resolves the issue, invokes the harness, applies the patch it returns, and opens
15
- # a pull request. That split is what keeps the harness's credentials read-only --
16
- # it never pushes, so it needs no write access to any repository, and the token
17
- # that opens the PR is this run's own ephemeral, repo-scoped GITHUB_TOKEN.
7
+ # The agent and its full orchestration (clone, toolchain, deps, fix, verify) run
8
+ # inside the harness microVM. This workflow is the thin client: it resolves the
9
+ # issue, makes one API call, and applies the returned patch.
10
+ #
11
+ # The split is:
12
+ # - Inside the harness: clone → mise install → npm ci → agent loop → git diff → npm test
13
+ # - On this runner: read issue → call API → apply patch → push → open PR → comment
18
14
  #
19
- # One harness serves every toolchain. It ships mise and installs what the
20
- # repository declares in .tool-versions at session start, so there is no
21
- # per-language routing to configure here.
15
+ # The harness image contains strands-agents + the full orchestrator. A single
16
+ # InvokeAgentRuntimeCommand does everything. No client code is fetched per-run.
22
17
  #
23
18
  #
24
19
  # AUTHORIZATION MODEL
25
20
  #
26
21
  # The trigger is `issues: [labeled]`, gated on a specific label. That is the
27
- # security control, and it is load-bearing: anybody with a GitHub account can
28
- # open an issue in a public repo, but applying a label requires triage
29
- # permission or above. Triggering on `issues: [opened]` instead would let any
30
- # stranger spend your Bedrock budget and put a branch in your repository.
22
+ # security control: anybody can open an issue, but applying a label requires
23
+ # triage permission. Private repo access: the GITHUB_TOKEN is passed in the
24
+ # clone URL, so it is visible to the model's shell tool for the duration of
25
+ # this run (it expires when the run ends).
31
26
  #
32
27
  # NOTE: `issues` events only trigger a workflow run if this file exists on the
33
- # DEFAULT branch. You cannot test changes to it from a feature branch -- nothing
34
- # fires, and nothing reports why. Merge first.
28
+ # DEFAULT branch. You cannot test changes from a feature branch.
35
29
  #
36
30
  #
37
31
  # REQUIRED SETUP
38
- # - prism-cli bootstrapper setup-github-oidc (grants bedrock:InvokeModel)
32
+ # - prism-cli bootstrapper setup-github-oidc (grants bedrock-agentcore + SSM + SM)
39
33
  # - Repository secret PRISM_METRICS_ROLE_ARN
40
- # - Repository or org variable PRISM_HARNESS_ARN (an ARN is an identifier, not
41
- # a credential, so it belongs in `vars` -- and an org variable is inherited by
42
- # every repo, which is what "deploy the harness once" should feel like)
43
- # - A label matching AGENT_LABEL below, created in the repo
34
+ # - Repository or org variable PRISM_HARNESS_ARN
35
+ # - A label matching 'agent-fix', created in the repo
44
36
  # - .coding-agent/config.json committed (written by install-coding-agent)
45
- #
46
- # The agent is NOT vendored into this repository. Only config.json, prompt.md,
47
- # fixtures/ and this file live here; the orchestrator client is fetched at run
48
- # time from the sample repo, and the agent itself runs in an AgentCore harness in
49
- # your own account. That means an agent fix reaches every repo without an N-repo
50
- # migration, which vendoring could not offer.
51
- #
52
- # Pin PRISM_AGENT_REF to a tag or commit SHA for anything you care about. It
53
- # defaults to `main` so the workshop works on day one, but a floating default
54
- # means a third party's push can change what runs in your CI.
55
37
 
56
38
  name: PRISM Coding Agent
57
39
 
@@ -69,26 +51,15 @@ env:
69
51
  PRISM_AWS_REGION: us-west-2
70
52
  PRISM_REPO: github.com/${{ github.repository }}
71
53
  PRISM_HARNESS_ARN: ${{ vars.PRISM_HARNESS_ARN }}
72
- # Where the orchestrator client comes from. Overridable as repo/org variables so
73
- # a fork, an internal mirror, or a pinned SHA needs no edit to this file.
74
- PRISM_AGENT_REPO: ${{ vars.PRISM_AGENT_REPO || 'aws-samples/sample-prism-d1-velocity' }}
75
- PRISM_AGENT_REF: ${{ vars.PRISM_AGENT_REF || 'main' }}
76
54
 
77
55
  permissions:
78
56
  id-token: write # OIDC → AWS
79
57
  contents: write # push the fix branch
80
58
  pull-requests: write # open the PR
81
- issues: write # comment the outcome back on the issue
59
+ issues: write # comment the outcome
82
60
 
83
61
  jobs:
84
62
  fix-issue:
85
- # On the label path, run only for the agent label. Any other label added to
86
- # any issue also fires this workflow, so this condition is what keeps the
87
- # job from running on every triage action in the repository.
88
- #
89
- # The label is a literal here, not an env var, because the `env` context is
90
- # not available in a job-level `if`. An AGENT_LABEL variable above would look
91
- # configurable and change nothing — if you rename the label, edit this line.
92
63
  if: >-
93
64
  github.event_name == 'workflow_dispatch' ||
94
65
  github.event.label.name == 'agent-fix'
@@ -98,44 +69,22 @@ jobs:
98
69
  steps:
99
70
  - name: Checkout
100
71
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
101
- with:
102
- fetch-depth: 0
103
72
 
104
- - name: Warn if no harness variable (SSM is the fallback)
105
- run: |
106
- if [[ -z "${PRISM_HARNESS_ARN}" ]]; then
107
- echo "::notice::PRISM_HARNESS_ARN variable is not set. Will try SSM /prism/d1/harness-arn after OIDC."
108
- fi
109
-
110
- - name: Resolve the issue to work on
73
+ - name: Resolve the issue
111
74
  id: issue
112
- env:
113
- GH_TOKEN: ${{ github.token }}
114
- DISPATCH_NUMBER: ${{ inputs.issue_number }}
115
75
  run: |
116
- set -euo pipefail
117
-
118
- # Both branches write the same shape: a GitHub event payload with the
119
- # issue under an `issue` key, which is what agentcore.invoke reads. The
120
- # dispatch branch has to wrap it, because `gh issue view` returns a
121
- # bare issue object.
122
76
  if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
123
- if ! [[ "${DISPATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
124
- echo "::error::issue_number must be numeric, got '${DISPATCH_NUMBER}'"
125
- exit 1
126
- fi
127
- gh issue view "${DISPATCH_NUMBER}" --json number,title,body,labels \
128
- | jq '{issue: .}' > /tmp/issue.json
77
+ NUMBER="${{ inputs.issue_number }}"
129
78
  else
130
- # Read from the payload rather than re-fetching: avoids a race where
131
- # the issue is edited between the label and this step.
132
- jq '{issue: (.issue | {number, title, body, labels})}' \
133
- "${GITHUB_EVENT_PATH}" > /tmp/issue.json
79
+ NUMBER="${{ github.event.issue.number }}"
134
80
  fi
135
-
136
- NUMBER=$(jq -r '.issue.number' /tmp/issue.json)
137
81
  echo "number=${NUMBER}" >> "$GITHUB_OUTPUT"
138
- echo "Working on issue #${NUMBER}: $(jq -r '.issue.title' /tmp/issue.json)"
82
+ # Wrap under an `issue` key for the agent
83
+ gh issue view "${NUMBER}" --json number,title,body \
84
+ | jq '{issue: .}' > /tmp/issue.json
85
+ echo "Issue #${NUMBER}: $(jq -r .issue.title /tmp/issue.json)"
86
+ env:
87
+ GH_TOKEN: ${{ github.token }}
139
88
 
140
89
  - name: Acknowledge on the issue
141
90
  env:
@@ -144,251 +93,314 @@ jobs:
144
93
  gh issue comment "${{ steps.issue.outputs.number }}" \
145
94
  --body "🤖 PRISM coding agent picked this up — [run log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})."
146
95
 
147
- - name: Set up Python
148
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
149
- with:
150
- python-version: '3.12'
151
-
152
- - name: Install runner dependencies
153
- run: |
154
- set -euo pipefail
155
- sudo apt-get update -qq && sudo apt-get install -y -qq jq
156
- # Only boto3 -- the agent's own dependencies live in the harness image,
157
- # not here. This runner never loads a model.
158
- pip install --quiet boto3
159
-
160
- - name: Fetch the orchestrator client
161
- id: client
162
- env:
163
- AGENT_REPO: ${{ env.PRISM_AGENT_REPO }}
164
- AGENT_REF: ${{ env.PRISM_AGENT_REF }}
165
- run: |
166
- set -euo pipefail
167
- # Cloned into RUNNER_TEMP, NOT the workspace. actions/checkout can only
168
- # write under GITHUB_WORKSPACE, and anything there is inside the tree the
169
- # agent's patch is applied to and committed from -- a client checkout
170
- # sitting in the working tree is one `git add -A` away from being
171
- # committed into somebody's fix.
172
- DEST="${RUNNER_TEMP}/prism-agent"
173
- rm -rf "${DEST}"
174
- mkdir -p "${DEST}"
175
-
176
- # init + fetch rather than `clone --branch`, because --branch takes a
177
- # branch or tag and rejects a commit SHA -- which is exactly what a repo
178
- # that has pinned PRISM_AGENT_REF will be passing.
179
- git -C "${DEST}" init -q
180
- git -C "${DEST}" remote add origin "https://github.com/${AGENT_REPO}.git"
181
- git -C "${DEST}" sparse-checkout init --cone
182
- git -C "${DEST}" sparse-checkout set coding-agent
183
- git -C "${DEST}" fetch -q --depth 1 origin "${AGENT_REF}"
184
- git -C "${DEST}" checkout -q FETCH_HEAD
185
-
186
- CLIENT="${DEST}/coding-agent"
187
- # Preflight by importing rather than by checking that files exist. The
188
- # runtime dependency is the whole `coding-agent/` directory, not just the
189
- # `agentcore/` package: invoke.py puts its parent on sys.path and imports
190
- # `config`, and contract.py imports it too -- from inside
191
- # FixRequest.validate(), so a missing config.py would not surface until a
192
- # request was already being validated. An import check covers both, plus
193
- # any module inside the package that a narrowed sparse path dropped.
194
- #
195
- # `python -c` rather than a heredoc: a heredoc terminator has to sit at
196
- # column 0, which inside a YAML block scalar means breaking out of the
197
- # block and invalidating the workflow file.
198
- if ! ( cd "${CLIENT}" && python -c "import sys; sys.path.insert(0, '.'); import agentcore.invoke; import config" ); then
199
- echo "::error::${AGENT_REPO}@${AGENT_REF} does not provide a usable" \
200
- "coding-agent/ client (agentcore/ and config.py are both required)"
201
- exit 1
202
- fi
203
- echo "dir=${CLIENT}" >> "$GITHUB_OUTPUT"
204
- echo "Client ${AGENT_REPO}@$(git -C "${DEST}" rev-parse --short FETCH_HEAD)"
205
-
206
96
  - name: Configure AWS credentials (OIDC)
207
97
  uses: aws-actions/configure-aws-credentials@b47578312673ae6fa5b5096b330d9fbac3d116df # v4.2.1
208
98
  with:
209
99
  role-to-assume: ${{ secrets.PRISM_METRICS_ROLE_ARN }}
210
100
  aws-region: ${{ env.PRISM_AWS_REGION }}
211
101
 
212
- - name: Load telemetry config from SSM
213
- # One source of truth: CDK writes these at deploy time, so they cannot
214
- # drift from what's actually deployed. Falls back gracefully — a missing
215
- # parameter means telemetry is unconfigured, not that the run fails.
102
+ - name: Resolve harness ARN
103
+ id: harness
216
104
  run: |
217
- set -euo pipefail
218
- read_param() {
219
- aws ssm get-parameter --name "$1" --query Parameter.Value --output text 2>/dev/null || echo ""
220
- }
221
- PRISM_COLLECTOR_URL=$(read_param /prism/d1/collector-url)
222
- PRISM_OIDC_TOKEN_ENDPOINT=$(read_param /prism/d1/token-endpoint)
223
- PRISM_AGENT_SECRET_ID=$(read_param /prism/d1/agent-secret-id)
224
- echo "PRISM_COLLECTOR_URL=${PRISM_COLLECTOR_URL}" >> "$GITHUB_ENV"
225
- echo "PRISM_OIDC_TOKEN_ENDPOINT=${PRISM_OIDC_TOKEN_ENDPOINT}" >> "$GITHUB_ENV"
226
- echo "PRISM_AGENT_SECRET_ID=${PRISM_AGENT_SECRET_ID}" >> "$GITHUB_ENV"
227
- # Harness ARN: the GitHub variable is the primary (pre-OIDC early-fail),
228
- # but if it was empty (e.g. fork with no org vars), SSM is the fallback.
229
- if [[ -z "${PRISM_HARNESS_ARN}" ]]; then
230
- PRISM_HARNESS_ARN=$(read_param /prism/d1/harness-arn)
231
- if [[ -n "${PRISM_HARNESS_ARN}" ]]; then
232
- echo "PRISM_HARNESS_ARN=${PRISM_HARNESS_ARN}" >> "$GITHUB_ENV"
233
- echo "Harness ARN loaded from SSM (GitHub variable was empty)"
234
- fi
105
+ ARN="${PRISM_HARNESS_ARN}"
106
+ if [[ -z "${ARN}" ]]; then
107
+ ARN=$(aws ssm get-parameter --name /prism/d1/harness-arn \
108
+ --query Parameter.Value --output text 2>/dev/null || echo "")
235
109
  fi
236
- if [[ -n "${PRISM_COLLECTOR_URL}" ]]; then
237
- echo "Telemetry: ${PRISM_COLLECTOR_URL} (secret: ${PRISM_AGENT_SECRET_ID})"
238
- else
239
- echo "Telemetry: not configured (SSM params empty or missing)"
110
+ if [[ -z "${ARN}" ]]; then
111
+ echo "::error::No harness ARN (checked variable + SSM /prism/d1/harness-arn)"
112
+ exit 1
240
113
  fi
114
+ echo "arn=${ARN}" >> "$GITHUB_OUTPUT"
115
+ echo "Harness: ${ARN}"
116
+
117
+ - name: Set up Python
118
+ uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
119
+ with:
120
+ python-version: '3.12'
241
121
 
242
- - name: Invoke the harness
122
+ - name: Invoke the agent
243
123
  id: invoke
244
- env:
245
- PRISM_HARNESS_ARN: ${{ env.PRISM_HARNESS_ARN }}
246
124
  run: |
247
- if [[ -z "${PRISM_HARNESS_ARN}" ]]; then
248
- echo "::error::No harness ARN available (checked GitHub variable AND SSM /prism/d1/harness-arn)."
249
- echo "::error::Run coding-agent/deploy/deploy-harness.sh and set the org variable."
250
- exit 1
251
- fi
252
- # GitHub runs `run:` blocks under `bash -e`, so a non-zero exit would
253
- # end the step and skip straight past reporting. `set +e` keeps the exit
254
- # code as data instead of as control flow.
255
- set -o pipefail
256
- set +e
257
- # --repo is the checkout under test, which is NOT the working directory:
258
- # this step runs from the client, and config.json lives in the target
259
- # repo. Passing `.` here would resolve to the client and fail config
260
- # loading with a confusing "no .coding-agent/config.json".
261
- python -m agentcore.invoke \
262
- --repo "${GITHUB_WORKSPACE}" \
263
- --github-event /tmp/issue.json \
264
- --patch-out /tmp/fix.patch \
265
- --result-out /tmp/result.json \
266
- --transcript-out /tmp/agent-transcript.log \
267
- --repo-url "${{ github.server_url }}/${{ github.repository }}.git" \
268
- --ref "${{ github.event.repository.default_branch }}" \
269
- --region "${PRISM_AWS_REGION}" \
270
- 2>&1 | tee /tmp/invoke.log
271
- EXIT_CODE=$?
272
- set -e
273
- echo "exit_code=${EXIT_CODE}" >> "$GITHUB_OUTPUT"
274
-
275
- OUTCOME=$(jq -r '.outcome // "failed"' /tmp/result.json 2>/dev/null || echo failed)
125
+ pip install --quiet boto3
126
+
127
+ # Read subdir from config
128
+ SUBDIR=$(python -c "
129
+ import json, pathlib
130
+ cfg = pathlib.Path('.coding-agent/config.json')
131
+ print(json.loads(cfg.read_text()).get('subdir', '.') if cfg.exists() else '.')
132
+ ")
133
+
134
+ # The repo URL includes the token so the harness can clone private repos.
135
+ # This token is ephemeral (dies when the run ends) and scoped to this repo.
136
+ REPO_URL="https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
137
+
138
+ python - <<PYTHON
139
+ import boto3, json, sys, uuid
140
+ from botocore.config import Config as BotoConfig
141
+
142
+ RESULT_MARKER = "===PRISM_RESULT_JSON==="
143
+ PATCH_MARKER = "===PRISM_PATCH_START==="
144
+ PATCH_END_MARKER = "===PRISM_PATCH_END==="
145
+
146
+ harness_arn = "${{ steps.harness.outputs.arn }}"
147
+ region = "${{ env.PRISM_AWS_REGION }}"
148
+ repo_url = """${REPO_URL}"""
149
+ ref = "${{ github.event.repository.default_branch }}"
150
+ subdir = """${SUBDIR}"""
151
+ issue_json = open("/tmp/issue.json").read().strip().replace("'", "'\\''")
152
+ session_id = f"prism-agent-{uuid.uuid4()}"
153
+
154
+ cmd = (
155
+ f"echo '{issue_json}' > /tmp/agent-issue.json && "
156
+ f"python /opt/prism-agent/run.py "
157
+ f"--repo-url '{repo_url}' "
158
+ f"--ref '{ref}' "
159
+ f"--subdir '{subdir}' "
160
+ f"--issue @/tmp/agent-issue.json "
161
+ f"--region '{region}'"
162
+ )
163
+
164
+ print(f"Session: {session_id}", file=sys.stderr)
165
+ client = boto3.client("bedrock-agentcore", region_name=region,
166
+ config=BotoConfig(read_timeout=1800, connect_timeout=30,
167
+ retries={"max_attempts": 0}))
168
+
169
+ # Retry once on transient stream drops (ProtocolError / ReadTimeout)
170
+ MAX_ATTEMPTS = 2
171
+ stdout_parts, exit_code = [], -1
172
+ for attempt in range(1, MAX_ATTEMPTS + 1):
173
+ try:
174
+ if attempt > 1:
175
+ session_id = f"prism-agent-{uuid.uuid4()}"
176
+ print(f"[retry] attempt {attempt}, new session: {session_id}", file=sys.stderr)
177
+
178
+ response = client.invoke_agent_runtime_command(
179
+ agentRuntimeArn=harness_arn,
180
+ runtimeSessionId=session_id,
181
+ body={"command": cmd, "timeout": 1800},
182
+ )
183
+
184
+ stdout_parts, exit_code = [], -1
185
+ for event in response.get("stream", []):
186
+ chunk = event.get("chunk", {})
187
+ if "contentDelta" in chunk:
188
+ delta = chunk["contentDelta"]
189
+ if delta.get("stdout"):
190
+ stdout_parts.append(delta["stdout"])
191
+ if delta.get("stderr"):
192
+ print(delta["stderr"], end="", file=sys.stderr)
193
+ elif "contentStop" in chunk:
194
+ exit_code = int(chunk["contentStop"].get("exitCode", -1))
195
+ break # success -- exit retry loop
196
+
197
+ except Exception as exc:
198
+ err_name = type(exc).__name__
199
+ if attempt < MAX_ATTEMPTS and ("Protocol" in err_name or "Timeout" in err_name or "Read" in str(exc)):
200
+ print(f"::warning::Stream interrupted ({err_name}), retrying...", file=sys.stderr)
201
+ import time; time.sleep(5)
202
+ continue
203
+ print(f"::error::InvokeAgentRuntimeCommand failed: {exc}", file=sys.stderr)
204
+ json.dump({"outcome": "failed", "reason": str(exc)}, open("/tmp/result.json", "w"))
205
+ open("/tmp/fix.patch", "w").write("")
206
+ sys.exit(0)
207
+
208
+ stdout = "".join(stdout_parts)
209
+
210
+ # Parse structured output
211
+ if RESULT_MARKER in stdout:
212
+ start = stdout.index(RESULT_MARKER) + len(RESULT_MARKER)
213
+ end = stdout.index(PATCH_MARKER) if PATCH_MARKER in stdout else len(stdout)
214
+ result = json.loads(stdout[start:end].strip())
215
+ else:
216
+ result = {"outcome": "failed", "reason": f"no result marker (exit {exit_code})"}
217
+
218
+ patch = ""
219
+ if PATCH_MARKER in stdout:
220
+ ps = stdout.index(PATCH_MARKER) + len(PATCH_MARKER) + 1
221
+ pe = stdout.index(PATCH_END_MARKER) if PATCH_END_MARKER in stdout else len(stdout)
222
+ patch = stdout[ps:pe]
223
+
224
+ open("/tmp/fix.patch", "w").write(patch)
225
+ json.dump(result, open("/tmp/result.json", "w"), indent=2)
226
+
227
+ outcome = result.get("outcome", "failed")
228
+ verified = result.get("verified", False)
229
+ print(f"\nOutcome: {outcome} (verified={verified}, exit {exit_code})", file=sys.stderr)
230
+ PYTHON
231
+
232
+ # Export for subsequent steps
233
+ OUTCOME=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('outcome','failed'))")
276
234
  echo "outcome=${OUTCOME}" >> "$GITHUB_OUTPUT"
277
- VERIFIED=$(jq -r '.verified // false' /tmp/result.json 2>/dev/null || echo false)
278
- echo "verified=${VERIFIED}" >> "$GITHUB_OUTPUT"
279
- echo "Harness outcome: ${OUTCOME} (verified=${VERIFIED}, exit ${EXIT_CODE})"
280
- working-directory: ${{ steps.client.outputs.dir }}
281
235
 
282
- - name: Apply the patch
236
+ - name: Check push result
283
237
  id: apply
284
238
  if: steps.invoke.outputs.outcome == 'patched'
285
239
  run: |
286
- set -euo pipefail
287
- BRANCH="agent/issue-${{ steps.issue.outputs.number }}"
288
- git switch -c "${BRANCH}"
289
-
290
- # --check first so a stale or malformed patch is reported as such rather
291
- # than leaving a half-applied tree behind.
292
- if ! git apply --check /tmp/fix.patch; then
293
- echo "::error::the returned patch does not apply to this tree"
240
+ # Push now happens inside the harness (run.py). Extract results.
241
+ BRANCH=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('branch',''))")
242
+ SHA=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('sha',''))")
243
+ PUSH_ERR=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('push_error',''))")
244
+
245
+ if [[ -n "${PUSH_ERR}" ]]; then
246
+ echo "::error::Push failed inside harness: ${PUSH_ERR}"
247
+ echo "applied=false" >> "$GITHUB_OUTPUT"
248
+ exit 1
249
+ fi
250
+ if [[ -z "${BRANCH}" ]]; then
251
+ echo "::error::No branch in result — push may have been skipped"
294
252
  echo "applied=false" >> "$GITHUB_OUTPUT"
295
253
  exit 1
296
254
  fi
297
- git apply /tmp/fix.patch
298
-
299
- NAME=$(jq -r '.agent_name // "PRISM Coding Agent"' .coding-agent/config.json)
300
- EMAIL=$(jq -r '.agent_email // "prism-agent@example.com"' .coding-agent/config.json)
301
- SUMMARY=$(jq -r '.summary // "Autonomous fix"' /tmp/result.json)
302
-
303
- git -c "user.name=${NAME}" -c "user.email=${EMAIL}" commit -a -m "$(printf 'fix: %s\n\n%s\n\nCloses #%s\n' \
304
- "$(jq -r '.issue.title' /tmp/issue.json)" \
305
- "${SUMMARY}" \
306
- "${{ steps.issue.outputs.number }}")"
307
255
 
308
256
  echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
257
+ echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
309
258
  echo "applied=true" >> "$GITHUB_OUTPUT"
310
- echo "files=$(git diff --name-only HEAD~1 HEAD | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
311
- echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
312
- git log --oneline -1
313
-
314
- - name: Emit commit attribution
315
- # After the commit, because the SHA does not exist before it -- the harness
316
- # returns a patch and this workflow makes the commit. Reuses the trace id
317
- # from result.json: a fresh one would leave the commit with no correlated
318
- # usage, and the receiver would freeze its origin as human-authored.
319
- #
320
- # continue-on-error, because a reporting gap must not fail a run that has
321
- # already produced a reviewed fix.
322
- if: steps.apply.outputs.applied == 'true'
323
- continue-on-error: true
324
- run: |
325
- set -euo pipefail
326
- python -m agentcore.emit_commit \
327
- --result /tmp/result.json \
328
- --repo "${PRISM_REPO}" \
329
- --sha "${{ steps.apply.outputs.sha }}"
330
- working-directory: ${{ steps.client.outputs.dir }}
259
+ echo "Pushed: ${BRANCH} (${SHA})"
331
260
 
332
261
  - name: Open the pull request
333
- id: pr
334
262
  if: steps.apply.outputs.applied == 'true'
335
263
  env:
336
264
  GH_TOKEN: ${{ github.token }}
337
265
  run: |
338
- set -euo pipefail
339
- git push -u origin "${{ steps.apply.outputs.branch }}"
340
- VERIFIED=$(jq -r 'if .verified then "The agent ran this repository'"'"'s checks and they passed." else "⚠️ The agent did NOT verify this change. Review with that in mind." end' /tmp/result.json)
341
- URL=$(gh pr create \
342
- --title "fix: $(jq -r '.issue.title' /tmp/issue.json)" \
343
- --body "$(printf 'Closes #%s\n\n%s\n\n%s\n\nGenerated by the PRISM coding agent on AgentCore. Nothing here was merged automatically.\n\n[Run log](%s)\n' \
344
- "${{ steps.issue.outputs.number }}" \
345
- "$(jq -r '.summary' /tmp/result.json)" \
346
- "${VERIFIED}" \
347
- "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}")" \
348
- --base "${{ github.event.repository.default_branch }}")
349
- echo "url=${URL}" >> "$GITHUB_OUTPUT"
350
-
351
- - name: Report the outcome on the issue
266
+ VERIFIED=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('verified', False))")
267
+ BADGE=""
268
+ if [[ "${VERIFIED}" == "True" ]]; then
269
+ BADGE="✅ Tests pass in the harness"
270
+ else
271
+ BADGE="⚠️ Not independently verified"
272
+ fi
273
+
274
+ # Extract changed files and summary from result
275
+ FILES=$(python -c "
276
+ import json
277
+ r = json.load(open('/tmp/result.json'))
278
+ files = r.get('changed_files', [])
279
+ print(', '.join(f'\`{f}\`' for f in files) if files else '(unknown)')
280
+ ")
281
+
282
+ SUMMARY=$(python -c "
283
+ import json
284
+ r = json.load(open('/tmp/result.json'))
285
+ # The patch itself serves as the summary — show the diffstat
286
+ patch = r.get('patch', '')
287
+ lines = [l for l in patch.splitlines() if l.startswith('+') or l.startswith('-')]
288
+ adds = len([l for l in lines if l.startswith('+') and not l.startswith('+++')])
289
+ dels = len([l for l in lines if l.startswith('-') and not l.startswith('---')])
290
+ print(f'{adds} insertion(s), {dels} deletion(s)')
291
+ ")
292
+
293
+ # Build rich PR body
294
+ MODEL=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('model', 'unknown'))")
295
+ IN_TOK=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('usage', {}).get('input_tokens', 0))")
296
+ OUT_TOK=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('usage', {}).get('output_tokens', 0))")
297
+ COST=$(python -c "
298
+ import json
299
+ r = json.load(open('/tmp/result.json'))
300
+ cost = r.get('cost_usd', 0)
301
+ print(f'\${cost:.2f}')
302
+ ")
303
+ PR_SUMMARY=$(python -c "import json; r=json.load(open('/tmp/result.json')); s=r.get('summary',''); print(s) if s else None")
304
+
305
+ BRANCH="${{ steps.apply.outputs.branch }}"
306
+ PR_BODY="Fixes #${{ steps.issue.outputs.number }}
307
+
308
+ ${BADGE}
309
+
310
+ ${PR_SUMMARY}
311
+
312
+ **Changed files:** ${FILES}
313
+ **Diff:** ${SUMMARY}
314
+
315
+ ---
316
+ **Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
317
+ *Authored by the [PRISM coding agent](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).*"
318
+
319
+ # Check if a PR already exists for this branch
320
+ EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number' 2>/dev/null || echo "")
321
+
322
+ if [[ -n "${EXISTING_PR}" ]]; then
323
+ echo "PR #${EXISTING_PR} already exists — adding comment with new run results"
324
+ gh pr comment "${EXISTING_PR}" --body "### 🔄 New commit pushed
325
+
326
+ ${BADGE}
327
+
328
+ ${PR_SUMMARY}
329
+
330
+ **Changed files:** ${FILES}
331
+ **Diff:** ${SUMMARY}
332
+
333
+ ---
334
+ **Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
335
+ *[Run log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
336
+ else
337
+ gh pr create \
338
+ --base "${{ github.event.repository.default_branch }}" \
339
+ --head "${BRANCH}" \
340
+ --title "fix: $(jq -r '.issue.title' /tmp/issue.json)" \
341
+ --body "${PR_BODY}"
342
+ fi
343
+
344
+ - name: Report outcome on the issue
352
345
  if: always()
353
346
  env:
354
347
  GH_TOKEN: ${{ github.token }}
355
- OUTCOME: ${{ steps.invoke.outputs.outcome }}
356
- PR_URL: ${{ steps.pr.outputs.url }}
357
348
  run: |
358
- set -euo pipefail
349
+ OUTCOME=$(cat /tmp/result.json 2>/dev/null | jq -r '.outcome // "unknown"')
350
+ REASON=$(cat /tmp/result.json 2>/dev/null | jq -r '.reason // ""' | head -c 500)
351
+ SUMMARY=$(cat /tmp/result.json 2>/dev/null | jq -r '.summary // ""' | head -c 1000)
352
+ IN_TOK=$(cat /tmp/result.json 2>/dev/null | jq -r '.usage.input_tokens // 0')
353
+ OUT_TOK=$(cat /tmp/result.json 2>/dev/null | jq -r '.usage.output_tokens // 0')
354
+ MODEL=$(cat /tmp/result.json 2>/dev/null | jq -r '.model // "unknown"')
355
+ COST=$(python -c "
356
+ import json
357
+ r = json.load(open('/tmp/result.json'))
358
+ cost = r.get('cost_usd', 0)
359
+ print(f'\${cost:.2f}')
360
+ " 2>/dev/null || echo '\$0.00')
361
+ BRANCH=$(cat /tmp/result.json 2>/dev/null | jq -r '.branch // ""')
362
+ SHA=$(cat /tmp/result.json 2>/dev/null | jq -r '.sha // ""' | head -c 8)
363
+
359
364
  RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
360
- REASON=$(jq -r '.reason // ""' /tmp/result.json 2>/dev/null || echo "")
361
365
 
362
- case "${OUTCOME:-failed}" in
366
+ case "${OUTCOME}" in
363
367
  patched)
364
- if [[ -n "${PR_URL}" ]]; then
365
- BODY=$(printf '✅ **Fix proposed** — %s\n\nNothing has been merged.\n\n[Run log](%s)' "${PR_URL}" "${RUN_URL}")
366
- else
367
- BODY=$(printf '⚠️ **The agent produced a patch but it could not be applied or pushed.** See the run log for the reason.\n\n[Run log](%s)' "${RUN_URL}")
368
- fi
369
- ;;
368
+ BODY="### ✅ Fix applied
369
+
370
+ ${SUMMARY}
371
+
372
+ **PR branch:** \`${BRANCH}\` (${SHA})
373
+ **Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
374
+ **Run:** [workflow log](${RUN_URL})" ;;
370
375
  declined)
371
- # Declining is frequently the right answer -- for an issue asking the
372
- # agent to weaken a test suite, or one whose premise does not hold.
373
- # Reporting it as a failure would train people to ignore the signal.
374
- BODY=$(printf '🛑 **The agent declined to make a change.**\n\n> %s\n\nThis is often correct: the request may ask for something it should refuse, or rest on a premise that does not hold.\n\n[Run log](%s)' "${REASON}" "${RUN_URL}")
375
- ;;
376
+ BODY="### 🛑 Declined
377
+
378
+ The agent could not confirm a fix was needed.
379
+
380
+ > ${REASON}
381
+
382
+ **Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
383
+ **Run:** [workflow log](${RUN_URL})
384
+
385
+ _If this is wrong, add more reproduction details to the issue body and re-label \`agent-fix\`._" ;;
376
386
  *)
377
- BODY=$(printf '❌ **The agent could not complete this.**\n\n> %s\n\n[Run log](%s)' "${REASON:-see the run log}" "${RUN_URL}")
378
- ;;
387
+ BODY="### ❌ Failed
388
+
389
+ > ${REASON}
390
+
391
+ **Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
392
+ **Run:** [workflow log](${RUN_URL})" ;;
379
393
  esac
380
394
 
381
395
  gh issue comment "${{ steps.issue.outputs.number }}" --body "${BODY}"
382
396
 
383
- - name: Upload the transcript
397
+ - name: Upload artifacts
384
398
  if: always()
385
399
  uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
386
400
  with:
387
401
  name: agent-run-issue-${{ steps.issue.outputs.number }}
388
402
  path: |
389
- /tmp/invoke.log
390
403
  /tmp/result.json
391
404
  /tmp/fix.patch
392
- /tmp/agent-transcript.log
393
405
  retention-days: 14
394
406
  if-no-files-found: ignore