@paths.design/caws-cli 10.0.1 → 10.2.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/README.md +13 -5
- package/dist/budget-derivation.js +221 -74
- package/dist/commands/agents.js +124 -0
- package/dist/commands/evaluate.js +26 -12
- package/dist/commands/gates.js +31 -4
- package/dist/commands/init.js +7 -4
- package/dist/commands/iterate.js +7 -3
- package/dist/commands/scope.js +264 -0
- package/dist/commands/sidecar.js +6 -3
- package/dist/commands/specs.js +359 -4
- package/dist/commands/status.js +29 -4
- package/dist/commands/templates.js +0 -8
- package/dist/commands/validate.js +34 -13
- package/dist/commands/verify-acs.js +25 -10
- package/dist/commands/waivers.js +147 -5
- package/dist/commands/worktree.js +200 -4
- package/dist/gates/budget-limit.js +6 -1
- package/dist/gates/scope-boundary.js +26 -7
- package/dist/gates/spec-completeness.js +8 -1
- package/dist/index.js +56 -0
- package/dist/policy/PolicyManager.js +14 -7
- package/dist/session/session-manager.js +34 -0
- package/dist/templates/.caws/schemas/policy.schema.json +101 -34
- package/dist/templates/.caws/schemas/scope.schema.json +3 -3
- package/dist/templates/.caws/schemas/waivers.schema.json +91 -21
- package/dist/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/dist/templates/.caws/templates/working-spec.template.yml +3 -1
- package/dist/templates/.caws/tools/scope-guard.js +66 -15
- package/dist/templates/.claude/README.md +1 -1
- package/dist/templates/.claude/hooks/protected-paths.sh +39 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +106 -27
- package/dist/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/dist/templates/.claude/rules/worktree-isolation.md +21 -3
- package/dist/templates/.claude/settings.json +5 -0
- package/dist/templates/CLAUDE.md +56 -0
- package/dist/templates/agents.md +47 -0
- package/dist/utils/agent-display.js +210 -0
- package/dist/utils/agent-session.js +142 -0
- package/dist/utils/event-log.js +584 -0
- package/dist/utils/event-renderer.js +521 -0
- package/dist/utils/schema-validator.js +10 -2
- package/dist/utils/working-state.js +25 -0
- package/dist/validation/spec-validation.js +102 -9
- package/dist/waivers-manager.js +84 -0
- package/dist/worktree/worktree-manager.js +593 -26
- package/package.json +5 -4
- package/templates/.caws/schemas/policy.schema.json +101 -34
- package/templates/.caws/schemas/scope.schema.json +3 -3
- package/templates/.caws/schemas/waivers.schema.json +91 -21
- package/templates/.caws/schemas/working-spec.schema.json +253 -89
- package/templates/.caws/templates/working-spec.template.yml +3 -1
- package/templates/.caws/tools/scope-guard.js +66 -15
- package/templates/.claude/README.md +1 -1
- package/templates/.claude/hooks/protected-paths.sh +39 -0
- package/templates/.claude/hooks/scope-guard.sh +106 -27
- package/templates/.claude/hooks/worktree-write-guard.sh +96 -3
- package/templates/.claude/rules/worktree-isolation.md +21 -3
- package/templates/.claude/settings.json +5 -0
- package/templates/CLAUDE.md +56 -0
- package/templates/agents.md +47 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWSFIX-31 — agent claim display formatters.
|
|
3
|
+
*
|
|
4
|
+
* Single-purpose helpers for rendering agent / claim information so the
|
|
5
|
+
* format ("<sessionId>:<platform>", claim panels, soft-block warnings)
|
|
6
|
+
* is consistent across `caws status`, `caws agents`, and the
|
|
7
|
+
* worktree-manager soft-block surface.
|
|
8
|
+
*
|
|
9
|
+
* Display only — no I/O of its own beyond the small loaders it needs.
|
|
10
|
+
*
|
|
11
|
+
* @author @darianrosebrook
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
loadAgentRegistry,
|
|
19
|
+
findSessionLogs,
|
|
20
|
+
} = require('./agent-session');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Composite identifier used in every visible reference to an agent.
|
|
24
|
+
* Format: `<sessionId>:<platform>`. Lets readers trace provenance to
|
|
25
|
+
* platform-specific transcript directories.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} sessionId
|
|
28
|
+
* @param {string} platform - 'claude-code' | 'cursor' | 'unknown'
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function formatAgentRef(sessionId, platform) {
|
|
32
|
+
const sid = sessionId || 'unknown';
|
|
33
|
+
const plat = platform || 'unknown';
|
|
34
|
+
return `${sid}:${plat}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute a short human-readable age for a heartbeat timestamp.
|
|
39
|
+
* @param {string|null} iso
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
function formatHeartbeatAge(iso) {
|
|
43
|
+
if (!iso) return 'unknown';
|
|
44
|
+
const t = Date.parse(iso);
|
|
45
|
+
if (isNaN(t)) return 'unknown';
|
|
46
|
+
const ms = Date.now() - t;
|
|
47
|
+
if (ms < 0) return 'in the future';
|
|
48
|
+
const sec = Math.round(ms / 1000);
|
|
49
|
+
if (sec < 60) return `${sec}s ago`;
|
|
50
|
+
const min = Math.round(sec / 60);
|
|
51
|
+
if (min < 60) return `${min} min ago`;
|
|
52
|
+
const hr = Math.round(min / 60);
|
|
53
|
+
if (hr < 24) return `${hr}h ago`;
|
|
54
|
+
const days = Math.round(hr / 24);
|
|
55
|
+
return `${days}d ago`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Format a single session-log pointer for inclusion in a warning.
|
|
60
|
+
* Path is project-relative when possible.
|
|
61
|
+
*
|
|
62
|
+
* @param {object} log - Result from findSessionLogs
|
|
63
|
+
* @param {string} root - Project root (for relative path)
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
function formatSessionLogPointer(log, root) {
|
|
67
|
+
const rel = path.relative(root, log.path) || log.path;
|
|
68
|
+
const parts = [`tmp/${path.basename(log.path)}`, `${log.turnCount} turns`];
|
|
69
|
+
if (log.lastTurn) parts.push(`last turn ${log.lastTurn}`);
|
|
70
|
+
return ` Session log: ${rel}\n ${parts.join(', ')}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build the structured warning printed when a foreign claim is
|
|
75
|
+
* detected on a worktree (for `assertWorktreeOwnership` soft-block,
|
|
76
|
+
* `caws worktree claim` read-only mode, etc.).
|
|
77
|
+
*
|
|
78
|
+
* @param {object} args
|
|
79
|
+
* @param {string} args.worktree - Worktree name
|
|
80
|
+
* @param {object|null} args.priorOwnerEntry - The agents.json entry for the
|
|
81
|
+
* prior owner, or null when TTL-pruned.
|
|
82
|
+
* @param {string} args.priorOwnerSessionId - Sid from worktrees.json:owner
|
|
83
|
+
* @param {Array} args.sessionLogs - findSessionLogs() result
|
|
84
|
+
* @param {string} args.root - Project root for relative paths
|
|
85
|
+
* @param {string} args.takeoverCommand - Exact command to suggest
|
|
86
|
+
* @returns {string}
|
|
87
|
+
*/
|
|
88
|
+
function formatClaimNotice(args) {
|
|
89
|
+
const {
|
|
90
|
+
worktree,
|
|
91
|
+
priorOwnerEntry,
|
|
92
|
+
priorOwnerSessionId,
|
|
93
|
+
sessionLogs = [],
|
|
94
|
+
root,
|
|
95
|
+
takeoverCommand,
|
|
96
|
+
} = args;
|
|
97
|
+
|
|
98
|
+
const platform = priorOwnerEntry ? priorOwnerEntry.platform : 'unknown';
|
|
99
|
+
const ref = formatAgentRef(priorOwnerSessionId, platform);
|
|
100
|
+
|
|
101
|
+
const lines = [
|
|
102
|
+
`Worktree '${worktree}' is claimed by ${ref}`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (priorOwnerEntry) {
|
|
106
|
+
const age = formatHeartbeatAge(priorOwnerEntry.lastSeen);
|
|
107
|
+
lines.push(` Last heartbeat: ${priorOwnerEntry.lastSeen} (${age})`);
|
|
108
|
+
} else {
|
|
109
|
+
lines.push(` Last heartbeat: no live agent registry entry (pruned or stale)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (const log of sessionLogs) {
|
|
113
|
+
lines.push(formatSessionLogPointer(log, root));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (takeoverCommand) {
|
|
117
|
+
lines.push(` To proceed: ${takeoverCommand}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Build a softer hint when a worktree has no CAWS-tracked owner but a
|
|
125
|
+
* matching session-log directory exists (the "may still be active"
|
|
126
|
+
* scenario from AC A8).
|
|
127
|
+
*
|
|
128
|
+
* @param {object} args
|
|
129
|
+
* @param {string} args.worktree
|
|
130
|
+
* @param {Array} args.sessionLogs
|
|
131
|
+
* @param {string} args.root
|
|
132
|
+
* @returns {string}
|
|
133
|
+
*/
|
|
134
|
+
function formatOrphanLogHint(args) {
|
|
135
|
+
const { worktree, sessionLogs = [], root } = args;
|
|
136
|
+
const lines = [
|
|
137
|
+
`No active CAWS claim on worktree '${worktree}', but a session log exists.`,
|
|
138
|
+
` The previous session may still be active — read for context before continuing:`,
|
|
139
|
+
];
|
|
140
|
+
for (const log of sessionLogs) {
|
|
141
|
+
lines.push(formatSessionLogPointer(log, root));
|
|
142
|
+
}
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render the Claim panel that `caws status` includes when cwd is
|
|
148
|
+
* inside a worktree. Returns a multi-line string (caller prints it).
|
|
149
|
+
*
|
|
150
|
+
* @param {string} root - Project root
|
|
151
|
+
* @param {string} worktreeName - Worktree to inspect
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
function renderClaimPanel(root, worktreeName) {
|
|
155
|
+
let entry = null;
|
|
156
|
+
try {
|
|
157
|
+
const wtRegistryPath = path.join(root, '.caws', 'worktrees.json');
|
|
158
|
+
if (fs.existsSync(wtRegistryPath)) {
|
|
159
|
+
const reg = JSON.parse(fs.readFileSync(wtRegistryPath, 'utf8'));
|
|
160
|
+
entry = reg.worktrees && reg.worktrees[worktreeName];
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Best-effort.
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!entry || !entry.owner) {
|
|
167
|
+
return `Claim: no active claim on worktree '${worktreeName}'`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const agentRegistry = loadAgentRegistry(root);
|
|
171
|
+
const ownerEntry = agentRegistry.agents[entry.owner] || null;
|
|
172
|
+
const platform = ownerEntry ? ownerEntry.platform : 'unknown';
|
|
173
|
+
const ref = formatAgentRef(entry.owner, platform);
|
|
174
|
+
|
|
175
|
+
const lines = [`Claim: worktree '${worktreeName}' owned by ${ref}`];
|
|
176
|
+
if (ownerEntry) {
|
|
177
|
+
lines.push(
|
|
178
|
+
` Last heartbeat: ${ownerEntry.lastSeen} (${formatHeartbeatAge(ownerEntry.lastSeen)})`
|
|
179
|
+
);
|
|
180
|
+
if (ownerEntry.specId) {
|
|
181
|
+
lines.push(` Spec: ${ownerEntry.specId}`);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
lines.push(` Last heartbeat: no live agent registry entry (pruned)`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Surface session-log pointers if present (filter by branch when known).
|
|
188
|
+
const branch = entry.branch || null;
|
|
189
|
+
const logs = findSessionLogs(root, { sessionId: entry.owner }).concat(
|
|
190
|
+
branch ? findSessionLogs(root, { branch }) : []
|
|
191
|
+
);
|
|
192
|
+
// Dedupe by path
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
for (const log of logs) {
|
|
195
|
+
if (seen.has(log.path)) continue;
|
|
196
|
+
seen.add(log.path);
|
|
197
|
+
lines.push(formatSessionLogPointer(log, root));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
formatAgentRef,
|
|
205
|
+
formatHeartbeatAge,
|
|
206
|
+
formatSessionLogPointer,
|
|
207
|
+
formatClaimNotice,
|
|
208
|
+
formatOrphanLogHint,
|
|
209
|
+
renderClaimPanel,
|
|
210
|
+
};
|
|
@@ -122,18 +122,26 @@ function saveAgentRegistry(root, registry) {
|
|
|
122
122
|
* @param {string} agent.platform - 'claude-code' | 'cursor' | 'unknown'
|
|
123
123
|
* @param {string} [agent.model] - Model name if known
|
|
124
124
|
* @param {string} [agent.specId] - Active spec ID if known
|
|
125
|
+
* @param {string|null} [agent.worktree] - Active worktree name if known
|
|
125
126
|
* @param {number} [agent.ttl] - Custom TTL in ms (default 30 min)
|
|
126
127
|
*/
|
|
127
128
|
function heartbeatAgent(root, agent) {
|
|
128
129
|
const registry = loadAgentRegistry(root);
|
|
129
130
|
const existing = registry.agents[agent.sessionId] || {};
|
|
130
131
|
|
|
132
|
+
// CAWSFIX-31: `worktree` is allowed to be set to null explicitly (e.g.,
|
|
133
|
+
// refreshing a spec-only operation). Distinguish "not provided" from
|
|
134
|
+
// "explicitly null" using `in` so the previous worktree binding isn't
|
|
135
|
+
// silently preserved when the caller intends to clear it.
|
|
136
|
+
const worktreeProvided = Object.prototype.hasOwnProperty.call(agent, 'worktree');
|
|
137
|
+
|
|
131
138
|
registry.agents[agent.sessionId] = {
|
|
132
139
|
...existing,
|
|
133
140
|
sessionId: agent.sessionId,
|
|
134
141
|
platform: agent.platform || existing.platform || 'unknown',
|
|
135
142
|
model: agent.model || existing.model || null,
|
|
136
143
|
specId: agent.specId || existing.specId || null,
|
|
144
|
+
worktree: worktreeProvided ? agent.worktree : (existing.worktree || null),
|
|
137
145
|
ttl: agent.ttl || existing.ttl || DEFAULT_TTL_MS,
|
|
138
146
|
firstSeen: existing.firstSeen || new Date().toISOString(),
|
|
139
147
|
lastSeen: new Date().toISOString(),
|
|
@@ -142,6 +150,138 @@ function heartbeatAgent(root, agent) {
|
|
|
142
150
|
saveAgentRegistry(root, registry);
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Refresh the current agent session's claim with the operation's specId
|
|
155
|
+
* and (optionally) worktree context.
|
|
156
|
+
*
|
|
157
|
+
* CAWSFIX-31: every CAWS lifecycle CLI op (specs create/close/archive/
|
|
158
|
+
* delete, worktree create/bind/merge) calls this so agents.json stays
|
|
159
|
+
* fresh even when the IDE session-log hook hasn't fired (e.g., between
|
|
160
|
+
* SessionStart and PreCompact, or in non-Claude-Code environments).
|
|
161
|
+
*
|
|
162
|
+
* Best-effort: silently no-ops when no session id can be determined or
|
|
163
|
+
* when the project root has no .caws/ directory. Never throws.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} root - Project root
|
|
166
|
+
* @param {object} ctx - Refresh context
|
|
167
|
+
* @param {string|null} [ctx.specId] - Spec the operation touched
|
|
168
|
+
* @param {string|null} [ctx.worktree] - Worktree the operation touched
|
|
169
|
+
*/
|
|
170
|
+
function refreshAgentClaim(root, ctx = {}) {
|
|
171
|
+
try {
|
|
172
|
+
const sessionId = getAgentSessionId(root);
|
|
173
|
+
if (!sessionId) return;
|
|
174
|
+
const platform = getAgentPlatform();
|
|
175
|
+
heartbeatAgent(root, {
|
|
176
|
+
sessionId,
|
|
177
|
+
platform,
|
|
178
|
+
specId: ctx.specId || null,
|
|
179
|
+
worktree: ctx.worktree !== undefined ? ctx.worktree : null,
|
|
180
|
+
});
|
|
181
|
+
} catch {
|
|
182
|
+
// Best-effort: a failure here must never break the lifecycle op.
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find session-log directories under `tmp/` whose `.meta.json` matches
|
|
188
|
+
* the given session id or branch.
|
|
189
|
+
*
|
|
190
|
+
* CAWSFIX-31: the CLI surfaces these paths as pointers when a foreign
|
|
191
|
+
* claim is detected. It does not interpret the contents — the agent
|
|
192
|
+
* reads them and decides whether to take over.
|
|
193
|
+
*
|
|
194
|
+
* Returns an array of `{ sessionId, path, branch, turnCount, lastTurn }`.
|
|
195
|
+
* Tolerates missing/malformed `.meta.json`. Refuses to follow symlinks
|
|
196
|
+
* outside `<root>/tmp/` for safety.
|
|
197
|
+
*
|
|
198
|
+
* @param {string} root - Project root
|
|
199
|
+
* @param {object} [filters] - Optional filters
|
|
200
|
+
* @param {string} [filters.sessionId] - Only return logs for this id
|
|
201
|
+
* @param {string} [filters.branch] - Only return logs whose meta.branch matches
|
|
202
|
+
* @returns {Array<object>}
|
|
203
|
+
*/
|
|
204
|
+
function findSessionLogs(root, filters = {}) {
|
|
205
|
+
const results = [];
|
|
206
|
+
const tmpDir = path.join(root, 'tmp');
|
|
207
|
+
if (!fs.existsSync(tmpDir)) return results;
|
|
208
|
+
|
|
209
|
+
let realTmp;
|
|
210
|
+
try {
|
|
211
|
+
realTmp = fs.realpathSync(tmpDir);
|
|
212
|
+
} catch {
|
|
213
|
+
return results;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let entries;
|
|
217
|
+
try {
|
|
218
|
+
entries = fs.readdirSync(tmpDir, { withFileTypes: true });
|
|
219
|
+
} catch {
|
|
220
|
+
return results;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
if (!entry.isDirectory()) continue;
|
|
225
|
+
const sid = entry.name;
|
|
226
|
+
if (filters.sessionId && sid !== filters.sessionId) continue;
|
|
227
|
+
|
|
228
|
+
const dirPath = path.join(tmpDir, sid);
|
|
229
|
+
let realDir;
|
|
230
|
+
try {
|
|
231
|
+
realDir = fs.realpathSync(dirPath);
|
|
232
|
+
} catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
// Symlink-escape guard: realpath must remain under realTmp.
|
|
236
|
+
if (!realDir.startsWith(realTmp + path.sep) && realDir !== realTmp) continue;
|
|
237
|
+
|
|
238
|
+
const metaPath = path.join(dirPath, '.meta.json');
|
|
239
|
+
if (!fs.existsSync(metaPath)) continue;
|
|
240
|
+
|
|
241
|
+
let meta = {};
|
|
242
|
+
try {
|
|
243
|
+
meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
244
|
+
} catch {
|
|
245
|
+
// Malformed .meta.json — skip but don't crash.
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (filters.branch && meta.branch !== filters.branch) continue;
|
|
250
|
+
|
|
251
|
+
// Count turn files and capture latest turn timestamp.
|
|
252
|
+
let turnCount = 0;
|
|
253
|
+
let lastTurn = null;
|
|
254
|
+
try {
|
|
255
|
+
const files = fs.readdirSync(dirPath);
|
|
256
|
+
const turnFiles = files
|
|
257
|
+
.filter((f) => /^turn-\d+\.json$/.test(f))
|
|
258
|
+
.sort();
|
|
259
|
+
turnCount = turnFiles.length;
|
|
260
|
+
if (turnFiles.length > 0) {
|
|
261
|
+
const latest = turnFiles[turnFiles.length - 1];
|
|
262
|
+
try {
|
|
263
|
+
const turnData = JSON.parse(fs.readFileSync(path.join(dirPath, latest), 'utf8'));
|
|
264
|
+
lastTurn = turnData.ts_end || turnData.ts_start || null;
|
|
265
|
+
} catch {
|
|
266
|
+
lastTurn = null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
// best-effort; leave defaults
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
results.push({
|
|
274
|
+
sessionId: sid,
|
|
275
|
+
path: dirPath,
|
|
276
|
+
branch: meta.branch || null,
|
|
277
|
+
turnCount,
|
|
278
|
+
lastTurn,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return results;
|
|
283
|
+
}
|
|
284
|
+
|
|
145
285
|
/**
|
|
146
286
|
* Remove an agent session from the registry.
|
|
147
287
|
* Called on session stop.
|
|
@@ -194,6 +334,8 @@ module.exports = {
|
|
|
194
334
|
loadAgentRegistry,
|
|
195
335
|
saveAgentRegistry,
|
|
196
336
|
heartbeatAgent,
|
|
337
|
+
refreshAgentClaim,
|
|
338
|
+
findSessionLogs,
|
|
197
339
|
removeAgent,
|
|
198
340
|
findActiveAgent,
|
|
199
341
|
listActiveAgents,
|