@lumenflow/core 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +119 -0
  3. package/dist/active-wu-detector.d.ts +33 -0
  4. package/dist/active-wu-detector.js +106 -0
  5. package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
  6. package/dist/adapters/filesystem-metrics.adapter.js +519 -0
  7. package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
  8. package/dist/adapters/terminal-renderer.adapter.js +337 -0
  9. package/dist/arg-parser.d.ts +63 -0
  10. package/dist/arg-parser.js +560 -0
  11. package/dist/backlog-editor.d.ts +98 -0
  12. package/dist/backlog-editor.js +179 -0
  13. package/dist/backlog-generator.d.ts +111 -0
  14. package/dist/backlog-generator.js +381 -0
  15. package/dist/backlog-parser.d.ts +45 -0
  16. package/dist/backlog-parser.js +102 -0
  17. package/dist/backlog-sync-validator.d.ts +78 -0
  18. package/dist/backlog-sync-validator.js +294 -0
  19. package/dist/branch-drift.d.ts +34 -0
  20. package/dist/branch-drift.js +51 -0
  21. package/dist/cleanup-install-config.d.ts +33 -0
  22. package/dist/cleanup-install-config.js +37 -0
  23. package/dist/cleanup-lock.d.ts +139 -0
  24. package/dist/cleanup-lock.js +313 -0
  25. package/dist/code-path-validator.d.ts +146 -0
  26. package/dist/code-path-validator.js +537 -0
  27. package/dist/code-paths-overlap.d.ts +55 -0
  28. package/dist/code-paths-overlap.js +245 -0
  29. package/dist/commands-logger.d.ts +77 -0
  30. package/dist/commands-logger.js +254 -0
  31. package/dist/commit-message-utils.d.ts +25 -0
  32. package/dist/commit-message-utils.js +41 -0
  33. package/dist/compliance-parser.d.ts +150 -0
  34. package/dist/compliance-parser.js +507 -0
  35. package/dist/constants/backlog-patterns.d.ts +20 -0
  36. package/dist/constants/backlog-patterns.js +23 -0
  37. package/dist/constants/dora-constants.d.ts +49 -0
  38. package/dist/constants/dora-constants.js +53 -0
  39. package/dist/constants/gate-constants.d.ts +15 -0
  40. package/dist/constants/gate-constants.js +15 -0
  41. package/dist/constants/linter-constants.d.ts +16 -0
  42. package/dist/constants/linter-constants.js +16 -0
  43. package/dist/constants/tokenizer-constants.d.ts +15 -0
  44. package/dist/constants/tokenizer-constants.js +15 -0
  45. package/dist/core/scope-checker.d.ts +97 -0
  46. package/dist/core/scope-checker.js +163 -0
  47. package/dist/core/tool-runner.d.ts +161 -0
  48. package/dist/core/tool-runner.js +393 -0
  49. package/dist/core/tool.constants.d.ts +105 -0
  50. package/dist/core/tool.constants.js +101 -0
  51. package/dist/core/tool.schemas.d.ts +226 -0
  52. package/dist/core/tool.schemas.js +226 -0
  53. package/dist/core/worktree-guard.d.ts +130 -0
  54. package/dist/core/worktree-guard.js +242 -0
  55. package/dist/coverage-gate.d.ts +108 -0
  56. package/dist/coverage-gate.js +196 -0
  57. package/dist/date-utils.d.ts +75 -0
  58. package/dist/date-utils.js +140 -0
  59. package/dist/dependency-graph.d.ts +142 -0
  60. package/dist/dependency-graph.js +550 -0
  61. package/dist/dependency-guard.d.ts +54 -0
  62. package/dist/dependency-guard.js +142 -0
  63. package/dist/dependency-validator.d.ts +105 -0
  64. package/dist/dependency-validator.js +154 -0
  65. package/dist/docs-path-validator.d.ts +36 -0
  66. package/dist/docs-path-validator.js +95 -0
  67. package/dist/domain/orchestration.constants.d.ts +99 -0
  68. package/dist/domain/orchestration.constants.js +97 -0
  69. package/dist/domain/orchestration.schemas.d.ts +280 -0
  70. package/dist/domain/orchestration.schemas.js +211 -0
  71. package/dist/domain/orchestration.types.d.ts +133 -0
  72. package/dist/domain/orchestration.types.js +12 -0
  73. package/dist/error-handler.d.ts +116 -0
  74. package/dist/error-handler.js +136 -0
  75. package/dist/file-classifiers.d.ts +62 -0
  76. package/dist/file-classifiers.js +108 -0
  77. package/dist/gates-agent-mode.d.ts +81 -0
  78. package/dist/gates-agent-mode.js +94 -0
  79. package/dist/generate-traceability.d.ts +107 -0
  80. package/dist/generate-traceability.js +411 -0
  81. package/dist/git-adapter.d.ts +395 -0
  82. package/dist/git-adapter.js +649 -0
  83. package/dist/git-staged-validator.d.ts +32 -0
  84. package/dist/git-staged-validator.js +48 -0
  85. package/dist/hardcoded-strings.d.ts +61 -0
  86. package/dist/hardcoded-strings.js +270 -0
  87. package/dist/incremental-lint.d.ts +78 -0
  88. package/dist/incremental-lint.js +129 -0
  89. package/dist/incremental-test.d.ts +39 -0
  90. package/dist/incremental-test.js +61 -0
  91. package/dist/index.d.ts +42 -0
  92. package/dist/index.js +61 -0
  93. package/dist/invariants/check-automated-tests.d.ts +50 -0
  94. package/dist/invariants/check-automated-tests.js +166 -0
  95. package/dist/invariants-runner.d.ts +103 -0
  96. package/dist/invariants-runner.js +527 -0
  97. package/dist/lane-checker.d.ts +50 -0
  98. package/dist/lane-checker.js +319 -0
  99. package/dist/lane-inference.d.ts +39 -0
  100. package/dist/lane-inference.js +195 -0
  101. package/dist/lane-lock.d.ts +211 -0
  102. package/dist/lane-lock.js +474 -0
  103. package/dist/lane-validator.d.ts +48 -0
  104. package/dist/lane-validator.js +114 -0
  105. package/dist/logs-lib.d.ts +104 -0
  106. package/dist/logs-lib.js +207 -0
  107. package/dist/lumenflow-config-schema.d.ts +272 -0
  108. package/dist/lumenflow-config-schema.js +207 -0
  109. package/dist/lumenflow-config.d.ts +95 -0
  110. package/dist/lumenflow-config.js +236 -0
  111. package/dist/manual-test-validator.d.ts +80 -0
  112. package/dist/manual-test-validator.js +200 -0
  113. package/dist/merge-lock.d.ts +115 -0
  114. package/dist/merge-lock.js +251 -0
  115. package/dist/micro-worktree.d.ts +159 -0
  116. package/dist/micro-worktree.js +427 -0
  117. package/dist/migration-deployer.d.ts +69 -0
  118. package/dist/migration-deployer.js +151 -0
  119. package/dist/orchestration-advisory-loader.d.ts +28 -0
  120. package/dist/orchestration-advisory-loader.js +87 -0
  121. package/dist/orchestration-advisory.d.ts +58 -0
  122. package/dist/orchestration-advisory.js +94 -0
  123. package/dist/orchestration-di.d.ts +48 -0
  124. package/dist/orchestration-di.js +57 -0
  125. package/dist/orchestration-rules.d.ts +57 -0
  126. package/dist/orchestration-rules.js +201 -0
  127. package/dist/orphan-detector.d.ts +131 -0
  128. package/dist/orphan-detector.js +226 -0
  129. package/dist/path-classifiers.d.ts +57 -0
  130. package/dist/path-classifiers.js +93 -0
  131. package/dist/piped-command-detector.d.ts +34 -0
  132. package/dist/piped-command-detector.js +64 -0
  133. package/dist/ports/dashboard-renderer.port.d.ts +112 -0
  134. package/dist/ports/dashboard-renderer.port.js +25 -0
  135. package/dist/ports/metrics-collector.port.d.ts +132 -0
  136. package/dist/ports/metrics-collector.port.js +26 -0
  137. package/dist/process-detector.d.ts +84 -0
  138. package/dist/process-detector.js +172 -0
  139. package/dist/prompt-linter.d.ts +72 -0
  140. package/dist/prompt-linter.js +312 -0
  141. package/dist/prompt-monitor.d.ts +15 -0
  142. package/dist/prompt-monitor.js +205 -0
  143. package/dist/rebase-artifact-cleanup.d.ts +145 -0
  144. package/dist/rebase-artifact-cleanup.js +433 -0
  145. package/dist/retry-strategy.d.ts +189 -0
  146. package/dist/retry-strategy.js +283 -0
  147. package/dist/risk-detector.d.ts +108 -0
  148. package/dist/risk-detector.js +252 -0
  149. package/dist/rollback-utils.d.ts +76 -0
  150. package/dist/rollback-utils.js +104 -0
  151. package/dist/section-headings.d.ts +43 -0
  152. package/dist/section-headings.js +49 -0
  153. package/dist/spawn-escalation.d.ts +90 -0
  154. package/dist/spawn-escalation.js +253 -0
  155. package/dist/spawn-monitor.d.ts +229 -0
  156. package/dist/spawn-monitor.js +672 -0
  157. package/dist/spawn-recovery.d.ts +82 -0
  158. package/dist/spawn-recovery.js +298 -0
  159. package/dist/spawn-registry-schema.d.ts +98 -0
  160. package/dist/spawn-registry-schema.js +108 -0
  161. package/dist/spawn-registry-store.d.ts +146 -0
  162. package/dist/spawn-registry-store.js +273 -0
  163. package/dist/spawn-tree.d.ts +121 -0
  164. package/dist/spawn-tree.js +285 -0
  165. package/dist/stamp-status-validator.d.ts +84 -0
  166. package/dist/stamp-status-validator.js +134 -0
  167. package/dist/stamp-utils.d.ts +100 -0
  168. package/dist/stamp-utils.js +229 -0
  169. package/dist/state-machine.d.ts +26 -0
  170. package/dist/state-machine.js +83 -0
  171. package/dist/system-map-validator.d.ts +80 -0
  172. package/dist/system-map-validator.js +272 -0
  173. package/dist/telemetry.d.ts +80 -0
  174. package/dist/telemetry.js +213 -0
  175. package/dist/token-counter.d.ts +51 -0
  176. package/dist/token-counter.js +145 -0
  177. package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
  178. package/dist/usecases/get-dashboard-data.usecase.js +61 -0
  179. package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
  180. package/dist/usecases/get-suggestions.usecase.js +153 -0
  181. package/dist/user-normalizer.d.ts +41 -0
  182. package/dist/user-normalizer.js +141 -0
  183. package/dist/validators/phi-constants.d.ts +97 -0
  184. package/dist/validators/phi-constants.js +152 -0
  185. package/dist/validators/phi-scanner.d.ts +58 -0
  186. package/dist/validators/phi-scanner.js +215 -0
  187. package/dist/worktree-ownership.d.ts +50 -0
  188. package/dist/worktree-ownership.js +74 -0
  189. package/dist/worktree-scanner.d.ts +103 -0
  190. package/dist/worktree-scanner.js +168 -0
  191. package/dist/worktree-symlink.d.ts +99 -0
  192. package/dist/worktree-symlink.js +359 -0
  193. package/dist/wu-backlog-updater.d.ts +17 -0
  194. package/dist/wu-backlog-updater.js +37 -0
  195. package/dist/wu-checkpoint.d.ts +124 -0
  196. package/dist/wu-checkpoint.js +233 -0
  197. package/dist/wu-claim-helpers.d.ts +26 -0
  198. package/dist/wu-claim-helpers.js +63 -0
  199. package/dist/wu-claim-resume.d.ts +106 -0
  200. package/dist/wu-claim-resume.js +276 -0
  201. package/dist/wu-consistency-checker.d.ts +95 -0
  202. package/dist/wu-consistency-checker.js +567 -0
  203. package/dist/wu-constants.d.ts +1275 -0
  204. package/dist/wu-constants.js +1382 -0
  205. package/dist/wu-create-validators.d.ts +42 -0
  206. package/dist/wu-create-validators.js +93 -0
  207. package/dist/wu-done-branch-only.d.ts +63 -0
  208. package/dist/wu-done-branch-only.js +191 -0
  209. package/dist/wu-done-messages.d.ts +119 -0
  210. package/dist/wu-done-messages.js +185 -0
  211. package/dist/wu-done-pr.d.ts +72 -0
  212. package/dist/wu-done-pr.js +174 -0
  213. package/dist/wu-done-retry-helpers.d.ts +85 -0
  214. package/dist/wu-done-retry-helpers.js +172 -0
  215. package/dist/wu-done-ui.d.ts +37 -0
  216. package/dist/wu-done-ui.js +69 -0
  217. package/dist/wu-done-validators.d.ts +411 -0
  218. package/dist/wu-done-validators.js +1229 -0
  219. package/dist/wu-done-worktree.d.ts +182 -0
  220. package/dist/wu-done-worktree.js +1097 -0
  221. package/dist/wu-helpers.d.ts +128 -0
  222. package/dist/wu-helpers.js +248 -0
  223. package/dist/wu-lint.d.ts +70 -0
  224. package/dist/wu-lint.js +234 -0
  225. package/dist/wu-paths.d.ts +171 -0
  226. package/dist/wu-paths.js +178 -0
  227. package/dist/wu-preflight-validators.d.ts +86 -0
  228. package/dist/wu-preflight-validators.js +251 -0
  229. package/dist/wu-recovery.d.ts +138 -0
  230. package/dist/wu-recovery.js +341 -0
  231. package/dist/wu-repair-core.d.ts +131 -0
  232. package/dist/wu-repair-core.js +669 -0
  233. package/dist/wu-schema-normalization.d.ts +17 -0
  234. package/dist/wu-schema-normalization.js +82 -0
  235. package/dist/wu-schema.d.ts +793 -0
  236. package/dist/wu-schema.js +881 -0
  237. package/dist/wu-spawn-helpers.d.ts +121 -0
  238. package/dist/wu-spawn-helpers.js +271 -0
  239. package/dist/wu-spawn.d.ts +158 -0
  240. package/dist/wu-spawn.js +1306 -0
  241. package/dist/wu-state-schema.d.ts +213 -0
  242. package/dist/wu-state-schema.js +156 -0
  243. package/dist/wu-state-store.d.ts +264 -0
  244. package/dist/wu-state-store.js +691 -0
  245. package/dist/wu-status-transition.d.ts +63 -0
  246. package/dist/wu-status-transition.js +382 -0
  247. package/dist/wu-status-updater.d.ts +25 -0
  248. package/dist/wu-status-updater.js +116 -0
  249. package/dist/wu-transaction-collectors.d.ts +116 -0
  250. package/dist/wu-transaction-collectors.js +272 -0
  251. package/dist/wu-transaction.d.ts +170 -0
  252. package/dist/wu-transaction.js +273 -0
  253. package/dist/wu-validation-constants.d.ts +60 -0
  254. package/dist/wu-validation-constants.js +66 -0
  255. package/dist/wu-validation.d.ts +118 -0
  256. package/dist/wu-validation.js +243 -0
  257. package/dist/wu-validator.d.ts +62 -0
  258. package/dist/wu-validator.js +325 -0
  259. package/dist/wu-yaml-fixer.d.ts +97 -0
  260. package/dist/wu-yaml-fixer.js +264 -0
  261. package/dist/wu-yaml.d.ts +86 -0
  262. package/dist/wu-yaml.js +222 -0
  263. package/package.json +114 -0
