sentinelayer-cli 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (153) hide show
  1. package/README.md +23 -2
  2. package/package.json +4 -4
  3. package/src/agents/ai-governance/index.js +12 -0
  4. package/src/agents/ai-governance/tools/base.js +171 -0
  5. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  6. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  7. package/src/agents/ai-governance/tools/index.js +52 -0
  8. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  9. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  10. package/src/agents/backend/index.js +12 -0
  11. package/src/agents/backend/tools/base.js +189 -0
  12. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  13. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  14. package/src/agents/backend/tools/index.js +87 -0
  15. package/src/agents/backend/tools/retry-audit.js +132 -0
  16. package/src/agents/backend/tools/timeout-audit.js +144 -0
  17. package/src/agents/code-quality/index.js +12 -0
  18. package/src/agents/code-quality/tools/base.js +159 -0
  19. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  20. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  21. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  22. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  23. package/src/agents/code-quality/tools/index.js +89 -0
  24. package/src/agents/data-layer/index.js +12 -0
  25. package/src/agents/data-layer/tools/base.js +181 -0
  26. package/src/agents/data-layer/tools/index-audit.js +165 -0
  27. package/src/agents/data-layer/tools/index.js +83 -0
  28. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  29. package/src/agents/data-layer/tools/query-explain.js +120 -0
  30. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  31. package/src/agents/documentation/index.js +12 -0
  32. package/src/agents/documentation/tools/api-diff.js +91 -0
  33. package/src/agents/documentation/tools/base.js +151 -0
  34. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  35. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  36. package/src/agents/documentation/tools/index.js +52 -0
  37. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  38. package/src/agents/envelope/fix-cycle.js +45 -0
  39. package/src/agents/envelope/index.js +31 -0
  40. package/src/agents/envelope/loop.js +150 -0
  41. package/src/agents/envelope/pulse.js +18 -0
  42. package/src/agents/envelope/stream.js +40 -0
  43. package/src/agents/infrastructure/index.js +12 -0
  44. package/src/agents/infrastructure/tools/base.js +171 -0
  45. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  46. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  47. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  48. package/src/agents/infrastructure/tools/index.js +52 -0
  49. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  50. package/src/agents/jules/loop.js +7 -4
  51. package/src/agents/jules/swarm/sub-agent.js +5 -1
  52. package/src/agents/jules/tools/auth-audit.js +10 -1
  53. package/src/agents/mode.js +113 -0
  54. package/src/agents/observability/index.js +12 -0
  55. package/src/agents/observability/tools/alert-audit.js +39 -0
  56. package/src/agents/observability/tools/base.js +181 -0
  57. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  58. package/src/agents/observability/tools/index.js +54 -0
  59. package/src/agents/observability/tools/log-schema-check.js +74 -0
  60. package/src/agents/observability/tools/span-coverage.js +74 -0
  61. package/src/agents/persona-visuals.js +38 -0
  62. package/src/agents/release/index.js +12 -0
  63. package/src/agents/release/tools/base.js +181 -0
  64. package/src/agents/release/tools/changelog-diff.js +86 -0
  65. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  66. package/src/agents/release/tools/index.js +61 -0
  67. package/src/agents/release/tools/rollback-verify.js +129 -0
  68. package/src/agents/release/tools/semver-check.js +109 -0
  69. package/src/agents/reliability/index.js +12 -0
  70. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  71. package/src/agents/reliability/tools/base.js +181 -0
  72. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  73. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  74. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  75. package/src/agents/reliability/tools/index.js +87 -0
  76. package/src/agents/run-persona.js +109 -0
  77. package/src/agents/security/index.js +12 -0
  78. package/src/agents/security/tools/authz-audit.js +134 -0
  79. package/src/agents/security/tools/base.js +190 -0
  80. package/src/agents/security/tools/crypto-review.js +175 -0
  81. package/src/agents/security/tools/index.js +97 -0
  82. package/src/agents/security/tools/sast-scan.js +175 -0
  83. package/src/agents/security/tools/secrets-scan.js +216 -0
  84. package/src/agents/supply-chain/index.js +12 -0
  85. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  86. package/src/agents/supply-chain/tools/base.js +151 -0
  87. package/src/agents/supply-chain/tools/index.js +52 -0
  88. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  89. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  90. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  91. package/src/agents/testing/index.js +12 -0
  92. package/src/agents/testing/tools/base.js +202 -0
  93. package/src/agents/testing/tools/coverage-gap.js +144 -0
  94. package/src/agents/testing/tools/flake-detect.js +125 -0
  95. package/src/agents/testing/tools/index.js +85 -0
  96. package/src/agents/testing/tools/mutation-test.js +143 -0
  97. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  98. package/src/auth/gate.js +65 -37
  99. package/src/cli.js +1 -1
  100. package/src/commands/chat.js +3 -10
  101. package/src/commands/legacy-args.js +10 -0
  102. package/src/commands/omargate.js +36 -2
  103. package/src/commands/persona.js +46 -1
  104. package/src/commands/scan.js +3 -10
  105. package/src/commands/session.js +654 -6
  106. package/src/commands/spec.js +3 -10
  107. package/src/coord/events-log.js +141 -0
  108. package/src/coord/handshake.js +719 -0
  109. package/src/coord/index.js +35 -0
  110. package/src/coord/paths.js +84 -0
  111. package/src/coord/priority.js +62 -0
  112. package/src/coord/tarjan.js +157 -0
  113. package/src/cost/tokenizer.js +160 -0
  114. package/src/cost/tracker.js +61 -0
  115. package/src/daemon/artifact-lineage.js +362 -0
  116. package/src/daemon/assignment-ledger.js +117 -0
  117. package/src/daemon/ast-drift.js +496 -0
  118. package/src/daemon/ingest-refresh.js +69 -2
  119. package/src/ingest/engine.js +15 -0
  120. package/src/ingest/ownership.js +380 -0
  121. package/src/legacy-cli.js +68 -1
  122. package/src/orchestrator/kai-chen.js +126 -0
  123. package/src/review/ai-review.js +3 -10
  124. package/src/review/compliance-pack.js +389 -0
  125. package/src/review/investor-dd-config.js +54 -0
  126. package/src/review/investor-dd-file-loop.js +303 -0
  127. package/src/review/investor-dd-file-router.js +406 -0
  128. package/src/review/investor-dd-html-report.js +233 -0
  129. package/src/review/investor-dd-notification.js +120 -0
  130. package/src/review/investor-dd-orchestrator.js +405 -0
  131. package/src/review/investor-dd-persona-runner.js +275 -0
  132. package/src/review/live-validator.js +253 -0
  133. package/src/review/omargate-orchestrator.js +90 -2
  134. package/src/review/persona-prompts.js +244 -56
  135. package/src/review/reconciliation-rules.js +329 -0
  136. package/src/review/reproducibility-chain.js +136 -0
  137. package/src/review/scan-modes.js +102 -3
  138. package/src/session/agent-registry.js +7 -0
  139. package/src/session/analytics.js +479 -0
  140. package/src/session/daemon.js +609 -14
  141. package/src/session/file-locks.js +666 -0
  142. package/src/session/paths.js +4 -0
  143. package/src/session/recap.js +567 -0
  144. package/src/session/redact.js +82 -0
  145. package/src/session/runtime-bridge.js +24 -1
  146. package/src/session/scoring.js +406 -0
  147. package/src/session/setup-guides.js +304 -0
  148. package/src/session/store.js +318 -2
  149. package/src/session/stream.js +9 -1
  150. package/src/session/sync.js +753 -0
  151. package/src/session/tasks.js +1054 -0
  152. package/src/session/templates.js +188 -0
  153. package/src/swarm/runtime.js +1 -8
