atris 3.30.8 → 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 +4 -2
- 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 +367 -93
- package/bin/atris.js +317 -155
- 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 +3659 -282
- 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/mission-runtime-loop.js +320 -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
package/commands/land.js
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { spawnSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const { listWorktrees, statusCounts } = require('./worktree');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TTL_DAYS = 7;
|
|
8
|
+
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
9
|
+
|
|
10
|
+
function runGit(args, { cwd = process.cwd(), check = true } = {}) {
|
|
11
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
12
|
+
if (check && result.status !== 0) {
|
|
13
|
+
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function repoRoot(cwd = process.cwd()) {
|
|
19
|
+
const result = runGit(['rev-parse', '--show-toplevel'], { cwd, check: false });
|
|
20
|
+
if (result.status !== 0) return '';
|
|
21
|
+
return result.stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function refExists(root, ref) {
|
|
25
|
+
return runGit(['rev-parse', '--verify', '--quiet', ref], { cwd: root, check: false }).status === 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function baseBranch(root, override = '') {
|
|
29
|
+
if (override && refExists(root, override)) return override;
|
|
30
|
+
if (refExists(root, 'master')) return 'master';
|
|
31
|
+
if (refExists(root, 'main')) return 'main';
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function ageDays(unixSeconds, now = Date.now()) {
|
|
36
|
+
return Math.floor((now / 1000 - Number(unixSeconds)) / 86400);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function listBranches(root) {
|
|
40
|
+
const result = runGit(
|
|
41
|
+
['for-each-ref', 'refs/heads', '--format=%(refname:short)%09%(committerdate:unix)'],
|
|
42
|
+
{ cwd: root, check: false }
|
|
43
|
+
);
|
|
44
|
+
if (result.status !== 0) return [];
|
|
45
|
+
return result.stdout
|
|
46
|
+
.split(/\r?\n/)
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((line) => {
|
|
49
|
+
const [name, date] = line.split('\t');
|
|
50
|
+
return { name, lastCommitUnix: Number(date) };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function aheadCount(root, base, ref) {
|
|
55
|
+
const result = runGit(['rev-list', '--count', `${base}..${ref}`], { cwd: root, check: false });
|
|
56
|
+
if (result.status !== 0) return 0;
|
|
57
|
+
return Number(result.stdout.trim()) || 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Board: every branch and worktree with unlanded state, classified.
|
|
61
|
+
// landed — no commits ahead of base; the branch pointer is residue
|
|
62
|
+
// active — has unlanded commits, younger than TTL
|
|
63
|
+
// due — has unlanded commits, older than TTL; --reap collects it
|
|
64
|
+
function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '' } = {}) {
|
|
65
|
+
const base = baseBranch(root, baseOverride);
|
|
66
|
+
if (!base) throw new Error('no master/main branch found');
|
|
67
|
+
|
|
68
|
+
const branches = [];
|
|
69
|
+
for (const branch of listBranches(root)) {
|
|
70
|
+
if (PROTECTED_BRANCHES.has(branch.name)) continue;
|
|
71
|
+
const ahead = aheadCount(root, base, branch.name);
|
|
72
|
+
const age = ageDays(branch.lastCommitUnix);
|
|
73
|
+
let state = 'active';
|
|
74
|
+
if (ahead === 0) state = 'landed';
|
|
75
|
+
else if (age > ttlDays) state = 'due';
|
|
76
|
+
branches.push({ name: branch.name, ahead, ageDays: age, state });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const worktrees = [];
|
|
80
|
+
const all = listWorktrees(root);
|
|
81
|
+
for (const wt of all.slice(1)) {
|
|
82
|
+
const branch = (wt.branch || '').replace(/^refs\/heads\//, '');
|
|
83
|
+
const counts = statusCounts(wt.path) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
84
|
+
const dirty = counts.staged + counts.unstaged + counts.untracked;
|
|
85
|
+
const entry = branches.find((b) => b.name === branch) || null;
|
|
86
|
+
worktrees.push({
|
|
87
|
+
path: wt.path,
|
|
88
|
+
branch,
|
|
89
|
+
dirty,
|
|
90
|
+
ageDays: entry ? entry.ageDays : null,
|
|
91
|
+
state: entry ? entry.state : 'detached',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const due = branches.filter((b) => b.state === 'due');
|
|
96
|
+
const landed = branches.filter((b) => b.state === 'landed');
|
|
97
|
+
const active = branches.filter((b) => b.state === 'active');
|
|
98
|
+
return {
|
|
99
|
+
base,
|
|
100
|
+
ttlDays,
|
|
101
|
+
branches,
|
|
102
|
+
worktrees,
|
|
103
|
+
summary: {
|
|
104
|
+
unlanded: branches.length,
|
|
105
|
+
active: active.length,
|
|
106
|
+
due: due.length,
|
|
107
|
+
landed: landed.length,
|
|
108
|
+
worktrees: worktrees.length,
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fast counts for the boot banner: no per-branch ahead checks.
|
|
114
|
+
function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
|
|
115
|
+
const root = repoRoot(cwd);
|
|
116
|
+
if (!root) return null;
|
|
117
|
+
let branches = 0;
|
|
118
|
+
let due = 0;
|
|
119
|
+
for (const branch of listBranches(root)) {
|
|
120
|
+
if (PROTECTED_BRANCHES.has(branch.name)) continue;
|
|
121
|
+
branches += 1;
|
|
122
|
+
if (ageDays(branch.lastCommitUnix) > ttlDays) due += 1;
|
|
123
|
+
}
|
|
124
|
+
return { branches, due, ttlDays };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function salvageDir(root) {
|
|
128
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
129
|
+
const dir = path.join(root, '.atris', 'salvage', stamp);
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
return dir;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function timeStamp() {
|
|
135
|
+
return new Date().toISOString().slice(11, 19).replace(/:/g, '');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function remoteHeads(root) {
|
|
139
|
+
const result = runGit(['ls-remote', '--heads', 'origin'], { cwd: root, check: false });
|
|
140
|
+
if (result.status !== 0) return null;
|
|
141
|
+
const heads = new Set();
|
|
142
|
+
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
143
|
+
const ref = line.split(/\s+/)[1] || '';
|
|
144
|
+
if (ref.startsWith('refs/heads/')) heads.add(ref.slice('refs/heads/'.length));
|
|
145
|
+
}
|
|
146
|
+
return heads;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Reap: salvage then delete everything landed (residue) or past TTL.
|
|
150
|
+
// Salvage first, always: unlanded commits go into a git bundle, dirty
|
|
151
|
+
// worktrees into patch files, so a reap is never a loss of work.
|
|
152
|
+
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '', dryRun = false, remote = true } = {}) {
|
|
153
|
+
const board = collectBoard(root, { ttlDays, base: baseOverride });
|
|
154
|
+
const targets = board.branches.filter((b) => b.state === 'landed' || b.state === 'due');
|
|
155
|
+
const targetNames = new Set(targets.map((b) => b.name));
|
|
156
|
+
const worktreeTargets = board.worktrees.filter(
|
|
157
|
+
(w) => targetNames.has(w.branch) || w.state === 'detached' || (typeof w.ageDays === 'number' && w.ageDays > ttlDays)
|
|
158
|
+
);
|
|
159
|
+
for (const w of worktreeTargets) {
|
|
160
|
+
if (w.branch && !targetNames.has(w.branch)) targetNames.add(w.branch);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const receipt = {
|
|
164
|
+
base: board.base,
|
|
165
|
+
ttlDays,
|
|
166
|
+
dryRun,
|
|
167
|
+
bundle: null,
|
|
168
|
+
patches: [],
|
|
169
|
+
removedWorktrees: [],
|
|
170
|
+
deletedBranches: [],
|
|
171
|
+
deletedRemote: [],
|
|
172
|
+
kept: board.branches.filter((b) => !targetNames.has(b.name)).map((b) => b.name),
|
|
173
|
+
};
|
|
174
|
+
if (targetNames.size === 0 && worktreeTargets.length === 0) return receipt;
|
|
175
|
+
if (dryRun) {
|
|
176
|
+
receipt.removedWorktrees = worktreeTargets.map((w) => w.path);
|
|
177
|
+
receipt.deletedBranches = [...targetNames];
|
|
178
|
+
return receipt;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const dir = salvageDir(root);
|
|
182
|
+
const withCommits = [...targetNames].filter((name) => {
|
|
183
|
+
const entry = board.branches.find((b) => b.name === name);
|
|
184
|
+
return entry && entry.ahead > 0;
|
|
185
|
+
});
|
|
186
|
+
if (withCommits.length > 0) {
|
|
187
|
+
const bundlePath = path.join(dir, `reap-${timeStamp()}.bundle`);
|
|
188
|
+
const result = runGit(['bundle', 'create', bundlePath, ...withCommits, '--not', board.base], {
|
|
189
|
+
cwd: root,
|
|
190
|
+
check: false,
|
|
191
|
+
});
|
|
192
|
+
if (result.status === 0) receipt.bundle = bundlePath;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const w of worktreeTargets) {
|
|
196
|
+
if (w.dirty > 0) {
|
|
197
|
+
const diff = runGit(['diff'], { cwd: w.path, check: false });
|
|
198
|
+
if (diff.status === 0 && diff.stdout.trim()) {
|
|
199
|
+
const patchPath = path.join(dir, `${path.basename(w.path)}.dirty.patch`);
|
|
200
|
+
fs.writeFileSync(patchPath, diff.stdout);
|
|
201
|
+
receipt.patches.push(patchPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const removed = runGit(['worktree', 'remove', '--force', w.path], { cwd: root, check: false });
|
|
205
|
+
if (removed.status === 0) receipt.removedWorktrees.push(w.path);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const name of targetNames) {
|
|
209
|
+
const deleted = runGit(['branch', '-D', name], { cwd: root, check: false });
|
|
210
|
+
if (deleted.status === 0) receipt.deletedBranches.push(name);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (remote && receipt.deletedBranches.length > 0) {
|
|
214
|
+
const heads = remoteHeads(root);
|
|
215
|
+
if (heads) {
|
|
216
|
+
const remoteTargets = receipt.deletedBranches.filter((name) => heads.has(name));
|
|
217
|
+
for (let i = 0; i < remoteTargets.length; i += 25) {
|
|
218
|
+
const batch = remoteTargets.slice(i, i + 25);
|
|
219
|
+
const pushed = runGit(['push', 'origin', '--delete', ...batch], { cwd: root, check: false });
|
|
220
|
+
if (pushed.status === 0) receipt.deletedRemote.push(...batch);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return receipt;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// The story of one piece of work: what it tried to do, whether it made it
|
|
229
|
+
// into master some other way, and what would be lost if it were cleared.
|
|
230
|
+
function collectStory(root, name, { base: baseOverride = '' } = {}) {
|
|
231
|
+
const base = baseBranch(root, baseOverride);
|
|
232
|
+
if (!base) throw new Error('no master/main branch found');
|
|
233
|
+
if (!refExists(root, name)) return null;
|
|
234
|
+
|
|
235
|
+
const logResult = runGit(
|
|
236
|
+
['log', '--format=%h%x09%cs%x09%an%x09%s', `${base}..${name}`],
|
|
237
|
+
{ cwd: root, check: false }
|
|
238
|
+
);
|
|
239
|
+
const changes = logResult.stdout
|
|
240
|
+
.split(/\r?\n/)
|
|
241
|
+
.filter(Boolean)
|
|
242
|
+
.map((line) => {
|
|
243
|
+
const [hash, date, author, subject] = line.split('\t');
|
|
244
|
+
return { hash, date, author, subject };
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// git cherry marks a change '-' when an equivalent patch already exists
|
|
248
|
+
// in base — the honest "it landed some other way" signal.
|
|
249
|
+
const cherry = runGit(['cherry', base, name], { cwd: root, check: false });
|
|
250
|
+
const landedHashes = new Set();
|
|
251
|
+
for (const line of cherry.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
252
|
+
if (line.startsWith('- ')) landedHashes.add(line.slice(2).trim());
|
|
253
|
+
}
|
|
254
|
+
const fullHashes = runGit(['log', '--format=%H %h', `${base}..${name}`], { cwd: root, check: false });
|
|
255
|
+
const shortToLanded = new Set();
|
|
256
|
+
for (const line of fullHashes.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
257
|
+
const [full, short] = line.split(' ');
|
|
258
|
+
if (landedHashes.has(full)) shortToLanded.add(short);
|
|
259
|
+
}
|
|
260
|
+
for (const c of changes) c.landedElsewhere = shortToLanded.has(c.hash);
|
|
261
|
+
|
|
262
|
+
const stat = runGit(['diff', '--stat', `${base}...${name}`], { cwd: root, check: false });
|
|
263
|
+
const statLines = stat.stdout.split(/\r?\n/).filter(Boolean);
|
|
264
|
+
const files = statLines.slice(0, -1).map((l) => l.split('|')[0].trim());
|
|
265
|
+
|
|
266
|
+
const info = listBranches(root).find((b) => b.name === name);
|
|
267
|
+
return {
|
|
268
|
+
name,
|
|
269
|
+
base,
|
|
270
|
+
ageDays: info ? ageDays(info.lastCommitUnix) : null,
|
|
271
|
+
changes,
|
|
272
|
+
landedElsewhere: changes.filter((c) => c.landedElsewhere).length,
|
|
273
|
+
uniqueChanges: changes.filter((c) => !c.landedElsewhere).length,
|
|
274
|
+
files,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function printStory(story) {
|
|
279
|
+
console.log('');
|
|
280
|
+
console.log(`what happened in ${story.name}`);
|
|
281
|
+
console.log('');
|
|
282
|
+
if (story.changes.length === 0) {
|
|
283
|
+
console.log(` everything here is already in ${story.base}. this is an empty leftover,`);
|
|
284
|
+
console.log(" safe to clear. nothing would be lost.");
|
|
285
|
+
console.log('');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const who = [...new Set(story.changes.map((c) => c.author))].join(', ');
|
|
289
|
+
const when = story.changes[story.changes.length - 1].date;
|
|
290
|
+
const last = story.changes[0].date;
|
|
291
|
+
const age = story.ageDays === null ? '' : ` (${story.ageDays} days ago)`;
|
|
292
|
+
console.log(` started ${when}, last touched ${last}${age}, by ${who}.`);
|
|
293
|
+
console.log('');
|
|
294
|
+
console.log(' what it tried to do:');
|
|
295
|
+
for (const c of [...story.changes].reverse()) {
|
|
296
|
+
const mark = c.landedElsewhere ? 'made it in ' : 'never made it';
|
|
297
|
+
console.log(` ${mark} ${c.subject}`);
|
|
298
|
+
}
|
|
299
|
+
console.log('');
|
|
300
|
+
if (story.files.length > 0) {
|
|
301
|
+
const shown = story.files.slice(0, 6).join(', ');
|
|
302
|
+
const more = story.files.length > 6 ? ` and ${story.files.length - 6} more` : '';
|
|
303
|
+
console.log(` it touched: ${shown}${more}`);
|
|
304
|
+
console.log('');
|
|
305
|
+
}
|
|
306
|
+
if (story.uniqueChanges === 0) {
|
|
307
|
+
console.log(` bottom line: all ${story.changes.length} changes made it into ${story.base}`);
|
|
308
|
+
console.log(' some other way. nothing here would be lost by clearing it.');
|
|
309
|
+
} else if (story.landedElsewhere > 0) {
|
|
310
|
+
console.log(` bottom line: ${story.landedElsewhere} of ${story.changes.length} changes made it into ${story.base} another`);
|
|
311
|
+
console.log(` way. ${story.uniqueChanges} exist only here (marked "never made it") — they may have`);
|
|
312
|
+
console.log(' been redone differently, or they may be real lost work. clearing backs');
|
|
313
|
+
console.log(' them up first either way.');
|
|
314
|
+
} else {
|
|
315
|
+
console.log(` bottom line: none of this is in ${story.base}. this is real work that only`);
|
|
316
|
+
console.log(' exists here. land it or clear it knowing the backup keeps a copy.');
|
|
317
|
+
}
|
|
318
|
+
console.log('');
|
|
319
|
+
console.log(` see the actual changes: git diff ${story.base}...${story.name}`);
|
|
320
|
+
console.log('');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function printBoard(board) {
|
|
324
|
+
console.log('');
|
|
325
|
+
console.log('the landing — what is actually done vs still in the air');
|
|
326
|
+
console.log('');
|
|
327
|
+
if (board.branches.length === 0 && board.worktrees.length === 0) {
|
|
328
|
+
console.log(' everything has landed. nothing in the air, nothing stuck.');
|
|
329
|
+
console.log('');
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const order = { due: 0, detached: 0, landed: 1, active: 2 };
|
|
333
|
+
const rows = [...board.branches].sort((a, b) => (order[a.state] ?? 3) - (order[b.state] ?? 3) || b.ageDays - a.ageDays);
|
|
334
|
+
const labels = { due: 'overdue', landed: 'landed', active: 'in the air' };
|
|
335
|
+
for (const b of rows) {
|
|
336
|
+
const changes = b.ahead === 1 ? '1 change ' : `${b.ahead} changes`;
|
|
337
|
+
console.log(` ${(labels[b.state] || b.state).padEnd(11)} ${String(b.ageDays).padStart(3)}d ${changes} ${b.name}`);
|
|
338
|
+
}
|
|
339
|
+
for (const w of board.worktrees) {
|
|
340
|
+
const edits = w.dirty === 1 ? '1 unsaved edit' : `${w.dirty} unsaved edits`;
|
|
341
|
+
console.log(` side copy ${w.path} (${edits})`);
|
|
342
|
+
}
|
|
343
|
+
console.log('');
|
|
344
|
+
const s = board.summary;
|
|
345
|
+
console.log(` ${s.unlanded} pieces of work in the air, ${s.due} overdue, ${s.landed} landed and safe to clear.`);
|
|
346
|
+
console.log(' work counts as done only when it lands in master.');
|
|
347
|
+
if (s.due + s.landed > 0) {
|
|
348
|
+
console.log(` back up + clear the ${s.due + s.landed} overdue/landed: atris land --reap`);
|
|
349
|
+
}
|
|
350
|
+
console.log('');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function printReceipt(receipt) {
|
|
354
|
+
console.log('');
|
|
355
|
+
if (receipt.dryRun) {
|
|
356
|
+
console.log(`landing cleanup preview — would clear ${receipt.deletedBranches.length} pieces of work, ${receipt.removedWorktrees.length} side copies:`);
|
|
357
|
+
for (const b of receipt.deletedBranches) console.log(` would clear ${b}`);
|
|
358
|
+
for (const w of receipt.removedWorktrees) console.log(` would remove ${w}`);
|
|
359
|
+
} else {
|
|
360
|
+
console.log(`landing cleanup done — ${receipt.deletedBranches.length} pieces cleared, ${receipt.removedWorktrees.length} side copies removed`);
|
|
361
|
+
if (receipt.bundle) console.log(` backed up first, nothing lost: ${receipt.bundle}`);
|
|
362
|
+
for (const p of receipt.patches) console.log(` unsaved edits saved: ${p}`);
|
|
363
|
+
if (receipt.deletedRemote.length > 0) console.log(` also cleared on github: ${receipt.deletedRemote.length}`);
|
|
364
|
+
}
|
|
365
|
+
console.log(` still flying (recent work, left alone): ${receipt.kept.length}`);
|
|
366
|
+
console.log('');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function readFlag(args, name, fallback = '') {
|
|
370
|
+
const idx = args.indexOf(name);
|
|
371
|
+
if (idx === -1) return fallback;
|
|
372
|
+
return args[idx + 1] || fallback;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function showHelp() {
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log('atris land — the landing: what is actually done vs still in the air');
|
|
378
|
+
console.log('');
|
|
379
|
+
console.log('work counts as done only when it lands in master. everything else is');
|
|
380
|
+
console.log('in the air, and this shows it so nothing quietly dies.');
|
|
381
|
+
console.log('');
|
|
382
|
+
console.log(' atris land show everything still in the air');
|
|
383
|
+
console.log(' atris land <name> the story of one piece: what it tried,');
|
|
384
|
+
console.log(' whether it landed some other way, what');
|
|
385
|
+
console.log(' would be lost by clearing it');
|
|
386
|
+
console.log(' atris land --reap back up, then clear anything overdue');
|
|
387
|
+
console.log(' or already landed (here and on github)');
|
|
388
|
+
console.log(' atris land --reap --dry-run preview what would be cleared');
|
|
389
|
+
console.log(' atris land --ttl <days> change the 7-day overdue line');
|
|
390
|
+
console.log(' atris land --json machine-readable output');
|
|
391
|
+
console.log('');
|
|
392
|
+
console.log('backups go to .atris/salvage/<date>/ — clearing never loses work.');
|
|
393
|
+
console.log('');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function landCommand(args = []) {
|
|
397
|
+
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
398
|
+
showHelp();
|
|
399
|
+
return 0;
|
|
400
|
+
}
|
|
401
|
+
const root = repoRoot();
|
|
402
|
+
if (!root) {
|
|
403
|
+
console.error('not a git repository');
|
|
404
|
+
return 1;
|
|
405
|
+
}
|
|
406
|
+
const ttlRaw = readFlag(args, '--ttl', '');
|
|
407
|
+
const ttlParsed = Number(ttlRaw);
|
|
408
|
+
const ttlDays = ttlRaw !== '' && Number.isFinite(ttlParsed) && ttlParsed >= 0 ? ttlParsed : DEFAULT_TTL_DAYS;
|
|
409
|
+
const base = readFlag(args, '--base', '');
|
|
410
|
+
const json = args.includes('--json');
|
|
411
|
+
|
|
412
|
+
if (args.includes('--reap')) {
|
|
413
|
+
const receipt = reap(root, { ttlDays, base, dryRun: args.includes('--dry-run'), remote: !args.includes('--no-remote') });
|
|
414
|
+
if (json) console.log(JSON.stringify(receipt, null, 2));
|
|
415
|
+
else printReceipt(receipt);
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const name = args.find((a) => !a.startsWith('-') && a !== readFlag(args, '--ttl') && a !== readFlag(args, '--base'));
|
|
420
|
+
if (name) {
|
|
421
|
+
const story = collectStory(root, name, { base });
|
|
422
|
+
if (!story) {
|
|
423
|
+
console.error(`nothing called '${name}' is in the air right now`);
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
if (json) console.log(JSON.stringify(story, null, 2));
|
|
427
|
+
else printStory(story);
|
|
428
|
+
return 0;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const board = collectBoard(root, { ttlDays, base });
|
|
432
|
+
if (json) console.log(JSON.stringify(board, null, 2));
|
|
433
|
+
else printBoard(board);
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
module.exports = {
|
|
438
|
+
collectBoard,
|
|
439
|
+
landCommand,
|
|
440
|
+
landSummary,
|
|
441
|
+
reap,
|
|
442
|
+
};
|
package/commands/loop-front.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// atris loop status liveness, last tick, reward -> pulse.js
|
|
15
15
|
// atris loop stop remove the durable heartbeat -> pulse.js
|
|
16
16
|
// atris loop wiki wiki upkeep (the old `loop`) -> loop.js
|
|
17
|
+
// atris loop create-next create + claim the suggested task
|
|
17
18
|
//
|
|
18
19
|
// The loop reads ROADMAP.md for what to pursue.
|
|
19
20
|
|
|
@@ -21,6 +22,13 @@ function isFlag(arg) {
|
|
|
21
22
|
return typeof arg === 'string' && arg.startsWith('-');
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function flagValue(argv, name) {
|
|
26
|
+
const index = argv.indexOf(name);
|
|
27
|
+
if (index === -1) return null;
|
|
28
|
+
const value = argv[index + 1];
|
|
29
|
+
return value && !isFlag(value) ? value : true;
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
// Pure router: decide what `atris loop ...` means without executing anything.
|
|
25
33
|
// Kept side-effect-free so it is trivially testable.
|
|
26
34
|
function routeLoop(argv = []) {
|
|
@@ -36,6 +44,10 @@ function routeLoop(argv = []) {
|
|
|
36
44
|
return { action: 'status' };
|
|
37
45
|
case 'stop':
|
|
38
46
|
return { action: 'stop' };
|
|
47
|
+
case 'create-next':
|
|
48
|
+
case 'claim-next':
|
|
49
|
+
case 'take-next':
|
|
50
|
+
return { action: 'create-next' };
|
|
39
51
|
case 'wiki':
|
|
40
52
|
// Forward the remaining args (e.g. --json, --limit=) to wiki upkeep.
|
|
41
53
|
return { action: 'wiki', rest: argv.filter((a) => a.toLowerCase() !== 'wiki') };
|
|
@@ -57,7 +69,7 @@ function routeLoop(argv = []) {
|
|
|
57
69
|
}
|
|
58
70
|
}
|
|
59
71
|
|
|
60
|
-
function renderLoopHome(route = { action: 'home' }) {
|
|
72
|
+
function renderLoopHome(route = { action: 'home' }, moves = []) {
|
|
61
73
|
const lines = [
|
|
62
74
|
'',
|
|
63
75
|
'atris loop: the self-improvement loop',
|
|
@@ -73,6 +85,7 @@ function renderLoopHome(route = { action: 'home' }) {
|
|
|
73
85
|
' atris loop start run it now, here (local)',
|
|
74
86
|
' atris loop start --once one cycle, then stop',
|
|
75
87
|
' atris loop start --overnight install the durable heartbeat (~15m)',
|
|
88
|
+
' atris loop start --overnight --hours 6',
|
|
76
89
|
'',
|
|
77
90
|
' watch it',
|
|
78
91
|
' atris loop status liveness, last tick, reward',
|
|
@@ -85,6 +98,16 @@ function renderLoopHome(route = { action: 'home' }) {
|
|
|
85
98
|
' wiki upkeep is at: atris loop wiki',
|
|
86
99
|
'',
|
|
87
100
|
];
|
|
101
|
+
if (Array.isArray(moves) && moves.length) {
|
|
102
|
+
lines.splice(lines.length - 2, 0, ' ranked next moves');
|
|
103
|
+
moves.slice(0, 3).forEach((move) => {
|
|
104
|
+
lines.splice(lines.length - 2, 0, ` [${move.source}] ${move.title}`);
|
|
105
|
+
});
|
|
106
|
+
lines.splice(lines.length - 2, 0, '');
|
|
107
|
+
}
|
|
108
|
+
lines.splice(lines.length - 2, 0, ' act on it');
|
|
109
|
+
lines.splice(lines.length - 2, 0, ' atris loop create-next create + claim the suggested task');
|
|
110
|
+
lines.splice(lines.length - 2, 0, '');
|
|
88
111
|
if (route && route.unknown) {
|
|
89
112
|
lines.splice(1, 0, ` (unknown: "${route.unknown}". here is the loop:)`);
|
|
90
113
|
}
|
|
@@ -95,7 +118,11 @@ function renderLoopHome(route = { action: 'home' }) {
|
|
|
95
118
|
}
|
|
96
119
|
|
|
97
120
|
function printLoopHome(route) {
|
|
98
|
-
|
|
121
|
+
let moves = [];
|
|
122
|
+
try {
|
|
123
|
+
moves = require('../lib/next-moves').nextMoves(process.cwd(), 3);
|
|
124
|
+
} catch { /* next moves are optional on a fresh checkout */ }
|
|
125
|
+
console.log(renderLoopHome(route, moves));
|
|
99
126
|
}
|
|
100
127
|
|
|
101
128
|
// Parse the handful of local-run flags `atris loop start` forwards to run.js.
|
|
@@ -148,7 +175,7 @@ function printLocalRunSummary() {
|
|
|
148
175
|
// Combined machine-readable status: the overnight pulse heartbeat and the local
|
|
149
176
|
// runs in one object. Best-effort per engine so a missing one does not fail it.
|
|
150
177
|
function loopStatusJson(root = process.cwd()) {
|
|
151
|
-
const out = { ok: true, action: 'loop_status', pulse: null, local_runs: { count: 0, latest: null } };
|
|
178
|
+
const out = { ok: true, action: 'loop_status', pulse: null, local_runs: { count: 0, latest: null }, next_moves: [] };
|
|
152
179
|
try {
|
|
153
180
|
const lp = require('../lib/pulse');
|
|
154
181
|
const { cronInstalled } = require('./pulse');
|
|
@@ -158,9 +185,88 @@ function loopStatusJson(root = process.cwd()) {
|
|
|
158
185
|
try {
|
|
159
186
|
out.local_runs = localRunSummary(require('./run').getRunLogDir());
|
|
160
187
|
} catch { /* runs optional */ }
|
|
188
|
+
try {
|
|
189
|
+
out.next_moves = require('../lib/next-moves').nextMoves(root, 5);
|
|
190
|
+
} catch { /* next moves optional */ }
|
|
161
191
|
return out;
|
|
162
192
|
}
|
|
163
193
|
|
|
194
|
+
function loopSeedMove(root = process.cwd()) {
|
|
195
|
+
const moves = require('../lib/next-moves').nextMoves(root, 5);
|
|
196
|
+
const activeTask = moves.find((move) => move && move.source === 'task');
|
|
197
|
+
if (activeTask) return { ok: false, reason: 'active_task', move: activeTask, moves };
|
|
198
|
+
const seed = moves.find((move) => (
|
|
199
|
+
move
|
|
200
|
+
&& move.source === 'mission'
|
|
201
|
+
&& (
|
|
202
|
+
move.why === 'active mission has no concrete task queued'
|
|
203
|
+
|| move.why === 'latest proof timeline suggested this self-improvement target'
|
|
204
|
+
)
|
|
205
|
+
));
|
|
206
|
+
if (!seed) return { ok: false, reason: 'no_seed', moves };
|
|
207
|
+
return { ok: true, move: seed, moves };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function createNextLoopTask(argv = [], root = process.cwd(), options = {}) {
|
|
211
|
+
const shouldPrint = options.print !== false;
|
|
212
|
+
const json = argv.includes('--json');
|
|
213
|
+
const owner = flagValue(argv, '--as') || flagValue(argv, '--owner') || process.env.ATRIS_AGENT_ID || 'auto-improver';
|
|
214
|
+
const seed = loopSeedMove(root);
|
|
215
|
+
if (!seed.ok) {
|
|
216
|
+
const payload = { ok: false, action: 'create_next_skipped', reason: seed.reason, move: seed.move || null };
|
|
217
|
+
if (shouldPrint) {
|
|
218
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
219
|
+
else if (seed.reason === 'active_task') console.log(`not created: active task already exists (${seed.move.ref || seed.move.title})`);
|
|
220
|
+
else console.log('not created: no loop seed is available');
|
|
221
|
+
}
|
|
222
|
+
return payload;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const note = `Goal: ${seed.move.title}. Files: inspect atris/MAP.md first, then the relevant code. Done: one bounded proof-backed self-improvement task is moved to Review. Check: focused verifier; git diff --check; atris clean --dry-run --json; atris brain compile --root . --verify.`;
|
|
226
|
+
const taskArgs = [
|
|
227
|
+
'task',
|
|
228
|
+
'delegate',
|
|
229
|
+
seed.move.title,
|
|
230
|
+
'--tag', 'loop',
|
|
231
|
+
'--claim',
|
|
232
|
+
'--as', String(owner),
|
|
233
|
+
'--note', note,
|
|
234
|
+
'--json',
|
|
235
|
+
];
|
|
236
|
+
const { spawnSync } = require('child_process');
|
|
237
|
+
const path = require('path');
|
|
238
|
+
const cli = path.join(__dirname, '..', 'bin', 'atris.js');
|
|
239
|
+
const child = spawnSync(process.execPath, [cli, ...taskArgs], {
|
|
240
|
+
cwd: root,
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
timeout: 20000,
|
|
243
|
+
env: { ...process.env, ATRIS_SKIP_UPDATE_CHECK: '1' },
|
|
244
|
+
});
|
|
245
|
+
if (child.status !== 0) {
|
|
246
|
+
const payload = { ok: false, action: 'create_next_failed', reason: 'task_delegate_failed', stderr: child.stderr, stdout: child.stdout };
|
|
247
|
+
if (shouldPrint) {
|
|
248
|
+
if (json) console.log(JSON.stringify(payload, null, 2));
|
|
249
|
+
else console.error(child.stderr || child.stdout || 'task creation failed');
|
|
250
|
+
}
|
|
251
|
+
return payload;
|
|
252
|
+
}
|
|
253
|
+
let delegated = null;
|
|
254
|
+
try { delegated = JSON.parse(child.stdout); } catch { delegated = null; }
|
|
255
|
+
const task = delegated && delegated.task ? delegated.task : null;
|
|
256
|
+
const payload = { ok: true, action: 'created_next', owner: String(owner), move: seed.move, task, delegated };
|
|
257
|
+
if (shouldPrint) {
|
|
258
|
+
if (json) {
|
|
259
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
260
|
+
} else {
|
|
261
|
+
const ref = task?.display_id || task?.id || 'task';
|
|
262
|
+
console.log(`created next loop task: ${ref} ${seed.move.title}`);
|
|
263
|
+
console.log(`claimed by: ${owner}`);
|
|
264
|
+
console.log(`next: atris task show ${ref}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return payload;
|
|
268
|
+
}
|
|
269
|
+
|
|
164
270
|
// The evidence that the loop is improving things: ROADMAP items it has handled,
|
|
165
271
|
// what is in flight and queued, and the heartbeat's reward/verify trend. Pure of
|
|
166
272
|
// console so it is testable.
|
|
@@ -170,7 +276,7 @@ function loopReport(root = process.cwd()) {
|
|
|
170
276
|
try {
|
|
171
277
|
roadmap = require('../lib/next-moves').roadmapItemsByState(root);
|
|
172
278
|
} catch { /* roadmap optional */ }
|
|
173
|
-
return { ok: true, action: 'loop_report', roadmap, pulse: status.pulse, local_runs: status.local_runs };
|
|
279
|
+
return { ok: true, action: 'loop_report', roadmap, next_moves: status.next_moves, pulse: status.pulse, local_runs: status.local_runs };
|
|
174
280
|
}
|
|
175
281
|
|
|
176
282
|
function renderLoopReport(rep) {
|
|
@@ -193,6 +299,11 @@ function renderLoopReport(rep) {
|
|
|
193
299
|
lines.push(' next up:');
|
|
194
300
|
r.open.slice(0, 4).forEach((t) => lines.push(` [ ] ${t}`));
|
|
195
301
|
}
|
|
302
|
+
if (Array.isArray(rep.next_moves) && rep.next_moves.length) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push(' ranked next:');
|
|
305
|
+
rep.next_moves.slice(0, 5).forEach((move) => lines.push(` [${move.source}] ${move.title}`));
|
|
306
|
+
}
|
|
196
307
|
if (rep.pulse) {
|
|
197
308
|
lines.push('');
|
|
198
309
|
lines.push(` heartbeat: ${rep.pulse.total_ticks} ticks, reward ${rep.pulse.reward_sum}, verify ${rep.pulse.verify_pass}/${rep.pulse.verify_fail}`);
|
|
@@ -236,6 +347,11 @@ function loopFront(argv = []) {
|
|
|
236
347
|
return Promise.resolve(0);
|
|
237
348
|
}
|
|
238
349
|
|
|
350
|
+
case 'create-next': {
|
|
351
|
+
const result = createNextLoopTask(argv, process.cwd());
|
|
352
|
+
return Promise.resolve(result.ok ? 0 : 1);
|
|
353
|
+
}
|
|
354
|
+
|
|
239
355
|
case 'status': {
|
|
240
356
|
if (jsonFlag.length) {
|
|
241
357
|
// One machine-readable object covering BOTH engines (overnight pulse
|
|
@@ -284,6 +400,8 @@ module.exports = {
|
|
|
284
400
|
startLocalOptions,
|
|
285
401
|
localRunSummary,
|
|
286
402
|
loopStatusJson,
|
|
403
|
+
loopSeedMove,
|
|
404
|
+
createNextLoopTask,
|
|
287
405
|
loopReport,
|
|
288
406
|
renderLoopReport,
|
|
289
407
|
loopFront,
|