atris 3.34.0 → 3.36.0
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 +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const HUMAN_BLOCKING_REASONS = new Set(['auth-required', 'model-unavailable', 'rate-limit-exceeded-wall']);
|
|
7
|
+
const OPEN_TASK_STATUSES = new Set(['open', 'claimed', 'review']);
|
|
8
|
+
|
|
9
|
+
const SWARLO_HUB = process.env.SWARLO_HUB_URL || process.env.SWARLO_HUB || 'http://localhost:8090';
|
|
10
|
+
const SWARLO_HUB_ID = process.env.SWARLO_HUB_ID || 'atris';
|
|
11
|
+
const SWARLO_FLEET_TIMEOUT_MS = 3000;
|
|
12
|
+
const SWARLO_ESCALATIONS_CHANNEL = 'escalations';
|
|
13
|
+
|
|
14
|
+
function loadSwarloApiKey() {
|
|
15
|
+
if (process.env.SWARLO_API_KEY) return process.env.SWARLO_API_KEY;
|
|
16
|
+
const keyDir = path.join(require('os').homedir(), '.swarlo');
|
|
17
|
+
let files;
|
|
18
|
+
try {
|
|
19
|
+
// Newest key first: the hub DB rotates and old keys 401, so mtime beats
|
|
20
|
+
// lexicographic order (live repro 2026-07-10: first-sorted key was dead,
|
|
21
|
+
// the most recently minted one was valid).
|
|
22
|
+
files = fs.readdirSync(keyDir)
|
|
23
|
+
.filter((name) => name.endsWith('.key'))
|
|
24
|
+
.map((name) => {
|
|
25
|
+
const full = path.join(keyDir, name);
|
|
26
|
+
let mtime = 0;
|
|
27
|
+
try { mtime = fs.statSync(full).mtimeMs; } catch (error) { /* skip */ }
|
|
28
|
+
return { full, mtime };
|
|
29
|
+
})
|
|
30
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (!files.length) return null;
|
|
35
|
+
const key = fs.readFileSync(files[0].full, 'utf8').trim();
|
|
36
|
+
return key || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Default synchronous HTTP transport: shells out to curl so this stays
|
|
40
|
+
// callable from handleMissionBlocker without turning it into an async fn
|
|
41
|
+
// (existing callers in commands/mission.js read the return value inline).
|
|
42
|
+
function curlPost(url, { headers, body, timeoutMs }) {
|
|
43
|
+
const { spawnSync } = require('child_process');
|
|
44
|
+
const args = ['-sS', '-o', '-', '-w', '\n%{http_code}', '--max-time', String(Math.ceil(timeoutMs / 1000)), '-X', 'POST'];
|
|
45
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
46
|
+
args.push('-H', `${key}: ${value}`);
|
|
47
|
+
}
|
|
48
|
+
args.push('-d', body, url);
|
|
49
|
+
const result = spawnSync('curl', args, { encoding: 'utf8' });
|
|
50
|
+
if (result.error || result.status !== 0) {
|
|
51
|
+
return { ok: false, status: 0, body: '' };
|
|
52
|
+
}
|
|
53
|
+
const output = result.stdout || '';
|
|
54
|
+
const lastNewline = output.lastIndexOf('\n');
|
|
55
|
+
const statusCode = Number(output.slice(lastNewline + 1).trim());
|
|
56
|
+
const responseBody = lastNewline >= 0 ? output.slice(0, lastNewline) : '';
|
|
57
|
+
return { ok: statusCode >= 200 && statusCode < 300, status: statusCode, body: responseBody };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function postBlockerToFleet({ task, blockerClass, stopReason, mission }, injected = {}) {
|
|
61
|
+
const loadKey = injected.loadSwarloApiKey || loadSwarloApiKey;
|
|
62
|
+
const httpPost = injected.httpPost || curlPost;
|
|
63
|
+
const channel = SWARLO_ESCALATIONS_CHANNEL;
|
|
64
|
+
try {
|
|
65
|
+
const apiKey = loadKey();
|
|
66
|
+
if (!apiKey) return { posted: false, channel };
|
|
67
|
+
const ref = taskRef(task);
|
|
68
|
+
const content = `Mission blocker filed with no local engine ready: ${ref} — ${String(stopReason || blockerClass).trim()}. Mission: ${String(mission?.objective || mission?.id || '').trim()}.`;
|
|
69
|
+
const response = httpPost(`${SWARLO_HUB}/api/${SWARLO_HUB_ID}/channels/${channel}/posts`, {
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
Authorization: `Bearer ${apiKey}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ content, kind: 'alert', task_key: ref }),
|
|
75
|
+
timeoutMs: SWARLO_FLEET_TIMEOUT_MS,
|
|
76
|
+
});
|
|
77
|
+
if (!response || !response.ok) return { posted: false, channel };
|
|
78
|
+
return { posted: true, channel };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { posted: false, channel };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function reasonClass(value) {
|
|
85
|
+
const reason = String(value || 'blocked').trim().toLowerCase();
|
|
86
|
+
return reason.replace(/\s+/g, '-').slice(0, 120) || 'blocked';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function clipPlainClause(value, max) {
|
|
90
|
+
const plain = String(value || '')
|
|
91
|
+
.replace(/["'“”‘’]/g, '')
|
|
92
|
+
.replace(/\s+/g, ' ')
|
|
93
|
+
.trim()
|
|
94
|
+
.replace(/[.!?]+$/, '');
|
|
95
|
+
if (plain.length <= max) return plain;
|
|
96
|
+
const clipped = plain.slice(0, max + 1);
|
|
97
|
+
const boundary = clipped.lastIndexOf(' ');
|
|
98
|
+
return clipped.slice(0, boundary > 0 ? boundary : max).trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shortObjectiveClause(value) {
|
|
102
|
+
const plain = String(value || '')
|
|
103
|
+
.replace(/["'“”‘’]/g, '')
|
|
104
|
+
.replace(/\s+/g, ' ')
|
|
105
|
+
.trim();
|
|
106
|
+
const boundary = plain.search(/:|[.!?](?=\s|$)/);
|
|
107
|
+
const clause = boundary > 0 ? plain.slice(0, boundary) : plain;
|
|
108
|
+
return clause.charAt(0).toLowerCase() + clause.slice(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function plainStopReason(value) {
|
|
112
|
+
const raw = String(value || 'blocked').trim().toLowerCase();
|
|
113
|
+
const repeated = /^repeated[-_ ]+error\s*[:/ -]*/.test(raw);
|
|
114
|
+
const plain = raw
|
|
115
|
+
.replace(/^repeated[-_ ]+error\s*[:/ -]*/, '')
|
|
116
|
+
.replace(/["'“”‘’]/g, '')
|
|
117
|
+
.replace(/[_:/-]+/g, ' ')
|
|
118
|
+
.replace(/\s+/g, ' ')
|
|
119
|
+
.trim() || 'blocked';
|
|
120
|
+
return repeated ? `${plain} repeatedly` : plain;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function missionBlockerTitle(objective, stopReason) {
|
|
124
|
+
const max = 80;
|
|
125
|
+
const fixedLength = 'get '.length + ' moving again: '.length;
|
|
126
|
+
const objectiveClause = shortObjectiveClause(objective) || 'mission';
|
|
127
|
+
const reasonClause = plainStopReason(stopReason);
|
|
128
|
+
const shortObjective = clipPlainClause(objectiveClause, max - fixedLength - 16);
|
|
129
|
+
const shortReason = clipPlainClause(reasonClause, max - fixedLength - shortObjective.length);
|
|
130
|
+
return `get ${shortObjective} moving again: ${shortReason}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function taskRef(task) {
|
|
134
|
+
return task?.display_id || task?.legacy_ref || task?.id || null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function defaultDependencies() {
|
|
138
|
+
const taskDb = require('./task-db');
|
|
139
|
+
return {
|
|
140
|
+
taskDb,
|
|
141
|
+
resolveEngineForRole: require('./engine-registry').resolveEngineForRole,
|
|
142
|
+
dispatchToEngine: require('./fleet').dispatchToEngine,
|
|
143
|
+
createAgentWorktree: require('../commands/worktree').createAgentWorktree,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function projectTasks(taskDb, db, workspaceRoot) {
|
|
148
|
+
if (typeof taskDb.taskProjection !== 'function') return;
|
|
149
|
+
const file = path.join(workspaceRoot, '.atris', 'state', 'tasks.projection.json');
|
|
150
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
151
|
+
fs.writeFileSync(file, `${JSON.stringify(taskDb.taskProjection(db, { workspaceRoot, limit: 500 }), null, 2)}\n`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function handleMissionBlocker({ mission, stopReason, workspaceRoot, appendEvent }, injected = {}) {
|
|
155
|
+
if (!mission || !mission.id) return { taskId: null, dispatched: false, engine: null, reason: 'missing mission' };
|
|
156
|
+
const blockerClass = reasonClass(stopReason || mission.stop_reason || mission.last_tick_reason);
|
|
157
|
+
if (HUMAN_BLOCKING_REASONS.has(blockerClass) || (Array.isArray(mission.human_asks) && mission.human_asks.length)) {
|
|
158
|
+
return { taskId: null, dispatched: false, engine: null, reason: 'human-blocking' };
|
|
159
|
+
}
|
|
160
|
+
if (['ready', 'complete'].includes(String(mission.status || '').toLowerCase())) {
|
|
161
|
+
return { taskId: null, dispatched: false, engine: null, reason: 'stop condition met' };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const deps = { ...defaultDependencies(), ...injected };
|
|
165
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
166
|
+
const db = deps.taskDb.open();
|
|
167
|
+
const scopedRoot = deps.taskDb.workspaceRoot ? deps.taskDb.workspaceRoot(root) : root;
|
|
168
|
+
const rows = deps.taskDb.withTaskDisplayRefs(deps.taskDb.listTasks(db, { workspaceRoot: scopedRoot }));
|
|
169
|
+
const matching = rows.filter((row) => row.metadata?.mission_id === mission.id
|
|
170
|
+
&& row.metadata?.mission_blocker_class === blockerClass);
|
|
171
|
+
const existing = matching.find((row) => OPEN_TASK_STATUSES.has(row.status));
|
|
172
|
+
let task = existing;
|
|
173
|
+
|
|
174
|
+
if (task) {
|
|
175
|
+
return { taskId: taskRef(task), dispatched: false, engine: null, reason: 'existing blocker task' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!task) {
|
|
179
|
+
const evidence = String(mission.receipt_path || mission.last_tick_reason || stopReason || blockerClass).trim();
|
|
180
|
+
const objective = String(mission.objective || mission.id).trim();
|
|
181
|
+
const fullStopReason = String(stopReason || blockerClass).trim();
|
|
182
|
+
const title = missionBlockerTitle(objective, fullStopReason);
|
|
183
|
+
const created = deps.taskDb.addTask(db, {
|
|
184
|
+
title,
|
|
185
|
+
tag: 'mission-blocker',
|
|
186
|
+
workspaceRoot: scopedRoot,
|
|
187
|
+
sourceKey: `mission-blocker:${mission.id}:${blockerClass}:${matching.length}`,
|
|
188
|
+
metadata: {
|
|
189
|
+
mission_id: mission.id,
|
|
190
|
+
mission_objective: objective,
|
|
191
|
+
mission_blocker_class: blockerClass,
|
|
192
|
+
stop_reason: fullStopReason,
|
|
193
|
+
evidence,
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
const refreshed = deps.taskDb.withTaskDisplayRefs(deps.taskDb.listTasks(db, { workspaceRoot: scopedRoot }));
|
|
197
|
+
task = refreshed.find((row) => row.id === created.id) || deps.taskDb.getTask(db, created.id);
|
|
198
|
+
if (created.inserted !== false && task && typeof deps.taskDb.noteTask === 'function') {
|
|
199
|
+
deps.taskDb.noteTask(db, {
|
|
200
|
+
id: task.id,
|
|
201
|
+
actor: 'self-drive',
|
|
202
|
+
content: `Mission objective: ${objective}\nStop reason: ${fullStopReason}\nEvidence: ${evidence}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
projectTasks(deps.taskDb, db, scopedRoot);
|
|
206
|
+
if (created.inserted === false) {
|
|
207
|
+
return { taskId: taskRef(task), dispatched: false, engine: null, reason: 'existing blocker task' };
|
|
208
|
+
}
|
|
209
|
+
if (created.inserted !== false) {
|
|
210
|
+
appendEvent?.('mission_blocker_task_filed', {
|
|
211
|
+
task_id: taskRef(task),
|
|
212
|
+
stop_reason: String(stopReason || blockerClass),
|
|
213
|
+
blocker_class: blockerClass,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ref = taskRef(task);
|
|
219
|
+
const engine = deps.resolveEngineForRole('executor', scopedRoot);
|
|
220
|
+
if (!engine) {
|
|
221
|
+
const fleet = postBlockerToFleet({ task, blockerClass, stopReason, mission }, injected);
|
|
222
|
+
if (fleet.posted) {
|
|
223
|
+
appendEvent?.('mission_blocker_fleet_escalated', {
|
|
224
|
+
task_id: ref,
|
|
225
|
+
blocker_class: blockerClass,
|
|
226
|
+
channel: fleet.channel,
|
|
227
|
+
});
|
|
228
|
+
return { taskId: ref, dispatched: false, engine: null, fleet_posted: true, reason: 'no ready executor engine; escalated to fleet' };
|
|
229
|
+
}
|
|
230
|
+
return { taskId: ref, dispatched: false, engine: null, fleet_posted: false, reason: 'no ready executor engine' };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
if (task.status === 'open' && typeof deps.taskDb.claimTask === 'function') {
|
|
235
|
+
const claim = deps.taskDb.claimTask(db, { id: task.id, claimedBy: `fleet-${engine.id}` });
|
|
236
|
+
if (!claim.claimed) return { taskId: ref, dispatched: false, engine: null, reason: 'existing blocker task' };
|
|
237
|
+
task = { ...task, ...claim.row };
|
|
238
|
+
}
|
|
239
|
+
const worktree = deps.createAgentWorktree({
|
|
240
|
+
root: scopedRoot,
|
|
241
|
+
agent: engine.id,
|
|
242
|
+
task: `mission-blocker-${String(ref).toLowerCase()}`,
|
|
243
|
+
});
|
|
244
|
+
deps.dispatchToEngine({ task, engine: engine.id, worktreePath: worktree.path, root: scopedRoot });
|
|
245
|
+
projectTasks(deps.taskDb, db, scopedRoot);
|
|
246
|
+
appendEvent?.('mission_blocker_dispatched', {
|
|
247
|
+
task_id: ref,
|
|
248
|
+
engine: engine.id,
|
|
249
|
+
blocker_class: blockerClass,
|
|
250
|
+
worktree_path: worktree.path,
|
|
251
|
+
});
|
|
252
|
+
return { taskId: ref, dispatched: true, engine: engine.id, reason: 'dispatched' };
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return { taskId: ref, dispatched: false, engine: engine.id, reason: error.message || String(error) };
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
module.exports = { handleMissionBlocker };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const FILLER_WORDS = new Set([
|
|
4
|
+
'a',
|
|
5
|
+
'an',
|
|
6
|
+
'and',
|
|
7
|
+
'are',
|
|
8
|
+
'as',
|
|
9
|
+
'at',
|
|
10
|
+
'be',
|
|
11
|
+
'by',
|
|
12
|
+
'claim',
|
|
13
|
+
'for',
|
|
14
|
+
'from',
|
|
15
|
+
'in',
|
|
16
|
+
'is',
|
|
17
|
+
'of',
|
|
18
|
+
'on',
|
|
19
|
+
'or',
|
|
20
|
+
'please',
|
|
21
|
+
'pls',
|
|
22
|
+
'that',
|
|
23
|
+
'the',
|
|
24
|
+
'these',
|
|
25
|
+
'this',
|
|
26
|
+
'those',
|
|
27
|
+
'to',
|
|
28
|
+
'via',
|
|
29
|
+
'with',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
function displayNumber(value) {
|
|
33
|
+
const number = Number(value);
|
|
34
|
+
return Number.isInteger(number) && number > 0 ? number : 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function nextRecordNumber(records = []) {
|
|
38
|
+
const assigned = new Set();
|
|
39
|
+
const max = records.reduce((largest, record) => {
|
|
40
|
+
const number = displayNumber(record && record.n);
|
|
41
|
+
if (!number) return largest;
|
|
42
|
+
const id = String(record && record.id || '').trim();
|
|
43
|
+
if (id && assigned.has(id)) return largest;
|
|
44
|
+
if (id) assigned.add(id);
|
|
45
|
+
return Math.max(largest, number);
|
|
46
|
+
}, 0);
|
|
47
|
+
return max + 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function shortId(id) {
|
|
51
|
+
const text = String(id || '').trim();
|
|
52
|
+
if (!text) return 'unknown';
|
|
53
|
+
return text.length <= 8 ? text : text.slice(-8);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function meaningfulWords(text, limit = 3) {
|
|
57
|
+
const words = String(text || '')
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.match(/[a-z0-9][a-z0-9']*/g) || [];
|
|
60
|
+
return words
|
|
61
|
+
.filter((word) => !FILLER_WORDS.has(word))
|
|
62
|
+
.slice(0, limit);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function shortRecordLabel(record, text, options = {}) {
|
|
66
|
+
const number = displayNumber(record && record.n);
|
|
67
|
+
const words = meaningfulWords(text, options.wordLimit || 3);
|
|
68
|
+
if (!number) {
|
|
69
|
+
return words.length ? words.join(' ') : shortId(record && record.id);
|
|
70
|
+
}
|
|
71
|
+
const suffix = words.length ? ` ${words.join(' ')}` : '';
|
|
72
|
+
return `#${number}${suffix}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function shortRecordRef(record) {
|
|
76
|
+
const number = displayNumber(record && record.n);
|
|
77
|
+
return number ? String(number) : shortId(record && record.id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function recordMatchesRef(record, ref, options = {}) {
|
|
81
|
+
if (!record) return false;
|
|
82
|
+
const raw = String(ref || '').trim();
|
|
83
|
+
if (!raw) return false;
|
|
84
|
+
const withoutHash = raw.startsWith('#') ? raw.slice(1) : raw;
|
|
85
|
+
const wantedNumber = displayNumber(withoutHash);
|
|
86
|
+
if (wantedNumber) return displayNumber(record.n) === wantedNumber;
|
|
87
|
+
const id = String(record.id || '').trim();
|
|
88
|
+
if (!id) return false;
|
|
89
|
+
if (id === raw || id === withoutHash) return true;
|
|
90
|
+
if (options.prefix !== false && (id.startsWith(raw) || id.startsWith(withoutHash))) return true;
|
|
91
|
+
if (options.suffix !== false && (id.endsWith(raw) || id.endsWith(withoutHash))) return true;
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
displayNumber,
|
|
97
|
+
meaningfulWords,
|
|
98
|
+
nextRecordNumber,
|
|
99
|
+
recordMatchesRef,
|
|
100
|
+
shortId,
|
|
101
|
+
shortRecordLabel,
|
|
102
|
+
shortRecordRef,
|
|
103
|
+
};
|
package/lib/spawn-env.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// Autonomously launched runtimes (cron, launchd, pulse ticks) carry a minimal
|
|
6
|
+
// PATH without node's directory, so any shell command that may invoke
|
|
7
|
+
// `node ...` fails only in the one environment nobody is watching. Three
|
|
8
|
+
// subsystems hit this on 2026-07-07 alone (experiment applies, atris2 relayed
|
|
9
|
+
// ops, always-on mission verifiers). Prepend the directory of the node that
|
|
10
|
+
// runs this CLI before spawning shell commands from autonomous paths.
|
|
11
|
+
function envWithNodeDir(base = process.env) {
|
|
12
|
+
const nodeDir = path.dirname(process.execPath);
|
|
13
|
+
const basePath = String(base.PATH || '');
|
|
14
|
+
if (basePath.split(path.delimiter).includes(nodeDir)) return base;
|
|
15
|
+
return { ...base, PATH: `${nodeDir}${path.delimiter}${basePath}` };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { envWithNodeDir };
|
package/lib/state-detection.js
CHANGED
|
@@ -185,6 +185,60 @@ function getTaskCounts(atrisDir) {
|
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
// One read of the task source returning counts plus a few actual titles, so
|
|
189
|
+
// boot surfaces can show the work itself instead of just numbers.
|
|
190
|
+
// Same source rules as getTaskCounts: DB wins, TODO.md is the fallback,
|
|
191
|
+
// and nothing here may create a DB that doesn't exist.
|
|
192
|
+
function getTaskGlance(atrisDir, sampleSize = 3) {
|
|
193
|
+
try {
|
|
194
|
+
const taskDb = require('./task-db');
|
|
195
|
+
if (fs.existsSync(taskDb.getDbPath())) {
|
|
196
|
+
const workspaceRoot = taskDb.workspaceRoot(path.dirname(path.resolve(atrisDir)));
|
|
197
|
+
const rows = taskDb.listTasks(taskDb.open(), { workspaceRoot, limit: 500 });
|
|
198
|
+
if (rows.length) {
|
|
199
|
+
const glance = {
|
|
200
|
+
backlog: 0, active: 0, review: 0, reviewCertified: 0,
|
|
201
|
+
activeTitles: [], backlogTitles: [], certifiedTitles: [], source: 'db'
|
|
202
|
+
};
|
|
203
|
+
for (const row of rows) {
|
|
204
|
+
const status = String(row.status || '').toLowerCase();
|
|
205
|
+
if (TASK_STATUS_BUCKETS.backlog.has(status)) {
|
|
206
|
+
glance.backlog++;
|
|
207
|
+
if (row.title && glance.backlogTitles.length < sampleSize) glance.backlogTitles.push(row.title);
|
|
208
|
+
} else if (TASK_STATUS_BUCKETS.inProgress.has(status)) {
|
|
209
|
+
glance.active++;
|
|
210
|
+
if (row.title && glance.activeTitles.length < sampleSize) glance.activeTitles.push(row.title);
|
|
211
|
+
} else if (TASK_STATUS_BUCKETS.review.has(status)) {
|
|
212
|
+
glance.review++;
|
|
213
|
+
try {
|
|
214
|
+
const meta = typeof row.metadata === 'string' ? JSON.parse(row.metadata || '{}') : (row.metadata || {});
|
|
215
|
+
if (meta.agent_certified && meta.approval_status !== 'accepted') {
|
|
216
|
+
glance.reviewCertified++;
|
|
217
|
+
if (row.title && glance.certifiedTitles.length < sampleSize) glance.certifiedTitles.push(row.title);
|
|
218
|
+
}
|
|
219
|
+
} catch { /* unreadable metadata - count as uncertified */ }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return glance;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (process.env.ATRIS_DEBUG) console.error('[state-detection] db glance read failed:', e.message);
|
|
227
|
+
}
|
|
228
|
+
const backlogTitles = getBacklogTasks(atrisDir);
|
|
229
|
+
const activeTitles = getInProgressTasks(atrisDir);
|
|
230
|
+
return {
|
|
231
|
+
backlog: backlogTitles.length,
|
|
232
|
+
active: activeTitles.length,
|
|
233
|
+
review: 0,
|
|
234
|
+
reviewCertified: 0,
|
|
235
|
+
activeTitles: activeTitles.slice(0, sampleSize),
|
|
236
|
+
backlogTitles: backlogTitles.slice(0, sampleSize),
|
|
237
|
+
certifiedTitles: [],
|
|
238
|
+
source: 'todo'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
188
242
|
function getTodayInboxItems(workspaceDir) {
|
|
189
243
|
const atrisDir = path.join(workspaceDir, 'atris');
|
|
190
244
|
const logsDir = path.join(atrisDir, 'logs');
|
|
@@ -357,5 +411,6 @@ module.exports = {
|
|
|
357
411
|
getBacklogTasks,
|
|
358
412
|
getInProgressTasks,
|
|
359
413
|
getCompletedTasks,
|
|
360
|
-
getTaskCounts
|
|
414
|
+
getTaskCounts,
|
|
415
|
+
getTaskGlance
|
|
361
416
|
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { loadManifest, computeLocalHashes, filterSyncFiles } = require('./manifest');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Count files whose local hash differs from the last-synced manifest.
|
|
7
|
+
* A path present on only one side (new local, deleted since sync) counts as
|
|
8
|
+
* drifted. Pure function so it is unit-testable without touching disk.
|
|
9
|
+
*/
|
|
10
|
+
function driftCount(localFiles = {}, manifest = null) {
|
|
11
|
+
const local = filterSyncFiles(localFiles);
|
|
12
|
+
const base = filterSyncFiles(manifest && manifest.files ? manifest.files : {});
|
|
13
|
+
const paths = new Set([...Object.keys(local), ...Object.keys(base)]);
|
|
14
|
+
let drifted = 0;
|
|
15
|
+
for (const p of paths) {
|
|
16
|
+
const l = local[p] ? local[p].hash : null;
|
|
17
|
+
const b = base[p] ? base[p].hash : null;
|
|
18
|
+
if (l !== b) drifted += 1;
|
|
19
|
+
}
|
|
20
|
+
return drifted;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the configured cloud business slug for a workspace, or null.
|
|
25
|
+
*/
|
|
26
|
+
function readBusinessSlug(root) {
|
|
27
|
+
const bizFile = path.join(root, '.atris', 'business.json');
|
|
28
|
+
if (!fs.existsSync(bizFile)) return null;
|
|
29
|
+
try {
|
|
30
|
+
const biz = JSON.parse(fs.readFileSync(bizFile, 'utf8'));
|
|
31
|
+
return biz.slug || biz.name || null;
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One-line cloud sync status for the boot surface. Local-only, never blocks:
|
|
39
|
+
* - "offline" no cloud business configured or never synced
|
|
40
|
+
* - "in sync" every synced file matches the manifest
|
|
41
|
+
* - "N files drifted" N local files differ from the last sync
|
|
42
|
+
*/
|
|
43
|
+
function syncStatus(root = process.cwd()) {
|
|
44
|
+
const slug = readBusinessSlug(root);
|
|
45
|
+
if (!slug) return 'offline';
|
|
46
|
+
const manifest = loadManifest(slug);
|
|
47
|
+
if (!manifest) return 'offline';
|
|
48
|
+
let localFiles;
|
|
49
|
+
try {
|
|
50
|
+
localFiles = computeLocalHashes(root);
|
|
51
|
+
} catch {
|
|
52
|
+
return 'offline';
|
|
53
|
+
}
|
|
54
|
+
const n = driftCount(localFiles, manifest);
|
|
55
|
+
if (n === 0) return 'in sync';
|
|
56
|
+
return `${n} file${n === 1 ? '' : 's'} drifted`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = { driftCount, readBusinessSlug, syncStatus };
|