Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions HANDOFF-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,12 @@ money".

### UI

The header meter ([ui/widgets/quota_meter.py](ui/widgets/quota_meter.py)) gains a project
selector and shows three bars rather than one. The Quota tab gains a per-project breakdown by
stage. Both read the ledger; no new logic in the UI, per §10.
The header meter ([ui/models.py](ui/models.py) `quota_model`, rendered by
[ui/windows/quota.py](ui/windows/quota.py)) gains a project selector and shows three bars
rather than one. The Quota window gains a per-project breakdown by stage. Both read the
ledger; no new logic in the UI, per §10. (The tab strip and `ui/widgets/*` this section
originally named were replaced by the window system; the behaviour described here is
unchanged.)

---

Expand Down Expand Up @@ -675,9 +678,17 @@ about exit 7 the hard way.
python -m tools.evolve init --task-dir pipeline/evolve-lr --json
python -m tools.evolve run --project proj-scaling-w2 --generations 20 --local --json
python -m tools.evolve status --campaign camp-... --json
python -m tools.evolve halt --campaign camp-... --json # stops at the next generation boundary
python -m tools.evolve promote --campaign camp-... --candidate 47 --json # into a normal run
```

`halt` is a request written to the ledger, not a signal: `run` holds the loop in
whatever process started it, so the UI and a second terminal have no handle on
it. The loop reads the request between generations — the same boundary the
budget gate stops at, and for the same reason. Killing mid-generation would
abandon an in-flight candidate, which goes stale and blocks every future
submission (exit 7).

Default models per §16: Sonnet 5 primary plus Haiku 4.5 explorer.

Sources: [ShinkaEvolve](https://github.com/SakanaAI/ShinkaEvolve) ·
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,15 @@ core/ the machinery the CLIs share, so no tool can forget a rule
haiku.py funnel stages 0 and 3, via forced SDK tools
http.py Semantic Scholar, rerank, embeddings, Context7
tools/ the CLIs
ui/ NiceGUI app and the widgets
config/jupyter/ the Lab server config: framing headers and overrides
ui/ the NiceGUI workspace: a tiling shell over eleven windows
tokens.py the design tokens; the stylesheet is generated from them
layout.py the pane tree and the moves over it -- pure, tested
models.py what each window shows, as plain data -- pure, tested
registry.py the one list of windows the shell is derived from
shell.py the chrome, and how a window survives a retile
windows/ eleven renderers, none of which read a ledger directly
jupyter_theme.py the same tokens, emitted as JupyterLab's custom.css
config/jupyter/ the Lab server config: framing headers, overrides, theme
skills/ loaded on demand, not into the default context
ledger/ expectations.jsonl, runs.jsonl, quota.jsonl, projects.jsonl,
campaigns.jsonl, candidates.jsonl, preflight records
Expand Down
204 changes: 204 additions & 0 deletions config/jupyter/custom/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/* generated by ui/jupyter_theme.py — edit the tokens, not this file */

/* ------------------------------------------------------------------ tokens */
:root {
--jp-layout-color0: #F7F3E8;
--jp-layout-color1: #F7F3E8;
--jp-layout-color2: #EFE8D8;
--jp-layout-color3: #E8E2D4;
--jp-layout-color4: #E8E2D4;

--jp-inverse-layout-color0: #14100C;
--jp-inverse-layout-color1: #14100C;
--jp-inverse-layout-color2: #14100C;
--jp-inverse-layout-color3: #8A8272;

--jp-ui-font-color0: #14100C;
--jp-ui-font-color1: #14100C;
--jp-ui-font-color2: #8A8272;
--jp-ui-font-color3: #9C9484;
--jp-content-font-color0: #14100C;
--jp-content-font-color1: #14100C;
--jp-content-font-color2: #8A8272;
--jp-content-font-color3: #9C9484;

--jp-border-color0: #14100C;
--jp-border-color1: #14100C;
--jp-border-color2: rgba(20,16,12,0.15);
--jp-border-color3: rgba(20,16,12,0.15);
--jp-border-width: 2px;
--jp-border-radius: 0px;

--jp-brand-color0: #14100C;
--jp-brand-color1: #FFD400;
--jp-brand-color2: #FFD400;
--jp-brand-color3: #FFD400;
--jp-accent-color1: #12A594;
--jp-warn-color0: #FFD400;
--jp-warn-color1: #FFD400;
--jp-error-color0: #A3122F;
--jp-error-color1: #A3122F;
--jp-success-color1: #12A594;
--jp-info-color1: #B04A2C;

--jp-cell-editor-background: #FFFDF8;
--jp-cell-editor-border-color: #14100C;
--jp-cell-editor-active-background: #FFFDF8;
--jp-cell-editor-active-border-color: #14100C;
--jp-cell-prompt-not-active-opacity: 0.55;
--jp-cell-inprompt-font-color: #14100C;
--jp-cell-outprompt-font-color: #B04A2C;

--jp-code-font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
--jp-code-font-size: 13.5px;
--jp-code-line-height: 1.7;
--jp-ui-font-family: 'Space Grotesk', Helvetica, Arial, sans-serif;
--jp-ui-font-size1: 13px;
--jp-content-font-family: 'Space Grotesk', Helvetica, Arial, sans-serif;
--jp-content-font-size1: 15px;
--jp-content-line-height: 1.58;

--jp-toolbar-background: #EFE8D8;
--jp-toolbar-border-color: #14100C;
--jp-toolbar-box-shadow: none;
--jp-notebook-padding: 0px;

--jp-rendermime-error-background: #FDEEF1;
--jp-rendermime-table-row-background: #F7F3E8;
--jp-rendermime-table-row-hover-background: #FDFAF2;

/* Radius 0 everywhere, and no blur shadows anywhere. */
--jp-elevation-z0: none;
--jp-elevation-z1: none;
--jp-elevation-z2: none;
--jp-elevation-z4: none;
--jp-elevation-z6: none;
--jp-elevation-z8: none;
--jp-elevation-z12: none;
--jp-elevation-z16: none;
--jp-elevation-z20: none;
--jp-elevation-z24: none;
}

/* -------------------------------------------------------------- structure */
body, .jp-Notebook, .jp-NotebookPanel { background: #F7F3E8; }
* { border-radius: 0 !important; box-shadow: none !important; }

.jp-Toolbar {
background: #EFE8D8;
border-bottom: 2px solid #14100C;
min-height: 30px;
}

/* The gutter: paper-sunk, a 2px right rule, right-aligned mono prompt. */
.jp-InputArea-prompt, .jp-OutputArea-prompt {
background: #EFE8D8;
border-right: 2px solid #14100C;
font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
font-size: 12px;
color: #14100C;
text-align: right;
padding-right: 9px;
}

/* The 6px state stripe, driven by what Lab already knows about the cell. */
.jp-Cell {
border-left: 6px solid transparent;
border-bottom: 1px solid rgba(20,16,12,0.15);
padding: 0;
}
.jp-Cell.jp-mod-selected { border-left-color: #14100C; background: #F7F3E8; }
.jp-Cell:has(.jp-OutputArea-output[data-mime-type="application/vnd.jupyter.stderr"]) {
border-left-color: #A3122F;
}
.jp-Cell:has(.jp-InputArea-prompt:empty) { border-left-color: #FFD400; }

.jp-CodeCell .jp-Editor {
background: #FFFDF8;
border: 0;
}
.jp-InputArea-editor { border: 0; border-left: 1px solid rgba(20,16,12,0.15); }

/* -------------------------------------------------------------- markdown */
.jp-RenderedHTMLCommon h1,
.jp-RenderedHTMLCommon h2,
.jp-RenderedHTMLCommon h3,
.jp-RenderedHTMLCommon h4 {
font-family: 'Instrument Serif', Georgia, 'Times New Roman', serif;
font-weight: 400;
color: #14100C;
margin: 14px 0 6px;
}
.jp-RenderedHTMLCommon h1 { font-size: 30px; }
.jp-RenderedHTMLCommon h2 { font-size: 21px; }
.jp-RenderedHTMLCommon h3 { font-size: 19px; }
.jp-RenderedHTMLCommon p { max-width: 74ch; }
.jp-RenderedHTMLCommon a { color: #B04A2C; text-underline-offset: 2px; }
.jp-RenderedHTMLCommon code { background: #EFE8D8; padding: 1px 4px; }
.jp-RenderedHTMLCommon .MathJax_Display,
.jp-RenderedHTMLCommon mjx-container[display="true"] {
border: 2px solid #14100C;
background: #FFFDF8;
padding: 12px;
}

/* ---------------------------------------------------------------- output */
.jp-OutputArea-output {
background: #F7F3E8;
font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
font-size: 12.5px;
}
.jp-RenderedText {
font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
font-size: 12.5px;
line-height: 1.7;
color: #14100C;
}
.jp-RenderedText[data-mime-type="application/vnd.jupyter.stderr"] {
background: #FDEEF1;
border-left: 6px solid #A3122F;
color: #5A1020;
}

.jp-RenderedHTMLCommon table { border-collapse: collapse; font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace; }
.jp-RenderedHTMLCommon thead th {
background: #14100C;
color: #F7F3E8;
text-align: left;
padding: 7px 9px;
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.jp-RenderedHTMLCommon tbody td { border-bottom: 1px solid rgba(20,16,12,0.15); padding: 7px 9px; }
.jp-RenderedHTMLCommon tbody tr:nth-child(even) { background: #FDFAF2; }

.jp-RenderedImage img { border: 2px solid #14100C; background: #FFFDF8; }

/* ----------------------------------------------------------------- chrome */
.jp-SideBar, .jp-FileBrowser, .jp-DirListing { background: #EFE8D8; }
.jp-DirListing-item.jp-mod-selected { background: #14100C; color: #F7F3E8; }
.jp-Toolbar-item .jp-ToolbarButtonComponent:hover { background: #EFE8D8; }
.lm-TabBar-tab.lm-mod-current {
background: #14100C !important;
color: #F7F3E8 !important;
border: 0;
}
.lm-TabBar-tab { background: #EFE8D8; border-right: 1px solid rgba(20,16,12,0.15); }
.jp-Statusbar, #jp-bottom-panel {
background: #14100C;
color: #F7F3E8;
font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;
font-size: 11px;
}

/* CodeMirror token colours, matching the host's code blocks exactly. */
.cm-editor .cm-content { font-family: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace; font-size: 13.5px; }
.cm-keyword, .cm-atom { color: #14100C; font-weight: 700; }
.cm-def, .cm-variable-2, .cm-property { color: #B04A2C; }
.cm-number, .cm-string { color: #0B7B6E; }
.cm-comment { color: #9C9484; font-style: italic; }
.cm-operator, .cm-punctuation { color: #8A8272; }
.cm-editor .cm-ruler { border-right: 1px dashed rgba(20,16,12,0.3); }
.cm-cursor { border-left: 2px solid #14100C; }
.cm-editor .cm-selectionBackground { background: #FFD400 !important; }
2 changes: 1 addition & 1 deletion config/jupyter/overrides.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"@jupyterlab/apputils-extension:themes": {
"theme": "JupyterLab Dark"
"theme": "JupyterLab Light"
},
"@jupyterlab/notebook-extension:tracker": {
"recordTiming": true
Expand Down
69 changes: 66 additions & 3 deletions core/campaign.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,16 @@
from typing import Any

from core import jsonl, paths
from core.errors import NotFound
from core.errors import EXIT_USAGE, GradError, NotFound

T_CAMPAIGN = "campaign"
T_GENERATION = "campaign_generation"
T_CAMPAIGN_CLOSED = "campaign_closed"
T_HALT_REQUESTED = "campaign_halt_requested"
T_CANDIDATE = "candidate"
T_CANDIDATE_PROMOTED = "candidate_promoted"

STATUSES = ("open", "closed", "exhausted", "failed")
STATUSES = ("open", "closed", "exhausted", "failed", "halted")

# Shinka's own markers, kept verbatim so a task directory works with the
# upstream tool unmodified.
Expand Down Expand Up @@ -152,11 +153,23 @@ def campaigns() -> dict[str, dict[str, Any]]:
folded[cid] = {**{k: v for k, v in rec.items() if k != "type"}, "generations_run": 0}
elif cid in folded and kind == T_GENERATION:
node = folded[cid]
node["generations_run"] = max(node["generations_run"], int(rec.get("generation", 0)) + 1)
# A record carrying `halted` documents the generation the loop
# stopped *before*, not one it ran -- both the budget gate and a
# halt request write one at the boundary they break on. Counting it
# reported a campaign halted after generation 0 as having run two,
# which is the number the evolve window puts in its title bar.
if not rec.get("halted"):
node["generations_run"] = max(
node["generations_run"], int(rec.get("generation", 0)) + 1
)
node["last_generation_at"] = rec.get("at")
node.setdefault("generation_log", []).append(
{k: v for k, v in rec.items() if k not in ("type", "id")}
)
elif cid in folded and kind == T_HALT_REQUESTED:
folded[cid]["halt_requested"] = True
folded[cid]["halt_requested_at"] = rec.get("at")
folded[cid]["halt_reason"] = rec.get("reason")
elif cid in folded and kind == T_CAMPAIGN_CLOSED:
folded[cid]["status"] = rec.get("status", "closed")
folded[cid]["closed_at"] = rec.get("at")
Expand Down Expand Up @@ -191,6 +204,56 @@ def close_campaign(campaign_id: str, *, status: str = "closed", reason: str = ""
)


def request_halt(campaign_id: str, *, reason: str = "") -> dict[str, Any]:
"""Ask a running campaign to stop at the next generation boundary.

A *request*, written to the ledger, rather than a signal: `evolve run` holds
the loop in whatever process started it -- usually the agent's -- and the UI
or a second terminal has no handle on it. An event both processes can see is
the only mechanism that works across all three callers, and it has the
side benefit of being diffable afterwards.

The boundary matters as much as the request. Killing the loop mid-generation
abandons an in-flight candidate, which goes stale and blocks every future
submission (exit 7) -- so `_drive` checks this exactly where the budget gate
already stops cleanly, with every candidate collected.

The campaign must exist and still be open, and that is checked *inside the
append lock* for the same reason `append_run_event` re-checks its binding
there: the loop closing a campaign and a human asking it to halt are two
processes racing over one file, and a check made before the lock is a check
the other process can win. `tools.evolve halt` still checks first, because
it produces the better message; this is the backstop, not the explanation.
"""

def _still_open() -> None:
record = campaigns().get(campaign_id)
if record is None:
raise NotFound(
f"campaign {campaign_id!r} does not exist",
fix="python -m tools.evolve status --json # lists known campaigns",
)
status = record.get("status")
if status != "open":
raise GradError(
"campaign_not_open",
f"campaign {campaign_id!r} is {status}, so there is nothing to halt",
exit_code=EXIT_USAGE,
fix=f"python -m tools.evolve status --campaign {campaign_id} --json",
)

return jsonl.append(
campaigns_path(),
{"type": T_HALT_REQUESTED, "id": campaign_id, "at": now_iso(), "reason": reason},
precondition=_still_open,
)


def halt_requested(campaign_id: str) -> bool:
record = campaigns().get(campaign_id) or {}
return bool(record.get("halt_requested"))


# ---------------------------------------------------------------------------
# candidates
# ---------------------------------------------------------------------------
Expand Down
Loading