@clearlydev/nx-archweave 0.2.2 → 0.3.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.
@@ -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)",
@@ -431,7 +493,7 @@ function renderBlockStep() {
431
493
  "- name: Fail on a blocking verdict",
432
494
  " if: steps.verdict.outputs.blocked == 'true'",
433
495
  " run: |",
434
- ' echo "The review verdict is BLOCK - see the verdict comment on this pull request."',
496
+ ' echo "The review check failed: the verdict is BLOCK, or the AI review left no readable summary. See the verdict comment on this pull request."',
435
497
  " exit 1"
436
498
  ].join("\n");
437
499
  }
@@ -446,6 +508,9 @@ function renderVerdictCommentStep() {
446
508
  " PREVIOUS_ID: ${{ needs.review.outputs.previous-comment-id }}",
447
509
  " run: |",
448
510
  ` payload=$(jq -n --arg body "$VERDICT_BODY" '{body: $body}')`,
511
+ ' if [ -z "$PREVIOUS_ID" ]; then',
512
+ ` 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=`,
513
+ " fi",
449
514
  ' case "$PREVIOUS_ID" in',
450
515
  " ''|*[!0-9]*) PREVIOUS_ID= ;;",
451
516
  " esac",
@@ -516,10 +581,11 @@ function renderWorkspaceSetupSteps(packageManager) {
516
581
  ].join("\n"));
517
582
  return steps.join("\n\n");
518
583
  }
