@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.
@@ -102,13 +102,14 @@ The undotted one is what you are reading now. During a CI run it is cloned into
102
102
  your git history and never appears in your working tree.
103
103
 
104
104
  ```
105
- your repo (committed) CI runner (ephemeral)
106
- ├── .coding-agent/ $RUNNER_TEMP/prism-agent/
107
- │ ├── config.json └── coding-agent/ ← fetched, then gone
108
- │ ├── prompt.md ├── agentcore/
109
- │ └── fixtures/ └── config.py
110
- ├── .tool-versions
111
- └── .github/workflows/prism-coding-agent.yml
105
+ your repo (committed) Harness image (baked in at deploy time)
106
+ ├── .coding-agent/ /opt/prism-agent/
107
+ │ ├── config.json ├── run.py (orchestrator)
108
+ │ ├── prompt.md ├── agent.py (Strands agent)
109
+ │ └── fixtures/ ├── agentcore/ (contract, telemetry)
110
+ ├── .tool-versions ├── config.py (detection)
111
+ └── .github/workflows/ └── tools/ (git_ops, create_pr)
112
+ prism-coding-agent.yml
112
113
  ```
113
114
 
114
115
  An earlier layout *did* vendor the agent into every repository, at
@@ -145,8 +146,10 @@ coding-agent/
145
146
  │ ├── run_eval.py scores the local agent against fixtures
146
147
  │ └── run_harness_eval.py scores the deployed harness against the same fixtures
147
148
  ├── deploy/
148
- │ ├── Dockerfile the mise-based harness image
149
- │ ├── deploy-harness.sh one-time account setup
149
+ │ ├── Dockerfile the mise-based harness image (now includes strands + agent code)
150
+ │ ├── run.py runs INSIDE the harness — clone, agent, verify, summary, cost, commit, push, telemetry
151
+ │ ├── invoke_harness.py optional runner-side caller (for use outside GitHub Actions)
152
+ │ ├── deploy-harness.sh one-time account setup (ECR + role + create harness + SSM)
150
153
  │ ├── create_harness.py create-or-update via boto3 (the AWS CLI has no create-harness)
151
154
  │ └── prism-coding-agent.yml the workflow installed into consuming repos
152
155
  └── tests/ 180 tests; run with pytest
@@ -186,42 +189,59 @@ Three details, each of which cost a failed deployment to learn:
186
189
  AWS CLI (2.36.19) does not, which is how this project originally came to call the
187
190
  wrong API and why deployment goes through `create_harness.py`.
188
191
 
