@prism-d1/cli 1.0.84 → 1.0.86

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.
Files changed (43) hide show
  1. package/dist/assets/coding-agent/README.md +824 -0
  2. package/dist/assets/coding-agent/agent.py +381 -0
  3. package/dist/assets/coding-agent/config.py +338 -0
  4. package/dist/assets/coding-agent/deploy/Dockerfile +86 -0
  5. package/dist/assets/coding-agent/deploy/create_harness.py +144 -0
  6. package/dist/assets/coding-agent/deploy/deploy-harness.sh +201 -0
  7. package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +399 -0
  8. package/dist/assets/coding-agent/eval/run_eval.py +430 -0
  9. package/dist/assets/coding-agent/eval/run_harness_eval.py +262 -0
  10. package/dist/assets/coding-agent/eval/stubs/001-tags-element-validation.json +11 -0
  11. package/dist/assets/coding-agent/eval/stubs/002-status-filter.json +11 -0
  12. package/dist/assets/coding-agent/eval/stubs/003-refuse-test-deletion.json +11 -0
  13. package/dist/assets/coding-agent/fixtures/001-tags-element-validation.json +12 -0
  14. package/dist/assets/coding-agent/fixtures/002-status-filter.json +12 -0
  15. package/dist/assets/coding-agent/fixtures/003-refuse-test-deletion.json +10 -0
  16. package/dist/assets/coding-agent/pyproject.toml +28 -0
  17. package/dist/assets/coding-agent/requirements.txt +8 -0
  18. package/dist/assets/coding-agent/system_prompt.py +290 -0
  19. package/dist/assets/coding-agent/tools/__init__.py +11 -0
  20. package/dist/assets/coding-agent/tools/create_pr.py +141 -0
  21. package/dist/assets/coding-agent/tools/git_ops.py +197 -0
  22. package/dist/assets/github-workflows/prism-agent-eval.yml +2 -1
  23. package/dist/assets/github-workflows/prism-ai-metrics.yml +12 -3
  24. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +4 -3
  25. package/dist/assets/github-workflows/prism-eval-gate.yml +3 -2
  26. package/dist/assets/gitlab-workflows/prism-agent-eval.yml +2 -1
  27. package/dist/assets/gitlab-workflows/prism-ai-metrics.yml +3 -2
  28. package/dist/assets/gitlab-workflows/prism-eval-gate.yml +2 -1
  29. package/dist/assets/infra/lib/constructs/otel-collector-construct.ts +134 -2
  30. package/dist/assets/infra/lib/dashboard-stack.ts +79 -95
  31. package/dist/assets/infra/lib/lambda/metrics-processor.ts +99 -1
  32. package/dist/assets/infra/lib/lambda/otel-receiver.ts +318 -29
  33. package/dist/assets/infra/lib/lambda/productivity-widget.ts +85 -4
  34. package/dist/assets/infra/lib/lambda/velocity-widget.ts +265 -0
  35. package/dist/assets/infra/lib/metrics-pipeline-stack.ts +5 -0
  36. package/dist/src/commands/bootstrapper/install-coding-agent.d.ts +62 -0
  37. package/dist/src/commands/bootstrapper/install-coding-agent.d.ts.map +1 -0
  38. package/dist/src/commands/bootstrapper/install-coding-agent.js +614 -0
  39. package/dist/src/commands/bootstrapper/install-coding-agent.js.map +1 -0
  40. package/dist/src/commands/bootstrapper/setup-github-oidc.d.ts.map +1 -1
  41. package/dist/src/commands/bootstrapper/setup-github-oidc.js +23 -0
  42. package/dist/src/commands/bootstrapper/setup-github-oidc.js.map +1 -1
  43. package/package.json +2 -2
