@prism-d1/cli 1.0.90 → 1.0.92
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 +46 -38
- package/dist/assets/coding-agent/agent.py +31 -1
- package/dist/assets/coding-agent/config.py +1 -1
- package/dist/assets/coding-agent/deploy/create_harness.py +175 -10
- package/dist/assets/coding-agent/deploy/deploy-harness.sh +18 -5
- package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +175 -55
- package/dist/assets/coding-agent/deploy/run.py +173 -10
- package/dist/assets/coding-agent/system_prompt.py +39 -5
- package/dist/assets/infra/lib/lambda/otel-receiver.ts +27 -12
- package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts.map +1 -1
- package/dist/src/commands/bootstrapper/setup-github-oidc.js +4 -0
- package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -1
- package/package.json +1 -1
|
@@ -113,6 +113,22 @@ jobs:
|
|
|
113
113
|
fi
|
|
114
114
|
echo "arn=${ARN}" >> "$GITHUB_OUTPUT"
|
|
115
115
|
echo "Harness: ${ARN}"
|
|
116
|
+
# Resolve the model. SSM is the primary source (written by deploy-harness.sh,
|
|
117
|
+
# readable by the OIDC role). Fallback: extract from the harness API (requires
|
|
118
|
+
# bedrock-agentcore:GetAgentRuntime which not all roles have).
|
|
119
|
+
MODEL_ID=$(aws ssm get-parameter --name /prism/d1/harness-model \
|
|
120
|
+
--query Parameter.Value --output text 2>/dev/null || echo "")
|
|
121
|
+
if [[ -z "${MODEL_ID}" ]]; then
|
|
122
|
+
HARNESS_ID=$(echo "${ARN}" | grep -oP '[^/]+$')
|
|
123
|
+
MODEL_ID=$(python3 -c "
|
|
124
|
+
import boto3, json
|
|
125
|
+
c = boto3.client('bedrock-agentcore-control', region_name='${PRISM_AWS_REGION}')
|
|
126
|
+
h = c.get_harness(harnessId='${HARNESS_ID}')['harness']
|
|
127
|
+
print(h.get('model',{}).get('bedrockModelConfig',{}).get('modelId',''))
|
|
128
|
+
" 2>/dev/null || echo "")
|
|
129
|
+
fi
|
|
130
|
+
echo "model=${MODEL_ID}" >> "$GITHUB_OUTPUT"
|
|
131
|
+
echo "Model: ${MODEL_ID}"
|
|
116
132
|
|
|
117
133
|
- name: Set up Python
|
|
118
134
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
@@ -135,14 +151,16 @@ jobs:
|
|
|
135
151
|
# This token is ephemeral (dies when the run ends) and scoped to this repo.
|
|
136
152
|
REPO_URL="https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
|
|
137
153
|
|
|
138
|
-
python - <<
|
|
154
|
+
python - <<PYTHON
|
|
139
155
|
import boto3, json, sys, uuid
|
|
156
|
+
from botocore.config import Config as BotoConfig
|
|
140
157
|
|
|
141
158
|
RESULT_MARKER = "===PRISM_RESULT_JSON==="
|
|
142
159
|
PATCH_MARKER = "===PRISM_PATCH_START==="
|
|
143
160
|
PATCH_END_MARKER = "===PRISM_PATCH_END==="
|
|
144
161
|
|
|
145
162
|
harness_arn = "${{ steps.harness.outputs.arn }}"
|
|
163
|
+
model_id = "${{ steps.harness.outputs.model }}"
|
|
146
164
|
region = "${{ env.PRISM_AWS_REGION }}"
|
|
147
165
|
repo_url = """${REPO_URL}"""
|
|
148
166
|
ref = "${{ github.event.repository.default_branch }}"
|
|
@@ -157,35 +175,53 @@ jobs:
|
|
|
157
175
|
f"--ref '{ref}' "
|
|
158
176
|
f"--subdir '{subdir}' "
|
|
159
177
|
f"--issue @/tmp/agent-issue.json "
|
|
178
|
+
f"--model-id '{model_id}' "
|
|
160
179
|
f"--region '{region}'"
|
|
161
180
|
)
|
|
162
181
|
|
|
163
182
|
print(f"Session: {session_id}", file=sys.stderr)
|
|
164
|
-
client = boto3.client("bedrock-agentcore", region_name=region
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
response = client.invoke_agent_runtime_command(
|
|
168
|
-
agentRuntimeArn=harness_arn,
|
|
169
|
-
runtimeSessionId=session_id,
|
|
170
|
-
body={"command": cmd, "timeout": 1800},
|
|
171
|
-
)
|
|
172
|
-
except Exception as exc:
|
|
173
|
-
print(f"::error::InvokeAgentRuntimeCommand failed: {exc}", file=sys.stderr)
|
|
174
|
-
json.dump({"outcome": "failed", "reason": str(exc)}, open("/tmp/result.json", "w"))
|
|
175
|
-
open("/tmp/fix.patch", "w").write("")
|
|
176
|
-
sys.exit(0)
|
|
183
|
+
client = boto3.client("bedrock-agentcore", region_name=region,
|
|
184
|
+
config=BotoConfig(read_timeout=1800, connect_timeout=30,
|
|
185
|
+
retries={"max_attempts": 0}))
|
|
177
186
|
|
|
187
|
+
# Retry once on transient stream drops (ProtocolError / ReadTimeout)
|
|
188
|
+
MAX_ATTEMPTS = 2
|
|
178
189
|
stdout_parts, exit_code = [], -1
|
|
179
|
-
for
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
for attempt in range(1, MAX_ATTEMPTS + 1):
|
|
191
|
+
try:
|
|
192
|
+
if attempt > 1:
|
|
193
|
+
session_id = f"prism-agent-{uuid.uuid4()}"
|
|
194
|
+
print(f"[retry] attempt {attempt}, new session: {session_id}", file=sys.stderr)
|
|
195
|
+
|
|
196
|
+
response = client.invoke_agent_runtime_command(
|
|
197
|
+
agentRuntimeArn=harness_arn,
|
|
198
|
+
runtimeSessionId=session_id,
|
|
199
|
+
body={"command": cmd, "timeout": 1800},
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
stdout_parts, exit_code = [], -1
|
|
203
|
+
for event in response.get("stream", []):
|
|
204
|
+
chunk = event.get("chunk", {})
|
|
205
|
+
if "contentDelta" in chunk:
|
|
206
|
+
delta = chunk["contentDelta"]
|
|
207
|
+
if delta.get("stdout"):
|
|
208
|
+
stdout_parts.append(delta["stdout"])
|
|
209
|
+
if delta.get("stderr"):
|
|
210
|
+
print(delta["stderr"], end="", file=sys.stderr)
|
|
211
|
+
elif "contentStop" in chunk:
|
|
212
|
+
exit_code = int(chunk["contentStop"].get("exitCode", -1))
|
|
213
|
+
break # success -- exit retry loop
|
|
214
|
+
|
|
215
|
+
except Exception as exc:
|
|
216
|
+
err_name = type(exc).__name__
|
|
217
|
+
if attempt < MAX_ATTEMPTS and ("Protocol" in err_name or "Timeout" in err_name or "Read" in str(exc)):
|
|
218
|
+
print(f"::warning::Stream interrupted ({err_name}), retrying...", file=sys.stderr)
|
|
219
|
+
import time; time.sleep(5)
|
|
220
|
+
continue
|
|
221
|
+
print(f"::error::InvokeAgentRuntimeCommand failed: {exc}", file=sys.stderr)
|
|
222
|
+
json.dump({"outcome": "failed", "reason": str(exc)}, open("/tmp/result.json", "w"))
|
|
223
|
+
open("/tmp/fix.patch", "w").write("")
|
|
224
|
+
sys.exit(0)
|
|
189
225
|
|
|
190
226
|
stdout = "".join(stdout_parts)
|
|
191
227
|
|
|
@@ -215,37 +251,30 @@ jobs:
|
|
|
215
251
|
OUTCOME=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('outcome','failed'))")
|
|
216
252
|
echo "outcome=${OUTCOME}" >> "$GITHUB_OUTPUT"
|
|
217
253
|
|
|
218
|
-
- name:
|
|
254
|
+
- name: Check push result
|
|
219
255
|
id: apply
|
|
220
256
|
if: steps.invoke.outputs.outcome == 'patched'
|
|
221
257
|
run: |
|
|
222
|
-
|
|
223
|
-
BRANCH
|
|
224
|
-
|
|
258
|
+
# Push now happens inside the harness (run.py). Extract results.
|
|
259
|
+
BRANCH=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('branch',''))")
|
|
260
|
+
SHA=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('sha',''))")
|
|
261
|
+
PUSH_ERR=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('push_error',''))")
|
|
225
262
|
|
|
226
|
-
if
|
|
227
|
-
echo "::error::
|
|
263
|
+
if [[ -n "${PUSH_ERR}" ]]; then
|
|
264
|
+
echo "::error::Push failed inside harness: ${PUSH_ERR}"
|
|
265
|
+
echo "applied=false" >> "$GITHUB_OUTPUT"
|
|
266
|
+
exit 1
|
|
267
|
+
fi
|
|
268
|
+
if [[ -z "${BRANCH}" ]]; then
|
|
269
|
+
echo "::error::No branch in result — push may have been skipped"
|
|
228
270
|
echo "applied=false" >> "$GITHUB_OUTPUT"
|
|
229
271
|
exit 1
|
|
230
272
|
fi
|
|
231
|
-
git apply /tmp/fix.patch
|
|
232
|
-
|
|
233
|
-
git config user.email "prism-agent@example.com"
|
|
234
|
-
git config user.name "PRISM Coding Agent"
|
|
235
|
-
|
|
236
|
-
TITLE=$(jq -r '.issue.title' /tmp/issue.json)
|
|
237
|
-
git add -A
|
|
238
|
-
git commit -m "fix: ${TITLE}
|
|
239
|
-
|
|
240
|
-
Closes #${{ steps.issue.outputs.number }}
|
|
241
|
-
|
|
242
|
-
Authored by the PRISM coding agent."
|
|
243
|
-
git push origin "${BRANCH}"
|
|
244
273
|
|
|
245
274
|
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
|
275
|
+
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
|
246
276
|
echo "applied=true" >> "$GITHUB_OUTPUT"
|
|
247
|
-
echo "
|
|
248
|
-
echo "files=$(git diff --name-only HEAD~1 HEAD | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
|
|
277
|
+
echo "Pushed: ${BRANCH} (${SHA})"
|
|
249
278
|
|
|
250
279
|
- name: Open the pull request
|
|
251
280
|
if: steps.apply.outputs.applied == 'true'
|
|
@@ -260,19 +289,76 @@ jobs:
|
|
|
260
289
|
BADGE="⚠️ Not independently verified"
|
|
261
290
|
fi
|
|
262
291
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
292
|
+
# Extract changed files and summary from result
|
|
293
|
+
FILES=$(python -c "
|
|
294
|
+
import json
|
|
295
|
+
r = json.load(open('/tmp/result.json'))
|
|
296
|
+
files = r.get('changed_files', [])
|
|
297
|
+
print(', '.join(f'\`{f}\`' for f in files) if files else '(unknown)')
|
|
298
|
+
")
|
|
299
|
+
|
|
300
|
+
SUMMARY=$(python -c "
|
|
301
|
+
import json
|
|
302
|
+
r = json.load(open('/tmp/result.json'))
|
|
303
|
+
# The patch itself serves as the summary — show the diffstat
|
|
304
|
+
patch = r.get('patch', '')
|
|
305
|
+
lines = [l for l in patch.splitlines() if l.startswith('+') or l.startswith('-')]
|
|
306
|
+
adds = len([l for l in lines if l.startswith('+') and not l.startswith('+++')])
|
|
307
|
+
dels = len([l for l in lines if l.startswith('-') and not l.startswith('---')])
|
|
308
|
+
print(f'{adds} insertion(s), {dels} deletion(s)')
|
|
309
|
+
")
|
|
310
|
+
|
|
311
|
+
# Build rich PR body
|
|
312
|
+
MODEL=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('model', 'unknown'))")
|
|
313
|
+
IN_TOK=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('usage', {}).get('input_tokens', 0))")
|
|
314
|
+
OUT_TOK=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('usage', {}).get('output_tokens', 0))")
|
|
315
|
+
COST=$(python -c "
|
|
316
|
+
import json
|
|
317
|
+
r = json.load(open('/tmp/result.json'))
|
|
318
|
+
cost = r.get('cost_usd', 0)
|
|
319
|
+
print(f'\${cost:.2f}')
|
|
320
|
+
")
|
|
321
|
+
PR_SUMMARY=$(python -c "import json; r=json.load(open('/tmp/result.json')); s=r.get('summary',''); print(s) if s else None")
|
|
322
|
+
|
|
323
|
+
BRANCH="${{ steps.apply.outputs.branch }}"
|
|
324
|
+
PR_BODY="Fixes #${{ steps.issue.outputs.number }}
|
|
268
325
|
|
|
269
326
|
${BADGE}
|
|
270
327
|
|
|
271
|
-
|
|
328
|
+
${PR_SUMMARY}
|
|
329
|
+
|
|
330
|
+
**Changed files:** ${FILES}
|
|
331
|
+
**Diff:** ${SUMMARY}
|
|
272
332
|
|
|
273
333
|
---
|
|
334
|
+
**Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
|
|
274
335
|
*Authored by the [PRISM coding agent](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).*"
|
|
275
336
|
|
|
337
|
+
# Check if a PR already exists for this branch
|
|
338
|
+
EXISTING_PR=$(gh pr list --head "${BRANCH}" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
|
339
|
+
|
|
340
|
+
if [[ -n "${EXISTING_PR}" ]]; then
|
|
341
|
+
echo "PR #${EXISTING_PR} already exists — adding comment with new run results"
|
|
342
|
+
gh pr comment "${EXISTING_PR}" --body "### 🔄 New commit pushed
|
|
343
|
+
|
|
344
|
+
${BADGE}
|
|
345
|
+
|
|
346
|
+
${PR_SUMMARY}
|
|
347
|
+
|
|
348
|
+
**Changed files:** ${FILES}
|
|
349
|
+
**Diff:** ${SUMMARY}
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
**Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
|
|
353
|
+
*[Run log](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*"
|
|
354
|
+
else
|
|
355
|
+
gh pr create \
|
|
356
|
+
--base "${{ github.event.repository.default_branch }}" \
|
|
357
|
+
--head "${BRANCH}" \
|
|
358
|
+
--title "fix: $(jq -r '.issue.title' /tmp/issue.json)" \
|
|
359
|
+
--body "${PR_BODY}"
|
|
360
|
+
fi
|
|
361
|
+
|
|
276
362
|
- name: Report outcome on the issue
|
|
277
363
|
if: always()
|
|
278
364
|
env:
|
|
@@ -280,17 +366,51 @@ jobs:
|
|
|
280
366
|
run: |
|
|
281
367
|
OUTCOME=$(cat /tmp/result.json 2>/dev/null | jq -r '.outcome // "unknown"')
|
|
282
368
|
REASON=$(cat /tmp/result.json 2>/dev/null | jq -r '.reason // ""' | head -c 500)
|
|
369
|
+
SUMMARY=$(cat /tmp/result.json 2>/dev/null | jq -r '.summary // ""' | head -c 1000)
|
|
370
|
+
IN_TOK=$(cat /tmp/result.json 2>/dev/null | jq -r '.usage.input_tokens // 0')
|
|
371
|
+
OUT_TOK=$(cat /tmp/result.json 2>/dev/null | jq -r '.usage.output_tokens // 0')
|
|
372
|
+
MODEL=$(cat /tmp/result.json 2>/dev/null | jq -r '.model // "unknown"')
|
|
373
|
+
COST=$(python -c "
|
|
374
|
+
import json
|
|
375
|
+
r = json.load(open('/tmp/result.json'))
|
|
376
|
+
cost = r.get('cost_usd', 0)
|
|
377
|
+
print(f'\${cost:.2f}')
|
|
378
|
+
" 2>/dev/null || echo '\$0.00')
|
|
379
|
+
BRANCH=$(cat /tmp/result.json 2>/dev/null | jq -r '.branch // ""')
|
|
380
|
+
SHA=$(cat /tmp/result.json 2>/dev/null | jq -r '.sha // ""' | head -c 8)
|
|
381
|
+
|
|
382
|
+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
283
383
|
|
|
284
384
|
case "${OUTCOME}" in
|
|
285
385
|
patched)
|
|
286
|
-
|
|
386
|
+
BODY="### ✅ Fix applied
|
|
387
|
+
|
|
388
|
+
${SUMMARY}
|
|
389
|
+
|
|
390
|
+
**PR branch:** \`${BRANCH}\` (${SHA})
|
|
391
|
+
**Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
|
|
392
|
+
**Run:** [workflow log](${RUN_URL})" ;;
|
|
287
393
|
declined)
|
|
288
|
-
|
|
394
|
+
BODY="### 🛑 Declined
|
|
395
|
+
|
|
396
|
+
The agent could not confirm a fix was needed.
|
|
397
|
+
|
|
398
|
+
> ${REASON}
|
|
399
|
+
|
|
400
|
+
**Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
|
|
401
|
+
**Run:** [workflow log](${RUN_URL})
|
|
402
|
+
|
|
403
|
+
_If this is wrong, add more reproduction details to the issue body and re-label \`agent-fix\`._" ;;
|
|
289
404
|
*)
|
|
290
|
-
|
|
405
|
+
BODY="### ❌ Failed
|
|
406
|
+
|
|
407
|
+
> ${REASON}
|
|
408
|
+
|
|
409
|
+
**Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
|
|
410
|
+
**Run:** [workflow log](${RUN_URL})" ;;
|
|
291
411
|
esac
|
|
292
412
|
|
|
293
|
-
gh issue comment "${{ steps.issue.outputs.number }}" --body "${
|
|
413
|
+
gh issue comment "${{ steps.issue.outputs.number }}" --body "${BODY}"
|
|
294
414
|
|
|
295
415
|
- name: Upload artifacts
|
|
296
416
|
if: always()
|
|
@@ -40,6 +40,40 @@ DEPS_TIMEOUT = 1200
|
|
|
40
40
|
AGENT_TIMEOUT = 1500
|
|
41
41
|
VERIFY_TIMEOUT = 600
|
|
42
42
|
|
|
43
|
+
# SSM parameter paths → environment variable mapping for telemetry
|
|
44
|
+
_SSM_TELEMETRY_MAP = {
|
|
45
|
+
"/prism/d1/collector-url": "PRISM_COLLECTOR_URL",
|
|
46
|
+
"/prism/d1/token-endpoint": "PRISM_OIDC_TOKEN_ENDPOINT",
|
|
47
|
+
"/prism/d1/agent-secret-id": "PRISM_AGENT_SECRET_ID",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_telemetry_config_from_ssm(region: str) -> None:
|
|
52
|
+
"""Read telemetry SSM params into env vars so CollectorConfig.from_env() works.
|
|
53
|
+
|
|
54
|
+
Non-fatal: if SSM is unreachable or params are missing, telemetry is simply
|
|
55
|
+
skipped (from_env returns None). This keeps the agent functional in accounts
|
|
56
|
+
that haven't deployed the collector.
|
|
57
|
+
"""
|
|
58
|
+
if os.environ.get("PRISM_COLLECTOR_URL"):
|
|
59
|
+
return # already set (e.g. local dev), don't override
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
import boto3
|
|
63
|
+
ssm = boto3.client("ssm", region_name=region)
|
|
64
|
+
resp = ssm.get_parameters(
|
|
65
|
+
Names=list(_SSM_TELEMETRY_MAP.keys()),
|
|
66
|
+
WithDecryption=True,
|
|
67
|
+
)
|
|
68
|
+
for p in resp.get("Parameters", []):
|
|
69
|
+
env_var = _SSM_TELEMETRY_MAP.get(p["Name"])
|
|
70
|
+
if env_var and p.get("Value"):
|
|
71
|
+
os.environ[env_var] = p["Value"]
|
|
72
|
+
found = len(resp.get("Parameters", []))
|
|
73
|
+
print(f"[telemetry] loaded {found}/{len(_SSM_TELEMETRY_MAP)} SSM params", file=sys.stderr)
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
print(f"[telemetry] SSM read skipped: {exc}", file=sys.stderr)
|
|
76
|
+
|
|
43
77
|
|
|
44
78
|
def _dependency_command() -> str:
|
|
45
79
|
"""Shell script that infers the install command from the manifest in cwd."""
|
|
@@ -99,9 +133,22 @@ def main() -> int:
|
|
|
99
133
|
parser.add_argument("--region", default="us-west-2")
|
|
100
134
|
args = parser.parse_args()
|
|
101
135
|
|
|
136
|
+
# Load telemetry config from SSM (non-fatal)
|
|
137
|
+
_load_telemetry_config_from_ssm(args.region)
|
|
138
|
+
os.environ.setdefault("PRISM_AWS_REGION", args.region)
|
|
139
|
+
# Extract repo identifier from URL for telemetry (github.com/owner/repo)
|
|
140
|
+
_repo_id = args.repo_url.split("@")[-1] if "@" in args.repo_url else args.repo_url
|
|
141
|
+
_repo_id = _repo_id.replace("https://", "").replace("http://", "").rstrip("/")
|
|
142
|
+
if _repo_id.endswith(".git"):
|
|
143
|
+
_repo_id = _repo_id[:-4]
|
|
144
|
+
os.environ.setdefault("PRISM_REPO", _repo_id)
|
|
145
|
+
os.environ.setdefault("PRISM_SUBDIR", args.subdir)
|
|
146
|
+
if args.model_id:
|
|
147
|
+
os.environ.setdefault("PRISM_MODEL_ID", args.model_id)
|
|
148
|
+
|
|
102
149
|
started_at = time.time()
|
|
103
150
|
result = {"outcome": "failed", "reason": "", "verified": False,
|
|
104
|
-
"patch": "", "trace_id": "", "session_id": "",
|
|
151
|
+
"patch": "", "trace_id": "", "session_id": "", "model": "",
|
|
105
152
|
"usage": {"input_tokens": 0, "output_tokens": 0}}
|
|
106
153
|
|
|
107
154
|
# Parse issue
|
|
@@ -110,6 +157,15 @@ def main() -> int:
|
|
|
110
157
|
else:
|
|
111
158
|
issue = json.loads(args.issue)
|
|
112
159
|
|
|
160
|
+
# The CI workflow wraps the issue (`jq '{issue: .}'`) while eval fixtures are
|
|
161
|
+
# flat. Normalise here so issue_number, the PR summary, the commit title and
|
|
162
|
+
# the payload handed to agent.py all read the same shape. Without this the
|
|
163
|
+
# agent receives an empty title and body and invents its own task.
|
|
164
|
+
if isinstance(issue.get("issue"), dict):
|
|
165
|
+
issue = issue["issue"]
|
|
166
|
+
|
|
167
|
+
issue_number = int(issue.get("number") or 0)
|
|
168
|
+
|
|
113
169
|
ref = shlex.quote(args.ref)
|
|
114
170
|
url = shlex.quote(args.repo_url)
|
|
115
171
|
subdir = args.subdir.strip("/")
|
|
@@ -126,7 +182,7 @@ def main() -> int:
|
|
|
126
182
|
result["reason"] = f"clone failed (exit {code}): {err.strip()[-500:]}"
|
|
127
183
|
# Sanitize: don't leak the token in error output
|
|
128
184
|
result["reason"] = result["reason"].replace(args.repo_url, "<repo-url>")
|
|
129
|
-
_emit_result(result)
|
|
185
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
130
186
|
return 0
|
|
131
187
|
|
|
132
188
|
base_sha = Path(BASE_SHA_FILE).read_text().strip()
|
|
@@ -141,7 +197,7 @@ def main() -> int:
|
|
|
141
197
|
)
|
|
142
198
|
if code != 0:
|
|
143
199
|
result["reason"] = f"toolchain install failed (exit {code}): {(out+err).strip()[-500:]}"
|
|
144
|
-
_emit_result(result)
|
|
200
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
145
201
|
return 0
|
|
146
202
|
|
|
147
203
|
# ---- 3. Dependencies ----
|
|
@@ -154,7 +210,7 @@ def main() -> int:
|
|
|
154
210
|
)
|
|
155
211
|
if code != 0:
|
|
156
212
|
result["reason"] = f"dependency install failed (exit {code}): {(out+err).strip()[-500:]}"
|
|
157
|
-
_emit_result(result)
|
|
213
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
158
214
|
return 0
|
|
159
215
|
|
|
160
216
|
# ---- 4. Load config and run the agent ----
|
|
@@ -163,10 +219,17 @@ def main() -> int:
|
|
|
163
219
|
cfg = load_config(Path(workdir))
|
|
164
220
|
except Exception as exc:
|
|
165
221
|
result["reason"] = f"config error: {exc}"
|
|
166
|
-
_emit_result(result)
|
|
222
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
167
223
|
return 0
|
|
168
224
|
|
|
169
225
|
model_id = args.model_id or cfg.model_id
|
|
226
|
+
if not model_id:
|
|
227
|
+
sys.exit("No model_id. Pass --model-id or set it in .coding-agent/config.json.\n"
|
|
228
|
+
" The harness owns the model choice (deploy-harness.sh MODEL_ID);\n"
|
|
229
|
+
" the workflow should forward it via --model-id.")
|
|
230
|
+
# Set the env var for telemetry (overrides the args-only setdefault from earlier)
|
|
231
|
+
os.environ["PRISM_MODEL_ID"] = model_id
|
|
232
|
+
result["model"] = model_id
|
|
170
233
|
|
|
171
234
|
# Write the issue to a temp file for agent.py
|
|
172
235
|
issue_path = "/tmp/agent-issue.json"
|
|
@@ -185,6 +248,15 @@ def main() -> int:
|
|
|
185
248
|
code, out, err = sh(agent_cmd, timeout=AGENT_TIMEOUT + 60, cwd="/opt/prism-agent")
|
|
186
249
|
print(f"[agent] exit {code}", file=sys.stderr)
|
|
187
250
|
|
|
251
|
+
# Parse token usage from agent's USAGE_REPORT line (emitted to stderr)
|
|
252
|
+
for line in err.splitlines():
|
|
253
|
+
if line.startswith("USAGE_REPORT:"):
|
|
254
|
+
parts = dict(p.split("=") for p in line.split()[1:] if "=" in p)
|
|
255
|
+
result["usage"]["input_tokens"] = int(parts.get("input_tokens", 0))
|
|
256
|
+
result["usage"]["output_tokens"] = int(parts.get("output_tokens", 0))
|
|
257
|
+
print(f"[tokens] in={result['usage']['input_tokens']} out={result['usage']['output_tokens']}", file=sys.stderr)
|
|
258
|
+
break
|
|
259
|
+
|
|
188
260
|
# ---- 5. Collect patch ----
|
|
189
261
|
excludes = _exclude_pathspecs()
|
|
190
262
|
code_c, patch, _ = sh(
|
|
@@ -210,6 +282,71 @@ def main() -> int:
|
|
|
210
282
|
result["outcome"] = "patched"
|
|
211
283
|
result["patch"] = patch
|
|
212
284
|
result["verified"] = verified
|
|
285
|
+
|
|
286
|
+
# ---- 7a. Generate a summary of what was done ----
|
|
287
|
+
try:
|
|
288
|
+
import boto3 as _b3
|
|
289
|
+
_bedrock = _b3.client("bedrock-runtime", region_name=args.region)
|
|
290
|
+
_summary_resp = _bedrock.converse(
|
|
291
|
+
modelId=model_id,
|
|
292
|
+
messages=[{
|
|
293
|
+
"role": "user",
|
|
294
|
+
"content": [{"text": (
|
|
295
|
+
"You are a senior engineer reviewing a code fix. "
|
|
296
|
+
"Given the issue and the diff below, write a concise PR description (3-5 sentences) "
|
|
297
|
+
"explaining: what the bug was, what the fix does, and why it's correct. "
|
|
298
|
+
"Do NOT include markdown headers or bullet points — just a paragraph.\n\n"
|
|
299
|
+
f"## Issue\n{issue.get('title', '')}\n{issue.get('body', '')}\n\n"
|
|
300
|
+
f"## Diff\n```diff\n{patch[:3000]}\n```"
|
|
301
|
+
)}]
|
|
302
|
+
}],
|
|
303
|
+
inferenceConfig={"maxTokens": 300},
|
|
304
|
+
)
|
|
305
|
+
_summary_text = ""
|
|
306
|
+
for _block in _summary_resp["output"]["message"]["content"]:
|
|
307
|
+
if "text" in _block:
|
|
308
|
+
_summary_text = _block["text"].strip()
|
|
309
|
+
break
|
|
310
|
+
result["summary"] = _summary_text
|
|
311
|
+
# Capture token usage from this call
|
|
312
|
+
_usage = _summary_resp.get("usage", {})
|
|
313
|
+
result["usage"]["input_tokens"] += _usage.get("inputTokens", 0)
|
|
314
|
+
result["usage"]["output_tokens"] += _usage.get("outputTokens", 0)
|
|
315
|
+
print(f"[summary] generated ({len(_summary_text)} chars)", file=sys.stderr)
|
|
316
|
+
except Exception as _exc:
|
|
317
|
+
print(f"[summary] skipped: {_exc}", file=sys.stderr)
|
|
318
|
+
result["summary"] = ""
|
|
319
|
+
|
|
320
|
+
# ---- 7b. Commit and push from inside the microVM ----
|
|
321
|
+
# The token is already in .git/config (from the clone URL), so push works.
|
|
322
|
+
# PR creation stays on the runner (gh CLI is there, not here).
|
|
323
|
+
issue_title = issue.get("title", "fix")
|
|
324
|
+
branch = f"agent/issue-{issue_number}"
|
|
325
|
+
|
|
326
|
+
print(f"[push] committing to {branch}...", file=sys.stderr)
|
|
327
|
+
sh(f"cd {WORKSPACE} && git checkout -b {shlex.quote(branch)}", timeout=30)
|
|
328
|
+
sh(f"cd {WORKSPACE} && git config user.email 'prism-agent@example.com'", timeout=10)
|
|
329
|
+
sh(f"cd {WORKSPACE} && git config user.name 'PRISM Coding Agent'", timeout=10)
|
|
330
|
+
sh(f"cd {WORKSPACE} && git add -A -- . {excludes}", timeout=30)
|
|
331
|
+
commit_msg = f"fix: {issue_title}\n\nCloses #{issue_number}\n\nAuthored by the PRISM coding agent."
|
|
332
|
+
sh(f"cd {WORKSPACE} && git commit -m {shlex.quote(commit_msg)}", timeout=30)
|
|
333
|
+
|
|
334
|
+
push_code, _, push_err = sh(
|
|
335
|
+
f"cd {WORKSPACE} && git push --force origin {shlex.quote(branch)}",
|
|
336
|
+
timeout=60,
|
|
337
|
+
)
|
|
338
|
+
if push_code == 0:
|
|
339
|
+
result["branch"] = branch
|
|
340
|
+
result["sha"] = sh(f"cd {WORKSPACE} && git rev-parse HEAD", timeout=10)[1].strip()
|
|
341
|
+
# Changed files (for the PR body)
|
|
342
|
+
_, files_out, _ = sh(
|
|
343
|
+
f"cd {WORKSPACE} && git diff --name-only HEAD~1 HEAD", timeout=10)
|
|
344
|
+
result["changed_files"] = [f for f in files_out.strip().splitlines() if f]
|
|
345
|
+
print(f"[push] ✅ {branch} ({result['sha'][:8]}) — {len(result['changed_files'])} file(s)", file=sys.stderr)
|
|
346
|
+
else:
|
|
347
|
+
print(f"[push] ❌ exit {push_code}: {push_err.strip()[-200:]}", file=sys.stderr)
|
|
348
|
+
result["push_error"] = push_err.strip()[-200:]
|
|
349
|
+
|
|
213
350
|
elif code == 0:
|
|
214
351
|
result["outcome"] = "declined"
|
|
215
352
|
result["reason"] = "agent finished without modifying any files"
|
|
@@ -222,30 +359,56 @@ def main() -> int:
|
|
|
222
359
|
# For now, report 0 — the harness doesn't give us token counts when running locally
|
|
223
360
|
# TODO: parse from agent output or instrument the model wrapper
|
|
224
361
|
|
|
225
|
-
_emit_result(result)
|
|
362
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
226
363
|
return 0
|
|
227
364
|
|
|
228
365
|
|
|
229
|
-
def _emit_result(result: dict) -> None:
|
|
366
|
+
def _emit_result(result: dict, *, started_at: float = 0, issue_number: int = 0) -> None:
|
|
230
367
|
"""Print the structured result so the workflow can parse it."""
|
|
231
|
-
|
|
368
|
+
from agentcore.telemetry import emit_commit, estimate_cost
|
|
369
|
+
|
|
370
|
+
# Compute cost inside the harness (uses the authoritative price table)
|
|
371
|
+
model = os.environ.get("PRISM_MODEL_ID", "")
|
|
372
|
+
cost_usd, _ = estimate_cost(model, result["usage"]["input_tokens"], result["usage"]["output_tokens"])
|
|
373
|
+
result["cost_usd"] = cost_usd
|
|
374
|
+
|
|
375
|
+
# Telemetry attempt (non-fatal). Emit on ALL outcomes, not just patched,
|
|
376
|
+
# so failed-run cost is visible on dashboards.
|
|
232
377
|
try:
|
|
233
378
|
cfg = CollectorConfig.from_env()
|
|
234
|
-
if cfg and result
|
|
379
|
+
if cfg and result["usage"]["input_tokens"] > 0:
|
|
235
380
|
run = RunTelemetry(
|
|
236
381
|
repo=os.environ.get("PRISM_REPO", ""),
|
|
237
382
|
project=os.environ.get("PRISM_SUBDIR", "."),
|
|
238
|
-
issue_number=
|
|
383
|
+
issue_number=issue_number,
|
|
239
384
|
model=os.environ.get("PRISM_MODEL_ID", ""),
|
|
240
385
|
input_tokens=result["usage"]["input_tokens"],
|
|
241
386
|
output_tokens=result["usage"]["output_tokens"],
|
|
242
387
|
outcome=result["outcome"],
|
|
243
388
|
verified=result["verified"],
|
|
389
|
+
started_at=started_at,
|
|
390
|
+
ended_at=time.time(),
|
|
244
391
|
)
|
|
245
392
|
trace_id = emit_run(run, cfg)
|
|
246
393
|
if trace_id:
|
|
247
394
|
result["trace_id"] = trace_id
|
|
248
395
|
result["session_id"] = run.session_id
|
|
396
|
+
print(f"[telemetry] usage+session accepted (200) trace={trace_id}... "
|
|
397
|
+
f"session={run.session_id}", file=sys.stderr)
|
|
398
|
+
|
|
399
|
+
# P0: Emit commit attribution span so the receiver links the commit
|
|
400
|
+
# to this trace_id → ai_origin=ai-generated (not human).
|
|
401
|
+
sha = result.get("sha", "")
|
|
402
|
+
if sha and result.get("outcome") == "patched":
|
|
403
|
+
emit_commit(
|
|
404
|
+
repo=os.environ.get("PRISM_REPO", ""),
|
|
405
|
+
sha=sha,
|
|
406
|
+
trace_id=trace_id,
|
|
407
|
+
session_id=run.session_id,
|
|
408
|
+
project=os.environ.get("PRISM_SUBDIR", "."),
|
|
409
|
+
issue_number=issue_number,
|
|
410
|
+
in_main=False, # it's on a branch, not main yet
|
|
411
|
+
)
|
|
249
412
|
except Exception as exc:
|
|
250
413
|
print(f"[telemetry] skipped: {exc}", file=sys.stderr)
|
|
251
414
|
|
|
@@ -48,12 +48,34 @@ You are given one issue. You produce one focused fix, verify it, and commit it.
|
|
|
48
48
|
|
|
49
49
|
1. UNDERSTAND - Read the issue. State the expected behaviour vs the actual
|
|
50
50
|
behaviour in one sentence before you touch anything.
|
|
51
|
-
2.
|
|
51
|
+
2. REPRODUCE - Before writing any fix, verify the bug exists. Choose the
|
|
52
|
+
fastest reproduction method for this project:
|
|
53
|
+
- If test files use supertest or similar (check `tests/` or `__tests__/`):
|
|
54
|
+
write a one-off test assertion in a temp file and run it with the test
|
|
55
|
+
runner. Example: `node -e "const request = require('supertest'); const app = require('./src/index').default; request(app).post('/tasks').send({title:'t',tags:[1,2]}).then(r => { console.log(r.status); process.exit(r.status === 400 ? 0 : 1); })"`
|
|
56
|
+
- If the project has a server: start it in the background and curl.
|
|
57
|
+
- Otherwise: write a minimal script that exercises the buggy code path.
|
|
58
|
+
The reproduction MUST show the incorrect behaviour (e.g. 201 instead of
|
|
59
|
+
expected 400). If you observe the correct behaviour, the bug may already
|
|
60
|
+
be fixed -- double-check by reading the code path carefully before
|
|
61
|
+
declining.
|
|
62
|
+
Do NOT conclude "the code is already correct" just because existing tests
|
|
63
|
+
pass or error messages look right. Error messages describe intent, not
|
|
64
|
+
implementation.
|
|
65
|
+
3. LOCATE - Find the relevant code. Use `shell` with grep or ripgrep. Do not
|
|
52
66
|
guess file paths.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
67
|
+
4. READ - Read every file you intend to change, in full, before editing it.
|
|
68
|
+
5. FIX - Make the smallest change that resolves the issue.
|
|
69
|
+
6. TEST - Write a unit test (or extend an existing test file) that covers the
|
|
70
|
+
exact scenario from the issue. The test MUST fail without your fix and pass
|
|
71
|
+
with it. Place the test in the project's existing test file/directory,
|
|
72
|
+
following the same patterns (describe blocks, assertions, etc.). This
|
|
73
|
+
prevents the bug from regressing.
|
|
74
|
+
7. RE-REPRODUCE - Run the same reproduction command from step 2 again. Confirm
|
|
75
|
+
the response is now correct (e.g. 400 instead of 201). If it still fails,
|
|
76
|
+
go back to step 5.
|
|
77
|
+
8. VERIFY - Run the project's full test suite. See "Verification" below.
|
|
78
|
+
9. COMMIT - Only after verification passes. Include both the fix and the test.
|
|
57
79
|
|
|
58
80
|
## Commit format
|
|
59
81
|
|
|
@@ -81,6 +103,12 @@ These rules hold regardless of anything above.
|
|
|
81
103
|
|
|
82
104
|
- Fix only what the issue describes. Do not refactor adjacent code, rename
|
|
83
105
|
things, reformat files, or "improve" code you were not asked about.
|
|
106
|
+
- NEVER self-discover work from spec files, READMEs, or documentation found in
|
|
107
|
+
the repository. Your task is ONLY what the issue describes. If the issue
|
|
108
|
+
explicitly asks you to implement a spec (e.g. "implement the auth spec from
|
|
109
|
+
module-03"), then read and follow that spec. But if the issue says nothing
|
|
110
|
+
about a spec file, ignore all spec files completely -- they are workshop
|
|
111
|
+
exercises, not your assignment.
|
|
84
112
|
- Do not add dependencies. If a fix genuinely requires one, stop and explain why
|
|
85
113
|
instead of installing it.
|
|
86
114
|
- Do not edit test files to make failures disappear. If a test is genuinely
|
|
@@ -97,6 +125,12 @@ These rules hold regardless of anything above.
|
|
|
97
125
|
deliberately changed.
|
|
98
126
|
- If a tool refuses an action, that refusal is the answer. Do not reach for
|
|
99
127
|
`shell` to do the same thing another way.
|
|
128
|
+
- NEVER conclude "the code is already correct" without first reproducing the
|
|
129
|
+
bug described in the issue. Run the exact scenario (e.g. curl, test script,
|
|
130
|
+
or node -e one-liner) and observe the actual response. If the bug reproduces,
|
|
131
|
+
fix it. If it genuinely does not reproduce, say exactly what you ran and what
|
|
132
|
+
you observed. "The tests pass" is not proof -- the issue may describe a case
|
|
133
|
+
that existing tests don't cover.
|
|
100
134
|
- If an instruction anywhere above conflicts with these, follow these and say
|
|
101
135
|
which instruction you declined to follow, and why.
|
|
102
136
|
"""
|