@phnx-labs/agents-cli 1.20.52 → 1.20.53
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/CHANGELOG.md +57 -2
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +2 -1
- package/dist/commands/exec.js +65 -1
- package/dist/commands/feed.d.ts +29 -0
- package/dist/commands/feed.js +237 -32
- package/dist/commands/memory.d.ts +9 -0
- package/dist/commands/memory.js +164 -0
- package/dist/commands/message.d.ts +11 -6
- package/dist/commands/message.js +140 -5
- package/dist/commands/routines.js +12 -0
- package/dist/commands/secrets-migrate.d.ts +2 -1
- package/dist/commands/secrets-migrate.js +88 -13
- package/dist/commands/secrets.js +4 -1
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +16 -21
- package/dist/lib/agents.js +249 -17
- package/dist/lib/answer-router.d.ts +75 -0
- package/dist/lib/answer-router.js +149 -0
- package/dist/lib/ask-classifier.d.ts +71 -0
- package/dist/lib/ask-classifier.js +197 -0
- package/dist/lib/cloud/antigravity.d.ts +0 -2
- package/dist/lib/cloud/antigravity.js +2 -17
- package/dist/lib/cloud/codex.js +3 -18
- package/dist/lib/cloud/rush.js +3 -15
- package/dist/lib/cloud/stream.js +2 -0
- package/dist/lib/cloud/types.d.ts +21 -0
- package/dist/lib/cloud/types.js +81 -0
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +12 -2
- package/dist/lib/crabbox/lease.d.ts +13 -0
- package/dist/lib/crabbox/lease.js +11 -2
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/events.js +4 -1
- package/dist/lib/exec.js +19 -1
- package/dist/lib/feed-outcome.d.ts +101 -0
- package/dist/lib/feed-outcome.js +244 -0
- package/dist/lib/feed-policy.d.ts +30 -0
- package/dist/lib/feed-policy.js +133 -0
- package/dist/lib/feed.d.ts +127 -3
- package/dist/lib/feed.js +416 -40
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +522 -12
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -4
- package/dist/lib/mailbox-gc.d.ts +22 -0
- package/dist/lib/mailbox-gc.js +161 -0
- package/dist/lib/mailbox.d.ts +26 -2
- package/dist/lib/mailbox.js +80 -5
- package/dist/lib/mcp.js +82 -0
- package/dist/lib/memory.d.ts +55 -0
- package/dist/lib/memory.js +274 -0
- package/dist/lib/notify.d.ts +16 -0
- package/dist/lib/notify.js +61 -0
- package/dist/lib/operator.d.ts +26 -0
- package/dist/lib/operator.js +107 -0
- package/dist/lib/plugins.d.ts +35 -0
- package/dist/lib/plugins.js +217 -0
- package/dist/lib/remote-agents-json.d.ts +14 -0
- package/dist/lib/remote-agents-json.js +94 -0
- package/dist/lib/resources/mcp.js +44 -0
- package/dist/lib/resources/memory.d.ts +15 -0
- package/dist/lib/resources/memory.js +46 -0
- package/dist/lib/resources/types.d.ts +2 -2
- package/dist/lib/runner.d.ts +43 -0
- package/dist/lib/runner.js +323 -74
- package/dist/lib/sandbox.js +6 -0
- package/dist/lib/secrets/bundles.js +38 -13
- package/dist/lib/secrets/icloud-import.d.ts +12 -3
- package/dist/lib/secrets/icloud-import.js +37 -7
- package/dist/lib/secrets/index.d.ts +106 -2
- package/dist/lib/secrets/index.js +603 -28
- package/dist/lib/session/active.d.ts +18 -0
- package/dist/lib/session/active.js +47 -17
- package/dist/lib/session/db.d.ts +9 -1
- package/dist/lib/session/db.js +18 -3
- package/dist/lib/session/discover.d.ts +13 -0
- package/dist/lib/session/discover.js +31 -0
- package/dist/lib/session/parse.d.ts +8 -0
- package/dist/lib/session/parse.js +42 -21
- package/dist/lib/session/remote-active.js +8 -89
- package/dist/lib/session/state.d.ts +11 -0
- package/dist/lib/session/state.js +37 -0
- package/dist/lib/session/tail.d.ts +23 -4
- package/dist/lib/session/tail.js +34 -16
- package/dist/lib/session/throughput.d.ts +30 -0
- package/dist/lib/session/throughput.js +86 -0
- package/dist/lib/shim-heal.d.ts +12 -3
- package/dist/lib/shim-heal.js +12 -6
- package/dist/lib/staleness/detectors/subagents.js +57 -3
- package/dist/lib/staleness/writers/hooks.js +7 -3
- package/dist/lib/staleness/writers/subagents.js +37 -6
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +10 -2
- package/dist/lib/versions.js +38 -48
- package/package.json +4 -3
- package/scripts/postinstall.js +61 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { injectTargetFromReplyRail } from './session/inject.js';
|
|
2
|
+
/**
|
|
3
|
+
* Match free-text answer against question options.
|
|
4
|
+
* Exact (case-insensitive) > startsWith > includes. Returns 0-based index or -1.
|
|
5
|
+
*/
|
|
6
|
+
export function matchOptionIndex(answer, options) {
|
|
7
|
+
if (!options?.length)
|
|
8
|
+
return -1;
|
|
9
|
+
const needle = answer.trim().toLowerCase();
|
|
10
|
+
if (!needle)
|
|
11
|
+
return -1;
|
|
12
|
+
const labels = options.map((o) => (o.label ?? '').trim());
|
|
13
|
+
const exact = labels.findIndex((l) => l.toLowerCase() === needle);
|
|
14
|
+
if (exact >= 0)
|
|
15
|
+
return exact;
|
|
16
|
+
const starts = labels.findIndex((l) => l.toLowerCase().startsWith(needle));
|
|
17
|
+
if (starts >= 0)
|
|
18
|
+
return starts;
|
|
19
|
+
const includes = labels.findIndex((l) => l.toLowerCase().includes(needle));
|
|
20
|
+
return includes;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the keystroke payload that closes an AskUserQuestion TUI.
|
|
24
|
+
* Numbered options are selected by digit (1-based) + Enter. Free text with an
|
|
25
|
+
* "Other" option selects Other then types; without Other, types the answer.
|
|
26
|
+
*/
|
|
27
|
+
export function keystrokesForAnswer(answer, options) {
|
|
28
|
+
const idx = matchOptionIndex(answer, options);
|
|
29
|
+
if (idx >= 0) {
|
|
30
|
+
// AskUserQuestion / plan select-lists are 1-indexed digits.
|
|
31
|
+
return { payload: `${idx + 1}`, matched: 'option' };
|
|
32
|
+
}
|
|
33
|
+
if (options?.length) {
|
|
34
|
+
const otherIdx = options.findIndex((o) => /^other$/i.test((o.label ?? '').trim()));
|
|
35
|
+
if (otherIdx >= 0) {
|
|
36
|
+
// Select Other, then type the free text on the next field.
|
|
37
|
+
return { payload: `${otherIdx + 1}\n${answer}`, matched: 'other' };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { payload: answer, matched: 'free-text' };
|
|
41
|
+
}
|
|
42
|
+
/** True when the session is waiting on user input (parked on a question/plan). */
|
|
43
|
+
export function isParkedOnInput(session) {
|
|
44
|
+
if (!session)
|
|
45
|
+
return false;
|
|
46
|
+
if (session.status === 'input_required')
|
|
47
|
+
return true;
|
|
48
|
+
if (session.activity === 'waiting_input')
|
|
49
|
+
return true;
|
|
50
|
+
if (session.awaitingReason === 'question' || session.awaitingReason === 'plan_review' || session.awaitingReason === 'permission') {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
/** True when an open feed block still needs an answer. */
|
|
56
|
+
export function isOpenQuestionBlock(block) {
|
|
57
|
+
if (!block)
|
|
58
|
+
return false;
|
|
59
|
+
if (block.answer || block.parkedAt || block.continuedAt || block.defaultedAt)
|
|
60
|
+
return false;
|
|
61
|
+
return (block.questions?.length ?? 0) > 0;
|
|
62
|
+
}
|
|
63
|
+
function injectTargetForSession(session) {
|
|
64
|
+
if (session.provenance?.reply) {
|
|
65
|
+
const fromRail = injectTargetFromReplyRail(session.provenance.reply);
|
|
66
|
+
if (fromRail)
|
|
67
|
+
return fromRail;
|
|
68
|
+
}
|
|
69
|
+
// Only the agents-pty sidecar is addressable by session id. Generic headless
|
|
70
|
+
// runs have no PTY server entry — those go through resume, not inject.
|
|
71
|
+
if (session.sessionId && session.host === 'pty') {
|
|
72
|
+
return { backend: 'pty', id: session.sessionId };
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Pick the delivery mechanism for one answer.
|
|
78
|
+
*
|
|
79
|
+
* Precedence:
|
|
80
|
+
* 1. Parked on open question + injectable rail (tmux/iterm/pty) → keystroke
|
|
81
|
+
* 2. Parked on open question + headless (no rail) → resume with answer
|
|
82
|
+
* 3. Parked on open question + no rail + interactive → refuse (don't mailbox-drop)
|
|
83
|
+
* 4. Otherwise → mailbox (running agent between tool calls)
|
|
84
|
+
*/
|
|
85
|
+
export function resolveAnswerRoute(input) {
|
|
86
|
+
const { answer, block, session } = input;
|
|
87
|
+
const openQ = isOpenQuestionBlock(block);
|
|
88
|
+
const parked = isParkedOnInput(session);
|
|
89
|
+
// Options from the first question on the block (or the session's structured question).
|
|
90
|
+
const options = block?.questions?.[0]?.options ??
|
|
91
|
+
session?.question?.options?.map((o) => ({ label: o.label }));
|
|
92
|
+
if (openQ && parked && session) {
|
|
93
|
+
const inject = injectTargetForSession(session);
|
|
94
|
+
const { payload, matched } = keystrokesForAnswer(answer, options);
|
|
95
|
+
if (inject) {
|
|
96
|
+
const kind = inject.backend === 'tmux' ? 'tmux'
|
|
97
|
+
: inject.backend === 'iterm' ? 'iterm'
|
|
98
|
+
: inject.backend === 'pty' ? 'pty'
|
|
99
|
+
: 'pty';
|
|
100
|
+
return {
|
|
101
|
+
kind,
|
|
102
|
+
reason: `Parked on open question — drive ${inject.backend} selection (${matched}).`,
|
|
103
|
+
payload,
|
|
104
|
+
inject,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// Headless / no rail: re-enter via resume.
|
|
108
|
+
if (session.context === 'headless' || session.context === 'teams' || !session.tty) {
|
|
109
|
+
const sid = session.sessionId ?? input.mailboxId;
|
|
110
|
+
if (!sid) {
|
|
111
|
+
return {
|
|
112
|
+
kind: 'refuse',
|
|
113
|
+
reason: 'Parked headless agent has no session id to resume.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
kind: 'resume',
|
|
118
|
+
reason: 'Parked headless agent — resume with the answer as the next user turn.',
|
|
119
|
+
payload: answer,
|
|
120
|
+
resume: { sessionId: sid, agent: session.kind },
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
kind: 'refuse',
|
|
125
|
+
reason: 'Agent is parked on a question but has no addressable terminal (no tmux/iterm/pty rail). ' +
|
|
126
|
+
'Open its terminal and answer there, or run `agents sessions focus <id>` first.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Open block but agent is still looping between tool calls — mailbox is correct.
|
|
130
|
+
// No block — free-form mid-flight steer — mailbox.
|
|
131
|
+
return {
|
|
132
|
+
kind: 'mailbox',
|
|
133
|
+
reason: openQ
|
|
134
|
+
? 'Open block on a running agent — deliver via mailbox at next tool call.'
|
|
135
|
+
: 'No open question — deliver via mailbox at next tool call.',
|
|
136
|
+
payload: answer,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/** Build argv for a headless resume that continues with the answer text. */
|
|
140
|
+
export function resumeArgv(route) {
|
|
141
|
+
if (route.kind !== 'resume' || !route.resume) {
|
|
142
|
+
throw new Error('resumeArgv requires a resume route');
|
|
143
|
+
}
|
|
144
|
+
const { agent, sessionId } = route.resume;
|
|
145
|
+
const text = route.payload ?? '';
|
|
146
|
+
// `agents run <agent> --resume <id> -- <answer>` — the trailing prompt is the
|
|
147
|
+
// next user turn after native resume (claude/codex) or /continue replay.
|
|
148
|
+
return ['run', agent, '--resume', sessionId, '--', text];
|
|
149
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ask classifier + stall suppression (RUSH-1477).
|
|
3
|
+
*
|
|
4
|
+
* ~39% of AskUserQuestion calls are workflow-stalls ("should I…?", "what's next?",
|
|
5
|
+
* "merge now?"). The feed's first job is to make those disappear so real Decisions
|
|
6
|
+
* and Approvals stay visible.
|
|
7
|
+
*
|
|
8
|
+
* Pipeline:
|
|
9
|
+
* 1. Classify every block into Decision / Approval / Clarification / Stall / Fyi
|
|
10
|
+
* 2. Suppress stalls (and pure FYIs) with an auto-answer so they never render
|
|
11
|
+
* 3. Surface Decision + Approval (+ Clarification when it needs a real fact)
|
|
12
|
+
*
|
|
13
|
+
* Rules-based on stable high-volume shapes. Pure classify/match; suppression has
|
|
14
|
+
* side effects via the feed store when applied.
|
|
15
|
+
*/
|
|
16
|
+
import type { OpenBlock } from './feed.js';
|
|
17
|
+
/** Taxonomy for a published ask. Exactly one class per block. */
|
|
18
|
+
export type AskClass = 'decision' | 'approval' | 'clarification' | 'stall' | 'fyi';
|
|
19
|
+
export interface Classification {
|
|
20
|
+
class: AskClass;
|
|
21
|
+
/** Why this class was chosen (rule name). */
|
|
22
|
+
rule: string;
|
|
23
|
+
/** True when the feed should hide this block and auto-answer it. */
|
|
24
|
+
suppress: boolean;
|
|
25
|
+
/** Auto-answer text when suppress is true. */
|
|
26
|
+
autoAnswer?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Scan text used for classification. */
|
|
29
|
+
export declare function askScanText(block: Pick<OpenBlock, 'questions'>): string;
|
|
30
|
+
/**
|
|
31
|
+
* Classify one ask from free text (+ optional explicit blockClass from the agent).
|
|
32
|
+
* Explicit blockClass from the agent is honored as a floor: 'decision' never becomes
|
|
33
|
+
* a suppressible stall (false-suppress rate ~0 for real Decisions).
|
|
34
|
+
*/
|
|
35
|
+
export declare function classifyAsk(text: string, opts?: {
|
|
36
|
+
header?: string;
|
|
37
|
+
blockClass?: OpenBlock['blockClass'];
|
|
38
|
+
}): Classification;
|
|
39
|
+
/** Classify a full open block (all questions concatenated). */
|
|
40
|
+
export declare function classifyBlock(block: OpenBlock): Classification;
|
|
41
|
+
export interface SuppressResult {
|
|
42
|
+
blockId: string;
|
|
43
|
+
class: AskClass;
|
|
44
|
+
rule: string;
|
|
45
|
+
autoAnswer: string;
|
|
46
|
+
/** True when the block was removed from the visible feed. */
|
|
47
|
+
suppressed: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Auto-answer + remove a suppressible block so it never renders as a card.
|
|
51
|
+
* Logs the stall as answered by policy:stall-suppression.
|
|
52
|
+
*/
|
|
53
|
+
export declare function suppressStallBlock(block: OpenBlock, root?: string): SuppressResult;
|
|
54
|
+
export interface FeedFilterResult {
|
|
55
|
+
/** Blocks that should render as cards. */
|
|
56
|
+
surfaced: OpenBlock[];
|
|
57
|
+
/** Suppression audit rows (stalls auto-resolved). */
|
|
58
|
+
suppressed: SuppressResult[];
|
|
59
|
+
/** Per-class counts over the input set (before suppression). */
|
|
60
|
+
counts: Record<AskClass, number>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Classify every block; optionally apply stall suppression (mutate store).
|
|
64
|
+
* When `apply` is false, only classifies (dry run for --json audit).
|
|
65
|
+
*/
|
|
66
|
+
export declare function filterBlocksForFeed(blocks: OpenBlock[], opts?: {
|
|
67
|
+
apply?: boolean;
|
|
68
|
+
root?: string;
|
|
69
|
+
}): FeedFilterResult;
|
|
70
|
+
/** Human digest: "31 stalls auto-resolved by policy". */
|
|
71
|
+
export declare function suppressionDigest(result: FeedFilterResult): string;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { recordAnswer, recordMessageReceipt, removeBlock } from './feed.js';
|
|
2
|
+
import { enqueue, mailboxDir } from './mailbox.js';
|
|
3
|
+
/** Scan text used for classification. */
|
|
4
|
+
export function askScanText(block) {
|
|
5
|
+
return block.questions
|
|
6
|
+
.map((q) => [q.header, q.text].filter(Boolean).join(' '))
|
|
7
|
+
.join('\n')
|
|
8
|
+
.toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
const RULES = [
|
|
11
|
+
// FYI / done — no action needed
|
|
12
|
+
{
|
|
13
|
+
name: 'fyi-done',
|
|
14
|
+
class: 'fyi',
|
|
15
|
+
suppress: true,
|
|
16
|
+
autoAnswer: 'acknowledged',
|
|
17
|
+
test: (t) => /\b(fyi|for your information|just letting you know|heads.?up|no action needed|already done|completed successfully)\b/.test(t) ||
|
|
18
|
+
/^(done|shipped|merged|complete)[.!]?$/.test(t.trim()),
|
|
19
|
+
},
|
|
20
|
+
// Stall: "should I / want me to / shall I continue"
|
|
21
|
+
{
|
|
22
|
+
name: 'stall-should-i',
|
|
23
|
+
class: 'stall',
|
|
24
|
+
suppress: true,
|
|
25
|
+
autoAnswer: 'Yes, continue.',
|
|
26
|
+
test: (t) => /\b(should i|shall i|want me to|would you like me to|do you want me to|can i go ahead|ok to proceed|okay to continue)\b/.test(t),
|
|
27
|
+
},
|
|
28
|
+
// Stall: "what's next / continue?"
|
|
29
|
+
{
|
|
30
|
+
name: 'stall-whats-next',
|
|
31
|
+
class: 'stall',
|
|
32
|
+
suppress: true,
|
|
33
|
+
autoAnswer: 'Continue with the next step.',
|
|
34
|
+
test: (t) => /\b(what('?s| is)? next|what now|continue\??|keep going\??|ready for the next|next step\??)\b/.test(t) ||
|
|
35
|
+
/^continue\??$/.test(t.trim()),
|
|
36
|
+
},
|
|
37
|
+
// Stall: verify-then-proceed / looks good?
|
|
38
|
+
{
|
|
39
|
+
name: 'stall-verify-proceed',
|
|
40
|
+
class: 'stall',
|
|
41
|
+
suppress: true,
|
|
42
|
+
autoAnswer: 'Looks good — proceed.',
|
|
43
|
+
test: (t) => /\b(looks good\??|does this look (ok|right|good)|sound good\??|ready to (go|ship|land)\??|any objections\??)\b/.test(t),
|
|
44
|
+
},
|
|
45
|
+
// Approval: merge/release/ship/commit yes-no (surfaces; policy can default later)
|
|
46
|
+
{
|
|
47
|
+
name: 'approval-merge-release',
|
|
48
|
+
class: 'approval',
|
|
49
|
+
suppress: false,
|
|
50
|
+
test: (t) => /\b(merge (now|this|the pr|it)\??|release\??|ship (it|now)\??|publish\??|cut (a )?release|tag (and )?release|commit (this|these changes)\??)\b/.test(t) ||
|
|
51
|
+
/\b(approve|deny|allow|reject)\b.*\b(merge|release|deploy|pr)\b/.test(t),
|
|
52
|
+
},
|
|
53
|
+
// Clarification: which X / which repo / pick one fact
|
|
54
|
+
{
|
|
55
|
+
name: 'clarification-which',
|
|
56
|
+
class: 'clarification',
|
|
57
|
+
suppress: false,
|
|
58
|
+
test: (t) => /\b(which (repo|branch|host|file|path|environment|env|project|package|version|account|device|machine)|what is the (path|url|id|name) of)\b/.test(t) ||
|
|
59
|
+
/\b(missing (the )?(path|url|id|name|repo)|need (the )?(path|url|id|repo) for)\b/.test(t),
|
|
60
|
+
},
|
|
61
|
+
// Decision: scope / approach / tradeoff (never auto-suppress)
|
|
62
|
+
{
|
|
63
|
+
name: 'decision-scope-approach',
|
|
64
|
+
class: 'decision',
|
|
65
|
+
suppress: false,
|
|
66
|
+
test: (t) => /\b(scope|approach|architecture|design|trade-?off|which approach|how should we|prefer A or B|option a or b)\b/.test(t) ||
|
|
67
|
+
/\b(break(ing)? change|public api|migrate everyone|rewrite)\b/.test(t),
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
/**
|
|
71
|
+
* Classify one ask from free text (+ optional explicit blockClass from the agent).
|
|
72
|
+
* Explicit blockClass from the agent is honored as a floor: 'decision' never becomes
|
|
73
|
+
* a suppressible stall (false-suppress rate ~0 for real Decisions).
|
|
74
|
+
*/
|
|
75
|
+
export function classifyAsk(text, opts) {
|
|
76
|
+
const scan = [opts?.header, text].filter(Boolean).join(' ').toLowerCase();
|
|
77
|
+
for (const rule of RULES) {
|
|
78
|
+
if (!rule.test(scan))
|
|
79
|
+
continue;
|
|
80
|
+
// Safety: agent-tagged decisions never suppress.
|
|
81
|
+
if (opts?.blockClass === 'decision' && rule.suppress) {
|
|
82
|
+
return { class: 'decision', rule: 'agent-blockClass-decision', suppress: false };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
class: rule.class,
|
|
86
|
+
rule: rule.name,
|
|
87
|
+
suppress: rule.suppress,
|
|
88
|
+
autoAnswer: rule.autoAnswer,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// Explicit agent tag with no text rule.
|
|
92
|
+
if (opts?.blockClass === 'decision') {
|
|
93
|
+
return { class: 'decision', rule: 'agent-blockClass-decision', suppress: false };
|
|
94
|
+
}
|
|
95
|
+
if (opts?.blockClass === 'approval') {
|
|
96
|
+
return { class: 'approval', rule: 'agent-blockClass-approval', suppress: false };
|
|
97
|
+
}
|
|
98
|
+
// Default: surface as decision so we never silent-drop unknowns.
|
|
99
|
+
return { class: 'decision', rule: 'default-decision', suppress: false };
|
|
100
|
+
}
|
|
101
|
+
/** Classify a full open block (all questions concatenated). */
|
|
102
|
+
export function classifyBlock(block) {
|
|
103
|
+
const text = block.questions.map((q) => q.text).join(' ');
|
|
104
|
+
const header = block.questions.map((q) => q.header).filter(Boolean).join(' ');
|
|
105
|
+
return classifyAsk(text, { header, blockClass: block.blockClass });
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Auto-answer + remove a suppressible block so it never renders as a card.
|
|
109
|
+
* Logs the stall as answered by policy:stall-suppression.
|
|
110
|
+
*/
|
|
111
|
+
export function suppressStallBlock(block, root) {
|
|
112
|
+
const c = classifyBlock(block);
|
|
113
|
+
if (!c.suppress || !c.autoAnswer) {
|
|
114
|
+
return { blockId: block.blockId, class: c.class, rule: c.rule, autoAnswer: '', suppressed: false };
|
|
115
|
+
}
|
|
116
|
+
const claim = recordAnswer(block.blockId, {
|
|
117
|
+
answeredFrom: 'policy',
|
|
118
|
+
answeredBy: 'stall-suppression',
|
|
119
|
+
operatorId: 'policy',
|
|
120
|
+
verified: true,
|
|
121
|
+
}, root);
|
|
122
|
+
if (!claim.ok) {
|
|
123
|
+
return { blockId: block.blockId, class: c.class, rule: c.rule, autoAnswer: c.autoAnswer, suppressed: false };
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
// Mailbox lives under the global mailbox root (not the feed dir).
|
|
127
|
+
const msgId = enqueue(mailboxDir(block.mailboxId), {
|
|
128
|
+
to: block.mailboxId,
|
|
129
|
+
text: c.autoAnswer,
|
|
130
|
+
from: 'stall-suppression',
|
|
131
|
+
blockId: block.blockId,
|
|
132
|
+
});
|
|
133
|
+
recordMessageReceipt(block.blockId, { msgId, status: 'queued', at: new Date().toISOString(), from: 'stall-suppression' }, root);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Mailbox write can fail if the box id is invalid; still drop the card.
|
|
137
|
+
}
|
|
138
|
+
removeBlock(block.blockId, root);
|
|
139
|
+
return {
|
|
140
|
+
blockId: block.blockId,
|
|
141
|
+
class: c.class,
|
|
142
|
+
rule: c.rule,
|
|
143
|
+
autoAnswer: c.autoAnswer,
|
|
144
|
+
suppressed: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Classify every block; optionally apply stall suppression (mutate store).
|
|
149
|
+
* When `apply` is false, only classifies (dry run for --json audit).
|
|
150
|
+
*/
|
|
151
|
+
export function filterBlocksForFeed(blocks, opts) {
|
|
152
|
+
const counts = {
|
|
153
|
+
decision: 0,
|
|
154
|
+
approval: 0,
|
|
155
|
+
clarification: 0,
|
|
156
|
+
stall: 0,
|
|
157
|
+
fyi: 0,
|
|
158
|
+
};
|
|
159
|
+
const surfaced = [];
|
|
160
|
+
const suppressed = [];
|
|
161
|
+
const apply = opts?.apply === true;
|
|
162
|
+
for (const block of blocks) {
|
|
163
|
+
const c = classifyBlock(block);
|
|
164
|
+
counts[c.class] += 1;
|
|
165
|
+
if (c.suppress) {
|
|
166
|
+
if (apply) {
|
|
167
|
+
const r = suppressStallBlock(block, opts?.root);
|
|
168
|
+
suppressed.push(r);
|
|
169
|
+
if (!r.suppressed)
|
|
170
|
+
surfaced.push(block);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
suppressed.push({
|
|
174
|
+
blockId: block.blockId,
|
|
175
|
+
class: c.class,
|
|
176
|
+
rule: c.rule,
|
|
177
|
+
autoAnswer: c.autoAnswer ?? '',
|
|
178
|
+
suppressed: false,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
surfaced.push(block);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { surfaced, suppressed, counts };
|
|
187
|
+
}
|
|
188
|
+
/** Human digest: "31 stalls auto-resolved by policy". */
|
|
189
|
+
export function suppressionDigest(result) {
|
|
190
|
+
const n = result.suppressed.filter((s) => s.suppressed || s.class === 'stall' || s.class === 'fyi').length;
|
|
191
|
+
if (n === 0)
|
|
192
|
+
return '';
|
|
193
|
+
const applied = result.suppressed.filter((s) => s.suppressed).length;
|
|
194
|
+
if (applied > 0)
|
|
195
|
+
return `${applied} stall${applied === 1 ? '' : 's'} auto-resolved by policy`;
|
|
196
|
+
return `${n} stall${n === 1 ? '' : 's'} eligible for suppression`;
|
|
197
|
+
}
|
|
@@ -31,8 +31,6 @@ interface InteractionResponse {
|
|
|
31
31
|
message?: string;
|
|
32
32
|
} | string;
|
|
33
33
|
}
|
|
34
|
-
/** Map an Interactions API status string to the canonical CloudTaskStatus. */
|
|
35
|
-
export declare function mapStatus(s: string | undefined): CloudTaskStatus;
|
|
36
34
|
/** Build the Interactions API request body for a fresh dispatch. */
|
|
37
35
|
export declare function buildInteractionBody(prompt: string, model: string): Record<string, unknown>;
|
|
38
36
|
/** Parse an Interactions API response into a CloudTask (minus prompt/timestamps). */
|
|
@@ -19,26 +19,11 @@
|
|
|
19
19
|
* `cloud.providers.antigravity.secretsBundle` (never from agents.yaml), falling
|
|
20
20
|
* back to GEMINI_API_KEY / GOOGLE_API_KEY in the environment.
|
|
21
21
|
*/
|
|
22
|
-
import { resolveDispatchRepos } from './types.js';
|
|
22
|
+
import { resolveDispatchRepos, normalizeProviderStatus } from './types.js';
|
|
23
23
|
import { readAndResolveBundleEnv } from '../secrets/bundles.js';
|
|
24
24
|
const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
|
|
25
25
|
const DEFAULT_MODEL = 'antigravity-preview-05-2026';
|
|
26
26
|
const KEY_NAMES = ['GEMINI_API_KEY', 'GOOGLE_API_KEY'];
|
|
27
|
-
/** Map an Interactions API status string to the canonical CloudTaskStatus. */
|
|
28
|
-
export function mapStatus(s) {
|
|
29
|
-
const lower = (s ?? '').toLowerCase();
|
|
30
|
-
if (lower.includes('queue') || lower.includes('pending'))
|
|
31
|
-
return 'queued';
|
|
32
|
-
if (lower.includes('run') || lower.includes('progress'))
|
|
33
|
-
return 'running';
|
|
34
|
-
if (lower.includes('complete') || lower.includes('success'))
|
|
35
|
-
return 'completed';
|
|
36
|
-
if (lower.includes('fail') || lower.includes('error'))
|
|
37
|
-
return 'failed';
|
|
38
|
-
if (lower.includes('cancel'))
|
|
39
|
-
return 'cancelled';
|
|
40
|
-
return 'completed';
|
|
41
|
-
}
|
|
42
27
|
/** Build the Interactions API request body for a fresh dispatch. */
|
|
43
28
|
export function buildInteractionBody(prompt, model) {
|
|
44
29
|
return {
|
|
@@ -52,7 +37,7 @@ export function parseInteraction(resp) {
|
|
|
52
37
|
const id = resp.id ?? resp.interaction_id ?? `antigravity-${Date.now()}`;
|
|
53
38
|
return {
|
|
54
39
|
id,
|
|
55
|
-
status:
|
|
40
|
+
status: normalizeProviderStatus('antigravity', resp.status),
|
|
56
41
|
summary: resp.output_text,
|
|
57
42
|
environmentId: resp.environment_id,
|
|
58
43
|
};
|
package/dist/lib/cloud/codex.js
CHANGED
|
@@ -8,24 +8,9 @@
|
|
|
8
8
|
import { spawn, execFileSync } from 'child_process';
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
|
-
import { resolveDispatchRepos, MissingTargetError } from './types.js';
|
|
11
|
+
import { resolveDispatchRepos, normalizeProviderStatus, MissingTargetError } from './types.js';
|
|
12
12
|
import { getShimsDir } from '../state.js';
|
|
13
13
|
const SHIMS_DIR = getShimsDir();
|
|
14
|
-
/** Map a Codex Cloud status string to the canonical CloudTaskStatus enum. */
|
|
15
|
-
function mapStatus(s) {
|
|
16
|
-
const lower = s.toLowerCase();
|
|
17
|
-
if (lower.includes('queued') || lower.includes('pending'))
|
|
18
|
-
return 'queued';
|
|
19
|
-
if (lower.includes('running') || lower.includes('in_progress'))
|
|
20
|
-
return 'running';
|
|
21
|
-
if (lower.includes('completed') || lower.includes('succeeded') || lower.includes('success'))
|
|
22
|
-
return 'completed';
|
|
23
|
-
if (lower.includes('failed') || lower.includes('error'))
|
|
24
|
-
return 'failed';
|
|
25
|
-
if (lower.includes('cancelled') || lower.includes('canceled'))
|
|
26
|
-
return 'cancelled';
|
|
27
|
-
return 'running';
|
|
28
|
-
}
|
|
29
14
|
/** Locate the codex binary, checking agents-cli shims first then PATH. */
|
|
30
15
|
function findCodexBinary() {
|
|
31
16
|
// Check agents-cli shims first
|
|
@@ -77,7 +62,7 @@ function parseTaskFromText(text) {
|
|
|
77
62
|
}
|
|
78
63
|
return {
|
|
79
64
|
id: result.id || result.task_id,
|
|
80
|
-
status: result.status ?
|
|
65
|
+
status: result.status ? normalizeProviderStatus('codex', result.status) : undefined,
|
|
81
66
|
summary: result.summary || result.output,
|
|
82
67
|
};
|
|
83
68
|
}
|
|
@@ -186,7 +171,7 @@ export class CodexCloudProvider {
|
|
|
186
171
|
const tasks = (data.tasks ?? data ?? []).map((t) => ({
|
|
187
172
|
id: (t.id || t.task_id),
|
|
188
173
|
provider: 'codex',
|
|
189
|
-
status:
|
|
174
|
+
status: normalizeProviderStatus('codex', t.status ?? ''),
|
|
190
175
|
agent: 'codex',
|
|
191
176
|
prompt: (t.prompt || t.query || ''),
|
|
192
177
|
branch: t.branch || undefined,
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as os from 'os';
|
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { getCloudDir } from '../state.js';
|
|
13
|
-
import { resolveDispatchRepos, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
13
|
+
import { resolveDispatchRepos, normalizeProviderStatus, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
14
14
|
import { parseSSE } from './stream.js';
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
16
16
|
import { getAccountInfo } from '../agents.js';
|
|
@@ -56,18 +56,6 @@ function recordRushUploadConsent(grantedBy, accountFingerprint) {
|
|
|
56
56
|
// boundary. Worst case, the user is asked to consent again next dispatch.
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
-
/** Map a Factory Floor status string to the canonical CloudTaskStatus enum. */
|
|
60
|
-
function mapStatus(s) {
|
|
61
|
-
switch (s) {
|
|
62
|
-
case 'allocating': return 'allocating';
|
|
63
|
-
case 'running': return 'running';
|
|
64
|
-
case 'needs_review': return 'input_required';
|
|
65
|
-
case 'completed': return 'completed';
|
|
66
|
-
case 'failed': return 'failed';
|
|
67
|
-
case 'cancelled': return 'cancelled';
|
|
68
|
-
default: return 'running';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
59
|
/** Read the Rush session access token from ~/.rush/user.yaml. */
|
|
72
60
|
function readToken() {
|
|
73
61
|
if (!fs.existsSync(USER_YAML)) {
|
|
@@ -452,7 +440,7 @@ export class RushCloudProvider {
|
|
|
452
440
|
return {
|
|
453
441
|
id: taskId,
|
|
454
442
|
provider: 'rush',
|
|
455
|
-
status:
|
|
443
|
+
status: normalizeProviderStatus('rush', data.status),
|
|
456
444
|
agent: data.agent || undefined,
|
|
457
445
|
prompt: data.prompt || '',
|
|
458
446
|
repo: data.repo_owner && data.repo_name ? `${data.repo_owner}/${data.repo_name}` : undefined,
|
|
@@ -477,7 +465,7 @@ export class RushCloudProvider {
|
|
|
477
465
|
return (data.executions ?? []).map((e) => ({
|
|
478
466
|
id: e.execution_id,
|
|
479
467
|
provider: 'rush',
|
|
480
|
-
status:
|
|
468
|
+
status: normalizeProviderStatus('rush', e.status),
|
|
481
469
|
agent: e.agent || undefined,
|
|
482
470
|
prompt: e.prompt || '',
|
|
483
471
|
repo: e.repo_owner && e.repo_name ? `${e.repo_owner}/${e.repo_name}` : undefined,
|
package/dist/lib/cloud/stream.js
CHANGED
|
@@ -179,6 +179,8 @@ function statusLabel(status) {
|
|
|
179
179
|
return chalk.blue(`[${status}]`);
|
|
180
180
|
case 'running':
|
|
181
181
|
return chalk.yellow(`[${status}]`);
|
|
182
|
+
case 'idle':
|
|
183
|
+
return chalk.dim('[idle]');
|
|
182
184
|
case 'completed':
|
|
183
185
|
return chalk.green('[completed]');
|
|
184
186
|
case 'needs_review':
|
|
@@ -27,6 +27,27 @@ export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity';
|
|
|
27
27
|
* failed | cancelled` states, which cannot transition back to `running`.
|
|
28
28
|
*/
|
|
29
29
|
export type CloudTaskStatus = 'queued' | 'allocating' | 'running' | 'idle' | 'input_required' | 'completed' | 'failed' | 'cancelled';
|
|
30
|
+
/** Cloud backends whose wire status is normalized by `normalizeProviderStatus`. */
|
|
31
|
+
export type StatusNormalizingProvider = 'rush' | 'codex' | 'antigravity';
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a provider's raw wire status into the canonical `CloudTaskStatus`.
|
|
34
|
+
*
|
|
35
|
+
* Each cloud backend speaks its own status vocabulary and had its own copy of
|
|
36
|
+
* this mapping, which had drifted. This dispatches per provider while keeping
|
|
37
|
+
* provider-specific defaults explicit:
|
|
38
|
+
* - `rush` — switch over the Factory Floor's known strings; includes
|
|
39
|
+
* `allocating` and stopped/resumable `idle` states, has no
|
|
40
|
+
* `queued`, default `running`.
|
|
41
|
+
* - `codex` — substring match on the lowercased CLI status; default
|
|
42
|
+
* `running`.
|
|
43
|
+
* - `antigravity` — substring match on the (possibly `undefined`) Interactions
|
|
44
|
+
* API status; default `completed` (its synchronous response
|
|
45
|
+
* is terminal), and `undefined`-safe.
|
|
46
|
+
*
|
|
47
|
+
* Factory's `mapResultStatus` is structurally different (it maps a droid *exit*
|
|
48
|
+
* result, not a lifecycle string) and deliberately stays in `factory.ts`.
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeProviderStatus(provider: StatusNormalizingProvider, wireStatus: string | undefined): CloudTaskStatus;
|
|
30
51
|
/** Snapshot of a dispatched task, stored locally and refreshed from the provider. */
|
|
31
52
|
export interface CloudTask {
|
|
32
53
|
id: string;
|