@cleocode/core 2026.4.98 → 2026.4.100

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 (85) hide show
  1. package/dist/gc/daemon-entry.d.ts +15 -0
  2. package/dist/gc/daemon-entry.d.ts.map +1 -0
  3. package/dist/gc/daemon.d.ts +71 -0
  4. package/dist/gc/daemon.d.ts.map +1 -0
  5. package/dist/gc/daemon.js +481 -0
  6. package/dist/gc/daemon.js.map +7 -0
  7. package/dist/gc/index.d.ts +14 -0
  8. package/dist/gc/index.d.ts.map +1 -0
  9. package/dist/gc/index.js +669 -0
  10. package/dist/gc/index.js.map +7 -0
  11. package/dist/gc/runner.d.ts +132 -0
  12. package/dist/gc/runner.d.ts.map +1 -0
  13. package/dist/gc/runner.js +360 -0
  14. package/dist/gc/runner.js.map +7 -0
  15. package/dist/gc/state.d.ts +94 -0
  16. package/dist/gc/state.d.ts.map +1 -0
  17. package/dist/gc/state.js +49 -0
  18. package/dist/gc/state.js.map +7 -0
  19. package/dist/gc/transcript.d.ts +130 -0
  20. package/dist/gc/transcript.d.ts.map +1 -0
  21. package/dist/gc/transcript.js +209 -0
  22. package/dist/gc/transcript.js.map +7 -0
  23. package/dist/memory/brain-backfill.js +14643 -0
  24. package/dist/memory/brain-backfill.js.map +7 -0
  25. package/dist/memory/precompact-flush.js +47725 -0
  26. package/dist/memory/precompact-flush.js.map +7 -0
  27. package/dist/sentient/daemon-entry.d.ts +11 -0
  28. package/dist/sentient/daemon-entry.d.ts.map +1 -0
  29. package/dist/sentient/daemon.d.ts +160 -0
  30. package/dist/sentient/daemon.d.ts.map +1 -0
  31. package/dist/sentient/daemon.js +1100 -0
  32. package/dist/sentient/daemon.js.map +7 -0
  33. package/dist/sentient/index.d.ts +18 -0
  34. package/dist/sentient/index.d.ts.map +1 -0
  35. package/dist/sentient/index.js +1162 -0
  36. package/dist/sentient/index.js.map +7 -0
  37. package/dist/sentient/ingesters/brain-ingester.d.ts +44 -0
  38. package/dist/sentient/ingesters/brain-ingester.d.ts.map +1 -0
  39. package/dist/sentient/ingesters/nexus-ingester.d.ts +45 -0
  40. package/dist/sentient/ingesters/nexus-ingester.d.ts.map +1 -0
  41. package/dist/sentient/ingesters/test-ingester.d.ts +43 -0
  42. package/dist/sentient/ingesters/test-ingester.d.ts.map +1 -0
  43. package/dist/sentient/proposal-rate-limiter.d.ts +93 -0
  44. package/dist/sentient/proposal-rate-limiter.d.ts.map +1 -0
  45. package/dist/sentient/propose-tick.d.ts +105 -0
  46. package/dist/sentient/propose-tick.d.ts.map +1 -0
  47. package/dist/sentient/propose-tick.js +549 -0
  48. package/dist/sentient/propose-tick.js.map +7 -0
  49. package/dist/sentient/state.d.ts +143 -0
  50. package/dist/sentient/state.d.ts.map +1 -0
  51. package/dist/sentient/state.js +85 -0
  52. package/dist/sentient/state.js.map +7 -0
  53. package/dist/sentient/tick.d.ts +193 -0
  54. package/dist/sentient/tick.d.ts.map +1 -0
  55. package/dist/sentient/tick.js +396 -0
  56. package/dist/sentient/tick.js.map +7 -0
  57. package/dist/system/platform-paths.js +36 -0
  58. package/dist/system/platform-paths.js.map +7 -0
  59. package/package.json +76 -8
  60. package/src/gc/__tests__/runner.test.ts +367 -0
  61. package/src/gc/__tests__/state.test.ts +169 -0
  62. package/src/gc/__tests__/transcript.test.ts +371 -0
  63. package/src/gc/daemon-entry.ts +26 -0
  64. package/src/gc/daemon.ts +251 -0
  65. package/src/gc/index.ts +14 -0
  66. package/src/gc/runner.ts +378 -0
  67. package/src/gc/state.ts +140 -0
  68. package/src/gc/transcript.ts +380 -0
  69. package/src/sentient/__tests__/brain-ingester.test.ts +154 -0
  70. package/src/sentient/__tests__/daemon.test.ts +472 -0
  71. package/src/sentient/__tests__/dream-tick.test.ts +200 -0
  72. package/src/sentient/__tests__/nexus-ingester.test.ts +138 -0
  73. package/src/sentient/__tests__/proposal-rate-limiter.test.ts +247 -0
  74. package/src/sentient/__tests__/propose-tick.test.ts +296 -0
  75. package/src/sentient/__tests__/test-ingester.test.ts +104 -0
  76. package/src/sentient/daemon-entry.ts +20 -0
  77. package/src/sentient/daemon.ts +471 -0
  78. package/src/sentient/index.ts +18 -0
  79. package/src/sentient/ingesters/brain-ingester.ts +122 -0
  80. package/src/sentient/ingesters/nexus-ingester.ts +171 -0
  81. package/src/sentient/ingesters/test-ingester.ts +205 -0
  82. package/src/sentient/proposal-rate-limiter.ts +172 -0
  83. package/src/sentient/propose-tick.ts +415 -0
  84. package/src/sentient/state.ts +229 -0
  85. package/src/sentient/tick.ts +688 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"propose-tick.d.ts","sourceRoot":"","sources":["../../src/sentient/propose-tick.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAiBH;;;GAGG;AACH,eAAO,MAAM,sBAAsB,QAA+B,CAAC;AAEnE;;;GAGG;AACH,eAAO,MAAM,WAAW,mBAAmB,CAAC;AAM5C,iDAAiD;AACjD,MAAM,MAAM,uBAAuB,GAC/B,QAAQ,GACR,UAAU,GACV,cAAc,GACd,eAAe,GACf,OAAO,GACP,OAAO,CAAC;AAEZ,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,kDAAkD;IAClD,IAAI,EAAE,uBAAuB,CAAC;IAC9B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB;AAMD,0CAA0C;AAC1C,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,WAAW,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACpD;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACpD;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,aAAa,EAAE,YAAY,GAAG,IAAI,CAAC;IACpD;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;CACxC;AA0BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+O7F;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAYjG"}
