reflectt-node 0.1.7 → 0.1.8
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/README.md +13 -0
- package/defaults/gitignore.template +23 -0
- package/dist/boardHealthWorker.d.ts +4 -0
- package/dist/boardHealthWorker.d.ts.map +1 -1
- package/dist/boardHealthWorker.js +36 -1
- package/dist/boardHealthWorker.js.map +1 -1
- package/dist/buildInfo.d.ts.map +1 -1
- package/dist/buildInfo.js +47 -10
- package/dist/buildInfo.js.map +1 -1
- package/dist/chat.d.ts +4 -0
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +6 -2
- package/dist/chat.js.map +1 -1
- package/dist/cli.js +37 -12
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +131 -64
- package/dist/cloud.js.map +1 -1
- package/dist/continuity-loop.d.ts.map +1 -1
- package/dist/continuity-loop.js +297 -29
- package/dist/continuity-loop.js.map +1 -1
- package/dist/deploy-monitor.d.ts +18 -0
- package/dist/deploy-monitor.d.ts.map +1 -0
- package/dist/deploy-monitor.js +165 -0
- package/dist/deploy-monitor.js.map +1 -0
- package/dist/executionSweeper.d.ts +1 -0
- package/dist/executionSweeper.d.ts.map +1 -1
- package/dist/executionSweeper.js +43 -7
- package/dist/executionSweeper.js.map +1 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +17 -3
- package/dist/files.js.map +1 -1
- package/dist/fingerprint.d.ts +30 -0
- package/dist/fingerprint.d.ts.map +1 -0
- package/dist/fingerprint.js +122 -0
- package/dist/fingerprint.js.map +1 -0
- package/dist/github-webhook-attribution.d.ts +38 -0
- package/dist/github-webhook-attribution.d.ts.map +1 -0
- package/dist/github-webhook-attribution.js +123 -0
- package/dist/github-webhook-attribution.js.map +1 -0
- package/dist/inbox.d.ts.map +1 -1
- package/dist/inbox.js +4 -0
- package/dist/inbox.js.map +1 -1
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/pulse.d.ts +7 -0
- package/dist/pulse.d.ts.map +1 -1
- package/dist/pulse.js +15 -0
- package/dist/pulse.js.map +1 -1
- package/dist/review-state.d.ts +9 -0
- package/dist/review-state.d.ts.map +1 -0
- package/dist/review-state.js +17 -0
- package/dist/review-state.js.map +1 -0
- package/dist/schedule.d.ts +60 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +176 -0
- package/dist/schedule.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +486 -14
- package/dist/server.js.map +1 -1
- package/dist/suppression-ledger.d.ts.map +1 -1
- package/dist/suppression-ledger.js +12 -3
- package/dist/suppression-ledger.js.map +1 -1
- package/dist/system-loop-state.d.ts +1 -1
- package/dist/system-loop-state.d.ts.map +1 -1
- package/dist/system-loop-state.js +1 -0
- package/dist/system-loop-state.js.map +1 -1
- package/dist/tasks.d.ts +9 -1
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +193 -41
- package/dist/tasks.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/usage-tracking.d.ts +26 -0
- package/dist/usage-tracking.d.ts.map +1 -1
- package/dist/usage-tracking.js +91 -4
- package/dist/usage-tracking.js.map +1 -1
- package/package.json +1 -1
- package/public/dashboard.js +119 -37
- package/public/docs.md +18 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suppression-ledger.d.ts","sourceRoot":"","sources":["../src/suppression-ledger.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACvB;AAID,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAQ;gBAEZ,QAAQ,CAAC,EAAE,MAAM;IAI7B;;;OAGG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;
|
|
1
|
+
{"version":3,"file":"suppression-ledger.d.ts","sourceRoot":"","sources":["../src/suppression-ledger.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,eAAe,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,WAAW,EAAE,OAAO,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;IACjB,QAAQ,CAAC,EAAE,gBAAgB,CAAA;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClF,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACjF,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,MAAM,CAAA;CACvB;AAID,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAQ;gBAEZ,QAAQ,CAAC,EAAE,MAAM;IAI7B;;;OAGG;IACH,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;IAuB3E;;;;OAIG;IACH,KAAK,CAAC,IAAI,EAAE;QACV,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,MAAM,CAAA;QACf,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;KAChB,GAAG,sBAAsB;IAyD1B;;OAEG;IACH,QAAQ,IAAI,gBAAgB;IAqC5B;;OAEG;IACH,KAAK,IAAI,MAAM;IAOf;;OAEG;IACH,WAAW,IAAI,MAAM;IAIrB;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;CAG9B;AAED,eAAO,MAAM,iBAAiB,mBAA0B,CAAA"}
|
|
@@ -16,13 +16,22 @@ export class SuppressionLedger {
|
|
|
16
16
|
* Content is normalized: timestamps, task IDs, and message IDs stripped.
|
|
17
17
|
*/
|
|
18
18
|
computeDedupKey(category, channel, content) {
|
|
19
|
-
|
|
19
|
+
let normalized = content
|
|
20
20
|
.trim()
|
|
21
21
|
.toLowerCase()
|
|
22
22
|
.replace(/\b(msg-|task-|tcomment-|ins-|ref-)\S+/g, '')
|
|
23
23
|
.replace(/\d{13,}/g, '')
|
|
24
|
-
.replace(/\s+/g, ' ')
|
|
25
|
-
|
|
24
|
+
.replace(/\s+/g, ' ');
|
|
25
|
+
// For digest messages, aggressively strip volatile counts so that
|
|
26
|
+
// "32 todo · 2 doing" vs "31 todo · 3 doing" hashes identically.
|
|
27
|
+
// This prevents process restarts from re-emitting the same digest
|
|
28
|
+
// just because task counts shifted by 1-2.
|
|
29
|
+
if (category === 'digest') {
|
|
30
|
+
normalized = normalized
|
|
31
|
+
.replace(/\d+/g, 'N') // normalize all remaining numbers
|
|
32
|
+
.replace(/\bN+\b/g, 'N'); // collapse repeated N
|
|
33
|
+
}
|
|
34
|
+
normalized = normalized.slice(0, 300);
|
|
26
35
|
const raw = `${category}:${channel}:${normalized}`;
|
|
27
36
|
return createHash('sha256').update(raw).digest('hex').substring(0, 20);
|
|
28
37
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"suppression-ledger.js","sourceRoot":"","sources":["../src/suppression-ledger.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,iEAAiE;AACjE,EAAE;AACF,mEAAmE;AACnE,iFAAiF;AAEjF,OAAO,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAgC/B,MAAM,iBAAiB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA,CAAC,aAAa;AAEtD,MAAM,OAAO,iBAAiB;IACpB,QAAQ,CAAQ;IAExB,YAAY,QAAiB;QAC3B,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,iBAAiB,CAAA;IAC/C,CAAC;IAED;;;OAGG;IACH,eAAe,CAAC,QAAgB,EAAE,OAAe,EAAE,OAAe;QAChE,
|
|
1
|
+
{"version":3,"file":"suppression-ledger.js","sourceRoot":"","sources":["../src/suppression-ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SystemLoopName = 'idle_nudge' | 'cadence_watchdog' | 'mention_rescue' | 'reflection_pipeline' | 'board_health';
|
|
1
|
+
export type SystemLoopName = 'idle_nudge' | 'cadence_watchdog' | 'mention_rescue' | 'reflection_pipeline' | 'board_health' | 'deploy_monitor';
|
|
2
2
|
export declare function recordSystemLoopTick(name: SystemLoopName, now?: number): void;
|
|
3
3
|
export declare function getSystemLoopTicks(): Record<SystemLoopName, number>;
|
|
4
4
|
//# sourceMappingURL=system-loop-state.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-loop-state.d.ts","sourceRoot":"","sources":["../src/system-loop-state.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,kBAAkB,GAClB,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"system-loop-state.d.ts","sourceRoot":"","sources":["../src/system-loop-state.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GACtB,YAAY,GACZ,kBAAkB,GAClB,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,GACd,gBAAgB,CAAA;AAEpB,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,SAAa,GAAG,IAAI,CAWjF;AAED,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAsBnE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-loop-state.js","sourceRoot":"","sources":["../src/system-loop-state.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;
|
|
1
|
+
{"version":3,"file":"system-loop-state.js","sourceRoot":"","sources":["../src/system-loop-state.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAA;AAU/B,MAAM,UAAU,oBAAoB,CAAC,IAAoB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACzE,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,IAAI,CAAC;QACH,EAAE,CAAC,OAAO,CACR;;4EAEsE,CACvE,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;IAClB,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;IACnE,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,GAAG,GAAmC;QAC1C,UAAU,EAAE,CAAC;QACb,gBAAgB,EAAE,CAAC;QACnB,cAAc,EAAE,CAAC;QACjB,mBAAmB,EAAE,CAAC;QACtB,YAAY,EAAE,CAAC;QACf,cAAc,EAAE,CAAC;KAClB,CAAA;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,OAAO,CAAC,kDAAkD,CAAC,CAAC,GAAG,EAAmD,CAAA;QAClI,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAmB,CAAA;YAC7C,IAAI,IAAI,IAAI,GAAG;gBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,YAAY,IAAI,CAAC,CAAC,CAAA;QAC1D,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC"}
|
package/dist/tasks.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ declare class TaskManager {
|
|
|
32
32
|
created: number;
|
|
33
33
|
skipped: number;
|
|
34
34
|
}>;
|
|
35
|
+
private appendTaskTombstone;
|
|
35
36
|
/** Write a single task to SQLite + JSONL audit */
|
|
36
37
|
private writeTaskToDb;
|
|
37
38
|
/** Legacy bulk persist — now just writes all tasks from DB to JSONL */
|
|
@@ -73,6 +74,13 @@ declare class TaskManager {
|
|
|
73
74
|
* fields without triggering full validation or history events.
|
|
74
75
|
*/
|
|
75
76
|
patchTaskMetadata(id: string, metadataUpdates: Record<string, unknown>): boolean;
|
|
77
|
+
getTaskDeletionTombstone(inputId: string): {
|
|
78
|
+
taskId: string;
|
|
79
|
+
deletedAt: number;
|
|
80
|
+
deletedBy: string;
|
|
81
|
+
previousStatus: string;
|
|
82
|
+
title: string;
|
|
83
|
+
} | null;
|
|
76
84
|
getTaskHistory(id: string): TaskHistoryEvent[];
|
|
77
85
|
getTaskComments(id: string, options?: {
|
|
78
86
|
includeSuppressed?: boolean;
|
|
@@ -105,7 +113,7 @@ declare class TaskManager {
|
|
|
105
113
|
private parseLaneTransition;
|
|
106
114
|
private applyLaneStateLock;
|
|
107
115
|
updateTask(id: string, updates: Partial<Omit<Task, 'id' | 'createdAt' | 'createdBy'>>): Promise<Task | undefined>;
|
|
108
|
-
deleteTask(id: string): Promise<boolean>;
|
|
116
|
+
deleteTask(id: string, actor?: string): Promise<boolean>;
|
|
109
117
|
subscribe(callback: (task: Task, action: 'created' | 'updated' | 'deleted') => void): () => boolean;
|
|
110
118
|
private notifySubscribers;
|
|
111
119
|
private checkUnblockedTasks;
|
package/dist/tasks.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../src/tasks.ts"],"names":[],"mappings":"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"}
|
package/dist/tasks.js
CHANGED
|
@@ -25,8 +25,14 @@ function importTasks(db, records) {
|
|
|
25
25
|
tags, metadata, team_id, comment_count, due_at, scheduled_for
|
|
26
26
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
27
|
`);
|
|
28
|
+
const del = db.prepare('DELETE FROM tasks WHERE id = ?');
|
|
28
29
|
const insertMany = db.transaction((tasks) => {
|
|
29
30
|
for (const record of tasks) {
|
|
31
|
+
const tombstone = record;
|
|
32
|
+
if (tombstone?.deleted === true && typeof tombstone.id === 'string' && tombstone.id.trim()) {
|
|
33
|
+
del.run(tombstone.id);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
30
36
|
const task = record;
|
|
31
37
|
insert.run(task.id, task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, 0, // comment_count will be recalculated when comments are imported
|
|
32
38
|
task.dueAt ?? null, task.scheduledFor ?? null);
|
|
@@ -336,12 +342,18 @@ class TaskManager {
|
|
|
336
342
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
337
343
|
`);
|
|
338
344
|
insert.run(event.id, event.taskId, event.type, event.actor, event.timestamp, safeJsonStringify(event.data));
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
console.error('[Tasks] Failed to append task history (DB):', err);
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
339
351
|
// Append to JSONL (audit log)
|
|
340
352
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
341
353
|
await fs.appendFile(TASK_HISTORY_FILE, `${JSON.stringify(event)}\n`, 'utf-8');
|
|
342
354
|
}
|
|
343
355
|
catch (err) {
|
|
344
|
-
console.error('[Tasks] Failed to append task history:', err);
|
|
356
|
+
console.error('[Tasks] Failed to append task history (JSONL):', err);
|
|
345
357
|
}
|
|
346
358
|
}
|
|
347
359
|
async recordTaskHistoryEvent(taskId, type, actor, data) {
|
|
@@ -356,39 +368,48 @@ class TaskManager {
|
|
|
356
368
|
await this.appendTaskHistory(event);
|
|
357
369
|
}
|
|
358
370
|
async appendTaskComment(comment) {
|
|
371
|
+
const db = getDb();
|
|
372
|
+
// DB writes must be atomic; otherwise callers may observe a "phantom" comment ID
|
|
373
|
+
// that never persisted (breaking review_handoff.comment_id).
|
|
374
|
+
const insert = db.prepare(`
|
|
375
|
+
INSERT INTO task_comments (
|
|
376
|
+
id, task_id, author, content, timestamp,
|
|
377
|
+
category, suppressed, suppressed_reason, suppressed_rule
|
|
378
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
379
|
+
`);
|
|
380
|
+
const updateTask = db.prepare(`
|
|
381
|
+
UPDATE tasks
|
|
382
|
+
SET
|
|
383
|
+
comment_count = (SELECT COUNT(*) FROM task_comments WHERE task_id = ?),
|
|
384
|
+
updated_at = ?
|
|
385
|
+
WHERE id = ?
|
|
386
|
+
`);
|
|
387
|
+
const touchUpdatedAt = db.prepare(`
|
|
388
|
+
UPDATE tasks
|
|
389
|
+
SET updated_at = MAX(updated_at, ?)
|
|
390
|
+
WHERE id = ?
|
|
391
|
+
`);
|
|
392
|
+
try {
|
|
393
|
+
const tx = db.transaction(() => {
|
|
394
|
+
insert.run(comment.id, comment.taskId, comment.author, comment.content, comment.timestamp, comment.category ?? null, comment.suppressed ? 1 : 0, comment.suppressedReason ?? null, comment.suppressedRule ?? null);
|
|
395
|
+
// Comments are material activity and should advance updated_at to avoid autonomy/heartbeat false positives.
|
|
396
|
+
updateTask.run(comment.taskId, comment.timestamp, comment.taskId);
|
|
397
|
+
touchUpdatedAt.run(comment.timestamp, comment.taskId);
|
|
398
|
+
});
|
|
399
|
+
tx();
|
|
400
|
+
}
|
|
401
|
+
catch (err) {
|
|
402
|
+
// Critical: propagate failures so API callers don't record unresolvable comment IDs.
|
|
403
|
+
console.error('[Tasks] Failed to append task comment (DB):', err);
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
// Audit log is best-effort; DB is the source of truth.
|
|
359
407
|
try {
|
|
360
|
-
const db = getDb();
|
|
361
|
-
// Write to SQLite (primary)
|
|
362
|
-
const insert = db.prepare(`
|
|
363
|
-
INSERT INTO task_comments (
|
|
364
|
-
id, task_id, author, content, timestamp,
|
|
365
|
-
category, suppressed, suppressed_reason, suppressed_rule
|
|
366
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
367
|
-
`);
|
|
368
|
-
insert.run(comment.id, comment.taskId, comment.author, comment.content, comment.timestamp, comment.category ?? null, comment.suppressed ? 1 : 0, comment.suppressedReason ?? null, comment.suppressedRule ?? null);
|
|
369
|
-
// Update comment count + updated_at for the task.
|
|
370
|
-
// Comments are material activity and should advance updated_at to avoid autonomy/heartbeat false positives.
|
|
371
|
-
const updateTask = db.prepare(`
|
|
372
|
-
UPDATE tasks
|
|
373
|
-
SET
|
|
374
|
-
comment_count = (SELECT COUNT(*) FROM task_comments WHERE task_id = ?),
|
|
375
|
-
updated_at = ?
|
|
376
|
-
WHERE id = ?
|
|
377
|
-
`);
|
|
378
|
-
updateTask.run(comment.taskId, comment.timestamp, comment.taskId);
|
|
379
|
-
// Touch task updated_at so comment activity counts as task activity (autonomy, SLA, sorting)
|
|
380
|
-
const touchUpdatedAt = db.prepare(`
|
|
381
|
-
UPDATE tasks
|
|
382
|
-
SET updated_at = MAX(updated_at, ?)
|
|
383
|
-
WHERE id = ?
|
|
384
|
-
`);
|
|
385
|
-
touchUpdatedAt.run(comment.timestamp, comment.taskId);
|
|
386
|
-
// Append to JSONL (audit log)
|
|
387
408
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
388
409
|
await fs.appendFile(TASK_COMMENTS_FILE, `${JSON.stringify(comment)}\n`, 'utf-8');
|
|
389
410
|
}
|
|
390
411
|
catch (err) {
|
|
391
|
-
console.error('[Tasks] Failed to append task comment:', err);
|
|
412
|
+
console.error('[Tasks] Failed to append task comment (JSONL):', err);
|
|
392
413
|
}
|
|
393
414
|
}
|
|
394
415
|
async persistRecurringTasks() {
|
|
@@ -526,18 +547,48 @@ class TaskManager {
|
|
|
526
547
|
}
|
|
527
548
|
return { created, skipped };
|
|
528
549
|
}
|
|
550
|
+
async appendTaskTombstone(task, deletedBy, deletedAt) {
|
|
551
|
+
try {
|
|
552
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
553
|
+
await fs.appendFile(TASKS_FILE, `${JSON.stringify({
|
|
554
|
+
id: task.id,
|
|
555
|
+
deleted: true,
|
|
556
|
+
deletedAt,
|
|
557
|
+
deletedBy,
|
|
558
|
+
})}\n`, 'utf-8');
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
console.error('[Tasks] Failed to append task tombstone:', err);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
529
564
|
/** Write a single task to SQLite + JSONL audit */
|
|
530
565
|
writeTaskToDb(task) {
|
|
531
566
|
try {
|
|
532
567
|
const db = getDb();
|
|
533
568
|
const commentCount = this.getTaskCommentCount(task.id);
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
569
|
+
// Use UPDATE for existing tasks to avoid INSERT OR REPLACE which triggers
|
|
570
|
+
// ON DELETE CASCADE on foreign keys (task_comments, task_history), wiping
|
|
571
|
+
// all comments/history for the task. Only INSERT for genuinely new tasks.
|
|
572
|
+
const existing = db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(task.id);
|
|
573
|
+
if (existing) {
|
|
574
|
+
db.prepare(`
|
|
575
|
+
UPDATE tasks SET
|
|
576
|
+
title = ?, description = ?, status = ?, assignee = ?, reviewer = ?,
|
|
577
|
+
done_criteria = ?, created_by = ?, created_at = ?, updated_at = ?,
|
|
578
|
+
priority = ?, blocked_by = ?, epic_id = ?, tags = ?, metadata = ?,
|
|
579
|
+
team_id = ?, comment_count = ?, due_at = ?, scheduled_for = ?
|
|
580
|
+
WHERE id = ?
|
|
581
|
+
`).run(task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, commentCount, task.dueAt ?? null, task.scheduledFor ?? null, task.id);
|
|
582
|
+
}
|
|
583
|
+
else {
|
|
584
|
+
db.prepare(`
|
|
585
|
+
INSERT INTO tasks (
|
|
586
|
+
id, title, description, status, assignee, reviewer, done_criteria,
|
|
587
|
+
created_by, created_at, updated_at, priority, blocked_by, epic_id,
|
|
588
|
+
tags, metadata, team_id, comment_count, due_at, scheduled_for
|
|
589
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
590
|
+
`).run(task.id, task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, commentCount, task.dueAt ?? null, task.scheduledFor ?? null);
|
|
591
|
+
}
|
|
541
592
|
}
|
|
542
593
|
catch (err) {
|
|
543
594
|
console.error(`[Tasks] Failed to write task ${task.id} to SQLite:`, err);
|
|
@@ -750,6 +801,45 @@ class TaskManager {
|
|
|
750
801
|
this.writeTaskToDb(updated);
|
|
751
802
|
return true;
|
|
752
803
|
}
|
|
804
|
+
getTaskDeletionTombstone(inputId) {
|
|
805
|
+
const db = getDb();
|
|
806
|
+
const raw = String(inputId || '').trim();
|
|
807
|
+
if (!raw)
|
|
808
|
+
return null;
|
|
809
|
+
// Try exact match first, then prefix match against task_history task_ids.
|
|
810
|
+
let taskId = raw;
|
|
811
|
+
const exactRow = db.prepare(`SELECT task_id, data FROM task_history WHERE task_id = ? AND type = 'deleted' ORDER BY timestamp DESC LIMIT 1`).get(raw);
|
|
812
|
+
if (!exactRow) {
|
|
813
|
+
// Prefix match: find task_ids in history that start with the input.
|
|
814
|
+
const prefixRows = db.prepare(`SELECT task_id FROM task_history WHERE task_id LIKE ? AND type = 'deleted' LIMIT 2`).all(raw + '%');
|
|
815
|
+
if (prefixRows.length !== 1)
|
|
816
|
+
return null;
|
|
817
|
+
taskId = prefixRows[0].task_id;
|
|
818
|
+
const prefixRow = db.prepare(`SELECT data FROM task_history WHERE task_id = ? AND type = 'deleted' ORDER BY timestamp DESC LIMIT 1`).get(taskId);
|
|
819
|
+
if (!prefixRow)
|
|
820
|
+
return null;
|
|
821
|
+
const prefixData = safeJsonParse(prefixRow.data);
|
|
822
|
+
if (!prefixData)
|
|
823
|
+
return null;
|
|
824
|
+
return {
|
|
825
|
+
taskId,
|
|
826
|
+
deletedAt: typeof prefixData.deletedAt === 'number' ? prefixData.deletedAt : 0,
|
|
827
|
+
deletedBy: typeof prefixData.deletedBy === 'string' ? prefixData.deletedBy : 'unknown',
|
|
828
|
+
previousStatus: typeof prefixData.previousStatus === 'string' ? prefixData.previousStatus : 'unknown',
|
|
829
|
+
title: typeof prefixData.title === 'string' ? prefixData.title : '',
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const data = safeJsonParse(exactRow.data);
|
|
833
|
+
if (!data)
|
|
834
|
+
return null;
|
|
835
|
+
return {
|
|
836
|
+
taskId,
|
|
837
|
+
deletedAt: typeof data.deletedAt === 'number' ? data.deletedAt : 0,
|
|
838
|
+
deletedBy: typeof data.deletedBy === 'string' ? data.deletedBy : 'unknown',
|
|
839
|
+
previousStatus: typeof data.previousStatus === 'string' ? data.previousStatus : 'unknown',
|
|
840
|
+
title: typeof data.title === 'string' ? data.title : '',
|
|
841
|
+
};
|
|
842
|
+
}
|
|
753
843
|
getTaskHistory(id) {
|
|
754
844
|
const db = getDb();
|
|
755
845
|
const rows = db.prepare('SELECT * FROM task_history WHERE task_id = ? ORDER BY timestamp ASC').all(id);
|
|
@@ -986,6 +1076,37 @@ class TaskManager {
|
|
|
986
1076
|
return transition;
|
|
987
1077
|
};
|
|
988
1078
|
let transitionEvent;
|
|
1079
|
+
// ── Bounce gate: validating → doing ────────────────────────────────────
|
|
1080
|
+
// A task bouncing from validating back to doing is a signal of rework.
|
|
1081
|
+
// Track bounce_count in metadata. On the 3rd bounce (bounce_count >= 2),
|
|
1082
|
+
// require a documented reason before allowing the regression.
|
|
1083
|
+
if (task.status === 'validating' && nextStatus === 'doing') {
|
|
1084
|
+
const meta = (task.metadata || {});
|
|
1085
|
+
const currentBounce = typeof meta.bounce_count === 'number' ? meta.bounce_count : 0;
|
|
1086
|
+
const newBounceCount = currentBounce + 1;
|
|
1087
|
+
if (currentBounce >= 2) {
|
|
1088
|
+
// 3rd+ bounce: require documented reason
|
|
1089
|
+
if (!transition || typeof transition !== 'object') {
|
|
1090
|
+
throw new Error(`Bounce gate: this task has bounced ${currentBounce} times. ` +
|
|
1091
|
+
`Provide metadata.transition = { type: "bounce_back", reason: "..." } explaining the rework.`);
|
|
1092
|
+
}
|
|
1093
|
+
const bounceReason = transition.reason;
|
|
1094
|
+
if (typeof bounceReason !== 'string' || bounceReason.trim().length === 0) {
|
|
1095
|
+
throw new Error(`Bounce gate: metadata.transition.reason is required when re-opening a task that has bounced ${currentBounce} times.`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
// Merge bounce_count into metadata, preserving existing task metadata
|
|
1099
|
+
// (task.metadata has eta etc; updates.metadata may have transition or be empty)
|
|
1100
|
+
const existingMeta = {
|
|
1101
|
+
...(task.metadata || {}),
|
|
1102
|
+
...(updates.metadata || {}),
|
|
1103
|
+
};
|
|
1104
|
+
updates.metadata = {
|
|
1105
|
+
...existingMeta,
|
|
1106
|
+
bounce_count: newBounceCount,
|
|
1107
|
+
last_bounce_at: Date.now(),
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
989
1110
|
if (task.status === 'doing' && nextStatus === 'blocked') {
|
|
990
1111
|
const parsed = requireTransition('pause', ['reason'], 'doing->blocked transition');
|
|
991
1112
|
transitionEvent = {
|
|
@@ -1125,20 +1246,51 @@ class TaskManager {
|
|
|
1125
1246
|
}
|
|
1126
1247
|
return updated;
|
|
1127
1248
|
}
|
|
1128
|
-
async deleteTask(id) {
|
|
1249
|
+
async deleteTask(id, actor = 'system') {
|
|
1129
1250
|
const task = queryTask(id);
|
|
1130
1251
|
if (!task)
|
|
1131
1252
|
return false;
|
|
1132
|
-
|
|
1253
|
+
const deletedAt = Date.now();
|
|
1254
|
+
const event = {
|
|
1255
|
+
id: `thevt-${deletedAt}-${Math.random().toString(36).substr(2, 9)}`,
|
|
1256
|
+
taskId: id,
|
|
1257
|
+
type: 'deleted',
|
|
1258
|
+
actor,
|
|
1259
|
+
timestamp: deletedAt,
|
|
1260
|
+
data: {
|
|
1261
|
+
deletedAt,
|
|
1262
|
+
deletedBy: actor,
|
|
1263
|
+
previousStatus: task.status,
|
|
1264
|
+
title: task.title,
|
|
1265
|
+
},
|
|
1266
|
+
};
|
|
1267
|
+
// Make the live-row delete visible immediately, even if a caller forgets to await deleteTask().
|
|
1268
|
+
// Keep history/comments as audit.
|
|
1133
1269
|
try {
|
|
1134
1270
|
const db = getDb();
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1271
|
+
const insertHistory = db.prepare(`
|
|
1272
|
+
INSERT INTO task_history (id, task_id, type, actor, timestamp, data)
|
|
1273
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1274
|
+
`);
|
|
1275
|
+
const delTask = db.prepare('DELETE FROM tasks WHERE id = ?');
|
|
1276
|
+
const tx = db.transaction(() => {
|
|
1277
|
+
insertHistory.run(event.id, event.taskId, event.type, event.actor, event.timestamp, safeJsonStringify(event.data));
|
|
1278
|
+
delTask.run(id);
|
|
1279
|
+
});
|
|
1280
|
+
tx();
|
|
1138
1281
|
}
|
|
1139
1282
|
catch (err) {
|
|
1140
1283
|
console.error(`[Tasks] SQLite delete failed for ${id}:`, err);
|
|
1284
|
+
throw err;
|
|
1285
|
+
}
|
|
1286
|
+
try {
|
|
1287
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
1288
|
+
await fs.appendFile(TASK_HISTORY_FILE, `${JSON.stringify(event)}\n`, 'utf-8');
|
|
1289
|
+
}
|
|
1290
|
+
catch (err) {
|
|
1291
|
+
console.error('[Tasks] Failed to append delete history (JSONL):', err);
|
|
1141
1292
|
}
|
|
1293
|
+
await this.appendTaskTombstone(task, actor, deletedAt);
|
|
1142
1294
|
await this.syncTaskDeleteToCloud(id);
|
|
1143
1295
|
this.notifySubscribers(task, 'deleted');
|
|
1144
1296
|
return true;
|