@elevasis/sdk 1.21.0 → 1.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/cli.cjs +1239 -173
  2. package/dist/index.d.ts +1752 -464
  3. package/dist/index.js +3477 -143
  4. package/dist/node/index.d.ts +1 -0
  5. package/dist/node/index.js +19 -1
  6. package/dist/test-utils/index.d.ts +1188 -127
  7. package/dist/test-utils/index.js +3359 -152
  8. package/dist/worker/index.js +3148 -80
  9. package/package.json +2 -2
  10. package/reference/claude-config/hooks/post-edit-validate.mjs +98 -98
  11. package/reference/claude-config/hooks/scaffold-registry-reminder.mjs +188 -188
  12. package/reference/claude-config/hooks/tool-failure-recovery.mjs +73 -73
  13. package/reference/claude-config/registries/graph-skills.json +4 -4
  14. package/reference/claude-config/registries/knowledge-flags.json +0 -2
  15. package/reference/claude-config/rules/active-change-index.md +80 -80
  16. package/reference/claude-config/rules/agent-start-here.md +277 -277
  17. package/reference/claude-config/rules/deployment.md +57 -57
  18. package/reference/claude-config/rules/error-handling.md +56 -56
  19. package/reference/claude-config/rules/execution.md +40 -40
  20. package/reference/claude-config/rules/frontend.md +4 -4
  21. package/reference/claude-config/rules/observability.md +31 -31
  22. package/reference/claude-config/rules/operations.md +29 -17
  23. package/reference/claude-config/rules/organization-model.md +113 -81
  24. package/reference/claude-config/rules/organization-os.md +115 -113
  25. package/reference/claude-config/rules/package-taxonomy.md +33 -33
  26. package/reference/claude-config/rules/platform.md +42 -42
  27. package/reference/claude-config/rules/shared-types.md +49 -46
  28. package/reference/claude-config/rules/task-tracking.md +47 -47
  29. package/reference/claude-config/rules/ui.md +200 -200
  30. package/reference/claude-config/rules/vibe.md +235 -235
  31. package/reference/claude-config/scripts/statusline-command.js +18 -18
  32. package/reference/claude-config/settings.json +34 -34
  33. package/reference/claude-config/skills/deploy/{SKILL.md → skill.md} +156 -156
  34. package/reference/claude-config/skills/dsp/SKILL.md +66 -66
  35. package/reference/claude-config/skills/elevasis/SKILL.md +235 -235
  36. package/reference/claude-config/skills/explore/SKILL.md +6 -6
  37. package/reference/claude-config/skills/git-sync/SKILL.md +126 -126
  38. package/reference/claude-config/skills/knowledge/SKILL.md +314 -299
  39. package/reference/claude-config/skills/knowledge/operations/codify-level-a.md +100 -100
  40. package/reference/claude-config/skills/knowledge/operations/codify-level-b.md +159 -159
  41. package/reference/claude-config/skills/knowledge/operations/customers.md +109 -109
  42. package/reference/claude-config/skills/knowledge/operations/features.md +76 -76
  43. package/reference/claude-config/skills/knowledge/operations/goals.md +118 -118
  44. package/reference/claude-config/skills/knowledge/operations/identity.md +93 -93
  45. package/reference/claude-config/skills/knowledge/operations/labels.md +94 -94
  46. package/reference/claude-config/skills/knowledge/operations/offerings.md +109 -109
  47. package/reference/claude-config/skills/knowledge/operations/roles.md +99 -99
  48. package/reference/claude-config/skills/knowledge/operations/techStack.md +30 -30
  49. package/reference/claude-config/skills/project/SKILL.md +1088 -1088
  50. package/reference/claude-config/skills/run-ui/SKILL.md +73 -73
  51. package/reference/claude-config/skills/save/SKILL.md +3 -3
  52. package/reference/claude-config/skills/setup/SKILL.md +275 -275
  53. package/reference/claude-config/skills/status/SKILL.md +59 -59
  54. package/reference/claude-config/skills/submit-request/SKILL.md +180 -180
  55. package/reference/claude-config/skills/sync/SKILL.md +47 -47
  56. package/reference/claude-config/skills/tutorial/SKILL.md +259 -259
  57. package/reference/claude-config/skills/tutorial/progress-template.md +74 -74
  58. package/reference/claude-config/skills/tutorial/technical.md +1303 -1303
  59. package/reference/claude-config/skills/tutorial/vibe-coder.md +890 -890
  60. package/reference/claude-config/sync-notes/2026-04-22-git-sync-and-sync-notes.md +27 -27
  61. package/reference/claude-config/sync-notes/2026-04-22-lead-gen-deliverability-removal.md +30 -30
  62. package/reference/claude-config/sync-notes/2026-04-24-test-utils-and-template-tests.md +73 -73
  63. package/reference/claude-config/sync-notes/2026-04-24-ui-consolidation-and-sdk-cli-train.md +86 -86
  64. package/reference/claude-config/sync-notes/2026-04-25-auth-role-system-and-settings-roles.md +55 -55
  65. package/reference/claude-config/sync-notes/2026-04-27-crm-hitl-action-layer-cutover.md +97 -97
  66. package/reference/claude-config/sync-notes/2026-04-27-lead-gen-substrate-train.md +112 -112
  67. package/reference/claude-config/sync-notes/2026-04-29-crm-state-and-lead-gen-processing-status.md +93 -93
  68. package/reference/claude-config/sync-notes/2026-05-02-crm-ownership-next-action.md +58 -58
  69. package/reference/claude-config/sync-notes/2026-05-02-template-hardcode-workos-config.md +56 -56
  70. package/reference/claude-config/sync-notes/2026-05-04-elevasis-workspace.md +71 -71
  71. package/reference/claude-config/sync-notes/2026-05-04-knowledge-bundle.md +83 -83
  72. package/reference/claude-config/sync-notes/2026-05-04-template-skills-run-ui-and-tutorial.md +59 -59
  73. package/reference/claude-config/sync-notes/2026-05-05-list-builder.md +42 -42
  74. package/reference/claude-config/sync-notes/2026-05-06-crm-spine.md +60 -60
  75. package/reference/claude-config/sync-notes/2026-05-06-sdk-changes-release-train.md +37 -37
  76. package/reference/claude-config/sync-notes/2026-05-07-sdk-changes-release-train.md +34 -34
  77. package/reference/claude-config/sync-notes/2026-05-08-resource-governance-scaffold-guidance.md +38 -38
  78. package/reference/claude-config/sync-notes/2026-05-09-clients-domain.md +32 -32
  79. package/reference/claude-config/sync-notes/2026-05-09-command-system.md +33 -33
  80. package/reference/claude-config/sync-notes/2026-05-09-resource-governance-and-misc.md +69 -69
  81. package/reference/claude-config/sync-notes/2026-05-12-sdk-ready-release-train.md +30 -30
  82. package/reference/claude-config/sync-notes/2026-05-14-organization-model-ontology-refactor.md +45 -0
  83. package/reference/claude-config/sync-notes/README.md +43 -43
  84. package/reference/cli.mdx +808 -808
  85. package/reference/concepts.mdx +146 -146
  86. package/reference/deployment/api.mdx +297 -297
  87. package/reference/deployment/command-center.mdx +209 -209
  88. package/reference/deployment/index.mdx +195 -195
  89. package/reference/deployment/provided-features.mdx +107 -107
  90. package/reference/deployment/ui-execution.mdx +250 -250
  91. package/reference/examples/organization-model.ts +171 -84
  92. package/reference/framework/agent.mdx +156 -156
  93. package/reference/framework/index.mdx +195 -195
  94. package/reference/framework/interaction-guidance.mdx +182 -182
  95. package/reference/framework/memory.mdx +326 -326
  96. package/reference/framework/project-structure.mdx +282 -282
  97. package/reference/framework/tutorial-system.mdx +135 -135
  98. package/reference/getting-started.mdx +142 -142
  99. package/reference/index.mdx +106 -106
  100. package/reference/packages/core/src/README.md +14 -14
  101. package/reference/packages/core/src/business/README.md +2 -2
  102. package/reference/packages/core/src/knowledge/README.md +32 -32
  103. package/reference/packages/core/src/organization-model/README.md +149 -149
  104. package/reference/packages/core/src/test-utils/README.md +37 -37
  105. package/reference/packages/ui/src/api/README.md +18 -18
  106. package/reference/packages/ui/src/app/README.md +24 -24
  107. package/reference/packages/ui/src/auth/README.md +18 -18
  108. package/reference/packages/ui/src/components/README.md +24 -24
  109. package/reference/packages/ui/src/execution/README.md +16 -16
  110. package/reference/packages/ui/src/features/README.md +28 -28
  111. package/reference/packages/ui/src/graph/README.md +16 -16
  112. package/reference/packages/ui/src/hooks/README.md +23 -23
  113. package/reference/packages/ui/src/initialization/README.md +19 -19
  114. package/reference/packages/ui/src/knowledge/README.md +31 -31
  115. package/reference/packages/ui/src/organization/README.md +18 -18
  116. package/reference/packages/ui/src/profile/README.md +19 -19
  117. package/reference/packages/ui/src/provider/README.md +32 -32
  118. package/reference/packages/ui/src/router/README.md +18 -18
  119. package/reference/packages/ui/src/sse/README.md +13 -13
  120. package/reference/packages/ui/src/test-utils/README.md +7 -7
  121. package/reference/packages/ui/src/theme/README.md +23 -23
  122. package/reference/packages/ui/src/theme/presets/README.md +19 -19
  123. package/reference/packages/ui/src/types/README.md +16 -16
  124. package/reference/packages/ui/src/utils/README.md +18 -18
  125. package/reference/packages/ui/src/zustand/README.md +18 -18
  126. package/reference/platform-tools/adapters-integration.mdx +301 -301
  127. package/reference/platform-tools/adapters-platform.mdx +553 -553
  128. package/reference/platform-tools/index.mdx +217 -217
  129. package/reference/platform-tools/type-safety.mdx +82 -82
  130. package/reference/resources/index.mdx +349 -349
  131. package/reference/resources/patterns.mdx +449 -449
  132. package/reference/resources/types.mdx +116 -116
  133. package/reference/roadmap.mdx +165 -165
  134. package/reference/runtime.mdx +173 -173
  135. package/reference/scaffold/core/organization-graph.mdx +110 -90
  136. package/reference/scaffold/core/organization-model.mdx +225 -213
  137. package/reference/scaffold/index.mdx +67 -67
  138. package/reference/scaffold/operations/propagation-pipeline.md +77 -77
  139. package/reference/scaffold/operations/scaffold-maintenance.md +12 -12
  140. package/reference/scaffold/operations/workflow-recipes.md +138 -138
  141. package/reference/scaffold/recipes/add-a-feature.md +307 -85
  142. package/reference/scaffold/recipes/add-a-resource.md +137 -103
  143. package/reference/scaffold/recipes/customize-knowledge-browser.md +5 -5
  144. package/reference/scaffold/recipes/customize-organization-model.md +275 -138
  145. package/reference/scaffold/recipes/extend-a-base-entity.md +8 -8
  146. package/reference/scaffold/recipes/extend-crm.md +3 -3
  147. package/reference/scaffold/recipes/extend-lead-gen.md +394 -394
  148. package/reference/scaffold/recipes/gate-by-feature-or-admin.md +118 -118
  149. package/reference/scaffold/recipes/index.md +46 -46
  150. package/reference/scaffold/recipes/query-the-knowledge-graph.md +197 -170
  151. package/reference/scaffold/reference/contracts.md +2136 -2093
  152. package/reference/scaffold/reference/glossary.md +76 -76
  153. package/reference/scaffold/ui/composition-extensibility.mdx +233 -233
  154. package/reference/scaffold/ui/customization.md +243 -243
  155. package/reference/scaffold/ui/feature-flags-and-gating.md +46 -46
  156. package/reference/scaffold/ui/feature-shell.mdx +72 -72
  157. package/reference/scaffold/ui/recipes.md +221 -214
  158. package/reference/spine/spine-primer.md +96 -96
  159. package/reference/templates/index.mdx +47 -47
  160. package/reference/troubleshooting.mdx +223 -223
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elevasis/sdk",
3
- "version": "1.21.0",
3
+ "version": "1.22.1",
4
4
  "description": "SDK for building Elevasis organization resources",
