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
@@ -439,12 +439,102 @@ function deriveGlobalAcceptanceCriteria(projectType) {
439
439
  ];
440
440
  }
441
441
 
442
+ function countAgentInstructions(agentsMarkdown) {
443
+ const markdown = String(agentsMarkdown || "");
444
+ if (!markdown) {
445
+ return 0;
446
+ }
447
+
448
+ const lines = markdown.split(/\r?\n/);
449
+ let inAgentsSection = false;
450
+ let sectionCount = 0;
451
+ let globalCount = 0;
452
+ for (const rawLine of lines) {
453
+ const line = String(rawLine || "");
454
+ const trimmed = line.trim();
455
+ if (!trimmed) {
456
+ continue;
457
+ }
458
+
459
+ if (/^#{1,6}\s+.*\bagents?\b/i.test(trimmed)) {
460
+ inAgentsSection = true;
461
+ continue;
462
+ }
463
+ if (inAgentsSection && /^#{1,6}\s+/.test(trimmed)) {
464
+ inAgentsSection = false;
465
+ }
466
+
467
+ if (!/^\s*[-*]\s+/.test(line)) {
468
+ continue;
469
+ }
470
+
471
+ if (/\b(agent|coder|reviewer|tester|observer|persona|daemon)\b/i.test(trimmed)) {
472
+ globalCount += 1;
473
+ if (inAgentsSection) {
474
+ sectionCount += 1;
475
+ }
476
+ }
477
+ }
478
+
479
+ if (sectionCount >= 2) {
480
+ return sectionCount;
481
+ }
482
+ return globalCount;
483
+ }
484
+
485
+ function hasCollaborationSignals(description) {
486
+ const normalized = String(description || "").toLowerCase();
487
+ if (!normalized) {
488
+ return false;
489
+ }
490
+ return /\b(team|pair|paired|multi-agent|multi agent|swarm|collaborat)\b/.test(normalized);
491
+ }
492
+
493
+ function shouldIncludeCoordinationPhase({
494
+ description = "",
495
+ agentsMarkdown = "",
496
+ sessionActive = false,
497
+ } = {}) {
498
+ if (sessionActive === true) {
499
+ return true;
500
+ }
501
+ if (hasCollaborationSignals(description)) {
502
+ return true;
503
+ }
504
+ return countAgentInstructions(agentsMarkdown) >= 2;
505
+ }
506
+
507
+ function buildCoordinationPhase(phaseNumber, previousPhaseTitle = "") {
508
+ return {
509
+ title: `Phase ${phaseNumber}: Multi-Agent Coordination Protocol`,
510
+ items: [
511
+ "Check for active sessions: `sl session list`.",
512
+ "If a session exists, join it: `sl session join <id> --name <your-name> --role coder`.",
513
+ "Emit status updates every 5 minutes: `sl session say <id> \"status: <what you're doing>\"`.",
514
+ "Before modifying a shared file, check recent session activity for that file.",
515
+ "On unexpected file changes, ask in-session instead of stopping: `sl session say <id> \"help: <question>\"`.",
516
+ "Post findings in-session: `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"`.",
517
+ "On completion, update `tasks/todo.md` and emit completion status in-session.",
518
+ "Leave the session when done: `sl session leave <id>`.",
519
+ ],
520
+ dependencies: previousPhaseTitle ? [previousPhaseTitle] : [],
521
+ effort: "4-8 hours",
522
+ acceptanceCriteria: [
523
+ "Session participation path is explicit for all collaborating agents.",
524
+ "Unexpected file-change handling favors in-session coordination over stop-and-wait behavior.",
525
+ "Status and finding updates are emitted with actionable file-level context.",
526
+ ],
527
+ };
528
+ }
529
+
442
530
  export function generateSpecMarkdown({
443
531
  template,
444
532
  description,
445
533
  ingest,
446
534
  projectPath,
447
535
  projectType,
536
+ agentsMarkdown = "",
537
+ sessionActive = false,
448
538
  generatedAt = new Date().toISOString(),
449
539
  } = {}) {
450
540
  const resolvedTemplate = template || getDefaultTemplate();
@@ -471,6 +561,16 @@ export function generateSpecMarkdown({
471
561
  description,
472
562
  });
473
563
 
564
+ if (
565
+ shouldIncludeCoordinationPhase({
566
+ description,
567
+ agentsMarkdown,
568
+ sessionActive,
569
+ })
570
+ ) {
571
+ phases.push(buildCoordinationPhase(phases.length + 1, phases[phases.length - 1]?.title || ""));
572
+ }
573
+
474
574
  const phaseMarkdown = phases
475
575
  .map(
476
576
  (phase) =>
@@ -1,4 +1,6 @@
1
1
  import pc from "picocolors";
2
+ import crypto from "node:crypto";
3
+ import process from "node:process";
2
4
 
3
5
  /**
4
6
  * Session Tracker — tracks tokens, tool calls, cost, and time per CLI run.
@@ -10,14 +12,100 @@ import pc from "picocolors";
10
12
  * - Print summary on completion
11
13
  */
12
14
 
13
- let SESSION = null;
15
+ const SESSIONS = new Map();
16
+ let ACTIVE_SESSION_ID = null;
17
+ const MAX_SESSIONS = 50;
18
+ const SESSION_TTL_MS = 60 * 60 * 1000;
19
+ const VERBOSE_TELEMETRY_ENV = "SENTINELAYER_VERBOSE_TELEMETRY";
20
+ const DEBUG_ERRORS_ENV = "SENTINELAYER_DEBUG_ERRORS";
21
+ const UNMASK_TRACE_ID_ENV = "SENTINELAYER_UNMASK_TRACE_ID";
22
+ const EMIT_TRACE_ID_ENV = "SENTINELAYER_EMIT_TRACE_ID";
23
+
24
+ function isTruthyEnvFlag(value) {
25
+ const normalized = String(value || "").trim().toLowerCase();
26
+ return normalized === "true" || normalized === "1" || normalized === "yes";
27
+ }
28
+
29
+ function normalizeNonNegativeNumber(value) {
30
+ const normalized = Number(value);
31
+ if (!Number.isFinite(normalized) || normalized < 0) {
32
+ return 0;
33
+ }
34
+ return normalized;
35
+ }
36
+
37
+ function resolveSession(sessionId) {
38
+ const resolvedId = String(sessionId || ACTIVE_SESSION_ID || "").trim();
39
+ if (!resolvedId) {
40
+ return null;
41
+ }
42
+ return SESSIONS.get(resolvedId) || null;
43
+ }
44
+
45
+ function pruneSessions(now = Date.now()) {
46
+ for (const [sessionId, session] of SESSIONS.entries()) {
47
+ if (!session || !session.startedAt) continue;
48
+ if (now - session.startedAt > SESSION_TTL_MS) {
49
+ SESSIONS.delete(sessionId);
50
+ }
51
+ }
52
+ while (SESSIONS.size > MAX_SESSIONS) {
53
+ const oldestKey = SESSIONS.keys().next().value;
54
+ if (!oldestKey) break;
55
+ SESSIONS.delete(oldestKey);
56
+ }
57
+ }
58
+
59
+ function shouldExposeTraceId() {
60
+ const verbose = isTruthyEnvFlag(process.env[VERBOSE_TELEMETRY_ENV]);
61
+ const debug = isTruthyEnvFlag(process.env[DEBUG_ERRORS_ENV]);
62
+ if (!verbose && !debug) {
63
+ return false;
64
+ }
65
+ if (!isTruthyEnvFlag(process.env[UNMASK_TRACE_ID_ENV])) {
66
+ return false;
67
+ }
68
+ const nodeEnv = String(process.env.NODE_ENV || "").trim().toLowerCase();
69
+ if (nodeEnv !== "development" && nodeEnv !== "test") {
70
+ return false;
71
+ }
72
+ return Boolean(process.stderr && process.stderr.isTTY);
73
+ }
74
+
75
+ function shouldEmitTraceId() {
76
+ if (!isTruthyEnvFlag(process.env[EMIT_TRACE_ID_ENV])) {
77
+ return false;
78
+ }
79
+ return Boolean(process.stderr && process.stderr.isTTY);
80
+ }
81
+
82
+ function maskTraceId(traceId) {
83
+ const normalized = String(traceId || "").trim();
84
+ if (normalized.length <= 8) {
85
+ return "trace_id=****";
86
+ }
87
+ const prefix = normalized.slice(0, 4);
88
+ const suffix = normalized.slice(-4);
89
+ return `trace_id=${prefix}…${suffix}`;
90
+ }
14
91
 
15
92
  /**
16
93
  * Initialize a new tracking session.
17
94
  * Call this at the start of any auditable command.
18
95
  */
19
96
  export function startSession(command) {
20
- SESSION = {
97
+ pruneSessions();
98
+ let sessionId;
99
+ try {
100
+ sessionId = crypto.randomUUID();
101
+ } catch {
102
+ const ts = Date.now().toString(36);
103
+ const rand = crypto.randomBytes(16).toString("hex");
104
+ sessionId = `sess-${ts}-${rand}`;
105
+ }
106
+ const session = {
107
+ id: sessionId,
108
+ traceId: sessionId,
21
109
  command: command || "unknown",
22
110
  startedAt: Date.now(),
23
111
  inputTokens: 0,
@@ -27,55 +115,79 @@ export function startSession(command) {
27
115
  llmCalls: 0,
28
116
  findings: { P0: 0, P1: 0, P2: 0, P3: 0 },
29
117
  };
30
- return SESSION;
118
+ SESSIONS.set(sessionId, session);
119
+ ACTIVE_SESSION_ID = sessionId;
120
+ return session;
121
+ }
122
+
123
+ export function endSession({ sessionId } = {}) {
124
+ const resolvedId = String(sessionId || ACTIVE_SESSION_ID || "").trim();
125
+ if (!resolvedId) return false;
126
+ const existed = SESSIONS.delete(resolvedId);
127
+ if (ACTIVE_SESSION_ID === resolvedId) {
128
+ ACTIVE_SESSION_ID = null;
129
+ }
130
+ return existed;
31
131
  }
32
132
 
33
133
  /**
34
134
  * Record token usage from an LLM call.
35
135
  */
36
- export function recordLlmUsage({ inputTokens = 0, outputTokens = 0, costUsd = 0 } = {}) {
37
- if (!SESSION) return;
38
- SESSION.inputTokens += inputTokens;
39
- SESSION.outputTokens += outputTokens;
40
- SESSION.costUsd += costUsd;
41
- SESSION.llmCalls += 1;
136
+ export function recordLlmUsage({ inputTokens = 0, outputTokens = 0, costUsd = 0, sessionId } = {}) {
137
+ const session = resolveSession(sessionId);
138
+ if (!session) return;
139
+ const safeInput = normalizeNonNegativeNumber(inputTokens);
140
+ const safeOutput = normalizeNonNegativeNumber(outputTokens);
141
+ const safeCost = normalizeNonNegativeNumber(costUsd);
142
+ session.inputTokens += safeInput;
143
+ session.outputTokens += safeOutput;
144
+ session.costUsd += safeCost;
145
+ session.llmCalls += 1;
42
146
  }
43
147
 
44
148
  /**
45
149
  * Record a tool call.
46
150
  */
47
- export function recordToolCall() {
48
- if (!SESSION) return;
49
- SESSION.toolCalls += 1;
151
+ export function recordToolCall({ sessionId } = {}) {
152
+ const session = resolveSession(sessionId);
153
+ if (!session) return;
154
+ session.toolCalls += 1;
50
155
  }
51
156
 
52
157
  /**
53
158
  * Record findings.
54
159
  */
55
- export function recordFindings(summary) {
56
- if (!SESSION) return;
57
- if (summary?.P0) SESSION.findings.P0 += summary.P0;
58
- if (summary?.P1) SESSION.findings.P1 += summary.P1;
59
- if (summary?.P2) SESSION.findings.P2 += summary.P2;
60
- if (summary?.P3) SESSION.findings.P3 += summary.P3;
160
+ export function recordFindings(summary, { sessionId } = {}) {
161
+ const session = resolveSession(sessionId);
162
+ if (!session) return;
163
+ const p0 = normalizeNonNegativeNumber(summary?.P0);
164
+ const p1 = normalizeNonNegativeNumber(summary?.P1);
165
+ const p2 = normalizeNonNegativeNumber(summary?.P2);
166
+ const p3 = normalizeNonNegativeNumber(summary?.P3);
167
+ if (p0) session.findings.P0 += p0;
168
+ if (p1) session.findings.P1 += p1;
169
+ if (p2) session.findings.P2 += p2;
170
+ if (p3) session.findings.P3 += p3;
61
171
  }
62
172
 
63
173
  /**
64
174
  * Get the current session summary.
65
175
  */
66
- export function getSessionSummary() {
67
- if (!SESSION) return null;
68
- const durationMs = Date.now() - SESSION.startedAt;
176
+ export function getSessionSummary({ sessionId } = {}) {
177
+ const session = resolveSession(sessionId);
178
+ if (!session) return null;
179
+ const durationMs = Date.now() - session.startedAt;
69
180
  return {
70
- command: SESSION.command,
181
+ traceId: session.traceId || session.id,
182
+ command: session.command,
71
183
  durationMs,
72
- inputTokens: SESSION.inputTokens,
73
- outputTokens: SESSION.outputTokens,
74
- totalTokens: SESSION.inputTokens + SESSION.outputTokens,
75
- costUsd: SESSION.costUsd,
76
- toolCalls: SESSION.toolCalls,
77
- llmCalls: SESSION.llmCalls,
78
- findings: { ...SESSION.findings },
184
+ inputTokens: session.inputTokens,
185
+ outputTokens: session.outputTokens,
186
+ totalTokens: session.inputTokens + session.outputTokens,
187
+ costUsd: session.costUsd,
188
+ toolCalls: session.toolCalls,
189
+ llmCalls: session.llmCalls,
190
+ findings: { ...session.findings },
79
191
  };
80
192
  }
81
193
 
@@ -83,8 +195,8 @@ export function getSessionSummary() {
83
195
  * Print the session summary to stderr.
84
196
  * Called at the end of any auditable command.
85
197
  */
86
- export function printSessionSummary() {
87
- const summary = getSessionSummary();
198
+ export function printSessionSummary({ sessionId } = {}) {
199
+ const summary = getSessionSummary({ sessionId });
88
200
  if (!summary) return;
89
201
 
90
202
  const duration = summary.durationMs < 60000
@@ -101,6 +213,10 @@ export function printSessionSummary() {
101
213
  parts.push(pc.white(summary.toolCalls + " tools"));
102
214
  if (summary.costUsd > 0) parts.push(pc.white("$" + summary.costUsd.toFixed(2)));
103
215
  parts.push(pc.white(duration));
216
+ if (summary.traceId && shouldEmitTraceId()) {
217
+ const traceLabel = shouldExposeTraceId() ? `trace_id=${summary.traceId}` : maskTraceId(summary.traceId);
218
+ parts.push(pc.gray(traceLabel));
219
+ }
104
220
 
105
221
  const findingParts = [];
106
222
  if (summary.findings.P0 > 0) findingParts.push(pc.red("P0=" + summary.findings.P0));
@@ -113,6 +229,6 @@ export function printSessionSummary() {
113
229
  process.stderr.write(" | " + findingParts.join(" "));
114
230
  }
115
231
  process.stderr.write("\n");
116
-
232
+ endSession({ sessionId });
117
233
  return summary;
118
234
  }
@@ -3,7 +3,7 @@ import { randomUUID } from "node:crypto";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { dirname, resolve } from "node:path";
6
- import { readStoredSession } from "../auth/session-store.js";
6
+ import { resolveActiveAuthSession } from "../auth/service.js";
7
7
 
8
8
  // Read CLI version from package.json at module load
9
9
  const __filename = fileURLToPath(import.meta.url);
@@ -62,7 +62,11 @@ export async function syncRunToDashboard(runData) {
62
62
 
63
63
  let session;
64
64
  try {
65
- session = await readStoredSession();
65
+ session = await resolveActiveAuthSession({
66
+ cwd: process.cwd(),
67
+ env: process.env,
68
+ autoRotate: false,
69
+ });
66
70
  } catch {
67
71
  return { synced: false, reason: "no_session" };
68
72
  }
@@ -0,0 +1,13 @@
1
+ import process from "node:process";
2
+
3
+ export function preferredCliCommand({ platform = process.platform, env = process.env } = {}) {
4
+ const override = String(env?.SENTINELAYER_CLI_COMMAND || "").trim();
5
+ if (override) {
6
+ return override;
7
+ }
8
+ return platform === "win32" ? "sentinelayer-cli" : "sl";
9
+ }
10
+
11
+ export function authLoginHint({ platform = process.platform, env = process.env } = {}) {
12
+ return `${preferredCliCommand({ platform, env })} auth login`;
13
+ }