aspectcode 0.4.0 → 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 (242) hide show
  1. package/README.md +13 -0
  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 +385 -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 +12 -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 +149 -8
  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 +21 -18
  43. package/dist/pipeline.d.ts.map +1 -1
  44. package/dist/pipeline.js +1139 -162
  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 -141
  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 -20
  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 -89
  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 -29
  164. package/node_modules/@aspectcode/evaluator/dist/probes.d.ts.map +1 -1
  165. package/node_modules/@aspectcode/evaluator/dist/probes.js +188 -204
  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/package.json +6 -7
  203. package/dist/complaintProcessor.d.ts +0 -16
  204. package/dist/complaintProcessor.d.ts.map +0 -1
  205. package/dist/complaintProcessor.js +0 -134
  206. package/dist/complaintProcessor.js.map +0 -1
  207. package/node_modules/@aspectcode/emitters/dist/emitter.d.ts +0 -72
  208. package/node_modules/@aspectcode/emitters/dist/emitter.d.ts.map +0 -1
  209. package/node_modules/@aspectcode/emitters/dist/emitter.js +0 -10
  210. package/node_modules/@aspectcode/emitters/dist/emitter.js.map +0 -1
  211. package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts +0 -15
  212. package/node_modules/@aspectcode/emitters/dist/instructions/content.d.ts.map +0 -1
  213. package/node_modules/@aspectcode/emitters/dist/instructions/content.js +0 -289
  214. package/node_modules/@aspectcode/emitters/dist/instructions/content.js.map +0 -1
  215. package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts +0 -13
  216. package/node_modules/@aspectcode/emitters/dist/instructions/detection.d.ts.map +0 -1
  217. package/node_modules/@aspectcode/emitters/dist/instructions/detection.js +0 -55
  218. package/node_modules/@aspectcode/emitters/dist/instructions/detection.js.map +0 -1
  219. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts +0 -9
  220. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.d.ts.map +0 -1
  221. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js +0 -30
  222. package/node_modules/@aspectcode/emitters/dist/instructions/instructionsEmitter.js.map +0 -1
  223. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts +0 -21
  224. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.d.ts.map +0 -1
  225. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js +0 -125
  226. package/node_modules/@aspectcode/emitters/dist/kb/kbEmitter.js.map +0 -1
  227. package/node_modules/@aspectcode/emitters/dist/manifest.d.ts +0 -37
  228. package/node_modules/@aspectcode/emitters/dist/manifest.d.ts.map +0 -1
  229. package/node_modules/@aspectcode/emitters/dist/manifest.js +0 -50
  230. package/node_modules/@aspectcode/emitters/dist/manifest.js.map +0 -1
  231. package/node_modules/@aspectcode/emitters/dist/report.d.ts +0 -22
  232. package/node_modules/@aspectcode/emitters/dist/report.d.ts.map +0 -1
  233. package/node_modules/@aspectcode/emitters/dist/report.js +0 -3
  234. package/node_modules/@aspectcode/emitters/dist/report.js.map +0 -1
  235. package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts +0 -14
  236. package/node_modules/@aspectcode/emitters/dist/stableJson.d.ts.map +0 -1
  237. package/node_modules/@aspectcode/emitters/dist/stableJson.js +0 -40
  238. package/node_modules/@aspectcode/emitters/dist/stableJson.js.map +0 -1
  239. package/node_modules/@aspectcode/emitters/dist/transaction.d.ts +0 -29
  240. package/node_modules/@aspectcode/emitters/dist/transaction.d.ts.map +0 -1
  241. package/node_modules/@aspectcode/emitters/dist/transaction.js +0 -104
  242. 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. Write static-template AGENTS.md for immediate feedback
