sentinelayer-cli 0.1.2 → 0.4.4
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 +998 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +63 -54
- package/src/agents/jules/config/definition.js +209 -209
- package/src/agents/jules/config/system-prompt.js +175 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +377 -377
- package/src/agents/jules/loop.js +367 -367
- package/src/agents/jules/pulse.js +327 -319
- package/src/agents/jules/stream.js +186 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +308 -308
- package/src/agents/jules/tools/auth-audit.js +557 -222
- package/src/agents/jules/tools/dispatch.js +327 -327
- package/src/agents/jules/tools/file-edit.js +180 -180
- package/src/agents/jules/tools/file-read.js +100 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +168 -168
- package/src/agents/jules/tools/grep.js +228 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +161 -161
- package/src/agents/jules/tools/runtime-audit.js +503 -493
- package/src/agents/jules/tools/shell.js +383 -383
- package/src/agents/jules/tools/url-policy.js +100 -0
- package/src/ai/aidenid.js +972 -945
- package/src/ai/client.js +508 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/gate.js +45 -11
- package/src/auth/http.js +270 -113
- package/src/auth/service.js +891 -848
- package/src/auth/session-store.js +359 -345
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1337
- package/src/commands/ai/provision-governance.js +1272 -1246
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1166 -1166
- package/src/commands/auth.js +375 -366
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -404
- package/src/commands/omargate.js +15 -15
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +866 -788
- package/src/commands/spec.js +716 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +510 -510
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +770 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +626 -626
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/watchdog.js +971 -971
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/legacy-cli.js +2592 -2435
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +118 -106
- package/src/review/ai-review.js +669 -669
- package/src/review/local-review.js +1295 -1284
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -0
- package/src/scaffold/templates.js +150 -0
- package/src/scan/generator.js +418 -351
- package/src/scan/gh-secrets.js +107 -0
- package/src/spec/generator.js +519 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/sync.js +107 -61
- package/src/ui/markdown.js +220 -220
package/src/legacy-cli.js
CHANGED
|
@@ -1,2435 +1,2592 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
|
-
import fs from "node:fs";
|
|
5
|
-
import fsp from "node:fs/promises";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import process from "node:process";
|
|
8
|
-
import { spawnSync } from "node:child_process";
|
|
9
|
-
import { createInterface } from "node:readline/promises";
|
|
10
|
-
import { stdin as input, stdout as output } from "node:process";
|
|
11
|
-
import { pathToFileURL } from "node:url";
|
|
12
|
-
|
|
13
|
-
import open from "open";
|
|
14
|
-
import pc from "picocolors";
|
|
15
|
-
import prompts from "prompts";
|
|
16
|
-
import {
|
|
17
|
-
DEFAULT_CODING_AGENT_ID,
|
|
18
|
-
detectCodingAgentFromEnv,
|
|
19
|
-
detectIdeFromEnv,
|
|
20
|
-
listSupportedCodingAgents,
|
|
21
|
-
resolveCodingAgent,
|
|
22
|
-
} from "./config/agent-dictionary.js";
|
|
23
|
-
import { resolveOutputRoot } from "./config/service.js";
|
|
24
|
-
import { collectCodebaseIngest, formatIngestSummary } from "./ingest/engine.js";
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
.
|
|
97
|
-
.replace(
|
|
98
|
-
.
|
|
99
|
-
.replace(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
let
|
|
137
|
-
let
|
|
138
|
-
let
|
|
139
|
-
let
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
console.log(
|
|
193
|
-
console.log("
|
|
194
|
-
console.log("
|
|
195
|
-
console.log("
|
|
196
|
-
console.log("
|
|
197
|
-
console.log("
|
|
198
|
-
console.log("
|
|
199
|
-
console.log("
|
|
200
|
-
console.log("
|
|
201
|
-
console.log("
|
|
202
|
-
console.log("");
|
|
203
|
-
console.log("
|
|
204
|
-
console.log("
|
|
205
|
-
console.log("
|
|
206
|
-
console.log("
|
|
207
|
-
console.log(" --
|
|
208
|
-
console.log(" --
|
|
209
|
-
console.log("
|
|
210
|
-
console.log("
|
|
211
|
-
console.log("
|
|
212
|
-
console.log("");
|
|
213
|
-
console.log("
|
|
214
|
-
console.log("
|
|
215
|
-
console.log("
|
|
216
|
-
console.log("
|
|
217
|
-
console.log("
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
if (interview.
|
|
311
|
-
throw new Error("
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
},
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
function
|
|
515
|
-
return
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const
|
|
525
|
-
cwd,
|
|
526
|
-
encoding: "utf-8",
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const
|
|
538
|
-
if (
|
|
539
|
-
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
);
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
)
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
const
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
"
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
"
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
return
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
-
|
|
1528
|
-
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
-
|
|
1538
|
-
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
-
|
|
1543
|
-
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
-
|
|
1589
|
-
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
-
|
|
1611
|
-
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
)
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
)
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
);
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
)
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
})
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
await
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
);
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
const
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from "node:crypto";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import fsp from "node:fs/promises";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { createInterface } from "node:readline/promises";
|
|
10
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
|
|
13
|
+
import open from "open";
|
|
14
|
+
import pc from "picocolors";
|
|
15
|
+
import prompts from "prompts";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_CODING_AGENT_ID,
|
|
18
|
+
detectCodingAgentFromEnv,
|
|
19
|
+
detectIdeFromEnv,
|
|
20
|
+
listSupportedCodingAgents,
|
|
21
|
+
resolveCodingAgent,
|
|
22
|
+
} from "./config/agent-dictionary.js";
|
|
23
|
+
import { resolveOutputRoot } from "./config/service.js";
|
|
24
|
+
import { collectCodebaseIngest, formatIngestSummary } from "./ingest/engine.js";
|
|
25
|
+
import { getExpressTemplate, getPackageJsonTemplate, buildReadmeContent } from "./scaffold/templates.js";
|
|
26
|
+
import { generateScaffold } from "./scaffold/generator.js";
|
|
27
|
+
|
|
28
|
+
let DEFAULT_API_URL = process.env.SENTINELAYER_API_URL || "https://api.sentinelayer.com";
|
|
29
|
+
let DEFAULT_WEB_URL = process.env.SENTINELAYER_WEB_URL || "https://sentinelayer.com";
|
|
30
|
+
let DEFAULT_GITHUB_CLONE_BASE_URL =
|
|
31
|
+
process.env.SENTINELAYER_GITHUB_CLONE_BASE_URL || "https://github.com";
|
|
32
|
+
const DEFAULT_AUTH_TIMEOUT_MS = 10 * 60 * 1000;
|
|
33
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 20_000;
|
|
34
|
+
const PACKAGE_JSON_PATH = new URL("../package.json", import.meta.url);
|
|
35
|
+
|
|
36
|
+
function refreshRuntimeDefaults() {
|
|
37
|
+
DEFAULT_API_URL = process.env.SENTINELAYER_API_URL || "https://api.sentinelayer.com";
|
|
38
|
+
DEFAULT_WEB_URL = process.env.SENTINELAYER_WEB_URL || "https://sentinelayer.com";
|
|
39
|
+
DEFAULT_GITHUB_CLONE_BASE_URL =
|
|
40
|
+
process.env.SENTINELAYER_GITHUB_CLONE_BASE_URL || "https://github.com";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function resolveCliVersion() {
|
|
44
|
+
try {
|
|
45
|
+
const raw = fs.readFileSync(PACKAGE_JSON_PATH, "utf-8");
|
|
46
|
+
const pkg = JSON.parse(raw);
|
|
47
|
+
const version = String(pkg && pkg.version ? pkg.version : "").trim();
|
|
48
|
+
if (version) {
|
|
49
|
+
return version;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Ignore and fall through to static fallback.
|
|
53
|
+
}
|
|
54
|
+
return "0.1.0";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const CLI_VERSION = resolveCliVersion();
|
|
58
|
+
|
|
59
|
+
const DEFAULT_MODEL_BY_PROVIDER = {
|
|
60
|
+
openai: "gpt-5.3-codex",
|
|
61
|
+
anthropic: "claude-sonnet-4-6",
|
|
62
|
+
google: "gemini-2.5-flash",
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const VALID_AI_PROVIDERS = new Set(["openai", "anthropic", "google"]);
|
|
66
|
+
const VALID_GENERATION_MODES = new Set(["detailed", "quick", "enterprise"]);
|
|
67
|
+
const VALID_AUDIENCE_LEVELS = new Set(["developer", "intermediate", "beginner"]);
|
|
68
|
+
const VALID_PROJECT_TYPES = new Set(["greenfield", "add_feature", "bugfix"]);
|
|
69
|
+
const VALID_AUTH_MODES = new Set(["sentinelayer", "byok"]);
|
|
70
|
+
|
|
71
|
+
class SentinelayerApiError extends Error {
|
|
72
|
+
constructor(message, { code = "API_ERROR", status = 500, requestId = null } = {}) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = "SentinelayerApiError";
|
|
75
|
+
this.code = code;
|
|
76
|
+
this.status = status;
|
|
77
|
+
this.requestId = requestId;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nowIso() {
|
|
82
|
+
return new Date().toISOString();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseCommaList(value) {
|
|
86
|
+
return String(value || "")
|
|
87
|
+
.split(",")
|
|
88
|
+
.map((item) => item.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sanitizeProjectName(value) {
|
|
93
|
+
const raw = String(value || "").trim();
|
|
94
|
+
if (!raw) return "";
|
|
95
|
+
return raw
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.replace(/[^a-z0-9-_ ]+/g, "")
|
|
98
|
+
.trim()
|
|
99
|
+
.replace(/\s+/g, "-")
|
|
100
|
+
.replace(/-+/g, "-")
|
|
101
|
+
.replace(/^-|-$/g, "");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function normalizeRepoSlug(value) {
|
|
105
|
+
return String(value || "").trim().replace(/\.git$/i, "");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isValidRepoSlug(value) {
|
|
109
|
+
return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalizeRepoSlug(value));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function getRepoNameFromSlug(value) {
|
|
113
|
+
const normalized = normalizeRepoSlug(value);
|
|
114
|
+
const parts = normalized.split("/");
|
|
115
|
+
if (parts.length !== 2) return "";
|
|
116
|
+
return sanitizeProjectName(parts[1]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isValidSecretName(value) {
|
|
120
|
+
return /^[A-Z][A-Z0-9_]{1,127}$/.test(String(value || "").trim());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function boolFromEnv(value) {
|
|
124
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
125
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeListInput(value) {
|
|
129
|
+
if (Array.isArray(value)) {
|
|
130
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
131
|
+
}
|
|
132
|
+
return parseCommaList(value);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseCliArgs(argv) {
|
|
136
|
+
let projectName = "";
|
|
137
|
+
let interviewFile = "";
|
|
138
|
+
let nonInteractive = boolFromEnv(process.env.SENTINELAYER_CLI_NON_INTERACTIVE);
|
|
139
|
+
let skipBrowserOpen = boolFromEnv(process.env.SENTINELAYER_CLI_SKIP_BROWSER_OPEN);
|
|
140
|
+
let showHelp = false;
|
|
141
|
+
let showVersion = false;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
144
|
+
const arg = String(argv[i] || "").trim();
|
|
145
|
+
if (!arg) continue;
|
|
146
|
+
if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
147
|
+
showHelp = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (arg === "--version" || arg === "-v") {
|
|
151
|
+
showVersion = true;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (arg === "--non-interactive") {
|
|
155
|
+
nonInteractive = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (arg === "--skip-browser-open") {
|
|
159
|
+
skipBrowserOpen = true;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (arg === "--interview-file") {
|
|
163
|
+
const next = String(argv[i + 1] || "").trim();
|
|
164
|
+
if (!next) {
|
|
165
|
+
throw new Error("Missing value for --interview-file");
|
|
166
|
+
}
|
|
167
|
+
interviewFile = next;
|
|
168
|
+
i += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (arg.startsWith("-")) {
|
|
172
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
173
|
+
}
|
|
174
|
+
if (!projectName) {
|
|
175
|
+
projectName = arg;
|
|
176
|
+
} else {
|
|
177
|
+
throw new Error(`Unexpected extra argument: ${arg}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
projectName,
|
|
183
|
+
interviewFile,
|
|
184
|
+
nonInteractive,
|
|
185
|
+
skipBrowserOpen,
|
|
186
|
+
showHelp,
|
|
187
|
+
showVersion,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function printUsage() {
|
|
192
|
+
console.log(`sentinelayer-cli v${CLI_VERSION}`);
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log("Usage: sl <command> [options]");
|
|
195
|
+
console.log("");
|
|
196
|
+
console.log("Scaffold:");
|
|
197
|
+
console.log(" sl [project-name] Create a new project with SentinelLayer scaffolding");
|
|
198
|
+
console.log(" sl init [project-name] Same as above (interactive or --non-interactive)");
|
|
199
|
+
console.log("");
|
|
200
|
+
console.log("Authentication:");
|
|
201
|
+
console.log(" sl auth login Log in via browser (provisions SentinelLayer + AIdenID)");
|
|
202
|
+
console.log(" sl auth status Show authentication and AIdenID provisioning status");
|
|
203
|
+
console.log(" sl auth sessions List stored session metadata");
|
|
204
|
+
console.log(" sl auth logout Clear local session");
|
|
205
|
+
console.log("");
|
|
206
|
+
console.log("Security & Review:");
|
|
207
|
+
console.log(" sl review scan --path . --json Deterministic code review (full or --mode diff)");
|
|
208
|
+
console.log(" sl /omargate deep --path . --json Local Omar Gate security scan (P0/P1/P2 findings)");
|
|
209
|
+
console.log(" sl scan init Generate .github/workflows/omar-gate.yml from spec");
|
|
210
|
+
console.log(" sl scan setup-secrets --repo <slug> Inject SENTINELAYER_TOKEN into GitHub repo secrets");
|
|
211
|
+
console.log("");
|
|
212
|
+
console.log("Specification & Planning:");
|
|
213
|
+
console.log(" sl spec list-templates List available project templates");
|
|
214
|
+
console.log(" sl spec generate Generate SPEC.md from template or AI");
|
|
215
|
+
console.log(" sl prompt generate Generate agent execution prompt from spec");
|
|
216
|
+
console.log(" sl guide generate Generate BUILD_GUIDE.md from spec");
|
|
217
|
+
console.log(" sl ingest map --json Codebase AST ingest with framework detection");
|
|
218
|
+
console.log("");
|
|
219
|
+
console.log("Audit & Quality:");
|
|
220
|
+
console.log(" sl audit --path . --json Full 15-agent audit swarm");
|
|
221
|
+
console.log(" sl audit frontend --path . --json Jules frontend audit (--stream for NDJSON, --url for runtime)");
|
|
222
|
+
console.log(" sl audit security --path . --json Security-focused audit");
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log("AIdenID (Identity Testing):");
|
|
225
|
+
console.log(" sl ai identity provision --execute Provision ephemeral test email (auto-credentials after login)");
|
|
226
|
+
console.log(" sl ai identity wait-for-otp <id> Poll for OTP extraction from provisioned email");
|
|
227
|
+
console.log(" sl ai identity list List tracked identities");
|
|
228
|
+
console.log(" sl ai identity lineage <id> Show identity parent/child tree");
|
|
229
|
+
console.log(" sl ai identity revoke <id> Revoke a provisioned identity");
|
|
230
|
+
console.log("");
|
|
231
|
+
console.log("Cost & Policy:");
|
|
232
|
+
console.log(" sl cost show --json Show accumulated cost tracking");
|
|
233
|
+
console.log(" sl policy list List available policy packs");
|
|
234
|
+
console.log(" sl policy use <pack> Switch active policy pack");
|
|
235
|
+
console.log("");
|
|
236
|
+
console.log("Advanced:");
|
|
237
|
+
console.log(" sl swarm plan --path . --json Multi-agent swarm planning");
|
|
238
|
+
console.log(" sl mcp list --json List MCP registries and adapters");
|
|
239
|
+
console.log(" sl telemetry show --json Show run event ledger");
|
|
240
|
+
console.log(" sl config list Show current configuration");
|
|
241
|
+
console.log("");
|
|
242
|
+
console.log("Options:");
|
|
243
|
+
console.log(" -h, --help Show this help");
|
|
244
|
+
console.log(" -v, --version Show CLI version");
|
|
245
|
+
console.log(" --json Machine-readable JSON output");
|
|
246
|
+
console.log(" --path PATH Target workspace path");
|
|
247
|
+
console.log(" --non-interactive Disable prompts (require --interview-file)");
|
|
248
|
+
console.log("");
|
|
249
|
+
console.log("Quickstart:");
|
|
250
|
+
console.log(" sl auth login && npx create-sentinelayer my-app && cd my-app");
|
|
251
|
+
console.log(" # Then hand docs/spec.md to your coding agent");
|
|
252
|
+
console.log("");
|
|
253
|
+
console.log("Docs: https://sentinelayer.com/docs");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function normalizeInterviewInput(
|
|
257
|
+
raw,
|
|
258
|
+
{ argProjectName = "", detectedRepo = "", detectedCodingAgent = DEFAULT_CODING_AGENT_ID } = {}
|
|
259
|
+
) {
|
|
260
|
+
const obj = raw && typeof raw === "object" ? raw : {};
|
|
261
|
+
const aiProvider = String(obj.aiProvider || "openai").trim().toLowerCase();
|
|
262
|
+
const generationMode = String(obj.generationMode || "detailed").trim().toLowerCase();
|
|
263
|
+
const audienceLevel = String(obj.audienceLevel || "developer").trim().toLowerCase();
|
|
264
|
+
const codingAgentCandidate = String(obj.codingAgent || detectedCodingAgent || DEFAULT_CODING_AGENT_ID)
|
|
265
|
+
.trim()
|
|
266
|
+
.toLowerCase();
|
|
267
|
+
const authMode = String(obj.authMode || "sentinelayer").trim().toLowerCase();
|
|
268
|
+
const explicitRepoSlug = normalizeRepoSlug(obj.repoSlug || "");
|
|
269
|
+
const connectRepo = Boolean(obj.connectRepo) || isValidRepoSlug(explicitRepoSlug);
|
|
270
|
+
const repoSlug = normalizeRepoSlug(obj.repoSlug || detectedRepo || "");
|
|
271
|
+
const buildFromExistingRepo = connectRepo ? Boolean(obj.buildFromExistingRepo) : false;
|
|
272
|
+
const rawProjectType = String(obj.projectType || "")
|
|
273
|
+
.trim()
|
|
274
|
+
.toLowerCase();
|
|
275
|
+
const fallbackProjectType =
|
|
276
|
+
buildFromExistingRepo || (connectRepo && isValidRepoSlug(repoSlug)) || isValidRepoSlug(detectedRepo)
|
|
277
|
+
? "add_feature"
|
|
278
|
+
: "greenfield";
|
|
279
|
+
const projectType = VALID_PROJECT_TYPES.has(rawProjectType) ? rawProjectType : fallbackProjectType;
|
|
280
|
+
const normalizedAuthMode = VALID_AUTH_MODES.has(authMode) ? authMode : "sentinelayer";
|
|
281
|
+
const derivedProjectName = sanitizeProjectName(obj.projectName || argProjectName) || getRepoNameFromSlug(repoSlug);
|
|
282
|
+
let resolvedCodingAgent;
|
|
283
|
+
try {
|
|
284
|
+
resolvedCodingAgent = resolveCodingAgent(codingAgentCandidate).id;
|
|
285
|
+
} catch {
|
|
286
|
+
resolvedCodingAgent = DEFAULT_CODING_AGENT_ID;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const normalized = {
|
|
290
|
+
projectName: derivedProjectName,
|
|
291
|
+
projectDescription: String(obj.projectDescription || "").trim(),
|
|
292
|
+
aiProvider: VALID_AI_PROVIDERS.has(aiProvider) ? aiProvider : "openai",
|
|
293
|
+
generationMode: VALID_GENERATION_MODES.has(generationMode) ? generationMode : "detailed",
|
|
294
|
+
audienceLevel: VALID_AUDIENCE_LEVELS.has(audienceLevel) ? audienceLevel : "developer",
|
|
295
|
+
projectType,
|
|
296
|
+
codingAgent: resolvedCodingAgent,
|
|
297
|
+
techStack: normalizeListInput(obj.techStack),
|
|
298
|
+
features: normalizeListInput(obj.features),
|
|
299
|
+
authMode: normalizedAuthMode,
|
|
300
|
+
connectRepo,
|
|
301
|
+
repoSlug: connectRepo ? repoSlug : "",
|
|
302
|
+
buildFromExistingRepo,
|
|
303
|
+
injectSecret: connectRepo && normalizedAuthMode === "sentinelayer" ? Boolean(obj.injectSecret) : false,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return normalized;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function validateInterviewInput(interview) {
|
|
310
|
+
if (!interview.projectName) {
|
|
311
|
+
throw new Error("Project name is required.");
|
|
312
|
+
}
|
|
313
|
+
if (String(interview.projectDescription || "").trim().length < 15) {
|
|
314
|
+
throw new Error("Project description must be at least 15 characters.");
|
|
315
|
+
}
|
|
316
|
+
if (!VALID_AI_PROVIDERS.has(interview.aiProvider)) {
|
|
317
|
+
throw new Error("Invalid aiProvider. Use openai, anthropic, or google.");
|
|
318
|
+
}
|
|
319
|
+
if (!VALID_GENERATION_MODES.has(interview.generationMode)) {
|
|
320
|
+
throw new Error("Invalid generationMode. Use detailed, quick, or enterprise.");
|
|
321
|
+
}
|
|
322
|
+
if (!VALID_AUDIENCE_LEVELS.has(interview.audienceLevel)) {
|
|
323
|
+
throw new Error("Invalid audienceLevel. Use developer, intermediate, or beginner.");
|
|
324
|
+
}
|
|
325
|
+
if (!VALID_PROJECT_TYPES.has(interview.projectType)) {
|
|
326
|
+
throw new Error("Invalid projectType. Use greenfield, add_feature, or bugfix.");
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
resolveCodingAgent(interview.codingAgent || DEFAULT_CODING_AGENT_ID);
|
|
330
|
+
} catch {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`Invalid codingAgent. Use one of: ${listSupportedCodingAgents()
|
|
333
|
+
.map((agent) => agent.id)
|
|
334
|
+
.join(", ")}.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (!VALID_AUTH_MODES.has(interview.authMode)) {
|
|
338
|
+
throw new Error("Invalid authMode. Use sentinelayer or byok.");
|
|
339
|
+
}
|
|
340
|
+
if (interview.connectRepo && !isValidRepoSlug(interview.repoSlug)) {
|
|
341
|
+
throw new Error("Invalid repo slug. Expected owner/repo.");
|
|
342
|
+
}
|
|
343
|
+
if (interview.buildFromExistingRepo && !interview.connectRepo) {
|
|
344
|
+
throw new Error("buildFromExistingRepo requires connectRepo=true.");
|
|
345
|
+
}
|
|
346
|
+
if (interview.injectSecret && interview.authMode !== "sentinelayer") {
|
|
347
|
+
throw new Error("injectSecret requires authMode=sentinelayer.");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async function loadAutomatedInterview({
|
|
352
|
+
argProjectName,
|
|
353
|
+
detectedRepo,
|
|
354
|
+
detectedCodingAgent,
|
|
355
|
+
interviewFile,
|
|
356
|
+
}) {
|
|
357
|
+
const envPayload = String(process.env.SENTINELAYER_CLI_INTERVIEW_JSON || "").trim();
|
|
358
|
+
let payload = null;
|
|
359
|
+
let source = "";
|
|
360
|
+
|
|
361
|
+
if (interviewFile) {
|
|
362
|
+
const filePath = path.resolve(process.cwd(), interviewFile);
|
|
363
|
+
const fileContents = await fsp.readFile(filePath, "utf-8");
|
|
364
|
+
payload = JSON.parse(fileContents);
|
|
365
|
+
source = `--interview-file (${filePath})`;
|
|
366
|
+
} else if (envPayload) {
|
|
367
|
+
payload = JSON.parse(envPayload);
|
|
368
|
+
source = "SENTINELAYER_CLI_INTERVIEW_JSON";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (payload === null) {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const normalized = normalizeInterviewInput(payload, {
|
|
376
|
+
argProjectName,
|
|
377
|
+
detectedRepo,
|
|
378
|
+
detectedCodingAgent,
|
|
379
|
+
});
|
|
380
|
+
try {
|
|
381
|
+
validateInterviewInput(normalized);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
384
|
+
throw new Error(`Invalid interview payload from ${source}: ${message}`);
|
|
385
|
+
}
|
|
386
|
+
return normalized;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function waitForEnter(message) {
|
|
390
|
+
const rl = createInterface({ input, output });
|
|
391
|
+
try {
|
|
392
|
+
await rl.question(`${message}\n`);
|
|
393
|
+
} finally {
|
|
394
|
+
rl.close();
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function sleep(ms) {
|
|
399
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function requestJson(url, { method = "GET", headers = {}, body, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = {}) {
|
|
403
|
+
const controller = new AbortController();
|
|
404
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
405
|
+
try {
|
|
406
|
+
const response = await fetch(url, {
|
|
407
|
+
method,
|
|
408
|
+
headers: {
|
|
409
|
+
"Content-Type": "application/json",
|
|
410
|
+
...headers,
|
|
411
|
+
},
|
|
412
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
413
|
+
signal: controller.signal,
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
const text = await response.text();
|
|
417
|
+
let payload = null;
|
|
418
|
+
if (text.trim().length > 0) {
|
|
419
|
+
try {
|
|
420
|
+
payload = JSON.parse(text);
|
|
421
|
+
} catch {
|
|
422
|
+
payload = null;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (!response.ok) {
|
|
427
|
+
const errorEnvelope = payload && typeof payload === "object" ? payload.error || null : null;
|
|
428
|
+
const code = errorEnvelope?.code || `HTTP_${response.status}`;
|
|
429
|
+
const message = errorEnvelope?.message || `Request failed (${response.status})`;
|
|
430
|
+
const requestId = errorEnvelope?.request_id || null;
|
|
431
|
+
throw new SentinelayerApiError(message, {
|
|
432
|
+
code,
|
|
433
|
+
status: response.status,
|
|
434
|
+
requestId,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (payload === null) {
|
|
439
|
+
return {};
|
|
440
|
+
}
|
|
441
|
+
return payload;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (error instanceof SentinelayerApiError) {
|
|
444
|
+
throw error;
|
|
445
|
+
}
|
|
446
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
447
|
+
throw new SentinelayerApiError("Sentinelayer request timed out", {
|
|
448
|
+
code: "NETWORK_TIMEOUT",
|
|
449
|
+
status: 504,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
throw new SentinelayerApiError("Unable to reach Sentinelayer API", {
|
|
453
|
+
code: "NETWORK_ERROR",
|
|
454
|
+
status: 503,
|
|
455
|
+
});
|
|
456
|
+
} finally {
|
|
457
|
+
clearTimeout(timeout);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function detectIde() {
|
|
462
|
+
return detectIdeFromEnv(process.env).id;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function startCliSession({ apiUrl, challenge, cliVersion }) {
|
|
466
|
+
return requestJson(`${apiUrl}/api/v1/auth/cli/sessions/start`, {
|
|
467
|
+
method: "POST",
|
|
468
|
+
body: {
|
|
469
|
+
challenge,
|
|
470
|
+
ide: detectIde(),
|
|
471
|
+
cli_version: cliVersion,
|
|
472
|
+
},
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function pollCliSession({
|
|
477
|
+
apiUrl,
|
|
478
|
+
sessionId,
|
|
479
|
+
challenge,
|
|
480
|
+
pollIntervalSeconds,
|
|
481
|
+
timeoutMs = DEFAULT_AUTH_TIMEOUT_MS,
|
|
482
|
+
}) {
|
|
483
|
+
const start = Date.now();
|
|
484
|
+
while (Date.now() - start < timeoutMs) {
|
|
485
|
+
const response = await requestJson(`${apiUrl}/api/v1/auth/cli/sessions/poll`, {
|
|
486
|
+
method: "POST",
|
|
487
|
+
body: {
|
|
488
|
+
session_id: sessionId,
|
|
489
|
+
challenge,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
if (response.status === "approved" && response.auth_token) {
|
|
493
|
+
return response;
|
|
494
|
+
}
|
|
495
|
+
await sleep(Math.max(1, Number(pollIntervalSeconds) || 2) * 1000);
|
|
496
|
+
}
|
|
497
|
+
throw new SentinelayerApiError("CLI authentication timed out. Restart and try again.", {
|
|
498
|
+
code: "CLI_AUTH_TIMEOUT",
|
|
499
|
+
status: 408,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function generateArtifacts({ apiUrl, authToken, payload }) {
|
|
504
|
+
return requestJson(`${apiUrl}/api/v1/builder/generate`, {
|
|
505
|
+
method: "POST",
|
|
506
|
+
headers: {
|
|
507
|
+
Authorization: `Bearer ${authToken}`,
|
|
508
|
+
},
|
|
509
|
+
body: payload,
|
|
510
|
+
timeoutMs: 180_000,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function issueBootstrapToken({ apiUrl, authToken }) {
|
|
515
|
+
return requestJson(`${apiUrl}/api/v1/builder/bootstrap-token`, {
|
|
516
|
+
method: "POST",
|
|
517
|
+
headers: {
|
|
518
|
+
Authorization: `Bearer ${authToken}`,
|
|
519
|
+
},
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function detectRepoSlug(cwd) {
|
|
524
|
+
const gitRemote = spawnSync("git", ["config", "--get", "remote.origin.url"], {
|
|
525
|
+
cwd,
|
|
526
|
+
encoding: "utf-8",
|
|
527
|
+
});
|
|
528
|
+
if (gitRemote.status !== 0) return null;
|
|
529
|
+
const remote = String(gitRemote.stdout || "").trim();
|
|
530
|
+
if (!remote) return null;
|
|
531
|
+
|
|
532
|
+
const sshMatch = remote.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/i);
|
|
533
|
+
if (sshMatch) {
|
|
534
|
+
return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const httpsMatch = remote.match(/^https?:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/i);
|
|
538
|
+
if (httpsMatch) {
|
|
539
|
+
return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const sshUrlMatch = remote.match(/^ssh:\/\/git@github\.com\/([^/]+)\/(.+?)(?:\.git)?$/i);
|
|
543
|
+
if (sshUrlMatch) {
|
|
544
|
+
return `${sshUrlMatch[1]}/${sshUrlMatch[2]}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function getGhCommand() {
|
|
551
|
+
return String(process.env.SENTINELAYER_GH_BIN || "").trim() || "gh";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function getGitCommand() {
|
|
555
|
+
return String(process.env.SENTINELAYER_GIT_BIN || "").trim() || "git";
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function isGitRepo(cwd) {
|
|
559
|
+
const gitCommand = getGitCommand();
|
|
560
|
+
const probe = spawnSync(gitCommand, ["rev-parse", "--is-inside-work-tree"], {
|
|
561
|
+
cwd,
|
|
562
|
+
encoding: "utf-8",
|
|
563
|
+
});
|
|
564
|
+
return probe.status === 0;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function buildGithubCloneUrl(repoSlug) {
|
|
568
|
+
const base = String(DEFAULT_GITHUB_CLONE_BASE_URL || "https://github.com").trim().replace(/\/+$/g, "");
|
|
569
|
+
return `${base}/${normalizeRepoSlug(repoSlug)}.git`;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function ensureGhCliAvailable(ghCommand) {
|
|
573
|
+
const ghVersion = spawnSync(ghCommand, ["--version"], { encoding: "utf-8" });
|
|
574
|
+
if (ghVersion.status !== 0) {
|
|
575
|
+
throw new Error("GitHub CLI (gh) is not installed or not in PATH.");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function ensureGhAuthSession(ghCommand) {
|
|
580
|
+
ensureGhCliAvailable(ghCommand);
|
|
581
|
+
const status = spawnSync(ghCommand, ["auth", "status", "-h", "github.com"], {
|
|
582
|
+
encoding: "utf-8",
|
|
583
|
+
});
|
|
584
|
+
if (status.status === 0) {
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
console.log("GitHub authorization required. Opening browser for gh auth login...");
|
|
589
|
+
const login = spawnSync(ghCommand, ["auth", "login", "-h", "github.com", "-s", "repo", "-w"], {
|
|
590
|
+
encoding: "utf-8",
|
|
591
|
+
stdio: "inherit",
|
|
592
|
+
});
|
|
593
|
+
if (login.status !== 0) {
|
|
594
|
+
throw new Error("GitHub authorization failed. Complete gh auth login and retry.");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function listReposViaGh(ghCommand) {
|
|
599
|
+
const endpoint = "/user/repos?per_page=100&sort=updated&affiliation=owner,collaborator,organization_member";
|
|
600
|
+
const apiResult = spawnSync(
|
|
601
|
+
ghCommand,
|
|
602
|
+
["api", "--paginate", "--slurp", endpoint],
|
|
603
|
+
{ encoding: "utf-8" }
|
|
604
|
+
);
|
|
605
|
+
if (apiResult.status !== 0) {
|
|
606
|
+
const fallback = spawnSync(ghCommand, ["api", endpoint], { encoding: "utf-8" });
|
|
607
|
+
if (fallback.status !== 0) {
|
|
608
|
+
throw new Error(
|
|
609
|
+
String(
|
|
610
|
+
fallback.stderr ||
|
|
611
|
+
fallback.stdout ||
|
|
612
|
+
apiResult.stderr ||
|
|
613
|
+
apiResult.stdout ||
|
|
614
|
+
"Unable to fetch repositories with gh api."
|
|
615
|
+
).trim()
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
return parseGhRepoListPayload(String(fallback.stdout || "[]"));
|
|
619
|
+
}
|
|
620
|
+
return parseGhRepoListPayload(String(apiResult.stdout || "[]"));
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function parseGhRepoListPayload(rawJson) {
|
|
624
|
+
let payload = [];
|
|
625
|
+
try {
|
|
626
|
+
payload = JSON.parse(rawJson);
|
|
627
|
+
} catch {
|
|
628
|
+
throw new Error("GitHub repo list response was not valid JSON.");
|
|
629
|
+
}
|
|
630
|
+
if (!Array.isArray(payload)) {
|
|
631
|
+
throw new Error("GitHub repo list response was not an array.");
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const flattened = [];
|
|
635
|
+
for (const entry of payload) {
|
|
636
|
+
if (Array.isArray(entry)) {
|
|
637
|
+
flattened.push(...entry);
|
|
638
|
+
} else {
|
|
639
|
+
flattened.push(entry);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const seen = new Set();
|
|
644
|
+
const repos = [];
|
|
645
|
+
for (const item of flattened) {
|
|
646
|
+
const slug = normalizeRepoSlug(item && typeof item.full_name === "string" ? item.full_name : "");
|
|
647
|
+
if (!isValidRepoSlug(slug)) continue;
|
|
648
|
+
const key = slug.toLowerCase();
|
|
649
|
+
if (seen.has(key)) continue;
|
|
650
|
+
seen.add(key);
|
|
651
|
+
repos.push({
|
|
652
|
+
slug,
|
|
653
|
+
privateRepo: Boolean(item.private),
|
|
654
|
+
defaultBranch: String(item.default_branch || "").trim() || "main",
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
return repos;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function selectRepoSlugFromGithub() {
|
|
661
|
+
const ghCommand = getGhCommand();
|
|
662
|
+
ensureGhAuthSession(ghCommand);
|
|
663
|
+
const repos = listReposViaGh(ghCommand);
|
|
664
|
+
if (repos.length === 0) {
|
|
665
|
+
throw new Error("No accessible GitHub repos found for this account.");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const result = await prompts(
|
|
669
|
+
[
|
|
670
|
+
{
|
|
671
|
+
type: "select",
|
|
672
|
+
name: "repoSlug",
|
|
673
|
+
message: "Choose a GitHub repo",
|
|
674
|
+
choices: repos.map((repo) => ({
|
|
675
|
+
title: `${repo.slug}${repo.privateRepo ? " (private)" : ""} [${repo.defaultBranch}]`,
|
|
676
|
+
value: repo.slug,
|
|
677
|
+
})),
|
|
678
|
+
initial: 0,
|
|
679
|
+
},
|
|
680
|
+
],
|
|
681
|
+
{
|
|
682
|
+
onCancel: () => {
|
|
683
|
+
throw new Error("GitHub repo selection cancelled.");
|
|
684
|
+
},
|
|
685
|
+
}
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
const selected = normalizeRepoSlug(result.repoSlug);
|
|
689
|
+
if (!isValidRepoSlug(selected)) {
|
|
690
|
+
throw new Error("GitHub repo selection returned an invalid repository slug.");
|
|
691
|
+
}
|
|
692
|
+
return selected;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function cloneGithubRepo({ repoSlug, cwd }) {
|
|
696
|
+
const normalizedRepo = normalizeRepoSlug(repoSlug);
|
|
697
|
+
const repoName = getRepoNameFromSlug(normalizedRepo) || "repo";
|
|
698
|
+
const targetDir = path.resolve(cwd, repoName);
|
|
699
|
+
const gitCommand = getGitCommand();
|
|
700
|
+
const cloneUrl = buildGithubCloneUrl(normalizedRepo);
|
|
701
|
+
|
|
702
|
+
if (path.resolve(cwd) === path.resolve(targetDir)) {
|
|
703
|
+
throw new Error("Target clone directory cannot match the current working directory.");
|
|
704
|
+
}
|
|
705
|
+
if (fs.existsSync(targetDir) && !isGitRepo(targetDir)) {
|
|
706
|
+
const entries = await fsp.readdir(targetDir);
|
|
707
|
+
if (entries.length > 0) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
`Cannot clone ${normalizedRepo}: target directory '${repoName}' already exists and is not an empty git repo.`
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (isGitRepo(targetDir)) {
|
|
714
|
+
const localSlug = normalizeRepoSlug(detectRepoSlug(targetDir) || "");
|
|
715
|
+
if (!localSlug) {
|
|
716
|
+
throw new Error(
|
|
717
|
+
`Directory '${repoName}' already contains a git repo without a detectable GitHub origin. Refusing to overwrite it.`
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
if (localSlug && localSlug.toLowerCase() !== normalizedRepo.toLowerCase()) {
|
|
721
|
+
throw new Error(
|
|
722
|
+
`Directory '${repoName}' already contains a different repo (${localSlug}). Choose another project name or folder.`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
return {
|
|
726
|
+
projectDir: targetDir,
|
|
727
|
+
cloneUrl,
|
|
728
|
+
cloned: false,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const cloneResult = spawnSync(gitCommand, ["clone", "--depth", "1", cloneUrl, targetDir], {
|
|
733
|
+
cwd,
|
|
734
|
+
encoding: "utf-8",
|
|
735
|
+
});
|
|
736
|
+
if (cloneResult.status !== 0) {
|
|
737
|
+
throw new Error(String(cloneResult.stderr || cloneResult.stdout || "git clone failed").trim());
|
|
738
|
+
}
|
|
739
|
+
return {
|
|
740
|
+
projectDir: targetDir,
|
|
741
|
+
cloneUrl,
|
|
742
|
+
cloned: true,
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
async function ensureGitRepositorySetup({ projectDir, repoSlug }) {
|
|
747
|
+
const gitCommand = getGitCommand();
|
|
748
|
+
if (!isGitRepo(projectDir)) {
|
|
749
|
+
const initResult = spawnSync(gitCommand, ["init"], {
|
|
750
|
+
cwd: projectDir,
|
|
751
|
+
encoding: "utf-8",
|
|
752
|
+
});
|
|
753
|
+
if (initResult.status !== 0) {
|
|
754
|
+
throw new Error(String(initResult.stderr || initResult.stdout || "git init failed").trim());
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const normalizedRepo = normalizeRepoSlug(repoSlug);
|
|
759
|
+
if (!isValidRepoSlug(normalizedRepo)) return;
|
|
760
|
+
|
|
761
|
+
const remoteGet = spawnSync(gitCommand, ["config", "--get", "remote.origin.url"], {
|
|
762
|
+
cwd: projectDir,
|
|
763
|
+
encoding: "utf-8",
|
|
764
|
+
});
|
|
765
|
+
const remote = String(remoteGet.stdout || "").trim();
|
|
766
|
+
if (remote) return;
|
|
767
|
+
|
|
768
|
+
const remoteUrl = buildGithubCloneUrl(normalizedRepo);
|
|
769
|
+
const remoteAdd = spawnSync(gitCommand, ["remote", "add", "origin", remoteUrl], {
|
|
770
|
+
cwd: projectDir,
|
|
771
|
+
encoding: "utf-8",
|
|
772
|
+
});
|
|
773
|
+
if (remoteAdd.status !== 0) {
|
|
774
|
+
throw new Error(String(remoteAdd.stderr || remoteAdd.stdout || "git remote add failed").trim());
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
async function buildRepoIngestSummary(projectDir) {
|
|
779
|
+
try {
|
|
780
|
+
const ingest = await collectCodebaseIngest({ rootPath: projectDir });
|
|
781
|
+
return formatIngestSummary(ingest);
|
|
782
|
+
} catch {
|
|
783
|
+
return "";
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function formatTimestampForFile() {
|
|
788
|
+
const now = new Date();
|
|
789
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
790
|
+
return `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}-${pad(
|
|
791
|
+
now.getUTCHours()
|
|
792
|
+
)}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function getCommandOptionValue(args, optionName) {
|
|
796
|
+
const index = args.findIndex((arg) => String(arg || "").trim() === optionName);
|
|
797
|
+
if (index < 0) return "";
|
|
798
|
+
const next = String(args[index + 1] || "").trim();
|
|
799
|
+
if (!next || next.startsWith("-")) {
|
|
800
|
+
throw new Error(`Missing value for ${optionName}`);
|
|
801
|
+
}
|
|
802
|
+
return next;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function hasCommandOption(args, optionName) {
|
|
806
|
+
return args.some((arg) => String(arg || "").trim() === optionName);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
async function collectScanFiles(rootPath) {
|
|
810
|
+
const files = [];
|
|
811
|
+
const stack = [rootPath];
|
|
812
|
+
const ignoredDirs = new Set([".git", "node_modules", ".venv", ".next", "dist", "build", ".sentinelayer"]);
|
|
813
|
+
const maxFileSizeBytes = 512 * 1024;
|
|
814
|
+
|
|
815
|
+
while (stack.length > 0) {
|
|
816
|
+
const current = stack.pop();
|
|
817
|
+
if (!current) continue;
|
|
818
|
+
let entries = [];
|
|
819
|
+
try {
|
|
820
|
+
entries = await fsp.readdir(current, { withFileTypes: true });
|
|
821
|
+
} catch {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
for (const entry of entries) {
|
|
825
|
+
const fullPath = path.join(current, entry.name);
|
|
826
|
+
if (entry.isDirectory()) {
|
|
827
|
+
if (ignoredDirs.has(entry.name)) continue;
|
|
828
|
+
stack.push(fullPath);
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
if (!entry.isFile()) continue;
|
|
832
|
+
try {
|
|
833
|
+
const stat = await fsp.stat(fullPath);
|
|
834
|
+
if (stat.size > maxFileSizeBytes) continue;
|
|
835
|
+
} catch {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
files.push(fullPath);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return files;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async function runCredentialScan(targetPath) {
|
|
845
|
+
const testOrFixturePathPattern = /(?:^|[\\/])(?:test|tests|__tests__|fixtures?)(?:[\\/]|$)/i;
|
|
846
|
+
const localReviewSourcePathPattern = /(?:^|[\\/])src[\\/]review[\\/]local-review\.js$/i;
|
|
847
|
+
const workItemExcludePathPattern = new RegExp(
|
|
848
|
+
`${testOrFixturePathPattern.source}|${localReviewSourcePathPattern.source}`,
|
|
849
|
+
"i"
|
|
850
|
+
);
|
|
851
|
+
const rules = [
|
|
852
|
+
{
|
|
853
|
+
severity: "P1",
|
|
854
|
+
message: "Possible AWS access key detected.",
|
|
855
|
+
regex: /AKIA[0-9A-Z]{16}/,
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
severity: "P1",
|
|
859
|
+
message: "Possible private key material detected.",
|
|
860
|
+
regex: /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
severity: "P1",
|
|
864
|
+
message: "Possible provider API key detected.",
|
|
865
|
+
regex: /\b(sk-[A-Za-z0-9]{20,}|ghp_[A-Za-z0-9]{20,})\b/,
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
severity: "P2",
|
|
869
|
+
message: "Possible hardcoded credential literal.",
|
|
870
|
+
regex: /(api[_-]?key|secret|token)\s*[:=]\s*['"][^'"]{20,}['"]/i,
|
|
871
|
+
excludePathPattern: testOrFixturePathPattern,
|
|
872
|
+
},
|
|
873
|
+
{
|
|
874
|
+
severity: "P2",
|
|
875
|
+
message: "Work-item marker found.",
|
|
876
|
+
regex: /\b(?:\x54\x4f\x44\x4f|\x46\x49\x58\x4d\x45|\x48\x41\x43\x4b)\b/,
|
|
877
|
+
excludePathPattern: workItemExcludePathPattern,
|
|
878
|
+
},
|
|
879
|
+
];
|
|
880
|
+
|
|
881
|
+
const files = await collectScanFiles(targetPath);
|
|
882
|
+
const findings = [];
|
|
883
|
+
const maxFindings = 200;
|
|
884
|
+
|
|
885
|
+
for (const filePath of files) {
|
|
886
|
+
const relativePath = path.relative(targetPath, filePath).replace(/\\/g, "/");
|
|
887
|
+
let text = "";
|
|
888
|
+
try {
|
|
889
|
+
text = await fsp.readFile(filePath, "utf-8");
|
|
890
|
+
} catch {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const lines = text.split(/\r?\n/);
|
|
894
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
895
|
+
const line = lines[lineIndex];
|
|
896
|
+
if (!line) continue;
|
|
897
|
+
if (line.includes("<your-token>") || line.includes("example")) continue;
|
|
898
|
+
for (const rule of rules) {
|
|
899
|
+
if (rule.excludePathPattern && rule.excludePathPattern.test(relativePath)) continue;
|
|
900
|
+
if (!rule.regex.test(line)) continue;
|
|
901
|
+
findings.push({
|
|
902
|
+
severity: rule.severity,
|
|
903
|
+
file: relativePath,
|
|
904
|
+
line: lineIndex + 1,
|
|
905
|
+
message: rule.message,
|
|
906
|
+
excerpt: line.trim().slice(0, 180),
|
|
907
|
+
});
|
|
908
|
+
if (findings.length >= maxFindings) break;
|
|
909
|
+
}
|
|
910
|
+
if (findings.length >= maxFindings) break;
|
|
911
|
+
}
|
|
912
|
+
if (findings.length >= maxFindings) break;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const p1 = findings.filter((item) => item.severity === "P1").length;
|
|
916
|
+
const p2 = findings.filter((item) => item.severity === "P2").length;
|
|
917
|
+
|
|
918
|
+
return {
|
|
919
|
+
scannedFiles: files.length,
|
|
920
|
+
findings,
|
|
921
|
+
p1,
|
|
922
|
+
p2,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
async function writeLocalCommandReport(targetPath, prefix, body, { outputDir = "" } = {}) {
|
|
927
|
+
const outputRoot = await resolveOutputRoot({
|
|
928
|
+
cwd: targetPath,
|
|
929
|
+
outputDirOverride: outputDir,
|
|
930
|
+
});
|
|
931
|
+
const reportDir = path.join(outputRoot, "reports");
|
|
932
|
+
await ensureDirectory(reportDir);
|
|
933
|
+
const reportPath = path.join(reportDir, `${prefix}-${formatTimestampForFile()}.md`);
|
|
934
|
+
await writeTextFile(reportPath, `${body}\n`);
|
|
935
|
+
return reportPath;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function formatFindingsMarkdown(findings) {
|
|
939
|
+
if (!findings.length) return "- none";
|
|
940
|
+
return findings
|
|
941
|
+
.map((item, index) => `${index + 1}. [${item.severity}] ${item.file}:${item.line} - ${item.message}`)
|
|
942
|
+
.join("\n");
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
async function runLocalOmarGateCommand(args) {
|
|
946
|
+
const mode = String(args[0] || "").trim().toLowerCase();
|
|
947
|
+
if (mode && mode !== "deep") {
|
|
948
|
+
throw new Error(`Unsupported /omargate mode '${mode}'. Use: /omargate deep`);
|
|
949
|
+
}
|
|
950
|
+
const asJson = hasCommandOption(args, "--json");
|
|
951
|
+
const pathArg = getCommandOptionValue(args, "--path") || ".";
|
|
952
|
+
const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
|
|
953
|
+
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
954
|
+
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
955
|
+
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (!asJson) {
|
|
959
|
+
printSection("Local Omar Gate Deep");
|
|
960
|
+
printInfo(`Target: ${targetPath}`);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const scan = await runCredentialScan(targetPath);
|
|
964
|
+
const report = `# Local Omar Gate Deep Scan
|
|
965
|
+
|
|
966
|
+
Generated: ${nowIso()}
|
|
967
|
+
Target: ${targetPath}
|
|
968
|
+
|
|
969
|
+
Summary:
|
|
970
|
+
- Files scanned: ${scan.scannedFiles}
|
|
971
|
+
- P1 findings: ${scan.p1}
|
|
972
|
+
- P2 findings: ${scan.p2}
|
|
973
|
+
|
|
974
|
+
Findings:
|
|
975
|
+
${formatFindingsMarkdown(scan.findings)}
|
|
976
|
+
`;
|
|
977
|
+
|
|
978
|
+
const reportPath = await writeLocalCommandReport(targetPath, "omargate-deep", report, {
|
|
979
|
+
outputDir: outputDirArg,
|
|
980
|
+
});
|
|
981
|
+
if (asJson) {
|
|
982
|
+
console.log(
|
|
983
|
+
JSON.stringify(
|
|
984
|
+
{
|
|
985
|
+
command: "/omargate deep",
|
|
986
|
+
targetPath,
|
|
987
|
+
reportPath,
|
|
988
|
+
scannedFiles: scan.scannedFiles,
|
|
989
|
+
p1: scan.p1,
|
|
990
|
+
p2: scan.p2,
|
|
991
|
+
blocking: scan.p1 > 0,
|
|
992
|
+
},
|
|
993
|
+
null,
|
|
994
|
+
2
|
|
995
|
+
)
|
|
996
|
+
);
|
|
997
|
+
} else {
|
|
998
|
+
console.log(pc.cyan(`Report: ${reportPath}`));
|
|
999
|
+
console.log(`P1 findings: ${scan.p1}`);
|
|
1000
|
+
console.log(`P2 findings: ${scan.p2}`);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
if (scan.p1 > 0) {
|
|
1004
|
+
if (!asJson) {
|
|
1005
|
+
console.log(pc.red("Blocking findings detected (P1 > 0)."));
|
|
1006
|
+
}
|
|
1007
|
+
return 2;
|
|
1008
|
+
}
|
|
1009
|
+
return 0;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
async function runLocalAuditCommand(args) {
|
|
1013
|
+
const asJson = hasCommandOption(args, "--json");
|
|
1014
|
+
const pathArg = getCommandOptionValue(args, "--path") || ".";
|
|
1015
|
+
const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
|
|
1016
|
+
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
1017
|
+
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1018
|
+
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (!asJson) {
|
|
1022
|
+
printSection("Local Audit");
|
|
1023
|
+
printInfo(`Target: ${targetPath}`);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const requiredChecks = [
|
|
1027
|
+
{
|
|
1028
|
+
key: ".github/workflows/omar-gate.yml",
|
|
1029
|
+
severity: "P1",
|
|
1030
|
+
ok: fs.existsSync(path.join(targetPath, ".github", "workflows", "omar-gate.yml")),
|
|
1031
|
+
message: "Omar workflow is present.",
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
key: "docs/spec.md",
|
|
1035
|
+
severity: "P2",
|
|
1036
|
+
ok: fs.existsSync(path.join(targetPath, "docs", "spec.md")),
|
|
1037
|
+
message: "Spec doc is present.",
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
key: "tasks/todo.md",
|
|
1041
|
+
severity: "P2",
|
|
1042
|
+
ok: fs.existsSync(path.join(targetPath, "tasks", "todo.md")),
|
|
1043
|
+
message: "Todo plan is present.",
|
|
1044
|
+
},
|
|
1045
|
+
];
|
|
1046
|
+
|
|
1047
|
+
const scan = await runCredentialScan(targetPath);
|
|
1048
|
+
const failedP1Checks = requiredChecks.filter((item) => !item.ok && item.severity === "P1").length;
|
|
1049
|
+
const failedP2Checks = requiredChecks.filter((item) => !item.ok && item.severity === "P2").length;
|
|
1050
|
+
const totalP1 = scan.p1 + failedP1Checks;
|
|
1051
|
+
const totalP2 = scan.p2 + failedP2Checks;
|
|
1052
|
+
const overallStatus = totalP1 > 0 ? "FAIL" : "PASS";
|
|
1053
|
+
|
|
1054
|
+
const checkText = requiredChecks
|
|
1055
|
+
.map(
|
|
1056
|
+
(item) =>
|
|
1057
|
+
`- [${item.ok ? "x" : " "}] (${item.severity}) ${item.key} :: ${item.message}${item.ok ? "" : " [missing]"}`
|
|
1058
|
+
)
|
|
1059
|
+
.join("\n");
|
|
1060
|
+
const report = `# Local Sentinelayer Audit
|
|
1061
|
+
|
|
1062
|
+
Generated: ${nowIso()}
|
|
1063
|
+
Target: ${targetPath}
|
|
1064
|
+
Overall status: ${overallStatus}
|
|
1065
|
+
|
|
1066
|
+
Readiness checks:
|
|
1067
|
+
${checkText}
|
|
1068
|
+
|
|
1069
|
+
Scan summary:
|
|
1070
|
+
- Files scanned: ${scan.scannedFiles}
|
|
1071
|
+
- P1 findings: ${scan.p1}
|
|
1072
|
+
- P2 findings: ${scan.p2}
|
|
1073
|
+
|
|
1074
|
+
Findings:
|
|
1075
|
+
${formatFindingsMarkdown(scan.findings)}
|
|
1076
|
+
`;
|
|
1077
|
+
|
|
1078
|
+
const reportPath = await writeLocalCommandReport(targetPath, "audit", report, {
|
|
1079
|
+
outputDir: outputDirArg,
|
|
1080
|
+
});
|
|
1081
|
+
if (asJson) {
|
|
1082
|
+
console.log(
|
|
1083
|
+
JSON.stringify(
|
|
1084
|
+
{
|
|
1085
|
+
command: "/audit",
|
|
1086
|
+
targetPath,
|
|
1087
|
+
reportPath,
|
|
1088
|
+
overallStatus,
|
|
1089
|
+
scannedFiles: scan.scannedFiles,
|
|
1090
|
+
p1: scan.p1,
|
|
1091
|
+
p2: scan.p2,
|
|
1092
|
+
p1Total: totalP1,
|
|
1093
|
+
p2Total: totalP2,
|
|
1094
|
+
blocking: totalP1 > 0,
|
|
1095
|
+
},
|
|
1096
|
+
null,
|
|
1097
|
+
2
|
|
1098
|
+
)
|
|
1099
|
+
);
|
|
1100
|
+
} else {
|
|
1101
|
+
console.log(pc.cyan(`Report: ${reportPath}`));
|
|
1102
|
+
console.log(`Overall status: ${overallStatus}`);
|
|
1103
|
+
console.log(`P1 total: ${totalP1}`);
|
|
1104
|
+
console.log(`P2 total: ${totalP2}`);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (totalP1 > 0) {
|
|
1108
|
+
if (!asJson) {
|
|
1109
|
+
console.log(pc.red("Audit failed due to blocking findings (P1 > 0)."));
|
|
1110
|
+
}
|
|
1111
|
+
return 2;
|
|
1112
|
+
}
|
|
1113
|
+
return 0;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
async function runLocalPersonaCommand(args) {
|
|
1117
|
+
const subcommand = String(args[0] || "").trim().toLowerCase();
|
|
1118
|
+
const optionArgs = subcommand === "orchestrator" ? args.slice(1) : args;
|
|
1119
|
+
if (subcommand && subcommand !== "orchestrator") {
|
|
1120
|
+
throw new Error(`Unsupported /persona subcommand '${subcommand}'. Use: /persona orchestrator --mode <mode>`);
|
|
1121
|
+
}
|
|
1122
|
+
const asJson = hasCommandOption(optionArgs, "--json");
|
|
1123
|
+
|
|
1124
|
+
const mode = String(getCommandOptionValue(optionArgs, "--mode") || "builder").trim().toLowerCase();
|
|
1125
|
+
const validModes = new Set(["builder", "reviewer", "hardener"]);
|
|
1126
|
+
if (!validModes.has(mode)) {
|
|
1127
|
+
throw new Error("Invalid --mode for /persona. Use builder, reviewer, or hardener.");
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const pathArg = getCommandOptionValue(optionArgs, "--path") || ".";
|
|
1131
|
+
const outputDirArg = getCommandOptionValue(optionArgs, "--output-dir") || "";
|
|
1132
|
+
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
1133
|
+
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1134
|
+
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (!asJson) {
|
|
1138
|
+
printSection("Persona Orchestrator");
|
|
1139
|
+
printInfo(`Mode: ${mode}`);
|
|
1140
|
+
printInfo(`Target: ${targetPath}`);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const modeInstructions = {
|
|
1144
|
+
builder: [
|
|
1145
|
+
"Prioritize implementation throughput and deterministic delivery.",
|
|
1146
|
+
"Keep PR scope tight and finish one batch before opening the next.",
|
|
1147
|
+
"Use Omar loop after each PR and fix all P0/P1 before merge.",
|
|
1148
|
+
],
|
|
1149
|
+
reviewer: [
|
|
1150
|
+
"Prioritize risk discovery, regressions, and missing tests.",
|
|
1151
|
+
"Focus findings-first output ordered by severity.",
|
|
1152
|
+
"Escalate architecture/security concerns before code changes.",
|
|
1153
|
+
],
|
|
1154
|
+
hardener: [
|
|
1155
|
+
"Prioritize security posture, policy controls, and failure modes.",
|
|
1156
|
+
"Add guardrails for auth, secrets handling, and CI enforceability.",
|
|
1157
|
+
"Treat P2 debt as merge-blocking unless explicitly waived.",
|
|
1158
|
+
],
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
const ingest = await buildRepoIngestSummary(targetPath);
|
|
1162
|
+
const report = `# Persona Orchestrator Plan
|
|
1163
|
+
|
|
1164
|
+
Generated: ${nowIso()}
|
|
1165
|
+
Target: ${targetPath}
|
|
1166
|
+
Mode: ${mode}
|
|
1167
|
+
|
|
1168
|
+
Instructions:
|
|
1169
|
+
${modeInstructions[mode].map((line, index) => `${index + 1}. ${line}`).join("\n")}
|
|
1170
|
+
|
|
1171
|
+
Repo summary:
|
|
1172
|
+
${ingest || "No repository summary available."}
|
|
1173
|
+
`;
|
|
1174
|
+
|
|
1175
|
+
const reportPath = await writeLocalCommandReport(targetPath, `persona-orchestrator-${mode}`, report, {
|
|
1176
|
+
outputDir: outputDirArg,
|
|
1177
|
+
});
|
|
1178
|
+
if (asJson) {
|
|
1179
|
+
console.log(
|
|
1180
|
+
JSON.stringify(
|
|
1181
|
+
{
|
|
1182
|
+
command: "/persona orchestrator",
|
|
1183
|
+
mode,
|
|
1184
|
+
targetPath,
|
|
1185
|
+
reportPath,
|
|
1186
|
+
},
|
|
1187
|
+
null,
|
|
1188
|
+
2
|
|
1189
|
+
)
|
|
1190
|
+
);
|
|
1191
|
+
} else {
|
|
1192
|
+
console.log(pc.cyan(`Report: ${reportPath}`));
|
|
1193
|
+
}
|
|
1194
|
+
return 0;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function parseTodoPlanTasks(content) {
|
|
1198
|
+
const tasks = [];
|
|
1199
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
1200
|
+
for (const line of lines) {
|
|
1201
|
+
const unchecked = line.match(/^\s*-\s*\[\s\]\s+(.+)\s*$/);
|
|
1202
|
+
if (unchecked) {
|
|
1203
|
+
tasks.push(unchecked[1].trim());
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const ordered = line.match(/^\s*\d+\.\s+(.+)\s*$/);
|
|
1207
|
+
if (ordered) {
|
|
1208
|
+
tasks.push(ordered[1].trim());
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return tasks.filter(Boolean);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function runLocalApplyCommand(args) {
|
|
1215
|
+
const asJson = hasCommandOption(args, "--json");
|
|
1216
|
+
const pathArg = getCommandOptionValue(args, "--path") || ".";
|
|
1217
|
+
const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
|
|
1218
|
+
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
1219
|
+
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1220
|
+
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const planArg = getCommandOptionValue(args, "--plan") || "tasks/todo.md";
|
|
1224
|
+
const planPath = path.resolve(targetPath, planArg);
|
|
1225
|
+
if (!fs.existsSync(planPath)) {
|
|
1226
|
+
throw new Error(`Plan file not found: ${planPath}`);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (!asJson) {
|
|
1230
|
+
printSection("Apply Plan");
|
|
1231
|
+
printInfo(`Target: ${targetPath}`);
|
|
1232
|
+
printInfo(`Plan: ${planPath}`);
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
const planText = await fsp.readFile(planPath, "utf-8");
|
|
1236
|
+
const tasks = parseTodoPlanTasks(planText);
|
|
1237
|
+
if (!tasks.length) {
|
|
1238
|
+
throw new Error("No executable checklist items were found in the plan file.");
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const report = `# Apply Plan Preview
|
|
1242
|
+
|
|
1243
|
+
Generated: ${nowIso()}
|
|
1244
|
+
Target: ${targetPath}
|
|
1245
|
+
Plan: ${planPath}
|
|
1246
|
+
|
|
1247
|
+
Execution order:
|
|
1248
|
+
${tasks.map((task, index) => `${index + 1}. ${task}`).join("\n")}
|
|
1249
|
+
|
|
1250
|
+
Next action:
|
|
1251
|
+
- Execute each item PR-by-PR and run Omar loop before every merge.
|
|
1252
|
+
`;
|
|
1253
|
+
|
|
1254
|
+
const reportPath = await writeLocalCommandReport(targetPath, "apply-plan", report, {
|
|
1255
|
+
outputDir: outputDirArg,
|
|
1256
|
+
});
|
|
1257
|
+
if (asJson) {
|
|
1258
|
+
console.log(
|
|
1259
|
+
JSON.stringify(
|
|
1260
|
+
{
|
|
1261
|
+
command: "/apply",
|
|
1262
|
+
targetPath,
|
|
1263
|
+
planPath,
|
|
1264
|
+
reportPath,
|
|
1265
|
+
taskCount: tasks.length,
|
|
1266
|
+
},
|
|
1267
|
+
null,
|
|
1268
|
+
2
|
|
1269
|
+
)
|
|
1270
|
+
);
|
|
1271
|
+
} else {
|
|
1272
|
+
console.log(pc.cyan(`Report: ${reportPath}`));
|
|
1273
|
+
console.log(`Parsed tasks: ${tasks.length}`);
|
|
1274
|
+
}
|
|
1275
|
+
return 0;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async function tryRunLocalCommandMode(argv) {
|
|
1279
|
+
const command = String(argv[0] || "").trim().toLowerCase();
|
|
1280
|
+
if (command !== "/omargate" && command !== "/audit" && command !== "/persona" && command !== "/apply") {
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
const args = argv.slice(1);
|
|
1284
|
+
if (command === "/omargate") {
|
|
1285
|
+
return runLocalOmarGateCommand(args);
|
|
1286
|
+
}
|
|
1287
|
+
if (command === "/audit") {
|
|
1288
|
+
return runLocalAuditCommand(args);
|
|
1289
|
+
}
|
|
1290
|
+
if (command === "/persona") {
|
|
1291
|
+
return runLocalPersonaCommand(args);
|
|
1292
|
+
}
|
|
1293
|
+
return runLocalApplyCommand(args);
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
async function resolveProjectDirectory({ cwd, interview, detectedRepo }) {
|
|
1297
|
+
const normalizedTargetRepo = normalizeRepoSlug(interview.repoSlug);
|
|
1298
|
+
const normalizedDetected = normalizeRepoSlug(detectedRepo || "");
|
|
1299
|
+
|
|
1300
|
+
if (interview.connectRepo && interview.buildFromExistingRepo && isValidRepoSlug(normalizedTargetRepo)) {
|
|
1301
|
+
if (normalizedDetected && normalizedDetected.toLowerCase() === normalizedTargetRepo.toLowerCase()) {
|
|
1302
|
+
return {
|
|
1303
|
+
projectDir: cwd,
|
|
1304
|
+
clonedRepo: false,
|
|
1305
|
+
reusedCurrentRepo: true,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
const cloned = await cloneGithubRepo({
|
|
1309
|
+
repoSlug: normalizedTargetRepo,
|
|
1310
|
+
cwd,
|
|
1311
|
+
});
|
|
1312
|
+
return {
|
|
1313
|
+
projectDir: cloned.projectDir,
|
|
1314
|
+
clonedRepo: cloned.cloned,
|
|
1315
|
+
reusedCurrentRepo: false,
|
|
1316
|
+
cloneUrl: cloned.cloneUrl,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
return {
|
|
1321
|
+
projectDir: path.resolve(cwd, interview.projectName),
|
|
1322
|
+
clonedRepo: false,
|
|
1323
|
+
reusedCurrentRepo: false,
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
async function ensureDirectory(targetPath) {
|
|
1328
|
+
await fsp.mkdir(targetPath, { recursive: true });
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function writeTextFile(filePath, content) {
|
|
1332
|
+
await ensureDirectory(path.dirname(filePath));
|
|
1333
|
+
await fsp.writeFile(filePath, content, "utf-8");
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
async function upsertEnvVariable(filePath, key, value) {
|
|
1337
|
+
let existing = "";
|
|
1338
|
+
if (fs.existsSync(filePath)) {
|
|
1339
|
+
existing = await fsp.readFile(filePath, "utf-8");
|
|
1340
|
+
}
|
|
1341
|
+
const line = `${key}=${value}`;
|
|
1342
|
+
const regex = new RegExp(`^${key}=.*$`, "m");
|
|
1343
|
+
let next;
|
|
1344
|
+
if (regex.test(existing)) {
|
|
1345
|
+
next = existing.replace(regex, line);
|
|
1346
|
+
} else if (existing.trim().length === 0) {
|
|
1347
|
+
next = `${line}\n`;
|
|
1348
|
+
} else if (existing.endsWith("\n")) {
|
|
1349
|
+
next = `${existing}${line}\n`;
|
|
1350
|
+
} else {
|
|
1351
|
+
next = `${existing}\n${line}\n`;
|
|
1352
|
+
}
|
|
1353
|
+
await writeTextFile(filePath, next);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
async function ensureEnvFileIgnored(projectDir) {
|
|
1357
|
+
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
1358
|
+
let existing = "";
|
|
1359
|
+
if (fs.existsSync(gitignorePath)) {
|
|
1360
|
+
existing = await fsp.readFile(gitignorePath, "utf-8");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const lines = existing.split(/\r?\n/);
|
|
1364
|
+
const hasEntry = lines.some((line) => {
|
|
1365
|
+
const normalized = String(line || "").trim();
|
|
1366
|
+
return normalized === ".env" || normalized === "/.env";
|
|
1367
|
+
});
|
|
1368
|
+
if (hasEntry) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
const envEntry = ".env";
|
|
1373
|
+
let next = "";
|
|
1374
|
+
if (existing.trim().length === 0) {
|
|
1375
|
+
next = `${envEntry}\n`;
|
|
1376
|
+
} else if (existing.endsWith("\n")) {
|
|
1377
|
+
next = `${existing}${envEntry}\n`;
|
|
1378
|
+
} else {
|
|
1379
|
+
next = `${existing}\n${envEntry}\n`;
|
|
1380
|
+
}
|
|
1381
|
+
await writeTextFile(gitignorePath, next);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
async function ensureSentinelStartScript(projectDir, projectName) {
|
|
1385
|
+
const packagePath = path.join(projectDir, "package.json");
|
|
1386
|
+
const fallback = {
|
|
1387
|
+
name: sanitizeProjectName(projectName) || "sentinelayer-project",
|
|
1388
|
+
version: "0.1.0",
|
|
1389
|
+
private: true,
|
|
1390
|
+
scripts: {},
|
|
1391
|
+
};
|
|
1392
|
+
let payload = fallback;
|
|
1393
|
+
if (fs.existsSync(packagePath)) {
|
|
1394
|
+
try {
|
|
1395
|
+
const parsed = JSON.parse(await fsp.readFile(packagePath, "utf-8"));
|
|
1396
|
+
if (parsed && typeof parsed === "object") {
|
|
1397
|
+
payload = parsed;
|
|
1398
|
+
}
|
|
1399
|
+
} catch {
|
|
1400
|
+
payload = fallback;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
payload.scripts = payload.scripts && typeof payload.scripts === "object" ? payload.scripts : {};
|
|
1404
|
+
payload.scripts["sentinel:start"] =
|
|
1405
|
+
payload.scripts["sentinel:start"] ||
|
|
1406
|
+
"echo \"Sentinelayer artifacts are ready. Open AGENT_HANDOFF_PROMPT.md and start your coding agent.\"";
|
|
1407
|
+
const scriptDefaults = {
|
|
1408
|
+
"sentinel:omargate": "npx sentinelayer-cli@latest /omargate deep --path .",
|
|
1409
|
+
"sentinel:omargate:json": "npx sentinelayer-cli@latest /omargate deep --path . --json",
|
|
1410
|
+
"sentinel:audit": "npx sentinelayer-cli@latest /audit --path .",
|
|
1411
|
+
"sentinel:audit:json": "npx sentinelayer-cli@latest /audit --path . --json",
|
|
1412
|
+
"sentinel:persona:builder":
|
|
1413
|
+
"npx sentinelayer-cli@latest /persona orchestrator --mode builder --path .",
|
|
1414
|
+
"sentinel:persona:reviewer":
|
|
1415
|
+
"npx sentinelayer-cli@latest /persona orchestrator --mode reviewer --path .",
|
|
1416
|
+
"sentinel:persona:hardener":
|
|
1417
|
+
"npx sentinelayer-cli@latest /persona orchestrator --mode hardener --path .",
|
|
1418
|
+
"sentinel:apply": "npx sentinelayer-cli@latest /apply --plan tasks/todo.md --path .",
|
|
1419
|
+
};
|
|
1420
|
+
for (const [name, command] of Object.entries(scriptDefaults)) {
|
|
1421
|
+
if (!payload.scripts[name]) {
|
|
1422
|
+
payload.scripts[name] = command;
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
await writeTextFile(packagePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function buildCodingAgentConfigTemplate({ agentProfile, projectName }) {
|
|
1429
|
+
const projectLabel = String(projectName || "sentinelayer-project").trim() || "sentinelayer-project";
|
|
1430
|
+
const commonChecklist = [
|
|
1431
|
+
"Read docs/spec.md, docs/build-guide.md, tasks/todo.md, and AGENT_HANDOFF_PROMPT.md in order.",
|
|
1432
|
+
"Work one PR scope at a time and keep changes deterministic.",
|
|
1433
|
+
"Run local checks before push: /omargate deep and /audit.",
|
|
1434
|
+
];
|
|
1435
|
+
|
|
1436
|
+
if (agentProfile.id === "aider") {
|
|
1437
|
+
return `model: gpt-5.3-codex
|
|
1438
|
+
read:
|
|
1439
|
+
- docs/spec.md
|
|
1440
|
+
- docs/build-guide.md
|
|
1441
|
+
- tasks/todo.md
|
|
1442
|
+
- AGENT_HANDOFF_PROMPT.md
|
|
1443
|
+
notes:
|
|
1444
|
+
- ${commonChecklist.join("\n - ")}
|
|
1445
|
+
`;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (agentProfile.id === "continue" || agentProfile.id === "cody") {
|
|
1449
|
+
return `${JSON.stringify(
|
|
1450
|
+
{
|
|
1451
|
+
profile: "sentinelayer",
|
|
1452
|
+
project: projectLabel,
|
|
1453
|
+
promptTarget: agentProfile.promptTarget,
|
|
1454
|
+
instructions: commonChecklist,
|
|
1455
|
+
},
|
|
1456
|
+
null,
|
|
1457
|
+
2
|
|
1458
|
+
)}\n`;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const markdownBody = [
|
|
1462
|
+
`# Sentinelayer ${agentProfile.name} Profile`,
|
|
1463
|
+
"",
|
|
1464
|
+
`Project: ${projectLabel}`,
|
|
1465
|
+
`Prompt target: ${agentProfile.promptTarget}`,
|
|
1466
|
+
"",
|
|
1467
|
+
"Rules:",
|
|
1468
|
+
...commonChecklist.map((item) => `- ${item}`),
|
|
1469
|
+
"",
|
|
1470
|
+
].join("\n");
|
|
1471
|
+
|
|
1472
|
+
return `${markdownBody}\n`;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
async function ensureCodingAgentConfigFile({ projectDir, projectName, codingAgent }) {
|
|
1476
|
+
const agentProfile = resolveCodingAgent(codingAgent || DEFAULT_CODING_AGENT_ID);
|
|
1477
|
+
if (!agentProfile.configFile) {
|
|
1478
|
+
return {
|
|
1479
|
+
created: false,
|
|
1480
|
+
path: "",
|
|
1481
|
+
agent: agentProfile,
|
|
1482
|
+
};
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const configPath = path.join(projectDir, agentProfile.configFile);
|
|
1486
|
+
if (fs.existsSync(configPath)) {
|
|
1487
|
+
return {
|
|
1488
|
+
created: false,
|
|
1489
|
+
path: configPath,
|
|
1490
|
+
agent: agentProfile,
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const configContent = buildCodingAgentConfigTemplate({
|
|
1495
|
+
agentProfile,
|
|
1496
|
+
projectName,
|
|
1497
|
+
});
|
|
1498
|
+
await writeTextFile(configPath, configContent);
|
|
1499
|
+
return {
|
|
1500
|
+
created: true,
|
|
1501
|
+
path: configPath,
|
|
1502
|
+
agent: agentProfile,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function buildTodoContent({
|
|
1507
|
+
projectName,
|
|
1508
|
+
aiProvider,
|
|
1509
|
+
codingAgent,
|
|
1510
|
+
authMode,
|
|
1511
|
+
repoSlug,
|
|
1512
|
+
buildFromExistingRepo,
|
|
1513
|
+
generationMode,
|
|
1514
|
+
audienceLevel,
|
|
1515
|
+
projectType,
|
|
1516
|
+
}) {
|
|
1517
|
+
const codingAgentProfile = resolveCodingAgent(codingAgent || DEFAULT_CODING_AGENT_ID);
|
|
1518
|
+
return `# Sentinelayer Autonomous Build Plan
|
|
1519
|
+
|
|
1520
|
+
Generated: ${nowIso()}
|
|
1521
|
+
Project: ${projectName}
|
|
1522
|
+
|
|
1523
|
+
## Inputs
|
|
1524
|
+
- AI provider: \`${aiProvider}\`
|
|
1525
|
+
- Coding agent: \`${codingAgentProfile.name} (${codingAgentProfile.id})\`
|
|
1526
|
+
- Auth mode: \`${authMode}\`
|
|
1527
|
+
- Generation mode: \`${generationMode}\`
|
|
1528
|
+
- Audience level: \`${audienceLevel}\`
|
|
1529
|
+
- Project type: \`${projectType}\`
|
|
1530
|
+
- Repo: \`${repoSlug || "not connected"}\`
|
|
1531
|
+
- Workspace mode: \`${buildFromExistingRepo ? "existing repo clone" : "new scaffold"}\`
|
|
1532
|
+
|
|
1533
|
+
## Execution Checklist
|
|
1534
|
+
- [ ] PR 1: repository bootstrap, CI checks, and deterministic scaffolding baseline
|
|
1535
|
+
- [ ] PR 2: domain model + migrations + persistence abstraction
|
|
1536
|
+
- [ ] PR 3: API contracts + auth/session lifecycle hardening
|
|
1537
|
+
- [ ] PR 4: existing-codebase ingest path and repo context extraction
|
|
1538
|
+
- [ ] PR 5: build planner generation quality and prompt artifact validation
|
|
1539
|
+
- [ ] PR 6: workflow orchestration integration with Omar Gate policy defaults
|
|
1540
|
+
- [ ] PR 7: local scan command runner (\`sentinel /omargate deep\`) MVP
|
|
1541
|
+
- [ ] PR 8: local audit command runner (\`sentinel /audit\`) MVP
|
|
1542
|
+
- [ ] PR 9: persona orchestrator command router + policy templates
|
|
1543
|
+
- [ ] PR 10: scale/performance tuning and caching strategy
|
|
1544
|
+
- [ ] PR 11: observability, retries, timeout policies, and structured logs
|
|
1545
|
+
- [ ] PR 12: docs, release, rollout safety checks, and production readiness
|
|
1546
|
+
|
|
1547
|
+
## Omar Loop Contract (Per PR)
|
|
1548
|
+
- [ ] Run Omar Gate for the PR.
|
|
1549
|
+
- [ ] Fix all P0 and P1 findings.
|
|
1550
|
+
- [ ] Fix P2 findings before merge when feasible.
|
|
1551
|
+
- [ ] Re-run gate and confirm clean status.
|
|
1552
|
+
- [ ] Merge only after quality gates are green.
|
|
1553
|
+
|
|
1554
|
+
## Command Roadmap (Local Terminal)
|
|
1555
|
+
- [ ] \`sentinel /omargate deep --path <repo>\`: local deep scan pipeline
|
|
1556
|
+
- [ ] \`sentinel /audit --path <repo>\`: security + quality audit summary
|
|
1557
|
+
- [ ] \`sentinel /persona orchestrator --mode <builder|reviewer|hardener>\`: agent persona routing
|
|
1558
|
+
- [ ] \`sentinel /apply --plan tasks/todo.md\`: execute roadmap batches autonomously
|
|
1559
|
+
|
|
1560
|
+
## Required Read Order
|
|
1561
|
+
1. \`docs/spec.md\`
|
|
1562
|
+
2. \`docs/build-guide.md\`
|
|
1563
|
+
3. \`prompts/execution-prompt.md\`
|
|
1564
|
+
4. \`.github/workflows/omar-gate.yml\`
|
|
1565
|
+
5. \`AGENT_HANDOFF_PROMPT.md\`
|
|
1566
|
+
`;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
function buildAgentPromptGuidance(promptTarget) {
|
|
1570
|
+
const normalized = String(promptTarget || "generic").trim().toLowerCase();
|
|
1571
|
+
if (normalized === "claude") {
|
|
1572
|
+
return `- Use explicit plan -> implement -> verify loops.
|
|
1573
|
+
- Keep deterministic checks first, then optional AI steps.
|
|
1574
|
+
- Capture concrete evidence per PR before handoff.`;
|
|
1575
|
+
}
|
|
1576
|
+
if (normalized === "cursor") {
|
|
1577
|
+
return `- Keep edits small and keep scope to one PR id.
|
|
1578
|
+
- Run local verification before each push.
|
|
1579
|
+
- Keep repository conventions and test style unchanged.`;
|
|
1580
|
+
}
|
|
1581
|
+
if (normalized === "copilot") {
|
|
1582
|
+
return `- Keep error handling explicit on all new paths.
|
|
1583
|
+
- Avoid implicit behavior changes in existing modules.
|
|
1584
|
+
- Add targeted tests for each new branch introduced.`;
|
|
1585
|
+
}
|
|
1586
|
+
if (normalized === "codex") {
|
|
1587
|
+
return `- Execute autonomously, one bounded PR at a time.
|
|
1588
|
+
- Use deterministic ingest/spec context as primary source.
|
|
1589
|
+
- Fail closed when scope or safety requirements are ambiguous.`;
|
|
1590
|
+
}
|
|
1591
|
+
return `- Follow the provided spec and todo list exactly.
|
|
1592
|
+
- Implement incrementally with deterministic checkpoints.
|
|
1593
|
+
- Document assumptions and unresolved risks clearly.`;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function buildHandoffPrompt({
|
|
1597
|
+
projectName,
|
|
1598
|
+
repoSlug,
|
|
1599
|
+
secretName,
|
|
1600
|
+
buildFromExistingRepo,
|
|
1601
|
+
authMode,
|
|
1602
|
+
codingAgent,
|
|
1603
|
+
}) {
|
|
1604
|
+
const codingAgentProfile = resolveCodingAgent(codingAgent || DEFAULT_CODING_AGENT_ID);
|
|
1605
|
+
const codingAgentConfigPath = codingAgentProfile.configFile || "none";
|
|
1606
|
+
const codingAgentGuidance = buildAgentPromptGuidance(codingAgentProfile.promptTarget);
|
|
1607
|
+
const tokenContract =
|
|
1608
|
+
authMode === "sentinelayer"
|
|
1609
|
+
? `- Required secret name: ${secretName}
|
|
1610
|
+
- Workflow input binding: sentinelayer_token: \${{ secrets.${secretName} }}
|
|
1611
|
+
- Optional: OPENAI_API_KEY for runtime policy/BYOK scenarios.`
|
|
1612
|
+
: `- Sentinelayer token: not configured (BYOK mode).
|
|
1613
|
+
- Keep provider credentials in your own environment (OPENAI_API_KEY / ANTHROPIC_API_KEY / GOOGLE_API_KEY).
|
|
1614
|
+
- If you later adopt Omar Gate GitHub Action, set secrets.${secretName} and wire sentinelayer_token accordingly.`;
|
|
1615
|
+
const workflowTuning =
|
|
1616
|
+
authMode === "sentinelayer"
|
|
1617
|
+
? `- scan_mode: deep (default) or quick
|
|
1618
|
+
- severity_gate: P1 (default) or P2`
|
|
1619
|
+
: `- BYOK workflow is guidance-only and does not call the Sentinelayer action.
|
|
1620
|
+
- To enable Omar Gate later, set ${secretName} and configure scan_mode/severity_gate in workflow inputs.`;
|
|
1621
|
+
|
|
1622
|
+
return `# Sentinelayer Agent Handoff Prompt
|
|
1623
|
+
|
|
1624
|
+
You are executing "${projectName}" autonomously.
|
|
1625
|
+
|
|
1626
|
+
Read files in this exact order:
|
|
1627
|
+
1. docs/spec.md
|
|
1628
|
+
2. docs/build-guide.md
|
|
1629
|
+
3. prompts/execution-prompt.md
|
|
1630
|
+
4. tasks/todo.md
|
|
1631
|
+
5. .github/workflows/omar-gate.yml
|
|
1632
|
+
|
|
1633
|
+
Execution mode:
|
|
1634
|
+
- Work PR-by-PR from tasks/todo.md.
|
|
1635
|
+
- For each PR run Omar loop until P0/P1 are zero and quality checks pass.
|
|
1636
|
+
- Keep commits scoped and deterministic.
|
|
1637
|
+
- Stop only for blocking secrets/permission gaps.
|
|
1638
|
+
|
|
1639
|
+
Coding agent profile:
|
|
1640
|
+
- Selected agent: ${codingAgentProfile.name} (${codingAgentProfile.id})
|
|
1641
|
+
- Prompt target: ${codingAgentProfile.promptTarget}
|
|
1642
|
+
- Suggested config path: ${codingAgentConfigPath}
|
|
1643
|
+
|
|
1644
|
+
Agent-specific guidance:
|
|
1645
|
+
${codingAgentGuidance}
|
|
1646
|
+
|
|
1647
|
+
GitHub Action contract:
|
|
1648
|
+
${tokenContract}
|
|
1649
|
+
|
|
1650
|
+
Terminal command options:
|
|
1651
|
+
- sentinel /omargate deep --path .
|
|
1652
|
+
- sentinel /audit --path .
|
|
1653
|
+
- sentinel /persona orchestrator --mode builder --path .
|
|
1654
|
+
- sentinel /persona orchestrator --mode reviewer --path .
|
|
1655
|
+
- sentinel /persona orchestrator --mode hardener --path .
|
|
1656
|
+
- sentinel /apply --plan tasks/todo.md --path .
|
|
1657
|
+
- Add --json to /omargate, /audit, /persona, or /apply for machine-readable CI output.
|
|
1658
|
+
|
|
1659
|
+
Workflow tuning options:
|
|
1660
|
+
${workflowTuning}
|
|
1661
|
+
|
|
1662
|
+
Repo context:
|
|
1663
|
+
- Target repo: ${repoSlug || "not provided"}
|
|
1664
|
+
- Workspace mode: ${buildFromExistingRepo ? "existing codebase" : "new scaffold"}
|
|
1665
|
+
|
|
1666
|
+
Start now and continue autonomously.
|
|
1667
|
+
`;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
function fallbackWorkflow({ secretName = "SENTINELAYER_TOKEN", authMode = "sentinelayer" } = {}) {
|
|
1671
|
+
const normalizedSecret = isValidSecretName(secretName) ? secretName : "SENTINELAYER_TOKEN";
|
|
1672
|
+
const workflowName = authMode === "byok" ? "Omar Gate (BYOK Mode)" : "Omar Gate";
|
|
1673
|
+
return `name: ${workflowName}
|
|
1674
|
+
|
|
1675
|
+
on:
|
|
1676
|
+
pull_request:
|
|
1677
|
+
types:
|
|
1678
|
+
- opened
|
|
1679
|
+
- synchronize
|
|
1680
|
+
- reopened
|
|
1681
|
+
workflow_dispatch:
|
|
1682
|
+
inputs:
|
|
1683
|
+
scan_mode:
|
|
1684
|
+
description: Sentinelayer scan profile
|
|
1685
|
+
required: false
|
|
1686
|
+
default: deep
|
|
1687
|
+
type: choice
|
|
1688
|
+
options:
|
|
1689
|
+
- deep
|
|
1690
|
+
- nightly
|
|
1691
|
+
severity_gate:
|
|
1692
|
+
description: Severity threshold that blocks merge
|
|
1693
|
+
required: false
|
|
1694
|
+
default: P1
|
|
1695
|
+
type: choice
|
|
1696
|
+
options:
|
|
1697
|
+
- P0
|
|
1698
|
+
- P1
|
|
1699
|
+
- P2
|
|
1700
|
+
- none
|
|
1701
|
+
p2_max_allowed:
|
|
1702
|
+
description: Maximum allowed P2 findings before Omar Gate blocks merge
|
|
1703
|
+
required: false
|
|
1704
|
+
default: "5"
|
|
1705
|
+
type: string
|
|
1706
|
+
|
|
1707
|
+
permissions:
|
|
1708
|
+
contents: read
|
|
1709
|
+
checks: write
|
|
1710
|
+
pull-requests: write
|
|
1711
|
+
id-token: write
|
|
1712
|
+
|
|
1713
|
+
env:
|
|
1714
|
+
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
1715
|
+
|
|
1716
|
+
jobs:
|
|
1717
|
+
omar_gate:
|
|
1718
|
+
name: Omar Gate
|
|
1719
|
+
runs-on: ubuntu-latest
|
|
1720
|
+
permissions:
|
|
1721
|
+
contents: read
|
|
1722
|
+
checks: write
|
|
1723
|
+
pull-requests: write
|
|
1724
|
+
id-token: write
|
|
1725
|
+
steps:
|
|
1726
|
+
- uses: actions/checkout@v4
|
|
1727
|
+
- name: Validate Sentinelayer token secret
|
|
1728
|
+
shell: bash
|
|
1729
|
+
env:
|
|
1730
|
+
SENTINELAYER_TOKEN: \${{ secrets.${normalizedSecret} }}
|
|
1731
|
+
run: |
|
|
1732
|
+
set -euo pipefail
|
|
1733
|
+
if [ -z "\${SENTINELAYER_TOKEN}" ]; then
|
|
1734
|
+
echo "::warning::SENTINELAYER_TOKEN not set. Set it with: gh secret set ${normalizedSecret} --body <your-token>"
|
|
1735
|
+
echo "Skipping Omar Gate scan — run locally with: npx sentinelayer-cli@latest /omargate deep --path ."
|
|
1736
|
+
exit 0
|
|
1737
|
+
fi
|
|
1738
|
+
- name: Run Omar Gate
|
|
1739
|
+
id: omar
|
|
1740
|
+
uses: mrrCarter/sentinelayer-v1-action@v1
|
|
1741
|
+
with:
|
|
1742
|
+
sentinelayer_token: \${{ secrets.${normalizedSecret} }}
|
|
1743
|
+
scan_mode: \${{ github.event_name == 'workflow_dispatch' && inputs.scan_mode || 'deep' }}
|
|
1744
|
+
severity_gate: \${{ github.event_name == 'workflow_dispatch' && inputs.severity_gate || 'P1' }}
|
|
1745
|
+
- name: Enforce Omar reviewer merge thresholds
|
|
1746
|
+
shell: bash
|
|
1747
|
+
env:
|
|
1748
|
+
P0_COUNT: \${{ steps.omar.outputs.p0_count || '0' }}
|
|
1749
|
+
P1_COUNT: \${{ steps.omar.outputs.p1_count || '0' }}
|
|
1750
|
+
P2_COUNT: \${{ steps.omar.outputs.p2_count || '0' }}
|
|
1751
|
+
P2_MAX_ALLOWED: \${{ github.event_name == 'workflow_dispatch' && inputs.p2_max_allowed || '5' }}
|
|
1752
|
+
run: |
|
|
1753
|
+
set -euo pipefail
|
|
1754
|
+
p0="\$(echo "\${P0_COUNT}" | tr -d '\\r' | xargs || true)"
|
|
1755
|
+
p1="\$(echo "\${P1_COUNT}" | tr -d '\\r' | xargs || true)"
|
|
1756
|
+
p2="\$(echo "\${P2_COUNT}" | tr -d '\\r' | xargs || true)"
|
|
1757
|
+
p2_max="\$(echo "\${P2_MAX_ALLOWED}" | tr -d '\\r' | xargs || true)"
|
|
1758
|
+
case "\${p0}" in ''|*[!0-9]*) echo "::error::Invalid P0 count" ; exit 1 ;; esac
|
|
1759
|
+
case "\${p1}" in ''|*[!0-9]*) echo "::error::Invalid P1 count" ; exit 1 ;; esac
|
|
1760
|
+
case "\${p2}" in ''|*[!0-9]*) echo "::error::Invalid P2 count" ; exit 1 ;; esac
|
|
1761
|
+
case "\${p2_max}" in ''|*[!0-9]*) echo "::error::Invalid p2_max" ; exit 1 ;; esac
|
|
1762
|
+
if [ "\${p0}" -gt 0 ] || [ "\${p1}" -gt 0 ]; then
|
|
1763
|
+
echo "::error::Omar Gate blocked: P0=\${p0}, P1=\${p1}. Requires P0=0 and P1=0."
|
|
1764
|
+
exit 1
|
|
1765
|
+
fi
|
|
1766
|
+
if [ "\${p2}" -gt "\${p2_max}" ]; then
|
|
1767
|
+
echo "::error::Omar Gate blocked: P2=\${p2} exceeds max \${p2_max}."
|
|
1768
|
+
exit 1
|
|
1769
|
+
fi
|
|
1770
|
+
- name: Emit Omar run summary
|
|
1771
|
+
shell: bash
|
|
1772
|
+
run: |
|
|
1773
|
+
set -euo pipefail
|
|
1774
|
+
echo "## Omar Gate" >> "\$GITHUB_STEP_SUMMARY"
|
|
1775
|
+
echo "- run_id: \\\`\${{ steps.omar.outputs.run_id }}\\\`" >> "\$GITHUB_STEP_SUMMARY"
|
|
1776
|
+
echo "- gate_status: \\\`\${{ steps.omar.outputs.gate_status }}\\\`" >> "\$GITHUB_STEP_SUMMARY"
|
|
1777
|
+
echo "- findings: P0=\${{ steps.omar.outputs.p0_count }} P1=\${{ steps.omar.outputs.p1_count }} P2=\${{ steps.omar.outputs.p2_count }} P3=\${{ steps.omar.outputs.p3_count }}" >> "\$GITHUB_STEP_SUMMARY"
|
|
1778
|
+
`;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function hasAuthKeywords(text) {
|
|
1782
|
+
const lower = String(text || "").toLowerCase();
|
|
1783
|
+
return ["login", "signup", "sign up", "register", "authentication", "auth flow", "otp", "verification", "password reset"].some((kw) => lower.includes(kw));
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function buildByokArtifacts({ interview, description }) {
|
|
1787
|
+
const featureList =
|
|
1788
|
+
interview.features.length > 0
|
|
1789
|
+
? interview.features.map((item, index) => `${index + 1}. ${item}`).join("\n")
|
|
1790
|
+
: "1. Implement the core workflow end-to-end.\n2. Add observability and hardening.\n3. Add tests and docs.";
|
|
1791
|
+
const techStack =
|
|
1792
|
+
interview.techStack.length > 0 ? interview.techStack.join(", ") : "Node.js, TypeScript, PostgreSQL";
|
|
1793
|
+
|
|
1794
|
+
return {
|
|
1795
|
+
project_name: interview.projectName,
|
|
1796
|
+
spec_sheet: `# Spec
|
|
1797
|
+
|
|
1798
|
+
## Project
|
|
1799
|
+
${interview.projectName}
|
|
1800
|
+
|
|
1801
|
+
## Goal
|
|
1802
|
+
${description}
|
|
1803
|
+
|
|
1804
|
+
## Target audience
|
|
1805
|
+
${interview.audienceLevel}
|
|
1806
|
+
|
|
1807
|
+
## Preferred provider
|
|
1808
|
+
${interview.aiProvider}
|
|
1809
|
+
|
|
1810
|
+
## Project type
|
|
1811
|
+
${interview.projectType}
|
|
1812
|
+
|
|
1813
|
+
## Suggested stack
|
|
1814
|
+
${techStack}
|
|
1815
|
+
|
|
1816
|
+
## Key features
|
|
1817
|
+
${featureList}
|
|
1818
|
+
${hasAuthKeywords(description) ? `
|
|
1819
|
+
## AIdenID E2E Verification
|
|
1820
|
+
When authentication flows are implemented, use AIdenID to test them at scale:
|
|
1821
|
+
1. Confirm AIdenID credentials via \`sl auth status\` (auto-provisioned at login).
|
|
1822
|
+
2. Provision ephemeral test identity: \`sl ai provision-email --execute --json\`.
|
|
1823
|
+
3. Run automated signup flow with provisioned email.
|
|
1824
|
+
4. Extract OTP from inbound email: \`sl ai identity wait-for-otp <identityId> --timeout 30\`.
|
|
1825
|
+
5. Complete login flow with extracted OTP and verify authenticated session.
|
|
1826
|
+
6. Revoke test identity after verification: \`sl ai identity revoke <identityId>\`.
|
|
1827
|
+
` : ""}`,
|
|
1828
|
+
playbook: `# Build Guide
|
|
1829
|
+
|
|
1830
|
+
## Scope
|
|
1831
|
+
- Keep each PR bounded and shippable.
|
|
1832
|
+
- Run tests and local scans before each handoff.
|
|
1833
|
+
- Keep secrets out of source control.
|
|
1834
|
+
|
|
1835
|
+
## Implementation order
|
|
1836
|
+
1. Establish repo baseline and CI checks.
|
|
1837
|
+
2. Implement domain model and persistence boundaries.
|
|
1838
|
+
3. Implement API/worker surface and auth/session policies.
|
|
1839
|
+
4. Add observability, retries, and production hardening.
|
|
1840
|
+
5. Finalize docs and operational runbooks.
|
|
1841
|
+
|
|
1842
|
+
## Review loop
|
|
1843
|
+
- Run \`sentinel /omargate deep --path .\` and \`sentinel /audit --path .\`.
|
|
1844
|
+
- Fix P0/P1 issues before merge.
|
|
1845
|
+
- Fix P2 findings before merge when feasible.
|
|
1846
|
+
`,
|
|
1847
|
+
builder_prompt: `You are operating in Sentinelayer BYOK mode.
|
|
1848
|
+
|
|
1849
|
+
Read files in order:
|
|
1850
|
+
1. docs/spec.md
|
|
1851
|
+
2. docs/build-guide.md
|
|
1852
|
+
3. tasks/todo.md
|
|
1853
|
+
4. AGENT_HANDOFF_PROMPT.md
|
|
1854
|
+
|
|
1855
|
+
Execute PR-by-PR from tasks/todo.md.
|
|
1856
|
+
Run local scans after each PR:
|
|
1857
|
+
- sentinel /omargate deep --path .
|
|
1858
|
+
- sentinel /audit --path .
|
|
1859
|
+
|
|
1860
|
+
Continue autonomously unless blocked by missing credentials or permissions.`,
|
|
1861
|
+
omar_gate_yaml: fallbackWorkflow({ authMode: "byok" }),
|
|
1862
|
+
};
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function runGhSecretSet({ repoSlug, secretName, secretValue }) {
|
|
1866
|
+
const normalizedRepo = normalizeRepoSlug(repoSlug);
|
|
1867
|
+
const ghCommand = getGhCommand();
|
|
1868
|
+
const secretSinkFile = String(process.env.SENTINELAYER_SECRET_SINK_FILE || "").trim();
|
|
1869
|
+
if (!isValidRepoSlug(normalizedRepo)) {
|
|
1870
|
+
return {
|
|
1871
|
+
ok: false,
|
|
1872
|
+
reason: "Invalid repo format. Use owner/repo.",
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
if (!isValidSecretName(secretName)) {
|
|
1876
|
+
return {
|
|
1877
|
+
ok: false,
|
|
1878
|
+
reason: "Invalid secret name from bootstrap response.",
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
if (secretSinkFile) {
|
|
1882
|
+
try {
|
|
1883
|
+
fs.appendFileSync(secretSinkFile, `${normalizedRepo}|${secretName}|${secretValue}\n`, "utf-8");
|
|
1884
|
+
return { ok: true };
|
|
1885
|
+
} catch (error) {
|
|
1886
|
+
return {
|
|
1887
|
+
ok: false,
|
|
1888
|
+
reason: `Failed to write SENTINELAYER_SECRET_SINK_FILE: ${error instanceof Error ? error.message : String(error)}`,
|
|
1889
|
+
};
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
try {
|
|
1893
|
+
ensureGhCliAvailable(ghCommand);
|
|
1894
|
+
} catch (error) {
|
|
1895
|
+
return {
|
|
1896
|
+
ok: false,
|
|
1897
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
const result = spawnSync(ghCommand, ["secret", "set", secretName, "--repo", normalizedRepo], {
|
|
1902
|
+
encoding: "utf-8",
|
|
1903
|
+
input: `${secretValue}\n`,
|
|
1904
|
+
});
|
|
1905
|
+
if (result.status !== 0) {
|
|
1906
|
+
return {
|
|
1907
|
+
ok: false,
|
|
1908
|
+
reason: String(result.stderr || result.stdout || "gh secret set failed").trim(),
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
const verifyResult = spawnSync(ghCommand, ["secret", "list", "--repo", normalizedRepo], {
|
|
1913
|
+
encoding: "utf-8",
|
|
1914
|
+
});
|
|
1915
|
+
if (verifyResult.status !== 0) {
|
|
1916
|
+
return {
|
|
1917
|
+
ok: false,
|
|
1918
|
+
reason: String(verifyResult.stderr || verifyResult.stdout || "gh secret list failed").trim(),
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
const listedSecrets = String(verifyResult.stdout || "");
|
|
1923
|
+
const escapedSecretName = String(secretName || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1924
|
+
const secretRegex = new RegExp(`(^|\\r?\\n)\\s*${escapedSecretName}(\\s|$)`, "m");
|
|
1925
|
+
if (!secretRegex.test(listedSecrets)) {
|
|
1926
|
+
return {
|
|
1927
|
+
ok: false,
|
|
1928
|
+
reason: `Secret '${secretName}' was not visible in gh secret list output after injection.`,
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
return { ok: true };
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
async function collectInterview({ initialProjectName, detectedRepo, detectedCodingAgent }) {
|
|
1936
|
+
const onCancel = () => {
|
|
1937
|
+
throw new Error("Prompt flow cancelled by user.");
|
|
1938
|
+
};
|
|
1939
|
+
const detectedAgentRecord = resolveCodingAgent(detectedCodingAgent || DEFAULT_CODING_AGENT_ID);
|
|
1940
|
+
const codingAgentChoices = listSupportedCodingAgents().map((agent) => ({
|
|
1941
|
+
title:
|
|
1942
|
+
agent.id === detectedAgentRecord.id
|
|
1943
|
+
? `${agent.name} (${agent.id}, detected)`
|
|
1944
|
+
: `${agent.name} (${agent.id})`,
|
|
1945
|
+
value: agent.id,
|
|
1946
|
+
}));
|
|
1947
|
+
const defaultCodingAgentIndex = Math.max(
|
|
1948
|
+
0,
|
|
1949
|
+
codingAgentChoices.findIndex((choice) => choice.value === detectedAgentRecord.id)
|
|
1950
|
+
);
|
|
1951
|
+
const projectTypeChoices = [
|
|
1952
|
+
{ title: "Greenfield", value: "greenfield" },
|
|
1953
|
+
{ title: "Add feature", value: "add_feature" },
|
|
1954
|
+
{ title: "Bugfix / hardening", value: "bugfix" },
|
|
1955
|
+
];
|
|
1956
|
+
const inferredProjectType = isValidRepoSlug(detectedRepo || "") ? "add_feature" : "greenfield";
|
|
1957
|
+
const defaultProjectTypeIndex = Math.max(
|
|
1958
|
+
0,
|
|
1959
|
+
projectTypeChoices.findIndex((choice) => choice.value === inferredProjectType)
|
|
1960
|
+
);
|
|
1961
|
+
|
|
1962
|
+
const base = await prompts(
|
|
1963
|
+
[
|
|
1964
|
+
{
|
|
1965
|
+
type: initialProjectName ? null : "text",
|
|
1966
|
+
name: "projectName",
|
|
1967
|
+
message: "Project folder name",
|
|
1968
|
+
initial: "my-agent-app",
|
|
1969
|
+
validate: (value) =>
|
|
1970
|
+
sanitizeProjectName(value).length > 0 ? true : "Enter a valid project folder name.",
|
|
1971
|
+
},
|
|
1972
|
+
{
|
|
1973
|
+
type: "text",
|
|
1974
|
+
name: "projectDescription",
|
|
1975
|
+
message: "What are you building?",
|
|
1976
|
+
validate: (value) =>
|
|
1977
|
+
String(value || "").trim().length >= 15
|
|
1978
|
+
? true
|
|
1979
|
+
: "Describe your project in at least 15 characters.",
|
|
1980
|
+
},
|
|
1981
|
+
{
|
|
1982
|
+
type: "select",
|
|
1983
|
+
name: "aiProvider",
|
|
1984
|
+
message: "Select your AI provider",
|
|
1985
|
+
choices: [
|
|
1986
|
+
{ title: "OpenAI (Codex)", value: "openai" },
|
|
1987
|
+
{ title: "Anthropic (Claude)", value: "anthropic" },
|
|
1988
|
+
{ title: "Google (Gemini)", value: "google" },
|
|
1989
|
+
],
|
|
1990
|
+
initial: 0,
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
type: "select",
|
|
1994
|
+
name: "codingAgent",
|
|
1995
|
+
message: "Which coding agent will you use?",
|
|
1996
|
+
choices: codingAgentChoices,
|
|
1997
|
+
initial: defaultCodingAgentIndex,
|
|
1998
|
+
},
|
|
1999
|
+
{
|
|
2000
|
+
type: "select",
|
|
2001
|
+
name: "generationMode",
|
|
2002
|
+
message: "Artifact depth",
|
|
2003
|
+
choices: [
|
|
2004
|
+
{ title: "Detailed (recommended)", value: "detailed" },
|
|
2005
|
+
{ title: "Quick", value: "quick" },
|
|
2006
|
+
{ title: "Enterprise", value: "enterprise" },
|
|
2007
|
+
],
|
|
2008
|
+
initial: 0,
|
|
2009
|
+
},
|
|
2010
|
+
{
|
|
2011
|
+
type: "select",
|
|
2012
|
+
name: "audienceLevel",
|
|
2013
|
+
message: "Primary audience",
|
|
2014
|
+
choices: [
|
|
2015
|
+
{ title: "Developer", value: "developer" },
|
|
2016
|
+
{ title: "Intermediate", value: "intermediate" },
|
|
2017
|
+
{ title: "Beginner", value: "beginner" },
|
|
2018
|
+
],
|
|
2019
|
+
initial: 0,
|
|
2020
|
+
},
|
|
2021
|
+
{
|
|
2022
|
+
type: "select",
|
|
2023
|
+
name: "projectType",
|
|
2024
|
+
message: "Project type",
|
|
2025
|
+
choices: projectTypeChoices,
|
|
2026
|
+
initial: defaultProjectTypeIndex,
|
|
2027
|
+
},
|
|
2028
|
+
{
|
|
2029
|
+
type: "text",
|
|
2030
|
+
name: "techStack",
|
|
2031
|
+
message: "Tech stack (comma-separated, optional)",
|
|
2032
|
+
initial: "TypeScript, Node.js, PostgreSQL",
|
|
2033
|
+
},
|
|
2034
|
+
{
|
|
2035
|
+
type: "text",
|
|
2036
|
+
name: "features",
|
|
2037
|
+
message: "Key features (comma-separated, optional)",
|
|
2038
|
+
},
|
|
2039
|
+
{
|
|
2040
|
+
type: "select",
|
|
2041
|
+
name: "authMode",
|
|
2042
|
+
message: "Auth mode",
|
|
2043
|
+
choices: [
|
|
2044
|
+
{ title: "Sentinelayer managed token (recommended)", value: "sentinelayer" },
|
|
2045
|
+
{ title: "BYOK only (skip Sentinelayer token)", value: "byok" },
|
|
2046
|
+
],
|
|
2047
|
+
initial: 0,
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
type: "toggle",
|
|
2051
|
+
name: "advanced",
|
|
2052
|
+
message: "Advanced options?",
|
|
2053
|
+
initial: true,
|
|
2054
|
+
active: "yes",
|
|
2055
|
+
inactive: "no",
|
|
2056
|
+
},
|
|
2057
|
+
],
|
|
2058
|
+
{ onCancel }
|
|
2059
|
+
);
|
|
2060
|
+
|
|
2061
|
+
let advanced = {
|
|
2062
|
+
connectRepo: false,
|
|
2063
|
+
repoSlug: detectedRepo || "",
|
|
2064
|
+
buildFromExistingRepo: false,
|
|
2065
|
+
injectSecret: false,
|
|
2066
|
+
};
|
|
2067
|
+
if (base.advanced) {
|
|
2068
|
+
const repoChoices = [];
|
|
2069
|
+
if (detectedRepo) {
|
|
2070
|
+
repoChoices.push({
|
|
2071
|
+
title: `Use current repo (${detectedRepo})`,
|
|
2072
|
+
value: "current",
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
repoChoices.push({
|
|
2076
|
+
title: "Choose from GitHub account (browser auth)",
|
|
2077
|
+
value: "picker",
|
|
2078
|
+
});
|
|
2079
|
+
repoChoices.push({
|
|
2080
|
+
title: "Enter owner/repo manually",
|
|
2081
|
+
value: "manual",
|
|
2082
|
+
});
|
|
2083
|
+
|
|
2084
|
+
const repoSetup = await prompts(
|
|
2085
|
+
[
|
|
2086
|
+
{
|
|
2087
|
+
type: "toggle",
|
|
2088
|
+
name: "connectRepo",
|
|
2089
|
+
message: "Connect a GitHub repo and inject Actions secret?",
|
|
2090
|
+
initial: Boolean(detectedRepo),
|
|
2091
|
+
active: "yes",
|
|
2092
|
+
inactive: "no",
|
|
2093
|
+
},
|
|
2094
|
+
{
|
|
2095
|
+
type: (prev) => (prev ? "select" : null),
|
|
2096
|
+
name: "repoSource",
|
|
2097
|
+
message: "How should we choose the repo?",
|
|
2098
|
+
choices: repoChoices,
|
|
2099
|
+
initial: detectedRepo ? 0 : 1,
|
|
2100
|
+
},
|
|
2101
|
+
],
|
|
2102
|
+
{ onCancel }
|
|
2103
|
+
);
|
|
2104
|
+
|
|
2105
|
+
advanced.connectRepo = Boolean(repoSetup.connectRepo);
|
|
2106
|
+
if (advanced.connectRepo) {
|
|
2107
|
+
let repoSlug;
|
|
2108
|
+
const repoSource = String(repoSetup.repoSource || "").trim().toLowerCase();
|
|
2109
|
+
|
|
2110
|
+
if (repoSource === "manual") {
|
|
2111
|
+
const manual = await prompts(
|
|
2112
|
+
[
|
|
2113
|
+
{
|
|
2114
|
+
type: "text",
|
|
2115
|
+
name: "repoSlug",
|
|
2116
|
+
message: "GitHub repo (owner/repo)",
|
|
2117
|
+
initial: detectedRepo || "",
|
|
2118
|
+
validate: (value) => (isValidRepoSlug(value) ? true : "Use owner/repo format."),
|
|
2119
|
+
},
|
|
2120
|
+
],
|
|
2121
|
+
{ onCancel }
|
|
2122
|
+
);
|
|
2123
|
+
repoSlug = normalizeRepoSlug(manual.repoSlug);
|
|
2124
|
+
} else if (repoSource === "picker") {
|
|
2125
|
+
repoSlug = await selectRepoSlugFromGithub();
|
|
2126
|
+
} else {
|
|
2127
|
+
repoSlug = normalizeRepoSlug(detectedRepo);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (!isValidRepoSlug(repoSlug)) {
|
|
2131
|
+
throw new Error("GitHub repo selection did not produce a valid owner/repo value.");
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
const repoMode = await prompts(
|
|
2135
|
+
[
|
|
2136
|
+
{
|
|
2137
|
+
type: "toggle",
|
|
2138
|
+
name: "buildFromExistingRepo",
|
|
2139
|
+
message: "Clone this repo locally and build directly into it now?",
|
|
2140
|
+
initial: base.projectType === "add_feature" || base.projectType === "bugfix",
|
|
2141
|
+
active: "yes",
|
|
2142
|
+
inactive: "no",
|
|
2143
|
+
},
|
|
2144
|
+
{
|
|
2145
|
+
type: base.authMode === "sentinelayer" ? "toggle" : null,
|
|
2146
|
+
name: "injectSecret",
|
|
2147
|
+
message: "Inject SENTINELAYER_TOKEN into GitHub Actions secrets now?",
|
|
2148
|
+
initial: true,
|
|
2149
|
+
active: "yes",
|
|
2150
|
+
inactive: "no",
|
|
2151
|
+
},
|
|
2152
|
+
],
|
|
2153
|
+
{ onCancel }
|
|
2154
|
+
);
|
|
2155
|
+
|
|
2156
|
+
advanced.repoSlug = repoSlug;
|
|
2157
|
+
advanced.buildFromExistingRepo = Boolean(repoMode.buildFromExistingRepo);
|
|
2158
|
+
advanced.injectSecret = base.authMode === "sentinelayer" ? Boolean(repoMode.injectSecret) : false;
|
|
2159
|
+
}
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
const projectName =
|
|
2163
|
+
sanitizeProjectName(initialProjectName || base.projectName) || getRepoNameFromSlug(advanced.repoSlug);
|
|
2164
|
+
|
|
2165
|
+
const interviewResult = {
|
|
2166
|
+
projectName,
|
|
2167
|
+
projectDescription: String(base.projectDescription || "").trim(),
|
|
2168
|
+
aiProvider: base.aiProvider,
|
|
2169
|
+
generationMode: base.generationMode,
|
|
2170
|
+
audienceLevel: base.audienceLevel,
|
|
2171
|
+
projectType: base.projectType,
|
|
2172
|
+
codingAgent: resolveCodingAgent(base.codingAgent || detectedAgentRecord.id).id,
|
|
2173
|
+
techStack: parseCommaList(base.techStack),
|
|
2174
|
+
features: parseCommaList(base.features),
|
|
2175
|
+
authMode: base.authMode,
|
|
2176
|
+
connectRepo: Boolean(advanced.connectRepo),
|
|
2177
|
+
repoSlug: normalizeRepoSlug(advanced.repoSlug),
|
|
2178
|
+
buildFromExistingRepo: Boolean(advanced.buildFromExistingRepo),
|
|
2179
|
+
injectSecret: Boolean(advanced.injectSecret),
|
|
2180
|
+
};
|
|
2181
|
+
|
|
2182
|
+
printSection("Interview Review");
|
|
2183
|
+
printInfo(`Project: ${interviewResult.projectName}`);
|
|
2184
|
+
printInfo(`Type: ${interviewResult.projectType}`);
|
|
2185
|
+
printInfo(`Provider: ${interviewResult.aiProvider}`);
|
|
2186
|
+
printInfo(`Coding agent: ${interviewResult.codingAgent}`);
|
|
2187
|
+
printInfo(`Auth mode: ${interviewResult.authMode}`);
|
|
2188
|
+
printInfo(`Repo: ${interviewResult.repoSlug || "not connected"}`);
|
|
2189
|
+
printInfo(
|
|
2190
|
+
`Existing repo mode: ${interviewResult.buildFromExistingRepo ? "enabled (clone/reuse)" : "disabled"}`
|
|
2191
|
+
);
|
|
2192
|
+
|
|
2193
|
+
const review = await prompts(
|
|
2194
|
+
[
|
|
2195
|
+
{
|
|
2196
|
+
type: "toggle",
|
|
2197
|
+
name: "proceed",
|
|
2198
|
+
message: "Proceed with these selections?",
|
|
2199
|
+
initial: true,
|
|
2200
|
+
active: "yes",
|
|
2201
|
+
inactive: "no",
|
|
2202
|
+
},
|
|
2203
|
+
],
|
|
2204
|
+
{ onCancel }
|
|
2205
|
+
);
|
|
2206
|
+
|
|
2207
|
+
if (!review.proceed) {
|
|
2208
|
+
const next = await prompts(
|
|
2209
|
+
[
|
|
2210
|
+
{
|
|
2211
|
+
type: "select",
|
|
2212
|
+
name: "action",
|
|
2213
|
+
message: "What do you want to do?",
|
|
2214
|
+
choices: [
|
|
2215
|
+
{ title: "Restart interview", value: "restart" },
|
|
2216
|
+
{ title: "Cancel", value: "cancel" },
|
|
2217
|
+
],
|
|
2218
|
+
initial: 0,
|
|
2219
|
+
},
|
|
2220
|
+
],
|
|
2221
|
+
{ onCancel }
|
|
2222
|
+
);
|
|
2223
|
+
if (next.action === "restart") {
|
|
2224
|
+
return collectInterview({ initialProjectName, detectedRepo, detectedCodingAgent });
|
|
2225
|
+
}
|
|
2226
|
+
throw new Error("Prompt flow cancelled by user.");
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
return interviewResult;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
function printSection(title) {
|
|
2233
|
+
console.log(`\n${pc.bold(pc.cyan(title))}`);
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
function printInfo(message) {
|
|
2237
|
+
console.log(pc.gray(`- ${message}`));
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
export async function runLegacyCli(rawArgs = process.argv.slice(2)) {
|
|
2241
|
+
refreshRuntimeDefaults();
|
|
2242
|
+
const commandExitCode = await tryRunLocalCommandMode(rawArgs);
|
|
2243
|
+
if (commandExitCode !== null) {
|
|
2244
|
+
if (commandExitCode !== 0) {
|
|
2245
|
+
process.exitCode = commandExitCode;
|
|
2246
|
+
}
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
const args = parseCliArgs(rawArgs);
|
|
2251
|
+
if (args.showHelp) {
|
|
2252
|
+
printUsage();
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
if (args.showVersion) {
|
|
2256
|
+
console.log(CLI_VERSION);
|
|
2257
|
+
return;
|
|
2258
|
+
}
|
|
2259
|
+
const argProjectName = args.projectName;
|
|
2260
|
+
const detectedRepo = detectRepoSlug(process.cwd());
|
|
2261
|
+
const detectedCodingAgent = detectCodingAgentFromEnv(process.env).id;
|
|
2262
|
+
|
|
2263
|
+
printSection("Sentinelayer Scaffold");
|
|
2264
|
+
printInfo(`API: ${DEFAULT_API_URL}`);
|
|
2265
|
+
printInfo(`Web: ${DEFAULT_WEB_URL}`);
|
|
2266
|
+
if (detectedRepo) {
|
|
2267
|
+
printInfo(`Detected repo: ${detectedRepo}`);
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2270
|
+
const automatedInterview = await loadAutomatedInterview({
|
|
2271
|
+
argProjectName,
|
|
2272
|
+
detectedRepo,
|
|
2273
|
+
detectedCodingAgent,
|
|
2274
|
+
interviewFile: args.interviewFile,
|
|
2275
|
+
});
|
|
2276
|
+
|
|
2277
|
+
const interview =
|
|
2278
|
+
automatedInterview ||
|
|
2279
|
+
(args.nonInteractive
|
|
2280
|
+
? null
|
|
2281
|
+
: await collectInterview({
|
|
2282
|
+
initialProjectName: argProjectName,
|
|
2283
|
+
detectedRepo,
|
|
2284
|
+
detectedCodingAgent,
|
|
2285
|
+
}));
|
|
2286
|
+
|
|
2287
|
+
if (!interview) {
|
|
2288
|
+
throw new Error(
|
|
2289
|
+
"Non-interactive mode requires SENTINELAYER_CLI_INTERVIEW_JSON or --interview-file."
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
validateInterviewInput(interview);
|
|
2293
|
+
|
|
2294
|
+
const workspace = await resolveProjectDirectory({
|
|
2295
|
+
cwd: process.cwd(),
|
|
2296
|
+
interview,
|
|
2297
|
+
detectedRepo,
|
|
2298
|
+
});
|
|
2299
|
+
const projectDir = workspace.projectDir;
|
|
2300
|
+
|
|
2301
|
+
printSection("Workspace");
|
|
2302
|
+
if (workspace.reusedCurrentRepo) {
|
|
2303
|
+
printInfo(`Using current repo workspace: ${projectDir}`);
|
|
2304
|
+
} else if (workspace.clonedRepo) {
|
|
2305
|
+
printInfo(`Cloned repo workspace: ${projectDir}`);
|
|
2306
|
+
if (workspace.cloneUrl) {
|
|
2307
|
+
printInfo(`Clone URL: ${workspace.cloneUrl}`);
|
|
2308
|
+
}
|
|
2309
|
+
} else {
|
|
2310
|
+
printInfo(`Target scaffold workspace: ${projectDir}`);
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
const requestedAuthMode = interview.authMode === "byok" ? "byok" : "sentinelayer";
|
|
2314
|
+
let authToken = "";
|
|
2315
|
+
|
|
2316
|
+
printSection("Authentication");
|
|
2317
|
+
if (requestedAuthMode === "byok") {
|
|
2318
|
+
printInfo("BYOK mode selected. Skipping Sentinelayer browser auth and token bootstrap.");
|
|
2319
|
+
} else {
|
|
2320
|
+
if (args.nonInteractive) {
|
|
2321
|
+
console.log("Non-interactive mode: skipping Enter confirmation.");
|
|
2322
|
+
} else {
|
|
2323
|
+
await waitForEnter("Press Enter to authenticate with Sentinelayer in your browser...");
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
const challenge = crypto.randomBytes(32).toString("hex");
|
|
2327
|
+
const session = await startCliSession({
|
|
2328
|
+
apiUrl: DEFAULT_API_URL,
|
|
2329
|
+
challenge,
|
|
2330
|
+
cliVersion: CLI_VERSION,
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
if (args.skipBrowserOpen || args.nonInteractive) {
|
|
2334
|
+
console.log(`Browser open skipped. Authorize manually: ${session.authorize_url}`);
|
|
2335
|
+
} else {
|
|
2336
|
+
console.log(`Opening browser: ${session.authorize_url}`);
|
|
2337
|
+
try {
|
|
2338
|
+
await open(session.authorize_url);
|
|
2339
|
+
} catch {
|
|
2340
|
+
console.log(pc.yellow("Could not auto-open browser. Open this URL manually:"));
|
|
2341
|
+
console.log(pc.yellow(session.authorize_url));
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
console.log("Waiting for browser approval...");
|
|
2346
|
+
const approval = await pollCliSession({
|
|
2347
|
+
apiUrl: DEFAULT_API_URL,
|
|
2348
|
+
sessionId: session.session_id,
|
|
2349
|
+
challenge,
|
|
2350
|
+
pollIntervalSeconds: session.poll_interval_seconds || 2,
|
|
2351
|
+
timeoutMs: DEFAULT_AUTH_TIMEOUT_MS,
|
|
2352
|
+
});
|
|
2353
|
+
|
|
2354
|
+
authToken = String(approval.auth_token || "").trim();
|
|
2355
|
+
if (!authToken) {
|
|
2356
|
+
throw new Error("Authentication completed but no auth token was returned.");
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
printSection("Artifact Generation");
|
|
2361
|
+
let description = interview.projectDescription;
|
|
2362
|
+
if (interview.buildFromExistingRepo) {
|
|
2363
|
+
const repoSummary = await buildRepoIngestSummary(projectDir);
|
|
2364
|
+
if (repoSummary) {
|
|
2365
|
+
description = `${description}\n\nExisting repo context:\n${repoSummary}`;
|
|
2366
|
+
printInfo("Included existing repo ingest summary in generation payload.");
|
|
2367
|
+
} else {
|
|
2368
|
+
printInfo("No repo ingest summary was available. Continuing with base description.");
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
const generatePayload = {
|
|
2372
|
+
description,
|
|
2373
|
+
tech_stack: interview.techStack,
|
|
2374
|
+
features: interview.features,
|
|
2375
|
+
generation_mode: interview.generationMode,
|
|
2376
|
+
audience_level: interview.audienceLevel,
|
|
2377
|
+
project_type: interview.projectType,
|
|
2378
|
+
model_provider: interview.aiProvider,
|
|
2379
|
+
model_id: DEFAULT_MODEL_BY_PROVIDER[interview.aiProvider] || undefined,
|
|
2380
|
+
};
|
|
2381
|
+
let generated = null;
|
|
2382
|
+
let sentinelayerToken = "";
|
|
2383
|
+
let secretName = "SENTINELAYER_TOKEN";
|
|
2384
|
+
|
|
2385
|
+
if (requestedAuthMode === "byok") {
|
|
2386
|
+
generated = buildByokArtifacts({
|
|
2387
|
+
interview,
|
|
2388
|
+
description,
|
|
2389
|
+
});
|
|
2390
|
+
} else {
|
|
2391
|
+
generated = await generateArtifacts({
|
|
2392
|
+
apiUrl: DEFAULT_API_URL,
|
|
2393
|
+
authToken,
|
|
2394
|
+
payload: generatePayload,
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
let bootstrapToken = generated?.bootstrap_token || null;
|
|
2398
|
+
if (!bootstrapToken || !String(bootstrapToken.token || "").trim()) {
|
|
2399
|
+
try {
|
|
2400
|
+
bootstrapToken = await issueBootstrapToken({
|
|
2401
|
+
apiUrl: DEFAULT_API_URL,
|
|
2402
|
+
authToken,
|
|
2403
|
+
});
|
|
2404
|
+
} catch (error) {
|
|
2405
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2406
|
+
console.log(
|
|
2407
|
+
pc.yellow(`Token bootstrap unavailable. Continuing in BYOK mode for this scaffold. (${message})`)
|
|
2408
|
+
);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
sentinelayerToken = String(bootstrapToken?.token || "").trim();
|
|
2413
|
+
if (sentinelayerToken) {
|
|
2414
|
+
const requestedSecretName = String(bootstrapToken.required_secret_name || "").trim();
|
|
2415
|
+
secretName = isValidSecretName(requestedSecretName) ? requestedSecretName : "SENTINELAYER_TOKEN";
|
|
2416
|
+
if (requestedSecretName && requestedSecretName !== secretName) {
|
|
2417
|
+
console.log(
|
|
2418
|
+
pc.yellow(
|
|
2419
|
+
`Received invalid secret name '${requestedSecretName}' from API. Falling back to ${secretName}.`
|
|
2420
|
+
)
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
} else {
|
|
2424
|
+
console.log(pc.yellow("Sentinelayer token unavailable. Continuing in BYOK mode for this scaffold."));
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
const effectiveAuthMode = sentinelayerToken ? "sentinelayer" : "byok";
|
|
2428
|
+
|
|
2429
|
+
const effectiveProjectName =
|
|
2430
|
+
sanitizeProjectName(generated.project_name || interview.projectName || path.basename(projectDir)) ||
|
|
2431
|
+
path.basename(projectDir);
|
|
2432
|
+
const docsDir = path.join(projectDir, "docs");
|
|
2433
|
+
const promptsDir = path.join(projectDir, "prompts");
|
|
2434
|
+
const tasksDir = path.join(projectDir, "tasks");
|
|
2435
|
+
|
|
2436
|
+
await writeTextFile(path.join(docsDir, "spec.md"), String(generated.spec_sheet || "").trim() + "\n");
|
|
2437
|
+
await writeTextFile(
|
|
2438
|
+
path.join(docsDir, "build-guide.md"),
|
|
2439
|
+
String(generated.playbook || "").trim() + "\n"
|
|
2440
|
+
);
|
|
2441
|
+
await writeTextFile(
|
|
2442
|
+
path.join(promptsDir, "execution-prompt.md"),
|
|
2443
|
+
String(generated.builder_prompt || "").trim() + "\n"
|
|
2444
|
+
);
|
|
2445
|
+
await writeTextFile(
|
|
2446
|
+
path.join(projectDir, ".github", "workflows", "omar-gate.yml"),
|
|
2447
|
+
(
|
|
2448
|
+
(effectiveAuthMode === "sentinelayer" ? String(generated.omar_gate_yaml || "").trim() : "") ||
|
|
2449
|
+
fallbackWorkflow({ secretName, authMode: effectiveAuthMode })
|
|
2450
|
+
) + "\n"
|
|
2451
|
+
);
|
|
2452
|
+
await writeTextFile(
|
|
2453
|
+
path.join(tasksDir, "todo.md"),
|
|
2454
|
+
buildTodoContent({
|
|
2455
|
+
projectName: effectiveProjectName,
|
|
2456
|
+
aiProvider: interview.aiProvider,
|
|
2457
|
+
codingAgent: interview.codingAgent,
|
|
2458
|
+
authMode: effectiveAuthMode,
|
|
2459
|
+
repoSlug: interview.repoSlug,
|
|
2460
|
+
buildFromExistingRepo: interview.buildFromExistingRepo,
|
|
2461
|
+
generationMode: interview.generationMode,
|
|
2462
|
+
audienceLevel: interview.audienceLevel,
|
|
2463
|
+
projectType: interview.projectType,
|
|
2464
|
+
})
|
|
2465
|
+
);
|
|
2466
|
+
await writeTextFile(
|
|
2467
|
+
path.join(projectDir, "AGENT_HANDOFF_PROMPT.md"),
|
|
2468
|
+
buildHandoffPrompt({
|
|
2469
|
+
projectName: effectiveProjectName,
|
|
2470
|
+
repoSlug: interview.repoSlug,
|
|
2471
|
+
secretName,
|
|
2472
|
+
buildFromExistingRepo: interview.buildFromExistingRepo,
|
|
2473
|
+
authMode: effectiveAuthMode,
|
|
2474
|
+
codingAgent: interview.codingAgent,
|
|
2475
|
+
})
|
|
2476
|
+
);
|
|
2477
|
+
const codingAgentConfig = await ensureCodingAgentConfigFile({
|
|
2478
|
+
projectDir,
|
|
2479
|
+
projectName: effectiveProjectName,
|
|
2480
|
+
codingAgent: interview.codingAgent,
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2483
|
+
await ensureSentinelStartScript(projectDir, effectiveProjectName);
|
|
2484
|
+
|
|
2485
|
+
// Code scaffold: write starter source files, skip existing
|
|
2486
|
+
const templateFiles = getExpressTemplate({
|
|
2487
|
+
projectName: effectiveProjectName,
|
|
2488
|
+
description: interview.description,
|
|
2489
|
+
});
|
|
2490
|
+
const packageJsonTemplate = getPackageJsonTemplate({
|
|
2491
|
+
projectName: effectiveProjectName,
|
|
2492
|
+
description: interview.description,
|
|
2493
|
+
});
|
|
2494
|
+
const readmeContent = buildReadmeContent({
|
|
2495
|
+
projectName: effectiveProjectName,
|
|
2496
|
+
description: interview.description,
|
|
2497
|
+
techStack: interview.projectType || "Node.js + Express",
|
|
2498
|
+
});
|
|
2499
|
+
const scaffoldResult = await generateScaffold({
|
|
2500
|
+
projectDir,
|
|
2501
|
+
templateFiles,
|
|
2502
|
+
packageJsonTemplate,
|
|
2503
|
+
readmeContent,
|
|
2504
|
+
force: false,
|
|
2505
|
+
});
|
|
2506
|
+
if (scaffoldResult.written.length > 0) {
|
|
2507
|
+
console.log(pc.green(`Scaffold: wrote ${scaffoldResult.written.length} starter files`));
|
|
2508
|
+
for (const f of scaffoldResult.written) {
|
|
2509
|
+
console.log(pc.gray(` + ${f}`));
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
if (scaffoldResult.skipped.length > 0) {
|
|
2513
|
+
for (const s of scaffoldResult.skipped) {
|
|
2514
|
+
console.log(pc.gray(` ~ ${s.path} (${s.reason})`));
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
if (sentinelayerToken) {
|
|
2519
|
+
await ensureEnvFileIgnored(projectDir);
|
|
2520
|
+
await upsertEnvVariable(path.join(projectDir, ".env"), secretName, sentinelayerToken);
|
|
2521
|
+
}
|
|
2522
|
+
await ensureGitRepositorySetup({
|
|
2523
|
+
projectDir,
|
|
2524
|
+
repoSlug: interview.connectRepo ? interview.repoSlug : "",
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
let secretInjection = { ok: false, reason: "Skipped." };
|
|
2528
|
+
if (interview.connectRepo && interview.injectSecret && interview.repoSlug && sentinelayerToken) {
|
|
2529
|
+
secretInjection = runGhSecretSet({
|
|
2530
|
+
repoSlug: interview.repoSlug,
|
|
2531
|
+
secretName,
|
|
2532
|
+
secretValue: sentinelayerToken,
|
|
2533
|
+
});
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
printSection("Complete");
|
|
2537
|
+
console.log(pc.green(`✔ Sentinelayer orchestration initialized in ${projectDir}`));
|
|
2538
|
+
if (sentinelayerToken) {
|
|
2539
|
+
console.log(pc.green(`✔ ${secretName} injected into ${path.join(projectDir, ".env")}`));
|
|
2540
|
+
} else {
|
|
2541
|
+
console.log(pc.yellow("! BYOK mode active: Sentinelayer token was not injected."));
|
|
2542
|
+
}
|
|
2543
|
+
if (codingAgentConfig.created) {
|
|
2544
|
+
console.log(
|
|
2545
|
+
pc.green(`✔ ${codingAgentConfig.agent.name} config scaffolded at ${codingAgentConfig.path}`)
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
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}`));
|
|
2553
|
+
console.log(
|
|
2554
|
+
pc.yellow(
|
|
2555
|
+
` Run manually: gh secret set ${secretName} --repo ${interview.repoSlug || "<owner/repo>"}`
|
|
2556
|
+
)
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
console.log("\nNext:");
|
|
2562
|
+
const nextCd = path.relative(process.cwd(), projectDir) || ".";
|
|
2563
|
+
console.log(`1. cd ${nextCd}`);
|
|
2564
|
+
console.log("2. npm run sentinel:start");
|
|
2565
|
+
console.log("3. Copy/paste AGENT_HANDOFF_PROMPT.md into your coding agent and let it run autonomously.");
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
export function renderCliFailure(error) {
|
|
2569
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2570
|
+
const code = error instanceof SentinelayerApiError ? ` [${error.code}]` : "";
|
|
2571
|
+
const requestId =
|
|
2572
|
+
error instanceof SentinelayerApiError && error.requestId ? ` request_id=${error.requestId}` : "";
|
|
2573
|
+
console.error(pc.red(`\nSentinelayer scaffold failed${code}:${requestId}`));
|
|
2574
|
+
console.error(pc.red(message));
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
export async function runLegacyCliWithErrorHandling(rawArgs = process.argv.slice(2)) {
|
|
2578
|
+
try {
|
|
2579
|
+
await runLegacyCli(rawArgs);
|
|
2580
|
+
} catch (error) {
|
|
2581
|
+
renderCliFailure(error);
|
|
2582
|
+
process.exitCode = 1;
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
const invokedAsEntrypoint =
|
|
2587
|
+
process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
2588
|
+
|
|
2589
|
+
if (invokedAsEntrypoint) {
|
|
2590
|
+
runLegacyCliWithErrorHandling();
|
|
2591
|
+
}
|
|
2592
|
+
|