@@ -0,0 +1,549 @@
1
+ // packages/core/src/sentient/ingesters/brain-ingester.ts
2
+ var BRAIN_INGESTER_LIMIT = 10;
3
+ var BRAIN_MIN_CITATION_COUNT = 3;
4
+ var BRAIN_MIN_QUALITY_SCORE = 0.5;
5
+ var BRAIN_LOOKBACK_DAYS = 7;
6
+ function computeBrainWeight(citationCount, qualityScore) {
7
+ return Math.min(citationCount / 10 * qualityScore, 1);
8
+ }
9
+ function runBrainIngester(nativeDb) {
10
+ if (!nativeDb) {
11
+ return [];
12
+ }
13
+ try {
14
+ const stmt = nativeDb.prepare(`
15
+ SELECT id, title, text, citation_count, quality_score
16
+ FROM brain_observations
17
+ WHERE type IN ('bugfix', 'decision')
18
+ AND citation_count >= :minCitations
19
+ AND created_at >= datetime('now', :lookback)
20
+ AND quality_score >= :minQuality
21
+ ORDER BY citation_count DESC, quality_score DESC
22
+ LIMIT :limit
23
+ `);
24
+ const rows = stmt.all({
25
+ minCitations: BRAIN_MIN_CITATION_COUNT,
26
+ lookback: `-${BRAIN_LOOKBACK_DAYS} days`,
27
+ minQuality: BRAIN_MIN_QUALITY_SCORE,
28
+ limit: BRAIN_INGESTER_LIMIT
29
+ });
30
+ const candidates = rows.map((row) => {
31
+ const label = row.title ?? row.text.slice(0, 80);
32
+ return {
33
+ source: "brain",
34
+ sourceId: row.id,
35
+ title: `[T2-BRAIN] Recurring issue: ${label}`,
36
+ rationale: `Brain entry ${row.id} cited ${row.citation_count} times (quality ${row.quality_score.toFixed(2)}) in the last ${BRAIN_LOOKBACK_DAYS} days`,
37
+ weight: computeBrainWeight(row.citation_count, row.quality_score)
38
+ };
39
+ });
40
+ candidates.sort((a, b) => b.weight - a.weight);
41
+ return candidates;
42
+ } catch (err) {
43
+ const message = err instanceof Error ? err.message : String(err);
44
+ process.stderr.write(`[sentient/brain-ingester] WARNING: ${message}
45
+ `);
46
+ return [];
47
+ }
48
+ }
49
+
50
+ // packages/core/src/sentient/ingesters/nexus-ingester.ts
51
+ var NEXUS_BASE_WEIGHT = 0.3;
52
+ var NEXUS_MIN_CALLER_COUNT = 5;
53
+ var NEXUS_MIN_DEGREE = 20;
54
+ var NEXUS_QUERY_LIMIT = 5;
55
+ function toFingerprint(nodeId) {
56
+ return nodeId;
57
+ }
58
+ function runNexusIngester(nativeDb) {
59
+ if (!nativeDb) {
60
+ return [];
61
+ }
62
+ const seenIds = /* @__PURE__ */ new Set();
63
+ const candidates = [];
64
+ try {
65
+ const stmtA = nativeDb.prepare(`
66
+ SELECT n.id, n.name, n.file_path, COUNT(r.id) as caller_count
67
+ FROM nexus_nodes n
68
+ JOIN nexus_relations r ON r.target_id = n.id AND r.kind = 'calls'
69
+ WHERE NOT EXISTS (
70
+ SELECT 1 FROM nexus_relations r2
71
+ WHERE r2.source_id = n.id AND r2.kind = 'calls'
72
+ )
73
+ AND n.kind = 'function'
74
+ GROUP BY n.id
75
+ HAVING caller_count > :minCallers
76
+ ORDER BY caller_count DESC
77
+ LIMIT :limit
78
+ `);
79
+ const rowsA = stmtA.all({
80
+ minCallers: NEXUS_MIN_CALLER_COUNT,
81
+ limit: NEXUS_QUERY_LIMIT
82
+ });
83
+ for (const row of rowsA) {
84
+ const fp = toFingerprint(row.id);
85
+ if (seenIds.has(fp)) continue;
86
+ seenIds.add(fp);
87
+ candidates.push({
88
+ source: "nexus",
89
+ sourceId: row.id,
90
+ title: `[T2-NEXUS] Over-coupled symbol: ${row.name} (${row.caller_count} callers)`,
91
+ rationale: `Function ${row.name} in ${row.file_path} has ${row.caller_count} callers but makes no outbound calls \u2014 review for abstraction opportunity`,
92
+ weight: NEXUS_BASE_WEIGHT
93
+ });
94
+ }
95
+ } catch (err) {
96
+ const message = err instanceof Error ? err.message : String(err);
97
+ process.stderr.write(`[sentient/nexus-ingester] WARNING query A: ${message}
98
+ `);
99
+ }
100
+ try {
101
+ const stmtB = nativeDb.prepare(`
102
+ SELECT n.id, n.name, n.file_path, COUNT(r.id) as degree
103
+ FROM nexus_nodes n
104
+ JOIN nexus_relations r ON r.source_id = n.id OR r.target_id = n.id
105
+ GROUP BY n.id
106
+ HAVING degree > :minDegree
107
+ ORDER BY degree DESC
108
+ LIMIT :limit
109
+ `);
110
+ const rowsB = stmtB.all({
111
+ minDegree: NEXUS_MIN_DEGREE,
112
+ limit: NEXUS_QUERY_LIMIT
113
+ });
114
+ for (const row of rowsB) {
115
+ const fp = toFingerprint(row.id);
116
+ if (seenIds.has(fp)) continue;
117
+ seenIds.add(fp);
118
+ candidates.push({
119
+ source: "nexus",
120
+ sourceId: row.id,
121
+ title: `[T2-NEXUS] Over-coupled symbol: ${row.name} (${row.degree} edges)`,
122
+ rationale: `Symbol ${row.name} in ${row.file_path} has ${row.degree} total edges \u2014 review for over-coupling`,
123
+ weight: NEXUS_BASE_WEIGHT
124
+ });
125
+ }
126
+ } catch (err) {
127
+ const message = err instanceof Error ? err.message : String(err);
128
+ process.stderr.write(`[sentient/nexus-ingester] WARNING query B: ${message}
129
+ `);
130
+ }
131
+ return candidates;
132
+ }
133
+
134
+ // packages/core/src/sentient/ingesters/test-ingester.ts
135
+ import { readFileSync } from "node:fs";
136
+ import { join } from "node:path";
137
+ var GATES_JSONL_PATH = ".cleo/audit/gates.jsonl";
138
+ var COVERAGE_SUMMARY_PATH = ".cleo/coverage-summary.json";
139
+ var MIN_LINE_COVERAGE_PCT = 80;
140
+ var TEST_BASE_WEIGHT = 0.5;
141
+ function runGatesIngester(projectRoot) {
142
+ const gatesPath = join(projectRoot, GATES_JSONL_PATH);
143
+ let raw;
144
+ try {
145
+ raw = readFileSync(gatesPath, "utf-8");
146
+ } catch {
147
+ return [];
148
+ }
149
+ const candidates = [];
150
+ const seenKeys = /* @__PURE__ */ new Set();
151
+ for (const line of raw.split("\n")) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed) continue;
154
+ let record;
155
+ try {
156
+ record = JSON.parse(trimmed);
157
+ } catch {
158
+ continue;
159
+ }
160
+ const taskId = record.taskId;
161
+ const gate = record.gate ?? "unknown";
162
+ const failCount = record.failCount ?? 0;
163
+ if (typeof taskId !== "string" || failCount <= 0) continue;
164
+ const key = `${taskId}.${gate}`;
165
+ if (seenKeys.has(key)) continue;
166
+ seenKeys.add(key);
167
+ candidates.push({
168
+ source: "test",
169
+ sourceId: key,
170
+ title: `[T2-TEST] Fix flaky gate: ${taskId}.${gate}`,
171
+ rationale: `Gate '${gate}' on task ${taskId} has failed ${failCount} time(s)`,
172
+ weight: TEST_BASE_WEIGHT
173
+ });
174
+ }
175
+ return candidates;
176
+ }
177
+ function runCoverageIngester(projectRoot) {
178
+ const coveragePath = join(projectRoot, COVERAGE_SUMMARY_PATH);
179
+ let summary;
180
+ try {
181
+ const raw = readFileSync(coveragePath, "utf-8");
182
+ summary = JSON.parse(raw);
183
+ } catch {
184
+ return [];
185
+ }
186
+ const candidates = [];
187
+ for (const [filePath, entry] of Object.entries(summary)) {
188
+ if (filePath === "total") continue;
189
+ const pct = entry?.lines?.pct;
190
+ if (typeof pct !== "number" || pct >= MIN_LINE_COVERAGE_PCT) continue;
191
+ candidates.push({
192
+ source: "test",
193
+ sourceId: filePath,
194
+ title: `[T2-TEST] Increase coverage: ${filePath} (${pct}% lines)`,
195
+ rationale: `File ${filePath} has ${pct}% line coverage (target: ${MIN_LINE_COVERAGE_PCT}%)`,
196
+ weight: TEST_BASE_WEIGHT
197
+ });
198
+ }
199
+ return candidates;
200
+ }
201
+ function runTestIngester(projectRoot) {
202
+ try {
203
+ const gatesCandidates = runGatesIngester(projectRoot);
204
+ const coverageCandidates = runCoverageIngester(projectRoot);
205
+ const seenSourceIds = /* @__PURE__ */ new Set();
206
+ const merged = [];
207
+ for (const candidate of [...gatesCandidates, ...coverageCandidates]) {
208
+ if (seenSourceIds.has(candidate.sourceId)) continue;
209
+ seenSourceIds.add(candidate.sourceId);
210
+ merged.push(candidate);
211
+ }
212
+ return merged;
213
+ } catch (err) {
214
+ const message = err instanceof Error ? err.message : String(err);
215
+ process.stderr.write(`[sentient/test-ingester] WARNING: ${message}
216
+ `);
217
+ return [];
218
+ }
219
+ }
220
+
221
+ // packages/core/src/sentient/proposal-rate-limiter.ts
222
+ var SENTIENT_TIER2_TAG = "sentient-tier2";
223
+ var DEFAULT_DAILY_PROPOSAL_LIMIT = 3;
224
+ var SQLITE_BUSY_CODE = "SQLITE_BUSY";
225
+ function countTodayProposals(nativeDb) {
226
+ if (!nativeDb) return 0;
227
+ const stmt = nativeDb.prepare(`
228
+ SELECT COUNT(*) as cnt
229
+ FROM tasks
230
+ WHERE labels_json LIKE :labelPattern
231
+ AND date(created_at) = date('now')
232
+ AND status IN ('proposed', 'pending', 'active', 'done')
233
+ `);
234
+ const row = stmt.get({ labelPattern: `%${SENTIENT_TIER2_TAG}%` });
235
+ return row?.cnt ?? 0;
236
+ }
237
+ function transactionalInsertProposal(nativeDb, insertSql, insertParams, limit = DEFAULT_DAILY_PROPOSAL_LIMIT) {
238
+ try {
239
+ nativeDb.exec("BEGIN IMMEDIATE");
240
+ } catch (err) {
241
+ const msg = err instanceof Error ? err.message : String(err);
242
+ if (msg.includes(SQLITE_BUSY_CODE)) {
243
+ return { inserted: false, countBeforeInsert: 0, reason: "busy" };
244
+ }
245
+ throw err;
246
+ }
247
+ try {
248
+ const countBeforeInsert = countTodayProposals(nativeDb);
249
+ if (countBeforeInsert >= limit) {
250
+ nativeDb.exec("ROLLBACK");
251
+ return { inserted: false, countBeforeInsert, reason: "rate-limit" };
252
+ }
253
+ const stmt = nativeDb.prepare(insertSql);
254
+ stmt.run(insertParams);
255
+ nativeDb.exec("COMMIT");
256
+ return { inserted: true, countBeforeInsert };
257
+ } catch (err) {
258
+ try {
259
+ nativeDb.exec("ROLLBACK");
260
+ } catch {
261
+ }
262
+ throw err;
263
+ }
264
+ }
265
+
266
+ // packages/core/src/sentient/state.ts
267
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
268
+ import { dirname, join as join2 } from "node:path";
269
+ var SENTIENT_STATE_SCHEMA_VERSION = "1.0";
270
+ var DEFAULT_SENTIENT_STATE = {
271
+ schemaVersion: SENTIENT_STATE_SCHEMA_VERSION,
272
+ pid: null,
273
+ startedAt: null,
274
+ lastTickAt: null,
275
+ killSwitch: false,
276
+ killSwitchReason: null,
277
+ stats: {
278
+ tasksPicked: 0,
279
+ tasksCompleted: 0,
280
+ tasksFailed: 0,
281
+ ticksExecuted: 0,
282
+ ticksKilled: 0
283
+ },
284
+ stuckTasks: {},
285
+ stuckTimestamps: [],
286
+ activeTaskId: null,
287
+ tier2Enabled: false,
288
+ tier2Stats: {
289
+ proposalsGenerated: 0,
290
+ proposalsAccepted: 0,
291
+ proposalsRejected: 0
292
+ }
293
+ };
294
+ async function readSentientState(statePath) {
295
+ try {
296
+ const raw = await readFile(statePath, "utf-8");
297
+ const parsed = JSON.parse(raw);
298
+ return {
299
+ ...DEFAULT_SENTIENT_STATE,
300
+ ...parsed,
301
+ stats: { ...DEFAULT_SENTIENT_STATE.stats, ...parsed.stats ?? {} },
302
+ stuckTasks: parsed.stuckTasks ?? {},
303
+ stuckTimestamps: parsed.stuckTimestamps ?? [],
304
+ tier2Enabled: parsed.tier2Enabled ?? false,
305
+ tier2Stats: { ...DEFAULT_SENTIENT_STATE.tier2Stats, ...parsed.tier2Stats ?? {} }
306
+ };
307
+ } catch {
308
+ return { ...DEFAULT_SENTIENT_STATE };
309
+ }
310
+ }
311
+ async function writeSentientState(statePath, state) {
312
+ const dir = dirname(statePath);
313
+ await mkdir(dir, { recursive: true });
314
+ const tmpPath = join2(dir, `.sentient-state-${process.pid}.tmp`);
315
+ const json = JSON.stringify(state, null, 2);
316
+ await writeFile(tmpPath, json, "utf-8");
317
+ await rename(tmpPath, statePath);
318
+ }
319
+ async function patchSentientState(statePath, patch) {
320
+ const current = await readSentientState(statePath);
321
+ const updated = {
322
+ ...current,
323
+ ...patch,
324
+ stats: { ...current.stats, ...patch.stats ?? {} }
325
+ };
326
+ await writeSentientState(statePath, updated);
327
+ return updated;
328
+ }
329
+
330
+ // packages/core/src/sentient/propose-tick.ts
331
+ var PROPOSAL_TITLE_PATTERN = /^\[T2-(BRAIN|NEXUS|TEST)\]/;
332
+ var TIER2_LABEL = "sentient-tier2";
333
+ function fingerprint(candidate) {
334
+ return `${candidate.source}:${candidate.sourceId}`;
335
+ }
336
+ async function killSwitchActive(statePath) {
337
+ const state = await readSentientState(statePath);
338
+ return state.killSwitch === true;
339
+ }
340
+ async function runProposeTick(options) {
341
+ const { projectRoot, statePath } = options;
342
+ if (await killSwitchActive(statePath)) {
343
+ return { kind: "killed", written: 0, count: 0, detail: "killSwitch active before ingest" };
344
+ }
345
+ const state = await readSentientState(statePath);
346
+ if (!state.tier2Enabled) {
347
+ return {
348
+ kind: "disabled",
349
+ written: 0,
350
+ count: 0,
351
+ detail: "tier2Enabled=false; enable via cleo sentient propose enable"
352
+ };
353
+ }
354
+ let brainDb;
355
+ let nexusDb;
356
+ let tasksNativeDb;
357
+ if (options.brainDb !== void 0) {
358
+ brainDb = options.brainDb;
359
+ } else {
360
+ try {
361
+ const { getBrainDb, getBrainNativeDb } = await import("@cleocode/core/internal");
362
+ await getBrainDb(projectRoot);
363
+ brainDb = getBrainNativeDb();
364
+ } catch {
365
+ brainDb = null;
366
+ }
367
+ }
368
+ if (options.nexusDb !== void 0) {
369
+ nexusDb = options.nexusDb;
370
+ } else {
371
+ try {
372
+ const { getNexusNativeDb } = await import("@cleocode/core/internal");
373
+ nexusDb = getNexusNativeDb();
374
+ } catch {
375
+ nexusDb = null;
376
+ }
377
+ }
378
+ if (options.tasksDb !== void 0) {
379
+ tasksNativeDb = options.tasksDb;
380
+ } else {
381
+ const { getNativeDb, getDb } = await import("@cleocode/core/internal");
382
+ await getDb(projectRoot);
383
+ tasksNativeDb = getNativeDb();
384
+ }
385
+ const [brainCandidates, nexusCandidates, testCandidates] = await Promise.all([
386
+ Promise.resolve(runBrainIngester(brainDb)),
387
+ Promise.resolve(runNexusIngester(nexusDb)),
388
+ Promise.resolve(runTestIngester(projectRoot))
389
+ ]);
390
+ if (await killSwitchActive(statePath)) {
391
+ return {
392
+ kind: "killed",
393
+ written: 0,
394
+ count: 0,
395
+ detail: "killSwitch active after ingest phase"
396
+ };
397
+ }
398
+ const seenFingerprints = /* @__PURE__ */ new Set();
399
+ const merged = [];
400
+ for (const candidate of [...brainCandidates, ...nexusCandidates, ...testCandidates]) {
401
+ if (!PROPOSAL_TITLE_PATTERN.test(candidate.title)) {
402
+ process.stderr.write(
403
+ `[sentient/propose-tick] Rejected candidate with invalid title format: "${candidate.title}"
404
+ `
405
+ );
406
+ continue;
407
+ }
408
+ const fp = fingerprint(candidate);
409
+ if (seenFingerprints.has(fp)) continue;
410
+ seenFingerprints.add(fp);
411
+ merged.push(candidate);
412
+ }
413
+ if (merged.length === 0) {
414
+ return { kind: "no-candidates", written: 0, count: 0, detail: "no candidates from ingesters" };
415
+ }
416
+ merged.sort((a, b) => b.weight - a.weight);
417
+ const currentCount = tasksNativeDb ? countTodayProposals(tasksNativeDb) : 0;
418
+ const slotsRemaining = Math.max(0, DEFAULT_DAILY_PROPOSAL_LIMIT - currentCount);
419
+ if (slotsRemaining === 0) {
420
+ return {
421
+ kind: "rate-limited",
422
+ written: 0,
423
+ count: currentCount,
424
+ detail: `daily limit reached (${currentCount}/${DEFAULT_DAILY_PROPOSAL_LIMIT})`
425
+ };
426
+ }
427
+ const toWrite = merged.slice(0, slotsRemaining);
428
+ if (await killSwitchActive(statePath)) {
429
+ return {
430
+ kind: "killed",
431
+ written: 0,
432
+ count: currentCount,
433
+ detail: "killSwitch active before write phase"
434
+ };
435
+ }
436
+ let written = 0;
437
+ for (const candidate of toWrite) {
438
+ let taskId;
439
+ if (options.allocateTaskId) {
440
+ taskId = await options.allocateTaskId();
441
+ } else {
442
+ const { allocateNextTaskId } = await import("@cleocode/core/internal");
443
+ taskId = await allocateNextTaskId(projectRoot);
444
+ }
445
+ const now = (/* @__PURE__ */ new Date()).toISOString();
446
+ const labels = JSON.stringify([TIER2_LABEL, `source:${candidate.source}`]);
447
+ if (!tasksNativeDb) {
448
+ process.stderr.write("[sentient/propose-tick] tasks DB not available; skipping write\n");
449
+ break;
450
+ }
451
+ const notesJson = JSON.stringify([
452
+ JSON.stringify({
453
+ kind: "proposal-meta",
454
+ proposedBy: "sentient-tier2",
455
+ source: candidate.source,
456
+ sourceId: candidate.sourceId,
457
+ weight: candidate.weight,
458
+ proposedAt: now
459
+ })
460
+ ]);
461
+ const insertSql = `
462
+ INSERT INTO tasks (
463
+ id, title, description, status, priority,
464
+ labels_json, notes_json,
465
+ created_at, updated_at,
466
+ role, scope
467
+ ) VALUES (
468
+ :id, :title, :description, :status, :priority,
469
+ :labelsJson, :notesJson,
470
+ :createdAt, :updatedAt,
471
+ :role, :scope
472
+ )
473
+ `;
474
+ const insertParams = {
475
+ id: taskId,
476
+ title: candidate.title,
477
+ description: candidate.rationale,
478
+ status: "proposed",
479
+ priority: "medium",
480
+ labelsJson: labels,
481
+ notesJson,
482
+ createdAt: now,
483
+ updatedAt: now,
484
+ role: "work",
485
+ scope: "feature"
486
+ };
487
+ try {
488
+ const result = transactionalInsertProposal(
489
+ tasksNativeDb,
490
+ insertSql,
491
+ insertParams,
492
+ DEFAULT_DAILY_PROPOSAL_LIMIT
493
+ );
494
+ if (result.inserted) {
495
+ written++;
496
+ } else if (result.reason === "rate-limit") {
497
+ break;
498
+ }
499
+ } catch (err) {
500
+ const message = err instanceof Error ? err.message : String(err);
501
+ process.stderr.write(`[sentient/propose-tick] INSERT failed for ${taskId}: ${message}
502
+ `);
503
+ }
504
+ }
505
+ if (written > 0) {
506
+ const latestState = await readSentientState(statePath);
507
+ await patchSentientState(statePath, {
508
+ tier2Stats: {
509
+ ...latestState.tier2Stats,
510
+ proposalsGenerated: latestState.tier2Stats.proposalsGenerated + written
511
+ }
512
+ });
513
+ }
514
+ const finalCount = tasksNativeDb ? countTodayProposals(tasksNativeDb) : currentCount + written;
515
+ if (written === 0) {
516
+ return {
517
+ kind: "no-candidates",
518
+ written: 0,
519
+ count: finalCount,
520
+ detail: "candidates available but none written (rate limit or DB unavailable)"
521
+ };
522
+ }
523
+ return {
524
+ kind: "wrote",
525
+ written,
526
+ count: finalCount,
527
+ detail: `wrote ${written} proposal(s) (${finalCount}/${DEFAULT_DAILY_PROPOSAL_LIMIT} today)`
528
+ };
529
+ }
530
+ async function safeRunProposeTick(options) {
531
+ try {
532
+ return await runProposeTick(options);
533
+ } catch (err) {
534
+ const message = err instanceof Error ? err.message : String(err);
535
+ return {
536
+ kind: "error",
537
+ written: 0,
538
+ count: 0,
539
+ detail: `propose tick threw: ${message}`
540
+ };
541
+ }
542
+ }
543
+ export {
544
+ PROPOSAL_TITLE_PATTERN,
545
+ TIER2_LABEL,
546
+ runProposeTick,
547
+ safeRunProposeTick
548
+ };
549
+ //# sourceMappingURL=propose-tick.js.map