9
- * 5. If API key present → LLM generates AGENTS.md from KB → overwrite
10
- * If no API key → keep static AGENTS.md + warn
11
- * 6. If --kb flag → also write kb.md
12
- * 7. If not --once → watch for changes and repeat
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 });
48
- exports.resolveOwnership = resolveOwnership;
43
+ exports.isIgnoredPath = isIgnoredPath;
44
+ exports.isSupportedSourceFile = isSupportedSourceFile;
45
+ exports.createFileWatcher = createFileWatcher;
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,25 +92,236 @@ 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
- const configPath = path.join(root, 'aspectcode.json');
93
- if (!fs.existsSync(agentsPath) && !fs.existsSync(configPath)) {
317
+ if (!fs.existsSync(agentsPath)) {
94
318
  store_1.store.setFirstRun(true);
95
319
  }
96
320
  // ── 1. Discover & read files ──────────────────────────────
97
321
  store_1.store.setPhase('discovering');
98
322
  const workspace = await (0, workspace_1.loadWorkspaceFiles)(root, config, log, { quiet: flags.quiet, spin: ctx.spin });
99
323
  if (workspace.discoveredPaths.length === 0) {
100
- log.warn('No source files found. Check your workspace or exclude patterns.');
324
+ log.warn('No source files found.');
101
325
  return { code: cli_1.ExitCode.ERROR, kbContent: '' };
102
326
  }
103
327
  // ── 2. Analyze ────────────────────────────────────────────
@@ -105,210 +329,963 @@ async function runOnce(ctx, ownership) {
105
329
  const model = await (0, core_1.analyzeRepoWithDependencies)(root, workspace.relativeFiles, workspace.absoluteFiles, workspace.host);
106
330
  spinAnalyze.stop(`Analyzed ${model.files.length} files, ${model.graph.edges.length} edges`);
107
331
  store_1.store.setStats(model.files.length, model.graph.edges.length);
108
- // ── 3. Build KB content in memory ─────────────────────────
332
+ // ── 3. Build KB ───────────────────────────────────────────
109
333
  const spinKb = ctx.spin('Building knowledge base…', 'building-kb');
110
334
  const kbContent = (0, kbBuilder_1.buildKbContent)(model, root, workspace.relativeFiles);
111
335
  spinKb.stop('Knowledge base built');
112
- // ── 4. Read other AI tool instruction files ───────────────
336
+ // ── 4. Read tool instruction files ────────────────────────
113
337
  const host = (0, emitters_1.createNodeEmitterHost)();
114
338
  const toolInstructions = await (0, toolIngestion_1.readToolInstructions)(host, root);
115
339
  if (toolInstructions.size > 0) {
116
- const toolNames = [...toolInstructions.keys()].join(', ');
117
- store_1.store.addSetupNote(`context: ${toolNames}`);
118
- log.debug(`Read ${toolInstructions.size} AI tool instruction file(s) as context`);
119
- }
120
- // ── 5. Write static-template AGENTS.md for immediate feedback ─
121
- // Written to disk right away so the user sees output early,
122
- // even before the LLM generation finishes.
123
- const baseContent = (0, emitters_1.generateCanonicalContentForMode)('safe', kbContent.length > 0);
124
- if (!flags.dryRun) {
125
- await (0, writer_1.writeAgentsMd)(host, root, baseContent, ownership);
126
- store_1.store.addOutput('AGENTS.md written (base)');
127
- log.debug('Base AGENTS.md written from static analysis');
128
- }
129
- // ── 6. LLM generation or static fallback ───────────────
130
- store_1.store.setPhase('optimizing');
131
- const optimizeResult = await (0, optimize_1.tryOptimize)(ctx, kbContent, toolInstructions, config, baseContent);
132
- // ── 7. Write LLM-generated AGENTS.md ───────────────────
133
- store_1.store.setPhase('writing');
134
- if (flags.dryRun) {
135
- log.info(logger_1.fmt.bold('Dry run — proposed AGENTS.md:'));
136
- log.blank();
137
- log.info(optimizeResult.content);
138
- log.blank();
340
+ store_1.store.addSetupNote(`context: ${[...toolInstructions.keys()].join(', ')}`);
139
341
  }
140
- else {
141
- // Compute diff before overwriting (for watch-mode change summary)
142
- let previousContent;
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;
346
+ if (ctx.generate) {
347
+ if (!flags.dryRun) {
348
+ await (0, writer_1.writeAgentsMd)(host, root, baseContent, ownership);
349
+ store_1.store.addOutput('AGENTS.md written (base)');
350
+ }
351
+ store_1.store.setPhase('optimizing');
352
+ // Skip probe-and-refine if tier is already exhausted
353
+ const skipProbe = probeAndRefine && store_1.store.state.tierExhausted;
354
+ let optimizeResult;
143
355
  try {
144
- if (fs.existsSync(agentsPath)) {
145
- previousContent = fs.readFileSync(agentsPath, 'utf-8');
146
- }
147
- }
148
- catch { /* ignore read errors */ }
149
- await (0, writer_1.writeAgentsMd)(host, root, optimizeResult.content, ownership);
150
- const modeLabel = ownership === 'section' ? ' (section)' : '';
151
- const verb = optimizeResult.reasoning.length > 0 ? 'generated' : 'written';
152
- store_1.store.addOutput(`AGENTS.md ${verb}${modeLabel}`);
153
- log.success(`AGENTS.md ${verb}${modeLabel}`);
154
- // Diff summary (skip on first write when previous was just the base template)
155
- if (previousContent !== undefined && previousContent !== baseContent) {
156
- const diff = (0, diffSummary_1.diffSummary)(previousContent, optimizeResult.content);
157
- store_1.store.setDiffSummary(diff);
158
- }
159
- // Content summary for the dashboard
160
- const summary = (0, summary_1.summarizeContent)(optimizeResult.content);
161
- store_1.store.setSummary(summary);
162
- }
163
- // ── 8. Optionally write kb.md ─────────────────────────────
164
- if (flags.kb && !flags.dryRun) {
165
- await (0, writer_1.writeKbMd)(host, root, kbContent);
166
- store_1.store.addOutput('kb.md written');
167
- log.success('kb.md written');
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;
368
+ store_1.store.setPhase('writing');
369
+ if (flags.dryRun) {
370
+ log.info(logger_1.fmt.bold('Dry run — proposed AGENTS.md:'));
371
+ log.blank();
372
+ log.info(finalContent);
373
+ }
374
+ else {
375
+ let previousContent;
376
+ try {
377
+ if (fs.existsSync(agentsPath)) {
378
+ previousContent = fs.readFileSync(agentsPath, 'utf-8');
379
+ }
380
+ }
381
+ catch { /* ignore */ }
382
+ await (0, writer_1.writeAgentsMd)(host, root, finalContent, ownership);
383
+ const modeLabel = ownership === 'section' ? ' (section)' : '';
384
+ const verb = optimizeResult.reasoning.length > 0 ? 'generated' : 'written';
385
+ store_1.store.addOutput(`AGENTS.md ${verb}${modeLabel}`);
386
+ if (previousContent !== undefined && previousContent !== baseContent) {
387
+ store_1.store.setDiffSummary((0, diffSummary_1.diffSummary)(previousContent, finalContent));
388
+ }
389
+ store_1.store.setSummary((0, summary_1.summarizeContent)(finalContent));
390
+ }
168
391
  }
392
+ else {
393
+ store_1.store.setPhase('writing');
394
+ if (flags.dryRun) {
395
+ log.info(logger_1.fmt.bold('Dry run — proposed AGENTS.md (KB-custom):'));
396
+ log.blank();
397
+ log.info(baseContent);
398
+ }
399
+ else {
400
+ await (0, writer_1.writeAgentsMd)(host, root, baseContent, ownership);
401
+ store_1.store.addOutput('AGENTS.md written (KB-custom)');
402
+ store_1.store.setSummary((0, summary_1.summarizeContent)(baseContent));
403
+ }
404
+ // No LLM available — use static extraction for scoped rules
405
+ // No static rules in non-LLM path — dream cycle handles rules
406
+ }
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);
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));
169
425
  const elapsedMs = Date.now() - startMs;