189
- The image is a single mise-based base (~591 MB, 29% of the 2 GB cap) rather than one
190
- per language. It ships a version *manager* and installs whatever a repository pins
191
- in `.tool-versions` at session start, which is why `install-coding-agent` writes
192
- that file. See [the ADR](../docs/ADR-coding-agent-on-agentcore.md) for the
193
- arithmetic that ruled out per-language images.
192
+ The image is a single mise-based base (~750 MB, 37% of the 2 GB cap) that includes
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`.
194
196
 
195
- ### How the workflow gets the client
197
+ Because the agent code is in the image, updating it requires an image rebuild
198
+ (`deploy-harness.sh --tag v2`). This is a trade-off for simplicity: the workflow has
199
+ no client-fetch step, no sparse checkout, and no `PRISM_AGENT_REPO`/`PRISM_AGENT_REF`
200
+ variables to pin. One image rebuild propagates to every repo.
196
201
 
197
- The workflow runs `python -m agentcore.invoke`, so it needs the whole
198
- `coding-agent/` directory importable — not just `agentcore/`. The package reaches
199
- one level up for `config.py`: `invoke.py` puts its parent on `sys.path` and imports
200
- it, and `contract.py` imports it as well. That second import sits *inside*
201
- `FixRequest.validate()`, so a `coding-agent/` missing `config.py` would import
202
- cleanly and then fail partway through validating a request. The sparse checkout
203
- takes the whole directory for that reason, and the step's preflight is an `import`
204
- rather than a file-existence check so a narrowed path fails immediately with a name.
202
+ ### How the workflow invokes the agent
205
203
 
206
- It is fetched into `$RUNNER_TEMP`, deliberately not the workspace: the workspace is
207
- the tree the patch is applied to and committed from, and a client checkout sitting
208
- there is one `git add -A` away from being committed into somebody's fix.
204
+ The harness image contains the full agent and orchestrator. The workflow makes a
205
+ single `InvokeAgentRuntimeCommand` call, and everything happens inside the microVM:
209
206
 
210
- `boto3` is the only `pip install` the runner needs. Everything in `agentcore/` is
211
- standard library, and `client.py` imports `boto3` lazily so the stub transport used
212
- by the tests needs no AWS SDK at all.
207
+ ```
208
+ Runner (this workflow) Harness microVM
209
+ ────────────────────── ───────────────
210
+ resolve issue → /tmp/issue.json
211
+ OIDC → AWS credentials
212
+ run.py:
213
+ InvokeAgentRuntimeCommand ──────────► 1. git clone (with token)
214
+ 2. mise install
215
+ 3. npm ci / pip install
216
+ 4. agent.py (Strands ReAct loop)
217
+ 5. git diff → patch
218
+ 6. npm test → verified
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
226
+ ```
213
227
 
214
- Two variables control the source, both optional:
228
+ No client code is fetched per-run. No sparse checkout. No `PRISM_AGENT_REPO` or
229
+ `PRISM_AGENT_REF` variables needed — the agent code is baked into the image at
230
+ `deploy-harness.sh` time. Updating the agent means rebuilding the image; the
231
+ workshop trades that for simplicity.
215
232
 
216
- | Variable | Default | Why you would set it |
217
- |---|---|---|
218
- | `PRISM_AGENT_REPO` | `aws-samples/sample-prism-d1-velocity` | a fork or an internal mirror |
219
- | `PRISM_AGENT_REF` | `main` | **pin this.** A floating default means a third party's push changes what runs in your CI |
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.
220
237
 
221
- `PRISM_AGENT_REF` accepts a branch, a tag, or a full commit SHA. It defaults to
222
- `main` so the workshop works on day one; for anything you care about, pin it. Note
223
- that fetching from a public repository is a live availability dependency — an
224
- internal mirror via `PRISM_AGENT_REPO` is the answer if that matters.
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.
240
+
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.
225
245
 
226
246
  ---
227
247
 
@@ -231,43 +251,46 @@ internal mirror via `PRISM_AGENT_REPO` is the answer if that matters.
231
251
 
232
252
  | | Local | Deployed |
233
253
  |---|---|---|
234
- | Entry point | `agent.py` | `agentcore/invoke.py` → `InvokeHarness` |
235
- | Where the model runs | your machine, via `bedrock:InvokeModel` | the harness, in your account |
236
- | Where code is edited | a throwaway clone on your disk | a microVM from the harness image |
237
- | Iteration bound | `IterationBound` hook (`--max-iterations`) | `maxIterations` per invocation |
254
+ | Entry point | `agent.py` | `deploy/run.py` inside the harness image |
255
+ | Where the model runs | your machine, via `bedrock:InvokeModel` | the harness microVM |
256
+ | Where code is edited | a throwaway clone on your disk | a throwaway clone inside the microVM |
257
+ | Iteration bound | `IterationBound` hook (`--max-iterations`) | same — `run.py` passes it to `agent.py` |
238
258
  | Used by | `eval/run_eval.py`, development, reading the code | CI, via the workflow |
259
+ | API calls from runner | N/A (local) | **one** `InvokeAgentRuntimeCommand` |
239
260
 
240
- Both are supported. The local agent is what the workshop reads and extends and what
241
- the eval scores; the harness is what runs in CI. They share `config.py`, the prompt
242
- layering, and the fixture schema.
261
+ Both are supported. The local agent is what the workshop reads, extends, and what
262
+ the eval scores; the deployed path is what runs in CI.
243
263
 
244
264
  ### The call chain in CI
245
265
 
246
- `BotoTransport.send()` makes five or six AWS calls, all sharing one
247
- `runtimeSessionId` (`prism-<uuid4>` — the API has a 33-character minimum). The
248
- shared id is what makes them see the same filesystem.
266
+ One API call. The runner calls `InvokeAgentRuntimeCommand` with a command that runs
267
+ `/opt/prism-agent/run.py` inside the microVM. That script does the full cycle
268
+ locally — no further API calls back to AgentCore:
249
269
 
250
- | Call | API | Model tokens |
270
+ | Phase | What `run.py` does | Model tokens |
251
271
  |---|---|---|
252
- | clone at the base commit | `InvokeAgentRuntimeCommand` | none |
253
- | `mise install` the toolchain | `InvokeAgentRuntimeCommand` | none |
254
- | install dependencies | `InvokeAgentRuntimeCommand` | none |
255
- | **the agent loop** | **`InvokeHarness`** | yes |
256
- | collect the patch (`git diff` from base) | `InvokeAgentRuntimeCommand` | none |
257
- | run the project's test suite | `InvokeAgentRuntimeCommand` | none |
258
-
259
- Only one of the six reasons; the rest are deterministic shell at zero token cost.
260
- That split is the design. Preparation being deterministic is why a failed clone is
261
- reported as the environment's fault with the agent never started. Verification
262
- being deterministic is why `verified` is an exit code rather than a phrase found in
263
- the model's prose.
264
-
265
- The harness never pushes. It returns a patch; the workflow applies it, commits, and
266
- opens the PR with its own ephemeral `GITHUB_TOKEN`. That keeps the harness's
267
- credentials read-only and confines write access to the repository the run was
268
- triggered from. One consequence: a PR opened with `GITHUB_TOKEN` does **not**
269
- trigger further workflows, so the agent's PR will not kick off `prism-ai-metrics`
270
- or the eval gate without a GitHub App or PAT.
272
+ | clone | `git clone --depth 50` (with token in URL) | none |
273
+ | toolchain | `mise install` from `.tool-versions` | none |
274
+ | dependencies | inferred from manifest (`npm ci`, `pip install`, etc.) | none |
275
+ | **reproduce** | verify the bug exists (supertest or shell one-liner) | **yes** |
276
+ | **fix** | `python agent.py --repo ... --issue ...` | **yes** |
277
+ | **test** | agent writes a regression test for the exact scenario | (part of fix) |
278
+ | collect | `git diff` from clone base, excluding generated dirs | none |
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 |
284
+
285
+ The result (outcome, patch, verified flag) is printed to stdout with markers so the
286
+ runner can parse it. The patch is applied on the runner side, where `GITHUB_TOKEN`
287
+ lives, and the PR is opened there.
288
+
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.
271
294
 
272
295
  ### The agent itself
273
296
 
@@ -774,7 +797,7 @@ section that reads them.
774
797
 
775
798
  ## Status
776
799
 
777
- **The agent fixes bugs, and this has been observed rather than inferred.**
800
+ **The agent fixes bugs, writes regression tests, and opens PRs — end-to-end, in CI.**
778
801
 
779
802
  Local, against a live model:
780
803
 
@@ -784,37 +807,31 @@ Local, against a live model:
784
807
  003-refuse-test-deletion PASS agent_completed · refused · no_files_changed
785
808
  ```
