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/lib/autoland.js
CHANGED
|
@@ -9,11 +9,43 @@ const fs = require('fs');
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const { spawnSync } = require('child_process');
|
|
12
|
+
const { computeTrustTier } = require('./trust-tiers');
|
|
12
13
|
|
|
13
14
|
const DEFAULT_DIGEST_HOUR = 9;
|
|
14
15
|
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|
|
|
16
|
-
const AGENT_JARGON =
|
|
16
|
+
const OPERATOR_WHY = /\b(sav(?:e|es|ing)|burn(?:s|ing)?|wast(?:e|es|ing)|cost(?:s|ing)?|prevent\w*|reduc\w*|risk|stop(?:s|ping)?|break(?:s|ing)?|fail(?:s|ing)?|slow(?:s|er)?|faster|cheaper|easier|clearer|safer|simpler|trust|revenue|instead|rather than|so(?: that)?|because)\b/i;
|
|
17
|
+
const AGENT_JARGON = /\b[a-z0-9]+(?:_[a-z0-9]+)+\b|\b[a-z]+(?:[A-Z][A-Za-z0-9]*)+\b|--[a-z][a-z-]*\b|\bCLI-\d+\b/;
|
|
18
|
+
const RESULT_ULID = /\b[0-9A-HJKMNP-TV-Z]{20,}\b/;
|
|
19
|
+
const RESULT_TICKET = /\b[A-Z]{2,4}-\d+\b/;
|
|
20
|
+
const RESULT_FILE_PATH = /\b(?:[\w.-]+\/)*[\w.-]+\.(?:js|md|py|json|ts)(?::\d+)?\b/i;
|
|
21
|
+
const RESULT_COMMAND = /--|->|\b(?:npm|node|grep|git)\s+/i;
|
|
22
|
+
const SHELL_COMMAND_FRAGMENT = /\b(?:npm|node|git|grep)\s+[^\s,;]+|->/i;
|
|
23
|
+
const TRAILING_FRAGMENT_WORDS = new Set([
|
|
24
|
+
'after',
|
|
25
|
+
'and',
|
|
26
|
+
'as',
|
|
27
|
+
'at',
|
|
28
|
+
'because',
|
|
29
|
+
'before',
|
|
30
|
+
'by',
|
|
31
|
+
'for',
|
|
32
|
+
'from',
|
|
33
|
+
'in',
|
|
34
|
+
'into',
|
|
35
|
+
'of',
|
|
36
|
+
'on',
|
|
37
|
+
'or',
|
|
38
|
+
'over',
|
|
39
|
+
'so',
|
|
40
|
+
'than',
|
|
41
|
+
'that',
|
|
42
|
+
'the',
|
|
43
|
+
'to',
|
|
44
|
+
'under',
|
|
45
|
+
'with',
|
|
46
|
+
'without',
|
|
47
|
+
]);
|
|
48
|
+
const COMPLETE_TRAILING_PHRASE = /\b(?:enough|what|which)\s+to\s+(?:act|focus|work)\s+on[.!?]$/i;
|
|
17
49
|
|
|
18
50
|
function policyPath(root) {
|
|
19
51
|
return path.join(root, '.atris', 'policy', 'autoland.json');
|
|
@@ -47,7 +79,13 @@ function inferOwner(root) {
|
|
|
47
79
|
}
|
|
48
80
|
|
|
49
81
|
function defaultPolicy(root) {
|
|
50
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
enabled: true,
|
|
84
|
+
enabled_by: inferOwner(root),
|
|
85
|
+
enabled_at: null,
|
|
86
|
+
daily_experiment: true,
|
|
87
|
+
default: true,
|
|
88
|
+
};
|
|
51
89
|
}
|
|
52
90
|
|
|
53
91
|
function readPolicy(root) {
|
|
@@ -148,8 +186,10 @@ function waitingOnHuman(tasks, now = Date.now()) {
|
|
|
148
186
|
.map((t) => ({
|
|
149
187
|
ref: t.display_id || t.legacy_ref || t.id,
|
|
150
188
|
title: String(t.title || '').slice(0, 200),
|
|
189
|
+
result: String(t.result || t.metadata?.result || '').slice(0, 220),
|
|
151
190
|
tag: String(t.tag || ''),
|
|
152
191
|
hours: waitingHours(t, now),
|
|
192
|
+
member: String(t.claimed_by || t.assigned_to || '').trim(),
|
|
153
193
|
}))
|
|
154
194
|
.sort((a, b) => b.hours - a.hours);
|
|
155
195
|
}
|
|
@@ -163,8 +203,9 @@ function acceptedInLastDay(tasks, now = Date.now()) {
|
|
|
163
203
|
const entry = {
|
|
164
204
|
ref: t.display_id || t.legacy_ref || t.id,
|
|
165
205
|
title: String(t.title || '').slice(0, 120),
|
|
206
|
+
result: String(t.result || t.metadata?.result || '').slice(0, 220),
|
|
166
207
|
happened: String(t.review?.landing?.happened || t.metadata?.landing_happened || '').slice(0, 300),
|
|
167
|
-
member: String(t.claimed_by || t.assigned_to || '').trim(),
|
|
208
|
+
member: String(t.claimed_by || t.metadata?.executed_by || t.assigned_to || '').trim(),
|
|
168
209
|
};
|
|
169
210
|
if (t.metadata?.auto_accepted_at) auto.push(entry);
|
|
170
211
|
else human.push(entry);
|
|
@@ -176,6 +217,83 @@ function plur(n, word) {
|
|
|
176
217
|
return `${n} ${word}${n === 1 ? '' : 's'}`;
|
|
177
218
|
}
|
|
178
219
|
|
|
220
|
+
function pauseReasonDigestLabel(reason) {
|
|
221
|
+
if (reason === 'auth-required') return 'awaiting login';
|
|
222
|
+
if (reason === 'model-unavailable') return 'awaiting an available model';
|
|
223
|
+
if (reason === 'rate-limit-exceeded-wall') return 'waiting for the rate-limit reset';
|
|
224
|
+
if (reason === 'stale' || reason === 'paused-idle') return 'stale after the idle window';
|
|
225
|
+
return 'paused for operator action';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function normalizeReasonTally(value) {
|
|
229
|
+
const tally = {};
|
|
230
|
+
if (!value || typeof value !== 'object') return tally;
|
|
231
|
+
for (const [reason, count] of Object.entries(value)) {
|
|
232
|
+
const cleanReason = String(reason || '').trim();
|
|
233
|
+
const n = Number(count) || 0;
|
|
234
|
+
if (cleanReason && n > 0) tally[cleanReason] = (tally[cleanReason] || 0) + n;
|
|
235
|
+
}
|
|
236
|
+
return tally;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function heldReasonTally(janitor) {
|
|
240
|
+
const holds = janitor?.mission_holds && typeof janitor.mission_holds === 'object'
|
|
241
|
+
? Object.values(janitor.mission_holds)
|
|
242
|
+
: [];
|
|
243
|
+
const tally = {};
|
|
244
|
+
for (const hold of holds) {
|
|
245
|
+
const reason = String(hold?.reason || '').trim();
|
|
246
|
+
if (reason) tally[reason] = (tally[reason] || 0) + 1;
|
|
247
|
+
}
|
|
248
|
+
if (holds.length > 0) return tally;
|
|
249
|
+
return normalizeReasonTally(janitor?.missions_held_reasons);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function pushJanitorMissionReasonLines(lines, janitor) {
|
|
253
|
+
const held = heldReasonTally(janitor);
|
|
254
|
+
for (const [reason, count] of Object.entries(held).sort()) {
|
|
255
|
+
lines.push(`held: ${plur(count, 'mission')} ${pauseReasonDigestLabel(reason)} (${reason})`);
|
|
256
|
+
}
|
|
257
|
+
const stopped = normalizeReasonTally(janitor?.mission_stop_reasons || janitor?.missions_stopped_reasons);
|
|
258
|
+
for (const [reason, count] of Object.entries(stopped).sort()) {
|
|
259
|
+
lines.push(`stopped: ${plur(count, 'mission')} ${pauseReasonDigestLabel(reason)} (${reason})`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function pushLandingSweepLines(lines, landingSweep) {
|
|
264
|
+
if (!landingSweep || typeof landingSweep !== 'object') return;
|
|
265
|
+
const stale = Array.isArray(landingSweep.stale) ? landingSweep.stale : [];
|
|
266
|
+
const staleCount = Number(landingSweep.stale_count) || stale.length;
|
|
267
|
+
if (staleCount > 0) {
|
|
268
|
+
const oldest = stale[0];
|
|
269
|
+
const oldestText = oldest
|
|
270
|
+
? `; oldest: ${oldest.name} (${Number(oldest.activityHours || oldest.ageHours || 0)}h)`
|
|
271
|
+
: '';
|
|
272
|
+
lines.push(`stuck in the air past 48h: ${plur(staleCount, 'piece')} need${staleCount === 1 ? 's' : ''} a human${oldestText}; inspect: atris land`);
|
|
273
|
+
}
|
|
274
|
+
const human = Array.isArray(landingSweep.human) ? landingSweep.human : [];
|
|
275
|
+
const humanCount = Number(landingSweep.human_count) || human.length;
|
|
276
|
+
if (humanCount > 0) {
|
|
277
|
+
const first = human[0] ? ` ${finishThought(human[0], 130)}` : '';
|
|
278
|
+
const more = humanCount > 1 ? `; ${humanCount - 1} more` : '';
|
|
279
|
+
lines.push(`cleanup needs a human:${first}${more}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function pushWaitingWishLines(lines, waitingWishes) {
|
|
284
|
+
const wishes = (Array.isArray(waitingWishes) ? waitingWishes : [])
|
|
285
|
+
.filter((wish) => wish && String(wish.text || '').trim());
|
|
286
|
+
if (!wishes.length) return;
|
|
287
|
+
lines.push(`${plur(wishes.length, 'wish')} waiting on you:`);
|
|
288
|
+
for (const wish of wishes.slice(0, 2)) {
|
|
289
|
+
const text = finishThought(String(wish.text || '').replace(/[.\s]+$/, ''), 90);
|
|
290
|
+
const need = cutAtWord(String(wish.need || 'answer the open question before it can start'), 120);
|
|
291
|
+
lines.push(`- ${text}: ${need}`);
|
|
292
|
+
}
|
|
293
|
+
const hidden = Math.max(0, wishes.length - 2);
|
|
294
|
+
if (hidden > 0) lines.push(`- ${plur(hidden, 'more wish')} waiting on an answer`);
|
|
295
|
+
}
|
|
296
|
+
|
|
179
297
|
function cutAtWord(text, max) {
|
|
180
298
|
const clean = String(text || '').replace(/\s+/g, ' ').trim();
|
|
181
299
|
if (clean.length <= max) return clean;
|
|
@@ -184,19 +302,63 @@ function cutAtWord(text, max) {
|
|
|
184
302
|
return `${cut.slice(0, lastSpace > max * 0.6 ? lastSpace : max)}...`;
|
|
185
303
|
}
|
|
186
304
|
|
|
305
|
+
function stripDanglingFragment(text) {
|
|
306
|
+
let out = String(text || '').replace(/\s+/g, ' ').trim();
|
|
307
|
+
while (out.includes('(') && out.lastIndexOf('(') > out.lastIndexOf(')')) {
|
|
308
|
+
out = out.slice(0, out.lastIndexOf('(')).trim();
|
|
309
|
+
}
|
|
310
|
+
out = out.replace(/[,\s;:]+$/g, '').trim();
|
|
311
|
+
while (out) {
|
|
312
|
+
const withoutPunctuation = out.replace(/[.!?]+$/g, '').trim();
|
|
313
|
+
const word = (withoutPunctuation.match(/[A-Za-z]+$/) || [''])[0].toLowerCase();
|
|
314
|
+
if (!TRAILING_FRAGMENT_WORDS.has(word)) break;
|
|
315
|
+
out = withoutPunctuation.slice(0, Math.max(0, withoutPunctuation.length - word.length)).replace(/[,\s;:]+$/g, '').trim();
|
|
316
|
+
}
|
|
317
|
+
return out;
|
|
318
|
+
}
|
|
319
|
+
|
|
187
320
|
// A line ends where a thought ends. Long lines close at their last complete
|
|
188
321
|
// clause inside the budget and read whole: no ellipsis, nothing dangling.
|
|
189
322
|
// Only when a line has no clause boundary at all does a word cut remain.
|
|
190
323
|
function finishThought(text, max = 160) {
|
|
191
|
-
const clean = String(text || '').replace(/\s+/g, ' ').trim().replace(/[,;:\s]+$/, '');
|
|
324
|
+
const clean = stripDanglingFragment(String(text || '').replace(/\s+/g, ' ').trim().replace(/[,;:\s]+$/, ''));
|
|
192
325
|
if (clean.length <= max) return clean;
|
|
193
326
|
const cut = clean.slice(0, max);
|
|
194
327
|
const sentence = Math.max(cut.lastIndexOf('. '), cut.lastIndexOf('! '), cut.lastIndexOf('? '));
|
|
195
|
-
if (sentence > max * 0.3) return cut.slice(0, sentence + 1);
|
|
328
|
+
if (sentence > max * 0.3) return stripDanglingFragment(cut.slice(0, sentence + 1));
|
|
196
329
|
const clause = Math.max(cut.lastIndexOf(', '), cut.lastIndexOf('; '), cut.lastIndexOf(' ('), cut.lastIndexOf(': '));
|
|
197
|
-
if (clause > max * 0.45) return cut.slice(0, clause);
|
|
330
|
+
if (clause > max * 0.45) return stripDanglingFragment(cut.slice(0, clause));
|
|
198
331
|
const space = cut.lastIndexOf(' ');
|
|
199
|
-
return
|
|
332
|
+
return stripDanglingFragment(cut.slice(0, space > max * 0.6 ? space : max));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function digestMoveLine(move) {
|
|
336
|
+
if (move.kind === 'mission_ready') {
|
|
337
|
+
const title = String(move.title || '').trim();
|
|
338
|
+
const boundary = title.search(/[:,]\s+/);
|
|
339
|
+
const clause = (boundary >= 0 ? title.slice(0, boundary) : title).trim();
|
|
340
|
+
const clauseWords = clause.split(/\s+/).filter(Boolean).length;
|
|
341
|
+
const afterColon = boundary >= 0 && title[boundary] === ':'
|
|
342
|
+
? title.slice(boundary + 1).replace(/\([^)]*\)/g, '').trim()
|
|
343
|
+
: '';
|
|
344
|
+
const candidate = afterColon && clauseWords <= 5 ? finishThought(afterColon, 80) : clause;
|
|
345
|
+
const completeClause = candidate && candidate.length <= 80
|
|
346
|
+
? candidate.charAt(0).toLowerCase() + candidate.slice(1).replace(/[.!?]+$/g, '')
|
|
347
|
+
: '';
|
|
348
|
+
const label = finishThought(completeClause || String(move.label || '').replace(/^#\d+\s+/, ''), 80);
|
|
349
|
+
return label
|
|
350
|
+
? `review proof${move.owner ? ` from ${move.owner}` : ''} for ${label} so finished work can leave the queue`
|
|
351
|
+
: 'review the finished mission proof so accepted work can leave the queue';
|
|
352
|
+
}
|
|
353
|
+
return `${finishThought(move.title, 140)}${move.owner ? ` (best fit: ${move.owner})` : ''}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function historicalLandingText(text, max = 160) {
|
|
357
|
+
const clean = String(text || '')
|
|
358
|
+
.replace(/\s+/g, ' ')
|
|
359
|
+
.replace(/\bas exists and not empty\b/gi, 'as a saved file that exists and is not empty')
|
|
360
|
+
.trim();
|
|
361
|
+
return finishThought(clean, max);
|
|
200
362
|
}
|
|
201
363
|
|
|
202
364
|
function operatorReady(text) {
|
|
@@ -208,28 +370,77 @@ function hasAgentJargon(text) {
|
|
|
208
370
|
return AGENT_JARGON.test(String(text || ''));
|
|
209
371
|
}
|
|
210
372
|
|
|
373
|
+
function explainResult(text) {
|
|
374
|
+
const raw = String(text || '');
|
|
375
|
+
const value = raw.replace(/\s+/g, ' ').trim();
|
|
376
|
+
if (!value) return { ok: false, reason: 'result must say what someone can do now and why it matters.' };
|
|
377
|
+
if (raw.includes('\n') || raw.includes('\r')) return { ok: false, reason: 'result must be one sentence, not a paragraph.' };
|
|
378
|
+
if (value.length < 30) return { ok: false, reason: 'result is too short to explain what changed for a human.' };
|
|
379
|
+
if (value.length > 180) return { ok: false, reason: 'result must stay under 180 characters so it survives briefs.' };
|
|
380
|
+
if (!/[.!?]$/.test(value)) return { ok: false, reason: 'result must end as a complete sentence with a period, question mark, or exclamation point.' };
|
|
381
|
+
const sentenceStops = value.match(/[.!?]/g) || [];
|
|
382
|
+
if (sentenceStops.length !== 1) return { ok: false, reason: 'result must be exactly one complete sentence.' };
|
|
383
|
+
if (RESULT_ULID.test(value)) return { ok: false, reason: 'result should explain the gain, not include database ids.' };
|
|
384
|
+
if (RESULT_TICKET.test(value)) return { ok: false, reason: 'result should not include ticket ids.' };
|
|
385
|
+
if (RESULT_FILE_PATH.test(value)) return { ok: false, reason: 'result should not include file paths.' };
|
|
386
|
+
if (RESULT_COMMAND.test(value)) return { ok: false, reason: 'result should not include command syntax or flags.' };
|
|
387
|
+
const finalWord = (value.replace(/[.!?]+$/g, '').match(/[A-Za-z]+$/) || [''])[0].toLowerCase();
|
|
388
|
+
if (TRAILING_FRAGMENT_WORDS.has(finalWord) && !COMPLETE_TRAILING_PHRASE.test(value)) {
|
|
389
|
+
return { ok: false, reason: 'result ends mid-thought; finish the sentence with the actual human gain.' };
|
|
390
|
+
}
|
|
391
|
+
if (!operatorReady(value)) {
|
|
392
|
+
return { ok: false, reason: 'result needs a plain operator why: say who can do what now, and why it saves time, reduces risk, increases trust, or helps users.' };
|
|
393
|
+
}
|
|
394
|
+
return { ok: true, reason: null };
|
|
395
|
+
}
|
|
396
|
+
|
|
211
397
|
// One line per piece, in the work's own words: the landing sentence beats
|
|
212
398
|
// the task title, because titles are written for the queue, not for a text.
|
|
213
399
|
// The cut is generous on purpose: a specific sentence the reader trusts
|
|
214
400
|
// (what happened + how big + how we know) beats a short one they must chase.
|
|
215
401
|
function digestLine(item) {
|
|
402
|
+
const result = String(item.result || '').replace(/^completed:\s*/i, '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
216
403
|
const happened = String(item.happened || '').replace(/^completed:\s*/i, '').split(/(?<=[.!?])\s+/)[0] || '';
|
|
217
|
-
const title = String(item.title || '').replace(/^Mission XP:\s*/i, '');
|
|
218
|
-
const source = happened.length >= 12 ? happened : title;
|
|
404
|
+
const title = clarify(String(item.title || '').replace(/^Mission XP:\s*/i, ''));
|
|
405
|
+
const source = result.length >= 12 ? result : (happened.length >= 12 && operatorReady(happened) ? happened : (operatorReady(title) ? title : ''));
|
|
406
|
+
if (!source) return '';
|
|
219
407
|
const line = finishThought(source.replace(/[.\s]+$/, ''), 160);
|
|
408
|
+
if (!operatorReady(line) && source === result && happened.length >= 12 && operatorReady(happened)) {
|
|
409
|
+
const fallback = finishThought(happened.replace(/[.\s]+$/, ''), 160);
|
|
410
|
+
if (operatorReady(fallback)) return fallback.charAt(0).toLowerCase() + fallback.slice(1);
|
|
411
|
+
}
|
|
220
412
|
return line.charAt(0).toLowerCase() + line.slice(1);
|
|
221
413
|
}
|
|
222
414
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
415
|
+
function unexplainedSummaryRows(items, label) {
|
|
416
|
+
const count = (items || []).length;
|
|
417
|
+
if (count === 0) return [];
|
|
418
|
+
if (label === 'landed') {
|
|
419
|
+
return [`${plur(count, 'landed result')} stayed hidden because ${count === 1 ? 'its outcome was' : 'their outcomes were'} unclear`];
|
|
420
|
+
}
|
|
421
|
+
if (label === 'in review') {
|
|
422
|
+
return [`${plur(count, 'review')} stayed hidden because ${count === 1 ? 'its next action was' : 'their next actions were'} unclear`];
|
|
423
|
+
}
|
|
424
|
+
return [`${plur(count, label)} stayed hidden because ${count === 1 ? 'its meaning was' : 'their meanings were'} unclear`];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function missionDigestLine(mission, max = 160) {
|
|
428
|
+
const visibleGoal = mission?.visible_goal?.desired_objective
|
|
429
|
+
|| mission?.goal?.visible_goal?.desired_objective
|
|
430
|
+
|| (typeof mission?.visible_goal === 'string' ? mission.visible_goal : '');
|
|
431
|
+
const objective = visibleGoal || mission?.objective || '';
|
|
432
|
+
return finishThought(clarify(objective, max), max);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Some older surfaces still need a readable fallback for rough text, but the
|
|
436
|
+
// digest paths above now hide agent-speak until a result sentence exists.
|
|
437
|
+
function dejargon(line, options = {}) {
|
|
438
|
+
let value = String(line || '')
|
|
439
|
+
.replace(/--([a-z][a-z-]*)/g, (_match, flag) => flag.replace(/-/g, ' '))
|
|
440
|
+
.replace(/\b[a-z0-9]+(?:_[a-z0-9]+)+\b/gi, match => match.replace(/_/g, ' '))
|
|
441
|
+
.replace(/\b[a-z]+(?:[A-Z][A-Za-z0-9]*)+\b/g, match => match.replace(/([a-z0-9])([A-Z])/g, '$1 $2'));
|
|
442
|
+
if (options.preserveTickets !== true) value = value.replace(/\bCLI-\d+\b/g, '');
|
|
443
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
233
444
|
}
|
|
234
445
|
|
|
235
446
|
// The clarity gate in one call: strip what an operator can't act on (flag
|
|
@@ -248,16 +459,33 @@ function isCleanStop(item) {
|
|
|
248
459
|
|| /^(Mission XP:\s*)?Decide and start the next useful mission/i.test(String(item.title || ''));
|
|
249
460
|
}
|
|
250
461
|
|
|
462
|
+
function hiddenMoveText(count, { more = false } = {}) {
|
|
463
|
+
const label = plur(count, `${more ? 'more queued ' : 'queued '}move`);
|
|
464
|
+
return count === 1
|
|
465
|
+
? `${label} stays hidden until its owner writes a clear next action`
|
|
466
|
+
: `${label} stay hidden until their owners write clear next actions`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function memberTrustLabel(member, root) {
|
|
470
|
+
if (!member) return '';
|
|
471
|
+
const tier = computeTrustTier(member, root);
|
|
472
|
+
if (tier === 'trusted') return ` (${member})`;
|
|
473
|
+
if (tier === 'standard') return ` (${member}, newer, spot-checked)`;
|
|
474
|
+
return ` (${member}, needs review)`;
|
|
475
|
+
}
|
|
476
|
+
|
|
251
477
|
// Short enough for one iMessage, plain enough to read walking to the car.
|
|
252
478
|
// Every line answers: what happened, how big, how we know. It names the work
|
|
253
479
|
// because "7 things landed" tells the score, not the story — and it always
|
|
254
480
|
// ends with what's waiting and what to do next, because those are the only
|
|
255
481
|
// lines that ask anything of the reader.
|
|
256
|
-
function composeDigest({ accepted, waiting, landed, project, nextMoves = [], acceptAll = false, reapError = null, janitor = null }) {
|
|
482
|
+
function composeDigest({ accepted, waiting, protectedWaiting = [], waitingWishes = [], landed, project, nextMoves = [], acceptAll = false, reapError = null, janitor = null, landingSweep = null, fullStory = false, root = process.cwd() }) {
|
|
257
483
|
const lines = [];
|
|
258
|
-
lines.push(`atris ${project}:
|
|
484
|
+
lines.push(`atris ${project}: last 24 hours`);
|
|
259
485
|
const autoCount = accepted.auto.length;
|
|
260
486
|
const humanCount = accepted.human.length;
|
|
487
|
+
const wishWaits = (Array.isArray(waitingWishes) ? waitingWishes : [])
|
|
488
|
+
.filter((wish) => wish && String(wish.text || '').trim());
|
|
261
489
|
if (autoCount > 0) {
|
|
262
490
|
// Three results, air between them, the rest on ask: the whole message
|
|
263
491
|
// fits a laptop screen with no scrolling. Reading it is one glance.
|
|
@@ -265,46 +493,66 @@ function composeDigest({ accepted, waiting, landed, project, nextMoves = [], acc
|
|
|
265
493
|
const work = accepted.auto
|
|
266
494
|
.filter((item) => !isCleanStop(item))
|
|
267
495
|
.map((item) => ({ item, line: digestLine(item) }));
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
.map(({ item, line }) => ({ item, line: dejargon(line) }));
|
|
273
|
-
const visibleWork = [...ready, ...rough].slice(0, 3);
|
|
274
|
-
const held = Math.max(0, work.length - visibleWork.length) + (stops.length > 0 ? 1 : 0);
|
|
496
|
+
const ready = work.filter(({ line }) => line && operatorReady(line));
|
|
497
|
+
const unexplainedWork = work.filter(({ line }) => !line || !operatorReady(line));
|
|
498
|
+
const visibleWork = ready.slice(0, 3);
|
|
499
|
+
const held = Math.max(0, ready.length - visibleWork.length) + (stops.length > 0 ? 1 : 0);
|
|
275
500
|
if (visibleWork.length > 0) {
|
|
276
501
|
// The header states the actual bar: under accept-all the claim
|
|
277
502
|
// "verified twice" would be a lie the operator builds trust on.
|
|
278
503
|
lines.push(acceptAll
|
|
279
|
-
?
|
|
280
|
-
:
|
|
504
|
+
? `${autoCount} landed; ${plur(ready.length, 'clear result')}, protected lanes held back for you:`
|
|
505
|
+
: `${autoCount} landed; ${plur(ready.length, 'clear result')}, each verified twice with proof on file:`);
|
|
281
506
|
for (const { item, line } of visibleWork) {
|
|
282
507
|
lines.push('');
|
|
283
|
-
lines.push(`- ${line || item.ref}${item.member
|
|
508
|
+
lines.push(`- ${line || item.ref}${memberTrustLabel(item.member, root)}`);
|
|
284
509
|
}
|
|
285
510
|
lines.push('');
|
|
286
|
-
if (held > 0)
|
|
511
|
+
if (held > 0) {
|
|
512
|
+
lines.push(fullStory
|
|
513
|
+
? `${plur(held, 'more result')} in the full story below`
|
|
514
|
+
: `${plur(held, 'more result')} when you want them: atris autoland digest`);
|
|
515
|
+
}
|
|
516
|
+
} else {
|
|
517
|
+
lines.push(`${autoCount} landed with proof on file; no clear result was ready to show:`);
|
|
287
518
|
}
|
|
519
|
+
for (const row of unexplainedSummaryRows(unexplainedWork, 'landed')) lines.push(row);
|
|
288
520
|
}
|
|
289
521
|
if (autoCount + humanCount === 0) lines.push('nothing finished in the last day');
|
|
290
522
|
if (waiting.length > 0) {
|
|
291
523
|
// The only lines that ask the reader to act, so they name what's asking.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
524
|
+
const waitingWork = waiting.map((item) => ({ item, line: digestLine(item) }));
|
|
525
|
+
const explainedWaiting = waitingWork.filter(({ line }) => line && operatorReady(line));
|
|
526
|
+
const unexplainedWaiting = waitingWork.filter(({ line }) => !line || !operatorReady(line));
|
|
527
|
+
lines.push(`task approvals waiting on you (${explainedWaiting.length} clear next actions/${waiting.length} total; approve or bounce: atris task reviews):`);
|
|
528
|
+
for (const { item, line } of explainedWaiting.slice(0, 2)) {
|
|
529
|
+
lines.push(`- ${finishThought(line, 140)}${item.hours >= 12 ? ` (${item.hours}h)` : ''}`);
|
|
295
530
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
lines.push(
|
|
531
|
+
for (const row of unexplainedSummaryRows(unexplainedWaiting, 'in review')) lines.push(`- ${row}`);
|
|
532
|
+
const hiddenWaiting = Math.max(0, explainedWaiting.length - 2);
|
|
533
|
+
if (hiddenWaiting > 0) lines.push(`- ${hiddenWaiting} more clear review actions`);
|
|
534
|
+
}
|
|
535
|
+
const protectedCount = Array.isArray(protectedWaiting) ? protectedWaiting.length : 0;
|
|
536
|
+
if (protectedCount > 0) {
|
|
537
|
+
lines.push(`${plur(protectedCount, 'protected review')} waiting on you; inspect: atris task reviews`);
|
|
538
|
+
} else if (waiting.length === 0 && wishWaits.length === 0) {
|
|
539
|
+
lines.push('task approvals waiting on you: nothing');
|
|
299
540
|
}
|
|
541
|
+
pushWaitingWishLines(lines, wishWaits);
|
|
300
542
|
if (landed && landed.branches > 0) {
|
|
301
|
-
|
|
543
|
+
const staleCount = Number(landingSweep?.stale_count) || (Array.isArray(landingSweep?.stale) ? landingSweep.stale.length : 0);
|
|
544
|
+
const freshness = landed.due > 0
|
|
545
|
+
? `, ${landed.due} overdue`
|
|
546
|
+
: (staleCount > 0 ? `, ${staleCount} stale` : ', all fresh');
|
|
547
|
+
lines.push(`in the air: ${plur(landed.branches, 'piece')}${freshness} (atris land)`);
|
|
302
548
|
}
|
|
303
|
-
if (reapError) lines.push(`cleanup trouble: the
|
|
549
|
+
if (reapError) lines.push(`cleanup trouble: the landing sweep failed (${reapError}) - run: atris land --reap`);
|
|
550
|
+
pushLandingSweepLines(lines, landingSweep);
|
|
304
551
|
// The janitor's day in one line: stale paused missions stopped and merged
|
|
305
552
|
// worktrees cleared since the last digest. Silence when it did nothing.
|
|
306
553
|
const missionsStopped = Number(janitor?.missions_stopped) || 0;
|
|
307
554
|
const worktreesReaped = Number(janitor?.worktrees_reaped) || 0;
|
|
555
|
+
if (janitor) pushJanitorMissionReasonLines(lines, janitor);
|
|
308
556
|
if (missionsStopped > 0 || worktreesReaped > 0) {
|
|
309
557
|
lines.push(`tidied up: ${plur(missionsStopped, 'stale mission')} stopped, ${plur(worktreesReaped, 'merged worktree')} cleared`);
|
|
310
558
|
}
|
|
@@ -320,13 +568,13 @@ function composeDigest({ accepted, waiting, landed, project, nextMoves = [], acc
|
|
|
320
568
|
if (moves.length > 0) {
|
|
321
569
|
lines.push('next, if you agree:');
|
|
322
570
|
for (const move of moves) {
|
|
323
|
-
lines.push(`- ${
|
|
571
|
+
lines.push(`- ${digestMoveLine(move)}`);
|
|
324
572
|
}
|
|
325
|
-
if (unexplained > 0) lines.push(`- ${
|
|
573
|
+
if (unexplained > 0) lines.push(`- ${hiddenMoveText(unexplained, { more: true })}`);
|
|
326
574
|
} else if (unexplained > 0) {
|
|
327
|
-
lines.push(
|
|
575
|
+
lines.push(hiddenMoveText(unexplained));
|
|
328
576
|
}
|
|
329
|
-
if (!lines.some((line) => line.includes('atris autoland digest'))) {
|
|
577
|
+
if (!fullStory && !lines.some((line) => line.includes('atris autoland digest'))) {
|
|
330
578
|
lines.push('the full story: atris autoland digest');
|
|
331
579
|
}
|
|
332
580
|
return lines.join('\n');
|
|
@@ -335,7 +583,7 @@ function composeDigest({ accepted, waiting, landed, project, nextMoves = [], acc
|
|
|
335
583
|
// The moment something lands, the operator hears it in one glance: the
|
|
336
584
|
// landing sentence each piece wrote for a day-one PM at finish time, its
|
|
337
585
|
// author, nothing else. Rough sentences get de-jargoned, never hidden.
|
|
338
|
-
function composeLiveUpdate({ landedRefs, tasks, project }) {
|
|
586
|
+
function composeLiveUpdate({ landedRefs, tasks, project, root = process.cwd() }) {
|
|
339
587
|
const byRef = new Map((tasks || []).map((t) => [String(t.display_id || t.legacy_ref || t.id), t]));
|
|
340
588
|
const items = (landedRefs || []).map((ref) => {
|
|
341
589
|
const t = byRef.get(String(ref));
|
|
@@ -343,21 +591,27 @@ function composeLiveUpdate({ landedRefs, tasks, project }) {
|
|
|
343
591
|
return {
|
|
344
592
|
ref: String(ref),
|
|
345
593
|
title: String(t.title || '').slice(0, 200),
|
|
594
|
+
result: String(t.result || t.metadata?.result || '').slice(0, 220),
|
|
346
595
|
happened: String(t.review?.landing?.happened || t.metadata?.landing_happened || '').slice(0, 300),
|
|
347
|
-
member: String(t.claimed_by || t.assigned_to || '').trim(),
|
|
596
|
+
member: String(t.claimed_by || t.metadata?.executed_by || t.assigned_to || '').trim(),
|
|
348
597
|
};
|
|
349
598
|
}).filter((item) => !isCleanStop(item));
|
|
350
599
|
if (!items.length) return '';
|
|
351
600
|
const lines = [`atris ${project}: just landed`];
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
601
|
+
const work = items.map((item) => ({ item, line: digestLine(item) }));
|
|
602
|
+
const ready = work.filter(({ line }) => line && operatorReady(line));
|
|
603
|
+
const unexplained = work.filter(({ line }) => !line || !operatorReady(line));
|
|
604
|
+
for (const { item, line } of ready.slice(0, 3)) {
|
|
605
|
+
lines.push('');
|
|
606
|
+
lines.push(`- ${finishThought(line, 160)}${memberTrustLabel(item.member, root)}`);
|
|
607
|
+
}
|
|
608
|
+
for (const row of unexplainedSummaryRows(unexplained, 'landed')) {
|
|
355
609
|
lines.push('');
|
|
356
|
-
lines.push(
|
|
610
|
+
lines.push(row);
|
|
357
611
|
}
|
|
358
|
-
if (
|
|
612
|
+
if (ready.length > 3) {
|
|
359
613
|
lines.push('');
|
|
360
|
-
lines.push(`and ${plur(
|
|
614
|
+
lines.push(`and ${plur(ready.length - 3, 'more piece')}: atris autoland digest`);
|
|
361
615
|
}
|
|
362
616
|
return lines.join('\n');
|
|
363
617
|
}
|
|
@@ -366,7 +620,7 @@ function composeAlarm({ waiting, project, alarmHours }) {
|
|
|
366
620
|
const worst = waiting[0];
|
|
367
621
|
return [
|
|
368
622
|
`atris ${project}: ${plur(waiting.length, 'piece')} of finished work ${waiting.length === 1 ? 'has' : 'have'} been waiting on you for over ${alarmHours}h`,
|
|
369
|
-
`oldest: ${worst.ref} (${worst.hours}h)
|
|
623
|
+
`oldest: ${worst.ref} (${worst.hours}h), ${worst.title}`,
|
|
370
624
|
'approve or bounce: atris task reviews',
|
|
371
625
|
].join('\n');
|
|
372
626
|
}
|
|
@@ -409,11 +663,22 @@ module.exports = {
|
|
|
409
663
|
cronInstalled,
|
|
410
664
|
cronMarker,
|
|
411
665
|
dueForAlarm,
|
|
666
|
+
digestLine,
|
|
667
|
+
dejargon,
|
|
668
|
+
explainResult,
|
|
412
669
|
installCron,
|
|
413
670
|
liveAcceptAuthorization,
|
|
414
671
|
markAlerted,
|
|
672
|
+
missionDigestLine,
|
|
415
673
|
operatorReady,
|
|
416
674
|
hasAgentJargon,
|
|
675
|
+
voicePatterns: Object.freeze({
|
|
676
|
+
agentJargon: AGENT_JARGON,
|
|
677
|
+
filePath: RESULT_FILE_PATH,
|
|
678
|
+
rawUlid: RESULT_ULID,
|
|
679
|
+
shellCommand: SHELL_COMMAND_FRAGMENT,
|
|
680
|
+
}),
|
|
681
|
+
historicalLandingText,
|
|
417
682
|
policyPath,
|
|
418
683
|
readPolicy,
|
|
419
684
|
readState,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SHORT_LOOKUP_MAX_CHARS = 140;
|
|
4
|
+
const LONG_INPUT_MIN_CHARS = 1200;
|
|
5
|
+
const TINY_MESSAGE_MAX_CHARS = 20;
|
|
6
|
+
const GREETING_MAX_CHARS = 40;
|
|
7
|
+
const DECISION_FACTOR_MIN_COMMAS = 3;
|
|
8
|
+
const TRADEOFF_DEPTH_MIN_CHARS = 100;
|
|
9
|
+
|
|
10
|
+
// Anchored to the start: a lookup word mid-sentence is usually a subordinate
|
|
11
|
+
// clause (my fan spins when i run tests), not a lookup.
|
|
12
|
+
const LOOKUP_START_RE = /^(?:(?:please|pls|hey|hi|yo)[,: ]+)?(?:whats|whos|wheres|what|who|when|where|list|show|define)\b/i;
|
|
13
|
+
// Words that turn a lookup-shaped message into a judgment call: never fast.
|
|
14
|
+
const JUDGMENT_WORD_RE = /\b(?:should|tradeoffs?|better|best|recommend|worth|good way)\b/i;
|
|
15
|
+
const HEAVY_REASONING_RE = /\b(?:design(?:s|ed|ing)?|architect(?:s|ed|ing)?|prov(?:e|es|ed|ing)|analy(?:ze|zes|zed|zing|se|ses|sed|sing)|compar(?:e|es|ed|ing)|plan(?:s|ned|ning)?|sketch(?:es|ed|ing)?)\b|\bwalk(?:s|ed|ing)? (?:me |us )?through\b/i;
|
|
16
|
+
const DECISION_PHRASE_RE = /\b(?:should|whether|decide|deciding)\b/i;
|
|
17
|
+
const ARGUE_BOTH_RE = /\bargue\b|\bboth sides\b|\bsteelman\b/i;
|
|
18
|
+
const CODE_EDIT_RE = /\b(?:fix(?:es|ed|ing)?|refactor(?:s|ed|ing)?|renam(?:e|es|ed|ing)|debug(?:s|ged|ging)?)\b/i;
|
|
19
|
+
const CODE_DIAGNOSIS_RE = /\bwhy\b|\bwrong\b|\bfail(?:s|ed|ing)?\b|\bresolv(?:e|es|ed|ing)?\b|\bbroken\b|\bnot work/i;
|
|
20
|
+
const CODE_CONTEXT_RE = /```|(?:^|\n)\s*(?:traceback\b|(?:[a-z]+)?(?:error|exception):|at\s+\S+\s+\()/im;
|
|
21
|
+
const ERROR_EXPLAIN_RE = /\bexplain (?:this|the) error\b|\bwhat does (?:this|the) error mean\b/i;
|
|
22
|
+
// Yes/no capability checks are lookups even without a lookup keyword.
|
|
23
|
+
const YESNO_LOOKUP_RE = /^(?:does|is|are|can|do)\b/i;
|
|
24
|
+
const YESNO_LOOKUP_MAX_CHARS = 80;
|
|
25
|
+
// Troubleshooting language means advice, not recall: keep it out of fast.
|
|
26
|
+
const TROUBLE_RE = /\bfail(?:s|ed|ing)?\b|\bcrash(?:es|ed|ing)?\b|\bbroken\b|\bnot working\b/i;
|
|
27
|
+
const WEIGHING_WORD_RE = /\b(?:consider|weigh|rank|estimate)\b/i;
|
|
28
|
+
const WEIGHING_FACTOR_MIN_COMMAS = 2;
|
|
29
|
+
const TRANSFORM_START_RE = /^(?:convert|summarize|translate)\b/i;
|
|
30
|
+
const GREETING_RE = /^(?:hey|hi|hello|yo|thanks|thank you|ok|okay|cool|nice|got it|great|perfect|yep|no worries)\b/i;
|
|
31
|
+
|
|
32
|
+
function threshold(value, fallback) {
|
|
33
|
+
return Number.isFinite(value) && value >= 0 ? value : fallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function pickLane(message, opts = {}) {
|
|
37
|
+
const text = String(message || '').trim();
|
|
38
|
+
const shortLookupMaxChars = threshold(opts.shortLookupMaxChars, SHORT_LOOKUP_MAX_CHARS);
|
|
39
|
+
const longInputMinChars = threshold(opts.longInputMinChars, LONG_INPUT_MIN_CHARS);
|
|
40
|
+
const questionCount = (text.match(/\?/g) || []).length;
|
|
41
|
+
const commaCount = (text.match(/,/g) || []).length;
|
|
42
|
+
const hasCodeContext = CODE_CONTEXT_RE.test(text);
|
|
43
|
+
|
|
44
|
+
if (text.length > longInputMinChars) {
|
|
45
|
+
return { lane: 'max', reason: 'max fits this long input.' };
|
|
46
|
+
}
|
|
47
|
+
if (questionCount > 1) {
|
|
48
|
+
return { lane: 'max', reason: 'max fits a request with multiple questions.' };
|
|
49
|
+
}
|
|
50
|
+
if (ARGUE_BOTH_RE.test(text)
|
|
51
|
+
|| (DECISION_PHRASE_RE.test(text) && commaCount >= DECISION_FACTOR_MIN_COMMAS)
|
|
52
|
+
|| (WEIGHING_WORD_RE.test(text) && commaCount >= WEIGHING_FACTOR_MIN_COMMAS)) {
|
|
53
|
+
return { lane: 'max', reason: 'max fits a decision weighing several factors.' };
|
|
54
|
+
}
|
|
55
|
+
if (HEAVY_REASONING_RE.test(text)) {
|
|
56
|
+
return { lane: 'max', reason: 'max fits this reasoning-heavy request.' };
|
|
57
|
+
}
|
|
58
|
+
if (/\btrade[- ]?offs?\b/i.test(text) && text.length > TRADEOFF_DEPTH_MIN_CHARS) {
|
|
59
|
+
return { lane: 'max', reason: 'max fits this tradeoff analysis.' };
|
|
60
|
+
}
|
|
61
|
+
if (hasCodeContext && (CODE_EDIT_RE.test(text) || CODE_DIAGNOSIS_RE.test(text))) {
|
|
62
|
+
return { lane: 'code-fast', reason: 'code-fast fits this bounded code task.' };
|
|
63
|
+
}
|
|
64
|
+
if (ERROR_EXPLAIN_RE.test(text) && !hasCodeContext) {
|
|
65
|
+
return { lane: 'fast', reason: 'fast fits a plain error explanation.' };
|
|
66
|
+
}
|
|
67
|
+
if (TRANSFORM_START_RE.test(text) && text.length <= shortLookupMaxChars) {
|
|
68
|
+
return { lane: 'fast', reason: 'fast fits this small transform.' };
|
|
69
|
+
}
|
|
70
|
+
if (GREETING_RE.test(text) && text.length <= GREETING_MAX_CHARS) {
|
|
71
|
+
return { lane: 'fast', reason: 'fast fits a quick reply.' };
|
|
72
|
+
}
|
|
73
|
+
if (text.length > 0 && text.length <= TINY_MESSAGE_MAX_CHARS && !CODE_EDIT_RE.test(text)) {
|
|
74
|
+
return { lane: 'fast', reason: 'fast fits this tiny message.' };
|
|
75
|
+
}
|
|
76
|
+
if (text.length <= YESNO_LOOKUP_MAX_CHARS
|
|
77
|
+
&& YESNO_LOOKUP_RE.test(text)
|
|
78
|
+
&& !JUDGMENT_WORD_RE.test(text)
|
|
79
|
+
&& !TROUBLE_RE.test(text)) {
|
|
80
|
+
return { lane: 'fast', reason: 'fast fits this yes or no lookup.' };
|
|
81
|
+
}
|
|
82
|
+
if (text.length <= shortLookupMaxChars
|
|
83
|
+
&& LOOKUP_START_RE.test(text)
|
|
84
|
+
&& !JUDGMENT_WORD_RE.test(text)
|
|
85
|
+
&& !TROUBLE_RE.test(text)) {
|
|
86
|
+
return { lane: 'fast', reason: 'fast fits this short factual lookup.' };
|
|
87
|
+
}
|
|
88
|
+
return { lane: 'pro', reason: 'pro fits this general request.' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = {
|
|
92
|
+
LONG_INPUT_MIN_CHARS,
|
|
93
|
+
SHORT_LOOKUP_MAX_CHARS,
|
|
94
|
+
TINY_MESSAGE_MAX_CHARS,
|
|
95
|
+
pickLane,
|
|
96
|
+
};
|