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
package/src/legacy-cli.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  resolveCodingAgent,
22
22
  } from "./config/agent-dictionary.js";
23
23
  import { resolveOutputRoot } from "./config/service.js";
24
+ import { normalizeAgentEvent } from "./events/schema.js";
24
25
  import { collectCodebaseIngest, formatIngestSummary } from "./ingest/engine.js";
25
26
  import { getExpressTemplate, getPackageJsonTemplate, buildReadmeContent } from "./scaffold/templates.js";
26
27
  import { generateScaffold } from "./scaffold/generator.js";
@@ -203,6 +204,16 @@ function printUsage() {
203
204
  console.log(" sl auth sessions List stored session metadata");
204
205
  console.log(" sl auth logout Clear local session");
205
206
  console.log("");
207
+ console.log("Session Coordination:");
208
+ console.log(" sl session start --json Create an agent coordination session");
209
+ console.log(" sl session join <id> --name <n> Join a session as an agent");
210
+ console.log(" sl session say <id> \"msg\" --json Append a message event to session stream");
211
+ console.log(" sl session read <id> --tail 20 Read session stream events");
212
+ console.log(" sl session status <id> --json Show session health, agents, runs, leases");
213
+ console.log(" sl session leave <id> Leave a session");
214
+ console.log(" sl session list --json List active sessions");
215
+ console.log(" sl session kill --session <id> --agent <id> Kill agent + revoke active leases");
216
+ console.log("");
206
217
  console.log("Security & Review:");
207
218
  console.log(" sl review scan --path . --json Deterministic code review (full or --mode diff)");
208
219
  console.log(" sl /omargate deep --path . --json Local Omar Gate security scan (P0/P1/P2 findings)");
@@ -809,7 +820,7 @@ function hasCommandOption(args, optionName) {
809
820
  async function collectScanFiles(rootPath) {
810
821
  const files = [];
811
822
  const stack = [rootPath];
812
- const ignoredDirs = new Set([".git", "node_modules", ".venv", ".next", "dist", "build", ".sentinelayer"]);
823
+ const ignoredDirs = new Set([".git", "node_modules", ".venv", ".next", "dist", "build", "out", "coverage", "__pycache__", ".turbo", ".cache", ".parcel-cache", ".svelte-kit", ".nuxt", ".output", ".vercel", ".sentinelayer"]);
813
824
  const maxFileSizeBytes = 512 * 1024;
814
825
 
815
826
  while (stack.length > 0) {
@@ -942,7 +953,151 @@ function formatFindingsMarkdown(findings) {
942
953
  .join("\n");
943
954
  }
944
955
 
956
+ const OMAR_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
957
+ const PERSONA_ICONS = {
958
+ security: "🛡️",
959
+ architecture: "🏗️",
960
+ backend: "⚙️",
961
+ testing: "🧪",
962
+ performance: "⚡",
963
+ compliance: "📋",
964
+ reliability: "🔄",
965
+ release: "🚀",
966
+ observability: "📊",
967
+ infrastructure: "☁️",
968
+ "supply-chain": "📦",
969
+ frontend: "🎨",
970
+ documentation: "📝",
971
+ "ai-governance": "🤖",
972
+ "code-quality": "💎",
973
+ data: "🗄️",
974
+ };
975
+
976
+ function formatElapsed(ms) {
977
+ const seconds = Math.max(0, Math.round(ms / 1000));
978
+ if (seconds < 60) return `${seconds}s`;
979
+ const minutes = Math.floor(seconds / 60);
980
+ const rem = seconds % 60;
981
+ return `${minutes}m${String(rem).padStart(2, "0")}s`;
982
+ }
983
+
984
+ function labelForPersona(payload) {
985
+ const identity = payload?.identity || {};
986
+ const short = identity.shortName || identity.fullName || "";
987
+ const id = payload?.personaId || "persona";
988
+ return short ? `${short} (${id})` : id;
989
+ }
990
+
991
+ function buildOmarTerminalHandler({ startedAt = Date.now() } = {}) {
992
+ let spinIdx = 0;
993
+ let spinInterval = null;
994
+ let currentMessage = "";
995
+
996
+ function startSpinner(msg) {
997
+ currentMessage = msg;
998
+ if (spinInterval) clearInterval(spinInterval);
999
+ spinInterval = setInterval(() => {
1000
+ const frame = OMAR_SPINNER[spinIdx % OMAR_SPINNER.length];
1001
+ process.stderr.write(`\r${pc.cyan(frame)} ${currentMessage}`);
1002
+ spinIdx++;
1003
+ }, 80);
1004
+ }
1005
+
1006
+ function stopSpinner() {
1007
+ if (spinInterval) {
1008
+ clearInterval(spinInterval);
1009
+ spinInterval = null;
1010
+ }
1011
+ process.stderr.write("\r" + " ".repeat(80) + "\r");
1012
+ }
1013
+
1014
+ return (evt) => {
1015
+ const normalizedEvent = normalizeAgentEvent(evt);
1016
+ const event = normalizedEvent?.event || evt?.event || "";
1017
+ const payload = normalizedEvent?.payload || evt?.payload || {};
1018
+ const elapsed = formatElapsed(Date.now() - startedAt);
1019
+
1020
+ switch (event) {
1021
+ case "omargate_start": {
1022
+ const mode = payload.mode || "deep";
1023
+ const roster = Array.isArray(payload.roster) ? payload.roster : [];
1024
+ const count = roster.length || payload.personas?.length || 0;
1025
+ console.error("");
1026
+ console.error(pc.bold(pc.cyan(` Omar Gate AI Analysis (${mode} — ${count} personas) ${pc.gray(`[${elapsed}]`)}`)));
1027
+ console.error(pc.gray(` Budget: $${(payload.maxCostUsd || 5).toFixed(2)} | Parallel: ${payload.maxParallel || 4}`));
1028
+ if (roster.length) {
1029
+ console.error("");
1030
+ console.error(pc.bold(" Roster:"));
1031
+ for (const member of roster) {
1032
+ const icon = PERSONA_ICONS[member.id] || "🔍";
1033
+ console.error(` ${icon} ${pc.white(member.fullName || member.id)} ${pc.gray(`— ${member.domain || member.id}`)}`);
1034
+ }
1035
+ }
1036
+ console.error("");
1037
+ startSpinner("Dispatching personas...");
1038
+ break;
1039
+ }
1040
+ case "persona_start": {
1041
+ const icon = PERSONA_ICONS[payload.personaId] || "🔍";
1042
+ const label = labelForPersona(payload);
1043
+ console.error(` ${icon} ${pc.cyan("→")} Dispatching ${pc.bold(label)} ${pc.gray(`[${elapsed}]`)}`);
1044
+ startSpinner(`${icon} ${label} analyzing...`);
1045
+ break;
1046
+ }
1047
+ case "persona_finding": {
1048
+ stopSpinner();
1049
+ const sev = payload.severity || "P3";
1050
+ const color = sev === "P0" ? pc.red : sev === "P1" ? pc.red : sev === "P2" ? pc.yellow : pc.gray;
1051
+ const icon = PERSONA_ICONS[payload.personaId] || "🔍";
1052
+ console.error(` ${icon} ${color(`[${sev}]`)} ${pc.white(payload.title || payload.message || "finding")} ${pc.gray(`(${payload.file || "?"}:${payload.line || "?"})`)}`);
1053
+ startSpinner(`${icon} ${labelForPersona(payload)} analyzing...`);
1054
+ break;
1055
+ }
1056
+ case "persona_complete": {
1057
+ stopSpinner();
1058
+ const icon = PERSONA_ICONS[payload.personaId] || "🔍";
1059
+ const count = payload.findings || 0;
1060
+ const cost = payload.costUsd || 0;
1061
+ const dur = ((payload.durationMs || 0) / 1000).toFixed(1);
1062
+ const label = labelForPersona(payload);
1063
+ console.error(` ${icon} ${pc.green("✓")} ${label} — ${count} finding${count === 1 ? "" : "s"} ${pc.gray(`($${cost.toFixed(4)}, ${dur}s, elapsed ${elapsed})`)}`);
1064
+ break;
1065
+ }
1066
+ case "persona_skipped": {
1067
+ stopSpinner();
1068
+ const icon = PERSONA_ICONS[payload.personaId] || "🔍";
1069
+ console.error(` ${icon} ${pc.gray("○")} ${labelForPersona(payload)} — skipped (${payload.reason || "budget"})`);
1070
+ break;
1071
+ }
1072
+ case "persona_error": {
1073
+ stopSpinner();
1074
+ const icon = PERSONA_ICONS[payload.personaId] || "🔍";
1075
+ console.error(` ${icon} ${pc.red("✗")} ${labelForPersona(payload)} — error: ${payload.error || "unknown"}`);
1076
+ break;
1077
+ }
1078
+ case "omargate_complete": {
1079
+ stopSpinner();
1080
+ const s = payload.summary || {};
1081
+ const total = payload.findings || 0;
1082
+ const cost = (payload.totalCostUsd || 0).toFixed(4);
1083
+ const dur = ((payload.totalDurationMs || 0) / 1000).toFixed(1);
1084
+ const rec = payload.reconciliation || {};
1085
+ console.error("");
1086
+ console.error(pc.bold(` AI Analysis Complete ${pc.gray(`[${elapsed}]`)}`));
1087
+ console.error(` Findings: ${pc.red(`P0=${s.P0 || 0}`)} ${pc.red(`P1=${s.P1 || 0}`)} ${pc.yellow(`P2=${s.P2 || 0}`)} ${pc.gray(`P3=${s.P3 || 0}`)} (${total} total)`);
1088
+ if (rec.deterministicFindings !== undefined) {
1089
+ console.error(pc.gray(` Reconciled: ${rec.deterministicFindings} deterministic + ${rec.aiFindings} AI → ${rec.reconciledFindings} unique (${rec.multiSourceFindings || 0} confirmed by multiple layers)`));
1090
+ }
1091
+ console.error(` Cost: $${cost} | Duration: ${dur}s | Personas: ${payload.personaCount || 0}`);
1092
+ console.error("");
1093
+ break;
1094
+ }
1095
+ }
1096
+ };
1097
+ }
1098
+
945
1099
  async function runLocalOmarGateCommand(args) {
1100
+ const commandStartedAt = Date.now();
946
1101
  const mode = String(args[0] || "").trim().toLowerCase();
947
1102
  if (mode && mode !== "deep") {
948
1103
  throw new Error(`Unsupported /omargate mode '${mode}'. Use: /omargate deep`);
@@ -950,6 +1105,14 @@ async function runLocalOmarGateCommand(args) {
950
1105
  const asJson = hasCommandOption(args, "--json");
951
1106
  const pathArg = getCommandOptionValue(args, "--path") || ".";
952
1107
  const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
1108
+ const aiEnabled = !hasCommandOption(args, "--no-ai");
1109
+ const aiDryRun = hasCommandOption(args, "--ai-dry-run");
1110
+ const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "5.0") || 5.0;
1111
+ const modelOverride = getCommandOptionValue(args, "--model") || "";
1112
+ const providerOverride = getCommandOptionValue(args, "--provider") || "";
1113
+ const scanMode = getCommandOptionValue(args, "--scan-mode") || "deep";
1114
+ const maxParallel = parseInt(getCommandOptionValue(args, "--max-parallel") || "4", 10) || 4;
1115
+ const streamEnabled = hasCommandOption(args, "--stream");
953
1116
  const targetPath = path.resolve(process.cwd(), pathArg);
954
1117
  if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
955
1118
  throw new Error(`Invalid --path target: ${targetPath}`);
@@ -958,21 +1121,208 @@ async function runLocalOmarGateCommand(args) {
958
1121
  if (!asJson) {
959
1122
  printSection("Local Omar Gate Deep");
960
1123
  printInfo(`Target: ${targetPath}`);
1124
+ printInfo(`Scan mode: ${scanMode} | AI: ${aiEnabled ? "enabled" : "disabled"}`);
1125
+ console.error("");
1126
+ console.error(pc.gray(` [${formatElapsed(Date.now() - commandStartedAt)}] Phase 1: Deterministic analysis (22 rules)...`));
961
1127
  }
962
1128
 
963
- const scan = await runCredentialScan(targetPath);
1129
+ // Phase 1: Full 22-rule deterministic pipeline (replaces legacy 5-rule credential scan)
1130
+ const { runDeterministicReviewPipeline } = await import("./review/local-review.js");
1131
+ const deterministic = await runDeterministicReviewPipeline({
1132
+ targetPath,
1133
+ mode: "full",
1134
+ outputDir: outputDirArg,
1135
+ });
1136
+
1137
+ const detFindings = deterministic.findings || [];
1138
+ const detSummary = deterministic.summary || { P0: 0, P1: 0, P2: 0, P3: 0, blocking: false };
1139
+ const scannedFiles = deterministic.metadata?.ingest?.filesScanned || deterministic.metadata?.scannedFiles || detFindings.length;
1140
+
1141
+ if (!asJson) {
1142
+ console.error(` ${pc.green("✓")} [${formatElapsed(Date.now() - commandStartedAt)}] Deterministic: ${scannedFiles} files → P1=${detSummary.P1} P2=${detSummary.P2} findings`);
1143
+ if (aiEnabled) {
1144
+ console.error("");
1145
+ console.error(pc.gray(` [${formatElapsed(Date.now() - commandStartedAt)}] Phase 2: AI persona analysis via LLM...`));
1146
+ }
1147
+ }
1148
+
1149
+ // Phase 2: AI review layer (optional, default enabled)
1150
+ let aiResult = null;
1151
+ let orchestratorResult = null;
1152
+ if (aiEnabled && scanMode) {
1153
+ // Multi-persona orchestrator mode
1154
+ try {
1155
+ const { runOmarGateOrchestrator } = await import("./review/omargate-orchestrator.js");
1156
+ const terminalHandler = (!asJson && !streamEnabled)
1157
+ ? buildOmarTerminalHandler({ startedAt: commandStartedAt })
1158
+ : null;
1159
+ const streamHandler = streamEnabled
1160
+ ? (evt) => console.log(JSON.stringify(evt))
1161
+ : terminalHandler;
1162
+
1163
+ orchestratorResult = await runOmarGateOrchestrator({
1164
+ targetPath,
1165
+ scanMode,
1166
+ maxParallel,
1167
+ provider: providerOverride || undefined,
1168
+ model: modelOverride || undefined,
1169
+ maxCostUsd,
1170
+ dryRun: aiDryRun,
1171
+ outputDir: outputDirArg,
1172
+ deterministic: {
1173
+ summary: detSummary,
1174
+ findings: detFindings,
1175
+ metadata: deterministic.metadata || {},
1176
+ },
1177
+ onEvent: streamHandler,
1178
+ });
1179
+
1180
+ // Use orchestrator results as the AI layer. aiResult represents ONLY
1181
+ // the AI contribution — reconciliation is a separate top-level view.
1182
+ const personaErrors = (orchestratorResult.personas || []).filter((p) => p.status === "error" || p.error);
1183
+ const aiOnlyFindings = (orchestratorResult.findings || []).filter(
1184
+ (f) => Array.isArray(f.sources) && f.sources.includes("ai")
1185
+ );
1186
+ const aiOnlySummary = {
1187
+ P0: aiOnlyFindings.filter((f) => f.severity === "P0").length,
1188
+ P1: aiOnlyFindings.filter((f) => f.severity === "P1").length,
1189
+ P2: aiOnlyFindings.filter((f) => f.severity === "P2").length,
1190
+ P3: aiOnlyFindings.filter((f) => f.severity === "P3").length,
1191
+ };
1192
+ aiResult = {
1193
+ findings: aiOnlyFindings,
1194
+ summary: aiOnlySummary,
1195
+ costUsd: orchestratorResult.totalCostUsd || 0,
1196
+ model: modelOverride || "multi-persona",
1197
+ provider: providerOverride || "sentinelayer",
1198
+ dryRun: aiDryRun,
1199
+ personas: (orchestratorResult.personas || []).map((p) => ({
1200
+ id: p.id || p.personaId,
1201
+ identity: p.identity || null,
1202
+ status: p.status,
1203
+ findings: p.findings || 0,
1204
+ costUsd: p.costUsd || 0,
1205
+ durationMs: p.durationMs || 0,
1206
+ error: p.error || null,
1207
+ })),
1208
+ errors: personaErrors.length > 0
1209
+ ? personaErrors.map((p) => `${p.id || p.personaId}: ${p.error}`).join("; ")
1210
+ : null,
1211
+ };
1212
+ } catch (aiError) {
1213
+ if (!asJson) {
1214
+ console.log(pc.yellow(`Orchestrator skipped: ${aiError.message}`));
1215
+ }
1216
+ aiResult = { skipped: true, reason: aiError.message, findings: [], summary: { P0: 0, P1: 0, P2: 0, P3: 0 } };
1217
+ }
1218
+ } else if (aiEnabled) {
1219
+ // Single AI review layer (legacy, no --scan-mode)
1220
+ try {
1221
+ const { runAiReviewLayer } = await import("./review/ai-review.js");
1222
+ aiResult = await runAiReviewLayer({
1223
+ targetPath,
1224
+ mode: "full",
1225
+ runId: deterministic.metadata?.runId || `omargate-${nowIso()}`,
1226
+ runDirectory: deterministic.artifacts?.runDirectory || targetPath,
1227
+ deterministic: {
1228
+ summary: detSummary,
1229
+ findings: detFindings,
1230
+ metadata: deterministic.metadata || {},
1231
+ },
1232
+ outputDir: outputDirArg,
1233
+ provider: providerOverride || undefined,
1234
+ model: modelOverride || undefined,
1235
+ maxCostUsd,
1236
+ dryRun: aiDryRun,
1237
+ env: process.env,
1238
+ });
1239
+ } catch (aiError) {
1240
+ if (!asJson) {
1241
+ console.log(pc.yellow(`AI review layer skipped: ${aiError.message}`));
1242
+ }
1243
+ aiResult = { skipped: true, reason: aiError.message, findings: [], summary: { P0: 0, P1: 0, P2: 0, P3: 0 } };
1244
+ }
1245
+ }
1246
+
1247
+ // Reconciled findings: orchestrator already merges+dedupes deterministic+AI
1248
+ // via reconcileReviewFindings. Use its output directly to avoid double-counting.
1249
+ // Fallback path (legacy single ai-review): union of det + AI findings.
1250
+ const aiFindings = aiResult?.findings || [];
1251
+ const reconciledFromOrchestrator = orchestratorResult?.findings;
1252
+ const allFindings = reconciledFromOrchestrator && reconciledFromOrchestrator.length >= 0
1253
+ ? reconciledFromOrchestrator
1254
+ : [...detFindings, ...aiFindings];
1255
+ const combinedSummary = orchestratorResult?.summary || {
1256
+ P0: detSummary.P0 + (aiResult?.summary?.P0 || 0),
1257
+ P1: detSummary.P1 + (aiResult?.summary?.P1 || 0),
1258
+ P2: detSummary.P2 + (aiResult?.summary?.P2 || 0),
1259
+ P3: (detSummary.P3 || 0) + (aiResult?.summary?.P3 || 0),
1260
+ blocking: (detSummary.P0 + (aiResult?.summary?.P0 || 0)) > 0 ||
1261
+ (detSummary.P1 + (aiResult?.summary?.P1 || 0)) > 0,
1262
+ };
1263
+ const combinedP0 = combinedSummary.P0 || 0;
1264
+ const combinedP1 = combinedSummary.P1 || 0;
1265
+ const combinedP2 = combinedSummary.P2 || 0;
1266
+ const combinedP3 = combinedSummary.P3 || 0;
1267
+
1268
+ // Write per-phase artifacts alongside REVIEW_DETERMINISTIC so post-mortems
1269
+ // can inspect exactly what each layer contributed.
1270
+ const reviewDir = deterministic?.artifacts?.runDirectory || "";
1271
+ const writeJsonArtifact = async (name, payload) => {
1272
+ if (!reviewDir) return null;
1273
+ try {
1274
+ const fp = path.join(reviewDir, name);
1275
+ await fsp.writeFile(fp, JSON.stringify(payload, null, 2), "utf-8");
1276
+ return fp;
1277
+ } catch {
1278
+ return null;
1279
+ }
1280
+ };
1281
+ const artifactPaths = {};
1282
+ if (orchestratorResult) {
1283
+ artifactPaths.ai = await writeJsonArtifact("REVIEW_AI.json", {
1284
+ runId: orchestratorResult.runId,
1285
+ mode: orchestratorResult.mode,
1286
+ roster: orchestratorResult.roster,
1287
+ findings: (orchestratorResult.personas || []).flatMap((p) => []),
1288
+ aiFindings: aiFindings,
1289
+ personaCount: (orchestratorResult.personas || []).length,
1290
+ totalCostUsd: orchestratorResult.totalCostUsd,
1291
+ totalDurationMs: orchestratorResult.totalDurationMs,
1292
+ });
1293
+ artifactPaths.personas = await writeJsonArtifact("REVIEW_PERSONAS.json", {
1294
+ runId: orchestratorResult.runId,
1295
+ personas: orchestratorResult.personas || [],
1296
+ });
1297
+ artifactPaths.reconciled = await writeJsonArtifact("REVIEW_RECONCILED.json", {
1298
+ runId: orchestratorResult.runId,
1299
+ findings: orchestratorResult.findings || [],
1300
+ summary: orchestratorResult.summary,
1301
+ reconciliation: orchestratorResult.reconciliation,
1302
+ findingsBySource: orchestratorResult.findingsBySource,
1303
+ });
1304
+ }
1305
+
1306
+ const totalElapsedMs = Date.now() - commandStartedAt;
1307
+ const totalElapsed = formatElapsed(totalElapsedMs);
1308
+
964
1309
  const report = `# Local Omar Gate Deep Scan
965
1310
 
966
1311
  Generated: ${nowIso()}
967
1312
  Target: ${targetPath}
1313
+ Elapsed: ${totalElapsed}
968
1314
 
969
1315
  Summary:
970
- - Files scanned: ${scan.scannedFiles}
971
- - P1 findings: ${scan.p1}
972
- - P2 findings: ${scan.p2}
1316
+ - Files scanned: ${scannedFiles}
1317
+ - Deterministic findings: P0=${detSummary.P0} P1=${detSummary.P1} P2=${detSummary.P2} P3=${detSummary.P3 || 0}
1318
+ - AI findings (raw, pre-reconciliation): ${aiResult ? `P0=${aiResult.summary?.P0 || 0} P1=${aiResult.summary?.P1 || 0} P2=${aiResult.summary?.P2 || 0} P3=${aiResult.summary?.P3 || 0}` : "skipped"}
1319
+ - Reconciled (deduped + confidence-boosted): P0=${combinedP0} P1=${combinedP1} P2=${combinedP2} P3=${combinedP3}
1320
+ ${orchestratorResult?.reconciliation
1321
+ ? `- Reconciliation: ${orchestratorResult.reconciliation.deterministicFindings} deterministic + ${orchestratorResult.reconciliation.aiFindings} AI → ${orchestratorResult.reconciliation.reconciledFindings} unique (${orchestratorResult.reconciliation.multiSourceFindings} multi-source confirmed, ${orchestratorResult.reconciliation.dedupedCount} deduped)`
1322
+ : ""}
973
1323
 
974
1324
  Findings:
975
- ${formatFindingsMarkdown(scan.findings)}
1325
+ ${formatFindingsMarkdown(allFindings)}
976
1326
  `;
977
1327
 
978
1328
  const reportPath = await writeLocalCommandReport(targetPath, "omargate-deep", report, {
@@ -985,10 +1335,31 @@ ${formatFindingsMarkdown(scan.findings)}
985
1335
  command: "/omargate deep",
986
1336
  targetPath,
987
1337
  reportPath,
988
- scannedFiles: scan.scannedFiles,
989
- p1: scan.p1,
990
- p2: scan.p2,
991
- blocking: scan.p1 > 0,
1338
+ scannedFiles,
1339
+ p0: combinedP0,
1340
+ p1: combinedP1,
1341
+ p2: combinedP2,
1342
+ p3: combinedP3,
1343
+ blocking: combinedP0 > 0 || combinedP1 > 0,
1344
+ elapsedMs: totalElapsedMs,
1345
+ artifacts: artifactPaths,
1346
+ deterministic: {
1347
+ findings: detFindings.length,
1348
+ summary: detSummary,
1349
+ },
1350
+ ai: aiResult
1351
+ ? {
1352
+ findings: aiFindings.length,
1353
+ summary: aiResult.summary || {},
1354
+ model: aiResult.model || null,
1355
+ provider: aiResult.provider || null,
1356
+ costUsd: aiResult.costUsd || 0,
1357
+ dryRun: aiDryRun,
1358
+ personas: aiResult.personas || [],
1359
+ }
1360
+ : null,
1361
+ reconciliation: orchestratorResult?.reconciliation || null,
1362
+ roster: orchestratorResult?.roster || [],
992
1363
  },
993
1364
  null,
994
1365
  2
@@ -996,13 +1367,19 @@ ${formatFindingsMarkdown(scan.findings)}
996
1367
  );
997
1368
  } else {
998
1369
  console.log(pc.cyan(`Report: ${reportPath}`));
999
- console.log(`P1 findings: ${scan.p1}`);
1000
- console.log(`P2 findings: ${scan.p2}`);
1370
+ console.log(`Deterministic: P1=${detSummary.P1} P2=${detSummary.P2}`);
1371
+ if (aiResult) {
1372
+ console.log(`AI layer: P1=${aiResult.summary?.P1 || 0} P2=${aiResult.summary?.P2 || 0} (model: ${aiResult.model || "default"}, cost: $${(aiResult.costUsd || 0).toFixed(4)})`);
1373
+ } else if (aiEnabled) {
1374
+ console.log(pc.gray("AI layer: skipped (no credentials or --no-ai)"));
1375
+ }
1376
+ console.log(pc.bold(`Reconciled: P0=${combinedP0} P1=${combinedP1} P2=${combinedP2} P3=${combinedP3}`));
1377
+ console.log(pc.gray(`Elapsed: ${totalElapsed}`));
1001
1378
  }
1002
1379
 
1003
- if (scan.p1 > 0) {
1380
+ if (combinedP0 > 0 || combinedP1 > 0) {
1004
1381
  if (!asJson) {
1005
- console.log(pc.red("Blocking findings detected (P1 > 0)."));
1382
+ console.log(pc.red(`Blocking findings detected (P0=${combinedP0}, P1=${combinedP1}) after reconciliation.`));
1006
1383
  }
1007
1384
  return 2;
1008
1385
  }
@@ -1503,7 +1880,7 @@ async function ensureCodingAgentConfigFile({ projectDir, projectName, codingAgen
1503
1880
  };
1504
1881
  }
1505
1882
 
1506
- function buildTodoContent({
1883
+ export function buildTodoContent({
1507
1884
  projectName,
1508
1885
  aiProvider,
1509
1886
  codingAgent,
@@ -1543,6 +1920,8 @@ Project: ${projectName}
1543
1920
  - [ ] PR 10: scale/performance tuning and caching strategy
1544
1921
  - [ ] PR 11: observability, retries, timeout policies, and structured logs
1545
1922
  - [ ] PR 12: docs, release, rollout safety checks, and production readiness
1923
+ - [ ] If working with other agents, join the SentinelLayer session and emit status updates
1924
+ - [ ] Update tasks/lessons.md with coordination patterns learned during this session
1546
1925
 
1547
1926
  ## Omar Loop Contract (Per PR)
1548
1927
  - [ ] Run Omar Gate for the PR.
@@ -1593,7 +1972,7 @@ function buildAgentPromptGuidance(promptTarget) {
1593
1972
  - Document assumptions and unresolved risks clearly.`;
1594
1973
  }
1595
1974
 
1596
- function buildHandoffPrompt({
1975
+ export function buildHandoffPrompt({
1597
1976
  projectName,
1598
1977
  repoSlug,
1599
1978
  secretName,
@@ -1614,8 +1993,8 @@ function buildHandoffPrompt({
1614
1993
  - If you later adopt Omar Gate GitHub Action, set secrets.${secretName} and wire sentinelayer_token accordingly.`;
1615
1994
  const workflowTuning =
1616
1995
  authMode === "sentinelayer"
1617
- ? `- scan_mode: deep (default) or quick
1618
- - severity_gate: P1 (default) or P2`
1996
+ ? `- scan_mode: baseline | deep (default) | audit | full-depth
1997
+ - severity_gate: P0 | P1 (default) | P2 | none`
1619
1998
  : `- BYOK workflow is guidance-only and does not call the Sentinelayer action.
1620
1999
  - To enable Omar Gate later, set ${secretName} and configure scan_mode/severity_gate in workflow inputs.`;
1621
2000
 
@@ -1663,12 +2042,51 @@ Repo context:
1663
2042
  - Target repo: ${repoSlug || "not provided"}
1664
2043
  - Workspace mode: ${buildFromExistingRepo ? "existing codebase" : "new scaffold"}
1665
2044
 
2045
+ ## Multi-Agent Coordination (if session active)
2046
+
2047
+ If a SentinelLayer session is active (check \`sl session list\`):
2048
+
2049
+ 1. **On start:** Run \`sl session join <id> --name <your-name> --role coder\`
2050
+ 2. **Periodically:** Run \`sl session say <id> "status: working on <file>"\` so others know what you're doing
2051
+ 3. **On unexpected file change:** Instead of stopping, run:
2052
+ \`sl session say <id> "help: unexpected change in <file> - who modified this?"\`
2053
+ Then check \`sl session read <id> --tail 5\` for the answer and continue.
2054
+ 4. **On findings:** Run \`sl session say <id> "finding: [P2] <title> in <file>:<line>"\`
2055
+ 5. **On completion:** Run \`sl session leave <id>\`
2056
+ 6. **Before modifying a file:** Check \`sl session read <id> --tail 10\` for recent activity on that file
2057
+
1666
2058
  Start now and continue autonomously.
1667
2059
  `;
1668
2060
  }
1669
2061
 
1670
- function fallbackWorkflow({ secretName = "SENTINELAYER_TOKEN", authMode = "sentinelayer" } = {}) {
2062
+ export function buildAgentsSessionGuideContent() {
2063
+ return `# SentinelLayer Session Guide for AI Agents
2064
+
2065
+ ## Quick Start
2066
+ 1. Check: \`sl session list\` - is there an active session?
2067
+ 2. Join: \`sl session join <id> --name <your-short-name> --role <coder|reviewer|tester>\`
2068
+ 3. Read context: \`sl session read <id> --tail 20\` — see what others are doing
2069
+ 4. Work: emit status every 5 min, post findings, ask for help instead of stopping
2070
+ 5. Leave: \`sl session leave <id>\` when done
2071
+
2072
+ ## Why This Matters
2073
+ - Other agents can see what you're working on and avoid file conflicts
2074
+ - If you see an unexpected file change, ask in the session first
2075
+ - Findings are shared immediately so other agents can act quickly
2076
+ - The daemon can monitor health and alert when agents appear stuck
2077
+
2078
+ ## What to Emit
2079
+ - Status: \`sl session say <id> "status: implementing JWT middleware in src/middleware/auth.js"\`
2080
+ - Finding: \`sl session say <id> "finding: [P2] missing rate limit on POST /api/auth/login"\`
2081
+ - Help: \`sl session say <id> "help: unexpected change in package.json - who modified it?"\`
2082
+ - Done: \`sl session say <id> "done: PR merged, auth hardening complete"\`
2083
+ `;
2084
+ }
2085
+
2086
+ function fallbackWorkflow({ secretName = "SENTINELAYER_TOKEN", authMode = "sentinelayer", specId = "" } = {}) {
1671
2087
  const normalizedSecret = isValidSecretName(secretName) ? secretName : "SENTINELAYER_TOKEN";
2088
+ const normalizedSpecId = String(specId || "").trim();
2089
+ const specIdBindingLine = normalizedSpecId ? `\n sentinelayer_spec_id: ${normalizedSpecId}` : "";
1672
2090
  const workflowName = authMode === "byok" ? "Omar Gate (BYOK Mode)" : "Omar Gate";
1673
2091
  return `name: ${workflowName}
1674
2092
 
@@ -1686,8 +2104,10 @@ on:
1686
2104
  default: deep
1687
2105
  type: choice
1688
2106
  options:
2107
+ - baseline
1689
2108
  - deep
1690
- - nightly
2109
+ - audit
2110
+ - full-depth
1691
2111
  severity_gate:
1692
2112
  description: Severity threshold that blocks merge
1693
2113
  required: false
@@ -1737,9 +2157,9 @@ jobs:
1737
2157
  fi
1738
2158
  - name: Run Omar Gate
1739
2159
  id: omar
1740
- uses: mrrCarter/sentinelayer-v1-action@v1
2160
+ uses: mrrCarter/sentinelayer-v1-action@55a2c158f637d7d92e26ab0ef3ba81db791da4be
1741
2161
  with:
1742
- sentinelayer_token: \${{ secrets.${normalizedSecret} }}
2162
+ sentinelayer_token: \${{ secrets.${normalizedSecret} }}${specIdBindingLine}
1743
2163
  scan_mode: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
1744
2164
  severity_gate: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
1745
2165
  - name: Enforce Omar reviewer merge thresholds
@@ -1932,6 +2352,71 @@ function runGhSecretSet({ repoSlug, secretName, secretValue }) {
1932
2352
  return { ok: true };
1933
2353
  }
1934
2354
 
2355
+ function extractWorkflowSpecId(workflowMarkdown) {
2356
+ const normalized = String(workflowMarkdown || "");
2357
+ const match = normalized.match(/sentinelayer_spec_id:\s*([^\s#]+)/);
2358
+ return match ? String(match[1] || "").trim() : "";
2359
+ }
2360
+
2361
+ function resolveGeneratedSpecId(generated) {
2362
+ const candidates = [
2363
+ generated?.spec_id,
2364
+ generated?.specId,
2365
+ generated?.spec_hash,
2366
+ generated?.specHash,
2367
+ ];
2368
+ for (const candidate of candidates) {
2369
+ const normalized = String(candidate || "").trim();
2370
+ if (normalized) return normalized;
2371
+ }
2372
+ return "";
2373
+ }
2374
+
2375
+ async function writeInitConfigLockfile({
2376
+ projectDir,
2377
+ specId,
2378
+ sentinelayerToken,
2379
+ secretName,
2380
+ repoSlug,
2381
+ workflowPath,
2382
+ }) {
2383
+ const lockDir = path.join(projectDir, ".sentinelayer");
2384
+ const configPath = path.join(lockDir, "config.json");
2385
+ const payload = {
2386
+ schema_version: 1,
2387
+ generated_at: new Date().toISOString(),
2388
+ spec_id: String(specId || "").trim(),
2389
+ sentinelayer_token: String(sentinelayerToken || "").trim(),
2390
+ required_secret_name: String(secretName || "SENTINELAYER_TOKEN").trim() || "SENTINELAYER_TOKEN",
2391
+ repo_slug: normalizeRepoSlug(repoSlug || ""),
2392
+ workflow_path: path.relative(projectDir, workflowPath).replace(/\\/g, "/"),
2393
+ };
2394
+
2395
+ await fsp.mkdir(lockDir, { recursive: true });
2396
+ await fsp.writeFile(configPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
2397
+ return configPath;
2398
+ }
2399
+
2400
+ async function validateWorkflowSpecBinding({ workflowPath, expectedSpecId }) {
2401
+ const expected = String(expectedSpecId || "").trim();
2402
+ if (!expected) {
2403
+ throw new Error("Missing spec_id/spec_hash in generated builder response. Cannot validate Omar spec binding.");
2404
+ }
2405
+ const workflowMarkdown = await fsp.readFile(workflowPath, "utf-8");
2406
+ const workflowSpecId = extractWorkflowSpecId(workflowMarkdown);
2407
+ if (!workflowSpecId) {
2408
+ throw new Error(
2409
+ `Generated workflow '${workflowPath}' is missing sentinelayer_spec_id. Regenerate the workflow before continuing.`
2410
+ );
2411
+ }
2412
+ if (workflowSpecId !== expected) {
2413
+ throw new Error(
2414
+ `Workflow spec binding mismatch: expected '${expected}' but workflow has '${workflowSpecId}'.`
2415
+ );
2416
+ }
2417
+ return workflowSpecId;
2418
+ }
2419
+
1935
2420
  async function collectInterview({ initialProjectName, detectedRepo, detectedCodingAgent }) {
1936
2421
  const onCancel = () => {
1937
2422
  throw new Error("Prompt flow cancelled by user.");
@@ -2432,6 +2917,7 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
2432
2917
  const docsDir = path.join(projectDir, "docs");
2433
2918
  const promptsDir = path.join(projectDir, "prompts");
2434
2919
  const tasksDir = path.join(projectDir, "tasks");
2920
+ const workflowPath = path.join(projectDir, ".github", "workflows", "omar-gate.yml");
2435
2921
 
2436
2922
  await writeTextFile(path.join(docsDir, "spec.md"), String(generated.spec_sheet || "").trim() + "\n");
2437
2923
  await writeTextFile(
@@ -2442,13 +2928,34 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
2442
2928
  path.join(promptsDir, "execution-prompt.md"),
2443
2929
  String(generated.builder_prompt || "").trim() + "\n"
2444
2930
  );
2445
- await writeTextFile(
2446
- path.join(projectDir, ".github", "workflows", "omar-gate.yml"),
2931
+ const generatedSpecId = resolveGeneratedSpecId(generated);
2932
+ if (effectiveAuthMode === "sentinelayer" && !generatedSpecId) {
2933
+ throw new Error("Builder response is missing spec_id/spec_hash. Cannot generate a validated Omar Gate workflow.");
2934
+ }
2935
+ const workflowMarkdown =
2447
2936
  (
2448
2937
  (effectiveAuthMode === "sentinelayer" ? String(generated.omar_gate_yaml || "").trim() : "") ||
2449
- fallbackWorkflow({ secretName, authMode: effectiveAuthMode })
2450
- ) + "\n"
2451
- );
2938
+ fallbackWorkflow({ secretName, authMode: effectiveAuthMode, specId: generatedSpecId })
2939
+ ) + "\n";
2940
+ await writeTextFile(workflowPath, workflowMarkdown);
2941
+
2942
+ const workflowSpecIdFromTemplate = extractWorkflowSpecId(workflowMarkdown);
2943
+ let workflowSpecId = "";
2944
+ if (generatedSpecId || workflowSpecIdFromTemplate) {
2945
+ workflowSpecId = await validateWorkflowSpecBinding({
2946
+ workflowPath,
2947
+ expectedSpecId: generatedSpecId || workflowSpecIdFromTemplate,
2948
+ });
2949
+ }
2950
+ const configLockfilePath = await writeInitConfigLockfile({
2951
+ projectDir,
2952
+ specId: workflowSpecId || generatedSpecId || workflowSpecIdFromTemplate,
2953
+ sentinelayerToken,
2954
+ secretName,
2955
+ repoSlug: interview.repoSlug || detectRepoSlug(projectDir) || "",
2956
+ workflowPath,
2957
+ });
2958
+
2452
2959
  await writeTextFile(
2453
2960
  path.join(tasksDir, "todo.md"),
2454
2961
  buildTodoContent({
@@ -2474,6 +2981,10 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
2474
2981
  codingAgent: interview.codingAgent,
2475
2982
  })
2476
2983
  );
2984
+ await writeTextFile(
2985
+ path.join(projectDir, ".sentinelayer", "AGENTS_SESSION_GUIDE.md"),
2986
+ buildAgentsSessionGuideContent()
2987
+ );
2477
2988
  const codingAgentConfig = await ensureCodingAgentConfigFile({
2478
2989
  projectDir,
2479
2990
  projectName: effectiveProjectName,
@@ -2524,17 +3035,59 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
2524
3035
  repoSlug: interview.connectRepo ? interview.repoSlug : "",
2525
3036
  });
2526
3037
 
2527
- let secretInjection = { ok: false, reason: "Skipped." };
2528
- if (interview.connectRepo && interview.injectSecret && interview.repoSlug && sentinelayerToken) {
2529
- secretInjection = runGhSecretSet({
2530
- repoSlug: interview.repoSlug,
3038
+ const repoSlugForSecrets = normalizeRepoSlug(interview.repoSlug || detectRepoSlug(projectDir) || "");
3039
+ const openAiApiKey = String(process.env.OPENAI_API_KEY || "").trim();
3040
+ const secretTargets = [];
3041
+ if (sentinelayerToken) {
3042
+ secretTargets.push({
2531
3043
  secretName,
2532
3044
  secretValue: sentinelayerToken,
3045
+ placeholder: "<sentinelayer-token>",
2533
3046
  });
2534
3047
  }
3048
+ secretTargets.push({
3049
+ secretName: "OPENAI_API_KEY",
3050
+ secretValue: openAiApiKey,
3051
+ placeholder: "<your-openai-api-key>",
3052
+ });
3053
+
3054
+ const githubSecretResults = [];
3055
+ if (repoSlugForSecrets) {
3056
+ for (const target of secretTargets) {
3057
+ const value = String(target.secretValue || "").trim();
3058
+ if (!value) {
3059
+ githubSecretResults.push({
3060
+ secretName: target.secretName,
3061
+ ok: false,
3062
+ skipped: true,
3063
+ reason: `No value resolved for ${target.secretName} in current environment/config.`,
3064
+ placeholder: target.placeholder,
3065
+ });
3066
+ continue;
3067
+ }
3068
+ const result = runGhSecretSet({
3069
+ repoSlug: repoSlugForSecrets,
3070
+ secretName: target.secretName,
3071
+ secretValue: value,
3072
+ });
3073
+ githubSecretResults.push({
3074
+ secretName: target.secretName,
3075
+ ok: Boolean(result.ok),
3076
+ skipped: false,
3077
+ reason: String(result.reason || "").trim(),
3078
+ placeholder: target.placeholder,
3079
+ });
3080
+ }
3081
+ }
2535
3082
 
2536
3083
  printSection("Complete");
2537
3084
  console.log(pc.green(`✔ Sentinelayer orchestration initialized in ${projectDir}`));
3085
+ console.log(pc.green(`✔ Config lockfile written: ${configLockfilePath}`));
3086
+ if (workflowSpecId) {
3087
+ console.log(pc.green(`✔ Omar workflow spec binding validated: ${workflowSpecId}`));
3088
+ } else {
3089
+ console.log(pc.yellow("! Omar workflow did not expose sentinelayer_spec_id (BYOK/fallback mode)."));
3090
+ }
2538
3091
  if (sentinelayerToken) {
2539
3092
  console.log(pc.green(`✔ ${secretName} injected into ${path.join(projectDir, ".env")}`));
2540
3093
  } else {
@@ -2545,14 +3098,30 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
2545
3098
  pc.green(`✔ ${codingAgentConfig.agent.name} config scaffolded at ${codingAgentConfig.path}`)
2546
3099
  );
2547
3100
  }
2548
- if (interview.connectRepo && interview.injectSecret && sentinelayerToken) {
2549
- if (secretInjection.ok) {
2550
- console.log(pc.green(`✔ ${secretName} injected into GitHub repo secret (${interview.repoSlug})`));
2551
- } else {
2552
- console.log(pc.yellow(`! GitHub secret injection skipped/failed: ${secretInjection.reason}`));
3101
+ if (repoSlugForSecrets) {
3102
+ for (const result of githubSecretResults) {
3103
+ if (result.ok) {
3104
+ console.log(pc.green(`✔ ${result.secretName} injected into GitHub repo secret (${repoSlugForSecrets})`));
3105
+ continue;
3106
+ }
3107
+ const stateLabel = result.skipped ? "skipped" : "failed";
3108
+ console.log(pc.yellow(`! GitHub secret injection ${stateLabel} for ${result.secretName}: ${result.reason}`));
3109
+ console.log(
3110
+ pc.yellow(
3111
+ ` Run manually: gh secret set ${result.secretName} --repo ${repoSlugForSecrets} --body ${result.placeholder}`
3112
+ )
3113
+ );
3114
+ }
3115
+ } else if (secretTargets.length > 0) {
3116
+ console.log(
3117
+ pc.yellow(
3118
+ "! GitHub secret auto-injection skipped: no repo slug detected. Connect a repo or run manual secret commands."
3119
+ )
3120
+ );
3121
+ for (const target of secretTargets) {
2553
3122
  console.log(
2554
3123
  pc.yellow(
2555
- ` Run manually: gh secret set ${secretName} --repo ${interview.repoSlug || "<owner/repo>"}`
3124
+ ` Run manually: gh secret set ${target.secretName} --repo <owner/repo> --body ${target.placeholder}`
2556
3125
  )
2557
3126
  );
2558
3127
  }