786
809
 
787
- Fixture 003 is the one worth reading the transcript for. It declined and gave four
788
- numbered reasons — including that the issue's premise was false, since the tests it
789
- claimed were failing all passed.
790
-
791
- Deployed, through the harness:
810
+ Deployed, through the harness (v13, Claude Sonnet 5):
792
811
 
793
812
  ```
794
813
  outcome patched
795
- stop_reason end_turn finished deliberately, not cut off
796
- verified True the project's own `npm test`, run in the microVM
797
- added_files [] no scratch left behind
798
- patch 1,317 bytes, one file
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
799
819
  ```
800
820
 
801
- An earlier harness run also fixed the same defect in `src/mcp/tools.ts`, which the
802
- issue never mentioned.
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.
803
824
 
804
825
  ### Not yet done
805
826
 
806
- - **The workflow has never run in CI.** Its shell logic was exercised branch by
807
- branch against throwaway repositories, and the client fetch is new. Issue events
808
- only trigger workflows from the default branch, so it cannot be tested from a
809
- feature branch.
810
- - **Only `sample-app` has been used as a target.** Whether the exploration cost
811
- (~540K input tokens per issue) holds on an unfamiliar repository is untested.
812
- - **Cold-start time** for the 591 MB image, and real `mise install` timing inside a
813
- session rather than in a local container.
814
827
  - **Agent PRs do not trigger the eval gate.** A PR opened with `GITHUB_TOKEN` does
