sentinelayer-cli 0.46.5 → 0.48.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 +47 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +32 -1
- package/src/auth/session-store.js +37 -0
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +516 -30
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +25 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +110 -15
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// judge-verdict v1: the document a judging model posts back to a coder.
|
|
2
|
+
//
|
|
3
|
+
// One schema, enforced the same way by this CLI and by the SentinelLayer API (the shared
|
|
4
|
+
// agreement cases live in tests/fixtures/judge-verdict-cases.json). The judge is never named
|
|
5
|
+
// in the document: the server records who judged from the credential or the stored post.
|
|
6
|
+
|
|
7
|
+
export const JUDGE_VERDICT_KIND = "judge-verdict";
|
|
8
|
+
export const JUDGE_VERDICT_VERSION = 1;
|
|
9
|
+
export const JUDGE_VERDICT_MAX_BYTES = 32768;
|
|
10
|
+
export const JUDGE_VERDICT_DEFAULT_MAX_ROUNDS = 3;
|
|
11
|
+
export const JUDGE_VERDICT_SEVERITIES = Object.freeze(["P0", "P1", "P2", "P3"]);
|
|
12
|
+
export const JUDGE_VERDICT_TARGET_TYPES = Object.freeze(["pr", "ticket", "message"]);
|
|
13
|
+
|
|
14
|
+
const TOP_KEYS = new Set([
|
|
15
|
+
"kind", "version", "verdict", "round", "maxRounds", "target", "coder", "summary", "comments",
|
|
16
|
+
"escalate", "doNotTrain",
|
|
17
|
+
]);
|
|
18
|
+
const TARGET_KEYS = new Set(["type", "ref", "sha"]);
|
|
19
|
+
const COMMENT_KEYS = new Set(["file", "line", "severity", "text", "fix"]);
|
|
20
|
+
const MAX_ROUNDS_LIMIT = 10;
|
|
21
|
+
const MAX_COMMENTS = 50;
|
|
22
|
+
const MAX_LINE = 10_000_000;
|
|
23
|
+
const PR_REF_RE = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}#[1-9][0-9]{0,9}$/;
|
|
24
|
+
const MESSAGE_REF_RE = /^[1-9][0-9]{0,18}$/;
|
|
25
|
+
const SHA_RE = /^[0-9a-fA-F]{7,64}$/;
|
|
26
|
+
const AGENT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
27
|
+
// Control characters are U+0000..U+001F and U+007F. Multi-line text may keep \t \n \r.
|
|
28
|
+
const ANY_CONTROL_RE = /[\u0000-\u001f\u007f]/;
|
|
29
|
+
const TEXT_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
|
|
30
|
+
const ASCII_EDGE_WHITESPACE_RE = /^[ \t\n\r\f\v]+|[ \t\n\r\f\v]+$/g;
|
|
31
|
+
// The fenced-block scanner works line by line, so its cost is linear in the message whatever the
|
|
32
|
+
// message holds (an unclosed fence included). Each pattern is tested on one line, never the text.
|
|
33
|
+
const FENCE_OPEN_RE = /^```(?:judge-verdict|json)[ \t]*$/;
|
|
34
|
+
const FENCE_CLOSE_RE = /^```[ \t]*$/;
|
|
35
|
+
export const JUDGE_VERDICT_BLOCK_MAX_BYTES = 64 * 1024;
|
|
36
|
+
|
|
37
|
+
function isPlainObject(value) {
|
|
38
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function trimAscii(value) {
|
|
42
|
+
return value.replace(ASCII_EDGE_WHITESPACE_RE, "");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function codePoints(value) {
|
|
46
|
+
return [...value].length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isWholeNumber(value) {
|
|
50
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Validate a judge-verdict document.
|
|
55
|
+
*
|
|
56
|
+
* Returns `{ ok: true, verdict }` with the normalized document (strings trimmed, defaults
|
|
57
|
+
* filled, sha lowercased), or `{ ok: false, errors }` where each error is `{ path, message }`.
|
|
58
|
+
*/
|
|
59
|
+
export function validateJudgeVerdict(document) {
|
|
60
|
+
const errors = [];
|
|
61
|
+
const fail = (path, message) => errors.push({ path, message });
|
|
62
|
+
|
|
63
|
+
if (!isPlainObject(document)) {
|
|
64
|
+
return { ok: false, errors: [{ path: "", message: "must be a JSON object" }] };
|
|
65
|
+
}
|
|
66
|
+
let serialized;
|
|
67
|
+
try {
|
|
68
|
+
serialized = JSON.stringify(document);
|
|
69
|
+
} catch {
|
|
70
|
+
return { ok: false, errors: [{ path: "", message: "must be serializable JSON" }] };
|
|
71
|
+
}
|
|
72
|
+
if (Buffer.byteLength(serialized, "utf8") > JUDGE_VERDICT_MAX_BYTES) {
|
|
73
|
+
return { ok: false, errors: [{ path: "", message: `must be at most ${JUDGE_VERDICT_MAX_BYTES} bytes as compact JSON` }] };
|
|
74
|
+
}
|
|
75
|
+
for (const key of Object.keys(document)) {
|
|
76
|
+
if (!TOP_KEYS.has(key)) fail(key, "is not a judge-verdict field (the judge is never named in the document)");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const text = (path, value, { min, max, multiline = false, required = true }) => {
|
|
80
|
+
if (value === undefined && !required) return undefined;
|
|
81
|
+
if (typeof value !== "string") {
|
|
82
|
+
fail(path, "must be a string");
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
if ((multiline ? TEXT_CONTROL_RE : ANY_CONTROL_RE).test(value)) {
|
|
86
|
+
fail(path, multiline ? "must not contain control characters other than tab and newline" : "must not contain control characters");
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const trimmed = trimAscii(value);
|
|
90
|
+
const length = codePoints(trimmed);
|
|
91
|
+
if (length < min || length > max) {
|
|
92
|
+
fail(path, min > 0 ? `must be ${min}..${max} characters` : `must be at most ${max} characters`);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return trimmed;
|
|
96
|
+
};
|
|
97
|
+
const integer = (path, value, min, max) => {
|
|
98
|
+
if (!isWholeNumber(value) || value < min || value > max) {
|
|
99
|
+
fail(path, `must be an integer ${min}..${max}`);
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
return value;
|
|
103
|
+
};
|
|
104
|
+
const flag = (path, value) => {
|
|
105
|
+
if (value === undefined) return false;
|
|
106
|
+
if (typeof value !== "boolean") {
|
|
107
|
+
fail(path, "must be true or false");
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
if (document.kind !== JUDGE_VERDICT_KIND) fail("kind", `must be "${JUDGE_VERDICT_KIND}"`);
|
|
114
|
+
if (document.version !== JUDGE_VERDICT_VERSION) fail("version", `must be ${JUDGE_VERDICT_VERSION}`);
|
|
115
|
+
const verdict = document.verdict;
|
|
116
|
+
if (verdict !== "ACCEPT" && verdict !== "CHANGES") fail("verdict", 'must be "ACCEPT" or "CHANGES"');
|
|
117
|
+
const round = integer("round", document.round, 1, MAX_ROUNDS_LIMIT);
|
|
118
|
+
const maxRounds = document.maxRounds === undefined
|
|
119
|
+
? JUDGE_VERDICT_DEFAULT_MAX_ROUNDS
|
|
120
|
+
: integer("maxRounds", document.maxRounds, 1, MAX_ROUNDS_LIMIT);
|
|
121
|
+
if (round !== undefined && maxRounds !== undefined && round > maxRounds) {
|
|
122
|
+
fail("round", "must not exceed maxRounds");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let target;
|
|
126
|
+
if (!isPlainObject(document.target)) {
|
|
127
|
+
fail("target", "must be an object with type and ref");
|
|
128
|
+
} else {
|
|
129
|
+
for (const key of Object.keys(document.target)) {
|
|
130
|
+
if (!TARGET_KEYS.has(key)) fail(`target.${key}`, "is not a target field");
|
|
131
|
+
}
|
|
132
|
+
const type = document.target.type;
|
|
133
|
+
if (!JUDGE_VERDICT_TARGET_TYPES.includes(type)) fail("target.type", 'must be "pr", "ticket" or "message"');
|
|
134
|
+
const ref = text("target.ref", document.target.ref, { min: 1, max: 200 });
|
|
135
|
+
if (ref !== undefined && type === "pr" && !PR_REF_RE.test(ref)) fail("target.ref", "must look like owner/repo#123");
|
|
136
|
+
if (ref !== undefined && type === "message" && !MESSAGE_REF_RE.test(ref)) {
|
|
137
|
+
fail("target.ref", "must be the message's sequence id");
|
|
138
|
+
}
|
|
139
|
+
let sha;
|
|
140
|
+
if (document.target.sha !== undefined) {
|
|
141
|
+
sha = text("target.sha", document.target.sha, { min: 1, max: 64 });
|
|
142
|
+
if (sha !== undefined && !SHA_RE.test(sha)) {
|
|
143
|
+
fail("target.sha", "must be 7..64 hex characters");
|
|
144
|
+
sha = undefined;
|
|
145
|
+
}
|
|
146
|
+
} else if (type === "pr") {
|
|
147
|
+
fail("target.sha", "is required for a pr target (the exact head you judged)");
|
|
148
|
+
}
|
|
149
|
+
target = { type, ref, ...(sha !== undefined ? { sha: sha.toLowerCase() } : {}) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const coder = text("coder", document.coder, { min: 1, max: 64 });
|
|
153
|
+
if (coder !== undefined && (!AGENT_ID_RE.test(coder) || coder.toLowerCase().startsWith("human-"))) {
|
|
154
|
+
fail("coder", "must be the coding agent's id");
|
|
155
|
+
}
|
|
156
|
+
const summary = text("summary", document.summary, { min: 1, max: 2000, multiline: true });
|
|
157
|
+
|
|
158
|
+
const comments = [];
|
|
159
|
+
if (!Array.isArray(document.comments)) {
|
|
160
|
+
fail("comments", "must be an array");
|
|
161
|
+
} else if (document.comments.length > MAX_COMMENTS) {
|
|
162
|
+
fail("comments", `must have at most ${MAX_COMMENTS} entries`);
|
|
163
|
+
} else {
|
|
164
|
+
document.comments.forEach((comment, index) => {
|
|
165
|
+
const at = `comments[${index}]`;
|
|
166
|
+
if (!isPlainObject(comment)) {
|
|
167
|
+
fail(at, "must be an object with file, line, severity and text");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
for (const key of Object.keys(comment)) {
|
|
171
|
+
if (!COMMENT_KEYS.has(key)) fail(`${at}.${key}`, "is not a comment field");
|
|
172
|
+
}
|
|
173
|
+
const file = text(`${at}.file`, comment.file, { min: 1, max: 300 });
|
|
174
|
+
const line = integer(`${at}.line`, comment.line, 0, MAX_LINE);
|
|
175
|
+
const severity = comment.severity;
|
|
176
|
+
if (!JUDGE_VERDICT_SEVERITIES.includes(severity)) fail(`${at}.severity`, "must be P0, P1, P2 or P3");
|
|
177
|
+
const body = text(`${at}.text`, comment.text, { min: 1, max: 2000, multiline: true });
|
|
178
|
+
const fix = text(`${at}.fix`, comment.fix, { min: 0, max: 500, multiline: true, required: false });
|
|
179
|
+
comments.push({ file, line, severity, text: body, ...(fix !== undefined ? { fix } : {}) });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const escalate = flag("escalate", document.escalate);
|
|
184
|
+
const doNotTrain = flag("doNotTrain", document.doNotTrain);
|
|
185
|
+
if (verdict === "CHANGES" && Array.isArray(document.comments) && document.comments.length === 0) {
|
|
186
|
+
fail("comments", "CHANGES needs at least one comment");
|
|
187
|
+
}
|
|
188
|
+
if (verdict === "ACCEPT" && comments.some((comment) => comment.severity === "P0" || comment.severity === "P1")) {
|
|
189
|
+
fail("verdict", "ACCEPT cannot carry a P0 or P1 comment");
|
|
190
|
+
}
|
|
191
|
+
if (escalate && verdict !== "CHANGES") fail("escalate", "only a CHANGES verdict escalates");
|
|
192
|
+
if (verdict === "CHANGES" && round !== undefined && maxRounds !== undefined && round === maxRounds && !escalate) {
|
|
193
|
+
fail("escalate", "the last round's CHANGES verdict must escalate to the human");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (errors.length) return { ok: false, errors };
|
|
197
|
+
return {
|
|
198
|
+
ok: true,
|
|
199
|
+
verdict: {
|
|
200
|
+
kind: JUDGE_VERDICT_KIND,
|
|
201
|
+
version: JUDGE_VERDICT_VERSION,
|
|
202
|
+
verdict,
|
|
203
|
+
round,
|
|
204
|
+
maxRounds,
|
|
205
|
+
target,
|
|
206
|
+
coder,
|
|
207
|
+
summary,
|
|
208
|
+
comments,
|
|
209
|
+
escalate,
|
|
210
|
+
doNotTrain,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function formatJudgeVerdictErrors(errors = []) {
|
|
216
|
+
return errors.map((error) => (error.path ? `${error.path} ${error.message}` : error.message)).join("; ");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function targetLabel(target) {
|
|
220
|
+
if (target.type === "pr") return `${target.ref} @ ${target.sha.slice(0, 12)}`;
|
|
221
|
+
if (target.type === "message") return `message #${target.ref}${target.sha ? ` @ ${target.sha.slice(0, 12)}` : ""}`;
|
|
222
|
+
return `ticket ${target.ref}${target.sha ? ` @ ${target.sha.slice(0, 12)}` : ""}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function formatJudgeVerdictBlock(verdict) {
|
|
226
|
+
return ["```judge-verdict", JSON.stringify(verdict), "```"].join("\n");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Render a validated verdict as the room message the coder receives: the mention that wakes
|
|
231
|
+
* the coder, the numbered comments in plain words, and the machine-readable block.
|
|
232
|
+
*/
|
|
233
|
+
export function renderJudgeVerdictMessage(verdict, { escalateTo = [] } = {}) {
|
|
234
|
+
const mentions = [`@${verdict.coder}`];
|
|
235
|
+
if (verdict.escalate) {
|
|
236
|
+
for (const handle of escalateTo) mentions.push(`@${handle}`);
|
|
237
|
+
}
|
|
238
|
+
const lines = [
|
|
239
|
+
`${mentions.join(" ")} Judge verdict: ${verdict.verdict} (round ${verdict.round} of ${verdict.maxRounds}) on ${targetLabel(verdict.target)}`,
|
|
240
|
+
verdict.summary,
|
|
241
|
+
];
|
|
242
|
+
if (verdict.comments.length) {
|
|
243
|
+
lines.push("");
|
|
244
|
+
verdict.comments.forEach((comment, index) => {
|
|
245
|
+
const where = comment.line > 0 ? `${comment.file}:${comment.line}` : comment.file;
|
|
246
|
+
lines.push(`${index + 1}. [${comment.severity}] ${where} - ${comment.text}`);
|
|
247
|
+
if (comment.fix) lines.push(` Fix: ${comment.fix}`);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
lines.push("");
|
|
251
|
+
if (verdict.escalate) {
|
|
252
|
+
lines.push(`Round limit reached. Stopping the loop here; a human decides what happens next.`);
|
|
253
|
+
} else if (verdict.verdict === "CHANGES") {
|
|
254
|
+
lines.push(`Fix each numbered comment, then reply with the new head and what changed per number. I will re-check every one.`);
|
|
255
|
+
} else {
|
|
256
|
+
lines.push(`Accepted. No further rounds.`);
|
|
257
|
+
}
|
|
258
|
+
lines.push("", formatJudgeVerdictBlock(verdict));
|
|
259
|
+
return lines.join("\n");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Find the judge-verdict block in a room message. Returns `{ status, document }` where status
|
|
264
|
+
* is "none", "one" or "ambiguous" (more than one block: nothing is taken). The document is
|
|
265
|
+
* returned unvalidated; pass it to validateJudgeVerdict.
|
|
266
|
+
*
|
|
267
|
+
* A block opens at a line that is exactly ```judge-verdict or ```json (trailing spaces or tabs
|
|
268
|
+
* allowed) and closes at the next line that is exactly ```. An opener with no closing line ends
|
|
269
|
+
* the scan. A block body over JUDGE_VERDICT_BLOCK_MAX_BYTES (UTF-8) is skipped unparsed, and the
|
|
270
|
+
* scan stops at the second candidate. The API runs the same algorithm against the same vectors
|
|
271
|
+
* (tests/fixtures/judge-verdict-block-cases.json).
|
|
272
|
+
*/
|
|
273
|
+
export function extractJudgeVerdictBlock(message) {
|
|
274
|
+
const text = typeof message === "string" ? message : "";
|
|
275
|
+
// Every candidate carries "kind": "judge-verdict", so a message without the word has none.
|
|
276
|
+
if (!text.includes(JUDGE_VERDICT_KIND)) return { status: "none", document: null };
|
|
277
|
+
const lines = text.split("\n");
|
|
278
|
+
const line = (index) => (lines[index].endsWith("\r") ? lines[index].slice(0, -1) : lines[index]);
|
|
279
|
+
let document = null;
|
|
280
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
281
|
+
if (!FENCE_OPEN_RE.test(line(index))) continue;
|
|
282
|
+
const body = [];
|
|
283
|
+
let bytes = 0;
|
|
284
|
+
let close = index + 1;
|
|
285
|
+
for (; close < lines.length; close += 1) {
|
|
286
|
+
const current = line(close);
|
|
287
|
+
if (FENCE_CLOSE_RE.test(current)) break;
|
|
288
|
+
bytes += Buffer.byteLength(current, "utf8") + (close > index + 1 ? 1 : 0);
|
|
289
|
+
if (bytes <= JUDGE_VERDICT_BLOCK_MAX_BYTES) body.push(current);
|
|
290
|
+
}
|
|
291
|
+
if (close >= lines.length) break;
|
|
292
|
+
index = close;
|
|
293
|
+
if (bytes > JUDGE_VERDICT_BLOCK_MAX_BYTES) continue;
|
|
294
|
+
let parsed;
|
|
295
|
+
try {
|
|
296
|
+
parsed = JSON.parse(body.join("\n"));
|
|
297
|
+
} catch {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (!isPlainObject(parsed) || parsed.kind !== JUDGE_VERDICT_KIND) continue;
|
|
301
|
+
if (document) return { status: "ambiguous", document: null };
|
|
302
|
+
document = parsed;
|
|
303
|
+
}
|
|
304
|
+
return document ? { status: "one", document } : { status: "none", document: null };
|
|
305
|
+
}
|
package/src/legacy-cli.js
CHANGED
|
@@ -23,12 +23,12 @@ import {
|
|
|
23
23
|
resolveTrustContext,
|
|
24
24
|
userCredential,
|
|
25
25
|
} from "./auth/credential-destinations.js";
|
|
26
|
-
import {
|
|
26
|
+
import { resolveOutputContext } from "./config/service.js";
|
|
27
27
|
import { normalizeAgentEvent } from "./events/schema.js";
|
|
28
28
|
import { collectCodebaseIngest, formatIngestSummary } from "./ingest/engine.js";
|
|
29
29
|
import { getExpressTemplate, getPackageJsonTemplate, buildReadmeContent } from "./scaffold/templates.js";
|
|
30
30
|
import { generateScaffold } from "./scaffold/generator.js";
|
|
31
|
-
import { isGhRepoSlug } from "./net/gh-api-path.js";
|
|
31
|
+
import { ghRepoApiPath, isGhRepoSlug } from "./net/gh-api-path.js";
|
|
32
32
|
import {
|
|
33
33
|
getCoordinationEtiquetteItems,
|
|
34
34
|
renderCoordinationNumberedList,
|
|
@@ -42,6 +42,7 @@ let DEFAULT_GITHUB_CLONE_BASE_URL =
|
|
|
42
42
|
const DEFAULT_AUTH_TIMEOUT_MS = 10 * 60 * 1000;
|
|
43
43
|
const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
|
|
44
44
|
const PACKAGE_JSON_PATH = new URL("../package.json", import.meta.url);
|
|
45
|
+
const GH_REPOS_ENDPOINT = "/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member";
|
|
45
46
|
|
|
46
47
|
function refreshRuntimeDefaults() {
|
|
47
48
|
DEFAULT_API_URL = process.env.SENTINELAYER_API_URL || "https://api.sentinelayer.com";
|
|
@@ -232,7 +233,8 @@ function printUsage() {
|
|
|
232
233
|
console.log(" sl session start --json Create an agent coordination session");
|
|
233
234
|
console.log(" sl session start --template code-review Start from quick-start preset + launch plan");
|
|
234
235
|
console.log(" sl session templates --json List available session quick-start templates");
|
|
235
|
-
console.log(" sl session join <id> --
|
|
236
|
+
console.log(" sl session join <id> --agent <agent> --goal \"<goal>\" Request owner-approved agent admission");
|
|
237
|
+
console.log(" sl session access --help Request and govern scoped agent access");
|
|
236
238
|
console.log(" sl session say <id> \"msg\" --json Append a message event to session stream");
|
|
237
239
|
console.log(" sl session lock <id> <file> --agent <agent> --intent <text> Claim an API-backed file lease outside chat");
|
|
238
240
|
console.log(" sl session guard-install <id> --agent <agent> Install terminal/editor lease preflights");
|
|
@@ -248,6 +250,7 @@ function printUsage() {
|
|
|
248
250
|
console.log(" sl session comment <id> <seq> \"msg\" Alias for threaded reply");
|
|
249
251
|
console.log(" sl session read <id> --remote --agent <id> Read events and advance delivery cursor");
|
|
250
252
|
console.log(" sl session edit <id> <seq|reply-uuid> \"msg\" Edit your message without losing history");
|
|
253
|
+
console.log(" sl session observe <id> \"summary\" --agent <agent> Record product/process feedback");
|
|
251
254
|
console.log(" sl session pins <id> --json List pinned messages with content (readable by agents)");
|
|
252
255
|
console.log(" sl session lock <id> <files...> --intent <why> Claim file locks (fail-closed, TTL auto-release)");
|
|
253
256
|
console.log(" sl session unlock <id> <files...> Release file locks you hold");
|
|
@@ -256,7 +259,10 @@ function printUsage() {
|
|
|
256
259
|
console.log(" sl session recap now <id> --remote --json Summarize current owners, locks, and work");
|
|
257
260
|
console.log(" sl session daemon --session <id> Run Senti recaps/checkpoints for long rooms");
|
|
258
261
|
console.log(" sl session read <id> --tail 20 Read local session stream events");
|
|
259
|
-
console.log(" sl session status <id> --json Show
|
|
262
|
+
console.log(" sl session status <id> --json Show workspace status and file-lock health");
|
|
263
|
+
console.log(" sl session ticket --help List, claim, renew, release, submit tickets and read events");
|
|
264
|
+
console.log(" sl session ticket list <id> --available --agent <agent> Find currently claimable tickets");
|
|
265
|
+
console.log(" sl session checkpoint --help List, show, create and generate durable checkpoints");
|
|
260
266
|
console.log(" sl session leave <id> Leave a session");
|
|
261
267
|
console.log(" sl session list --json List active sessions");
|
|
262
268
|
console.log(" sl session setup-guides <id> --json Upsert AGENTS.md/CLAUDE.md coordination section");
|
|
@@ -321,7 +327,7 @@ function printUsage() {
|
|
|
321
327
|
console.log(" sl mcp registry init-session --force && sl mcp server run --path .");
|
|
322
328
|
console.log(" sl mcp registry init-cli --json");
|
|
323
329
|
console.log("");
|
|
324
|
-
console.log("Docs: https://github.com/
|
|
330
|
+
console.log("Docs: https://github.com/Sentinelayer-AIdenID/create-sentinelayer#readme");
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
function normalizeInterviewInput(
|
|
@@ -597,7 +603,7 @@ async function issueBootstrapToken({ apiUrl, credential }) {
|
|
|
597
603
|
}
|
|
598
604
|
|
|
599
605
|
// The init flow's credential steps, for tests.
|
|
600
|
-
export const
|
|
606
|
+
export const __initCallsForTests = Object.freeze({ pollCliSession, generateArtifacts, issueBootstrapToken, requestJson });
|
|
601
607
|
|
|
602
608
|
function detectRepoSlug(cwd) {
|
|
603
609
|
const gitRemote = spawnSync("git", ["config", "--get", "remote.origin.url"], {
|
|
@@ -675,14 +681,13 @@ function ensureGhAuthSession(ghCommand) {
|
|
|
675
681
|
}
|
|
676
682
|
|
|
677
683
|
function listReposViaGh(ghCommand) {
|
|
678
|
-
const endpoint = "/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member";
|
|
679
684
|
const apiResult = spawnSync(
|
|
680
685
|
ghCommand,
|
|
681
|
-
["api", "--paginate", "--slurp",
|
|
686
|
+
["api", "--paginate", "--slurp", GH_REPOS_ENDPOINT],
|
|
682
687
|
{ encoding: "utf-8" }
|
|
683
688
|
);
|
|
684
689
|
if (apiResult.status !== 0) {
|
|
685
|
-
const fallback = spawnSync(ghCommand, ["api",
|
|
690
|
+
const fallback = spawnSync(ghCommand, ["api", GH_REPOS_ENDPOINT], { encoding: "utf-8" });
|
|
686
691
|
if (fallback.status !== 0) {
|
|
687
692
|
throw new Error(
|
|
688
693
|
String(
|
|
@@ -1059,14 +1064,14 @@ async function runCredentialScan(targetPath) {
|
|
|
1059
1064
|
}
|
|
1060
1065
|
|
|
1061
1066
|
async function writeLocalCommandReport(targetPath, prefix, body, { outputDir = "" } = {}) {
|
|
1062
|
-
const outputRoot = await
|
|
1067
|
+
const { outputRoot, outputFs } = await resolveOutputContext({
|
|
1063
1068
|
cwd: targetPath,
|
|
1064
1069
|
outputDirOverride: outputDir,
|
|
1065
1070
|
});
|
|
1066
1071
|
const reportDir = path.join(outputRoot, "reports");
|
|
1067
|
-
await
|
|
1072
|
+
await outputFs.mkdir(reportDir, { recursive: true });
|
|
1068
1073
|
const reportPath = path.join(reportDir, `${prefix}-${formatTimestampForFile()}.md`);
|
|
1069
|
-
await
|
|
1074
|
+
await outputFs.writeFile(reportPath, `${body}\n`, "utf-8");
|
|
1070
1075
|
return reportPath;
|
|
1071
1076
|
}
|
|
1072
1077
|
|
|
@@ -1348,7 +1353,12 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1348
1353
|
const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
|
|
1349
1354
|
const aiEnabled = !hasCommandOption(args, "--no-ai");
|
|
1350
1355
|
const aiDryRun = hasCommandOption(args, "--ai-dry-run");
|
|
1351
|
-
const
|
|
1356
|
+
const { normalizeSeverityClassifier, parseSeverityEvaluationMaxCost } = await import("./review/typesafe-severity.js");
|
|
1357
|
+
const severityClassifier = normalizeSeverityClassifier(getCommandOptionValue(args, "--severity-classifier") || "off");
|
|
1358
|
+
const maxCostFlag = getCommandOptionValue(args, "--max-cost");
|
|
1359
|
+
const maxCostUsd = severityClassifier === "typesafe"
|
|
1360
|
+
? parseSeverityEvaluationMaxCost(maxCostFlag, 5)
|
|
1361
|
+
: parseFloat(maxCostFlag || "5.0") || 5.0;
|
|
1352
1362
|
const modelOverride = getCommandOptionValue(args, "--model") || "";
|
|
1353
1363
|
const providerOverride = getCommandOptionValue(args, "--provider") || "";
|
|
1354
1364
|
const notifySessionFlag = getCommandOptionValue(args, "--notify-session") || "";
|
|
@@ -1444,6 +1454,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1444
1454
|
provider: providerOverride || undefined,
|
|
1445
1455
|
model: modelOverride || undefined,
|
|
1446
1456
|
maxCostUsd,
|
|
1457
|
+
...(severityClassifier === "typesafe" ? { severityClassifier } : {}),
|
|
1447
1458
|
dryRun: aiDryRun,
|
|
1448
1459
|
outputDir: outputDirArg,
|
|
1449
1460
|
deterministic: {
|
|
@@ -1479,6 +1490,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1479
1490
|
summary: aiOnlySummary,
|
|
1480
1491
|
costUsd: orchestratorResult.totalCostUsd || 0,
|
|
1481
1492
|
model: modelOverride || "multi-persona",
|
|
1493
|
+
...(severityClassifier === "typesafe" ? { severityClassification: orchestratorResult.severityClassification } : {}),
|
|
1482
1494
|
provider: providerOverride || "sentinelayer",
|
|
1483
1495
|
dryRun: aiDryRun,
|
|
1484
1496
|
personas: (orchestratorResult.personas || []).map((p) => ({
|
|
@@ -1520,6 +1532,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1520
1532
|
model: modelOverride || undefined,
|
|
1521
1533
|
sessionId: notifySession,
|
|
1522
1534
|
maxCostUsd,
|
|
1535
|
+
...(severityClassifier === "typesafe" ? { severityClassifier } : {}),
|
|
1523
1536
|
dryRun: aiDryRun,
|
|
1524
1537
|
requireUsageLedger,
|
|
1525
1538
|
sourceCommand: "omargate deep",
|
|
@@ -1543,17 +1556,26 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1543
1556
|
// via reconcileReviewFindings. Use its output directly to avoid double-counting.
|
|
1544
1557
|
// Fallback path (legacy single ai-review): union of det + AI findings.
|
|
1545
1558
|
const aiFindings = aiResult?.findings || [];
|
|
1559
|
+
const aiCoverageFailed = aiEnabled && aiResult?.skipped === true;
|
|
1560
|
+
const coverage = orchestratorResult?.coverage || (aiCoverageFailed ? { status: "incomplete" } : null);
|
|
1561
|
+
const coverageFailureFindings = aiCoverageFailed ? [{
|
|
1562
|
+
severity: "P1", file: "<omargate>", line: 1,
|
|
1563
|
+
message: "Omar AI coverage incomplete: the requested AI review did not complete.",
|
|
1564
|
+
category: "Omar AI Coverage", source: "orchestrator", sources: ["orchestrator"],
|
|
1565
|
+
confidence: 1,
|
|
1566
|
+
remediation: "Resolve the reported AI review failure and rerun the requested review. Partial coverage cannot satisfy a full AI review gate.",
|
|
1567
|
+
}] : [];
|
|
1546
1568
|
const reconciledFromOrchestrator = orchestratorResult?.findings;
|
|
1547
1569
|
const allFindings = reconciledFromOrchestrator && reconciledFromOrchestrator.length >= 0
|
|
1548
1570
|
? reconciledFromOrchestrator
|
|
1549
|
-
: [...detFindings, ...aiFindings];
|
|
1571
|
+
: [...detFindings, ...aiFindings, ...coverageFailureFindings];
|
|
1550
1572
|
const combinedSummary = orchestratorResult?.summary || {
|
|
1551
1573
|
P0: detSummary.P0 + (aiResult?.summary?.P0 || 0),
|
|
1552
|
-
P1: detSummary.P1 + (aiResult?.summary?.P1 || 0),
|
|
1574
|
+
P1: detSummary.P1 + (aiResult?.summary?.P1 || 0) + coverageFailureFindings.length,
|
|
1553
1575
|
P2: detSummary.P2 + (aiResult?.summary?.P2 || 0),
|
|
1554
1576
|
P3: (detSummary.P3 || 0) + (aiResult?.summary?.P3 || 0),
|
|
1555
1577
|
blocking: (detSummary.P0 + (aiResult?.summary?.P0 || 0)) > 0 ||
|
|
1556
|
-
(detSummary.P1 + (aiResult?.summary?.P1 || 0)) > 0,
|
|
1578
|
+
(detSummary.P1 + (aiResult?.summary?.P1 || 0) + coverageFailureFindings.length) > 0,
|
|
1557
1579
|
};
|
|
1558
1580
|
const combinedP0 = combinedSummary.P0 || 0;
|
|
1559
1581
|
const combinedP1 = combinedSummary.P1 || 0;
|
|
@@ -1582,6 +1604,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1582
1604
|
roster: orchestratorResult.roster,
|
|
1583
1605
|
basePersonas: orchestratorResult.basePersonas || [],
|
|
1584
1606
|
personaRouting: orchestratorResult.personaRouting || null,
|
|
1607
|
+
coverage,
|
|
1585
1608
|
findings: (orchestratorResult.personas || []).flatMap((p) => []),
|
|
1586
1609
|
aiFindings: aiFindings,
|
|
1587
1610
|
personaCount: (orchestratorResult.personas || []).length,
|
|
@@ -1591,6 +1614,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1591
1614
|
artifactPaths.personas = await writeJsonArtifact("REVIEW_PERSONAS.json", {
|
|
1592
1615
|
runId: orchestratorResult.runId,
|
|
1593
1616
|
personas: orchestratorResult.personas || [],
|
|
1617
|
+
coverage,
|
|
1594
1618
|
});
|
|
1595
1619
|
artifactPaths.reconciled = await writeJsonArtifact("REVIEW_RECONCILED.json", {
|
|
1596
1620
|
runId: orchestratorResult.runId,
|
|
@@ -1599,6 +1623,7 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1599
1623
|
reconciliation: orchestratorResult.reconciliation,
|
|
1600
1624
|
personaRouting: orchestratorResult.personaRouting || null,
|
|
1601
1625
|
findingsBySource: orchestratorResult.findingsBySource,
|
|
1626
|
+
coverage,
|
|
1602
1627
|
});
|
|
1603
1628
|
}
|
|
1604
1629
|
|
|
@@ -1616,6 +1641,7 @@ Summary:
|
|
|
1616
1641
|
- Files scanned: ${scannedFiles}
|
|
1617
1642
|
- Scope mode: ${scopeMode}
|
|
1618
1643
|
- Persona routing: ${orchestratorResult?.personaRouting?.enabled ? `${orchestratorResult.personaRouting.effectivePersonas.length}/${orchestratorResult.personaRouting.basePersonas.length} personas for ${orchestratorResult.personaRouting.changedFileCount} changed files` : "full roster"}
|
|
1644
|
+
- AI coverage: ${coverage?.status || (aiEnabled ? "unknown" : "not requested")}
|
|
1619
1645
|
- Deterministic findings: P0=${detSummary.P0} P1=${detSummary.P1} P2=${detSummary.P2} P3=${detSummary.P3 || 0}
|
|
1620
1646
|
- 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"}
|
|
1621
1647
|
- Reconciled (deduped + confidence-boosted): P0=${combinedP0} P1=${combinedP1} P2=${combinedP2} P3=${combinedP3}
|
|
@@ -1657,6 +1683,7 @@ ${formatFindingsMarkdown(allFindings)}
|
|
|
1657
1683
|
p2: combinedP2,
|
|
1658
1684
|
p3: combinedP3,
|
|
1659
1685
|
blocking: combinedP0 > 0 || combinedP1 > 0,
|
|
1686
|
+
coverage,
|
|
1660
1687
|
elapsedMs: totalElapsedMs,
|
|
1661
1688
|
artifacts: artifactPaths,
|
|
1662
1689
|
deterministic: {
|
|
@@ -2134,6 +2161,55 @@ async function ensureEnvFileIgnored(projectDir) {
|
|
|
2134
2161
|
await writeTextFile(gitignorePath, next);
|
|
2135
2162
|
}
|
|
2136
2163
|
|
|
2164
|
+
export async function ensureSentinelConfigIgnored(projectDir) {
|
|
2165
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
2166
|
+
let existing = "";
|
|
2167
|
+
if (fs.existsSync(gitignorePath)) {
|
|
2168
|
+
existing = await fsp.readFile(gitignorePath, "utf-8");
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
const entry = ".sentinelayer/";
|
|
2172
|
+
const lines = existing.split(/\r?\n/);
|
|
2173
|
+
const hasEntry = lines.some((line) => {
|
|
2174
|
+
const normalized = String(line || "").trim();
|
|
2175
|
+
return normalized === entry || normalized === ".sentinelayer";
|
|
2176
|
+
});
|
|
2177
|
+
if (hasEntry) {
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
let next = "";
|
|
2182
|
+
if (existing.trim().length === 0) {
|
|
2183
|
+
next = `${entry}\n`;
|
|
2184
|
+
} else if (existing.endsWith("\n")) {
|
|
2185
|
+
next = `${existing}${entry}\n`;
|
|
2186
|
+
} else {
|
|
2187
|
+
next = `${existing}\n${entry}\n`;
|
|
2188
|
+
}
|
|
2189
|
+
await writeTextFile(gitignorePath, next);
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
export async function warnIfSentinelConfigTracked(projectDir) {
|
|
2193
|
+
try {
|
|
2194
|
+
const configRelPath = ".sentinelayer/config.json";
|
|
2195
|
+
const result = spawnSync("git", ["-C", projectDir, "ls-files", "--error-unmatch", configRelPath], {
|
|
2196
|
+
encoding: "utf-8",
|
|
2197
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2198
|
+
});
|
|
2199
|
+
if (result.status === 0) {
|
|
2200
|
+
console.log(
|
|
2201
|
+
pc.yellow(
|
|
2202
|
+
`! ${configRelPath} is tracked by git and may contain a token from a previous init. ` +
|
|
2203
|
+
`To remove it: git rm --cached ${configRelPath} && git commit -m "Remove tracked SentinelLayer config". ` +
|
|
2204
|
+
`Then rotate your token at https://sentinelayer.com/dashboard.`
|
|
2205
|
+
)
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
} catch {
|
|
2209
|
+
// git may not be installed or projectDir may not be a repo; silently skip.
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2137
2213
|
async function ensureSentinelStartScript(projectDir, projectName) {
|
|
2138
2214
|
const packagePath = path.join(projectDir, "package.json");
|
|
2139
2215
|
const fallback = {
|
|
@@ -2789,7 +2865,7 @@ function confirmSecretsRepository(repoSlug) {
|
|
|
2789
2865
|
} catch (error) {
|
|
2790
2866
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
2791
2867
|
}
|
|
2792
|
-
const view = spawnSync(ghCommand, ["api",
|
|
2868
|
+
const view = spawnSync(ghCommand, ["api", ghRepoApiPath(repoSlug)], { encoding: "utf-8" });
|
|
2793
2869
|
if (view.status !== 0) {
|
|
2794
2870
|
return { ok: false, reason: `gh could not read ${repoSlug}: ${String(view.stderr || view.stdout || "").trim()}` };
|
|
2795
2871
|
}
|
|
@@ -2895,7 +2971,7 @@ function resolveGeneratedSpecId(generated) {
|
|
|
2895
2971
|
return "";
|
|
2896
2972
|
}
|
|
2897
2973
|
|
|
2898
|
-
async function writeInitConfigLockfile({
|
|
2974
|
+
export async function writeInitConfigLockfile({
|
|
2899
2975
|
projectDir,
|
|
2900
2976
|
specId,
|
|
2901
2977
|
sentinelayerToken,
|
|
@@ -2910,7 +2986,6 @@ async function writeInitConfigLockfile({
|
|
|
2910
2986
|
schema_version: 1,
|
|
2911
2987
|
generated_at: new Date().toISOString(),
|
|
2912
2988
|
spec_id: String(specId || "").trim(),
|
|
2913
|
-
sentinelayer_token: String(sentinelayerToken || "").trim(),
|
|
2914
2989
|
required_secret_name: String(secretName || "SENTINELAYER_TOKEN").trim() || "SENTINELAYER_TOKEN",
|
|
2915
2990
|
repo_slug: normalizeRepoSlug(repoSlug || ""),
|
|
2916
2991
|
workflow_path: path.relative(projectDir, workflowPath).replace(/\\/g, "/"),
|
|
@@ -3602,8 +3677,10 @@ export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
|
|
|
3602
3677
|
|
|
3603
3678
|
if (sentinelayerToken) {
|
|
3604
3679
|
await ensureEnvFileIgnored(projectDir);
|
|
3680
|
+
await ensureSentinelConfigIgnored(projectDir);
|
|
3605
3681
|
await upsertEnvVariable(path.join(projectDir, ".env"), secretName, sentinelayerToken);
|
|
3606
3682
|
}
|
|
3683
|
+
await warnIfSentinelConfigTracked(projectDir);
|
|
3607
3684
|
await ensureGitRepositorySetup({
|
|
3608
3685
|
projectDir,
|
|
3609
3686
|
repoSlug: interview.connectRepo ? interview.repoSlug : "",
|