sentinelayer-cli 0.4.5 → 0.8.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.
Files changed (72) hide show
  1. package/README.md +16 -18
  2. package/package.json +7 -6
  3. package/src/agents/jules/config/definition.js +13 -62
  4. package/src/agents/jules/config/system-prompt.js +8 -1
  5. package/src/agents/jules/fix-cycle.js +12 -372
  6. package/src/agents/jules/loop.js +116 -26
  7. package/src/agents/jules/pulse.js +10 -327
  8. package/src/agents/jules/stream.js +13 -12
  9. package/src/agents/jules/swarm/orchestrator.js +3 -3
  10. package/src/agents/jules/swarm/sub-agent.js +6 -3
  11. package/src/agents/jules/tools/aidenid-email.js +189 -0
  12. package/src/agents/jules/tools/auth-audit.js +1187 -45
  13. package/src/agents/jules/tools/dispatch.js +25 -12
  14. package/src/agents/jules/tools/file-edit.js +2 -180
  15. package/src/agents/jules/tools/file-read.js +2 -100
  16. package/src/agents/jules/tools/glob.js +2 -168
  17. package/src/agents/jules/tools/grep.js +2 -228
  18. package/src/agents/jules/tools/path-guards.js +2 -161
  19. package/src/agents/jules/tools/runtime-audit.js +6 -2
  20. package/src/agents/jules/tools/shell.js +2 -383
  21. package/src/agents/persona-visuals.js +64 -0
  22. package/src/agents/shared-tools/dispatch-core.js +320 -0
  23. package/src/agents/shared-tools/file-edit.js +180 -0
  24. package/src/agents/shared-tools/file-read.js +100 -0
  25. package/src/agents/shared-tools/glob.js +168 -0
  26. package/src/agents/shared-tools/grep.js +228 -0
  27. package/src/agents/shared-tools/index.js +46 -0
  28. package/src/agents/shared-tools/path-guards.js +161 -0
  29. package/src/agents/shared-tools/shell.js +383 -0
  30. package/src/ai/aidenid.js +56 -7
  31. package/src/ai/client.js +45 -0
  32. package/src/ai/proxy.js +137 -0
  33. package/src/auth/gate.js +290 -16
  34. package/src/auth/http.js +450 -39
  35. package/src/auth/service.js +262 -47
  36. package/src/auth/session-store.js +475 -21
  37. package/src/cli.js +5 -0
  38. package/src/commands/audit.js +13 -8
  39. package/src/commands/auth.js +53 -9
  40. package/src/commands/omargate.js +10 -2
  41. package/src/commands/scan.js +10 -4
  42. package/src/commands/session.js +590 -0
  43. package/src/commands/spec.js +62 -0
  44. package/src/commands/watch.js +3 -2
  45. package/src/daemon/assignment-ledger.js +196 -0
  46. package/src/daemon/error-worker.js +599 -16
  47. package/src/daemon/fix-cycle.js +384 -0
  48. package/src/daemon/ingest-refresh.js +10 -9
  49. package/src/daemon/jira-lifecycle.js +135 -0
  50. package/src/daemon/pulse.js +327 -0
  51. package/src/daemon/scope-engine.js +1068 -0
  52. package/src/events/schema.js +190 -0
  53. package/src/interactive/index.js +18 -16
  54. package/src/legacy-cli.js +606 -37
  55. package/src/prompt/generator.js +19 -1
  56. package/src/review/ai-review.js +11 -1
  57. package/src/review/local-review.js +75 -19
  58. package/src/review/omargate-interactive.js +68 -0
  59. package/src/review/omargate-orchestrator.js +404 -0
  60. package/src/review/persona-prompts.js +296 -0
  61. package/src/review/scan-modes.js +48 -0
  62. package/src/scan/generator.js +1 -1
  63. package/src/session/agent-registry.js +352 -0
  64. package/src/session/daemon.js +801 -0
  65. package/src/session/paths.js +33 -0
  66. package/src/session/runtime-bridge.js +739 -0
  67. package/src/session/store.js +388 -0
  68. package/src/session/stream.js +325 -0
  69. package/src/spec/generator.js +100 -0
  70. package/src/telemetry/session-tracker.js +148 -32
  71. package/src/telemetry/sync.js +6 -2
  72. package/src/ui/command-hints.js +13 -0
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Persona-scoped system prompts for Omar Gate AI analysis.
3
+ *
4
+ * Each persona gets a domain-focused prompt that constrains the LLM
5
+ * to analyze code through a specific security/quality lens.
6
+ */
7
+
8
+ const PERSONA_PROMPTS = {
9
+ security: {
10
+ role: "Nina Patel — Security Specialist",
11
+ focus: `You are a security specialist reviewing code for exploitable vulnerabilities.
12
+
13
+ Focus areas:
14
+ - Authentication and authorization bypass paths
15
+ - Secret/credential exposure in code, configs, logs, and environment
16
+ - Injection vectors: SQL, shell, XSS, SSRF, path traversal
17
+ - Cryptographic weaknesses: weak hashing, hardcoded keys, insecure TLS
18
+ - Session management: fixation, token leakage, cookie misconfiguration
19
+ - Rate limiting gaps on auth and payment endpoints
20
+ - CORS misconfiguration allowing unauthorized origins
21
+ - Insecure deserialization and dynamic code execution (eval, Function)
22
+
23
+ Evidence standard: Every finding MUST include file:line, exploit scenario, and remediation.
24
+ Do NOT report hypothetical issues without concrete code evidence.`,
25
+ },
26
+
27
+ architecture: {
28
+ role: "Maya Volkov — Architecture Specialist",
29
+ focus: `You are an architecture specialist reviewing code for structural quality.
30
+
31
+ Focus areas:
32
+ - God components/modules (>300 LOC, >10 responsibilities)
33
+ - Circular dependencies between modules
34
+ - Tight coupling between layers (presentation → data access)
35
+ - Missing abstraction boundaries (business logic in route handlers)
36
+ - State management sprawl (>15 useState in a component)
37
+ - Missing error boundaries and fallback handling
38
+ - Inconsistent naming/organization patterns
39
+ - Dead code and unreachable paths
40
+
41
+ Evidence standard: Every finding MUST include file:line, coupling graph or complexity metric, and refactoring guidance.`,
42
+ },
43
+
44
+ testing: {
45
+ role: "Priya Raman — Testing Specialist",
46
+ focus: `You are a testing specialist reviewing code for coverage gaps and test quality.
47
+
48
+ Focus areas:
49
+ - Critical paths without test coverage (auth, payment, data mutation)
50
+ - Tests that mock too much (false confidence)
51
+ - Missing edge case tests (empty inputs, boundary values, error paths)
52
+ - Flaky test patterns (timing, external dependencies, shared state)
53
+ - Missing integration tests for API endpoints
54
+ - No E2E tests for critical user flows
55
+ - Test data that doesn't represent production scenarios
56
+ - Missing assertion specificity (assertTrue vs assertEquals)
57
+
58
+ Evidence standard: Every finding MUST include the untested code path (file:line) and a concrete test case outline.`,
59
+ },
60
+
61
+ performance: {
62
+ role: "Arjun Mehta — Performance Specialist",
63
+ focus: `You are a performance specialist reviewing code for latency and efficiency issues.
64
+
65
+ Focus areas:
66
+ - N+1 query patterns (loop-based database calls)
67
+ - Missing database indexes on WHERE/JOIN/ORDER BY columns
68
+ - Unbounded data fetching (no LIMIT, no pagination)
69
+ - Synchronous blocking in async contexts
70
+ - Memory leaks (unclosed connections, event listeners, timers)
71
+ - Bundle size bloat (large imports, no tree shaking, no code splitting)
72
+ - Missing caching for expensive computations
73
+ - Render performance (unnecessary re-renders, missing memoization)
74
+
75
+ Evidence standard: Every finding MUST include file:line, estimated performance impact, and optimization approach.`,
76
+ },
77
+
78
+ compliance: {
79
+ role: "Leila Farouk — Compliance Specialist",
80
+ focus: `You are a compliance specialist reviewing code for regulatory adherence.
81
+
82
+ Focus areas:
83
+ - PII handling without encryption or access controls
84
+ - Missing audit logging for data access and mutations
85
+ - GDPR: data retention without deletion mechanisms
86
+ - SOC2: missing access controls, no principle of least privilege
87
+ - HIPAA: PHI exposure, missing BAA requirements
88
+ - Missing consent tracking for data collection
89
+ - Insecure data export/download without authorization
90
+ - Missing data classification and sensitivity labels
91
+
92
+ Evidence standard: Every finding MUST include the regulatory requirement, the gap, and the remediation with compliance evidence.`,
93
+ },
94
+
95
+ documentation: {
96
+ role: "Samir Okafor — Documentation Specialist",
97
+ focus: `You are a documentation specialist reviewing for operational clarity.
98
+
99
+ Focus areas:
100
+ - Missing or outdated README/setup instructions
101
+ - API endpoints without documentation
102
+ - Missing runbooks for incident response
103
+ - Configuration options without documentation
104
+ - Missing architecture decision records (ADRs)
105
+ - Outdated deployment instructions
106
+ - Missing onboarding documentation for new developers
107
+
108
+ Evidence standard: Every finding MUST include what is missing, where it should live, and a draft outline.`,
109
+ },
110
+
111
+ reliability: {
112
+ role: "Noah Ben-David — Reliability Specialist",
113
+ focus: `You are a reliability specialist reviewing code for fault tolerance.
114
+
115
+ Focus areas:
116
+ - Missing timeout configuration on external calls
117
+ - No retry logic or exponential backoff for transient failures
118
+ - Missing circuit breakers on external service calls
119
+ - No graceful degradation when dependencies are down
120
+ - Missing health check endpoints
121
+ - Queue backpressure handling gaps
122
+ - Missing dead letter queue for failed jobs
123
+ - No idempotency keys on mutation endpoints
124
+
125
+ Evidence standard: Every finding MUST include the failure scenario, blast radius, and resilience pattern to apply.`,
126
+ },
127
+
128
+ release: {
129
+ role: "Omar Singh — Release Engineering Specialist",
130
+ focus: `You are a release engineering specialist reviewing CI/CD and deployment.
131
+
132
+ Focus areas:
133
+ - Unpinned GitHub Actions (using @main instead of SHA)
134
+ - Missing artifact signing or provenance attestation
135
+ - No rollback mechanism in deployment pipeline
136
+ - Missing smoke tests after deploy
137
+ - Secrets in CI/CD logs or artifacts
138
+ - Missing branch protection rules
139
+ - No canary or staged rollout strategy
140
+ - Deploy pipeline without quality gates
141
+
142
+ Evidence standard: Every finding MUST include the workflow file:line, risk, and the hardened alternative.`,
143
+ },
144
+
145
+ observability: {
146
+ role: "Sofia Alvarez — Observability Specialist",
147
+ focus: `You are an observability specialist reviewing telemetry and alerting.
148
+
149
+ Focus areas:
150
+ - Missing structured logging (console.log without context)
151
+ - No request tracing (missing correlation IDs)
152
+ - Missing error tracking integration
153
+ - No alerting on error rate spikes
154
+ - Missing latency tracking on critical paths
155
+ - No dashboard for key business metrics
156
+ - Missing SLO/SLI definitions
157
+ - Blind spots: operations without any telemetry
158
+
159
+ Evidence standard: Every finding MUST include what metric/signal is missing, where to instrument, and the alert threshold.`,
160
+ },
161
+
162
+ infrastructure: {
163
+ role: "Kat Hughes — Infrastructure Specialist",
164
+ focus: `You are an infrastructure specialist reviewing cloud and deployment config.
165
+
166
+ Focus areas:
167
+ - Overly permissive IAM policies (wildcard actions/resources)
168
+ - Public-facing resources without WAF/rate limiting
169
+ - Missing encryption at rest or in transit
170
+ - Hardcoded infrastructure values (IPs, ARNs, account IDs)
171
+ - Missing VPC/subnet isolation
172
+ - No secrets rotation policy
173
+ - Missing backup and disaster recovery configuration
174
+ - Infrastructure drift (manual changes not in IaC)
175
+
176
+ Evidence standard: Every finding MUST include the resource, the misconfiguration, blast radius, and the IaC fix.`,
177
+ },
178
+
179
+ "supply-chain": {
180
+ role: "Nora Kline — Supply Chain Specialist",
181
+ focus: `You are a supply chain specialist reviewing dependency security.
182
+
183
+ Focus areas:
184
+ - Dependencies with known CVEs (critical/high severity)
185
+ - Unpinned dependency versions (using ^/~ instead of exact)
186
+ - Dependencies from untrusted or abandoned packages
187
+ - Missing lockfile integrity checks
188
+ - No SBOM generation in build pipeline
189
+ - Typosquatting risk (similar package names)
190
+ - Excessive dependency tree depth
191
+ - Missing license compliance checks
192
+
193
+ Evidence standard: Every finding MUST include the package name, version, CVE/risk, and the pinned/patched alternative.`,
194
+ },
195
+
196
+ frontend: {
197
+ role: "Jules Tanaka — Frontend Specialist",
198
+ focus: `You are a frontend specialist reviewing UI code for production readiness.
199
+
200
+ Focus areas:
201
+ - XSS via dangerouslySetInnerHTML without sanitization
202
+ - Client-side token storage in localStorage (use httpOnly cookies)
203
+ - Missing input validation on forms
204
+ - Accessibility failures (missing alt text, labels, keyboard navigation)
205
+ - Bundle size > 200KB initial JS
206
+ - Missing error boundaries around route components
207
+ - CLS-causing patterns (images without dimensions, dynamic content injection)
208
+ - Missing loading/error states on data fetching
209
+
210
+ Evidence standard: Every finding MUST include file:line, user impact, and the specific fix.`,
211
+ },
212
+
213
+ "ai-governance": {
214
+ role: "Amina Chen — AI Governance Specialist",
215
+ focus: `You are an AI governance specialist reviewing AI/ML code safety.
216
+
217
+ Focus areas:
218
+ - Prompt injection vectors in user-facing LLM prompts
219
+ - Missing input sanitization before LLM calls
220
+ - No rate limiting on AI endpoints
221
+ - Missing cost/token budget enforcement
222
+ - No human-in-the-loop for high-risk AI decisions
223
+ - Missing model versioning and eval regression checks
224
+ - Tool/agent permission escalation risks
225
+ - Missing audit trail for AI-generated actions
226
+
227
+ Evidence standard: Every finding MUST include the injection/bypass scenario, the affected code path, and the guardrail to add.`,
228
+ },
229
+ };
230
+
231
+ /**
232
+ * Build a persona-scoped system prompt for Omar Gate AI analysis.
233
+ *
234
+ * @param {object} options
235
+ * @param {string} options.personaId - Agent ID (e.g., "security", "architecture")
236
+ * @param {string} [options.targetPath] - Repository path
237
+ * @param {object} [options.deterministicSummary] - Summary from deterministic scan
238
+ * @param {number} [options.maxFindings] - Max findings to return (default 20)
239
+ * @returns {string} System prompt
240
+ */
241
+ export function buildPersonaReviewPrompt({
242
+ personaId,
243
+ targetPath = "",
244
+ deterministicSummary = {},
245
+ maxFindings = 20,
246
+ } = {}) {
247
+ const persona = PERSONA_PROMPTS[personaId];
248
+ if (!persona) {
249
+ return buildGenericPrompt({ targetPath, deterministicSummary, maxFindings });
250
+ }
251
+
252
+ return `# ${persona.role}
253
+
254
+ ${persona.focus}
255
+
256
+ ## Context
257
+ Target: ${targetPath || "(not provided)"}
258
+ Deterministic scan: P0=${deterministicSummary.P0 || 0} P1=${deterministicSummary.P1 || 0} P2=${deterministicSummary.P2 || 0} P3=${deterministicSummary.P3 || 0}
259
+
260
+ ## Output Contract
261
+ Return a JSON array of findings. Maximum ${maxFindings} findings. Each finding:
262
+ \`\`\`json
263
+ {
264
+ "severity": "P0|P1|P2|P3",
265
+ "file": "path/to/file.ext",
266
+ "line": 42,
267
+ "title": "Brief description",
268
+ "evidence": "Concrete code evidence at file:line",
269
+ "rootCause": "Why this is a problem",
270
+ "recommendedFix": "Specific fix to apply",
271
+ "confidence": 0.85
272
+ }
273
+ \`\`\`
274
+
275
+ Rules:
276
+ - Only report findings you have HIGH confidence in (>= 0.7)
277
+ - Every finding MUST have concrete file:line evidence
278
+ - Do NOT repeat findings already in the deterministic scan
279
+ - Do NOT report hypothetical/speculative issues
280
+ - Focus on REAL, EXPLOITABLE, IMPACTFUL problems in your domain
281
+ - Return ONLY the JSON array, no other text
282
+ `;
283
+ }
284
+
285
+ function buildGenericPrompt({ targetPath, deterministicSummary, maxFindings }) {
286
+ return `You are a senior code reviewer. Analyze the code for security, quality, and reliability issues.
287
+
288
+ Target: ${targetPath || "(not provided)"}
289
+ Deterministic scan: P0=${deterministicSummary.P0 || 0} P1=${deterministicSummary.P1 || 0} P2=${deterministicSummary.P2 || 0}
290
+
291
+ Return a JSON array of up to ${maxFindings} findings with: severity, file, line, title, evidence, rootCause, recommendedFix, confidence.
292
+ Only report findings with concrete evidence. Do NOT repeat deterministic findings.`;
293
+ }
294
+
295
+ export const PERSONA_IDS = Object.keys(PERSONA_PROMPTS);
296
+ export { PERSONA_PROMPTS };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Scan mode → persona list resolution for Omar Gate.
3
+ *
4
+ * baseline: security only (~30s)
5
+ * deep: all 13 personas (~3-5min) — alias of full-depth from v0.7+
6
+ * audit: alias for full-depth
7
+ * full-depth: all 13 personas (~3-5min)
8
+ *
9
+ * v0.7 change (2026-04-16): `deep` was a 6-persona subset and missed
10
+ * domain specialists (Maya/backend, Jules/frontend, Nora/supply-chain,
11
+ * Samir/documentation, etc.). Users running `sl /omargate deep` expected
12
+ * a full dispatch. Deep now matches full-depth to prevent silent coverage
13
+ * gaps; run `baseline` when you only need security.
14
+ */
15
+
16
+ const FULL_DEPTH_PERSONAS = [
17
+ "security",
18
+ "architecture",
19
+ "testing",
20
+ "performance",
21
+ "compliance",
22
+ "reliability",
23
+ "release",
24
+ "observability",
25
+ "infrastructure",
26
+ "supply-chain",
27
+ "frontend",
28
+ "documentation",
29
+ "ai-governance",
30
+ ];
31
+
32
+ const SCAN_MODES = {
33
+ baseline: ["security"],
34
+ deep: FULL_DEPTH_PERSONAS,
35
+ "full-depth": FULL_DEPTH_PERSONAS,
36
+ audit: FULL_DEPTH_PERSONAS,
37
+ };
38
+
39
+ export function resolveScanMode(mode = "deep") {
40
+ const normalized = String(mode || "deep").trim().toLowerCase();
41
+ const personas = SCAN_MODES[normalized];
42
+ if (!personas) {
43
+ throw new Error(`Unknown scan mode '${mode}'. Use: ${Object.keys(SCAN_MODES).join(", ")}`);
44
+ }
45
+ return { mode: normalized, personas: [...personas] };
46
+ }
47
+
48
+ export const AVAILABLE_SCAN_MODES = Object.keys(SCAN_MODES);
@@ -161,7 +161,7 @@ export function buildSecurityReviewWorkflow({ secretName = DEFAULT_SCAN_SECRET_N
161
161
  required: false,
162
162
  default: profile.scanMode || "deep",
163
163
  type: "choice",
164
- options: ["deep", "nightly"],
164
+ options: ["baseline", "deep", "audit", "full-depth"],
165
165
  },
166
166
  severity_gate: {
167
167
  description: "Severity threshold that blocks merge",
@@ -0,0 +1,352 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import fsp from "node:fs/promises";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ import { STUCK_THRESHOLDS } from "../agents/jules/pulse.js";
7
+ import { createAgentEvent } from "../events/schema.js";
8
+ import { resolveSessionPaths } from "./paths.js";
9
+ import { appendToStream } from "./stream.js";
10
+
11
+ const AGENT_SNAPSHOT_SCHEMA_VERSION = "1.0.0";
12
+
13
+ const AGENT_ROLES = new Set([
14
+ "coder",
15
+ "reviewer",
16
+ "tester",
17
+ "daemon",
18
+ "observer",
19
+ "persona",
20
+ ]);
21
+
22
+ const AGENT_STATUSES = new Set([
23
+ "coding",
24
+ "reviewing",
25
+ "testing",
26
+ "idle",
27
+ "blocked",
28
+ "watching",
29
+ ]);
30
+
31
+ const LEAVE_REASONS = new Set([
32
+ "task_complete",
33
+ "error",
34
+ "timeout",
35
+ "manual",
36
+ "killed",
37
+ ]);
38
+
39
+ function normalizeString(value) {
40
+ return String(value || "").trim();
41
+ }
42
+
43
+ function normalizeIsoTimestamp(value, fallbackIso = new Date().toISOString()) {
44
+ const normalized = normalizeString(value);
45
+ if (!normalized) {
46
+ return fallbackIso;
47
+ }
48
+ const epoch = Date.parse(normalized);
49
+ if (!Number.isFinite(epoch)) {
50
+ return fallbackIso;
51
+ }
52
+ return new Date(epoch).toISOString();
53
+ }
54
+
55
+ function normalizeRole(value) {
56
+ const normalized = normalizeString(value).toLowerCase();
57
+ if (!AGENT_ROLES.has(normalized)) {
58
+ throw new Error(`role must be one of: ${[...AGENT_ROLES].join(", ")}.`);
59
+ }
60
+ return normalized;
61
+ }
62
+
63
+ function normalizeStatus(value, fallbackValue = "idle") {
64
+ const normalized = normalizeString(value).toLowerCase();
65
+ if (!normalized) {
66
+ return fallbackValue;
67
+ }
68
+ if (!AGENT_STATUSES.has(normalized)) {
69
+ throw new Error(`status must be one of: ${[...AGENT_STATUSES].join(", ")}.`);
70
+ }
71
+ return normalized;
72
+ }
73
+
74
+ function normalizeLeaveReason(value) {
75
+ const normalized = normalizeString(value).toLowerCase();
76
+ if (!normalized) {
77
+ return "manual";
78
+ }
79
+ if (!LEAVE_REASONS.has(normalized)) {
80
+ throw new Error(`reason must be one of: ${[...LEAVE_REASONS].join(", ")}.`);
81
+ }
82
+ return normalized;
83
+ }
84
+
85
+ function sanitizePrefix(value) {
86
+ const normalized = normalizeString(value)
87
+ .toLowerCase()
88
+ .replace(/[^a-z0-9]+/g, "-")
89
+ .replace(/^-+|-+$/g, "");
90
+ if (!normalized) {
91
+ return "";
92
+ }
93
+ return normalized.slice(0, 12);
94
+ }
95
+
96
+ function deriveModelPrefix(modelName) {
97
+ const normalized = normalizeString(modelName).toLowerCase();
98
+ if (!normalized) {
99
+ return "agent";
100
+ }
101
+ if (normalized.includes("claude")) return "claude";
102
+ if (normalized.includes("codex")) return "codex";
103
+ if (normalized.includes("gpt")) return "codex";
104
+ if (normalized.includes("sonnet")) return "sonnet";
105
+ if (normalized.includes("senti") || normalized.includes("sentinel")) return "senti";
106
+
107
+ const token = normalized.split(/[\s:/_-]+/).find(Boolean) || normalized;
108
+ return sanitizePrefix(token) || "agent";
109
+ }
110
+
111
+ function normalizeAgentSnapshot(snapshot = {}, nowIso = new Date().toISOString()) {
112
+ return {
113
+ schemaVersion: AGENT_SNAPSHOT_SCHEMA_VERSION,
114
+ sessionId: normalizeString(snapshot.sessionId),
115
+ agentId: normalizeString(snapshot.agentId),
116
+ model: normalizeString(snapshot.model) || "unknown",
117
+ role: normalizeRole(snapshot.role || "observer"),
118
+ status: normalizeStatus(snapshot.status, "idle"),
119
+ detail: normalizeString(snapshot.detail) || "",
120
+ file: normalizeString(snapshot.file) || null,
121
+ joinedAt: normalizeIsoTimestamp(snapshot.joinedAt, nowIso),
122
+ lastActivityAt: normalizeIsoTimestamp(snapshot.lastActivityAt, nowIso),
123
+ leftAt: snapshot.leftAt ? normalizeIsoTimestamp(snapshot.leftAt, nowIso) : null,
124
+ leaveReason: snapshot.leaveReason ? normalizeLeaveReason(snapshot.leaveReason) : null,
125
+ active: snapshot.active !== false,
126
+ updatedAt: normalizeIsoTimestamp(snapshot.updatedAt, nowIso),
127
+ };
128
+ }
129
+
130
+ async function readAgentSnapshot(snapshotPath) {
131
+ try {
132
+ const raw = await fsp.readFile(snapshotPath, "utf-8");
133
+ const parsed = JSON.parse(raw);
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
135
+ return null;
136
+ }
137
+ return parsed;
138
+ } catch (error) {
139
+ if (error && typeof error === "object" && error.code === "ENOENT") {
140
+ return null;
141
+ }
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ async function writeAgentSnapshot(snapshotPath, snapshot) {
147
+ await fsp.mkdir(path.dirname(snapshotPath), { recursive: true });
148
+ const tmpPath = `${snapshotPath}.${process.pid}.${Date.now()}.tmp`;
149
+ await fsp.writeFile(tmpPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
150
+ await fsp.rename(tmpPath, snapshotPath);
151
+ }
152
+
153
+ async function emitAgentEvent(sessionId, event, payload, { targetPath = process.cwd() } = {}) {
154
+ const envelope = createAgentEvent({
155
+ event,
156
+ agentId: payload.agentId,
157
+ sessionId,
158
+ payload,
159
+ });
160
+ await appendToStream(sessionId, envelope, { targetPath });
161
+ }
162
+
163
+ function buildAgentSnapshotPath(paths, agentId) {
164
+ const normalizedAgentId = normalizeString(agentId);
165
+ if (!normalizedAgentId) {
166
+ throw new Error("agentId is required.");
167
+ }
168
+ return path.join(paths.agentsDir, `${normalizedAgentId}.json`);
169
+ }
170
+
171
+ export function generateAgentId(modelName) {
172
+ const prefix = deriveModelPrefix(modelName);
173
+ const suffix = randomBytes(2).toString("hex");
174
+ return `${prefix}-${suffix}`;
175
+ }
176
+
177
+ export async function registerAgent(
178
+ sessionId,
179
+ { agentId = "", model = "", role = "observer", targetPath = process.cwd() } = {}
180
+ ) {
181
+ const paths = resolveSessionPaths(sessionId, { targetPath });
182
+ const nowIso = new Date().toISOString();
183
+ const resolvedAgentId = normalizeString(agentId) || generateAgentId(model);
184
+ const snapshotPath = buildAgentSnapshotPath(paths, resolvedAgentId);
185
+
186
+ const snapshot = normalizeAgentSnapshot(
187
+ {
188
+ sessionId: paths.sessionId,
189
+ agentId: resolvedAgentId,
190
+ model: normalizeString(model) || "unknown",
191
+ role,
192
+ status: "idle",
193
+ detail: "",
194
+ file: null,
195
+ joinedAt: nowIso,
196
+ lastActivityAt: nowIso,
197
+ leftAt: null,
198
+ leaveReason: null,
199
+ active: true,
200
+ updatedAt: nowIso,
201
+ },
202
+ nowIso
203
+ );
204
+
205
+ await writeAgentSnapshot(snapshotPath, snapshot);
206
+ await emitAgentEvent(paths.sessionId, "agent_join", {
207
+ agentId: snapshot.agentId,
208
+ model: snapshot.model,
209
+ role: snapshot.role,
210
+ status: snapshot.status,
211
+ }, { targetPath });
212
+
213
+ return {
214
+ ...snapshot,
215
+ snapshotPath,
216
+ };
217
+ }
218
+
219
+ export async function heartbeatAgent(
220
+ sessionId,
221
+ agentId,
222
+ { status = "", detail = "", file = "", targetPath = process.cwd() } = {}
223
+ ) {
224
+ const paths = resolveSessionPaths(sessionId, { targetPath });
225
+ const nowIso = new Date().toISOString();
226
+ const snapshotPath = buildAgentSnapshotPath(paths, agentId);
227
+ const existing = await readAgentSnapshot(snapshotPath);
228
+ if (!existing) {
229
+ throw new Error(`Agent '${normalizeString(agentId)}' is not registered in session '${paths.sessionId}'.`);
230
+ }
231
+
232
+ const snapshot = normalizeAgentSnapshot(
233
+ {
234
+ ...existing,
235
+ status: normalizeStatus(status, normalizeStatus(existing.status || "idle")),
236
+ detail: normalizeString(detail) || normalizeString(existing.detail),
237
+ file: normalizeString(file) || normalizeString(existing.file) || null,
238
+ lastActivityAt: nowIso,
239
+ updatedAt: nowIso,
240
+ active: true,
241
+ },
242
+ nowIso
243
+ );
244
+
245
+ await writeAgentSnapshot(snapshotPath, snapshot);
246
+ return {
247
+ ...snapshot,
248
+ snapshotPath,
249
+ };
250
+ }
251
+
252
+ export async function unregisterAgent(
253
+ sessionId,
254
+ agentId,
255
+ { reason = "manual", targetPath = process.cwd() } = {}
256
+ ) {
257
+ const paths = resolveSessionPaths(sessionId, { targetPath });
258
+ const nowIso = new Date().toISOString();
259
+ const snapshotPath = buildAgentSnapshotPath(paths, agentId);
260
+ const existing = await readAgentSnapshot(snapshotPath);
261
+ if (!existing) {
262
+ throw new Error(`Agent '${normalizeString(agentId)}' is not registered in session '${paths.sessionId}'.`);
263
+ }
264
+
265
+ const normalizedReason = normalizeLeaveReason(reason);
266
+ const snapshot = normalizeAgentSnapshot(
267
+ {
268
+ ...existing,
269
+ active: false,
270
+ leftAt: nowIso,
271
+ leaveReason: normalizedReason,
272
+ updatedAt: nowIso,
273
+ },
274
+ nowIso
275
+ );
276
+
277
+ await writeAgentSnapshot(snapshotPath, snapshot);
278
+ await emitAgentEvent(paths.sessionId, "agent_leave", {
279
+ agentId: snapshot.agentId,
280
+ reason: normalizedReason,
281
+ role: snapshot.role,
282
+ model: snapshot.model,
283
+ }, { targetPath });
284
+
285
+ return {
286
+ ...snapshot,
287
+ snapshotPath,
288
+ };
289
+ }
290
+
291
+ export async function listAgents(
292
+ sessionId,
293
+ { targetPath = process.cwd(), includeInactive = true } = {}
294
+ ) {
295
+ const paths = resolveSessionPaths(sessionId, { targetPath });
296
+ let entries = [];
297
+ try {
298
+ entries = await fsp.readdir(paths.agentsDir, { withFileTypes: true });
299
+ } catch (error) {
300
+ if (error && typeof error === "object" && error.code === "ENOENT") {
301
+ return [];
302
+ }
303
+ throw error;
304
+ }
305
+
306
+ const agents = [];
307
+ for (const entry of entries) {
308
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
309
+ const snapshotPath = path.join(paths.agentsDir, entry.name);
310
+ const raw = await readAgentSnapshot(snapshotPath);
311
+ if (!raw) continue;
312
+ const normalized = normalizeAgentSnapshot(raw);
313
+ if (normalized.sessionId !== paths.sessionId) continue;
314
+ if (!includeInactive && normalized.active === false) continue;
315
+ agents.push({
316
+ ...normalized,
317
+ snapshotPath,
318
+ });
319
+ }
320
+
321
+ agents.sort((left, right) => right.lastActivityAt.localeCompare(left.lastActivityAt));
322
+ return agents;
323
+ }
324
+
325
+ export function detectStaleAgents(
326
+ agents,
327
+ {
328
+ idleThresholdSeconds = Number(STUCK_THRESHOLDS?.noToolCallSeconds || 90),
329
+ nowIso = new Date().toISOString(),
330
+ } = {}
331
+ ) {
332
+ const normalizedThreshold = Math.max(1, Math.floor(Number(idleThresholdSeconds || 90)));
333
+ const nowEpoch = Date.parse(normalizeIsoTimestamp(nowIso, new Date().toISOString()));
334
+ if (!Number.isFinite(nowEpoch)) {
335
+ return [];
336
+ }
337
+ const list = Array.isArray(agents) ? agents : [];
338
+ return list
339
+ .map((agent) => normalizeAgentSnapshot(agent, nowIso))
340
+ .filter((agent) => agent.active !== false)
341
+ .map((agent) => {
342
+ const activityEpoch = Date.parse(normalizeIsoTimestamp(agent.lastActivityAt, agent.joinedAt || nowIso));
343
+ const idleSeconds = Number.isFinite(activityEpoch)
344
+ ? Math.max(0, Math.floor((nowEpoch - activityEpoch) / 1000))
345
+ : normalizedThreshold + 1;
346
+ return {
347
+ ...agent,
348
+ idleSeconds,
349
+ };
350
+ })
351
+ .filter((agent) => agent.idleSeconds >= normalizedThreshold);
352
+ }