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,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Receipt writer for task proofs. Mirrors the mission receipt shape
|
|
4
|
+
// (commands/mission.js writeReceipt) so lib/receipt-evidence.js can validate
|
|
5
|
+
// paths named in a proof at the review gate: schema tag, `at` timestamp,
|
|
6
|
+
// result.passed. Every call writes a receipt file, pass or fail — a failing
|
|
7
|
+
// verifier still leaves a record, it just can't be cited to move a task to
|
|
8
|
+
// ready.
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { spawnSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
function tailText(text, max = 400) {
|
|
15
|
+
const trimmed = String(text || '').trim();
|
|
16
|
+
if (trimmed.length <= max) return trimmed;
|
|
17
|
+
return `...${trimmed.slice(-max)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function gitCommitHash(root) {
|
|
21
|
+
try {
|
|
22
|
+
const result = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
23
|
+
if (result.status === 0) return String(result.stdout || '').trim() || null;
|
|
24
|
+
} catch {
|
|
25
|
+
// ignore — not every workspace is a git repo
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runsDir(root) {
|
|
31
|
+
return path.join(root, 'atris', 'runs');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function slugifyTaskId(taskId) {
|
|
35
|
+
return String(taskId || 'task')
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
38
|
+
.replace(/^-+|-+$/g, '')
|
|
39
|
+
.slice(0, 40) || 'task';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Runs `command` for `taskId`, writes atris/runs/<date>-task-<id>-<time>.json,
|
|
43
|
+
// and returns { ok, passed, receiptPath, exit, signal, output }. `ok` mirrors
|
|
44
|
+
// `passed` — kept as a separate field so callers reading either name work.
|
|
45
|
+
function writeTaskReceipt({ taskId, command, root = process.cwd(), runner } = {}) {
|
|
46
|
+
const cmd = String(command || '').trim();
|
|
47
|
+
if (!taskId) return { ok: false, passed: false, reason: 'task_id_required' };
|
|
48
|
+
if (!cmd) return { ok: false, passed: false, reason: 'verify_command_required' };
|
|
49
|
+
const run = runner || spawnSync;
|
|
50
|
+
const result = run('bash', ['-lc', cmd], {
|
|
51
|
+
cwd: root,
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
timeout: 120000,
|
|
54
|
+
});
|
|
55
|
+
if (result.error) {
|
|
56
|
+
return { ok: false, passed: false, reason: 'verifier_spawn_failed', error: result.error.message, cmd };
|
|
57
|
+
}
|
|
58
|
+
const output = tailText(`${result.stdout || ''}${result.stderr || ''}`);
|
|
59
|
+
const passed = result.status === 0;
|
|
60
|
+
const dir = runsDir(root);
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
const now = new Date();
|
|
63
|
+
const date = now.toISOString().slice(0, 10);
|
|
64
|
+
const safeTime = now.toISOString().replace(/[:.]/g, '-');
|
|
65
|
+
const receiptFile = path.join(dir, `${date}-task-${slugifyTaskId(taskId)}-${safeTime}.json`);
|
|
66
|
+
const receiptPath = path.relative(root, receiptFile);
|
|
67
|
+
const receipt = {
|
|
68
|
+
schema: 'atris.task_receipt.v1',
|
|
69
|
+
task_id: String(taskId),
|
|
70
|
+
command: cmd,
|
|
71
|
+
at: now.toISOString(),
|
|
72
|
+
commit: gitCommitHash(root),
|
|
73
|
+
result: {
|
|
74
|
+
passed,
|
|
75
|
+
exit: result.status,
|
|
76
|
+
signal: result.signal || null,
|
|
77
|
+
output,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
fs.writeFileSync(receiptFile, JSON.stringify(receipt, null, 2) + '\n', 'utf8');
|
|
81
|
+
return {
|
|
82
|
+
ok: passed,
|
|
83
|
+
passed,
|
|
84
|
+
receiptPath,
|
|
85
|
+
receiptFile,
|
|
86
|
+
exit: result.status,
|
|
87
|
+
signal: result.signal || null,
|
|
88
|
+
output,
|
|
89
|
+
cmd,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { writeTaskReceipt };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_FRESHNESS_WINDOW_MS = 15 * 60 * 1000;
|
|
4
|
+
const ACTIVE_TASK_STATES = new Set(['claimed', 'do', 'doing', 'in_progress', 'review']);
|
|
5
|
+
const ACTIVE_MISSION_STATES = new Set(['planning', 'active', 'running', 'ready']);
|
|
6
|
+
|
|
7
|
+
function timestampMs(value) {
|
|
8
|
+
if (value == null || value === '') return 0;
|
|
9
|
+
if (typeof value === 'number') {
|
|
10
|
+
if (!Number.isFinite(value)) return 0;
|
|
11
|
+
return value > 1000000000000 ? value : value * 1000;
|
|
12
|
+
}
|
|
13
|
+
const numeric = Number(value);
|
|
14
|
+
if (Number.isFinite(numeric) && String(value).trim() !== '') return timestampMs(numeric);
|
|
15
|
+
const parsed = Date.parse(String(value));
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function latestTimestamp(...values) {
|
|
20
|
+
return values.reduce((latest, value) => Math.max(latest, timestampMs(value)), 0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isoTimestamp(value) {
|
|
24
|
+
const ms = timestampMs(value);
|
|
25
|
+
return ms ? new Date(ms).toISOString() : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function memberName(value) {
|
|
29
|
+
let text = String(value || '').trim();
|
|
30
|
+
if (!text) return '';
|
|
31
|
+
if (text.includes('/')) text = text.split('/')[0];
|
|
32
|
+
text = text.replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
33
|
+
if (!text || /^(team|unknown)$/i.test(text)) return '';
|
|
34
|
+
return text.slice(0, 32);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function operatorSentence(value, fallback = 'current activity was recorded') {
|
|
38
|
+
let text = String(value || '')
|
|
39
|
+
.split(/\r?\n/)
|
|
40
|
+
.map((line) => line.trim().replace(/^[-*]\s+/, ''))
|
|
41
|
+
.find(Boolean) || fallback;
|
|
42
|
+
text = text
|
|
43
|
+
.replace(/(^|\s)--[a-z0-9][a-z0-9-]*(?:=[^\s]+)?/ig, '$1')
|
|
44
|
+
.replace(/\b[0-9A-HJKMNP-TV-Z]{20,26}\b/g, 'the item')
|
|
45
|
+
.replace(/\b[A-Z]{2,12}-\d+\b/gi, 'the task')
|
|
46
|
+
.replace(/\bmission-[a-z0-9][a-z0-9-]{8,}\b/ig, 'the mission')
|
|
47
|
+
.replace(/\s+/g, ' ')
|
|
48
|
+
.trim();
|
|
49
|
+
if (!text) text = fallback;
|
|
50
|
+
if (text.length > 180) text = `${text.slice(0, 177).trim()}...`;
|
|
51
|
+
if (!/[.!?]$/.test(text)) text += '.';
|
|
52
|
+
return text;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function taskOwner(task) {
|
|
56
|
+
return task && (task.claimed_by || task.assigned_to || task.metadata?.assigned_to) || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function taskActivityMs(task) {
|
|
60
|
+
const events = Array.isArray(task?.events) ? task.events : [];
|
|
61
|
+
return latestTimestamp(
|
|
62
|
+
task?.updated_at,
|
|
63
|
+
task?.created_at,
|
|
64
|
+
...events.map((event) => event && event.created_at),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function missionActivityMs(mission) {
|
|
69
|
+
return latestTimestamp(
|
|
70
|
+
mission?.last_tick_at,
|
|
71
|
+
mission?.last_tick?.finished_at,
|
|
72
|
+
mission?.last_tick?.at,
|
|
73
|
+
mission?.updated_at,
|
|
74
|
+
mission?.created_at,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function activityOrder(a, b, activityFn) {
|
|
79
|
+
return activityFn(b) - activityFn(a)
|
|
80
|
+
|| String(a.id || a.title || a.objective || '').localeCompare(String(b.id || b.title || b.objective || ''));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function taskRows(input) {
|
|
84
|
+
if (Array.isArray(input.tasks)) return input.tasks;
|
|
85
|
+
if (Array.isArray(input.taskStatus?.tasks)) return input.taskStatus.tasks;
|
|
86
|
+
if (Array.isArray(input.taskStatus?.streams)) {
|
|
87
|
+
return input.taskStatus.streams.flatMap((stream) => Array.isArray(stream.tasks) ? stream.tasks : []);
|
|
88
|
+
}
|
|
89
|
+
return [input.taskStatus?.current].filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function missionRows(input) {
|
|
93
|
+
if (Array.isArray(input.missions)) return input.missions;
|
|
94
|
+
if (Array.isArray(input.missionStatus?.missions)) return input.missionStatus.missions;
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function loopMissionName(mission) {
|
|
99
|
+
const raw = mission?.name || mission?.title || mission?.objective || mission?.id || 'mission';
|
|
100
|
+
return operatorSentence(raw, 'mission').replace(/[.!?]+$/, '').slice(0, 90);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loopForMission(mission) {
|
|
104
|
+
if (!mission) return null;
|
|
105
|
+
const lastTick = isoTimestamp(
|
|
106
|
+
mission.last_tick_at
|
|
107
|
+
|| mission.last_tick?.finished_at
|
|
108
|
+
|| mission.last_tick?.at,
|
|
109
|
+
);
|
|
110
|
+
return {
|
|
111
|
+
mission: loopMissionName(mission),
|
|
112
|
+
cadence: String(mission.cadence || 'manual'),
|
|
113
|
+
runner: String(mission.runner || mission.executed_by || 'manual'),
|
|
114
|
+
last_tick: lastTick || 'never',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function streamActiveRows(stream) {
|
|
119
|
+
return (Array.isArray(stream?.active) ? stream.active : []).map((row) => {
|
|
120
|
+
if (Array.isArray(row)) return { name: row[0], summary: row[1], last_seen: row[2] };
|
|
121
|
+
return {
|
|
122
|
+
name: row?.name || row?.agent || row?.member,
|
|
123
|
+
summary: row?.summary || row?.doing || row?.work,
|
|
124
|
+
last_seen: row?.last_seen || row?.ts || row?.at,
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildTeamPresence(input = {}) {
|
|
130
|
+
const nowMs = timestampMs(input.nowMs ?? input.now ?? Date.now()) || Date.now();
|
|
131
|
+
const freshnessWindowMs = Number(input.freshnessWindowMs) > 0
|
|
132
|
+
? Number(input.freshnessWindowMs)
|
|
133
|
+
: DEFAULT_FRESHNESS_WINDOW_MS;
|
|
134
|
+
const stream = input.stream || input.streamSnapshot || {};
|
|
135
|
+
const events = Array.isArray(input.streamEvents)
|
|
136
|
+
? input.streamEvents
|
|
137
|
+
: Array.isArray(input.events) ? input.events : [];
|
|
138
|
+
const tasks = taskRows(input).filter(Boolean);
|
|
139
|
+
const missions = missionRows(input).filter(Boolean);
|
|
140
|
+
const candidates = new Map();
|
|
141
|
+
const lastSeen = new Map();
|
|
142
|
+
const summaries = new Map();
|
|
143
|
+
const tasksByMember = new Map();
|
|
144
|
+
const missionsByMember = new Map();
|
|
145
|
+
|
|
146
|
+
const keyFor = (value) => memberName(value).toLowerCase();
|
|
147
|
+
const addCandidate = (value) => {
|
|
148
|
+
const name = memberName(value);
|
|
149
|
+
const key = name.toLowerCase();
|
|
150
|
+
if (!key) return '';
|
|
151
|
+
if (!candidates.has(key)) candidates.set(key, name);
|
|
152
|
+
return key;
|
|
153
|
+
};
|
|
154
|
+
const noteSeen = (value, timestamp) => {
|
|
155
|
+
const key = keyFor(value);
|
|
156
|
+
const ms = timestampMs(timestamp);
|
|
157
|
+
if (!key || !ms) return;
|
|
158
|
+
lastSeen.set(key, Math.max(lastSeen.get(key) || 0, ms));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const event of events) {
|
|
162
|
+
noteSeen(event?.agent || event?.member || event?.owner, event?.ms || event?.ts || event?.at || event?.created_at);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const row of streamActiveRows(stream)) {
|
|
166
|
+
const key = addCandidate(row.name);
|
|
167
|
+
if (!key) continue;
|
|
168
|
+
if (row.summary) summaries.set(key, row.summary);
|
|
169
|
+
noteSeen(row.name, row.last_seen);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const task of tasks) {
|
|
173
|
+
if (!ACTIVE_TASK_STATES.has(String(task.status || '').toLowerCase())) continue;
|
|
174
|
+
const owner = taskOwner(task);
|
|
175
|
+
const key = addCandidate(owner);
|
|
176
|
+
if (!key) continue;
|
|
177
|
+
noteSeen(owner, taskActivityMs(task));
|
|
178
|
+
if (!tasksByMember.has(key)) tasksByMember.set(key, []);
|
|
179
|
+
tasksByMember.get(key).push(task);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const mission of missions) {
|
|
183
|
+
if (!ACTIVE_MISSION_STATES.has(String(mission.status || '').toLowerCase())) continue;
|
|
184
|
+
const owner = mission.owner || mission.member;
|
|
185
|
+
const key = addCandidate(owner);
|
|
186
|
+
if (!key) continue;
|
|
187
|
+
noteSeen(owner, missionActivityMs(mission));
|
|
188
|
+
if (!missionsByMember.has(key)) missionsByMember.set(key, []);
|
|
189
|
+
missionsByMember.get(key).push(mission);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const rows of tasksByMember.values()) rows.sort((a, b) => activityOrder(a, b, taskActivityMs));
|
|
193
|
+
for (const rows of missionsByMember.values()) rows.sort((a, b) => activityOrder(a, b, missionActivityMs));
|
|
194
|
+
|
|
195
|
+
const cutoffMs = nowMs - freshnessWindowMs;
|
|
196
|
+
const members = [...candidates.entries()]
|
|
197
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
198
|
+
.flatMap(([key, name]) => {
|
|
199
|
+
const seenMs = lastSeen.get(key) || 0;
|
|
200
|
+
if (seenMs < cutoffMs) return [];
|
|
201
|
+
const task = tasksByMember.get(key)?.[0] || null;
|
|
202
|
+
const mission = missionsByMember.get(key)?.[0] || null;
|
|
203
|
+
const doing = summaries.get(key)
|
|
204
|
+
|| task?.title
|
|
205
|
+
|| mission?.next_action
|
|
206
|
+
|| mission?.objective;
|
|
207
|
+
return [{
|
|
208
|
+
name,
|
|
209
|
+
awake: true,
|
|
210
|
+
doing: operatorSentence(doing),
|
|
211
|
+
loop: loopForMission(mission),
|
|
212
|
+
last_seen: new Date(seenMs).toISOString(),
|
|
213
|
+
}];
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
schema: 'atris.team_presence.v1',
|
|
218
|
+
generated_at: new Date(nowMs).toISOString(),
|
|
219
|
+
freshness_window_seconds: Math.round(freshnessWindowMs / 1000),
|
|
220
|
+
totals: {
|
|
221
|
+
awake: members.length,
|
|
222
|
+
waiting_operator: Math.max(0, Number(stream.waiting_operator) || 0),
|
|
223
|
+
landing_wait: Math.max(0, Number(stream.landing_wait) || 0),
|
|
224
|
+
},
|
|
225
|
+
members,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function renderTeamPresence(presence) {
|
|
230
|
+
const minutes = presence.freshness_window_seconds / 60;
|
|
231
|
+
const windowText = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? '' : 's'}` : `${presence.freshness_window_seconds} seconds`;
|
|
232
|
+
const lines = [
|
|
233
|
+
`team presence: ${presence.totals.awake} awake`,
|
|
234
|
+
`freshness window: ${windowText}`,
|
|
235
|
+
`waiting on operator: ${presence.totals.waiting_operator}`,
|
|
236
|
+
`landing wait: ${presence.totals.landing_wait}`,
|
|
237
|
+
];
|
|
238
|
+
if (!presence.members.length) {
|
|
239
|
+
lines.push('awake roster: empty');
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
lines.push('awake roster:');
|
|
243
|
+
for (const member of presence.members) {
|
|
244
|
+
lines.push(` ${member.name}: ${member.doing}`);
|
|
245
|
+
if (member.loop) {
|
|
246
|
+
lines.push(` loop: ${member.loop.mission} [${member.loop.cadence} | ${member.loop.runner} | last tick ${member.loop.last_tick}]`);
|
|
247
|
+
}
|
|
248
|
+
lines.push(` last seen: ${member.last_seen}`);
|
|
249
|
+
}
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
ACTIVE_MISSION_STATES,
|
|
255
|
+
ACTIVE_TASK_STATES,
|
|
256
|
+
DEFAULT_FRESHNESS_WINDOW_MS,
|
|
257
|
+
buildTeamPresence,
|
|
258
|
+
operatorSentence,
|
|
259
|
+
renderTeamPresence,
|
|
260
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const HISTORY_FILES = [
|
|
7
|
+
path.join('.atris', 'state', 'career_xp_receipts.jsonl'),
|
|
8
|
+
path.join('.atris', 'state', 'scorecards.jsonl'),
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const PASS_OUTCOMES = new Set(['accepted', 'approved', 'done', 'pass', 'passed', 'success', 'succeeded']);
|
|
12
|
+
const FAIL_OUTCOMES = new Set(['bounced', 'fail', 'failed', 'rejected', 'revised', 'rework_requested']);
|
|
13
|
+
|
|
14
|
+
function normalizedActor(value) {
|
|
15
|
+
return String(value || '').trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function rowActor(row) {
|
|
19
|
+
return normalizedActor(row?.claimed_by || row?.metadata?.executed_by);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function rowPassed(row) {
|
|
23
|
+
for (const value of [row?.passed, row?.verify_passed, row?.metadata?.verify_passed]) {
|
|
24
|
+
if (typeof value === 'boolean') return value;
|
|
25
|
+
}
|
|
26
|
+
const outcome = String(row?.outcome || row?.status || '').trim().toLowerCase();
|
|
27
|
+
if (PASS_OUTCOMES.has(outcome)) return true;
|
|
28
|
+
if (FAIL_OUTCOMES.has(outcome)) return false;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rowTime(row) {
|
|
33
|
+
for (const value of [row?.accepted_at, row?.ts, row?.recorded_at, row?.created_at, row?.updated_at]) {
|
|
34
|
+
const parsed = Date.parse(value || '');
|
|
35
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readHistory(root) {
|
|
41
|
+
const rows = [];
|
|
42
|
+
let sequence = 0;
|
|
43
|
+
for (const relative of HISTORY_FILES) {
|
|
44
|
+
let text;
|
|
45
|
+
try {
|
|
46
|
+
text = fs.readFileSync(path.join(root, relative), 'utf8');
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
for (const line of text.split(/\r?\n/)) {
|
|
51
|
+
if (!line.trim()) continue;
|
|
52
|
+
let row;
|
|
53
|
+
try {
|
|
54
|
+
row = JSON.parse(line);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!row || typeof row !== 'object' || Array.isArray(row)) return null;
|
|
59
|
+
rows.push({ row, sequence: sequence++, time: rowTime(row) });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function computeTrustTier(actor, root = process.cwd()) {
|
|
66
|
+
const target = normalizedActor(actor);
|
|
67
|
+
if (!target) return 'probation';
|
|
68
|
+
const history = readHistory(root);
|
|
69
|
+
if (!history) return 'probation';
|
|
70
|
+
|
|
71
|
+
const outcomes = history
|
|
72
|
+
.filter(({ row }) => rowActor(row) === target)
|
|
73
|
+
.map((entry) => ({ ...entry, passed: rowPassed(entry.row) }))
|
|
74
|
+
.filter(({ passed }) => passed !== null)
|
|
75
|
+
.sort((a, b) => {
|
|
76
|
+
if (a.time !== null && b.time !== null && a.time !== b.time) return a.time - b.time;
|
|
77
|
+
if (a.time !== null && b.time === null) return 1;
|
|
78
|
+
if (a.time === null && b.time !== null) return -1;
|
|
79
|
+
return a.sequence - b.sequence;
|
|
80
|
+
})
|
|
81
|
+
.slice(-20);
|
|
82
|
+
|
|
83
|
+
const passed = outcomes.filter((outcome) => outcome.passed).length;
|
|
84
|
+
const passRate = outcomes.length ? passed / outcomes.length : 0;
|
|
85
|
+
if (outcomes.length >= 10 && passRate >= 0.9) return 'trusted';
|
|
86
|
+
if (outcomes.length >= 5 && passRate >= 0.7) return 'standard';
|
|
87
|
+
return 'probation';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { computeTrustTier };
|
package/lib/usage.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { knownCommands } = require('./known-commands');
|
|
6
|
+
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
const USAGE_RELATIVE_PATH = path.join('.atris', 'state', 'usage.jsonl');
|
|
9
|
+
const USAGE_MAX_BYTES = 5 * 1024 * 1024;
|
|
10
|
+
|
|
11
|
+
// Usage telemetry used to write to `<cwd>/.atris/state/usage.jsonl`, so a
|
|
12
|
+
// command run from a subdirectory that already had a nested .atris (e.g.
|
|
13
|
+
// backend/) split its usage away from the workspace root's store — the same
|
|
14
|
+
// footgun the mission and task stores hit. Resolve to the shared workspace
|
|
15
|
+
// root (spine-first -> git toplevel -> cwd) so usage lands with the rest of the
|
|
16
|
+
// state, whichever subdir the caller stood in. Falls back to the raw cwd if the
|
|
17
|
+
// shared resolver can't load; telemetry must never break the command path.
|
|
18
|
+
function resolveUsageRoot(cwd = process.cwd()) {
|
|
19
|
+
try {
|
|
20
|
+
const { workspaceRoot } = require('./task-db');
|
|
21
|
+
const root = workspaceRoot(cwd);
|
|
22
|
+
if (root) return root;
|
|
23
|
+
} catch {}
|
|
24
|
+
return cwd;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function usagePath(cwd = process.cwd()) {
|
|
28
|
+
return path.join(resolveUsageRoot(cwd), USAGE_RELATIVE_PATH);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseUsageLine(line) {
|
|
32
|
+
try {
|
|
33
|
+
const entry = JSON.parse(line);
|
|
34
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
35
|
+
if (typeof entry.at !== 'string' || typeof entry.cmd !== 'string') return null;
|
|
36
|
+
return entry;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readAllUsage(cwd = process.cwd()) {
|
|
43
|
+
const file = usagePath(cwd);
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(file)) return [];
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const out = [];
|
|
50
|
+
try {
|
|
51
|
+
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed) continue;
|
|
54
|
+
const entry = parseUsageLine(trimmed);
|
|
55
|
+
if (entry) out.push(entry);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function readUsage(cwd = process.cwd(), options = {}) {
|
|
64
|
+
const entries = readAllUsage(cwd);
|
|
65
|
+
const sinceDays = Number(options.sinceDays);
|
|
66
|
+
if (!Number.isFinite(sinceDays) || sinceDays <= 0) return entries;
|
|
67
|
+
const now = options.now ? new Date(options.now).getTime() : Date.now();
|
|
68
|
+
const cutoff = now - sinceDays * DAY_MS;
|
|
69
|
+
return entries.filter((entry) => {
|
|
70
|
+
const at = Date.parse(entry.at);
|
|
71
|
+
return Number.isFinite(at) && at >= cutoff;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function trimUsageFile(cwd = process.cwd()) {
|
|
76
|
+
const file = usagePath(cwd);
|
|
77
|
+
const stat = fs.statSync(file);
|
|
78
|
+
if (stat.size <= USAGE_MAX_BYTES) return;
|
|
79
|
+
const cutoff = Date.now() - 30 * DAY_MS;
|
|
80
|
+
const kept = readAllUsage(cwd).filter((entry) => {
|
|
81
|
+
const at = Date.parse(entry.at);
|
|
82
|
+
return Number.isFinite(at) && at >= cutoff;
|
|
83
|
+
});
|
|
84
|
+
fs.writeFileSync(file, kept.map((entry) => JSON.stringify(entry)).join('\n') + (kept.length ? '\n' : ''), 'utf8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function recordUsage(command, cwd = process.cwd()) {
|
|
88
|
+
try {
|
|
89
|
+
if (!command || !knownCommands.includes(command)) return;
|
|
90
|
+
const root = resolveUsageRoot(cwd);
|
|
91
|
+
// The sensor observes workspaces, it never creates them: recording into a
|
|
92
|
+
// resolved root with no .atris/ would litter pristine cwds (even `--help`).
|
|
93
|
+
if (!fs.existsSync(path.join(root, '.atris'))) return;
|
|
94
|
+
const file = usagePath(root);
|
|
95
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
96
|
+
fs.appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), cmd: String(command) })}\n`, 'utf8');
|
|
97
|
+
trimUsageFile(root);
|
|
98
|
+
} catch {
|
|
99
|
+
// Usage telemetry must never affect the command path.
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
usagePath,
|
|
105
|
+
recordUsage,
|
|
106
|
+
readUsage,
|
|
107
|
+
};
|