@cleocode/core 2026.4.99 → 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.
- package/dist/gc/daemon.js +481 -0
- package/dist/gc/daemon.js.map +7 -0
- package/dist/gc/index.js +669 -0
- package/dist/gc/index.js.map +7 -0
- package/dist/gc/runner.js +360 -0
- package/dist/gc/runner.js.map +7 -0
- package/dist/gc/state.js +49 -0
- package/dist/gc/state.js.map +7 -0
- package/dist/gc/transcript.js +209 -0
- package/dist/gc/transcript.js.map +7 -0
- package/dist/memory/brain-backfill.js +14643 -0
- package/dist/memory/brain-backfill.js.map +7 -0
- package/dist/memory/precompact-flush.js +47725 -0
- package/dist/memory/precompact-flush.js.map +7 -0
- package/dist/sentient/daemon.js +1100 -0
- package/dist/sentient/daemon.js.map +7 -0
- package/dist/sentient/index.js +1162 -0
- package/dist/sentient/index.js.map +7 -0
- package/dist/sentient/propose-tick.js +549 -0
- package/dist/sentient/propose-tick.js.map +7 -0
- package/dist/sentient/state.js +85 -0
- package/dist/sentient/state.js.map +7 -0
- package/dist/sentient/tick.js +396 -0
- package/dist/sentient/tick.js.map +7 -0
- package/dist/system/platform-paths.js +36 -0
- package/dist/system/platform-paths.js.map +7 -0
- package/package.json +8 -8
|
@@ -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
|