segreant 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/LICENSING.md +5 -3
  2. package/README.md +57 -19
  3. package/bin/segreant-statusline.mjs +20 -0
  4. package/bin/segreant.mjs +19 -8
  5. package/dist/alerts/detect.js +13 -0
  6. package/dist/billing/epistemic.js +4 -0
  7. package/dist/billing/importer.js +29 -10
  8. package/dist/billing/reconcile.js +122 -32
  9. package/dist/billing/scope.js +7 -0
  10. package/dist/budget/guard.js +33 -4
  11. package/dist/cli/allocCmd.js +73 -4
  12. package/dist/cli/askModelPrices.js +59 -0
  13. package/dist/cli/backupCmd.js +2 -1
  14. package/dist/cli/basisLines.js +45 -0
  15. package/dist/cli/billingCmd.js +68 -9
  16. package/dist/cli/capitalCmd.js +2 -1
  17. package/dist/cli/connectCmd.js +50 -9
  18. package/dist/cli/egressCmd.js +15 -3
  19. package/dist/cli/evidenceCmd.js +6 -3
  20. package/dist/cli/flags.js +68 -0
  21. package/dist/cli/homeCmd.js +117 -0
  22. package/dist/cli/importCmd.js +210 -54
  23. package/dist/cli/launchCmd.js +2 -1
  24. package/dist/cli/opsCmd.js +49 -2
  25. package/dist/cli/planCmd.js +181 -0
  26. package/dist/cli/pricingDeclareCmd.js +178 -0
  27. package/dist/cli/quotaCmd.js +190 -0
  28. package/dist/cli/runCmd.js +203 -51
  29. package/dist/cli/showCmd.js +37 -11
  30. package/dist/cli/teamCmd.js +5 -5
  31. package/dist/cli/ui.js +24 -8
  32. package/dist/cli/valueCmd.js +189 -14
  33. package/dist/cli.js +102 -21
  34. package/dist/config.js +95 -6
  35. package/dist/connect/antigravity.js +310 -0
  36. package/dist/connect/claudeCode.js +43 -3
  37. package/dist/connect/codex.js +38 -1
  38. package/dist/connect/importShared.js +58 -5
  39. package/dist/connect/opencode.js +37 -9
  40. package/dist/connect/protobuf.js +126 -0
  41. package/dist/connect/registry.js +46 -0
  42. package/dist/connect/repoRoot.js +112 -0
  43. package/dist/cost/basis.js +168 -0
  44. package/dist/cost/declarations.js +37 -0
  45. package/dist/cost/exactPricing.js +4 -1
  46. package/dist/cost/firstSight.js +98 -0
  47. package/dist/cost/overrides.js +230 -0
  48. package/dist/cost/pricing.js +228 -15
  49. package/dist/cost/reprice.js +130 -0
  50. package/dist/dashboard/claim-support.js +11 -8
  51. package/dist/dashboard/contracts.js +23 -3
  52. package/dist/dashboard/routes.js +980 -112
  53. package/dist/dashboard/sessions.js +345 -0
  54. package/dist/dashboard/settings.js +2 -1
  55. package/dist/dashboard/value-cache.js +105 -0
  56. package/dist/dashboard/web/app/components/claimInspector.js +6 -5
  57. package/dist/dashboard/web/app/components/drawer.js +105 -35
  58. package/dist/dashboard/web/app/components/ruleFields.js +144 -0
  59. package/dist/dashboard/web/app/components/spine.js +54 -24
  60. package/dist/dashboard/web/app/components/workflow.js +82 -0
  61. package/dist/dashboard/web/app/core/actions.js +734 -23
  62. package/dist/dashboard/web/app/core/api.js +37 -1
  63. package/dist/dashboard/web/app/core/chain.js +11 -4
  64. package/dist/dashboard/web/app/core/claimLayers.js +179 -51
  65. package/dist/dashboard/web/app/core/claimTypes.js +30 -0
  66. package/dist/dashboard/web/app/core/fmt.js +19 -2
  67. package/dist/dashboard/web/app/core/generated-contract.js +23 -3
  68. package/dist/dashboard/web/app/core/generated-payload-contract.js +1944 -465
  69. package/dist/dashboard/web/app/core/generated-types.js +1 -1
  70. package/dist/dashboard/web/app/core/griffin.js +10 -0
  71. package/dist/dashboard/web/app/core/jobs.js +12 -0
  72. package/dist/dashboard/web/app/core/period.js +35 -0
  73. package/dist/dashboard/web/app/core/periodWords.js +10 -0
  74. package/dist/dashboard/web/app/core/periods.js +32 -0
  75. package/dist/dashboard/web/app/core/pricingActions.js +264 -0
  76. package/dist/dashboard/web/app/core/privacyText.js +78 -0
  77. package/dist/dashboard/web/app/core/reconcileWords.js +63 -0
  78. package/dist/dashboard/web/app/core/registry.js +79 -46
  79. package/dist/dashboard/web/app/core/slug.js +9 -0
  80. package/dist/dashboard/web/app/core/theme.js +48 -0
  81. package/dist/dashboard/web/app/core/view.js +69 -0
  82. package/dist/dashboard/web/app/main.js +197 -15
  83. package/dist/dashboard/web/app/views/allocation.js +249 -61
  84. package/dist/dashboard/web/app/views/control.js +57 -16
  85. package/dist/dashboard/web/app/views/data.js +4 -4
  86. package/dist/dashboard/web/app/views/evidence.js +336 -87
  87. package/dist/dashboard/web/app/views/privacy.js +13 -0
  88. package/dist/dashboard/web/app/views/spend.js +30 -15
  89. package/dist/dashboard/web/app/views/statement.js +189 -0
  90. package/dist/dashboard/web/app/views/system.js +44 -4
  91. package/dist/dashboard/web/app/views/value.js +105 -59
  92. package/dist/dashboard/web/app/views/workbench.js +312 -0
  93. package/dist/dashboard/web/classic.html +16 -4
  94. package/dist/dashboard/web/index.html +1 -1
  95. package/dist/dashboard/web/styles/app.css +536 -23
  96. package/dist/demo/seed.js +153 -14
  97. package/dist/economics/ledger.js +1 -1
  98. package/dist/egress/policy.js +129 -5
  99. package/dist/export/allocationCsv.js +70 -0
  100. package/dist/export/billingCsv.js +6 -2
  101. package/dist/export/csv.js +10 -2
  102. package/dist/export/economic.js +6 -2
  103. package/dist/export/statementCsv.js +104 -0
  104. package/dist/git/correlate.js +86 -9
  105. package/dist/git/quality.js +107 -24
  106. package/dist/git/repoScope.js +207 -0
  107. package/dist/git/survivalStore.js +186 -0
  108. package/dist/guide.js +9 -4
  109. package/dist/plans/detect.js +147 -0
  110. package/dist/proxy/server.js +134 -14
  111. package/dist/proxy/usage.js +9 -4
  112. package/dist/quota/limits.js +138 -0
  113. package/dist/quota/statusline.js +68 -0
  114. package/dist/quota/view.js +122 -0
  115. package/dist/scan/knownApps.js +6 -2
  116. package/dist/scan/scan.js +9 -0
  117. package/dist/store/allocation.js +14 -0
  118. package/dist/store/db.js +259 -34
  119. package/dist/store/realization.js +78 -14
  120. package/dist/store/schema.js +59 -1
  121. package/dist/store/scopeEvidence.js +67 -0
  122. package/dist/util/pool.js +19 -0
  123. package/dist/util/time.js +7 -0
  124. package/dist/value/realization.js +321 -30
  125. package/dist/value/report.js +28 -8
  126. package/dist/value/selfReported.js +10 -9
  127. package/docs/ARCHITECTURE.md +8 -4
  128. package/docs/CAPABILITY-EVIDENCE-CONTRACT.md +33 -11
  129. package/docs/CLI-JSON-CONTRACT.md +38 -0
  130. package/docs/DATA-BOUNDARIES.md +66 -6
  131. package/docs/FAQ.md +43 -9
  132. package/docs/GETTING-STARTED.md +19 -12
  133. package/docs/GUIDE.md +81 -18
  134. package/docs/INTEGRATIONS.md +9 -9
  135. package/docs/METHODOLOGY.md +3 -2
  136. package/docs/NEUTRALITY.md +1 -1
  137. package/docs/PRIVACY.md +51 -0
  138. package/docs/README.md +1 -0
  139. package/docs/RELEASE-GATE.md +52 -7
  140. package/docs/THREAT-MODEL.md +2 -1
  141. package/package.json +3 -2
  142. package/pricing/models.json +1366 -60
