atris 3.14.0 → 3.15.11
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/AGENTS.md +24 -4
- package/README.md +4 -3
- package/atris/atris.md +38 -13
- package/atris/features/company-brain-sync/build.md +140 -0
- package/atris/features/company-brain-sync/idea.md +52 -0
- package/atris/features/company-brain-sync/validate.md +229 -0
- package/atris/skills/imessage/SKILL.md +44 -0
- package/bin/atris.js +56 -6
- package/commands/aeo.js +197 -0
- package/commands/align.js +1 -1
- package/commands/brain.js +840 -0
- package/commands/business-sync.js +716 -0
- package/commands/init.js +15 -3
- package/commands/integrations.js +128 -0
- package/commands/live.js +311 -0
- package/commands/now.js +263 -0
- package/commands/pull.js +121 -6
- package/commands/push.js +146 -48
- package/commands/task.js +1658 -18
- package/lib/company-brain-sync.js +178 -0
- package/lib/manifest.js +2 -1
- package/lib/task-db.js +271 -4
- package/package.json +12 -2
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function scopedSyncPath(rootDir, prefix, cloudPath) {
|
|
5
|
+
const rel = String(cloudPath || '').replace(/^\/+/, '');
|
|
6
|
+
const target = path.resolve(rootDir, prefix, rel);
|
|
7
|
+
const base = path.resolve(rootDir, prefix);
|
|
8
|
+
if (!target.startsWith(`${base}${path.sep}`) && target !== base) {
|
|
9
|
+
throw new Error(`Refusing to write outside ${prefix}: ${cloudPath}`);
|
|
10
|
+
}
|
|
11
|
+
return target;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function classifyPath({ path, base, local, remote }) {
|
|
15
|
+
const inBase = Boolean(base);
|
|
16
|
+
const inLocal = Boolean(local);
|
|
17
|
+
const inRemote = Boolean(remote);
|
|
18
|
+
const baseHash = base && base.hash;
|
|
19
|
+
const localHash = local && local.hash;
|
|
20
|
+
const remoteHash = remote && remote.hash;
|
|
21
|
+
|
|
22
|
+
if (inLocal && inRemote && inBase) {
|
|
23
|
+
const localChanged = localHash !== baseHash;
|
|
24
|
+
const remoteChanged = remoteHash !== baseHash;
|
|
25
|
+
if (!localChanged && !remoteChanged) return { path, status: 'unchanged', action: 'none' };
|
|
26
|
+
if (localChanged && !remoteChanged) return { path, status: 'local_updated', action: 'push' };
|
|
27
|
+
if (!localChanged && remoteChanged) return { path, status: 'remote_updated', action: 'pull' };
|
|
28
|
+
if (localHash === remoteHash) return { path, status: 'converged', action: 'record' };
|
|
29
|
+
return { path, status: 'conflict_updated', action: 'review' };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!inBase && inLocal && !inRemote) return { path, status: 'local_created', action: 'push' };
|
|
33
|
+
if (!inBase && !inLocal && inRemote) return { path, status: 'remote_created', action: 'pull' };
|
|
34
|
+
if (!inBase && inLocal && inRemote) {
|
|
35
|
+
if (localHash === remoteHash) return { path, status: 'converged', action: 'record' };
|
|
36
|
+
return { path, status: 'conflict_created', action: 'review' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (inBase && !inLocal && inRemote) {
|
|
40
|
+
if (remoteHash === baseHash) return { path, status: 'local_deleted', action: 'hold_delete' };
|
|
41
|
+
return { path, status: 'conflict_local_deleted_remote_updated', action: 'review' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (inBase && inLocal && !inRemote) {
|
|
45
|
+
if (localHash === baseHash) return { path, status: 'remote_deleted', action: 'pull_delete' };
|
|
46
|
+
return { path, status: 'conflict_remote_deleted_local_updated', action: 'review' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (inBase && !inLocal && !inRemote) return { path, status: 'deleted_both', action: 'record' };
|
|
50
|
+
|
|
51
|
+
return { path, status: 'unknown', action: 'review' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function classifyBrainSync({ baseFiles = {}, localFiles = {}, remoteFiles = {}, scopePrefixes = ['/atris/'] } = {}) {
|
|
55
|
+
const inScope = (p) => !scopePrefixes || scopePrefixes.length === 0 || scopePrefixes.some((prefix) => p.startsWith(prefix));
|
|
56
|
+
const paths = new Set([
|
|
57
|
+
...Object.keys(baseFiles),
|
|
58
|
+
...Object.keys(localFiles),
|
|
59
|
+
...Object.keys(remoteFiles),
|
|
60
|
+
].filter(inScope));
|
|
61
|
+
|
|
62
|
+
const changes = [...paths].sort().map((p) => classifyPath({
|
|
63
|
+
path: p,
|
|
64
|
+
base: baseFiles[p],
|
|
65
|
+
local: localFiles[p],
|
|
66
|
+
remote: remoteFiles[p],
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const summary = {
|
|
70
|
+
push: changes.filter((c) => c.action === 'push').length,
|
|
71
|
+
pull: changes.filter((c) => c.action === 'pull' || c.action === 'pull_delete').length,
|
|
72
|
+
review: changes.filter((c) => c.action === 'review').length,
|
|
73
|
+
holdDelete: changes.filter((c) => c.action === 'hold_delete').length,
|
|
74
|
+
unchanged: changes.filter((c) => c.action === 'none' || c.action === 'record').length,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return { changes, summary };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renderSyncSummary(plan) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push('Company brain sync');
|
|
83
|
+
lines.push(` push: ${plan.summary.push}`);
|
|
84
|
+
lines.push(` pull: ${plan.summary.pull}`);
|
|
85
|
+
lines.push(` review: ${plan.summary.review}`);
|
|
86
|
+
lines.push(` held deletes: ${plan.summary.holdDelete}`);
|
|
87
|
+
if (plan.summary.review > 0) {
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push('Needs review:');
|
|
90
|
+
for (const change of plan.changes.filter((c) => c.action === 'review')) {
|
|
91
|
+
lines.push(` - ${change.path.replace(/^\//, '')} (${change.status})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `${lines.join('\n')}\n`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildConflictReviewPacket({ plan, baseContents = {}, localContents = {}, remoteContents = {}, timestamp = 'sync-review' } = {}) {
|
|
98
|
+
const conflicts = (plan && plan.changes ? plan.changes : []).filter((change) => change.action === 'review');
|
|
99
|
+
const files = {};
|
|
100
|
+
const summary = [];
|
|
101
|
+
|
|
102
|
+
summary.push('# Company Brain Sync Review');
|
|
103
|
+
summary.push('');
|
|
104
|
+
summary.push(`Created: ${timestamp}`);
|
|
105
|
+
summary.push('');
|
|
106
|
+
summary.push(`${conflicts.length} file${conflicts.length === 1 ? '' : 's'} need review before publishing.`);
|
|
107
|
+
summary.push('');
|
|
108
|
+
|
|
109
|
+
for (const change of conflicts) {
|
|
110
|
+
const rel = change.path.replace(/^\/+/, '');
|
|
111
|
+
const packetBase = `.atris/sync/conflicts/${timestamp}/${rel}`;
|
|
112
|
+
const basePath = `${packetBase}.base`;
|
|
113
|
+
const localPath = `${packetBase}.local`;
|
|
114
|
+
const remotePath = `${packetBase}.remote`;
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(baseContents, change.path)) {
|
|
116
|
+
files[basePath] = baseContents[change.path] || '';
|
|
117
|
+
}
|
|
118
|
+
files[localPath] = localContents[change.path] || '';
|
|
119
|
+
files[remotePath] = remoteContents[change.path] || '';
|
|
120
|
+
summary.push(`- ${rel} (${change.status})`);
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(baseContents, change.path)) {
|
|
122
|
+
summary.push(` - base: ${basePath}`);
|
|
123
|
+
}
|
|
124
|
+
summary.push(` - local: ${localPath}`);
|
|
125
|
+
summary.push(` - remote: ${remotePath}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
files[`.atris/sync/conflicts/${timestamp}/summary.md`] = `${summary.join('\n')}\n`;
|
|
129
|
+
return { files, conflicts };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeConflictReviewPacket(rootDir, packet) {
|
|
133
|
+
const written = [];
|
|
134
|
+
for (const [relPath, content] of Object.entries((packet && packet.files) || {})) {
|
|
135
|
+
const fullPath = path.join(rootDir, relPath);
|
|
136
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
137
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
138
|
+
written.push(fullPath);
|
|
139
|
+
}
|
|
140
|
+
return written;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function readBaseContent(rootDir, cloudPath) {
|
|
144
|
+
try {
|
|
145
|
+
return fs.readFileSync(scopedSyncPath(rootDir, path.join('.atris', 'sync', 'base'), cloudPath), 'utf8');
|
|
146
|
+
} catch {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function writeBaseContents(rootDir, contents = {}) {
|
|
152
|
+
const written = [];
|
|
153
|
+
for (const [cloudPath, content] of Object.entries(contents)) {
|
|
154
|
+
if (typeof content !== 'string') continue;
|
|
155
|
+
const fullPath = scopedSyncPath(rootDir, path.join('.atris', 'sync', 'base'), cloudPath);
|
|
156
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
157
|
+
fs.writeFileSync(fullPath, content, 'utf8');
|
|
158
|
+
written.push(fullPath);
|
|
159
|
+
}
|
|
160
|
+
return written;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function removeBaseContents(rootDir, cloudPaths = []) {
|
|
164
|
+
for (const cloudPath of cloudPaths) {
|
|
165
|
+
fs.rmSync(scopedSyncPath(rootDir, path.join('.atris', 'sync', 'base'), cloudPath), { force: true });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = {
|
|
170
|
+
buildConflictReviewPacket,
|
|
171
|
+
classifyBrainSync,
|
|
172
|
+
classifyPath,
|
|
173
|
+
readBaseContent,
|
|
174
|
+
renderSyncSummary,
|
|
175
|
+
removeBaseContents,
|
|
176
|
+
writeBaseContents,
|
|
177
|
+
writeConflictReviewPacket,
|
|
178
|
+
};
|
package/lib/manifest.js
CHANGED
|
@@ -47,10 +47,11 @@ function computeFileHash(content) {
|
|
|
47
47
|
* Build a manifest object from a set of files.
|
|
48
48
|
* files: { [path]: { hash, size } }
|
|
49
49
|
*/
|
|
50
|
-
function buildManifest(files, commitHash) {
|
|
50
|
+
function buildManifest(files, commitHash, metadata = {}) {
|
|
51
51
|
return {
|
|
52
52
|
last_sync: new Date().toISOString(),
|
|
53
53
|
last_commit: commitHash || null,
|
|
54
|
+
workspace_root: metadata.workspaceRoot || null,
|
|
54
55
|
files,
|
|
55
56
|
};
|
|
56
57
|
}
|
package/lib/task-db.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SQLite-backed task store. node:sqlite (built-in, v22+).
|
|
2
|
-
// Local state layer for `atris task`. TODO.md
|
|
3
|
-
//
|
|
2
|
+
// Local state layer for `atris task`. TODO.md is a regenerated human-readable
|
|
3
|
+
// view; this store gives agents atomic claims plus an append-only event trail.
|
|
4
4
|
//
|
|
5
5
|
// Path: ~/.atris/tasks.db (gitignored, never blobbed). Per-workspace scope via
|
|
6
6
|
// workspace_root column. Rows survive across machines only when explicitly
|
|
@@ -29,6 +29,7 @@ const crypto = require('crypto');
|
|
|
29
29
|
const { DatabaseSync } = require('node:sqlite');
|
|
30
30
|
|
|
31
31
|
const DEFAULT_DB_PATH = path.join(os.homedir(), '.atris', 'tasks.db');
|
|
32
|
+
const TASK_EPISODES_FILE = path.join('.atris', 'state', 'task_episodes.jsonl');
|
|
32
33
|
|
|
33
34
|
const SCHEMA = `
|
|
34
35
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
@@ -45,9 +46,21 @@ CREATE TABLE IF NOT EXISTS tasks (
|
|
|
45
46
|
done_at INTEGER,
|
|
46
47
|
metadata TEXT
|
|
47
48
|
);
|
|
49
|
+
CREATE TABLE IF NOT EXISTS task_events (
|
|
50
|
+
event_id TEXT PRIMARY KEY,
|
|
51
|
+
task_id TEXT NOT NULL,
|
|
52
|
+
version INTEGER NOT NULL,
|
|
53
|
+
workspace_root TEXT NOT NULL,
|
|
54
|
+
actor TEXT,
|
|
55
|
+
event_type TEXT NOT NULL,
|
|
56
|
+
payload TEXT,
|
|
57
|
+
created_at INTEGER NOT NULL
|
|
58
|
+
);
|
|
48
59
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
49
60
|
CREATE INDEX IF NOT EXISTS idx_tasks_workspace ON tasks(workspace_root);
|
|
50
61
|
CREATE INDEX IF NOT EXISTS idx_tasks_claimed_by ON tasks(claimed_by);
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_task_events_task ON task_events(task_id, version);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_task_events_ws ON task_events(workspace_root, created_at);
|
|
51
64
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_tasks_source ON tasks(workspace_root, source_key)
|
|
52
65
|
WHERE source_key IS NOT NULL;
|
|
53
66
|
`;
|
|
@@ -74,9 +87,9 @@ function open(dbPath) {
|
|
|
74
87
|
withBusyRetry(() => db.exec('PRAGMA busy_timeout = 30000'));
|
|
75
88
|
withBusyRetry(() => db.exec('PRAGMA foreign_keys = ON'));
|
|
76
89
|
withBusyRetry(() => db.exec(SCHEMA));
|
|
77
|
-
// Schema version. Bump at every additive migration
|
|
90
|
+
// Schema version. Bump at every additive migration.
|
|
78
91
|
// Future migrations read this and apply diffs idempotently.
|
|
79
|
-
withBusyRetry(() => db.exec('PRAGMA user_version =
|
|
92
|
+
withBusyRetry(() => db.exec('PRAGMA user_version = 2'));
|
|
80
93
|
_cachedDb = db;
|
|
81
94
|
_cachedPath = target;
|
|
82
95
|
return db;
|
|
@@ -177,6 +190,19 @@ function addTask(db, { title, tag, workspaceRoot: ws, sourceKey: sk, metadata, s
|
|
|
177
190
|
now,
|
|
178
191
|
metadata ? JSON.stringify(metadata) : null,
|
|
179
192
|
));
|
|
193
|
+
appendTaskEvent(db, {
|
|
194
|
+
taskId: id,
|
|
195
|
+
workspaceRoot: ws,
|
|
196
|
+
actor: claimedBy || null,
|
|
197
|
+
eventType: taskStatus === 'claimed' ? 'claimed' : 'created',
|
|
198
|
+
payload: {
|
|
199
|
+
title: String(title).trim(),
|
|
200
|
+
tag: tag || null,
|
|
201
|
+
status: taskStatus,
|
|
202
|
+
source_key: sk || null,
|
|
203
|
+
metadata: metadata || null,
|
|
204
|
+
},
|
|
205
|
+
});
|
|
180
206
|
return { id, inserted: true };
|
|
181
207
|
}
|
|
182
208
|
|
|
@@ -201,6 +227,17 @@ function listTasks(db, { workspaceRoot: ws, status, claimedBy, limit }) {
|
|
|
201
227
|
}));
|
|
202
228
|
}
|
|
203
229
|
|
|
230
|
+
function getTask(db, id) {
|
|
231
|
+
if (!id) throw new Error('id required');
|
|
232
|
+
const row = db.prepare(`
|
|
233
|
+
SELECT id, title, status, tag, workspace_root, source_key, claimed_by, claimed_at, created_at, updated_at, done_at, metadata
|
|
234
|
+
FROM tasks
|
|
235
|
+
WHERE id = ?
|
|
236
|
+
`).get(id);
|
|
237
|
+
if (!row) return null;
|
|
238
|
+
return { ...row, metadata: row.metadata ? safeJSON(row.metadata) : null };
|
|
239
|
+
}
|
|
240
|
+
|
|
204
241
|
// Atomic claim. Returns { claimed: true, row } only if THIS call won the row.
|
|
205
242
|
// Race-safe via single UPDATE with WHERE status='open' guard. SQLite serializes
|
|
206
243
|
// writes; busy_timeout absorbs contention. Caller must check `.claimed`.
|
|
@@ -220,6 +257,13 @@ function claimTask(db, { id, claimedBy }) {
|
|
|
220
257
|
const result = withBusyRetry(() => stmt.run(claimedBy, now, now, id));
|
|
221
258
|
if (result.changes === 1) {
|
|
222
259
|
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
260
|
+
appendTaskEvent(db, {
|
|
261
|
+
taskId: id,
|
|
262
|
+
workspaceRoot: row.workspace_root,
|
|
263
|
+
actor: claimedBy,
|
|
264
|
+
eventType: 'claimed',
|
|
265
|
+
payload: { claimed_by: claimedBy },
|
|
266
|
+
});
|
|
223
267
|
return { claimed: true, row: { ...row, metadata: row.metadata ? safeJSON(row.metadata) : null } };
|
|
224
268
|
}
|
|
225
269
|
// Either id doesn't exist or status != 'open'. Tell the caller which.
|
|
@@ -239,9 +283,225 @@ function doneTask(db, { id, status }) {
|
|
|
239
283
|
WHERE id = ?
|
|
240
284
|
AND status IN ('open', 'claimed')
|
|
241
285
|
`).run(final, now, now, id));
|
|
286
|
+
if (result.changes === 1) {
|
|
287
|
+
const row = db.prepare('SELECT id, workspace_root FROM tasks WHERE id = ?').get(id);
|
|
288
|
+
appendTaskEvent(db, {
|
|
289
|
+
taskId: id,
|
|
290
|
+
workspaceRoot: row.workspace_root,
|
|
291
|
+
actor: process.env.ATRIS_AGENT_ID || process.env.USER || null,
|
|
292
|
+
eventType: final === 'done' ? 'completed' : 'blocked',
|
|
293
|
+
payload: { status: final },
|
|
294
|
+
});
|
|
295
|
+
}
|
|
242
296
|
return { updated: result.changes === 1 };
|
|
243
297
|
}
|
|
244
298
|
|
|
299
|
+
function appendTaskEvent(db, { taskId, workspaceRoot: ws, actor, eventType, payload }) {
|
|
300
|
+
if (!taskId) throw new Error('taskId required');
|
|
301
|
+
if (!ws) throw new Error('workspaceRoot required');
|
|
302
|
+
if (!eventType) throw new Error('eventType required');
|
|
303
|
+
const current = db.prepare('SELECT MAX(version) AS version FROM task_events WHERE task_id = ?').get(taskId);
|
|
304
|
+
const version = Number(current && current.version || 0) + 1;
|
|
305
|
+
const eventId = newId();
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
withBusyRetry(() => db.prepare(`
|
|
308
|
+
INSERT INTO task_events (event_id, task_id, version, workspace_root, actor, event_type, payload, created_at)
|
|
309
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
310
|
+
`).run(
|
|
311
|
+
eventId,
|
|
312
|
+
taskId,
|
|
313
|
+
version,
|
|
314
|
+
ws,
|
|
315
|
+
actor || null,
|
|
316
|
+
eventType,
|
|
317
|
+
payload ? JSON.stringify(payload) : null,
|
|
318
|
+
now,
|
|
319
|
+
));
|
|
320
|
+
return {
|
|
321
|
+
event_id: eventId,
|
|
322
|
+
task_id: taskId,
|
|
323
|
+
version,
|
|
324
|
+
workspace_root: ws,
|
|
325
|
+
actor: actor || null,
|
|
326
|
+
event_type: eventType,
|
|
327
|
+
payload: payload || null,
|
|
328
|
+
created_at: now,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function listTaskEvents(db, { taskId, workspaceRoot: ws, limit }) {
|
|
333
|
+
const where = [];
|
|
334
|
+
const args = [];
|
|
335
|
+
if (taskId) { where.push('task_id = ?'); args.push(taskId); }
|
|
336
|
+
if (ws) { where.push('workspace_root = ?'); args.push(ws); }
|
|
337
|
+
const sql = `
|
|
338
|
+
SELECT event_id, task_id, version, workspace_root, actor, event_type, payload, created_at
|
|
339
|
+
FROM task_events
|
|
340
|
+
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
|
341
|
+
ORDER BY created_at ASC, version ASC
|
|
342
|
+
${limit ? 'LIMIT ' + Number(limit) : ''}
|
|
343
|
+
`;
|
|
344
|
+
return db.prepare(sql).all(...args).map(r => ({
|
|
345
|
+
...r,
|
|
346
|
+
payload: r.payload ? safeJSON(r.payload) : null,
|
|
347
|
+
}));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function noteTask(db, { id, actor, content }) {
|
|
351
|
+
if (!id) throw new Error('id required');
|
|
352
|
+
const text = String(content || '').trim();
|
|
353
|
+
if (!text) throw new Error('content required');
|
|
354
|
+
const row = getTask(db, id);
|
|
355
|
+
if (!row) return { noted: false, reason: 'not_found' };
|
|
356
|
+
const event = appendTaskEvent(db, {
|
|
357
|
+
taskId: id,
|
|
358
|
+
workspaceRoot: row.workspace_root,
|
|
359
|
+
actor: actor || null,
|
|
360
|
+
eventType: 'message',
|
|
361
|
+
payload: { content: text },
|
|
362
|
+
});
|
|
363
|
+
return { noted: true, event };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof }) {
|
|
367
|
+
if (!id) throw new Error('id required');
|
|
368
|
+
const row = getTask(db, id);
|
|
369
|
+
if (!row) return { reviewed: false, reason: 'not_found' };
|
|
370
|
+
const numericReward = Number.isFinite(Number(reward)) ? Number(reward) : 0;
|
|
371
|
+
const payload = {
|
|
372
|
+
reward: numericReward,
|
|
373
|
+
lesson: String(lesson || '').trim(),
|
|
374
|
+
next_task: String(nextTask || '').trim() || null,
|
|
375
|
+
proof: String(proof || '').trim() || null,
|
|
376
|
+
};
|
|
377
|
+
const event = appendTaskEvent(db, {
|
|
378
|
+
taskId: id,
|
|
379
|
+
workspaceRoot: row.workspace_root,
|
|
380
|
+
actor: actor || null,
|
|
381
|
+
eventType: 'reviewed',
|
|
382
|
+
payload,
|
|
383
|
+
});
|
|
384
|
+
const episode = taskEpisodeFromReview(row, event, payload);
|
|
385
|
+
appendTaskEpisode(row.workspace_root, episode);
|
|
386
|
+
return { reviewed: true, event, episode };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function taskEpisodeFromReview(row, event, payload) {
|
|
390
|
+
return {
|
|
391
|
+
schema: 'atris.task_episode.v1',
|
|
392
|
+
episode_id: event.event_id,
|
|
393
|
+
task_id: row.id,
|
|
394
|
+
workspace_root: row.workspace_root,
|
|
395
|
+
created_at: new Date(event.created_at).toISOString(),
|
|
396
|
+
state: {
|
|
397
|
+
title: row.title,
|
|
398
|
+
status: row.status,
|
|
399
|
+
tag: row.tag,
|
|
400
|
+
claimed_by: row.claimed_by,
|
|
401
|
+
metadata: row.metadata || {},
|
|
402
|
+
},
|
|
403
|
+
action: {
|
|
404
|
+
event_type: 'reviewed',
|
|
405
|
+
actor: event.actor || null,
|
|
406
|
+
version: event.version,
|
|
407
|
+
},
|
|
408
|
+
reward: {
|
|
409
|
+
value: payload.reward,
|
|
410
|
+
source: 'task_review',
|
|
411
|
+
},
|
|
412
|
+
lesson: payload.lesson,
|
|
413
|
+
proof: payload.proof,
|
|
414
|
+
next_task_suggestion: payload.next_task,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function appendTaskEpisode(workspaceRoot, episode) {
|
|
419
|
+
const filePath = path.join(workspaceRoot, TASK_EPISODES_FILE);
|
|
420
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
421
|
+
fs.appendFileSync(filePath, JSON.stringify(episode) + '\n', 'utf8');
|
|
422
|
+
return filePath;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function taskProjection(db, { workspaceRoot: ws, taskId, limit = 500 } = {}) {
|
|
426
|
+
const rows = taskId
|
|
427
|
+
? [getTask(db, taskId)].filter(Boolean)
|
|
428
|
+
: listTasks(db, { workspaceRoot: ws || null, limit });
|
|
429
|
+
const events = listTaskEvents(db, {
|
|
430
|
+
taskId: taskId || null,
|
|
431
|
+
workspaceRoot: taskId ? null : (ws || null),
|
|
432
|
+
limit: limit * 20,
|
|
433
|
+
});
|
|
434
|
+
const byTask = new Map();
|
|
435
|
+
for (const e of events) {
|
|
436
|
+
if (!byTask.has(e.task_id)) byTask.set(e.task_id, []);
|
|
437
|
+
byTask.get(e.task_id).push(e);
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
schema: 'atris.task_projection.v1',
|
|
441
|
+
generated_at: new Date().toISOString(),
|
|
442
|
+
workspace_root: ws || (rows[0] && rows[0].workspace_root) || null,
|
|
443
|
+
tasks: rows.map(row => {
|
|
444
|
+
const taskEvents = byTask.get(row.id) || [];
|
|
445
|
+
const latest = taskEvents.length ? taskEvents[taskEvents.length - 1] : null;
|
|
446
|
+
const messages = taskEvents
|
|
447
|
+
.filter(e => e.event_type === 'message')
|
|
448
|
+
.map(e => ({
|
|
449
|
+
version: e.version,
|
|
450
|
+
actor: e.actor,
|
|
451
|
+
content: e.payload && e.payload.content || '',
|
|
452
|
+
created_at: e.created_at,
|
|
453
|
+
}));
|
|
454
|
+
return {
|
|
455
|
+
id: row.id,
|
|
456
|
+
title: row.title,
|
|
457
|
+
status: row.status,
|
|
458
|
+
tag: row.tag,
|
|
459
|
+
workspace_root: row.workspace_root,
|
|
460
|
+
claimed_by: row.claimed_by,
|
|
461
|
+
created_at: row.created_at,
|
|
462
|
+
updated_at: row.updated_at,
|
|
463
|
+
done_at: row.done_at,
|
|
464
|
+
metadata: row.metadata || {},
|
|
465
|
+
current_version: latest ? latest.version : 0,
|
|
466
|
+
latest_event_type: latest ? latest.event_type : null,
|
|
467
|
+
messages,
|
|
468
|
+
events: taskEvents,
|
|
469
|
+
};
|
|
470
|
+
}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function renderTodoMarkdown(rows, { title = 'TODO.md' } = {}) {
|
|
475
|
+
const buckets = {
|
|
476
|
+
open: rows.filter(r => r.status === 'open'),
|
|
477
|
+
claimed: rows.filter(r => r.status === 'claimed'),
|
|
478
|
+
failed: rows.filter(r => r.status === 'failed'),
|
|
479
|
+
done: rows.filter(r => r.status === 'done'),
|
|
480
|
+
};
|
|
481
|
+
const lines = [`# ${title}`, '', '> Regenerated from durable Atris task state. Do not treat this file as truth.', ''];
|
|
482
|
+
appendSection(lines, 'Backlog', buckets.open);
|
|
483
|
+
appendSection(lines, 'In Progress', buckets.claimed);
|
|
484
|
+
appendSection(lines, 'Blocked', buckets.failed);
|
|
485
|
+
appendSection(lines, 'Completed', buckets.done);
|
|
486
|
+
return lines.join('\n') + '\n';
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function appendSection(lines, name, rows) {
|
|
490
|
+
lines.push(`## ${name}`, '');
|
|
491
|
+
if (!rows.length) {
|
|
492
|
+
lines.push('(Empty)', '');
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
for (const row of rows) {
|
|
496
|
+
const tag = row.tag ? ` [${row.tag}]` : '';
|
|
497
|
+
lines.push(`- **[${row.id}]** ${row.title}${tag}`);
|
|
498
|
+
if (row.claimed_by && row.status === 'claimed') lines.push(` **Claimed by:** ${row.claimed_by}`);
|
|
499
|
+
const meta = row.metadata || {};
|
|
500
|
+
if (meta.verify) lines.push(` **Verify:** ${meta.verify}`);
|
|
501
|
+
}
|
|
502
|
+
lines.push('');
|
|
503
|
+
}
|
|
504
|
+
|
|
245
505
|
function safeJSON(s) {
|
|
246
506
|
try { return JSON.parse(s); } catch { return null; }
|
|
247
507
|
}
|
|
@@ -279,9 +539,16 @@ module.exports = {
|
|
|
279
539
|
sourceKey,
|
|
280
540
|
normalizeTitle,
|
|
281
541
|
addTask,
|
|
542
|
+
getTask,
|
|
282
543
|
listTasks,
|
|
283
544
|
claimTask,
|
|
284
545
|
doneTask,
|
|
546
|
+
noteTask,
|
|
547
|
+
reviewTask,
|
|
548
|
+
appendTaskEvent,
|
|
549
|
+
listTaskEvents,
|
|
550
|
+
taskProjection,
|
|
551
|
+
renderTodoMarkdown,
|
|
285
552
|
newId,
|
|
286
553
|
// Test surface
|
|
287
554
|
_SCHEMA: SCHEMA,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.15.11",
|
|
4
4
|
"description": "Atris — an operating system for intelligence. Integrates with any agent.",
|
|
5
5
|
"main": "bin/atris.js",
|
|
6
6
|
"bin": {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"atris/team/researcher/MEMBER.md",
|
|
30
30
|
"atris/team/_template/MEMBER.md",
|
|
31
31
|
"atris/features/_templates/",
|
|
32
|
+
"atris/features/company-brain-sync/",
|
|
32
33
|
"atris/policies/",
|
|
33
34
|
"atris/skills/"
|
|
34
35
|
],
|
|
@@ -56,5 +57,14 @@
|
|
|
56
57
|
"repository": {
|
|
57
58
|
"type": "git",
|
|
58
59
|
"url": "git+https://github.com/atrislabs/atris.git"
|
|
59
|
-
}
|
|
60
|
+
},
|
|
61
|
+
"directories": {
|
|
62
|
+
"lib": "lib",
|
|
63
|
+
"test": "test"
|
|
64
|
+
},
|
|
65
|
+
"type": "commonjs",
|
|
66
|
+
"bugs": {
|
|
67
|
+
"url": "https://github.com/atrislabs/atris/issues"
|
|
68
|
+
},
|
|
69
|
+
"homepage": "https://github.com/atrislabs/atris#readme"
|
|
60
70
|
}
|