sentinelayer-cli 0.22.0 → 0.24.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 CHANGED
@@ -101,6 +101,28 @@ Inputs for non-interactive mode:
101
101
 
102
102
  ## Multi-Agent Session Workflow
103
103
 
104
+ Create a managed session (the golden path — one command):
105
+
106
+ ```bash
107
+ sl session start --title "my room" --force-new
108
+ ```
109
+
110
+ `session start` resumes this workspace's most recent active session when it was
111
+ active within the last hour (`--force-new` always mints a fresh one) and spawns
112
+ the **detached Senti daemon** that manages the room — agent greetings, mention
113
+ routing, recaps, durable checkpoints — surviving your terminal. `--no-daemon`
114
+ opts out; `sl session daemon <id>` runs the manager in the foreground. One
115
+ daemon per session is enforced via `senti-daemon.json` in the session directory
116
+ (logs in `senti-daemon.log` next to it), and the daemon exits on its own when
117
+ the session expires.
118
+
119
+ Then point your agents at it:
120
+
121
+ ```bash
122
+ sl session join <session-id> --agent <agent-name>
123
+ sl session say <session-id> "status: starting on auth middleware" --agent <agent-name>
124
+ ```
125
+
104
126
  Sentinelayer includes a deterministic session coordination surface for multi-agent coding loops:
105
127
 
106
128
  - session event stream and replay (`start`, `join`, `say`, `read`, `status`, `leave`, `list`, `kill`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sentinelayer-cli",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Scaffold Sentinelayer spec/prompt/guide artifacts with secure browser auth and token bootstrap.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -753,6 +753,23 @@ export async function runAuditOrchestrator({
753
753
  });
754
754
  const agentPath = path.join(agentsDirectory, `${agent.id}.json`);
755
755
  await fsp.writeFile(agentPath, `${JSON.stringify(result, null, 2)}\n`, "utf-8");
