Skip to content

fix(intent): review stale skills against artifact coverage#120

Merged
LadyBluenotes merged 16 commits intomainfrom
bug-fix
Apr 23, 2026
Merged

fix(intent): review stale skills against artifact coverage#120
LadyBluenotes merged 16 commits intomainfrom
bug-fix

Conversation

@LadyBluenotes
Copy link
Copy Markdown
Member

@LadyBluenotes LadyBluenotes commented Apr 23, 2026

🎯 Changes

  • Remove the notify-intent workflow path and keep stale review in the same repo.
  • Harden check-skills.yml so release/manual runs open or update one review PR for stale skills, artifact drift, and workspace coverage gaps.
  • Add stale signals[] for artifact parse warnings, unresolved artifact skill paths, source drift, library version drift, and missing workspace package coverage.
  • Parse repo-root _artifacts/*domain_map.yaml and _artifacts/*skill_tree.yaml as maintainer-reviewed coverage.
  • Detect public workspace packages that are not covered by generated skills or _artifacts, while respecting coverage.ignored_packages.
  • Fix workspace-root stale resolution so root skills/ does not shadow package-local generated skills.
  • Skip private workspace packages when checking missing package coverage.
  • Add workflow version advisory so maintainers know when to rerun npx @tanstack/intent@latest setup.

Summary by CodeRabbit

  • New Features
    • Improved intent stale command for monorepos with repository artifact coverage evaluation and warnings for uncovered public packages; private workspaces are now skipped
    • Enhanced PR workflow with single grouped review containing maintainer instructions
    • Workflow version tracking with update notifications

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Warning

Rate limit exceeded

@LadyBluenotes has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 28 minutes and 42 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 28 minutes and 42 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 896d5036-a5d0-4426-9411-984196fa3fba

📥 Commits

Reviewing files that changed from the base of the PR and between 54f8c23 and f655d04.

📒 Files selected for processing (16)
  • docs/cli/intent-scaffold.md
  • docs/cli/intent-setup.md
  • docs/cli/intent-stale.md
  • docs/config.json
  • docs/getting-started/quick-start-maintainers.md
  • docs/registry.md
  • packages/intent/README.md
  • packages/intent/meta/templates/workflows/check-skills.yml
  • packages/intent/src/cli-support.ts
  • packages/intent/src/cli.ts
  • packages/intent/src/commands/stale.ts
  • packages/intent/src/staleness.ts
  • packages/intent/tests/cli.test.ts
  • packages/intent/tests/stale-command.test.ts
  • packages/intent/tests/workflow-review.test.ts
  • scripts/create-github-release.mjs
📝 Walkthrough

Walkthrough

Introduces artifact-based coverage tracking and signals generation to the Intent system. Adds a new artifact-coverage module to read and validate YAML artifacts from _artifacts directories. Extends staleness reporting with signal generation for coverage gaps and artifact inconsistencies. Implements a new workflow review system that aggregates stale skills and signals into structured review items and generates PR bodies for maintainer action.

Changes

Cohort / File(s) Summary
Artifact Coverage System
packages/intent/src/artifact-coverage.ts, packages/intent/tests/artifact-coverage.test.ts
New module reads _artifacts/ YAML files, validates structure, parses skills and coverage ignores, tracks warnings, and returns normalized artifact metadata set.
Staleness & Signal Generation
packages/intent/src/staleness.ts, packages/intent/tests/staleness.test.ts
Extends checkStaleness to read artifacts and emit signals for parse warnings, missing/mismatched metadata, and version drift. Adds buildWorkspaceCoverageSignals to flag non-private packages lacking skill or artifact coverage. Exports readPackageName utility.
Workflow Review System
packages/intent/src/workflow-review.ts, packages/intent/tests/workflow-review.test.ts
New module transforms staleness reports into structured review items and builds markdown PR body with counts, grouped types, and maintainer instructions. Provides helpers for stale-check failure items.
CLI Support & Advisories
packages/intent/src/cli-support.ts, packages/intent/tests/cli.test.ts, packages/intent/tests/stale-command.test.ts
Updates resolveStaleTargets to return StaleTargetResult with workflow advisories. Adds getCheckSkillsWorkflowAdvisories to check workflow version stamps. Integrates workspace coverage signals and artifact reading.
Stale Command Updates
packages/intent/src/commands/stale.ts
Modified to output workflow advisories and render both stale skills and coverage signals with fallback subject derivation logic.
Workflow Templates
packages/intent/meta/templates/workflows/check-skills.yml, packages/intent/meta/templates/workflows/notify-intent.yml
Rewrote check-skills.yml to run intent stale --json, collect review items, and open/update a single grouped PR with grouped branch strategy. Deleted notify-intent.yml workflow entirely.
Type Definitions & Exports
packages/intent/src/types.ts, packages/intent/src/index.ts
Added StalenessSignal and artifact-related interfaces (IntentArtifactSet, IntentArtifactFile, IntentArtifactSkill, IntentArtifactCoverageIgnore, IntentArtifactWarning). Exported new functions and types for workflow review and artifact coverage.
Workspace Patterns
packages/intent/src/workspace-patterns.ts, packages/intent/tests/workspace-patterns.test.ts
Added findWorkspacePackages helper to resolve all workspace package directories independent of skill presence.
Test Infrastructure
packages/intent/tests/setup.test.ts
Updated test setup to provision check-skills.yml and validate-skills.yml instead of notify-intent.yml. Expanded assertions for workflow content including signals and reviewer prompts.
Release Tagging
scripts/create-github-release.mjs
Modified createReleaseTag to accept changedPackages and generate v<version> tags for single-version releases instead of time-based tags. Multi-version releases include package list in title.
Changeset Documentation
.changeset/ten-shrimps-bow.md
Patch-level changeset documenting improvements to intent stale for monorepos and workflow-based review PR generation.

Sequence Diagram

sequenceDiagram
    actor GitHub as GitHub Actions
    participant Workflow as check-skills.yml
    participant CLI as intent stale
    participant Artifacts as _artifacts/
    participant Review as workflow-review
    participant API as GitHub API

    GitHub->>Workflow: Trigger (on schedule/push)
    Workflow->>CLI: Run intent stale --json
    CLI->>Artifacts: readIntentArtifacts()
    Artifacts-->>CLI: Parse YAML artifacts
    CLI->>CLI: buildWorkspaceCoverageSignals()
    CLI-->>Workflow: JSON report + signals
    Workflow->>Review: collectStaleReviewItems()
    Review->>Review: Merge skills + signals
    Review-->>Workflow: Structured items array
    Workflow->>Review: buildStaleReviewBody()
    Review-->>Workflow: Markdown PR body
    Workflow->>API: Check for existing PR
    alt PR exists on branch
        API-->>Workflow: Found PR
        Workflow->>API: Update PR body
    else No PR found
        Workflow->>API: Create new PR
    end
    API-->>GitHub: PR created/updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Artifacts bloom in _artifacts bright,
Signals dance through coverage's light,
Stale skills grouped in review's embrace,
One PR per branch—a organized place!
Workflows now speak with wisdom and care,
hop hop Intent goes everywhere! 🌱

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title directly describes the main change: adding stale skill review against artifact coverage, which aligns with the primary objective of hardening stale review.
Description check ✅ Passed The PR description fully addresses the template requirements: it provides a detailed "🎯 Changes" section covering all major modifications, and includes a completed checklist confirming contributor guidelines compliance and testing.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bug-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 23, 2026

View your CI Pipeline Execution ↗ for commit f655d04

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 3s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-23 23:24:53 UTC

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Apr 23, 2026

View your CI Pipeline Execution ↗ for commit 74951e4

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-04-23 22:52:56 UTC

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@tanstack/intent@120

commit: f655d04

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 23, 2026

Merging this PR will not alter performance

✅ 3 untouched benchmarks


Comparing bug-fix (f655d04) with main (ff87349)1

Open in CodSpeed

Footnotes

  1. No successful run was found on main (68e076d) during the generation of this report, so ff87349 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (7)
packages/intent/tests/cli.test.ts (1)

1527-1568: Defensive: mock fetch here too.

Unlike the other four new tests in this block, this one omits the fetchSpy mock. It currently works because no package has skills and the staleness pipeline skips the npm probe, but that is an implementation detail. If a future change starts reading library.name/library.version from _artifacts to resolve the current version, this test will hit the real npm registry and become slow/flaky in CI.

🧪 Suggested defensive mock
     writeFileSync(
       join(root, '_artifacts', 'skill_tree.yaml'),
       [
         'library:',
         "  name: '@tanstack/router'",
         "  version: '1.0.0'",
         'skills: []',
       ].join('\n'),
     )

+    const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
+      ok: true,
+      json: async () => ({ version: '1.0.0' }),
+    } as Response)
+
     process.chdir(root)
@@
     expect(reports[0]?.signals).toEqual([
       expect.objectContaining({
         type: 'missing-package-coverage',
         packageName: '@tanstack/react-start-rsc',
       }),
     ])
+
+    fetchSpy.mockRestore()
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/tests/cli.test.ts` around lines 1527 - 1568, This test is
missing the defensive npm network mock used by the other staleness tests; add
the same fetch mock setup/teardown (e.g., create a fetchSpy that stubs
global.fetch and returns a resolved Response) before calling main(['stale',
'--json']) and restore it afterward so the test doesn't hit the real npm
registry if the code starts resolving library.name/library.version; locate the
pattern used in the sibling tests in packages/intent/tests/cli.test.ts and apply
the same mocking around this specific it(...) block.
packages/intent/tests/stale-command.test.ts (1)

101-113: Import the workflow-version constant instead of hardcoding version numbers.

The test assertions use hardcoded values 1 (old) and 2 (current), which are tied to the exported INTENT_CHECK_SKILLS_WORKFLOW_VERSION constant. When that constant is bumped, the "current version" test will silently fail (since it checks for an exact value) while the "old version" test can pass for the wrong reason (any value below current still reads as old). Importing the constant makes the tests resilient to future bumps and documents the intent explicitly.

🔧 Suggested change
-import { getCheckSkillsWorkflowAdvisories } from '../src/cli-support.js'
+import {
+  INTENT_CHECK_SKILLS_WORKFLOW_VERSION,
+  getCheckSkillsWorkflowAdvisories,
+} from '../src/cli-support.js'
   it('advises when the workflow has an old intent version stamp', () => {
-    const root = writeWorkflow('# intent-workflow-version: 1\n')
+    const root = writeWorkflow(
+      `# intent-workflow-version: ${INTENT_CHECK_SKILLS_WORKFLOW_VERSION - 1}\n`,
+    )
@@
   it('does not advise when the workflow has the current version stamp', () => {
-    const root = writeWorkflow('# intent-workflow-version: 2\n')
+    const root = writeWorkflow(
+      `# intent-workflow-version: ${INTENT_CHECK_SKILLS_WORKFLOW_VERSION}\n`,
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/tests/stale-command.test.ts` around lines 101 - 113, Replace
the hardcoded version numbers in the tests with the exported
INTENT_CHECK_SKILLS_WORKFLOW_VERSION constant: import
INTENT_CHECK_SKILLS_WORKFLOW_VERSION at the top of the test file and use
INTENT_CHECK_SKILLS_WORKFLOW_VERSION for the "current version" case and
INTENT_CHECK_SKILLS_WORKFLOW_VERSION - 1 (or Math.max(0, ... - 1) if needed) for
the "old version" case when calling writeWorkflow; keep the existing assertions
that call getCheckSkillsWorkflowAdvisories so behavior remains the same but
resilient to future bumps of the constant.
packages/intent/meta/templates/workflows/check-skills.yml (1)

55-107: Inline Node script duplicates and has already drifted from workflow-review.ts.

This template re-implements collectStaleReviewItems / createFailedStaleReviewItem, but with subtle divergences from the exported helpers in packages/intent/src/workflow-review.ts:

  • Line 87-92 omits signal?.subject from the subject fallback chain (present at workflow-review.ts:40). Any signal that populates only subject (e.g. the artifact-parse-warning signal whose subject is warning.artifactPath) will fall through to report.library here even though the CLI and the helper would show the artifact path.
  • Line 93 references signal?.message, but StalenessSignal (see packages/intent/src/types.ts:89-99) has no message field — this fallback is dead and misleading.
  • Lines 55-64 hardcode the same shape that createFailedStaleReviewItem already returns.

Since @tanstack/intent is installed globally on line 40, the workflow can call the exported helpers directly and remove the drift risk entirely:

♻️ Sketch of the refactor
           if [ "$STATUS" -ne 0 ]; then
             echo "has_review=true" >> "$GITHUB_OUTPUT"
             echo "check_failed=true" >> "$GITHUB_OUTPUT"
-            cat > review-items.json <<'JSON'
-          [
-            {
-              "type": "stale-check-failed",
-              "library": "{{PACKAGE_LABEL}}",
-              "subject": "intent stale --json",
-              "reasons": ["The stale check command failed. Review the workflow logs before updating skills."]
-            }
-          ]
-          JSON
+            node <<'NODE'
+          const fs = require('fs')
+          const { createFailedStaleReviewItem } = require('@tanstack/intent')
+          fs.writeFileSync(
+            'review-items.json',
+            JSON.stringify([createFailedStaleReviewItem('{{PACKAGE_LABEL}}')], null, 2) + '\n',
+          )
+          NODE
           else
             node <<'NODE'
           const fs = require('fs')
-          const reports = JSON.parse(fs.readFileSync('stale.json', 'utf8'))
-          const items = []
-
-          for (const report of reports) {
-            for (const skill of report.skills ?? []) {
-              /* …inline duplication… */
-            }
-            for (const signal of report.signals ?? []) {
-              /* …inline duplication with drift… */
-            }
-          }
+          const { collectStaleReviewItems } = require('@tanstack/intent')
+          const reports = JSON.parse(fs.readFileSync('stale.json', 'utf8'))
+          const items = collectStaleReviewItems(reports)
           fs.writeFileSync('review-items.json', JSON.stringify(items, null, 2) + '\n')