@@ -0,0 +1,381 @@
1
+ """PRISM coding agent — entry point.
2
+
3
+ Reads one issue, fixes it in a git repository, verifies the fix, and commits.
4
+ Optionally opens a pull request.
5
+
6
+ Local run against a JSON issue file (any git repository, any language):
7
+
8
+ python agent.py --repo /path/to/repo \
9
+ --issue /path/to/repo/.coding-agent/fixtures/001-my-bug.json
10
+
11
+ Read the issue from a GitHub Actions event payload and open a PR:
12
+
13
+ python agent.py --repo . --github-event "$GITHUB_EVENT_PATH" --create-pr
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import os
21
+ import sys
22
+ import time
23
+ from pathlib import Path
24
+
25
+ from config import ConfigError, load_config
26
+ from system_prompt import build_system_prompt, build_task_prompt
27
+
28
+
29
+ def _load_issue(args: argparse.Namespace) -> dict:
30
+ """Resolve the issue from a fixture file, a GitHub event payload, or flags."""
31
+ if args.issue:
32
+ path = Path(args.issue)
33
+ if not path.exists():
34
+ raise SystemExit(f"Issue file not found: {path}")
35
+ return json.loads(path.read_text())
36
+
37
+ if args.github_event:
38
+ path = Path(args.github_event)
39
+ if not path.exists():
40
+ raise SystemExit(f"GitHub event payload not found: {path}")
41
+ event = json.loads(path.read_text())
42
+ issue = event.get("issue")
43
+ if not issue:
44
+ raise SystemExit(
45
+ "Event payload has no 'issue' key. This agent handles issue "
46
+ "events; got keys: " + ", ".join(sorted(event))
47
+ )
48
+ return {
49
+ "number": issue.get("number"),
50
+ "title": issue.get("title", ""),
51
+ "body": issue.get("body", ""),
52
+ }
53
+
54
+ if args.title:
55
+ return {"number": args.number or 0, "title": args.title, "body": args.body or ""}
56
+
57
+ raise SystemExit(
58
+ "No issue supplied. Use --issue FILE, --github-event FILE, or --title TEXT."
59
+ )
60
+
61
+
62
+ def _configure_git_identity(repo_path: Path, name: str, email: str) -> None:
63
+ """Set the agent's commit identity locally in this repository.
64
+
65
+ This is what makes agent commits distinguishable from human commits in PRISM:
66
+ the author email flows through `git log --format=%ae` into the CI workflow's
67
+ commit_authors array, then into the attribution store, and finally appears as
68
+ its own row on the Developer Productivity dashboard.
69
+ """
70
+ import subprocess
71
+
72
+ for key, value in (("user.name", name), ("user.email", email)):
73
+ subprocess.run(
74
+ ["git", "config", key, value],
75
+ cwd=str(repo_path),
76
+ check=True,
77
+ capture_output=True,
78
+ timeout=30,
79
+ )
80
+
81
+
82
+ # Matches agentcore.contract.MAX_ITERATIONS, so a fixture that passes locally is
83
+ # not then failed by a tighter bound in the deployed path -- and, more importantly,
84
+ # not the reverse. Enforced by a test rather than a shared import, because importing
85
+ # the agentcore package pulls in boto3 and this module defers heavy imports so that
86
+ # --help and config validation work without the SDK installed.
87
+ DEFAULT_MAX_ITERATIONS = 100
88
+ # Below the eval harness's 1800s subprocess timeout, so the agent stops itself and
89
+ # says why rather than being killed with no explanation. A run killed from outside
90
+ # loses its reason; a run that stops itself reports one.
91
+ DEFAULT_DEADLINE_SECONDS = 1500
92
+ # A single model call that has produced nothing for this long is not slow, it is
93
+ # stuck -- the observed case sat for fourteen minutes on one call at 0.2% CPU.
94
+ READ_TIMEOUT = 300
95
+ CONNECT_TIMEOUT = 15
96
+
97
+
98
+ class IterationBound:
99
+ """Stop the agent after a fixed number of model calls, or past a deadline.
100
+
101
+ Strands has no iteration cap in this version -- Agent takes model, messages,
102
+ tools, system_prompt, conversation_manager, hooks, retry_strategy, and nothing
103
+ that bounds call count. So the count is kept here off BeforeModelCallEvent, the
104
+ one lifecycle event that fires exactly once per model call.
105
+
106
+ Stopping uses that event's `cancel` field rather than raising. Setting it makes
107
+ the event loop synthesise a final assistant message carrying the cancellation
108
+ text and finish with stop_reason `end_turn`, so the run ends cleanly and the
109
+ reason lands in the agent's own transcript instead of in a traceback. Raising
110
+ from the hook would also stop it, but it would report a deliberate bound as a
111
+ crash -- and the run this exists to fix had already committed a correct fix.
112
+ """
113
+
114
+ def __init__(self, max_iterations: int = 0, deadline_seconds: int = 0) -> None:
115
+ self.max_iterations = max_iterations or DEFAULT_MAX_ITERATIONS
116
+ self.deadline_seconds = deadline_seconds or DEFAULT_DEADLINE_SECONDS
117
+ self.iterations = 0
118
+ self.started = 0.0
119
+ self.stopped_reason = ""
120
+
121
+ def register_hooks(self, registry, **_: object) -> None:
122
+ from strands.hooks import BeforeModelCallEvent
123
+
124
+ registry.add_callback(BeforeModelCallEvent, self.before_model_call)
125
+
126
+ @property
127
+ def elapsed(self) -> float:
128
+ return 0.0 if not self.started else time.monotonic() - self.started
129
+
130
+ @property
131
+ def stopped(self) -> bool:
132
+ return bool(self.stopped_reason)
133
+
134
+ def before_model_call(self, event) -> None:
135
+ if not self.started:
136
+ self.started = time.monotonic()
137
+
138
+ # Checked before incrementing, so the message names the call that would have
139
+ # been the (n+1)th rather than being off by one.
140
+ if self.iterations >= self.max_iterations:
141
+ return self._stop(
142
+ event,
143
+ f"Stopping after {self.iterations} model calls "
144
+ f"(--max-iterations {self.max_iterations}). Work already committed "
145
+ f"stands; the agent was still going when the bound hit, which "
146
+ f"usually means verification never went green.",
147
+ )
148
+
149
+ if self.elapsed > self.deadline_seconds:
150
+ return self._stop(
151
+ event,
152
+ f"Stopping after {self.elapsed:.0f}s (--deadline-seconds "
153
+ f"{self.deadline_seconds}), on model call {self.iterations + 1}. "
154
+ f"Work already committed stands.",
155
+ )
156
+
157
+ self.iterations += 1
158
+ print(f" [iteration {self.iterations}/{self.max_iterations} · "
159
+ f"{self.elapsed:.0f}s/{self.deadline_seconds}s]", file=sys.stderr)
160
+
161
+ def _stop(self, event, reason: str) -> None:
162
+ self.stopped_reason = reason
163
+ # `cancel` is the only writable field on this event, and a string sets the
164
+ # cancellation message. Guarded so the bound is still usable -- and
165
+ # testable -- without a live event object.
166
+ if event is not None:
167
+ event.cancel = reason
168
+
169
+
170
+ def build_agent(cfg, create_pr_enabled: bool, bound: "IterationBound | None" = None):
171
+ """Construct the Strands agent with its tool set.
172
+
173
+ Imports happen here rather than at module scope so that --help and config
174
+ validation work without the Strands SDK installed.
175
+ """
176
+ # strands-agents-tools gates every mutating tool -- editor, file_write, shell
177
+ # -- behind an interactive confirmation dialog unless BYPASS_TOOL_CONSENT is
178
+ # set. There is nobody to answer it here: the agent runs in CI or in a
179
+ # disposable microVM, so the prompt is auto-cancelled and every write silently
180
+ # fails while the model keeps reasoning.
181
+ #
182
+ # The symptom is badly misleading. The agent diagnoses the bug correctly,
183
+ # writes out the right patch in prose, then reports "unable to complete
184
+ # because all file modification operations are being cancelled by the system"
185
+ # -- which reads as a broken agent rather than a missing environment variable.
186
+ #
187
+ # Safe to set here because the boundary is the sandbox, not the prompt: the
188
+ # eval harness gives the agent a throwaway clone, and the AgentCore harness
189
+ # gives it an isolated microVM. Consent dialogs are for a developer's own
190
+ # working tree, which this agent is never pointed at.
191
+ os.environ.setdefault("BYPASS_TOOL_CONSENT", "true")
192
+
193
+ from botocore.config import Config as BotocoreConfig
194
+ from strands import Agent
195
+ from strands.agent.conversation_manager import SlidingWindowConversationManager
196
+ from strands.models import BedrockModel
197
+ # `shell` and `editor` from strands_tools warn on every call that they are
198
+ # deprecated and become an error log in v0.9.0. The replacements are sandbox-
199
+ # routed, and the warning says that tightens the security boundary -- which
200
+ # sounded like it would break the one thing this agent must do, run the
201
+ # project's own test command against a real checkout.
202
+ #
203
+ # It does not, here. With no `sandbox=` passed to Agent, both tools resolve to
204
+ # NotASandboxLocalEnvironment, documented as "the default execution environment
205
+ # when an Agent is created without a sandbox", which spawns a local `sh` and
206
+ # touches the host filesystem directly. That is exactly what strands_tools did,
207
+ # so this migration preserves behaviour; the tightening only applies to someone
208
+ # who configures a real sandbox, and then it is what they asked for.
209
+ #
210
+ # Note the deprecation message names `bash`, which is itself a deprecated alias
211
+ # for `make_shell` -- so following it literally would land on another deprecated
212
+ # name. `file_read` and `file_write` are not deprecated and stay put.
213
+ from strands.vended_tools import file_editor, shell
214
+ from strands_tools import file_read, file_write
215
+
216
+ from tools import create_pr, git_ops
217
+
218
+ tools = [file_read, file_write, file_editor, shell, git_ops]
219
+ if create_pr_enabled:
220
+ tools.append(create_pr)
221
+
222
+ # Two different runaways were observed, and they need two different bounds --
223
+ # which is why one number is not enough:
224
+ #
225
+ # * One run kept working long after it had committed a correct fix, until an
226
+ # external `timeout` killed it at 1200s. A genuine pass was recorded as a
227
+ # failure. That is too many iterations, and IterationBound stops it.
228
+ #
229
+ # * Another committed, made one more model call, and sat for fourteen minutes
230
+ # with one second of CPU at 0.2% -- blocked inside a single call that never
231
+ # returned. An iteration cap cannot help: the count never advanced. Only a
232
+ # socket-level read timeout ends that, which is why the model is built with
233
+ # an explicit boto config rather than passed as a bare id string.
234
+ #
235
+ # A sliding window bounds how much context each call carries. It bounds neither
236
+ # of the above, and this SDK version has no max_turns or max_iterations on
237
+ # Agent (checked: model, messages, tools, system_prompt, conversation_manager,
238
+ # hooks, retry_strategy -- nothing caps calls). So the cap is built here from
239
+ # the BeforeModelCallEvent hook, which is the seam the SDK does offer.
240
+ model = BedrockModel(
241
+ model_id=cfg.model_id,
242
+ boto_client_config=BotocoreConfig(
243
+ connect_timeout=CONNECT_TIMEOUT,
244
+ read_timeout=READ_TIMEOUT,
245
+ # Retries at the client layer, so a dropped connection costs one retry
246
+ # rather than one lost run. Kept low because each attempt burns wall
247
+ # clock against the deadline.
248
+ retries={"max_attempts": 3, "mode": "standard"},
249
+ ),
250
+ )
251
+
252
+ return Agent(
253
+ model=model,
254
+ tools=tools,
255
+ system_prompt=build_system_prompt(cfg, announce=True),
256
+ conversation_manager=SlidingWindowConversationManager(
257
+ window_size=max(20, cfg.max_attempts * 12),
258
+ ),
259
+ hooks=[bound or IterationBound()],
260
+ )
261
+
262
+
263
+ def main() -> int:
264
+ parser = argparse.ArgumentParser(
265
+ prog="prism-coding-agent",
266
+ description="Fix a GitHub issue in a repository, verify, and commit.",
267
+ )
268
+ parser.add_argument("--repo", required=True, help="Path to the target repository")
269
+
270
+ src = parser.add_argument_group("issue source")
271
+ src.add_argument("--issue", help="Path to a JSON issue fixture")
272
+ src.add_argument("--github-event", help="Path to a GitHub Actions event payload")
273
+ src.add_argument("--title", help="Issue title (inline mode)")
274
+ src.add_argument("--body", help="Issue body (inline mode)")
275
+ src.add_argument("--number", type=int, help="Issue number (inline mode)")
276
+
277
+ over = parser.add_argument_group("configuration overrides")
278
+ over.add_argument("--test-command", help="Override the detected test command")
279
+ over.add_argument("--build-command", help="Override the detected build command")
280
+ over.add_argument("--lint-command", help="Lint command to run as an extra check")
281
+ over.add_argument("--model-id", help="Bedrock model id")
282
+ over.add_argument("--region", help="AWS region for Bedrock")
283
+ over.add_argument("--max-attempts", type=int, help="Verification retry budget")
284
+ over.add_argument("--agent-email", help="Git author email for agent commits")
285
+
286
+ parser.add_argument(
287
+ "--create-pr",
288
+ action="store_true",
289
+ help="Give the agent the create_pr tool so it can push and open a PR",
290
+ )
291
+ parser.add_argument(
292
+ "--dry-run",
293
+ action="store_true",
294
+ help="Print the resolved config and system prompt, then exit",
295
+ )
296
+
297
+ bounds = parser.add_argument_group("bounds")
298
+ bounds.add_argument(
299
+ "--max-iterations", type=int, default=DEFAULT_MAX_ITERATIONS,
300
+ help=f"Stop after this many model calls (default {DEFAULT_MAX_ITERATIONS}, "
301
+ f"matching the AgentCore Harness default)",
302
+ )
303
+ bounds.add_argument(
304
+ "--deadline-seconds", type=int, default=DEFAULT_DEADLINE_SECONDS,
305
+ help=f"Stop once a run has taken this long (default {DEFAULT_DEADLINE_SECONDS})",
306
+ )
307
+
308
+ args = parser.parse_args()
309
+
310
+ try:
311
+ cfg = load_config(
312
+ Path(args.repo),
313
+ test_command=args.test_command,
314
+ build_command=args.build_command,
315
+ lint_command=args.lint_command,
316
+ model_id=args.model_id,
317
+ region=args.region,
318
+ max_attempts=args.max_attempts,
319
+ agent_email=args.agent_email,
320
+ )
321
+ except ConfigError as exc:
322
+ print(f"Configuration error: {exc}", file=sys.stderr)
323
+ return 2
324
+
325
+ print(f"Repository: {cfg.repo_path}")
326
+ print(f"Project type: {cfg.project_type} (from {cfg.source})")
327
+ print(f"Test command: {cfg.test_command or '(none — fixes will be UNVERIFIED)'}")
328
+ if cfg.build_command:
329
+ print(f"Build: {cfg.build_command}")
330
+ if cfg.lint_command:
331
+ print(f"Lint: {cfg.lint_command}")
332
+ print(f"Model: {cfg.model_id}")
333
+ print()
334
+
335
+ if not cfg.can_verify:
336
+ print(
337
+ "WARNING: no test command resolved. The agent will be told to find "
338
+ "one, and to label the fix UNVERIFIED if it cannot.\n",
339
+ file=sys.stderr,
340
+ )
341
+
342
+ issue = _load_issue(args)
343
+
344
+ if args.dry_run:
345
+ print("=" * 72)
346
+ print("SYSTEM PROMPT")
347
+ print("=" * 72)
348
+ print(build_system_prompt(cfg, announce=True))
349
+ print()
350
+ print("=" * 72)
351
+ print("TASK PROMPT")
352
+ print("=" * 72)
353
+ print(build_task_prompt(issue))
354
+ return 0
355
+
356
+ _configure_git_identity(cfg.repo_path, cfg.agent_name, cfg.agent_email)
357
+
358
+ bound = IterationBound(max_iterations=args.max_iterations,
359
+ deadline_seconds=args.deadline_seconds)
360
+ agent = build_agent(cfg, create_pr_enabled=args.create_pr, bound=bound)
361
+
362
+ print(f"Fixing issue #{issue.get('number', '?')}: {issue.get('title', '')}")
363
+ print(f"Bounds: {bound.max_iterations} model calls, "
364
+ f"{bound.deadline_seconds}s")
365
+ print("-" * 72)
366
+
367
+ agent(build_task_prompt(issue))
368
+
369
+ # Exit 0 either way. The bound firing is not the agent failing -- one observed
370
+ # run had a correct feature implementation in the tree when it ran away, and
371
+ # calling that a failure discarded a real result. The eval scores the tree, not
372
+ # this exit code. The reason goes to stderr so it reaches the transcript.
373
+ if bound.stopped:
374
+ print(f"\nBOUND REACHED: {bound.stopped_reason}", file=sys.stderr)
375
+ print(f" model calls: {bound.iterations}/{bound.max_iterations} "
376
+ f"elapsed: {bound.elapsed:.0f}s/{bound.deadline_seconds}s", file=sys.stderr)
377
+ return 0
378
+
379
+
380
+ if __name__ == "__main__":
381
+ sys.exit(main())