756
+ emitAuditLifecycleEvent(
757
+ onEvent,
758
+ runId,
759
+ "agent_complete",
760
+ {
761
+ phase: "dispatch",
762
+ agentId: agent.id,
763
+ persona: agent.persona,
764
+ domain: agent.domain,
765
+ status: agentStatus,
766
+ findingCount: findings.length,
767
+ summary,
768
+ confidence,
769
+ durationMs: result.durationMs,
770
+ },
771
+ `${agent.id} persona complete: ${findings.length} finding(s).`
772
+ );
756
773
  return {
757
774
  ...result,
758
775
  artifactPath: agentPath,
@@ -9,6 +9,7 @@ import { writeAuditComparisonArtifact } from "../audit/replay.js";
9
9
  import { loadAuditRegistry, selectAuditAgents } from "../audit/registry.js";
10
10
  import { resolveOutputRoot } from "../config/service.js";
11
11
  import { createAgentEvent } from "../events/schema.js";
12
+ import { createAuditSessionReporter, resolveAuditSessionId } from "../session/audit-reporter.js";
12
13
  import { buildLegacyArgs } from "./legacy-args.js";
13
14
 
14
15
  function shouldEmitJson(options, command) {
@@ -89,6 +90,11 @@ export function registerAuditCommand(program, invokeLegacy) {
89
90
  .option("--no-seed-from-deterministic", "Run personas without deterministic baseline or specialist seed findings")
90
91
  .option("--reuse-omargate <runId>", "Reuse deterministic findings from an OmarGate run id or latest")
91
92
  .option("--stream", "Emit NDJSON agent events to stdout")
93
+ .option(
94
+ "--session <id>",
95
+ "Senti session id to relay audit progress into (defaults to the workspace's most recent active session)"
96
+ )
97
+ .option("--no-session", "Disable senti session progress relay")
92
98
  .option("--json", "Emit machine-readable output")
93
99
  .action(async (targetPathArg, options, command) => {
94
100
  const emitJson = shouldEmitJson(options, command);
@@ -105,18 +111,49 @@ export function registerAuditCommand(program, invokeLegacy) {
105
111
  throw new Error("No agents selected for audit run.");
106
112
  }
107
113
 
108
- const result = await runAuditOrchestrator({
114
+ const auditSessionId = await resolveAuditSessionId({
115
+ targetPath,
116
+ explicitSessionId: typeof options.session === "string" ? options.session : "",
117
+ disabled: options.session === false,
118
+ });
119
+ const sessionReporter = createAuditSessionReporter({
120
+ sessionId: auditSessionId,
109
121
  targetPath,
110
- agents: selected.selected,
111
- maxParallel: parseMaxParallel(options.maxParallel),
112
- outputDir: options.outputDir,
113
- dryRun: Boolean(options.dryRun),
114
- refreshIngest: Boolean(options.refresh),
115
- isolation: parseIsolationMode(options.isolation),
116
- seedFromDeterministic: options.seedFromDeterministic !== false,
117
- reuseOmarGate: options.reuseOmargate,
118
- onEvent: buildAuditOrchestratorEventHandler(emitStream),
119
122
  });
123
+ const streamHandler = buildAuditOrchestratorEventHandler(emitStream);
124
+ const onEvent =
125
+ streamHandler || sessionReporter
126
+ ? (evt) => {
127
+ if (streamHandler) {
128
+ streamHandler(evt);
129
+ }
130
+ if (sessionReporter) {
131
+ sessionReporter.handleEvent(evt);
132
+ }
133
+ }
134
+ : null;
135
+
136
+ let result;
137
+ try {
138
+ result = await runAuditOrchestrator({
139
+ targetPath,
140
+ agents: selected.selected,
141
+ maxParallel: parseMaxParallel(options.maxParallel),
142
+ outputDir: options.outputDir,
143
+ dryRun: Boolean(options.dryRun),
144
+ refreshIngest: Boolean(options.refresh),
145
+ isolation: parseIsolationMode(options.isolation),
146
+ seedFromDeterministic: options.seedFromDeterministic !== false,
147
+ reuseOmarGate: options.reuseOmargate,
148
+ onEvent,
149
+ });
150
+ } catch (error) {
151
+ if (sessionReporter) {
152
+ await sessionReporter.failed(error);
153
+ }
154
+ throw error;
155
+ }
156
+ const sessionRelay = sessionReporter ? await sessionReporter.completed(result) : null;
120
157
 
121
158
  const payload = {
122
159
  command: "audit",
@@ -144,12 +181,23 @@ export function registerAuditCommand(program, invokeLegacy) {
144
181
  ddPackageFindingsPath: result.ddPackage?.findingsIndexPath || "",
145
182
  ddPackageSummaryPath: result.ddPackage?.executiveSummaryPath || "",
146
183
  ingestRefresh: result.ingest?.refresh || null,
184
+ sessionId: auditSessionId || "",
185
+ sessionRelay: sessionRelay || null,
147
186
  };
148
187
 
149
188
  if (emitJson) {
150
189
  console.log(JSON.stringify(payload, null, 2));
151
190
  } else if (!emitStream) {
152
191
  printAuditSummary(result);
192
+ if (auditSessionId) {
193
+ console.log(
194
+ pc.gray(
195
+ `Senti session: ${auditSessionId} (posted ${sessionRelay?.posted ?? 0} update(s)${
196
+ sessionRelay?.failed ? `, ${sessionRelay.failed} failed` : ""
197
+ })`
198
+ )
199
+ );
200
+ }
153
201
  }
154
202
 
155
203
  if (result.summary.blocking) {
@@ -4,6 +4,13 @@ import path from "node:path";
4
4
 
5
5
  import pc from "picocolors";
6
6
 
7
+ import {
8
+ createMultiProviderApiClient,
9
+ resolveApiKey,
10
+ resolveModel,
11
+ resolveProvider,
12
+ } from "../ai/client.js";
13
+ import { enrichGuideTickets } from "../guide/enrich.js";
7
14
  import {
8
15
  defaultGuideExportFileName,
9
16
  generateBuildGuide,
@@ -12,6 +19,40 @@ import {
12
19
  } from "../guide/generator.js";
13
20
  import { renderTerminalMarkdown } from "../ui/markdown.js";
14
21
 
22
+ // Optionally split each phase into per-PR tickets with an LLM. Best-effort:
23
+ // any failure leaves the deterministic tickets untouched. Returns the number
24
+ // of phases enriched (0 when disabled or unavailable).
25
+ async function maybeEnrichGuide(guideDoc, options) {
26
+ if (!options || !options.enrich) return 0;
27
+ try {
28
+ const provider = resolveProvider({ provider: options.provider });
29
+ const model = resolveModel({ provider, model: options.model });
30
+ const apiKey = resolveApiKey({ provider, explicitApiKey: options.apiKey });
31
+ if (!apiKey) {
32
+ console.error(
33
+ pc.yellow(`! --enrich skipped: no API key for provider '${provider}'. Set the provider key or pass --api-key.`)
34
+ );
35
+ return 0;
36
+ }
37
+ const limits = {};
38
+ if (options.maxPhases) limits.maxPhases = Number(options.maxPhases);
39
+ if (options.maxPrsPerPhase) limits.maxTicketsPerPhase = Number(options.maxPrsPerPhase);
40
+ const { tickets, enrichedPhases } = await enrichGuideTickets({
41
+ guide: guideDoc,
42
+ client: createMultiProviderApiClient(),
43
+ provider,
44
+ model,
45
+ apiKey,
46
+ limits,
47
+ });
48
+ if (enrichedPhases > 0) guideDoc.tickets = tickets;
49
+ return enrichedPhases;
50
+ } catch (error) {
51
+ console.error(pc.yellow(`! --enrich failed, using deterministic tickets: ${error?.message || error}`));
52
+ return 0;
53
+ }
54
+ }
55
+
15
56
  function shouldEmitJson(options, command) {
16
57
  const local = Boolean(options && options.json);
17
58
  const globalFromCommand =
@@ -101,6 +142,12 @@ export function registerGuideCommand(program) {
101
142
  .option("--path <path>", "Target workspace path", ".")
102
143
  .option("--spec-file <path>", "Spec file path relative to --path")
103
144
  .option("--output-file <path>", "Output export file path relative to --path")
145
+ .option("--enrich", "Split each phase into per-PR tickets with an LLM (opt-in, capped)")
146
+ .option("--provider <provider>", "LLM provider for --enrich (openai|anthropic|google)")
147
+ .option("--model <model>", "LLM model for --enrich")
148
+ .option("--api-key <key>", "Explicit API key for --enrich (else from env)")
149
+ .option("--max-phases <n>", "Cap how many phases --enrich expands")
150
+ .option("--max-prs-per-phase <n>", "Cap PRs per phase for --enrich")
104
151
  .option("--json", "Emit machine-readable output")
105
152
  .action(async (options, command) => {
106
153
  const targetPath = path.resolve(process.cwd(), String(options.path || "."));
@@ -116,6 +163,7 @@ export function registerGuideCommand(program) {
116
163
  projectPath: targetPath,
117
164
  specPath,
118
165
  });
166
+ const enrichedPhases = await maybeEnrichGuide(guideDoc, options);
119
167
  const exportBody = renderGuideExport({ format, guide: guideDoc });
120
168
 
121
169
  await fsp.mkdir(path.dirname(outputPath), { recursive: true });
@@ -128,6 +176,7 @@ export function registerGuideCommand(program) {
128
176
  specPath,
129
177
  outputPath,
130
178
  issueCount: guideDoc.tickets.length,
179
+ enrichedPhases,
131
180
  };
132
181
 
133
182
  if (shouldEmitJson(options, command)) {
@@ -139,6 +188,9 @@ export function registerGuideCommand(program) {
139
188
  console.log(pc.gray(`Format: ${format}`));
140
189
  console.log(pc.gray(`Output: ${outputPath}`));
141
190
  console.log(pc.gray(`Issues: ${guideDoc.tickets.length}`));
191
+ if (enrichedPhases > 0) {
192
+ console.log(pc.gray(`LLM-enriched phases: ${enrichedPhases}`));
193
+ }
142
194
  });
143
195
 
144
196
  guide