package/src/auth/gate.js CHANGED
@@ -292,25 +292,6 @@ function hasTrustedBypassContext(args = []) {
292
292
  return consumeNonceEnvelope(nonceEnvelope.nonceFile);
293
293
  }
294
294
 
295
- function isValidSessionToken(session) {
296
- const token = String(session?.token || "");
297
- if (!token || token !== token.trim()) {
298
- return false;
299
- }
300
- if (/\s/.test(token)) {
301
- return false;
302
- }
303
- // Require printable ASCII only for bearer token material in local metadata.
304
- if (/[^\x21-\x7E]/.test(token)) {
305
- return false;
306
- }
307
- const tokenPrefix = String(session?.tokenPrefix || "").trim();
308
- if (tokenPrefix && !token.includes(tokenPrefix)) {
309
- return false;
310
- }
311
- return true;
312
- }
313
-
314
295
  function isSessionUnexpired(tokenExpiresAt) {
315
296
  const normalized = String(tokenExpiresAt || "").trim();
316
297
  if (!normalized) {
@@ -323,13 +304,25 @@ function isSessionUnexpired(tokenExpiresAt) {
323
304
  return expiresAt >= Date.now();
324
305
  }
325
306
 
307
+ // Gate-level session validation.
308
+ //
309
+ // Design principle: the gate is a "do they have a token?" check, not a
310
+ // "is the token cryptographically well-formed?" check. Server-side /auth/me
311
+ // and per-call bearer validation are the authoritative gate on the token
312
+ // material itself. Over-strict client-side checks (ASCII-only, exact-prefix
313
+ // inclusion, etc.) surface as "Authentication required" even when the user
314
+ // has a perfectly valid keyring entry, forcing them to logout/login repeatedly
315
+ // without fixing anything.
316
+ //
317
+ // So the gate checks:
318
+ // - session.token is present and non-empty
319
+ // - for source === "session", expiry is in the future
320
+ // - for source === "env" or "config", the downstream API call is the gate
326
321
  function isAuthenticatedSessionValid(session) {
327
- if (!isValidSessionToken(session)) {
322
+ const token = String(session?.token || "").trim();
323
+ if (!token) {
328
324
  return false;
329
325
  }
330
-
331
- // Persisted sessions must include a valid expiry bound. Env/config tokens
332
- // are accepted as active auth sources and validated downstream by API calls.
333
326
  if (String(session?.source || "").trim() === "session") {
334
327
  return isSessionUnexpired(session?.tokenExpiresAt);
335
328
  }
@@ -341,28 +334,29 @@ function isAuthenticatedSessionValid(session) {
341
334
  * Returns true if auth is required but user is not logged in.
342
335
  *
343
336
  * @param {string[]} args - CLI arguments (after normalization)
344
- * @returns {Promise<{ authenticated: boolean, session: object|null, bypassReason: string|null }>}
337
+ * @returns {Promise<{ authenticated: boolean, session: object|null, bypassReason: string|null, failureReason: string|null }>}
345
338
  */
346
339
  export async function checkAuthGate(args) {
347
340
  const first = String(args[0] || "").trim().toLowerCase();
348
341
 
349
342
  if (!first || AUTH_BYPASS_COMMANDS.has(first)) {
350
- return { authenticated: true, session: null, bypassReason: "auth_bypass_command" };
343
+ return { authenticated: true, session: null, bypassReason: "auth_bypass_command", failureReason: null };
351
344
  }
352
345
 
353
346
  if (NO_AUTH_REQUIRED.has(first)) {
354
- return { authenticated: true, session: null, bypassReason: "no_auth_required" };
347
+ return { authenticated: true, session: null, bypassReason: "no_auth_required", failureReason: null };
355
348
  }
356
349
 
357
350
  if (isSessionNoAuthCommand(args)) {
358
- return { authenticated: true, session: null, bypassReason: "session_no_auth_required" };
351
+ return { authenticated: true, session: null, bypassReason: "session_no_auth_required", failureReason: null };
359
352
  }
360
353
 
361
354
  if (process.env.SENTINELAYER_CLI_SKIP_AUTH === "1" && hasTrustedBypassContext(args)) {
362
- return { authenticated: true, session: null, bypassReason: "env_bypass_guarded" };
355
+ return { authenticated: true, session: null, bypassReason: "env_bypass_guarded", failureReason: null };
363
356
  }
364
357
 
365
358
  // Check for active auth session across env -> config -> stored session.
359
+ let resolveError = null;
366
360
  try {
367
361
  const session = await resolveActiveAuthSession({
368
362
  cwd: process.cwd(),
@@ -370,31 +364,65 @@ export async function checkAuthGate(args) {
370
364
  autoRotate: false,
371
365
  });
372
366
  if (session && isAuthenticatedSessionValid(session)) {
373
- return { authenticated: true, session, bypassReason: null };
367
+ return { authenticated: true, session, bypassReason: null, failureReason: null };
374
368
  }
375
- } catch {
376
- // Session read failed treat as not authenticated
369
+ if (session) {
370
+ // Session resolved but failed validation (empty token or expired).
371
+ const tokenPresent = Boolean(String(session?.token || "").trim());
372
+ if (!tokenPresent) {
373
+ resolveError = "session_token_missing";
374
+ } else if (String(session?.source || "").trim() === "session" && !isSessionUnexpired(session?.tokenExpiresAt)) {
375
+ resolveError = "session_expired";
376
+ } else {
377
+ resolveError = "session_invalid";
378
+ }
379
+ } else {
380
+ resolveError = "no_session";
381
+ }
382
+ } catch (error) {
383
+ resolveError = error instanceof Error ? `session_read_error: ${error.message}` : "session_read_error";
377
384
  }
378
385
 
379
- return { authenticated: false, session: null, bypassReason: null };
386
+ return { authenticated: false, session: null, bypassReason: null, failureReason: resolveError };
380
387
  }
381
388
 
382
389
  /**
383
- * Print auth required message and exit.
390
+ * Print auth required message and exit. Optional failureReason surfaces the
391
+ * specific reason so users can diagnose stale sessions, expired tokens, and
392
+ * keyring failures without a round trip.
384
393
  */
385
- export function printAuthRequired() {
394
+ export function printAuthRequired(failureReason = null) {
395
+ const reason = String(failureReason || "").trim();
386
396
  console.error("");
387
397
  console.error(pc.bold(pc.red("Authentication required.")));
388
398
  console.error("");
389
- console.error(" Log in to SentinelLayer to use CLI commands:");
399
+ if (reason === "session_expired") {
400
+ console.error(" Your stored session has expired. Log in again:");
401
+ } else if (reason === "session_token_missing") {
402
+ console.error(" Your session metadata is present but the token is missing");
403
+ console.error(" (likely a keyring read failure or mismatched storage).");
404
+ console.error("");
405
+ console.error(" " + pc.yellow("Fix:") + " log out to clear the stale metadata, then log in:");
406
+ console.error(" " + pc.cyan("sentinelayer-cli auth logout"));
407
+ } else if (reason && reason.startsWith("session_read_error")) {
408
+ console.error(" Session read failed: " + pc.yellow(reason.replace(/^session_read_error:\s*/, "")));
409
+ console.error(" Log out and back in to reset local state:");
410
+ console.error(" " + pc.cyan("sentinelayer-cli auth logout"));
411
+ } else {
412
+ console.error(" Log in to SentinelLayer to use CLI commands:");
413
+ }
390
414
  console.error("");
391
415
  console.error(" " + pc.cyan(authLoginHint()));
392
416
  console.error("");
393
417
  console.error(" This opens your browser to authenticate via GitHub or Google.");
394
418
  console.error(" Your session is encrypted and stored locally.");
395
419
  console.error("");
396
- console.error(" " + pc.gray("Why? All CLI operations sync to your SentinelLayer account —"));
397
- console.error(" " + pc.gray("audit reports, findings, cost tracking, and run history."));
420
+ if (!reason || reason === "no_session") {
421
+ console.error(" " + pc.gray("Why? All CLI operations sync to your SentinelLayer account —"));
422
+ console.error(" " + pc.gray("audit reports, findings, cost tracking, and run history."));
423
+ } else {
424
+ console.error(" " + pc.gray(`Diagnostic: ${reason}`));
425
+ }
398
426
  console.error("");
399
427
  process.exitCode = 1;
400
428
  }
package/src/cli.js CHANGED
@@ -239,7 +239,7 @@ export async function runCli(rawArgs = process.argv.slice(2)) {
239
239
  const { checkAuthGate, printAuthRequired } = await import("./auth/gate.js");
240
240
  const authResult = await checkAuthGate(normalizedArgs);
241
241
  if (!authResult.authenticated) {
242
- printAuthRequired();
242
+ printAuthRequired(authResult.failureReason);
243
243
  return;
244
244
  }
245
245
 
@@ -10,6 +10,7 @@ import {
10
10
  resolveProvider,
11
11
  } from "../ai/client.js";
12
12
  import { resolveOutputRoot } from "../config/service.js";
13
+ import { estimateTokens } from "../cost/tokenizer.js";
13
14
 
14
15
  function shouldEmitJson(options, command) {
15
16
  const local = Boolean(options && options.json);
@@ -24,14 +25,6 @@ function createSessionId() {
24
25
  return `${stamp}-${random}`;
25
26
  }
26
27
 
27
- function estimateTokens(text) {
28
- const normalized = String(text || "");
29
- if (!normalized) {
30
- return 0;
31
- }
32
- return Math.max(1, Math.ceil(normalized.length / 4));
33
- }
34
-
35
28
  async function readPromptFromStdin() {
36
29
  if (process.stdin.isTTY) {
37
30
  return "";
@@ -132,8 +125,8 @@ export function registerChatCommand(program) {
132
125
 
133
126
  const durationMs = Date.now() - startedAt;
134
127
  const generatedAt = new Date().toISOString();
135
- const inputTokens = estimateTokens(prompt);
136
- const outputTokens = estimateTokens(responseText);
128
+ const inputTokens = estimateTokens(prompt, { model });
129
+ const outputTokens = estimateTokens(responseText, { model });
137
130
 
138
131
  await appendTranscriptEntries({
139
132
  transcriptPath,
@@ -19,6 +19,13 @@ function appendOutputDirFlag(args, maybeOutputDir) {
19
19
  }
20
20
  }
21
21
 
22
+ function appendPassthroughFlag(args, flagName, maybeValue) {
23
+ const value = String(maybeValue || "").trim();
24
+ if (value) {
25
+ args.push(flagName, value);
26
+ }
27
+ }
28
+
22
29
  export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {}) {
23
30
  const args = [...baseArgs];
24
31
  appendPathFlag(args, commandOptions.path);
@@ -26,5 +33,8 @@ export function buildLegacyArgs(baseArgs, { commandOptions = {}, command } = {})
26
33
  if (wantsJsonOutput(commandOptions, command)) {
27
34
  args.push("--json");
28
35
  }
36
+ // Omar Gate per-persona filter flags (A-CLI-1).
37
+ appendPassthroughFlag(args, "--persona", commandOptions.persona);
38
+ appendPassthroughFlag(args, "--skip-persona", commandOptions.skipPersona);
29
39
  return args;
30
40
  }
@@ -12,11 +12,13 @@ export function registerOmarGateCommand(program, invokeLegacy) {
12
12
  .option("--output-dir <path>", "Artifact root for report output")
13
13
  .option("--no-ai", "Skip AI review layer (deterministic only)")
14
14
  .option("--ai-dry-run", "Run AI layer in dry-run mode (no LLM call)")
15
- .option("--scan-mode <mode>", "Scan depth: baseline (1 persona), deep (6), full-depth (13)")
15
+ .option("--scan-mode <mode>", "Scan depth: baseline (1 persona), deep (13), full-depth (13)")
16
16
  .option("--max-parallel <n>", "Max concurrent persona calls (default: 4)")
17
17
  .option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
18
- .option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic, google")
18
+ .option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic")
19
19
  .option("--max-cost <usd>", "Maximum AI layer cost in USD (default: 5.0)")
20
+ .option("--persona <csv>", "Only run these personas (comma-separated IDs); unknown IDs are dropped + warned")
21
+ .option("--skip-persona <csv>", "Skip these personas (comma-separated IDs)")
20
22
  .option("--stream", "Emit NDJSON events to stdout as personas run")
21
23
  .option("--json", "Emit machine-readable output")
22
24
  .action(async (options, command) => {
@@ -26,4 +28,36 @@ export function registerOmarGateCommand(program, invokeLegacy) {
26
28
  });
27
29
  await invokeLegacy(legacyArgs);
28
30
  });
31
+
32
+ // Investor-DD mode (docs/INVESTOR_DD_ARCHITECTURE.md). Per-file agentic
33
+ // review across all 13 personas with deterministic file routing,
34
+ // reproducibility chain per finding, Senti session streaming, and a
35
+ // final report shipped via email + dashboard card. Trades runtime +
36
+ // cost for depth — budgets default to 45min / $25 vs deep's 2min / $5.
37
+ omargate
38
+ .command("investor-dd")
39
+ .description("Investor-grade due-diligence audit: per-file agentic review + reproducibility chain + email/dashboard report")
40
+ .option("--path <path>", "Target repository path")
41
+ .option("--output-dir <path>", "Artifact root for report output")
42
+ .option("--max-cost <usd>", "Maximum LLM cost in USD (default: 25.0)")
43
+ .option("--max-runtime-minutes <n>", "Maximum wall-clock runtime (default: 45)")
44
+ .option("--max-parallel <n>", "Max concurrent persona loops (default: 3)")
45
+ .option("--model <id>", "LLM model override (default: gpt-5.3-codex)")
46
+ .option("--provider <name>", "LLM provider: sentinelayer, openai, anthropic")
47
+ .option("--persona <csv>", "Only run these personas (comma-separated IDs)")
48
+ .option("--skip-persona <csv>", "Skip these personas (comma-separated IDs)")
49
+ .option("--stream", "Emit NDJSON events to stdout as personas work file-by-file")
50
+ .option("--notify-email <addr>", "Send final report to this email (default: account email)")
51
+ .option("--notify-session <session-id>", "Stream progress into this Senti session (default: auto-start)")
52
+ .option("--no-email", "Skip email dispatch")
53
+ .option("--no-dashboard", "Skip dashboard card persistence")
54
+ .option("--dry-run", "Validate config + emit plan.json; skip LLM calls")
55
+ .option("--json", "Emit machine-readable final output")
56
+ .action(async (options, command) => {
57
+ const legacyArgs = buildLegacyArgs(["/omargate", "investor-dd"], {
58
+ commandOptions: options,
59
+ command,
60
+ });
61
+ await invokeLegacy(legacyArgs);
62
+ });
29
63
  }
@@ -1,9 +1,16 @@
1
+ import process from "node:process";
2
+
3
+ import { PERSONA_MODES } from "../agents/mode.js";
4
+ import {
5
+ SUPPORTED_PERSONA_IDS,
6
+ runPersona,
7
+ } from "../agents/run-persona.js";
1
8
  import { buildLegacyArgs } from "./legacy-args.js";
2
9
 
3
10
  export function registerPersonaCommand(program, invokeLegacy) {
4
11
  const persona = program
5
12
  .command("persona")
6
- .description("Generate orchestrator persona context");
13
+ .description("Run persona-scoped domain-tool sweeps or orchestrator reports");
7
14
 
8
15
  persona
9
16
  .command("orchestrator")
@@ -24,4 +31,42 @@ export function registerPersonaCommand(program, invokeLegacy) {
24
31
  });
25
32
  await invokeLegacy(legacyArgs);
26
33
  });
34
+
35
+ persona
36
+ .command("run <personaId>")
37
+ .description(
38
+ `Run a single persona's domain tools over the repo and emit findings. Supported ids: ${SUPPORTED_PERSONA_IDS.join(", ")}.`
39
+ )
40
+ .option(
41
+ "--mode <mode>",
42
+ `Persona mode: ${PERSONA_MODES.join("|")}. Audit emits findings; codegen attaches allowed-tools + prompt-suffix plan so callers can drive the LLM edit loop.`,
43
+ "audit"
44
+ )
45
+ .option("--path <path>", "Repository root to scan (default: cwd)", ".")
46
+ .option(
47
+ "--files <csv>",
48
+ "Optional comma-separated list of files to focus the sweep. Empty = whole repo."
49
+ )
50
+ .option(
51
+ "--json",
52
+ "Always-on for this subcommand; kept for interface parity. Output is a single-line JSON object on stdout.",
53
+ true
54
+ )
55
+ .action(async (personaId, options) => {
56
+ try {
57
+ const result = await runPersona({
58
+ personaId,
59
+ mode: options.mode,
60
+ rootPath: options.path,
61
+ files: options.files,
62
+ });
63
+ process.stdout.write(JSON.stringify(result));
64
+ process.stdout.write("\n");
65
+ process.exitCode = 0;
66
+ } catch (err) {
67
+ const message = err && err.message ? err.message : String(err);
68
+ process.stderr.write(`persona run failed: ${message}\n`);
69
+ process.exitCode = 2;
70
+ }
71
+ });
27
72
  }
@@ -15,6 +15,7 @@ import { loadConfig, resolveOutputRoot } from "../config/service.js";
15
15
  import { evaluateBudget } from "../cost/budget.js";
16
16
  import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
17
17
  import { estimateModelCost } from "../cost/tracker.js";
18
+ import { estimateTokens } from "../cost/tokenizer.js";
18
19
  import {
19
20
  applyPolicyPackToScanProfile,
20
21
  resolveActivePolicyPack,
@@ -168,14 +169,6 @@ function parsePercent(rawValue, field) {
168
169
  return normalized;
169
170
  }
170
171
 
171
- function estimateTokenCount(text) {
172
- const normalized = String(text || "");
173
- if (!normalized) {
174
- return 0;
175
- }
176
- return Math.max(1, Math.ceil(normalized.length / 4));
177
- }
178
-
179
172
  function resolveConfiguredApiKey(provider, resolvedConfig = {}) {
180
173
  const normalizedProvider = String(provider || "").trim().toLowerCase();
181
174
  if (normalizedProvider === "openai") {
@@ -622,8 +615,8 @@ export function registerScanCommand(program) {
622
615
  await fsp.mkdir(path.dirname(reportPath), { recursive: true });
623
616
  await fsp.writeFile(reportPath, reportMarkdown, "utf-8");
624
617
 
625
- const inputTokens = estimateTokenCount(prompt);
626
- const outputTokens = estimateTokenCount(aiMarkdown);
618
+ const inputTokens = estimateTokens(prompt, { model: response.model });
619
+ const outputTokens = estimateTokens(aiMarkdown, { model: response.model });
627
620
  const modelCost = maybeEstimateModelCost({
628
621
  modelId: response.model,
629
622
  inputTokens,