atris 3.31.0 → 3.32.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/README.md +3 -3
- package/atris/CLAUDE.md +8 -0
- package/atris.md +8 -0
- package/ax +94 -4
- package/bin/atris.js +148 -73
- package/commands/autoland.js +70 -10
- package/commands/autopilot-front.js +273 -0
- package/commands/init.js +21 -0
- package/commands/member.js +32 -0
- package/commands/mission.js +386 -30
- package/commands/run-front.js +144 -0
- package/commands/run.js +9 -6
- package/commands/sign.js +90 -0
- package/commands/task.js +207 -6
- package/commands/truth.js +2 -1
- package/lib/autoland.js +133 -25
- package/lib/operator-next.js +7 -0
- package/lib/pulse.js +4 -3
- package/lib/runner-command.js +32 -13
- package/lib/task-db.js +19 -2
- package/package.json +2 -2
package/lib/autoland.js
CHANGED
|
@@ -12,6 +12,8 @@ const { spawnSync } = require('child_process');
|
|
|
12
12
|
|
|
13
13
|
const DEFAULT_DIGEST_HOUR = 9;
|
|
14
14
|
const DEFAULT_ALARM_HOURS = 24;
|
|
15
|
+
const OPERATOR_WHY = /\b(sav(?:e|es|ing)|burn(?:s|ing)?|wast(?:e|es|ing)|cost(?:s|ing)?|prevent\w*|stop(?:s|ping)?|break(?:s|ing)?|fail(?:s|ing)?|slow(?:s|er)?|faster|cheaper|easier|clearer|safer|simpler|trust|revenue|users?|customers?|so that|because)\b/i;
|
|
16
|
+
const AGENT_JARGON = /[a-z0-9]_[a-z0-9]|\b[a-z]+[A-Z]\w*|--[a-z]|\bCLI-\d+/;
|
|
15
17
|
|
|
16
18
|
function policyPath(root) {
|
|
17
19
|
return path.join(root, '.atris', 'policy', 'autoland.json');
|
|
@@ -131,7 +133,7 @@ function waitingOnHuman(tasks, now = Date.now()) {
|
|
|
131
133
|
.filter((t) => t.review?.agent_certified === true || t.metadata?.agent_certified === true)
|
|
132
134
|
.map((t) => ({
|
|
133
135
|
ref: t.display_id || t.legacy_ref || t.id,
|
|
134
|
-
title: String(t.title || '').slice(0,
|
|
136
|
+
title: String(t.title || '').slice(0, 200),
|
|
135
137
|
tag: String(t.tag || ''),
|
|
136
138
|
hours: waitingHours(t, now),
|
|
137
139
|
}))
|
|
@@ -168,16 +170,54 @@ function cutAtWord(text, max) {
|
|
|
168
170
|
return `${cut.slice(0, lastSpace > max * 0.6 ? lastSpace : max)}...`;
|
|
169
171
|
}
|
|
170
172
|
|
|
173
|
+
// A line ends where a thought ends. Long lines close at their last complete
|
|
174
|
+
// clause inside the budget and read whole: no ellipsis, nothing dangling.
|
|
175
|
+
// Only when a line has no clause boundary at all does a word cut remain.
|
|
176
|
+
function finishThought(text, max = 160) {
|
|
177
|
+
const clean = String(text || '').replace(/\s+/g, ' ').trim().replace(/[,;:\s]+$/, '');
|
|
178
|
+
if (clean.length <= max) return clean;
|
|
179
|
+
const cut = clean.slice(0, max);
|
|
180
|
+
const sentence = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '));
|
|
181
|
+
if (sentence > max * 0.3) return cut.slice(0, sentence + 1);
|
|
182
|
+
const clause = Math.max(cut.lastIndexOf(', '), cut.lastIndexOf('; '), cut.lastIndexOf(' ('), cut.lastIndexOf(': '));
|
|
183
|
+
if (clause > max * 0.45) return cut.slice(0, clause);
|
|
184
|
+
const space = cut.lastIndexOf(' ');
|
|
185
|
+
return `${cut.slice(0, space > max * 0.6 ? space : max)}...`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function operatorReady(text) {
|
|
189
|
+
const value = String(text || '');
|
|
190
|
+
return OPERATOR_WHY.test(value) && !AGENT_JARGON.test(value);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function hasAgentJargon(text) {
|
|
194
|
+
return AGENT_JARGON.test(String(text || ''));
|
|
195
|
+
}
|
|
196
|
+
|
|
171
197
|
// One line per piece, in the work's own words: the landing sentence beats
|
|
172
198
|
// the task title, because titles are written for the queue, not for a text.
|
|
199
|
+
// The cut is generous on purpose: a specific sentence the reader trusts
|
|
200
|
+
// (what happened + how big + how we know) beats a short one they must chase.
|
|
173
201
|
function digestLine(item) {
|
|
174
|
-
const happened = String(item.happened || '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
202
|
+
const happened = String(item.happened || '').replace(/^completed:\s*/i, '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
175
203
|
const title = String(item.title || '').replace(/^Mission XP:\s*/i, '');
|
|
176
204
|
const source = happened.length >= 12 ? happened : title;
|
|
177
|
-
const line =
|
|
205
|
+
const line = finishThought(source.replace(/[.\s]+$/, ''), 160);
|
|
178
206
|
return line.charAt(0).toLowerCase() + line.slice(1);
|
|
179
207
|
}
|
|
180
208
|
|
|
209
|
+
// A rough sentence still beats a count: when a landing sentence carries agent
|
|
210
|
+
// vocabulary, strip what a reader can't use (flag dashes, ids, underscores)
|
|
211
|
+
// and show it anyway. Content always ships; the gate only changes how it reads.
|
|
212
|
+
function dejargon(line) {
|
|
213
|
+
return String(line || '')
|
|
214
|
+
.replace(/--([a-z][a-z-]*)/g, '$1')
|
|
215
|
+
.replace(/\bCLI-\d+\b/g, '')
|
|
216
|
+
.replace(/([a-z0-9])_([a-z0-9])/gi, '$1 $2')
|
|
217
|
+
.replace(/\s+/g, ' ')
|
|
218
|
+
.trim();
|
|
219
|
+
}
|
|
220
|
+
|
|
181
221
|
// The mission loop files a check-off every time it stops instead of
|
|
182
222
|
// inventing work; those all say the same thing, so they collapse into one line.
|
|
183
223
|
function isCleanStop(item) {
|
|
@@ -185,40 +225,105 @@ function isCleanStop(item) {
|
|
|
185
225
|
|| /^(Mission XP:\s*)?Decide and start the next useful mission/i.test(String(item.title || ''));
|
|
186
226
|
}
|
|
187
227
|
|
|
188
|
-
// Short enough for one iMessage, plain enough to read
|
|
189
|
-
//
|
|
190
|
-
|
|
228
|
+
// Short enough for one iMessage, plain enough to read walking to the car.
|
|
229
|
+
// Every line answers: what happened, how big, how we know. It names the work
|
|
230
|
+
// because "7 things landed" tells the score, not the story — and it always
|
|
231
|
+
// ends with what's waiting and what to do next, because those are the only
|
|
232
|
+
// lines that ask anything of the reader.
|
|
233
|
+
function composeDigest({ accepted, waiting, landed, project, nextMoves = [] }) {
|
|
191
234
|
const lines = [];
|
|
192
|
-
lines.push(`atris ${project}
|
|
235
|
+
lines.push(`atris ${project}: yesterday`);
|
|
193
236
|
const autoCount = accepted.auto.length;
|
|
194
237
|
const humanCount = accepted.human.length;
|
|
195
238
|
if (autoCount > 0) {
|
|
196
|
-
|
|
239
|
+
// Three results, air between them, the rest on ask: the whole message
|
|
240
|
+
// fits a laptop screen with no scrolling. Reading it is one glance.
|
|
197
241
|
const stops = accepted.auto.filter(isCleanStop);
|
|
198
|
-
const work = accepted.auto
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
242
|
+
const work = accepted.auto
|
|
243
|
+
.filter((item) => !isCleanStop(item))
|
|
244
|
+
.map((item) => ({ item, line: digestLine(item) }));
|
|
245
|
+
// Operator-ready sentences lead; rough ones get de-jargoned and still
|
|
246
|
+
// shown. An empty report is the one thing a report must never be.
|
|
247
|
+
const ready = work.filter(({ line }) => operatorReady(line));
|
|
248
|
+
const rough = work.filter(({ line }) => !operatorReady(line))
|
|
249
|
+
.map(({ item, line }) => ({ item, line: dejargon(line) }));
|
|
250
|
+
const visibleWork = [...ready, ...rough].slice(0, 3);
|
|
251
|
+
const held = Math.max(0, work.length - visibleWork.length) + (stops.length > 0 ? 1 : 0);
|
|
252
|
+
if (visibleWork.length > 0) {
|
|
253
|
+
lines.push('landed on their own (verified twice, proof on file):');
|
|
254
|
+
for (const { item, line } of visibleWork) {
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push(`- ${line || item.ref}${item.member ? ` (${item.member})` : ''}`);
|
|
257
|
+
}
|
|
258
|
+
lines.push('');
|
|
259
|
+
if (held > 0) lines.push(`${plur(held, 'more result')} when you want them: atris autoland digest`);
|
|
260
|
+
}
|
|
202
261
|
}
|
|
203
|
-
if (humanCount > 0) lines.push(`you approved ${plur(humanCount, 'piece')} yourself`);
|
|
204
262
|
if (autoCount + humanCount === 0) lines.push('nothing finished in the last day');
|
|
205
|
-
const byMember = new Map();
|
|
206
|
-
for (const item of [...accepted.auto, ...accepted.human]) {
|
|
207
|
-
if (!item.member || isCleanStop(item)) continue;
|
|
208
|
-
byMember.set(item.member, (byMember.get(item.member) || 0) + 1);
|
|
209
|
-
}
|
|
210
|
-
if (byMember.size > 0) {
|
|
211
|
-
lines.push(`workers: ${Array.from(byMember, ([who, n]) => `${who} landed ${n}`).join(', ')}`);
|
|
212
|
-
}
|
|
213
263
|
if (waiting.length > 0) {
|
|
214
|
-
lines
|
|
264
|
+
// The only lines that ask the reader to act, so they name what's asking.
|
|
265
|
+
lines.push(`waiting on you (approve or bounce: atris task reviews):`);
|
|
266
|
+
for (const item of waiting.slice(0, 2)) {
|
|
267
|
+
lines.push(`- ${finishThought(dejargon(item.title), 140)}${item.hours >= 12 ? ` (${item.hours}h)` : ''}`);
|
|
268
|
+
}
|
|
269
|
+
if (waiting.length > 2) lines.push(`- and ${waiting.length - 2} more`);
|
|
215
270
|
} else {
|
|
216
|
-
lines.push('
|
|
271
|
+
lines.push('waiting on you: nothing');
|
|
217
272
|
}
|
|
218
273
|
if (landed && landed.branches > 0) {
|
|
219
|
-
lines.push(
|
|
274
|
+
lines.push(`in the air: ${plur(landed.branches, 'piece')}${landed.due > 0 ? `, ${landed.due} overdue` : ', all fresh'} (atris land)`);
|
|
275
|
+
}
|
|
276
|
+
// nextMoves: array of {title, owner}, or {moves, unexplained} where
|
|
277
|
+
// unexplained counts queue items whose sentence carries no operator why.
|
|
278
|
+
// Those are counted, not shown — a raw title the reader can't act on is
|
|
279
|
+
// noise here and a writing bug at its source.
|
|
280
|
+
const nextInfo = Array.isArray(nextMoves)
|
|
281
|
+
? { moves: nextMoves, unexplained: 0 }
|
|
282
|
+
: (nextMoves || { moves: [], unexplained: 0 });
|
|
283
|
+
const moves = (nextInfo.moves || []).filter((move) => move && move.title).slice(0, 3);
|
|
284
|
+
const unexplained = Number(nextInfo.unexplained || 0);
|
|
285
|
+
if (moves.length > 0) {
|
|
286
|
+
lines.push('next, if you agree:');
|
|
287
|
+
for (const move of moves) {
|
|
288
|
+
lines.push(`- ${finishThought(move.title, 140)}${move.owner ? ` (best fit: ${move.owner})` : ''}`);
|
|
289
|
+
}
|
|
290
|
+
if (unexplained > 0) lines.push(`- ${plur(unexplained, 'more idea')} that can't explain themselves yet (atris now)`);
|
|
291
|
+
} else if (unexplained > 0) {
|
|
292
|
+
lines.push(`${plur(unexplained, 'idea')} in the queue can't explain themselves yet (atris now)`);
|
|
293
|
+
}
|
|
294
|
+
if (!lines.some((line) => line.includes('atris autoland digest'))) {
|
|
295
|
+
lines.push('the full story: atris autoland digest');
|
|
296
|
+
}
|
|
297
|
+
return lines.join('\n');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// The moment something lands, the operator hears it in one glance: the
|
|
301
|
+
// landing sentence each piece wrote for a day-one PM at finish time, its
|
|
302
|
+
// author, nothing else. Rough sentences get de-jargoned, never hidden.
|
|
303
|
+
function composeLiveUpdate({ landedRefs, tasks, project }) {
|
|
304
|
+
const byRef = new Map((tasks || []).map((t) => [String(t.display_id || t.legacy_ref || t.id), t]));
|
|
305
|
+
const items = (landedRefs || []).map((ref) => {
|
|
306
|
+
const t = byRef.get(String(ref));
|
|
307
|
+
if (!t) return { title: String(ref) };
|
|
308
|
+
return {
|
|
309
|
+
ref: String(ref),
|
|
310
|
+
title: String(t.title || '').slice(0, 200),
|
|
311
|
+
happened: String(t.review?.landing?.happened || t.metadata?.landing_happened || '').slice(0, 300),
|
|
312
|
+
member: String(t.claimed_by || t.assigned_to || '').trim(),
|
|
313
|
+
};
|
|
314
|
+
}).filter((item) => !isCleanStop(item));
|
|
315
|
+
if (!items.length) return '';
|
|
316
|
+
const lines = [`atris ${project}: just landed`];
|
|
317
|
+
for (const item of items.slice(0, 3)) {
|
|
318
|
+
const raw = digestLine(item);
|
|
319
|
+
const line = operatorReady(raw) ? raw : dejargon(raw);
|
|
320
|
+
lines.push('');
|
|
321
|
+
lines.push(`- ${finishThought(line, 160)}${item.member ? ` (${item.member})` : ''}`);
|
|
322
|
+
}
|
|
323
|
+
if (items.length > 3) {
|
|
324
|
+
lines.push('');
|
|
325
|
+
lines.push(`and ${plur(items.length - 3, 'more piece')}: atris autoland digest`);
|
|
220
326
|
}
|
|
221
|
-
lines.push('the full story: atris autoland digest');
|
|
222
327
|
return lines.join('\n');
|
|
223
328
|
}
|
|
224
329
|
|
|
@@ -264,12 +369,15 @@ module.exports = {
|
|
|
264
369
|
buildCronLine,
|
|
265
370
|
composeAlarm,
|
|
266
371
|
composeDigest,
|
|
372
|
+
composeLiveUpdate,
|
|
267
373
|
cronInstalled,
|
|
268
374
|
cronMarker,
|
|
269
375
|
dueForAlarm,
|
|
270
376
|
installCron,
|
|
271
377
|
liveAcceptAuthorization,
|
|
272
378
|
markAlerted,
|
|
379
|
+
operatorReady,
|
|
380
|
+
hasAgentJargon,
|
|
273
381
|
policyPath,
|
|
274
382
|
readPolicy,
|
|
275
383
|
readState,
|
package/lib/pulse.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const { DEFAULT_CLAUDE_RUNNER_MODEL } = require('./runner-command');
|
|
17
18
|
|
|
18
19
|
const PULSE_RECEIPT_SCHEMA = 'atris.pulse_tick.v1';
|
|
19
20
|
// Reuse the improve-tick scorecard schema so the brain + policy-lessons see
|
|
@@ -354,7 +355,7 @@ function buildTickScript(opts = {}) {
|
|
|
354
355
|
stateHome,
|
|
355
356
|
deadlineEpoch,
|
|
356
357
|
marker = PULSE_MARKER,
|
|
357
|
-
model =
|
|
358
|
+
model = DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
358
359
|
runnerProfile = '',
|
|
359
360
|
runnerBin = '',
|
|
360
361
|
runnerCommandTemplate = '',
|
|
@@ -419,8 +420,8 @@ log="$LOG_DIR/$stamp.log"
|
|
|
419
420
|
|
|
420
421
|
cd "$ROOT" || { echo "$(date -Iseconds) ROOT missing" >> "$LOG_DIR/control.log"; exit 1; }
|
|
421
422
|
|
|
422
|
-
# Autonomous ticks
|
|
423
|
-
#
|
|
423
|
+
# Autonomous ticks use the pinned default unless the installer supplied a model.
|
|
424
|
+
# Operators can still override per install with --model or env.
|
|
424
425
|
${runnerModelExport}
|
|
425
426
|
${runnerProfileExport}
|
|
426
427
|
${runnerBinExport}
|
package/lib/runner-command.js
CHANGED
|
@@ -10,24 +10,40 @@
|
|
|
10
10
|
// ATRIS_RUNNER_PROFILE -> legacy ATRIS_CLAUDE_MODEL env -> pinned default.
|
|
11
11
|
const DEFAULT_CLAUDE_RUNNER_MODEL = 'claude-opus-4-8';
|
|
12
12
|
const DEFAULT_CLAUDE_RUNNER_BIN = 'claude';
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
// Canonical runner profiles. Each entry is the config an operator would pick
|
|
15
|
+
// on purpose. Aliases (spelling variants of the same profile) resolve to the
|
|
16
|
+
// canonical config but are kept OUT of operator-facing lists so config errors
|
|
17
|
+
// stay honest: "one of: atris-fast", not three duplicate spellings.
|
|
18
|
+
const RUNNER_PROFILE_DEFS = Object.freeze({
|
|
14
19
|
'atris-fast': Object.freeze({
|
|
15
20
|
bin: 'ax',
|
|
16
21
|
model: 'atris:fast',
|
|
17
22
|
commandTemplate: '{bin} --fast {prompt}',
|
|
18
23
|
}),
|
|
19
|
-
'atris2-fast': Object.freeze({
|
|
20
|
-
bin: 'ax',
|
|
21
|
-
model: 'atris:fast',
|
|
22
|
-
commandTemplate: '{bin} --fast {prompt}',
|
|
23
|
-
}),
|
|
24
|
-
'atris-2-fast': Object.freeze({
|
|
25
|
-
bin: 'ax',
|
|
26
|
-
model: 'atris:fast',
|
|
27
|
-
commandTemplate: '{bin} --fast {prompt}',
|
|
28
|
-
}),
|
|
29
24
|
});
|
|
30
25
|
|
|
26
|
+
// Alias -> canonical profile name. Every alias resolves to the same config as
|
|
27
|
+
// its canonical target; adding a spelling variant here is a config change, not
|
|
28
|
+
// a duplicated profile body that can drift.
|
|
29
|
+
const RUNNER_PROFILE_ALIASES = Object.freeze({
|
|
30
|
+
'atris2-fast': 'atris-fast',
|
|
31
|
+
'atris-2-fast': 'atris-fast',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Back-compat surface: RUNNER_PROFILES still resolves every accepted name
|
|
35
|
+
// (canonical + alias) to its frozen config, so existing lookups by any spelling
|
|
36
|
+
// keep working.
|
|
37
|
+
const RUNNER_PROFILES = Object.freeze(
|
|
38
|
+
Object.fromEntries([
|
|
39
|
+
...Object.entries(RUNNER_PROFILE_DEFS),
|
|
40
|
+
...Object.entries(RUNNER_PROFILE_ALIASES).map(([alias, target]) => [alias, RUNNER_PROFILE_DEFS[target]]),
|
|
41
|
+
])
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Operator-facing list: canonical profile names only (no alias noise).
|
|
45
|
+
const RUNNER_PROFILE_NAMES = Object.freeze(Object.keys(RUNNER_PROFILE_DEFS));
|
|
46
|
+
|
|
31
47
|
function shellWord(value) {
|
|
32
48
|
const s = String(value || '');
|
|
33
49
|
if (/^[A-Za-z0-9_./:-]+$/.test(s)) return s;
|
|
@@ -51,7 +67,7 @@ function resolveRunnerProfile() {
|
|
|
51
67
|
if (!name) return null;
|
|
52
68
|
const profile = RUNNER_PROFILES[name];
|
|
53
69
|
if (!profile) {
|
|
54
|
-
throw new Error(`Unknown ATRIS_RUNNER_PROFILE "${name}". Known profiles: ${
|
|
70
|
+
throw new Error(`Unknown ATRIS_RUNNER_PROFILE "${name}". Known profiles: ${RUNNER_PROFILE_NAMES.join(', ')}`);
|
|
55
71
|
}
|
|
56
72
|
return profile;
|
|
57
73
|
}
|
|
@@ -98,7 +114,7 @@ function buildRunnerAvailabilityCommand() {
|
|
|
98
114
|
function runnerAvailabilityFailureMessage(error) {
|
|
99
115
|
const message = error && error.message ? String(error.message).trim() : '';
|
|
100
116
|
if (message.startsWith('Unknown ATRIS_RUNNER_PROFILE')) {
|
|
101
|
-
return `${message}. Set ATRIS_RUNNER_PROFILE to one of: ${
|
|
117
|
+
return `${message}. Set ATRIS_RUNNER_PROFILE to one of: ${RUNNER_PROFILE_NAMES.join(', ')}.`;
|
|
102
118
|
}
|
|
103
119
|
|
|
104
120
|
let runnerBin = 'configured runner';
|
|
@@ -154,6 +170,9 @@ module.exports = {
|
|
|
154
170
|
DEFAULT_CLAUDE_RUNNER_MODEL,
|
|
155
171
|
DEFAULT_CLAUDE_RUNNER_BIN,
|
|
156
172
|
RUNNER_PROFILES,
|
|
173
|
+
RUNNER_PROFILE_DEFS,
|
|
174
|
+
RUNNER_PROFILE_ALIASES,
|
|
175
|
+
RUNNER_PROFILE_NAMES,
|
|
157
176
|
resolveRunnerProfileName,
|
|
158
177
|
resolveRunnerProfile,
|
|
159
178
|
resolveRunnerModel: resolveClaudeRunnerModel,
|
package/lib/task-db.js
CHANGED
|
@@ -1198,7 +1198,7 @@ function chatTask(db, { id, actor, content, goal, summary }) {
|
|
|
1198
1198
|
};
|
|
1199
1199
|
}
|
|
1200
1200
|
|
|
1201
|
-
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [] }) {
|
|
1201
|
+
function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, careerXpEligible = false, clearedFields = [], landing = {} }) {
|
|
1202
1202
|
if (!id) throw new Error('id required');
|
|
1203
1203
|
const row = getTask(db, id);
|
|
1204
1204
|
if (!row) return { reviewed: false, reason: 'not_found' };
|
|
@@ -1213,6 +1213,15 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1213
1213
|
const clearedReviewFields = Array.isArray(clearedFields)
|
|
1214
1214
|
? Array.from(new Set(clearedFields.filter(field => field === 'lesson' || field === 'next_task')))
|
|
1215
1215
|
: [];
|
|
1216
|
+
const landingInput = landing && typeof landing === 'object' ? landing : {};
|
|
1217
|
+
let landingUpdated = false;
|
|
1218
|
+
for (const key of ['happened', 'checked', 'tested', 'decision']) {
|
|
1219
|
+
const cleaned = cleanLandingText(landingInput[key]);
|
|
1220
|
+
if (cleaned) {
|
|
1221
|
+
metadata[`landing_${key}`] = cleaned;
|
|
1222
|
+
landingUpdated = true;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1216
1225
|
const reviewingPendingProof = row.status === 'review'
|
|
1217
1226
|
&& metadata.approval_status === 'pending'
|
|
1218
1227
|
&& numericReward <= 0
|
|
@@ -1250,7 +1259,7 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1250
1259
|
metadata.accepted_at = new Date().toISOString();
|
|
1251
1260
|
metadata.accepted_by = reviewer;
|
|
1252
1261
|
}
|
|
1253
|
-
if (reviewingPendingProof || updatingPendingReviewProof || (numericReward > 0 && row.status === 'done')) {
|
|
1262
|
+
if (reviewingPendingProof || updatingPendingReviewProof || (numericReward > 0 && row.status === 'done') || landingUpdated) {
|
|
1254
1263
|
withBusyRetry(() => db.prepare(`
|
|
1255
1264
|
UPDATE tasks
|
|
1256
1265
|
SET metadata = ?,
|
|
@@ -1271,6 +1280,14 @@ function reviewTask(db, { id, actor, reward, lesson, nextTask, proof, verify, ca
|
|
|
1271
1280
|
payload.agent_certified = metadata.agent_certified === true;
|
|
1272
1281
|
payload.agent_certification_policy = metadata.agent_certification_policy || null;
|
|
1273
1282
|
}
|
|
1283
|
+
if (landingUpdated) {
|
|
1284
|
+
payload.landing = {
|
|
1285
|
+
happened: metadata.landing_happened || null,
|
|
1286
|
+
checked: metadata.landing_checked || null,
|
|
1287
|
+
tested: metadata.landing_tested || null,
|
|
1288
|
+
decision: metadata.landing_decision || null,
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1274
1291
|
if (clearedReviewFields.length) payload.cleared_review_fields = clearedReviewFields;
|
|
1275
1292
|
const event = appendTaskEvent(db, {
|
|
1276
1293
|
taskId: id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "atris",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.32.0",
|
|
4
4
|
"main": "bin/atris.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"atris": "bin/atris.js",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"test": "node --test",
|
|
56
56
|
"lint:decks": "node scripts/lint-all-decks.js",
|
|
57
57
|
"publish:release": "node scripts/publish-atris-release.js",
|
|
58
|
-
"prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && chmod +x .git/hooks/pre-commit || true"
|
|
58
|
+
"prepare": "cp scripts/pre-commit .git/hooks/pre-commit 2>/dev/null && cp scripts/commit-msg .git/hooks/commit-msg 2>/dev/null && cp scripts/prepare-commit-msg .git/hooks/prepare-commit-msg 2>/dev/null && chmod +x .git/hooks/pre-commit .git/hooks/commit-msg .git/hooks/prepare-commit-msg || true"
|
|
59
59
|
},
|
|
60
60
|
"keywords": [
|
|
61
61
|
"atrisdev",
|