@@ -20,7 +20,16 @@ import type {
2020} from '@colony/storage' ;
2121import type { Command } from 'commander' ;
2222import kleur from 'kleur' ;
23- import { withStorage } from '../util/store.js' ;
23+ import { withStorage , withStore } from '../util/store.js' ;
24+
25+ /**
26+ * Observation kind written by `colony gain` to mark a savings-review
27+ * invocation. `colony health --coach` reads this kind to detect step 7 of
28+ * the first-week ladder ("review your savings"). Kept in sync with
29+ * `apps/cli/src/commands/health-coach.ts::GAIN_REVIEW_OBSERVATION_KIND`.
30+ */
31+ const COACH_GAIN_REVIEW_KIND = 'coach_gain_review' ;
32+ const COACH_GAIN_REVIEW_SESSION_ID = 'observer' ;
2433
2534interface GainOptions {
2635 json ?: boolean ;
@@ -135,8 +144,7 @@ export function registerGainCommand(program: Command): void {
135144 const recentHours = resolveRecentHours ( opts . recentHours , windowHours ) ;
136145 const recentSince = recentHours !== null ? now - recentHours * 60 * 60_000 : null ;
137146
138- const summaryRequested =
139- opts . summary === true || opts . graph === true || opts . daily === true ;
147+ const summaryRequested = opts . summary === true || opts . graph === true || opts . daily === true ;
140148 const dailyDays = parsePositiveInt ( opts . days ) ?? 30 ;
141149 const topOpsLimit = parsePositiveInt ( opts . topOps ) ?? 10 ;
142150 const dailySince = summaryRequested
@@ -224,6 +232,16 @@ export function registerGainCommand(program: Command): void {
224232 ...livePayload ,
225233 } ;
226234
235+ // Record a lightweight `coach_gain_review` observation so the coach
236+ // walkthrough can detect step 7 ("review savings") on the next
237+ // `colony health --coach`. Best-effort: a failure here must never
238+ // mask the gain output the user came for.
239+ try {
240+ await recordCoachGainReview ( opts ) ;
241+ } catch {
242+ // Swallow — see comment above.
243+ }
244+
227245 if ( opts . json === true ) {
228246 process . stdout . write ( `${ JSON . stringify ( payload , null , 2 ) } \n` ) ;
229247 return ;
@@ -349,6 +367,27 @@ export function registerGainCommand(program: Command): void {
349367 } ) ;
350368}
351369
370+ async function recordCoachGainReview ( opts : GainOptions ) : Promise < void > {
371+ const settings = loadSettings ( ) ;
372+ await withStore ( settings , ( store ) => {
373+ store . startSession ( {
374+ id : COACH_GAIN_REVIEW_SESSION_ID ,
375+ ide : 'observer' ,
376+ cwd : process . cwd ( ) ,
377+ } ) ;
378+ store . addObservation ( {
379+ session_id : COACH_GAIN_REVIEW_SESSION_ID ,
380+ kind : COACH_GAIN_REVIEW_KIND ,
381+ content : 'colony gain invocation recorded by the coach walkthrough' ,
382+ metadata : {
383+ summary : opts . summary === true ,
384+ json : opts . json === true ,
385+ operation : opts . operation ?? null ,
386+ } ,
387+ } ) ;
388+ } ) ;
389+ }
390+
352391export function writeGainReport (
353392 referenceRows : ReadonlyArray < SavingsReferenceRow > ,
354393 referenceTotals : SavingsReferenceTotals ,
@@ -1284,10 +1323,7 @@ export function renderImpactBar(value: number, max: number, width: number): stri
12841323 if ( ! Number . isFinite ( value ) || ! Number . isFinite ( max ) || max <= 0 || width <= 0 ) {
12851324 return '░' . repeat ( Math . max ( 0 , width ) ) ;
12861325 }
1287- const filled = Math . min (
1288- width ,
1289- Math . max ( 0 , Math . round ( ( Math . max ( 0 , value ) / max ) * width ) ) ,
1290- ) ;
1326+ const filled = Math . min ( width , Math . max ( 0 , Math . round ( ( Math . max ( 0 , value ) / max ) * width ) ) ) ;
12911327 return '█' . repeat ( filled ) + '░' . repeat ( Math . max ( 0 , width - filled ) ) ;
12921328}
12931329
@@ -1394,7 +1430,9 @@ export function writeSummaryReport(input: SummaryReportInput): void {
13941430
13951431 if ( showHeadline ) {
13961432 const filter = operationFilter ? ` (op=${ operationFilter } )` : '' ;
1397- w . write ( `${ kleur . bold ( `Colony Token Savings (last ${ formatHoursLabel ( windowHours ) } ${ filter } )` ) } \n` ) ;
1433+ w . write (
1434+ `${ kleur . bold ( `Colony Token Savings (last ${ formatHoursLabel ( windowHours ) } ${ filter } )` ) } \n` ,
1435+ ) ;
13981436 w . write ( `${ HEAVY_RULE } \n` ) ;
13991437 writeSummaryHeadline ( totals , comparison , costBasis ) ;
14001438
@@ -1461,20 +1499,15 @@ function writeSummaryHeadline(
14611499 ) ;
14621500 }
14631501 if ( savingsPct !== null ) {
1464- const savedLabel = savedTokens >= 0
1465- ? `${ formatTokens ( savedTokens ) } (${ formatPctSigned ( savingsPct ) } )`
1466- : `${ formatTokens ( Math . abs ( savedTokens ) ) } over (${ formatPctSigned ( savingsPct ) } )` ;
1502+ const savedLabel =
1503+ savedTokens >= 0
1504+ ? `${ formatTokens ( savedTokens ) } (${ formatPctSigned ( savingsPct ) } )`
1505+ : `${ formatTokens ( Math . abs ( savedTokens ) ) } over (${ formatPctSigned ( savingsPct ) } )` ;
14671506 lines . push ( [ 'Tokens saved:' , savedLabel ] ) ;
14681507 } else {
1469- lines . push ( [
1470- 'Tokens saved:' ,
1471- kleur . dim ( '— (no reference baseline matched in this window)' ) ,
1472- ] ) ;
1508+ lines . push ( [ 'Tokens saved:' , kleur . dim ( '— (no reference baseline matched in this window)' ) ] ) ;
14731509 }
1474- lines . push ( [
1475- 'Total exec time:' ,
1476- `${ formatDurationMs ( totalMs ) } (avg ${ formatDurationMs ( avgMs ) } )` ,
1477- ] ) ;
1510+ lines . push ( [ 'Total exec time:' , `${ formatDurationMs ( totalMs ) } (avg ${ formatDurationMs ( avgMs ) } )` ] ) ;
14781511
14791512 for ( const [ label , value ] of lines ) {
14801513 w . write ( `${ padVisible ( kleur . dim ( label ) , labelWidth ) } ${ value } \n` ) ;
@@ -1489,9 +1522,7 @@ function writeSummaryHeadline(
14891522 const colored = colorByEfficiency ( meterPct , `${ meter } ${ pctLabel } ` ) ;
14901523 w . write ( `${ padVisible ( kleur . dim ( 'Efficiency meter:' ) , labelWidth ) } ${ colored } \n` ) ;
14911524 } else {
1492- w . write (
1493- `${ padVisible ( kleur . dim ( 'Efficiency meter:' ) , labelWidth ) } ${ kleur . dim ( '—' ) } \n` ,
1494- ) ;
1525+ w . write ( `${ padVisible ( kleur . dim ( 'Efficiency meter:' ) , labelWidth ) } ${ kleur . dim ( '—' ) } \n` ) ;
14951526 }
14961527}
14971528
@@ -1530,8 +1561,8 @@ function writeSummaryByOperation(
15301561 }
15311562
15321563 const sorted = [ ...operations ] . sort ( ( a , b ) => {
1533- const savedA = savedByOp . get ( a . operation ) ?? - Infinity ;
1534- const savedB = savedByOp . get ( b . operation ) ?? - Infinity ;
1564+ const savedA = savedByOp . get ( a . operation ) ?? Number . NEGATIVE_INFINITY ;
1565+ const savedB = savedByOp . get ( b . operation ) ?? Number . NEGATIVE_INFINITY ;
15351566 if ( savedA !== savedB ) return savedB - savedA ;
15361567 return b . total_tokens - a . total_tokens ;
15371568 } ) ;
@@ -1584,15 +1615,14 @@ function writeSummaryByOperation(
15841615 w . write ( `${ kleur . dim ( '-' . repeat ( SUMMARY_TABLE_WIDTH ) ) } \n` ) ;
15851616}
15861617
1587- function writeSummaryDailyGraph (
1588- daily : ReadonlyArray < McpMetricsDailyRow > ,
1589- days : number ,
1590- ) : void {
1618+ function writeSummaryDailyGraph ( daily : ReadonlyArray < McpMetricsDailyRow > , days : number ) : void {
15911619 const w = process . stdout ;
15921620 const window = fillDailyWindow ( daily , days ) ;
15931621 const maxTokens = window . reduce ( ( m , row ) => Math . max ( m , row . total_tokens ) , 0 ) ;
15941622 w . write ( `${ kleur . bold ( `Daily Activity (last ${ days } days)` ) } \n` ) ;
1595- w . write ( `${ kleur . dim ( '-' . repeat ( SUMMARY_GRAPH_LABEL_WIDTH + 3 + SUMMARY_GRAPH_BAR_WIDTH + 1 + SUMMARY_GRAPH_VALUE_WIDTH ) ) } \n` ) ;
1623+ w . write (
1624+ `${ kleur . dim ( '-' . repeat ( SUMMARY_GRAPH_LABEL_WIDTH + 3 + SUMMARY_GRAPH_BAR_WIDTH + 1 + SUMMARY_GRAPH_VALUE_WIDTH ) ) } \n` ,
1625+ ) ;
15961626 if ( maxTokens === 0 ) {
15971627 w . write ( kleur . dim ( ' (no token activity in window)\n' ) ) ;
15981628 return ;
@@ -1607,10 +1637,7 @@ function writeSummaryDailyGraph(
16071637 }
16081638}
16091639
1610- function writeSummaryDailyBreakdown (
1611- daily : ReadonlyArray < McpMetricsDailyRow > ,
1612- days : number ,
1613- ) : void {
1640+ function writeSummaryDailyBreakdown ( daily : ReadonlyArray < McpMetricsDailyRow > , days : number ) : void {
16141641 const w = process . stdout ;
16151642 const window = fillDailyWindow ( daily , days ) . slice ( - SUMMARY_BREAKDOWN_LIMIT ) ;
16161643 const totals = window . reduce (
@@ -1625,7 +1652,9 @@ function writeSummaryDailyBreakdown(
16251652 { calls : 0 , input_tokens : 0 , output_tokens : 0 , total_tokens : 0 , total_duration_ms : 0 } ,
16261653 ) ;
16271654
1628- w . write ( `${ kleur . bold ( `Daily Breakdown (${ window . length } day${ window . length === 1 ? '' : 's' } )` ) } \n` ) ;
1655+ w . write (
1656+ `${ kleur . bold ( `Daily Breakdown (${ window . length } day${ window . length === 1 ? '' : 's' } )` ) } \n` ,
1657+ ) ;
16291658 const ruleWidth = 74 ;
16301659 w . write ( `${ kleur . dim ( '=' . repeat ( ruleWidth ) ) } \n` ) ;
16311660 const head = [
0 commit comments