Compare commits
4 Commits
fix/empty-
...
chore/posh
| Author | SHA1 | Date | |
|---|---|---|---|
| ec32c6c8a9 | |||
| 8bed2f75c1 | |||
|
|
2f4a0a114a | ||
|
|
8d750139a1 |
@@ -112,7 +112,7 @@ export function CalibrationScreen({ onBack, data }: { onBack: () => void; data?:
|
||||
<circle key={i} cx={X(e) + ((i % 5) - 2) * 3} cy={Y(a)} r="3" fill="var(--accent)" opacity="0.55" />
|
||||
))}
|
||||
</svg>
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: '8px 0 0' }}>estimated (x) vs actual days (y) · actuals inferred from git events, never tracked</p>
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: '8px 0 0' }}>estimated (x) vs actual days (y)</p>
|
||||
</Card>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||||
@@ -189,9 +189,9 @@ export function CalibrationScreen({ onBack, data }: { onBack: () => void; data?:
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 0' }}>
|
||||
{c.active
|
||||
? 'You are not bad at estimating; you are optimistic in a very stable way. Stable, I can work with.'
|
||||
: 'Not enough closed history yet — I’m forecasting from cold-start priors and widening the cone to stay honest. The curve takes over at 20.'}
|
||||
: 'Not enough closed history yet: I’m forecasting from cold-start priors and widening the cone to stay honest. The curve takes over at 20.'}
|
||||
{!c.active && c.excludedSameDay > 0
|
||||
? ` And ${c.excludedSameDay} closed ${c.excludedSameDay === 1 ? 'issue' : 'issues'} closed the same day they were started — 0 working days can’t calibrate, so they don’t count toward the 20.`
|
||||
? ` And ${c.excludedSameDay} closed ${c.excludedSameDay === 1 ? 'issue' : 'issues'} closed the same day they were started. 0 working days can’t calibrate, so they don’t count toward the 20.`
|
||||
: ''}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -70,10 +70,10 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
setLiveTickets(res.issues)
|
||||
setStage('review')
|
||||
} else {
|
||||
setError(res.ok ? 'I could not find concrete work in that — try a bit more detail.' : `Capture failed: ${res.reason === 'error' ? res.message : 'no model'}`)
|
||||
setError(res.ok ? 'I could not find concrete work in that; try a bit more detail.' : `Capture failed: ${res.reason === 'error' ? res.message : 'no model'}`)
|
||||
}
|
||||
})
|
||||
.catch(() => { setBrewing(false); setError('I could not reach the model.') })
|
||||
.catch(() => { setBrewing(false); setError('I was unable to reach the model, regrettably.') })
|
||||
}
|
||||
|
||||
const fileLive = () => {
|
||||
@@ -84,7 +84,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
.then((res) => {
|
||||
setBrewing(false)
|
||||
if (res.ok) { setFiledCount(res.created.length); setStage('filed') }
|
||||
else setError('Filing failed — gitea is not configured.')
|
||||
else setError('Filing failed; gitea is not configured.')
|
||||
})
|
||||
.catch(() => { setBrewing(false); setError('Filing failed.') })
|
||||
}
|
||||
@@ -99,12 +99,12 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
|
||||
const QUESTIONS: Question[] = [
|
||||
{
|
||||
q: 'The auth work — one ticket, or shall I split token refresh from session storage? They fail differently.',
|
||||
q: 'The auth work: one ticket, or shall I split token refresh from session storage? They fail differently.',
|
||||
chips: ['One ticket', 'Split them'],
|
||||
set: (a) => setSplit(a === 'Split them'),
|
||||
},
|
||||
{
|
||||
q: 'The webhook double-fire — how long? I should mention your "quick" has averaged two days.',
|
||||
q: 'The webhook double-fire, how long? I should mention your "quick" has averaged two days.',
|
||||
chips: ['est/1d', 'est/2d', 'est/3d'],
|
||||
set: (a) => setWebEst(a),
|
||||
},
|
||||
@@ -205,7 +205,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
</header>
|
||||
|
||||
{stage === 'dump' ? (
|
||||
<Card overline="Braindump" title="Tell me what you're planning" jade>
|
||||
<Card overline="Braindump" title="Do tell me what you are planning" jade>
|
||||
<textarea
|
||||
value={dump}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => setDump(e.target.value)}
|
||||
@@ -217,7 +217,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
}}
|
||||
></textarea>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 14px' }}>
|
||||
Sentences, fragments, grievances — all welcome. I'll sort it into tickets and only ask what I can't infer.
|
||||
Sentences, fragments, grievances; all are welcome. I shall sort them into tickets and enquire only where I cannot infer.
|
||||
</p>
|
||||
<Button icon="sparkles" onClick={() => void onBrew()} disabled={brewing}>
|
||||
{brewing ? 'Brewing…' : 'Brew tickets'}
|
||||
@@ -262,7 +262,7 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
{live ? (
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
{tickets.length} issue{tickets.length === 1 ? '' : 's'} from your braindump, estimated and prioritized.
|
||||
Adjust the labels, then approve — I'll open them in gitea with only est/* and p/* labels.
|
||||
Adjust the labels, then approve; I'll open them in gitea with only est/* and p/* labels.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -286,10 +286,10 @@ export function CaptureScreen({ onDone }: { onDone: () => void }) {
|
||||
<Icon name="circle-check" size={22} /> Filed
|
||||
</span>
|
||||
<p style={{ font: 'var(--text-body)', margin: 0 }}>
|
||||
{live ? filedCount : tickets.length} issues opened in gitea with <span style={{ font: 'var(--text-data)' }}>est/*</span> and <span style={{ font: 'var(--text-data)' }}>p/*</span> labels — nothing else touched.
|
||||
{live ? filedCount : tickets.length} issues opened in gitea with <span style={{ font: 'var(--text-data)' }}>est/*</span> and <span style={{ font: 'var(--text-data)' }}>p/*</span> labels, nothing else touched.
|
||||
</p>
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
Elapsed {clock} — under budget. No bot comments, no synthetic issues; your repo remains yours.
|
||||
Elapsed {clock}.
|
||||
</p>
|
||||
<Button iconRight="arrow-right" onClick={onDone}>To morning service</Button>
|
||||
</div>
|
||||
|
||||
@@ -28,9 +28,9 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
|
||||
const mapError = (e?: string) =>
|
||||
e === '401' || e === '403'
|
||||
? 'The token was rejected — check it has repo + issue scopes.'
|
||||
? 'The token was rejected, check it has repo + issue scopes.'
|
||||
: e === '404'
|
||||
? "Couldn't reach that Gitea — check the URL."
|
||||
? "Couldn't reach that Gitea, check the URL."
|
||||
: `Discovery failed${e ? ` (${e})` : ''}.`
|
||||
|
||||
// Discover the owners + repos this token can see. Triggered when the token
|
||||
@@ -96,7 +96,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
</span>
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-body)', color: 'var(--ink-2)', margin: '0 0 18px' }}>
|
||||
Connect your Gitea. Your token is stored encrypted on this machine and never leaves it.
|
||||
Connect your Gitea.
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
@@ -114,7 +114,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
label="Access token"
|
||||
type="password"
|
||||
value={token}
|
||||
placeholder={existing?.hasToken ? '•••••••• (saved — enter a new one to change repos)' : 'gitea PAT · scopes: repo + issue'}
|
||||
placeholder={existing?.hasToken ? '•••••••• (saved, enter a new one to change repos)' : 'gitea PAT · scopes: repo + issue'}
|
||||
hint={
|
||||
discovery === 'discovering'
|
||||
? 'Discovering your repositories…'
|
||||
@@ -157,7 +157,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
value={repo}
|
||||
disabled={!pickerReady || repoOptions.length === 0}
|
||||
options={
|
||||
repoOptions.length ? repoOptions.map((r) => ({ value: r, label: r })) : placeholder('—')
|
||||
repoOptions.length ? repoOptions.map((r) => ({ value: r, label: r })) : placeholder('·')
|
||||
}
|
||||
onChange={(e) => setRepo(e.target.value)}
|
||||
/>
|
||||
@@ -167,7 +167,7 @@ export function ConnectScreen({ onConnected, existing }: { onConnected: () => vo
|
||||
label="Model URL"
|
||||
value={modelUrl}
|
||||
placeholder="http://localhost:1234/v1"
|
||||
hint="Optional — an OpenAI-compatible endpoint for Reginald. Leave blank to keep chat off."
|
||||
hint="Optional: an OpenAI-compatible endpoint for Reginald. Leave blank to keep chat off."
|
||||
onChange={(e) => setModelUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -80,7 +80,7 @@ export function DirectivesScreen() {
|
||||
<EmptyState
|
||||
icon="flag"
|
||||
title="No directives yet"
|
||||
line="When you overrule the scheduler in chat, it goes on the record here — who, when, what, why."
|
||||
line="When you overrule the scheduler in chat, it goes on the record here: who, when, what, why."
|
||||
/>
|
||||
) : (
|
||||
<Card overline="The ledger" flush>
|
||||
@@ -115,10 +115,6 @@ export function DirectivesScreen() {
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
Entries are never edited. Corrections are new entries — the ledger remembers everything, politely.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ export function FocusScreen({
|
||||
<EmptyState
|
||||
icon="coffee"
|
||||
title="Nothing to pour"
|
||||
line="Capture some work, or enjoy the silence — it never lasts."
|
||||
line="Capture some work, or enjoy the silence, it never lasts."
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -113,7 +113,7 @@ export function FocusScreen({
|
||||
<BurnUpCone data={forecast.cone} />
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 0' }}>
|
||||
{forecast.coldStart
|
||||
? `${forecast.scope} open ${forecast.scope === 1 ? 'issue' : 'issues'} in scope. Cold-start priors — ${forecast.calibratedN}/20 estimated closes so far; the cone tightens as the team closes work.`
|
||||
? `${forecast.scope} open ${forecast.scope === 1 ? 'issue' : 'issues'} in scope. Cold-start priors: ${forecast.calibratedN}/20 estimated closes so far; the cone tightens as the team closes work.`
|
||||
: `${forecast.scope} open ${forecast.scope === 1 ? 'issue' : 'issues'} in scope, calibrated on ${forecast.calibratedN} closed ${forecast.calibratedN === 1 ? 'issue' : 'issues'} of your own.`}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { type IssueDetail, type IssueRef } from '../../data/view-types.js'
|
||||
import { Badge, Button, Card, Dialog, Icon, Select, Tag } from '../ui/index.js'
|
||||
|
||||
const NONE = '—'
|
||||
const NONE = '·'
|
||||
|
||||
export function IssueScreen({
|
||||
issue,
|
||||
@@ -214,7 +214,7 @@ export function IssueScreen({
|
||||
<div style={{ marginTop: 14, minHeight: 40 }}>
|
||||
{pendingChanges.length === 0 ? (
|
||||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
No change yet — pick a different estimate, priority, assignee, or milestone.
|
||||
No change yet, pick a different estimate, priority, assignee, or milestone.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
@@ -265,7 +265,7 @@ export function IssueScreen({
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: 'flex', gap: 8, padding: '14px 20px', alignItems: 'flex-end' }}>
|
||||
<textarea placeholder="Comment — this writes to gitea, as you" rows={2} style={{
|
||||
<textarea placeholder="Comment: this writes to gitea, as you" rows={2} style={{
|
||||
flex: 1, resize: 'none', font: 'var(--text-body)', color: 'var(--ink-1)', lineHeight: 1.5,
|
||||
background: 'var(--paper-0)', border: '1px solid var(--line-2)', borderRadius: 'var(--radius-2)',
|
||||
padding: '8px 11px', outline: 'none',
|
||||
@@ -335,12 +335,6 @@ export function IssueScreen({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* provenance note */}
|
||||
<div style={{ padding: '10px 20px 14px', borderTop: '1px solid var(--line-1)' }}>
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
Lives in pm-state. Your repo never sees any of it.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function MilestoneScreen({
|
||||
|
||||
const groups = data.groups.map((g) => ({ label: g.label, issues: g.issues, muted: g.label === 'Done' }))
|
||||
const name = data.name
|
||||
const dueLine = `milestone · due ${data.due} · ${data.soft ? 'soft — scope may flex' : 'hard deadline'}`
|
||||
const dueLine = `milestone · due ${data.due} · ${data.soft ? 'soft, scope may flex' : 'hard deadline'}`
|
||||
const forecastLabel = data.forecastRange ? `80% ${data.forecastRange}` : 'all shipped'
|
||||
const scopeStat = `${data.scopeCount} issues · est ${data.scopeEstDays}d`
|
||||
const doneStat = `${data.doneCount} · ${data.donePct}%`
|
||||
|
||||
@@ -43,9 +43,9 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
setFound(null)
|
||||
setError(
|
||||
res.error === '401' || res.error === '403'
|
||||
? 'The token was rejected — check it has repo + issue scopes.'
|
||||
? 'The token was rejected: check it has repo + issue scopes.'
|
||||
: res.error === '404'
|
||||
? "Couldn't reach that Gitea — check the URL."
|
||||
? "Couldn't reach that Gitea; check the URL."
|
||||
: `Connection failed${res.error ? ` (${res.error})` : ''}.`,
|
||||
)
|
||||
return
|
||||
@@ -66,7 +66,7 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
.catch(() => ({ ok: false as const, error: 'unreachable' }))
|
||||
if (!res.ok) {
|
||||
setBoot('idle')
|
||||
setError(`Bootstrap failed${res.error ? ` (${res.error})` : ''}. Nothing was half-applied — you can retry.`)
|
||||
setError(`Bootstrap failed${res.error ? ` (${res.error})` : ''}. Nothing was half-applied, you can retry.`)
|
||||
return
|
||||
}
|
||||
// persist the connection now that the repo is prepared
|
||||
@@ -155,11 +155,11 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
<Frame footer={<Button iconRight="arrow-right" onClick={() => setStep(1)}>Begin</Button>}>
|
||||
<h1 style={{ font: 'var(--text-title)', color: 'var(--ink-1)', margin: 0 }}>Good morning.</h1>
|
||||
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
I'm Reginald, your project manager. I interview you instead of making you fill in forms, I forecast in
|
||||
honest ranges, and I never do the arithmetic myself — there's a scheduler for that.
|
||||
I am Reginald, your project manager. I interview you rather than trouble you with forms, and I forecast
|
||||
in honest ranges.
|
||||
</p>
|
||||
<p style={{ font: 'var(--text-body)', color: 'var(--ink-2)', margin: 0 }}>
|
||||
Your plans live in your own Gitea as ordinary issues and labels. Delete me and nothing human is lost.
|
||||
Your plans reside in your own Gitea as ordinary issues and labels.
|
||||
</p>
|
||||
</Frame>
|
||||
) : null}
|
||||
@@ -196,7 +196,7 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
type="password"
|
||||
value={token}
|
||||
placeholder="gitea PAT"
|
||||
hint="Scopes: repo, issue. Nothing more."
|
||||
hint="Scopes: repo, issue."
|
||||
onChange={(e) => {
|
||||
setToken(e.target.value)
|
||||
setDiscovery('idle')
|
||||
@@ -352,14 +352,11 @@ export function OnboardingScreen({ onConnected }: { onConnected: (dest: 'focus'
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: 0 }}>
|
||||
No bot comments, no body frontmatter, no synthetic issues — ever. Labels are the only footprint.
|
||||
</p>
|
||||
</Frame>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}>first run · everything reversible</span>
|
||||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}>first run</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -76,7 +76,6 @@ export function SettingsScreen({
|
||||
<Icon name="layers" size={12} />
|
||||
sidecar: {connection.pmStateRepo ?? `${connection.repo}-pm-state`}
|
||||
</Row>
|
||||
<Note>Your token is stored encrypted on this machine. Delete the sidecar and resync — no truth is lost.</Note>
|
||||
<Row style={{ gap: 8, marginTop: 2 }}>
|
||||
<Button variant="secondary" size="sm" icon="settings-2" onClick={onReconnect}>
|
||||
Reconfigure
|
||||
@@ -105,11 +104,7 @@ export function SettingsScreen({
|
||||
Reconciles on launch and after every write
|
||||
</span>
|
||||
</Row>
|
||||
<Note>
|
||||
The board is a rebuildable index over gitea — the durable truth. When gitea is unreachable the last
|
||||
snapshot is served from cache (the rail's dot turns red), and the next successful reconcile supersedes it.
|
||||
No webhooks, no polling: reads are on demand.
|
||||
</Note>
|
||||
<Note>Offline, the board reads from the last cached snapshot.</Note>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -127,7 +122,7 @@ export function SettingsScreen({
|
||||
<Row style={{ gap: 8 }}>
|
||||
<Icon name="sparkles" size={14} style={{ color: 'var(--ink-3)' }} />
|
||||
<span style={{ font: 'var(--text-body)', color: 'var(--ink-2)', flex: 1 }}>
|
||||
Chat is off — no model reachable.
|
||||
Chat is off: no model reachable.
|
||||
</span>
|
||||
<Button variant="secondary" size="sm" onClick={onReconnect}>
|
||||
Set a Model URL
|
||||
@@ -135,8 +130,7 @@ export function SettingsScreen({
|
||||
</Row>
|
||||
)}
|
||||
<Note>
|
||||
An OpenAI-compatible endpoint (set it in Reconfigure). Reginald names, negotiates and explains; it never
|
||||
does the arithmetic — the scheduler and Monte Carlo do that.
|
||||
An OpenAI-compatible endpoint (set it in Reconfigure).
|
||||
</Note>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -154,7 +148,7 @@ export function SettingsScreen({
|
||||
))}
|
||||
<Tag label="deadline/hard" />
|
||||
</Row>
|
||||
<Note>Fixed sets, human-meaningful, visible in gitea. Not configurable — that is rather the point.</Note>
|
||||
<Note>The fixed label vocabulary, visible in gitea.</Note>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ export function StandupScreen({
|
||||
<EmptyState
|
||||
icon="sun"
|
||||
title="Nothing to report"
|
||||
line="No overnight drift — I'll have a plan when there's work to pour."
|
||||
line="No overnight drift. I'll have a plan when there's work to pour."
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -228,7 +228,7 @@ export function StandupScreen({
|
||||
}}
|
||||
>
|
||||
<p style={{ font: 'var(--text-agent-lg)', color: 'var(--ink-1)', margin: 0, flex: 1 }}>
|
||||
The kettle’s on. — R.
|
||||
The kettle is on. Yours, Reginald.
|
||||
</p>
|
||||
<Button variant="ghost" onClick={onBegin}>
|
||||
Ask about the drift
|
||||
|
||||
@@ -399,7 +399,7 @@ export function AppShell() {
|
||||
backlog.status === 'loading'
|
||||
? `Connecting to ${hostLabel}…`
|
||||
: offline
|
||||
? `Offline — reading from cache${staleSince ? ` (since ${new Date(staleSince).toLocaleString()})` : ''}`
|
||||
? `Offline, reading from cache${staleSince ? ` (since ${new Date(staleSince).toLocaleString()})` : ''}`
|
||||
: `Connected to ${hostLabel}`
|
||||
}
|
||||
aria-label={
|
||||
|
||||
@@ -198,9 +198,6 @@ export function ChatPanel({ onOpenDirectives, offline, onApplyChange }: ChatPane
|
||||
<Icon name="send" size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: '8px 2px 0' }}>
|
||||
Chat is the write-path. Destructive changes are proposed, never assumed.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -200,7 +200,7 @@ export function calibrationData(
|
||||
return {
|
||||
label: `est/${b}d`,
|
||||
n: fit ? fit.n : inBucket.length,
|
||||
median: inBucket.length ? `${(b * Math.exp(mu)).toFixed(1)}d` : '—',
|
||||
median: inBucket.length ? `${(b * Math.exp(mu)).toFixed(1)}d` : '·',
|
||||
bias: fit ? pctFromMu(fit.mu) : null,
|
||||
}
|
||||
})
|
||||
@@ -216,7 +216,7 @@ export function calibrationData(
|
||||
.filter((i) => i.state === 'open')
|
||||
.reduce((sum, i) => sum + (i.facts.estimateDays ?? 2), 0)
|
||||
const effect = model.coldStart
|
||||
? { raw: `${model.n}/${COLD_START_THRESHOLD} estimated closes`, banded: 'cold-start priors', p50: '—' }
|
||||
? { raw: `${model.n}/${COLD_START_THRESHOLD} estimated closes`, banded: 'cold-start priors', p50: '·' }
|
||||
: {
|
||||
raw: `${openEst}d estimated`,
|
||||
banded: `×${Math.exp(model.global.mu).toFixed(2)} median drift`,
|
||||
|
||||
@@ -6,12 +6,12 @@ import { type ChatMessage } from '../data/view-types.js'
|
||||
|
||||
const LIVE_GREETING: ChatMessage = {
|
||||
from: 'agent',
|
||||
text: 'Morning. Ask me anything about the project — I check the real board before I answer.',
|
||||
text: 'Good morning. Do ask me anything about the project; I shall consult the board before I venture an answer.',
|
||||
}
|
||||
|
||||
const NO_MODEL: ChatMessage = {
|
||||
from: 'agent',
|
||||
text: "Reginald is off — I need a model. Add a Model URL in Settings and chat turns on.",
|
||||
text: 'I am afraid I am rather off duty without a model. Kindly add a Model URL in Settings and I shall be at your service.',
|
||||
}
|
||||
|
||||
export interface ChatState {
|
||||
@@ -116,14 +116,17 @@ export function useChat(onApplyChange?: (change: IssueChange) => Promise<{ ok: b
|
||||
...c,
|
||||
{
|
||||
from: 'agent',
|
||||
text: res.reason === 'error' ? `I hit a snag: ${res.message ?? 'unknown error'}` : 'No model is configured.',
|
||||
text:
|
||||
res.reason === 'error'
|
||||
? `I have struck a snag, I am afraid: ${res.message ?? 'unknown error'}.`
|
||||
: 'No model is configured.',
|
||||
},
|
||||
])
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
finish()
|
||||
setConvo((c) => [...c, { from: 'agent', text: 'I could not reach the model.' }])
|
||||
setConvo((c) => [...c, { from: 'agent', text: 'I was unable to reach the model, regrettably.' }])
|
||||
})
|
||||
},
|
||||
[live],
|
||||
@@ -139,8 +142,8 @@ export function useChat(onApplyChange?: (change: IssueChange) => Promise<{ ok: b
|
||||
{
|
||||
from: 'agent',
|
||||
text: res.ok
|
||||
? `Done — #${p.change.issue}: ${p.summary}. The plan's been re-run.`
|
||||
: `That didn't take — #${p.change.issue} is unchanged.`,
|
||||
? `Very good. #${p.change.issue}: ${p.summary}. The plan has been re-run.`
|
||||
: `Regrettably that did not take; #${p.change.issue} remains unchanged.`,
|
||||
},
|
||||
])
|
||||
})
|
||||
@@ -150,7 +153,7 @@ export function useChat(onApplyChange?: (change: IssueChange) => Promise<{ ok: b
|
||||
|
||||
const dismiss = useCallback((p: ChangeProposal) => {
|
||||
drop(p)
|
||||
setConvo((c) => [...c, { from: 'agent', text: `Left #${p.change.issue} as it was.` }])
|
||||
setConvo((c) => [...c, { from: 'agent', text: `As you wish. #${p.change.issue} remains as it was.` }])
|
||||
}, [])
|
||||
|
||||
return { msgs: [...seed, ...convo], thinking, live, model, steps, proposals, streaming, send, approve, dismiss }
|
||||
|
||||
@@ -53,7 +53,7 @@ export function depsGraphView(d: ProjectData): DepsData {
|
||||
nodes: [],
|
||||
milestone: fallback
|
||||
? { name: fallback.title, due: fallback.dueOn ? formatShort(new Date(fallback.dueOn)) : 'no date', col: 0, row: 0 }
|
||||
: { name: '—', due: 'no date', col: 0, row: 0 },
|
||||
: { name: '·', due: 'no date', col: 0, row: 0 },
|
||||
edges: [],
|
||||
critical: [],
|
||||
unattached: [],
|
||||
@@ -134,7 +134,7 @@ export function depsGraphView(d: ProjectData): DepsData {
|
||||
let milestone: DepsData['milestone']
|
||||
const medianRow = median(nodes.map((n) => n.row))
|
||||
if (openMilestones.length === 0) {
|
||||
milestone = { name: '—', due: 'no date', col: maxCol + 1, row: medianRow }
|
||||
milestone = { name: '·', due: 'no date', col: maxCol + 1, row: medianRow }
|
||||
} else {
|
||||
let best: { id: number; title: string; dueOn: string | null | undefined; count: number } | null = null
|
||||
for (const m of openMilestones) {
|
||||
|
||||
@@ -8,7 +8,7 @@ const TOTAL_CAP = 12
|
||||
|
||||
/** "HH:MM" from an ISO timestamp; '—' when absent (never fabricated). */
|
||||
function hhmm(iso: string | null | undefined): string {
|
||||
if (!iso) return '—'
|
||||
if (!iso) return '·'
|
||||
const d = new Date(iso)
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
@@ -103,7 +103,7 @@ export function inboxView(d: ProjectData): InboxItem[] {
|
||||
// left at the honest placeholder rather than borrowed from an issue.
|
||||
text: `${m.title} closed`,
|
||||
detail: 'milestone complete',
|
||||
time: '—',
|
||||
time: '·',
|
||||
unread: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function issueDetailView(id: number, d: ProjectData): IssueDetail | null
|
||||
return {
|
||||
state: inf.column === 'review' ? 'review' : inf.column,
|
||||
assignee: issue.assignee ?? 'unassigned',
|
||||
milestone: issue.milestone?.title ?? '—',
|
||||
milestone: issue.milestone?.title ?? '·',
|
||||
body: issue.body,
|
||||
comments: [],
|
||||
lifecycle: lifecycle(inf.stages),
|
||||
|
||||
@@ -11,7 +11,7 @@ export const QUERY_PROJECT_TOOL: ToolDecl = {
|
||||
name: 'query_project',
|
||||
description:
|
||||
'Read the current project state. A `view` selects the shape; deterministic code (scheduler, ' +
|
||||
'lifecycle inference, calibration) backs every number — you report it, you never compute it.',
|
||||
'lifecycle inference, calibration) backs every number: you report it, you never compute it.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -39,11 +39,11 @@ export const QUERY_PROJECT_TOOL: ToolDecl = {
|
||||
export const PROPOSE_CHANGE_TOOL: ToolDecl = {
|
||||
name: 'propose_change',
|
||||
description:
|
||||
'Propose a change to an issue — estimate, priority, assignee, and/or milestone. This does NOT apply ' +
|
||||
'Propose a change to an issue: estimate, priority, assignee, and/or milestone. This does NOT apply ' +
|
||||
'anything; it shows the human a diff to approve. Use it whenever the user asks to re-estimate, reprioritize, ' +
|
||||
'assign someone (or unassign), or move an issue to a milestone. Pass a login for `assignee` (null to ' +
|
||||
'unassign) and a milestone id for `milestone` (null to remove). After calling it, tell the user you have ' +
|
||||
'*proposed* the change for approval — never say it is done.',
|
||||
'*proposed* the change for approval; never say it is done.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -60,7 +60,7 @@ export const PROPOSE_CHANGE_TOOL: ToolDecl = {
|
||||
export const RECORD_DIRECTIVE_TOOL: ToolDecl = {
|
||||
name: 'record_directive',
|
||||
description:
|
||||
'Log a standing instruction from the PM to the durable directive ledger — a reprioritization, ' +
|
||||
'Log a standing instruction from the PM to the durable directive ledger: a reprioritization, ' +
|
||||
'a re-estimate policy, a deadline, a scope or capacity call, or a plain note. Use it when the user ' +
|
||||
'states intent that should persist ("pilots come first", "freeze scope for beta"). This records the ' +
|
||||
'intent verbatim; the actual issue edits still go through propose_change.',
|
||||
@@ -86,11 +86,11 @@ export const RECORD_DIRECTIVE_TOOL: ToolDecl = {
|
||||
export const REGINALD_TOOLS: ToolDecl[] = [QUERY_PROJECT_TOOL, PROPOSE_CHANGE_TOOL, RECORD_DIRECTIVE_TOOL]
|
||||
|
||||
export const REGINALD_SYSTEM = [
|
||||
'You are Reginald, the calm, dry project manager inside CommiTea — a tool that runs projects on Gitea.',
|
||||
'Call query_project to ground every answer in the real project; never invent issues, numbers, or dates.',
|
||||
'The scheduler and forecasts are deterministic code — report their output, do not recompute it.',
|
||||
'To change an estimate, priority, assignee, or milestone, call propose_change — it shows the human a diff to approve.',
|
||||
'When the PM states standing intent ("pilots first", "freeze scope"), call record_directive to log it.',
|
||||
'Never claim a change is applied; you propose, the human approves. Forecasts are ranges, never single dates.',
|
||||
'Refer to issues as #<number>. Be brief and plain — a sentence or two. No preamble, no bullet dumps.',
|
||||
'You are Reginald, the unfailingly composed project manager within CommiTea, a tool that runs projects on Gitea. You carry yourself with the dry, understated poise of a seasoned butler: courteous, precise, and never effusive.',
|
||||
'Ground every answer in the real project by calling query_project first; you never invent issues, figures, or dates.',
|
||||
'The scheduler and forecasts are deterministic; report their output faithfully and do not attempt the arithmetic yourself.',
|
||||
'To alter an estimate, priority, assignee, or milestone, call propose_change, which lays the matter before the principal as a diff for approval.',
|
||||
'When the principal states standing intent (say, "pilots first" or "freeze scope"), record it with record_directive.',
|
||||
'Never declare a change applied; you propose, and the principal disposes. Forecasts are offered as ranges, never a single date.',
|
||||
'Refer to issues as #<number>. Be concise and courteous: a sentence or two, no preamble, no bulleted dumps. Write in plain prose, and never use an em dash; a comma, colon, or full stop will serve.',
|
||||
].join(' ')
|
||||
|
||||
@@ -53,10 +53,11 @@ export const PROPOSE_ISSUES_TOOL: ToolDecl = {
|
||||
}
|
||||
|
||||
export const CAPTURE_SYSTEM = [
|
||||
'You are Reginald, decomposing a rough braindump into a small set of concrete Gitea issues.',
|
||||
'Call propose_issues exactly once. Split genuinely separate work; merge trivially-coupled work; invent no scope.',
|
||||
'Each issue gets an imperative title, a one-line body, an estimate (est/1d…8d) and a priority (p/1…4).',
|
||||
'Estimate honestly — a "quick" task is rarely one day. Keep the set tight; three good issues beat eight vague ones.',
|
||||
'You are Reginald, distilling a rough braindump into a small set of concrete Gitea issues, with your customary butlerly precision.',
|
||||
'Call propose_issues exactly once. Separate genuinely distinct work; consolidate trivially-coupled work; invent no scope.',
|
||||
'Each issue receives an imperative title, a one-line body, an estimate (est/1d…8d) and a priority (p/1…4).',
|
||||
'Estimate honestly; a "quick" task is seldom a single day. Keep the set tight, for three considered issues surpass eight vague ones.',
|
||||
'Write in plain prose, and never use an em dash; a comma, colon, or full stop will serve.',
|
||||
].join(' ')
|
||||
|
||||
function isEstimate(v: unknown): v is EstimateLabel {
|
||||
|
||||
@@ -66,6 +66,6 @@ describe('memory-v0 (#27)', () => {
|
||||
|
||||
it('degrades to just focus when there is no charter or directives', () => {
|
||||
const out = assembleHotContext({ charter: '', directives: [], focus })
|
||||
expect(out).toBe(['## Focus', 'Now: #7 Fix lifecycle inference', 'Next: #8 Webhook listener', 'Later: —'].join('\n'))
|
||||
expect(out).toBe(['## Focus', 'Now: #7 Fix lifecycle inference', 'Next: #8 Webhook listener', 'Later: ·'].join('\n'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,7 +46,7 @@ export interface HotContextInputs {
|
||||
|
||||
function focusBlock(focus: Focus | null): string {
|
||||
if (!focus) return ''
|
||||
const slot = (label: string, item: Focus['now']) => (item ? `${label}: #${item.number} ${item.title}` : `${label}: —`)
|
||||
const slot = (label: string, item: Focus['now']) => (item ? `${label}: #${item.number} ${item.title}` : `${label}: ·`)
|
||||
return ['## Focus', slot('Now', focus.now), slot('Next', focus.next), slot('Later', focus.later)].join('\n')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user