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
@@ -11,7 +11,7 @@ import { loadConfig, mutateConfig, dbPath, isDemo } from '../config.js';
11
11
  import { isGitRepo, projectName, resolveCommit } from '../git/correlate.js';
12
12
  import { computeArtifactPersistence } from '../git/quality.js';
13
13
  import { contributionEvidenceLines, summarizeContributionEvidence } from '../git/contribution.js';
14
- import { loadRealization } from '../value/realization.js';
14
+ import { loadRealization, keptSummary, CLI_PERIOD_DAYS, CLI_GIT_BUDGET_MS } from '../value/realization.js';
15
15
  import { WORK_WEEK_MINUTES } from '../value/timeReclaimed.js';
16
16
  import { computeFrontier } from '../value/frontier.js';
17
17
  // The value report's one composition — shared with the dashboard's '/api/value'.
@@ -25,6 +25,87 @@ import { describeDriftReading, driftReading } from '../value/drift.js';
25
25
  import { C, color, usd, num, pct, gateGlyph, noteSource, printNotAGitRepo, printJson } from './ui.js';
26
26
  import {} from './flags.js';
27
27
  import { retentionNotice } from './retention.js';
28
+ /**
29
+ * With no request recorded there is no AI spend for git history to be measured
30
+ * against. The value commands used to walk every commit of the last 90 days
31
+ * anyway (54-87 s on a real repository) and then print 0% realized, an RoI of
32
+ * 0 and $0.00 per commit: a zero where the honest answer is "nothing to
33
+ * measure yet". Returns true when it has answered for the caller.
34
+ */
35
+ async function answeredNothingSpent(store, flags, repo) {
36
+ if (store.earliestRequestMs() !== null)
37
+ return false;
38
+ // A path that is not a repository is the user's real problem, and the
39
+ // command's own check reports it with a failing status; answering "nothing
40
+ // spent" first hid that behind exit 0 (PR #131 review).
41
+ if (!(await isGitRepo(repo)))
42
+ return false;
43
+ // No surviving request is not "nothing spent" once retention has pruned
44
+ // rows or a realization snapshot is stored: the stored report is the answer
45
+ // then, and the command must reach it (review 4, F2).
46
+ if (store.retentionFloor().requestsPrunedBeforeMs !== null || store.countRealizationUnits() > 0)
47
+ return false;
48
+ const next = ['segreant scan --setup', 'segreant demo --serve'];
49
+ if (flags.json) {
50
+ printJson({ status: 'no_spend_recorded', measured: false, next });
51
+ }
52
+ else {
53
+ const tty = process.stdout.isTTY ?? false;
54
+ console.log('');
55
+ console.log(' No AI spend is recorded yet, so there is nothing to measure against this repository.');
56
+ console.log(color(tty, C.gray, ` Import what your tools already logged: ${next[0]}`));
57
+ console.log(color(tty, C.gray, ` Or see every figure on sample data: ${next[1]}`));
58
+ console.log('');
59
+ }
60
+ store.close();
61
+ return true;
62
+ }
63
+ /**
64
+ * The budget decision in two sentences: whether the evidence shows the
65
+ * recommended cap doing better than the current one, and where the full record
66
+ * is. The record itself (renderBudgetCapDecision) stays one flag away.
67
+ */
68
+ function plainCapVerdict(decision, canonical, soft) {
69
+ const label = (capUsd) => (capUsd === null || capUsd === undefined ? 'no cap' : usd(capUsd));
70
+ const recommendedCap = decision.actions.find((a) => a.action === 'apply_recommended')?.capUsd;
71
+ const currentCap = decision.actions.find((a) => a.action === 'keep_current')?.capUsd;
72
+ const recommended = label(recommendedCap);
73
+ const current = label(currentCap);
74
+ // Equal caps are one setting, not two options: there is nothing to compare,
75
+ // and blaming partly observed days for "no winner" would be a false reason
76
+ // (PR #142 review).
77
+ // The soft warning is part of the recommendation too: an equal daily cap
78
+ // with a different soft warning is a change, and is said as one (PR #145 review).
79
+ if (recommendedCap !== null && recommendedCap !== undefined && recommendedCap === currentCap) {
80
+ const first = soft.current === soft.recommended
81
+ ? ` Your current ${current} cap and ${usd(soft.recommended)} soft warning already match the recommendation; nothing to change.`
82
+ : ` Your current ${current} daily cap already matches the recommendation. Only the soft warning differs: recommended ${usd(soft.recommended)}, yours is ${soft.current === null ? 'off' : usd(soft.current)}.`;
83
+ return [first, ' Full decision record: segreant budget --recommend --detail'];
84
+ }
85
+ const certificate = canonical.certificateBundle.dominance;
86
+ const currentPhrase = current === 'no cap' ? 'having no cap' : `your current ${current}`;
87
+ const verdict = certificate.status === 'proven_dominant'
88
+ ? certificate.action === 'apply_recommended'
89
+ ? ` On this evidence the recommended ${recommended} cap does better than ${currentPhrase}.`
90
+ : ` On this evidence ${currentPhrase} does better than the recommended ${recommended} cap.`
91
+ : ` Is ${recommended} better than ${currentPhrase}? Not shown: which days' spend was kept is only partly observed, so neither is proven better.`;
92
+ return [verdict, ' Full decision record, with each assumption and what would settle it: segreant budget --recommend --detail'];
93
+ }
94
+ /** The last line of the budget advice when nothing is applied: why, in the reader's terms. */
95
+ export function budgetClosingLine(applyPermitted, dominance, sameCap = null) {
96
+ if (sameCap?.softMatches)
97
+ return ' No change needed: your config already has this cap and soft warning.';
98
+ if (sameCap)
99
+ return ` Daily cap unchanged. To set only the soft warning: segreant budget --soft ${sameCap.softRecommended.toFixed(2)}`;
100
+ if (applyPermitted)
101
+ return ' Re-run with --apply to write these to your config.';
102
+ if (dominance?.status === 'proven_dominant' && dominance.action === 'apply_recommended') {
103
+ return ' No cap written: the evidence favours the new cap, but its inputs are not verified enough to change spend. --detail lists what is missing.';
104
+ }
105
+ if (dominance?.status === 'proven_dominant')
106
+ return ' No cap written: on this evidence your current setting does better.';
107
+ return ' No cap written: --apply needs evidence that the new cap does better, and this data does not show it. --detail lists what is missing.';
108
+ }
28
109
  export async function cmdYield(flags) {
29
110
  const repo = flags.repo ?? process.cwd();
30
111
  const limit = flags.limit ? Number(flags.limit) : 30;
@@ -35,6 +116,8 @@ export async function cmdYield(flags) {
35
116
  return;
36
117
  }
37
118
  const store = new Store(dbPath());
119
+ if (await answeredNothingSpent(store, flags, repo))
120
+ return;
38
121
  const report = await computeArtifactPersistence(store, repo, { limit, windowDays, persist: true });
39
122
  if (flags.json) {
40
123
  printJson(report);
@@ -52,15 +135,25 @@ export async function cmdYield(flags) {
52
135
  console.log(color(tty, C.gray, ' Recent commits below are provisional (retention still settling).'));
53
136
  }
54
137
  else {
55
- const yieldStr = m.aiYield === null ? 'n/a (no AI cost attributed)' : `${m.aiYield.toFixed(1)} retained lines / $`;
138
+ const withheld = m.aiYield === null && m.spendWindowTruncatedCommits > 0;
139
+ const yieldStr = m.aiYield !== null
140
+ ? `${m.aiYield.toFixed(1)} retained lines / $`
141
+ : withheld ? 'withheld (retention deleted the spend records)' : 'n/a (no AI cost attributed)';
56
142
  console.log(` Retained lines / $ ${color(tty, C.green, yieldStr)}`);
57
143
  console.log(` Effective spend ${m.effectiveSpendRatio === null ? '—' : color(tty, m.effectiveSpendRatio > 0.5 ? C.green : C.yellow, pct(m.effectiveSpendRatio))} ${color(tty, C.gray, 'of $ associated with retained artifacts')}`);
58
144
  console.log(` Artifact retention ${color(tty, m.survivalRatio > 0.7 ? C.green : C.yellow, pct(m.survivalRatio))} ${color(tty, C.gray, `non-retained ${pct(m.churnRatio)}`)}`);
59
145
  console.log(` Revert rate ${color(tty, m.revertRate < 0.05 ? C.green : C.red, pct(m.revertRate))}`);
60
- console.log(` AI cost (matured) ${usd(m.totalCostUsd)} ${color(tty, C.gray, `${num(m.survivingLines)} retained lines`)}`);
146
+ const costScope = m.spendWindowTruncatedCommits > 0
147
+ ? `${num(m.survivingLines)} retained lines · excludes ${m.spendWindowTruncatedCommits} commit(s) with pruned spend`
148
+ : `${num(m.survivingLines)} retained lines`;
149
+ console.log(` AI cost (matured) ${usd(m.totalCostUsd)} ${color(tty, C.gray, costScope)}`);
61
150
  if (m.costPerSurvivingLine !== null) {
62
151
  console.log(` Cost / retained line ${usd(m.costPerSurvivingLine)}`);
63
152
  }
153
+ if (m.spendWindowTruncatedCommits > 0) {
154
+ console.log(color(tty, C.yellow, ` ● ${m.spendWindowTruncatedCommits} matured commit(s) lost spend records to retention inside their window.`));
155
+ console.log(color(tty, C.yellow, ' What remains understates what that work cost, so they are left out of AI cost and every per-dollar figure.'));
156
+ }
64
157
  }
65
158
  console.log('');
66
159
  console.log(color(tty, C.bold, ' Per commit'));
@@ -73,10 +166,12 @@ export async function cmdYield(flags) {
73
166
  const yld = c.aiYield === null ? '—' : c.aiYield.toFixed(0);
74
167
  const status = c.reverted
75
168
  ? color(tty, C.red, 'REVERTED')
76
- : c.maturing
77
- ? color(tty, C.yellow, 'maturing')
78
- : color(tty, C.green, 'matured');
79
- console.log(` ${short} ${age.padStart(4)} ${usd(c.attributedCostUsd).padStart(9)} ${String(c.linesAdded).padStart(6)} ${surv.padStart(9)} ${churn.padStart(5)} ${yld.padStart(5)} ${status}`);
169
+ : c.spendWindowTruncated !== false
170
+ ? color(tty, C.yellow, 'spend pruned')
171
+ : c.maturing
172
+ ? color(tty, C.yellow, 'maturing')
173
+ : color(tty, C.green, 'matured');
174
+ console.log(` ${short} ${age.padStart(4)} ${(c.spendWindowTruncated !== false ? 'pruned' : usd(c.attributedCostUsd)).padStart(9)} ${String(c.linesAdded).padStart(6)} ${surv.padStart(9)} ${churn.padStart(5)} ${yld.padStart(5)} ${status}`);
80
175
  }
81
176
  console.log('');
82
177
  console.log(color(tty, C.gray, ' Retained lines ÷ AI cost is an artifact-persistence lens, not code quality —'));
@@ -84,12 +179,75 @@ export async function cmdYield(flags) {
84
179
  console.log('');
85
180
  store.close();
86
181
  }
182
+ /**
183
+ * The first answer a person wants, from git alone: of the AI work that became
184
+ * commits, how much is still in the code. Printed before the Standard, which
185
+ * needs more gates to call anything realized. Every figure is list cost.
186
+ */
187
+ function printKeptAnswer(tty, report) {
188
+ const k = keptSummary(report);
189
+ const total = k.kept.units + k.notKept.units + k.contested.units + k.unknown.units + k.maturing.units;
190
+ if (total === 0)
191
+ return;
192
+ const days = report.windowDays;
193
+ const row = (label, b, what, tone) => {
194
+ if (b.units === 0)
195
+ return;
196
+ console.log(` ${color(tty, C.bold, label.padEnd(10))} ${color(tty, tone, usd(b.costUsd).padStart(10))} ${String(b.units).padStart(4)} commit${b.units === 1 ? ' ' : 's'} ${color(tty, C.gray, what)}`);
197
+ };
198
+ console.log('');
199
+ console.log(color(tty, C.bold, ' Did the AI work stay in the code?') + color(tty, C.gray, ' from git history · list cost (estimate), not your bill'));
200
+ row('Kept', k.kept, `most of its lines are still in the code after ${days} days, and no tracked check failed`, C.green);
201
+ row('Not kept', k.notKept, 'most of its lines were rewritten or removed, or it was reverted', C.yellow);
202
+ row('Disputed', k.contested, 'the checks disagreed, and nothing failed outright; not settled either way', C.gray);
203
+ row('Unknown', k.unknown, 'not measured in this run (the git time budget ran out)', C.gray);
204
+ const due = k.maturing.nextVerdictMs === null ? '' : `; first verdict ${new Date(k.maturing.nextVerdictMs).toISOString().slice(0, 10)}`;
205
+ row('Maturing', k.maturing, `younger than ${days} days${due}`, C.gray);
206
+ const c = report.periodCoverage;
207
+ if (c !== undefined) {
208
+ const days90 = Math.round((c.periodEndMs - c.periodStartMs) / 86_400_000);
209
+ const off = c.beforeOldestUsd + c.noCommitFollowedUsd + c.notCommittedYetUsd;
210
+ const share = c.scopedCostUsd > 0 ? Math.round((c.onCommitsUsd / c.scopedCostUsd) * 100) : 0;
211
+ console.log(color(tty, C.gray, ` ${share}% of this repository's ${usd(c.scopedCostUsd)} over the last ${days90} days is on a commit above.`));
212
+ if (off > 0.005) {
213
+ const parts = [];
214
+ if (c.noCommitFollowedUsd > 0.005)
215
+ parts.push(`${usd(c.noCommitFollowedUsd)} no commit followed within 8 hours`);
216
+ if (c.beforeOldestUsd > 0.005)
217
+ parts.push(`${usd(c.beforeOldestUsd)} before the oldest commit measured`);
218
+ if (c.notCommittedYetUsd > 0.005)
219
+ parts.push(`${usd(c.notCommittedYetUsd)} not committed yet`);
220
+ console.log(color(tty, C.gray, ` Not on a commit: ${parts.join(' · ')}.`));
221
+ }
222
+ }
223
+ const s = report.spendScope;
224
+ if (s !== undefined) {
225
+ const moved = s.linkedFolders.filter((f) => f.reason === 'moved').length;
226
+ const worktrees = s.linkedFolders.filter((f) => f.reason === 'worktree' || f.reason === 'clone').length;
227
+ const extra = [
228
+ `${s.linkedSessions} agent session${s.linkedSessions === 1 ? '' : 's'}`,
229
+ ...(moved > 0 ? [`${moved} earlier location${moved === 1 ? '' : 's'} of this checkout`] : []),
230
+ ...(worktrees > 0 ? [`${worktrees} other checkout${worktrees === 1 ? '' : 's'} (worktrees, clones)`] : []),
231
+ ];
232
+ console.log(color(tty, C.gray, ` Includes ${extra.join(', ')}, linked by ${s.verifiedObservations} commits git confirmed they made.`));
233
+ }
234
+ }
87
235
  export async function cmdRealize(flags) {
88
236
  const repo = flags.repo ?? process.cwd();
89
- const limit = flags.limit ? Number(flags.limit) : 30;
237
+ const limit = flags.limit ? Number(flags.limit) : undefined;
90
238
  const windowDays = flags.window ? Number(flags.window) : 14;
91
239
  const store = new Store(dbPath());
92
- const loaded = await loadRealization(store, repo, { limit, windowDays, persist: true });
240
+ if (await answeredNothingSpent(store, flags, repo))
241
+ return;
242
+ // Without --limit: every commit of the last 90 days, so the answer covers
243
+ // the same period the spend does.
244
+ const loaded = await loadRealization(store, repo, {
245
+ limit,
246
+ windowDays,
247
+ persist: true,
248
+ sinceDays: limit === undefined ? CLI_PERIOD_DAYS : undefined,
249
+ gitScanBudgetMs: CLI_GIT_BUDGET_MS,
250
+ });
93
251
  if (!loaded) {
94
252
  printNotAGitRepo(repo);
95
253
  process.exitCode = 1;
@@ -105,6 +263,7 @@ export async function cmdRealize(flags) {
105
263
  const tty = process.stdout.isTTY ?? false;
106
264
  const m = report.matured;
107
265
  const wiredGates = GATE_LADDER.filter((g) => m.instrumentation[g] > 0).length;
266
+ printKeptAnswer(tty, report);
108
267
  console.log('');
109
268
  console.log(color(tty, C.bold, ' The Realization Standard — did AI spend become real outcomes?'));
110
269
  console.log(color(tty, C.gray, ` ${m.units} matured units (older than ${windowDays}d) · ${wiredGates} of ${GATE_LADDER.length} gates instrumented`));
@@ -339,6 +498,8 @@ export async function cmdRoi(flags) {
339
498
  const windowDays = flags.window ? Number(flags.window) : 14;
340
499
  const cfg = loadConfig();
341
500
  const store = new Store(dbPath());
501
+ if (await answeredNothingSpent(store, flags, repo))
502
+ return;
342
503
  // The whole composition — realization, this project's resolved baseline, the
343
504
  // Lift source, the money inputs, RoI, the drift streams, sensitivity — is one
344
505
  // sequence in src/value/report.ts, shared with `/api/value`. The three inputs
@@ -353,6 +514,8 @@ export async function cmdRoi(flags) {
353
514
  repo,
354
515
  windowDays,
355
516
  persist: true,
517
+ sinceDays: CLI_PERIOD_DAYS,
518
+ gitScanBudgetMs: CLI_GIT_BUDGET_MS,
356
519
  laborRatePerHour: flags['labor-rate'] !== undefined ? Number(flags['labor-rate']) : undefined,
357
520
  tsfUpperBound: flags.tsf !== undefined ? Number(flags.tsf) : undefined,
358
521
  riskAversion: flags['risk'] !== undefined ? Number(flags['risk']) : 0,
@@ -644,7 +807,13 @@ export async function cmdBudgetAdvisor(flags) {
644
807
  console.log(color(tty, C.gray, ` · ${r}`));
645
808
  if (decision) {
646
809
  console.log('');
647
- for (const line of renderBudgetCapDecision(decision, certificates, canonicalDecision))
810
+ // The full record (certificate, assurance level, every unmet input claim)
811
+ // is for an auditor; printed by default it buried the answer under internal
812
+ // claim ids and read as contradicting the recommendation above it.
813
+ const lines = flags.detail
814
+ ? renderBudgetCapDecision(decision, certificates, canonicalDecision)
815
+ : plainCapVerdict(decision, canonicalDecision, { current: cfg.budget.dailySoftUsd, recommended: softCap });
816
+ for (const line of lines)
648
817
  console.log(color(tty, C.gray, line));
649
818
  }
650
819
  // Raw allocation and frontier trim/grow hints are withheld from this surface
@@ -676,9 +845,11 @@ export async function cmdBudgetAdvisor(flags) {
676
845
  }
677
846
  else {
678
847
  console.log('');
679
- console.log(color(tty, C.gray, decision && applyPermitted
680
- ? ' Re-run with --apply to write these to your config.'
681
- : ' No cap written. Applying a recommendation requires a certified DecisionCertificate with DAL-3 assurance; the decision block above says what is missing.'));
848
+ // Three different reasons, said differently: the evidence picks no winner,
849
+ // it picks the current cap, or it picks the new cap but its inputs are not
850
+ // verified enough to change spend (PR #142 review).
851
+ const dominance = canonicalDecision?.certificateBundle.dominance;
852
+ console.log(color(tty, C.gray, budgetClosingLine(decision !== null && applyPermitted, dominance ?? null, cfg.budget.dailyUsd === dailyCap ? { softRecommended: softCap, softMatches: cfg.budget.dailySoftUsd === softCap } : null)));
682
853
  }
683
854
  console.log('');
684
855
  store.close();
@@ -687,7 +858,11 @@ export async function cmdFrontier(flags) {
687
858
  const repo = flags.repo ?? process.cwd();
688
859
  const windowDays = flags.window ? Number(flags.window) : 14;
689
860
  const store = new Store(dbPath());
690
- const loaded = await loadRealization(store, repo, { windowDays, persist: true });
861
+ if (await answeredNothingSpent(store, flags, repo))
862
+ return;
863
+ const loaded = await loadRealization(store, repo, {
864
+ windowDays, persist: true, sinceDays: CLI_PERIOD_DAYS, gitScanBudgetMs: CLI_GIT_BUDGET_MS,
865
+ });
691
866
  if (!loaded) {
692
867
  printNotAGitRepo(repo);
693
868
  process.exitCode = 1;
package/dist/cli.js CHANGED
@@ -10,9 +10,11 @@
10
10
  * segreant prune prune old rows and compact the database
11
11
  */
12
12
  import './util/quiet.js';
13
- import { demoDbPath, envOverrideKey } from './config.js';
13
+ import { join } from 'node:path';
14
+ import { demoDbPath, envOverrideKey, segreantHome } from './config.js';
14
15
  import { packageVersion } from './version.js';
15
- import { parseFlags, UserInputError } from './cli/flags.js';
16
+ import { configureSurvivalStore } from './git/survivalStore.js';
17
+ import { parseFlags, trackFlags, suggestOption, UserInputError } from './cli/flags.js';
16
18
  // Command modules load on demand. Importing every command up front cost about
17
19
  // two seconds on every invocation, `--help` included; a command now pays only
18
20
  // for its own module graph. All specifiers are literal, so the import-closure
@@ -29,6 +31,9 @@ const causalCmd = () => import('./cli/causalCmd.js');
29
31
  const capitalCmd = () => import('./cli/capitalCmd.js');
30
32
  const opsCmd = () => import('./cli/opsCmd.js');
31
33
  const showCmd = () => import('./cli/showCmd.js');
34
+ const planCmd = () => import('./cli/planCmd.js');
35
+ const homeCmd = () => import('./cli/homeCmd.js');
36
+ const quotaCmd = () => import('./cli/quotaCmd.js');
32
37
  const runCmd = () => import('./cli/runCmd.js');
33
38
  const launchCmd = () => import('./cli/launchCmd.js');
34
39
  const backupCmd = () => import('./cli/backupCmd.js');
@@ -51,13 +56,17 @@ function cmdHelpShort() {
51
56
  Usage: segreant <command> [options]
52
57
 
53
58
  Get started
54
- guide Where you are and the single next step (bare "segreant")
59
+ (no command) This month, your plans, what stayed in the code, and the next step
60
+ guide Where you are and the single next step
55
61
  scan Find the AI tools and git repos on this machine and preview
56
62
  a setup plan; --setup imports and correlates them
57
63
  demo --serve Every screen on clearly-labeled synthetic data, no API key
58
64
 
59
65
  Every day
60
66
  today | week | month Spend for a window (--json)
67
+ plan Your plans, what you pay, and the work they did (set <tool> <usd>)
68
+ quota The vendors' usage meters, pace to reset, and Segreant's count (--json)
69
+ statusline Claude Code status line: records its live usage meter (--setup shows how)
61
70
  start Start the proxy and the local dashboard
62
71
  launch -- <command> Run a tool metered through the proxy while it runs
63
72
  budget Set caps: --daily N --soft N --session N --runaway N
@@ -76,15 +85,18 @@ function cmdHelp() {
76
85
  Usage: segreant <command> [options]
77
86
 
78
87
  Commands
88
+ (no command) Bare "segreant": this month, your plans, how much of the
89
+ AI work stayed in the code, and the next step
79
90
  guide Where you are + the single next step, read from your
80
- actual state — also what bare "segreant" shows (--json)
91
+ actual state — the full checklist (--json)
81
92
  start Start the proxy + local dashboard
82
93
  egress Inspect/verify Segreant-process egress; plan exact cloud
83
94
  rules without mutation, then persist only with:
84
95
  egress apply --apply --mode controlled_cloud
85
96
  (--id, --purpose, --data-class, --method, --origin,
86
- --path-prefix). Default local_locked permits literal
87
- loopback only; this is not a machine-wide firewall.
97
+ --path-prefix). Built in: loopback, the public price
98
+ list and the official OpenAI/Anthropic APIs. This is
99
+ not a machine-wide firewall.
88
100
  causal Retained randomized-study evidence: status, inspect,
89
101
  replay verification, and review-only OPE from the
90
102
  Store-owned action log. V1 is inspect-only; all causal
@@ -101,6 +113,10 @@ function cmdHelp() {
101
113
  commitment, consumption, showback, opportunity and
102
114
  policy-relative fairness; no chargeback or action.
103
115
  today | week | month Show spend for a window (--json)
116
+ plan Plans, prices you set, and the list-price work they did
117
+ quota Vendor usage meters and the pace to their reset
118
+ statusline Claude Code status line: records its live usage meter
119
+ on every refresh (--setup prints the line to add)
104
120
  economic Inspect exact economic events, roles, bases, and legacy coverage
105
121
  (--days N | --all, --target-currency UNIT, --as-of <ISO>,
106
122
  --effective-at <ISO>, --json). Period controls use canonical
@@ -116,11 +132,18 @@ function cmdHelp() {
116
132
  recipe). No tool lists the connectors.
117
133
  import <tool|all> NATIVE metering, no routing: read the usage a tool
118
134
  already logs locally — works on subscriptions the proxy
119
- can never see. Tools: claude-code, opencode, codex (or
120
- all). Idempotent. --watch keeps it live (poll every N
135
+ can never see. Tools: claude-code, opencode, codex,
136
+ antigravity (or all). Idempotent. --watch keeps it live (poll every N
121
137
  sec: --every N). Files unchanged since their last full
122
138
  import are skipped; --rescan reads them again.
123
139
  (--root <dir>, --days N, --json)
140
+ alloc <action> Cost-centre allocation: who has DECIDED owns the money.
141
+ alloc centre <id> --name N [--owner O]; alloc rule <id>
142
+ --method direct|fixed_split|proportional_to_direct
143
+ --centre C --match-project P; alloc run --from <date>
144
+ --to <date> previews a split, --apply records it (--json).
145
+ alloc centres | rules | runs list them; alloc revoke <id>
146
+ closes a rule; alloc export <run-id> --out <csv>.
124
147
  billing <action> Import, inspect, or export LOCAL operator-supplied
125
148
  provider billing evidence. V1 accepts a strict OpenAI
126
149
  evidence JSON only; it never overwrites metered estimates
@@ -199,7 +222,7 @@ function cmdHelp() {
199
222
  --include-imported on|off: whether imported subscription
200
223
  spend counts toward cap ENFORCEMENT (default off — the
201
224
  cap governs live proxy traffic, the spend it can block)
202
- budget --recommend Suggest a value-aware budget from usage + realized value
225
+ budget --recommend Suggest a value-aware budget from usage + realized value (--detail: full decision record)
203
226
  (--repo <path> for value-based, --apply to write, --json)
204
227
  budget --control Evaluate one bounded autonomous daily-cap control step
205
228
  from a versioned JSON policy (--policy <file>, --repo <path>,
@@ -226,6 +249,15 @@ function cmdHelp() {
226
249
  when stale; --auto off to disable)
227
250
  Evidence: pricing --coverage [--days N|--all] (--json;
228
251
  read-only historical card/match cohorts)
252
+ Declare: pricing set <model> --input <usd> --output <usd>
253
+ [--cache-read <usd>] [--cache-write <usd>]
254
+ [--cache-write-1h <usd>] (USD per 1M tokens)
255
+ Or: pricing set <model> --like <known-model>
256
+ Or: pricing set <model> --free (user-declared local price)
257
+ Remove: pricing unset <model>
258
+ Still need a price: pricing unknown [--json]
259
+ Declarations leave recorded cost unchanged; segreant reprice
260
+ previews changes, --apply writes them.
229
261
  reprice Re-cost rows that were priced with a fallback estimate,
230
262
  using the current rate card — only rows the card now
231
263
  resolves EXACTLY are rewritten; remaining estimates are
@@ -268,13 +300,11 @@ function cmdHelp() {
268
300
  --version Print the Segreant version
269
301
 
270
302
  Setup
271
- 1) If routing a cloud provider, review its exact rule first:
272
- segreant egress plan --mode controlled_cloud --id openai-inference
273
- --purpose provider_inference --data-class provider_request --method POST
274
- --origin https://api.openai.com --path-prefix /v1/
275
- Persist the reviewed plan only with the same command as:
276
- segreant egress apply --apply ...
277
- (Skip this for a local loopback-only model.)
303
+ 1) OpenAI and Anthropic work with no setup. Another provider (OpenRouter, a
304
+ gateway) needs one rule; a refused request prints the exact command:
305
+ segreant egress apply --apply --mode controlled_cloud --id <as printed>
306
+ --purpose provider_inference --data-class provider_request --method POST
307
+ --origin https://<host> --path-prefix /
278
308
  2) segreant start
279
309
  3) $env:ANTHROPIC_BASE_URL="http://localhost:8090" (PowerShell)
280
310
  $env:OPENAI_BASE_URL="http://localhost:8090/v1"
@@ -287,14 +317,30 @@ function cmdHelp() {
287
317
  }
288
318
  async function main() {
289
319
  const argv = process.argv.slice(2);
290
- // Bare `segreant` opens the guide, not the reference: the tool's first job
291
- // is to tell you where you are and the single next step. `help` is one word away.
292
- const cmd = argv[0] ?? 'guide';
320
+ // Survival measurements are kept between runs in the home directory, so a
321
+ // later value run spends its git budget on what an earlier one did not reach.
322
+ // Only the CLI (and the dashboard it starts) turns this on.
323
+ configureSurvivalStore(join(segreantHome(), 'survival'));
324
+ // Bare `segreant` opens the home view, not the reference: what was spent this
325
+ // month, what stayed in the code, and the next step (or, on a new machine, the
326
+ // one command that reads the logs). `guide` keeps the full checklist.
327
+ // Flags with no command (`segreant --demo`) are flags for the home view; the
328
+ // help text says --demo works on any read command, and bare `segreant` is one.
329
+ // --help/-h and --version/-v stay commands of their own.
330
+ // Only long options qualify: parseFlags reads `--name` alone, so a single-dash
331
+ // token here would be swallowed as a positional and `segreant -bogus` would
332
+ // render the home view and exit 0 (PR #142 review).
333
+ const flagsOnly = argv[0] !== undefined && argv[0].startsWith('--')
334
+ && !['--help', '--version', '--'].includes(argv[0]);
335
+ const cmd = flagsOnly ? 'home' : argv[0] ?? 'home';
336
+ const rest = flagsOnly ? argv : argv.slice(1);
293
337
  // `exec` wraps another command: everything after the bare `--` belongs to the
294
338
  // wrapped command verbatim and must never be flag-parsed.
295
339
  const sep = argv.indexOf('--');
296
340
  const wraps = cmd === 'exec' || cmd === 'launch';
297
- const flags = parseFlags(wraps && sep !== -1 ? argv.slice(1, sep) : argv.slice(1));
341
+ const parsed = parseFlags(wraps && sep !== -1 ? argv.slice(1, sep) : rest);
342
+ const tracked = trackFlags(parsed);
343
+ const flags = tracked.flags;
298
344
  const wrapped = wraps && sep !== -1 ? argv.slice(sep + 1) : [];
299
345
  // Demo mode: point every store-open at an isolated demo.db and flag surfaces
300
346
  // to render the DEMO label. One switch covers the CLI and the in-process
@@ -331,6 +377,17 @@ async function main() {
331
377
  case 'month':
332
378
  (await showCmd()).cmdShow('month', flags);
333
379
  break;
380
+ case 'plan':
381
+ case 'plans':
382
+ (await planCmd()).cmdPlan(flags);
383
+ break;
384
+ case 'quota':
385
+ case 'limits':
386
+ (await quotaCmd()).cmdQuota(flags);
387
+ break;
388
+ case 'statusline':
389
+ (await quotaCmd()).cmdStatusline(flags);
390
+ break;
334
391
  case 'economic':
335
392
  case 'economics':
336
393
  (await economicCmd()).cmdEconomic(flags);
@@ -377,12 +434,15 @@ async function main() {
377
434
  case 'export':
378
435
  (await showCmd()).cmdExport(flags);
379
436
  break;
437
+ case 'home':
438
+ (await homeCmd()).cmdHome(flags);
439
+ break;
380
440
  case 'guide':
381
441
  case 'next':
382
442
  await (await opsCmd()).cmdGuide(flags);
383
443
  break;
384
444
  case 'doctor':
385
- await (await opsCmd()).cmdDoctor();
445
+ await (await opsCmd()).cmdDoctor(flags);
386
446
  break;
387
447
  case 'audit':
388
448
  await (await opsCmd()).cmdAudit(flags);
@@ -506,6 +566,27 @@ async function main() {
506
566
  default:
507
567
  console.error(` Unknown command: ${cmd}\n Run "segreant help" for usage.`);
508
568
  process.exitCode = 1;
569
+ return;
570
+ }
571
+ // Long-running commands keep reading options after this point, so only
572
+ // commands that have finished are checked.
573
+ // A command that already failed has said why; its exit code stays.
574
+ // --watch and --serve count only when the command itself read them: read
575
+ // here through the tracking proxy they would mark themselves used, and
576
+ // `segreant --watch` would render the home view and exit 0 (PR #142 review).
577
+ const consumed = (key) => parsed[key] === true && !tracked.unread().includes(key);
578
+ const longRunning = cmd === 'start' || wraps || consumed('watch') || consumed('serve');
579
+ const failed = typeof process.exitCode === 'number' && process.exitCode !== 0;
580
+ if (!longRunning && !failed) {
581
+ const unread = tracked.unread().filter((k) => k !== 'debug');
582
+ for (const k of unread) {
583
+ const near = suggestOption(k);
584
+ console.error(near === k
585
+ ? ` --${k} does nothing for "segreant ${cmd}"; nothing was done with it.`
586
+ : ` Unknown option --${k} for "segreant ${cmd}"; it did nothing.${near ? ` Did you mean --${near}?` : ''}`);
587
+ }
588
+ if (unread.length > 0)
589
+ process.exitCode = 2;
509
590
  }
510
591
  }
511
592
  // A reader closing early (e.g. `segreant scan | head`) makes further console.log