@tanstack/intent 0.0.29 → 0.0.33

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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -12
  3. package/dist/artifact-coverage-BAN2W6aH.mjs +3 -0
  4. package/dist/artifact-coverage-wLNVX8yC.mjs +128 -0
  5. package/dist/cli.d.mts +0 -0
  6. package/dist/cli.mjs +77 -76
  7. package/dist/{display-hdsqb4w-.mjs → display-BTZWCjzT.mjs} +10 -1
  8. package/dist/display-DvLbcWzq.mjs +5 -0
  9. package/dist/index.d.mts +76 -5
  10. package/dist/index.mjs +126 -6
  11. package/dist/install-PUnIfBNC.mjs +542 -0
  12. package/dist/intent-library.mjs +9 -5
  13. package/dist/{library-scanner-B51qV5aX.mjs → library-scanner-Cl-XPEMf.mjs} +10 -3
  14. package/dist/library-scanner.d.mts +2 -2
  15. package/dist/library-scanner.mjs +2 -1
  16. package/dist/{project-context-D6A5sBBO.mjs → project-context-IDLpJU3S.mjs} +1 -1
  17. package/dist/resolver-D2CgIYGg.mjs +70 -0
  18. package/dist/{scanner-B-bbXBLY.mjs → scanner-CW59cxE_.mjs} +176 -117
  19. package/dist/scanner-DlkcbVye.mjs +6 -0
  20. package/dist/{setup-B4ZwN5Hg.mjs → setup-DfLsziXU.mjs} +2 -2
  21. package/dist/setup.d.mts +1 -1
  22. package/dist/setup.mjs +3 -3
  23. package/dist/skill-paths-8k9K9y26.mjs +33 -0
  24. package/dist/skill-use-uwGleSOz.mjs +42 -0
  25. package/dist/staleness-DpbmYod4.mjs +5 -0
  26. package/dist/staleness-PdgakrCQ.mjs +243 -0
  27. package/dist/{types-BTQ9efv-.d.mts → types-_y9b00bI.d.mts} +54 -1
  28. package/dist/{workspace-patterns-Cndd-7vB.mjs → workspace-patterns-BN2A_60g.mjs} +6 -1
  29. package/dist/workspace-patterns-x-dLZxx4.mjs +4 -0
  30. package/meta/templates/workflows/check-skills.yml +199 -91
  31. package/package.json +3 -4
  32. package/dist/display-DdmZXLZm.mjs +0 -3
  33. package/dist/install-BzDmD5yI.mjs +0 -69
  34. package/dist/scanner-CP4U8F_n.mjs +0 -5
  35. package/dist/staleness-LRbiWWZK.mjs +0 -4
  36. package/dist/staleness-SY7-mZMH.mjs +0 -104
  37. package/dist/workspace-patterns-D_y6rlqX.mjs +0 -4
  38. package/meta/templates/workflows/notify-intent.yml +0 -51
  39. /package/dist/{setup-BA9RkENh.d.mts → setup-D2CGdTsx.d.mts} +0 -0
