atris 3.57.2 → 3.57.4
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/atris/skills/engines/SKILL.md +4 -3
- package/atris/skills/x-search/SKILL.md +11 -5
- package/atris/skills/youtube/SKILL.md +6 -4
- package/ax +4 -1
- package/bin/atris.js +1 -1
- package/commands/close.js +294 -12
- package/commands/integrations.js +11 -2
- package/commands/mission.js +7 -16
- package/commands/worktree.js +56 -0
- package/commands/x-search.js +231 -14
- package/commands/youtube.js +331 -33
- package/lib/apply-gate.js +13 -0
- package/lib/engine-ask.js +21 -2
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: engines
|
|
3
|
-
description: "Dispatch work to an installed terminal agent or named Atris engine profile. Supports Atris Fast, Claude, Codex, Cursor, Fable, Composer, Haiku, Devin, Grok, and Antigravity (agy). Triggers on: use codex, use cursor, use devin, use grok, use agy, use antigravity, use fable, use claude, use atris, engine, dispatch to, worker agent, second opinion build."
|
|
4
|
-
version: 1.4.
|
|
3
|
+
description: "Dispatch work to an installed terminal agent or named Atris engine profile. Supports Atris Fast, Claude, Codex, Cursor, Fable, Composer, Haiku, Devin, Grok, and Antigravity (agy). Triggers on: use codex, use cursor, use devin, use grok, use agy, use antigravity, use gemini, gemini session, use fable, use claude, use atris, engine, dispatch to, worker agent, second opinion build."
|
|
4
|
+
version: 1.4.2
|
|
5
5
|
tags:
|
|
6
6
|
- engines
|
|
7
7
|
- claude
|
|
@@ -14,6 +14,7 @@ tags:
|
|
|
14
14
|
- grok
|
|
15
15
|
- agy
|
|
16
16
|
- antigravity
|
|
17
|
+
- gemini
|
|
17
18
|
- atris
|
|
18
19
|
- orchestration
|
|
19
20
|
---
|
|
@@ -50,7 +51,7 @@ Raw spawns are not the default because they skip Atris receipts, watch, and coac
|
|
|
50
51
|
| Haiku | `claude -p "<prompt>" --model claude-haiku-4-5` | Fast validation and bounded read-only checks. |
|
|
51
52
|
| Devin | `devin -p --permission-mode dangerous -- "<prompt>"` (run from the target repo) | Default permission mode is read-only for writes — build work NEEDS `--permission-mode dangerous`, so only run it in an isolated worktree. Also `devin cloud` for sessions that outlive this machine. Supports `--model swe-1.7` |
|
|
52
53
|
| Grok | `grok --always-approve -p "<prompt>"` (run from the target repo) | Headless single-turn via `-p`; default model grok-4.6. Very fast on lookups (~5-10s, reads MAP first). Great for quick second opinions; use `--best-of-n <N>` for tricky bounded builds. Uses grok.com login |
|
|
53
|
-
| Antigravity | `agy --mode accept-edits -p "<prompt>"` | `agy` executor profile. Use `--mode plan --sandbox` for read-only review
|
|
54
|
+
| Antigravity | `agy --mode accept-edits --add-dir "$PWD" -p "<prompt>"` (run from the target repo) | `agy` executor profile; also answers to "gemini". **`--add-dir` is mandatory for writes** — without it agy edits its own scratch folder (`~/.gemini/antigravity-cli/scratch/`) and the project never changes, which looks like a silent failure (verified live 2026-08-28). Use `--mode plan --sandbox` for read-only review, `--model <id>` to pin a model, and `--dangerously-skip-permissions` if a build still stalls on an approval prompt. |
|
|
54
55
|
|
|
55
56
|
Headless dispatch permissions (verified 2026-08-11): `codex exec`, `grok`, and `cursor-agent` are allowlisted in `~/.claude/settings.json` so fresh and one-shot sessions can dispatch without a human approval click. A cold session that gets "requires approval" on an engine command means that allowlist regressed.
|
|
56
57
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: x-search
|
|
3
3
|
description: "X/Twitter search via xAI Grok API. Use when user wants to search tweets, monitor topics, find viral posts, or run social listening. Costs 5 credits per search. Triggers on x search, tweet search, twitter search, social listening, revenue intel, viral tweets."
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0
|
|
5
5
|
tags:
|
|
6
6
|
- x-search
|
|
7
7
|
- social
|
|
@@ -21,15 +21,19 @@ login
|
|
|
21
21
|
|
|
|
22
22
|
x-search QUERY (5 credits)
|
|
23
23
|
|
|
|
24
|
-
|
|
24
|
+
stdout only unless --save
|
|
25
|
+
|
|
|
26
|
+
rich ephemeral --> one apply next-step, no files
|
|
27
|
+
--save + rich --> brief + pack + one Apply
|
|
28
|
+
--save + thin --> refuse, no files, exit 2
|
|
25
29
|
|
|
|
26
30
|
stop
|
|
27
31
|
```
|
|
28
32
|
|
|
29
33
|
1. Login: stored login, then `atris login --agent` if needed. Never paste tokens. Never `/auth/cli`.
|
|
30
|
-
2. Search: `atris x-search "QUERY"`. Always billed (5 credits). Empty or failed search refunds.
|
|
31
|
-
3.
|
|
32
|
-
4. Stop.
|
|
34
|
+
2. Search: `atris x-search "QUERY"`. Always billed (5 credits). Prints to stdout. Rich ephemeral prints one apply next-step and writes nothing. Empty or failed search refunds and writes nothing.
|
|
35
|
+
3. `--save` only when you want a brief. Rich results (a number-with-units or a named mechanism) file the brief, mint `atris/experiments/x-search-<slug>/`, and write one Apply that names that pack and the keep rule. Thin `--save` prints `thin: no number or named mechanism. no brief.` and writes nothing.
|
|
36
|
+
4. Stop. Thin, empty, or failed search does not print the ephemeral apply next-step.
|
|
33
37
|
|
|
34
38
|
## Customer path (preferred)
|
|
35
39
|
|
|
@@ -38,6 +42,7 @@ Logged-in customers should use the CLI. Same auth and billing as `atris youtube
|
|
|
38
42
|
```bash
|
|
39
43
|
atris x-search "MCP agents"
|
|
40
44
|
atris x-search "MCP agents" --limit 5 --days 2
|
|
45
|
+
atris x-search "MCP agents" --save
|
|
41
46
|
atris x-search person --name "Leah Bonvissuto" --handle leahbon
|
|
42
47
|
atris x-search --help
|
|
43
48
|
```
|
|
@@ -245,6 +250,7 @@ npm install -g atris && atris login
|
|
|
245
250
|
|
|
246
251
|
# Preferred: CLI
|
|
247
252
|
atris x-search "AI agents" --limit 10 --days 7
|
|
253
|
+
atris x-search "AI agents" --save
|
|
248
254
|
atris x-search person --name "John Doe" --handle johndoe --company Acme
|
|
249
255
|
|
|
250
256
|
# Raw API (debug)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: youtube
|
|
3
3
|
description: "YouTube discovery and learning. Get watch permalinks with atris youtube search QUERY (free, local ytsearch/yt-dlp). On 429 the CLI already retries; use cached rows if printed, else STOP. Never run --paid after a 429. --paid only when the user explicitly asked to buy permalinks (5 credits). After a URL is picked, atris youtube notes URL (free, ephemeral unless --save). atris youtube process only to store knowledge (5 credits). Never paste tokens. Never /auth/cli. Mint with atris login --agent from a stored login. Never summarize a video from model memory. Triggers on: youtube search, find videos, paid youtube search, any youtube.com or youtu.be link, youtube, video, watch this, notes on this."
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.12.0
|
|
5
5
|
tags:
|
|
6
6
|
- youtube
|
|
7
7
|
- research
|
|
@@ -24,8 +24,10 @@ search QUERY (free)
|
|
|
24
24
|
do not run --paid
|
|
25
25
|
|
|
|
26
26
|
pick a URL --> notes URL (free, ephemeral unless --save)
|
|
27
|
-
|
|
|
27
|
+
| rich ephemeral prints one apply next-step, no files
|
|
28
|
+
| --save files brief + pack-named apply when notes have a number or named mechanism; thin --save refuses
|
|
28
29
|
| or teach URL [--section N] (one chapter: claim numbers, named mechanisms, one check; free unless --save)
|
|
30
|
+
| next --section refuses until recap/skip
|
|
29
31
|
|
|
|
30
32
|
write one Apply (claimable) before process
|
|
31
33
|
|
|
|
@@ -39,8 +41,8 @@ never paste tokens, never /auth/cli
|
|
|
39
41
|
1. Get watch permalinks: `atris youtube search QUERY` (free).
|
|
40
42
|
2. If 429: wait/retry is already in the CLI. If it prints cached rows, use those. If it prints `youtube rate-limited local search. do not use --paid as a fallback; retry later.`, STOP. Do not run `--paid`.
|
|
41
43
|
3. `--paid` only when the user explicitly asked to buy permalinks. The CLI hard-refuses `--paid` when the free cache still has a fresh same-query hit.
|
|
42
|
-
4. `atris youtube notes URL` after a URL is picked (free). Notes is ephemeral unless `--save`.
|
|
43
|
-
5. Write one Apply (change + receipt) before `atris youtube process`. Process still requires a filled Apply (so you `--save
|
|
44
|
+
4. `atris youtube notes URL` after a URL is picked (free). Notes is ephemeral unless `--save`. Rich ephemeral prints one apply next-step and writes no files. Rich `--save` files the brief and mints `atris/experiments/notes-<id>/`; thin `--save` (no number-with-units and no named mechanism) refuses with no brief and exit 2. Do not auto `--paid`.
|
|
45
|
+
5. Write one Apply (change + receipt) before `atris youtube process`. Process still requires a filled Apply (so you `--save` a rich brief, fill Apply, then process).
|
|
44
46
|
6. Never paste tokens. Never `/auth/cli`. Mint with `atris login --agent` from a stored login.
|
|
45
47
|
|
|
46
48
|
If the user says "find videos", "search youtube", or "get youtube links" → search. If they say "learn from", "notes on", "alpha", or "rabbit hole" → notes. If they say "process", "store", "add to knowledge" → process. "Buy permalinks" or "paid search" is the only ask that unlocks `--paid`.
|
package/ax
CHANGED
|
@@ -4020,7 +4020,10 @@ async function runSelfTest(options = {}) {
|
|
|
4020
4020
|
try {
|
|
4021
4021
|
await chat({
|
|
4022
4022
|
mode: 'fast',
|
|
4023
|
-
|
|
4023
|
+
// A stray .atris under os.tmpdir() (left by a concurrent run) makes the
|
|
4024
|
+
// walk-up detector treat temp as a workspace, flipping this offline
|
|
4025
|
+
// check into local mode. Pin a path with no workspace ancestor.
|
|
4026
|
+
cwd: '/nonexistent/ax-non-workspace',
|
|
4024
4027
|
input: Readable.from(['hi\n', 'exit\n']),
|
|
4025
4028
|
output: chatOutput,
|
|
4026
4029
|
runtimeHealth: async () => {
|
package/bin/atris.js
CHANGED
|
@@ -583,7 +583,7 @@ function showHelpAll() {
|
|
|
583
583
|
console.log(' revisions - show possible human revisions after atris-assisted landings');
|
|
584
584
|
console.log(' search - Search workspace memory (atris search <keyword>)');
|
|
585
585
|
console.log(' clean - Housekeeping (stale tasks, archive journals, broken refs)');
|
|
586
|
-
console.log(' close -
|
|
586
|
+
console.log(' close - keep ten active loops and park the rest with their history');
|
|
587
587
|
console.log(' harvest - Find bugs and next actions from receipts, run logs, and thinking');
|
|
588
588
|
console.log(' verify - Validate work is done (tests, MAP.md, changes)');
|
|
589
589
|
console.log(' slop - Deterministic slop detector: frontend/prose tells, dead code (slop dead --exports)');
|
package/commands/close.js
CHANGED
|
@@ -23,6 +23,7 @@ const FAILED_TASK_BATCH_PREFIX = 'tasks:failed:';
|
|
|
23
23
|
const PROBE_TIMEOUT_MS = 60 * 1000;
|
|
24
24
|
const PROBE_HOLD_GAP_MS = 60 * 60 * 1000;
|
|
25
25
|
const PROBE_STOP_AFTER_FAILED_CHECKS = 2;
|
|
26
|
+
const MAX_OPEN_FLAGS = 10;
|
|
26
27
|
|
|
27
28
|
function ledgerPath(cwd = process.cwd()) {
|
|
28
29
|
return path.join(cwd, LEDGER_RELATIVE_PATH);
|
|
@@ -144,6 +145,15 @@ function foldEvents(events, options = {}) {
|
|
|
144
145
|
flag.why = event.why || '';
|
|
145
146
|
} else if (event.kind === 'snoozed') {
|
|
146
147
|
flag.snoozed_until = event.until || null;
|
|
148
|
+
} else if (event.kind === 'parked') {
|
|
149
|
+
flag.status = 'parked';
|
|
150
|
+
flag.parked_at = event.parked_at || eventTime(event);
|
|
151
|
+
flag.why = normalizeSpaces(event.why || 'parked by request');
|
|
152
|
+
} else if (event.kind === 'reopened') {
|
|
153
|
+
flag.status = 'open';
|
|
154
|
+
flag.reopened_at = eventTime(event);
|
|
155
|
+
delete flag.parked_at;
|
|
156
|
+
delete flag.why;
|
|
147
157
|
} else if (event.kind === 'escalated') {
|
|
148
158
|
const day = event.day || (event.at ? String(event.at).slice(0, 10) : null);
|
|
149
159
|
if (day && !flag.escalated_days.includes(day)) flag.escalated_days.push(day);
|
|
@@ -223,6 +233,12 @@ function openFlags(cwd = process.cwd(), options = {}) {
|
|
|
223
233
|
return foldEvents(readEvents(cwd), options).filter((flag) => flag.status === 'open');
|
|
224
234
|
}
|
|
225
235
|
|
|
236
|
+
function parkedFlags(cwd = process.cwd(), options = {}) {
|
|
237
|
+
return foldEvents(readEvents(cwd), options)
|
|
238
|
+
.filter((flag) => flag.status === 'parked')
|
|
239
|
+
.sort((a, b) => (parseDate(b.parked_at) || 0) - (parseDate(a.parked_at) || 0));
|
|
240
|
+
}
|
|
241
|
+
|
|
226
242
|
function ownerWaitRank(flag) {
|
|
227
243
|
return /^(you|me|operator|human|keshav)$/.test(String(flag.owner || '').toLowerCase()) ? 0 : 1;
|
|
228
244
|
}
|
|
@@ -264,6 +280,16 @@ function listLine(flag) {
|
|
|
264
280
|
return parts.join(', ');
|
|
265
281
|
}
|
|
266
282
|
|
|
283
|
+
function parkedLine(flag) {
|
|
284
|
+
const what = trimSentenceEnd(flag.what.toLowerCase());
|
|
285
|
+
const day = flag.parked_at ? String(flag.parked_at).slice(0, 10) : 'unknown date';
|
|
286
|
+
return `${what}, parked ${day}, ${trimSentenceEnd(flag.why || 'parked by request').toLowerCase()}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function migratedParkLine(flag) {
|
|
290
|
+
return `parked ${trimSentenceEnd(flag.what).toLowerCase()}: ${trimSentenceEnd(flag.why).toLowerCase()}.`;
|
|
291
|
+
}
|
|
292
|
+
|
|
267
293
|
function sweepLine(flag) {
|
|
268
294
|
const what = trimSentenceEnd(flag.what.toLowerCase());
|
|
269
295
|
const closeCondition = trimSentenceEnd(flag.close_condition.toLowerCase());
|
|
@@ -325,9 +351,12 @@ function publicFlag(flag) {
|
|
|
325
351
|
}
|
|
326
352
|
|
|
327
353
|
function printHelp() {
|
|
328
|
-
console.log('usage: atris close <add|list|done|dissolve|snooze|sweep|scan|check>');
|
|
329
|
-
console.log('add "<what>" [--owner x] [--lane life|business|code] [--ttl 7] [--when "<condition>"] [--source y] [--probe "<cmd that fails now>"]');
|
|
354
|
+
console.log('usage: atris close <add|list|park|parked|reopen|done|dissolve|snooze|sweep|scan|check>');
|
|
355
|
+
console.log('add "<what>" [--owner x] [--lane life|business|code] [--ttl 7] [--when "<condition>"] [--source y] [--probe "<cmd that fails now>"] [--replace <id>]');
|
|
330
356
|
console.log('list [--json] [--lane x]');
|
|
357
|
+
console.log('park <id> [--why "..."]');
|
|
358
|
+
console.log('parked [--json]');
|
|
359
|
+
console.log('reopen <id> [--replace <id>]');
|
|
331
360
|
console.log('done <id> [--proof "..."] (a probed incident closes only after its probe holds)');
|
|
332
361
|
console.log('check [id] [--json] (run live probes; failing probe = old theory still live)');
|
|
333
362
|
console.log('dissolve <id> --why "..."');
|
|
@@ -336,6 +365,58 @@ function printHelp() {
|
|
|
336
365
|
console.log('scan [--json]');
|
|
337
366
|
}
|
|
338
367
|
|
|
368
|
+
function appendParkedEvent(flag, cwd, now, why, options = {}) {
|
|
369
|
+
const at = now.toISOString();
|
|
370
|
+
const event = {
|
|
371
|
+
kind: 'parked',
|
|
372
|
+
at,
|
|
373
|
+
id: flag.id,
|
|
374
|
+
parked_at: at,
|
|
375
|
+
why: normalizeSpaces(why || 'parked by request'),
|
|
376
|
+
};
|
|
377
|
+
if (options.automatic) event.automatic = true;
|
|
378
|
+
appendEvent(event, cwd);
|
|
379
|
+
return {
|
|
380
|
+
id: flag.id,
|
|
381
|
+
what: flag.what,
|
|
382
|
+
parked_at: at,
|
|
383
|
+
why: event.why,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function slotCapError(flags) {
|
|
388
|
+
const fullest = flags
|
|
389
|
+
.slice()
|
|
390
|
+
.sort(compareOpenFlags)
|
|
391
|
+
.slice(0, 3)
|
|
392
|
+
.map((flag) => trimSentenceEnd(flag.what).toLowerCase())
|
|
393
|
+
.join(', ');
|
|
394
|
+
const names = fullest ? ` the fullest loops are ${fullest}.` : '';
|
|
395
|
+
return new Error(`ten open loops is the cap.${names} park one first or use --replace <id>`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function makeRoomForOpenFlag(cwd, now, replaceId, incomingWhat) {
|
|
399
|
+
const flags = openFlags(cwd, { now });
|
|
400
|
+
const hasReplacement = replaceId !== undefined;
|
|
401
|
+
if (hasReplacement && (replaceId === true || !normalizeSpaces(replaceId))) {
|
|
402
|
+
throw new Error('--replace needs an id');
|
|
403
|
+
}
|
|
404
|
+
if (!hasReplacement) {
|
|
405
|
+
if (flags.length >= MAX_OPEN_FLAGS) throw slotCapError(flags);
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
if (flags.length > MAX_OPEN_FLAGS) throw slotCapError(flags);
|
|
409
|
+
|
|
410
|
+
const replacement = flags.find((flag) => flag.id === replaceId);
|
|
411
|
+
if (!replacement) throw new Error(`${replaceId} is not open`);
|
|
412
|
+
return appendParkedEvent(
|
|
413
|
+
replacement,
|
|
414
|
+
cwd,
|
|
415
|
+
now,
|
|
416
|
+
`replaced to make room for ${trimSentenceEnd(incomingWhat).toLowerCase()}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
339
420
|
function commandAdd(args, context = {}) {
|
|
340
421
|
const { positional, options } = parseArgs(args);
|
|
341
422
|
const what = normalizeSpaces(positional.join(' '));
|
|
@@ -366,6 +447,8 @@ function commandAdd(args, context = {}) {
|
|
|
366
447
|
probeExit = result.exit_code;
|
|
367
448
|
}
|
|
368
449
|
|
|
450
|
+
makeRoomForOpenFlag(context.cwd, now, options.replace, what);
|
|
451
|
+
|
|
369
452
|
const openedAt = now.toISOString();
|
|
370
453
|
const event = {
|
|
371
454
|
kind: 'opened',
|
|
@@ -461,21 +544,88 @@ function commandCheck(args, context = {}) {
|
|
|
461
544
|
return anyFailing ? 1 : 0;
|
|
462
545
|
}
|
|
463
546
|
|
|
547
|
+
function hasRunnableNextAction(flag) {
|
|
548
|
+
if (flag.probe) return true;
|
|
549
|
+
return /\b(accept|add|approve|book|build|call|check|choose|close|confirm|deploy|dissolve|email|fix|merge|pay|publish|reply|resolve|retry|review|run|send|ship|sign|test|update|verify|write)\b/.test(canonicalWhat(flag.what));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function hasMachineCheckableClose(flag) {
|
|
553
|
+
if (flag.probe) return true;
|
|
554
|
+
if (canonicalWhat(flag.close_condition).includes('source store resolves')) return true;
|
|
555
|
+
return /^(experiment|lesson|mission|pulse|task|usage|watch):/.test(canonicalWhat(flag.source));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function migrationReason(flag, duplicateIds) {
|
|
559
|
+
if (ownerWaitRank(flag) === 0) return 'waiting on a human';
|
|
560
|
+
if (!hasRunnableNextAction(flag)) return 'no runnable next action';
|
|
561
|
+
if (!hasMachineCheckableClose(flag)) return 'no machine-checkable close condition';
|
|
562
|
+
if (duplicateIds.has(flag.id)) return 'duplicates another open loop';
|
|
563
|
+
return '';
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function compareOldestFlags(a, b) {
|
|
567
|
+
const aOpened = parseDate(a.opened_at) || 0;
|
|
568
|
+
const bOpened = parseDate(b.opened_at) || 0;
|
|
569
|
+
if (aOpened !== bOpened) return aOpened - bOpened;
|
|
570
|
+
return a.id.localeCompare(b.id);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function migrateOpenSlots(cwd = process.cwd(), now = new Date(), options = {}) {
|
|
574
|
+
const flags = openFlags(cwd, { now });
|
|
575
|
+
const excess = flags.length - MAX_OPEN_FLAGS;
|
|
576
|
+
const shouldWrite = options.dryRun !== true && options.write !== false && options.readOnly !== true;
|
|
577
|
+
if (excess <= 0 || !shouldWrite) return [];
|
|
578
|
+
|
|
579
|
+
const byWhat = new Map();
|
|
580
|
+
for (const flag of flags) {
|
|
581
|
+
const key = canonicalWhat(flag.what);
|
|
582
|
+
if (!byWhat.has(key)) byWhat.set(key, []);
|
|
583
|
+
byWhat.get(key).push(flag);
|
|
584
|
+
}
|
|
585
|
+
const duplicateIds = new Set(
|
|
586
|
+
Array.from(byWhat.values())
|
|
587
|
+
.filter((group) => group.length > 1)
|
|
588
|
+
.flat()
|
|
589
|
+
.map((flag) => flag.id)
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
const preferred = [];
|
|
593
|
+
const fallback = [];
|
|
594
|
+
for (const flag of flags.slice().sort(compareOldestFlags)) {
|
|
595
|
+
const why = migrationReason(flag, duplicateIds);
|
|
596
|
+
if (why) preferred.push({ flag, why });
|
|
597
|
+
else fallback.push({ flag, why: 'oldest loop above the active slot cap' });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return preferred.concat(fallback).slice(0, excess).map(({ flag, why }) => (
|
|
601
|
+
appendParkedEvent(flag, cwd, now, why, { automatic: true })
|
|
602
|
+
));
|
|
603
|
+
}
|
|
604
|
+
|
|
464
605
|
function commandList(args, context = {}) {
|
|
465
606
|
const { options } = parseArgs(args);
|
|
466
607
|
const lane = options.lane ? String(options.lane).toLowerCase() : null;
|
|
467
608
|
if (lane && !VALID_LANES.has(lane)) throw new Error('lane must be business, life, or code');
|
|
468
609
|
|
|
610
|
+
const now = context.now ? new Date(context.now) : new Date();
|
|
611
|
+
const autoClosed = autoCloseResolvedTaskReviewFlags(context.cwd, now);
|
|
612
|
+
const autoParked = migrateOpenSlots(context.cwd, now);
|
|
469
613
|
let flags = openFlags(context.cwd, { now: context.now });
|
|
470
614
|
if (lane) flags = flags.filter((flag) => flag.lane === lane);
|
|
471
615
|
flags.sort(compareOpenFlags);
|
|
472
616
|
|
|
473
617
|
if (options.json) {
|
|
474
|
-
|
|
618
|
+
const payload = { open: flags.map(publicFlag) };
|
|
619
|
+
if (autoClosed.length > 0) payload.auto_closed = autoClosed.map(publicTaskReviewClosed);
|
|
620
|
+
if (autoParked.length > 0) payload.auto_parked = autoParked;
|
|
621
|
+
printJson(payload);
|
|
475
622
|
return 0;
|
|
476
623
|
}
|
|
477
624
|
|
|
625
|
+
autoClosed.forEach((item) => console.log(taskReviewClosedLine(item)));
|
|
626
|
+
autoParked.forEach((flag) => console.log(migratedParkLine(flag)));
|
|
478
627
|
if (flags.length === 0) {
|
|
628
|
+
if (autoClosed.length > 0) return 0;
|
|
479
629
|
console.log('nothing open.');
|
|
480
630
|
return 0;
|
|
481
631
|
}
|
|
@@ -484,6 +634,50 @@ function commandList(args, context = {}) {
|
|
|
484
634
|
return 0;
|
|
485
635
|
}
|
|
486
636
|
|
|
637
|
+
function commandPark(args, context = {}) {
|
|
638
|
+
const { positional, options } = parseArgs(args);
|
|
639
|
+
const id = positional[0];
|
|
640
|
+
if (!id) throw new Error('id is required');
|
|
641
|
+
const flag = requireOpenFlag(id, context);
|
|
642
|
+
const now = context.now ? new Date(context.now) : new Date();
|
|
643
|
+
const why = options.why === true ? undefined : options.why;
|
|
644
|
+
appendParkedEvent(flag, context.cwd, now, why);
|
|
645
|
+
console.log(`parked ${id}`);
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function commandParked(args, context = {}) {
|
|
650
|
+
const { options } = parseArgs(args);
|
|
651
|
+
const flags = parkedFlags(context.cwd, { now: context.now });
|
|
652
|
+
if (options.json) {
|
|
653
|
+
printJson({ parked: flags.map(publicFlag) });
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
if (flags.length === 0) {
|
|
657
|
+
console.log('nothing parked.');
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
flags.forEach((flag) => console.log(parkedLine(flag)));
|
|
661
|
+
return 0;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function commandReopen(args, context = {}) {
|
|
665
|
+
const { positional, options } = parseArgs(args);
|
|
666
|
+
const id = positional[0];
|
|
667
|
+
if (!id) throw new Error('id is required');
|
|
668
|
+
const flag = parkedFlags(context.cwd, { now: context.now }).find((item) => item.id === id);
|
|
669
|
+
if (!flag) throw new Error(`${id} is not parked`);
|
|
670
|
+
const now = context.now ? new Date(context.now) : new Date();
|
|
671
|
+
makeRoomForOpenFlag(context.cwd, now, options.replace, flag.what);
|
|
672
|
+
appendEvent({
|
|
673
|
+
kind: 'reopened',
|
|
674
|
+
at: now.toISOString(),
|
|
675
|
+
id,
|
|
676
|
+
}, context.cwd);
|
|
677
|
+
console.log(`reopened ${id}`);
|
|
678
|
+
return 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
487
681
|
function requireOpenFlag(id, context = {}) {
|
|
488
682
|
const flag = openFlags(context.cwd, { now: context.now }).find((item) => item.id === id);
|
|
489
683
|
if (!flag) throw new Error(`${id} is not open`);
|
|
@@ -559,6 +753,8 @@ function commandSnooze(args, context = {}) {
|
|
|
559
753
|
// one escalated event per overdue flag per day, return the resulting state.
|
|
560
754
|
function sweepState(cwd = process.cwd(), now = new Date(), options = {}) {
|
|
561
755
|
const today = isoDateOnly(now);
|
|
756
|
+
const autoClosed = autoCloseResolvedTaskReviewFlags(cwd, now);
|
|
757
|
+
const autoParked = migrateOpenSlots(cwd, now, options);
|
|
562
758
|
let flags = openFlags(cwd, { now });
|
|
563
759
|
const overdue = flags.filter((flag) => flag.overdue).sort(compareSweepFlags);
|
|
564
760
|
const escalatedToday = [];
|
|
@@ -578,24 +774,39 @@ function sweepState(cwd = process.cwd(), now = new Date(), options = {}) {
|
|
|
578
774
|
}
|
|
579
775
|
|
|
580
776
|
flags = openFlags(cwd, { now });
|
|
581
|
-
|
|
777
|
+
const result = { open: flags.length, overdue, escalated_today: escalatedToday };
|
|
778
|
+
if (autoClosed.length > 0) result.auto_closed = autoClosed;
|
|
779
|
+
if (autoParked.length > 0) result.auto_parked = autoParked;
|
|
780
|
+
return result;
|
|
582
781
|
}
|
|
583
782
|
|
|
584
783
|
function commandSweep(args, context = {}) {
|
|
585
784
|
const { options } = parseArgs(args);
|
|
586
785
|
const now = context.now ? new Date(context.now) : new Date();
|
|
587
|
-
const {
|
|
786
|
+
const {
|
|
787
|
+
open,
|
|
788
|
+
overdue,
|
|
789
|
+
escalated_today: escalatedToday,
|
|
790
|
+
auto_closed: autoClosed = [],
|
|
791
|
+
auto_parked: autoParked = [],
|
|
792
|
+
} = sweepState(context.cwd, now);
|
|
588
793
|
|
|
589
794
|
if (options.json) {
|
|
590
|
-
|
|
795
|
+
const payload = {
|
|
591
796
|
open,
|
|
592
797
|
overdue: overdue.length,
|
|
593
798
|
escalated_today: escalatedToday,
|
|
594
|
-
}
|
|
799
|
+
};
|
|
800
|
+
if (autoClosed.length > 0) payload.auto_closed = autoClosed.map(publicTaskReviewClosed);
|
|
801
|
+
if (autoParked.length > 0) payload.auto_parked = autoParked;
|
|
802
|
+
printJson(payload);
|
|
595
803
|
return 0;
|
|
596
804
|
}
|
|
597
805
|
|
|
806
|
+
autoClosed.forEach((item) => console.log(taskReviewClosedLine(item)));
|
|
807
|
+
autoParked.forEach((flag) => console.log(migratedParkLine(flag)));
|
|
598
808
|
if (overdue.length === 0) {
|
|
809
|
+
if (autoClosed.length > 0) return 0;
|
|
599
810
|
console.log('nothing is overdue.');
|
|
600
811
|
return 0;
|
|
601
812
|
}
|
|
@@ -830,7 +1041,11 @@ function taskSource(task) {
|
|
|
830
1041
|
|
|
831
1042
|
function taskResolved(task) {
|
|
832
1043
|
const status = normalizeSpaces(task && task.status).toLowerCase();
|
|
833
|
-
|
|
1044
|
+
const review = reviewObject(task);
|
|
1045
|
+
const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
|
|
1046
|
+
const approval = normalizeSpaces(review.approval_status || metadata.approval_status).toLowerCase();
|
|
1047
|
+
return ['done', 'accepted', 'closed'].includes(status)
|
|
1048
|
+
|| ['accepted', 'approved', 'done', 'closed'].includes(approval);
|
|
834
1049
|
}
|
|
835
1050
|
|
|
836
1051
|
function reviewObject(task) {
|
|
@@ -938,7 +1153,15 @@ function shortText(text, maxLength = 70) {
|
|
|
938
1153
|
return clipped || value.slice(0, maxLength).trim();
|
|
939
1154
|
}
|
|
940
1155
|
|
|
941
|
-
function appendOpenedFromSource({
|
|
1156
|
+
function appendOpenedFromSource({
|
|
1157
|
+
what,
|
|
1158
|
+
owner = 'operator',
|
|
1159
|
+
lane = 'code',
|
|
1160
|
+
ttlDays = 3,
|
|
1161
|
+
source,
|
|
1162
|
+
probe,
|
|
1163
|
+
closeCondition = 'the source store resolves it',
|
|
1164
|
+
}, cwd, now) {
|
|
942
1165
|
const event = {
|
|
943
1166
|
kind: 'opened',
|
|
944
1167
|
at: now.toISOString(),
|
|
@@ -948,25 +1171,79 @@ function appendOpenedFromSource({ what, owner = 'operator', lane = 'code', ttlDa
|
|
|
948
1171
|
lane: VALID_LANES.has(lane) ? lane : 'code',
|
|
949
1172
|
opened_at: now.toISOString(),
|
|
950
1173
|
ttl_days: Number(ttlDays) || 3,
|
|
951
|
-
close_condition: 'the source store resolves it',
|
|
1174
|
+
close_condition: normalizeSpaces(closeCondition) || 'the source store resolves it',
|
|
952
1175
|
source,
|
|
953
1176
|
};
|
|
1177
|
+
const normalizedProbe = normalizeSpaces(probe || '');
|
|
1178
|
+
if (normalizedProbe) event.probe = normalizedProbe;
|
|
954
1179
|
appendEvent(event, cwd);
|
|
955
1180
|
return event;
|
|
956
1181
|
}
|
|
957
1182
|
|
|
958
|
-
function
|
|
1183
|
+
function upsertSourceFlag(candidate, cwd = process.cwd(), options = {}) {
|
|
1184
|
+
const now = options.now ? new Date(options.now) : new Date();
|
|
1185
|
+
const source = normalizeSpaces(candidate && candidate.source);
|
|
1186
|
+
if (!source) throw new Error('source is required');
|
|
1187
|
+
|
|
1188
|
+
const flags = openFlags(cwd, { now });
|
|
1189
|
+
const existing = flags.find((flag) => flag.source === source);
|
|
1190
|
+
if (!existing && flags.length >= MAX_OPEN_FLAGS) {
|
|
1191
|
+
return { filed: false, refreshed: false, reason: 'full' };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
const event = appendOpenedFromSource({ ...candidate, source }, cwd, now);
|
|
1195
|
+
return { filed: true, refreshed: Boolean(existing), event };
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function appendClosedFromSource(flag, cwd, now, note, proof = RESOLVED_IN_SOURCE_PROOF) {
|
|
959
1199
|
const event = {
|
|
960
1200
|
kind: 'closed',
|
|
961
1201
|
at: now.toISOString(),
|
|
962
1202
|
id: flag.id,
|
|
963
|
-
proof
|
|
1203
|
+
proof,
|
|
964
1204
|
};
|
|
965
1205
|
if (note) event.note = note;
|
|
966
1206
|
appendEvent(event, cwd);
|
|
967
1207
|
return event;
|
|
968
1208
|
}
|
|
969
1209
|
|
|
1210
|
+
function taskReviewFlag(flag) {
|
|
1211
|
+
return flag && flag.source && flag.source.startsWith('task:')
|
|
1212
|
+
&& canonicalWhat(flag.what).includes('has waited in review too long');
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function autoCloseResolvedTaskReviewFlags(cwd = process.cwd(), now = new Date()) {
|
|
1216
|
+
const tasksById = latestById(readTaskProjection(cwd));
|
|
1217
|
+
if (tasksById.size === 0) return [];
|
|
1218
|
+
|
|
1219
|
+
const closed = [];
|
|
1220
|
+
const flags = foldEvents(readEvents(cwd), { now })
|
|
1221
|
+
.filter((flag) => flag.status === 'open' || flag.status === 'parked')
|
|
1222
|
+
.filter(taskReviewFlag);
|
|
1223
|
+
for (const flag of flags) {
|
|
1224
|
+
const watchedTaskId = flag.source.slice('task:'.length);
|
|
1225
|
+
const task = tasksById.get(watchedTaskId);
|
|
1226
|
+
if (!task || !taskResolved(task)) continue;
|
|
1227
|
+
const ref = taskRef(task) || watchedTaskId;
|
|
1228
|
+
const proof = `task ${ref.toLowerCase()} is closed in task projection`;
|
|
1229
|
+
const event = appendClosedFromSource(flag, cwd, now, null, proof);
|
|
1230
|
+
closed.push({ flag, event, task_ref: ref });
|
|
1231
|
+
}
|
|
1232
|
+
return closed;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
function taskReviewClosedLine(item) {
|
|
1236
|
+
return `auto-closed review reminder for task ${item.task_ref.toLowerCase()}.`;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function publicTaskReviewClosed(item) {
|
|
1240
|
+
return {
|
|
1241
|
+
id: item.event.id,
|
|
1242
|
+
task_ref: item.task_ref,
|
|
1243
|
+
proof: item.event.proof,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
|
|
970
1247
|
function scanState(cwd = process.cwd(), options = {}) {
|
|
971
1248
|
const now = options.now ? new Date(options.now) : new Date();
|
|
972
1249
|
const tasks = readTaskProjection(cwd);
|
|
@@ -1256,6 +1533,9 @@ function run(args = [], context = {}) {
|
|
|
1256
1533
|
}
|
|
1257
1534
|
if (subcommand === 'add') return commandAdd(rest, context);
|
|
1258
1535
|
if (subcommand === 'list') return commandList(rest, context);
|
|
1536
|
+
if (subcommand === 'park') return commandPark(rest, context);
|
|
1537
|
+
if (subcommand === 'parked') return commandParked(rest, context);
|
|
1538
|
+
if (subcommand === 'reopen') return commandReopen(rest, context);
|
|
1259
1539
|
if (subcommand === 'done') return commandDone(rest, context);
|
|
1260
1540
|
if (subcommand === 'dissolve') return commandDissolve(rest, context);
|
|
1261
1541
|
if (subcommand === 'snooze') return commandSnooze(rest, context);
|
|
@@ -1277,6 +1557,8 @@ module.exports = {
|
|
|
1277
1557
|
closeIdForWhat,
|
|
1278
1558
|
foldEvents,
|
|
1279
1559
|
openFlags,
|
|
1560
|
+
parkedFlags,
|
|
1561
|
+
upsertSourceFlag,
|
|
1280
1562
|
ledgerPath,
|
|
1281
1563
|
readEvents,
|
|
1282
1564
|
sweepLine,
|
package/commands/integrations.js
CHANGED
|
@@ -443,8 +443,16 @@ async function gmailTriage(options = {}) {
|
|
|
443
443
|
const accountId = resolveGmailAccountId(options.accountId);
|
|
444
444
|
const limit = Number.isInteger(options.limit) && options.limit > 0 ? options.limit : 25;
|
|
445
445
|
const messages = await gmailInbox({ accountId, limit, quiet: true });
|
|
446
|
+
// The hourly pass rereads the newest window, so without this filter a message
|
|
447
|
+
// gets one row per hour it lingers and counts measure dwell time, not preference.
|
|
448
|
+
const seen = new Set(
|
|
449
|
+
readGmailVerdicts({ root: options.root, filePath: options.filePath, account: accountId, limit: null })
|
|
450
|
+
.map((row) => row.message_id),
|
|
451
|
+
);
|
|
452
|
+
const fresh = messages.filter((message) => !seen.has(String(message.id || message.message_id || '')));
|
|
453
|
+
const skipped = messages.length - fresh.length;
|
|
446
454
|
const ts = new Date().toISOString();
|
|
447
|
-
const rows =
|
|
455
|
+
const rows = fresh.map((message) => {
|
|
448
456
|
const { verdict, reason } = gmailTriageVerdict(message, { details: true });
|
|
449
457
|
return {
|
|
450
458
|
ts,
|
|
@@ -460,7 +468,8 @@ async function gmailTriage(options = {}) {
|
|
|
460
468
|
|
|
461
469
|
const keepCount = rows.filter((row) => row.verdict === 'keep').length;
|
|
462
470
|
const archiveCount = rows.length - keepCount;
|
|
463
|
-
|
|
471
|
+
const skippedNote = skipped > 0 ? ` (${skipped} already judged)` : '';
|
|
472
|
+
console.log(`gmail triage recorded ${keepCount} keep and ${archiveCount} archive verdict${rows.length === 1 ? '' : 's'}${skippedNote}.`);
|
|
464
473
|
return rows;
|
|
465
474
|
}
|
|
466
475
|
|
package/commands/mission.js
CHANGED
|
@@ -978,11 +978,6 @@ function loadMissionMap(root = process.cwd()) {
|
|
|
978
978
|
return map;
|
|
979
979
|
}
|
|
980
980
|
|
|
981
|
-
function hasLocalMissionState(root = process.cwd()) {
|
|
982
|
-
return readJsonLines(statePaths(root).missionsJsonl)
|
|
983
|
-
.some((mission) => mission && mission.id && mission.cloud !== true);
|
|
984
|
-
}
|
|
985
|
-
|
|
986
981
|
const LIVE_MISSION_TICK_MS = 24 * 60 * 60 * 1000;
|
|
987
982
|
|
|
988
983
|
function isLiveInFlightMission(mission, now = Date.now(), root = process.cwd()) {
|
|
@@ -1024,19 +1019,15 @@ function speakMissionDoor(args = [], root = resolveWorkspaceRoot()) {
|
|
|
1024
1019
|
if (live) {
|
|
1025
1020
|
return statusMission(asJson ? [live.id, '--json'] : [live.id]);
|
|
1026
1021
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
fresh: isFreshWorkspace(root),
|
|
1031
|
-
asJson,
|
|
1032
|
-
});
|
|
1033
|
-
process.exitCode = code;
|
|
1034
|
-
return code;
|
|
1035
|
-
}
|
|
1036
|
-
return require('./human-missions').currentMissionCommand(args, {
|
|
1022
|
+
// Nothing running in this folder: same two spoken lines as bare atris.
|
|
1023
|
+
// A finished or account-global mission from another room is not the live card.
|
|
1024
|
+
const code = speakFirstMinute({
|
|
1037
1025
|
root,
|
|
1038
|
-
|
|
1026
|
+
fresh: isFreshWorkspace(root),
|
|
1027
|
+
asJson,
|
|
1039
1028
|
});
|
|
1029
|
+
process.exitCode = code;
|
|
1030
|
+
return code;
|
|
1040
1031
|
}
|
|
1041
1032
|
|
|
1042
1033
|
function terminalNextAction(status) {
|