170
426
  store_1.store.setElapsed(`${(elapsedMs / 1000).toFixed(1)}s`);
171
427
  store_1.store.setPhase('done');
172
- log.info(logger_1.fmt.dim(`Done in ${(elapsedMs / 1000).toFixed(1)}s`));
173
428
  return { code: cli_1.ExitCode.OK, kbContent };
174
429
  }
175
- // ── Pipeline entry point ─────────────────────────────────────
176
- /**
177
- * Resolve AGENTS.md ownership mode.
178
- *
179
- * Called from main() BEFORE the ink dashboard is mounted, because
180
- * the interactive prompt uses raw stdin which conflicts with ink's useInput.
181
- */
182
- async function resolveOwnership(root) {
430
+ // ── Resolve ownership mode ───────────────────────────────────
431
+ async function resolveRunMode(root) {
183
432
  const config = (0, config_1.loadConfig)(root);
184
- if (config?.ownership)
185
- return config.ownership;
433
+ if (config?.ownership) {
434
+ return { ownership: config.ownership, generate: true };
435
+ }
436
+ const agentsPath = path.join(root, 'AGENTS.md');
437
+ let existingContent = null;
186
438
  try {
187
- const fs = await Promise.resolve().then(() => __importStar(require('fs')));
188
- const agentsPath = path.join(root, 'AGENTS.md');
189
439
  if (fs.existsSync(agentsPath)) {
190
- const existing = fs.readFileSync(agentsPath, 'utf-8');
191
- if ((0, writer_1.hasMarkers)(existing))
192
- return 'section';
193
- const idx = await (0, prompts_1.selectPrompt)('AGENTS.md already exists. How should AspectCode manage it?', ['Replace entire file (full ownership)', 'Own a section (preserve your content)'], 0);
440
+ existingContent = fs.readFileSync(agentsPath, 'utf-8');
441
+ if ((0, writer_1.hasMarkers)(existingContent))
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;
458
+ }
194
459
  const ownership = idx === 1 ? 'section' : 'full';
460
+ // Save choice so we don't ask again
195
461
  (0, config_1.saveConfig)(root, { ownership });
196
- return ownership;
462
+ return { ownership, generate: true };
463
+ }
464
+ }
465
+ catch {
466
+ return { ownership: 'full', generate: true };
467
+ }
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
+ }
197
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);
573
+ try {
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;
198
584
  }