The "Build review PR body" step (lines 128-218) can likewise defer to buildStaleReviewBody(items) instead of hand-rolling signalRows/itemRows/prompt.

If keeping the logic inlined is intentional (e.g. to decouple the workflow from the CLI's JS API surface), at minimum please restore the missing signal?.subject fallback on lines 87-92 and drop the dead signal?.message reference on line 93 so the inline version matches the helper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/meta/templates/workflows/check-skills.yml` around lines 55 -
107, The inline Node script duplicates and has drifted from the exported
helpers; either replace the inline logic with calls to the package helpers
(import and call collectStaleReviewItems and createFailedStaleReviewItem from
packages/intent/src/workflow-review.ts and use buildStaleReviewBody for the PR
body) so the workflow defers to the canonical implementations, or if you must
keep the inline script, restore the exact fallbacks from workflow-review.ts by
adding signal?.subject into the subject fallback chain and remove the dead
signal?.message fallback so the reasons use signal?.reasons only; reference the
functions collectStaleReviewItems, createFailedStaleReviewItem and
buildStaleReviewBody to locate the authoritative logic.
packages/intent/src/commands/stale.ts (1)

49-58: Consider centralizing the signal-subject fallback.

The fallback here is subject → packageName → packageRoot → skill → artifactPath → type, but collectStaleReviewItems in packages/intent/src/workflow-review.ts uses packageName → packageRoot → skill → artifactPath → subject → library, and the inline Node script in packages/intent/meta/templates/workflows/check-skills.yml uses yet another order (and drops subject entirely). Three different orderings for the same derivation will inevitably drift. Consider extracting a shared resolveSignalSubject(signal, fallback) helper and reusing it from both stale.ts and workflow-review.ts (and having the workflow template call those helpers, see the separate comment on check-skills.yml).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/commands/stale.ts` around lines 49 - 58, Multiple places
derive a signal "subject" with different fallback orders (the loop in stale.ts
and collectStaleReviewItems in workflow-review.ts plus the inline Node script),
causing inconsistent results; extract a single helper (e.g.,
resolveSignalSubject(signal, fallbackOrder?)) that implements the canonical
fallback order and replace the inline fallback logic in the for loop in stale.ts
and in collectStaleReviewItems to call resolveSignalSubject, and update the
workflow template to call the same helper (or mirror its order) so all consumers
use the same ordering (reference resolveSignalSubject, the for-loop over signals
in stale.ts, and collectStaleReviewItems).
packages/intent/src/cli-support.ts (2)

95-99: Redundant sub-condition on Line 98.

With !isWorkspaceRootTarget now gating entry, resolvedRoot !== context.workspaceRoot is tautological: either workspaceRoot is null (so resolvedRoot !== null trivially holds) or it's non-null and !isWorkspaceRootTarget already implies inequality. The (targetSkillsDir !== null || …) disjunction therefore always evaluates true. The outer condition collapses to context.packageRoot && !isWorkspaceRootTarget.

♻️ Proposed simplification
-  if (
-    context.packageRoot &&
-    !isWorkspaceRootTarget &&
-    (context.targetSkillsDir !== null || resolvedRoot !== context.workspaceRoot)
-  ) {
+  if (context.packageRoot && !isWorkspaceRootTarget) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/cli-support.ts` around lines 95 - 99, The if condition in
the block using context.packageRoot, isWorkspaceRootTarget,
context.targetSkillsDir, resolvedRoot and context.workspaceRoot contains a
redundant disjunction; simplify the guard to just check context.packageRoot &&
!isWorkspaceRootTarget by removing the "(context.targetSkillsDir !== null ||
resolvedRoot !== context.workspaceRoot)" part so the conditional logic is
clearer and equivalent under the current semantics (update the if expression
where these symbols are used).

