@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.
@@ -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/* | true/false)",
389
- ' echo "The AI review did not run (no secret, or the round limit) - nothing to judge."',
390
- ' echo "run=false" >> "$GITHUB_OUTPUT"',
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 on a blocking verdict",
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
- ' echo "The review verdict is BLOCK - see the verdict comment on this pull request."',
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
- "Review the diff of this pull request using `gh pr diff` and `gh pr view`.",
580
- "Post specific findings as inline comments and exactly one top-level summary comment.",
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
- "\uC774 pull request\uC758 diff\uB97C `gh pr diff`\uC640 `gh pr view`\uB85C \uD655\uC778\uD558\uACE0 \uB9AC\uBDF0\uD558\uC138\uC694.",
587
- "\uAD6C\uCCB4\uC801 \uC9C0\uC801\uC740 inline comment\uB85C, \uC804\uCCB4 \uC694\uC57D\uC740 \uC815\uD655\uD788 \uD55C \uAC1C\uC758 top-level comment\uB85C \uAC8C\uC2DC\uD558\uC138\uC694.",
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
- ` agent -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
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
- checks.push(createDoctorCheck({
1397
- id: "boundary-tags-exist",
1398
- label: CHECK_LABELS["boundary-tags-exist"],
1399
- status: "not-applicable",
1400
- message: "Skipped \u2014 boundary constraints are not enforced (see boundary-constraints-applied)",
1401
- evidence: []
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 VALIDATED_NX_MAJOR = 23;
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 === VALIDATED_NX_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 (${VALIDATED_NX_MAJOR}.x)`,
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 < VALIDATED_NX_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 (${VALIDATED_NX_MAJOR}.x) \u2014 generator behavior is not guaranteed`,
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 ${VALIDATED_NX_MAJOR}.x before running the generators.`
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 (${VALIDATED_NX_MAJOR}.x) \u2014 may work, unvalidated`,
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) };