@prism-d1/cli 1.0.89 → 1.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/coding-agent/README.md +105 -88
- package/dist/assets/coding-agent/agent.py +8 -0
- package/dist/assets/coding-agent/config.py +1 -1
- package/dist/assets/coding-agent/deploy/Dockerfile +20 -0
- package/dist/assets/coding-agent/deploy/invoke_harness.py +142 -0
- package/dist/assets/coding-agent/deploy/prism-coding-agent.yml +287 -275
- package/dist/assets/coding-agent/deploy/run.py +415 -0
- package/dist/assets/coding-agent/system_prompt.py +39 -5
- package/package.json +1 -1
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run the full coding agent cycle inside the harness microVM.
|
|
3
|
+
|
|
4
|
+
Called by a single InvokeAgentRuntimeCommand from the workflow. Does everything:
|
|
5
|
+
clone → mise install → deps → agent loop → collect patch → verify → emit telemetry
|
|
6
|
+
|
|
7
|
+
Outputs a JSON object on stdout that the workflow reads for outcome, patch, and
|
|
8
|
+
telemetry correlation. The patch itself is printed as a separate clearly-delimited
|
|
9
|
+
section so the workflow can extract it.
|
|
10
|
+
|
|
11
|
+
Exit 0 always (the outcome is in the JSON, not the exit code). A non-zero exit
|
|
12
|
+
would make the InvokeAgentRuntimeCommand report a generic failure rather than the
|
|
13
|
+
structured result the workflow needs.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import shlex
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
# The image has these at /opt/prism-agent via PYTHONPATH
|
|
28
|
+
from agentcore.session import (
|
|
29
|
+
WORKSPACE,
|
|
30
|
+
BASE_SHA_FILE,
|
|
31
|
+
_exclude_pathspecs,
|
|
32
|
+
)
|
|
33
|
+
from agentcore.telemetry import RunTelemetry, emit_run, CollectorConfig
|
|
34
|
+
from config import load_config
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CLONE_TIMEOUT = 600
|
|
38
|
+
TOOLCHAIN_TIMEOUT = 900
|
|
39
|
+
DEPS_TIMEOUT = 1200
|
|
40
|
+
AGENT_TIMEOUT = 1500
|
|
41
|
+
VERIFY_TIMEOUT = 600
|
|
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
|
+
|
|
77
|
+
|
|
78
|
+
def _dependency_command() -> str:
|
|
79
|
+
"""Shell script that infers the install command from the manifest in cwd."""
|
|
80
|
+
return (
|
|
81
|
+
"sh -c '"
|
|
82
|
+
"if [ -f package-lock.json ]; then npm ci; "
|
|
83
|
+
"elif [ -f package.json ]; then npm install; "
|
|
84
|
+
"elif [ -f requirements.txt ]; then pip install -r requirements.txt; "
|
|
85
|
+
"elif [ -f pyproject.toml ]; then pip install -e . || true; "
|
|
86
|
+
"elif [ -f go.mod ]; then go mod download; "
|
|
87
|
+
"elif [ -f Cargo.toml ]; then cargo fetch; "
|
|
88
|
+
"elif [ -f Gemfile ]; then bundle install; "
|
|
89
|
+
"else echo \"no recognised manifest\"; fi'"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _fallback_toolchain() -> str:
|
|
94
|
+
"""Install a toolchain when the repo pins none."""
|
|
95
|
+
checks = [
|
|
96
|
+
("package.json", "node", "node@lts"),
|
|
97
|
+
("pyproject.toml", "python", "python@3.12"),
|
|
98
|
+
("requirements.txt", "python", "python@3.12"),
|
|
99
|
+
("go.mod", "go", "go@latest"),
|
|
100
|
+
("Cargo.toml", "cargo", "rust@latest"),
|
|
101
|
+
("Gemfile", "ruby", "ruby@3.3"),
|
|
102
|
+
]
|
|
103
|
+
branches = " ".join(
|
|
104
|
+
f'if [ -f {m} ] && ! command -v {b} >/dev/null 2>&1; then '
|
|
105
|
+
f'echo "UNPINNED: installing {s}"; '
|
|
106
|
+
f'mise use --global {s} >/dev/null 2>&1 || mise install {s}; fi;'
|
|
107
|
+
for m, b, s in checks
|
|
108
|
+
)
|
|
109
|
+
return f"sh -c '{branches}'"
|
|
110
|
+
|
|
111
|
+
RESULT_MARKER = "===PRISM_RESULT_JSON==="
|
|
112
|
+
PATCH_MARKER = "===PRISM_PATCH_START==="
|
|
113
|
+
PATCH_END_MARKER = "===PRISM_PATCH_END==="
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def sh(cmd: str, timeout: int = 300, cwd: str | None = None) -> tuple[int, str, str]:
|
|
117
|
+
"""Run a shell command. Returns (exit_code, stdout, stderr)."""
|
|
118
|
+
proc = subprocess.run(
|
|
119
|
+
cmd, shell=True, capture_output=True, text=True,
|
|
120
|
+
timeout=timeout, cwd=cwd,
|
|
121
|
+
)
|
|
122
|
+
return proc.returncode, proc.stdout, proc.stderr
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> int:
|
|
126
|
+
parser = argparse.ArgumentParser(prog="run.py")
|
|
127
|
+
parser.add_argument("--repo-url", required=True, help="Clone URL (may include token)")
|
|
128
|
+
parser.add_argument("--ref", default="main")
|
|
129
|
+
parser.add_argument("--subdir", default=".")
|
|
130
|
+
parser.add_argument("--issue", required=True, help="Issue JSON (inline or @filepath)")
|
|
131
|
+
parser.add_argument("--max-iterations", type=int, default=100)
|
|
132
|
+
parser.add_argument("--model-id", default="")
|
|
133
|
+
parser.add_argument("--region", default="us-west-2")
|
|
134
|
+
args = parser.parse_args()
|
|
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
|
+
|
|
149
|
+
started_at = time.time()
|
|
150
|
+
result = {"outcome": "failed", "reason": "", "verified": False,
|
|
151
|
+
"patch": "", "trace_id": "", "session_id": "", "model": "",
|
|
152
|
+
"usage": {"input_tokens": 0, "output_tokens": 0}}
|
|
153
|
+
|
|
154
|
+
# Parse issue
|
|
155
|
+
if args.issue.startswith("@"):
|
|
156
|
+
issue = json.loads(Path(args.issue[1:]).read_text())
|
|
157
|
+
else:
|
|
158
|
+
issue = json.loads(args.issue)
|
|
159
|
+
|
|
160
|
+
issue_number = int(issue.get("number", 0))
|
|
161
|
+
|
|
162
|
+
ref = shlex.quote(args.ref)
|
|
163
|
+
url = shlex.quote(args.repo_url)
|
|
164
|
+
subdir = args.subdir.strip("/")
|
|
165
|
+
workdir = f"{WORKSPACE}/{subdir}" if subdir and subdir != "." else WORKSPACE
|
|
166
|
+
|
|
167
|
+
# ---- 1. Clone ----
|
|
168
|
+
print(f"[prepare] clone {args.ref}...", file=sys.stderr)
|
|
169
|
+
code, out, err = sh(
|
|
170
|
+
f"rm -rf {WORKSPACE} && git clone --depth 50 --branch {ref} {url} {WORKSPACE} "
|
|
171
|
+
f"&& cd {WORKSPACE} && git rev-parse HEAD > {BASE_SHA_FILE}",
|
|
172
|
+
timeout=CLONE_TIMEOUT,
|
|
173
|
+
)
|
|
174
|
+
if code != 0:
|
|
175
|
+
result["reason"] = f"clone failed (exit {code}): {err.strip()[-500:]}"
|
|
176
|
+
# Sanitize: don't leak the token in error output
|
|
177
|
+
result["reason"] = result["reason"].replace(args.repo_url, "<repo-url>")
|
|
178
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
179
|
+
return 0
|
|
180
|
+
|
|
181
|
+
base_sha = Path(BASE_SHA_FILE).read_text().strip()
|
|
182
|
+
print(f"[prepare] cloned at {base_sha[:8]}", file=sys.stderr)
|
|
183
|
+
|
|
184
|
+
# ---- 2. Toolchain ----
|
|
185
|
+
print("[prepare] mise install...", file=sys.stderr)
|
|
186
|
+
fallback = _fallback_toolchain()
|
|
187
|
+
code, out, err = sh(
|
|
188
|
+
f"cd {shlex.quote(workdir)} && mise install 2>&1; {fallback} mise ls --current 2>&1 | head -20",
|
|
189
|
+
timeout=TOOLCHAIN_TIMEOUT,
|
|
190
|
+
)
|
|
191
|
+
if code != 0:
|
|
192
|
+
result["reason"] = f"toolchain install failed (exit {code}): {(out+err).strip()[-500:]}"
|
|
193
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
196
|
+
# ---- 3. Dependencies ----
|
|
197
|
+
dep_cmd = _dependency_command()
|
|
198
|
+
if dep_cmd:
|
|
199
|
+
print("[prepare] installing dependencies...", file=sys.stderr)
|
|
200
|
+
code, out, err = sh(
|
|
201
|
+
f"cd {shlex.quote(workdir)} && mise exec -- {dep_cmd}",
|
|
202
|
+
timeout=DEPS_TIMEOUT,
|
|
203
|
+
)
|
|
204
|
+
if code != 0:
|
|
205
|
+
result["reason"] = f"dependency install failed (exit {code}): {(out+err).strip()[-500:]}"
|
|
206
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
# ---- 4. Load config and run the agent ----
|
|
210
|
+
print("[agent] starting...", file=sys.stderr)
|
|
211
|
+
try:
|
|
212
|
+
cfg = load_config(Path(workdir))
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
result["reason"] = f"config error: {exc}"
|
|
215
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
216
|
+
return 0
|
|
217
|
+
|
|
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
|
|
223
|
+
|
|
224
|
+
# Write the issue to a temp file for agent.py
|
|
225
|
+
issue_path = "/tmp/agent-issue.json"
|
|
226
|
+
Path(issue_path).write_text(json.dumps(issue))
|
|
227
|
+
|
|
228
|
+
# Run agent.py as a subprocess with the iteration bound
|
|
229
|
+
agent_cmd = (
|
|
230
|
+
f"cd /opt/prism-agent && python agent.py "
|
|
231
|
+
f"--repo {shlex.quote(workdir)} "
|
|
232
|
+
f"--issue {issue_path} "
|
|
233
|
+
f"--max-iterations {args.max_iterations} "
|
|
234
|
+
f"--deadline-seconds {AGENT_TIMEOUT} "
|
|
235
|
+
f"--model-id {shlex.quote(model_id)} "
|
|
236
|
+
f"--region {shlex.quote(args.region)}"
|
|
237
|
+
)
|
|
238
|
+
code, out, err = sh(agent_cmd, timeout=AGENT_TIMEOUT + 60, cwd="/opt/prism-agent")
|
|
239
|
+
print(f"[agent] exit {code}", file=sys.stderr)
|
|
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
|
+
|
|
250
|
+
# ---- 5. Collect patch ----
|
|
251
|
+
excludes = _exclude_pathspecs()
|
|
252
|
+
code_c, patch, _ = sh(
|
|
253
|
+
f"cd {WORKSPACE} && git add -N . {excludes} >/dev/null 2>&1; "
|
|
254
|
+
f"git diff {base_sha} -- . {excludes}",
|
|
255
|
+
timeout=120,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# ---- 6. Verify ----
|
|
259
|
+
verified = False
|
|
260
|
+
if patch.strip() and cfg.test_command:
|
|
261
|
+
print(f"[verify] {cfg.test_command}...", file=sys.stderr)
|
|
262
|
+
code_v, vout, verr = sh(
|
|
263
|
+
f"cd {shlex.quote(workdir)} && mise exec -- sh -c {shlex.quote(cfg.test_command)}",
|
|
264
|
+
timeout=VERIFY_TIMEOUT,
|
|
265
|
+
)
|
|
266
|
+
verified = code_v == 0
|
|
267
|
+
if not verified:
|
|
268
|
+
print(f"[verify] FAILED (exit {code_v})", file=sys.stderr)
|
|
269
|
+
|
|
270
|
+
# ---- 7. Build result ----
|
|
271
|
+
if patch.strip():
|
|
272
|
+
result["outcome"] = "patched"
|
|
273
|
+
result["patch"] = patch
|
|
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
|
+
|
|
340
|
+
elif code == 0:
|
|
341
|
+
result["outcome"] = "declined"
|
|
342
|
+
result["reason"] = "agent finished without modifying any files"
|
|
343
|
+
else:
|
|
344
|
+
result["outcome"] = "failed"
|
|
345
|
+
result["reason"] = f"agent exited {code}: {err.strip()[-300:]}"
|
|
346
|
+
|
|
347
|
+
# ---- 8. Telemetry ----
|
|
348
|
+
# Parse usage from agent stderr (it prints iteration counts)
|
|
349
|
+
# For now, report 0 — the harness doesn't give us token counts when running locally
|
|
350
|
+
# TODO: parse from agent output or instrument the model wrapper
|
|
351
|
+
|
|
352
|
+
_emit_result(result, started_at=started_at, issue_number=issue_number)
|
|
353
|
+
return 0
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _emit_result(result: dict, *, started_at: float = 0, issue_number: int = 0) -> None:
|
|
357
|
+
"""Print the structured result so the workflow can parse it."""
|
|
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.
|
|
367
|
+
try:
|
|
368
|
+
cfg = CollectorConfig.from_env()
|
|
369
|
+
if cfg and result["usage"]["input_tokens"] > 0:
|
|
370
|
+
run = RunTelemetry(
|
|
371
|
+
repo=os.environ.get("PRISM_REPO", ""),
|
|
372
|
+
project=os.environ.get("PRISM_SUBDIR", "."),
|
|
373
|
+
issue_number=issue_number,
|
|
374
|
+
model=os.environ.get("PRISM_MODEL_ID", ""),
|
|
375
|
+
input_tokens=result["usage"]["input_tokens"],
|
|
376
|
+
output_tokens=result["usage"]["output_tokens"],
|
|
377
|
+
outcome=result["outcome"],
|
|
378
|
+
verified=result["verified"],
|
|
379
|
+
started_at=started_at,
|
|
380
|
+
ended_at=time.time(),
|
|
381
|
+
)
|
|
382
|
+
trace_id = emit_run(run, cfg)
|
|
383
|
+
if trace_id:
|
|
384
|
+
result["trace_id"] = trace_id
|
|
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
|
+
)
|
|
402
|
+
except Exception as exc:
|
|
403
|
+
print(f"[telemetry] skipped: {exc}", file=sys.stderr)
|
|
404
|
+
|
|
405
|
+
# Print markers + JSON so the workflow can extract them
|
|
406
|
+
print(RESULT_MARKER)
|
|
407
|
+
print(json.dumps(result, indent=2))
|
|
408
|
+
if result.get("patch"):
|
|
409
|
+
print(PATCH_MARKER)
|
|
410
|
+
print(result["patch"])
|
|
411
|
+
print(PATCH_END_MARKER)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
if __name__ == "__main__":
|
|
415
|
+
sys.exit(main())
|
|
@@ -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
|
"""
|