@prism-d1/cli 1.9.0 → 1.10.0

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 (64) hide show
  1. package/README.md +48 -0
  2. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +133 -42
  3. package/dist/assets/gitlab-workflows/prism-eval-gate.yml +100 -35
  4. package/dist/assets/kirocrew-app/README.md +12 -9
  5. package/dist/assets/kirocrew-app/app.json +3 -1
  6. package/dist/assets/kirocrew-app/assets/screenshots/page-dark.png +0 -0
  7. package/dist/assets/kirocrew-app/assets/screenshots/page-light.png +0 -0
  8. package/dist/assets/kirocrew-app/assets/screenshots/report-dark.png +0 -0
  9. package/dist/assets/kirocrew-app/assets/screenshots/report-light.png +0 -0
  10. package/dist/bin/cli.d.ts +2 -0
  11. package/dist/bin/cli.d.ts.map +1 -0
  12. package/dist/bin/cli.js +19 -0
  13. package/dist/bin/cli.js.map +1 -0
  14. package/dist/bin/prism-cli.js +12 -17
  15. package/dist/bin/prism-cli.js.map +1 -1
  16. package/dist/src/commands/_top/dashboard.d.ts +2 -0
  17. package/dist/src/commands/_top/dashboard.d.ts.map +1 -1
  18. package/dist/src/commands/_top/dashboard.js +43 -15
  19. package/dist/src/commands/_top/dashboard.js.map +1 -1
  20. package/dist/src/commands/_top/mcp.d.ts.map +1 -1
  21. package/dist/src/commands/_top/mcp.js +3 -15
  22. package/dist/src/commands/_top/mcp.js.map +1 -1
  23. package/dist/src/commands/_top/statusline.d.ts +13 -0
  24. package/dist/src/commands/_top/statusline.d.ts.map +1 -0
  25. package/dist/src/commands/_top/statusline.js +12 -0
  26. package/dist/src/commands/_top/statusline.js.map +1 -0
  27. package/dist/src/commands/_top/version.d.ts +2 -0
  28. package/dist/src/commands/_top/version.d.ts.map +1 -1
  29. package/dist/src/commands/_top/version.js +4 -0
  30. package/dist/src/commands/_top/version.js.map +1 -1
  31. package/dist/src/commands/assessment/deploy-cdk.js +1 -1
  32. package/dist/src/commands/assessment/deploy-cdk.js.map +1 -1
  33. package/dist/src/commands/enable/claude-code.d.ts +81 -0
  34. package/dist/src/commands/enable/claude-code.d.ts.map +1 -0
  35. package/dist/src/commands/enable/claude-code.js +381 -0
  36. package/dist/src/commands/enable/claude-code.js.map +1 -0
  37. package/dist/src/dashboard/page.d.ts +25 -0
  38. package/dist/src/dashboard/page.d.ts.map +1 -1
  39. package/dist/src/dashboard/page.js +66 -7
  40. package/dist/src/dashboard/page.js.map +1 -1
  41. package/dist/src/dashboard/setup-page.d.ts +1 -1
  42. package/dist/src/dashboard/setup-page.d.ts.map +1 -1
  43. package/dist/src/dashboard/setup-page.js +15 -2
  44. package/dist/src/dashboard/setup-page.js.map +1 -1
  45. package/dist/src/dashboard/snapshot.d.ts +31 -0
  46. package/dist/src/dashboard/snapshot.d.ts.map +1 -0
  47. package/dist/src/dashboard/snapshot.js +65 -0
  48. package/dist/src/dashboard/snapshot.js.map +1 -0
  49. package/dist/src/mcp/dashboard-process.d.ts +83 -0
  50. package/dist/src/mcp/dashboard-process.d.ts.map +1 -0
  51. package/dist/src/mcp/dashboard-process.js +184 -0
  52. package/dist/src/mcp/dashboard-process.js.map +1 -0
  53. package/dist/src/mcp/server.d.ts.map +1 -1
  54. package/dist/src/mcp/server.js +8 -3
  55. package/dist/src/mcp/server.js.map +1 -1
  56. package/dist/src/mcp/tools.d.ts +7 -1
  57. package/dist/src/mcp/tools.d.ts.map +1 -1
  58. package/dist/src/mcp/tools.js +100 -2
  59. package/dist/src/mcp/tools.js.map +1 -1
  60. package/dist/src/statusline/statusline.d.ts +135 -0
  61. package/dist/src/statusline/statusline.d.ts.map +1 -0
  62. package/dist/src/statusline/statusline.js +398 -0
  63. package/dist/src/statusline/statusline.js.map +1 -0
  64. package/package.json +3 -1
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # prism-cli
2
+
3
+ The command line for [PRISM D1 Velocity](https://github.com/aws-samples/sample-prism-d1-velocity): measure, secure and review AI-assisted development from your own machine. Codewatch git hooks, a local dashboard, an MCP server for AI agents, AWS Security Agent scans, read-only AWS account checks, and the setup helpers for a team's PRISM deployment.
4
+
5
+ > Sample project, not production-ready. See [SECURITY.md](https://github.com/aws-samples/sample-prism-d1-velocity/blob/main/SECURITY.md).
6
+
7
+ ## Install
8
+
9
+ Node.js 22 or later:
10
+
11
+ ```bash
12
+ npm install -g @prism-d1/cli
13
+ prism-cli version # installed version, newer release on npm, other copies on PATH
14
+ ```
15
+
16
+ ## Quick start
17
+
18
+ ```bash
19
+ prism-cli enable codewatch # in a repo: secret scan on commit, AI review on push (report only)
20
+ prism-cli dashboard # local page on 127.0.0.1: results, AI usage, setup checklist
21
+ prism-cli aws check # read-only AWS account checks; then prism-cli aws recommend
22
+ ```
23
+
24
+ Send AI usage to your team's PRISM with `prism-cli enable codeburn --url <OtelCollectorUrl>` (the URL comes from your platform team). Every command takes `--help`.
25
+
26
+ ## Documentation
27
+
28
+ - [Using PRISM locally](https://github.com/aws-samples/sample-prism-d1-velocity/blob/main/USER_GUIDE.md#using-prism-locally): what each feature does and where it shows up (VS Code family editors, Kiro Crew, Claude Code)
29
+ - [Command reference](https://github.com/aws-samples/sample-prism-d1-velocity/blob/main/docs/CLI-REFERENCE.md): every command and flag
30
+ - [prism-cli Overview](https://github.com/aws-samples/sample-prism-d1-velocity/blob/main/docs/PRISM-CLI-OVERVIEW.md): what each command touches in AWS, its cost and its safety defaults
31
+ - [User Guide](https://github.com/aws-samples/sample-prism-d1-velocity/blob/main/USER_GUIDE.md): deploying PRISM, CI workflows and eval gates
32
+
33
+ ## Develop
34
+
35
+ From `cli/` in a clone:
36
+
37
+ ```bash
38
+ npm ci
39
+ npm run build # tsc -> dist/, plus the bundled assets
40
+ npm test # type-checks the tests, then the node:test suite
41
+ npm run docs # regenerate docs/CLI-REFERENCE.md after changing a command or flag
42
+ ```
43
+
44
+ Releases are published to npm by `.github/workflows/publish.yml` when a `v*` tag is pushed.
45
+
46
+ ## License
47
+
48
+ MIT
@@ -8,6 +8,8 @@
8
8
  # .kiro/steering/code-review.md in your repo (review rules)
9
9
  # - (Optional) PRISM_METRICS_ROLE_ARN for EventBridge metrics emission
10
10
  # - (Optional) AWS Continuum configured for security scanning
11
+ # - (Optional) Review model: repository variable PRISM_REVIEW_MODEL, else
12
+ # eval.model in .prism/config.json on the base branch, else `auto`
11
13
  #
12
14
  # Secret scanning (gitleaks) needs no setup and no secret. It installs itself
13
15
  # and always runs, so it is the only gate that still functions on a fork PR.
@@ -226,10 +228,50 @@ jobs:
226
228
  KIRO_API_KEY: ${{ secrets.KIRO_API_KEY }}
227
229
  BASE_SHA: ${{ github.event.pull_request.base.sha }}
228
230
  HEAD_SHA: ${{ github.sha }}
231
+ # Repository variable (Settings > Secrets and variables > Actions > Variables).
232
+ PRISM_REVIEW_MODEL: ${{ vars.PRISM_REVIEW_MODEL }}
229
233
  run: |
230
234
  # Trim whitespace/newlines from API key (GitHub Secrets can add trailing chars)
231
235
  export KIRO_API_KEY=$(echo -n "${KIRO_API_KEY}" | tr -d '[:space:]')
232
236
 
237
+ # --- review model: start (identical in every eval-gate workflow; cli/test/eval-gate-retry.test.ts checks) ---
238
+ # Order: the PRISM_REVIEW_MODEL variable, then eval.model in the BASE
239
+ # commit's .prism/config.json, then auto. Never the PR head's config:
240
+ # the PR author controls it, and a PR must not pick a weaker model to
241
+ # pass its own review. The value reaches a CLI argument, so it is
242
+ # validated here and never echoed when invalid.
243
+ REVIEW_MODEL="${PRISM_REVIEW_MODEL:-}"
244
+ REVIEW_MODEL_SOURCE="the PRISM_REVIEW_MODEL variable"
245
+ MODEL_ERROR=""
246
+ if [ -z "${REVIEW_MODEL}" ]; then
247
+ REVIEW_MODEL_SOURCE="the default"
248
+ if [ -n "${BASE_SHA}" ] && git cat-file -e "${BASE_SHA}:.prism/config.json" 2>/dev/null; then
249
+ if ! BASE_CFG_MODEL=$(git show "${BASE_SHA}:.prism/config.json" | jq -r '.eval.model? // empty | tostring'); then
250
+ MODEL_ERROR="the base branch's .prism/config.json could not be read as JSON"
251
+ elif [ -n "${BASE_CFG_MODEL}" ]; then
252
+ REVIEW_MODEL="${BASE_CFG_MODEL}"
253
+ REVIEW_MODEL_SOURCE="eval.model in the base branch's .prism/config.json"
254
+ fi
255
+ fi
256
+ REVIEW_MODEL="${REVIEW_MODEL:-auto}"
257
+ fi
258
+ if [ -z "${MODEL_ERROR}" ] && ! [[ "${REVIEW_MODEL}" =~ ^[A-Za-z0-9._:/-]{1,100}$ ]]; then
259
+ MODEL_ERROR="the model from ${REVIEW_MODEL_SOURCE} must match ^[A-Za-z0-9._:/-]{1,100}\$"
260
+ fi
261
+ # --- review model: end ---
262
+
263
+ if [ -n "${MODEL_ERROR}" ]; then
264
+ # FAIL CLOSED: a misconfigured model must not fall back silently.
265
+ echo "::error::Invalid review model: ${MODEL_ERROR}. Failing closed."
266
+ echo "result=ERROR" >> "$GITHUB_OUTPUT"
267
+ echo "model_error=true" >> "$GITHUB_OUTPUT"
268
+ echo "Eval gate review: invalid review model (${MODEL_ERROR}) -- failed closed." >> "$GITHUB_STEP_SUMMARY"
269
+ exit 0
270
+ fi
271
+ echo "Review model: ${REVIEW_MODEL} (from ${REVIEW_MODEL_SOURCE})"
272
+ echo "model=${REVIEW_MODEL}" >> "$GITHUB_OUTPUT"
273
+ echo "Eval gate review model: \`${REVIEW_MODEL}\` (from ${REVIEW_MODEL_SOURCE})." >> "$GITHUB_STEP_SUMMARY"
274
+
233
275
  # Fork PRs don't receive secrets -- report neutral, never silently pass
234
276
  if [ "${HAS_KIRO_KEY}" != "true" ]; then
235
277
  echo "::warning::KIRO_API_KEY unavailable (fork PR or missing secret) -- review skipped"
@@ -273,7 +315,7 @@ jobs:
273
315
  echo "::endgroup::"
274
316
 
275
317
  echo "::group::kiro-cli smoke test"
276
- echo "Say hello in JSON" | kiro-cli chat --no-interactive --model auto || echo "SMOKE TEST FAILED"
318
+ echo "Say hello in JSON" | kiro-cli chat --no-interactive --model "${REVIEW_MODEL}" || echo "SMOKE TEST FAILED"
277
319
  echo "::endgroup::"
278
320
 
279
321
  echo "Reviewing ${FILES}"
@@ -282,49 +324,85 @@ jobs:
282
324
  # on long content. Use a minimal CLI arg prompt and let kiro read files itself.
283
325
  git diff "${BASE_SHA}..${HEAD_SHA}" -- ${FILES} > /tmp/pr-diff.patch
284
326
 
285
- REVIEW_OUTPUT=$(kiro-cli chat --no-interactive \
286
- --trust-all-tools \
287
- --model auto \
288
- "Review /tmp/pr-diff.patch for code quality. Read it, then output ONLY JSON: {\"findings\":[{\"file\":\"path\",\"line\":1,\"severity\":\"high|medium|low\",\"category\":\"str\",\"message\":\"str\"}],\"score\":0.0,\"summary\":\"str\"}") || true
289
-
290
- # Strip ANSI escapes, then extract the LAST JSON object that parses and
291
- # has a numeric score and a findings array. kiro-cli writes reasoning
292
- # notes first, and they quote code with braces, so a greedy
293
- # first-brace-to-last-brace match is not the verdict.
294
- CLEANED=$(echo "${REVIEW_OUTPUT}" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' | tr -d '\r')
295
- REVIEW_JSON=$(printf '%s' "${CLEANED}" | python3 -c 'if True:
296
- import json, math, sys
297
- text = sys.stdin.read()
298
- dec = json.JSONDecoder(strict=False)
299
- pos = text.rfind("{")
300
- while pos != -1:
301
- try:
302
- obj = dec.raw_decode(text, pos)[0]
303
- except ValueError:
304
- obj = None
305
- score = obj.get("score") if isinstance(obj, dict) else None
306
- if (isinstance(score, (int, float)) and not isinstance(score, bool)
307
- and math.isfinite(score) and isinstance(obj.get("findings"), list)):
308
- print(json.dumps(obj))
309
- sys.exit(0)
310
- pos = text.rfind("{", 0, pos)
311
- sys.exit(1)
312
- ' || true)
313
-
314
- if [ -z "${REVIEW_JSON}" ] || ! echo "${REVIEW_JSON}" | jq -e '.score' > /dev/null 2>&1; then
315
- # FAIL CLOSED: a gate that can't evaluate must not pass
316
- echo "::group::Debug: raw kiro-cli output (first 2000 chars)"
327
+ # One retry, only when the reply cannot be parsed. Attempt 2 re-runs the
328
+ # same review (same model, same diff file) with one sentence added that
329
+ # asks for a single JSON object. It does not hand the bad reply back
330
+ # for "repair": a broken reply is often truncated, and repairing it
331
+ # would make the model invent the missing findings. Each attempt reads
332
+ # the diff, so any accepted verdict comes from a real review. Nothing
333
+ # is repaired by regex; two unparseable replies still fail closed.
334
+ REVIEW_PROMPT="Review /tmp/pr-diff.patch for code quality. Read it, then output ONLY JSON: {\"findings\":[{\"file\":\"path\",\"line\":1,\"severity\":\"high|medium|low\",\"category\":\"str\",\"message\":\"str\"}],\"score\":0.0,\"summary\":\"str\"}"
335
+ RETRY_NOTE="Your previous reply was not valid JSON. Return exactly one JSON object matching that schema and nothing else: no prose, no code fences."
336
+ # Per-attempt cap. The retry gets the same budget as the first attempt.
337
+ REVIEW_TIMEOUT_SECS=600
338
+ REVIEW_JSON=""
339
+ REVIEW_ATTEMPT=""
340
+ for ATTEMPT in 1 2; do
341
+ PROMPT="${REVIEW_PROMPT}"
342
+ if [ "${ATTEMPT}" = "2" ]; then PROMPT="${REVIEW_PROMPT} ${RETRY_NOTE}"; fi
343
+ REVIEW_OUTPUT=$(timeout "${REVIEW_TIMEOUT_SECS}" kiro-cli chat --no-interactive \
344
+ --trust-all-tools \
345
+ --model "${REVIEW_MODEL}" \
346
+ "${PROMPT}") || true
347
+
348
+ # Strip ANSI escapes, then extract the LAST JSON object that parses and
349
+ # has a numeric score and a findings array. kiro-cli writes reasoning
350
+ # notes first, and they quote code with braces, so a greedy
351
+ # first-brace-to-last-brace match is not the verdict. Same rule as
352
+ # extractReviewJson in cli/src/codewatch/reviewers/types.ts; the copy
353
+ # in each eval-gate workflow is kept identical by cli/test/eval-gate-extract.test.ts.
354
+ CLEANED=$(echo "${REVIEW_OUTPUT}" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' | tr -d '\r')
355
+ REVIEW_JSON=$(printf '%s' "${CLEANED}" | python3 -c 'if True:
356
+ import json, math, sys
357
+ text = sys.stdin.read()
358
+ dec = json.JSONDecoder(strict=False)
359
+ pos = text.rfind("{")
360
+ while pos != -1:
361
+ try:
362
+ obj = dec.raw_decode(text, pos)[0]
363
+ except ValueError:
364
+ obj = None
365
+ score = obj.get("score") if isinstance(obj, dict) else None
366
+ if (isinstance(score, (int, float)) and not isinstance(score, bool)
367
+ and math.isfinite(score) and isinstance(obj.get("findings"), list)):
368
+ print(json.dumps(obj))
369
+ sys.exit(0)
370
+ pos = text.rfind("{", 0, pos)
371
+ sys.exit(1)
372
+ ' || true)
373
+
374
+ if [ -n "${REVIEW_JSON}" ] && echo "${REVIEW_JSON}" | jq -e '.score' > /dev/null 2>&1; then
375
+ REVIEW_ATTEMPT="${ATTEMPT}"
376
+ break
377
+ fi
378
+ REVIEW_JSON=""
379
+
380
+ # The reply is untrusted model output. Workflow commands are paused
381
+ # while it is printed, so a "::" line in it is shown, not executed.
382
+ STOP_TOKEN=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')
383
+ echo "::group::Debug: attempt ${ATTEMPT} raw kiro-cli output (first 2000 chars)"
384
+ echo "::stop-commands::${STOP_TOKEN}"
317
385
  echo "${REVIEW_OUTPUT:-<empty>}" | head -c 2000
318
386
  echo ""
319
- echo "::endgroup::"
320
- echo "::group::Debug: cleaned output"
387
+ echo "--- cleaned ---"
321
388
  echo "${CLEANED:-<empty>}" | head -c 2000
322
389
  echo ""
390
+ echo "::${STOP_TOKEN}::"
323
391
  echo "::endgroup::"
324
- echo "::error::Could not parse kiro-cli review output -- failing closed"
392
+ if [ "${ATTEMPT}" = "1" ]; then
393
+ echo "::warning::Attempt 1: could not parse kiro-cli review output -- retrying the review once"
394
+ fi
395
+ done
396
+
397
+ if [ -z "${REVIEW_JSON}" ]; then
398
+ # FAIL CLOSED: a gate that can't evaluate must not pass
399
+ echo "::error::Could not parse kiro-cli review output on either attempt (both unparseable) -- failing closed"
325
400
  echo "result=ERROR" >> "$GITHUB_OUTPUT"
401
+ echo "Eval gate review: both attempts returned unparseable output -- failed closed." >> "$GITHUB_STEP_SUMMARY"
326
402
  exit 0
327
403
  fi
404
+ echo "Review verdict parsed on attempt ${REVIEW_ATTEMPT} of 2"
405
+ echo "attempt=${REVIEW_ATTEMPT}" >> "$GITHUB_OUTPUT"
328
406
 
329
407
  SCORE=$(echo "${REVIEW_JSON}" | jq -r '.score')
330
408
  FINDINGS=$(echo "${REVIEW_JSON}" | jq '.findings | length')
@@ -343,10 +421,13 @@ jobs:
343
421
  # Determine pass/fail (awk instead of bc -- no extra package needed)
344
422
  PASS=$(awk -v s="${SCORE}" 'BEGIN { print (s >= 0.82) ? "true" : "false" }')
345
423
  if [ "${HIGH}" -gt 0 ] || [ "${PASS}" != "true" ]; then
346
- echo "result=FAIL" >> "$GITHUB_OUTPUT"
424
+ RESULT=FAIL
347
425
  else
348
- echo "result=PASS" >> "$GITHUB_OUTPUT"
426
+ RESULT=PASS
349
427
  fi
428
+ echo "result=${RESULT}" >> "$GITHUB_OUTPUT"
429
+ # Only numbers and fixed words here; no model text reaches the summary.
430
+ echo "Eval gate review: ${RESULT} (score ${SCORE}, ${HIGH} high), verdict parsed on attempt ${REVIEW_ATTEMPT} of 2." >> "$GITHUB_STEP_SUMMARY"
350
431
 
351
432
  - name: Post PR comment
352
433
  if: always()
@@ -358,6 +439,8 @@ jobs:
358
439
  REVIEW_MEDIUM: ${{ steps.review.outputs.medium }}
359
440
  REVIEW_RESULT: ${{ steps.review.outputs.result }}
360
441
  REVIEW_SUMMARY: ${{ steps.review.outputs.summary }}
442
+ REVIEW_MODEL: ${{ steps.review.outputs.model }}
443
+ REVIEW_MODEL_ERROR: ${{ steps.review.outputs.model_error }}
361
444
  GITLEAKS_LEAKS: ${{ steps.gitleaks.outputs.leaks }}
362
445
  GITLEAKS_ERROR: ${{ steps.gitleaks.outputs.error }}
363
446
  GITLEAKS_INSTALL_ERROR: ${{ steps.gitleaks-install.outputs.error }}
@@ -371,8 +454,10 @@ jobs:
371
454
  const result = process.env.REVIEW_RESULT || 'ERROR';
372
455
  if (result === 'SKIPPED') {
373
456
  body = '⚠️ **PRISM Eval Gate** -- Review skipped (no source files changed or no API key on fork PRs).';
457
+ } else if (result === 'ERROR' && process.env.REVIEW_MODEL_ERROR === 'true') {
458
+ body = '❌ **PRISM Eval Gate** -- The review model is invalid (see the job summary). Failing closed; fix `PRISM_REVIEW_MODEL` or `eval.model` in `.prism/config.json`.';
374
459
  } else if (result === 'ERROR') {
375
- body = '❌ **PRISM Eval Gate** -- Review output could not be parsed. Failing closed; re-run the workflow or review manually.';
460
+ body = '❌ **PRISM Eval Gate** -- Review output could not be parsed on either attempt (the review is retried once). Failing closed; re-run the workflow or review manually.';
376
461
  } else {
377
462
  const score = process.env.REVIEW_SCORE || '?';
378
463
  const findings = process.env.REVIEW_FINDINGS || '0';
@@ -401,7 +486,8 @@ jobs:
401
486
  body += `| File | Line | Severity | Category | Issue |\n|---|---|---|---|---|\n${table}\n\n`;
402
487
  }
403
488
 
404
- body += `> Powered by kiro-cli headless - Model: claude-haiku-4.5 - Threshold: 0.82`;
489
+ // The model the review step resolved and validated, not a fixed label.
490
+ body += `> Powered by kiro-cli headless - Model: ${process.env.REVIEW_MODEL || '?'} - Threshold: 0.82`;
405
491
  }
406
492
 
407
493
  // Secret scanning is reported for every result path, including the
@@ -462,6 +548,7 @@ jobs:
462
548
  REVIEW_RESULT: ${{ steps.review.outputs.result }}
463
549
  REVIEW_FINDINGS: ${{ steps.review.outputs.findings }}
464
550
  REVIEW_HIGH: ${{ steps.review.outputs.high }}
551
+ REVIEW_MODEL: ${{ steps.review.outputs.model }}
465
552
  PR_NUMBER: ${{ github.event.pull_request.number }}
466
553
  run: |
467
554
  # Only emit when we have a real score (not SKIPPED/ERROR)
@@ -479,9 +566,10 @@ jobs:
479
566
  --arg findings "${REVIEW_FINDINGS:-0}" \
480
567
  --arg high "${REVIEW_HIGH:-0}" \
481
568
  --arg pr_number "${PR_NUMBER}" \
569
+ --arg model "${REVIEW_MODEL}" \
482
570
  '{team_id:$team_id,repo:$repo,timestamp:$timestamp,prism_level:2,
483
571
  metric:{name:"eval_score",value:($score|tonumber),unit:"score"},
484
- ai_context:{tool:"kiro-cli",model:"claude-haiku-4.5",origin:"ai-generated"},
572
+ ai_context:{tool:"kiro-cli",model:$model,origin:"ai-generated"},
485
573
  eval:{score:($score|tonumber),result:$result,rubric:"kiro-headless",
486
574
  findings:($findings|tonumber),high_findings:($high|tonumber),
487
575
  pr_number:($pr_number|tonumber),method:"kiro-headless"}}')
@@ -823,6 +911,7 @@ jobs:
823
911
  REVIEW_HIGH: ${{ steps.review.outputs.high }}
824
912
  REVIEW_SCORE: ${{ steps.review.outputs.score }}
825
913
  REVIEW_SUMMARY: ${{ steps.review.outputs.summary }}
914
+ REVIEW_MODEL_ERROR: ${{ steps.review.outputs.model_error }}
826
915
  SEC_CRITICAL: ${{ steps.security-gate.outputs.critical }}
827
916
  SEC_HIGH: ${{ steps.security-gate.outputs.high }}
828
917
  SEC_SCAN_ERROR: ${{ steps.security-gate.outputs.scan_error }}
@@ -841,7 +930,9 @@ jobs:
841
930
  echo " Rotate the credential first -- deleting it from the branch does not un-expose it."
842
931
  echo " False positive? Add an allowlist entry to .gitleaks.toml."
843
932
  fi
844
- if [ "${REVIEW_RESULT}" = "ERROR" ]; then
933
+ if [ "${REVIEW_MODEL_ERROR}" = "true" ]; then
934
+ echo "❌ Eval gate failed closed: the review model is invalid (see the job summary)"
935
+ elif [ "${REVIEW_RESULT}" = "ERROR" ]; then
845
936
  echo "❌ Eval gate failed closed: review output could not be parsed"
846
937
  elif [ "${REVIEW_RESULT}" = "FAIL" ]; then
847
938
  echo "❌ Kiro found ${REVIEW_HIGH:-?} high-severity issues (score: ${REVIEW_SCORE:-?})"
@@ -9,6 +9,8 @@
9
9
  # - CI/CD variable KIRO_API_KEY (masked). Generate at https://app.kiro.dev
10
10
  # - PRISM_AWS_REGION variable in the variables: block below controls the region
11
11
  # - (Optional) CI/CD variable PRISM_METRICS_ROLE_ARN for EventBridge metrics
12
+ # - (Optional) Review model: CI/CD variable PRISM_REVIEW_MODEL, else
13
+ # eval.model in .prism/config.json on the target branch, else `auto`
12
14
  #
13
15
  # The Bedrock rubric gate that used to live in this file is retired. It scored
14
16
  # changed files against .prism/eval-harness/rubrics/*.json via run-eval.sh; the
@@ -180,7 +182,39 @@ prism-eval-gate:
180
182
 
181
183
  echo ""
182
184
  echo "── Code quality review (kiro-cli) ──"
183
- if [ -z "${KIRO_API_KEY:-}" ]; then
185
+
186
+ # --- review model: start (identical in every eval-gate workflow; cli/test/eval-gate-retry.test.ts checks) ---
187
+ # Order: the PRISM_REVIEW_MODEL variable, then eval.model in the BASE
188
+ # commit's .prism/config.json, then auto. Never the PR head's config:
189
+ # the PR author controls it, and a PR must not pick a weaker model to
190
+ # pass its own review. The value reaches a CLI argument, so it is
191
+ # validated here and never echoed when invalid.
192
+ REVIEW_MODEL="${PRISM_REVIEW_MODEL:-}"
193
+ REVIEW_MODEL_SOURCE="the PRISM_REVIEW_MODEL variable"
194
+ MODEL_ERROR=""
195
+ if [ -z "${REVIEW_MODEL}" ]; then
196
+ REVIEW_MODEL_SOURCE="the default"
197
+ if [ -n "${BASE_SHA}" ] && git cat-file -e "${BASE_SHA}:.prism/config.json" 2>/dev/null; then
198
+ if ! BASE_CFG_MODEL=$(git show "${BASE_SHA}:.prism/config.json" | jq -r '.eval.model? // empty | tostring'); then
199
+ MODEL_ERROR="the base branch's .prism/config.json could not be read as JSON"
200
+ elif [ -n "${BASE_CFG_MODEL}" ]; then
201
+ REVIEW_MODEL="${BASE_CFG_MODEL}"
202
+ REVIEW_MODEL_SOURCE="eval.model in the base branch's .prism/config.json"
203
+ fi
204
+ fi
205
+ REVIEW_MODEL="${REVIEW_MODEL:-auto}"
206
+ fi
207
+ if [ -z "${MODEL_ERROR}" ] && ! [[ "${REVIEW_MODEL}" =~ ^[A-Za-z0-9._:/-]{1,100}$ ]]; then
208
+ MODEL_ERROR="the model from ${REVIEW_MODEL_SOURCE} must match ^[A-Za-z0-9._:/-]{1,100}\$"
209
+ fi
210
+ # --- review model: end ---
211
+
212
+ if [ -n "${MODEL_ERROR}" ]; then
213
+ # FAIL CLOSED: a misconfigured model must not fall back silently.
214
+ echo "❌ Invalid review model: ${MODEL_ERROR}. Failing closed."
215
+ REVIEW_MODEL=""
216
+ REVIEW_RESULT="ERROR"
217
+ elif [ -z "${KIRO_API_KEY:-}" ]; then
184
218
  # Fork MRs receive no protected variables. Reported, never silently passed.
185
219
  echo "⚠️ KIRO_API_KEY unavailable (fork MR or unset variable) — review skipped."
186
220
  elif [ -z "${BASE_SHA}" ] || [ -z "${HEAD_SHA}" ]; then
@@ -213,45 +247,72 @@ prism-eval-gate:
213
247
  REVIEW_RESULT="ERROR"
214
248
  else
215
249
  kiro-cli --version || true
250
+ echo "Review model: ${REVIEW_MODEL} (from ${REVIEW_MODEL_SOURCE})"
216
251
  # Short prompt on purpose: kiro-cli has a bug putting long stdin
217
252
  # into HTTP headers, so let it read the patch off disk itself.
218
253
  git diff "${BASE_SHA}..${HEAD_SHA}" -- ${FILES} > /tmp/mr-diff.patch
219
254
 
220
- set +e
221
- REVIEW_OUTPUT=$(kiro-cli chat --no-interactive --trust-all-tools --model auto \
222
- "Review /tmp/mr-diff.patch for code quality. Read it, then output ONLY JSON: {\"findings\":[{\"file\":\"path\",\"line\":1,\"severity\":\"high|medium|low\",\"category\":\"str\",\"message\":\"str\"}],\"score\":0.0,\"summary\":\"str\"}")
223
- set -e
255
+ # One retry, only when the reply cannot be parsed. Attempt 2 re-runs
256
+ # the same review (same model, same diff file) with one sentence
257
+ # added that asks for a single JSON object. It does not hand the bad
258
+ # reply back for "repair": a broken reply is often truncated, and
259
+ # repairing it would make the model invent the missing findings.
260
+ # Nothing is repaired by regex; two unparseable replies fail closed.
261
+ REVIEW_PROMPT="Review /tmp/mr-diff.patch for code quality. Read it, then output ONLY JSON: {\"findings\":[{\"file\":\"path\",\"line\":1,\"severity\":\"high|medium|low\",\"category\":\"str\",\"message\":\"str\"}],\"score\":0.0,\"summary\":\"str\"}"
262
+ RETRY_NOTE="Your previous reply was not valid JSON. Return exactly one JSON object matching that schema and nothing else: no prose, no code fences."
263
+ # Per-attempt cap. The retry gets the same budget as the first attempt.
264
+ REVIEW_TIMEOUT_SECS=600
265
+ REVIEW_JSON=""
266
+ REVIEW_ATTEMPT=""
267
+ for ATTEMPT in 1 2; do
268
+ PROMPT="${REVIEW_PROMPT}"
269
+ if [ "${ATTEMPT}" = "2" ]; then PROMPT="${REVIEW_PROMPT} ${RETRY_NOTE}"; fi
270
+ set +e
271
+ REVIEW_OUTPUT=$(timeout "${REVIEW_TIMEOUT_SECS}" kiro-cli chat --no-interactive --trust-all-tools --model "${REVIEW_MODEL}" "${PROMPT}")
272
+ set -e
224
273
 
225
- # Extract the LAST JSON object that parses and has a numeric score
226
- # and a findings array. kiro-cli writes reasoning notes first, and
227
- # they quote code with braces, so a greedy first-brace-to-last-brace
228
- # match is not the verdict.
229
- CLEANED=$(printf '%s' "${REVIEW_OUTPUT}" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' | tr -d '\r')
230
- REVIEW_JSON=$(printf '%s' "${CLEANED}" | python3 -c 'if True:
231
- import json, math, sys
232
- text = sys.stdin.read()
233
- dec = json.JSONDecoder(strict=False)
234
- pos = text.rfind("{")
235
- while pos != -1:
236
- try:
237
- obj = dec.raw_decode(text, pos)[0]
238
- except ValueError:
239
- obj = None
240
- score = obj.get("score") if isinstance(obj, dict) else None
241
- if (isinstance(score, (int, float)) and not isinstance(score, bool)
242
- and math.isfinite(score) and isinstance(obj.get("findings"), list)):
243
- print(json.dumps(obj))
244
- sys.exit(0)
245
- pos = text.rfind("{", 0, pos)
246
- sys.exit(1)
247
- ' || true)
274
+ # Extract the LAST JSON object that parses and has a numeric score
275
+ # and a findings array. kiro-cli writes reasoning notes first, and
276
+ # they quote code with braces, so a greedy first-brace-to-last-brace
277
+ # match is not the verdict.
278
+ CLEANED=$(printf '%s' "${REVIEW_OUTPUT}" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g' | tr -d '\r')
279
+ REVIEW_JSON=$(printf '%s' "${CLEANED}" | python3 -c 'if True:
280
+ import json, math, sys
281
+ text = sys.stdin.read()
282
+ dec = json.JSONDecoder(strict=False)
283
+ pos = text.rfind("{")
284
+ while pos != -1:
285
+ try:
286
+ obj = dec.raw_decode(text, pos)[0]
287
+ except ValueError:
288
+ obj = None
289
+ score = obj.get("score") if isinstance(obj, dict) else None
290
+ if (isinstance(score, (int, float)) and not isinstance(score, bool)
291
+ and math.isfinite(score) and isinstance(obj.get("findings"), list)):
292
+ print(json.dumps(obj))
293
+ sys.exit(0)
294
+ pos = text.rfind("{", 0, pos)
295
+ sys.exit(1)
296
+ ' || true)
248
297
 
249
- if [ -z "${REVIEW_JSON}" ] || ! printf '%s' "${REVIEW_JSON}" | jq -e '.score' >/dev/null 2>&1; then
250
- echo "❌ Could not parse kiro-cli review output — failing closed."
298
+ if [ -n "${REVIEW_JSON}" ] && printf '%s' "${REVIEW_JSON}" | jq -e '.score' >/dev/null 2>&1; then
299
+ REVIEW_ATTEMPT="${ATTEMPT}"
300
+ break
301
+ fi
302
+ REVIEW_JSON=""
303
+ echo "⚠ Attempt ${ATTEMPT}: could not parse kiro-cli review output. Raw output (first 2000 chars):"
251
304
  printf '%s' "${REVIEW_OUTPUT:-<empty>}" | head -c 2000
252
305
  echo ""
306
+ if [ "${ATTEMPT}" = "1" ]; then
307
+ echo "Retrying the review once."
308
+ fi
309
+ done
310
+
311
+ if [ -z "${REVIEW_JSON}" ]; then
312
+ echo "❌ Could not parse kiro-cli review output on either attempt (both unparseable) — failing closed."
253
313
  REVIEW_RESULT="ERROR"
254
314
  else
315
+ echo "Review verdict parsed on attempt ${REVIEW_ATTEMPT} of 2."
255
316
  printf '%s' "${REVIEW_JSON}" > /tmp/kiro-review.json
256
317
  REVIEW_SCORE=$(jq -r '.score' /tmp/kiro-review.json)
257
318
  REVIEW_HIGH=$(jq '[.findings[] | select(.severity=="high")] | length' /tmp/kiro-review.json)
@@ -265,7 +326,7 @@ prism-eval-gate:
265
326
  REVIEW_RESULT="PASS"
266
327
  fi
267
328
 
268
- echo "Score: ${REVIEW_SCORE} | Findings: ${REVIEW_TOTAL} (${REVIEW_HIGH} high) | ${REVIEW_RESULT}"
329
+ echo "Score: ${REVIEW_SCORE} | Findings: ${REVIEW_TOTAL} (${REVIEW_HIGH} high) | ${REVIEW_RESULT} | Model: ${REVIEW_MODEL}"
269
330
  echo "Summary: ${REVIEW_SUMMARY}"
270
331
  jq -r '.findings[] | " [\(.severity)] \(.file):\(.line) — \(.message)"' /tmp/kiro-review.json || true
271
332
  fi
@@ -308,10 +369,10 @@ prism-eval-gate:
308
369
  DETAIL=$(jq -n -c \
309
370
  --arg team_id "${TEAM_ID}" --arg repo "${PRISM_REPO}" --arg timestamp "${TIMESTAMP}" \
310
371
  --arg score "${REVIEW_SCORE}" --arg result "${REVIEW_RESULT}" \
311
- --arg mr_iid "${CI_MERGE_REQUEST_IID:-0}" \
372
+ --arg mr_iid "${CI_MERGE_REQUEST_IID:-0}" --arg model "${REVIEW_MODEL}" \
312
373
  '{team_id:$team_id,repo:$repo,timestamp:$timestamp,prism_level:2,
313
374
  metric:{name:"eval_score",value:($score|tonumber),unit:"score"},
314
- ai_context:{tool:"kiro-cli",model:"claude-haiku-4.5",origin:"ai-generated"},
375
+ ai_context:{tool:"kiro-cli",model:$model,origin:"ai-generated"},
315
376
  eval:{score:($score|tonumber),result:$result,rubric:"kiro-headless",
316
377
  mr_iid:($mr_iid|tonumber),method:"kiro-headless"}}')
317
378
  aws events put-events --region "${AWS_DEFAULT_REGION}" --entries \
@@ -398,10 +459,14 @@ prism-eval-gate:
398
459
  echo " deleting it from the branch does not un-expose what was pushed."
399
460
  echo " False positive? Add an allowlist entry to .gitleaks.toml."
400
461
  fi
401
- [ "${REVIEW_RESULT}" = "ERROR" ] && echo " Review failed closed: output could not be parsed."
462
+ if [ -n "${MODEL_ERROR}" ]; then
463
+ echo " Review failed closed: invalid review model (${MODEL_ERROR})."
464
+ elif [ "${REVIEW_RESULT}" = "ERROR" ]; then
465
+ echo " Review failed closed: output could not be parsed."
466
+ fi
402
467
  [ "${REVIEW_RESULT}" = "FAIL" ] && echo " Review score ${REVIEW_SCORE} below threshold, or high-severity findings."
403
468
  exit 1
404
469
  fi
405
470
 
406
- echo "✅ PRISM Eval Gate passed (secret scan: clean, review: ${REVIEW_RESULT})"
471
+ echo "✅ PRISM Eval Gate passed (secret scan: clean, review: ${REVIEW_RESULT}, model: ${REVIEW_MODEL})"
407
472
  exit 0
@@ -1,4 +1,4 @@
1
- # PRISM app for Kiro Crew (v0.2)
1
+ # PRISM app for Kiro Crew
2
2
 
3
3
  A [Kiro Crew](https://github.com/kirodotdev/KiroCrew) app that brings your local PRISM data into
4
4
  Kiro Crew. It adds:
@@ -50,14 +50,13 @@ With prism-cli 1.9.0 or later, which bundles this app:
50
50
  npm i -g @prism-d1/cli && prism-cli enable kirocrew
51
51
  ```
52
52
 
53
- `prism-cli enable kirocrew` prints its plan (install, update or already current, with the
54
- versions) and asks before it changes anything. It copies the bundled app to
55
- `~/.prism/kirocrew-app`, then runs `kirocrew app install`, or `kirocrew app update prism --source`
56
- when the app is already installed (also from another folder, such as a clone), and
57
- `kirocrew app enable prism` on a first install. It never installs Kiro Crew. `--dry-run` prints
58
- the plan only, `--status` shows the installed and bundled versions, the source folder, the API
59
- grants and the digest cron, and `--uninstall` removes the app. After a prism-cli upgrade,
60
- `prism-cli version` says when the bundled app is newer than the installed one.
53
+ What the command does, its `--status`, `--dry-run` and `--uninstall` options and the two steps
54
+ that stay with you (trust the app, approve its API routes) are in the User Guide's
55
+ [Kiro Crew app](../USER_GUIDE.md#kiro-crew-app) section; every flag is in the
56
+ [command reference](../docs/CLI-REFERENCE.md#prism-cli-enable-kirocrew). It updates an app that
57
+ was installed from another folder, such as a clone, too. It never installs Kiro Crew.
58
+
59
+ ### Install from a folder
61
60
 
62
61
  From a local folder instead (a clone, for development):
63
62
 
@@ -129,6 +128,10 @@ is kept across app updates unless a later version of the app changes the schedul
129
128
 
130
129
  ## The page
131
130
 
131
+ ![PRISM page in Kiro Crew with demo data: spend cards, top models and projects, four repos with codewatch, one expanded with its findings](../assets/images/prism-cli/kirocrew-app-page-light.png)
132
+
133
+ *The PRISM page, rendered with made-up demo data.*
134
+
132
135
  The **PRISM** sidebar page shows the same data as the editor extension's machine mode:
133
136
 
134
137
  - spend today and this month (calls, sessions), sync mode, prism-cli version, and when the data
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "prism",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "displayName": "PRISM",
5
5
  "description": "Codewatch findings, AI spend and setup status from your local prism-cli for Kiro Crew agents, and a daily digest that speaks only when something changed.",
6
6
  "author": "aws-samples",
7
7
  "license": "MIT",
8
8
  "minKiroCrewVersion": "0.12.1",
9
9
  "iconPath": "assets/icon.png",
10
+ "screenshots": ["assets/screenshots/page-light.png", "assets/screenshots/report-light.png"],
11
+ "screenshotsDark": ["assets/screenshots/page-dark.png", "assets/screenshots/report-dark.png"],
10
12
  "tags": ["code-review", "developer-tools", "code-quality"],
11
13
  "highlights": [
12
14
  "PRISM page: spend, setup checklist, repos with codewatch newest commit first, their findings and a sandboxed scan report, all read-only",
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,19 @@
1
+ // The commander CLI; bin/prism-cli.ts loads it for every command except the status line fast path.
2
+ import { program } from 'commander';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { registerCommands } from '../src/index.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ // Works from both cli/bin/ (source via tsx) and dist/bin/ (published)
9
+ const pkgPath = existsSync(resolve(__dirname, '..', 'package.json'))
10
+ ? resolve(__dirname, '..', 'package.json')
11
+ : resolve(__dirname, '..', '..', 'package.json');
12
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
13
+ program
14
+ .name('prism-cli')
15
+ .description('PRISM D1 Velocity CLI tool')
16
+ .version(pkg.version);
17
+ await registerCommands(program);
18
+ program.parse();
19
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,sEAAsE;AACtE,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;IAClE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,cAAc,CAAC;IAC1C,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAC;AACnD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAEtD,OAAO;KACJ,IAAI,CAAC,WAAW,CAAC;KACjB,WAAW,CAAC,4BAA4B,CAAC;KACzC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AAExB,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;AAEhC,OAAO,CAAC,KAAK,EAAE,CAAC"}