@zeph-to/cli 1.13.2 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-rules-fetch.d.ts +33 -0
- package/dist/agent-rules-fetch.d.ts.map +1 -0
- package/dist/agent-rules-fetch.js +190 -0
- package/dist/agent-rules.default.d.ts +25 -0
- package/dist/agent-rules.default.d.ts.map +1 -0
- package/dist/agent-rules.default.js +89 -0
- package/dist/agent-state.d.ts +99 -0
- package/dist/agent-state.d.ts.map +1 -0
- package/dist/agent-state.js +140 -0
- package/dist/cli.js +41 -25
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -1
- package/dist/gate.d.ts +36 -0
- package/dist/gate.d.ts.map +1 -0
- package/dist/gate.js +88 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/listener.d.ts +87 -13
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +216 -93
- package/dist/remote-agents.d.ts +55 -0
- package/dist/remote-agents.d.ts.map +1 -0
- package/dist/remote-agents.js +115 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +27 -150
- package/dist/wrapper.d.ts +2 -1
- package/dist/wrapper.d.ts.map +1 -1
- package/dist/wrapper.js +4 -5
- package/dist/zeph-core.generated.d.ts +7 -0
- package/dist/zeph-core.generated.d.ts.map +1 -0
- package/dist/zeph-core.generated.js +14 -0
- package/package.json +2 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type DetectionManifest } from './agent-state.js';
|
|
2
|
+
export declare const RULES_CACHE_FILE: string;
|
|
3
|
+
export declare const RULES_REFRESH_INTERVAL_MS: number;
|
|
4
|
+
/**
|
|
5
|
+
* Structural validation of an untrusted manifest. Engine-version gate
|
|
6
|
+
* included: a manifest authored for a future engine is rejected whole —
|
|
7
|
+
* partial interpretation of unknown semantics is worse than falling
|
|
8
|
+
* back to bundled rules.
|
|
9
|
+
*/
|
|
10
|
+
export declare const validateManifest: (value: unknown) => DetectionManifest | null;
|
|
11
|
+
export type ManifestSource = 'remote' | 'cache' | 'bundled';
|
|
12
|
+
export declare const getActiveManifest: () => DetectionManifest;
|
|
13
|
+
export declare const getActiveManifestSource: () => ManifestSource;
|
|
14
|
+
/** Test hook. */
|
|
15
|
+
export declare const resetActiveManifest: () => void;
|
|
16
|
+
/**
|
|
17
|
+
* Startup path (synchronous): promote the disk cache if it validates,
|
|
18
|
+
* else stay on bundled. Never throws — detection must work offline.
|
|
19
|
+
*/
|
|
20
|
+
export declare const loadManifestFromCache: () => ManifestSource;
|
|
21
|
+
export declare const rulesUrl: () => string;
|
|
22
|
+
export interface RefreshResult {
|
|
23
|
+
source: ManifestSource;
|
|
24
|
+
/** 'updated' | 'not-modified' | 'invalid' | 'error' — for verbose logs. */
|
|
25
|
+
outcome: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One refresh attempt: conditional GET, validate, persist, activate.
|
|
30
|
+
* All failure modes degrade to the current active manifest.
|
|
31
|
+
*/
|
|
32
|
+
export declare const refreshManifest: (fetchImpl?: typeof fetch) => Promise<RefreshResult>;
|
|
33
|
+
//# sourceMappingURL=agent-rules-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-rules-fetch.d.ts","sourceRoot":"","sources":["../src/agent-rules-fetch.ts"],"names":[],"mappings":"AAeA,OAAO,EAEH,KAAK,iBAAiB,EACzB,MAAM,kBAAkB,CAAC;AAG1B,eAAO,MAAM,gBAAgB,QAAuC,CAAC;AACrE,eAAO,MAAM,yBAAyB,QAAqB,CAAC;AA6C5D;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAAI,OAAO,OAAO,KAAG,iBAAiB,GAAG,IAWrE,CAAC;AAIF,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;AAK5D,eAAO,MAAM,iBAAiB,QAAO,iBAAmC,CAAC;AACzE,eAAO,MAAM,uBAAuB,QAAO,cAA8B,CAAC;AAS1E,iBAAiB;AACjB,eAAO,MAAM,mBAAmB,QAAO,IAEtC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAO,cAWxC,CAAC;AAWF,eAAO,MAAM,QAAQ,QAAO,MAK3B,CAAC;AAEF,MAAM,WAAW,aAAa;IAC1B,MAAM,EAAE,cAAc,CAAC;IACvB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAU,YAAW,OAAO,KAAa,KAAG,OAAO,CAAC,aAAa,CAsC5F,CAAC"}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.refreshManifest = exports.rulesUrl = exports.loadManifestFromCache = exports.resetActiveManifest = exports.getActiveManifestSource = exports.getActiveManifest = exports.validateManifest = exports.RULES_REFRESH_INTERVAL_MS = exports.RULES_CACHE_FILE = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* OTA delivery for agent detection rules (SPEC-AGENT-AWARENESS §S7).
|
|
6
|
+
*
|
|
7
|
+
* Agent UIs change on their release cadence, not ours: when Claude
|
|
8
|
+
* Code reshapes its status line, detection must be fixable by shipping
|
|
9
|
+
* DATA, not a new daemon. The listener therefore resolves its manifest
|
|
10
|
+
* as: valid remote fetch → valid disk cache → bundled defaults. Every
|
|
11
|
+
* tier fails closed to the next — a dead endpoint, corrupt cache, or
|
|
12
|
+
* hostile payload can never leave the listener without rules, and
|
|
13
|
+
* `disabledRuleIds` in a fetched manifest acts as a same-day
|
|
14
|
+
* kill-switch for a misfiring rule (no release, no restart).
|
|
15
|
+
*/
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
const config_js_1 = require("./config.js");
|
|
19
|
+
const agent_state_js_1 = require("./agent-state.js");
|
|
20
|
+
const agent_rules_default_js_1 = require("./agent-rules.default.js");
|
|
21
|
+
exports.RULES_CACHE_FILE = (0, path_1.join)(config_js_1.CONFIG_DIR, 'agent-rules.json');
|
|
22
|
+
exports.RULES_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
23
|
+
// Fallback only — the manifest path normally derives from the resolved
|
|
24
|
+
// API base so the stage prefix (prod /v1, dev /d1) follows the user's
|
|
25
|
+
// config instead of being hardcoded here.
|
|
26
|
+
const DEFAULT_API_BASE = 'https://api.zeph.to/v1';
|
|
27
|
+
// Matches the server-side serving cap; anything bigger is not a manifest.
|
|
28
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
29
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
30
|
+
const VALID_STATES = new Set(['working', 'blocked', 'idle', 'unknown']);
|
|
31
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
|
|
32
|
+
const isCondition = (v) => {
|
|
33
|
+
if (typeof v !== 'object' || v === null)
|
|
34
|
+
return false;
|
|
35
|
+
const c = v;
|
|
36
|
+
if (c.contains !== undefined && !isStringArray(c.contains))
|
|
37
|
+
return false;
|
|
38
|
+
if (c.regex !== undefined && !isStringArray(c.regex))
|
|
39
|
+
return false;
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
const isRule = (v) => {
|
|
43
|
+
if (typeof v !== 'object' || v === null)
|
|
44
|
+
return false;
|
|
45
|
+
const r = v;
|
|
46
|
+
if (typeof r.id !== 'string' || r.id.length === 0)
|
|
47
|
+
return false;
|
|
48
|
+
if (typeof r.state !== 'string' || !VALID_STATES.has(r.state))
|
|
49
|
+
return false;
|
|
50
|
+
if (typeof r.priority !== 'number' || !Number.isFinite(r.priority))
|
|
51
|
+
return false;
|
|
52
|
+
if (r.region !== undefined && r.region !== 'tail' && r.region !== 'whole')
|
|
53
|
+
return false;
|
|
54
|
+
if (r.tailLines !== undefined && (typeof r.tailLines !== 'number' || r.tailLines < 1 || r.tailLines > 200))
|
|
55
|
+
return false;
|
|
56
|
+
if (r.contains !== undefined && !isStringArray(r.contains))
|
|
57
|
+
return false;
|
|
58
|
+
if (r.regex !== undefined && !isStringArray(r.regex))
|
|
59
|
+
return false;
|
|
60
|
+
if (r.any !== undefined && (!Array.isArray(r.any) || !r.any.every(isCondition)))
|
|
61
|
+
return false;
|
|
62
|
+
if (r.not !== undefined && (!Array.isArray(r.not) || !r.not.every(isCondition)))
|
|
63
|
+
return false;
|
|
64
|
+
if (r.skipStateUpdate !== undefined && typeof r.skipStateUpdate !== 'boolean')
|
|
65
|
+
return false;
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Structural validation of an untrusted manifest. Engine-version gate
|
|
70
|
+
* included: a manifest authored for a future engine is rejected whole —
|
|
71
|
+
* partial interpretation of unknown semantics is worse than falling
|
|
72
|
+
* back to bundled rules.
|
|
73
|
+
*/
|
|
74
|
+
const validateManifest = (value) => {
|
|
75
|
+
if (typeof value !== 'object' || value === null)
|
|
76
|
+
return null;
|
|
77
|
+
const m = value;
|
|
78
|
+
if (m.engineVersion !== agent_state_js_1.ENGINE_VERSION)
|
|
79
|
+
return null;
|
|
80
|
+
if (typeof m.version !== 'string' || m.version.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
if (m.disabledRuleIds !== undefined && !isStringArray(m.disabledRuleIds))
|
|
83
|
+
return null;
|
|
84
|
+
if (typeof m.agents !== 'object' || m.agents === null)
|
|
85
|
+
return null;
|
|
86
|
+
for (const rules of Object.values(m.agents)) {
|
|
87
|
+
if (!Array.isArray(rules) || !rules.every(isRule))
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
};
|
|
92
|
+
exports.validateManifest = validateManifest;
|
|
93
|
+
let activeManifest = agent_rules_default_js_1.DEFAULT_MANIFEST;
|
|
94
|
+
let activeSource = 'bundled';
|
|
95
|
+
const getActiveManifest = () => activeManifest;
|
|
96
|
+
exports.getActiveManifest = getActiveManifest;
|
|
97
|
+
const getActiveManifestSource = () => activeSource;
|
|
98
|
+
exports.getActiveManifestSource = getActiveManifestSource;
|
|
99
|
+
const activateManifest = (manifest, source) => {
|
|
100
|
+
activeManifest = manifest;
|
|
101
|
+
activeSource = source;
|
|
102
|
+
// Old manifest's compiled patterns must not pin memory forever.
|
|
103
|
+
(0, agent_state_js_1.clearRegexCache)();
|
|
104
|
+
};
|
|
105
|
+
/** Test hook. */
|
|
106
|
+
const resetActiveManifest = () => {
|
|
107
|
+
activateManifest(agent_rules_default_js_1.DEFAULT_MANIFEST, 'bundled');
|
|
108
|
+
};
|
|
109
|
+
exports.resetActiveManifest = resetActiveManifest;
|
|
110
|
+
/**
|
|
111
|
+
* Startup path (synchronous): promote the disk cache if it validates,
|
|
112
|
+
* else stay on bundled. Never throws — detection must work offline.
|
|
113
|
+
*/
|
|
114
|
+
const loadManifestFromCache = () => {
|
|
115
|
+
try {
|
|
116
|
+
const cache = JSON.parse((0, fs_1.readFileSync)(exports.RULES_CACHE_FILE, 'utf-8'));
|
|
117
|
+
const manifest = (0, exports.validateManifest)(cache.manifest);
|
|
118
|
+
if (manifest) {
|
|
119
|
+
activateManifest(manifest, 'cache');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Missing/corrupt cache — bundled rules carry the session.
|
|
124
|
+
}
|
|
125
|
+
return activeSource;
|
|
126
|
+
};
|
|
127
|
+
exports.loadManifestFromCache = loadManifestFromCache;
|
|
128
|
+
const readCachedEtag = () => {
|
|
129
|
+
try {
|
|
130
|
+
const cache = JSON.parse((0, fs_1.readFileSync)(exports.RULES_CACHE_FILE, 'utf-8'));
|
|
131
|
+
return typeof cache.etag === 'string' ? cache.etag : undefined;
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const rulesUrl = () => {
|
|
138
|
+
const override = (0, config_js_1.resolvedEnv)('ZEPH_AGENT_RULES_URL');
|
|
139
|
+
if (override)
|
|
140
|
+
return override;
|
|
141
|
+
const base = (0, config_js_1.resolvedEnv)('ZEPH_BASE_URL') ?? (0, config_js_1.loadConfig)().baseUrl ?? DEFAULT_API_BASE;
|
|
142
|
+
return `${base.replace(/\/$/, '')}/agent-detection/manifest`;
|
|
143
|
+
};
|
|
144
|
+
exports.rulesUrl = rulesUrl;
|
|
145
|
+
/**
|
|
146
|
+
* One refresh attempt: conditional GET, validate, persist, activate.
|
|
147
|
+
* All failure modes degrade to the current active manifest.
|
|
148
|
+
*/
|
|
149
|
+
const refreshManifest = async (fetchImpl = fetch) => {
|
|
150
|
+
try {
|
|
151
|
+
const etag = readCachedEtag();
|
|
152
|
+
const res = await fetchImpl((0, exports.rulesUrl)(), {
|
|
153
|
+
headers: etag ? { 'If-None-Match': etag } : {},
|
|
154
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
155
|
+
});
|
|
156
|
+
if (res.status === 304) {
|
|
157
|
+
return { source: activeSource, outcome: 'not-modified', version: activeManifest.version };
|
|
158
|
+
}
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
return { source: activeSource, outcome: 'error' };
|
|
161
|
+
}
|
|
162
|
+
const body = await res.text();
|
|
163
|
+
if (Buffer.byteLength(body, 'utf-8') > MAX_MANIFEST_BYTES) {
|
|
164
|
+
return { source: activeSource, outcome: 'invalid' };
|
|
165
|
+
}
|
|
166
|
+
const manifest = (0, exports.validateManifest)(JSON.parse(body));
|
|
167
|
+
if (!manifest) {
|
|
168
|
+
return { source: activeSource, outcome: 'invalid' };
|
|
169
|
+
}
|
|
170
|
+
const cache = {
|
|
171
|
+
etag: res.headers.get('etag') ?? undefined,
|
|
172
|
+
fetchedAt: new Date().toISOString(),
|
|
173
|
+
manifest,
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
(0, fs_1.mkdirSync)(config_js_1.CONFIG_DIR, { recursive: true });
|
|
177
|
+
(0, fs_1.writeFileSync)(exports.RULES_CACHE_FILE, JSON.stringify(cache, null, 2) + '\n');
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Cache write failure is non-fatal: the manifest still
|
|
181
|
+
// activates for this process lifetime.
|
|
182
|
+
}
|
|
183
|
+
activateManifest(manifest, 'remote');
|
|
184
|
+
return { source: 'remote', outcome: 'updated', version: manifest.version };
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return { source: activeSource, outcome: 'error' };
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
exports.refreshManifest = refreshManifest;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled default detection rules — the fallback when no OTA manifest
|
|
3
|
+
* has been fetched (or the fetched one fails validation, §S7).
|
|
4
|
+
*
|
|
5
|
+
* Rule content is authored HERE, from observation of each agent's UI.
|
|
6
|
+
* Do not import or transcribe rules from third-party projects: the
|
|
7
|
+
* bundled set is original work under this repo's license.
|
|
8
|
+
*
|
|
9
|
+
* Claude Code observations behind these rules (UI as of mid-2026):
|
|
10
|
+
* - While running, the status line shows an "esc to interrupt" hint
|
|
11
|
+
* next to the spinner/progress text.
|
|
12
|
+
* - Blocking dialogs (permission requests, plan approval, question
|
|
13
|
+
* forms) render a numbered/arrow-key option list with "esc" as the
|
|
14
|
+
* cancel affordance and Enter as the confirm affordance.
|
|
15
|
+
* - At rest, the input box renders a `❯` prompt and the footer offers
|
|
16
|
+
* "? for shortcuts".
|
|
17
|
+
* - The transcript viewer (ctrl+o) overlays the pane; the agent may
|
|
18
|
+
* still be working underneath, so it must not change the state.
|
|
19
|
+
*
|
|
20
|
+
* codex/gemini start with no rules: they report `unknown` until a
|
|
21
|
+
* vetted rule set ships via OTA. Honest ignorance beats guessed state.
|
|
22
|
+
*/
|
|
23
|
+
import { type DetectionManifest } from './agent-state.js';
|
|
24
|
+
export declare const DEFAULT_MANIFEST: DetectionManifest;
|
|
25
|
+
//# sourceMappingURL=agent-rules.default.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-rules.default.d.ts","sourceRoot":"","sources":["../src/agent-rules.default.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAkB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1E,eAAO,MAAM,gBAAgB,EAAE,iBA8D9B,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_MANIFEST = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Bundled default detection rules — the fallback when no OTA manifest
|
|
6
|
+
* has been fetched (or the fetched one fails validation, §S7).
|
|
7
|
+
*
|
|
8
|
+
* Rule content is authored HERE, from observation of each agent's UI.
|
|
9
|
+
* Do not import or transcribe rules from third-party projects: the
|
|
10
|
+
* bundled set is original work under this repo's license.
|
|
11
|
+
*
|
|
12
|
+
* Claude Code observations behind these rules (UI as of mid-2026):
|
|
13
|
+
* - While running, the status line shows an "esc to interrupt" hint
|
|
14
|
+
* next to the spinner/progress text.
|
|
15
|
+
* - Blocking dialogs (permission requests, plan approval, question
|
|
16
|
+
* forms) render a numbered/arrow-key option list with "esc" as the
|
|
17
|
+
* cancel affordance and Enter as the confirm affordance.
|
|
18
|
+
* - At rest, the input box renders a `❯` prompt and the footer offers
|
|
19
|
+
* "? for shortcuts".
|
|
20
|
+
* - The transcript viewer (ctrl+o) overlays the pane; the agent may
|
|
21
|
+
* still be working underneath, so it must not change the state.
|
|
22
|
+
*
|
|
23
|
+
* codex/gemini start with no rules: they report `unknown` until a
|
|
24
|
+
* vetted rule set ships via OTA. Honest ignorance beats guessed state.
|
|
25
|
+
*/
|
|
26
|
+
const agent_state_js_1 = require("./agent-state.js");
|
|
27
|
+
exports.DEFAULT_MANIFEST = {
|
|
28
|
+
engineVersion: agent_state_js_1.ENGINE_VERSION,
|
|
29
|
+
version: '2026.07.04.1',
|
|
30
|
+
agents: {
|
|
31
|
+
claude: [
|
|
32
|
+
{
|
|
33
|
+
// Transcript / verbose-output overlay: freeze state.
|
|
34
|
+
id: 'claude-transcript-overlay',
|
|
35
|
+
state: 'unknown',
|
|
36
|
+
priority: 1000,
|
|
37
|
+
skipStateUpdate: true,
|
|
38
|
+
any: [
|
|
39
|
+
{ contains: ['showing detailed transcript'] },
|
|
40
|
+
{ contains: ['ctrl+o to toggle'] },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// Blocking dialog: an option list waiting on the user.
|
|
45
|
+
// "esc" alone is ambiguous (working shows "esc to
|
|
46
|
+
// interrupt"), so require a selection affordance too.
|
|
47
|
+
id: 'claude-blocked-dialog',
|
|
48
|
+
state: 'blocked',
|
|
49
|
+
priority: 900,
|
|
50
|
+
contains: ['esc'],
|
|
51
|
+
any: [
|
|
52
|
+
{ contains: ['do you want'] },
|
|
53
|
+
{ contains: ['enter to select'] },
|
|
54
|
+
{ contains: ['enter to confirm'] },
|
|
55
|
+
{ contains: ['to navigate'] },
|
|
56
|
+
],
|
|
57
|
+
not: [{ contains: ['esc to interrupt'] }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'claude-working-interrupt-hint',
|
|
61
|
+
state: 'working',
|
|
62
|
+
priority: 800,
|
|
63
|
+
contains: ['esc to interrupt'],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// Idle prompt: `❯` at line start in the tail, with no
|
|
67
|
+
// dialog affordances around it.
|
|
68
|
+
id: 'claude-idle-prompt',
|
|
69
|
+
state: 'idle',
|
|
70
|
+
priority: 600,
|
|
71
|
+
regex: ['^\\s*❯'],
|
|
72
|
+
not: [
|
|
73
|
+
{ contains: ['do you want'] },
|
|
74
|
+
{ contains: ['enter to select'] },
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
// Fallback idle signal when the prompt glyph is themed
|
|
79
|
+
// away: the shortcuts footer only renders at rest.
|
|
80
|
+
id: 'claude-idle-shortcuts-footer',
|
|
81
|
+
state: 'idle',
|
|
82
|
+
priority: 500,
|
|
83
|
+
contains: ['? for shortcuts'],
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
codex: [],
|
|
87
|
+
gemini: [],
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent state detection engine — classifies a tmux pane's visible text
|
|
3
|
+
* into `working | blocked | idle | unknown` using declarative rules.
|
|
4
|
+
*
|
|
5
|
+
* Design constraints (see zeph/docs/SPEC-AGENT-AWARENESS.md §S1):
|
|
6
|
+
* - Rules are DATA, not code: they ship bundled but are replaceable
|
|
7
|
+
* over the air (§S7), so agent UI changes never require a daemon
|
|
8
|
+
* release. Everything here must therefore survive hostile or
|
|
9
|
+
* malformed manifests: pattern-length caps, input-size caps, and
|
|
10
|
+
* per-rule isolation of regex compile failures.
|
|
11
|
+
* - Pure functions only. The listener owns timing, tmux, and I/O;
|
|
12
|
+
* this module owns classification and flap suppression so both are
|
|
13
|
+
* unit-testable without a terminal.
|
|
14
|
+
* - `done` is deliberately NOT a wire state: "finished but unseen" is
|
|
15
|
+
* per-user view state, derived client-side. The daemon reports only
|
|
16
|
+
* what it can observe.
|
|
17
|
+
*/
|
|
18
|
+
import type { AgentKind } from './remote-agents.js';
|
|
19
|
+
export type AgentState = 'working' | 'blocked' | 'idle' | 'unknown';
|
|
20
|
+
export interface RuleCondition {
|
|
21
|
+
/** All strings must appear (case-insensitive). */
|
|
22
|
+
contains?: string[];
|
|
23
|
+
/** All patterns must match. */
|
|
24
|
+
regex?: string[];
|
|
25
|
+
}
|
|
26
|
+
export interface DetectionRule {
|
|
27
|
+
id: string;
|
|
28
|
+
state: AgentState;
|
|
29
|
+
/** Higher wins; first match ends evaluation. */
|
|
30
|
+
priority: number;
|
|
31
|
+
/** 'tail' = last N non-empty lines (default), 'whole' = full capture. */
|
|
32
|
+
region?: 'tail' | 'whole';
|
|
33
|
+
/** Lines for region 'tail'. Default 10. */
|
|
34
|
+
tailLines?: number;
|
|
35
|
+
contains?: string[];
|
|
36
|
+
regex?: string[];
|
|
37
|
+
/** At least one condition group must match (OR). */
|
|
38
|
+
any?: RuleCondition[];
|
|
39
|
+
/** No condition group may match (exclusion). */
|
|
40
|
+
not?: RuleCondition[];
|
|
41
|
+
/**
|
|
42
|
+
* On match, keep the previous state instead of this rule's state.
|
|
43
|
+
* For overlay screens (transcript viewer, menus) that would
|
|
44
|
+
* otherwise pollute the state while the agent is still working.
|
|
45
|
+
*/
|
|
46
|
+
skipStateUpdate?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface DetectionManifest {
|
|
49
|
+
engineVersion: number;
|
|
50
|
+
/** Date.revision, e.g. "2026.07.04.1" — for OTA freshness compare. */
|
|
51
|
+
version: string;
|
|
52
|
+
/** Kill-switch: rule ids to ignore without shipping a new manifest shape. */
|
|
53
|
+
disabledRuleIds?: string[];
|
|
54
|
+
agents: Partial<Record<AgentKind, DetectionRule[]>>;
|
|
55
|
+
}
|
|
56
|
+
export declare const ENGINE_VERSION = 1;
|
|
57
|
+
export interface EvaluationResult {
|
|
58
|
+
state: AgentState;
|
|
59
|
+
/** Rule that decided the state — for verbose logs and rule debugging. */
|
|
60
|
+
ruleId?: string;
|
|
61
|
+
}
|
|
62
|
+
/** Test hook: manifest swaps call this so stale patterns don't pin memory. */
|
|
63
|
+
export declare const clearRegexCache: () => void;
|
|
64
|
+
/**
|
|
65
|
+
* Classify one pane capture. `prev` feeds skipStateUpdate rules — an
|
|
66
|
+
* overlay match returns the previous confirmed state unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export declare const evaluateState: (paneText: string, agentKind: AgentKind, manifest: DetectionManifest, prev?: AgentState) => EvaluationResult;
|
|
69
|
+
/**
|
|
70
|
+
* Safe one-pattern probe for output-match watches (§S5 v2). Same caps
|
|
71
|
+
* as rule evaluation — user-authored watch patterns are exactly as
|
|
72
|
+
* untrusted as OTA rules. Returns the matched line for the push body.
|
|
73
|
+
*/
|
|
74
|
+
export declare const findPatternMatch: (pattern: string, paneText: string) => {
|
|
75
|
+
line: string;
|
|
76
|
+
} | null;
|
|
77
|
+
export interface StateTracker {
|
|
78
|
+
/** Last CONFIRMED state — what gets reported to the server. */
|
|
79
|
+
confirmed: AgentState;
|
|
80
|
+
confirmedAt: number;
|
|
81
|
+
ruleId?: string;
|
|
82
|
+
/** Pending different observation awaiting its second sighting. */
|
|
83
|
+
candidate?: AgentState;
|
|
84
|
+
candidateRuleId?: string;
|
|
85
|
+
/** Hash of the pane text behind the last evaluation (skip re-eval). */
|
|
86
|
+
contentHash?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Consecutive-confirmation debounce: a NEW state must be observed on
|
|
90
|
+
* two consecutive cycles (~10 s at the 5 s report interval) before it
|
|
91
|
+
* replaces the confirmed one. Menus flashed open, mid-render frames,
|
|
92
|
+
* and scroll artifacts all last one cycle and die as candidates.
|
|
93
|
+
*
|
|
94
|
+
* The very first observation confirms immediately — a fresh tracker
|
|
95
|
+
* has no baseline to protect, and the server treats a session's first
|
|
96
|
+
* reported state as baseline, not as a transition (§S2).
|
|
97
|
+
*/
|
|
98
|
+
export declare const advanceState: (tracker: StateTracker | undefined, observed: EvaluationResult, now: number) => StateTracker;
|
|
99
|
+
//# sourceMappingURL=agent-state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-state.d.ts","sourceRoot":"","sources":["../src/agent-state.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEpD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAEpE,MAAM,WAAW,aAAa;IAC1B,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,+BAA+B;IAC/B,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,UAAU,CAAC;IAClB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC1B,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,oDAAoD;IACpD,GAAG,CAAC,EAAE,aAAa,EAAE,CAAC;IACtB,gDAAgD;IAChD,GAAG,CAAC,EAAE,aAAa,EAAE,CAAC;IACtB;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAC9B,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,aAAa,EAAE,CAAC,CAAC,CAAC;CACvD;AAED,eAAO,MAAM,cAAc,IAAI,CAAC;AAUhC,MAAM,WAAW,gBAAgB;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAqBD,8EAA8E;AAC9E,eAAO,MAAM,eAAe,QAAO,IAElC,CAAC;AA8BF;;;GAGG;AACH,eAAO,MAAM,aAAa,GACtB,UAAU,MAAM,EAChB,WAAW,SAAS,EACpB,UAAU,iBAAiB,EAC3B,OAAM,UAAsB,KAC7B,gBAuBF,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAavF,CAAC;AAIF,MAAM,WAAW,YAAY;IACzB,+DAA+D;IAC/D,SAAS,EAAE,UAAU,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,SAAS,CAAC,EAAE,UAAU,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,GACrB,SAAS,YAAY,GAAG,SAAS,EACjC,UAAU,gBAAgB,EAC1B,KAAK,MAAM,KACZ,YAaF,CAAC"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.advanceState = exports.findPatternMatch = exports.evaluateState = exports.clearRegexCache = exports.ENGINE_VERSION = void 0;
|
|
4
|
+
exports.ENGINE_VERSION = 1;
|
|
5
|
+
// OTA manifests are semi-trusted: schema-validated but authored by
|
|
6
|
+
// humans and fetched over the network. Caps bound the worst case of a
|
|
7
|
+
// pathological regex (ReDoS) — a 200-char pattern over ≤8 KiB input
|
|
8
|
+
// keeps even catastrophic backtracking in the low milliseconds.
|
|
9
|
+
const MAX_PATTERN_LENGTH = 200;
|
|
10
|
+
const MAX_INPUT_BYTES = 8 * 1024;
|
|
11
|
+
const DEFAULT_TAIL_LINES = 10;
|
|
12
|
+
// Compiled-regex cache. Rules are static between manifest swaps, so
|
|
13
|
+
// compiling once per (pattern) is enough; a failed compile is cached as
|
|
14
|
+
// null so a bad OTA pattern logs once, not every 5-second cycle.
|
|
15
|
+
const regexCache = new Map();
|
|
16
|
+
const compilePattern = (pattern) => {
|
|
17
|
+
if (regexCache.has(pattern))
|
|
18
|
+
return regexCache.get(pattern) ?? null;
|
|
19
|
+
let compiled = null;
|
|
20
|
+
if (pattern.length <= MAX_PATTERN_LENGTH) {
|
|
21
|
+
try {
|
|
22
|
+
compiled = new RegExp(pattern, 'im');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
compiled = null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
regexCache.set(pattern, compiled);
|
|
29
|
+
return compiled;
|
|
30
|
+
};
|
|
31
|
+
/** Test hook: manifest swaps call this so stale patterns don't pin memory. */
|
|
32
|
+
const clearRegexCache = () => {
|
|
33
|
+
regexCache.clear();
|
|
34
|
+
};
|
|
35
|
+
exports.clearRegexCache = clearRegexCache;
|
|
36
|
+
const conditionMatches = (cond, text, lowerText) => {
|
|
37
|
+
for (const needle of cond.contains ?? []) {
|
|
38
|
+
if (!lowerText.includes(needle.toLowerCase()))
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
for (const pattern of cond.regex ?? []) {
|
|
42
|
+
const compiled = compilePattern(pattern);
|
|
43
|
+
// A pattern that failed to compile can never be satisfied —
|
|
44
|
+
// fail the condition rather than silently passing it, so a
|
|
45
|
+
// broken OTA rule becomes inert instead of over-matching.
|
|
46
|
+
if (!compiled || !compiled.test(text))
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
};
|
|
51
|
+
const ruleMatches = (rule, text, lowerText) => {
|
|
52
|
+
if (!conditionMatches({ contains: rule.contains, regex: rule.regex }, text, lowerText))
|
|
53
|
+
return false;
|
|
54
|
+
if (rule.any && rule.any.length > 0) {
|
|
55
|
+
if (!rule.any.some((c) => conditionMatches(c, text, lowerText)))
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
for (const excluded of rule.not ?? []) {
|
|
59
|
+
if (conditionMatches(excluded, text, lowerText))
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
};
|
|
64
|
+
const tailOf = (lines, count) => lines.filter((l) => l.trim().length > 0).slice(-count).join('\n');
|
|
65
|
+
/**
|
|
66
|
+
* Classify one pane capture. `prev` feeds skipStateUpdate rules — an
|
|
67
|
+
* overlay match returns the previous confirmed state unchanged.
|
|
68
|
+
*/
|
|
69
|
+
const evaluateState = (paneText, agentKind, manifest, prev = 'unknown') => {
|
|
70
|
+
// Truncate from the FRONT: the bottom of the pane is where every
|
|
71
|
+
// agent renders its live status, so the tail is the signal.
|
|
72
|
+
let text = paneText;
|
|
73
|
+
if (Buffer.byteLength(text, 'utf-8') > MAX_INPUT_BYTES) {
|
|
74
|
+
text = text.slice(-MAX_INPUT_BYTES);
|
|
75
|
+
}
|
|
76
|
+
const lines = text.split('\n');
|
|
77
|
+
const disabled = new Set(manifest.disabledRuleIds ?? []);
|
|
78
|
+
const rules = (manifest.agents[agentKind] ?? [])
|
|
79
|
+
.filter((r) => !disabled.has(r.id))
|
|
80
|
+
.sort((a, b) => b.priority - a.priority);
|
|
81
|
+
for (const rule of rules) {
|
|
82
|
+
const scope = rule.region === 'whole'
|
|
83
|
+
? text
|
|
84
|
+
: tailOf(lines, rule.tailLines ?? DEFAULT_TAIL_LINES);
|
|
85
|
+
if (ruleMatches(rule, scope, scope.toLowerCase())) {
|
|
86
|
+
if (rule.skipStateUpdate)
|
|
87
|
+
return { state: prev, ruleId: rule.id };
|
|
88
|
+
return { state: rule.state, ruleId: rule.id };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { state: 'unknown' };
|
|
92
|
+
};
|
|
93
|
+
exports.evaluateState = evaluateState;
|
|
94
|
+
/**
|
|
95
|
+
* Safe one-pattern probe for output-match watches (§S5 v2). Same caps
|
|
96
|
+
* as rule evaluation — user-authored watch patterns are exactly as
|
|
97
|
+
* untrusted as OTA rules. Returns the matched line for the push body.
|
|
98
|
+
*/
|
|
99
|
+
const findPatternMatch = (pattern, paneText) => {
|
|
100
|
+
let text = paneText;
|
|
101
|
+
if (Buffer.byteLength(text, 'utf-8') > MAX_INPUT_BYTES) {
|
|
102
|
+
text = text.slice(-MAX_INPUT_BYTES);
|
|
103
|
+
}
|
|
104
|
+
const compiled = compilePattern(pattern);
|
|
105
|
+
if (!compiled)
|
|
106
|
+
return null;
|
|
107
|
+
const match = compiled.exec(text);
|
|
108
|
+
if (!match)
|
|
109
|
+
return null;
|
|
110
|
+
const start = text.lastIndexOf('\n', match.index) + 1;
|
|
111
|
+
const endIdx = text.indexOf('\n', match.index);
|
|
112
|
+
const line = text.slice(start, endIdx === -1 ? undefined : endIdx).trim();
|
|
113
|
+
return { line };
|
|
114
|
+
};
|
|
115
|
+
exports.findPatternMatch = findPatternMatch;
|
|
116
|
+
/**
|
|
117
|
+
* Consecutive-confirmation debounce: a NEW state must be observed on
|
|
118
|
+
* two consecutive cycles (~10 s at the 5 s report interval) before it
|
|
119
|
+
* replaces the confirmed one. Menus flashed open, mid-render frames,
|
|
120
|
+
* and scroll artifacts all last one cycle and die as candidates.
|
|
121
|
+
*
|
|
122
|
+
* The very first observation confirms immediately — a fresh tracker
|
|
123
|
+
* has no baseline to protect, and the server treats a session's first
|
|
124
|
+
* reported state as baseline, not as a transition (§S2).
|
|
125
|
+
*/
|
|
126
|
+
const advanceState = (tracker, observed, now) => {
|
|
127
|
+
if (!tracker) {
|
|
128
|
+
return { confirmed: observed.state, confirmedAt: now, ruleId: observed.ruleId };
|
|
129
|
+
}
|
|
130
|
+
if (observed.state === tracker.confirmed) {
|
|
131
|
+
// Re-confirmation clears any pending candidate.
|
|
132
|
+
return { ...tracker, candidate: undefined, candidateRuleId: undefined, ruleId: observed.ruleId ?? tracker.ruleId };
|
|
133
|
+
}
|
|
134
|
+
if (observed.state === tracker.candidate) {
|
|
135
|
+
// Second consecutive sighting — promote.
|
|
136
|
+
return { confirmed: observed.state, confirmedAt: now, ruleId: observed.ruleId };
|
|
137
|
+
}
|
|
138
|
+
return { ...tracker, candidate: observed.state, candidateRuleId: observed.ruleId };
|
|
139
|
+
};
|
|
140
|
+
exports.advanceState = advanceState;
|