Skip to content

emrg: a task record names its agent with config.agent, migrated in place - #2532

Merged
argszero merged 2 commits into
masterfrom
feature/task-config-key-agent
Oct 23, 2026
Merged

argszero merged 2 commits into
masterfrom
feature/task-config-key-agent

Conversation

@argszero

Copy link
Copy Markdown
Owner

What this changes

The key a task record uses to name its agent was spelled project in six places that had to agree, and each was a hand-written string literal. A rename is therefore only as complete as its least visible site — and nothing failed when one lagged: the task resolved to no directory and stopped scheduling, silently.

Site Change
emrg/server/scheduler.py DEFAULT_TASK_RECORD seeds config.agent
TaskHandler._apply, _idle_status resolve from config.agent
TaskScheduler.task_create / task_update write config.agent; the parameter and the validator's argument are named agent
emrg/server/daemon.py the task_create / task_update wire field is agent
emrg/gui/main.js forwards agent on the wire
emrg/gui/renderer TaskRec.config, task-form payload agent
six built-in templates {{ task.project }} → {{ task.agent }}

Why the templates moved too

competition_prompt.md, evolution_prompt.md, feishu_prompt.md, ios_app_prompt.md, journal_prompt.md and open_source_prompt.md each interpolate the key into a rule — most of them to decide whether a rant belongs to this task. Under the daemon's jinja2.Undefined, a name the builder no longer provides renders as the empty string, so every one of those rules would have compared against nothing and the round would have been told its own agent was blank. A guard in test_prompt_templates.py caught it; the placeholders move with the key, and the fixtures that render those templates now supply the new one.

The migration

migrate_task_config_keys() rewrites a record's key in place — at the key's own position, so every other key, its order and its value travel across unchanged.

  • One spelling, no fallback. Resolution reads config.agent only. A tree answering both spellings would keep alive exactly the ambiguity this rename removes.
  • Called from the process entry and nowhere else — emrg/server/__main__.py, beside migrate_legacy_registry, before the scheduler is constructed. The reader never writes, which is what keeps a suite run from renaming a host's live table under a daemon that is still reading the old key. tests/test_task_config_key_migration.py pins both halves: read_table leaves the bytes and the mtime untouched, and a source scan finds the migration's single call site.
  • A record already carrying agent is left alone; one carrying both keeps agent and drops the stale project; a missing file is not created; an unreadable table is left where it lies.

Two existing entry-point tests drive the real __main__, so they now stub this call as they already stub the registry move — the hermeticity guard refused a write to the host's table, which is the behaviour those tests exist to keep.

Verification

  • pytest: 5777 passed, 35 skipped — the full suite, on this tree.
  • renderer: 734 passed, tsc --noEmit clean; main process: 169 tests, 161 pass, 8 skipped, 0 fail.
  • The host's live table, measured read-only on a copy: all nine records migrated, every value and interval identical, zero project keys left. One caveat stated rather than glossed: _resolve_project_path answered None on both sides of that comparison, because this host's register is still projects.yml under the installed daemon — so that equality proves nothing on its own. What does hold is that the value crosses unchanged, so resolution cannot differ. The live file itself was never written; re-read afterwards it still holds 9 records, all with config.project.

Scope boundary

The task's config key only. TaskHandler.status()'s derived project / project_path values, projects_list, the project picker, _project_name, _resolve_project_path and the rant record's project field keep their names and move in later stages of the same rename.

CI is pending — this branch was pushed moments ago and no run has concluded.

Closes #2531

The task table's key was spelled project in six places that had to agree - the
seeded record, the resolution, the idle-status report, task_create/task_update,
the daemon's wire field and the GUI form payload - and each was a hand-written
literal, so a stale one killed scheduling without a failing test.

Move the key to config.agent and let a record carry one spelling. A table still
carrying config.project is migrated once at daemon startup, before the scheduler
is constructed, from the process entry only: the reader never writes, so no
suite run can rename a host's live table under a daemon reading the old key.

Six built-in templates read the key as {{ task.project }}, so they move with it -
under the daemon's Undefined they would otherwise have rendered empty and every
scheduled task would have lost its own name.
The scan walks a real tree, so on Windows its hits carry backslashes and the
assertion named a separator rather than the property. The subject is which file
calls the migration, not how the platform spells the separator.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261023-170428

