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.
- package/README.md +16 -18
- package/package.json +7 -6
- package/src/agents/jules/config/definition.js +13 -62
- package/src/agents/jules/config/system-prompt.js +8 -1
- package/src/agents/jules/fix-cycle.js +12 -372
- package/src/agents/jules/loop.js +116 -26
- package/src/agents/jules/pulse.js +10 -327
- package/src/agents/jules/stream.js +13 -12
- package/src/agents/jules/swarm/orchestrator.js +3 -3
- package/src/agents/jules/swarm/sub-agent.js +6 -3
- package/src/agents/jules/tools/aidenid-email.js +189 -0
- package/src/agents/jules/tools/auth-audit.js +1187 -45
- package/src/agents/jules/tools/dispatch.js +25 -12
- package/src/agents/jules/tools/file-edit.js +2 -180
- package/src/agents/jules/tools/file-read.js +2 -100
- package/src/agents/jules/tools/glob.js +2 -168
- package/src/agents/jules/tools/grep.js +2 -228
- package/src/agents/jules/tools/path-guards.js +2 -161
- package/src/agents/jules/tools/runtime-audit.js +6 -2
- package/src/agents/jules/tools/shell.js +2 -383
- package/src/agents/persona-visuals.js +64 -0
- package/src/agents/shared-tools/dispatch-core.js +320 -0
- package/src/agents/shared-tools/file-edit.js +180 -0
- package/src/agents/shared-tools/file-read.js +100 -0
- package/src/agents/shared-tools/glob.js +168 -0
- package/src/agents/shared-tools/grep.js +228 -0
- package/src/agents/shared-tools/index.js +46 -0
- package/src/agents/shared-tools/path-guards.js +161 -0
- package/src/agents/shared-tools/shell.js +383 -0
- package/src/ai/aidenid.js +56 -7
- package/src/ai/client.js +45 -0
- package/src/ai/proxy.js +137 -0
- package/src/auth/gate.js +290 -16
- package/src/auth/http.js +450 -39
- package/src/auth/service.js +262 -47
- package/src/auth/session-store.js +475 -21
- package/src/cli.js +5 -0
- package/src/commands/audit.js +13 -8
- package/src/commands/auth.js +53 -9
- package/src/commands/omargate.js +10 -2
- package/src/commands/scan.js +10 -4
- package/src/commands/session.js +590 -0
- package/src/commands/spec.js +62 -0
- package/src/commands/watch.js +3 -2
- package/src/daemon/assignment-ledger.js +196 -0
- package/src/daemon/error-worker.js +599 -16
- package/src/daemon/fix-cycle.js +384 -0
- package/src/daemon/ingest-refresh.js +10 -9
- package/src/daemon/jira-lifecycle.js +135 -0
- package/src/daemon/pulse.js +327 -0
- package/src/daemon/scope-engine.js +1068 -0
- package/src/events/schema.js +190 -0
- package/src/interactive/index.js +18 -16
- package/src/legacy-cli.js +606 -37
- package/src/prompt/generator.js +19 -1
- package/src/review/ai-review.js +11 -1
- package/src/review/local-review.js +75 -19
- package/src/review/omargate-interactive.js +68 -0
- package/src/review/omargate-orchestrator.js +404 -0
- package/src/review/persona-prompts.js +296 -0
- package/src/review/scan-modes.js +48 -0
- package/src/scan/generator.js +1 -1
- package/src/session/agent-registry.js +352 -0
- package/src/session/daemon.js +801 -0
- package/src/session/paths.js +33 -0
- package/src/session/runtime-bridge.js +739 -0
- package/src/session/store.js +388 -0
- package/src/session/stream.js +325 -0
- package/src/spec/generator.js +100 -0
- package/src/telemetry/session-tracker.js +148 -32
- package/src/telemetry/sync.js +6 -2
- 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
|
-
|
|
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: ${
|
|
971
|
-
-
|
|
972
|
-
-
|
|
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(
|
|
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
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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(`
|
|
1000
|
-
|
|
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 (
|
|
1380
|
+
if (combinedP0 > 0 || combinedP1 > 0) {
|
|
1004
1381
|
if (!asJson) {
|
|
1005
|
-
console.log(pc.red(
|
|
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)
|
|
1618
|
-
- severity_gate: P1 (default)
|
|
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
|
|
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
|
-
-
|
|
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@
|
|
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
|
-
|
|
2446
|
-
|
|
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
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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 (
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
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
|
|
3124
|
+
` Run manually: gh secret set ${target.secretName} --repo <owner/repo> --body ${target.placeholder}`
|
|
2556
3125
|
)
|
|
2557
3126
|
);
|
|
2558
3127
|
}
|