@@ -0,0 +1,243 @@
1
+ import { a as parseFrontmatter, n as findSkillFiles } from "./utils-COlDcU72.mjs";
2
+ import { t as readIntentArtifacts } from "./artifact-coverage-wLNVX8yC.mjs";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+
6
+ //#region src/staleness.ts
7
+ function classifyVersionDrift(oldVer, newVer) {
8
+ if (oldVer === newVer) return null;
9
+ const oldParts = oldVer.replace(/[^0-9.]/g, "").split(".").map(Number);
10
+ const newParts = newVer.replace(/[^0-9.]/g, "").split(".").map(Number);
11
+ if ((newParts[0] ?? 0) > (oldParts[0] ?? 0)) return "major";
12
+ if ((newParts[1] ?? 0) > (oldParts[1] ?? 0)) return "minor";
13
+ if ((newParts[2] ?? 0) > (oldParts[2] ?? 0)) return "patch";
14
+ return null;
15
+ }
16
+ function readLocalVersion(packageDir) {
17
+ try {
18
+ const pkgJson = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
19
+ return typeof pkgJson.version === "string" ? pkgJson.version : null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ async function fetchNpmVersion(packageName) {
25
+ try {
26
+ const res = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
27
+ if (!res.ok) return null;
28
+ const data = await res.json();
29
+ return typeof data.version === "string" ? data.version : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ async function fetchCurrentVersion(packageDir, packageName) {
35
+ return readLocalVersion(packageDir) ?? await fetchNpmVersion(packageName);
36
+ }
37
+ function isStringRecord(value) {
38
+ return !!value && typeof value === "object" && !Array.isArray(value) && Object.values(value).every((entry) => typeof entry === "string");
39
+ }
40
+ function parseSyncState(value) {
41
+ if (!value || typeof value !== "object") return null;
42
+ const raw = value;
43
+ const parsed = {};
44
+ if (typeof raw.library_version === "string") parsed.library_version = raw.library_version;
45
+ if (raw.skills && typeof raw.skills === "object") {
46
+ const skills = {};
47
+ for (const [skillName, skillValue] of Object.entries(raw.skills)) {
48
+ if (!skillValue || typeof skillValue !== "object") continue;
49
+ const sourcesSha = skillValue.sources_sha;
50
+ if (sourcesSha !== void 0 && !isStringRecord(sourcesSha)) continue;
51
+ skills[skillName] = {};
52
+ if (sourcesSha) skills[skillName].sources_sha = sourcesSha;
53
+ }
54
+ parsed.skills = skills;
55
+ }
56
+ return parsed;
57
+ }
58
+ function readSyncState(packageDir) {
59
+ const statePath = join(packageDir, "skills", "sync-state.json");
60
+ try {
61
+ return parseSyncState(JSON.parse(readFileSync(statePath, "utf8")));
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+ function readPackageName(packageDir) {
67
+ const packageJson = readPackageJson(packageDir);
68
+ return typeof packageJson?.name === "string" ? packageJson.name : relative(process.cwd(), packageDir) || "unknown";
69
+ }
70
+ function readPackageJson(packageDir) {
71
+ try {
72
+ return JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
73
+ } catch {
74
+ return null;
75
+ }
76
+ }
77
+ function normalizeFilePath(path) {
78
+ return resolve(path).split(sep).join("/");
79
+ }
80
+ function normalizeList(values) {
81
+ return [...new Set(values ?? [])].sort((a, b) => a.localeCompare(b));
82
+ }
83
+ function sameStringList(a, b) {
84
+ const left = normalizeList(a);
85
+ const right = normalizeList(b);
86
+ return left.length === right.length && left.every((value, index) => value === right[index]);
87
+ }
88
+ function artifactPackageMatches(artifact, packageDir, packageName, artifactRoot) {
89
+ const relPackageDir = relative(artifactRoot, packageDir).split(sep).join("/");
90
+ if (!relPackageDir) return true;
91
+ if (artifact.packages.includes(packageName)) return true;
92
+ if (artifact.packages.includes(relPackageDir)) return true;
93
+ if (artifact.path?.startsWith(`${relPackageDir}/`)) return true;
94
+ return artifact.packages.length === 0 && artifact.path === void 0;
95
+ }
96
+ function resolveArtifactSkillPaths(artifact, packageDir, artifactRoot) {
97
+ if (!artifact.path) return [];
98
+ const candidatePaths = [isAbsolute(artifact.path) ? artifact.path : join(artifactRoot, artifact.path), isAbsolute(artifact.path) ? artifact.path : join(packageDir, artifact.path)];
99
+ if (artifact.package && artifact.path.startsWith("skills/")) candidatePaths.push(join(artifactRoot, artifact.package, artifact.path));
100
+ return [...new Set(candidatePaths.map(normalizeFilePath))];
101
+ }
102
+ function findMatchingSkill(artifact, skillMetas, packageDir, artifactRoot) {
103
+ const skillsByPath = new Map(skillMetas.map((skill) => [normalizeFilePath(skill.filePath), skill]));
104
+ for (const candidatePath of resolveArtifactSkillPaths(artifact, packageDir, artifactRoot)) {
105
+ const match = skillsByPath.get(candidatePath);
106
+ if (match) return match;
107
+ }
108
+ const skillsByName = /* @__PURE__ */ new Map();
109
+ for (const skill of skillMetas) {
110
+ skillsByName.set(skill.name, skill);
111
+ skillsByName.set(skill.relName, skill);
112
+ }
113
+ return (artifact.slug ? skillsByName.get(artifact.slug) : void 0) ?? (artifact.name ? skillsByName.get(artifact.name) : void 0) ?? null;
114
+ }
115
+ function buildArtifactSignals({ artifactRoot, artifacts, library, packageDir, skillMetas }) {
116
+ if (!artifacts) return [];
117
+ const artifactFiles = new Map([...artifacts.skillTrees, ...artifacts.domainMaps].map((file) => [file.path, file]));
118
+ const signals = artifacts.warnings.map((warning) => ({
119
+ type: "artifact-parse-warning",
120
+ library,
121
+ subject: warning.artifactPath,
122
+ reasons: [warning.message],
123
+ needsReview: true,
124
+ artifactPath: warning.artifactPath
125
+ }));
126
+ for (const artifact of artifacts.skills) {
127
+ if (!artifactPackageMatches(artifact, packageDir, library, artifactRoot)) continue;
128
+ const subject = artifact.slug ?? artifact.name ?? artifact.path;
129
+ const matchingSkill = findMatchingSkill(artifact, skillMetas, packageDir, artifactRoot);
130
+ if (artifact.path && !matchingSkill) {
131
+ signals.push({
132
+ type: "artifact-skill-missing",
133
+ library,
134
+ subject,
135
+ reasons: [`artifact skill path does not resolve to a generated SKILL.md (${artifact.path})`],
136
+ needsReview: true,
137
+ artifactPath: artifact.artifactPath,
138
+ skill: artifact.slug ?? artifact.name
139
+ });
140
+ continue;
141
+ }
142
+ if (!matchingSkill) continue;
143
+ if (matchingSkill.sources !== void 0 && artifact.sources.length > 0 && !sameStringList(matchingSkill.sources, artifact.sources)) signals.push({
144
+ type: "artifact-source-drift",
145
+ library,
146
+ subject,
147
+ reasons: ["artifact sources differ from SKILL.md frontmatter sources"],
148
+ needsReview: true,
149
+ artifactPath: artifact.artifactPath,
150
+ skill: matchingSkill.name
151
+ });
152
+ const artifactVersion = artifactFiles.get(artifact.artifactPath)?.libraryVersion;
153
+ if (artifactVersion && matchingSkill.libraryVersion && artifactVersion !== matchingSkill.libraryVersion) signals.push({
154
+ type: "artifact-library-version-drift",
155
+ library,
156
+ subject,
157
+ reasons: [`artifact library.version (${artifactVersion}) differs from SKILL.md library_version (${matchingSkill.libraryVersion})`],
158
+ needsReview: true,
159
+ artifactPath: artifact.artifactPath,
160
+ skill: matchingSkill.name
161
+ });
162
+ }
163
+ return signals;
164
+ }
165
+ function artifactCoversPackage(artifact, packageDir, packageName, artifactRoot) {
166
+ const relPackageDir = relative(artifactRoot, packageDir).split(sep).join("/");
167
+ return artifact.packages.includes(packageName) || artifact.packages.includes(relPackageDir) || artifact.package === packageName || artifact.package === relPackageDir || artifact.path?.startsWith(`${relPackageDir}/`) === true;
168
+ }
169
+ function artifactIgnoresPackage(artifacts, packageDir, packageName, artifactRoot) {
170
+ const relPackageDir = relative(artifactRoot, packageDir).split(sep).join("/");
171
+ return artifacts.ignoredPackages.some((ignored) => ignored.packageName === packageName || ignored.packageName === relPackageDir);
172
+ }
173
+ function buildWorkspaceCoverageSignals({ artifactRoot, artifacts, packageDirs }) {
174
+ if (!artifacts) return [];
175
+ const signals = [];
176
+ for (const packageDir of packageDirs) {
177
+ if (readPackageJson(packageDir)?.private === true) continue;
178
+ const packageName = readPackageName(packageDir);
179
+ if (artifactIgnoresPackage(artifacts, packageDir, packageName, artifactRoot)) continue;
180
+ const hasGeneratedSkill = findSkillFiles(join(packageDir, "skills")).length > 0;
181
+ const hasArtifactCoverage = artifacts.skills.some((artifact) => artifactCoversPackage(artifact, packageDir, packageName, artifactRoot));
182
+ if (hasGeneratedSkill || hasArtifactCoverage) continue;
183
+ signals.push({
184
+ type: "missing-package-coverage",
185
+ library: packageName,
186
+ subject: packageName,
187
+ reasons: ["workspace package is not represented by generated skills or _artifacts coverage"],
188
+ needsReview: true,
189
+ packageName,
190
+ packageRoot: relative(artifactRoot, packageDir).split(sep).join("/")
191
+ });
192
+ }
193
+ return signals;
194
+ }
195
+ async function checkStaleness(packageDir, packageName, artifactRoot = packageDir) {
196
+ const skillsDir = join(packageDir, "skills");
197
+ const library = packageName ?? "unknown";
198
+ const skillMetas = findSkillFiles(skillsDir).map((filePath) => {
199
+ const fm = parseFrontmatter(filePath);
200
+ const relName = relative(skillsDir, filePath).replace(/[/\\]SKILL\.md$/, "").split(sep).join("/");
201
+ return {
202
+ name: typeof fm?.name === "string" ? fm.name : relName,
203
+ relName,
204
+ filePath,
205
+ libraryVersion: fm?.library_version,
206
+ sources: Array.isArray(fm?.sources) ? fm.sources : void 0
207
+ };
208
+ });
209
+ const artifacts = existsSync(join(artifactRoot, "_artifacts")) ? readIntentArtifacts(artifactRoot) : null;
210
+ const skillVersion = skillMetas.find((s) => s.libraryVersion)?.libraryVersion ?? null;
211
+ const currentVersion = await fetchCurrentVersion(packageDir, library);
212
+ const versionDrift = skillVersion && currentVersion ? classifyVersionDrift(skillVersion, currentVersion) : null;
213
+ const syncState = readSyncState(packageDir);
214
+ return {
215
+ library,
216
+ currentVersion,
217
+ skillVersion,
218
+ versionDrift,
219
+ skills: skillMetas.map((skill) => {
220
+ const reasons = [];
221
+ if (currentVersion && skill.libraryVersion && skill.libraryVersion !== currentVersion) reasons.push(`version drift (${skill.libraryVersion} → ${currentVersion})`);
222
+ const storedShas = syncState?.skills?.[skill.name]?.sources_sha ?? {};
223
+ if (skill.sources && Object.keys(storedShas).length > 0) {
224
+ for (const source of skill.sources) if (!storedShas[source]) reasons.push(`new source (${source})`);
225
+ }
226
+ return {
227
+ name: skill.name,
228
+ reasons,
229
+ needsReview: reasons.length > 0
230
+ };
231
+ }),
232
+ signals: buildArtifactSignals({
233
+ artifactRoot,
234
+ artifacts,
235
+ library,
236
+ packageDir,
237
+ skillMetas
238
+ })
239
+ };
240
+ }
241
+
242
+ //#endregion
243
+ export { checkStaleness as n, readPackageName as r, buildWorkspaceCoverageSignals as t };
@@ -15,6 +15,11 @@ interface ScanResult {
15
15
  global: NodeModulesScanTarget;
16
16
  };
17
17
  }
18
+ type ScanScope = 'local' | 'local-and-global' | 'global';
19
+ interface ScanOptions {
20
+ includeGlobal?: boolean;
21
+ scope?: ScanScope;
22
+ }
18
23
  interface NodeModulesScanTarget {
19
24
  path: string | null;
20
25
  detected: boolean;
@@ -28,6 +33,7 @@ interface IntentPackage {
28
33
  intent: IntentConfig;
29
34
  skills: Array<SkillEntry>;
30
35
  packageRoot: string;
36
+ source: 'local' | 'global';
31
37
  }
32
38
  interface InstalledVariant {
33
39
  version: string;
@@ -51,12 +57,59 @@ interface StalenessReport {
51
57
  skillVersion: string | null;
52
58
  versionDrift: 'major' | 'minor' | 'patch' | null;
53
59
  skills: Array<SkillStaleness>;
60
+ signals: Array<StalenessSignal>;
54
61
  }
55
62
  interface SkillStaleness {
56
63
  name: string;
57
64
  reasons: Array<string>;
58
65
  needsReview: boolean;
59
66
  }
67
+ interface StalenessSignal {
68
+ type: string;
69
+ library?: string;
70
+ subject?: string;
71
+ reasons: Array<string>;
72
+ needsReview: boolean;
73
+ artifactPath?: string;
74
+ packageName?: string;
75
+ packageRoot?: string;
76
+ skill?: string;
77
+ }
78
+ interface IntentArtifactSet {
79
+ root: string;
80
+ artifactsDir: string;
81
+ skillTrees: Array<IntentArtifactFile>;
82
+ domainMaps: Array<IntentArtifactFile>;
83
+ skills: Array<IntentArtifactSkill>;
84
+ ignoredPackages: Array<IntentArtifactCoverageIgnore>;
85
+ warnings: Array<IntentArtifactWarning>;
86
+ }
87
+ interface IntentArtifactFile {
88
+ path: string;
89
+ kind: 'skill-tree' | 'domain-map';
90
+ libraryName?: string;
91
+ libraryVersion?: string;
92
+ }
93
+ interface IntentArtifactSkill {
94
+ artifactPath: string;
95
+ artifactKind: 'skill-tree' | 'domain-map';
96
+ name?: string;
97
+ slug?: string;
98
+ path?: string;
99
+ package?: string;
100
+ packages: Array<string>;
101
+ sources: Array<string>;
102
+ covers: Array<string>;
103
+ }
104
+ interface IntentArtifactCoverageIgnore {
105
+ packageName: string;
106
+ reason?: string;
107
+ artifactPath: string;
108
+ }
109
+ interface IntentArtifactWarning {
110
+ artifactPath: string;
111
+ message: string;
112
+ }
60
113
  interface FeedbackPayload {
61
114
  skill: string;
62
115
  package: string;
@@ -90,4 +143,4 @@ interface IntentProjectConfig {
90
143
  };
91
144
  }
92
145
  //#endregion
93
- export { IntentProjectConfig as a, ScanResult as c, StalenessReport as d, IntentPackage as i, SkillEntry as l, FeedbackPayload as n, MetaFeedbackPayload as o, IntentConfig as r, MetaSkillName as s, AgentName as t, SkillStaleness as u };
146
+ export { StalenessReport as _, IntentArtifactSet as a, IntentConfig as c, MetaFeedbackPayload as d, MetaSkillName as f, SkillStaleness as g, SkillEntry as h, IntentArtifactFile as i, IntentPackage as l, ScanResult as m, FeedbackPayload as n, IntentArtifactSkill as o, ScanOptions as p, IntentArtifactCoverageIgnore as r, IntentArtifactWarning as s, AgentName as t, IntentProjectConfig as u, StalenessSignal as v, VersionConflict as y };
@@ -162,6 +162,11 @@ function findPackagesWithSkills(root) {
162
162
  return existsSync(skillsDir) && findSkillFiles(skillsDir).length > 0;
163
163
  });
164
164
  }
165
+ function findWorkspacePackages(root) {
166
+ const patterns = readWorkspacePatterns(root);
167
+ if (!patterns) return [];
168
+ return resolveWorkspacePackages(root, patterns);
169
+ }
165
170
 
166
171
  //#endregion
167
- export { resolveWorkspacePackages as i, findWorkspaceRoot as n, readWorkspacePatterns as r, findPackagesWithSkills as t };
172
+ export { resolveWorkspacePackages as a, readWorkspacePatterns as i, findWorkspacePackages as n, findWorkspaceRoot as r, findPackagesWithSkills as t };
@@ -0,0 +1,4 @@
1
+ import "./utils-COlDcU72.mjs";
2
+ import { a as resolveWorkspacePackages, i as readWorkspacePatterns, n as findWorkspacePackages, r as findWorkspaceRoot, t as findPackagesWithSkills } from "./workspace-patterns-BN2A_60g.mjs";
3
+
4
+ export { findPackagesWithSkills, findWorkspacePackages, findWorkspaceRoot };
@@ -1,11 +1,12 @@
1
1
  # check-skills.yml — Drop this into your library repo's .github/workflows/
2
2
  #
3
- # Checks for stale intent skills after a release and opens a review PR
4
- # if any skills need attention. The PR body includes a prompt you can
5
- # paste into Claude Code, Cursor, or any coding agent to update them.
3
+ # Checks intent skills after a release and opens or updates one review PR when
4
+ # existing skills, artifact coverage, or workspace package coverage need review.
6
5
  #
7
6
  # Triggers: new release published, or manual workflow_dispatch.
8
7
  #
8
+ # intent-workflow-version: 2
9
+ #
9
10
  # Template variables (replaced by `intent setup`):
10
11
  # {{PACKAGE_LABEL}} — e.g. @tanstack/query or my-workspace workspace
11
12
 
@@ -22,7 +23,7 @@ permissions:
22
23
 
23
24
  jobs:
24
25
  check:
25
- name: Check for stale skills
26
+ name: Check intent skill coverage
26
27
  runs-on: ubuntu-latest
27
28
  steps:
28
29
  - name: Checkout
@@ -38,106 +39,213 @@ jobs:
38
39
  - name: Install intent
39
40
  run: npm install -g @tanstack/intent
40
41
 
41
- - name: Check staleness
42
+ - name: Check skills
42
43
  id: stale
43
44
  run: |
44
- OUTPUT=$(intent stale --json 2>&1) || true
45
- echo "$OUTPUT"
46
-
47
- # Check if any skills need review
48
- NEEDS_REVIEW=$(echo "$OUTPUT" | node -e "
49
- const input = require('fs').readFileSync('/dev/stdin','utf8');
50
- try {
51
- const reports = JSON.parse(input);
52
- const stale = reports.flatMap(r =>
53
- r.skills.filter(s => s.needsReview).map(s => ({ library: r.library, skill: s.name, reasons: s.reasons }))
54
- );
55
- if (stale.length > 0) {
56
- console.log(JSON.stringify(stale));
57
- }
58
- } catch {}
59
- ")
60
-
61
- if [ -z "$NEEDS_REVIEW" ]; then
62
- echo "has_stale=false" >> "$GITHUB_OUTPUT"
45
+ set +e
46
+ intent stale --json > stale.json
47
+ STATUS=$?
48
+ set -e
49
+
50
+ cat stale.json
51
+
52
+ if [ "$STATUS" -ne 0 ]; then
53
+ echo "has_review=true" >> "$GITHUB_OUTPUT"
54
+ echo "check_failed=true" >> "$GITHUB_OUTPUT"
55
+ cat > review-items.json <<'JSON'
56
+ [
57
+ {
58
+ "type": "stale-check-failed",
59
+ "library": "{{PACKAGE_LABEL}}",
60
+ "subject": "intent stale --json",
61
+ "reasons": ["The stale check command failed. Review the workflow logs before updating skills."]
62
+ }
63
+ ]
64
+ JSON
63
65
  else
64
- echo "has_stale=true" >> "$GITHUB_OUTPUT"
65
- # Escape for multiline GH output
66
- EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
67
- echo "stale_json<<$EOF" >> "$GITHUB_OUTPUT"
68
- echo "$NEEDS_REVIEW" >> "$GITHUB_OUTPUT"
69
- echo "$EOF" >> "$GITHUB_OUTPUT"
66
+ node <<'NODE'
67
+ const fs = require('fs')
68
+ const reports = JSON.parse(fs.readFileSync('stale.json', 'utf8'))
69
+ const items = []
70
+
71
+ for (const report of reports) {
72
+ for (const skill of report.skills ?? []) {
73
+ if (!skill?.needsReview) continue
74
+ items.push({
75
+ type: 'stale-skill',
76
+ library: report.library,
77
+ subject: skill.name,
78
+ reasons: skill.reasons ?? [],
79
+ })
80
+ }
81
+
82
+ for (const signal of report.signals ?? []) {
83
+ if (signal?.needsReview === false) continue
84
+ items.push({
85
+ type: signal?.type ?? 'review-signal',
86
+ library: signal?.library ?? report.library,
87
+ subject:
88
+ signal?.packageName ??
89
+ signal?.packageRoot ??
90
+ signal?.skill ??
91
+ signal?.artifactPath ??
92
+ signal?.subject ??
93
+ report.library,
94
+ reasons: signal?.reasons ?? [],
95
+ artifactPath: signal?.artifactPath,
96
+ packageName: signal?.packageName,
97
+ packageRoot: signal?.packageRoot,
98
+ skill: signal?.skill,
99
+ })
100
+ }
101
+ }
102
+
103
+ fs.writeFileSync('review-items.json', JSON.stringify(items, null, 2) + '\n')
104
+ fs.appendFileSync(
105
+ process.env.GITHUB_OUTPUT,
106
+ `has_review=${items.length > 0 ? 'true' : 'false'}\n`,
107
+ )
108
+ NODE
70
109
  fi
71
110
 
72
- - name: Build summary
73
- if: steps.stale.outputs.has_stale == 'true'
74
- id: summary
75
- run: |
76
- node -e "
77
- const stale = JSON.parse(process.env.STALE_JSON);
78
- const lines = stale.map(s =>
79
- '- **' + s.skill + '** (' + s.library + '): ' + s.reasons.join(', ')
80
- );
81
- const summary = lines.join('\n');
82
-
83
- const prompt = [
84
- 'Review and update the following stale intent skills for {{PACKAGE_LABEL}}:',
85
- '',
86
- ...stale.map(s => '- ' + s.skill + ': ' + s.reasons.join(', ')),
87
- '',
88
- 'For each stale skill:',
89
- '1. Read the current SKILL.md file',
90
- '2. Check what changed in the library since the skill was last updated',
91
- '3. Update the skill content to reflect current APIs and behavior',
92
- '4. Run \`npx @tanstack/intent validate\` to verify the updated skill',
93
- ].join('\n');
94
-
95
- // Write outputs
96
- const fs = require('fs');
97
- const env = fs.readFileSync(process.env.GITHUB_OUTPUT, 'utf8');
98
- const eof = require('crypto').randomBytes(15).toString('base64');
99
- fs.appendFileSync(process.env.GITHUB_OUTPUT,
100
- 'summary<<' + eof + '\n' + summary + '\n' + eof + '\n' +
101
- 'prompt<<' + eof + '\n' + prompt + '\n' + eof + '\n'
102
- );
103
- "
104
- env:
105
- STALE_JSON: ${{ steps.stale.outputs.stale_json }}
111
+ {
112
+ echo "review_items<<EOF"
113
+ cat review-items.json
114
+ echo "EOF"
115
+ } >> "$GITHUB_OUTPUT"
106
116
 
107
- - name: Open review PR
108
- if: steps.stale.outputs.has_stale == 'true'
117
+ - name: Write clean summary
118
+ if: steps.stale.outputs.has_review == 'false'
119
+ run: |
120
+ {
121
+ echo "### Intent skill review"
122
+ echo ""
123
+ echo "No stale skills or coverage gaps found."
124
+ } >> "$GITHUB_STEP_SUMMARY"
125
+
126
+ - name: Build review PR body
127
+ if: steps.stale.outputs.has_review == 'true'
128
+ run: |
129
+ node <<'NODE'
130
+ const fs = require('fs')
131
+ const items = JSON.parse(fs.readFileSync('review-items.json', 'utf8'))
132
+ const grouped = new Map()
133
+
134
+ for (const item of items) {
135
+ grouped.set(item.type, (grouped.get(item.type) ?? 0) + 1)
136
+ }
137
+
138
+ const signalRows = [...grouped.entries()]
139
+ .sort(([a], [b]) => a.localeCompare(b))
140
+ .map(([type, count]) => `| \`${type}\` | ${count} |`)
141
+
142
+ const itemRows = items.map((item) => {
143
+ const subject = item.subject ? `\`${item.subject}\`` : '-'
144
+ const reasons = item.reasons?.length ? item.reasons.join('; ') : '-'
145
+ return `| \`${item.type}\` | ${subject} | \`${item.library}\` | ${reasons} |`
146
+ })
147
+
148
+ const prompt = [
149
+ 'You are helping maintain Intent skills for this repository.',
150
+ '',
151
+ 'Goal:',
152
+ 'Resolve the Intent skill review signals below while preserving the existing scope, taxonomy, and maintainer-reviewed artifacts.',
153
+ '',
154
+ 'Review signals:',
155
+ JSON.stringify(items, null, 2),
156
+ '',
157
+ 'Required workflow:',
158
+ '1. Read the existing `_artifacts/*domain_map.yaml`, `_artifacts/*skill_tree.yaml`, and generated `skills/**/SKILL.md` files.',
159
+ '2. Read each flagged package package.json, public exports, README/docs if present, and source entry points.',
160
+ '3. Compare flagged packages against the existing domains, skills, tasks, packages, covers, sources, tensions, and cross-references in the artifacts.',
161
+ '4. For each signal, decide whether it means existing skill coverage, a missing generated skill, a new skill candidate, out-of-scope coverage, or deferred work.',
162
+ '',
163
+ 'Maintainer questions:',
164
+ 'Before editing skills or artifacts, ask the maintainer:',
165
+ '1. For each flagged package, is this package user-facing enough to need agent guidance?',
166
+ '2. If yes, should it extend an existing skill or become a new skill?',
167
+ '3. If it extends an existing skill, which current skill should own it?',
168
+ '4. If it is out of scope, what short reason should be recorded in artifact coverage ignores?',
169
+ '5. Are any of these packages experimental or unstable enough to exclude for now?',
170
+ '',
171
+ 'Decision rules:',
172
+ '- Do not auto-generate skills.',
173
+ '- Do not create broad new skill areas without maintainer confirmation.',
174
+ '- Prefer adding package coverage to an existing skill when the package is an implementation variant of an existing domain.',
175
+ '- Create a new skill only when the package introduces a distinct developer task or failure mode.',
176
+ '- Preserve current naming, path, and package layout conventions.',
177
+ '- Keep generated skills under the package-local `skills/` directory.',
178
+ '- Keep repo-root `_artifacts` as the reviewed plan.',
179
+ '',
180
+ 'If maintainer confirms updates:',
181
+ '1. Update the relevant `_artifacts/*domain_map.yaml` or `_artifacts/*skill_tree.yaml`.',
182
+ '2. Update or create `SKILL.md` files only for confirmed coverage changes.',
183
+ '3. Keep `sources` aligned between artifact skill entries and SKILL frontmatter.',
184
+ '4. Bump `library_version` only for skills whose covered source package version changed.',
185
+ '5. Run `npx @tanstack/intent@latest validate` on touched skill directories.',
186
+ '6. Summarize every package as one of: existing-skill coverage, new skill, ignored, or deferred.',
187
+ ].join('\n')
188
+
189
+ const body = [
190
+ '## Intent Skill Review Needed',
191
+ '',
192
+ 'Intent found skills, artifact coverage, or workspace package coverage that need maintainer review.',
193
+ '',
194
+ '### Summary',
195
+ '',
196
+ '| Signal | Count |',
197
+ '| --- | ---: |',
198
+ ...signalRows,
199
+ '',
200
+ '### Review Items',
201
+ '',
202
+ '| Signal | Subject | Library | Reason |',
203
+ '| --- | --- | --- | --- |',
204
+ ...itemRows,
205
+ '',
206
+ '### Agent Prompt',
207
+ '',
208
+ 'Paste this into your coding agent:',
209
+ '',
210
+ '```text',
211
+ prompt,
212
+ '```',
213
+ '',
214
+ 'This PR is a review reminder only. It does not update skills automatically.',
215
+ ].join('\n')
216
+
217
+ fs.writeFileSync('pr-body.md', body + '\n')
218
+ fs.writeFileSync(process.env.GITHUB_STEP_SUMMARY, body + '\n')
219
+ NODE
220
+
221
+ - name: Open or update review PR
222
+ if: steps.stale.outputs.has_review == 'true'
109
223
  env:
110
224
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
111
225
  run: |
112
226
  VERSION="${{ github.event.release.tag_name || 'manual' }}"
113
227
  BRANCH="skills/review-${VERSION}"
228
+ BASE_BRANCH="${{ github.event.repository.default_branch }}"
114
229
 
115
230
  git config user.name "github-actions[bot]"
116
231
  git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
117
- git checkout -b "$BRANCH"
118
- git commit --allow-empty -m "chore: review stale skills for ${VERSION}"
119
- git push origin "$BRANCH"
120
-
121
- gh pr create \
122
- --title "Review stale skills (${VERSION})" \
123
- --body "$(cat <<'PREOF'
124
- ## Stale Skills Detected
125
-
126
- The following skills may need updates after the latest release:
127
-
128
- ${{ steps.summary.outputs.summary }}
129
232
 
130
- ---
131
-
132
- ### Update Prompt
133
-
134
- Paste this into your coding agent (Claude Code, Cursor, etc.):
135
-
136
- ~~~
137
- ${{ steps.summary.outputs.prompt }}
138
- ~~~
233
+ git fetch origin "$BRANCH" || true
234
+ if git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
235
+ git checkout -B "$BRANCH" "origin/$BRANCH"
236
+ else
237
+ git checkout -b "$BRANCH"
238
+ git commit --allow-empty -m "chore: review intent skills for ${VERSION}"
239
+ git push origin "$BRANCH"
240
+ fi
139
241
 
140
- PREOF
141
- )" \
142
- --head "$BRANCH" \
143
- --base main
242
+ PR_URL="$(gh pr list --head "$BRANCH" --json url --jq '.[0].url')"
243
+ if [ -n "$PR_URL" ]; then
244
+ gh pr edit "$PR_URL" --body-file pr-body.md
245
+ else
246
+ gh pr create \
247
+ --title "Review intent skills (${VERSION})" \
248
+ --body-file pr-body.md \
249
+ --head "$BRANCH" \
250
+ --base "$BASE_BRANCH"
251
+ fi