sentinelayer-cli 0.8.0 → 0.8.1
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 +13 -0
- package/package.json +4 -4
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/loop.js +7 -4
- package/src/agents/jules/swarm/sub-agent.js +5 -1
- package/src/agents/jules/tools/auth-audit.js +10 -1
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +38 -0
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/auth/gate.js +65 -37
- package/src/cli.js +1 -1
- package/src/commands/chat.js +3 -10
- package/src/commands/legacy-args.js +10 -0
- package/src/commands/omargate.js +36 -2
- package/src/commands/persona.js +46 -1
- package/src/commands/scan.js +3 -10
- package/src/commands/session.js +654 -6
- package/src/commands/spec.js +3 -10
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +61 -0
- package/src/daemon/artifact-lineage.js +362 -0
- package/src/daemon/assignment-ledger.js +117 -0
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ingest-refresh.js +69 -2
- package/src/ingest/engine.js +15 -0
- package/src/ingest/ownership.js +380 -0
- package/src/legacy-cli.js +68 -1
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/review/ai-review.js +3 -10
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/omargate-orchestrator.js +90 -2
- package/src/review/persona-prompts.js +244 -56
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +102 -3
- package/src/session/agent-registry.js +7 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +609 -14
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +4 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +24 -1
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +318 -2
- package/src/session/stream.js +9 -1
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- package/src/swarm/runtime.js +1 -8
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// File-to-persona ownership routing (#A10, spec §5.7).
|
|
2
|
+
//
|
|
3
|
+
// Personas are expensive: every persona runs an LLM call over whatever files
|
|
4
|
+
// it thinks are in-scope. When 13 personas each scan the whole codebase the
|
|
5
|
+
// token usage compounds. This module lets the orchestrator send each finding
|
|
6
|
+
// (or each file) to only the persona that owns the code, which the spec
|
|
7
|
+
// measures at >40% token savings on multi-persona runs.
|
|
8
|
+
//
|
|
9
|
+
// Two routing modes:
|
|
10
|
+
// 1. Explicit — read `.sentinelayer/scaffold.yaml`, walk `ownership_rules`
|
|
11
|
+
// as a last-match-wins glob → persona list.
|
|
12
|
+
// 2. Heuristic — no scaffold.yaml, fall back to keyword / extension rules
|
|
13
|
+
// derived from the 13-persona canon (security, backend, frontend,
|
|
14
|
+
// testing, code-quality, data-layer, documentation, reliability,
|
|
15
|
+
// release, observability, infrastructure, supply-chain, ai-governance).
|
|
16
|
+
//
|
|
17
|
+
// All exports are pure functions: no filesystem work except
|
|
18
|
+
// `loadScaffoldConfig` which reads a single YAML file. The rest operate on
|
|
19
|
+
// in-memory inputs so they compose cleanly with existing ingest callers.
|
|
20
|
+
|
|
21
|
+
import fsp from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import process from "node:process";
|
|
24
|
+
|
|
25
|
+
import YAML from "yaml";
|
|
26
|
+
|
|
27
|
+
import { PERSONA_IDS } from "../review/persona-prompts.js";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_HEURISTIC_FALLBACK = "backend";
|
|
30
|
+
const SCAFFOLD_RELATIVE_PATH = ".sentinelayer/scaffold.yaml";
|
|
31
|
+
|
|
32
|
+
// --- Glob matching -------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
// Translate a shell-style glob into a RegExp. Supports `*` (single segment),
|
|
35
|
+
// `**` (cross-segment), `?` (single char), and character classes passed
|
|
36
|
+
// through. Not a full fnmatch — but enough for ownership routing and good
|
|
37
|
+
// enough that a pattern like `lib/auth/**/*.{ts,tsx}` could be rewritten as
|
|
38
|
+
// two entries: `lib/auth/**/*.ts` and `lib/auth/**/*.tsx`.
|
|
39
|
+
function globToRegExp(glob) {
|
|
40
|
+
const raw = String(glob || "").trim();
|
|
41
|
+
if (!raw) {
|
|
42
|
+
throw new Error("ownership_rules.pattern is required.");
|
|
43
|
+
}
|
|
44
|
+
const normalized = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
45
|
+
|
|
46
|
+
let escaped = "";
|
|
47
|
+
for (let idx = 0; idx < normalized.length; idx += 1) {
|
|
48
|
+
const ch = normalized[idx];
|
|
49
|
+
const next = normalized[idx + 1];
|
|
50
|
+
if (ch === "*") {
|
|
51
|
+
if (next === "*") {
|
|
52
|
+
// `**/` matches zero or more path segments, `/** ` matches any tail
|
|
53
|
+
if (normalized[idx + 2] === "/") {
|
|
54
|
+
escaped += "(?:.*/)?";
|
|
55
|
+
idx += 2;
|
|
56
|
+
} else {
|
|
57
|
+
escaped += ".*";
|
|
58
|
+
idx += 1;
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
escaped += "[^/]*";
|
|
62
|
+
}
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "?") {
|
|
66
|
+
escaped += "[^/]";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if ("\\.+^$(){}|".includes(ch)) {
|
|
70
|
+
escaped += `\\${ch}`;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
escaped += ch;
|
|
74
|
+
}
|
|
75
|
+
return new RegExp(`^${escaped}$`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function matchGlob(pattern, filePath) {
|
|
79
|
+
return globToRegExp(pattern).test(filePath);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizePathForMatch(filePath) {
|
|
83
|
+
return String(filePath || "")
|
|
84
|
+
.trim()
|
|
85
|
+
.replace(/\\/g, "/")
|
|
86
|
+
.replace(/^\.\//, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizePersonaId(value) {
|
|
90
|
+
return String(value || "").trim().toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assertKnownPersona(value) {
|
|
94
|
+
const normalized = normalizePersonaId(value);
|
|
95
|
+
if (!normalized) {
|
|
96
|
+
throw new Error("ownership_rules.persona is required.");
|
|
97
|
+
}
|
|
98
|
+
if (!PERSONA_IDS.includes(normalized)) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
`ownership_rules.persona must be one of ${PERSONA_IDS.join(", ")} (got "${value}").`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return normalized;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// --- Scaffold YAML -------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
export function parseScaffoldYaml(raw) {
|
|
109
|
+
const text = String(raw || "");
|
|
110
|
+
const trimmed = text.trim();
|
|
111
|
+
if (!trimmed) {
|
|
112
|
+
return { ownershipRules: [] };
|
|
113
|
+
}
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = YAML.parse(text);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
throw new Error(`scaffold.yaml is not valid YAML: ${err.message}`);
|
|
119
|
+
}
|
|
120
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
121
|
+
throw new Error("scaffold.yaml must be a mapping at the top level.");
|
|
122
|
+
}
|
|
123
|
+
const rawRules = parsed.ownership_rules;
|
|
124
|
+
if (rawRules === undefined || rawRules === null) {
|
|
125
|
+
return { ownershipRules: [] };
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(rawRules)) {
|
|
128
|
+
throw new Error("scaffold.yaml ownership_rules must be a list.");
|
|
129
|
+
}
|
|
130
|
+
const ownershipRules = rawRules.map((rule, idx) => {
|
|
131
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) {
|
|
132
|
+
throw new Error(`scaffold.yaml ownership_rules[${idx}] must be a mapping.`);
|
|
133
|
+
}
|
|
134
|
+
const pattern = String(rule.pattern || "").trim();
|
|
135
|
+
if (!pattern) {
|
|
136
|
+
throw new Error(`scaffold.yaml ownership_rules[${idx}].pattern is required.`);
|
|
137
|
+
}
|
|
138
|
+
const persona = assertKnownPersona(rule.persona);
|
|
139
|
+
return { pattern, persona };
|
|
140
|
+
});
|
|
141
|
+
return { ownershipRules };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function loadScaffoldConfig({
|
|
145
|
+
targetPath = process.cwd(),
|
|
146
|
+
relativePath = SCAFFOLD_RELATIVE_PATH,
|
|
147
|
+
} = {}) {
|
|
148
|
+
const absolutePath = path.join(
|
|
149
|
+
path.resolve(String(targetPath || ".")),
|
|
150
|
+
String(relativePath || SCAFFOLD_RELATIVE_PATH)
|
|
151
|
+
);
|
|
152
|
+
try {
|
|
153
|
+
const raw = await fsp.readFile(absolutePath, "utf-8");
|
|
154
|
+
return { found: true, path: absolutePath, ...parseScaffoldYaml(raw) };
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err && typeof err === "object" && err.code === "ENOENT") {
|
|
157
|
+
return { found: false, path: absolutePath, ownershipRules: [] };
|
|
158
|
+
}
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Heuristic routing ---------------------------------------------------
|
|
164
|
+
|
|
165
|
+
// The heuristic table is explicit rather than a big switch: earlier entries
|
|
166
|
+
// are more specific, later entries are broader catch-alls. We iterate in
|
|
167
|
+
// order and take the first match so "docs/api.md" sorts as documentation
|
|
168
|
+
// rather than getting routed to backend by the ".md" extension catch-all.
|
|
169
|
+
const HEURISTIC_RULES = [
|
|
170
|
+
{
|
|
171
|
+
persona: "testing",
|
|
172
|
+
match: (p) =>
|
|
173
|
+
/(^|\/)(tests?|__tests__|specs?)\//.test(p) ||
|
|
174
|
+
/\.(test|spec)\.(js|jsx|ts|tsx|mjs|cjs|py|rb|go|rs)$/.test(p),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
persona: "documentation",
|
|
178
|
+
match: (p) =>
|
|
179
|
+
/(^|\/)docs?\//.test(p) ||
|
|
180
|
+
/(^|\/)(README|CHANGELOG|CONTRIBUTING|ADR)(\.md)?$/i.test(p) ||
|
|
181
|
+
/(^|\/)adr[-_]/i.test(p),
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
persona: "supply-chain",
|
|
185
|
+
match: (p) =>
|
|
186
|
+
/(^|\/)package(-lock)?\.json$/.test(p) ||
|
|
187
|
+
/(^|\/)yarn\.lock$/.test(p) ||
|
|
188
|
+
/(^|\/)pnpm-lock\.yaml$/.test(p) ||
|
|
189
|
+
/(^|\/)requirements([-.]\w+)?\.txt$/.test(p) ||
|
|
190
|
+
/(^|\/)pyproject\.toml$/.test(p) ||
|
|
191
|
+
/(^|\/)Pipfile(\.lock)?$/.test(p) ||
|
|
192
|
+
/(^|\/)Gemfile(\.lock)?$/.test(p) ||
|
|
193
|
+
/(^|\/)go\.(mod|sum)$/.test(p) ||
|
|
194
|
+
/(^|\/)cargo\.toml$/i.test(p) ||
|
|
195
|
+
/(^|\/)renovate\.json$/.test(p),
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
persona: "release",
|
|
199
|
+
match: (p) =>
|
|
200
|
+
/(^|\/)\.github\/workflows\//.test(p) ||
|
|
201
|
+
/(^|\/)release-please/.test(p) ||
|
|
202
|
+
/(^|\/)action\.yml$/.test(p) ||
|
|
203
|
+
/(^|\/)\.releaserc/.test(p) ||
|
|
204
|
+
/(^|\/)(scripts|bin)\/release/.test(p),
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
persona: "infrastructure",
|
|
208
|
+
match: (p) =>
|
|
209
|
+
/(^|\/)(infra|terraform|k8s|kubernetes|helm)\//.test(p) ||
|
|
210
|
+
/\.(tf|tfvars|hcl)$/.test(p) ||
|
|
211
|
+
/(^|\/)Dockerfile(\.\w+)?$/.test(p) ||
|
|
212
|
+
/(^|\/)docker-compose(\.[-\w]+)?\.ya?ml$/.test(p) ||
|
|
213
|
+
/(^|\/)\.dockerignore$/.test(p),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
persona: "observability",
|
|
217
|
+
match: (p) =>
|
|
218
|
+
/(^|\/)(observability|telemetry|metrics|tracing|logging|monitoring)\//i.test(p) ||
|
|
219
|
+
/(^|\/)sentry\.(client|server)\./.test(p),
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
persona: "ai-governance",
|
|
223
|
+
match: (p) =>
|
|
224
|
+
/(^|\/)(prompts?|llm|ai|agents?)\//i.test(p) ||
|
|
225
|
+
/(^|\/)prompt[-_]/.test(p) ||
|
|
226
|
+
/\.prompt(\.md)?$/.test(p),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
persona: "data-layer",
|
|
230
|
+
match: (p) =>
|
|
231
|
+
/(^|\/)(migrations?|alembic|prisma|db|database|schema)\//i.test(p) ||
|
|
232
|
+
/\.sql$/.test(p) ||
|
|
233
|
+
/(^|\/)models?\//i.test(p),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
persona: "security",
|
|
237
|
+
match: (p) =>
|
|
238
|
+
/(^|\/)(auth|authn|authz|security)\//i.test(p) ||
|
|
239
|
+
/(^|\/)(middleware|guards?)\/(auth|security)/i.test(p) ||
|
|
240
|
+
/(^|\/)\.env(\.\w+)?$/.test(p),
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
persona: "frontend",
|
|
244
|
+
match: (p) =>
|
|
245
|
+
/(^|\/)(components?|pages?|app|views?|ui|styles?)\//i.test(p) ||
|
|
246
|
+
/\.(tsx|jsx|vue|svelte|css|scss|sass)$/.test(p),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
persona: "reliability",
|
|
250
|
+
match: (p) =>
|
|
251
|
+
/(^|\/)(health|liveness|readiness|circuit[-_]?breaker)\//i.test(p) ||
|
|
252
|
+
/(^|\/)retries?\//i.test(p),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
persona: "code-quality",
|
|
256
|
+
match: (p) =>
|
|
257
|
+
/(^|\/)\.?(eslintrc|prettierrc|biome|stylelintrc)(\.[-\w]+)?$/.test(p) ||
|
|
258
|
+
/(^|\/)\.editorconfig$/.test(p),
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
persona: "backend",
|
|
262
|
+
match: (p) =>
|
|
263
|
+
/(^|\/)(api|server|backend|routes?|services?|handlers?|controllers?)\//i.test(
|
|
264
|
+
p
|
|
265
|
+
) ||
|
|
266
|
+
/\.(py|rb|go|rs)$/.test(p) ||
|
|
267
|
+
/\.(ts|js|mts|mjs|cts|cjs)$/.test(p),
|
|
268
|
+
},
|
|
269
|
+
];
|
|
270
|
+
|
|
271
|
+
export function routeFileHeuristic(
|
|
272
|
+
filePath,
|
|
273
|
+
{ fallback = DEFAULT_HEURISTIC_FALLBACK } = {}
|
|
274
|
+
) {
|
|
275
|
+
const normalized = normalizePathForMatch(filePath);
|
|
276
|
+
if (!normalized) {
|
|
277
|
+
return fallback;
|
|
278
|
+
}
|
|
279
|
+
for (const rule of HEURISTIC_RULES) {
|
|
280
|
+
if (rule.match(normalized)) {
|
|
281
|
+
return rule.persona;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return fallback;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// --- Public API ---------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
// Given the file list plus (optional) scaffold config, produce a Map of
|
|
290
|
+
// posix-style relative path → persona id. Rules are last-match-wins: the
|
|
291
|
+
// scaffold ordering lets authors put a broad default first, then override
|
|
292
|
+
// subtrees below.
|
|
293
|
+
export function buildOwnershipMap(files, scaffoldConfig = null) {
|
|
294
|
+
const rules = Array.isArray(scaffoldConfig?.ownershipRules)
|
|
295
|
+
? scaffoldConfig.ownershipRules
|
|
296
|
+
: [];
|
|
297
|
+
const map = new Map();
|
|
298
|
+
const fileList = Array.isArray(files) ? files : [];
|
|
299
|
+
for (const rawFile of fileList) {
|
|
300
|
+
const file = normalizePathForMatch(rawFile);
|
|
301
|
+
if (!file) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (rules.length > 0) {
|
|
305
|
+
let owner = null;
|
|
306
|
+
for (const rule of rules) {
|
|
307
|
+
if (matchGlob(rule.pattern, file)) {
|
|
308
|
+
owner = rule.persona;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (owner) {
|
|
312
|
+
map.set(file, owner);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
map.set(file, routeFileHeuristic(file));
|
|
317
|
+
}
|
|
318
|
+
return map;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Bin findings by persona using the ownership map. Findings whose file is
|
|
322
|
+
// not in the map (e.g. a scanner reported on a path outside the ingest)
|
|
323
|
+
// fall back to the heuristic router so they don't get silently dropped.
|
|
324
|
+
export function routeFindingsToPersonas(findings, ownershipMap) {
|
|
325
|
+
const source = Array.isArray(findings) ? findings : [];
|
|
326
|
+
const map = ownershipMap instanceof Map ? ownershipMap : new Map();
|
|
327
|
+
const perPersona = {};
|
|
328
|
+
for (const finding of source) {
|
|
329
|
+
if (!finding || typeof finding !== "object") {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const filePath = normalizePathForMatch(
|
|
333
|
+
finding.file || finding.path || finding.location || ""
|
|
334
|
+
);
|
|
335
|
+
let persona = normalizePersonaId(map.get(filePath) || "");
|
|
336
|
+
if (!persona) {
|
|
337
|
+
persona = routeFileHeuristic(filePath);
|
|
338
|
+
}
|
|
339
|
+
if (!perPersona[persona]) {
|
|
340
|
+
perPersona[persona] = [];
|
|
341
|
+
}
|
|
342
|
+
perPersona[persona].push(finding);
|
|
343
|
+
}
|
|
344
|
+
return perPersona;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Lightweight metric for the spec's ≥40% token-reduction target. Given an
|
|
348
|
+
// ownership map + pre-routing cost assumption (every persona sees every
|
|
349
|
+
// file), report how many files each persona would actually need to scan.
|
|
350
|
+
export function computeRoutingStats(ownershipMap) {
|
|
351
|
+
const map = ownershipMap instanceof Map ? ownershipMap : new Map();
|
|
352
|
+
const totalFiles = map.size;
|
|
353
|
+
if (totalFiles === 0) {
|
|
354
|
+
return {
|
|
355
|
+
totalFiles: 0,
|
|
356
|
+
personaCoverage: {},
|
|
357
|
+
totalScansUnrouted: 0,
|
|
358
|
+
totalScansRouted: 0,
|
|
359
|
+
tokenReductionEstimatePct: 0,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const personaCoverage = {};
|
|
363
|
+
for (const persona of map.values()) {
|
|
364
|
+
personaCoverage[persona] = (personaCoverage[persona] || 0) + 1;
|
|
365
|
+
}
|
|
366
|
+
const totalScansUnrouted = totalFiles * PERSONA_IDS.length;
|
|
367
|
+
const totalScansRouted = totalFiles; // 1 persona per file with last-match-wins routing
|
|
368
|
+
const tokenReductionEstimatePct = Math.round(
|
|
369
|
+
(1 - totalScansRouted / totalScansUnrouted) * 100
|
|
370
|
+
);
|
|
371
|
+
return {
|
|
372
|
+
totalFiles,
|
|
373
|
+
personaCoverage,
|
|
374
|
+
totalScansUnrouted,
|
|
375
|
+
totalScansRouted,
|
|
376
|
+
tokenReductionEstimatePct,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export { DEFAULT_HEURISTIC_FALLBACK, SCAFFOLD_RELATIVE_PATH };
|
package/src/legacy-cli.js
CHANGED
|
@@ -206,12 +206,23 @@ function printUsage() {
|
|
|
206
206
|
console.log("");
|
|
207
207
|
console.log("Session Coordination:");
|
|
208
208
|
console.log(" sl session start --json Create an agent coordination session");
|
|
209
|
+
console.log(" sl session start --template code-review Start from quick-start preset + launch plan");
|
|
210
|
+
console.log(" sl session templates --json List available session quick-start templates");
|
|
209
211
|
console.log(" sl session join <id> --name <n> Join a session as an agent");
|
|
210
212
|
console.log(" sl session say <id> \"msg\" --json Append a message event to session stream");
|
|
213
|
+
console.log(" sl session say <id> \"lock: <file> - <intent>\" Request an exclusive file lock via Senti");
|
|
214
|
+
console.log(" sl session say <id> \"assign: @agent <task>\" Create task assignment + lease");
|
|
215
|
+
console.log(" sl session say <id> \"assign: @*:reviewer <task>\" Wildcard route to least-busy role");
|
|
216
|
+
console.log(" sl session say <id> \"accepted: task <task-id>\" / \"done: task <task-id>\" Task transitions");
|
|
211
217
|
console.log(" sl session read <id> --tail 20 Read session stream events");
|
|
212
218
|
console.log(" sl session status <id> --json Show session health, agents, runs, leases");
|
|
213
219
|
console.log(" sl session leave <id> Leave a session");
|
|
214
220
|
console.log(" sl session list --json List active sessions");
|
|
221
|
+
console.log(" sl session setup-guides <id> --json Upsert AGENTS.md/CLAUDE.md coordination section");
|
|
222
|
+
console.log(" sl session inject-guide <id> --json Inject section into existing AGENTS.md/CLAUDE.md files");
|
|
223
|
+
console.log(" sl session provision-emails <id> --count 5 Provision AIdenID emails for swarm testing");
|
|
224
|
+
console.log(" sl session admin-kill <id> --reason <reason> Admin kill one remote session");
|
|
225
|
+
console.log(" sl session admin-kill-all --confirm Admin kill ALL remote sessions");
|
|
215
226
|
console.log(" sl session kill --session <id> --agent <id> Kill agent + revoke active leases");
|
|
216
227
|
console.log("");
|
|
217
228
|
console.log("Security & Review:");
|
|
@@ -1099,8 +1110,54 @@ function buildOmarTerminalHandler({ startedAt = Date.now() } = {}) {
|
|
|
1099
1110
|
async function runLocalOmarGateCommand(args) {
|
|
1100
1111
|
const commandStartedAt = Date.now();
|
|
1101
1112
|
const mode = String(args[0] || "").trim().toLowerCase();
|
|
1113
|
+
if (mode === "investor-dd") {
|
|
1114
|
+
const pathArg = getCommandOptionValue(args, "--path") || ".";
|
|
1115
|
+
const outputDirArg = getCommandOptionValue(args, "--output-dir") || "";
|
|
1116
|
+
const asJson = hasCommandOption(args, "--json");
|
|
1117
|
+
const dryRun = hasCommandOption(args, "--dry-run");
|
|
1118
|
+
const maxCostUsd = parseFloat(getCommandOptionValue(args, "--max-cost") || "25.0") || 25.0;
|
|
1119
|
+
const maxRuntimeMinutes =
|
|
1120
|
+
parseInt(getCommandOptionValue(args, "--max-runtime-minutes") || "45", 10) || 45;
|
|
1121
|
+
const maxParallel =
|
|
1122
|
+
parseInt(getCommandOptionValue(args, "--max-parallel") || "3", 10) || 3;
|
|
1123
|
+
const streamEnabled = hasCommandOption(args, "--stream");
|
|
1124
|
+
|
|
1125
|
+
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
1126
|
+
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1127
|
+
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
const { runInvestorDd } = await import("./review/investor-dd-orchestrator.js");
|
|
1131
|
+
if (!asJson) {
|
|
1132
|
+
printSection("Investor-DD Audit");
|
|
1133
|
+
printInfo(`Target: ${targetPath}`);
|
|
1134
|
+
printInfo(
|
|
1135
|
+
`Budget: $${maxCostUsd.toFixed(2)} / ${maxRuntimeMinutes}min / ${maxParallel} parallel`,
|
|
1136
|
+
);
|
|
1137
|
+
if (dryRun) printInfo("Mode: dry-run (plan + stub report only)");
|
|
1138
|
+
}
|
|
1139
|
+
const result = await runInvestorDd({
|
|
1140
|
+
rootPath: targetPath,
|
|
1141
|
+
outputDir: outputDirArg,
|
|
1142
|
+
budgetOptions: { maxCostUsd, maxRuntimeMinutes, maxParallel },
|
|
1143
|
+
dryRun,
|
|
1144
|
+
onEvent: streamEnabled
|
|
1145
|
+
? (event) => process.stdout.write(`${JSON.stringify(event)}\n`)
|
|
1146
|
+
: () => {},
|
|
1147
|
+
});
|
|
1148
|
+
if (asJson) {
|
|
1149
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1150
|
+
} else {
|
|
1151
|
+
printInfo(`Report: ${path.join(result.artifactDir, "report.md")}`);
|
|
1152
|
+
printInfo(`Artifacts: ${result.artifactDir}`);
|
|
1153
|
+
printInfo(`Status: ${result.summary.terminationReason}`);
|
|
1154
|
+
printInfo(`Findings: ${result.summary.totalFindings}`);
|
|
1155
|
+
printInfo(`Elapsed: ${result.summary.durationSeconds.toFixed(1)}s`);
|
|
1156
|
+
}
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1102
1159
|
if (mode && mode !== "deep") {
|
|
1103
|
-
throw new Error(`Unsupported /omargate mode '${mode}'. Use: /omargate deep`);
|
|
1160
|
+
throw new Error(`Unsupported /omargate mode '${mode}'. Use: /omargate deep | /omargate investor-dd`);
|
|
1104
1161
|
}
|
|
1105
1162
|
const asJson = hasCommandOption(args, "--json");
|
|
1106
1163
|
const pathArg = getCommandOptionValue(args, "--path") || ".";
|
|
@@ -1113,6 +1170,14 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1113
1170
|
const scanMode = getCommandOptionValue(args, "--scan-mode") || "deep";
|
|
1114
1171
|
const maxParallel = parseInt(getCommandOptionValue(args, "--max-parallel") || "4", 10) || 4;
|
|
1115
1172
|
const streamEnabled = hasCommandOption(args, "--stream");
|
|
1173
|
+
// Per-persona filter flags (A-CLI-1). --persona <csv> narrows the dispatch
|
|
1174
|
+
// roster to the listed IDs; --skip-persona <csv> removes listed IDs from
|
|
1175
|
+
// whatever the mode's baseline roster is. Both can be combined.
|
|
1176
|
+
const personaCsvFlag = getCommandOptionValue(args, "--persona") || "";
|
|
1177
|
+
const skipPersonaCsvFlag = getCommandOptionValue(args, "--skip-persona") || "";
|
|
1178
|
+
const { parsePersonaCsv } = await import("./review/scan-modes.js");
|
|
1179
|
+
const includeOnly = parsePersonaCsv(personaCsvFlag);
|
|
1180
|
+
const skipPersonas = parsePersonaCsv(skipPersonaCsvFlag);
|
|
1116
1181
|
const targetPath = path.resolve(process.cwd(), pathArg);
|
|
1117
1182
|
if (!fs.existsSync(targetPath) || !fs.statSync(targetPath).isDirectory()) {
|
|
1118
1183
|
throw new Error(`Invalid --path target: ${targetPath}`);
|
|
@@ -1175,6 +1240,8 @@ async function runLocalOmarGateCommand(args) {
|
|
|
1175
1240
|
metadata: deterministic.metadata || {},
|
|
1176
1241
|
},
|
|
1177
1242
|
onEvent: streamHandler,
|
|
1243
|
+
includeOnly: includeOnly.length > 0 ? includeOnly : null,
|
|
1244
|
+
skipPersonas: skipPersonas.length > 0 ? skipPersonas : null,
|
|
1178
1245
|
});
|
|
1179
1246
|
|
|
1180
1247
|
// Use orchestrator results as the AI layer. aiResult represents ONLY
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dr. Kai Chen — Global Orchestrator (a.k.a. "Senti" / Telegram coordinator).
|
|
3
|
+
*
|
|
4
|
+
* Kai Chen is NOT a review persona; he is the orchestration tier that sits
|
|
5
|
+
* above the 13 domain reviewers (Nina, Maya, Ethan, Priya, Linh, Jules,
|
|
6
|
+
* Samir, Noah, Omar, Sofia, Kat, Nora, Amina). He picks which personas run,
|
|
7
|
+
* routes high-signal findings up to the user, and emits the final report.
|
|
8
|
+
*
|
|
9
|
+
* Background (Carter's canon):
|
|
10
|
+
* - Ex-Google Staff; Chrome V8 performance lead
|
|
11
|
+
* - Bias: performance budgets, operational simplicity, correctness over
|
|
12
|
+
* cleverness
|
|
13
|
+
* - Tone: crisp, evidence-first; hates vague claims; demands reproduction
|
|
14
|
+
* steps
|
|
15
|
+
* - Output signature: "Here's what breaks, where, why, and what to do next."
|
|
16
|
+
*
|
|
17
|
+
* Model routing:
|
|
18
|
+
* - Primary: Opus 4.6 (reasoning-heavy orchestration; called ~1-3 times
|
|
19
|
+
* per scan or build)
|
|
20
|
+
* - NEVER OpenAI gpt-5.3-codex (code-gen workers only)
|
|
21
|
+
* - NEVER Gemini (dropped from provider fallback order)
|
|
22
|
+
*
|
|
23
|
+
* This module exports the orchestrator DEFINITION + a prompt-assembly helper.
|
|
24
|
+
* Wiring Kai into actual review/build flows happens in subsequent PRs (the
|
|
25
|
+
* gate dispatcher, the Telegram entry-point, and the build-pathway planner
|
|
26
|
+
* all consume this definition).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const KAI_CHEN_BIAS = Object.freeze([
|
|
30
|
+
"performance budgets over premature optimization",
|
|
31
|
+
"operational simplicity over cleverness",
|
|
32
|
+
"correctness over features",
|
|
33
|
+
"evidence over vague claims",
|
|
34
|
+
"reproduction steps for every issue",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
const KAI_CHEN_TONE_RULES = Object.freeze([
|
|
38
|
+
"crisp sentences; no hedging",
|
|
39
|
+
"evidence-first; cite file:line or metric name on every claim",
|
|
40
|
+
"demand reproduction steps before accepting any finding as actionable",
|
|
41
|
+
"reject reviewer output that is vague, speculative, or missing coverage proof",
|
|
42
|
+
"call out 'looks fine' conclusions that aren't backed by enumerated checklist coverage",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const KAI_CHEN_OUTPUT_SIGNATURE = "Here's what breaks, where, why, and what to do next.";
|
|
46
|
+
|
|
47
|
+
const KAI_CHEN_SYSTEM_PROMPT = [
|
|
48
|
+
"You are Dr. Kai Chen, global orchestrator for the Sentinelayer review platform.",
|
|
49
|
+
"",
|
|
50
|
+
"Your job is NOT to review code directly. Your job is to:",
|
|
51
|
+
" 1. Pick which specialist personas should run against this target and why.",
|
|
52
|
+
" 2. Receive the specialists' findings + coverage enumerations.",
|
|
53
|
+
" 3. Deduplicate across personas (same file:line across domains boosts confidence, not noise).",
|
|
54
|
+
" 4. Rank by severity × confidence × blast radius.",
|
|
55
|
+
" 5. Emit a single consolidated report using your output signature.",
|
|
56
|
+
"",
|
|
57
|
+
"Non-negotiables:",
|
|
58
|
+
" - Every finding you surface to the user MUST have an enumerated reproduction path.",
|
|
59
|
+
" - If a specialist returned zero findings, the specialist MUST have enumerated their checklist coverage; if they did not, you reject their output and re-dispatch.",
|
|
60
|
+
" - You do not pad reports with speculative or 'theoretical' concerns. Cut them at the orchestrator tier.",
|
|
61
|
+
" - You are a performance-focused reviewer by training. Favor operational simplicity over cleverness in your recommendations.",
|
|
62
|
+
"",
|
|
63
|
+
"Output signature (end every summary with this exact phrasing, populated):",
|
|
64
|
+
` "${KAI_CHEN_OUTPUT_SIGNATURE}"`,
|
|
65
|
+
"",
|
|
66
|
+
"Your tone rules:",
|
|
67
|
+
...KAI_CHEN_TONE_RULES.map((rule) => ` - ${rule}`),
|
|
68
|
+
].join("\n");
|
|
69
|
+
|
|
70
|
+
export const ORCHESTRATOR_DEFINITION = Object.freeze({
|
|
71
|
+
id: "orchestrator-kai-chen",
|
|
72
|
+
name: "Dr. Kai Chen",
|
|
73
|
+
shortName: "Kai",
|
|
74
|
+
role: "Global Orchestrator / Senti",
|
|
75
|
+
background: "Ex-Google Staff; Chrome V8 performance lead",
|
|
76
|
+
model: "claude-opus-4-6",
|
|
77
|
+
modelProvider: "anthropic",
|
|
78
|
+
bias: KAI_CHEN_BIAS,
|
|
79
|
+
toneRules: KAI_CHEN_TONE_RULES,
|
|
80
|
+
outputSignature: KAI_CHEN_OUTPUT_SIGNATURE,
|
|
81
|
+
systemPrompt: KAI_CHEN_SYSTEM_PROMPT,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build a context-enriched orchestrator prompt for a specific scan/build run.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} [options]
|
|
88
|
+
* @param {string} [options.targetPath] - Repository path under review.
|
|
89
|
+
* @param {string} [options.mode] - e.g. "baseline" | "deep" | "full-depth" | "build".
|
|
90
|
+
* @param {string[]} [options.dispatchedPersonas] - Persona IDs dispatched for this run.
|
|
91
|
+
* @param {object} [options.deterministicSummary] - Pre-LLM deterministic scan summary.
|
|
92
|
+
* @returns {string} Assembled orchestrator system prompt.
|
|
93
|
+
*/
|
|
94
|
+
export function buildOrchestratorPrompt({
|
|
95
|
+
targetPath = "",
|
|
96
|
+
mode = "deep",
|
|
97
|
+
dispatchedPersonas = [],
|
|
98
|
+
deterministicSummary = {},
|
|
99
|
+
} = {}) {
|
|
100
|
+
const personaList = dispatchedPersonas.length > 0
|
|
101
|
+
? dispatchedPersonas.map((id) => ` - ${id}`).join("\n")
|
|
102
|
+
: " (none specified)";
|
|
103
|
+
|
|
104
|
+
const detSummary = [
|
|
105
|
+
`P0=${deterministicSummary.P0 || 0}`,
|
|
106
|
+
`P1=${deterministicSummary.P1 || 0}`,
|
|
107
|
+
`P2=${deterministicSummary.P2 || 0}`,
|
|
108
|
+
`P3=${deterministicSummary.P3 || 0}`,
|
|
109
|
+
].join(" ");
|
|
110
|
+
|
|
111
|
+
return [
|
|
112
|
+
ORCHESTRATOR_DEFINITION.systemPrompt,
|
|
113
|
+
"",
|
|
114
|
+
"## Run context",
|
|
115
|
+
`Target: ${targetPath || "(not provided)"}`,
|
|
116
|
+
`Mode: ${mode}`,
|
|
117
|
+
`Deterministic-scan summary (already surfaced, do NOT re-report): ${detSummary}`,
|
|
118
|
+
"",
|
|
119
|
+
"## Specialists dispatched for this run",
|
|
120
|
+
personaList,
|
|
121
|
+
"",
|
|
122
|
+
"Begin.",
|
|
123
|
+
].join("\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const KAI_CHEN_OUTPUT_SIGNATURE_VALUE = KAI_CHEN_OUTPUT_SIGNATURE;
|
package/src/review/ai-review.js
CHANGED
|
@@ -10,6 +10,7 @@ import { loadConfig } from "../config/service.js";
|
|
|
10
10
|
import { evaluateBudget } from "../cost/budget.js";
|
|
11
11
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
12
12
|
import { estimateModelCost } from "../cost/tracker.js";
|
|
13
|
+
import { estimateTokens } from "../cost/tokenizer.js";
|
|
13
14
|
import { appendRunEvent, deriveStopClassFromBudget } from "../telemetry/ledger.js";
|
|
14
15
|
|
|
15
16
|
const AI_SEVERITIES = new Set(["P0", "P1", "P2", "P3"]);
|
|
@@ -36,14 +37,6 @@ function parsePercent(rawValue, field) {
|
|
|
36
37
|
return normalized;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
function estimateTokenCount(text) {
|
|
40
|
-
const normalized = String(text || "");
|
|
41
|
-
if (!normalized) {
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
return Math.max(1, Math.ceil(normalized.length / 4));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
40
|
function resolveConfiguredApiKey(provider, resolvedConfig = {}) {
|
|
48
41
|
const normalizedProvider = normalizeString(provider).toLowerCase();
|
|
49
42
|
if (normalizedProvider === "openai") {
|
|
@@ -479,8 +472,8 @@ export async function runAiReviewLayer({
|
|
|
479
472
|
};
|
|
480
473
|
combinedSummary.blocking = combinedSummary.P0 > 0 || combinedSummary.P1 > 0;
|
|
481
474
|
|
|
482
|
-
const inputTokens =
|
|
483
|
-
const outputTokens =
|
|
475
|
+
const inputTokens = estimateTokens(prompt, { model: resolvedModel });
|
|
476
|
+
const outputTokens = estimateTokens(responseText, { model: resolvedModel });
|
|
484
477
|
const modelCost = maybeEstimateModelCost({
|
|
485
478
|
modelId: resolvedModel,
|
|
486
479
|
inputTokens,
|