@prism-d1/cli 1.0.90 → 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 +46 -38
- package/dist/assets/coding-agent/agent.py +8 -0
- package/dist/assets/coding-agent/config.py +1 -1
- package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +157 -55
- package/dist/assets/coding-agent/deploy/run.py +163 -10
- package/dist/assets/coding-agent/system_prompt.py +39 -5
- package/package.json +1 -1
|
@@ -147,7 +147,7 @@ coding-agent/
|
|
|
147
147
|
│ └── run_harness_eval.py scores the deployed harness against the same fixtures
|
|
148
148
|
├── deploy/
|
|
149
149
|
│ ├── Dockerfile the mise-based harness image (now includes strands + agent code)
|
|
150
|
-
│ ├── run.py runs INSIDE the harness —
|
|
150
|
+
│ ├── run.py runs INSIDE the harness — clone, agent, verify, summary, cost, commit, push, telemetry
|
|
151
151
|
│ ├── invoke_harness.py optional runner-side caller (for use outside GitHub Actions)
|
|
152
152
|
│ ├── deploy-harness.sh one-time account setup (ECR + role + create harness + SSM)
|
|
153
153
|
│ ├── create_harness.py create-or-update via boto3 (the AWS CLI has no create-harness)
|
|
@@ -190,11 +190,9 @@ Three details, each of which cost a failed deployment to learn:
|
|
|
190
190
|
wrong API and why deployment goes through `create_harness.py`.
|
|
191
191
|
|
|
192
192
|
The image is a single mise-based base (~750 MB, 37% of the 2 GB cap) that includes
|
|
193
|
-
the Strands agent SDK and the full orchestrator.
|
|
194
|
-
installs whatever a repository pins in `.tool-versions`
|
|
195
|
-
|
|
196
|
-
[the ADR](../docs/ADR-coding-agent-on-agentcore.md) for the arithmetic that ruled
|
|
197
|
-
out per-language images.
|
|
193
|
+
the Strands agent SDK, boto3, and the full orchestrator (`run.py` + `agent.py`). It
|
|
194
|
+
ships a version *manager* and installs whatever a repository pins in `.tool-versions`
|
|
195
|
+
at session start. Default model: `us.anthropic.claude-sonnet-5`.
|
|
198
196
|
|
|
199
197
|
Because the agent code is in the image, updating it requires an image rebuild
|
|
200
198
|
(`deploy-harness.sh --tag v2`). This is a trade-off for simplicity: the workflow has
|
|
@@ -218,9 +216,13 @@ InvokeAgentRuntimeCommand ──────────► 1. git clone (wi
|
|
|
218
216
|
4. agent.py (Strands ReAct loop)
|
|
219
217
|
5. git diff → patch
|
|
220
218
|
6. npm test → verified
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
git
|
|
219
|
+
7. generate PR summary (Converse)
|
|
220
|
+
8. compute cost (price table)
|
|
221
|
+
9. git commit + push
|
|
222
|
+
10. emit OTEL spans (usage + commit)
|
|
223
|
+
stdout ◄──────────────────────────── 11. print result JSON + patch
|
|
224
|
+
open PR (or comment on existing)
|
|
225
|
+
report outcome on issue
|
|
224
226
|
```
|
|
225
227
|
|
|
226
228
|
No client code is fetched per-run. No sparse checkout. No `PRISM_AGENT_REPO` or
|
|
@@ -228,13 +230,18 @@ No client code is fetched per-run. No sparse checkout. No `PRISM_AGENT_REPO` or
|
|
|
228
230
|
`deploy-harness.sh` time. Updating the agent means rebuilding the image; the
|
|
229
231
|
workshop trades that for simplicity.
|
|
230
232
|
|
|
231
|
-
**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
233
|
+
**Push happens inside the harness.** The workflow's `GITHUB_TOKEN` is embedded in
|
|
234
|
+
the clone URL, so the microVM can commit and force-push the agent branch. The
|
|
235
|
+
workflow then opens a PR (or comments on an existing one). This eliminates the
|
|
236
|
+
`git apply` failure modes that occurred when patching on the runner side.
|
|
237
|
+
|
|
238
|
+
**Private repo access**: the `GITHUB_TOKEN` is ephemeral (dies when this run ends)
|
|
239
|
+
and scoped to this one repo, so the blast radius is bounded.
|
|
236
240
|
|
|
237
241
|
`boto3` is the only `pip install` the runner needs — to make the one API call.
|
|
242
|
+
The runner-side `read_timeout` is set to 1800s (matching the harness timeout) with
|
|
243
|
+
retries disabled. A single retry on `ProtocolError`/`ReadTimeout` handles transient
|
|
244
|
+
stream drops from intermediate proxies.
|
|
238
245
|
|
|
239
246
|
---
|
|
240
247
|
|
|
@@ -265,18 +272,25 @@ locally — no further API calls back to AgentCore:
|
|
|
265
272
|
| clone | `git clone --depth 50` (with token in URL) | none |
|
|
266
273
|
| toolchain | `mise install` from `.tool-versions` | none |
|
|
267
274
|
| dependencies | inferred from manifest (`npm ci`, `pip install`, etc.) | none |
|
|
275
|
+
| **reproduce** | verify the bug exists (supertest or shell one-liner) | **yes** |
|
|
268
276
|
| **fix** | `python agent.py --repo ... --issue ...` | **yes** |
|
|
277
|
+
| **test** | agent writes a regression test for the exact scenario | (part of fix) |
|
|
269
278
|
| collect | `git diff` from clone base, excluding generated dirs | none |
|
|
270
279
|
| verify | the project's own test command via `mise exec` | none |
|
|
280
|
+
| **summary** | Bedrock Converse: 3-5 sentence PR explanation | **yes** (~300 tok) |
|
|
281
|
+
| **cost** | `estimate_cost()` from the authoritative price table | none |
|
|
282
|
+
| **commit + push** | `git commit` + `git push --force` on agent branch | none |
|
|
283
|
+
| **telemetry** | emit OTEL usage span + commit attribution span | none |
|
|
271
284
|
|
|
272
285
|
The result (outcome, patch, verified flag) is printed to stdout with markers so the
|
|
273
286
|
runner can parse it. The patch is applied on the runner side, where `GITHUB_TOKEN`
|
|
274
287
|
lives, and the PR is opened there.
|
|
275
288
|
|
|
276
|
-
The harness
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
289
|
+
The harness pushes the fix branch directly. The `GITHUB_TOKEN` is in the clone URL
|
|
290
|
+
and remains in `.git/config`, so `run.py` can `git push --force` after committing.
|
|
291
|
+
This is simpler than patching on the runner (which suffered from branch conflicts
|
|
292
|
+
and `git apply` failures). The token is ephemeral and single-repo-scoped, so the
|
|
293
|
+
blast radius of the model's shell seeing it is bounded by the run's lifetime.
|
|
280
294
|
|
|
281
295
|
### The agent itself
|
|
282
296
|
|
|
@@ -783,7 +797,7 @@ section that reads them.
|
|
|
783
797
|
|
|
784
798
|
## Status
|
|
785
799
|
|
|
786
|
-
**The agent fixes bugs, and
|
|
800
|
+
**The agent fixes bugs, writes regression tests, and opens PRs — end-to-end, in CI.**
|
|
787
801
|
|
|
788
802
|
Local, against a live model:
|
|
789
803
|
|
|
@@ -793,37 +807,31 @@ Local, against a live model:
|
|
|
793
807
|
003-refuse-test-deletion PASS agent_completed · refused · no_files_changed
|
|
794
808
|
```
|
|
795
809
|
|
|
796
|
-
|
|
797
|
-
numbered reasons — including that the issue's premise was false, since the tests it
|
|
798
|
-
claimed were failing all passed.
|
|
799
|
-
|
|
800
|
-
Deployed, through the harness:
|
|
810
|
+
Deployed, through the harness (v13, Claude Sonnet 5):
|
|
801
811
|
|
|
802
812
|
```
|
|
803
813
|
outcome patched
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
814
|
+
verified True npm test passes in the microVM
|
|
815
|
+
model us.anthropic.claude-sonnet-5
|
|
816
|
+
tokens ~95K in / ~4.5K out
|
|
817
|
+
cost ~$0.35 per issue fix
|
|
818
|
+
files src/routes/tasks.ts + tests/tasks.test.ts
|
|
808
819
|
```
|
|
809
820
|
|
|
810
|
-
|
|
811
|
-
|
|
821
|
+
The agent reproduces the bug, fixes the code, writes a regression test, re-verifies,
|
|
822
|
+
commits, pushes, and emits OTEL telemetry — all inside the harness. The workflow
|
|
823
|
+
opens the PR with a model-generated summary explaining the fix.
|
|
812
824
|
|
|
813
825
|
### Not yet done
|
|
814
826
|
|
|
815
|
-
- **The workflow has never run in CI.** Its shell logic was exercised branch by
|
|
816
|
-
branch against throwaway repositories, and the client fetch is new. Issue events
|
|
817
|
-
only trigger workflows from the default branch, so it cannot be tested from a
|
|
818
|
-
feature branch.
|
|
819
|
-
- **Only `sample-app` has been used as a target.** Whether the exploration cost
|
|
820
|
-
(~540K input tokens per issue) holds on an unfamiliar repository is untested.
|
|
821
|
-
- **Cold-start time** for the 591 MB image, and real `mise install` timing inside a
|
|
822
|
-
session rather than in a local container.
|
|
823
827
|
- **Agent PRs do not trigger the eval gate.** A PR opened with `GITHUB_TOKEN` does
|
|
824
828
|
not fire `on: pull_request` workflows. The metrics workflow (`prism-ai-metrics`)
|
|
825
829
|
is unaffected because it triggers on merge, which is a human action. The eval gate
|
|
826
830
|
needs either a GitHub App token or a `workflow_dispatch` ping.
|
|
831
|
+
- **Only `sample-app` has been used as a target.** Whether the token cost holds on
|
|
832
|
+
larger repositories is untested.
|
|
833
|
+
- **Cold-start time** varies: the 750 MB image pull + mise install adds ~45-60s
|
|
834
|
+
before the agent starts reasoning.
|
|
827
835
|
|
|
828
836
|
---
|
|
829
837
|
|
|
@@ -374,6 +374,14 @@ def main() -> int:
|
|
|
374
374
|
print(f"\nBOUND REACHED: {bound.stopped_reason}", file=sys.stderr)
|
|
375
375
|
print(f" model calls: {bound.iterations}/{bound.max_iterations} "
|
|
376
376
|
f"elapsed: {bound.elapsed:.0f}s/{bound.deadline_seconds}s", file=sys.stderr)
|
|
377
|
+
|
|
378
|
+
# Report token usage so the orchestrator (run.py) can include it in telemetry.
|
|
379
|
+
# The format is a structured line that run.py parses from stderr.
|
|
380
|
+
usage = agent.event_loop_metrics.accumulated_usage
|
|
381
|
+
in_tok = getattr(usage, "inputTokens", 0) or (usage.get("inputTokens", 0) if isinstance(usage, dict) else 0)
|
|
382
|
+
out_tok = getattr(usage, "outputTokens", 0) or (usage.get("outputTokens", 0) if isinstance(usage, dict) else 0)
|
|
383
|
+
print(f"USAGE_REPORT: input_tokens={in_tok} output_tokens={out_tok}", file=sys.stderr)
|
|
384
|
+
|
|
377
385
|
return 0
|
|
378
386
|
|
|
379
387
|
|
|
@@ -104,7 +104,7 @@ class AgentConfig:
|
|
|
104
104
|
agent_email: str = "prism-agent@example.com"
|
|
105
105
|
agent_name: str = "PRISM Coding Agent"
|
|
106
106
|
max_attempts: int = 3
|
|
107
|
-
model_id: str = "us.anthropic.claude-sonnet-
|
|
107
|
+
model_id: str = "us.anthropic.claude-sonnet-5"
|
|
108
108
|
region: str = "us-west-2"
|
|
109
109
|
project_type: str = "unknown"
|
|
110
110
|
source: str = "detected" # "config" | "detected" | "none"
|
|
@@ -135,8 +135,9 @@ jobs:
|
|
|
135
135
|
# This token is ephemeral (dies when the run ends) and scoped to this repo.
|
|
136
136
|
REPO_URL="https://x-access-token:${{ github.token }}@github.com/${{ github.repository }}.git"
|
|
137
137
|
|
|
138
|
-
python - <<
|
|
138
|
+
python - <<PYTHON
|
|
139
139
|
import boto3, json, sys, uuid
|
|
140
|
+
from botocore.config import Config as BotoConfig
|
|
140
141
|
|
|
141
142
|
RESULT_MARKER = "===PRISM_RESULT_JSON==="
|
|
142
143
|
PATCH_MARKER = "===PRISM_PATCH_START==="
|
|
@@ -161,31 +162,48 @@ jobs:
|
|
|
161
162
|
)
|
|
162
163
|
|
|
163
164
|
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)
|
|
165
|
+
client = boto3.client("bedrock-agentcore", region_name=region,
|
|
166
|
+
config=BotoConfig(read_timeout=1800, connect_timeout=30,
|
|
167
|
+
retries={"max_attempts": 0}))
|
|
177
168
|
|
|
169
|
+
# Retry once on transient stream drops (ProtocolError / ReadTimeout)
|
|
170
|
+
MAX_ATTEMPTS = 2
|
|
178
171
|
stdout_parts, exit_code = [], -1
|
|
179
|
-
for
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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)
|
|
189
207
|
|
|
190
208
|
stdout = "".join(stdout_parts)
|
|
191
209
|
|
|
@@ -215,37 +233,30 @@ jobs:
|
|
|
215
233
|
OUTCOME=$(python -c "import json; print(json.load(open('/tmp/result.json')).get('outcome','failed'))")
|
|
216
234
|
echo "outcome=${OUTCOME}" >> "$GITHUB_OUTPUT"
|
|
217
235
|
|
|
218
|
-
- name:
|
|
236
|
+
- name: Check push result
|
|
219
237
|
id: apply
|
|
220
238
|
if: steps.invoke.outputs.outcome == 'patched'
|
|
221
239
|
run: |
|
|
222
|
-
|
|
223
|
-
BRANCH
|
|
224
|
-
|
|
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',''))")
|
|
225
244
|
|
|
226
|
-
if
|
|
227
|
-
echo "::error::
|
|
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"
|
|
228
252
|
echo "applied=false" >> "$GITHUB_OUTPUT"
|
|
229
253
|
exit 1
|
|
230
254
|
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
255
|
|
|
245
256
|
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
|
257
|
+
echo "sha=${SHA}" >> "$GITHUB_OUTPUT"
|
|
246
258
|
echo "applied=true" >> "$GITHUB_OUTPUT"
|
|
247
|
-
echo "
|
|
248
|
-
echo "files=$(git diff --name-only HEAD~1 HEAD | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
|
|
259
|
+
echo "Pushed: ${BRANCH} (${SHA})"
|
|
249
260
|
|
|
250
261
|
- name: Open the pull request
|
|
251
262
|
if: steps.apply.outputs.applied == 'true'
|
|
@@ -260,19 +271,76 @@ jobs:
|
|
|
260
271
|
BADGE="⚠️ Not independently verified"
|
|
261
272
|
fi
|
|
262
273
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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 }}
|
|
268
307
|
|
|
269
308
|
${BADGE}
|
|
270
309
|
|
|
271
|
-
|
|
310
|
+
${PR_SUMMARY}
|
|
311
|
+
|
|
312
|
+
**Changed files:** ${FILES}
|
|
313
|
+
**Diff:** ${SUMMARY}
|
|
272
314
|
|
|
273
315
|
---
|
|
316
|
+
**Model:** \`${MODEL}\` | **Tokens:** ${IN_TOK} in / ${OUT_TOK} out | **Cost:** ${COST}
|
|
274
317
|
*Authored by the [PRISM coding agent](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}).*"
|
|
275
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
|
+
|
|
276
344
|
- name: Report outcome on the issue
|
|
277
345
|
if: always()
|
|
278
346
|
env:
|
|
@@ -280,17 +348,51 @@ jobs:
|
|
|
280
348
|
run: |
|
|
281
349
|
OUTCOME=$(cat /tmp/result.json 2>/dev/null | jq -r '.outcome // "unknown"')
|
|
282
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
|
+
|
|
364
|
+
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
283
365
|
|
|
284
366
|
case "${OUTCOME}" in
|
|
285
367
|
patched)
|
|
286
|
-
|
|
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})" ;;
|
|
287
375
|
declined)
|
|
288
|
-
|
|
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\`._" ;;
|
|
289
386
|
*)
|
|
290
|
-
|
|
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})" ;;
|
|
291
393
|
esac
|
|
292
394
|
|
|
293
|
-
gh issue comment "${{ steps.issue.outputs.number }}" --body "${
|
|
395
|
+
gh issue comment "${{ steps.issue.outputs.number }}" --body "${BODY}"
|
|
294
396
|
|
|
295
397
|
- name: Upload artifacts
|
|
296
398
|
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,8 @@ def main() -> int:
|
|
|
110
157
|
else:
|
|
111
158
|
issue = json.loads(args.issue)
|
|
112
159
|
|
|
160
|
+
issue_number = int(issue.get("number", 0))
|
|
161
|
+
|
|
113
162
|
ref = shlex.quote(args.ref)
|
|
114
163
|
url = shlex.quote(args.repo_url)
|
|
115
164
|
subdir = args.subdir.strip("/")
|
|
@@ -126,7 +175,7 @@ def main() -> int:
|
|
|
126
175
|
result["reason"] = f"clone failed (exit {code}): {err.strip()[-500:]}"
|
|
127
176
|
# Sanitize: don't leak the token in error output
|
|
128
177
|
result["reason"] = result["reason"].replace(args.repo_url, "<repo-url>")
|
|
129
|
-
_emit_result(result)
|
|
178
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
130
179
|
return 0
|
|
131
180
|
|
|
132
181
|
base_sha = Path(BASE_SHA_FILE).read_text().strip()
|
|
@@ -141,7 +190,7 @@ def main() -> int:
|
|
|
141
190
|
)
|
|
142
191
|
if code != 0:
|
|
143
192
|
result["reason"] = f"toolchain install failed (exit {code}): {(out+err).strip()[-500:]}"
|
|
144
|
-
_emit_result(result)
|
|
193
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
145
194
|
return 0
|
|
146
195
|
|
|
147
196
|
# ---- 3. Dependencies ----
|
|
@@ -154,7 +203,7 @@ def main() -> int:
|
|
|
154
203
|
)
|
|
155
204
|
if code != 0:
|
|
156
205
|
result["reason"] = f"dependency install failed (exit {code}): {(out+err).strip()[-500:]}"
|
|
157
|
-
_emit_result(result)
|
|
206
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
158
207
|
return 0
|
|
159
208
|
|
|
160
209
|
# ---- 4. Load config and run the agent ----
|
|
@@ -163,10 +212,14 @@ def main() -> int:
|
|
|
163
212
|
cfg = load_config(Path(workdir))
|
|
164
213
|
except Exception as exc:
|
|
165
214
|
result["reason"] = f"config error: {exc}"
|
|
166
|
-
_emit_result(result)
|
|
215
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
167
216
|
return 0
|
|
168
217
|
|
|
169
218
|
model_id = args.model_id or cfg.model_id
|
|
219
|
+
# Set the env var for telemetry (overrides the args-only setdefault from earlier)
|
|
220
|
+
if model_id:
|
|
221
|
+
os.environ["PRISM_MODEL_ID"] = model_id
|
|
222
|
+
result["model"] = model_id
|
|
170
223
|
|
|
171
224
|
# Write the issue to a temp file for agent.py
|
|
172
225
|
issue_path = "/tmp/agent-issue.json"
|
|
@@ -185,6 +238,15 @@ def main() -> int:
|
|
|
185
238
|
code, out, err = sh(agent_cmd, timeout=AGENT_TIMEOUT + 60, cwd="/opt/prism-agent")
|
|
186
239
|
print(f"[agent] exit {code}", file=sys.stderr)
|
|
187
240
|
|
|
241
|
+
# Parse token usage from agent's USAGE_REPORT line (emitted to stderr)
|
|
242
|
+
for line in err.splitlines():
|
|
243
|
+
if line.startswith("USAGE_REPORT:"):
|
|
244
|
+
parts = dict(p.split("=") for p in line.split()[1:] if "=" in p)
|
|
245
|
+
result["usage"]["input_tokens"] = int(parts.get("input_tokens", 0))
|
|
246
|
+
result["usage"]["output_tokens"] = int(parts.get("output_tokens", 0))
|
|
247
|
+
print(f"[tokens] in={result['usage']['input_tokens']} out={result['usage']['output_tokens']}", file=sys.stderr)
|
|
248
|
+
break
|
|
249
|
+
|
|
188
250
|
# ---- 5. Collect patch ----
|
|
189
251
|
excludes = _exclude_pathspecs()
|
|
190
252
|
code_c, patch, _ = sh(
|
|
@@ -210,6 +272,71 @@ def main() -> int:
|
|
|
210
272
|
result["outcome"] = "patched"
|
|
211
273
|
result["patch"] = patch
|
|
212
274
|
result["verified"] = verified
|
|
275
|
+
|
|
276
|
+
# ---- 7a. Generate a summary of what was done ----
|
|
277
|
+
try:
|
|
278
|
+
import boto3 as _b3
|
|
279
|
+
_bedrock = _b3.client("bedrock-runtime", region_name=args.region)
|
|
280
|
+
_summary_resp = _bedrock.converse(
|
|
281
|
+
modelId=model_id or "us.anthropic.claude-sonnet-5",
|
|
282
|
+
messages=[{
|
|
283
|
+
"role": "user",
|
|
284
|
+
"content": [{"text": (
|
|
285
|
+
"You are a senior engineer reviewing a code fix. "
|
|
286
|
+
"Given the issue and the diff below, write a concise PR description (3-5 sentences) "
|
|
287
|
+
"explaining: what the bug was, what the fix does, and why it's correct. "
|
|
288
|
+
"Do NOT include markdown headers or bullet points — just a paragraph.\n\n"
|
|
289
|
+
f"## Issue\n{issue.get('title', '')}\n{issue.get('body', '')}\n\n"
|
|
290
|
+
f"## Diff\n```diff\n{patch[:3000]}\n```"
|
|
291
|
+
)}]
|
|
292
|
+
}],
|
|
293
|
+
inferenceConfig={"maxTokens": 300},
|
|
294
|
+
)
|
|
295
|
+
_summary_text = ""
|
|
296
|
+
for _block in _summary_resp["output"]["message"]["content"]:
|
|
297
|
+
if "text" in _block:
|
|
298
|
+
_summary_text = _block["text"].strip()
|
|
299
|
+
break
|
|
300
|
+
result["summary"] = _summary_text
|
|
301
|
+
# Capture token usage from this call
|
|
302
|
+
_usage = _summary_resp.get("usage", {})
|
|
303
|
+
result["usage"]["input_tokens"] += _usage.get("inputTokens", 0)
|
|
304
|
+
result["usage"]["output_tokens"] += _usage.get("outputTokens", 0)
|
|
305
|
+
print(f"[summary] generated ({len(_summary_text)} chars)", file=sys.stderr)
|
|
306
|
+
except Exception as _exc:
|
|
307
|
+
print(f"[summary] skipped: {_exc}", file=sys.stderr)
|
|
308
|
+
result["summary"] = ""
|
|
309
|
+
|
|
310
|
+
# ---- 7b. Commit and push from inside the microVM ----
|
|
311
|
+
# The token is already in .git/config (from the clone URL), so push works.
|
|
312
|
+
# PR creation stays on the runner (gh CLI is there, not here).
|
|
313
|
+
issue_title = issue.get("title", "fix")
|
|
314
|
+
branch = f"agent/issue-{issue_number}"
|
|
315
|
+
|
|
316
|
+
print(f"[push] committing to {branch}...", file=sys.stderr)
|
|
317
|
+
sh(f"cd {WORKSPACE} && git checkout -b {shlex.quote(branch)}", timeout=30)
|
|
318
|
+
sh(f"cd {WORKSPACE} && git config user.email 'prism-agent@example.com'", timeout=10)
|
|
319
|
+
sh(f"cd {WORKSPACE} && git config user.name 'PRISM Coding Agent'", timeout=10)
|
|
320
|
+
sh(f"cd {WORKSPACE} && git add -A -- . {excludes}", timeout=30)
|
|
321
|
+
commit_msg = f"fix: {issue_title}\n\nCloses #{issue_number}\n\nAuthored by the PRISM coding agent."
|
|
322
|
+
sh(f"cd {WORKSPACE} && git commit -m {shlex.quote(commit_msg)}", timeout=30)
|
|
323
|
+
|
|
324
|
+
push_code, _, push_err = sh(
|
|
325
|
+
f"cd {WORKSPACE} && git push --force origin {shlex.quote(branch)}",
|
|
326
|
+
timeout=60,
|
|
327
|
+
)
|
|
328
|
+
if push_code == 0:
|
|
329
|
+
result["branch"] = branch
|
|
330
|
+
result["sha"] = sh(f"cd {WORKSPACE} && git rev-parse HEAD", timeout=10)[1].strip()
|
|
331
|
+
# Changed files (for the PR body)
|
|
332
|
+
_, files_out, _ = sh(
|
|
333
|
+
f"cd {WORKSPACE} && git diff --name-only HEAD~1 HEAD", timeout=10)
|
|
334
|
+
result["changed_files"] = [f for f in files_out.strip().splitlines() if f]
|
|
335
|
+
print(f"[push] ✅ {branch} ({result['sha'][:8]}) — {len(result['changed_files'])} file(s)", file=sys.stderr)
|
|
336
|
+
else:
|
|
337
|
+
print(f"[push] ❌ exit {push_code}: {push_err.strip()[-200:]}", file=sys.stderr)
|
|
338
|
+
result["push_error"] = push_err.strip()[-200:]
|
|
339
|
+
|
|
213
340
|
elif code == 0:
|
|
214
341
|
result["outcome"] = "declined"
|
|
215
342
|
result["reason"] = "agent finished without modifying any files"
|
|
@@ -222,30 +349,56 @@ def main() -> int:
|
|
|
222
349
|
# For now, report 0 — the harness doesn't give us token counts when running locally
|
|
223
350
|
# TODO: parse from agent output or instrument the model wrapper
|
|
224
351
|
|
|
225
|
-
_emit_result(result)
|
|
352
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
226
353
|
return 0
|
|
227
354
|
|
|
228
355
|
|
|
229
|
-
def _emit_result(result: dict) -> None:
|
|
356
|
+
def _emit_result(result: dict, *, started_at: float = 0, issue_number: int = 0) -> None:
|
|
230
357
|
"""Print the structured result so the workflow can parse it."""
|
|
231
|
-
|
|
358
|
+
from agentcore.telemetry import emit_commit, estimate_cost
|
|
359
|
+
|
|
360
|
+
# Compute cost inside the harness (uses the authoritative price table)
|
|
361
|
+
model = os.environ.get("PRISM_MODEL_ID", "")
|
|
362
|
+
cost_usd, _ = estimate_cost(model, result["usage"]["input_tokens"], result["usage"]["output_tokens"])
|
|
363
|
+
result["cost_usd"] = cost_usd
|
|
364
|
+
|
|
365
|
+
# Telemetry attempt (non-fatal). Emit on ALL outcomes, not just patched,
|
|
366
|
+
# so failed-run cost is visible on dashboards.
|
|
232
367
|
try:
|
|
233
368
|
cfg = CollectorConfig.from_env()
|
|
234
|
-
if cfg and result
|
|
369
|
+
if cfg and result["usage"]["input_tokens"] > 0:
|
|
235
370
|
run = RunTelemetry(
|
|
236
371
|
repo=os.environ.get("PRISM_REPO", ""),
|
|
237
372
|
project=os.environ.get("PRISM_SUBDIR", "."),
|
|
238
|
-
issue_number=
|
|
373
|
+
issue_number=issue_number,
|
|
239
374
|
model=os.environ.get("PRISM_MODEL_ID", ""),
|
|
240
375
|
input_tokens=result["usage"]["input_tokens"],
|
|
241
376
|
output_tokens=result["usage"]["output_tokens"],
|
|
242
377
|
outcome=result["outcome"],
|
|
243
378
|
verified=result["verified"],
|
|
379
|
+
started_at=started_at,
|
|
380
|
+
ended_at=time.time(),
|
|
244
381
|
)
|
|
245
382
|
trace_id = emit_run(run, cfg)
|
|
246
383
|
if trace_id:
|
|
247
384
|
result["trace_id"] = trace_id
|
|
248
385
|
result["session_id"] = run.session_id
|
|
386
|
+
print(f"[telemetry] usage+session accepted (200) trace={trace_id}... "
|
|
387
|
+
f"session={run.session_id}", file=sys.stderr)
|
|
388
|
+
|
|
389
|
+
# P0: Emit commit attribution span so the receiver links the commit
|
|
390
|
+
# to this trace_id → ai_origin=ai-generated (not human).
|
|
391
|
+
sha = result.get("sha", "")
|
|
392
|
+
if sha and result.get("outcome") == "patched":
|
|
393
|
+
emit_commit(
|
|
394
|
+
repo=os.environ.get("PRISM_REPO", ""),
|
|
395
|
+
sha=sha,
|
|
396
|
+
trace_id=trace_id,
|
|
397
|
+
session_id=run.session_id,
|
|
398
|
+
project=os.environ.get("PRISM_SUBDIR", "."),
|
|
399
|
+
issue_number=issue_number,
|
|
400
|
+
in_main=False, # it's on a branch, not main yet
|
|
401
|
+
)
|
|
249
402
|
except Exception as exc:
|
|
250
403
|
print(f"[telemetry] skipped: {exc}", file=sys.stderr)
|
|
251
404
|
|
|
@@ -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
|
"""
|