atris 3.74.22 → 3.74.24
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/README.md +17 -0
- package/bin/atris.js +10 -1
- package/commands/improve.js +1 -1
- package/commands/mission.js +20 -4
- package/commands/rsi.js +25 -1
- package/commands/scorecard.js +297 -0
- package/commands/spend.js +78 -0
- package/lib/claude-spend-guard.js +288 -0
- package/lib/engine-ask.js +5 -2
- package/lib/engine-validate.js +2 -1
- package/lib/known-commands.js +2 -2
- package/lib/member-save-work.js +2 -1
- package/lib/repo-scorecard.js +1135 -0
- package/lib/roster-models.js +3 -1
- package/lib/rsi-loop.js +353 -0
- package/lib/runner-command.js +66 -2
- package/package.json +1 -1
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Claude spend guard. On 2026-10-28 unattended jobs ran Opus and Fable models
|
|
4
|
+
// in loops and billed about $900 of paid Claude usage in one day.
|
|
5
|
+
//
|
|
6
|
+
// The rule, enforced in two places:
|
|
7
|
+
// 1. Our own launchers call checkClaudeSpend() before starting `claude`, and
|
|
8
|
+
// hand it claudeChildEnv() so the pay-per-use key never reaches it.
|
|
9
|
+
// 2. `atris spend guard install` adds a Claude Code hook on this computer that
|
|
10
|
+
// applies the same rule to every unattended `claude` run, whatever script
|
|
11
|
+
// started it.
|
|
12
|
+
//
|
|
13
|
+
// Haiku runs freely. Any other Claude model in an unattended run needs the
|
|
14
|
+
// owner's yes for that run: ATRIS_CLAUDE_SPEND_OK=<model>[,<model>].
|
|
15
|
+
// A person typing in an interactive Claude session is never blocked.
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const os = require('os');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
const APPROVAL_ENV = 'ATRIS_CLAUDE_SPEND_OK';
|
|
23
|
+
const KEY_OK_ENV = 'ATRIS_CLAUDE_KEY_OK';
|
|
24
|
+
// Either of these moves a `claude` run from the plan login to metered billing.
|
|
25
|
+
const PAID_KEY_VARS = ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN'];
|
|
26
|
+
const HOOK_MARK = 'claude-spend-guard';
|
|
27
|
+
|
|
28
|
+
function home(env = process.env) {
|
|
29
|
+
return env.HOME || os.homedir();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ledgerPath(env = process.env) {
|
|
33
|
+
return path.join(home(env), '.atris', 'spend', 'claude-guard.jsonl');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isFreeModel(model) {
|
|
37
|
+
return /haiku/i.test(String(model || ''));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function approvedModels(env = process.env) {
|
|
41
|
+
return String(env[APPROVAL_ENV] || '')
|
|
42
|
+
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasPaidKey(env = process.env) {
|
|
46
|
+
return PAID_KEY_VARS.some((name) => Boolean(env[name]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The model a `claude` run will use when no --model flag is given: the
|
|
50
|
+
// ANTHROPIC_MODEL env var, then the user's settings, then the account default,
|
|
51
|
+
// which on Max is Opus.
|
|
52
|
+
function defaultClaudeModel(env = process.env) {
|
|
53
|
+
if (env.ANTHROPIC_MODEL) return env.ANTHROPIC_MODEL;
|
|
54
|
+
try {
|
|
55
|
+
const settings = JSON.parse(fs.readFileSync(path.join(home(env), '.claude', 'settings.json'), 'utf8'));
|
|
56
|
+
if (settings && typeof settings.model === 'string' && settings.model) return settings.model;
|
|
57
|
+
} catch {}
|
|
58
|
+
return 'default';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// `--model haiku` can be remapped by ANTHROPIC_DEFAULT_HAIKU_MODEL; judge the
|
|
62
|
+
// model that actually runs.
|
|
63
|
+
function resolveAlias(model, env = process.env) {
|
|
64
|
+
const alias = String(model).toLowerCase().replace(/\[1m\]$/, '');
|
|
65
|
+
const mapped = { opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL', sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
66
|
+
haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL' }[alias];
|
|
67
|
+
return (mapped && env[mapped]) || model;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function record(entry, env = process.env) {
|
|
71
|
+
try {
|
|
72
|
+
const file = ledgerPath(env);
|
|
73
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
74
|
+
fs.appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), host: os.hostname(), ...entry }) + '\n');
|
|
75
|
+
} catch {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function blockMessage(model, job) {
|
|
79
|
+
const who = job ? `${job}: ` : '';
|
|
80
|
+
return `${who}${model} is a paid Claude model and this run is unattended. Haiku runs free. `
|
|
81
|
+
+ `Get the owner's yes with a dollar estimate, then rerun with ${APPROVAL_ENV}=${model}.`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Decide whether an unattended Claude run may start.
|
|
85
|
+
// Returns { ok, reason, message }. Never throws.
|
|
86
|
+
function checkClaudeSpend({ model, job = '', env = process.env, checkKey = false, log = true } = {}) {
|
|
87
|
+
const resolved = resolveAlias(String(model || '').trim() || defaultClaudeModel(env), env);
|
|
88
|
+
if (checkKey && hasPaidKey(env) && env[KEY_OK_ENV] !== '1') {
|
|
89
|
+
const message = `${job ? `${job}: ` : ''}this unattended Claude run carries a pay-per-use key `
|
|
90
|
+
+ `(${PAID_KEY_VARS.join(' or ')}). Unset it so the run uses the plan login.`;
|
|
91
|
+
if (log) record({ decision: 'blocked', reason: 'paid_key', model: resolved, job, cwd: process.cwd() }, env);
|
|
92
|
+
return { ok: false, reason: 'paid_key', model: resolved, message };
|
|
93
|
+
}
|
|
94
|
+
if (isFreeModel(resolved)) return { ok: true, reason: 'free_model', model: resolved };
|
|
95
|
+
if (approvedModels(env).includes(resolved.toLowerCase())) {
|
|
96
|
+
if (log) record({ decision: 'approved', model: resolved, job, cwd: process.cwd() }, env);
|
|
97
|
+
return { ok: true, reason: 'approved', model: resolved };
|
|
98
|
+
}
|
|
99
|
+
if (log) record({ decision: 'blocked', reason: 'paid_model', model: resolved, job, cwd: process.cwd() }, env);
|
|
100
|
+
return { ok: false, reason: 'paid_model', model: resolved, message: blockMessage(resolved, job) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// A copy of env safe to hand a `claude` child: no pay-per-use key, so the run
|
|
104
|
+
// bills the plan login. ATRIS_CLAUDE_KEY_OK=1 keeps the key (servers only).
|
|
105
|
+
function claudeChildEnv(env = process.env) {
|
|
106
|
+
const out = { ...env };
|
|
107
|
+
if (out[KEY_OK_ENV] === '1') return out;
|
|
108
|
+
for (const name of PAID_KEY_VARS) delete out[name];
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- Claude Code hook --------------------------------------------------
|
|
113
|
+
|
|
114
|
+
function processArgs(pid) {
|
|
115
|
+
try {
|
|
116
|
+
if (process.platform === 'linux') {
|
|
117
|
+
return fs.readFileSync(`/proc/${pid}/cmdline`, 'utf8').split('\0').filter(Boolean);
|
|
118
|
+
}
|
|
119
|
+
const line = execFileSync('ps', ['-o', 'args=', '-p', String(pid)], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
120
|
+
return line ? line.split(/\s+/) : [];
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parentPid(pid) {
|
|
127
|
+
try {
|
|
128
|
+
const out = execFileSync('ps', ['-o', 'ppid=', '-p', String(pid)], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
129
|
+
const n = Number(out);
|
|
130
|
+
return Number.isInteger(n) && n > 1 ? n : 0;
|
|
131
|
+
} catch {
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isClaudeArgv(argv) {
|
|
137
|
+
if (!argv.length) return false;
|
|
138
|
+
if (path.basename(argv[0]) === 'claude') return true;
|
|
139
|
+
return argv.slice(0, 3).some((a) => /claude-code|[\\/]claude$/.test(a));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The argv of the `claude` process that ran this hook. Hooks run as direct
|
|
143
|
+
// children of claude; walk a few levels up in case a shell sits between.
|
|
144
|
+
function claudeArgv(startPid = process.ppid) {
|
|
145
|
+
let pid = startPid;
|
|
146
|
+
for (let i = 0; i < 4 && pid; i += 1) {
|
|
147
|
+
const argv = processArgs(pid);
|
|
148
|
+
if (isClaudeArgv(argv)) return argv;
|
|
149
|
+
pid = parentPid(pid);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function modelFromArgv(argv) {
|
|
155
|
+
let model = '';
|
|
156
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
157
|
+
if (argv[i] === '--model' && argv[i + 1]) model = argv[i + 1];
|
|
158
|
+
else if (argv[i].startsWith('--model=')) model = argv[i].slice('--model='.length);
|
|
159
|
+
}
|
|
160
|
+
return model;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Unattended means print mode or an SDK run. A person's interactive session
|
|
164
|
+
// reports CLAUDE_CODE_ENTRYPOINT=cli and has no -p flag.
|
|
165
|
+
function isUnattended(argv, env = process.env) {
|
|
166
|
+
if (String(env.CLAUDE_CODE_ENTRYPOINT || '').startsWith('sdk')) return true;
|
|
167
|
+
return argv.includes('-p') || argv.includes('--print');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Decide for the hook. Returns { block, message }.
|
|
171
|
+
function hookDecision({ argv, env = process.env }) {
|
|
172
|
+
if (!argv || !isUnattended(argv, env)) return { block: false };
|
|
173
|
+
const verdict = checkClaudeSpend({ model: modelFromArgv(argv), job: 'claude spend guard', env, checkKey: true });
|
|
174
|
+
return verdict.ok ? { block: false } : { block: true, message: verdict.message };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readStdin() {
|
|
178
|
+
try {
|
|
179
|
+
return fs.readFileSync(0, 'utf8');
|
|
180
|
+
} catch {
|
|
181
|
+
return '';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Entry point for the UserPromptSubmit hook. Exit 2 stops the run before any
|
|
186
|
+
// model call; anything unexpected fails open so a guard bug never breaks work.
|
|
187
|
+
function hookMain() {
|
|
188
|
+
let event = {};
|
|
189
|
+
try { event = JSON.parse(readStdin() || '{}'); } catch {}
|
|
190
|
+
if (event.hook_event_name && event.hook_event_name !== 'UserPromptSubmit') return 0;
|
|
191
|
+
const decision = hookDecision({ argv: claudeArgv() });
|
|
192
|
+
if (!decision.block) return 0;
|
|
193
|
+
try { fs.writeSync(2, `${decision.message}\n`); } catch {}
|
|
194
|
+
return 2;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- install into ~/.claude/settings.json ------------------------------
|
|
198
|
+
|
|
199
|
+
function settingsPath(env = process.env) {
|
|
200
|
+
return path.join(home(env), '.claude', 'settings.json');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function hookScriptPath(env = process.env) {
|
|
204
|
+
return path.join(home(env), '.claude', 'hooks', `${HOOK_MARK}.js`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function nodeBinary() {
|
|
208
|
+
for (const candidate of ['/opt/homebrew/bin/node', '/usr/local/bin/node', '/usr/bin/node']) {
|
|
209
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
210
|
+
}
|
|
211
|
+
return process.execPath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function readSettings(file) {
|
|
215
|
+
if (!fs.existsSync(file)) return {};
|
|
216
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
217
|
+
if (!text.trim()) return {};
|
|
218
|
+
return JSON.parse(text);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function guardEntries(settings) {
|
|
222
|
+
const groups = (settings.hooks && settings.hooks.UserPromptSubmit) || [];
|
|
223
|
+
return groups.filter((g) => (g.hooks || []).some((h) => String(h.command || '').includes(HOOK_MARK)));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function guardStatus(env = process.env) {
|
|
227
|
+
let settings = {};
|
|
228
|
+
let readable = true;
|
|
229
|
+
try { settings = readSettings(settingsPath(env)); } catch { readable = false; }
|
|
230
|
+
return {
|
|
231
|
+
installed: readable && guardEntries(settings).length > 0 && fs.existsSync(hookScriptPath(env)),
|
|
232
|
+
settings: settingsPath(env),
|
|
233
|
+
script: hookScriptPath(env),
|
|
234
|
+
readable,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function installGuard({ env = process.env, modulePath = __filename } = {}) {
|
|
239
|
+
const file = settingsPath(env);
|
|
240
|
+
const settings = readSettings(file); // throws on unparseable settings: never clobber them
|
|
241
|
+
// The hook is a copy of this file, so it keeps working if the CLI moves.
|
|
242
|
+
// Rerun install after a CLI update to pick up guard fixes.
|
|
243
|
+
const script = hookScriptPath(env);
|
|
244
|
+
fs.mkdirSync(path.dirname(script), { recursive: true });
|
|
245
|
+
fs.copyFileSync(modulePath, script);
|
|
246
|
+
fs.chmodSync(script, 0o755);
|
|
247
|
+
settings.hooks = settings.hooks || {};
|
|
248
|
+
const groups = settings.hooks.UserPromptSubmit || [];
|
|
249
|
+
const kept = groups.filter((g) => !guardEntries({ hooks: { UserPromptSubmit: [g] } }).length);
|
|
250
|
+
// First in line, so it runs before any hook that might spend.
|
|
251
|
+
settings.hooks.UserPromptSubmit = [
|
|
252
|
+
{ hooks: [{ type: 'command', command: `${nodeBinary()} ${script}` }] },
|
|
253
|
+
...kept,
|
|
254
|
+
];
|
|
255
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
256
|
+
const tmp = `${file}.atris-tmp-${process.pid}`;
|
|
257
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
|
|
258
|
+
fs.renameSync(tmp, file);
|
|
259
|
+
return guardStatus(env);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function uninstallGuard({ env = process.env } = {}) {
|
|
263
|
+
const file = settingsPath(env);
|
|
264
|
+
const settings = readSettings(file);
|
|
265
|
+
if (settings.hooks && settings.hooks.UserPromptSubmit) {
|
|
266
|
+
const kept = settings.hooks.UserPromptSubmit.filter((g) => !guardEntries({ hooks: { UserPromptSubmit: [g] } }).length);
|
|
267
|
+
if (kept.length) settings.hooks.UserPromptSubmit = kept;
|
|
268
|
+
else delete settings.hooks.UserPromptSubmit;
|
|
269
|
+
const tmp = `${file}.atris-tmp-${process.pid}`;
|
|
270
|
+
fs.writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n');
|
|
271
|
+
fs.renameSync(tmp, file);
|
|
272
|
+
}
|
|
273
|
+
try { fs.unlinkSync(hookScriptPath(env)); } catch {}
|
|
274
|
+
return guardStatus(env);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
APPROVAL_ENV,
|
|
279
|
+
checkClaudeSpend,
|
|
280
|
+
claudeChildEnv,
|
|
281
|
+
hookDecision,
|
|
282
|
+
guardStatus,
|
|
283
|
+
installGuard,
|
|
284
|
+
uninstallGuard,
|
|
285
|
+
ledgerPath,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
if (require.main === module) process.exit(hookMain());
|
package/lib/engine-ask.js
CHANGED
|
@@ -7,6 +7,7 @@ const path = require('path');
|
|
|
7
7
|
const { spawn, spawnSync } = require('child_process');
|
|
8
8
|
const {
|
|
9
9
|
DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
10
|
+
applySpendGate,
|
|
10
11
|
RUNNER_PROFILE_DEFS,
|
|
11
12
|
} = require('./runner-command');
|
|
12
13
|
const {
|
|
@@ -252,7 +253,9 @@ function resolveAskModel(engine, modelName = '') {
|
|
|
252
253
|
: currentClaudeModel(requested);
|
|
253
254
|
}
|
|
254
255
|
if (engine !== 'claude' && engine !== 'fable' && engine !== 'haiku') return '';
|
|
255
|
-
|
|
256
|
+
// No model named: this is the unattended default, so it goes through the
|
|
257
|
+
// spend gate. A model a person typed is their own yes.
|
|
258
|
+
return applySpendGate(RUNNER_PROFILE_DEFS[engine]?.model || DEFAULT_CLAUDE_RUNNER_MODEL);
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
// The effort flag as argv, from the same profile table the runners use
|
|
@@ -289,7 +292,7 @@ function buildReadOnlyEngineInvocation(engineName, prompt, modelName = '', {
|
|
|
289
292
|
bin: profile.bin,
|
|
290
293
|
args: [
|
|
291
294
|
'-p', request,
|
|
292
|
-
'--model', model || profile.model || DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
295
|
+
'--model', model || applySpendGate(profile.model || DEFAULT_CLAUDE_RUNNER_MODEL),
|
|
293
296
|
...effortArgs,
|
|
294
297
|
'--tools', 'Read,Glob,Grep,WebSearch,WebFetch',
|
|
295
298
|
'--permission-mode', 'plan',
|
package/lib/engine-validate.js
CHANGED
|
@@ -6,6 +6,7 @@ const os = require('node:os');
|
|
|
6
6
|
const path = require('node:path');
|
|
7
7
|
const {
|
|
8
8
|
DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
9
|
+
applySpendGate,
|
|
9
10
|
RUNNER_PROFILE_DEFS,
|
|
10
11
|
} = require('./runner-command');
|
|
11
12
|
const { canonicalEngineName } = require('./engine-registry');
|
|
@@ -78,7 +79,7 @@ function refereeModel(engine) {
|
|
|
78
79
|
const profile = RUNNER_PROFILE_DEFS[engine];
|
|
79
80
|
if (!profile) return '';
|
|
80
81
|
if (engine === 'claude' || engine === 'fable' || engine === 'haiku') {
|
|
81
|
-
return profile.model || DEFAULT_CLAUDE_RUNNER_MODEL;
|
|
82
|
+
return applySpendGate(profile.model || DEFAULT_CLAUDE_RUNNER_MODEL);
|
|
82
83
|
}
|
|
83
84
|
return profile.model || '';
|
|
84
85
|
}
|
package/lib/known-commands.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
const knownCommands = ['init', 'guide', 'doctor', 'doc-health', 'install', 'workspace', 'log', 'logs', 'later', 'idea', 'done', 'wish', 'ask', 'approve', 'stop', 'ready', 'check', 'drill', 'dream', 'now', '99', 'goal', 'wtf', 'founder', 'orb', 'radar', 'who', 'stream', 'ctop', 'launchpad', 'status', 'analytics', 'revisions', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
4
4
|
'activate', '_activate', 'agent', 'team', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
5
5
|
'clean', 'close', 'harvest', 'verify', 'recover', 'search', 'scout', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'taste', 'teach', 'plugin', 'experiments', 'bench', 'router', 'tree', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'watch', 'cloud', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube', 'x-search',
|
|
6
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'sync-checkout', 'zen',
|
|
7
|
-
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
6
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'decide', 'campaign', 'agents', 'probe', 'worktree', 'land', 'caretaker', 'autoland', 'drive', 'aeo', 'slop', 'voice', 'strings', 'write', 'security-review', 'secure', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'study', 'rainmaker', 'xp', 'play', 'gm', 'game', 'x', 'recap', 'report', 'signup', 'clarity', 'interview', 'meet', 'moves', 'unknowns', 'avail', 'scoreboard', 'scorecard', 'sync-checkout', 'zen',
|
|
7
|
+
'ci', 'github', 'vercel', 'supabase', 'linear', 'stripe', 'balance', 'usage', 'spend', 'api-key', 'agent-client', 'topup', 'buy', 'transactions', 'hire', 'hires', 'mail', 'design', 'mcp', 'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
8
8
|
'fork', 'browse', 'publish', 'pack', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'fleet-report', 'loops', 'self-improve', 'compile', 'spaceship', 'truth', 'sign', 'engine', 'engines', 'link', 'playbook', 'feed', 'brief', 'social', 'host', 'people', 'follow', 'unfollow', 'friends', 'msg', 'inbox', 'invite', 'join', 'rsi', 'pay', 'trust'];
|
|
9
9
|
|
|
10
10
|
// Damerau-Levenshtein edit distance between two short strings. Counts an
|
package/lib/member-save-work.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { spawnSync } = require('child_process');
|
|
6
|
+
const { TRAILER } = require('../commands/sign');
|
|
6
7
|
|
|
7
8
|
// A member that passed its check but never committed leaves its work loose in
|
|
8
9
|
// a side checkout that cleanup later deletes (member lab 2026-10-24: navigator
|
|
@@ -45,7 +46,7 @@ function saveLeftoverMemberWork({ worktreePath, member = 'member', objective = '
|
|
|
45
46
|
const staged = stagedAll.filter((file) => !SECRET_PATH.test(file));
|
|
46
47
|
if (!staged.length) return { saved: false, reason: 'nothing_left', ...(skipped.length ? { skipped } : {}) };
|
|
47
48
|
const title = String(objective || '').split('\n').map((line) => line.trim()).find(Boolean) || 'finished work';
|
|
48
|
-
const message = `${member}: ${title.slice(0, 72)}\n\nSaved by atris after the check passed; the member left these changes uncommitted
|
|
49
|
+
const message = `${member}: ${title.slice(0, 72)}\n\nSaved by atris after the check passed; the member left these changes uncommitted.\n\n${TRAILER}`;
|
|
49
50
|
const commit = git(['commit', '-q', '-m', message], worktreePath);
|
|
50
51
|
if (!commit.ok) return { saved: false, reason: 'commit_failed', files: staged };
|
|
51
52
|
return { saved: true, files: staged, ...(skipped.length ? { skipped } : {}), sha: git(['rev-parse', 'HEAD'], worktreePath).stdout };
|