584
+ var REVIEW_JOB_TIMEOUT_MINUTES = 15;
519
585
  function renderReviewWorkflow(input, parts) {
520
586
  const aiReviewSteps = [renderSecretGateStep(parts.secretName), renderRoundGateStep(input.maxRounds), ...parts.reviewSteps];
521
587
  const reviewSteps = [
522
- renderAiReviewResultStep(),
588
+ renderAiReviewResultStep(parts.secretName, input.maxRounds, input.locale),
523
589
  renderCheckoutStep(JUDGE_CONDITION),
524
590
  renderWorkspaceSetupSteps(input.packageManager),
525
591
  renderVerdictGateStep(input.maxRounds, input.locale),
@@ -537,6 +603,7 @@ function renderReviewWorkflow(input, parts) {
537
603
  "jobs:",
538
604
  " ai-review:",
539
605
  " runs-on: ubuntu-latest",
606
+ ` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
540
607
  indentBlock(renderReviewPermissions(parts.idToken), 4),
541
608
  " outputs:",
542
609
  " enabled: ${{ steps.secret-gate.outputs.enabled }}",
@@ -549,11 +616,12 @@ function renderReviewWorkflow(input, parts) {
549
616
  " needs: ai-review",
550
617
  " if: always()",
551
618
  " runs-on: ubuntu-latest",
619
+ ` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
552
620
  " permissions:",
553
621
  " contents: read",
554
622
  " pull-requests: read",
555
623
  " outputs:",
556
- " comment-body: ${{ steps.verdict.outputs.comment-body }}",
624
+ " comment-body: ${{ steps.verdict.outputs.comment-body || steps.ai-review-result.outputs.notice-body }}",
557
625
  " previous-comment-id: ${{ steps.verdict.outputs.previous-comment-id }}",
558
626
  " steps:",
559
627
  indentBlock(reviewSteps.join("\n\n"), 6),
@@ -563,6 +631,7 @@ function renderReviewWorkflow(input, parts) {
563
631
  // Runs after a BLOCK failed the review job, too: the comment explains the red check.
564
632
  " if: ${{ !cancelled() && needs.review.outputs.comment-body != '' }}",
565
633
  " runs-on: ubuntu-latest",
634
+ ` timeout-minutes: ${REVIEW_JOB_TIMEOUT_MINUTES}`,
566
635
  " permissions:",
567
636
  " pull-requests: write",
568
637
  " steps:",
@@ -574,17 +643,27 @@ function renderReviewWorkflow(input, parts) {
574
643
  // ../template-engine/dist/review/claude/claude-provider.js
575
644
  var SECRET_NAME = "CLAUDE_CODE_OAUTH_TOKEN";
576
645
  var ALLOWED_TOOLS = "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)";
646
+ var CLAUDE_STEP_ID = "claude-review";
647
+ var PR_CONTEXT = ["REPO: ${{ github.repository }}", "PR NUMBER: ${{ github.event.pull_request.number }}"];
577
648
  function renderWorkflowInstructions(locale) {
578
649
  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.",
650
+ ...PR_CONTEXT,
651
+ "",
652
+ "Review the diff of this pull request using `gh pr diff <PR NUMBER>` and `gh pr view <PR NUMBER>`.",
653
+ "Post specific findings as inline comments with `mcp__github_inline_comment__create_inline_comment` (pass `confirmed: true`), and exactly one top-level summary comment.",
654
+ "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 `$(...)`.",
655
+ "Run each `gh` command on its own: no pipes to other programs, no `&&` chains.",
581
656
  `Always include the literal marker ${ROUND_MARKER} in the top-level summary comment.`,
582
657
  "If there are no findings that require action, post exactly one top-level comment saying so (and still include the marker).",
583
658
  "Do not modify any code, and never approve or merge the pull request.",
584
659
  "Write summaries and explanations in English; keep file names, function names, and identifiers verbatim."
585
660
  ] : [
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.",
661
+ ...PR_CONTEXT,
662
+ "",
663
+ "\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.",
664
+ "\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.",
665
+ "\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.",
666
+ "`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
667
  `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
668
  '\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
669
  "\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 +680,20 @@ function composePrompt(input) {
601
680
  sections.push(renderFindingsBlockInstruction(input.locale));
602
681
  return sections.join("\n\n---\n\n");
603
682
  }
683
+ function renderDeniedToolsStep() {
684
+ return [
685
+ "- name: Show denied tool calls",
686
+ ` if: always() && steps.${CLAUDE_STEP_ID}.outputs.execution_file != ''`,
687
+ " env:",
688
+ ` EXECUTION_FILE: \${{ steps.${CLAUDE_STEP_ID}.outputs.execution_file }}`,
689
+ " run: |",
690
+ ' [ -f "$EXECUTION_FILE" ] || exit 0',
691
+ ` stop_token="archweave-$(od -An -N16 -tx1 /dev/urandom | tr -d ' \\n')"`,
692
+ ' echo "::stop-commands::$stop_token"',
693
+ ` jq -c '.[] | select(.type == "result") | .permission_denials[]? | {tool: .tool_name, command: ((.tool_input.command // "") | tostring | .[0:200])}' "$EXECUTION_FILE" || echo "Could not read the execution file."`,
694
+ ' echo "::$stop_token::"'
695
+ ].join("\n");
696
+ }
604
697
  function buildWorkflowYaml(input) {
605
698
  return renderReviewWorkflow(input, {
606
699
  name: "Claude Code Review",
@@ -611,6 +704,7 @@ function buildWorkflowYaml(input) {
611
704
  renderCheckoutStep(REVIEW_GATE_CONDITION),
612
705
  [
613
706
  "- name: Run Claude review",
707
+ ` id: ${CLAUDE_STEP_ID}`,
614
708
  ` if: ${REVIEW_GATE_CONDITION}`,
615
709
  ` uses: ${pinnedUses("anthropics/claude-code-action")}`,
616
710
  " with:",
@@ -619,7 +713,8 @@ function buildWorkflowYaml(input) {
619
713
  indentBlock(composePrompt(input), 6),
620
714
  " claude_args: |",
621
715
  ` --allowedTools "${ALLOWED_TOOLS}"`
622
- ].join("\n")
716
+ ].join("\n"),
717
+ renderDeniedToolsStep()
623
718
  ]
624
719
  });
625
720
  }
@@ -687,7 +782,9 @@ function buildWorkflowYaml2(input) {
687
782
  ` CURSOR_API_KEY: \${{ secrets.${SECRET_NAME2} }}`,
688
783
  " GH_TOKEN: ${{ github.token }}",
689
784
  " run: |",
690
- ` agent -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
785
+ // cursor-agent stops at "Workspace Trust Required" without --trust (2026.10.22 on
786
+ // Actions); the folder is this run's own checkout, and forks never get the secret.
787
+ ` agent --trust -p "$(cat <<'${PROMPT_HEREDOC_DELIMITER}'`,
691
788
  indentBlock(composePrompt2(input), 4),
692
789
  ` ${PROMPT_HEREDOC_DELIMITER}`,
693
790
  ' )"'
@@ -731,6 +828,7 @@ function composePrompt3(input) {
731
828
  sections.push(renderFindingsBlockInstruction(input.locale));
732
829
  return sections.join("\n\n---\n\n");
733
830
  }
831
+ var GEMINI_CLI_VERSION = "0.65.0";
734
832
  function buildWorkflowYaml3(input) {
735
833
  return renderReviewWorkflow(input, {
736
834
  name: "Gemini Code Review",
@@ -745,8 +843,13 @@ function buildWorkflowYaml3(input) {
745
843
  ` uses: ${pinnedUses("google-github-actions/run-gemini-cli")}`,
746
844
  " env:",
747
845
  " GITHUB_TOKEN: ${{ github.token }}",
846
+ // Gemini CLI 0.65 refuses to run in a folder it hasn't been told to trust (measured
847
+ // 2026-10-25 on Actions). The folder is this run's own checkout; pull requests from forks
848
+ // never get here, because they get no secret.
849
+ " GEMINI_CLI_TRUST_WORKSPACE: 'true'",
748
850
  " with:",
749
851
  ` gemini_api_key: \${{ secrets.${SECRET_NAME3} }}`,
852
+ ` gemini_cli_version: '${GEMINI_CLI_VERSION}'`,
750
853
  " prompt: |",
751
854
  indentBlock(composePrompt3(input), 6)
752
855
  ].join("\n")
@@ -919,7 +1022,8 @@ var REVIEW_CHECK_IDS = [
919
1022
  "review-alt-file-coexistence",
920
1023
  "review-config-consistency",
921
1024
  "review-workflow-providers",
922
- "review-workflow-pnpm"
1025
+ "review-workflow-pnpm",
1026
+ "review-workflow-fail-open"
923
1027
  ];
924
1028
  var WORKFLOWS_DIR = ".github/workflows";
925
1029
  function archweaveWorkflows(root) {
@@ -976,6 +1080,25 @@ function buildReviewPnpmCheck(root, workflows) {
976
1080
  suggestedAction: "Regenerate the workflow: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite."
977
1081
  });
978
1082
  }
1083
+ var FAIL_CLOSED_VERDICT_MODE = "ARCHWEAVE_VERDICT_OUTPUT: 'github-output'";
1084
+ function buildReviewFailOpenCheck(root, workflows) {
1085
+ const id = "review-workflow-fail-open";
1086
+ const label = "Review check fails closed";
1087
+ if (workflows.length === 0)
1088
+ return createDoctorCheck({ id, label, status: "not-applicable", message: "no nx-archweave review workflow", evidence: [] });
1089
+ const failOpen = workflows.filter((path) => !readFileSync2(join2(root, path), "utf-8").includes(FAIL_CLOSED_VERDICT_MODE));
1090
+ if (failOpen.length === 0) {
1091
+ return createDoctorCheck({ id, label, status: "pass", message: "a review run that leaves no readable summary fails the review check", evidence: workflows });
1092
+ }
1093
+ return createDoctorCheck({
1094
+ id,
1095
+ label,
1096
+ status: "error",
1097
+ 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",
1098
+ evidence: failOpen,
1099
+ suggestedAction: "Re-run add-review: nx g @clearlydev/nx-archweave:add-review --onConflict=overwrite. The report shows the difference before anything is replaced."
1100
+ });
1101
+ }
979
1102
  function buildReviewChecks(root) {
980
1103
  const config = readToolkitConfigState(root);
981
1104
  const mainWorkflowPath = REVIEW_PROVIDERS[resolveProviderId(config)].workflowFileName;
@@ -989,7 +1112,8 @@ function buildReviewChecks(root) {
989
1112
  buildReviewAltFileCoexistenceCheck(mainWorkflowPath, altWorkflowPath, mainExists, altExists),
990
1113
  buildReviewConfigConsistencyCheck(root, mainWorkflowPath, mainExists, config),
991
1114
  buildReviewProvidersCheck(workflows),
992
- buildReviewPnpmCheck(root, workflows)
1115
+ buildReviewPnpmCheck(root, workflows),
1116
+ buildReviewFailOpenCheck(root, workflows)
993
1117
  ];
994
1118
  }
995
1119
  function buildReviewWorkflowPresentCheck(mainWorkflowPath, mainExists, mainIsOurs, config) {
@@ -1228,15 +1352,22 @@ var BACKEND_CHECK_IDS = [
1228
1352
  "backend-config-consistency",
1229
1353
  "shared-types-lib",
1230
1354
  "boundary-constraints-applied",
1231
- "boundary-tags-exist"
1355
+ "boundary-tags-exist",
1356
+ "boundary-untagged-projects"
1232
1357
  ];
1233
1358
  var CHECK_LABELS = {
1234
1359
  "backend-framework": "Backend framework (root package.json only)",
1235
1360
  "backend-config-consistency": "Backend config consistency",
1236
1361
  "shared-types-lib": "shared-types library",
1237
1362
  "boundary-constraints-applied": "Module-boundary constraints",
1238
- "boundary-tags-exist": "Boundary tags exist"
1363
+ "boundary-tags-exist": "Boundary tags exist",
1364
+ "boundary-untagged-projects": "Projects without a boundary tag"
1239
1365
  };
1366
+ var UNTAGGED_LISTED = 10;
1367
+ function isEndToEndRoot(projectRoot) {
1368
+ const base = projectRoot.split("/").pop() ?? "";
1369
+ return base === "e2e" || base.endsWith("-e2e");
1370
+ }
1240
1371
  function collectScopeTags(root, projects) {
1241
1372
  if (projects !== void 0)
1242
1373
  return new Set(projects.flatMap((project) => project.tags));
@@ -1393,13 +1524,15 @@ function buildBackendChecks(root, projects) {
1393
1524
  suggestedAction: classification === "enforced" ? void 0 : "Run add-backend with --applyEslint, or merge its snippet yourself."
1394
1525
  }));
1395
1526
  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
- }));
1527
+ for (const id of ["boundary-tags-exist", "boundary-untagged-projects"]) {
1528
+ checks.push(createDoctorCheck({
1529
+ id,
1530
+ label: CHECK_LABELS[id],
1531
+ status: "not-applicable",
1532
+ message: "Skipped \u2014 boundary constraints are not enforced (see boundary-constraints-applied)",
1533
+ evidence: []
1534
+ }));
1535
+ }
1403
1536
  } else {
1404
1537
  const tags = collectScopeTags(root, projects);
1405
1538
  const required = readsBackendOnly(root) ? [boundaryTags.backend] : [boundaryTags.frontend, boundaryTags.backend];
@@ -1412,9 +1545,36 @@ function buildBackendChecks(root, projects) {
1412
1545
  evidence: missing.length === 0 ? [] : missing,
1413
1546
  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
1547
  }));
1548
+ checks.push(buildUntaggedProjectsCheck(projects, [boundaryTags.frontend, boundaryTags.backend, boundaryTags.shared]));
1415
1549
  }
1416
1550
  return checks;
1417
1551
  }
1552
+ function buildUntaggedProjectsCheck(projects, roleTags) {
1553
+ const id = "boundary-untagged-projects";
1554
+ if (projects === void 0) {
1555
+ return createDoctorCheck({
1556
+ id,
1557
+ label: CHECK_LABELS[id],
1558
+ status: "not-applicable",
1559
+ message: "Skipped \u2014 needs the workspace's Nx project list (run doctor through nx g)",
1560
+ evidence: []
1561
+ });
1562
+ }
1563
+ 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);
1564
+ if (untagged.length === 0) {
1565
+ return createDoctorCheck({ id, label: CHECK_LABELS[id], status: "pass", message: `every project carries one of ${roleTags.join(", ")}`, evidence: [] });
1566
+ }
1567
+ const listed = untagged.slice(0, UNTAGGED_LISTED).join(", ");
1568
+ const more = untagged.length > UNTAGGED_LISTED ? ` and ${untagged.length - UNTAGGED_LISTED} more` : "";
1569
+ return createDoctorCheck({
1570
+ id,
1571
+ label: CHECK_LABELS[id],
1572
+ status: "warning",
1573
+ message: `${untagged.length} project(s) carry none of ${roleTags.join(", ")}, so the frontend/backend deny rules don't cover them: ${listed}${more}`,
1574
+ evidence: untagged,
1575
+ 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]}.`
1576
+ });
1577
+ }
1418
1578
 
1419
1579
  // ../template-engine/dist/doctor/fsd-like-check.js
1420
1580
  import { existsSync as existsSync4, readdirSync as readdirSync3 } from "node:fs";
@@ -1665,7 +1825,7 @@ var NOT_APPLICABLE_IDS = [
1665
1825
  ...REVIEW_CHECK_IDS,
1666
1826
  ...BACKEND_CHECK_IDS
1667
1827
  ];
1668
- function runDoctorChecks(root, projects) {
1828
+ function runDoctorChecks(root, projects, nxSupport) {
1669
1829
  const env = detectEnvironment(root);
1670
1830
  if (!env.hasNxWorkspace) {
1671
1831
  const checks2 = [
@@ -1695,7 +1855,7 @@ function runDoctorChecks(root, projects) {
1695
1855
  message: "nx.json found",
1696
1856
  evidence: ["nx.json"]
1697
1857
  }),
1698
- buildNxVersionCheck(env),
1858
+ buildNxVersionCheck(env, nxSupport),
1699
1859
  buildFsdLikeCheck(root, projects),
1700
1860
  buildFsdEnforcementCheck(root, projects),
1701
1861
  buildFsdRuleAnchoredCheck(root, projects),
@@ -1724,12 +1884,25 @@ function runDoctorChecks(root, projects) {
1724
1884
  const overallStatus = computeOverallStatus(checks);
1725
1885
  return { checks, overallStatus };
1726
1886
  }
1727
- var VALIDATED_NX_MAJOR = 23;
1887
+ var VALIDATED_NX_MAJORS = [23, 24];
1888
+ var VALIDATED_RANGE_TEXT = `${VALIDATED_NX_MAJORS[0]}.x\u2013${VALIDATED_NX_MAJORS[VALIDATED_NX_MAJORS.length - 1]}.x`;
1728
1889
  function extractMajor(version) {
1729
1890
  const match = /(\d+)/.exec(version);
1730
1891
  return match ? Number(match[1]) : null;
1731
1892
  }
1732
- function buildNxVersionCheck(env) {
1893
+ function buildNxVersionCheck(env, nxSupport) {
1894
+ if (nxSupport?.inRange === false && nxSupport.installedVersion !== void 0) {
1895
+ const major2 = extractMajor(nxSupport.installedVersion);
1896
+ const supported = `${nxSupport.supportedMajors.min}.x\u2013${nxSupport.supportedMajors.max}.x`;
1897
+ return createDoctorCheck({
1898
+ id: "nx-version",
1899
+ label: "Nx version",
1900
+ status: "error",
1901
+ 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`,
1902
+ evidence: [`nx@${nxSupport.installedVersion}`, `@nx/devkit@${nxSupport.devkitVersion}`],
1903
+ suggestedAction: `Install the @clearlydev/nx-archweave release built for Nx ${major2 ?? "this"}${major2 === null ? "" : ".x"}, or keep nx at ${supported}.`
1904
+ });
1905
+ }
1733
1906
  if (env.rootPackageJsonError) {
1734
1907
  return createDoctorCheck({
1735
1908
  id: "nx-version",
@@ -1759,30 +1932,30 @@ function buildNxVersionCheck(env) {
1759
1932
  evidence: [`nx@${env.nxVersion}`]
1760
1933
  });
1761
1934
  }
1762
- if (major === VALIDATED_NX_MAJOR) {
1935
+ if (VALIDATED_NX_MAJORS.includes(major)) {
1763
1936
  return createDoctorCheck({
1764
1937
  id: "nx-version",
1765
1938
  label: "Nx version",
1766
1939
  status: "pass",
1767
- message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${VALIDATED_NX_MAJOR}.x)`,
1940
+ message: `Nx ${env.nxVersion} \u2014 within the toolkit-validated range (${VALIDATED_RANGE_TEXT})`,
1768
1941
  evidence: [`nx@${env.nxVersion}`]
1769
1942
  });
1770
1943
  }
1771
- if (major < VALIDATED_NX_MAJOR) {
1944
+ if (major < VALIDATED_NX_MAJORS[0]) {
1772
1945
  return createDoctorCheck({
1773
1946
  id: "nx-version",
1774
1947
  label: "Nx version",
1775
1948
  status: "warning",
1776
- message: `Nx ${env.nxVersion} is below the toolkit-validated range (${VALIDATED_NX_MAJOR}.x) \u2014 generator behavior is not guaranteed`,
1949
+ message: `Nx ${env.nxVersion} is below the toolkit-validated range (${VALIDATED_RANGE_TEXT}) \u2014 generator behavior is not guaranteed`,
1777
1950
  evidence: [`nx@${env.nxVersion}`],
1778
- suggestedAction: `Upgrade the workspace to Nx ${VALIDATED_NX_MAJOR}.x before running the generators.`
1951
+ suggestedAction: `Upgrade the workspace to Nx ${VALIDATED_NX_MAJORS[VALIDATED_NX_MAJORS.length - 1]}.x before running the generators.`
1779
1952
  });
1780
1953
  }
1781
1954
  return createDoctorCheck({
1782
1955
  id: "nx-version",
1783
1956
  label: "Nx version",
1784
1957
  status: "warning",
1785
- message: `Nx ${env.nxVersion} is above the toolkit-validated range (${VALIDATED_NX_MAJOR}.x) \u2014 may work, unvalidated`,
1958
+ message: `Nx ${env.nxVersion} is above the toolkit-validated range (${VALIDATED_RANGE_TEXT}) \u2014 may work, unvalidated`,
1786
1959
  evidence: [`nx@${env.nxVersion}`],
1787
1960
  suggestedAction: "Proceed with care and report issues; validation for newer majors lands with toolkit updates."
1788
1961
  });
@@ -2714,7 +2887,7 @@ function diskRunNx(root) {
2714
2887
  var DOCTOR_TARGET_ENVIRONMENT = "claude-code";
2715
2888
  async function buildDoctorReportWithIntegrations(root, options = {}, deps = {}) {
2716
2889
  const integrationsRoot = deps.integrationsRoot ?? createIntegrationsCompositionRoot();
2717
- const baseReport = runDoctorChecks(root, options.projects);
2890
+ const baseReport = runDoctorChecks(root, options.projects, options.nxSupport);
2718
2891
  const declaredIds = readEnabledIntegrationIds(root);
2719
2892
  const runNx = diskRunNx(root);
2720
2893
  const driftResults = await detectIntegrationsDrift({
@@ -3163,11 +3336,49 @@ function buildPolicySuggestion(tree, sourceRoot, current) {
3163
3336
  });
3164
3337
  }
3165
3338
 
3339
+ // src/generators/doctor/nx-support.ts
3340
+ import { readFileSync as readFileSync9 } from "node:fs";
3341
+ import { createRequire } from "node:module";
3342
+ import { join as join12 } from "node:path";
3343
+ var MAJORS_EITHER_SIDE = 1;
3344
+ function majorOf(version) {
3345
+ const match = /^(\d+)\./.exec(version.trim());
3346
+ return match ? Number(match[1]) : void 0;
3347
+ }
3348
+ function readOwnDevkitVersion() {
3349
+ try {
3350
+ const path = createRequire(import.meta.url).resolve("@nx/devkit/package.json");
3351
+ const version = JSON.parse(readFileSync9(path, "utf-8")).version;
3352
+ return typeof version === "string" ? version : void 0;
3353
+ } catch {
3354
+ return void 0;
3355
+ }
3356
+ }
3357
+ function readInstalledNx(root) {
3358
+ try {
3359
+ const version = JSON.parse(readFileSync9(join12(root, "node_modules", "nx", "package.json"), "utf-8")).version;
3360
+ return typeof version === "string" ? version : void 0;
3361
+ } catch {
3362
+ return void 0;
3363
+ }
3364
+ }
3365
+ function readNxSupport(root, deps = {}) {
3366
+ const devkitVersion = (deps.devkitVersion ?? readOwnDevkitVersion)();
3367
+ const devkitMajor = devkitVersion === void 0 ? void 0 : majorOf(devkitVersion);
3368
+ if (devkitVersion === void 0 || devkitMajor === void 0) return void 0;
3369
+ const supportedMajors = { min: devkitMajor - MAJORS_EITHER_SIDE, max: devkitMajor + MAJORS_EITHER_SIDE };
3370
+ const installedVersion = (deps.installedNx ?? readInstalledNx)(root);
3371
+ const installedMajor = installedVersion === void 0 ? void 0 : majorOf(installedVersion);
3372
+ const inRange = installedMajor === void 0 ? void 0 : installedMajor >= supportedMajors.min && installedMajor <= supportedMajors.max;
3373
+ return { devkitVersion, supportedMajors, installedVersion, inRange };
3374
+ }
3375
+
3166
3376
  // src/generators/doctor/generator.ts
3167
3377
  async function doctorGenerator(tree, options) {
3168
3378
  const base = await buildDoctorReportWithIntegrations(tree.root, {
3169
3379
  liveIntegrations: options.liveIntegrations ?? false,
3170
- projects: doctorProjects(tree)
3380
+ projects: doctorProjects(tree),
3381
+ nxSupport: readNxSupport(tree.root)
3171
3382
  });
3172
3383
  const checks = base.overallStatus === "unsupported" ? base.checks : [...base.checks, ...buildWorkspaceChecks(tree)];
3173
3384
  const report = { checks, overallStatus: base.overallStatus === "unsupported" ? base.overallStatus : computeOverallStatus(checks) };