@@ -0,0 +1,312 @@
1
+ /**
2
+ * Prompt Linter with 3-Tier Token Budget Enforcement
3
+ *
4
+ * Enforces token budget constraints on LLM prompts:
5
+ * - BLOCK: >450 tokens OR +>120 delta (exit 1)
6
+ * - WARN: ≥400 tokens OR +>50 delta (continue, log warning)
7
+ * - LOG: Always log tokenCount, delta, hash, top 3 longest lines
8
+ *
9
+ * Uses proper telemetry via getLogger() (no console spam).
10
+ *
11
+ * Part of WU-676: Single-Call LLM Orchestrator token budget enforcement.
12
+ */
13
+ import { analyzePrompt, getLongestLines } from './token-counter.js';
14
+ import { readFile, writeFile, mkdir, appendFile, access } from 'node:fs/promises';
15
+ import { existsSync, readFileSync } from 'node:fs';
16
+ import { resolve, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import { glob } from 'glob';
19
+ import yaml from 'yaml';
20
+ import { EXIT_CODES, STRING_LITERALS } from './wu-constants.js';
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const ROOT_DIR = resolve(__dirname, '../..');
24
+ // Default config path
25
+ const DEFAULT_CONFIG_PATH = resolve(ROOT_DIR, 'config/prompts/linter.yml');
26
+ // Telemetry cache path (for storing previous metrics)
27
+ const METRICS_CACHE_PATH = resolve(ROOT_DIR, '.beacon/telemetry/prompt-metrics.json');
28
+ /**
29
+ * Load config from YAML file with fallback to defaults
30
+ * @param {string} configPath - Path to config file (optional)
31
+ * @returns {Object} Configuration object
32
+ */
33
+ export function loadConfig(configPath = DEFAULT_CONFIG_PATH) {
34
+ // Default configuration (fallback if file missing or invalid)
35
+ const defaults = {
36
+ version: 1,
37
+ token_budgets: {
38
+ default: {
39
+ hard_cap: 450,
40
+ warn_threshold: 400,
41
+ },
42
+ retry: {
43
+ hard_cap: 150,
44
+ },
45
+ },
46
+ delta_budgets: {
47
+ warn: 50,
48
+ block: 120,
49
+ },
50
+ retry_pattern: 'retry',
51
+ };
52
+ try {
53
+ if (!existsSync(configPath)) {
54
+ // Config file not found, use defaults
55
+ return defaults;
56
+ }
57
+ const content = readFileSync(configPath, { encoding: 'utf-8' });
58
+ const parsed = yaml.parse(content);
59
+ // Merge parsed config with defaults (handles incomplete configs)
60
+ return {
61
+ version: parsed.version ?? defaults.version,
62
+ token_budgets: {
63
+ default: {
64
+ hard_cap: parsed.token_budgets?.default?.hard_cap ?? defaults.token_budgets.default.hard_cap,
65
+ warn_threshold: parsed.token_budgets?.default?.warn_threshold ??
66
+ defaults.token_budgets.default.warn_threshold,
67
+ },
68
+ retry: {
69
+ hard_cap: parsed.token_budgets?.retry?.hard_cap ?? defaults.token_budgets.retry.hard_cap,
70
+ },
71
+ },
72
+ delta_budgets: {
73
+ warn: parsed.delta_budgets?.warn ?? defaults.delta_budgets.warn,
74
+ block: parsed.delta_budgets?.block ?? defaults.delta_budgets.block,
75
+ },
76
+ retry_pattern: parsed.retry_pattern ?? defaults.retry_pattern,
77
+ };
78
+ }
79
+ catch (_error) {
80
+ // YAML parsing error or other failure, use defaults
81
+ console.error(`⚠️ Failed to load config from ${configPath}: ${_error.message}`);
82
+ console.error(` Using default token budgets.`);
83
+ return defaults;
84
+ }
85
+ }
86
+ /**
87
+ * Load previous metrics from cache
88
+ * @returns {Promise<Object>} Previous metrics by file path
89
+ */
90
+ async function loadPreviousMetrics() {
91
+ try {
92
+ const fileExists = await access(METRICS_CACHE_PATH)
93
+ .then(() => true)
94
+ .catch(() => false);
95
+ if (fileExists) {
96
+ const data = await readFile(METRICS_CACHE_PATH, { encoding: 'utf-8' });
97
+ return JSON.parse(data);
98
+ }
99
+ }
100
+ catch {
101
+ // Ignore errors, return empty metrics
102
+ }
103
+ return {};
104
+ }
105
+ /**
106
+ * Save current metrics to cache
107
+ * @param {Object} metrics - Metrics by file path
108
+ * @returns {Promise<void>}
109
+ */
110
+ async function savemetrics(metrics) {
111
+ try {
112
+ const dir = dirname(METRICS_CACHE_PATH);
113
+ const dirExists = await access(dir)
114
+ .then(() => true)
115
+ .catch(() => false);
116
+ if (!dirExists) {
117
+ await mkdir(dir, { recursive: true });
118
+ }
119
+ await writeFile(METRICS_CACHE_PATH, JSON.stringify(metrics, null, 2));
120
+ }
121
+ catch {
122
+ // Ignore errors (cache is optional)
123
+ }
124
+ }
125
+ /**
126
+ * Log via proper telemetry (simulated getLogger for CLI context)
127
+ * In production, this would use apps/web/src/lib/logger.ts
128
+ * @param {string} level - Log level (info, warn, error)
129
+ * @param {string} event - Event name
130
+ * @param {Object} data - Structured data
131
+ * @param {LogOutputOptions} [output] - Output mode
132
+ * @returns {Promise<void>}
133
+ */
134
+ async function log(level, event, data, output = {}) {
135
+ const timestamp = new Date().toISOString();
136
+ const entry = {
137
+ timestamp,
138
+ level,
139
+ event,
140
+ ...data,
141
+ };
142
+ // For CLI, write to .beacon/telemetry/prompt-lint.ndjson
143
+ const ndjsonPath = resolve(ROOT_DIR, '.beacon/telemetry/prompt-lint.ndjson');
144
+ const line = `${JSON.stringify(entry)}${STRING_LITERALS.NEWLINE}`;
145
+ try {
146
+ const dir = dirname(ndjsonPath);
147
+ const dirExists = await access(dir)
148
+ .then(() => true)
149
+ .catch(() => false);
150
+ if (!dirExists) {
151
+ await mkdir(dir, { recursive: true });
152
+ }
153
+ await appendFile(ndjsonPath, line);
154
+ }
155
+ catch {
156
+ // Fallback to stderr if file write fails
157
+ console.error(JSON.stringify(entry));
158
+ }
159
+ const shouldPrintToConsole = output.verbose ? true : output.quiet ? level !== 'info' : true;
160
+ // Also output human-readable to stderr
161
+ const levelEmoji = {
162
+ info: '📊',
163
+ warn: '⚠️ ',
164
+ error: '❌',
165
+ }[level] || ' ';
166
+ if (shouldPrintToConsole) {
167
+ const message = `${levelEmoji} [${event}] ${data.file || ''} ${data.tokenCount ? `${data.tokenCount} tokens` : ''}`;
168
+ console.error(message);
169
+ if (data.delta !== undefined) {
170
+ console.error(` Delta: ${data.delta > 0 ? '+' : ''}${data.delta} tokens`);
171
+ }
172
+ if (data.message) {
173
+ console.error(` ${data.message}`);
174
+ }
175
+ }
176
+ }
177
+ /**
178
+ * Lint a single prompt file
179
+ * @param {string} filePath - Absolute path to prompt file
180
+ * @param {Object} previousMetrics - Previous metrics for delta calculation
181
+ * @param {string} mode - Mode (pre-commit, pre-push, ci, local)
182
+ * @param {Object} config - Configuration object from loadConfig()
183
+ * @param {{quiet?: boolean, verbose?: boolean}} output - Output mode
184
+ * @returns {Promise<{passed: boolean, tokenCount: number, delta: number, hash: string}>}
185
+ */
186
+ async function lintPromptFile(filePath, previousMetrics, mode, config, output) {
187
+ // Analyze prompt
188
+ const { tokenCount, hash, text } = analyzePrompt(filePath);
189
+ // Calculate delta from previous metrics
190
+ const previous = previousMetrics[filePath];
191
+ const delta = previous ? tokenCount - previous.tokenCount : 0;
192
+ // Determine cap based on file name and config pattern
193
+ const isRetryPrompt = filePath.includes(config.retry_pattern);
194
+ const cap = isRetryPrompt
195
+ ? config.token_budgets.retry.hard_cap
196
+ : config.token_budgets.default.hard_cap;
197
+ // Get top 3 longest lines for cleanup targeting
198
+ const longestLines = getLongestLines(text, 3);
199
+ // BLOCK: hard cap or sudden bloat
200
+ if (tokenCount > cap || delta > config.delta_budgets.block) {
201
+ await log('error', 'prompt.lint.blocked', {
202
+ file: filePath,
203
+ tokenCount,
204
+ delta,
205
+ hash,
206
+ cap,
207
+ mode,
208
+ message: `Exceeds ${cap} token cap or delta >${config.delta_budgets.block}`,
209
+ longestLines: longestLines.map((l) => `Line ${l.number}: ${l.length} chars`),
210
+ }, output);
211
+ return { passed: false, tokenCount, delta, hash };
212
+ }
213
+ // WARN: approaching cap or gradual creep
214
+ if (tokenCount >= config.token_budgets.default.warn_threshold ||
215
+ delta > config.delta_budgets.warn) {
216
+ await log('warn', 'prompt.lint.warning', {
217
+ file: filePath,
218
+ tokenCount,
219
+ delta,
220
+ hash,
221
+ threshold: config.token_budgets.default.warn_threshold,
222
+ mode,
223
+ message: 'Approaching token budget cap',
224
+ longestLines: longestLines.map((l) => `Line ${l.number}: ${l.length} chars`),
225
+ }, output);
226
+ }
227
+ // LOG: always log metrics
228
+ await log('info', 'prompt.lint.measured', {
229
+ file: filePath,
230
+ tokenCount,
231
+ delta,
232
+ hash,
233
+ mode,
234
+ longestLines: longestLines.map((l) => `Line ${l.number}: ${l.length} chars`),
235
+ }, output);
236
+ return { passed: true, tokenCount, delta, hash };
237
+ }
238
+ /**
239
+ * Main linter function
240
+ * @param {string[]} filePaths - Prompt files to lint (optional, finds all if empty)
241
+ * @param {string} mode - Mode (pre-commit, pre-push, ci, local)
242
+ * @param {string} configPath - Optional config file path
243
+ * @param {LintPromptsOptions} [options] - Output options
244
+ * @returns {Promise<{passed: boolean, results: Array, config: Object}>}
245
+ */
246
+ export async function lintPrompts(filePaths = [], mode = 'local', configPath = undefined, options = {}) {
247
+ // Load configuration
248
+ const config = loadConfig(configPath);
249
+ const output = { quiet: options.quiet === true, verbose: options.verbose === true };
250
+ // If no files provided, find all orchestrator prompt files (WU-676 scope only)
251
+ if (filePaths.length === 0) {
252
+ const pattern = 'packages/@patientpath/prompts/orchestrator-*/**/*.yaml';
253
+ filePaths = await glob(pattern, { cwd: ROOT_DIR, absolute: true });
254
+ }
255
+ // Load previous metrics for delta calculation
256
+ const previousMetrics = await loadPreviousMetrics();
257
+ // Lint each file
258
+ const results = [];
259
+ let allPassed = true;
260
+ for (const filePath of filePaths) {
261
+ const result = await lintPromptFile(filePath, previousMetrics, mode, config, output);
262
+ results.push({ filePath, ...result });
263
+ if (!result.passed) {
264
+ allPassed = false;
265
+ }
266
+ // Update metrics cache
267
+ previousMetrics[filePath] = {
268
+ tokenCount: result.tokenCount,
269
+ hash: result.hash,
270
+ timestamp: new Date().toISOString(),
271
+ };
272
+ }
273
+ // Save updated metrics
274
+ await savemetrics(previousMetrics);
275
+ return { passed: allPassed, results, config };
276
+ }
277
+ /**
278
+ * CLI entry point
279
+ */
280
+ async function main() {
281
+ const args = process.argv.slice(2);
282
+ const modeFlag = args.find((arg) => arg.startsWith('--mode='));
283
+ const mode = modeFlag ? modeFlag.split('=')[1] : 'local';
284
+ const quiet = args.includes('--quiet');
285
+ const verbose = args.includes('--verbose');
286
+ // Get files to lint (from args or find all)
287
+ const files = args.filter((arg) => !arg.startsWith('--'));
288
+ console.error(`\n🔍 Linting prompts (mode: ${mode})...\n`);
289
+ const { passed, results, config } = await lintPrompts(files, mode, undefined, { quiet, verbose });
290
+ // Summary
291
+ const total = results.length;
292
+ const blocked = results.filter((r) => !r.passed).length;
293
+ const warned = results.filter((r) => r.passed && r.tokenCount >= config.token_budgets.default.warn_threshold).length;
294
+ console.error(`\n📋 Summary: ${total} prompts analyzed`);
295
+ if (blocked > 0) {
296
+ console.error(` ❌ ${blocked} BLOCKED (>${config.token_budgets.default.hard_cap} tokens or delta >${config.delta_budgets.block})`);
297
+ }
298
+ if (warned > 0) {
299
+ console.error(` ⚠️ ${warned} WARNING (≥${config.token_budgets.default.warn_threshold} tokens or delta >${config.delta_budgets.warn})`);
300
+ }
301
+ if (blocked === 0 && warned === 0) {
302
+ console.error(` ✅ All prompts within budget`);
303
+ }
304
+ process.exit(passed ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
305
+ }
306
+ // Run CLI if executed directly
307
+ if (import.meta.url === `file://${process.argv[1]}`) {
308
+ main().catch((error) => {
309
+ console.error('Prompt linter failed:', error);
310
+ process.exit(EXIT_CODES.ERROR);
311
+ });
312
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Nightly Prompt Monitor
3
+ *
4
+ * Monitors all prompts for token budget drift and hash changes.
5
+ * Designed to run as a GitHub Actions cron job (nightly at 2 AM).
6
+ *
7
+ * Logs to .beacon/telemetry/prompt-nightly.ndjson and Axiom.
8
+ * Alerts if:
9
+ * - Any prompt ≥400 tokens (approaching cap)
10
+ * - Any delta >50 tokens since yesterday
11
+ * - rules_hash changed outside a WU (unintentional drift)
12
+ *
13
+ * Part of WU-676: Single-Call LLM Orchestrator token budget enforcement.
14
+ */
15
+ export {};
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Nightly Prompt Monitor
3
+ *
4
+ * Monitors all prompts for token budget drift and hash changes.
5
+ * Designed to run as a GitHub Actions cron job (nightly at 2 AM).
6
+ *
7
+ * Logs to .beacon/telemetry/prompt-nightly.ndjson and Axiom.
8
+ * Alerts if:
9
+ * - Any prompt ≥400 tokens (approaching cap)
10
+ * - Any delta >50 tokens since yesterday
11
+ * - rules_hash changed outside a WU (unintentional drift)
12
+ *
13
+ * Part of WU-676: Single-Call LLM Orchestrator token budget enforcement.
14
+ */
15
+ import { analyzePrompt } from './token-counter.js';
16
+ import { readFile, writeFile, mkdir, appendFile, access } from 'node:fs/promises';
17
+ import { resolve, dirname } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import { glob } from 'glob';
20
+ import { EXIT_CODES, STRING_LITERALS } from './wu-constants.js';
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+ const ROOT_DIR = resolve(__dirname, '../..');
24
+ // Paths
25
+ const YESTERDAY_METRICS_PATH = resolve(ROOT_DIR, '.beacon/telemetry/prompt-metrics-yesterday.json');
26
+ const TODAY_METRICS_PATH = resolve(ROOT_DIR, '.beacon/telemetry/prompt-metrics.json');
27
+ const NDJSON_LOG_PATH = resolve(ROOT_DIR, '.beacon/telemetry/prompt-nightly.ndjson');
28
+ // Alert thresholds
29
+ const WARN_THRESHOLD = 400;
30
+ const DELTA_THRESHOLD = 50;
31
+ /**
32
+ * Load yesterday's metrics
33
+ */
34
+ async function loadYesterdayMetrics() {
35
+ try {
36
+ const exists = await access(YESTERDAY_METRICS_PATH)
37
+ .then(() => true)
38
+ .catch(() => false);
39
+ if (exists) {
40
+ const data = await readFile(YESTERDAY_METRICS_PATH, { encoding: 'utf-8' });
41
+ return JSON.parse(data);
42
+ }
43
+ }
44
+ catch (error) {
45
+ console.error('Failed to load yesterday metrics:', error);
46
+ }
47
+ return {};
48
+ }
49
+ /**
50
+ * Save today's metrics
51
+ */
52
+ async function saveMetrics(metrics) {
53
+ try {
54
+ const dir = dirname(TODAY_METRICS_PATH);
55
+ const dirExists = await access(dir)
56
+ .then(() => true)
57
+ .catch(() => false);
58
+ if (!dirExists) {
59
+ await mkdir(dir, { recursive: true });
60
+ }
61
+ await writeFile(TODAY_METRICS_PATH, JSON.stringify(metrics, null, 2));
62
+ }
63
+ catch (error) {
64
+ console.error('Failed to save metrics:', error);
65
+ }
66
+ }
67
+ /**
68
+ * Log to NDJSON
69
+ */
70
+ async function log(event, data) {
71
+ const entry = {
72
+ timestamp: new Date().toISOString(),
73
+ event,
74
+ ...data,
75
+ };
76
+ const line = `${JSON.stringify(entry)}${STRING_LITERALS.NEWLINE}`;
77
+ try {
78
+ const dir = dirname(NDJSON_LOG_PATH);
79
+ const dirExists = await access(dir)
80
+ .then(() => true)
81
+ .catch(() => false);
82
+ if (!dirExists) {
83
+ await mkdir(dir, { recursive: true });
84
+ }
85
+ await appendFile(NDJSON_LOG_PATH, line);
86
+ }
87
+ catch (error) {
88
+ console.error('Failed to log:', error);
89
+ }
90
+ // Also output to stdout for GitHub Actions logs
91
+ console.log(JSON.stringify(entry));
92
+ }
93
+ /**
94
+ * Main monitor function
95
+ */
96
+ async function monitor() {
97
+ console.log('\n🌙 Nightly Prompt Monitor Starting...\n');
98
+ // Find all prompt files
99
+ const pattern = 'packages/@patientpath/prompts/**/*.yaml';
100
+ const promptFiles = await glob(pattern, { cwd: ROOT_DIR, absolute: true });
101
+ console.log(`Found ${promptFiles.length} prompt files to analyze\n`);
102
+ // Load yesterday's metrics for delta calculation
103
+ const yesterdayMetrics = await loadYesterdayMetrics();
104
+ const todayMetrics = {};
105
+ let totalAlerts = 0;
106
+ for (const filePath of promptFiles) {
107
+ try {
108
+ const { tokenCount, hash } = analyzePrompt(filePath);
109
+ const yesterday = yesterdayMetrics[filePath];
110
+ // Calculate delta
111
+ const delta = yesterday ? tokenCount - yesterday.tokenCount : 0;
112
+ const hashChanged = yesterday ? hash !== yesterday.hash : false;
113
+ // Store today's metrics
114
+ todayMetrics[filePath] = {
115
+ tokenCount,
116
+ hash,
117
+ timestamp: new Date().toISOString(),
118
+ };
119
+ // Log metrics
120
+ await log('prompt.nightly.metrics', {
121
+ prompt: filePath,
122
+ tokenCount,
123
+ hash,
124
+ delta,
125
+ hashChanged,
126
+ });
127
+ // Alert: approaching cap
128
+ if (tokenCount >= WARN_THRESHOLD) {
129
+ totalAlerts++;
130
+ await log('prompt.nightly.approaching_cap', {
131
+ prompt: filePath,
132
+ tokenCount,
133
+ cap: 450,
134
+ message: `Prompt at ${tokenCount} tokens (approaching 450 cap)`,
135
+ });
136
+ console.error(`⚠️ ${filePath}: ${tokenCount} tokens (approaching cap)`);
137
+ }
138
+ // Alert: significant delta
139
+ if (Math.abs(delta) > DELTA_THRESHOLD) {
140
+ totalAlerts++;
141
+ await log('prompt.nightly.significant_delta', {
142
+ prompt: filePath,
143
+ tokenCount,
144
+ delta,
145
+ message: `Delta ${delta > 0 ? '+' : ''}${delta} tokens exceeds threshold`,
146
+ });
147
+ console.error(`⚠️ ${filePath}: Delta ${delta > 0 ? '+' : ''}${delta} tokens`);
148
+ }
149
+ // Alert: hash changed
150
+ if (hashChanged) {
151
+ totalAlerts++;
152
+ await log('prompt.nightly.hash_changed', {
153
+ prompt: filePath,
154
+ oldHash: yesterday.hash,
155
+ newHash: hash,
156
+ message: 'Prompt hash changed - investigate if intentional',
157
+ });
158
+ console.error(`⚠️ ${filePath}: Hash changed (${yesterday.hash} → ${hash})`);
159
+ }
160
+ // Success log (no alerts)
161
+ if (tokenCount < WARN_THRESHOLD && Math.abs(delta) <= DELTA_THRESHOLD && !hashChanged) {
162
+ console.log(`✅ ${filePath}: ${tokenCount} tokens (delta: ${delta})`);
163
+ }
164
+ }
165
+ catch (error) {
166
+ console.error(`❌ Failed to analyze ${filePath}:`, error);
167
+ await log('prompt.nightly.analysis_failed', {
168
+ prompt: filePath,
169
+ error: error instanceof Error ? error.message : String(error),
170
+ });
171
+ }
172
+ }
173
+ // Save today's metrics for tomorrow's delta calculation
174
+ await saveMetrics(todayMetrics);
175
+ // Rotate metrics (today becomes yesterday)
176
+ const todayExists = await access(TODAY_METRICS_PATH)
177
+ .then(() => true)
178
+ .catch(() => false);
179
+ if (todayExists) {
180
+ try {
181
+ const todayData = await readFile(TODAY_METRICS_PATH, { encoding: 'utf-8' });
182
+ await writeFile(YESTERDAY_METRICS_PATH, todayData);
183
+ }
184
+ catch (error) {
185
+ console.error('Failed to rotate metrics:', error);
186
+ }
187
+ }
188
+ // Summary
189
+ console.log(`\n📊 Nightly Monitor Complete`);
190
+ console.log(` Total prompts: ${promptFiles.length}`);
191
+ console.log(` Alerts: ${totalAlerts}`);
192
+ if (totalAlerts > 0) {
193
+ console.log(`\n⚠️ Review alerts above and investigate if changes were intentional\n`);
194
+ process.exit(EXIT_CODES.ERROR); // Exit with error to trigger GitHub Actions notification
195
+ }
196
+ else {
197
+ console.log(`\n✅ All prompts within budget and stable\n`);
198
+ process.exit(EXIT_CODES.SUCCESS);
199
+ }
200
+ }
201
+ // Run monitor
202
+ monitor().catch((error) => {
203
+ console.error('Nightly monitor failed:', error);
204
+ process.exit(EXIT_CODES.ERROR);
205
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Rebase Artifact Cleanup
3
+ *
4
+ * Detects and cleans up completion artifacts (stamps, status=done)
5
+ * that appear in worktree after rebasing from main.
6
+ *
7
+ * This prevents contradictory state where an in_progress WU has
8
+ * completion markers from a previous completion cycle on main.
9
+ *
10
+ * Part of WU-1371: Post-rebase artifact cleanup
11
+ * WU-1449: Extended to handle backlog/status duplicates after rebase
12
+ *
13
+ * @see {@link tools/wu-done.mjs} - Creates completion artifacts
14
+ * @see {@link tools/lib/stamp-utils.mjs} - Stamp file utilities
15
+ * @see {@link tools/lib/wu-recovery.mjs} - Related zombie state handling
16
+ */
17
+ /**
18
+ * Detect rebased completion artifacts in a worktree
19
+ *
20
+ * WU-1817: Now verifies artifacts exist on origin/main before flagging.
21
+ * Only artifacts that exist on BOTH worktree AND origin/main are true
22
+ * rebased artifacts. Artifacts that exist only locally (created by the
23
+ * lane branch itself) should NOT be cleaned - this was the WU-1816 bug.
24
+ *
25
+ * Checks for:
26
+ * 1. Stamp files (.beacon/stamps/WU-{id}.done) that exist on origin/main
27
+ * 2. WU YAML with status=done that also has status=done on origin/main
28
+ *
29
+ * @param {string} worktreePath - Path to the worktree directory
30
+ * @param {string} wuId - WU ID (e.g., 'WU-1371')
31
+ * @param {object} gitAdapter - Git adapter instance with raw() method
32
+ * @returns {Promise<object>} Detection result
33
+ * @returns {string[]} result.stamps - Array of detected stamp file paths (only if on origin/main)
34
+ * @returns {boolean} result.yamlStatusDone - True if YAML has status=done AND origin/main has done
35
+ * @returns {boolean} result.hasArtifacts - True if any rebased artifacts detected
36
+ *
37
+ * @example
38
+ * const result = await detectRebasedArtifacts(worktreePath, wuId, gitAdapter);
39
+ * if (result.hasArtifacts) {
40
+ * console.log('Found rebased artifacts, cleaning up...');
41
+ * }
42
+ */
43
+ export declare function detectRebasedArtifacts(worktreePath: any, wuId: any, gitAdapter: any): Promise<{
44
+ stamps: any[];
45
+ yamlStatusDone: boolean;
46
+ hasArtifacts: boolean;
47
+ }>;
48
+ /**
49
+ * Clean up rebased completion artifacts from a worktree
50
+ *
51
+ * Actions:
52
+ * 1. Remove stamp files that shouldn't exist
53
+ * 2. Reset YAML status from done to in_progress
54
+ * 3. Remove locked and completed_at fields from YAML
55
+ * 4. Log warnings explaining cleanup actions
56
+ *
57
+ * Idempotent: Safe to call multiple times, won't throw if artifacts don't exist.
58
+ *
59
+ * @param {string} worktreePath - Path to the worktree directory
60
+ * @param {string} wuId - WU ID (e.g., 'WU-1371')
61
+ * @returns {Promise<object>} Cleanup result
62
+ * @returns {string[]} result.stampsCleaned - WU IDs whose stamps were removed
63
+ * @returns {boolean} result.yamlReset - True if YAML status was reset
64
+ * @returns {string[]} result.errors - Any errors encountered (non-fatal)
65
+ * @returns {boolean} result.cleaned - True if any cleanup was performed
66
+ *
67
+ * @example
68
+ * const result = await cleanupRebasedArtifacts(worktreePath, wuId);
69
+ * if (result.cleaned) {
70
+ * console.log('Cleaned rebased artifacts:', result);
71
+ * }
72
+ */
73
+ export declare function cleanupRebasedArtifacts(worktreePath: any, wuId: any): Promise<{
74
+ stampsCleaned: any[];
75
+ yamlReset: boolean;
76
+ backlogCleaned: boolean;
77
+ statusCleaned: boolean;
78
+ errors: any[];
79
+ cleaned: boolean;
80
+ }>;
81
+ /**
82
+ * Detect WU duplicates in backlog/status files after rebase
83
+ *
84
+ * Checks if a WU appears in both:
85
+ * - "In Progress" AND "Done" sections of backlog.md
86
+ * - "In Progress" AND "Completed" sections of status.md
87
+ *
88
+ * This state occurs when main advanced with WU completion,
89
+ * then rebase merged main's "Done" state into the worktree
90
+ * while the worktree already had the WU in "In Progress".
91
+ *
92
+ * Part of WU-1449: Extend rebase cleanup to remove backlog/status duplicates
93
+ *
94
+ * @param {string} worktreePath - Path to the worktree directory
95
+ * @param {string} wuId - WU ID (e.g., 'WU-1449')
96
+ * @returns {Promise<object>} Detection result
97
+ * @returns {boolean} result.backlogDuplicate - True if WU in both In Progress and Done in backlog.md
98
+ * @returns {boolean} result.statusDuplicate - True if WU in both In Progress and Completed in status.md
99
+ * @returns {boolean} result.hasDuplicates - True if any duplicates detected
100
+ *
101
+ * @example
102
+ * const result = await detectBacklogDuplicates(worktreePath, wuId);
103
+ * if (result.hasDuplicates) {
104
+ * console.log('Found backlog duplicates, cleaning up...');
105
+ * }
106
+ */
107
+ export declare function detectBacklogDuplicates(worktreePath: any, wuId: any): Promise<{
108
+ backlogDuplicate: boolean;
109
+ statusDuplicate: boolean;
110
+ hasDuplicates: boolean;
111
+ }>;
112
+ /**
113
+ * Remove WU from In Progress sections when already in Done/Completed after rebase
114
+ *
115
+ * This handles the specific case where:
116
+ * 1. WU is completing (wu:done in progress)
117
+ * 2. Auto-rebase pulls main's completion state
118
+ * 3. WU now appears in BOTH In Progress AND Done sections
119
+ * 4. This function removes the duplicate from In Progress (keeps Done)
120
+ *
121
+ * Applies to both backlog.md and status.md.
122
+ * Idempotent: Safe to call multiple times.
123
+ *
124
+ * Part of WU-1449: Extend rebase cleanup to remove backlog/status duplicates
125
+ *
126
+ * @param {string} worktreePath - Path to the worktree directory
127
+ * @param {string} wuId - WU ID (e.g., 'WU-1449')
128
+ * @returns {Promise<object>} Cleanup result
129
+ * @returns {boolean} result.backlogCleaned - True if WU removed from backlog.md In Progress
130
+ * @returns {boolean} result.statusCleaned - True if WU removed from status.md In Progress
131
+ * @returns {boolean} result.cleaned - True if any cleanup was performed
132
+ * @returns {string[]} result.errors - Any errors encountered (non-fatal)
133
+ *
134
+ * @example
135
+ * const result = await deduplicateBacklogAfterRebase(worktreePath, wuId);
136
+ * if (result.cleaned) {
137
+ * console.log('Cleaned backlog duplicates:', result);
138
+ * }
139
+ */
140
+ export declare function deduplicateBacklogAfterRebase(worktreePath: any, wuId: any): Promise<{
141
+ backlogCleaned: boolean;
142
+ statusCleaned: boolean;
143
+ cleaned: boolean;
144
+ errors: any[];
145
+ }>;