@@ -7,14 +7,17 @@
7
7
  * a local rate-card estimate is legitimate and allocating one while implying it
8
8
  * is settled cost is not.
9
9
  */
10
- import { dbPath } from '../config.js';
10
+ import { writeFileSync } from 'node:fs';
11
+ import { dbPath, isDemo } from '../config.js';
12
+ import { allocationRunToCsv } from '../export/allocationCsv.js';
11
13
  import { Store } from '../store/db.js';
12
14
  import { formatUsdMicros } from '../billing/types.js';
13
15
  import { displayUsd } from '../billing/reconcile.js';
14
16
  import { MATCH_FIELDS } from '../alloc/rules.js';
15
17
  import { color, C, printJson } from './ui.js';
18
+ import { recordedTime } from '../util/time.js';
16
19
  function usage() {
17
- console.error(' Usage: segreant alloc <centres|centre|rules|rule|revoke|run> [options]');
20
+ console.error(' Usage: segreant alloc <centres|centre|rules|rule|revoke|run|runs|export> [options]');
18
21
  console.error(' segreant alloc centre <id> --name "<name>" [--owner <who>] add or update a cost centre');
19
22
  console.error(' segreant alloc centre <id> --archive retain it, stop it receiving');
20
23
  console.error(' segreant alloc rule <id> --method <direct|fixed_split|proportional_to_direct> \\');
@@ -24,6 +27,8 @@ function usage() {
24
27
  console.error(' a proportional_to_direct rule derives its split, so its centre takes ratio 0');
25
28
  console.error(' segreant alloc revoke <rule-id>');
26
29
  console.error(' segreant alloc run --from <ISO> --to <ISO> [--apply] [--json]');
30
+ console.error(' segreant alloc runs [--json] recorded splits, newest first');
31
+ console.error(' segreant alloc export <run-id> [--out <file>] one recorded split as CSV');
27
32
  console.error(' Allocation is derived: it never modifies a request row, and never feeds budgets or RoI.');
28
33
  }
29
34
  function parseTs(value, label) {
@@ -81,7 +86,7 @@ function printRun(result, applied, tty) {
81
86
  if (result.byCostCentre.length === 0) {
82
87
  console.log(color(tty, C.gray, lost
83
88
  ? ' No spend that SURVIVES in this period reached a cost centre — rows before '
84
- + `${day(lost.prunedBeforeMs ?? 0)} were deleted by retention, and whether they did cannot be read from here.`
89
+ + `${recordedTime(lost.prunedBeforeMs, true)} were deleted by retention, and whether they did cannot be read from here.`
85
90
  : ' No spend reached a cost centre in this period.'));
86
91
  }
87
92
  else {
@@ -101,7 +106,7 @@ function printRun(result, applied, tty) {
101
106
  // percentage moves for a reason that has nothing to do with the rules --
102
107
  // measured at 76.2% becoming 100.0% on a period 95% of whose spend went.
103
108
  console.log(color(tty, C.yellow, ' Retention deleted request rows from inside this period '
104
- + `(before ${day(lost.prunedBeforeMs ?? 0)}), so the total above is what SURVIVES and the`));
109
+ + `(before ${recordedTime(lost.prunedBeforeMs, true)}), so the total above is what SURVIVES and the`));
105
110
  console.log(color(tty, C.yellow, ' percentages beside it are shares of that, not of what the period cost.'));
106
111
  }
107
112
  if (result.unallocated.length > 0) {
@@ -139,6 +144,70 @@ export function cmdAlloc(flags) {
139
144
  const action = typeof flags._[0] === 'string' ? flags._[0] : 'run';
140
145
  const store = new Store(dbPath());
141
146
  try {
147
+ if (action === 'runs') {
148
+ const runs = store.allocationRuns(50);
149
+ if (flags.json) {
150
+ return void printJson({
151
+ runs: runs.map((r) => ({
152
+ allocationRunId: r.allocationRunId,
153
+ computedAtMs: r.computedAtMs,
154
+ periodStartMs: r.result.periodStartMs,
155
+ periodEndMs: r.result.periodEndMs,
156
+ totalMicros: r.result.totalMicros,
157
+ allocatedMicros: r.result.allocatedMicros,
158
+ unallocatedMicros: r.result.unallocatedMicros,
159
+ })),
160
+ });
161
+ }
162
+ console.log('');
163
+ if (runs.length === 0)
164
+ console.log(color(tty, C.gray, ' No recorded splits. Record one: segreant alloc run --from <ISO> --to <ISO> --apply'));
165
+ for (const r of runs) {
166
+ // A bound that is not midnight UTC keeps its time: printing the date
167
+ // alone made a split ending at 06:48 read as covering the whole day,
168
+ // and re-running those dates gave a different total.
169
+ const bound = (ms) => {
170
+ const iso = new Date(ms).toISOString();
171
+ // Every nonzero part of the time is kept: allocation includes rows by
172
+ // the exact millisecond, so a bound shown to the minute could not be
173
+ // copied back to reproduce the split (PR #142 review).
174
+ if (iso.slice(11, 23) === '00:00:00.000')
175
+ return iso.slice(0, 10);
176
+ const time = iso.slice(17, 23) === '00.000' ? iso.slice(11, 16) : iso.slice(20, 23) === '000' ? iso.slice(11, 19) : iso.slice(11, 23);
177
+ return `${iso.slice(0, 10)} ${time}Z`;
178
+ };
179
+ const period = `${bound(r.result.periodStartMs)} → ${bound(r.result.periodEndMs)} (end exclusive)`;
180
+ const share = r.result.totalMicros > 0 ? `${((r.result.allocatedMicros / r.result.totalMicros) * 100).toFixed(1)}% allocated` : 'nothing to allocate';
181
+ console.log(` ${r.allocationRunId} ${period} ${displayUsd(r.result.totalMicros)} ${color(tty, C.gray, share)}`);
182
+ }
183
+ if (runs.length > 0)
184
+ console.log(color(tty, C.gray, '\n Export one as CSV: segreant alloc export <run-id> --out split.csv'));
185
+ return;
186
+ }
187
+ if (action === 'export') {
188
+ const id = typeof flags._[1] === 'string' ? flags._[1] : '';
189
+ if (id === '') {
190
+ console.error(' alloc export needs the run id: segreant alloc runs lists them.');
191
+ process.exitCode = 2;
192
+ return;
193
+ }
194
+ const run = store.allocationRunById(id);
195
+ if (run === null) {
196
+ console.error(` No recorded allocation run has the id "${id}". segreant alloc runs lists them.`);
197
+ process.exitCode = 1;
198
+ return;
199
+ }
200
+ const names = new Map(store.costCentres().map((c) => [c.costCentreId, c.name]));
201
+ const csv = allocationRunToCsv(run, (costCentreId) => names.get(costCentreId) ?? costCentreId, isDemo());
202
+ if (typeof flags.out === 'string') {
203
+ writeFileSync(flags.out, csv);
204
+ console.error(color(tty, C.green, ` Exported the split for ${new Date(run.result.periodStartMs).toISOString().slice(0, 10)} → ${flags.out}`));
205
+ }
206
+ else {
207
+ process.stdout.write(csv);
208
+ }
209
+ return;
210
+ }
142
211
  if (action === 'centres') {
143
212
  const centres = store.costCentres();
144
213
  if (flags.json)
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Ask once, at the end of an interactive `scan --setup`, for a price for the
3
+ * costliest models still priced by a guess: after the card and the public price
4
+ * list have had their turn, the person is the only one who knows what a private,
5
+ * fine-tuned or local model costs. Interactive terminals only, Enter skips, and
6
+ * at most five models, so a long tail never turns setup into a questionnaire.
7
+ */
8
+ import { C, color, usd, num } from './ui.js';
9
+ import { existsSync } from 'node:fs';
10
+ import { dbPath } from '../config.js';
11
+ import { unknownModels } from '../cost/declarations.js';
12
+ import { Store } from '../store/db.js';
13
+ import { promptForUnknownModels } from './pricingDeclareCmd.js';
14
+ const MAX_ASKED = 5;
15
+ /** Below this, a guess cannot move a total enough to be worth a question. */
16
+ const MIN_COST_USD = 0.5;
17
+ export async function askModelPrices(tty, io = {
18
+ input: process.stdin, output: process.stdout, interactive: process.stdin.isTTY === true,
19
+ }) {
20
+ if (!io.interactive)
21
+ return 0;
22
+ if (!existsSync(dbPath()))
23
+ return 0;
24
+ const store = new Store(dbPath());
25
+ let all;
26
+ try {
27
+ all = unknownModels(store);
28
+ }
29
+ finally {
30
+ store.close();
31
+ }
32
+ const guessed = all.filter((m) => m.costUsd >= MIN_COST_USD);
33
+ if (guessed.length === 0)
34
+ return 0;
35
+ const asked = guessed.slice(0, MAX_ASKED);
36
+ const write = (line) => io.output.write(`${line}\n`);
37
+ write(color(tty, C.bold, ' Models priced by a guess') + color(tty, C.gray, ' not on the rate card (Enter skips)'));
38
+ for (const m of asked) {
39
+ const how = m.rateMatchKind === 'fallback' ? 'generic rate' : 'nearest model on the card';
40
+ write(color(tty, C.gray, ` ${m.model}: ${num(m.requests)} requests, ${usd(m.costUsd)} recorded (estimated, ${how})`));
41
+ }
42
+ const { createInterface } = await import('node:readline/promises');
43
+ const rl = createInterface({ input: io.input, output: io.output, terminal: false });
44
+ let made = 0;
45
+ try {
46
+ made = (await promptForUnknownModels(asked, {
47
+ ask: (question) => rl.question(` ${question}: `),
48
+ print: (line) => write(color(tty, C.gray, ` ${line}`)),
49
+ })).length;
50
+ }
51
+ finally {
52
+ rl.close();
53
+ }
54
+ if (guessed.length > asked.length)
55
+ write(color(tty, C.gray, ` ${num(guessed.length - asked.length)} more: segreant pricing unknown`));
56
+ if (made > 0)
57
+ write(color(tty, C.gray, ' Recorded requests keep their estimate until you re-price them: segreant reprice (a preview), then --apply'));
58
+ return made;
59
+ }
@@ -14,7 +14,8 @@ function requiredPath(flags, name) {
14
14
  }
15
15
  function emit(result, flags, action) {
16
16
  if (flags.json) {
17
- printJson({ action, ...result });
17
+ const { schemaVersion: ledgerSchemaVersion, ...details } = result;
18
+ printJson({ action, ...details, ledgerSchemaVersion });
18
19
  if (!result.ok)
19
20
  process.exitCode = 1;
20
21
  return;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The lines that say how a CLI total was priced. Every command that prints a
3
+ * total with more than one part prints these under it, so the parts read the
4
+ * same everywhere (`today`, `week`, `month`, the first-run summary).
5
+ */
6
+ import { cohortModels } from '../cost/basis.js';
7
+ import { num, usd } from './ui.js';
8
+ export function basisLines(basis, indent = ' ') {
9
+ const out = [];
10
+ for (const c of basis.cohorts) {
11
+ if (c.id === 'unpriced') {
12
+ out.push(`${indent}${num(c.requests)} requests not priced: ${cohortModels(c)} (no rate applies)`);
13
+ continue;
14
+ }
15
+ if (c.id === 'not_sent') {
16
+ out.push(`${indent}${''.padStart(10)} ${num(c.requests)} requests refused before reaching the provider (no charge)`);
17
+ continue;
18
+ }
19
+ if (c.id === 'tool_reported' && c.costUsd < 0.005) {
20
+ // A free model is a real answer ("the tool said $0"), not a missing price.
21
+ out.push(`${indent}${''.padStart(10)} ${num(c.requests)} requests the tool reported at no charge: ${cohortModels(c)}`);
22
+ continue;
23
+ }
24
+ if (c.id === 'user_free') {
25
+ out.push(`${indent}${''.padStart(10)} ${num(c.requests)} requests on models you marked free: ${cohortModels(c)}`);
26
+ continue;
27
+ }
28
+ const models = c.id === 'fallback' || c.id === 'list_family' || c.id === 'user_declared' || c.id === 'unrecorded'
29
+ ? `: ${cohortModels(c)}`
30
+ : '';
31
+ out.push(`${indent}${usd(c.costUsd).padStart(10)} ${c.label}${models}`);
32
+ }
33
+ if (basis.cohorts.some((c) => c.id === 'fallback')) {
34
+ out.push(`${indent} fallback = the model was not on the rate card when imported; a generic rate, a rough estimate`);
35
+ out.push(`${indent} segreant reprice previews new prices for models the card has learned since (nothing changes without --apply)`);
36
+ }
37
+ if (basis.cohorts.some((c) => c.id === 'user_declared')) {
38
+ out.push(`${indent} your price = set with segreant pricing set; not checked against a price list`);
39
+ }
40
+ if (basis.cohorts.some((c) => c.id === 'tool_reported')) {
41
+ out.push(`${indent} tool-reported = what the tool recorded; not checked against any rate card or bill`);
42
+ }
43
+ out.push(`${indent}None of these is your bill.`);
44
+ return out;
45
+ }
@@ -4,20 +4,29 @@
4
4
  * a least-privilege provider connector, but it cannot establish reconciliation.
5
5
  */
6
6
  import { writeFileSync } from 'node:fs';
7
- import { dbPath, loadConfig } from '../config.js';
7
+ import { dbPath, isDemo, loadConfig } from '../config.js';
8
8
  import { OpenAiCostsPullError, previewOpenAiCosts, pullOpenAiCosts, } from '../billing/openaiCosts.js';
9
9
  import { newOpenAiScopeDeclaration } from '../billing/scope.js';
10
10
  import { assessAssumptionFragility } from '../epistemic/countermodel.js';
11
11
  import { reconciliationCountermodels } from '../billing/countermodels.js';
12
- import { describeOffPathBound, displayUsd, signedUsd } from '../billing/reconcile.js';
12
+ import { describeLocalQuantization, describeOffPathBound, displayUsd, signedUsd } from '../billing/reconcile.js';
13
13
  import { reconciliationReadiness } from '../billing/readiness.js';
14
14
  import { formatUsdMicros } from '../billing/types.js';
15
15
  import { readBillingImportFile } from '../billing/importer.js';
16
16
  import { billingEvidenceToCsv } from '../export/billingCsv.js';
17
17
  import { Store } from '../store/db.js';
18
- import { printJson } from './ui.js';
18
+ import { printJson as printJsonRaw, cliJsonResponse } from './ui.js';
19
+ /**
20
+ * Billing JSON names sample data in the payload itself. The bill the demo
21
+ * carries is generated, and a comparison or export lifted out of the terminal
22
+ * must still say so; the dashboard's banner does not travel with it.
23
+ */
24
+ function printJson(value) {
25
+ printJsonRaw(isDemo() && value !== null && typeof value === 'object' && !Array.isArray(value) ? { demo: true, ...value } : value);
26
+ }
19
27
  import { stringifyJson } from '../util/json.js';
20
28
  import { moneyToJson } from '../economics/money.js';
29
+ import { recordedTime } from '../util/time.js';
21
30
  function billingEgressRepairAction(failureCode) {
22
31
  return failureCode === 'egress_receipt_integrity_failed' || failureCode === 'egress_receipt_persistence_failed'
23
32
  ? 'Repair or restore the local receipt history before retrying; if the lock is stale, confirm no Segreant writer is active, then remove only that lock and rerun verify.'
@@ -66,12 +75,12 @@ function printCostsCoverage(coverage) {
66
75
  console.log(` Snapshot ${coverage.observation.observationRunId}`);
67
76
  console.log(` Period ${new Date(coverage.observation.periodStartMs).toISOString().slice(0, 10)} → ${new Date(coverage.observation.periodEndMs).toISOString().slice(0, 10)} (UTC, exclusive end)`);
68
77
  console.log(` Provider rows ${coverage.observation.providerLineCount} (${coverage.observation.currencies.join(', ') || 'none'}); values intentionally not summed here`);
69
- console.log(` Declared route ${captured.requestCount} live proxy request(s), $${formatLocalEstimate(captured.costUsd)} local rate-card estimate (${captured.estimatedRequestCount} estimated)`);
78
+ console.log(` Declared route ${captured.requestCount} live proxy request(s), $${formatLocalEstimate(captured.costUsd)} local estimate, not the bill (${captured.estimatedRequestCount} priced by a guess; the rest at list prices or prices you set)`);
70
79
  // Directly under the number it falsifies, and not in `Blockers`, which lists
71
80
  // the conditions that hold always (D-185). A zero here is what an operator
72
81
  // uses to decide whether minting an Admin credential is worth it.
73
82
  if (coverage.localLedgerRetention.truncated) {
74
- const before = new Date(coverage.localLedgerRetention.prunedBeforeMs ?? 0).toISOString().slice(0, 10);
83
+ const before = recordedTime(coverage.localLedgerRetention.prunedBeforeMs, true);
75
84
  console.log(` Local ledger TRUNCATED — retention deleted rows before ${before}, inside the period above.`);
76
85
  console.log(' Every local count on this screen is what SURVIVES, so an empty declared');
77
86
  console.log(' route here is not evidence that the route captured nothing.');
@@ -93,7 +102,7 @@ function printReadiness(readiness) {
93
102
  // null. A guard that goes quiet leaves nothing on the screen to be wrong,
94
103
  // which is why the silence itself has to be reported (D-186).
95
104
  if (readiness.localLedgerRetention.truncated) {
96
- const before = new Date(readiness.localLedgerRetention.prunedBeforeMs ?? 0).toISOString().slice(0, 10);
105
+ const before = recordedTime(readiness.localLedgerRetention.prunedBeforeMs, true);
97
106
  console.log('');
98
107
  if (c === null) {
99
108
  console.log(` NO OPENAI SPEND SURVIVES HERE, and retention deleted rows before ${before}.`);
@@ -210,6 +219,10 @@ function printReconciliation(result, applied) {
210
219
  for (const line of wrapText(describeOffPathBound(result.offPathBound), 74)) {
211
220
  console.log(` ${line}`);
212
221
  }
222
+ const rounding = describeLocalQuantization(result.localQuantization);
223
+ if (rounding)
224
+ for (const line of wrapText(rounding, 74))
225
+ console.log(` ${line}`);
213
226
  console.log('');
214
227
  console.log(` Coverage ${result.coverage.daysWithBoth} day(s) with both sides · ${result.coverage.providerOnlyDays} provider-only · ${result.coverage.localOnlyDays} local-only`);
215
228
  console.log(` Material ${result.coverage.materialDays} day(s) differ by more than $${result.materialityUsd.toFixed(2)}`);
@@ -829,9 +842,32 @@ function wrapText(text, width) {
829
842
  lines.push(current);
830
843
  return lines;
831
844
  }
845
+ /**
846
+ * A part's share of a whole, as a percentage label. Only the whole itself
847
+ * reads 100 and only nothing reads 0: rounding 99.6% up to 100% told the user
848
+ * the comparison covered everything when it did not (PR #142 review).
849
+ */
850
+ export function coveragePercent(part, whole) {
851
+ if (part >= whole)
852
+ return '100';
853
+ if (part <= 0)
854
+ return '0';
855
+ const share = (part / whole) * 100;
856
+ if (share < 0.1)
857
+ return '<0.1';
858
+ if (share < 1)
859
+ return share.toFixed(1);
860
+ if (share >= 99.5)
861
+ return Math.min(99.9, Math.floor(share * 10) / 10).toFixed(1);
862
+ return String(Math.round(share));
863
+ }
832
864
  /** `segreant billing` — immutable local provider-cost evidence, never an implicit reconciliation. */
833
865
  export async function cmdBilling(flags) {
834
866
  const action = typeof flags._[0] === 'string' ? flags._[0] : 'status';
867
+ // The same mark `show` and `home` print: everything below is generated.
868
+ if (isDemo() && !flags.json && !(action === 'export' && !flags.out)) {
869
+ console.log(' \u25CF DEMO DATA \u2014 synthetic, isolated in demo.db; this bill and comparison were generated, not received from a provider');
870
+ }
835
871
  if (action === 'scope') {
836
872
  cmdScope(flags);
837
873
  return;
@@ -919,7 +955,30 @@ export async function cmdBilling(flags) {
919
955
  console.log(` Imports ${summary.importCount}`);
920
956
  console.log(` Charge lines ${summary.recordCount}`);
921
957
  console.log(` Declared USD $${formatUsdMicros(summary.providerReportedUsdMicros)}`);
922
- console.log(' Reconcile not_reconciled — imported reports and local request estimates are not yet scope-comparable.');
958
+ // A recorded comparison is the answer to "has this been reconciled?";
959
+ // the status line used to say not_reconciled whatever the ledger held.
960
+ const latest = store.reconciliationRuns(20);
961
+ if (latest.length === 0) {
962
+ console.log(' Reconcile not_reconciled — no comparison recorded yet. Preview one: segreant billing reconcile');
963
+ }
964
+ else {
965
+ const run = latest[0].result;
966
+ const day = (ms) => new Date(ms).toISOString().slice(0, 10);
967
+ console.log(` Reconcile ${latest.length === 20 ? '20+' : latest.length} recorded comparison(s); latest: ${run.providerProjectRef}, ${day(run.periodStartMs)} → ${day(run.periodEndMs)} (UTC, end exclusive)`);
968
+ console.log(` provider $${formatUsdMicros(run.providerReportedMicros)} · metered $${formatUsdMicros(run.localCapturedMicros)} · unexplained ${signedUsd(run.unexplainedVarianceMicros)} — scope-conditional, not a clean reconciliation`);
969
+ // How much of what Segreant metered in that period the comparison
970
+ // actually covers: one project's bill is not the month's spend.
971
+ // Same day quantization as the comparison itself, so the share
972
+ // cannot exceed the whole (PR #142 review).
973
+ const metered = store.meteredMicrosLikeReconciliation(run.periodStartMs, run.periodEndMs, run.declaredScopeId);
974
+ if (metered !== null && metered.onRouteMicros !== run.localCapturedMicros) {
975
+ console.log(' coverage withheld: the ledger has changed for that period since this comparison was recorded; re-run segreant billing reconcile');
976
+ }
977
+ else if (metered !== null && metered.allMicros > 0) {
978
+ console.log(` covers $${formatUsdMicros(run.localCapturedMicros)} of the $${formatUsdMicros(metered.allMicros)} Segreant metered across all providers in that period (${coveragePercent(run.localCapturedMicros, metered.allMicros)}%)`);
979
+ }
980
+ console.log(' Details: segreant billing reconcile');
981
+ }
923
982
  if (summary.lastImportedAtMs !== null)
924
983
  console.log(` Last import ${new Date(summary.lastImportedAtMs).toISOString()}`);
925
984
  if (imports.length > 0)
@@ -932,10 +991,10 @@ export async function cmdBilling(flags) {
932
991
  const records = store.billingEvidenceRecords();
933
992
  const summary = store.billingSummary();
934
993
  if (flags.json) {
935
- writeOrPrint(stringifyJson({ summary, records }) + '\n', flags.out);
994
+ writeOrPrint(stringifyJson(cliJsonResponse({ ...(isDemo() ? { demo: true } : {}), summary, records })) + '\n', flags.out);
936
995
  }
937
996
  else {
938
- writeOrPrint(billingEvidenceToCsv(records), flags.out);
997
+ writeOrPrint(billingEvidenceToCsv(records, isDemo()), flags.out);
939
998
  }
940
999
  return;
941
1000
  }
@@ -36,7 +36,8 @@ function usage() {
36
36
  function emit(payload, flags) {
37
37
  // Keep the JSON path canonical; --json is accepted for parity with other
38
38
  // review commands and the human form remains machine-readable by design.
39
- void flags;
39
+ // Reading it marks it as used, so the dispatcher does not report it.
40
+ void flags.json;
40
41
  printJson(payload);
41
42
  }
42
43
  export function cmdCapital(flags) {
@@ -4,6 +4,7 @@
4
4
  * from cli.ts in the per-command-module split; only cmdConnect is exported,
5
5
  * the per-tool flows stay module-internal.
6
6
  */
7
+ import { isBuiltInProviderUpstream, providerRuleCommand } from '../egress/policy.js';
7
8
  import { homedir } from 'node:os';
8
9
  import { join, dirname } from 'node:path';
9
10
  import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync } from 'node:fs';
@@ -123,8 +124,8 @@ function wrapOpencodeFlow(cfg, flags, tty, providerName, path) {
123
124
  console.log(color(tty, C.green, ` ✓ Wrapped "${providerName}". opencode now routes through Segreant.`));
124
125
  console.log(color(tty, C.gray, ` opencode config: ${path} (backup at ${path}.bak)`));
125
126
  console.log(color(tty, C.gray, ` Segreant upstreams.openai → ${res.originalBaseUrl}`));
126
- console.log(color(tty, C.gray, ` Egress remains ${cfg.egress.mode}; this connection never grants cloud permission.`));
127
- console.log(color(tty, C.gray, ` Review the provider's exact rule with: segreant egress plan ...`));
127
+ const forwarding = upstreamForwarding(res.originalBaseUrl);
128
+ console.log(color(tty, forwarding.ok ? C.gray : C.yellow, ` ${forwarding.text}`));
128
129
  console.log(color(tty, C.gray, ' JSON comments were reformatted away; your settings + keys are preserved.'));
129
130
  console.log('');
130
131
  console.log(color(tty, C.gray, ' Restart Segreant (segreant start), run opencode, then: segreant sources'));
@@ -258,10 +259,11 @@ function connectOpencode(cfg, flags, tty) {
258
259
  finishConnectOpencode(tty);
259
260
  }
260
261
  /**
261
- * Antigravity's BUILT-IN Gemini agent routes through Google's own servers — a
262
- * managed backend no cooperative proxy can meter (same class as opencode Zen).
263
- * But Antigravity supports CUSTOM OpenAI-compatible providers (base URL + key),
264
- * and those we meter fully: provider → proxy → the upstream of your choice.
262
+ * Antigravity's BUILT-IN agent routes through Google's own servers, which no
263
+ * cooperative proxy sits in front of; its usage is read instead from the
264
+ * conversation databases Antigravity keeps locally (`segreant import
265
+ * antigravity`, src/connect/antigravity.ts). Its CUSTOM OpenAI-compatible
266
+ * providers can also be routed through the proxy: provider → proxy → upstream.
265
267
  * `--write` points the proxy's OpenAI upstream at Gemini's OpenAI-compatible
266
268
  * endpoint, the free-tier test path; the user's key passes through untouched.
267
269
  */
@@ -277,8 +279,10 @@ function connectAntigravity(cfg, flags, tty) {
277
279
  console.log('');
278
280
  console.log(color(tty, C.bold, ' Connect Google Antigravity as a source'));
279
281
  console.log(color(tty, C.gray, ' ' + '─'.repeat(52)));
280
- console.log(color(tty, C.gray, ' Antigravity’s built-in Gemini agent runs on Google’s servers — unmeterable'));
281
- console.log(color(tty, C.gray, ' by any cooperative proxy. Its CUSTOM providers, however, meter fully:'));
282
+ console.log(color(tty, C.gray, ' Antigravity’s built-in agent runs on Google’s servers, so no proxy sits in front'));
283
+ console.log(color(tty, C.gray, ' of it. Its usage is read from the logs it keeps on this machine instead:'));
284
+ console.log(color(tty, C.green, ' segreant import antigravity'));
285
+ console.log(color(tty, C.gray, ' Custom providers can also go through the proxy, live:'));
282
286
  console.log('');
283
287
  console.log(` 1) ${color(tty, C.bold, 'Choose the upstream')} the proxy forwards to. For the Gemini free tier:`);
284
288
  console.log(color(tty, C.green, ' segreant connect antigravity --write'));
@@ -359,6 +363,18 @@ function connectGenericApi(cfg, flags, tty) {
359
363
  console.log(color(tty, C.gray, ' The header wins if you send both. Either way it is your declaration, recorded'));
360
364
  console.log(color(tty, C.gray, ' as such — Segreant does not verify that the label is true.'));
361
365
  console.log('');
366
+ // The recipe covered the client side only; a user pointing scripts at another
367
+ // OpenAI-compatible provider had to find the other half in the docs.
368
+ console.log(color(tty, C.gray, ` Segreant forwards these requests to ${cfg.upstreams.openai}. For another`));
369
+ console.log(color(tty, C.gray, ' OpenAI-compatible provider (OpenRouter, Azure, a local server), set'));
370
+ console.log(color(tty, C.gray, ' "upstreams": { "openai": "<base URL>" } in config.json; docs/INTEGRATIONS.md has examples.'));
371
+ // The proxy appends the client's path to the upstream base, so a version
372
+ // segment in both places doubles it (/v1/v1/chat/completions) and 404s.
373
+ console.log(color(tty, C.gray, ' If that base URL already ends in its version (an Ollama base ending in /v1),'));
374
+ console.log(color(tty, C.gray, ` point the client at http://localhost:${port} without /v1. A remote upstream also`));
375
+ console.log(color(tty, C.gray, ' other than the official OpenAI and Anthropic APIs needs one egress rule; a refused'));
376
+ console.log(color(tty, C.gray, ' request prints the exact command.'));
377
+ console.log('');
362
378
  console.log(color(tty, C.gray, ' Run it, then check:'));
363
379
  console.log(color(tty, C.green, ' segreant sources'));
364
380
  console.log(color(tty, C.gray, ' segreant project --coverage # what the spend is attributed to, and how'));
@@ -369,6 +385,31 @@ function connectGenericApi(cfg, flags, tty) {
369
385
  * No tool (or --list) shows the menu; opencode writes/prints its provider block;
370
386
  * `api` prints the generic env + header recipe (with an optional custom source name).
371
387
  */
388
+ /**
389
+ * What forwarding to an upstream needs. Four cases: the official provider APIs
390
+ * are built in; plain HTTP on this machine is always allowed; any other HTTPS
391
+ * address needs the one rule printed; plain HTTP elsewhere cannot be allowed at
392
+ * all. The last case used to be told it was the official API (independent
393
+ * review, 2026-10-18).
394
+ */
395
+ export function upstreamForwarding(url) {
396
+ if (isBuiltInProviderUpstream(url))
397
+ return { ok: true, text: 'Requests to the official provider API go out with no extra step.' };
398
+ let target;
399
+ try {
400
+ target = new URL(url);
401
+ }
402
+ catch {
403
+ return { ok: false, text: `${url} is not a valid address.` };
404
+ }
405
+ if (target.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(target.hostname)) {
406
+ return { ok: true, text: 'A model on this machine is reached directly; no rule is needed.' };
407
+ }
408
+ const allow = providerRuleCommand(url);
409
+ return allow
410
+ ? { ok: false, text: `${url} is not a built-in provider API. Allow Segreant to forward there with:\n ${allow}` }
411
+ : { ok: false, text: `${url} cannot be allowed as it is: Segreant forwards only to HTTPS, or to plain HTTP on this machine (localhost, 127.0.0.1). Use the gateway's https:// address.` };
412
+ }
372
413
  export function cmdConnect(flags) {
373
414
  const tty = process.stdout.isTTY ?? false;
374
415
  const cfg = loadConfig();
@@ -387,7 +428,7 @@ export function cmdConnect(flags) {
387
428
  console.log(color(tty, C.gray, ' segreant connect opencode --write apply it for you (backs up first)'));
388
429
  console.log('');
389
430
  console.log(color(tty, C.gray, ' No base URL to wire? Meter subscription tools natively — no routing, no key:'));
390
- console.log(color(tty, C.green, ' segreant import claude-code | opencode | codex | all ') + color(tty, C.gray, '(--watch = live)'));
431
+ console.log(color(tty, C.green, ' segreant import claude-code | opencode | codex | antigravity | all ') + color(tty, C.gray, '(--watch = live)'));
391
432
  console.log('');
392
433
  return;
393
434
  }
@@ -32,11 +32,12 @@ function ruleFromFlags(flags) {
32
32
  function printUsage(tty) {
33
33
  console.log('');
34
34
  console.log(color(tty, C.bold, ' Segreant egress — process-scoped high-assurance transport'));
35
- console.log(color(tty, C.gray, ' Default is local_locked: literal loopback only. No remote probe is made by this command.'));
35
+ console.log(color(tty, C.gray, " Built in, in every mode: loopback, Segreant's public price list, and your requests to the"));
36
+ console.log(color(tty, C.gray, ' official OpenAI and Anthropic APIs. Anything else needs a rule. No remote probe is made here.'));
36
37
  console.log('');
37
38
  console.log(color(tty, C.gray, ' segreant egress status'));
38
- console.log(color(tty, C.gray, ' segreant egress plan --mode controlled_cloud --id openai-main --purpose provider_inference --data-class provider_request --method POST --origin https://api.openai.com --path-prefix /v1/'));
39
- console.log(color(tty, C.gray, ' segreant egress apply --apply --mode controlled_cloud --id openai-main --purpose provider_inference --data-class provider_request --method POST --origin https://api.openai.com --path-prefix /v1/'));
39
+ console.log(color(tty, C.gray, ' segreant egress plan --mode controlled_cloud --id openrouter --purpose provider_inference --data-class provider_request --method POST --origin https://openrouter.ai --path-prefix /api/'));
40
+ console.log(color(tty, C.gray, ' segreant egress apply --apply --mode controlled_cloud --id openrouter --purpose provider_inference --data-class provider_request --method POST --origin https://openrouter.ai --path-prefix /api/'));
40
41
  console.log(color(tty, C.gray, ' segreant egress apply --apply --mode local_locked'));
41
42
  console.log(color(tty, C.gray, ' segreant egress receipts | segreant egress verify'));
42
43
  console.log('');
@@ -109,6 +110,10 @@ export function cmdEgress(flags) {
109
110
  console.log('');
110
111
  console.log(color(tty, C.bold, ' Segreant egress status'));
111
112
  console.log(' Mode: ' + payload.mode);
113
+ if (payload.mode === 'invalid') {
114
+ console.log(' The egress section of config.json is malformed, so every cloud request is refused,');
115
+ console.log(' your provider included. Repair: segreant egress apply --apply --mode local_locked');
116
+ }
112
117
  const rules = payload.rules;
113
118
  console.log(' Rules: ' + (rules.length ? rules.map((rule) => rule.id + ' (' + (rule.enabled ? 'enabled' : 'disabled') + ')').join(', ') : 'none'));
114
119
  const receipts = payload.receipts;
@@ -171,6 +176,13 @@ export function cmdEgress(flags) {
171
176
  const failures = validateEgressRule(candidate);
172
177
  if (failures.length) {
173
178
  console.error(' Rule refused: ' + failures.join('; '));
179
+ // Git Bash (MSYS) rewrites an argument that starts with "/" into a Windows
180
+ // path before Segreant sees it, so "/v1/" arrives as "C:/Program Files/Git/v1/".
181
+ const prefix = flags['path-prefix'];
182
+ if (typeof prefix === 'string' && /^[A-Za-z]:[\\/]/.test(prefix)) {
183
+ console.error(' --path-prefix arrived as "' + prefix + '". Git Bash turns arguments that start with / into Windows paths.');
184
+ console.error(' Put MSYS_NO_PATHCONV=1 in front of the command, or run it from PowerShell or cmd.');
185
+ }
174
186
  process.exitCode = 1;
175
187
  return;
176
188
  }
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { dirname } from 'node:path';
5
5
  import { dbPath } from '../config.js';
6
6
  import { Store } from '../store/db.js';
7
+ import { printJson } from './ui.js';
7
8
  import { buildGithubActionsOutcome, importGithubActionsOutcome, signGithubActionsOutcome, } from '../githubActionsEvidence.js';
8
9
  import { readBoundedUtf8File, RESOURCE_LIMITS } from '../util/resource-limits.js';
9
10
  import { buildEpistemicUxBundle } from '../epistemic/ux.js';
@@ -35,11 +36,13 @@ function cmdUx(flags) {
35
36
  throw new Error('epistemic UX options must be a JSON object');
36
37
  }
37
38
  const bundle = buildEpistemicUxBundle(supplied);
38
- process.stdout.write(JSON.stringify({
39
+ // Always JSON; reading --json marks it as used so the dispatcher does not report it.
40
+ void flags.json;
41
+ printJson({
39
42
  operation: 'epistemic_ux_bundle',
40
43
  bundle,
41
44
  boundary: 'Review-only epistemic UX bundle; no claim issuance, strengthening, authorization, routing, or external truth inference.',
42
- }, null, 2) + '\n');
45
+ });
43
46
  }
44
47
  function readJsonArtifact(path) {
45
48
  try {
@@ -124,7 +127,7 @@ async function cmdGithubImport(flags) {
124
127
  if (!result.ok)
125
128
  throw new Error(`evidence rejected: ${result.reason}`);
126
129
  if (flags.json) {
127
- process.stdout.write(JSON.stringify(result, null, 2) + '\n');
130
+ printJson(result);
128
131
  }
129
132
  else {
130
133
  console.log(` Imported verified signed-CI tested=${artifact.body.verdict} evidence for ${result.commit.slice(0, 12)}.`);