@clearlydev/nx-archweave 0.2.2 → 0.3.1
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 +9 -6
- package/dist/bin/review-verdict.js +60 -17
- package/dist/generators/add-agent-rules/generator.js +76 -931
- package/dist/generators/add-backend/generator.js +6 -931
- package/dist/generators/add-fsd-lite/generator.js +4 -929
- package/dist/generators/add-review/generator.js +138 -945
- package/dist/generators/doctor/generator.js +255 -37
- package/dist/generators/init/generator.js +196 -952
- package/package.json +4 -6
- package/src/generators/init/schema.json +6 -0
|
@@ -288,6 +288,30 @@ function buildOverEngineeringPrompt(locale) {
|
|
|
288
288
|
return [...body, "", renderFindingSchemaInstruction(locale, OVER_ENGINEERING_PRIORITIES), "", renderDedupInstruction(locale)].join("\n");
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
// ../review-policy/dist/verdict-gate/verdict-comment.js
|
|
292
|
+
var VERDICT_MARKER = "<!-- archweave-verdict -->";
|
|
293
|
+
function renderCommitLine(locale, shortSha) {
|
|
294
|
+
return locale === "en" ? `<sub>Commit \`${shortSha}\` \xB7 this comment is updated on every review round.</sub>` : `<sub>\uCEE4\uBC0B \`${shortSha}\` \xB7 \uC774 \uCF54\uBA58\uD2B8\uB294 \uB9AC\uBDF0 \uB77C\uC6B4\uB4DC\uB9C8\uB2E4 \uAC31\uC2E0\uB429\uB2C8\uB2E4.</sub>`;
|
|
295
|
+
}
|
|
296
|
+
var NOT_RUN_REASON = {
|
|
297
|
+
en: {
|
|
298
|
+
secret: (secretName) => `the ${secretName} secret is not registered`,
|
|
299
|
+
fork: () => "this pull request comes from a fork, and GitHub passes no secrets to runs from forks",
|
|
300
|
+
dependabot: (secretName) => `Dependabot runs receive only Dependabot secrets, and ${secretName} is not one of them`,
|
|
301
|
+
rounds: (_secretName, maxRounds) => `the automated review round limit (${maxRounds}) is reached`
|
|
302
|
+
},
|
|
303
|
+
ko: {
|
|
304
|
+
secret: (secretName) => `${secretName} \uC2DC\uD06C\uB9BF\uC774 \uB4F1\uB85D\uB418\uC9C0 \uC54A\uC74C`,
|
|
305
|
+
fork: () => "\uD3EC\uD06C\uC5D0\uC11C \uC628 pull request\uC774\uACE0, GitHub\uB294 \uD3EC\uD06C \uC2E4\uD589\uC5D0 \uC2DC\uD06C\uB9BF\uC744 \uB118\uAE30\uC9C0 \uC54A\uC74C",
|
|
306
|
+
dependabot: (secretName) => `Dependabot \uC2E4\uD589\uC740 Dependabot \uC2DC\uD06C\uB9BF\uB9CC \uBC1B\uB294\uB370, ${secretName}\uB294 \uADF8\uC911\uC5D0 \uC5C6\uC74C`,
|
|
307
|
+
rounds: (_secretName, maxRounds) => `\uC790\uB3D9 \uB9AC\uBDF0 \uB77C\uC6B4\uB4DC \uD55C\uB3C4(${maxRounds})\uC5D0 \uB3C4\uB2EC\uD568`
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
function renderNotRunNotice(locale, reason, secretName, maxRounds) {
|
|
311
|
+
const why = NOT_RUN_REASON[locale][reason](secretName, maxRounds);
|
|
312
|
+
return locale === "en" ? `AI review did not run for this commit (${why}). A green check here does not mean the change was reviewed.` : `\uC774 \uCEE4\uBC0B\uC5D0\uB294 AI \uB9AC\uBDF0\uAC00 \uC2E4\uD589\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4(${why}). \uC774 green \uCCB4\uD06C\uB294 \uBCC0\uACBD \uC0AC\uD56D\uC774 \uB9AC\uBDF0\uB418\uC5C8\uB2E4\uB294 \uB73B\uC774 \uC544\uB2D9\uB2C8\uB2E4.`;
|
|
313
|
+
}
|
|
314
|
+
|
|
291
315
|
// ../review-policy/dist/verdict-gate/verdict-gate.js
|
|
292
316
|
var ROUND_MARKER = "<!-- archweave-review-round -->";
|
|
293
317
|
|
|
@@ -369,8 +393,35 @@ function renderRoundGateStep(maxRounds) {
|
|
|
369
393
|
" fi"
|
|
370
394
|
].join("\n");
|
|
371
395
|
}
|
|
396
|
+
function shellQuote(text) {
|
|
397
|
+
return `'${text.replace(/'/g, `'\\''`)}'`;
|
|
398
|
+
}
|
|
399
|
+
function renderNotRunFunction(secretName, maxRounds, locale) {
|
|
400
|
+
const reasons = ["secret", "fork", "dependabot", "rounds"];
|
|
401
|
+
const [commitBefore, commitAfter] = renderCommitLine(locale, "\0").split("\0");
|
|
402
|
+
return [
|
|
403
|
+
"not_run() {",
|
|
404
|
+
' case "$1" in',
|
|
405
|
+
...reasons.map((reason) => ` ${reason}) annotation=${shellQuote(renderNotRunNotice("en", reason, secretName, maxRounds))}; notice=${shellQuote(renderNotRunNotice(locale, reason, secretName, maxRounds))} ;;`),
|
|
406
|
+
" esac",
|
|
407
|
+
' echo "::notice title=AI review did not run::$annotation"',
|
|
408
|
+
` printf '%s\\n' "$notice" >> "$GITHUB_STEP_SUMMARY"`,
|
|
409
|
+
` short_sha=$(printf '%s' "$HEAD_SHA" | cut -c1-7)`,
|
|
410
|
+
' case "$short_sha" in',
|
|
411
|
+
" '' | *[!0-9a-f]*) short_sha= ;;",
|
|
412
|
+
" esac",
|
|
413
|
+
" {",
|
|
414
|
+
' echo "notice-body<<ARCHWEAVE_NOT_RUN_EOF"',
|
|
415
|
+
` printf '%s\\n' ${shellQuote(VERDICT_MARKER)} "\u2139\uFE0F $notice"`,
|
|
416
|
+
` if [ -n "$short_sha" ]; then printf '\\n%s\\n' ${shellQuote(commitBefore)}"$short_sha"${shellQuote(commitAfter)}; fi`,
|
|
417
|
+
' echo "ARCHWEAVE_NOT_RUN_EOF"',
|
|
418
|
+
' echo "run=false"',
|
|
419
|
+
' } >> "$GITHUB_OUTPUT"',
|
|
420
|
+
"}"
|
|
421
|
+
];
|
|
422
|
+
}
|
|
372
423
|
var JUDGE_CONDITION = "steps.ai-review-result.outputs.run == 'true'";
|
|
373
|
-
function renderAiReviewResultStep() {
|
|
424
|
+
function renderAiReviewResultStep(secretName, maxRounds, locale) {
|
|
374
425
|
return [
|
|
375
426
|
"- name: Check the AI review result",
|
|
376
427
|
" id: ai-review-result",
|
|
@@ -379,15 +430,26 @@ function renderAiReviewResultStep() {
|
|
|
379
430
|
" REVIEW_ENABLED: ${{ needs.ai-review.outputs.enabled }}",
|
|
380
431
|
" REVIEW_ALLOWED: ${{ needs.ai-review.outputs.allowed }}",
|
|
381
432
|
" LAST_COMMENT_ID: ${{ needs.ai-review.outputs.last-comment-id }}",
|
|
433
|
+
" HEAD_SHA: ${{ github.event.pull_request.head.sha }}",
|
|
434
|
+
" HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name }}",
|
|
435
|
+
" REPOSITORY: ${{ github.repository }}",
|
|
436
|
+
" PR_AUTHOR: ${{ github.event.pull_request.user.login }}",
|
|
382
437
|
" run: |",
|
|
438
|
+
...renderNotRunFunction(secretName, maxRounds, locale).map((line) => ` ${line}`),
|
|
383
439
|
' if [ "$AI_REVIEW_RESULT" != success ]; then',
|
|
384
440
|
` echo "::error title=AI review did not succeed::The AI review job ended with '$AI_REVIEW_RESULT'."`,
|
|
385
441
|
" exit 1",
|
|
386
442
|
" fi",
|
|
387
443
|
' case "$REVIEW_ENABLED/$REVIEW_ALLOWED" in',
|
|
388
|
-
" false/*
|
|
389
|
-
'
|
|
390
|
-
|
|
444
|
+
" false/*)",
|
|
445
|
+
' if [ "$HEAD_REPOSITORY" != "$REPOSITORY" ]; then not_run fork',
|
|
446
|
+
` elif [ "$PR_AUTHOR" = 'dependabot[bot]' ]; then not_run dependabot`,
|
|
447
|
+
" else not_run secret",
|
|
448
|
+
" fi",
|
|
449
|
+
" exit 0",
|
|
450
|
+
" ;;",
|
|
451
|
+
" true/false)",
|
|
452
|
+
" not_run rounds",
|
|
391
453
|
" exit 0",
|
|
392
454
|
" ;;",
|
|
393
455
|
" true/true)",
|
|
@@ -428,10 +490,17 @@ function renderVerdictGateStep(maxRounds, locale) {
|
|
|
428
490
|
}
|
|
429
491
|
function renderBlockStep() {
|
|
430
492
|
return [
|
|
431
|
-
"- name: Fail
|
|
493
|
+
"- name: Fail the review check",
|
|
432
494
|
" if: steps.verdict.outputs.blocked == 'true'",
|
|
495
|
+
" env:",
|
|
496
|
+
" BLOCK_REASON: ${{ steps.verdict.outputs.block-reason }}",
|
|
433
497
|
" run: |",
|
|
434
|
-
'
|
|
498
|
+
' case "$BLOCK_REASON" in',
|
|
499
|
+
' block) echo "The AI verdict is BLOCK: fix the P0/P1 findings in the verdict comment and push." ;;',
|
|
500
|
+
' no-summary) echo "The AI review posted no summary for this commit, so there is no verdict. Re-run all jobs to review it again, or review it yourself." ;;',
|
|
501
|
+
' unreadable) echo "The AI review summary has no readable findings block, so there is no verdict. Re-run all jobs to review it again, or review it yourself." ;;',
|
|
502
|
+
' *) echo "The review check failed. See the verdict comment on this pull request." ;;',
|
|
503
|
+
" esac",
|
|
435
504
|
" exit 1"
|
|
436
505
|
].join("\n");
|
|
437
506
|
}
|
|
@@ -446,6 +515,9 @@ function renderVerdictCommentStep() {
|
|
|
446
515
|
" PREVIOUS_ID: ${{ needs.review.outputs.previous-comment-id }}",
|
|
447
516
|
" run: |",
|
|
448
517
|
` payload=$(jq -n --arg body "$VERDICT_BODY" '{body: $body}')`,
|
|
518
|
+
' if [ -z "$PREVIOUS_ID" ]; then',
|
|
519
|
+
` PREVIOUS_ID=$(gh api "repos/$REPOSITORY/issues/$PR_NUMBER/comments" --paginate --jq '.[] | select(.user.login == "github-actions[bot]") | select(.body | contains("${VERDICT_MARKER}")) | .id' | tail -n 1) || PREVIOUS_ID=`,
|
|
520
|
+
" fi",
|
|
449
521
|
' case "$PREVIOUS_ID" in',
|
|
450
522
|
" ''|*[!0-9]*) PREVIOUS_ID= ;;",
|
|
451
523
|
" esac",
|
|
@@ -516,10 +588,11 @@ function renderWorkspaceSetupSteps(packageManager) {
|
|
|
516
588
|
].join("\n"));
|
|
517
589
|
return steps.join("\n\n");
|
|
518
590
|
}
|
|
591
|
+
var REVIEW_JOB_TIMEOUT_MINUTES = 15;
|
|
519
592
|
function renderReviewWorkflow(input, parts) {
|
|
520
593
|
const aiReviewSteps = [renderSecretGateStep(parts.secretName), renderRoundGateStep(input.maxRounds), ...parts.reviewSteps];
|
|
521
594
|
const reviewSteps = [
|
|
522
|
-
renderAiReviewResultStep(),
|
|
595
|
+
renderAiReviewResultStep(parts.secretName, input.maxRounds, input.locale),
|
|
523
596
|
renderCheckoutStep(JUDGE_CONDITION),
|
|
524
597
|
renderWorkspaceSetupSteps(input.packageManager),
|
|
525
598
|
renderVerdictGateStep(input.maxRounds, input.locale),
|
|
@@ -537,6 +610,7 @@ function renderReviewWorkflow(input, parts) {
|
|
|
537
610
|
"jobs:",
|
|
538
611
|
" ai-review:",
|
|
539
612
|
" runs-on: ubuntu-latest",
|
|
613
|
+
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
540
614
|
indentBlock(renderReviewPermissions(parts.idToken), 4),
|
|
541
615
|
" outputs:",
|
|
542
616
|
" enabled: ${{ steps.secret-gate.outputs.enabled }}",
|
|
@@ -549,11 +623,12 @@ function renderReviewWorkflow(input, parts) {
|
|
|
549
623
|
" needs: ai-review",
|
|
550
624
|
" if: always()",
|
|
551
625
|
" runs-on: ubuntu-latest",
|
|
626
|
+
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
552
627
|
" permissions:",
|
|
553
628
|
" contents: read",
|
|
554
629
|
" pull-requests: read",
|
|
555
630
|
" outputs:",
|
|
556
|
-
" comment-body: ${{ steps.verdict.outputs.comment-body }}",
|
|
631
|
+
" comment-body: ${{ steps.verdict.outputs.comment-body || steps.ai-review-result.outputs.notice-body }}",
|
|
557
632
|
" previous-comment-id: ${{ steps.verdict.outputs.previous-comment-id }}",
|
|
558
633
|
" steps:",
|
|
559
634
|
indentBlock(reviewSteps.join("\n\n"), 6),
|
|
@@ -563,6 +638,7 @@ function renderReviewWorkflow(input, parts) {
|
|
|
563
638
|
// Runs after a BLOCK failed the review job, too: the comment explains the red check.
|
|
564
639
|
" if: ${{ !cancelled() && needs.review.outputs.comment-body != '' }}",
|
|
565
640
|
" runs-on: ubuntu-latest",
|
|
641
|
+
` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
|
|
566
642
|
" permissions:",
|
|
567
643
|
" pull-requests: write",
|
|
568
644
|
" steps:",
|
|
@@ -574,17 +650,27 @@ function renderReviewWorkflow(input, parts) {
|
|
|
574
650
|
// ../template-engine/dist/review/claude/claude-provider.js
|
|
575
651
|
var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
|
|
576
652
|
var ALLOWED_TOOLS = "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)";
|
|
653
|
+
var CLAUDE_STEP_ID = "claude-review";
|
|
654
|
+
var PR_CONTEXT = ["REPO: ${{ github.repository }}", "PR NUMBER: ${{ github.event.pull_request.number }}"];
|
|
577
655
|
function renderWorkflowInstructions(locale) {
|
|
578
656
|
const lines = locale === "en" ? [
|
|
579
|
-
|
|
580
|
-
"
|
|
657
|
+
...PR_CONTEXT,
|
|
658
|
+
"",
|
|
659
|
+
"Review the diff of this pull request using `gh pr diff <PR NUMBER>` and `gh pr view <PR NUMBER>`.",
|
|
660
|
+
"Post specific findings as inline comments with `mcp__github_inline_comment__create_inline_comment` (pass `confirmed: true`), and exactly one top-level summary comment.",
|
|
661
|
+
"Post the summary with `gh pr comment <PR NUMBER> --body-file - <<'ARCHWEAVE_EOF'`, the body on the following lines, then a line `ARCHWEAVE_EOF` (the quoted delimiter keeps backticks and `$` literal). Never pass the body with `--body \"...\"` or `$(...)`.",
|
|
662
|
+
"Run each `gh` command on its own: no pipes to other programs, no `&&` chains.",
|
|
581
663
|
`Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
|
|
582
664
|
"If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
|
|
583
665
|
"Do not modify any code, and never approve or merge the pull request.",
|
|
584
666
|
"Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
|
|
585
667
|
] : [
|
|
586
|
-
|
|
587
|
-
"
|
|
668
|
+
...PR_CONTEXT,
|
|
669
|
+
"",
|
|
670
|
+
"\uC774 pull request\uC758 diff\uB97C `gh pr diff <PR NUMBER>`\uC640 `gh pr view <PR NUMBER>`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
|
|
671
|
+
"\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 `mcp__github_inline_comment__create_inline_comment`(`confirmed: true` \uC804\uB2EC)\uB85C inline comment\uB97C \uB2EC\uACE0, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
|
|
672
|
+
"\uC694\uC57D\uC740 `gh pr comment <PR NUMBER> --body-file - <<'ARCHWEAVE_EOF'`\uB85C \uAC8C\uC2DC\uD558\uC138\uC694. \uB2E4\uC74C \uC904\uBD80\uD130 \uBCF8\uBB38\uC744 \uC4F0\uACE0, \uB9C8\uC9C0\uB9C9 \uC904\uC5D0 `ARCHWEAVE_EOF`\uB9CC \uC4F0\uC138\uC694(\uB530\uC634\uD45C \uCE5C \uAD6C\uBD84\uC790\uB77C \uBC31\uD2F1\uACFC `$`\uAC00 \uADF8\uB300\uB85C \uB0A8\uC544\uC694). \uBCF8\uBB38\uC744 `--body \"...\"`\uB098 `$(...)`\uB85C \uB118\uAE30\uC9C0 \uB9C8\uC138\uC694.",
|
|
673
|
+
"`gh` \uBA85\uB839\uC740 \uD558\uB098\uC529 \uB530\uB85C \uC2E4\uD589\uD558\uC138\uC694. \uB2E4\uB978 \uD504\uB85C\uADF8\uB7A8\uC73C\uB85C \uD30C\uC774\uD504\uD558\uAC70\uB098 `&&`\uB85C \uC787\uC9C0 \uB9C8\uC138\uC694.",
|
|
588
674
|
`top-level \uC694\uC57D \uCF54\uBA58\uD2B8\uC5D0\uB294 \uBC18\uB4DC\uC2DC \uB9AC\uD130\uB7F4 \uB9C8\uCEE4 ${ROUND_MARKER}\uB97C \uD3EC\uD568\uD558\uC138\uC694.`,
|
|
589
675
|
'\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uAC00 \uC5C6\uC73C\uBA74 "\uC870\uCE58\uAC00 \uD544\uC694\uD55C \uBB38\uC81C\uB97C \uCC3E\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4."\uB77C\uB294 top-level comment\uB97C \uC815\uD655\uD788 \uD55C \uAC1C \uAC8C\uC2DC\uD558\uC138\uC694(\uC774\uB54C\uB3C4 \uB9C8\uCEE4\uB294 \uD3EC\uD568).',
|
|
590
676
|
"\uCF54\uB4DC\uB97C \uC218\uC815\uD558\uC9C0 \uB9C8\uC138\uC694(Do not modify any code). PR\uC744 \uC2B9\uC778\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC9C0 \uB9C8\uC138\uC694(never approve or merge).",
|
|
@@ -601,6 +687,20 @@ function composePrompt(input) {
|
|
|
601
687
|
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
602
688
|
return sections.join("\n\n---\n\n");
|
|
603
689
|
}
|
|
690
|
+
function renderDeniedToolsStep() {
|
|
691
|
+
return [
|
|
692
|
+
"- name: Show denied tool calls",
|
|
693
|
+
` if: always() && steps.${CLAUDE_STEP_ID}.outputs.execution_file != ''`,
|
|
694
|
+
" env:",
|
|
695
|
+
` EXECUTION_FILE: \${{ steps.${CLAUDE_STEP_ID}.outputs.execution_file }}`,
|
|
696
|
+
" run: |",
|
|
697
|
+
' [ -f "$EXECUTION_FILE" ] || exit 0',
|
|
698
|
+
` stop_token="archweave-$(od -An -N16 -tx1 /dev/urandom | tr -d ' \\n')"`,
|
|
699
|
+
' echo "::stop-commands::$stop_token"',
|
|
700
|
+
` jq -c '.[] | select(.type == "result") | ((.permission_denials[]? | {tool: .tool_name, command: ((.tool_input.command // "") | tostring | .[0:200])}), (select(.is_error == true) | {error: ((.result // "") | tostring | .[0:300])}))' "$EXECUTION_FILE" || echo "Could not read the execution file."`,
|
|
701
|
+
' echo "::$stop_token::"'
|
|
702
|
+
].join("\n");
|
|
703
|
+
}
|
|
604
704
|
function buildWorkflowYaml(input) {
|
|
605
705
|
return renderReviewWorkflow(input, {
|
|
606
706
|
name: "Claude Code Review",
|
|
@@ -611,6 +711,7 @@ function buildWorkflowYaml(input) {
|
|
|
611
711
|
renderCheckoutStep(REVIEW_GATE_CONDITION),
|
|
612
712
|
[
|
|
613
713
|
"- name: Run Claude review",
|
|
714
|
+
` id: ${CLAUDE_STEP_ID}`,
|
|
614
715
|
` if: ${REVIEW_GATE_CONDITION}`,
|
|
615
716
|
` uses: ${pinnedUses("anthropics/claude-code-action")}`,
|
|
616
717
|
" with:",
|
|
@@ -619,7 +720,8 @@ function buildWorkflowYaml(input) {
|
|
|
619
720
|
indentBlock(composePrompt(input), 6),
|
|
620
721
|
" claude_args: |",
|
|
621
722
|
` --allowedTools "${ALLOWED_TOOLS}"`
|
|
622
|
-
].join("\n")
|
|
723
|
+
].join("\n"),
|
|
724
|
+
renderDeniedToolsStep()
|
|
623
725
|
]
|
|
624
726
|
});
|
|
625
727
|
}
|
|
@@ -687,7 +789,9 @@ function buildWorkflowYaml2(input) {
|
|
|
687
789
|
` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
|
|
688
790
|
" GH_TOKEN: ${{ github.token }}",
|
|
689
791
|
" run: |",
|
|
690
|
-
|
|
792
|
+
// cursor-agent stops at "Workspace Trust Required" without --trust (2026.10.22 on
|
|
793
|
+
// Actions); the folder is this run's own checkout, and forks never get the secret.
|
|
794
|
+
` agent --trust -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
|
|
691
795
|
indentBlock(composePrompt2(input), 4),
|
|
692
796
|
` ${PROMPT_HEREDOC_DELIMITER}`,
|
|
693
797
|
' )"'
|
|
@@ -731,6 +835,7 @@ function composePrompt3(input) {
|
|
|
731
835
|
sections.push(renderFindingsBlockInstruction(input.locale));
|
|
732
836
|
return sections.join("\n\n---\n\n");
|
|
733
837
|
}
|
|
838
|
+
var GEMINI_CLI_VERSION = "0.65.0";
|
|
734
839
|
function buildWorkflowYaml3(input) {
|
|
735
840
|
return renderReviewWorkflow(input, {
|
|
736
841
|
name: "Gemini Code Review",
|
|
@@ -745,8 +850,13 @@ function buildWorkflowYaml3(input) {
|
|
|
745
850
|
` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
|
|
746
851
|
" env:",
|
|
747
852
|
" GITHUB_TOKEN: ${{ github.token }}",
|
|
853
|
+
// Gemini CLI 0.65 refuses to run in a folder it hasn't been told to trust (measured
|
|
854
|
+
// 2026-10-25 on Actions). The folder is this run's own checkout; pull requests from forks
|
|
855
|
+
// never get here, because they get no secret.
|
|
856
|
+
" GEMINI_CLI_TRUST_WORKSPACE: 'true'",
|
|
748
857
|
" with:",
|
|
749
858
|
` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
|
|
859
|
+
` gemini_cli_version: '${GEMINI_CLI_VERSION}'`,
|
|
750
860
|
" prompt: |",
|
|
751
861
|
indentBlock(composePrompt3(input), 6)
|
|
752
862
|
].join("\n")
|
|
@@ -919,7 +1029,8 @@ var REVIEW_CHECK_IDS = [
|
|
|
919
1029
|
"review-alt-file-coexistence",
|
|
920
1030
|
"review-config-consistency",
|
|
921
1031
|
"review-workflow-providers",
|
|
922
|
-
"review-workflow-pnpm"
|
|
1032
|
+
"review-workflow-pnpm",
|
|
1033
|
+
"review-workflow-fail-open"
|
|
923
1034
|
];
|
|
924
1035
|
var WORKFLOWS_DIR = ".github/workflows";
|
|
925
1036
|
function archweaveWorkflows(root) {
|
|
@@ -976,6 +1087,25 @@ function buildReviewPnpmCheck(root, workflows) {
|
|
|
976
1087
|
suggestedAction: "Regenerate the workflow: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite."
|
|
977
1088
|
});
|
|
978
1089
|
}
|
|
1090
|
+
var FAIL_CLOSED_VERDICT_MODE = "ARCHWEAVE_VERDICT_OUTPUT: 'github-output'";
|
|
1091
|
+
function buildReviewFailOpenCheck(root, workflows) {
|
|
1092
|
+
const id = "review-workflow-fail-open";
|
|
1093
|
+
const label = "Review check fails closed";
|
|
1094
|
+
if (workflows.length === 0)
|
|
1095
|
+
return createDoctorCheck({ id, label, status: "not-applicable", message: "no nx-archweave review workflow", evidence: [] });
|
|
1096
|
+
const failOpen = workflows.filter((path) => !readFileSync2(join2(root, path), "utf-8").includes(FAIL_CLOSED_VERDICT_MODE));
|
|
1097
|
+
if (failOpen.length === 0) {
|
|
1098
|
+
return createDoctorCheck({ id, label, status: "pass", message: "a review run that leaves no readable summary fails the review check", evidence: workflows });
|
|
1099
|
+
}
|
|
1100
|
+
return createDoctorCheck({
|
|
1101
|
+
id,
|
|
1102
|
+
label,
|
|
1103
|
+
status: "error",
|
|
1104
|
+
message: "fail-open workflow: generated before nx-archweave 0.2.2, it passes the review check when the AI review posts no summary or one that cannot be read, so an unreviewed pull request can merge",
|
|
1105
|
+
evidence: failOpen,
|
|
1106
|
+
suggestedAction: "Re-run add-review: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite. The report shows the difference before anything is replaced."
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
979
1109
|
function buildReviewChecks(root) {
|
|
980
1110
|
const config = readToolkitConfigState(root);
|
|
981
1111
|
const mainWorkflowPath = REVIEW_PROVIDERS[resolveProviderId(config)].workflowFileName;
|
|
@@ -989,7 +1119,8 @@ function buildReviewChecks(root) {
|
|
|
989
1119
|
buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists),
|
|
990
1120
|
buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config),
|
|
991
1121
|
buildReviewProvidersCheck(workflows),
|
|
992
|
-
buildReviewPnpmCheck(root, workflows)
|
|
1122
|
+
buildReviewPnpmCheck(root, workflows),
|
|
1123
|
+
buildReviewFailOpenCheck(root, workflows)
|
|
993
1124
|
];
|
|
994
1125
|
}
|
|
995
1126
|
function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config) {
|
|
@@ -1228,15 +1359,22 @@ var BACKEND_CHECK_IDS = [
|
|
|
1228
1359
|
"backend-config-consistency",
|
|
1229
1360
|
"shared-types-lib",
|
|
1230
1361
|
"boundary-constraints-applied",
|
|
1231
|
-
"boundary-tags-exist"
|
|
1362
|
+
"boundary-tags-exist",
|
|
1363
|
+
"boundary-untagged-projects"
|
|
1232
1364
|
];
|
|
1233
1365
|
var CHECK_LABELS = {
|
|
1234
1366
|
"backend-framework": "Backend framework (root package.json only)",
|
|
1235
1367
|
"backend-config-consistency": "Backend config consistency",
|
|
1236
1368
|
"shared-types-lib": "shared-types library",
|
|
1237
1369
|
"boundary-constraints-applied": "Module-boundary constraints",
|
|
1238
|
-
"boundary-tags-exist": "Boundary tags exist"
|
|
1370
|
+
"boundary-tags-exist": "Boundary tags exist",
|
|
1371
|
+
"boundary-untagged-projects": "Projects without a boundary tag"
|
|
1239
1372
|
};
|
|
1373
|
+
var UNTAGGED_LISTED = 10;
|
|
1374
|
+
function isEndToEndRoot(projectRoot) {
|
|
1375
|
+
const base = projectRoot.split("/").pop() ?? "";
|
|
1376
|
+
return base === "e2e" || base.endsWith("-e2e");
|
|
1377
|
+
}
|
|
1240
1378
|
function collectScopeTags(root, projects) {
|
|
1241
1379
|
if (projects !== void 0)
|
|
1242
1380
|
return new Set(projects.flatMap((project) => project.tags));
|
|
@@ -1393,13 +1531,15 @@ function buildBackendChecks(root, projects) {
|
|
|
1393
1531
|
suggestedAction: classification === "enforced" ? void 0 : "Run add-backend with --applyEslint, or merge its snippet yourself."
|
|
1394
1532
|
}));
|
|
1395
1533
|
if (classification !== "enforced") {
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1534
|
+
for (const id of ["boundary-tags-exist", "boundary-untagged-projects"]) {
|
|
1535
|
+
checks.push(createDoctorCheck({
|
|
1536
|
+
id,
|
|
1537
|
+
label: CHECK_LABELS[id],
|
|
1538
|
+
status: "not-applicable",
|
|
1539
|
+
message: "Skipped \u2014 boundary constraints are not enforced (see boundary-constraints-applied)",
|
|
1540
|
+
evidence: []
|
|
1541
|
+
}));
|
|
1542
|
+
}
|
|
1403
1543
|
} else {
|
|
1404
1544
|
const tags = collectScopeTags(root, projects);
|
|
1405
1545
|
const required = readsBackendOnly(root) ? [boundaryTags.backend] : [boundaryTags.frontend, boundaryTags.backend];
|
|
@@ -1412,9 +1552,36 @@ function buildBackendChecks(root, projects) {
|
|
|
1412
1552
|
evidence: missing.length === 0 ? [] : missing,
|
|
1413
1553
|
suggestedAction: missing.length === 0 ? void 0 : "Add the missing tag(s) to the app project.json tags or package.json nx.tags (add-backend does this for the two apps)."
|
|
1414
1554
|
}));
|
|
1555
|
+
checks.push(buildUntaggedProjectsCheck(projects, [boundaryTags.frontend, boundaryTags.backend, boundaryTags.shared]));
|
|
1415
1556
|
}
|
|
1416
1557
|
return checks;
|
|
1417
1558
|
}
|
|
1559
|
+
function buildUntaggedProjectsCheck(projects, roleTags) {
|
|
1560
|
+
const id = "boundary-untagged-projects";
|
|
1561
|
+
if (projects === void 0) {
|
|
1562
|
+
return createDoctorCheck({
|
|
1563
|
+
id,
|
|
1564
|
+
label: CHECK_LABELS[id],
|
|
1565
|
+
status: "not-applicable",
|
|
1566
|
+
message: "Skipped \u2014 needs the workspace's Nx project list (run doctor through nx g)",
|
|
1567
|
+
evidence: []
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
const untagged = projects.filter((project) => project.root !== "" && project.root !== "." && !isEndToEndRoot(project.root)).filter((project) => !project.tags.some((tag) => roleTags.includes(tag))).map((project) => project.name);
|
|
1571
|
+
if (untagged.length === 0) {
|
|
1572
|
+
return createDoctorCheck({ id, label: CHECK_LABELS[id], status: "pass", message: `every project carries one of ${roleTags.join(", ")}`, evidence: [] });
|
|
1573
|
+
}
|
|
1574
|
+
const listed = untagged.slice(0, UNTAGGED_LISTED).join(", ");
|
|
1575
|
+
const more = untagged.length > UNTAGGED_LISTED ? ` and ${untagged.length - UNTAGGED_LISTED} more` : "";
|
|
1576
|
+
return createDoctorCheck({
|
|
1577
|
+
id,
|
|
1578
|
+
label: CHECK_LABELS[id],
|
|
1579
|
+
status: "warning",
|
|
1580
|
+
message: `${untagged.length} project(s) carry none of ${roleTags.join(", ")}, so the frontend/backend deny rules don't cover them: ${listed}${more}`,
|
|
1581
|
+
evidence: untagged,
|
|
1582
|
+
suggestedAction: `Give each one the tag of its side (${roleTags.join(", ")}) in package.json nx.tags or project.json tags. When you generate a library, pass it right away: nx g @nx/js:lib libs/<name> --tags=${roleTags[2]}.`
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1418
1585
|
|
|
1419
1586
|
// ../template-engine/dist/doctor/fsd-like-check.js
|
|
1420
1587
|
import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
|
|
@@ -1665,7 +1832,7 @@ var NOT_APPLICABLE_IDS = [
|
|
|
1665
1832
|
...REVIEW_CHECK_IDS,
|
|
1666
1833
|
...BACKEND_CHECK_IDS
|
|
1667
1834
|
];
|
|
1668
|
-
function runDoctorChecks(root, projects) {
|
|
1835
|
+
function runDoctorChecks(root, projects, nxSupport) {
|
|
1669
1836
|
const env = detectEnvironment(root);
|
|
1670
1837
|
if (!env.hasNxWorkspace) {
|
|
1671
1838
|
const checks2 = [
|
|
@@ -1695,7 +1862,7 @@ function runDoctorChecks(root, projects) {
|
|
|
1695
1862
|
message: "nx.json found",
|
|
1696
1863
|
evidence: ["nx.json"]
|
|
1697
1864
|
}),
|
|
1698
|
-
buildNxVersionCheck(env),
|
|
1865
|
+
buildNxVersionCheck(env, nxSupport),
|
|
1699
1866
|
buildFsdLikeCheck(root, projects),
|
|
1700
1867
|
buildFsdEnforcementCheck(root, projects),
|
|
1701
1868
|
buildFsdRuleAnchoredCheck(root, projects),
|
|
@@ -1724,12 +1891,25 @@ function runDoctorChecks(root, projects) {
|
|
|
1724
1891
|
const overallStatus = computeOverallStatus(checks);
|
|
1725
1892
|
return { checks, overallStatus };
|
|
1726
1893
|
}
|
|
1727
|
-
var
|
|
1894
|
+
var VALIDATED_NX_MAJORS = [23, 24];
|
|
1895
|
+
var VALIDATED_RANGE_TEXT = `${VALIDATED_NX_MAJORS[0]}.x\u2013${VALIDATED_NX_MAJORS[VALIDATED_NX_MAJORS.length - 1]}.x`;
|
|
1728
1896
|
function extractMajor(version) {
|
|
1729
1897
|
const match = /(\d+)/.exec(version);
|
|
1730
1898
|
return match ? Number(match[1]) : null;
|
|
1731
1899
|
}
|
|
1732
|
-
function buildNxVersionCheck(env) {
|
|
1900
|
+
function buildNxVersionCheck(env, nxSupport) {
|
|
1901
|
+
if (nxSupport?.inRange === false && nxSupport.installedVersion !== void 0) {
|
|
1902
|
+
const major2 = extractMajor(nxSupport.installedVersion);
|
|
1903
|
+
const supported = `${nxSupport.supportedMajors.min}.x\u2013${nxSupport.supportedMajors.max}.x`;
|
|
1904
|
+
return createDoctorCheck({
|
|
1905
|
+
id: "nx-version",
|
|
1906
|
+
label: "Nx version",
|
|
1907
|
+
status: "error",
|
|
1908
|
+
message: `Nx ${nxSupport.installedVersion} is installed, outside the majors the plugin's @nx/devkit ${nxSupport.devkitVersion} supports (nx ${supported}) \u2014 the generators may fail to load or misbehave`,
|
|
1909
|
+
evidence: [`nx@${nxSupport.installedVersion}`, `@nx/devkit@${nxSupport.devkitVersion}`],
|
|
1910
|
+
suggestedAction: `Install the @clearlydev/nx-archweave release built for Nx ${major2 ?? "this"}${major2 === null ? "" : ".x"}, or keep nx at ${supported}.`
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1733
1913
|
if (env.rootPackageJsonError) {
|
|
1734
1914
|
return createDoctorCheck({
|
|
1735
1915
|
id: "nx-version",
|
|
@@ -1759,30 +1939,30 @@ function buildNxVersionCheck(env) {
|
|
|
1759
1939
|
evidence: [`nx@${env.nxVersion}`]
|
|
1760
1940
|
});
|
|
1761
1941
|
}
|
|
1762
|
-
if (major
|
|
1942
|
+
if (VALIDATED_NX_MAJORS.includes(major)) {
|
|
1763
1943
|
return createDoctorCheck({
|
|
1764
1944
|
id: "nx-version",
|
|
1765
1945
|
label: "Nx version",
|
|
1766
1946
|
status: "pass",
|
|
1767
|
-
message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${
|
|
1947
|
+
message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${VALIDATED_RANGE_TEXT})`,
|
|
1768
1948
|
evidence: [`nx@${env.nxVersion}`]
|
|
1769
1949
|
});
|
|
1770
1950
|
}
|
|
1771
|
-
if (major <
|
|
1951
|
+
if (major < VALIDATED_NX_MAJORS[0]) {
|
|
1772
1952
|
return createDoctorCheck({
|
|
1773
1953
|
id: "nx-version",
|
|
1774
1954
|
label: "Nx version",
|
|
1775
1955
|
status: "warning",
|
|
1776
|
-
message: `Nx ${env.nxVersion} is below the toolkit-validated range (${
|
|
1956
|
+
message: `Nx ${env.nxVersion} is below the toolkit-validated range (${VALIDATED_RANGE_TEXT}) \u2014 generator behavior is not guaranteed`,
|
|
1777
1957
|
evidence: [`nx@${env.nxVersion}`],
|
|
1778
|
-
suggestedAction: `Upgrade the workspace to Nx ${
|
|
1958
|
+
suggestedAction: `Upgrade the workspace to Nx ${VALIDATED_NX_MAJORS[VALIDATED_NX_MAJORS.length - 1]}.x before running the generators.`
|
|
1779
1959
|
});
|
|
1780
1960
|
}
|
|
1781
1961
|
return createDoctorCheck({
|
|
1782
1962
|
id: "nx-version",
|
|
1783
1963
|
label: "Nx version",
|
|
1784
1964
|
status: "warning",
|
|
1785
|
-
message: `Nx ${env.nxVersion} is above the toolkit-validated range (${
|
|
1965
|
+
message: `Nx ${env.nxVersion} is above the toolkit-validated range (${VALIDATED_RANGE_TEXT}) \u2014 may work, unvalidated`,
|
|
1786
1966
|
evidence: [`nx@${env.nxVersion}`],
|
|
1787
1967
|
suggestedAction: "Proceed with care and report issues; validation for newer majors lands with toolkit updates."
|
|
1788
1968
|
});
|
|
@@ -2714,7 +2894,7 @@ function diskRunNx(root) {
|
|
|
2714
2894
|
var DOCTOR_TARGET_ENVIRONMENT = "claude-code";
|
|
2715
2895
|
async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
|
|
2716
2896
|
const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
|
|
2717
|
-
const baseReport = runDoctorChecks(root, options.projects);
|
|
2897
|
+
const baseReport = runDoctorChecks(root, options.projects, options.nxSupport);
|
|
2718
2898
|
const declaredIds = readEnabledIntegrationIds(root);
|
|
2719
2899
|
const runNx = diskRunNx(root);
|
|
2720
2900
|
const driftResults = await detectIntegrationsDrift({
|
|
@@ -3163,11 +3343,49 @@ function buildPolicySuggestion(tree, sourceRoot, current) {
|
|
|
3163
3343
|
});
|
|
3164
3344
|
}
|
|
3165
3345
|
|
|
3346
|
+
// src/generators/doctor/nx-support.ts
|
|
3347
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
3348
|
+
import { createRequire } from "node:module";
|
|
3349
|
+
import { join as join12 } from "node:path";
|
|
3350
|
+
var MAJORS_EITHER_SIDE = 1;
|
|
3351
|
+
function majorOf(version) {
|
|
3352
|
+
const match = /^(\d+)\./.exec(version.trim());
|
|
3353
|
+
return match ? Number(match[1]) : void 0;
|
|
3354
|
+
}
|
|
3355
|
+
function readOwnDevkitVersion() {
|
|
3356
|
+
try {
|
|
3357
|
+
const path = createRequire(import.meta.url).resolve("@nx/devkit/package.json");
|
|
3358
|
+
const version = JSON.parse(readFileSync9(path, "utf-8")).version;
|
|
3359
|
+
return typeof version === "string" ? version : void 0;
|
|
3360
|
+
} catch {
|
|
3361
|
+
return void 0;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
function readInstalledNx(root) {
|
|
3365
|
+
try {
|
|
3366
|
+
const version = JSON.parse(readFileSync9(join12(root, "node_modules", "nx", "package.json"), "utf-8")).version;
|
|
3367
|
+
return typeof version === "string" ? version : void 0;
|
|
3368
|
+
} catch {
|
|
3369
|
+
return void 0;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
function readNxSupport(root, deps = {}) {
|
|
3373
|
+
const devkitVersion = (deps.devkitVersion ?? readOwnDevkitVersion)();
|
|
3374
|
+
const devkitMajor = devkitVersion === void 0 ? void 0 : majorOf(devkitVersion);
|
|
3375
|
+
if (devkitVersion === void 0 || devkitMajor === void 0) return void 0;
|
|
3376
|
+
const supportedMajors = { min: devkitMajor - MAJORS_EITHER_SIDE, max: devkitMajor + MAJORS_EITHER_SIDE };
|
|
3377
|
+
const installedVersion = (deps.installedNx ?? readInstalledNx)(root);
|
|
3378
|
+
const installedMajor = installedVersion === void 0 ? void 0 : majorOf(installedVersion);
|
|
3379
|
+
const inRange = installedMajor === void 0 ? void 0 : installedMajor >= supportedMajors.min && installedMajor <= supportedMajors.max;
|
|
3380
|
+
return { devkitVersion, supportedMajors, installedVersion, inRange };
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3166
3383
|
// src/generators/doctor/generator.ts
|
|
3167
3384
|
async function doctorGenerator(tree, options) {
|
|
3168
3385
|
const base = await buildDoctorReportWithIntegrations(tree.root, {
|
|
3169
3386
|
liveIntegrations: options.liveIntegrations ?? false,
|
|
3170
|
-
projects: doctorProjects(tree)
|
|
3387
|
+
projects: doctorProjects(tree),
|
|
3388
|
+
nxSupport: readNxSupport(tree.root)
|
|
3171
3389
|
});
|
|
3172
3390
|
const checks = base.overallStatus === "unsupported" ? base.checks : [...base.checks, ...buildWorkspaceChecks(tree)];
|
|
3173
3391
|
const report = { checks, overallStatus: base.overallStatus === "unsupported" ? base.overallStatus : computeOverallStatus(checks) };
|