132-146: Attaching workspace-level coverage signals to reports[0] conflates contexts.

When packageDirsWithSkills is non-empty, missing-package-coverage signals (describing uncovered packages B, C, …) are appended to the first package's report (library A). Consumers grouping signals by the enclosing report's library will misattribute them; callers that iterate report.signals but use report.library for display/headers will mislabel these entries.

Prefer always emitting a dedicated workspace-level synthetic report for coverage signals (or dispatch each signal to a report keyed by signal.library) so per-package reports only carry signals intrinsic to that package.

♻️ Proposed fix — always emit a dedicated workspace report
-    if (coverageSignals.length > 0) {
-      const workspaceReport = reports[0]
-      if (workspaceReport) {
-        workspaceReport.signals.push(...coverageSignals)
-      } else {
-        reports.push({
-          library: relative(process.cwd(), workspaceRoot) || 'workspace',
-          currentVersion: null,
-          skillVersion: null,
-          versionDrift: null,
-          skills: [],
-          signals: coverageSignals,
-        })
-      }
-    }
+    if (coverageSignals.length > 0) {
+      reports.push({
+        library: relative(process.cwd(), workspaceRoot) || 'workspace',
+        currentVersion: null,
+        skillVersion: null,
+        versionDrift: null,
+        skills: [],
+        signals: coverageSignals,
+      })
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/cli-support.ts` around lines 132 - 146, The current logic
appends workspace-level coverageSignals to reports[0] (via
workspaceReport.signals.push(...coverageSignals)) which misattributes workspace
"missing-package-coverage" signals to the first package; instead always create
and push a dedicated synthetic workspace-level report when
coverageSignals.length > 0 (use the same shape used in the else branch) rather
than mutating an existing per-package report, or alternatively distribute each
signal to the correct per-package report keyed by signal.library; update the
code paths in cli-support.ts around coverageSignals, reports and workspaceReport
so workspace-level signals are represented by their own report and not mixed
into package reports (also consider packageDirsWithSkills usage).
packages/intent/src/staleness.ts (1)

178-192: Nit: redundant check on Line 185.

relPackageDir is always a string (relative(...).split(sep).join('/')), so !relPackageDir already covers the empty-string case — the || relPackageDir === '' branch is dead. Minor cleanup only.

♻️ Proposed tidy-up
-  const relPackageDir = relative(artifactRoot, packageDir).split(sep).join('/')
-  if (!relPackageDir || relPackageDir === '') return true
+  const relPackageDir = relative(artifactRoot, packageDir).split(sep).join('/')
+  if (!relPackageDir) return true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/staleness.ts` around lines 178 - 192, In
artifactPackageMatches, remove the redundant relPackageDir === '' check because
relPackageDir is always a string and the existing !relPackageDir branch already
covers the empty-string case; update the conditional that currently reads if
(!relPackageDir || relPackageDir === '') return true to just if (!relPackageDir)
return true so behavior is unchanged but the dead branch is removed (references:
function artifactPackageMatches, variable relPackageDir).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/intent/meta/templates/workflows/check-skills.yml`:
- Around line 241-249: PR_URL is getting set to the literal string "null" when
no PR exists because the jq expression returns null, so the [ -n "$PR_URL" ]
check passes and gh pr edit is called; update the PR_URL assignment that uses gh
pr list/--jq so that it yields an empty string instead of "null" when no PR
exists (e.g., use jq's null-coalescing to emit empty), and keep the existing
conditional that tests PR_URL before calling gh pr edit/create (symbols to
change: PR_URL variable assignment and the gh pr list --head ... --json url --jq
expression).

In `@packages/intent/src/staleness.ts`:
- Around line 283-302: The computed subject (currently set via subject =
artifact.slug ?? artifact.name ?? artifact.path) can be undefined because
IntentArtifactSkill.slug/name/path are optional; change the fallback to a
guaranteed identifier (e.g., artifact.artifactPath) so subject is always a
string before you call findMatchingSkill and before emitting signals in
signals.push; update the subject declaration and any places that emit signals
(reference: subject, artifact.slug, artifact.name, artifact.path,
artifact.artifactPath, findMatchingSkill, and the signals.push block) to use the
new fallback and ensure the emitted signal.subject is never undefined.

In `@scripts/create-github-release.mjs`:
- Around line 207-224: The branch in createReleaseTag that returns a v<version>
tag when versions.length === 1 is dead/untested; either remove that shortcut so
createReleaseTag always produces the time-based tag (using changedPackages and
now-based slices) for consistent release names, or explicitly gate the
v<version> behavior behind a named option (e.g., perPackageRelease flag) and add
unit tests for both paths; update references to changedPackages/versions and any
callers to pass the new option if you choose the gated approach.

---

Nitpick comments:
In `@packages/intent/meta/templates/workflows/check-skills.yml`:
- Around line 55-107: The inline Node script duplicates and has drifted from the
exported helpers; either replace the inline logic with calls to the package
helpers (import and call collectStaleReviewItems and createFailedStaleReviewItem
from packages/intent/src/workflow-review.ts and use buildStaleReviewBody for the
PR body) so the workflow defers to the canonical implementations, or if you must
keep the inline script, restore the exact fallbacks from workflow-review.ts by
adding signal?.subject into the subject fallback chain and remove the dead
signal?.message fallback so the reasons use signal?.reasons only; reference the
functions collectStaleReviewItems, createFailedStaleReviewItem and
buildStaleReviewBody to locate the authoritative logic.

In `@packages/intent/src/cli-support.ts`:
- Around line 95-99: The if condition in the block using context.packageRoot,
isWorkspaceRootTarget, context.targetSkillsDir, resolvedRoot and
context.workspaceRoot contains a redundant disjunction; simplify the guard to
just check context.packageRoot && !isWorkspaceRootTarget by removing the
"(context.targetSkillsDir !== null || resolvedRoot !== context.workspaceRoot)"
part so the conditional logic is clearer and equivalent under the current
semantics (update the if expression where these symbols are used).
- Around line 132-146: The current logic appends workspace-level coverageSignals
to reports[0] (via workspaceReport.signals.push(...coverageSignals)) which
misattributes workspace "missing-package-coverage" signals to the first package;
instead always create and push a dedicated synthetic workspace-level report when
coverageSignals.length > 0 (use the same shape used in the else branch) rather
than mutating an existing per-package report, or alternatively distribute each
signal to the correct per-package report keyed by signal.library; update the
code paths in cli-support.ts around coverageSignals, reports and workspaceReport
so workspace-level signals are represented by their own report and not mixed
into package reports (also consider packageDirsWithSkills usage).

In `@packages/intent/src/commands/stale.ts`:
- Around line 49-58: Multiple places derive a signal "subject" with different
fallback orders (the loop in stale.ts and collectStaleReviewItems in
workflow-review.ts plus the inline Node script), causing inconsistent results;
extract a single helper (e.g., resolveSignalSubject(signal, fallbackOrder?))
that implements the canonical fallback order and replace the inline fallback
logic in the for loop in stale.ts and in collectStaleReviewItems to call
resolveSignalSubject, and update the workflow template to call the same helper
(or mirror its order) so all consumers use the same ordering (reference
resolveSignalSubject, the for-loop over signals in stale.ts, and
collectStaleReviewItems).

In `@packages/intent/src/staleness.ts`:
- Around line 178-192: In artifactPackageMatches, remove the redundant
relPackageDir === '' check because relPackageDir is always a string and the
existing !relPackageDir branch already covers the empty-string case; update the
conditional that currently reads if (!relPackageDir || relPackageDir === '')
return true to just if (!relPackageDir) return true so behavior is unchanged but
the dead branch is removed (references: function artifactPackageMatches,
variable relPackageDir).

In `@packages/intent/tests/cli.test.ts`:
- Around line 1527-1568: This test is missing the defensive npm network mock
used by the other staleness tests; add the same fetch mock setup/teardown (e.g.,
create a fetchSpy that stubs global.fetch and returns a resolved Response)
before calling main(['stale', '--json']) and restore it afterward so the test
doesn't hit the real npm registry if the code starts resolving
library.name/library.version; locate the pattern used in the sibling tests in
packages/intent/tests/cli.test.ts and apply the same mocking around this
specific it(...) block.

In `@packages/intent/tests/stale-command.test.ts`:
- Around line 101-113: Replace the hardcoded version numbers in the tests with
the exported INTENT_CHECK_SKILLS_WORKFLOW_VERSION constant: import
INTENT_CHECK_SKILLS_WORKFLOW_VERSION at the top of the test file and use
INTENT_CHECK_SKILLS_WORKFLOW_VERSION for the "current version" case and
INTENT_CHECK_SKILLS_WORKFLOW_VERSION - 1 (or Math.max(0, ... - 1) if needed) for
the "old version" case when calling writeWorkflow; keep the existing assertions
that call getCheckSkillsWorkflowAdvisories so behavior remains the same but
resilient to future bumps of the constant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb32aed2-5d34-492e-b2db-0fb738b10210

📥 Commits

Reviewing files that changed from the base of the PR and between 68e076d and 54f8c23.

📒 Files selected for processing (20)
  • .changeset/ten-shrimps-bow.md
  • .codex
  • packages/intent/meta/templates/workflows/check-skills.yml
  • packages/intent/meta/templates/workflows/notify-intent.yml
  • packages/intent/src/artifact-coverage.ts
  • packages/intent/src/cli-support.ts
  • packages/intent/src/commands/stale.ts
  • packages/intent/src/index.ts
  • packages/intent/src/staleness.ts
  • packages/intent/src/types.ts
  • packages/intent/src/workflow-review.ts
  • packages/intent/src/workspace-patterns.ts
  • packages/intent/tests/artifact-coverage.test.ts
  • packages/intent/tests/cli.test.ts
  • packages/intent/tests/setup.test.ts
  • packages/intent/tests/stale-command.test.ts
  • packages/intent/tests/staleness.test.ts
  • packages/intent/tests/workflow-review.test.ts
  • packages/intent/tests/workspace-patterns.test.ts
  • scripts/create-github-release.mjs
💤 Files with no reviewable changes (1)
  • packages/intent/meta/templates/workflows/notify-intent.yml

Comment on lines +241 to +249
PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url')"
if [ -n "$PR_URL" ]; then
gh pr edit "$PR_URL" --body-file pr-body.md
else
gh pr create \
--title "Review intent skills (${VERSION})" \
--body-file pr-body.md \
--head "$BRANCH" \
--base "$BASE_BRANCH"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

gh CLI --jq empty array null output behavior .[0].url

💡 Result:

When using gh CLI with --jq '.[0].url' on a command that returns an empty JSON array (e.g., gh pr list --json url --jq '.[0].url' when no PRs match), the output is null. This follows standard jq behavior: accessing .[0] on an empty array [] returns null, as there is no element at index 0. gh CLI uses an embedded jq implementation (gojq), which matches standard jq for this case. Official documentation confirms --jq processes JSON output using jq syntax (https://cli.github.com/manual/gh_help_formatting). Past bugs with empty arrays producing null unexpectedly (e.g., [] | unique) were fixed by updating gojq, but direct indexing like .[0] correctly yields null. To safely get the first URL or empty output, use: --jq '.[0].url // empty' or check length first: --jq 'if length > 0 then .[0].url else empty end'.

Citations:


PR_URL becomes the literal string "null" when no PR exists, causing gh pr edit null to fail.

When gh pr list --head "$BRANCH" --json url --jq '.[0].url' finds no matching PRs, it returns an empty JSON array. The jq expression .[0].url on an empty array evaluates to jq's null value, which the gh CLI (via its embedded jq implementation) outputs as the literal string null. This causes PR_URL="null" (a non-empty string), making the condition [ -n "$PR_URL" ] true. The workflow then attempts gh pr edit null --body-file pr-body.md, which fails instead of falling through to create a new PR.

Use --jq '.[0].url // empty' to output an empty string when no PR exists:

-          PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url')"
+          PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url // empty')"
           if [ -n "$PR_URL" ]; then
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url')"
if [ -n "$PR_URL" ]; then
gh pr edit "$PR_URL" --body-file pr-body.md
else
gh pr create \
--title "Review intent skills (${VERSION})" \
--body-file pr-body.md \
--head "$BRANCH" \
--base "$BASE_BRANCH"
PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url // empty')"
if [ -n "$PR_URL" ]; then
gh pr edit "$PR_URL" --body-file pr-body.md
else
gh pr create \
--title "Review intent skills (${VERSION})" \
--body-file pr-body.md \
--head "$BRANCH" \
--base "$BASE_BRANCH"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/meta/templates/workflows/check-skills.yml` around lines 241 -
249, PR_URL is getting set to the literal string "null" when no PR exists
because the jq expression returns null, so the [ -n "$PR_URL" ] check passes and
gh pr edit is called; update the PR_URL assignment that uses gh pr list/--jq so
that it yields an empty string instead of "null" when no PR exists (e.g., use
jq's null-coalescing to emit empty), and keep the existing conditional that
tests PR_URL before calling gh pr edit/create (symbols to change: PR_URL
variable assignment and the gh pr list --head ... --json url --jq expression).

Comment on lines +283 to +302
const subject = artifact.slug ?? artifact.name ?? artifact.path
const matchingSkill = findMatchingSkill(
artifact,
skillMetas,
packageDir,
artifactRoot,
)

if (artifact.path && !matchingSkill) {
signals.push({
type: 'artifact-skill-missing',
library,
subject,
reasons: [
`artifact skill path does not resolve to a generated SKILL.md (${artifact.path})`,
],
needsReview: true,
artifactPath: artifact.artifactPath,
skill: artifact.slug ?? artifact.name,
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

subject can be undefined when artifact lacks slug, name, and path.

IntentArtifactSkill.slug/name/path are all optional (per types.ts), so artifact.slug ?? artifact.name ?? artifact.path can resolve to undefined and be embedded in emitted signals. Downstream renderers/groupers that assume a string subject may produce "undefined" output or mis-key aggregates. Add a safe fallback (e.g. artifactPath) so every signal has an identifiable subject.

🛡️ Proposed fallback
-    const subject = artifact.slug ?? artifact.name ?? artifact.path
+    const subject =
+      artifact.slug ?? artifact.name ?? artifact.path ?? artifact.artifactPath
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/intent/src/staleness.ts` around lines 283 - 302, The computed
subject (currently set via subject = artifact.slug ?? artifact.name ??
artifact.path) can be undefined because IntentArtifactSkill.slug/name/path are
optional; change the fallback to a guaranteed identifier (e.g.,
artifact.artifactPath) so subject is always a string before you call
findMatchingSkill and before emitting signals in signals.push; update the
subject declaration and any places that emit signals (reference: subject,
artifact.slug, artifact.name, artifact.path, artifact.artifactPath,
findMatchingSkill, and the signals.push block) to use the new fallback and
ensure the emitted signal.subject is never undefined.

Comment on lines +207 to 224
function createReleaseTag(changedPackages) {
const versions = [...new Set(changedPackages.map((pkg) => pkg.version))]
if (versions.length === 1) {
const version = versions[0]
return {
tag: `v${version}`,
title: `v${version}`,
}
}

const now = new Date().toISOString()
const tag = `release-${now.slice(0, 10)}-${now.slice(11, 13)}${now.slice(14, 16)}`
const title = `Release ${now.slice(0, 10)} ${now.slice(11, 16)}`
const title = `Release ${changedPackages
.map((pkg) => `${pkg.name}@${pkg.version}`)
.join(', ')}`

return { tag, title }
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

TanStack release tag convention v<version> vs per-package

💡 Result:

TanStack uses a monorepo structure with multiple packages (e.g., @tanstack/react-query, @tanstack/vue-query). They use Changesets for version management, where contributors add changeset files describing changes and bump types (major/minor/patch). A "Version Packages" PR is automatically created/updates package.json versions and changelogs based on changesets. When merged, packages are published to npm automatically. GitHub tags are created per-package in the format @tanstack/package-name@Version (e.g., @tanstack/vue-query@5.96.1, @tanstack/solid-query@5.96.1), not a single v tag for the monorepo. GitHub Releases use descriptive names like release-2026-04-11-1610 listing all updated packages and their versions (e.g., @tanstack/react-query@5.98.0). This per-package tagging supports independent-yet-coordinated versioning via Changesets fixed groups/internal deps (e.g., adapters bump with core), avoiding a single monorepo vX.Y.Z tag which wouldn't reflect per-package semver.

Citations:


🏁 Script executed:

cat -n scripts/create-github-release.mjs | sed -n '200,230p'

Repository: TanStack/intent

Length of output: 1044


🏁 Script executed:

# Also check how the tag is used downstream
rg -A 10 -B 5 "createReleaseTag" scripts/create-github-release.mjs

Repository: TanStack/intent

Length of output: 747


🏁 Script executed:

# Check for any other tag creation or Git tag operations
rg "tag.*v\d|@tanstack.*@" scripts/create-github-release.mjs

Repository: TanStack/intent

Length of output: 41


🏁 Script executed:

# Find the createGitHubRelease function and see how it uses the tag
rg -A 30 "function createGitHubRelease|const createGitHubRelease" scripts/create-github-release.mjs

Repository: TanStack/intent

Length of output: 654


🏁 Script executed:

# Look for pushTag function
rg -A 15 "function pushTag|const pushTag" scripts/create-github-release.mjs

Repository: TanStack/intent

Length of output: 384


🏁 Script executed:

# Search for where the tag parameter is used in the file
rg "pushTag\(|gh release create" scripts/create-github-release.mjs

Repository: TanStack/intent

Length of output: 112


🏁 Script executed:

# Look at the main entry point and how createGitHubRelease is called
rg -B 10 -A 5 "createdTag = pushTag|createGitHubRelease\(" scripts/create-github-release.mjs | head -50

Repository: TanStack/intent

Length of output: 669


🏁 Script executed:

# Get broader context around the main release creation logic
rg -B 5 -A 20 "createdTag\s*=" scripts/create-github-release.mjs

Repository: TanStack/intent

Length of output: 322


🏁 Script executed:

# Check if this is actually TanStack/intent or TanStack/query
git config --get remote.origin.url 2>/dev/null || git remote -v | head -1

Repository: TanStack/intent

Length of output: 97


🏁 Script executed:

# Check the package.json to see what project this is
head -20 package.json

Repository: TanStack/intent

Length of output: 956


🏁 Script executed:

# Look for any changelog or docs that explain the versioning scheme
find . -name "CHANGELOG*" -o -name "release*.md" -o -name ".github/workflows/*release*" | head -5

Repository: TanStack/intent

Length of output: 437


🏁 Script executed:

# Check the actual CHANGELOG to see what tags are used
head -100 packages/intent/CHANGELOG.md

Repository: TanStack/intent

Length of output: 8233


🏁 Script executed:

# Check git tags to see actual tag format
git tag | head -20

Repository: TanStack/intent

Length of output: 41


🏁 Script executed:

# Check actual GitHub releases for this repo
rg "release|tag|@" .github/ -t md 2>/dev/null | head -20

Repository: TanStack/intent

Length of output: 143


🏁 Script executed:

# Check git history of the create-github-release.mjs file
git log --oneline scripts/create-github-release.mjs 2>/dev/null | head -10

Repository: TanStack/intent

Length of output: 92


🏁 Script executed:

# Check if there are any actual releases yet (check .git/refs/tags or git ls-remote)
git ls-remote --tags origin 2>/dev/null | head -20

Repository: TanStack/intent

Length of output: 364


🏁 Script executed:

# Check the .github/workflows to see current release process
find .github/workflows -name "*release*" -o -name "*publish*" | xargs cat 2>/dev/null | head -100

Repository: TanStack/intent

Length of output: 3309


The v<version> tag branch appears to be dead code or untested; actual releases use time-based tags.

All releases in this repository use release-YYYY-MM-DD-HHMM tags (e.g., release-2026-03-16-1953), not v<version> tags. The code branch returning v<version> (lines 209–214) when versions.length === 1 is either unreachable or never exercised in practice. Since @tanstack/intent is a single-package publish, multiple distinct versions across packages are unlikely, meaning releases consistently hit the time-based branch instead.

If this branch is intended for future use (e.g., supporting per-package releases), clarify the intent and add safeguards. Otherwise, simplify by removing the single-version shortcut and always using the time-based scheme for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/create-github-release.mjs` around lines 207 - 224, The branch in
createReleaseTag that returns a v<version> tag when versions.length === 1 is
dead/untested; either remove that shortcut so createReleaseTag always produces
the time-based tag (using changedPackages and now-based slices) for consistent
release names, or explicitly gate the v<version> behavior behind a named option
(e.g., perPackageRelease flag) and add unit tests for both paths; update
references to changedPackages/versions and any callers to pass the new option if
you choose the gated approach.

@LadyBluenotes LadyBluenotes merged commit bae9dd6 into main Apr 23, 2026
7 checks passed
@LadyBluenotes LadyBluenotes deleted the bug-fix branch April 23, 2026 23:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant