aspectcode 0.4.1 → 1.0.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 (248) hide show
  1. package/README.md +2 -2
  2. package/dist/agentsMdRenderer.d.ts +16 -0
  3. package/dist/agentsMdRenderer.d.ts.map +1 -0
  4. package/dist/agentsMdRenderer.js +137 -0
  5. package/dist/agentsMdRenderer.js.map +1 -0
  6. package/dist/auth.d.ts +31 -0
  7. package/dist/auth.d.ts.map +1 -0
  8. package/dist/auth.js +386 -0
  9. package/dist/auth.js.map +1 -0
  10. package/dist/autoResolve.d.ts +41 -0
  11. package/dist/autoResolve.d.ts.map +1 -0
  12. package/dist/autoResolve.js +196 -0
  13. package/dist/autoResolve.js.map +1 -0
  14. package/dist/changeEvaluator.d.ts +56 -0
  15. package/dist/changeEvaluator.d.ts.map +1 -0
  16. package/dist/changeEvaluator.js +674 -0
  17. package/dist/changeEvaluator.js.map +1 -0
  18. package/dist/cli.d.ts +3 -1
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +1 -1
  21. package/dist/cli.js.map +1 -1
  22. package/dist/config.d.ts +37 -17
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +50 -2
  25. package/dist/config.js.map +1 -1
  26. package/dist/dreamCycle.d.ts +57 -0
  27. package/dist/dreamCycle.d.ts.map +1 -0
  28. package/dist/dreamCycle.js +334 -0
  29. package/dist/dreamCycle.js.map +1 -0
  30. package/dist/kbBuilder.d.ts +1 -2
  31. package/dist/kbBuilder.d.ts.map +1 -1
  32. package/dist/kbBuilder.js +1 -2
  33. package/dist/kbBuilder.js.map +1 -1
  34. package/dist/main.d.ts +2 -1
  35. package/dist/main.d.ts.map +1 -1
  36. package/dist/main.js +148 -7
  37. package/dist/main.js.map +1 -1
  38. package/dist/optimize.d.ts +13 -6
  39. package/dist/optimize.d.ts.map +1 -1
  40. package/dist/optimize.js +433 -142
  41. package/dist/optimize.js.map +1 -1
  42. package/dist/pipeline.d.ts +19 -21
  43. package/dist/pipeline.d.ts.map +1 -1
  44. package/dist/pipeline.js +1093 -160
  45. package/dist/pipeline.js.map +1 -1
  46. package/dist/preferences.d.ts +80 -0
  47. package/dist/preferences.d.ts.map +1 -0
  48. package/dist/preferences.js +238 -0
  49. package/dist/preferences.js.map +1 -0
  50. package/dist/runtimeState.d.ts +30 -0
  51. package/dist/runtimeState.d.ts.map +1 -0
  52. package/dist/runtimeState.js +39 -0
  53. package/dist/runtimeState.js.map +1 -0
  54. package/dist/scopedRules.d.ts +84 -0
  55. package/dist/scopedRules.d.ts.map +1 -0
  56. package/dist/scopedRules.js +449 -0
  57. package/dist/scopedRules.js.map +1 -0
  58. package/dist/ui/Dashboard.d.ts +4 -16
  59. package/dist/ui/Dashboard.d.ts.map +1 -1
  60. package/dist/ui/Dashboard.js +339 -140
  61. package/dist/ui/Dashboard.js.map +1 -1
  62. package/dist/ui/MemoryMap.d.ts +16 -0
  63. package/dist/ui/MemoryMap.d.ts.map +1 -0
  64. package/dist/ui/MemoryMap.js +266 -0
  65. package/dist/ui/MemoryMap.js.map +1 -0
  66. package/dist/ui/SettingsPanel.d.ts +18 -0
  67. package/dist/ui/SettingsPanel.d.ts.map +1 -0
  68. package/dist/ui/SettingsPanel.js +241 -0
  69. package/dist/ui/SettingsPanel.js.map +1 -0
  70. package/dist/ui/prompts.d.ts +7 -0
  71. package/dist/ui/prompts.d.ts.map +1 -1
  72. package/dist/ui/prompts.js +63 -0
  73. package/dist/ui/prompts.js.map +1 -1
  74. package/dist/ui/store.d.ts +154 -18
  75. package/dist/ui/store.d.ts.map +1 -1
  76. package/dist/ui/store.js +154 -24
  77. package/dist/ui/store.js.map +1 -1
  78. package/dist/ui/theme.d.ts +1 -8
  79. package/dist/ui/theme.d.ts.map +1 -1
  80. package/dist/ui/theme.js +2 -21
  81. package/dist/ui/theme.js.map +1 -1
  82. package/dist/updateChecker.d.ts +13 -0
  83. package/dist/updateChecker.d.ts.map +1 -0
  84. package/dist/updateChecker.js +66 -0
  85. package/dist/updateChecker.js.map +1 -0
  86. package/dist/usageTracker.d.ts +12 -0
  87. package/dist/usageTracker.d.ts.map +1 -0
  88. package/dist/usageTracker.js +89 -0
  89. package/dist/usageTracker.js.map +1 -0
  90. package/dist/writer.d.ts +1 -7
  91. package/dist/writer.d.ts.map +1 -1
  92. package/dist/writer.js +1 -11
  93. package/dist/writer.js.map +1 -1
  94. package/node_modules/@aspectcode/core/dist/analysis/repo.d.ts.map +1 -1
  95. package/node_modules/@aspectcode/core/dist/analysis/repo.js +13 -2
  96. package/node_modules/@aspectcode/core/dist/analysis/repo.js.map +1 -1
  97. package/node_modules/@aspectcode/core/dist/index.d.ts +1 -3
  98. package/node_modules/@aspectcode/core/dist/index.d.ts.map +1 -1
  99. package/node_modules/@aspectcode/core/dist/index.js +1 -3
  100. package/node_modules/@aspectcode/core/dist/index.js.map +1 -1
  101. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.d.ts +14 -0
  102. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.d.ts.map +1 -0
  103. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.js +191 -0
  104. package/node_modules/@aspectcode/core/dist/parsers/genericExtractors.js.map +1 -0
  105. package/node_modules/@aspectcode/core/dist/parsers/index.d.ts +1 -0
  106. package/node_modules/@aspectcode/core/dist/parsers/index.d.ts.map +1 -1
  107. package/node_modules/@aspectcode/core/dist/parsers/index.js +6 -1
  108. package/node_modules/@aspectcode/core/dist/parsers/index.js.map +1 -1
  109. package/node_modules/@aspectcode/core/dist/parsers/languages.d.ts +20 -0
  110. package/node_modules/@aspectcode/core/dist/parsers/languages.d.ts.map +1 -1
  111. package/node_modules/@aspectcode/core/dist/parsers/languages.js +25 -0
  112. package/node_modules/@aspectcode/core/dist/parsers/languages.js.map +1 -1
  113. package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.d.ts.map +1 -1
  114. package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.js +4 -1
  115. package/node_modules/@aspectcode/core/dist/parsers/tsJsExtractors.js.map +1 -1
  116. package/node_modules/@aspectcode/core/package.json +2 -2
  117. package/node_modules/@aspectcode/core/parsers/cpp.wasm +0 -0
  118. package/node_modules/@aspectcode/core/parsers/go.wasm +0 -0
  119. package/node_modules/@aspectcode/core/parsers/php.wasm +0 -0
  120. package/node_modules/@aspectcode/core/parsers/ruby.wasm +0 -0
  121. package/node_modules/@aspectcode/core/parsers/rust.wasm +0 -0
  122. package/node_modules/@aspectcode/emitters/dist/index.d.ts +1 -17
  123. package/node_modules/@aspectcode/emitters/dist/index.d.ts.map +1 -1
  124. package/node_modules/@aspectcode/emitters/dist/index.js +2 -90
  125. package/node_modules/@aspectcode/emitters/dist/index.js.map +1 -1
  126. package/node_modules/@aspectcode/emitters/dist/instructions/index.d.ts +0 -2
  127. package/node_modules/@aspectcode/emitters/dist/instructions/index.d.ts.map +1 -1
  128. package/node_modules/@aspectcode/emitters/dist/instructions/index.js +1 -7
  129. package/node_modules/@aspectcode/emitters/dist/instructions/index.js.map +1 -1
  130. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.d.ts +0 -18
  131. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.d.ts.map +1 -1
  132. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.js +0 -57
  133. package/node_modules/@aspectcode/emitters/dist/kb/analyzers.js.map +1 -1
  134. package/node_modules/@aspectcode/emitters/dist/kb/conventions.d.ts +0 -18
  135. package/node_modules/@aspectcode/emitters/dist/kb/conventions.d.ts.map +1 -1
  136. package/node_modules/@aspectcode/emitters/dist/kb/conventions.js +0 -130
  137. package/node_modules/@aspectcode/emitters/dist/kb/conventions.js.map +1 -1
  138. package/node_modules/@aspectcode/emitters/dist/kb/index.d.ts +2 -4
  139. package/node_modules/@aspectcode/emitters/dist/kb/index.d.ts.map +1 -1
  140. package/node_modules/@aspectcode/emitters/dist/kb/index.js +1 -11
  141. package/node_modules/@aspectcode/emitters/dist/kb/index.js.map +1 -1
  142. package/node_modules/@aspectcode/emitters/package.json +3 -3
  143. package/node_modules/@aspectcode/evaluator/dist/apply.d.ts +55 -0
  144. package/node_modules/@aspectcode/evaluator/dist/apply.d.ts.map +1 -0
  145. package/node_modules/@aspectcode/evaluator/dist/apply.js +368 -0
  146. package/node_modules/@aspectcode/evaluator/dist/apply.js.map +1 -0
  147. package/node_modules/@aspectcode/evaluator/dist/diagnosis.d.ts +16 -25
  148. package/node_modules/@aspectcode/evaluator/dist/diagnosis.d.ts.map +1 -1
  149. package/node_modules/@aspectcode/evaluator/dist/diagnosis.js +115 -138
  150. package/node_modules/@aspectcode/evaluator/dist/diagnosis.js.map +1 -1
  151. package/node_modules/@aspectcode/evaluator/dist/index.d.ts +8 -43
  152. package/node_modules/@aspectcode/evaluator/dist/index.d.ts.map +1 -1
  153. package/node_modules/@aspectcode/evaluator/dist/index.js +15 -61
  154. package/node_modules/@aspectcode/evaluator/dist/index.js.map +1 -1
  155. package/node_modules/@aspectcode/evaluator/dist/judge.d.ts +32 -0
  156. package/node_modules/@aspectcode/evaluator/dist/judge.d.ts.map +1 -0
  157. package/node_modules/@aspectcode/evaluator/dist/judge.js +165 -0
  158. package/node_modules/@aspectcode/evaluator/dist/judge.js.map +1 -0
  159. package/node_modules/@aspectcode/evaluator/dist/llmUtil.d.ts +15 -0
  160. package/node_modules/@aspectcode/evaluator/dist/llmUtil.d.ts.map +1 -0
  161. package/node_modules/@aspectcode/evaluator/dist/llmUtil.js +41 -0
  162. package/node_modules/@aspectcode/evaluator/dist/llmUtil.js.map +1 -0
  163. package/node_modules/@aspectcode/evaluator/dist/probes.d.ts +20 -47
  164. package/node_modules/@aspectcode/evaluator/dist/probes.d.ts.map +1 -1
  165. package/node_modules/@aspectcode/evaluator/dist/probes.js +188 -278
  166. package/node_modules/@aspectcode/evaluator/dist/probes.js.map +1 -1
  167. package/node_modules/@aspectcode/evaluator/dist/runner.d.ts +7 -32
  168. package/node_modules/@aspectcode/evaluator/dist/runner.d.ts.map +1 -1
  169. package/node_modules/@aspectcode/evaluator/dist/runner.js +21 -146
  170. package/node_modules/@aspectcode/evaluator/dist/runner.js.map +1 -1
  171. package/node_modules/@aspectcode/evaluator/dist/types.d.ts +141 -99
  172. package/node_modules/@aspectcode/evaluator/dist/types.d.ts.map +1 -1
  173. package/node_modules/@aspectcode/evaluator/dist/types.js +10 -2
  174. package/node_modules/@aspectcode/evaluator/dist/types.js.map +1 -1
  175. package/node_modules/@aspectcode/evaluator/package.json +4 -4
  176. package/node_modules/@aspectcode/optimizer/dist/index.d.ts +3 -10
  177. package/node_modules/@aspectcode/optimizer/dist/index.d.ts.map +1 -1
  178. package/node_modules/@aspectcode/optimizer/dist/index.js +1 -19
  179. package/node_modules/@aspectcode/optimizer/dist/index.js.map +1 -1
  180. package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.d.ts.map +1 -1
  181. package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.js +40 -0
  182. package/node_modules/@aspectcode/optimizer/dist/providers/anthropic.js.map +1 -1
  183. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.d.ts +9 -0
  184. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.d.ts.map +1 -0
  185. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.js +83 -0
  186. package/node_modules/@aspectcode/optimizer/dist/providers/aspectcode.js.map +1 -0
  187. package/node_modules/@aspectcode/optimizer/dist/providers/index.d.ts +4 -3
  188. package/node_modules/@aspectcode/optimizer/dist/providers/index.d.ts.map +1 -1
  189. package/node_modules/@aspectcode/optimizer/dist/providers/index.js +24 -10
  190. package/node_modules/@aspectcode/optimizer/dist/providers/index.js.map +1 -1
  191. package/node_modules/@aspectcode/optimizer/dist/providers/openai.d.ts.map +1 -1
  192. package/node_modules/@aspectcode/optimizer/dist/providers/openai.js +22 -0
  193. package/node_modules/@aspectcode/optimizer/dist/providers/openai.js.map +1 -1
  194. package/node_modules/@aspectcode/optimizer/dist/providers/retry.d.ts +14 -0
  195. package/node_modules/@aspectcode/optimizer/dist/providers/retry.d.ts.map +1 -1
  196. package/node_modules/@aspectcode/optimizer/dist/providers/retry.js +1 -0
  197. package/node_modules/@aspectcode/optimizer/dist/providers/retry.js.map +1 -1
  198. package/node_modules/@aspectcode/optimizer/dist/types.d.ts +14 -0
  199. package/node_modules/@aspectcode/optimizer/dist/types.d.ts.map +1 -1
  200. package/node_modules/@aspectcode/optimizer/dist/types.js.map +1 -1
  201. package/node_modules/@aspectcode/optimizer/package.json +2 -2
  202. package/node_modules/web-tree-sitter/LICENSE +21 -0
  203. package/node_modules/web-tree-sitter/README.md +198 -0
  204. package/node_modules/web-tree-sitter/package.json +36 -0
  205. package/node_modules/web-tree-sitter/tree-sitter-web.d.ts +204 -0
  206. package/node_modules/web-tree-sitter/tree-sitter.js +1 -0
  207. package/node_modules/web-tree-sitter/tree-sitter.wasm +0 -0
  208. package/package.json +8 -8
  209. package/dist/complaintProcessor.d.ts +0 -16
  210. package/dist/complaintProcessor.d.ts.map +0 -1
  211. package/dist/complaintProcessor.js +0 -134
  212. package/dist/complaintProcessor.js.map +0 -1
  213. package/node_modules/@aspectcode/emitters/dist/emitter.d.ts +0 -72
  214. package/node_modules/@aspectcode/emitters/dist/emitter.d.ts.map +0 -1
  215. package/node_modules/@aspectcode/emitters/dist/emitter.js +0 -10
  216. package/node_modules/@aspectcode/emitters/dist/emitter.js.map +0 -1
  217. package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts +0 -26
  218. package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts.map +0 -1
  219. package/node_modules/@aspectcode/emitters/dist/instructions/content.js +0 -501
  220. package/node_modules/@aspectcode/emitters/dist/instructions/content.js.map +0 -1
  221. package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts +0 -13
  222. package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts.map +0 -1
  223. package/node_modules/@aspectcode/emitters/dist/instructions/detection.js +0 -55
  224. package/node_modules/@aspectcode/emitters/dist/instructions/detection.js.map +0 -1
  225. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts +0 -9
  226. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts.map +0 -1
  227. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js +0 -30
  228. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js.map +0 -1
  229. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts +0 -21
  230. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts.map +0 -1
  231. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js +0 -125
  232. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js.map +0 -1
  233. package/node_modules/@aspectcode/emitters/dist/manifest.d.ts +0 -37
  234. package/node_modules/@aspectcode/emitters/dist/manifest.d.ts.map +0 -1
  235. package/node_modules/@aspectcode/emitters/dist/manifest.js +0 -50
  236. package/node_modules/@aspectcode/emitters/dist/manifest.js.map +0 -1
  237. package/node_modules/@aspectcode/emitters/dist/report.d.ts +0 -22
  238. package/node_modules/@aspectcode/emitters/dist/report.d.ts.map +0 -1
  239. package/node_modules/@aspectcode/emitters/dist/report.js +0 -3
  240. package/node_modules/@aspectcode/emitters/dist/report.js.map +0 -1
  241. package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts +0 -14
  242. package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts.map +0 -1
  243. package/node_modules/@aspectcode/emitters/dist/stableJson.js +0 -40
  244. package/node_modules/@aspectcode/emitters/dist/stableJson.js.map +0 -1
  245. package/node_modules/@aspectcode/emitters/dist/transaction.d.ts +0 -29
  246. package/node_modules/@aspectcode/emitters/dist/transaction.d.ts.map +0 -1
  247. package/node_modules/@aspectcode/emitters/dist/transaction.js +0 -104
  248. package/node_modules/@aspectcode/emitters/dist/transaction.js.map +0 -1
package/dist/pipeline.js CHANGED
@@ -1,15 +1,10 @@
1
1
  "use strict";
2
2
  /**
3
- * aspectcode pipeline — the single pipeline that does everything.
3
+ * aspectcode pipeline — analyze, generate, watch, evaluate.
4
4
  *
5
- * 1. Discover files tree-sitter analysis
6
- * 2. Build KB in memory (architecture + map + context)
7
- * 3. Scan & read other AI tool instruction files as context
8
- * 4. Build KB-custom content from analysis
9
- * 5. If generate=true → LLM generates AGENTS.md (or static fallback)
10
- * If generate=false → write KB-custom content only (skip LLM)
11
- * 6. If --kb flag → also write kb.md
12
- * 7. If not --once → watch for changes and repeat (always generate)
5
+ * v2: After generating AGENTS.md, watch mode evaluates individual file
6
+ * changes in real time. Full probe-and-refine runs on first startup
7
+ * or when the user presses 'r'.
13
8
  */
14
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
10
  if (k2 === undefined) k2 = k;
@@ -45,7 +40,11 @@ var __importStar = (this && this.__importStar) || (function () {
45
40
  };
46
41
  })();
47
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.isIgnoredPath = isIgnoredPath;
44
+ exports.isSupportedSourceFile = isSupportedSourceFile;
45
+ exports.createFileWatcher = createFileWatcher;
48
46
  exports.resolveRunMode = resolveRunMode;
47
+ exports.resolvePlatforms = resolvePlatforms;
49
48
  exports.runPipeline = runPipeline;
50
49
  const fs = __importStar(require("fs"));
51
50
  const path = __importStar(require("path"));
@@ -59,17 +58,31 @@ const kbBuilder_1 = require("./kbBuilder");
59
58
  const toolIngestion_1 = require("./toolIngestion");
60
59
  const writer_1 = require("./writer");
61
60
  const optimize_1 = require("./optimize");
62
- const complaintProcessor_1 = require("./complaintProcessor");
63
61
  const prompts_1 = require("./ui/prompts");
64
62
  const store_1 = require("./ui/store");
65
63
  const summary_1 = require("./summary");
66
64
  const diffSummary_1 = require("./diffSummary");
67
- // ── Watch constants ──────────────────────────────────────────
68
- const DEBOUNCE_MS = 2000;
65
+ const runtimeState_1 = require("./runtimeState");
66
+ const preferences_1 = require("./preferences");
67
+ const changeEvaluator_1 = require("./changeEvaluator");
68
+ const dreamCycle_1 = require("./dreamCycle");
69
+ const scopedRules_1 = require("./scopedRules");
70
+ const autoResolve_1 = require("./autoResolve");
71
+ const agentsMdRenderer_1 = require("./agentsMdRenderer");
72
+ const usageTracker_1 = require("./usageTracker");
73
+ const auth_1 = require("./auth");
74
+ const optimizer_1 = require("@aspectcode/optimizer");
75
+ // ── Constants ────────────────────────────────────────────────
76
+ const EVAL_DEBOUNCE_MS = 500;
77
+ const CO_CHANGE_SETTLE_MS = 5000;
78
+ const AUTO_PROBE_IDLE_MS = 5 * 60 * 1000;
79
+ const AUTO_PROBE_MIN_CHANGES = 20;
80
+ const AUTO_PROBE_COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12 hours
69
81
  const IGNORED_SEGMENTS = [
70
82
  '/node_modules/', '/.git/', '/dist/', '/build/', '/target/',
71
83
  '/coverage/', '/.next/', '/__pycache__/', '/.venv/', '/venv/',
72
84
  '/.pytest_cache/', '/.mypy_cache/', '/.tox/', '/htmlcov/',
85
+ '/.aspectcode/',
73
86
  ];
