atris 3.35.0 → 3.37.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 +37 -0
- 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 +22 -4
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +2 -0
- package/ax +147 -19
- package/bin/atris.js +576 -267
- package/commands/activate.js +194 -88
- package/commands/agents.js +166 -0
- package/commands/autoland.js +459 -107
- 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/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- 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/engine.js +993 -32
- package/commands/experiments.js +28 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/gm.js +23 -0
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +72 -44
- package/commands/interview.js +67 -1
- package/commands/land.js +152 -52
- package/commands/lifecycle.js +39 -3
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +511 -34
- package/commands/mission.js +3144 -338
- package/commands/next.js +137 -0
- package/commands/now.js +220 -25
- 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/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/stream.js +861 -0
- package/commands/study.js +693 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +1346 -117
- package/commands/team.js +73 -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 +234 -13
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +331 -34
- package/lib/autoland.js +319 -54
- package/lib/ax-auto-lane.js +96 -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 +2219 -67
- 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/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/next-moves.js +327 -10
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -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/router-brain.js +352 -0
- package/lib/runner-command.js +10 -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 +108 -29
- package/lib/task-proof.js +23 -1
- 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/commands/land.js
CHANGED
|
@@ -5,11 +5,12 @@ const { spawnSync } = require('child_process');
|
|
|
5
5
|
const { listWorktrees, statusCounts } = require('./worktree');
|
|
6
6
|
|
|
7
7
|
const DEFAULT_TTL_DAYS = 7;
|
|
8
|
+
const DEFAULT_STALE_HOURS = 48;
|
|
8
9
|
const WORKTREE_REAP_GRACE_MS = 60 * 60 * 1000;
|
|
9
10
|
const PROTECTED_BRANCHES = new Set(['main', 'master']);
|
|
10
11
|
|
|
11
|
-
function runGit(args, { cwd = process.cwd(), check = true } = {}) {
|
|
12
|
-
const result = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
12
|
+
function runGit(args, { cwd = process.cwd(), check = true, maxBuffer } = {}) {
|
|
13
|
+
const result = spawnSync('git', args, { cwd, encoding: 'utf8', ...(maxBuffer ? { maxBuffer } : {}) });
|
|
13
14
|
if (check && result.status !== 0) {
|
|
14
15
|
throw new Error(`git ${args.join(' ')} failed: ${(result.stderr || result.stdout || '').trim()}`);
|
|
15
16
|
}
|
|
@@ -28,6 +29,8 @@ function refExists(root, ref) {
|
|
|
28
29
|
|
|
29
30
|
function baseBranch(root, override = '') {
|
|
30
31
|
if (override && refExists(root, override)) return override;
|
|
32
|
+
if (refExists(root, 'origin/master')) return 'origin/master';
|
|
33
|
+
if (refExists(root, 'origin/main')) return 'origin/main';
|
|
31
34
|
if (refExists(root, 'master')) return 'master';
|
|
32
35
|
if (refExists(root, 'main')) return 'main';
|
|
33
36
|
return '';
|
|
@@ -37,6 +40,10 @@ function ageDays(unixSeconds, now = Date.now()) {
|
|
|
37
40
|
return Math.floor((now / 1000 - Number(unixSeconds)) / 86400);
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
function ageHours(unixSeconds, now = Date.now()) {
|
|
44
|
+
return Math.floor((now / 1000 - Number(unixSeconds)) / 3600);
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
function worktreeMtimeMs(worktreePath) {
|
|
41
48
|
try {
|
|
42
49
|
return fs.statSync(worktreePath).mtimeMs;
|
|
@@ -50,18 +57,29 @@ function worktreeWithinReapGrace(worktreePath, now = Date.now()) {
|
|
|
50
57
|
return typeof mtime === 'number' && now - mtime < WORKTREE_REAP_GRACE_MS;
|
|
51
58
|
}
|
|
52
59
|
|
|
53
|
-
function listBranches(root) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
function listBranches(root, base = '') {
|
|
61
|
+
// With a base, ask git for ahead counts in the same single spawn
|
|
62
|
+
// (%(ahead-behind:) needs git >= 2.41; on failure we retry without it and
|
|
63
|
+
// collectBoard falls back to per-branch aheadCount).
|
|
64
|
+
const format = base
|
|
65
|
+
? `%(refname:short)%09%(committerdate:unix)%09%(ahead-behind:${base})`
|
|
66
|
+
: '%(refname:short)%09%(committerdate:unix)';
|
|
67
|
+
const result = runGit(['for-each-ref', 'refs/heads', `--format=${format}`], { cwd: root, check: false });
|
|
68
|
+
if (result.status !== 0) {
|
|
69
|
+
if (base) return listBranches(root);
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
59
72
|
return result.stdout
|
|
60
73
|
.split(/\r?\n/)
|
|
61
74
|
.filter(Boolean)
|
|
62
75
|
.map((line) => {
|
|
63
|
-
const [name, date] = line.split('\t');
|
|
64
|
-
|
|
76
|
+
const [name, date, aheadBehind] = line.split('\t');
|
|
77
|
+
const entry = { name, lastCommitUnix: Number(date) };
|
|
78
|
+
if (aheadBehind !== undefined) {
|
|
79
|
+
const ahead = Number(aheadBehind.split(' ')[0]);
|
|
80
|
+
if (Number.isFinite(ahead)) entry.ahead = ahead;
|
|
81
|
+
}
|
|
82
|
+
return entry;
|
|
65
83
|
});
|
|
66
84
|
}
|
|
67
85
|
|
|
@@ -71,30 +89,59 @@ function aheadCount(root, base, ref) {
|
|
|
71
89
|
return Number(result.stdout.trim()) || 0;
|
|
72
90
|
}
|
|
73
91
|
|
|
92
|
+
function cherryStats(root, base, ref) {
|
|
93
|
+
const result = runGit(['cherry', base, ref], { cwd: root, check: false });
|
|
94
|
+
if (result.status !== 0) return { landedElsewhere: 0, unique: 0 };
|
|
95
|
+
let landedElsewhere = 0;
|
|
96
|
+
let unique = 0;
|
|
97
|
+
for (const line of result.stdout.split(/\r?\n/).filter(Boolean)) {
|
|
98
|
+
if (line.startsWith('- ')) landedElsewhere += 1;
|
|
99
|
+
else if (line.startsWith('+ ')) unique += 1;
|
|
100
|
+
}
|
|
101
|
+
return { landedElsewhere, unique };
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
// Board: every branch and worktree with unlanded state, classified.
|
|
75
105
|
// landed — no commits ahead of base; the branch pointer is residue
|
|
76
106
|
// active — has unlanded commits, younger than TTL
|
|
77
107
|
// due — has unlanded commits, older than TTL; --reap collects it
|
|
78
|
-
function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '' } = {}) {
|
|
108
|
+
function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', now = Date.now(), light = false } = {}) {
|
|
79
109
|
const base = baseBranch(root, baseOverride);
|
|
80
110
|
if (!base) throw new Error('no master/main branch found');
|
|
81
111
|
|
|
82
112
|
const branches = [];
|
|
83
|
-
|
|
113
|
+
const lastCommitMsByBranch = new Map();
|
|
114
|
+
for (const branch of listBranches(root, base)) {
|
|
84
115
|
if (PROTECTED_BRANCHES.has(branch.name)) continue;
|
|
85
|
-
const
|
|
86
|
-
|
|
116
|
+
const lastCommitMs = Number(branch.lastCommitUnix) * 1000;
|
|
117
|
+
if (Number.isFinite(lastCommitMs)) lastCommitMsByBranch.set(branch.name, lastCommitMs);
|
|
118
|
+
const ahead = branch.ahead !== undefined ? branch.ahead : aheadCount(root, base, branch.name);
|
|
119
|
+
const age = ageDays(branch.lastCommitUnix, now);
|
|
120
|
+
const hours = ageHours(branch.lastCommitUnix, now);
|
|
121
|
+
const cherry = ahead > 0 ? cherryStats(root, base, branch.name) : { landedElsewhere: 0, unique: 0 };
|
|
87
122
|
let state = 'active';
|
|
88
|
-
if (ahead === 0) state = 'landed';
|
|
123
|
+
if (ahead === 0 || (cherry.landedElsewhere > 0 && cherry.unique === 0)) state = 'landed';
|
|
89
124
|
else if (age > ttlDays) state = 'due';
|
|
90
|
-
branches.push({
|
|
125
|
+
branches.push({
|
|
126
|
+
name: branch.name,
|
|
127
|
+
ahead,
|
|
128
|
+
ageDays: age,
|
|
129
|
+
ageHours: hours,
|
|
130
|
+
activityHours: hours,
|
|
131
|
+
landedElsewhere: cherry.landedElsewhere,
|
|
132
|
+
uniqueChanges: cherry.unique,
|
|
133
|
+
stale: false,
|
|
134
|
+
state,
|
|
135
|
+
});
|
|
91
136
|
}
|
|
92
137
|
|
|
93
138
|
const worktrees = [];
|
|
94
139
|
const all = listWorktrees(root);
|
|
95
140
|
for (const wt of all.slice(1)) {
|
|
96
141
|
const branch = (wt.branch || '').replace(/^refs\/heads\//, '');
|
|
97
|
-
|
|
142
|
+
// light mode skips the full `git status` per worktree — the banner summary
|
|
143
|
+
// never reads dirty counts, only worktree mtimes for staleness.
|
|
144
|
+
const counts = (light ? null : statusCounts(wt.path)) || { staged: 0, unstaged: 0, untracked: 0 };
|
|
98
145
|
const dirty = counts.staged + counts.unstaged + counts.untracked;
|
|
99
146
|
const entry = branches.find((b) => b.name === branch) || null;
|
|
100
147
|
worktrees.push({
|
|
@@ -102,16 +149,35 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '
|
|
|
102
149
|
branch,
|
|
103
150
|
dirty,
|
|
104
151
|
ageDays: entry ? entry.ageDays : null,
|
|
152
|
+
ageHours: entry ? entry.ageHours : null,
|
|
105
153
|
state: entry ? entry.state : 'detached',
|
|
106
154
|
});
|
|
107
155
|
}
|
|
108
156
|
|
|
157
|
+
const activityByBranch = new Map(branches.map((b) => [b.name, lastCommitMsByBranch.get(b.name)]));
|
|
158
|
+
for (const wt of worktrees) {
|
|
159
|
+
if (!wt.branch || !activityByBranch.has(wt.branch)) continue;
|
|
160
|
+
const mtime = worktreeMtimeMs(wt.path);
|
|
161
|
+
if (typeof mtime === 'number' && mtime > activityByBranch.get(wt.branch)) {
|
|
162
|
+
activityByBranch.set(wt.branch, mtime);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const branch of branches) {
|
|
166
|
+
const lastActivity = activityByBranch.get(branch.name);
|
|
167
|
+
if (Number.isFinite(lastActivity)) {
|
|
168
|
+
branch.activityHours = Math.floor((now - lastActivity) / 3600000);
|
|
169
|
+
}
|
|
170
|
+
branch.stale = branch.state === 'active' && branch.activityHours >= staleHours;
|
|
171
|
+
}
|
|
172
|
+
|
|
109
173
|
const due = branches.filter((b) => b.state === 'due');
|
|
110
174
|
const landed = branches.filter((b) => b.state === 'landed');
|
|
111
175
|
const active = branches.filter((b) => b.state === 'active');
|
|
176
|
+
const stale = active.filter((b) => b.stale);
|
|
112
177
|
return {
|
|
113
178
|
base,
|
|
114
179
|
ttlDays,
|
|
180
|
+
staleHours,
|
|
115
181
|
branches,
|
|
116
182
|
worktrees,
|
|
117
183
|
summary: {
|
|
@@ -119,23 +185,25 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '
|
|
|
119
185
|
active: active.length,
|
|
120
186
|
due: due.length,
|
|
121
187
|
landed: landed.length,
|
|
188
|
+
stale: stale.length,
|
|
122
189
|
worktrees: worktrees.length,
|
|
123
190
|
},
|
|
124
191
|
};
|
|
125
192
|
}
|
|
126
193
|
|
|
127
|
-
//
|
|
194
|
+
// Counts for the boot banner and digest. This intentionally pays the same
|
|
195
|
+
// classification cost as the landing board so "in the air" never includes
|
|
196
|
+
// merged branch residue.
|
|
128
197
|
function landSummary(cwd = process.cwd(), ttlDays = DEFAULT_TTL_DAYS) {
|
|
129
198
|
const root = repoRoot(cwd);
|
|
130
199
|
if (!root) return null;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
return { branches, due, ttlDays };
|
|
200
|
+
const board = collectBoard(root, { ttlDays, light: true });
|
|
201
|
+
return {
|
|
202
|
+
branches: board.summary.active + board.summary.due,
|
|
203
|
+
due: board.summary.due,
|
|
204
|
+
stale: board.summary.stale,
|
|
205
|
+
ttlDays,
|
|
206
|
+
};
|
|
139
207
|
}
|
|
140
208
|
|
|
141
209
|
function salvageDir(root) {
|
|
@@ -165,14 +233,18 @@ function remoteHeads(root) {
|
|
|
165
233
|
// piece could not be saved — the caller then keeps the worktree.
|
|
166
234
|
function salvageWorktree(w, dir, receipt) {
|
|
167
235
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
236
|
+
// git writes the patch file itself: routing the diff through spawnSync
|
|
237
|
+
// stdout hits Node's 1MB default maxBuffer, so any worktree more than
|
|
238
|
+
// ~1MB dirty read as "salvage failed" and was kept on every reap pass.
|
|
239
|
+
const patchPath = path.join(dir, `${path.basename(w.path)}.dirty.patch`);
|
|
240
|
+
const diff = runGit(['diff', 'HEAD', `--output=${patchPath}`], { cwd: w.path, check: false });
|
|
241
|
+
if (diff.status !== 0) {
|
|
242
|
+
fs.rmSync(patchPath, { force: true });
|
|
243
|
+
return false;
|
|
174
244
|
}
|
|
175
|
-
|
|
245
|
+
if (fs.statSync(patchPath).size > 0) receipt.patches.push(patchPath);
|
|
246
|
+
else fs.rmSync(patchPath, { force: true });
|
|
247
|
+
const untracked = runGit(['ls-files', '--others', '--exclude-standard', '-z'], { cwd: w.path, check: false, maxBuffer: 64 * 1024 * 1024 });
|
|
176
248
|
if (untracked.status !== 0) return false;
|
|
177
249
|
const files = untracked.stdout.split('\0').filter(Boolean);
|
|
178
250
|
if (files.length > 0) {
|
|
@@ -197,8 +269,8 @@ function salvageWorktree(w, dir, receipt) {
|
|
|
197
269
|
// includeDetached: detached-HEAD worktrees have no branch, so the salvage
|
|
198
270
|
// bundle cannot cover their commits — unattended reaps (autoland) pass false
|
|
199
271
|
// and leave them for a human reap.
|
|
200
|
-
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true } = {}) {
|
|
201
|
-
const board = collectBoard(root, { ttlDays, base: baseOverride });
|
|
272
|
+
function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, now = Date.now() } = {}) {
|
|
273
|
+
const board = collectBoard(root, { ttlDays, staleHours, base: baseOverride, now });
|
|
202
274
|
const targets = board.branches.filter((b) => b.state === 'landed' || b.state === 'due');
|
|
203
275
|
const targetNames = new Set(targets.map((b) => b.name));
|
|
204
276
|
const candidateWorktrees = board.worktrees.filter(
|
|
@@ -206,13 +278,18 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '', dryRu
|
|
|
206
278
|
);
|
|
207
279
|
const protectedWorktrees = [];
|
|
208
280
|
const worktreeTargets = [];
|
|
281
|
+
const current = path.resolve(root);
|
|
209
282
|
for (const w of candidateWorktrees) {
|
|
210
283
|
// Only the fresh-worktree grace protects here. Dirty worktrees are NOT
|
|
211
284
|
// skipped: reap's salvage (bundle + patches + untracked copies below)
|
|
212
285
|
// exists precisely so dirty residue can be cleared loss-free. Blanket
|
|
213
286
|
// dirty protection lives in the janitor's cleanupWorktrees, which has
|
|
214
287
|
// no salvage machinery.
|
|
215
|
-
if (
|
|
288
|
+
if (protectCurrent && path.resolve(w.path) === current) {
|
|
289
|
+
protectedWorktrees.push({ ...w, reason: 'current_checkout' });
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (worktreeWithinReapGrace(w.path, now)) {
|
|
216
293
|
protectedWorktrees.push({ ...w, reason: 'fresh_worktree_grace' });
|
|
217
294
|
continue;
|
|
218
295
|
}
|
|
@@ -276,8 +353,10 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '', dryRu
|
|
|
276
353
|
// copies bank everything force-remove would destroy. The fresh-worktree
|
|
277
354
|
// grace was already applied when candidates were selected.
|
|
278
355
|
if (w.dirty > 0 && !salvageWorktree(w, dir, receipt)) {
|
|
279
|
-
// could not fully back up what force-remove would destroy — keep it
|
|
280
|
-
receipt
|
|
356
|
+
// could not fully back up what force-remove would destroy — keep it,
|
|
357
|
+
// and say why: a bare path in the receipt reads as an unexplained
|
|
358
|
+
// no-op (the exact silence BCK-1232 was about).
|
|
359
|
+
receipt.keptWorktrees.push(`${w.path} (salvage incomplete: patch/copy failed, kept to avoid losing work)`);
|
|
281
360
|
if (w.branch) targetNames.delete(w.branch);
|
|
282
361
|
continue;
|
|
283
362
|
}
|
|
@@ -304,7 +383,15 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, base: baseOverride = '', dryRu
|
|
|
304
383
|
continue;
|
|
305
384
|
}
|
|
306
385
|
const deleted = runGit(['branch', '-D', name], { cwd: root, check: false });
|
|
307
|
-
if (deleted.status === 0)
|
|
386
|
+
if (deleted.status === 0) {
|
|
387
|
+
receipt.deletedBranches.push(name);
|
|
388
|
+
} else {
|
|
389
|
+
// Same silence class as the worktree-remove path: a branch checked
|
|
390
|
+
// out in a side copy the board never saw makes -D refuse, and an
|
|
391
|
+
// unreported refusal reads as "cleanup done" with nothing cleared.
|
|
392
|
+
const reason = String(deleted.stderr || deleted.stdout || 'branch delete failed').trim().slice(0, 160);
|
|
393
|
+
receipt.keptWorktrees.push(`branch ${name} (${reason})`);
|
|
394
|
+
}
|
|
308
395
|
}
|
|
309
396
|
|
|
310
397
|
if (remote && receipt.deletedBranches.length > 0) {
|
|
@@ -405,7 +492,7 @@ function printStory(story) {
|
|
|
405
492
|
console.log(' some other way. nothing here would be lost by clearing it.');
|
|
406
493
|
} else if (story.landedElsewhere > 0) {
|
|
407
494
|
console.log(` bottom line: ${story.landedElsewhere} of ${story.changes.length} changes made it into ${story.base} another`);
|
|
408
|
-
console.log(` way. ${story.uniqueChanges} exist only here (marked "never made it")
|
|
495
|
+
console.log(` way. ${story.uniqueChanges} exist only here (marked "never made it") - they may have`);
|
|
409
496
|
console.log(' been redone differently, or they may be real lost work. clearing backs');
|
|
410
497
|
console.log(' them up first either way.');
|
|
411
498
|
} else {
|
|
@@ -419,19 +506,20 @@ function printStory(story) {
|
|
|
419
506
|
|
|
420
507
|
function printBoard(board) {
|
|
421
508
|
console.log('');
|
|
422
|
-
console.log('the landing
|
|
509
|
+
console.log('the landing: what is actually done vs still in the air');
|
|
423
510
|
console.log('');
|
|
424
511
|
if (board.branches.length === 0 && board.worktrees.length === 0) {
|
|
425
512
|
console.log(' everything has landed. nothing in the air, nothing stuck.');
|
|
426
513
|
console.log('');
|
|
427
514
|
return;
|
|
428
515
|
}
|
|
429
|
-
const order = { due: 0, detached: 0,
|
|
516
|
+
const order = { due: 0, detached: 0, active: 1, landed: 2 };
|
|
430
517
|
const rows = [...board.branches].sort((a, b) => (order[a.state] ?? 3) - (order[b.state] ?? 3) || b.ageDays - a.ageDays);
|
|
431
518
|
const labels = { due: 'overdue', landed: 'landed', active: 'in the air' };
|
|
432
519
|
for (const b of rows) {
|
|
433
520
|
const changes = b.ahead === 1 ? '1 change ' : `${b.ahead} changes`;
|
|
434
|
-
|
|
521
|
+
const label = b.stale ? 'stale' : (labels[b.state] || b.state);
|
|
522
|
+
console.log(` ${label.padEnd(11)} ${String(b.ageDays).padStart(3)}d ${changes} ${b.name}`);
|
|
435
523
|
}
|
|
436
524
|
for (const w of board.worktrees) {
|
|
437
525
|
const edits = w.dirty === 1 ? '1 unsaved edit' : `${w.dirty} unsaved edits`;
|
|
@@ -439,10 +527,14 @@ function printBoard(board) {
|
|
|
439
527
|
}
|
|
440
528
|
console.log('');
|
|
441
529
|
const s = board.summary;
|
|
442
|
-
|
|
530
|
+
const stillInAir = s.active + s.due;
|
|
531
|
+
console.log(` ${s.unlanded} ${s.unlanded === 1 ? 'piece' : 'pieces'} tracked: ${stillInAir} still in the air, ${s.stale} stale, ${s.due} overdue, ${s.landed} landed and safe to clear.`);
|
|
443
532
|
console.log(' work counts as done only when it lands in master.');
|
|
444
533
|
if (s.due + s.landed > 0) {
|
|
445
|
-
|
|
534
|
+
const clearable = s.due > 0 && s.landed > 0
|
|
535
|
+
? `${s.due} overdue and ${s.landed} landed`
|
|
536
|
+
: s.due > 0 ? `${s.due} overdue` : `${s.landed} landed`;
|
|
537
|
+
console.log(` back up + clear ${clearable}: atris land --reap`);
|
|
446
538
|
}
|
|
447
539
|
console.log('');
|
|
448
540
|
}
|
|
@@ -450,16 +542,21 @@ function printBoard(board) {
|
|
|
450
542
|
function printReceipt(receipt) {
|
|
451
543
|
console.log('');
|
|
452
544
|
if (receipt.dryRun) {
|
|
453
|
-
console.log(`landing cleanup preview
|
|
545
|
+
console.log(`landing cleanup preview: would clear ${receipt.deletedBranches.length} pieces of work, ${receipt.removedWorktrees.length} side copies:`);
|
|
454
546
|
for (const b of receipt.deletedBranches) console.log(` would clear ${b}`);
|
|
455
547
|
for (const w of receipt.removedWorktrees) console.log(` would remove ${w}`);
|
|
456
548
|
} else {
|
|
457
|
-
console.log(`landing cleanup done
|
|
549
|
+
console.log(`landing cleanup done: ${receipt.deletedBranches.length} pieces cleared, ${receipt.removedWorktrees.length} side copies removed`);
|
|
458
550
|
if (receipt.bundle) console.log(` backed up first, nothing lost: ${receipt.bundle}`);
|
|
459
|
-
if (receipt.bundleError) console.log(` backup failed
|
|
551
|
+
if (receipt.bundleError) console.log(` backup failed: unlanded work left in place: ${receipt.bundleError}`);
|
|
460
552
|
for (const p of receipt.patches) console.log(` unsaved edits saved: ${p}`);
|
|
461
553
|
for (const u of receipt.untracked || []) console.log(` new files saved: ${u}`);
|
|
462
|
-
|
|
554
|
+
// fresh-grace keeps are healthy active work, not a call to action —
|
|
555
|
+
// "needs a human" on those trained readers to ignore the real ones.
|
|
556
|
+
for (const k of receipt.keptWorktrees || []) {
|
|
557
|
+
if (String(k).includes('(fresh_worktree_grace)')) console.log(` in use, left alone: ${k.replace(' (fresh_worktree_grace)', '')}`);
|
|
558
|
+
else console.log(` ✋ kept, needs a human: ${k}`);
|
|
559
|
+
}
|
|
463
560
|
for (const m of receipt.keptMovedBranches || []) console.log(` moved since scan, left alone: ${m}`);
|
|
464
561
|
if (receipt.deletedRemote.length > 0) console.log(` also cleared on github: ${receipt.deletedRemote.length}`);
|
|
465
562
|
}
|
|
@@ -475,13 +572,15 @@ function readFlag(args, name, fallback = '') {
|
|
|
475
572
|
|
|
476
573
|
function showHelp() {
|
|
477
574
|
console.log('');
|
|
478
|
-
console.log('atris land
|
|
575
|
+
console.log('atris land: the landing, what is actually done vs still in the air');
|
|
479
576
|
console.log('');
|
|
480
577
|
console.log('work counts as done only when it lands in master. everything else is');
|
|
481
578
|
console.log('in the air, and this shows it so nothing quietly dies.');
|
|
482
579
|
console.log('');
|
|
483
580
|
console.log(' atris land show everything still in the air');
|
|
581
|
+
console.log(' atris land status show everything still in the air');
|
|
484
582
|
console.log(' atris land <name> the story of one piece: what it tried,');
|
|
583
|
+
console.log(' atris land status <name> same story lookup from status mode');
|
|
485
584
|
console.log(' whether it landed some other way, what');
|
|
486
585
|
console.log(' would be lost by clearing it');
|
|
487
586
|
console.log(' atris land --reap back up, then clear anything overdue');
|
|
@@ -490,7 +589,7 @@ function showHelp() {
|
|
|
490
589
|
console.log(' atris land --ttl <days> change the 7-day overdue line');
|
|
491
590
|
console.log(' atris land --json machine-readable output');
|
|
492
591
|
console.log('');
|
|
493
|
-
console.log('backups go to .atris/salvage/<date
|
|
592
|
+
console.log('backups go to .atris/salvage/<date>/; clearing never loses work.');
|
|
494
593
|
console.log('');
|
|
495
594
|
}
|
|
496
595
|
|
|
@@ -517,7 +616,8 @@ function landCommand(args = []) {
|
|
|
517
616
|
return 0;
|
|
518
617
|
}
|
|
519
618
|
|
|
520
|
-
const
|
|
619
|
+
const lookupArgs = args[0] === 'status' ? args.slice(1) : args;
|
|
620
|
+
const name = lookupArgs.find((a) => !a.startsWith('-') && a !== readFlag(lookupArgs, '--ttl') && a !== readFlag(lookupArgs, '--base'));
|
|
521
621
|
if (name) {
|
|
522
622
|
const story = collectStory(root, name, { base });
|
|
523
623
|
if (!story) {
|
package/commands/lifecycle.js
CHANGED
|
@@ -3,6 +3,13 @@ const path = require('path');
|
|
|
3
3
|
const { loadCredentials } = require('../utils/auth');
|
|
4
4
|
const { apiRequestJson } = require('../utils/api');
|
|
5
5
|
const { loadBusinesses, saveBusinesses, businessMatchesSlug } = require('./business');
|
|
6
|
+
const {
|
|
7
|
+
isLocalMember,
|
|
8
|
+
memberArgFromArgv,
|
|
9
|
+
readLoopFlag,
|
|
10
|
+
setMemberAwake,
|
|
11
|
+
setMemberLoopAwake,
|
|
12
|
+
} = require('../lib/member-switches');
|
|
6
13
|
|
|
7
14
|
/**
|
|
8
15
|
* Resolve business slug from CLI arg or local .atris/business.json.
|
|
@@ -23,6 +30,29 @@ function resolveSlug() {
|
|
|
23
30
|
return slug;
|
|
24
31
|
}
|
|
25
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Per-member / per-loop switch path for atris sleep|wake <member> [--loop <id>].
|
|
35
|
+
* Returns true when handled so callers skip business-level lifecycle.
|
|
36
|
+
*/
|
|
37
|
+
function handleMemberSwitch(awake) {
|
|
38
|
+
const name = memberArgFromArgv(process.argv);
|
|
39
|
+
if (!name || name === '--help' || name === '-h' || name === 'help') return false;
|
|
40
|
+
if (!isLocalMember(name)) return false;
|
|
41
|
+
|
|
42
|
+
const loopId = readLoopFlag(process.argv);
|
|
43
|
+
if (loopId) {
|
|
44
|
+
setMemberLoopAwake(name, loopId, awake);
|
|
45
|
+
console.log(awake
|
|
46
|
+
? `${name} loop ${loopId} is awake`
|
|
47
|
+
: `${name} loop ${loopId} is asleep`);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setMemberAwake(name, awake);
|
|
52
|
+
console.log(awake ? `${name} is awake` : `${name} is asleep`);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
async function resolveBusiness(token, slug) {
|
|
27
57
|
const businesses = loadBusinesses();
|
|
28
58
|
|
|
@@ -91,12 +121,15 @@ async function waitForBusinessComputer(token, business, maxWait = 90000) {
|
|
|
91
121
|
* @returns {Promise<void>}
|
|
92
122
|
*/
|
|
93
123
|
async function sleepAtris() {
|
|
124
|
+
if (handleMemberSwitch(false)) return;
|
|
125
|
+
|
|
94
126
|
const slug = resolveSlug();
|
|
95
127
|
|
|
96
128
|
if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
|
|
97
|
-
console.log('Usage: atris sleep [business]');
|
|
129
|
+
console.log('Usage: atris sleep [business|member] [--loop <loop-id>]');
|
|
98
130
|
console.log('');
|
|
99
131
|
console.log(' Pause a workspace to save compute. Storage only.');
|
|
132
|
+
console.log(' Or flip a local member / member-loop switch asleep.');
|
|
100
133
|
process.exit(0);
|
|
101
134
|
}
|
|
102
135
|
|
|
@@ -131,7 +164,7 @@ async function sleepAtris() {
|
|
|
131
164
|
}
|
|
132
165
|
|
|
133
166
|
console.log(`Workspace '${slug}' is now sleeping. Context saved. Wake it with: atris wake ${slug}`);
|
|
134
|
-
console.log('Compute paused. Storage only
|
|
167
|
+
console.log('Compute paused. Storage only - pennies/day.');
|
|
135
168
|
}
|
|
136
169
|
|
|
137
170
|
/**
|
|
@@ -139,12 +172,15 @@ async function sleepAtris() {
|
|
|
139
172
|
* @returns {Promise<void>}
|
|
140
173
|
*/
|
|
141
174
|
async function wakeAtris() {
|
|
175
|
+
if (handleMemberSwitch(true)) return;
|
|
176
|
+
|
|
142
177
|
const slug = resolveSlug();
|
|
143
178
|
|
|
144
179
|
if (!slug || slug === '--help' || slug === '-h' || slug === 'help') {
|
|
145
|
-
console.log('Usage: atris wake [business]');
|
|
180
|
+
console.log('Usage: atris wake [business|member] [--loop <loop-id>]');
|
|
146
181
|
console.log('');
|
|
147
182
|
console.log(' Wake a sleeping workspace. Agents resume automatically.');
|
|
183
|
+
console.log(' Or flip a local member / member-loop switch awake.');
|
|
148
184
|
process.exit(0);
|
|
149
185
|
}
|
|
150
186
|
|
package/commands/log.js
CHANGED
|
@@ -52,4 +52,87 @@ function logAtris() {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
function localDateString(now = new Date()) {
|
|
56
|
+
const year = String(now.getFullYear());
|
|
57
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
58
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
59
|
+
return `${year}-${month}-${day}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function digestDate(args) {
|
|
63
|
+
const equalsArg = args.find((arg) => String(arg).startsWith('--date='));
|
|
64
|
+
const dateIndex = args.indexOf('--date');
|
|
65
|
+
const date = equalsArg
|
|
66
|
+
? equalsArg.slice('--date='.length)
|
|
67
|
+
: dateIndex >= 0
|
|
68
|
+
? args[dateIndex + 1]
|
|
69
|
+
: localDateString();
|
|
70
|
+
|
|
71
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(String(date || ''))) {
|
|
72
|
+
throw new Error('Usage: atris logs [today] [--date YYYY-MM-DD] [--json]');
|
|
73
|
+
}
|
|
74
|
+
return date;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readDigestSection(root, title, relativePath, maxLines = null) {
|
|
78
|
+
const absolutePath = path.join(root, relativePath);
|
|
79
|
+
if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) return null;
|
|
80
|
+
|
|
81
|
+
const content = fs.readFileSync(absolutePath, 'utf8');
|
|
82
|
+
const lines = content.split(/\r?\n/);
|
|
83
|
+
if (lines[lines.length - 1] === '') lines.pop();
|
|
84
|
+
|
|
85
|
+
if (maxLines !== null && lines.length > maxLines) {
|
|
86
|
+
return {
|
|
87
|
+
title,
|
|
88
|
+
path: relativePath.split(path.sep).join('/'),
|
|
89
|
+
lines: [...lines.slice(0, maxLines), `truncated after ${maxLines} lines`],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
title,
|
|
95
|
+
path: relativePath.split(path.sep).join('/'),
|
|
96
|
+
lines,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function logsDigest(args = [], options = {}) {
|
|
101
|
+
const root = options.cwd || process.cwd();
|
|
102
|
+
const date = digestDate(args);
|
|
103
|
+
const sections = [];
|
|
104
|
+
const workspacePath = path.join('atris', 'logs', date.slice(0, 4), `${date}.md`);
|
|
105
|
+
const workspaceSection = readDigestSection(root, 'Workspace journal', workspacePath);
|
|
106
|
+
if (workspaceSection) sections.push(workspaceSection);
|
|
107
|
+
|
|
108
|
+
const teamRoot = path.join(root, 'atris', 'team');
|
|
109
|
+
if (fs.existsSync(teamRoot) && fs.statSync(teamRoot).isDirectory()) {
|
|
110
|
+
const members = fs.readdirSync(teamRoot, { withFileTypes: true })
|
|
111
|
+
.filter((entry) => entry.isDirectory())
|
|
112
|
+
.map((entry) => entry.name)
|
|
113
|
+
.sort((left, right) => left.localeCompare(right));
|
|
114
|
+
|
|
115
|
+
for (const member of members) {
|
|
116
|
+
const relativePath = path.join('atris', 'team', member, 'logs', `${date}.md`);
|
|
117
|
+
const section = readDigestSection(root, `Team: ${member}`, relativePath, 80);
|
|
118
|
+
if (section) sections.push(section);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const payload = { date, sections };
|
|
123
|
+
if (args.includes('--json')) {
|
|
124
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
125
|
+
} else if (sections.length === 0) {
|
|
126
|
+
console.log(`No logs for ${date} yet.`);
|
|
127
|
+
} else {
|
|
128
|
+
console.log(sections.map((section) => [
|
|
129
|
+
`=== ${section.title} ===`,
|
|
130
|
+
section.path,
|
|
131
|
+
...section.lines,
|
|
132
|
+
].join('\n')).join('\n\n'));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return payload;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { logAtris, logsDigest };
|