5
5
  "type": "module",
6
6
  "bin": {
@@ -57,7 +57,7 @@
57
57
  "tsup": "^8.0.0",
58
58
  "typescript": "5.9.2",
59
59
  "zod": "^4.1.0",
60
- "@repo/core": "0.23.0",
60
+ "@repo/core": "0.24.1",
61
61
  "@repo/typescript-config": "0.0.0",
62
62
  "@repo/eslint-config": "0.0.0"
63
63
  },
@@ -1,98 +1,98 @@
1
- #!/usr/bin/env node
2
- // post-edit-validate.mjs
3
- // PostToolUse hook — auto-formats with prettier, type-checks .ts/.tsx files.
4
- // Fires after Edit|Write|MultiEdit succeeds.
5
-
6
- import { existsSync, readFileSync } from 'node:fs'
7
- import { resolve, normalize, extname, join, dirname, relative } from 'node:path'
8
- import { execSync } from 'node:child_process'
9
-
10
- const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
11
-
12
- // Extensions prettier should format
13
- const PRETTIER_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md'])
14
-
15
- // Extensions that trigger type-checking
16
- const TS_EXTENSIONS = new Set(['.ts', '.tsx'])
17
-
18
- function findNearestTsconfig(startDir) {
19
- let dir = startDir
20
- const root = normalize(ROOT)
21
- while (dir.length >= root.length) {
22
- const candidate = join(dir, 'tsconfig.json')
23
- if (existsSync(candidate)) return candidate
24
- const parent = dirname(dir)
25
- if (parent === dir) break
26
- dir = parent
27
- }
28
- return null
29
- }
30
-
31
- try {
32
- const chunks = []
33
- for await (const chunk of process.stdin) chunks.push(chunk)
34
- const input = JSON.parse(Buffer.concat(chunks).toString())
35
-
36
- const filePath = input.tool_input?.file_path
37
- if (!filePath) process.exit(0)
38
-
39
- const ext = extname(filePath).toLowerCase()
40
- const absPath = normalize(resolve(filePath))
41
- if (!existsSync(absPath)) process.exit(0)
42
-
43
- const results = []
44
-
45
- // 1. Prettier (skip silently if not installed yet, e.g. before first pnpm install)
46
- if (PRETTIER_EXTENSIONS.has(ext)) {
47
- try {
48
- execSync('pnpm exec prettier --version', { cwd: ROOT, stdio: 'pipe', timeout: 5_000 })
49
- try {
50
- execSync('pnpm exec prettier --write "' + absPath + '"', {
51
- cwd: ROOT,
52
- stdio: ['pipe', 'pipe', 'pipe'],
53
- timeout: 10_000
54
- })
55
- } catch (err) {
56
- const stderr = err.stderr?.toString().trim() || ''
57
- if (stderr && !/ignored/i.test(stderr)) {
58
- results.push('Prettier error: ' + stderr.slice(0, 300))
59
- }
60
- }
61
- } catch {
62
- // prettier not installed yet -- skip silently
63
- }
64
- }
65
-
66
- // 2. Type-check for .ts/.tsx
67
- if (TS_EXTENSIONS.has(ext)) {
68
- const tsconfig = findNearestTsconfig(dirname(absPath))
69
- if (tsconfig) {
70
- try {
71
- execSync('pnpm exec tsc --noEmit -p "' + tsconfig + '"', {
72
- cwd: ROOT,
73
- stdio: ['pipe', 'pipe', 'pipe'],
74
- timeout: 30_000,
75
- env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=4096' }
76
- })
77
- } catch (err) {
78
- if (err.killed) process.exit(0) // Don't block on timeout
79
- const stdout = err.stdout?.toString() || ''
80
- if (stdout.includes('error TS')) {
81
- const errorLines = stdout
82
- .split('\n')
83
- .filter((l) => l.includes('error TS'))
84
- .slice(0, 10)
85
- results.push('Type errors after editing ' + filePath + ':\n' + errorLines.join('\n'))
86
- }
87
- }
88
- }
89
- }
90
-
91
- // Output errors to Claude's context (silence = success)
92
- if (results.length > 0) {
93
- process.stderr.write(results.join('\n\n'))
94
- process.exit(2) // Exit 2 = send stderr as feedback to Claude
95
- }
96
- } catch {}
97
-
98
- process.exit(0)
1
+ #!/usr/bin/env node
2
+ // post-edit-validate.mjs
3
+ // PostToolUse hook — auto-formats with prettier, type-checks .ts/.tsx files.
4
+ // Fires after Edit|Write|MultiEdit succeeds.
5
+
6
+ import { existsSync, readFileSync } from 'node:fs'
7
+ import { resolve, normalize, extname, join, dirname, relative } from 'node:path'
8
+ import { execSync } from 'node:child_process'
9
+
10
+ const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
11
+
12
+ // Extensions prettier should format
13
+ const PRETTIER_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.json', '.css', '.md'])
14
+
15
+ // Extensions that trigger type-checking
16
+ const TS_EXTENSIONS = new Set(['.ts', '.tsx'])
17
+
18
+ function findNearestTsconfig(startDir) {
19
+ let dir = startDir
20
+ const root = normalize(ROOT)
21
+ while (dir.length >= root.length) {
22
+ const candidate = join(dir, 'tsconfig.json')
23
+ if (existsSync(candidate)) return candidate
24
+ const parent = dirname(dir)
25
+ if (parent === dir) break
26
+ dir = parent
27
+ }
28
+ return null
29
+ }
30
+
31
+ try {
32
+ const chunks = []
33
+ for await (const chunk of process.stdin) chunks.push(chunk)
34
+ const input = JSON.parse(Buffer.concat(chunks).toString())
35
+
36
+ const filePath = input.tool_input?.file_path
37
+ if (!filePath) process.exit(0)
38
+
39
+ const ext = extname(filePath).toLowerCase()
40
+ const absPath = normalize(resolve(filePath))
41
+ if (!existsSync(absPath)) process.exit(0)
42
+
43
+ const results = []
44
+
45
+ // 1. Prettier (skip silently if not installed yet, e.g. before first pnpm install)
46
+ if (PRETTIER_EXTENSIONS.has(ext)) {
47
+ try {
48
+ execSync('pnpm exec prettier --version', { cwd: ROOT, stdio: 'pipe', timeout: 5_000 })
49
+ try {
50
+ execSync('pnpm exec prettier --write "' + absPath + '"', {
51
+ cwd: ROOT,
52
+ stdio: ['pipe', 'pipe', 'pipe'],
53
+ timeout: 10_000
54
+ })
55
+ } catch (err) {
56
+ const stderr = err.stderr?.toString().trim() || ''
57
+ if (stderr && !/ignored/i.test(stderr)) {
58
+ results.push('Prettier error: ' + stderr.slice(0, 300))
59
+ }
60
+ }
61
+ } catch {
62
+ // prettier not installed yet -- skip silently
63
+ }
64
+ }
65
+
66
+ // 2. Type-check for .ts/.tsx
67
+ if (TS_EXTENSIONS.has(ext)) {
68
+ const tsconfig = findNearestTsconfig(dirname(absPath))
69
+ if (tsconfig) {
70
+ try {
71
+ execSync('pnpm exec tsc --noEmit -p "' + tsconfig + '"', {
72
+ cwd: ROOT,
73
+ stdio: ['pipe', 'pipe', 'pipe'],
74
+ timeout: 30_000,
75
+ env: { ...process.env, NODE_OPTIONS: '--max-old-space-size=4096' }
76
+ })
77
+ } catch (err) {
78
+ if (err.killed) process.exit(0) // Don't block on timeout
79
+ const stdout = err.stdout?.toString() || ''
80
+ if (stdout.includes('error TS')) {
81
+ const errorLines = stdout
82
+ .split('\n')
83
+ .filter((l) => l.includes('error TS'))
84
+ .slice(0, 10)
85
+ results.push('Type errors after editing ' + filePath + ':\n' + errorLines.join('\n'))
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ // Output errors to Claude's context (silence = success)
92
+ if (results.length > 0) {
93
+ process.stderr.write(results.join('\n\n'))
94
+ process.exit(2) // Exit 2 = send stderr as feedback to Claude
95
+ }
96
+ } catch {}
97
+
98
+ process.exit(0)
@@ -1,188 +1,188 @@
1
- #!/usr/bin/env node
2
- // scaffold-registry-reminder.mjs
3
- // PostToolUse hook — reads the compiled scaffold registry and emits advisory
4
- // reminders when an edited file matches a registry source pattern.
5
- //
6
- // Template twin of the monorepo hook. Gracefully no-ops when the compiled
7
- // registry is absent (e.g. before SDK delivers scaffold-registry.compiled.json
8
- // to external projects — Step 7/SDK milestone).
9
- //
10
- // Exit 0 always (advisory hook — never blocks).
11
-
12
- import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
13
- import { join, normalize, relative } from 'node:path'
14
-
15
- const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
16
- const LOG_DIR = join(ROOT, '.claude', 'logs')
17
- const LOG_FILE = join(LOG_DIR, 'scaffold-registry-reminder.log')
18
- const STATE_FILE = join(LOG_DIR, 'scaffold-registry-reminder.state.json')
19
- const REGISTRY_FILE = join(ROOT, '.claude', 'scaffold-registry.compiled.json')
20
-
21
- const DEFAULT_COOLDOWN_MS = 300_000 // 5 minutes
22
-
23
- const GENERATED_DIR_SEGMENTS = ['_generated', '_gen']
24
- const GENERATED_CONTENT_MARKER = '@generated'
25
-
26
- function log(msg) {
27
- try {
28
- mkdirSync(LOG_DIR, { recursive: true })
29
- appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`)
30
- } catch {}
31
- }
32
-
33
- function pathMatchesPattern(filePath, pattern) {
34
- const normalizedFile = filePath.replace(/\\/g, '/')
35
- const normalizedPattern = pattern.replace(/\\/g, '/')
36
-
37
- if (normalizedFile === normalizedPattern) return true
38
-
39
- if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
40
- const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
41
- return normalizedFile.startsWith(prefix + '/')
42
- }
43
-
44
- if (normalizedPattern.includes('*')) {
45
- const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
46
- return new RegExp(`^${escaped}$`).test(normalizedFile)
47
- }
48
-
49
- return normalizedFile.startsWith(normalizedPattern + '/')
50
- }
51
-
52
- function loadState() {
53
- try {
54
- return JSON.parse(readFileSync(STATE_FILE, 'utf-8'))
55
- } catch {
56
- return {}
57
- }
58
- }
59
-
60
- function saveState(state) {
61
- try {
62
- mkdirSync(LOG_DIR, { recursive: true })
63
- writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\n', 'utf-8')
64
- } catch {}
65
- }
66
-
67
- function throttleKey(entryId, filePath) {
68
- return `${entryId}:${filePath}`
69
- }
70
-
71
- function isCoolingDown(state, key, cooldownMs) {
72
- const last = state[key]
73
- if (!last) return false
74
- return Date.now() - last < cooldownMs
75
- }
76
-
77
- function formatDependentLine(dep) {
78
- const regen = dep.regen === 'manual' ? 'manual check' : dep.regen
79
- const hint = dep.hint ? ` [${dep.hint}]` : ''
80
- return ` - ${dep.path} -> ${regen}${hint}`
81
- }
82
-
83
- function emitReminder(entry, relFilePath) {
84
- const lines = [
85
- `\uD83D\uDD14 Scaffold reminder -- ${entry.id} (${relFilePath})`,
86
- ` Downstream scaffolds that may need updating:`
87
- ]
88
- for (const dep of entry.dependents) {
89
- lines.push(formatDependentLine(dep))
90
- }
91
- lines.push(
92
- ` If this is a scaffold-sensitive pattern not in the registry, also add an entry to .claude/scaffold-registry.yml.`
93
- )
94
- return lines.join('\n')
95
- }
96
-
97
- function emitMissingEntryHint(relFilePath) {
98
- return [
99
- `\uD83D\uDD14 Scaffold reminder -- unregistered generated path (${relFilePath})`,
100
- ` This path looks scaffold-generated but has no registry entry.`,
101
- ` If it is scaffold-sensitive, add a new entry to .claude/scaffold-registry.yml`,
102
- ` so the reminder hook and /work handoff can track it.`
103
- ].join('\n')
104
- }
105
-
106
- function looksLikeGeneratedPath(filePath) {
107
- const normalizedFile = filePath.replace(/\\/g, '/')
108
- const segments = normalizedFile.split('/')
109
- return segments.some((seg) => GENERATED_DIR_SEGMENTS.includes(seg))
110
- }
111
-
112
- function looksLikeGeneratedContent(absFilePath) {
113
- try {
114
- const content = readFileSync(absFilePath, 'utf-8').slice(0, 500)
115
- return content.includes(GENERATED_CONTENT_MARKER)
116
- } catch {
117
- return false
118
- }
119
- }
120
-
121
- try {
122
- const chunks = []
123
- for await (const chunk of process.stdin) chunks.push(chunk)
124
- const input = JSON.parse(Buffer.concat(chunks).toString())
125
-
126
- const rawFilePath = input.tool_input?.file_path
127
- if (!rawFilePath) process.exit(0)
128
-
129
- const absFilePath = normalize(rawFilePath)
130
- const relFilePath = relative(ROOT, absFilePath).replace(/\\/g, '/')
131
-
132
- // Graceful no-op when registry is absent (pre-SDK-delivery state)
133
- let registry
134
- try {
135
- const raw = readFileSync(REGISTRY_FILE, 'utf-8')
136
- registry = JSON.parse(raw)
137
- } catch {
138
- log(`SKIP — registry not found (pre-SDK-delivery)`)
139
- process.exit(0)
140
- }
141
-
142
- const entries = registry?.entries ?? []
143
-
144
- const matched = entries.filter((entry) =>
145
- (entry.sources ?? []).some((pattern) => pathMatchesPattern(relFilePath, pattern))
146
- )
147
-
148
- const state = loadState()
149
- const now = Date.now()
150
- const messages = []
151
-
152
- if (matched.length > 0) {
153
- for (const entry of matched) {
154
- const key = throttleKey(entry.id, relFilePath)
155
- const cooldown = entry.cooldown_ms ?? DEFAULT_COOLDOWN_MS
156
- if (isCoolingDown(state, key, cooldown)) {
157
- log(`THROTTLED — ${entry.id} for ${relFilePath}`)
158
- continue
159
- }
160
- messages.push(emitReminder(entry, relFilePath))
161
- state[key] = now
162
- log(`EMITTED — ${entry.id} for ${relFilePath}`)
163
- }
164
- } else {
165
- const isGenerated = looksLikeGeneratedPath(relFilePath) || looksLikeGeneratedContent(absFilePath)
166
-
167
- if (isGenerated) {
168
- const key = throttleKey('__missing__', relFilePath)
169
- if (!isCoolingDown(state, key, DEFAULT_COOLDOWN_MS)) {
170
- messages.push(emitMissingEntryHint(relFilePath))
171
- state[key] = now
172
- log(`EMITTED missing-entry hint for ${relFilePath}`)
173
- } else {
174
- log(`THROTTLED missing-entry hint for ${relFilePath}`)
175
- }
176
- }
177
- }
178
-
179
- if (messages.length > 0) {
180
- saveState(state)
181
- process.stderr.write(messages.join('\n\n') + '\n')
182
- process.exit(2)
183
- }
184
- } catch (err) {
185
- log(`ERROR: ${err.message}`)
186
- }
187
-
188
- process.exit(0)
1
+ #!/usr/bin/env node
2
+ // scaffold-registry-reminder.mjs
3
+ // PostToolUse hook — reads the compiled scaffold registry and emits advisory
4
+ // reminders when an edited file matches a registry source pattern.
5
+ //
6
+ // Template twin of the monorepo hook. Gracefully no-ops when the compiled
7
+ // registry is absent (e.g. before SDK delivers scaffold-registry.compiled.json
8
+ // to external projects — Step 7/SDK milestone).
9
+ //
10
+ // Exit 0 always (advisory hook — never blocks).
11
+
12
+ import { readFileSync, writeFileSync, mkdirSync, appendFileSync } from 'node:fs'
13
+ import { join, normalize, relative } from 'node:path'
14
+
15
+ const ROOT = process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
16
+ const LOG_DIR = join(ROOT, '.claude', 'logs')
17
+ const LOG_FILE = join(LOG_DIR, 'scaffold-registry-reminder.log')
18
+ const STATE_FILE = join(LOG_DIR, 'scaffold-registry-reminder.state.json')
19
+ const REGISTRY_FILE = join(ROOT, '.claude', 'scaffold-registry.compiled.json')
20
+
21
+ const DEFAULT_COOLDOWN_MS = 300_000 // 5 minutes
22
+
23
+ const GENERATED_DIR_SEGMENTS = ['_generated', '_gen']
24
+ const GENERATED_CONTENT_MARKER = '@generated'
25
+
26
+ function log(msg) {
27
+ try {
28
+ mkdirSync(LOG_DIR, { recursive: true })
29
+ appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`)
30
+ } catch {}
31
+ }
32
+
33
+ function pathMatchesPattern(filePath, pattern) {
34
+ const normalizedFile = filePath.replace(/\\/g, '/')
35
+ const normalizedPattern = pattern.replace(/\\/g, '/')
36
+
37
+ if (normalizedFile === normalizedPattern) return true
38
+
39
+ if (normalizedPattern.endsWith('/**') || normalizedPattern.endsWith('/*')) {
40
+ const prefix = normalizedPattern.slice(0, normalizedPattern.lastIndexOf('/*'))
41
+ return normalizedFile.startsWith(prefix + '/')
42
+ }
43
+
44
+ if (normalizedPattern.includes('*')) {
45
+ const escaped = normalizedPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')
46
+ return new RegExp(`^${escaped}$`).test(normalizedFile)
47
+ }
48
+
49
+ return normalizedFile.startsWith(normalizedPattern + '/')
50
+ }
51
+
52
+ function loadState() {
53
+ try {
54
+ return JSON.parse(readFileSync(STATE_FILE, 'utf-8'))
55
+ } catch {
56
+ return {}
57
+ }
58
+ }
59
+
60
+ function saveState(state) {
61
+ try {
62
+ mkdirSync(LOG_DIR, { recursive: true })
63
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2) + '\n', 'utf-8')
64
+ } catch {}
65
+ }
66
+
67
+ function throttleKey(entryId, filePath) {
68
+ return `${entryId}:${filePath}`
69
+ }
70
+
71
+ function isCoolingDown(state, key, cooldownMs) {
72
+ const last = state[key]
73
+ if (!last) return false
74
+ return Date.now() - last < cooldownMs
75
+ }
76
+
77
+ function formatDependentLine(dep) {
78
+ const regen = dep.regen === 'manual' ? 'manual check' : dep.regen
79
+ const hint = dep.hint ? ` [${dep.hint}]` : ''
80
+ return ` - ${dep.path} -> ${regen}${hint}`
81
+ }
82
+
83
+ function emitReminder(entry, relFilePath) {
84
+ const lines = [
85
+ `\uD83D\uDD14 Scaffold reminder -- ${entry.id} (${relFilePath})`,
86
+ ` Downstream scaffolds that may need updating:`
87
+ ]
88
+ for (const dep of entry.dependents) {
89
+ lines.push(formatDependentLine(dep))
90
+ }
91
+ lines.push(
92
+ ` If this is a scaffold-sensitive pattern not in the registry, also add an entry to .claude/scaffold-registry.yml.`
93
+ )
94
+ return lines.join('\n')
95
+ }
96
+
97
+ function emitMissingEntryHint(relFilePath) {
98
+ return [
99
+ `\uD83D\uDD14 Scaffold reminder -- unregistered generated path (${relFilePath})`,
100
+ ` This path looks scaffold-generated but has no registry entry.`,
101
+ ` If it is scaffold-sensitive, add a new entry to .claude/scaffold-registry.yml`,
102
+ ` so the reminder hook and /work handoff can track it.`
103
+ ].join('\n')
104
+ }
105
+
106
+ function looksLikeGeneratedPath(filePath) {
107
+ const normalizedFile = filePath.replace(/\\/g, '/')
108
+ const segments = normalizedFile.split('/')
109
+ return segments.some((seg) => GENERATED_DIR_SEGMENTS.includes(seg))
110
+ }
111
+
112
+ function looksLikeGeneratedContent(absFilePath) {
113
+ try {
114
+ const content = readFileSync(absFilePath, 'utf-8').slice(0, 500)
115
+ return content.includes(GENERATED_CONTENT_MARKER)
116
+ } catch {
117
+ return false
118
+ }
119
+ }
120
+
121
+ try {
122
+ const chunks = []
123
+ for await (const chunk of process.stdin) chunks.push(chunk)
124
+ const input = JSON.parse(Buffer.concat(chunks).toString())
125
+
126
+ const rawFilePath = input.tool_input?.file_path
127
+ if (!rawFilePath) process.exit(0)
128
+
129
+ const absFilePath = normalize(rawFilePath)
130
+ const relFilePath = relative(ROOT, absFilePath).replace(/\\/g, '/')
131
+
132
+ // Graceful no-op when registry is absent (pre-SDK-delivery state)
133
+ let registry
134
+ try {
135
+ const raw = readFileSync(REGISTRY_FILE, 'utf-8')
136
+ registry = JSON.parse(raw)
137
+ } catch {
138
+ log(`SKIP — registry not found (pre-SDK-delivery)`)
139
+ process.exit(0)
140
+ }
141
+
142
+ const entries = registry?.entries ?? []
143
+
144
+ const matched = entries.filter((entry) =>
145
+ (entry.sources ?? []).some((pattern) => pathMatchesPattern(relFilePath, pattern))
146
+ )
147
+
148
+ const state = loadState()
149
+ const now = Date.now()
150
+ const messages = []
151
+
152
+ if (matched.length > 0) {
153
+ for (const entry of matched) {
154
+ const key = throttleKey(entry.id, relFilePath)
155
+ const cooldown = entry.cooldown_ms ?? DEFAULT_COOLDOWN_MS
156
+ if (isCoolingDown(state, key, cooldown)) {
157
+ log(`THROTTLED — ${entry.id} for ${relFilePath}`)
158
+ continue
159
+ }
160
+ messages.push(emitReminder(entry, relFilePath))
161
+ state[key] = now
162
+ log(`EMITTED — ${entry.id} for ${relFilePath}`)
163
+ }
164
+ } else {
165
+ const isGenerated = looksLikeGeneratedPath(relFilePath) || looksLikeGeneratedContent(absFilePath)
166
+
167
+ if (isGenerated) {
168
+ const key = throttleKey('__missing__', relFilePath)
169
+ if (!isCoolingDown(state, key, DEFAULT_COOLDOWN_MS)) {
170
+ messages.push(emitMissingEntryHint(relFilePath))
171
+ state[key] = now
172
+ log(`EMITTED missing-entry hint for ${relFilePath}`)
173
+ } else {
174
+ log(`THROTTLED missing-entry hint for ${relFilePath}`)
175
+ }
176
+ }
177
+ }
178
+
179
+ if (messages.length > 0) {
180
+ saveState(state)
181
+ process.stderr.write(messages.join('\n\n') + '\n')
182
+ process.exit(2)
183
+ }
184
+ } catch (err) {
185
+ log(`ERROR: ${err.message}`)
186
+ }
187
+
188
+ process.exit(0)