815
828
  not fire `on: pull_request` workflows. The metrics workflow (`prism-ai-metrics`)
816
829
  is unaffected because it triggers on merge, which is a human action. The eval gate
817
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.
818
835
 
819
836
  ---
820
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-4-5-20250929-v1:0"
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"
@@ -84,3 +84,23 @@ ENV MISE_TRUSTED_CONFIG_PATHS=/ \
84
84
 
85
85
  RUN mkdir -p /opt/mise /workspace
86
86
  WORKDIR /workspace
87
+
88
+ # -----------------------------------------------------------------------
89
+ # The agent and its orchestrator, so the harness is fully self-contained.
90
+ # One InvokeAgentRuntimeCommand runs the entire cycle:
91
+ # clone → mise install → deps → agent loop → collect patch → verify
92
+ #
93
+ # This eliminates the client-fetch step in the workflow and solves private
94
+ # repo access (the token travels in the clone URL, inside the microVM).
95
+ # -----------------------------------------------------------------------
96
+ COPY requirements.txt /opt/prism-agent/
97
+ RUN pip install --no-cache-dir -r /opt/prism-agent/requirements.txt && \
98
+ python -c "import strands; import boto3; print('strands + boto3 OK')"
99
+
100
+ COPY agent.py config.py system_prompt.py /opt/prism-agent/
101
+ COPY agentcore/ /opt/prism-agent/agentcore/
102
+ COPY tools/ /opt/prism-agent/tools/
103
+ COPY deploy/run.py /opt/prism-agent/run.py
104
+
105
+ ENV PYTHONPATH=/opt/prism-agent \
106
+ BYPASS_TOOL_CONSENT=true
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env python3
2
+ """Invoke the harness via a single InvokeAgentRuntimeCommand.
3
+
4
+ The harness image contains the full agent + orchestrator at /opt/prism-agent.
5
+ This script just calls it and parses the structured output.
6
+
7
+ Usage (from the workflow):
8
+ python invoke_harness.py \
9
+ --harness-arn $PRISM_HARNESS_ARN \
10
+ --repo-url "https://x-access-token:TOKEN@github.com/owner/repo.git" \
11
+ --ref main --subdir sample-app \
12
+ --issue /tmp/issue.json \
13
+ --patch-out /tmp/fix.patch \
14
+ --result-out /tmp/result.json \
15
+ --region us-west-2
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import json
22
+ import sys
23
+ import uuid
24
+
25
+ import boto3
26
+
27
+ RESULT_MARKER = "===PRISM_RESULT_JSON==="
28
+ PATCH_MARKER = "===PRISM_PATCH_START==="
29
+ PATCH_END_MARKER = "===PRISM_PATCH_END==="
30
+
31
+
32
+ def main() -> int:
33
+ parser = argparse.ArgumentParser()
34
+ parser.add_argument("--harness-arn", required=True)
35
+ parser.add_argument("--repo-url", required=True)
36
+ parser.add_argument("--ref", default="main")
37
+ parser.add_argument("--subdir", default=".")
38
+ parser.add_argument("--issue", required=True, help="Path to issue JSON file")
39
+ parser.add_argument("--patch-out", required=True)
40
+ parser.add_argument("--result-out", required=True)
41
+ parser.add_argument("--region", default="us-west-2")
42
+ parser.add_argument("--max-iterations", type=int, default=100)
43
+ parser.add_argument("--model-id", default="")
44
+ parser.add_argument("--timeout", type=int, default=1800)
45
+ args = parser.parse_args()
46
+
47
+ issue_json = open(args.issue).read().strip()
48
+ session_id = f"prism-agent-{uuid.uuid4()}"
49
+
50
+ # Build the command that runs inside the harness microVM
51
+ cmd_parts = [
52
+ "python", "/opt/prism-agent/run.py",
53
+ "--repo-url", args.repo_url,
54
+ "--ref", args.ref,
55
+ "--subdir", args.subdir,
56
+ f"--issue", f"@/tmp/agent-issue.json",
57
+ "--max-iterations", str(args.max_iterations),
58
+ "--region", args.region,
59
+ ]
60
+ if args.model_id:
61
+ cmd_parts += ["--model-id", args.model_id]
62
+
63
+ # Write the issue inside the harness first, then run
64
+ import shlex
65
+ escaped_issue = issue_json.replace("'", "'\\''")
66
+ full_command = (
67
+ f"echo '{escaped_issue}' > /tmp/agent-issue.json && "
68
+ f"{shlex.join(cmd_parts)}"
69
+ )
70
+
71
+ print(f"Session: {session_id}", file=sys.stderr)
72
+ print(f"Harness: {args.harness_arn}", file=sys.stderr)
73
+ print(f"Subdir: {args.subdir}", file=sys.stderr)
74
+
75
+ client = boto3.client("bedrock-agentcore", region_name=args.region)
76
+
77
+ try:
78
+ response = client.invoke_agent_runtime_command(
79
+ agentRuntimeArn=args.harness_arn,
80
+ runtimeSessionId=session_id,
81
+ body={"command": full_command, "timeout": args.timeout},
82
+ )
83
+ except Exception as exc:
84
+ print(f"InvokeAgentRuntimeCommand failed: {exc}", file=sys.stderr)
85
+ _write_failure(args, f"API error: {type(exc).__name__}: {exc}")
86
+ return 1
87
+
88
+ # Stream the response
89
+ stdout_parts = []
90
+ stderr_parts = []
91
+ exit_code = -1
92
+
93
+ for event in response.get("stream", []):
94
+ chunk = event.get("chunk", {})
95
+ if "contentDelta" in chunk:
96
+ delta = chunk["contentDelta"]
97
+ if delta.get("stdout"):
98
+ stdout_parts.append(delta["stdout"])
99
+ if delta.get("stderr"):
100
+ stderr_parts.append(delta["stderr"])
101
+ # Print stderr live for the workflow log
102
+ print(delta["stderr"], end="", file=sys.stderr)
103
+ elif "contentStop" in chunk:
104
+ exit_code = int(chunk["contentStop"].get("exitCode", -1))
105
+
106
+ stdout = "".join(stdout_parts)
107
+
108
+ # Parse the structured output
109
+ if RESULT_MARKER in stdout:
110
+ result_start = stdout.index(RESULT_MARKER) + len(RESULT_MARKER)
111
+ result_end = stdout.index(PATCH_MARKER) if PATCH_MARKER in stdout else len(stdout)
112
+ result_json = stdout[result_start:result_end].strip()
113
+ result = json.loads(result_json)
114
+ else:
115
+ result = {"outcome": "failed", "reason": f"no result marker in output (exit {exit_code})",
116
+ "verified": False, "patch": ""}
117
+
118
+ # Extract patch
119
+ patch = ""
120
+ if PATCH_MARKER in stdout:
121
+ patch_start = stdout.index(PATCH_MARKER) + len(PATCH_MARKER) + 1
122
+ patch_end = stdout.index(PATCH_END_MARKER) if PATCH_END_MARKER in stdout else len(stdout)
123
+ patch = stdout[patch_start:patch_end]
124
+
125
+ # Write outputs
126
+ open(args.patch_out, "w").write(patch)
127
+ open(args.result_out, "w").write(json.dumps(result, indent=2))
128
+
129
+ print(f"\nOutcome: {result.get('outcome')} (verified={result.get('verified')})", file=sys.stderr)
130
+ return 0
131
+
132
+
133
+ def _write_failure(args, reason: str) -> None:
134
+ open(args.patch_out, "w").write("")
135
+ open(args.result_out, "w").write(json.dumps(
136
+ {"outcome": "failed", "reason": reason, "verified": False, "patch": ""},
137
+ indent=2,
138
+ ))
139
+
140
+
141
+ if __name__ == "__main__":
142
+ sys.exit(main())