74
87
  function isIgnoredPath(filePath) {
75
88
  const normalized = filePath.replace(/\\/g, '/').toLowerCase();
@@ -79,15 +92,227 @@ function isSupportedSourceFile(filePath) {
79
92
  const ext = path.extname(filePath).toLowerCase();
80
93
  return core_1.SUPPORTED_EXTENSIONS.includes(ext);
81
94
  }
82
- // ── Single pipeline run ──────────────────────────────────────
83
- async function runOnce(ctx, ownership) {
95
+ // ── Managed files for memory map ─────────────────────────────
96
+ function fileMtime(filePath) {
97
+ try {
98
+ return fs.statSync(filePath).mtimeMs;
99
+ }
100
+ catch {
101
+ return 0;
102
+ }
103
+ }
104
+ function buildManagedFiles(root, preferenceCount, platforms = []) {
105
+ const files = [];
106
+ // ── Workspace-scope: AGENTS.md ──────────────────────────────
107
+ const agentsAbs = path.join(root, 'AGENTS.md');
108
+ if (fs.existsSync(agentsAbs)) {
109
+ files.push({ path: 'AGENTS.md', annotation: '', updatedAt: fileMtime(agentsAbs), category: 'agents', scope: 'workspace', owner: 'aspectcode' });
110
+ }
111
+ // ── Workspace-scope: platform instruction files ─────────────
112
+ if (platforms.includes('claude')) {
113
+ const claudeMdAbs = path.join(root, 'CLAUDE.md');
114
+ if (fs.existsSync(claudeMdAbs)) {
115
+ files.push({ path: 'CLAUDE.md', annotation: '○ user', updatedAt: fileMtime(claudeMdAbs), category: 'workspace-config', scope: 'workspace', owner: 'user' });
116
+ }
117
+ }
118
+ if (platforms.includes('cursor')) {
119
+ const cursorrules = path.join(root, '.cursorrules');
120
+ if (fs.existsSync(cursorrules)) {
121
+ files.push({ path: '.cursorrules', annotation: '○ user', updatedAt: fileMtime(cursorrules), category: 'workspace-config', scope: 'workspace', owner: 'user' });
122
+ }
123
+ }
124
+ // ── Workspace-scope: scoped rules from manifest ─────────────
125
+ const manifestPath = path.join(root, '.aspectcode', 'scoped-rules.json');
126
+ const manifestRulePaths = new Set();
127
+ let manifestNeedsCleanup = false;
128
+ if (fs.existsSync(manifestPath)) {
129
+ try {
130
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
131
+ const validRules = [];
132
+ for (const entry of manifest.rules ?? []) {
133
+ if (manifestRulePaths.has(entry.path))
134
+ continue;
135
+ const absPath = path.join(root, entry.path);
136
+ // Skip files that no longer exist on disk
137
+ if (!fs.existsSync(absPath)) {
138
+ manifestNeedsCleanup = true;
139
+ continue;
140
+ }
141
+ manifestRulePaths.add(entry.path);
142
+ validRules.push(entry);
143
+ const cat = entry.path.startsWith('.claude/') ? 'claude-rule'
144
+ : entry.path.startsWith('.cursor/') ? 'cursor-rule'
145
+ : 'agents';
146
+ const ts = entry.updatedAt ? new Date(entry.updatedAt).getTime() : fileMtime(absPath);
147
+ files.push({ path: entry.path, annotation: '● active', updatedAt: ts, category: cat, scope: 'workspace', owner: 'aspectcode' });
148
+ }
149
+ // Clean stale entries from manifest so they don't persist across restarts
150
+ if (manifestNeedsCleanup) {
151
+ try {
152
+ fs.writeFileSync(manifestPath, JSON.stringify({ version: 1, rules: validRules }, null, 2) + '\n');
153
+ }
154
+ catch { /* ignore write errors */ }
155
+ }
156
+ }
157
+ catch { /* malformed manifest */ }
158
+ }
159
+ // ── Workspace-scope: user-created scoped rules ──────────────
160
+ const platformRuleDirs = [];
161
+ if (platforms.includes('claude'))
162
+ platformRuleDirs.push({ dir: '.claude/rules', ext: '.md' });
163
+ if (platforms.includes('cursor'))
164
+ platformRuleDirs.push({ dir: '.cursor/rules', ext: '.mdc' });
165
+ for (const { dir: rulesDir, ext } of platformRuleDirs) {
166
+ const absDir = path.join(root, rulesDir);
167
+ try {
168
+ if (fs.existsSync(absDir)) {
169
+ for (const entry of fs.readdirSync(absDir)) {
170
+ if (!entry.endsWith(ext))
171
+ continue;
172
+ if (entry.startsWith('ac-'))
173
+ continue; // aspectcode-managed
174
+ const relPath = `${rulesDir}/${entry}`;
175
+ if (manifestRulePaths.has(relPath))
176
+ continue; // already tracked
177
+ files.push({ path: relPath, annotation: '○ user', updatedAt: fileMtime(path.join(absDir, entry)), category: 'user-rule', scope: 'workspace', owner: 'user' });
178
+ }
179
+ }
180
+ }
181
+ catch { /* unreadable dir */ }
182
+ }
183
+ // ── Workspace-scope: settings ───────────────────────────────
184
+ if (platforms.includes('claude')) {
185
+ const settingsLocal = path.join(root, '.claude', 'settings.local.json');
186
+ if (fs.existsSync(settingsLocal)) {
187
+ files.push({ path: '.claude/settings.local.json', annotation: '○ user', updatedAt: fileMtime(settingsLocal), category: 'workspace-config', scope: 'workspace', owner: 'user' });
188
+ }
189
+ }
190
+ // ── Workspace-scope: .aspectcode files ──────────────────────
191
+ if (preferenceCount > 0) {
192
+ files.push({ path: '☁ preferences', annotation: `${preferenceCount} learned`, updatedAt: 0, category: 'cloud', scope: 'workspace', owner: 'aspectcode' });
193
+ }
194
+ const dreamStatePath = path.join(root, '.aspectcode', 'dream-state.json');
195
+ if (fs.existsSync(dreamStatePath)) {
196
+ try {
197
+ const ds = JSON.parse(fs.readFileSync(dreamStatePath, 'utf-8'));
198
+ const lastDream = ds.lastDreamAt ? new Date(ds.lastDreamAt).getTime() : 0;
199
+ files.push({ path: '.aspectcode/dream-state.json', annotation: '', updatedAt: lastDream, category: 'aspectcode', scope: 'workspace', owner: 'aspectcode' });
200
+ }
201
+ catch {
202
+ files.push({ path: '.aspectcode/dream-state.json', annotation: '', updatedAt: 0, category: 'aspectcode', scope: 'workspace', owner: 'aspectcode' });
203
+ }
204
+ }
205
+ // ── Device-scope: ~/.claude/ memory (Claude Code only) ──────
206
+ if (platforms.includes('claude')) {
207
+ const home = require('os').homedir();
208
+ // Device-root CLAUDE.md
209
+ const deviceClaudeMd = path.join(home, '.claude', 'CLAUDE.md');
210
+ if (fs.existsSync(deviceClaudeMd)) {
211
+ files.push({ path: '~/.claude/CLAUDE.md', annotation: '○ device', updatedAt: fileMtime(deviceClaudeMd), category: 'device', scope: 'device', owner: 'device' });
212
+ }
213
+ // Project-level CLAUDE.md (~/.claude/projects/<hash>/CLAUDE.md)
214
+ const projectClaudeMd = findDeviceProjectClaudeMd(home, root);
215
+ if (projectClaudeMd) {
216
+ files.push({ path: '~/.claude/projects/.../CLAUDE.md', annotation: '○ device', updatedAt: fileMtime(projectClaudeMd), category: 'device', scope: 'device', owner: 'device' });
217
+ }
218
+ }
219
+ return files;
220
+ }
221
+ /** Find the project-level CLAUDE.md in ~/.claude/projects/<hash>/ for the given workspace. */
222
+ function findDeviceProjectClaudeMd(home, root) {
223
+ const projectsBase = path.join(home, '.claude', 'projects');
224
+ if (!fs.existsSync(projectsBase))
225
+ return undefined;
226
+ const normalised = root.replace(/\\/g, '/');
227
+ const crypto = require('crypto');
228
+ // Try common hash strategies
229
+ const candidates = [
230
+ crypto.createHash('md5').update(normalised).digest('hex'),
231
+ crypto.createHash('sha256').update(normalised).digest('hex').slice(0, 32),
232
+ Buffer.from(normalised).toString('base64url'),
233
+ normalised.replace(/\//g, '-').replace(/^-/, ''),
234
+ ];
235
+ for (const candidate of candidates) {
236
+ const claudeMd = path.join(projectsBase, candidate, 'CLAUDE.md');
237
+ if (fs.existsSync(claudeMd))
238
+ return claudeMd;
239
+ }
240
+ // Fallback: scan project directories for CLAUDE.md
241
+ try {
242
+ for (const dir of fs.readdirSync(projectsBase)) {
243
+ const claudeMd = path.join(projectsBase, dir, 'CLAUDE.md');
244
+ if (fs.existsSync(claudeMd)) {
245
+ // Verify this directory is for our workspace by checking a jsonl file
246
+ const fullDir = path.join(projectsBase, dir);
247
+ const jsonls = fs.readdirSync(fullDir).filter((f) => f.endsWith('.jsonl'));
248
+ if (jsonls.length > 0) {
249
+ try {
250
+ const fd = fs.openSync(path.join(fullDir, jsonls[0]), 'r');
251
+ const buf = Buffer.alloc(2048);
252
+ const bytesRead = fs.readSync(fd, buf, 0, 2048, 0);
253
+ fs.closeSync(fd);
254
+ const sample = buf.slice(0, bytesRead).toString('utf-8');
255
+ if (sample.includes(normalised) || sample.includes(root.replace(/\\/g, '\\\\'))) {
256
+ return claudeMd;
257
+ }
258
+ }
259
+ catch { /* skip */ }
260
+ }
261
+ }
262
+ }
263
+ }
264
+ catch { /* scan failed */ }
265
+ return undefined;
266
+ }
267
+ // ── File watcher ─────────────────────────────────────────────
268
+ function createFileWatcher(root, onEvent) {
269
+ return fs.watch(root, { recursive: true }, (event, filename) => {
270
+ if (!filename)
271
+ return;
272
+ const posixPath = filename.replace(/\\/g, '/');
273
+ const abs = path.resolve(root, filename);
274
+ if (!isSupportedSourceFile(abs) || isIgnoredPath(abs))
275
+ return;
276
+ const type = event === 'rename'
277
+ ? (fs.existsSync(abs) ? 'add' : 'unlink')
278
+ : 'change';
279
+ onEvent(type, posixPath);
280
+ });
281
+ }
282
+ /**
283
+ * @param probeAndRefine Run probe-based evaluation after LLM generation.
284
+ * Only on first run or when user presses 'r'.
285
+ */
286
+ async function runOnce(ctx, ownership, probeAndRefine = false, preferences, activePlatforms = ['claude'], userSettings) {
84
287
  const { root, flags, log } = ctx;
85
288
  const config = (0, config_1.loadConfig)(root);
86
289
  const startMs = Date.now();
87
290
  store_1.store.resetRun();
88
291
  store_1.store.setRunStartMs(startMs);
89
- store_1.store.addSetupNote(config ? 'config loaded' : 'no config');
90
- // ── First-run detection ───────────────────────────────────
292
+ if (config)
293
+ store_1.store.addSetupNote('using config file');
294
+ // Clean stale manifest entries (files deleted by user)
295
+ const deletedSlugs = new Set();
296
+ const manifestPath = path.join(root, '.aspectcode', 'scoped-rules.json');
297
+ if (fs.existsSync(manifestPath)) {
298
+ try {
299
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
300
+ const validRules = [];
301
+ for (const entry of manifest.rules ?? []) {
302
+ if (fs.existsSync(path.join(root, entry.path))) {
303
+ validRules.push(entry);
304
+ }
305
+ else {
306
+ deletedSlugs.add(entry.slug);
307
+ }
308
+ }
309
+ if (deletedSlugs.size > 0) {
310
+ fs.writeFileSync(manifestPath, JSON.stringify({ version: 1, rules: validRules }, null, 2) + '\n');
311
+ log.debug(`Cleaned ${deletedSlugs.size} stale manifest entries`);
312
+ }
313
+ }
314
+ catch { /* ignore */ }
315
+ }
91
316
  const agentsPath = path.join(root, 'AGENTS.md');
92
317
  if (!fs.existsSync(agentsPath)) {
93
318
  store_1.store.setFirstRun(true);
@@ -96,7 +321,7 @@ async function runOnce(ctx, ownership) {
96
321
  store_1.store.setPhase('discovering');
97
322
  const workspace = await (0, workspace_1.loadWorkspaceFiles)(root, config, log, { quiet: flags.quiet, spin: ctx.spin });
98
323
  if (workspace.discoveredPaths.length === 0) {
99
- log.warn('No source files found. Check your workspace or exclude patterns.');
324
+ log.warn('No source files found.');
100
325
  return { code: cli_1.ExitCode.ERROR, kbContent: '' };
101
326
  }
102
327
  // ── 2. Analyze ────────────────────────────────────────────
@@ -104,255 +329,963 @@ async function runOnce(ctx, ownership) {
104
329
  const model = await (0, core_1.analyzeRepoWithDependencies)(root, workspace.relativeFiles, workspace.absoluteFiles, workspace.host);
105
330
  spinAnalyze.stop(`Analyzed ${model.files.length} files, ${model.graph.edges.length} edges`);
106
331
  store_1.store.setStats(model.files.length, model.graph.edges.length);
107
- // ── 3. Build KB content in memory ─────────────────────────
332
+ // ── 3. Build KB ───────────────────────────────────────────
108
333
  const spinKb = ctx.spin('Building knowledge base…', 'building-kb');
109
334
  const kbContent = (0, kbBuilder_1.buildKbContent)(model, root, workspace.relativeFiles);
110
335
  spinKb.stop('Knowledge base built');
111
- // ── 4. Read other AI tool instruction files ───────────────
336
+ // ── 4. Read tool instruction files ────────────────────────
112
337
  const host = (0, emitters_1.createNodeEmitterHost)();
113
338
  const toolInstructions = await (0, toolIngestion_1.readToolInstructions)(host, root);
114
339
  if (toolInstructions.size > 0) {
115
- const toolNames = [...toolInstructions.keys()].join(', ');
116
- store_1.store.addSetupNote(`context: ${toolNames}`);
117
- log.debug(`Read ${toolInstructions.size} AI tool instruction file(s) as context`);
118
- }
119
- // ── 5. Build base content from KB ──────────────────────────
120
- const baseContent = kbContent.length > 0
121
- ? (0, emitters_1.generateKbCustomContent)(kbContent, 'safe')
122
- : (0, emitters_1.generateCanonicalContentForMode)('safe', false);
123
- // ── 6. Generate or skip ────────────────────────────────────
124
- // When ctx.generate is true: write base template immediately for
125
- // early feedback, then run LLM (or static fallback), then overwrite.
126
- // When ctx.generate is false: write KB-custom content only.
340
+ store_1.store.addSetupNote(`context: ${[...toolInstructions.keys()].join(', ')}`);
341
+ }
342
+ // ── 5. Build base content (directly from model, no KB extraction) ──
343
+ const baseContent = (0, agentsMdRenderer_1.renderAgentsMd)(model, path.basename(root));
344
+ // ── 6. Generate or skip ───────────────────────────────────
345
+ let finalContent = baseContent;
127
346
  if (ctx.generate) {
128
- // Write base immediately so user sees output before LLM finishes
129
347
  if (!flags.dryRun) {
130
348
  await (0, writer_1.writeAgentsMd)(host, root, baseContent, ownership);
131
349
  store_1.store.addOutput('AGENTS.md written (base)');
132
- log.debug('Base AGENTS.md written from static analysis');
133
350
  }
134
351
  store_1.store.setPhase('optimizing');
135
- const optimizeResult = await (0, optimize_1.tryOptimize)(ctx, kbContent, toolInstructions, config, baseContent);
136
- // ── Write LLM-generated AGENTS.md ─────────────────────
352
+ // Skip probe-and-refine if tier is already exhausted
353
+ const skipProbe = probeAndRefine && store_1.store.state.tierExhausted;
354
+ let optimizeResult;
355
+ try {
356
+ optimizeResult = await (0, optimize_1.tryOptimize)(ctx, kbContent, toolInstructions, config, baseContent, skipProbe ? false : probeAndRefine, preferences, userSettings, []);
357
+ }
358
+ catch (err) {
359
+ if (err?.tierExhausted) {
360
+ store_1.store.setTierExhausted();
361
+ optimizeResult = { content: baseContent, reasoning: [], scopedRules: [], deleteSlugs: [] };
362
+ }
363
+ else {
364
+ throw err;
365
+ }
366
+ }
367
+ finalContent = optimizeResult.content;
137
368
  store_1.store.setPhase('writing');
138
369
  if (flags.dryRun) {
139
370
  log.info(logger_1.fmt.bold('Dry run — proposed AGENTS.md:'));
140
371
  log.blank();
141
- log.info(optimizeResult.content);
142
- log.blank();
372
+ log.info(finalContent);
143
373
  }
144
374
  else {
145
- // Compute diff before overwriting (for watch-mode change summary)
146
375
  let previousContent;
147
376
  try {
148
377
  if (fs.existsSync(agentsPath)) {
149
378
  previousContent = fs.readFileSync(agentsPath, 'utf-8');
150
379
  }
151
380
  }
152
- catch { /* ignore read errors */ }
153
- await (0, writer_1.writeAgentsMd)(host, root, optimizeResult.content, ownership);
381
+ catch { /* ignore */ }
382
+ await (0, writer_1.writeAgentsMd)(host, root, finalContent, ownership);
154
383
  const modeLabel = ownership === 'section' ? ' (section)' : '';
155
384
  const verb = optimizeResult.reasoning.length > 0 ? 'generated' : 'written';
156
385
  store_1.store.addOutput(`AGENTS.md ${verb}${modeLabel}`);
157
- log.success(`AGENTS.md ${verb}${modeLabel}`);
158
- // Diff summary (skip on first write when previous was just the base template)
159
386
  if (previousContent !== undefined && previousContent !== baseContent) {
160
- const diff = (0, diffSummary_1.diffSummary)(previousContent, optimizeResult.content);
161
- store_1.store.setDiffSummary(diff);
387
+ store_1.store.setDiffSummary((0, diffSummary_1.diffSummary)(previousContent, finalContent));
162
388
  }
163
- // Content summary for the dashboard
164
- const summary = (0, summary_1.summarizeContent)(optimizeResult.content);
165
- store_1.store.setSummary(summary);
389
+ store_1.store.setSummary((0, summary_1.summarizeContent)(finalContent));
166
390
  }
167
391
  }
168
392
  else {
169
- // Skip LLM — write KB-custom content only
170
393
  store_1.store.setPhase('writing');
171
394
  if (flags.dryRun) {
172
395
  log.info(logger_1.fmt.bold('Dry run — proposed AGENTS.md (KB-custom):'));
173
396
  log.blank();
174
397
  log.info(baseContent);
175
- log.blank();
176
398
  }
177
399
  else {
178
400
  await (0, writer_1.writeAgentsMd)(host, root, baseContent, ownership);
179
401
  store_1.store.addOutput('AGENTS.md written (KB-custom)');
180
- log.success('AGENTS.md written from static analysis');
181
- const summary = (0, summary_1.summarizeContent)(baseContent);
182
- store_1.store.setSummary(summary);
402
+ store_1.store.setSummary((0, summary_1.summarizeContent)(baseContent));
183
403
  }
184
- store_1.store.addSetupNote('generation skipped');
404
+ // No LLM available — use static extraction for scoped rules
405
+ // No static rules in non-LLM path — dream cycle handles rules
185
406
  }
186
- // ── 8. Optionally write kb.md ─────────────────────────────
187
- if (flags.kb && !flags.dryRun) {
188
- await (0, writer_1.writeKbMd)(host, root, kbContent);
189
- store_1.store.addOutput('kb.md written');
190
- log.success('kb.md written');
407
+ // ── 7. Persist runtime state ───────────────────────────────
408
+ // Snapshot hub counts before updating state (for new-hub detection)
409
+ const hubCounts = new Map();
410
+ for (const hub of model.metrics.hubs) {
411
+ hubCounts.set(hub.file, hub.inDegree);
191
412
  }
413
+ (0, runtimeState_1.updateRuntimeState)({
414
+ model,
415
+ kbContent,
416
+ agentsContent: finalContent,
417
+ fileContents: workspace.relativeFiles,
418
+ previousHubCounts: hubCounts,
419
+ });
420
+ // ── 8. Scoped rules are managed exclusively by the dream cycle.
421
+ // No static rules written during runOnce.
422
+ // ── 9. Populate memory map ─────────────────────────────────
423
+ const prefs = await (0, preferences_1.loadPreferences)(root);
424
+ store_1.store.setManagedFiles(buildManagedFiles(root, prefs.preferences.length, activePlatforms));
192
425
  const elapsedMs = Date.now() - startMs;
193
426
  store_1.store.setElapsed(`${(elapsedMs / 1000).toFixed(1)}s`);
194
427
  store_1.store.setPhase('done');
195
- log.info(logger_1.fmt.dim(`Done in ${(elapsedMs / 1000).toFixed(1)}s`));
196
428
  return { code: cli_1.ExitCode.OK, kbContent };
197
429
  }
198
- // ── Pipeline entry point ─────────────────────────────────────
199
- /**
200
- * Resolve AGENTS.md ownership mode and whether to generate on this run.
201
- *
202
- * Called from main() BEFORE the ink dashboard is mounted, because
203
- * the interactive prompt uses raw stdin which conflicts with ink's useInput.
204
- *
205
- * When AGENTS.md already has section markers → section + generate (auto).
206
- * Otherwise (no file, or file without markers) → 3-option prompt:
207
- * 1. Full control — generate now
208
- * 2. Full control — skip generation (use KB-custom only)
209
- * 3. Section control (preserve your content)
210
- */
430
+ // ── Resolve ownership mode ───────────────────────────────────
211
431
  async function resolveRunMode(root) {
212
432
  const config = (0, config_1.loadConfig)(root);
213
- // Config-driven ownership skips prompt but still generates
214
433
  if (config?.ownership) {
215
434
  return { ownership: config.ownership, generate: true };
216
435
  }
436
+ const agentsPath = path.join(root, 'AGENTS.md');
437
+ let existingContent = null;
217
438
  try {
218
- const agentsPath = path.join(root, 'AGENTS.md');
219
439
  if (fs.existsSync(agentsPath)) {
220
- const existing = fs.readFileSync(agentsPath, 'utf-8');
221
- // Auto-detect section markers → continue in section mode
222
- if ((0, writer_1.hasMarkers)(existing)) {
440
+ existingContent = fs.readFileSync(agentsPath, 'utf-8');
441
+ if ((0, writer_1.hasMarkers)(existingContent))
223
442
  return { ownership: 'section', generate: true };
443
+ }
444
+ }
445
+ catch { /* fall through */ }
446
+ try {
447
+ const hasExisting = existingContent !== null;
448
+ const options = hasExisting
449
+ ? ['Full control (replace entire file)', 'Section control (preserve your content)', 'Preview current AGENTS.md']
450
+ : ['Full control (replace entire file)', 'Section control (preserve your content)'];
451
+ // eslint-disable-next-line no-constant-condition
452
+ while (true) {
453
+ const idx = await (0, prompts_1.selectPrompt)('How should AspectCode manage AGENTS.md?', options, 0);
454
+ // Preview option — show content, then loop back to prompt
455
+ if (hasExisting && idx === 2) {
456
+ showFilePreview(existingContent);
457
+ continue;
224
458
  }
225
- // Existing file without markers full control, skip generation
226
- return { ownership: 'full', generate: false };
459
+ const ownership = idx === 1 ? 'section' : 'full';
460
+ // Save choice so we don't ask again
461
+ (0, config_1.saveConfig)(root, { ownership });
462
+ return { ownership, generate: true };
227
463
  }
228
464
  }
229
465
  catch {
230
- // Read error fall through to prompt
466
+ return { ownership: 'full', generate: true };
231
467
  }
232
- // No AGENTS.md → show 2-option prompt (default: full + generate)
468
+ }
469
+ /**
470
+ * Display file content in a scrollable pager-like view.
471
+ * Press any key to return.
472
+ */
473
+ function showFilePreview(content) {
474
+ return new Promise((resolve) => {
475
+ const lines = content.split('\n');
476
+ const termHeight = process.stdout.rows || 24;
477
+ const visibleLines = termHeight - 4; // leave room for header/footer
478
+ let scrollOffset = 0;
479
+ const render = () => {
480
+ process.stdout.write('\x1b[2J\x1b[H'); // clear screen
481
+ process.stdout.write('\x1b[35m── AGENTS.md preview ──\x1b[0m\n\n');
482
+ const slice = lines.slice(scrollOffset, scrollOffset + visibleLines);
483
+ for (const line of slice) {
484
+ process.stdout.write(` ${line}\n`);
485
+ }
486
+ const pct = lines.length <= visibleLines
487
+ ? 100
488
+ : Math.round(((scrollOffset + visibleLines) / lines.length) * 100);
489
+ process.stdout.write(`\n\x1b[90m ↑/↓ scroll · q/esc to go back · ${pct}%\x1b[0m`);
490
+ };
491
+ render();
492
+ process.stdin.setRawMode(true);
493
+ process.stdin.resume();
494
+ process.stdin.setEncoding('utf-8');
495
+ const onData = (key) => {
496
+ if (key === '\x1b[A' || key === 'k') {
497
+ scrollOffset = Math.max(0, scrollOffset - 1);
498
+ render();
499
+ }
500
+ else if (key === '\x1b[B' || key === 'j') {
501
+ scrollOffset = Math.min(Math.max(0, lines.length - visibleLines), scrollOffset + 1);
502
+ render();
503
+ }
504
+ else if (key === '\x1b[5~') {
505
+ // Page Up
506
+ scrollOffset = Math.max(0, scrollOffset - visibleLines);
507
+ render();
508
+ }
509
+ else if (key === '\x1b[6~') {
510
+ // Page Down
511
+ scrollOffset = Math.min(Math.max(0, lines.length - visibleLines), scrollOffset + visibleLines);
512
+ render();
513
+ }
514
+ else if (key === 'q' || key === '\x1b' || key === '\r' || key === '\n') {
515
+ process.stdin.setRawMode(false);
516
+ process.stdin.pause();
517
+ process.stdin.removeListener('data', onData);
518
+ process.stdout.write('\x1b[2J\x1b[H'); // clear screen
519
+ resolve();
520
+ }
521
+ else if (key === '\x03') {
522
+ process.stdin.setRawMode(false);
523
+ process.stdin.pause();
524
+ process.stdin.removeListener('data', onData);
525
+ process.exit(130);
526
+ }
527
+ };
528
+ process.stdin.on('data', onData);
529
+ });
530
+ }
531
+ // ── Platform resolution ─────────────────────────────────────
532
+ const ALL_PLATFORMS = [
533
+ { id: 'claude', label: 'Claude Code', detect: ['.claude'] },
534
+ { id: 'cursor', label: 'Cursor', detect: ['.cursor', '.cursorrules'] },
535
+ { id: 'copilot', label: 'GitHub Copilot', detect: ['.github'] },
536
+ { id: 'windsurf', label: 'Windsurf', detect: ['.windsurfrules'] },
537
+ { id: 'codex', label: 'Codex', detect: [] },
538
+ { id: 'cline', label: 'Cline', detect: ['.clinerules'] },
539
+ { id: 'gemini', label: 'Gemini', detect: ['GEMINI.md'] },
540
+ { id: 'aider', label: 'Aider', detect: ['CONVENTIONS.md'] },
541
+ ];
542
+ async function resolvePlatforms(root) {
543
+ const config = (0, config_1.loadConfig)(root);
544
+ const configured = (0, config_1.getConfigPlatforms)(config);
545
+ if (configured?.length) {
546
+ // Check if collaborator has a platform not in config
547
+ const declined = config?.declinedPlatforms ?? [];
548
+ const detected = ALL_PLATFORMS
549
+ .filter((p) => p.detect.some((d) => fs.existsSync(path.join(root, d))))
550
+ .map((p) => p.id);
551
+ const missing = detected.filter((d) => !configured.includes(d) && !declined.includes(d));
552
+ if (missing.length > 0) {
553
+ const names = missing.map((id) => ALL_PLATFORMS.find((p) => p.id === id)?.label ?? id).join(', ');
554
+ const { confirmPrompt } = await Promise.resolve().then(() => __importStar(require('./ui/prompts')));
555
+ const add = await confirmPrompt(`${names} detected but not configured. Add?`);
556
+ if (add) {
557
+ const updated = [...configured, ...missing];
558
+ (0, config_1.saveConfig)(root, { platforms: updated });
559
+ return updated;
560
+ }
561
+ else {
562
+ // Remember declined so we don't ask again
563
+ (0, config_1.saveConfig)(root, { declinedPlatforms: [...declined, ...missing] });
564
+ }
565
+ }
566
+ return configured;
567
+ }
568
+ // First run: auto-detect + prompt
569
+ const detected = ALL_PLATFORMS
570
+ .filter((p) => p.detect.some((d) => fs.existsSync(path.join(root, d))))
571
+ .map((p) => p.id);
572
+ const preselected = detected.map((id) => ALL_PLATFORMS.findIndex((p) => p.id === id)).filter((i) => i >= 0);
233
573
  try {
234
- const idx = await (0, prompts_1.selectPrompt)('How should AspectCode manage AGENTS.md?', [
235
- 'Full control (replace entire file)',
236
- 'Section control (preserve your content)',
237
- ], 0);
238
- const ownership = idx === 1 ? 'section' : 'full';
239
- return { ownership, generate: true };
574
+ const { multiSelectPrompt } = await Promise.resolve().then(() => __importStar(require('./ui/prompts')));
575
+ const labels = ALL_PLATFORMS.map((p) => {
576
+ const det = detected.includes(p.id) ? ' (detected)' : '';
577
+ return `${p.label}${det}`;
578
+ });
579
+ const indices = await multiSelectPrompt('Which editors do you use?', labels, preselected);
580
+ const selected = indices.map((i) => ALL_PLATFORMS[i].id);
581
+ const platforms = selected.length > 0 ? selected : ['claude']; // default to Claude Code
582
+ (0, config_1.saveConfig)(root, { platforms });
583
+ return platforms;
240
584
  }
241
585
  catch {
242
- // Non-interactive default to full + generate
243
- return { ownership: 'full', generate: true };
586
+ const platforms = detected.length > 0 ? detected : ['claude'];
587
+ (0, config_1.saveConfig)(root, { platforms });
588
+ return platforms;
589
+ }
590
+ }
591
+ async function handleAssessmentAction(action, prefs, root, _ownership) {
592
+ if (!action.assessment)
593
+ return prefs;
594
+ const a = action.assessment;
595
+ const dir = path.dirname(a.file) + '/';
596
+ if (action.type === 'dismiss') {
597
+ // Dismiss → directory-scoped allow (broad: "this is fine here")
598
+ prefs = (0, preferences_1.addPreference)(prefs, {
599
+ rule: a.rule,
600
+ pattern: a.message,
601
+ disposition: 'allow',
602
+ directory: dir,
603
+ details: a.details,
604
+ dependencyContext: a.dependencyContext,
605
+ });
606
+ (0, preferences_1.savePreferences)(root, prefs);
607
+ store_1.store.setPreferenceCount(prefs.preferences.length);
608
+ store_1.store.setLearnedMessage(`Suppressed: ${a.rule} in ${dir}`);
609
+ store_1.store.resolveAssessment('dismiss');
610
+ (0, dreamCycle_1.addCorrection)('dismiss', a);
611
+ }
612
+ else if (action.type === 'confirm') {
613
+ // Confirm → file-scoped deny (specific: "this matters for this file")
614
+ prefs = (0, preferences_1.addPreference)(prefs, {
615
+ rule: a.rule,
616
+ pattern: a.message,
617
+ disposition: 'deny',
618
+ file: a.file,
619
+ directory: dir,
620
+ details: a.details,
621
+ suggestion: a.suggestion,
622
+ dependencyContext: a.dependencyContext,
623
+ });
624
+ (0, preferences_1.savePreferences)(root, prefs);
625
+ store_1.store.setPreferenceCount(prefs.preferences.length);
626
+ store_1.store.setLearnedMessage(`Enforced: ${a.rule} for ${a.file}`);
627
+ store_1.store.setRecommendProbe(true);
628
+ store_1.store.resolveAssessment('confirm');
629
+ (0, dreamCycle_1.addCorrection)('confirm', a);
630
+ // Don't directly modify AGENTS.md — the dream cycle handles integration cleanly
631
+ }
632
+ else if (action.type === 'skip') {
633
+ store_1.store.advanceAssessment();
244
634
  }
635
+ store_1.store.setCorrectionCount((0, dreamCycle_1.getUnprocessedCount)());
636
+ return prefs;
245
637
  }
638
+ // ── Main pipeline ────────────────────────────────────────────
246
639
  async function runPipeline(ctx) {
247
640
  const { root, flags, log, ownership } = ctx;
641
+ // Set terminal title
642
+ const projectName = path.basename(root);
643
+ process.stdout.write(`\x1b]0;aspectcode — ${projectName}\x07`);
248
644
  log.info(`${logger_1.fmt.bold('aspectcode')} — ${logger_1.fmt.cyan(root)}`);
249
645
  log.blank();
250
- // ── Initial run ──────────────────────────────────────────
251
- const result = await runOnce(ctx, ownership);
252
- // After first run, always generate on subsequent watch-triggered runs
646
+ store_1.store.setRootPath(root);
647
+ // ── Show update message if available ──────────────────────
648
+ const updateMsg = globalThis.__updateMessage;
649
+ if (updateMsg)
650
+ store_1.store.setUpdateMessage(updateMsg);
651
+ // ── Set platforms from pre-resolved context ────────────────
652
+ const activePlatforms = ctx.platforms;
653
+ store_1.store.setPlatform(activePlatforms.join(', '));
654
+ // ── Check login status ────────────────────────────────────
655
+ const creds = (0, auth_1.loadCredentials)();
656
+ store_1.store.setUserEmail(creds?.email ?? '');
657
+ // ── Detect tier ──────────────────────────────────────────
658
+ const projectConfig = (0, config_1.loadConfig)(root);
659
+ const hasByokKey = !!(projectConfig?.apiKey || process.env.ASPECTCODE_LLM_KEY);
660
+ if (hasByokKey) {
661
+ store_1.store.setTierInfo('byok', 0, 0);
662
+ }
663
+ else if (creds) {
664
+ // Fetch tier + usage from verify endpoint
665
+ try {
666
+ const res = await fetch(`${auth_1.WEB_APP_URL}/api/cli/verify`, {
667
+ method: 'POST',
668
+ headers: { Authorization: `Bearer ${creds.token}` },
669
+ });
670
+ if (res.ok) {
671
+ const data = (await res.json());
672
+ const tier = (data.tier === 'PRO' ? 'pro' : 'free');
673
+ const used = data.usage?.tokensUsed ?? creds.tierTokensUsed ?? 0;
674
+ const cap = data.usage?.tokensCap ?? creds.tierTokensCap ?? 100000;
675
+ const resetAt = data.usage?.resetAt ?? '';
676
+ store_1.store.setTierInfo(tier, used, cap, resetAt || undefined);
677
+ if (used >= cap)
678
+ store_1.store.setTierExhausted();
679
+ (0, auth_1.updateCredentials)({ tier, tierTokensUsed: used, tierTokensCap: cap });
680
+ }
681
+ else if (creds.tier) {
682
+ // Offline fallback — use cached tier
683
+ const used = creds.tierTokensUsed ?? 0;
684
+ const cap = creds.tierTokensCap ?? 100000;
685
+ store_1.store.setTierInfo(creds.tier, used, cap);
686
+ if (used >= cap)
687
+ store_1.store.setTierExhausted();
688
+ }
689
+ }
690
+ catch {
691
+ // Offline — use cached tier if available
692
+ if (creds.tier) {
693
+ const used = creds.tierTokensUsed ?? 0;
694
+ const cap = creds.tierTokensCap ?? 100000;
695
+ store_1.store.setTierInfo(creds.tier, used, cap);
696
+ if (used >= cap)
697
+ store_1.store.setTierExhausted();
698
+ }
699
+ }
700
+ }
701
+ // ── Load user settings from cloud ─────────────────────────
702
+ const userSettings = await (0, config_1.loadUserSettings)();
703
+ // Stash for Dashboard settings panel access
704
+ store_1.store._userSettings = userSettings;
705
+ // ── Initial run — only probe-and-refine on first run (no existing AGENTS.md)
706
+ const initialProbeAndRefine = !fs.existsSync(path.join(root, 'AGENTS.md'));
707
+ const result = await runOnce(ctx, ownership, initialProbeAndRefine, undefined, activePlatforms, userSettings);
253
708
  ctx.generate = true;
254
709
  if (result.code !== cli_1.ExitCode.OK)
255
710
  return result.code;
256
- // Keep track of latest KB for complaint processing
257
- let latestKb = result.kbContent;
258
- // ── --once: process any queued complaints, then exit ──────
259
- if (flags.once) {
260
- if (store_1.store.state.complaintQueue.length > 0) {
261
- await (0, complaintProcessor_1.processComplaints)(ctx, ownership, latestKb);
262
- }
711
+ if (flags.once)
263
712
  return cli_1.ExitCode.OK;
713
+ // ── Load preferences ───────────────────────────────────────
714
+ let prefs = await (0, preferences_1.loadPreferences)(root);
715
+ store_1.store.setPreferenceCount(prefs.preferences.length);
716
+ // ── Fetch community suggestions ───────────────────────────
717
+ if (store_1.store.state.userTier !== 'byok') {
718
+ // Detect primary language from analysis model
719
+ const state = (0, runtimeState_1.getRuntimeState)();
720
+ if (state.model) {
721
+ const langCounts = new Map();
722
+ for (const f of state.model.files) {
723
+ langCounts.set(f.language, (langCounts.get(f.language) ?? 0) + 1);
724
+ }
725
+ let primaryLang = '';
726
+ let maxCount = 0;
727
+ for (const [lang, count] of langCounts) {
728
+ if (count > maxCount) {
729
+ primaryLang = lang;
730
+ maxCount = count;
731
+ }
732
+ }
733
+ if (primaryLang) {
734
+ // Fetch community suggestions, filter to relevant ones, feed to dream cycle
735
+ const projectDirs = new Set(state.model.files.map((f) => {
736
+ const parts = f.relativePath.split('/');
737
+ return parts.length > 1 ? parts[0] + '/' : '';
738
+ }));
739
+ (0, preferences_1.fetchSuggestions)(primaryLang, undefined, { byok: false })
740
+ .then((suggestions) => {
741
+ // Filter out suggestions for directories that don't exist in this project
742
+ const relevant = suggestions.filter((s) => {
743
+ if (!s.directory)
744
+ return true; // project-wide suggestions always apply
745
+ return projectDirs.has(s.directory) || [...projectDirs].some((d) => s.directory.startsWith(d));
746
+ });
747
+ if (relevant.length > 0) {
748
+ store_1.store.setSuggestions(relevant.map((s) => ({ rule: s.rule, disposition: s.disposition, directory: s.directory, suggestion: s.suggestion })));
749
+ }
750
+ })
751
+ .catch(() => { });
752
+ // Pro users: refresh suggestions every 10 minutes (interval cleaned up in shutdown)
753
+ if (store_1.store.state.userTier === 'pro') {
754
+ store_1.store._suggestionsRefresh = { primaryLang };
755
+ }
756
+ }
757
+ }
758
+ }
759
+ // ── Resolve provider for auto-resolve in watch mode ────────
760
+ let watchProvider;
761
+ try {
762
+ const env = (0, optimizer_1.loadEnvFile)(root);
763
+ const watchCreds = (0, auth_1.loadCredentials)();
764
+ if (watchCreds)
765
+ env['ASPECTCODE_CLI_TOKEN'] = watchCreds.token;
766
+ if (projectConfig?.apiKey && !env['ASPECTCODE_LLM_KEY'])
767
+ env['ASPECTCODE_LLM_KEY'] = projectConfig.apiKey;
768
+ watchProvider = (0, usageTracker_1.withUsageTracking)((0, optimizer_1.resolveProvider)(env));
264
769
  }
265
- // ── Watch mode ────────────────────────────────────────────
770
+ catch { /* no LLM — assessments go to user as before */ }
771
+ // ── Watch mode with real-time evaluation ───────────────────
266
772
  log.blank();
267
- log.info(logger_1.fmt.dim('Watching for changes… (Ctrl+C to stop)'));
268
773
  store_1.store.setPhase('watching');
269
- const chokidarModule = await Promise.resolve().then(() => __importStar(require('chokidar')));
270
- const chokidar = chokidarModule.default ?? chokidarModule;
271
- const watcher = chokidar.watch('.', {
272
- cwd: root,
273
- ignoreInitial: true,
274
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 },
275
- ignored: (watchedPath) => {
276
- const abs = path.resolve(root, watchedPath);
277
- return isIgnoredPath(abs);
278
- },
279
- });
280
- let timer;
281
- let running = false;
282
- let pending = false;
774
+ // Refresh memory map every 10s to catch file deletions/additions
775
+ const memoryMapInterval = setInterval(() => {
776
+ if (stopped)
777
+ return;
778
+ store_1.store.setManagedFiles(buildManagedFiles(root, prefs.preferences.length, activePlatforms));
779
+ }, 10000);
780
+ let evalTimer;
781
+ let pipelineRunning = false;
283
782
  let stopped = false;
284
- const triggerRun = async (reason) => {
783
+ let pendingEvalEvents = [];
784
+ // Pro: refresh suggestions once per day (checked every hour, skips if last fetch was <24h ago)
785
+ const suggestionsRefreshInfo = store_1.store._suggestionsRefresh;
786
+ let lastSuggestionsFetch = Date.now();
787
+ const SUGGESTIONS_REFRESH_MS = 24 * 60 * 60 * 1000;
788
+ const suggestionsInterval = suggestionsRefreshInfo ? setInterval(() => {
285
789
  if (stopped)
286
790
  return;
287
- if (running) {
288
- pending = true;
791
+ if (Date.now() - lastSuggestionsFetch < SUGGESTIONS_REFRESH_MS)
289
792
  return;
793
+ (0, preferences_1.fetchSuggestions)(suggestionsRefreshInfo.primaryLang, undefined, { byok: false })
794
+ .then((suggestions) => {
795
+ lastSuggestionsFetch = Date.now();
796
+ if (suggestions.length > 0 && !store_1.store.state.suggestionsDismissed) {
797
+ store_1.store.setSuggestions(suggestions.map((s) => ({ rule: s.rule, disposition: s.disposition, directory: s.directory, suggestion: s.suggestion })));
798
+ }
799
+ })
800
+ .catch(() => { });
801
+ }, 60 * 60 * 1000) : undefined;
802
+ // Co-change settle window — hold co-change assessments for 5s to see if dependents get updated
803
+ const pendingCoChange = new Map();
804
+ let settleTimer;
805
+ const recentlyChangedFiles = new Set();
806
+ function parseDependents(ctx) {
807
+ const match = ctx.match(/missing: \[([^\]]*)\]/);
808
+ if (!match)
809
+ return [];
810
+ return match[1].split(',').map((s) => s.trim()).filter(Boolean);
811
+ }
812
+ const resolveSettledCoChange = async (threshold, forwarded) => {
813
+ settleTimer = undefined;
814
+ const surviving = [];
815
+ for (const [, entry] of pendingCoChange) {
816
+ // Drop if all dependents were updated during settle window
817
+ const allUpdated = entry.dependents.length > 0 && entry.dependents.every((d) => recentlyChangedFiles.has(d));
818
+ if (!allUpdated)
819
+ surviving.push(entry.assessment);
290
820
  }
291
- running = true;
821
+ pendingCoChange.clear();
822
+ if (surviving.length > 0 && watchProvider) {
823
+ try {
824
+ const results = await (0, autoResolve_1.autoResolveBatch)(surviving, prefs, watchProvider, { threshold });
825
+ for (const result of results) {
826
+ if (result.autoResolved) {
827
+ const a = result.assessment;
828
+ const dir = path.dirname(a.file) + '/';
829
+ if (result.decision === 'allow') {
830
+ prefs = (0, preferences_1.addPreference)(prefs, { rule: a.rule, pattern: a.message, disposition: 'allow', directory: dir, details: a.details, dependencyContext: a.dependencyContext });
831
+ }
832
+ else {
833
+ prefs = (0, preferences_1.addPreference)(prefs, { rule: a.rule, pattern: a.message, disposition: 'deny', file: a.file, directory: dir, details: a.details, suggestion: a.suggestion, dependencyContext: a.dependencyContext });
834
+ }
835
+ (0, dreamCycle_1.addCorrection)(result.decision === 'allow' ? 'dismiss' : 'confirm', a);
836
+ const stats = { ...store_1.store.state.assessmentStats };
837
+ stats.autoResolved++;
838
+ store_1.store.state.assessmentStats = stats;
839
+ }
840
+ else {
841
+ const a = result.assessment;
842
+ a.llmRecommendation = { decision: result.decision, confidence: result.confidence, reasoning: result.reasoning };
843
+ forwarded.push(a);
844
+ }
845
+ }
846
+ (0, preferences_1.savePreferences)(root, prefs);
847
+ store_1.store.setPreferenceCount(prefs.preferences.length);
848
+ if (forwarded.length > 0)
849
+ store_1.store.pushAssessments(forwarded);
850
+ }
851
+ catch (err) {
852
+ if (err?.tierExhausted)
853
+ store_1.store.setTierExhausted();
854
+ store_1.store.pushAssessments(surviving);
855
+ }
856
+ }
857
+ else if (surviving.length > 0) {
858
+ store_1.store.pushAssessments(surviving);
859
+ }
860
+ };
861
+ // Auto-probe tracking
862
+ let lastChangeAt = Date.now();
863
+ let lastProbeAt = Date.now();
864
+ // Fire an immediate dream at session start to review/prune existing rules
865
+ let sessionDreamDone = false;
866
+ let lastDreamAt = Date.now();
867
+ const optLog = flags.quiet ? undefined : {
868
+ info(msg) { log.info(msg); },
869
+ warn(msg) { log.warn(msg); },
870
+ error(msg) { log.error(msg); },
871
+ debug(msg) { log.debug(msg); },
872
+ };
873
+ // ── Dream cycle (autonomous) ────────────────────────────────
874
+ const doDreamCycle = async () => {
875
+ if (pipelineRunning || stopped || store_1.store.state.tierExhausted)
876
+ return;
877
+ store_1.store.setDreamPrompt(false);
878
+ const state = (0, runtimeState_1.getRuntimeState)();
879
+ if (!state.agentsContent)
880
+ return;
881
+ let provider;
292
882
  try {
293
- log.blank();
294
- log.info(`${logger_1.fmt.bold('change detected:')} ${reason}`);
295
- store_1.store.setLastChange(reason);
296
- const runResult = await runOnce(ctx, ownership);
297
- if (runResult.kbContent)
298
- latestKb = runResult.kbContent;
883
+ const env = (0, optimizer_1.loadEnvFile)(root);
884
+ const creds = (0, auth_1.loadCredentials)();
885
+ if (creds && !env['ASPECTCODE_CLI_TOKEN'])
886
+ env['ASPECTCODE_CLI_TOKEN'] = creds.token;
887
+ provider = (0, usageTracker_1.withUsageTracking)((0, optimizer_1.resolveProvider)(env));
888
+ }
889
+ catch {
890
+ return;
891
+ }
892
+ store_1.store.setDreaming(true);
893
+ pipelineRunning = true;
894
+ try {
895
+ // Read current scoped rules for dream context
896
+ let scopedRulesContext = '';
897
+ try {
898
+ const manifestPath = path.join(root, '.aspectcode', 'scoped-rules.json');
899
+ if (fs.existsSync(manifestPath)) {
900
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
901
+ const parts = [];
902
+ for (const entry of manifest.rules ?? []) {
903
+ try {
904
+ const content = fs.readFileSync(path.join(root, entry.path), 'utf-8');
905
+ parts.push(`### ${entry.slug} (${entry.path})\n${content}`);
906
+ }
907
+ catch { /* skip missing files */ }
908
+ }
909
+ scopedRulesContext = parts.join('\n---\n');
910
+ }
911
+ }
912
+ catch { /* ignore */ }
913
+ // Read user-authored .claude/rules/ and .claude/skills/ (read-only context for dream)
914
+ let userRulesContext = '';
915
+ try {
916
+ const USER_CONTEXT_CAP = 3000;
917
+ const userParts = [];
918
+ const claudeRulesDir = path.join(root, '.claude', 'rules');
919
+ if (fs.existsSync(claudeRulesDir)) {
920
+ for (const file of fs.readdirSync(claudeRulesDir)) {
921
+ if (file.startsWith('ac-'))
922
+ continue;
923
+ try {
924
+ const content = fs.readFileSync(path.join(claudeRulesDir, file), 'utf-8');
925
+ userParts.push(`### ${file} (user rule, read-only)\n${content}`);
926
+ }
927
+ catch { /* skip */ }
928
+ }
929
+ }
930
+ const claudeSkillsDir = path.join(root, '.claude', 'skills');
931
+ if (fs.existsSync(claudeSkillsDir)) {
932
+ for (const file of fs.readdirSync(claudeSkillsDir)) {
933
+ try {
934
+ const content = fs.readFileSync(path.join(claudeSkillsDir, file), 'utf-8');
935
+ const truncated = content.length > 500
936
+ ? content.slice(0, 500) + `\n... (truncated, ${content.length} chars total)`
937
+ : content;
938
+ userParts.push(`### ${file} (user skill, read-only)\n${truncated}`);
939
+ }
940
+ catch { /* skip */ }
941
+ }
942
+ }
943
+ // Cap total user context
944
+ let combined = userParts.join('\n---\n');
945
+ if (combined.length > USER_CONTEXT_CAP) {
946
+ combined = combined.slice(0, USER_CONTEXT_CAP) + '\n... (truncated)';
947
+ }
948
+ userRulesContext = combined;
949
+ }
950
+ catch { /* ignore */ }
951
+ // Format community suggestions for dream cycle context
952
+ let communitySuggestions;
953
+ const suggestions = store_1.store.state.suggestions;
954
+ if (suggestions.length > 0 && !store_1.store.state.suggestionsDismissed) {
955
+ communitySuggestions = suggestions
956
+ .map((s) => `- [${s.rule}] ${s.suggestion}`)
957
+ .join('\n');
958
+ // Mark as consumed so they're only integrated once
959
+ store_1.store.dismissSuggestions();
960
+ }
961
+ const result = await (0, dreamCycle_1.runDreamCycle)({
962
+ currentAgentsMd: state.agentsContent,
963
+ corrections: (0, dreamCycle_1.getCorrections)(),
964
+ provider,
965
+ log: optLog,
966
+ scopedRulesContext,
967
+ userRulesContext: userRulesContext || undefined,
968
+ communitySuggestions,
969
+ });
970
+ const host = (0, emitters_1.createNodeEmitterHost)();
971
+ await (0, writer_1.writeAgentsMd)(host, root, result.updatedAgentsMd, ownership);
972
+ (0, runtimeState_1.updateRuntimeState)({ agentsContent: result.updatedAgentsMd });
973
+ // Write any scoped rules from the dream cycle
974
+ if ((result.scopedRules.length > 0 || result.deleteSlugs.length > 0) && activePlatforms.length > 0) {
975
+ // Delete rules the dream cycle marked for removal
976
+ if (result.deleteSlugs.length > 0) {
977
+ await (0, scopedRules_1.deleteScopedRules)(host, root, result.deleteSlugs);
978
+ }
979
+ if (result.scopedRules.length > 0) {
980
+ await (0, scopedRules_1.writeRulesForPlatforms)(host, root, result.scopedRules, activePlatforms);
981
+ }
982
+ }
983
+ (0, dreamCycle_1.markProcessed)();
984
+ store_1.store.setCorrectionCount((0, dreamCycle_1.getUnprocessedCount)());
985
+ (0, dreamCycle_1.saveDreamState)(root, { lastDreamAt: new Date().toISOString() });
986
+ if (result.changes.length > 0) {
987
+ store_1.store.setLearnedMessage(`Refined: ${result.changes.join(', ')}`);
988
+ }
989
+ // Refresh memory map to reflect any new files
990
+ store_1.store.setManagedFiles(buildManagedFiles(root, (await (0, preferences_1.loadPreferences)(root)).preferences.length, activePlatforms));
991
+ }
992
+ catch (err) {
993
+ if (err?.tierExhausted) {
994
+ store_1.store.setTierExhausted();
995
+ }
996
+ else {
997
+ const msg = err instanceof Error ? err.message : String(err);
998
+ log.warn(`Dream cycle failed: ${msg}`);
999
+ }
1000
+ }
1001
+ finally {
1002
+ pipelineRunning = false;
1003
+ store_1.store.setDreaming(false);
1004
+ }
1005
+ };
1006
+ // Auto-dream timer: fires every 30s, dreams if corrections exist and 2+ min since last dream
1007
+ const AUTO_DREAM_INTERVAL_MS = 2 * 60 * 1000;
1008
+ const autoDreamTimer = setInterval(() => {
1009
+ if (stopped || pipelineRunning)
1010
+ return;
1011
+ const hasCorrections = (0, dreamCycle_1.getUnprocessedCount)() > 0;
1012
+ const hasSuggestions = store_1.store.state.suggestions.length > 0 && !store_1.store.state.suggestionsDismissed;
1013
+ if (!hasCorrections && !hasSuggestions)
1014
+ return;
1015
+ if (Date.now() - lastDreamAt < AUTO_DREAM_INTERVAL_MS)
1016
+ return;
1017
+ void doDreamCycle().then(() => { lastDreamAt = Date.now(); });
1018
+ }, 30000);
1019
+ // ── Session-start dream: review rules immediately ───────────
1020
+ if (watchProvider) {
1021
+ // Small delay to let the dashboard render first
1022
+ setTimeout(() => {
1023
+ if (stopped || pipelineRunning || sessionDreamDone)
1024
+ return;
1025
+ sessionDreamDone = true;
1026
+ void doDreamCycle().then(() => { lastDreamAt = Date.now(); });
1027
+ }, 3000);
1028
+ }
1029
+ // ── Auto-probe timer: fires after sustained activity + idle period ──
1030
+ const autoProbeTimer = setInterval(() => {
1031
+ if (stopped || pipelineRunning)
1032
+ return;
1033
+ const stats = store_1.store.state.assessmentStats;
1034
+ if (stats.changes < AUTO_PROBE_MIN_CHANGES)
1035
+ return;
1036
+ if (Date.now() - lastChangeAt < AUTO_PROBE_IDLE_MS)
1037
+ return;
1038
+ if (Date.now() - lastProbeAt < AUTO_PROBE_COOLDOWN_MS)
1039
+ return;
1040
+ void doProbeAndRefine();
1041
+ }, 60000);
1042
+ // ── Probe and refine ──────────────────────────────────────
1043
+ const doProbeAndRefine = async () => {
1044
+ if (stopped || pipelineRunning)
1045
+ return;
1046
+ if (store_1.store.state.tierExhausted) {
1047
+ store_1.store.setLearnedMessage('Token limit reached — upgrade or add your own key');
1048
+ return;
1049
+ }
1050
+ // Run pending dream cycle before full probe-and-refine
1051
+ // Dream before re-running if corrections exist
1052
+ if ((0, dreamCycle_1.getUnprocessedCount)() > 0)
1053
+ await doDreamCycle();
1054
+ if (pipelineRunning)
1055
+ return;
1056
+ pipelineRunning = true;
1057
+ if (evalTimer) {
1058
+ clearTimeout(evalTimer);
1059
+ evalTimer = undefined;
1060
+ }
1061
+ pendingEvalEvents.length = 0;
1062
+ store_1.store.setRecommendProbe(false);
1063
+ try {
1064
+ await runOnce(ctx, ownership, true, prefs, activePlatforms, userSettings);
1065
+ prefs = await (0, preferences_1.loadPreferences)(root);
1066
+ store_1.store.setPreferenceCount(prefs.preferences.length);
299
1067
  store_1.store.setPhase('watching');
1068
+ lastProbeAt = Date.now();
300
1069
  }
301
- catch (error) {
302
- const msg = error instanceof Error ? error.message : String(error);
303
- log.error(`Pipeline failed: ${msg}`);
1070
+ catch (err) {
1071
+ const msg = err instanceof Error ? err.message : String(err);
1072
+ log.error(`Probe and refine failed: ${msg}`);
304
1073
  }
305
1074
  finally {
306
- running = false;
307
- if (pending && !stopped) {
308
- pending = false;
309
- void triggerRun('queued changes');
1075
+ pipelineRunning = false;
1076
+ }
1077
+ };
1078
+ // ── Evaluate file changes (fast, no LLM) ──────────────────
1079
+ const RECOMMEND_THRESHOLD = 10;
1080
+ const evaluateEvents = async (events) => {
1081
+ const state = (0, runtimeState_1.getRuntimeState)();
1082
+ if (!state.model || !state.agentsContent)
1083
+ return;
1084
+ for (const event of events) {
1085
+ (0, changeEvaluator_1.trackChange)(event);
1086
+ // Track add vs change counts
1087
+ if (event.type === 'add')
1088
+ store_1.store.incrementAddCount();
1089
+ else if (event.type === 'change')
1090
+ store_1.store.incrementChangeCount();
1091
+ if (event.type !== 'unlink') {
1092
+ try {
1093
+ const absPath = path.join(root, event.path);
1094
+ if (fs.existsSync(absPath)) {
1095
+ const content = fs.readFileSync(absPath, 'utf-8');
1096
+ state.fileContents?.set(event.path, content);
1097
+ }
1098
+ }
1099
+ catch { /* skip unreadable files */ }
1100
+ }
1101
+ const assessments = (0, changeEvaluator_1.evaluateChange)(event, {
1102
+ model: state.model,
1103
+ agentsContent: state.agentsContent,
1104
+ preferences: prefs,
1105
+ recentChanges: (0, changeEvaluator_1.getRecentChanges)(),
1106
+ fileContents: state.fileContents,
1107
+ previousHubCounts: state.previousHubCounts,
1108
+ });
1109
+ // Auto-resolve assessments with LLM when available
1110
+ const actionable = assessments.filter((a) => a.type !== 'ok');
1111
+ const threshold = flags.background ? 0.0 : (userSettings?.autoResolveThreshold ?? 0.8);
1112
+ if (watchProvider && actionable.length > 0) {
1113
+ // Split into cached (preference match) and uncached (need LLM)
1114
+ const cached = [];
1115
+ const uncached = [];
1116
+ for (const a of actionable) {
1117
+ const match = (0, autoResolve_1.matchExistingPreference)(a, prefs);
1118
+ if (match) {
1119
+ cached.push({ result: match });
1120
+ }
1121
+ else {
1122
+ uncached.push(a);
1123
+ }
1124
+ }
1125
+ // Apply cached decisions immediately
1126
+ for (const { result } of cached) {
1127
+ const a = result.assessment;
1128
+ (0, dreamCycle_1.addCorrection)(result.decision === 'allow' ? 'dismiss' : 'confirm', a);
1129
+ const stats = { ...store_1.store.state.assessmentStats };
1130
+ stats.autoResolved++;
1131
+ store_1.store.state.assessmentStats = stats;
1132
+ }
1133
+ // Batch-resolve uncached in a single LLM call
1134
+ const forwarded = [];
1135
+ if (uncached.length > 0) {
1136
+ // Check for co-change assessments that should wait for settle
1137
+ const coChange = [];
1138
+ const immediate = [];
1139
+ for (const a of uncached) {
1140
+ if (a.rule === 'co-change' && a.dependencyContext) {
1141
+ coChange.push(a);
1142
+ }
1143
+ else {
1144
+ immediate.push(a);
1145
+ }
1146
+ }
1147
+ // Hold co-change assessments for settle window
1148
+ if (coChange.length > 0) {
1149
+ for (const a of coChange) {
1150
+ pendingCoChange.set(a.file, { assessment: a, dependents: parseDependents(a.dependencyContext ?? ''), addedAt: Date.now() });
1151
+ }
1152
+ if (!settleTimer) {
1153
+ settleTimer = setTimeout(() => resolveSettledCoChange(threshold, forwarded), CO_CHANGE_SETTLE_MS);
1154
+ }
1155
+ }
1156
+ // Batch-resolve immediate assessments
1157
+ if (immediate.length > 0) {
1158
+ try {
1159
+ const results = await (0, autoResolve_1.autoResolveBatch)(immediate, prefs, watchProvider, { threshold });
1160
+ for (const result of results) {
1161
+ if (result.autoResolved) {
1162
+ const a = result.assessment;
1163
+ const dir = path.dirname(a.file) + '/';
1164
+ if (result.decision === 'allow') {
1165
+ prefs = (0, preferences_1.addPreference)(prefs, { rule: a.rule, pattern: a.message, disposition: 'allow', directory: dir, details: a.details, dependencyContext: a.dependencyContext });
1166
+ }
1167
+ else {
1168
+ prefs = (0, preferences_1.addPreference)(prefs, { rule: a.rule, pattern: a.message, disposition: 'deny', file: a.file, directory: dir, details: a.details, suggestion: a.suggestion, dependencyContext: a.dependencyContext });
1169
+ }
1170
+ (0, dreamCycle_1.addCorrection)(result.decision === 'allow' ? 'dismiss' : 'confirm', a);
1171
+ const stats = { ...store_1.store.state.assessmentStats };
1172
+ stats.autoResolved++;
1173
+ store_1.store.state.assessmentStats = stats;
1174
+ }
1175
+ else {
1176
+ const a = result.assessment;
1177
+ a.llmRecommendation = { decision: result.decision, confidence: result.confidence, reasoning: result.reasoning };
1178
+ forwarded.push(a);
1179
+ }
1180
+ }
1181
+ (0, preferences_1.savePreferences)(root, prefs);
1182
+ store_1.store.setPreferenceCount(prefs.preferences.length);
1183
+ if (cached.length > 0 || results.some((r) => r.autoResolved)) {
1184
+ const autoCount = cached.length + results.filter((r) => r.autoResolved).length;
1185
+ store_1.store.setLearnedMessage(`Auto-resolved ${autoCount} assessment${autoCount === 1 ? '' : 's'}`);
1186
+ }
1187
+ }
1188
+ catch (err) {
1189
+ if (err?.tierExhausted) {
1190
+ store_1.store.setTierExhausted();
1191
+ }
1192
+ forwarded.push(...immediate);
1193
+ }
1194
+ }
1195
+ else if (cached.length > 0) {
1196
+ store_1.store.setLearnedMessage(`Auto-resolved ${cached.length} assessment${cached.length === 1 ? '' : 's'}`);
1197
+ }
1198
+ }
1199
+ else if (cached.length > 0) {
1200
+ store_1.store.setLearnedMessage(`Auto-resolved ${cached.length} assessment${cached.length === 1 ? '' : 's'}`);
1201
+ }
1202
+ store_1.store.pushAssessments(forwarded);
1203
+ }
1204
+ else {
1205
+ store_1.store.pushAssessments(assessments);
310
1206
  }
1207
+ // Change flash for clean changes
1208
+ const hasNonOk = assessments.some((a) => a.type !== 'ok');
1209
+ if (!hasNonOk) {
1210
+ store_1.store.setLastChangeFlash(`${event.path} — ok`);
1211
+ }
1212
+ }
1213
+ // Auto-recommend probe-and-refine
1214
+ const stats = store_1.store.state.assessmentStats;
1215
+ if (stats.changes >= RECOMMEND_THRESHOLD && !store_1.store.state.recommendProbe) {
1216
+ store_1.store.setRecommendProbe(true);
311
1217
  }
312
1218
  };
1219
+ // ── File change handler ────────────────────────────────────
313
1220
  const onFsEvent = (eventType, eventPath) => {
1221
+ if (pipelineRunning)
1222
+ return;
314
1223
  const abs = path.resolve(root, eventPath);
315
1224
  if (!isSupportedSourceFile(abs) || isIgnoredPath(abs))
316
1225
  return;
317
- if (timer)
318
- clearTimeout(timer);
319
- timer = setTimeout(() => {
320
- timer = undefined;
321
- void triggerRun(`${eventType}: ${eventPath.replace(/\\/g, '/')}`);
322
- }, DEBOUNCE_MS);
1226
+ const posixPath = eventPath.replace(/\\/g, '/');
1227
+ pendingEvalEvents.push({ type: eventType, path: posixPath });
1228
+ lastChangeAt = Date.now();
1229
+ // Track recently changed files for co-change settle window
1230
+ recentlyChangedFiles.add(posixPath);
1231
+ setTimeout(() => recentlyChangedFiles.delete(posixPath), CO_CHANGE_SETTLE_MS + 1000);
1232
+ if (evalTimer)
1233
+ clearTimeout(evalTimer);
1234
+ evalTimer = setTimeout(() => {
1235
+ evalTimer = undefined;
1236
+ const events = pendingEvalEvents.splice(0);
1237
+ evaluateEvents(events);
1238
+ }, EVAL_DEBOUNCE_MS);
323
1239
  };
324
- watcher.on('add', (p) => onFsEvent('add', p));
325
- watcher.on('change', (p) => onFsEvent('change', p));
326
- watcher.on('unlink', (p) => onFsEvent('unlink', p));
1240
+ // ── Start watcher (must be after onFsEvent is defined) ─────
1241
+ const watcher = createFileWatcher(root, onFsEvent);
327
1242
  watcher.on('error', (e) => log.error(`Watcher error: ${String(e)}`));
328
- await new Promise((resolve) => watcher.once('ready', resolve));
329
- log.info(logger_1.fmt.dim('Watcher ready.'));
330
- // ── Complaint polling — check for queued complaints periodically ──
331
- const COMPLAINT_POLL_MS = 500;
332
- const complaintPoll = setInterval(async () => {
333
- if (stopped || running || store_1.store.state.complaintQueue.length === 0)
1243
+ // ── Expose action handler for keyboard input ───────────────
1244
+ const onAssessmentAction = (action) => {
1245
+ if (action.type === 'probe-and-refine') {
1246
+ void doProbeAndRefine();
334
1247
  return;
335
- running = true;
336
- try {
337
- await (0, complaintProcessor_1.processComplaints)(ctx, ownership, latestKb);
338
1248
  }
339
- catch (err) {
340
- const msg = err instanceof Error ? err.message : String(err);
341
- log.error(`Complaint processing failed: ${msg}`);
1249
+ // Dream cycle is autonomous — no manual trigger
1250
+ if (action.type === 'open-pricing') {
1251
+ const { exec } = require('child_process');
1252
+ const url = 'https://aspectcode.com/pricing';
1253
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start ""' : 'xdg-open';
1254
+ exec(`${cmd} "${url}"`);
1255
+ store_1.store.setLearnedMessage('opened pricing page');
1256
+ return;
342
1257
  }
343
- finally {
344
- running = false;
1258
+ if (action.type === 'login') {
1259
+ store_1.store.setLearnedMessage('opening browser…');
1260
+ void (0, auth_1.startBackgroundLogin)().then((email) => {
1261
+ if (email) {
1262
+ store_1.store.setUserEmail(email);
1263
+ store_1.store.setLearnedMessage(`logged in as ${email}`);
1264
+ }
1265
+ else {
1266
+ store_1.store.setLearnedMessage('login failed or timed out');
1267
+ }
1268
+ });
1269
+ return;
345
1270
  }
346
- }, COMPLAINT_POLL_MS);
1271
+ void handleAssessmentAction(action, prefs, root, ownership).then((updated) => {
1272
+ prefs = updated;
1273
+ });
1274
+ };
1275
+ store_1.store._onAssessmentAction = onAssessmentAction;
347
1276
  return await new Promise((resolve) => {
348
1277
  const shutdown = async (signal) => {
349
1278
  if (stopped)
350
1279
  return;
351
1280
  stopped = true;
352
- clearInterval(complaintPoll);
353
- if (timer) {
354
- clearTimeout(timer);
355
- timer = undefined;
1281
+ clearInterval(memoryMapInterval);
1282
+ clearInterval(autoDreamTimer);
1283
+ clearInterval(autoProbeTimer);
1284
+ if (suggestionsInterval)
1285
+ clearInterval(suggestionsInterval);
1286
+ if (evalTimer) {
1287
+ clearTimeout(evalTimer);
1288
+ evalTimer = undefined;
356
1289
  }
357
1290
  log.blank();
358
1291
  log.info(logger_1.fmt.dim(`Stopping (${signal})…`));