atris 3.30.12 → 3.31.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 +16 -3
- package/FOR_AGENTS.md +81 -0
- package/README.md +3 -1
- package/atris/atris.md +51 -19
- package/atris/skills/README.md +1 -0
- package/atris/skills/blocks/SKILL.md +134 -0
- package/atris/skills/clawhub/philosophy-of-work/SKILL.md +56 -0
- package/atris/skills/youtube/SKILL.md +31 -11
- package/atris.md +7 -0
- package/ax +95 -3
- package/bin/atris.js +126 -153
- package/commands/autoland.js +319 -0
- package/commands/autopilot.js +94 -4
- package/commands/business.js +1 -1
- package/commands/clean.js +72 -9
- package/commands/codex-goal.js +72 -22
- package/commands/computer.js +48 -3
- package/commands/gm.js +1 -1
- package/commands/harvest.js +179 -0
- package/commands/init.js +1 -1
- package/commands/land.js +442 -0
- package/commands/loop-front.js +122 -4
- package/commands/member.js +519 -19
- package/commands/mission.js +3330 -290
- package/commands/play.js +1 -1
- package/commands/pulse.js +65 -7
- package/commands/run.js +3 -3
- package/commands/strings.js +301 -0
- package/commands/task.js +575 -102
- package/commands/truth.js +170 -0
- package/commands/xp.js +32 -8
- package/commands/youtube.js +72 -5
- package/decks/README.md +6 -12
- package/lib/auto-accept-certified.js +10 -0
- package/lib/autoland.js +283 -0
- package/lib/context-gatherer.js +0 -8
- package/lib/mission-artifact.js +504 -0
- package/lib/mission-room.js +846 -0
- package/lib/next-moves.js +212 -6
- package/lib/pulse.js +74 -1
- package/lib/runner-command.js +20 -8
- package/lib/runs-prune.js +242 -0
- package/lib/task-proof.js +1 -1
- package/package.json +3 -3
- package/decks/atris-seed-pitch-v3.json +0 -118
- package/decks/atris-seed-pitch-v4-skeleton.json +0 -106
- package/decks/atris-seed-pitch-v5.json +0 -109
- package/decks/atris-seed-pitch-v6.json +0 -137
- package/decks/atris-seed-pitch-v7.json +0 -133
- package/decks/mark-pincus-narrative.json +0 -102
- package/decks/mark-pincus-sourcery.json +0 -94
- package/decks/yash-applied-compute-detailed.json +0 -150
- package/decks/yash-applied-compute-generalist.json +0 -82
- package/decks/yash-applied-compute-narrative.json +0 -54
- package/lib/ax-chat-input.js +0 -164
- package/lib/ax-goal.js +0 -307
- package/lib/ax-prefs.js +0 -63
- package/lib/ax-shimmer.js +0 -63
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const autoland = require('../lib/autoland');
|
|
9
|
+
|
|
10
|
+
function repoRoot(cwd = process.cwd()) {
|
|
11
|
+
const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8' });
|
|
12
|
+
if (result.status !== 0) return cwd;
|
|
13
|
+
return result.stdout.trim() || cwd;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function projectName(root) {
|
|
17
|
+
return path.basename(root);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function flag(args, name, fallback = '') {
|
|
21
|
+
const idx = args.indexOf(name);
|
|
22
|
+
if (idx === -1) return fallback;
|
|
23
|
+
return args[idx + 1] || fallback;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function runOwnCli(root, cliArgs) {
|
|
27
|
+
const bin = path.join(root, 'bin', 'atris.js');
|
|
28
|
+
const argv = fs.existsSync(bin) ? [process.execPath, [bin, ...cliArgs]] : ['atris', [cliArgs].flat()];
|
|
29
|
+
const result = spawnSync(argv[0], argv[1], { cwd: root, encoding: 'utf8', timeout: 300000 });
|
|
30
|
+
return { status: result.status, stdout: String(result.stdout || ''), stderr: String(result.stderr || '') };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readProjection(root) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(path.join(root, '.atris', 'state', 'tasks.projection.json'), 'utf8');
|
|
36
|
+
const projection = JSON.parse(raw);
|
|
37
|
+
return Array.isArray(projection.tasks) ? projection.tasks : [];
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function landSummarySafe(root) {
|
|
44
|
+
try {
|
|
45
|
+
return require('./land').landSummary(root);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function evaluateQueue(root, { strictVerify }) {
|
|
52
|
+
const cliArgs = ['task', 'auto-accept-certified', '--dry-run', '--json', '--limit', '50'];
|
|
53
|
+
if (strictVerify) cliArgs.push('--strict-verify');
|
|
54
|
+
const result = runOwnCli(root, cliArgs);
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(result.stdout);
|
|
57
|
+
return Array.isArray(parsed.results) ? parsed.results : [];
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function plainReason(reason) {
|
|
64
|
+
const map = {
|
|
65
|
+
denied_tag_billing: 'money — yours to approve',
|
|
66
|
+
denied_tag_deploy: 'a deploy — yours to approve',
|
|
67
|
+
denied_tag_security: 'security — yours to approve',
|
|
68
|
+
denied_tag_customer: 'customer-facing — yours to approve',
|
|
69
|
+
denied_tag_external: 'outward-facing — yours to approve',
|
|
70
|
+
denied_tag_feedback: 'customer feedback — yours to approve',
|
|
71
|
+
denied_tag_voice: 'voice/comms — yours to approve',
|
|
72
|
+
needs_second_reviewer_or_third_pass: 'needs one more independent check first',
|
|
73
|
+
not_agent_certified: 'not certified yet',
|
|
74
|
+
insufficient_review_passes: 'not enough review passes yet',
|
|
75
|
+
strict_verify_missing: 'no recorded check command to re-run',
|
|
76
|
+
verify_failed: 'its check command failed on re-run',
|
|
77
|
+
proof_unmerged_or_draft_pr_boundary: 'its proof points at an unmerged draft',
|
|
78
|
+
};
|
|
79
|
+
return map[reason] || reason.replace(/_/g, ' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function showStatus(root, args) {
|
|
83
|
+
const json = args.includes('--json');
|
|
84
|
+
const policy = autoland.readPolicy(root);
|
|
85
|
+
const enabled = Boolean(policy && policy.enabled);
|
|
86
|
+
const strictVerify = policy ? policy.strict_verify !== false : true;
|
|
87
|
+
const results = evaluateQueue(root, { strictVerify })
|
|
88
|
+
.filter((r) => r.reason !== 'not_in_review');
|
|
89
|
+
const wouldLand = results.filter((r) => r.action === 'would_accept');
|
|
90
|
+
const blocked = results.filter((r) => r.action !== 'would_accept');
|
|
91
|
+
const tasks = readProjection(root);
|
|
92
|
+
const waiting = autoland.waitingOnHuman(tasks);
|
|
93
|
+
|
|
94
|
+
if (json) {
|
|
95
|
+
console.log(JSON.stringify({
|
|
96
|
+
enabled,
|
|
97
|
+
policy,
|
|
98
|
+
heartbeat_installed: autoland.cronInstalled(root),
|
|
99
|
+
would_land: wouldLand,
|
|
100
|
+
blocked,
|
|
101
|
+
waiting_on_human: waiting,
|
|
102
|
+
}, null, 2));
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(`autoland — certified work lands itself; you keep the irreversible calls`);
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(` policy: ${enabled ? `on (flipped by ${policy.enabled_by}${policy.enabled_at ? ` on ${String(policy.enabled_at).slice(0, 10)}` : ''})` : 'off — everything waits for you'}`);
|
|
110
|
+
console.log(` heartbeat: ${autoland.cronInstalled(root) ? 'running hourly' : 'not installed'}`);
|
|
111
|
+
if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
|
|
112
|
+
console.log('');
|
|
113
|
+
if (wouldLand.length > 0) {
|
|
114
|
+
console.log(` ready to land on their own: ${wouldLand.length}`);
|
|
115
|
+
for (const r of wouldLand.slice(0, 10)) console.log(` lands itself ${r.ref}`);
|
|
116
|
+
} else {
|
|
117
|
+
console.log(' nothing is ready to land on its own right now.');
|
|
118
|
+
}
|
|
119
|
+
const humanOnly = blocked.filter((r) => String(r.reason || '').startsWith('denied_tag_'));
|
|
120
|
+
const needsWork = blocked.filter((r) => !String(r.reason || '').startsWith('denied_tag_'));
|
|
121
|
+
if (humanOnly.length > 0) {
|
|
122
|
+
console.log(` yours to approve (protected lanes): ${humanOnly.length}`);
|
|
123
|
+
for (const r of humanOnly.slice(0, 10)) console.log(` waits for you ${r.ref} — ${plainReason(r.reason)}`);
|
|
124
|
+
}
|
|
125
|
+
if (needsWork.length > 0) {
|
|
126
|
+
console.log(` not ready yet: ${needsWork.length}`);
|
|
127
|
+
for (const r of needsWork.slice(0, 10)) console.log(` held back ${r.ref} — ${plainReason(String(r.reason || ''))}`);
|
|
128
|
+
}
|
|
129
|
+
if (waiting.length > 0) {
|
|
130
|
+
console.log('');
|
|
131
|
+
console.log(` waiting on a human right now: ${waiting.length}, oldest ${waiting[0].hours}h`);
|
|
132
|
+
}
|
|
133
|
+
console.log('');
|
|
134
|
+
if (!enabled) console.log(" flip it on: atris autoland on (one decision; everything after is receipts)");
|
|
135
|
+
console.log('');
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function turnOn(root, args) {
|
|
140
|
+
const owner = flag(args, '--as', os.userInfo().username);
|
|
141
|
+
const to = flag(args, '--to', '');
|
|
142
|
+
const digestHour = Number(flag(args, '--digest-hour', autoland.DEFAULT_DIGEST_HOUR));
|
|
143
|
+
const alarmHours = Number(flag(args, '--alarm-hours', autoland.DEFAULT_ALARM_HOURS));
|
|
144
|
+
const policy = autoland.writePolicy(root, {
|
|
145
|
+
enabled: true,
|
|
146
|
+
enabled_by: owner,
|
|
147
|
+
enabled_at: new Date().toISOString(),
|
|
148
|
+
imessage_to: to || null,
|
|
149
|
+
digest_hour: Number.isFinite(digestHour) ? digestHour : autoland.DEFAULT_DIGEST_HOUR,
|
|
150
|
+
alarm_hours: Number.isFinite(alarmHours) && alarmHours > 0 ? alarmHours : autoland.DEFAULT_ALARM_HOURS,
|
|
151
|
+
strict_verify: !args.includes('--no-strict-verify'),
|
|
152
|
+
});
|
|
153
|
+
const cronOk = autoland.installCron(root);
|
|
154
|
+
console.log('');
|
|
155
|
+
console.log('autoland is on.');
|
|
156
|
+
console.log(` certified, verified, reversible work now lands itself, accepted as ${owner}.`);
|
|
157
|
+
console.log(' protected lanes (money, deploys, security, customer, outward) still wait for you.');
|
|
158
|
+
console.log(` heartbeat: ${cronOk ? 'installed, runs hourly' : 'could not install cron — run atris autoland tick yourself'}`);
|
|
159
|
+
if (policy.imessage_to) {
|
|
160
|
+
console.log(` daily message to ${policy.imessage_to} at ${policy.digest_hour}:00; anything waiting on you past ${policy.alarm_hours}h pings you.`);
|
|
161
|
+
} else {
|
|
162
|
+
console.log(' no phone number set — digest goes to the log only. add one: atris autoland on --to <your number>');
|
|
163
|
+
}
|
|
164
|
+
console.log(' turn it off any time: atris autoland off');
|
|
165
|
+
console.log('');
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function turnOff(root) {
|
|
170
|
+
const policy = autoland.readPolicy(root) || {};
|
|
171
|
+
autoland.writePolicy(root, { ...policy, enabled: false, disabled_at: new Date().toISOString() });
|
|
172
|
+
const cronOk = autoland.uninstallCron(root);
|
|
173
|
+
console.log('');
|
|
174
|
+
console.log('autoland is off. everything waits for your accept again.');
|
|
175
|
+
console.log(` heartbeat ${cronOk ? 'removed' : 'removal failed — check crontab -l'}.`);
|
|
176
|
+
console.log('');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function runDigest(root, args, { forceSend = false } = {}) {
|
|
181
|
+
const policy = autoland.readPolicy(root) || {};
|
|
182
|
+
const tasks = readProjection(root);
|
|
183
|
+
const accepted = autoland.acceptedInLastDay(tasks);
|
|
184
|
+
const text = autoland.composeDigest({
|
|
185
|
+
accepted,
|
|
186
|
+
waiting: autoland.waitingOnHuman(tasks),
|
|
187
|
+
landed: landSummarySafe(root),
|
|
188
|
+
project: projectName(root),
|
|
189
|
+
});
|
|
190
|
+
console.log(text);
|
|
191
|
+
// the full story: what each piece actually was, in its own words
|
|
192
|
+
const byRef = new Map(tasks.map((t) => [t.display_id || t.legacy_ref || t.id, t]));
|
|
193
|
+
const storied = accepted.auto.filter((item) => {
|
|
194
|
+
const t = byRef.get(item.ref);
|
|
195
|
+
return t && (t.review?.landing?.happened || t.metadata?.landing_happened);
|
|
196
|
+
});
|
|
197
|
+
if (storied.length > 0) {
|
|
198
|
+
console.log('');
|
|
199
|
+
for (const item of storied) {
|
|
200
|
+
const t = byRef.get(item.ref);
|
|
201
|
+
const happened = String(t.review?.landing?.happened || t.metadata?.landing_happened || '').replace(/\s+/g, ' ').slice(0, 160);
|
|
202
|
+
console.log(` ${item.ref} ${happened}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const shouldSend = (forceSend || args.includes('--send')) && policy.imessage_to;
|
|
206
|
+
if (shouldSend) {
|
|
207
|
+
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
208
|
+
console.log(sent.ok ? `(sent to ${policy.imessage_to})` : `(send failed: ${sent.output})`);
|
|
209
|
+
return sent.ok ? 0 : 1;
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function runTick(root, args) {
|
|
215
|
+
const json = args.includes('--json');
|
|
216
|
+
const policy = autoland.readPolicy(root);
|
|
217
|
+
const receipt = { at: new Date().toISOString(), landed: [], alarms: 0, digest_sent: false, enabled: Boolean(policy && policy.enabled) };
|
|
218
|
+
if (!policy || policy.enabled !== true) {
|
|
219
|
+
if (json) console.log(JSON.stringify(receipt));
|
|
220
|
+
else console.log('autoland is off; tick did nothing.');
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 1. land what is eligible — the policy is the standing authorization
|
|
225
|
+
const cliArgs = ['task', 'auto-accept-certified', '--json', '--limit', '12'];
|
|
226
|
+
if (policy.strict_verify !== false) cliArgs.push('--strict-verify');
|
|
227
|
+
const accept = runOwnCli(root, cliArgs);
|
|
228
|
+
try {
|
|
229
|
+
const parsed = JSON.parse(accept.stdout);
|
|
230
|
+
receipt.landed = (parsed.results || []).filter((r) => r.action === 'accepted').map((r) => r.ref);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
receipt.accept_error = accept.stderr.slice(0, 200) || 'auto-accept output unreadable';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 2. alarm on anything waiting on a human past the line
|
|
236
|
+
const state = autoland.readState(root);
|
|
237
|
+
const tasks = readProjection(root);
|
|
238
|
+
const waiting = autoland.waitingOnHuman(tasks);
|
|
239
|
+
const alarmHours = Number(policy.alarm_hours) || autoland.DEFAULT_ALARM_HOURS;
|
|
240
|
+
const due = autoland.dueForAlarm(waiting, state, { alarmHours });
|
|
241
|
+
if (due.length > 0 && policy.imessage_to) {
|
|
242
|
+
const text = autoland.composeAlarm({ waiting: due, project: projectName(root), alarmHours });
|
|
243
|
+
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
244
|
+
if (sent.ok) {
|
|
245
|
+
autoland.markAlerted(state, due);
|
|
246
|
+
receipt.alarms = due.length;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 3. daily digest at the configured hour
|
|
251
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
252
|
+
const digestHour = Number(policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR);
|
|
253
|
+
if (new Date().getHours() === digestHour && state.last_digest_date !== today) {
|
|
254
|
+
const text = autoland.composeDigest({
|
|
255
|
+
accepted: autoland.acceptedInLastDay(tasks),
|
|
256
|
+
waiting,
|
|
257
|
+
landed: landSummarySafe(root),
|
|
258
|
+
project: projectName(root),
|
|
259
|
+
});
|
|
260
|
+
if (policy.imessage_to) {
|
|
261
|
+
const sent = autoland.sendImessage(root, policy.imessage_to, text);
|
|
262
|
+
receipt.digest_sent = sent.ok;
|
|
263
|
+
}
|
|
264
|
+
receipt.digest_text = text;
|
|
265
|
+
state.last_digest_date = today;
|
|
266
|
+
|
|
267
|
+
// 4. once a day, keep the receipt shelf lean: compress old run receipts
|
|
268
|
+
// into the manifest and drop unreferenced clutter, newest 200 kept.
|
|
269
|
+
const prune = runOwnCli(root, ['mission', 'prune-runs', '--apply', '--days', '14', '--keep-newest', '200', '--json']);
|
|
270
|
+
try {
|
|
271
|
+
const pruned = JSON.parse(prune.stdout);
|
|
272
|
+
receipt.receipts_pruned = pruned.pruned_count ?? pruned.pruned ?? pruned.removed ?? 0;
|
|
273
|
+
} catch {
|
|
274
|
+
receipt.receipts_pruned = null;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
autoland.writeState(root, state);
|
|
278
|
+
|
|
279
|
+
if (json) console.log(JSON.stringify(receipt));
|
|
280
|
+
else {
|
|
281
|
+
console.log(`autoland tick: ${receipt.landed.length} landed${receipt.landed.length ? ` (${receipt.landed.join(', ')})` : ''}, ${receipt.alarms} alarms, digest ${receipt.digest_sent ? 'sent' : 'not due'}`);
|
|
282
|
+
}
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function showHelp() {
|
|
287
|
+
console.log('');
|
|
288
|
+
console.log('atris autoland — you approve the policy once; certified work lands itself');
|
|
289
|
+
console.log('');
|
|
290
|
+
console.log('finished work that passed its checks and two independent reviews lands');
|
|
291
|
+
console.log('automatically with a receipt. money, deploys, security, customer, and');
|
|
292
|
+
console.log('outward-facing work always waits for you.');
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(' atris autoland what would land, what waits for you');
|
|
295
|
+
console.log(' atris autoland on [--to <phone>] flip it on: hourly heartbeat, daily');
|
|
296
|
+
console.log(' message, ping when something waits');
|
|
297
|
+
console.log(' on you past 24h');
|
|
298
|
+
console.log(' atris autoland off back to approving every item');
|
|
299
|
+
console.log(' atris autoland digest [--send] the daily message, now');
|
|
300
|
+
console.log(' atris autoland tick one heartbeat (what the hourly cron runs)');
|
|
301
|
+
console.log('');
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function autolandCommand(args = []) {
|
|
306
|
+
const [sub, ...rest] = args;
|
|
307
|
+
if (!sub || sub.startsWith('--')) return showStatus(repoRoot(), args);
|
|
308
|
+
if (sub === 'help' || sub === '--help' || sub === '-h') return showHelp();
|
|
309
|
+
const root = repoRoot();
|
|
310
|
+
if (sub === 'on') return turnOn(root, rest);
|
|
311
|
+
if (sub === 'off') return turnOff(root);
|
|
312
|
+
if (sub === 'tick') return runTick(root, rest);
|
|
313
|
+
if (sub === 'digest') return runDigest(root, rest);
|
|
314
|
+
if (sub === 'status') return showStatus(root, rest);
|
|
315
|
+
console.error(`atris autoland: unknown subcommand '${sub}' (try: atris autoland help)`);
|
|
316
|
+
return 1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { autolandCommand };
|
package/commands/autopilot.js
CHANGED
|
@@ -15,7 +15,7 @@ const { parseTodo } = require('../lib/todo');
|
|
|
15
15
|
const {
|
|
16
16
|
buildRunnerCommand,
|
|
17
17
|
buildRunnerAvailabilityCommand,
|
|
18
|
-
|
|
18
|
+
runnerAvailabilityFailureMessage,
|
|
19
19
|
} = require('../lib/runner-command');
|
|
20
20
|
const { findStalePages, findStaleTasks, healBrokenMapRefs } = require('./clean');
|
|
21
21
|
const {
|
|
@@ -484,6 +484,91 @@ function executePhaseDetailed(phase, context, options = {}) {
|
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
function autopilotGlassLogDir(cwd = process.cwd()) {
|
|
488
|
+
const dir = path.join(cwd, 'atris', 'logs', 'autopilot');
|
|
489
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
490
|
+
return dir;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function autopilotGlassSlug(value) {
|
|
494
|
+
const slug = String(value || 'tick')
|
|
495
|
+
.toLowerCase()
|
|
496
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
497
|
+
.replace(/^-+|-+$/g, '')
|
|
498
|
+
.slice(0, 64);
|
|
499
|
+
return slug || 'tick';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function autopilotGlassTimestamp(now = new Date()) {
|
|
503
|
+
return now.toISOString().replace(/[:.]/g, '-');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function phaseGlassText(phaseResults, key) {
|
|
507
|
+
const phase = phaseResults && phaseResults[key] ? phaseResults[key] : {};
|
|
508
|
+
const output = String(phase.output || '').trim();
|
|
509
|
+
const elapsed = Number.isFinite(phase.elapsedSeconds) ? `\n\nElapsed: ${phase.elapsedSeconds}s` : '';
|
|
510
|
+
return `${output || '(no output)'}${elapsed}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function buildAutopilotGlassLog(context, execution, options = {}) {
|
|
514
|
+
const phaseResults = execution.phaseResults || {};
|
|
515
|
+
const task = context.task || 'Autopilot tick';
|
|
516
|
+
const generatedAt = options.generatedAt || new Date().toISOString();
|
|
517
|
+
const proof = execution.verifyRan
|
|
518
|
+
? `${execution.verifyPass ? 'PASS' : 'FAIL'}: ${phaseGlassText(phaseResults, 'verify')}`
|
|
519
|
+
: 'NOT RUN: no verifier command was available';
|
|
520
|
+
|
|
521
|
+
return [
|
|
522
|
+
'# Autopilot Glass Log',
|
|
523
|
+
'',
|
|
524
|
+
`Generated: ${generatedAt}`,
|
|
525
|
+
`Task: ${task}`,
|
|
526
|
+
`Kind: ${context.kind || 'unknown'}`,
|
|
527
|
+
`Verify: ${execution.verifyCmd || '(none)'}`,
|
|
528
|
+
`Outcome: ${execution.success ? 'success' : 'needs attention'}`,
|
|
529
|
+
'',
|
|
530
|
+
'## Plan',
|
|
531
|
+
'',
|
|
532
|
+
phaseGlassText(phaseResults, 'plan'),
|
|
533
|
+
'',
|
|
534
|
+
'## Plan Review',
|
|
535
|
+
'',
|
|
536
|
+
phaseGlassText(phaseResults, 'plan-review'),
|
|
537
|
+
'',
|
|
538
|
+
'## Action',
|
|
539
|
+
'',
|
|
540
|
+
phaseGlassText(phaseResults, 'do'),
|
|
541
|
+
'',
|
|
542
|
+
'## Result',
|
|
543
|
+
'',
|
|
544
|
+
phaseGlassText(phaseResults, 'review'),
|
|
545
|
+
'',
|
|
546
|
+
'## Proof',
|
|
547
|
+
'',
|
|
548
|
+
proof,
|
|
549
|
+
''
|
|
550
|
+
].join('\n');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function writeAutopilotGlassLog(cwd, context, execution, options = {}) {
|
|
554
|
+
const dir = autopilotGlassLogDir(cwd);
|
|
555
|
+
const now = options.now || new Date();
|
|
556
|
+
const file = `${autopilotGlassTimestamp(now)}-${autopilotGlassSlug(context.task)}.md`;
|
|
557
|
+
const absPath = path.join(dir, file);
|
|
558
|
+
fs.writeFileSync(absPath, buildAutopilotGlassLog(context, execution, {
|
|
559
|
+
generatedAt: now.toISOString(),
|
|
560
|
+
}));
|
|
561
|
+
return path.relative(cwd, absPath);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function maybeWriteAutopilotGlassLog(cwd, context, execution) {
|
|
565
|
+
try {
|
|
566
|
+
return writeAutopilotGlassLog(cwd, context, execution);
|
|
567
|
+
} catch {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
487
572
|
/**
|
|
488
573
|
* Build context-aware file list for prompts.
|
|
489
574
|
*/
|
|
@@ -1806,7 +1891,7 @@ function runTaskOnce(context, options = {}) {
|
|
|
1806
1891
|
}
|
|
1807
1892
|
}
|
|
1808
1893
|
|
|
1809
|
-
|
|
1894
|
+
const execution = {
|
|
1810
1895
|
success: verifyRan && verifyPass,
|
|
1811
1896
|
elapsedSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
1812
1897
|
phaseResults,
|
|
@@ -1815,6 +1900,8 @@ function runTaskOnce(context, options = {}) {
|
|
|
1815
1900
|
verifyPass,
|
|
1816
1901
|
verifyRan,
|
|
1817
1902
|
};
|
|
1903
|
+
execution.glassLogPath = maybeWriteAutopilotGlassLog(cwd, context, execution);
|
|
1904
|
+
return execution;
|
|
1818
1905
|
}
|
|
1819
1906
|
|
|
1820
1907
|
/**
|
|
@@ -3022,8 +3109,8 @@ async function autopilotAtris(description, options = {}) {
|
|
|
3022
3109
|
process.exit(1);
|
|
3023
3110
|
}
|
|
3024
3111
|
|
|
3025
|
-
try { execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' }); } catch {
|
|
3026
|
-
console.error(
|
|
3112
|
+
try { execSync(buildRunnerAvailabilityCommand(), { stdio: 'pipe' }); } catch (err) {
|
|
3113
|
+
console.error(runnerAvailabilityFailureMessage(err));
|
|
3027
3114
|
process.exit(1);
|
|
3028
3115
|
}
|
|
3029
3116
|
|
|
@@ -3679,5 +3766,8 @@ module.exports = {
|
|
|
3679
3766
|
isPhaseKillError,
|
|
3680
3767
|
execPhaseCommandSync,
|
|
3681
3768
|
executePhaseDetailed,
|
|
3769
|
+
autopilotGlassLogDir,
|
|
3770
|
+
buildAutopilotGlassLog,
|
|
3771
|
+
writeAutopilotGlassLog,
|
|
3682
3772
|
lessonSlug
|
|
3683
3773
|
};
|
package/commands/business.js
CHANGED
|
@@ -840,7 +840,7 @@ function renderBusinessOsLines(os = {}, prefix = '- ') {
|
|
|
840
840
|
`${prefix}Team goals: ${team.members || 0} member lanes, ${team.activeGoalMembers || 0} with active goals`,
|
|
841
841
|
`${prefix}${xp.metric || 'AgentXP'}: ${xp.total || 0} total, ${xp.today || 0} today, ${xp.receipts || 0} receipts, integrity ${xp.integrity || 'unknown'}`,
|
|
842
842
|
`${prefix}Loop: ${loop.ticks || 0} mission ticks; Codex goal ${loop.codexGoal || 'none'}`,
|
|
843
|
-
`${prefix}XP gate: proof can move to Review; XP
|
|
843
|
+
`${prefix}XP gate: proof can move to Review; XP is awarded only after human approval`,
|
|
844
844
|
];
|
|
845
845
|
}
|
|
846
846
|
|
package/commands/clean.js
CHANGED
|
@@ -15,20 +15,23 @@ function cleanAtris(options = {}) {
|
|
|
15
15
|
const atrisDir = path.join(cwd, 'atris');
|
|
16
16
|
|
|
17
17
|
if (!fs.existsSync(atrisDir)) {
|
|
18
|
+
if (options.json) {
|
|
19
|
+
console.log(JSON.stringify({
|
|
20
|
+
ok: false,
|
|
21
|
+
action: 'clean',
|
|
22
|
+
error: 'atris/ folder not found. Run "atris init" first.',
|
|
23
|
+
}, null, 2));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
18
26
|
console.log('✗ atris/ folder not found. Run "atris init" first.');
|
|
19
27
|
process.exit(1);
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
console.log('');
|
|
23
|
-
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
24
|
-
console.log('│ Atris Clean │');
|
|
25
|
-
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
26
|
-
console.log('');
|
|
27
|
-
|
|
28
30
|
const results = {
|
|
29
31
|
staleTasks: [],
|
|
30
32
|
brokenRefs: [],
|
|
31
33
|
healedRefs: 0,
|
|
34
|
+
healedRefDetails: [],
|
|
32
35
|
unhealableRefs: [],
|
|
33
36
|
archivedJournals: 0,
|
|
34
37
|
cleanedSections: 0
|
|
@@ -39,8 +42,9 @@ function cleanAtris(options = {}) {
|
|
|
39
42
|
results.staleTasks = staleTasks;
|
|
40
43
|
|
|
41
44
|
// 2. Find and HEAL broken MAP.md references
|
|
42
|
-
const { healed, unhealable } = healBrokenMapRefs(cwd, atrisDir, options.dryRun);
|
|
45
|
+
const { healed, unhealable, replacements } = healBrokenMapRefs(cwd, atrisDir, options.dryRun);
|
|
43
46
|
results.healedRefs = healed;
|
|
47
|
+
results.healedRefDetails = replacements;
|
|
44
48
|
results.unhealableRefs = unhealable;
|
|
45
49
|
|
|
46
50
|
// 3. Archive old journals (>30 days)
|
|
@@ -55,6 +59,17 @@ function cleanAtris(options = {}) {
|
|
|
55
59
|
const stalePages = findStalePages(cwd, atrisDir);
|
|
56
60
|
results.stalePages = stalePages;
|
|
57
61
|
|
|
62
|
+
if (options.json) {
|
|
63
|
+
console.log(JSON.stringify(cleanResultPayload(results, options, cwd), null, 2));
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
console.log('┌─────────────────────────────────────────────────────────────┐');
|
|
69
|
+
console.log('│ Atris Clean │');
|
|
70
|
+
console.log('└─────────────────────────────────────────────────────────────┘');
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
58
73
|
// Report results
|
|
59
74
|
console.log('Results:');
|
|
60
75
|
console.log('');
|
|
@@ -74,6 +89,12 @@ function cleanAtris(options = {}) {
|
|
|
74
89
|
if (healed > 0) {
|
|
75
90
|
const verb = options.dryRun ? 'Would heal' : 'Healed';
|
|
76
91
|
console.log(`✓ ${verb} ${healed} MAP.md ${healed === 1 ? 'reference' : 'references'}`);
|
|
92
|
+
(results.healedRefDetails || []).slice(0, 3).forEach(ref => {
|
|
93
|
+
console.log(` • ${ref.old} -> ${ref.new}${ref.symbol ? ` (${ref.symbol})` : ''}`);
|
|
94
|
+
});
|
|
95
|
+
if ((results.healedRefDetails || []).length > 3) {
|
|
96
|
+
console.log(` ... and ${results.healedRefDetails.length - 3} more`);
|
|
97
|
+
}
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
// Unhealable refs
|
|
@@ -138,6 +159,47 @@ function cleanAtris(options = {}) {
|
|
|
138
159
|
return results;
|
|
139
160
|
}
|
|
140
161
|
|
|
162
|
+
function cleanResultPayload(results, options = {}, cwd = process.cwd()) {
|
|
163
|
+
const manualAction = [];
|
|
164
|
+
if (results.staleTasks.length > 0) manualAction.push('Delete stale tasks or finish them');
|
|
165
|
+
if (results.unhealableRefs.length > 0) manualAction.push('Manually fix unhealable MAP.md refs');
|
|
166
|
+
if (results.stalePages.length > 0) manualAction.push('Re-compile stale pages');
|
|
167
|
+
return {
|
|
168
|
+
ok: manualAction.length === 0,
|
|
169
|
+
action: 'clean',
|
|
170
|
+
dry_run: !!options.dryRun,
|
|
171
|
+
results: {
|
|
172
|
+
stale_tasks: {
|
|
173
|
+
count: results.staleTasks.length,
|
|
174
|
+
items: results.staleTasks,
|
|
175
|
+
},
|
|
176
|
+
map_refs: {
|
|
177
|
+
healed_count: results.healedRefs,
|
|
178
|
+
verb: options.dryRun ? 'would_heal' : 'healed',
|
|
179
|
+
items: results.healedRefDetails || [],
|
|
180
|
+
unhealable_count: results.unhealableRefs.length,
|
|
181
|
+
unhealable: results.unhealableRefs,
|
|
182
|
+
},
|
|
183
|
+
journals: {
|
|
184
|
+
archived_count: results.archivedJournals,
|
|
185
|
+
verb: options.dryRun ? 'would_archive' : 'archived',
|
|
186
|
+
},
|
|
187
|
+
sections: {
|
|
188
|
+
cleaned_count: results.cleanedSections,
|
|
189
|
+
verb: options.dryRun ? 'would_clean' : 'cleaned',
|
|
190
|
+
},
|
|
191
|
+
stale_pages: {
|
|
192
|
+
count: results.stalePages.length,
|
|
193
|
+
items: results.stalePages.map(page => ({
|
|
194
|
+
page: path.relative(cwd, page.page),
|
|
195
|
+
stale_source: page.staleSource,
|
|
196
|
+
})),
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
manual_action: manualAction,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
141
203
|
/**
|
|
142
204
|
* Find tasks claimed but not completed after 3+ days
|
|
143
205
|
*/
|
|
@@ -295,7 +357,7 @@ function healBrokenMapRefs(cwd, atrisDir, dryRun = false) {
|
|
|
295
357
|
fs.writeFileSync(mapFile, mapContent);
|
|
296
358
|
}
|
|
297
359
|
|
|
298
|
-
return { healed, unhealable, replacements
|
|
360
|
+
return { healed, unhealable, replacements };
|
|
299
361
|
}
|
|
300
362
|
|
|
301
363
|
/**
|
|
@@ -337,7 +399,7 @@ function findFunctionEnd(lines, startLine) {
|
|
|
337
399
|
}
|
|
338
400
|
|
|
339
401
|
/**
|
|
340
|
-
* Extract a symbol name from context like "(
|
|
402
|
+
* Extract a symbol name from context like "(interactiveEntry function)" or "— Main entry"
|
|
341
403
|
*/
|
|
342
404
|
function extractSymbol(context) {
|
|
343
405
|
if (!context) return null;
|
|
@@ -568,6 +630,7 @@ function cleanEmptySections(atrisDir, dryRun = false) {
|
|
|
568
630
|
|
|
569
631
|
module.exports = {
|
|
570
632
|
cleanAtris,
|
|
633
|
+
cleanResultPayload,
|
|
571
634
|
findStaleTasks,
|
|
572
635
|
healBrokenMapRefs,
|
|
573
636
|
archiveOldJournals,
|