@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.
- package/README.md +48 -0
- package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +133 -42
- package/dist/assets/gitlab-workflows/prism-eval-gate.yml +100 -35
- package/dist/assets/kirocrew-app/README.md +12 -9
- package/dist/assets/kirocrew-app/app.json +3 -1
- package/dist/assets/kirocrew-app/assets/screenshots/page-dark.png +0 -0
- package/dist/assets/kirocrew-app/assets/screenshots/page-light.png +0 -0
- package/dist/assets/kirocrew-app/assets/screenshots/report-dark.png +0 -0
- package/dist/assets/kirocrew-app/assets/screenshots/report-light.png +0 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +19 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/prism-cli.js +12 -17
- package/dist/bin/prism-cli.js.map +1 -1
- package/dist/src/commands/_top/dashboard.d.ts +2 -0
- package/dist/src/commands/_top/dashboard.d.ts.map +1 -1
- package/dist/src/commands/_top/dashboard.js +43 -15
- package/dist/src/commands/_top/dashboard.js.map +1 -1
- package/dist/src/commands/_top/mcp.d.ts.map +1 -1
- package/dist/src/commands/_top/mcp.js +3 -15
- package/dist/src/commands/_top/mcp.js.map +1 -1
- package/dist/src/commands/_top/statusline.d.ts +13 -0
- package/dist/src/commands/_top/statusline.d.ts.map +1 -0
- package/dist/src/commands/_top/statusline.js +12 -0
- package/dist/src/commands/_top/statusline.js.map +1 -0
- package/dist/src/commands/_top/version.d.ts +2 -0
- package/dist/src/commands/_top/version.d.ts.map +1 -1
- package/dist/src/commands/_top/version.js +4 -0
- package/dist/src/commands/_top/version.js.map +1 -1
- package/dist/src/commands/assessment/deploy-cdk.js +1 -1
- package/dist/src/commands/assessment/deploy-cdk.js.map +1 -1
- package/dist/src/commands/enable/claude-code.d.ts +81 -0
- package/dist/src/commands/enable/claude-code.d.ts.map +1 -0
- package/dist/src/commands/enable/claude-code.js +381 -0
- package/dist/src/commands/enable/claude-code.js.map +1 -0
- package/dist/src/dashboard/page.d.ts +25 -0
- package/dist/src/dashboard/page.d.ts.map +1 -1
- package/dist/src/dashboard/page.js +66 -7
- package/dist/src/dashboard/page.js.map +1 -1
- package/dist/src/dashboard/setup-page.d.ts +1 -1
- package/dist/src/dashboard/setup-page.d.ts.map +1 -1
- package/dist/src/dashboard/setup-page.js +15 -2
- package/dist/src/dashboard/setup-page.js.map +1 -1
- package/dist/src/dashboard/snapshot.d.ts +31 -0
- package/dist/src/dashboard/snapshot.d.ts.map +1 -0
- package/dist/src/dashboard/snapshot.js +65 -0
- package/dist/src/dashboard/snapshot.js.map +1 -0
- package/dist/src/mcp/dashboard-process.d.ts +83 -0
- package/dist/src/mcp/dashboard-process.d.ts.map +1 -0
- package/dist/src/mcp/dashboard-process.js +184 -0
- package/dist/src/mcp/dashboard-process.js.map +1 -0
- package/dist/src/mcp/server.d.ts.map +1 -1
- package/dist/src/mcp/server.js +8 -3
- package/dist/src/mcp/server.js.map +1 -1
- package/dist/src/mcp/tools.d.ts +7 -1
- package/dist/src/mcp/tools.d.ts.map +1 -1
- package/dist/src/mcp/tools.js +100 -2
- package/dist/src/mcp/tools.js.map +1 -1
- package/dist/src/statusline/statusline.d.ts +135 -0
- package/dist/src/statusline/statusline.d.ts.map +1 -0
- package/dist/src/statusline/statusline.js +398 -0
- package/dist/src/statusline/statusline.js.map +1 -0
- 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
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
#
|
|
291
|
-
#
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
424
|
+
RESULT=FAIL
|
|
347
425
|
else
|
|
348
|
-
|
|
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
|
-
|
|
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
|
|
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 [ "${
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
[ "${
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+

|
|
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.
|
|
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",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":""}
|
package/dist/bin/cli.js
ADDED
|
@@ -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"}
|