@phnx-labs/agents-cli 1.20.51 → 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 +71 -0
- package/README.md +12 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +215 -7
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.js +8 -1
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +2 -3
- package/dist/commands/exec.js +82 -3
- package/dist/commands/factory.js +8 -0
- package/dist/commands/feed.d.ts +38 -0
- package/dist/commands/feed.js +274 -0
- package/dist/commands/logs.d.ts +5 -1
- package/dist/commands/logs.js +248 -3
- package/dist/commands/mcp.js +7 -0
- 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.d.ts +22 -0
- package/dist/commands/secrets.js +176 -42
- package/dist/commands/sessions.js +10 -1
- package/dist/commands/teams.js +4 -0
- package/dist/commands/worktree.js +4 -2
- package/dist/index.js +21 -22
- 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/browser/login-detection.d.ts +94 -0
- package/dist/lib/browser/login-detection.js +274 -0
- package/dist/lib/browser/profiles.d.ts +17 -8
- package/dist/lib/browser/profiles.js +27 -8
- package/dist/lib/browser/secret-ref.d.ts +10 -0
- package/dist/lib/browser/secret-ref.js +14 -0
- package/dist/lib/browser/service.js +14 -12
- 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.d.ts +15 -0
- package/dist/lib/cloud/rush.js +10 -16
- 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 +19 -0
- package/dist/lib/crabbox/lease.js +22 -11
- package/dist/lib/crabbox/progress.d.ts +62 -0
- package/dist/lib/crabbox/progress.js +129 -0
- package/dist/lib/crabbox/runtimes.d.ts +38 -1
- package/dist/lib/crabbox/runtimes.js +98 -5
- package/dist/lib/daemon.d.ts +12 -9
- package/dist/lib/daemon.js +32 -17
- package/dist/lib/events.d.ts +31 -5
- package/dist/lib/events.js +291 -101
- package/dist/lib/exec.js +20 -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 +180 -0
- package/dist/lib/feed.js +627 -0
- package/dist/lib/git.d.ts +17 -1
- package/dist/lib/git.js +20 -1
- package/dist/lib/hooks.js +529 -14
- package/dist/lib/hosts/passthrough.d.ts +3 -3
- package/dist/lib/hosts/passthrough.js +3 -3
- 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/rotate.js +2 -0
- 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/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +21 -0
- package/dist/lib/secrets/agent.js +63 -1
- package/dist/lib/secrets/bundles.d.ts +33 -1
- package/dist/lib/secrets/bundles.js +76 -21
- package/dist/lib/secrets/icloud-import.d.ts +79 -0
- package/dist/lib/secrets/icloud-import.js +203 -0
- package/dist/lib/secrets/index.d.ts +142 -2
- package/dist/lib/secrets/index.js +695 -30
- package/dist/lib/secrets/remote.js +1 -1
- package/dist/lib/secrets/sync.js +1 -1
- 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 +32 -2
- 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 +50 -1
- 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 +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +25 -8
- package/dist/lib/subagents.d.ts +52 -0
- package/dist/lib/subagents.js +315 -12
- package/dist/lib/teams/agents.js +6 -3
- package/dist/lib/teams/worktree.d.ts +8 -0
- package/dist/lib/teams/worktree.js +8 -0
- package/dist/lib/types.d.ts +20 -2
- package/dist/lib/versions.js +38 -48
- package/dist/lib/whats-new.d.ts +5 -3
- package/dist/lib/whats-new.js +25 -5
- 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
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort login-state detection for browser profiles.
|
|
3
|
+
*
|
|
4
|
+
* Reads a profile's Chromium cookie store (presence only — never decrypts the
|
|
5
|
+
* Keychain-encrypted values) to tell which login-gated services have a live
|
|
6
|
+
* session in that profile. Powers the `agents browser start` guardrail (warn
|
|
7
|
+
* when an agent opens a logged-out profile for a login-gated URL) and the
|
|
8
|
+
* `agents browser profiles logins` view.
|
|
9
|
+
*
|
|
10
|
+
* Everything here is advisory: any failure (missing DB, locked file, unknown
|
|
11
|
+
* schema) degrades to "no known session" and NEVER throws to callers — browser
|
|
12
|
+
* start must not slow down or break because cookie inspection hiccuped.
|
|
13
|
+
*/
|
|
14
|
+
/** A login-gated service signature: host substrings + the auth-cookie names
|
|
15
|
+
* whose presence indicates a live session, plus optional login metadata. */
|
|
16
|
+
interface ServiceSignature {
|
|
17
|
+
hosts: string[];
|
|
18
|
+
cookies: string[];
|
|
19
|
+
/**
|
|
20
|
+
* Where an interactive login for this service starts, and the bundle key
|
|
21
|
+
* PREFIX its credentials live under. By convention a profile's `--secrets`
|
|
22
|
+
* bundle holds `<PREFIX>_USERNAME` / `<PREFIX>_PASSWORD` (see
|
|
23
|
+
* `credKeysForService`). The prefix is explicit — never string-munged from the
|
|
24
|
+
* service id — so e.g. `x` → `X`, `google` → `GOOGLE`.
|
|
25
|
+
*/
|
|
26
|
+
login?: {
|
|
27
|
+
loginUrl: string;
|
|
28
|
+
keyPrefix: string;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Known login-gated services. Presence of ANY listed cookie on a matching host
|
|
33
|
+
* (unexpired, or a session cookie) = logged in. Deliberately conservative: a
|
|
34
|
+
* service absent from this map simply never triggers a warning — we do not
|
|
35
|
+
* guess. Each cookie name here is the definitive authenticated-session token for
|
|
36
|
+
* that service (e.g. LinkedIn's `li_at` is only set for a signed-in member;
|
|
37
|
+
* visitor cookies like `bcookie`/`JSESSIONID` are NOT auth). Services whose
|
|
38
|
+
* session is not reliably expressed as a recognizable cookie (e.g. Attio, whose
|
|
39
|
+
* only same-site cookies are third-party analytics) are deliberately omitted so
|
|
40
|
+
* we never emit a false "logged out" warning. Extend as needed.
|
|
41
|
+
*/
|
|
42
|
+
export declare const AUTH_SIGNATURES: Record<string, ServiceSignature>;
|
|
43
|
+
/**
|
|
44
|
+
* The bundle keys a profile's `--secrets` bundle should hold to log into
|
|
45
|
+
* `service`: `<PREFIX>_USERNAME` / `<PREFIX>_PASSWORD`. Null for services with no
|
|
46
|
+
* login metadata. Feed a resolved value to the page with
|
|
47
|
+
* `agents browser type <ref> --secret <bundle>/<KEY>` (never printed).
|
|
48
|
+
*/
|
|
49
|
+
export declare function credKeysForService(service: string): {
|
|
50
|
+
user: string;
|
|
51
|
+
pass: string;
|
|
52
|
+
} | null;
|
|
53
|
+
/** The interactive login URL for a service, or null. */
|
|
54
|
+
export declare function loginUrlForService(service: string): string | null;
|
|
55
|
+
export interface CookieRow {
|
|
56
|
+
host_key: string;
|
|
57
|
+
name: string;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Pure core: given a profile's (already expiry-filtered) cookie rows, return the
|
|
61
|
+
* services with a live session. Exported for direct unit testing without disk.
|
|
62
|
+
*/
|
|
63
|
+
export declare function detectServices(rows: CookieRow[]): string[];
|
|
64
|
+
/** Map a URL to a known login-gated service key, or null. */
|
|
65
|
+
export declare function serviceForUrl(url: string): string | null;
|
|
66
|
+
/** A saved-login row from Chromium `Login Data` (username plaintext; password
|
|
67
|
+
* encrypted and never read). */
|
|
68
|
+
export interface LoginRow {
|
|
69
|
+
origin_url: string;
|
|
70
|
+
username_value: string;
|
|
71
|
+
signon_realm: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Map a profile's saved logins to service → account username (identity, not
|
|
75
|
+
* session). Best-effort; `{}` if `Login Data` is unreadable. The first non-empty
|
|
76
|
+
* username per service wins.
|
|
77
|
+
*/
|
|
78
|
+
export declare function accountsForProfile(profileName: string): Promise<Record<string, string>>;
|
|
79
|
+
/**
|
|
80
|
+
* For each service with a LIVE session in the profile (cookie-gated), the
|
|
81
|
+
* account it is signed in as (from saved logins). Intersecting with live
|
|
82
|
+
* sessions means a stale saved-login for a service you've since logged out of
|
|
83
|
+
* never shows.
|
|
84
|
+
*/
|
|
85
|
+
export declare function loginsWithAccountsForProfile(profileName: string): Promise<Array<{
|
|
86
|
+
service: string;
|
|
87
|
+
username?: string;
|
|
88
|
+
}>>;
|
|
89
|
+
/** Services with a live session in the given profile (best-effort; [] if none
|
|
90
|
+
* detected or the cookie store is unreadable). */
|
|
91
|
+
export declare function loginsForProfile(profileName: string): Promise<string[]>;
|
|
92
|
+
/** Profile names that have a live session for the given service. */
|
|
93
|
+
export declare function profilesLoggedInto(service: string): Promise<string[]>;
|
|
94
|
+
export {};
|