199
585
  catch {
200
- // Non-interactive or read error default to full
586
+ const platforms = detected.length > 0 ? detected : ['claude'];
587
+ (0, config_1.saveConfig)(root, { platforms });
588
+ return platforms;
201
589
  }
202
- return 'full';
203
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();
634
+ }
635
+ store_1.store.setCorrectionCount((0, dreamCycle_1.getUnprocessedCount)());
636
+ return prefs;
637
+ }
638
+ // ── Main pipeline ────────────────────────────────────────────
204
639
  async function runPipeline(ctx) {
205
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`);
206
644
  log.info(`${logger_1.fmt.bold('aspectcode')} — ${logger_1.fmt.cyan(root)}`);
207
645
  log.blank();
208
- // ── Initial run ──────────────────────────────────────────
209
- const result = await runOnce(ctx, ownership);
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);
708
+ ctx.generate = true;
210
709
  if (result.code !== cli_1.ExitCode.OK)
211
710
  return result.code;
212
- // Keep track of latest KB for complaint processing
213
- let latestKb = result.kbContent;
214
- // ── --once: process any queued complaints, then exit ──────
215
- if (flags.once) {
216
- if (store_1.store.state.complaintQueue.length > 0) {
217
- await (0, complaintProcessor_1.processComplaints)(ctx, ownership, latestKb);
218
- }
711
+ if (flags.once)
219
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
+ }
220
758
  }
221
- // ── Watch mode ────────────────────────────────────────────
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));
769
+ }
770
+ catch { /* no LLM — assessments go to user as before */ }
771
+ // ── Watch mode with real-time evaluation ───────────────────
222
772
  log.blank();
223
- log.info(logger_1.fmt.dim('Watching for changes… (Ctrl+C to stop)'));
224
773
  store_1.store.setPhase('watching');
225
- const chokidarModule = await Promise.resolve().then(() => __importStar(require('chokidar')));
226
- const chokidar = chokidarModule.default ?? chokidarModule;
227
- const watcher = chokidar.watch('.', {
228
- cwd: root,
229
- ignoreInitial: true,
230
- awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 100 },
231
- ignored: (watchedPath) => {
232
- const abs = path.resolve(root, watchedPath);
233
- return isIgnoredPath(abs);
234
- },
235
- });
236
- let timer;
237
- let running = false;
238
- 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;
239
782
  let stopped = false;
240
- 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(() => {
241
789
  if (stopped)
242
790
  return;
243
- if (running) {
244
- pending = true;
791
+ if (Date.now() - lastSuggestionsFetch < SUGGESTIONS_REFRESH_MS)
245
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);
246
820
  }
247
- 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;
248
882
  try {
249
- log.blank();
250
- log.info(`${logger_1.fmt.bold('change detected:')} ${reason}`);
251
- store_1.store.setLastChange(reason);
252
- const runResult = await runOnce(ctx, ownership);
253
- if (runResult.kbContent)
254
- 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);
255
1067
  store_1.store.setPhase('watching');
1068
+ lastProbeAt = Date.now();
256
1069
  }
257
- catch (error) {
258
- const msg = error instanceof Error ? error.message : String(error);
259
- 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}`);
260
1073
  }
