atris 3.80.1 → 3.82.2
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/LICENSE +21 -0
- package/README.md +11 -1
- package/atris/atris.md +14 -0
- package/atris/features/README.md +15 -0
- package/atris/policies/README.md +24 -0
- package/atris/skills/README.md +61 -45
- package/atris/skills/design/SKILL.md +6 -5
- package/atris/skills/taste/SKILL.md +39 -0
- package/atris/skills/taste/notes.md +29 -0
- package/atris/skills/taste/principles.md +23 -0
- package/atris/skills/wiki/SKILL.md +14 -2
- package/atris/skills/wiki/index-lines.md +30 -0
- package/atris/skills/wiki/measure.md +20 -0
- package/atris/skills/wiki/note-shape.md +16 -0
- package/atris/skills/wiki/upkeep.md +16 -0
- package/atris/team/README.md +38 -0
- package/atris.md +14 -0
- package/bin/atris.js +1 -1
- package/commands/activate.js +1 -1
- package/commands/autoland.js +25 -26
- package/commands/autopilot-front.js +46 -1
- package/commands/autopilot.js +55 -25
- package/commands/clean.js +22 -11
- package/commands/computer.js +1 -1
- package/commands/doc-health.js +48 -1
- package/commands/improve.js +16 -3
- package/commands/init.js +4 -0
- package/commands/integrations.js +69 -1616
- package/commands/mission.js +112 -14
- package/commands/release.js +41 -10
- package/commands/status.js +1 -1
- package/commands/sync.js +12 -8
- package/commands/task.js +150 -137
- package/commands/unknowns.js +9 -5
- package/commands/visualize.js +1 -12
- package/commands/workflow.js +4 -3
- package/commands/worktree.js +107 -1610
- package/commands/xp.js +23 -2113
- package/commands/youtube.js +192 -3110
- package/lib/business-core.js +3 -4
- package/lib/campaign-run-lock.js +12 -1
- package/lib/engine-ask.js +1 -1
- package/lib/engine-receipt-sweep.js +64 -0
- package/lib/file-lock.js +175 -0
- package/lib/first-minute.js +37 -4
- package/lib/fleet.js +1 -1
- package/lib/integrations-core.js +1695 -0
- package/lib/intents.js +7 -0
- package/lib/json-file.js +22 -1
- package/lib/loop-doctor.js +36 -2
- package/lib/map-refs.js +68 -8
- package/lib/member-context.js +20 -1
- package/lib/member-engine.js +1 -1
- package/lib/member-map-fold.js +49 -11
- package/lib/member-map-inbox.js +2 -1
- package/lib/mission-core.js +9 -2
- package/lib/next-work-context.js +190 -0
- package/lib/recall-exam.js +171 -0
- package/lib/roster-markdown.js +2 -2
- package/lib/runner-command.js +5 -5
- package/lib/task-db.js +107 -38
- package/lib/taste-notes.js +35 -0
- package/lib/team-lineup.js +3 -3
- package/lib/todo.js +2 -0
- package/lib/wiki.js +11 -3
- package/lib/worktree-core.js +2032 -0
- package/lib/xp-core.js +2172 -0
- package/lib/youtube-core.js +3178 -0
- package/lib/zen-apply.js +3 -1
- package/package.json +2 -2
package/lib/business-core.js
CHANGED
|
@@ -900,10 +900,9 @@ function seedBusinessStarterTask(cwd, todoPath, starterAction) {
|
|
|
900
900
|
verify: 'atris business record atris/reports/<recap>.md --outcome mixed --metric "operator speed"',
|
|
901
901
|
},
|
|
902
902
|
});
|
|
903
|
-
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
fs.writeFileSync(projectionPath, `${JSON.stringify(projection, null, 2)}\n`, 'utf8');
|
|
903
|
+
// The one projection writer (same shape as `atris task`, temp file and rename).
|
|
904
|
+
const { writeDefaultProjection } = require('../commands/task');
|
|
905
|
+
const projectionPath = writeDefaultProjection(taskDb, db, { root: cwd }).outPath;
|
|
907
906
|
return { ok: true, inserted: Boolean(result.inserted), taskId: result.id, projectionPath };
|
|
908
907
|
} catch (error) {
|
|
909
908
|
return { ok: false, error: error && error.message ? error.message : String(error) };
|
package/lib/campaign-run-lock.js
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { stealStaleLock } = require('./file-lock');
|
|
13
|
+
|
|
14
|
+
// A lock is created empty and its holder record lands a moment later, so an
|
|
15
|
+
// unreadable lock this young is still someone's, not stale.
|
|
16
|
+
const UNREADABLE_GRACE_MS = 5000;
|
|
12
17
|
|
|
13
18
|
function lockPath(home, campaignId) {
|
|
14
19
|
return path.join(home, 'campaigns', campaignId, 'run.lock');
|
|
@@ -48,10 +53,16 @@ function acquireRunLock(home, campaignId, { pid = process.pid, host = os.hostnam
|
|
|
48
53
|
} catch (error) {
|
|
49
54
|
if (error.code !== 'EEXIST') throw error;
|
|
50
55
|
}
|
|
56
|
+
let judged;
|
|
57
|
+
try { judged = fs.statSync(file); } catch { continue; }
|
|
58
|
+
let raw = null;
|
|
59
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch {}
|
|
51
60
|
const holder = readHolder(file);
|
|
52
61
|
if (holder && holder.host === host && isAlive(holder.pid)) return { ok: false, holder };
|
|
53
62
|
if (holder && holder.host !== host) return { ok: false, holder };
|
|
54
|
-
|
|
63
|
+
if (!holder && Date.now() - judged.mtimeMs < UNREADABLE_GRACE_MS) return { ok: false, holder: null };
|
|
64
|
+
// Rename-then-verify, so a second waiter never deletes a fresh lock.
|
|
65
|
+
stealStaleLock(file, judged, raw);
|
|
55
66
|
}
|
|
56
67
|
return { ok: false, holder: readHolder(file) };
|
|
57
68
|
}
|
package/lib/engine-ask.js
CHANGED
|
@@ -246,7 +246,7 @@ function assertAskModelSupported(engine, model) {
|
|
|
246
246
|
function resolveAskModel(engine, modelName = '') {
|
|
247
247
|
const requested = String(modelName || '').trim();
|
|
248
248
|
if (requested) {
|
|
249
|
-
// A display name
|
|
249
|
+
// A spaced display name becomes an id only for the claude cli; every
|
|
250
250
|
// other engine gets its model name as typed.
|
|
251
251
|
return engine === 'claude' || engine === 'fable' || engine === 'haiku'
|
|
252
252
|
? claudeModelId(requested)
|
|
@@ -93,6 +93,70 @@ function sweepEngineAskReceipts(root, deps = {}) {
|
|
|
93
93
|
return summary;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Fleet and dispatch flights write a running receipt with the driver's pid
|
|
97
|
+
// and rewrite it when they finish. A crashed flight never rewrites it, so the
|
|
98
|
+
// receipt read as running forever. Flights run long, so the age bar is an hour
|
|
99
|
+
// and a live pid always keeps the receipt as it is.
|
|
100
|
+
const DISPATCH_RECEIPT_SCHEMA = 'atris.dispatch_receipt.v1';
|
|
101
|
+
const FLEET_GRACE_MS = 60 * 60 * 1000;
|
|
102
|
+
const FLEET_SWEEP_NOTE = 'stale receipt sweep marked this flight presumed dead: its process is gone and it never finished.';
|
|
103
|
+
|
|
104
|
+
function isFleetReceipt(name, receipt) {
|
|
105
|
+
if (receipt.schema === DISPATCH_RECEIPT_SCHEMA) return name.startsWith('dispatch-');
|
|
106
|
+
return !receipt.schema && name.startsWith('fleet-') && Array.isArray(receipt.results);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sweepFleetReceipts(root, deps = {}) {
|
|
110
|
+
const fsModule = deps.fs || fs;
|
|
111
|
+
const kill = deps.kill || process.kill;
|
|
112
|
+
const now = deps.now ? deps.now() : new Date();
|
|
113
|
+
const nowMs = now.getTime();
|
|
114
|
+
const graceMs = deps.graceMs === undefined ? FLEET_GRACE_MS : Number(deps.graceMs);
|
|
115
|
+
const summary = { scanned: 0, finalized: 0, skipped_alive: 0, skipped_young: 0 };
|
|
116
|
+
const runsDir = path.join(root, 'atris', 'runs');
|
|
117
|
+
let names;
|
|
118
|
+
try {
|
|
119
|
+
names = fsModule.readdirSync(runsDir)
|
|
120
|
+
.filter((name) => name.endsWith('.json') && (name.startsWith('fleet-') || name.startsWith('dispatch-')))
|
|
121
|
+
.sort();
|
|
122
|
+
} catch {
|
|
123
|
+
return summary;
|
|
124
|
+
}
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
const receiptPath = path.join(runsDir, name);
|
|
127
|
+
let receipt;
|
|
128
|
+
let stat;
|
|
129
|
+
try {
|
|
130
|
+
receipt = JSON.parse(fsModule.readFileSync(receiptPath, 'utf8'));
|
|
131
|
+
stat = fsModule.statSync(receiptPath);
|
|
132
|
+
} catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (!receipt || typeof receipt !== 'object' || receipt.status !== 'running' || !isFleetReceipt(name, receipt)) continue;
|
|
136
|
+
summary.scanned += 1;
|
|
137
|
+
// Age from the last write, not the start: a long flight rewrites its
|
|
138
|
+
// receipt as tasks land.
|
|
139
|
+
const lastWriteMs = Math.max(receiptStartedMs(receipt, stat), stat.mtimeMs);
|
|
140
|
+
if (nowMs - lastWriteMs < graceMs) {
|
|
141
|
+
summary.skipped_young += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (processIsAlive(Number(receipt.pid), kill)) {
|
|
145
|
+
summary.skipped_alive += 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
atomicWriteReceipt(receiptPath, {
|
|
149
|
+
...receipt,
|
|
150
|
+
status: 'presumed_dead',
|
|
151
|
+
swept_at: now.toISOString(),
|
|
152
|
+
note: FLEET_SWEEP_NOTE,
|
|
153
|
+
}, fsModule);
|
|
154
|
+
summary.finalized += 1;
|
|
155
|
+
}
|
|
156
|
+
return summary;
|
|
157
|
+
}
|
|
158
|
+
|
|
96
159
|
module.exports = {
|
|
97
160
|
sweepEngineAskReceipts,
|
|
161
|
+
sweepFleetReceipts,
|
|
98
162
|
};
|
package/lib/file-lock.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One exclusive lock file per resource, shared by the loops that must not run
|
|
4
|
+
// twice at once (autoland tick, autopilot, mission, campaign run).
|
|
5
|
+
//
|
|
6
|
+
// - Create is exclusive (`wx`), so two takers never both win.
|
|
7
|
+
// - The file records { pid, host, token, at }. Release removes it only while
|
|
8
|
+
// the token (or the open file) is still ours, so a tick that lost its lock
|
|
9
|
+
// to a stale-steal never deletes the next owner's lock.
|
|
10
|
+
// - A stale lock is stolen by renaming it to a unique tombstone first. If the
|
|
11
|
+
// renamed file turns out not to be the one judged stale (someone replaced
|
|
12
|
+
// it in between), it is linked back without clobbering. Two waiters that
|
|
13
|
+
// both judged the same lock stale can no longer delete each other's fresh
|
|
14
|
+
// lock.
|
|
15
|
+
|
|
16
|
+
const crypto = require('crypto');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
|
|
21
|
+
function pidAlive(pid) {
|
|
22
|
+
const n = Number(pid);
|
|
23
|
+
if (!Number.isInteger(n) || n <= 0) return false;
|
|
24
|
+
try {
|
|
25
|
+
process.kill(n, 0);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return Boolean(error) && error.code === 'EPERM';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readRaw(file) {
|
|
33
|
+
try { return fs.readFileSync(file, 'utf8'); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseHolder(raw) {
|
|
37
|
+
if (raw == null) return null;
|
|
38
|
+
try {
|
|
39
|
+
const value = JSON.parse(raw);
|
|
40
|
+
return value && typeof value === 'object' ? value : null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sameFile(a, b) {
|
|
47
|
+
return Boolean(a && b) && a.ino === b.ino && a.dev === b.dev;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Remove `file` only if it is still the file described by `judged` (stat) and
|
|
51
|
+
// `judgedRaw` (contents). Returns true when the caller should retry create.
|
|
52
|
+
function stealStaleLock(file, judged, judgedRaw) {
|
|
53
|
+
const tomb = `${file}.stale-${process.pid}-${crypto.randomBytes(4).toString('hex')}`;
|
|
54
|
+
try {
|
|
55
|
+
fs.renameSync(file, tomb);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (error && error.code === 'ENOENT') return true;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
let moved = null;
|
|
61
|
+
try { moved = fs.statSync(tomb); } catch {}
|
|
62
|
+
const movedRaw = readRaw(tomb);
|
|
63
|
+
if (!sameFile(moved, judged) || (judgedRaw != null && movedRaw !== judgedRaw)) {
|
|
64
|
+
// We grabbed a lock that was replaced after we judged it. Put it back
|
|
65
|
+
// without overwriting anything created since.
|
|
66
|
+
try { fs.linkSync(tomb, file); } catch {}
|
|
67
|
+
}
|
|
68
|
+
try { fs.unlinkSync(tomb); } catch {}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Decide whether a lock someone else holds may be taken over.
|
|
73
|
+
function judgeHolder(holder, ageMs, { host, isAlive, staleAfterMs, unreadableGraceMs }) {
|
|
74
|
+
if (!holder) return ageMs > unreadableGraceMs ? 'stale' : 'held';
|
|
75
|
+
const holderPid = Number(holder.pid);
|
|
76
|
+
const atMs = typeof holder.at === 'number' ? holder.at : Date.parse(holder.at || holder.started_at || '');
|
|
77
|
+
const heldFor = Number.isFinite(atMs) ? Date.now() - atMs : ageMs;
|
|
78
|
+
if (Number.isFinite(staleAfterMs) && heldFor > staleAfterMs) return 'stale';
|
|
79
|
+
if (holder.host && holder.host !== host) return 'held';
|
|
80
|
+
if (!Number.isInteger(holderPid) || holderPid <= 0) return ageMs > unreadableGraceMs ? 'stale' : 'held';
|
|
81
|
+
return isAlive(holderPid) ? 'held' : 'stale';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Returns { ok: true, file, token, fd?, release() } or { ok: false, file, holder }.
|
|
85
|
+
// Options:
|
|
86
|
+
// meta extra fields written into the lock record
|
|
87
|
+
// pid, host identity written into the record (defaults: this process)
|
|
88
|
+
// staleAfterMs a lock older than this is stale even with a live pid
|
|
89
|
+
// unreadableGraceMs an empty/unreadable lock younger than this is held
|
|
90
|
+
// (its owner may still be writing the record)
|
|
91
|
+
// keepOpen return the open fd (release then checks the inode)
|
|
92
|
+
// isAlive pid liveness probe (tests)
|
|
93
|
+
function acquireFileLock(file, options = {}) {
|
|
94
|
+
const {
|
|
95
|
+
meta = {},
|
|
96
|
+
pid = process.pid,
|
|
97
|
+
host = os.hostname(),
|
|
98
|
+
staleAfterMs = Infinity,
|
|
99
|
+
unreadableGraceMs = 5000,
|
|
100
|
+
keepOpen = false,
|
|
101
|
+
isAlive = pidAlive,
|
|
102
|
+
attempts = 5,
|
|
103
|
+
} = options;
|
|
104
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
105
|
+
const token = crypto.randomBytes(8).toString('hex');
|
|
106
|
+
const record = JSON.stringify({ ...meta, pid, host, token, at: new Date().toISOString() });
|
|
107
|
+
let lastHolder = null;
|
|
108
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
109
|
+
let fd;
|
|
110
|
+
try {
|
|
111
|
+
fd = fs.openSync(file, 'wx');
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
114
|
+
}
|
|
115
|
+
if (fd !== undefined) {
|
|
116
|
+
try {
|
|
117
|
+
fs.writeSync(fd, record);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
try { fs.closeSync(fd); } catch {}
|
|
120
|
+
try { fs.unlinkSync(file); } catch {}
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
let ino = null;
|
|
124
|
+
try { ino = fs.fstatSync(fd); } catch {}
|
|
125
|
+
if (!keepOpen) {
|
|
126
|
+
try { fs.closeSync(fd); } catch {}
|
|
127
|
+
fd = undefined;
|
|
128
|
+
}
|
|
129
|
+
const lock = { ok: true, file, token, fd, stat: ino, record };
|
|
130
|
+
lock.release = () => releaseFileLock(lock);
|
|
131
|
+
return lock;
|
|
132
|
+
}
|
|
133
|
+
let judged;
|
|
134
|
+
try {
|
|
135
|
+
judged = fs.statSync(file);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error && error.code === 'ENOENT') continue;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const raw = readRaw(file);
|
|
141
|
+
const holder = parseHolder(raw);
|
|
142
|
+
lastHolder = holder;
|
|
143
|
+
const verdict = judgeHolder(holder, Date.now() - judged.mtimeMs, { host, isAlive, staleAfterMs, unreadableGraceMs });
|
|
144
|
+
if (verdict === 'held') return { ok: false, file, holder };
|
|
145
|
+
stealStaleLock(file, judged, raw);
|
|
146
|
+
}
|
|
147
|
+
return { ok: false, file, holder: lastHolder || parseHolder(readRaw(file)) };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Remove the lock only while it is still ours.
|
|
151
|
+
function releaseFileLock(lock) {
|
|
152
|
+
if (!lock || !lock.ok || lock.released) return false;
|
|
153
|
+
lock.released = true;
|
|
154
|
+
let ours = false;
|
|
155
|
+
if (lock.fd != null) {
|
|
156
|
+
try {
|
|
157
|
+
ours = sameFile(fs.fstatSync(lock.fd), fs.statSync(lock.file));
|
|
158
|
+
} catch {
|
|
159
|
+
ours = false;
|
|
160
|
+
}
|
|
161
|
+
try { fs.closeSync(lock.fd); } catch {}
|
|
162
|
+
} else {
|
|
163
|
+
const holder = parseHolder(readRaw(lock.file));
|
|
164
|
+
ours = Boolean(holder) && holder.token === lock.token;
|
|
165
|
+
}
|
|
166
|
+
if (!ours) return false;
|
|
167
|
+
try { fs.unlinkSync(lock.file); } catch { return false; }
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
acquireFileLock,
|
|
173
|
+
stealStaleLock,
|
|
174
|
+
pidAlive,
|
|
175
|
+
};
|
package/lib/first-minute.js
CHANGED
|
@@ -8,6 +8,7 @@ const { spawnSync } = require('child_process');
|
|
|
8
8
|
const { isGenericScratchRoot } = require('./scratch-root');
|
|
9
9
|
const { resolveDefaultVerifier } = require('./default-verifier');
|
|
10
10
|
const { nonFalsifyingIssue } = require('./accept-verify-gate');
|
|
11
|
+
const { reconcileTaskRows } = require('./next-work-context');
|
|
11
12
|
|
|
12
13
|
const FIRST_USE_COMMAND = 'atris "help me choose the first useful step for this project"';
|
|
13
14
|
const FIRST_TALK_ASK = 'what do you want here';
|
|
@@ -176,8 +177,10 @@ function titlesFromTodoSection(root, section) {
|
|
|
176
177
|
}
|
|
177
178
|
}
|
|
178
179
|
|
|
179
|
-
function
|
|
180
|
+
function loadLocalTaskState(root = process.cwd(), { taskIds = [] } = {}) {
|
|
180
181
|
let dbRows = [];
|
|
182
|
+
let lookup = () => null;
|
|
183
|
+
let databaseRead = false;
|
|
181
184
|
try {
|
|
182
185
|
const taskDb = require('./task-db');
|
|
183
186
|
// Reading never creates the db, so a first look writes nothing.
|
|
@@ -185,20 +188,49 @@ function loadLocalTasks(root = process.cwd()) {
|
|
|
185
188
|
const db = taskDb.open();
|
|
186
189
|
const workspaceRoot = taskDb.workspaceRoot(root);
|
|
187
190
|
const rows = taskDb.listTasks(db, { workspaceRoot, limit: 200 });
|
|
191
|
+
// Busy queues must not crowd the last outcomes out of the handoff.
|
|
192
|
+
const recent = ['done', 'failed'].flatMap((status) => taskDb.listTasks(db, { workspaceRoot, status, limit: 3, orderBy: 'updated_at' }));
|
|
193
|
+
const seen = new Set(rows.map((row) => row.id));
|
|
194
|
+
rows.push(...recent.filter((row) => !seen.has(row.id)));
|
|
188
195
|
if (Array.isArray(rows) && rows.length) dbRows = taskDb.withTaskDisplayRefs(rows);
|
|
196
|
+
databaseRead = true;
|
|
197
|
+
lookup = (id) => {
|
|
198
|
+
const row = taskDb.getTask(db, id);
|
|
199
|
+
// A foreign id in a damaged projection is not this workspace's work.
|
|
200
|
+
if (row && !taskDb.rowInWorkspace(row, workspaceRoot)) return false;
|
|
201
|
+
return row ? taskDb.withTaskDisplayRefs([row])[0] : null;
|
|
202
|
+
};
|
|
203
|
+
// A shortlist is an output budget, not authority to invent a bound job.
|
|
204
|
+
for (const id of taskIds.slice(0, 8)) {
|
|
205
|
+
if (dbRows.some((row) => row.id === id)) continue;
|
|
206
|
+
const row = lookup(id);
|
|
207
|
+
if (row) dbRows.push(row);
|
|
208
|
+
}
|
|
189
209
|
} catch {
|
|
190
210
|
// Fall through to the local projection. Tests and fresh folders often have no db.
|
|
191
211
|
}
|
|
192
|
-
if (dbRows.some(isActionableTask)) return dbRows;
|
|
212
|
+
if (dbRows.some(isActionableTask)) return { tasks: dbRows, source: 'database' };
|
|
193
213
|
const projectionPath = path.join(root, '.atris', 'state', 'tasks.projection.json');
|
|
194
214
|
try {
|
|
195
215
|
const parsed = JSON.parse(fs.readFileSync(projectionPath, 'utf8'));
|
|
216
|
+
if (parsed.workspace_root && path.resolve(parsed.workspace_root) !== path.resolve(require('./task-db').workspaceRoot(root))) {
|
|
217
|
+
return { tasks: dbRows, source: databaseRead ? 'database' : 'none' };
|
|
218
|
+
}
|
|
196
219
|
const projected = Array.isArray(parsed.tasks) ? parsed.tasks : [];
|
|
197
|
-
if (projected.some(isActionableTask) || !dbRows.length)
|
|
220
|
+
if (projected.some(isActionableTask) || !dbRows.length) {
|
|
221
|
+
const tasks = reconcileTaskRows(dbRows, projected, lookup);
|
|
222
|
+
const known = new Set(dbRows.map((row) => row.id));
|
|
223
|
+
const unknown = tasks.some((row) => !row.id || (!known.has(row.id) && !lookup(row.id)));
|
|
224
|
+
return { tasks, source: unknown || !databaseRead ? (dbRows.length ? 'mixed' : 'projection') : 'database', generated_at: parsed.generated_at || null };
|
|
225
|
+
}
|
|
198
226
|
} catch {
|
|
199
227
|
// Keep any done-only db rows below.
|
|
200
228
|
}
|
|
201
|
-
return dbRows;
|
|
229
|
+
return { tasks: dbRows, source: databaseRead ? 'database' : 'none' };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function loadLocalTasks(root = process.cwd()) {
|
|
233
|
+
return loadLocalTaskState(root).tasks;
|
|
202
234
|
}
|
|
203
235
|
|
|
204
236
|
function loadLatestRecap(root = process.cwd()) {
|
|
@@ -1170,6 +1202,7 @@ module.exports = {
|
|
|
1170
1202
|
spokenBuiltList,
|
|
1171
1203
|
taskStartedAt,
|
|
1172
1204
|
loadLocalTasks,
|
|
1205
|
+
loadLocalTaskState,
|
|
1173
1206
|
firstTalkBuildCommand,
|
|
1174
1207
|
renderWhatsNextAsk,
|
|
1175
1208
|
isLeftoverVerbLook,
|
package/lib/fleet.js
CHANGED
|
@@ -179,7 +179,7 @@ function buildFleetPrompt(task, { worktreePath, yolo = false } = {}) {
|
|
|
179
179
|
lines.push(
|
|
180
180
|
'Rules:',
|
|
181
181
|
mapNotesPath(worktreePath || process.cwd())
|
|
182
|
-
? '- Read atris/MAP.md first to locate the code, then atris/refs/MAP-NOTES.md for exact file:line refs; never guess file locations.'
|
|
182
|
+
? '- Read atris/MAP.md first to locate the code, then atris/refs/MAP-NOTES.md (or the area note it points to) for exact file:line refs; never guess file locations.'
|
|
183
183
|
: '- Read atris/MAP.md first to locate the code; never guess file locations.',
|
|
184
184
|
...SHARED_BRIEF_RULES,
|
|
185
185
|
'- Run git status first. Stage ONLY files you changed. Never revert or touch files another agent modified.',
|