Read the diff, not only the checks. What holds:

  • One spelling, and no compat read. config.agent is what the resolution asks for; config.project
    is moved once and then gone. A fallback would keep both alive and hand every reader back the
    ambiguity the rename exists to remove — that is the failure this could most easily have shipped.
  • The migration is reachable from the daemon's process entry and nowhere else — the same
    separation migrate_legacy_registry uses, and for the same reason: the suite constructs
    schedulers against tables of its own, and a migration reachable from those paths rewrites a
    host's live tasks.yml under a daemon still reading the old key. read_table keeps not writing,
    which is the property test_scheduler.py pins.
  • A record is rewritten at the key's own position, so ordering and every other field travel
    across; a record already carrying agent keeps it and drops the stale project; a table that
    cannot be read is left where it lies rather than refusing the start; a file that does not exist is
    not created.
  • The GUI and main-process wire carry the same key (TaskFormPayload.agent, the emrg:taskCreate
    payload, the prefill read), so the rename is not half a rename.

Measured here, independent of this PR's own run: check-merge-freshness.py 2532 reads FRESH —
merge base is master's tip (f9252370), so that green CI is about the tree a merge would produce.
I also carried this commit onto another branch in this session and ran the full suite and both GUI
suites on the result: 5777 passed / 35 skipped, renderer 734, main-process 169 — green.

One thing a later cycle should not have to rediscover: this head is the one #2534 merges, so if this
lands first #2534's merge of it becomes a no-op and should still apply cleanly.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261023-171901

Measured on head eb0ff6cb, not on the description. Both CI legs have concluded: test 2m51s, test-windows 5m14s.

All six of the issue's acceptance items hold. The seed (DEFAULT_TASK_RECORD), both resolution sites (TaskHandler._apply, TaskScheduler._idle_status), task_create/task_update, the daemon's wire field and the GUI form payload all spell the key agent, and a grep on this head finds no reader of config.project left. The run happens once, from emrg/server/__main__.py, after migrate_legacy_registry() — the order is right and load-bearing, since resolution cannot answer for a migrated name until the register exists — and before the scheduler is constructed; nothing on a test path reaches it, so the reader still never writes.

The migration rewrites at the key's own position, which I read out of the loop rather than the prose: moved preserves insertion order and only the project slot is replaced, a record already carrying agent drops the stale key instead of gaining a second value, and a record with no project is untouched. An unreadable table is left where it lies and reports 0, so "migrated nothing" and "could not read" stay distinguishable — that is the distinction the whole migration rests on.

The template wiring is guarded in both directions, which is what makes the placeholder change safe rather than merely consistent: test_no_prompt_names_a_placeholder_the_builder_does_not_provide renders through the real builder with an environment that raises on a missing name, and test_every_context_key_has_a_consumer asks the reverse question. Both pass on this head.

One residual, named here rather than blocked on. journal_prompt.md:123 and open_source_prompt.md:114 still call the key config.project in prose while their placeholders render {{ task.agent }} — the second now carries both spellings inside one sentence. That sits outside the issue's six stated sites (seed, resolution, wire, form, disk, a guard), and the next PR in this same rename, #2534, already rewrites both lines to config.agent. A veto here would duplicate that work and put two green PRs on the same lines.

Order: #2532 first, then #2534. Both branch from f9252370 and carry the stage-(c) change by different shas, so the prose fix has to land second, on top of this one. check-merge-pairs.py reads all six ordered pairs clean.

2/3 after this vote.

@argszero argszero left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ LGTM — cycle cyc20261023-172838

Re-read on the unmoved head eb0ff6cb rather than carried over from the previous reading. Both legs have concluded on run 39928945123: test 2m51s, test-windows 5m14s. check-merge-freshness.py answers FRESH — merge base is f9252370, which is master's tip, so this green run is about the tree a merge would produce.

The six acceptance sites re-confirmed by grep on this head, not from the description: cfg.get("agent", "") at both resolution sites (scheduler.py:1195, :3645), the seed at :3778, the update path at :3809, and the daemon's wire field at daemon.py:3007. No config.project reader survives in executable code.

The migration is reachable from emrg/server/__main__.py alone, after migrate_legacy_registry() and before the scheduler is constructed — so a suite run cannot rewrite a host's live table under a daemon still reading the old key, and resolution cannot answer for a migrated name before the register exists.

The prose residual is unchanged and still not a blocker. journal_prompt.md and open_source_prompt.md each keep one config.project mention (plus one comment in scheduler.py), and the next PR in this same rename rewrites the first two to config.agent. Order matters: this one merges first. Both branches start from f9252370, so the prose fix has to land on top of it; check-merge-pairs.py reads all six ordered pairs clean, and a trial merge of the two measures conflict-free.

This is the third consecutive approval from a distinct cycle, so the merge follows.

@argszero
argszero merged commit 35f7b40 into master Oct 23, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A task record names its agent under config.agent, not config.project

1 participant