Files
boardroom-map/startos/actions/configure-grading.ts
T
Jonathan KirkwoodandClaude Fable 5 91212322c1 Ship v0.1.1–v0.1.5: first-live-run fixes and dashboard viewer
- v0.1.1: config persistence — FileHelper paths made absolute
  (/media/startos/volumes/main/...); relative paths resolved into the JS
  runtime's ephemeral cwd so action saves never reached /data
- v0.1.2: preJobStopContainers (Configure Grading) — docker-stop resident
  vLLM containers on the head Spark at job start, no auto-restart
- v0.1.3: preflight auth (LiteLLM master_key gates /models),
  poll-until-loaded, crash fast-fail (restarting counts as dead)
- v0.1.4: HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE in airgapped serving
  (--internal network has no DNS); grader _post timeout 600→1800s for
  ~3.6 tok/s GB10 generation
- v0.1.5: dashboard viewer survives the periodic background refresh;
  download buttons for deck reports, deck JSON, and SCORECARD.md
- .gitignore: .startos/ build workspace, start-technologies/

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 09:14:24 -05:00

270 lines
9.5 KiB
TypeScript

import { sdk } from '../sdk'
import { configFile } from '../file-models/config'
const { InputSpec, Value } = sdk
const inputSpec = InputSpec.of({
bdefOverride: Value.textarea({
name: 'BDEF Rubric Override (optional)',
description:
'Leave EMPTY to grade against the baked-in BDEF v1.1 framework ' +
'(Girdley + Munger/Buffett). Non-empty text replaces the rubric wholesale, ' +
'so include the qualitative categories A-H if you customize it.',
required: false,
default: null,
minRows: 5,
maxRows: 24,
placeholder: '(empty = the built-in BDEF v1.1 rubric)',
}),
extractorModel: Value.text({
name: 'Extractor Model (optional)',
description:
'Catalog alias of the model that runs the stage-1 structured KPI ' +
'extraction over each deck. Empty = use the first model.',
required: false,
default: null,
placeholder: 'grader-a',
}),
networkMode: Value.select({
name: 'Network Mode',
description:
'Air-gapped: graders reach ONLY the on-Spark model proxy — zero internet, ' +
'board decks never leave your hardware (models must be pre-pulled into the ' +
'Spark HF cache, all on the head Spark). Local services: graders may also ' +
'reach LAN services like SearXNG and the second Spark (this network has ' +
'egress unless you firewall it).',
default: 'airgapped',
values: {
airgapped: 'Air-gapped (no network, recommended)',
local_services: 'Local services (SearXNG / 2nd Spark)',
},
}),
searxngUrl: Value.text({
name: 'SearXNG URL (local-services only)',
description: 'JSON-search endpoint to give graders a web_search tool. Ignored in air-gapped mode. Empty = no web search.',
required: false,
default: null,
placeholder: 'https://searxng.local',
}),
adjudicatorEnabled: Value.toggle({
name: 'Run an Adjudicator',
description:
'After the panel finishes, run a local "lead grader" that reads every ' +
'grade sheet, reconciles disagreements, and settles the qualitative scores ' +
'the deterministic composite uses. No frontier model — stays on the Sparks.',
default: true,
}),
adjudicatorModel: Value.text({
name: 'Adjudicator Model (optional)',
description: 'Catalog alias of the model that adjudicates the panel. Empty = use the first model.',
required: false,
default: null,
placeholder: 'grader-a',
}),
adjudicatorPersona: Value.textarea({
name: 'Adjudicator Instructions (optional)',
description: 'Override how the adjudicator reconciles the panel. Empty = a sensible built-in default.',
required: false,
default: null,
minRows: 3,
maxRows: 14,
}),
wipeRemoteDocs: Value.toggle({
name: 'Wipe Decks From Sparks After Grading',
description:
'Delete the extracted deck text from the Sparks when a job finishes. ' +
'Scorecards and ledgers are always kept on this StartOS box. Recommended ' +
'for confidential board material.',
default: true,
}),
autoRunOnDrop: Value.toggle({
name: 'Auto-grade When Decks Are Dropped',
description:
'Start grading automatically (after a short debounce) whenever new decks ' +
'land in the inbox. Off by default so you trigger grading explicitly.',
default: false,
}),
preJobStopContainers: Value.text({
name: 'Stop These Containers Before Grading (optional)',
description:
'Space- or comma-separated docker container names stopped on the HEAD ' +
'Spark at the start of every grading job, so the job gets the GPU to ' +
'itself (e.g. an always-on vLLM another service runs). They are NOT ' +
'restarted afterwards — the service that owns them reloads its own ' +
'models (e.g. the Gazette\'s Fleet job). Empty = stop nothing.',
required: false,
default: null,
placeholder: 'vllm-gemma4-clerk',
}),
// --- Deterministic-scorer weights (composite = quant 60 + qual 40 - flags) ---
profitabilityKpi: Value.number({
name: 'Weight: Profitability KPIs',
description: 'Points for the profitability KPI attainment bucket — the heaviest slice of the quant 60. Default 30.',
required: true,
default: 30,
integer: false,
min: 0,
max: 100,
}),
otherKpi: Value.number({
name: 'Weight: Other KPIs',
description: 'Points for the non-profitability measurable-KPI bucket. Default 20.',
required: true,
default: 20,
integer: false,
min: 0,
max: 100,
}),
forecastIntegrity: Value.number({
name: 'Weight: Forecast Integrity',
description: 'Points for deck N actuals hitting what deck N-1 promised. Default 10.',
required: true,
default: 10,
integer: false,
min: 0,
max: 100,
}),
qualCategoryMax: Value.number({
name: 'Weight: Max Per Qualitative Category',
description: 'Max points per BDEF category A-H (8 categories x 5 = the qualitative 40). Default 5.',
required: true,
default: 5,
integer: false,
min: 0,
max: 100,
}),
redFlagCap: Value.number({
name: 'Red-flag Penalty Cap',
description: 'Maximum total points red flags can subtract from the composite. Default 15.',
required: true,
default: 15,
integer: false,
min: 0,
max: 100,
}),
kpiCreditFloor: Value.number({
name: 'KPI Credit Floor',
description: 'actual/target ratio below which a KPI earns zero credit (linear credit above it). Default 0.5.',
required: true,
default: 0.5,
integer: false,
min: 0,
max: 1,
}),
droppedKpiPenalty: Value.number({
name: 'Dropped-KPI Penalty',
description: 'Penalty per KPI that silently disappeared from the deck. Default 2.',
required: true,
default: 2,
integer: false,
min: 0,
max: 100,
}),
droppedKpiMax: Value.number({
name: 'Dropped-KPI Flag Limit',
description: 'Count at most this many dropped-KPI flags per deck. Default 3.',
required: true,
default: 3,
integer: true,
min: 0,
max: 50,
}),
evidenceFullCredit: Value.number({
name: 'Evidence Full-credit Threshold',
description: 'Quote characters a qualitative finding needs for full weight (thinner evidence scales down). Default 400.',
required: true,
default: 400,
integer: true,
min: 0,
max: 100000,
}),
singleSourceFlagFactor: Value.number({
name: 'Single-source Flag Damping',
description: 'Multiplier applied to red flags raised by only one grader. Default 0.5.',
required: true,
default: 0.5,
integer: false,
min: 0,
max: 1,
}),
})
export const configureGrading = sdk.Action.withInput(
'configure-grading',
async ({ effects }) => ({
name: 'Configure Grading',
description: 'Set the BDEF rubric, air-gap mode, adjudication, retention, and the scoring weights.',
warning: null,
allowedStatuses: 'any',
group: 'Grading',
visibility: 'enabled',
}),
inputSpec,
async ({ effects }) => {
const cfg = await configFile.read().const(effects)
if (!cfg) return {}
return {
bdefOverride: cfg.bdefOverride || undefined,
extractorModel: cfg.extractorModel || undefined,
networkMode: cfg.networkMode,
searxngUrl: cfg.searxngUrl || undefined,
adjudicatorEnabled: cfg.adjudicatorEnabled,
adjudicatorModel: cfg.adjudicatorModel || undefined,
adjudicatorPersona: cfg.adjudicatorPersona || undefined,
wipeRemoteDocs: cfg.wipeRemoteDocs,
autoRunOnDrop: cfg.autoRunOnDrop,
preJobStopContainers: cfg.preJobStopContainers || undefined,
profitabilityKpi: cfg.weights.profitabilityKpi,
otherKpi: cfg.weights.otherKpi,
forecastIntegrity: cfg.weights.forecastIntegrity,
qualCategoryMax: cfg.weights.qualCategoryMax,
redFlagCap: cfg.weights.redFlagCap,
kpiCreditFloor: cfg.weights.kpiCreditFloor,
droppedKpiPenalty: cfg.weights.droppedKpiPenalty,
droppedKpiMax: cfg.weights.droppedKpiMax,
evidenceFullCredit: cfg.weights.evidenceFullCredit,
singleSourceFlagFactor: cfg.weights.singleSourceFlagFactor,
}
},
async ({ effects, input }) => {
await configFile.merge(effects, {
bdefOverride: input.bdefOverride ?? '',
extractorModel: input.extractorModel ?? '',
networkMode: input.networkMode,
searxngUrl: input.searxngUrl ?? '',
adjudicatorEnabled: input.adjudicatorEnabled,
adjudicatorModel: input.adjudicatorModel ?? '',
adjudicatorPersona: input.adjudicatorPersona ?? '',
wipeRemoteDocs: input.wipeRemoteDocs,
autoRunOnDrop: input.autoRunOnDrop,
preJobStopContainers: input.preJobStopContainers ?? '',
weights: {
profitabilityKpi: input.profitabilityKpi,
otherKpi: input.otherKpi,
forecastIntegrity: input.forecastIntegrity,
qualCategoryMax: input.qualCategoryMax,
redFlagCap: input.redFlagCap,
kpiCreditFloor: input.kpiCreditFloor,
droppedKpiPenalty: input.droppedKpiPenalty,
droppedKpiMax: input.droppedKpiMax,
evidenceFullCredit: input.evidenceFullCredit,
singleSourceFlagFactor: input.singleSourceFlagFactor,
},
})
return {
version: '1',
title: 'Grading Settings Saved',
message:
input.networkMode === 'airgapped'
? 'Saved. Graders will run air-gapped (no internet). Ensure all models are on the head Spark and pre-pulled into its HF cache.'
: 'Saved. Graders run in local-services mode and may reach the network — make sure that is acceptable for these board decks.',
result: { type: 'single', value: input.networkMode, copyable: false, qr: false, masked: false },
}
},
)