261
1074
  finally {
262
- running = false;
263
- if (pending && !stopped) {
264
- pending = false;
265
- 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);
266
1203
  }
1204
+ else {
1205
+ store_1.store.pushAssessments(assessments);
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);
267
1217
  }
268
1218
  };
1219
+ // ── File change handler ────────────────────────────────────
269
1220
  const onFsEvent = (eventType, eventPath) => {
1221
+ if (pipelineRunning)
1222
+ return;
270
1223
  const abs = path.resolve(root, eventPath);
271
1224
  if (!isSupportedSourceFile(abs) || isIgnoredPath(abs))
272
1225
  return;
273
- if (timer)
274
- clearTimeout(timer);
275
- timer = setTimeout(() => {
276
- timer = undefined;
277
- void triggerRun(`${eventType}: ${eventPath.replace(/\\/g, '/')}`);
278
- }, 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);
279
1239
  };
280
- watcher.on('add', (p) => onFsEvent('add', p));
281
- watcher.on('change', (p) => onFsEvent('change', p));
282
- watcher.on('unlink', (p) => onFsEvent('unlink', p));
1240
+ // ── Start watcher (must be after onFsEvent is defined) ─────
1241
+ const watcher = createFileWatcher(root, onFsEvent);
283
1242
  watcher.on('error', (e) => log.error(`Watcher error: ${String(e)}`));
284
- await new Promise((resolve) => watcher.once('ready', resolve));
285
- log.info(logger_1.fmt.dim('Watcher ready.'));
286
- // ── Complaint polling — check for queued complaints periodically ──
287
- const COMPLAINT_POLL_MS = 500;
288
- const complaintPoll = setInterval(async () => {
289
- 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();
290
1247
  return;
291
- running = true;
292
- try {
293
- await (0, complaintProcessor_1.processComplaints)(ctx, ownership, latestKb);
294
1248
  }
295
- catch (err) {
296
- const msg = err instanceof Error ? err.message : String(err);
297
- 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;
298
1257
  }
299
- finally {
300
- 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;
301
1270
  }
302
- }, COMPLAINT_POLL_MS);
1271
+ void handleAssessmentAction(action, prefs, root, ownership).then((updated) => {
1272
+ prefs = updated;
1273
+ });
1274
+ };
1275
+ store_1.store._onAssessmentAction = onAssessmentAction;
303
1276
  return await new Promise((resolve) => {
304
1277
  const shutdown = async (signal) => {
305
1278
  if (stopped)
306
1279
  return;
307
1280
  stopped = true;
308
- clearInterval(complaintPoll);
309
- if (timer) {
310
- clearTimeout(timer);
311
- 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;
312
1289
  }
313
1290
  log.blank();
314
1291
  log.info(logger_1.fmt.dim(`Stopping (${signal})…`));