@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.
- package/dist/assets/coding-agent/README.md +105 -88
- package/dist/assets/coding-agent/agent.py +8 -0
- package/dist/assets/coding-agent/config.py +1 -1
- package/dist/assets/coding-agent/deploy/Dockerfile +20 -0
- package/dist/assets/coding-agent/deploy/invoke_harness.py +142 -0
- package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +287 -275
- package/dist/assets/coding-agent/deploy/run.py +415 -0
- package/dist/assets/coding-agent/system_prompt.py +39 -5
- package/package.json +1 -1
|
@@ -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
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
20
|
-
#
|
|
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
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
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
|
|
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
|
|
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
|
|
41
|
-
#
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
213
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
aws ssm get-parameter --name
|
|
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 [[ -
|
|
237
|
-
echo "
|
|
238
|
-
|
|
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
|
|
122
|
+
- name: Invoke the agent
|
|
243
123
|
id: invoke
|
|
244
|
-
env:
|
|
245
|
-
PRISM_HARNESS_ARN: ${{ env.PRISM_HARNESS_ARN }}
|
|
246
124
|
run: |
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
#
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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:
|
|
236
|
+
- name: Check push result
|
|
283
237
|
id: apply
|
|
284
238
|
if: steps.invoke.outputs.outcome == 'patched'
|
|
285
239
|
run: |
|
|
286
|
-
|
|
287
|
-
BRANCH
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
echo "
|
|
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 "
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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
|
|
366
|
+
case "${OUTCOME}" in
|
|
363
367
|
patched)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
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
|
|
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
|