From 650e2a76b2f22768ed7b92414ca3cb649f4d8d73 Mon Sep 17 00:00:00 2001 From: Sue the Coder Date: Wed, 9 Sep 2026 12:23:29 -0700 Subject: [PATCH 1/2] feat(prelude): WORKSPACE block tells the agent about local-disk tiering Step (3) of docs/local-disk-tiering.org. When tiering is on, the prelude gains a "WORKSPACE (local-disk tiering)" block after the target prompt: the working clone's path (literal for salloc launches, spelled with $SLURM_JOB_ID for confined ones, where the id is not yet known), the shared mirror as origin with the mailbox rule, what the post-commit hook prints and what a REJECTED publish means, the snapshot cadence and the one-line `git log` that shows the last snapshot (the prelude is rendered before launch, so it cannot print the time itself), where caches and $TMPDIR live, the per-mirror warn file, where the handoff note goes, and that a job dispatched to another node cannot see the clone. Docs: persistent-presence.org's handoff path moves into the working tree (committed) since writing into ~/mirrors/ would block every publish; README and the design note record step (3). Correction to PR #11's body: _maybe_run_poetry_auto_install and _maybe_suggest_mcp_servers already return early for remote targets, so there was no shared-mirror gap to close. Tests: block present with literal paths on an unconfined launch (staged prelude captured over the executor), absent without tiering, runtime job id and "deadline warning only" cadence on a confined launch. 719 passed CI-style; mypy unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01StcQgXQDE4F6eJer1sEXRb --- README.org | 4 +- docs/local-disk-tiering.org | 10 +++-- docs/persistent-presence.org | 8 ++-- sucoder/mirror.py | 60 +++++++++++++++++++++++++++++ tests/test_mirror.py | 73 ++++++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 7 deletions(-) diff --git a/README.org b/README.org index 08f8edd..40bc453 100644 --- a/README.org +++ b/README.org @@ -741,7 +741,9 @@ hand (an uncommitted tracked change there makes =updateInstead= refuse every publish from the clone; the prepare step warns loudly if it finds one). Ignored files (=.venv=, =node_modules=) are rebuilt each job; committed work is durable instantly, dirty work to within -=wip_snapshot_minutes=. +=wip_snapshot_minutes=. The agent is told all of this in a +=WORKSPACE (local-disk tiering)= block of its prelude, including the +command that shows the last snapshot. Earlier releases put the /whole/ mirror at =/local/mirrors= on one node. That layout is retired: a session that still records it gets a diff --git a/docs/local-disk-tiering.org b/docs/local-disk-tiering.org index 6074867..1f245a8 100644 --- a/docs/local-disk-tiering.org +++ b/docs/local-disk-tiering.org @@ -8,7 +8,9 @@ Design note. Written on a =carleton-htc= slice (job 38661192, n0036.savio4) against =main= at =80581c8=. Status: step (1) landed (shared timer + snapshotter, PR #9); step (2) landed for confined targets (PR #10) and for =salloc= targets, retiring the all-on-=/local= -layout (=sucoder/local_tier.py=, =slurm.local_disk= / =--local-disk=). +layout (=sucoder/local_tier.py=, =slurm.local_disk= / =--local-disk=); +step (3) landed: the prelude carries a =WORKSPACE (local-disk tiering)= +block whenever tiering is on (=MirrorManager._workspace_block=). * Punchline @@ -187,8 +189,10 @@ state the durability tiers: intervened. - Ignored :: never durable; rebuilt from scratch each job. -The prelude can print the last snapshot time so the agent knows the -loop is alive. +The prelude is rendered before launch, so it cannot print the last +snapshot time; it gives the one-line =git log= that answers it +instead, plus the clone and mirror paths, the mailbox rule, and the +warn-file path. * Config and docs diff --git a/docs/persistent-presence.org b/docs/persistent-presence.org index dd0f358..303958f 100644 --- a/docs/persistent-presence.org +++ b/docs/persistent-presence.org @@ -466,9 +466,11 @@ and makes re-grab fast when turnover does happen. Recommended: =time: "3-00:00:00"= with =max_idle= as the real courtesy guard. ** Handoff / rehydration -- Before turnover the agent writes a structured note to a fixed NFS - path, e.g. =~/mirrors//.sucoder/handoff.org=: current task, - branch, last commit SHA, open questions, next action. +- Before turnover the agent writes a structured note to + =.sucoder/handoff.org= in its working tree and commits it: current + task, branch, last commit SHA, open questions, next action. Under + local-disk tiering the commit is what carries it to the shared mirror + (writing into =~/mirrors/= by hand would block every publish). - On relaunch, =system_prompt_extra= instructs the agent to *first* read the handoff + =git log -5= + =git status=, summarize the resumed state back to the human, then continue. This turns a cold diff --git a/sucoder/mirror.py b/sucoder/mirror.py index 16acb21..f25000f 100644 --- a/sucoder/mirror.py +++ b/sucoder/mirror.py @@ -5254,6 +5254,13 @@ def _compose_context_prelude(self, ctx: MirrorContext) -> str: if target_block: blocks.append(target_block) + # Under local-disk tiering the agent's cwd is a node-local clone, + # not the mirror the prompts talk about; say so, with the facts + # it needs (where commits go, what survives, where to look). + workspace_block = self._workspace_block(ctx) + if workspace_block: + blocks.append(workspace_block) + agent_doc = self._agent_doc_block(ctx) if agent_doc: blocks.append(agent_doc) @@ -5316,6 +5323,59 @@ def _target_prompt_block(self, ctx: MirrorContext) -> Optional[str]: header = f"TARGET PROMPT ({self._collapse_home(prompt_path)})" return f"{header}\n{content}" + def _workspace_block(self, ctx: MirrorContext) -> Optional[str]: + """Describe the local-disk tiering layout to the agent, when it applies. + + Rendered on the laptop before launch, so it cannot carry runtime + state (last snapshot time); it gives the command that answers that + instead. For a confined launch the job id is not known yet and the + path is spelled with ``$SLURM_JOB_ID``, which the batch body exports. + """ + if not ctx.is_remote: + return None + local_disk_root = getattr(self.executor, "local_disk_root", None) or None + if not local_disk_root: + return None + token = _sanitize_session_token(ctx.settings.name) + try: + mirror_path = self._resolve_remote_path(ctx) + except Exception as exc: # noqa: BLE001 - informational block, never fatal + self.logger.debug("Workspace block: could not resolve mirror path: %s", exc) + mirror_path = ctx.remote_mirror_path or "~/mirrors/" + job_id = None if ctx.confined else getattr(self.executor, "slurm_job_id", None) + root = local_disk_root.rstrip("/") or "/" + if job_id: + work = work_path(local_disk_root, token, int(job_id)) + local_root = f"{root}/job{job_id}" + else: + work = f"{root}/job$SLURM_JOB_ID/mirrors/{token}" + local_root = f"{root}/job$SLURM_JOB_ID" + slurm = ctx.settings.remote.slurm if ctx.settings.remote else None + minutes = slurm.wip_snapshot_minutes if slurm else 10 + cadence = ( + f"every {minutes} minutes and at each deadline warning" + if minutes else "at each deadline warning only" + ) + lines = [ + "WORKSPACE (local-disk tiering)", + "You are working in a node-local clone, not in the shared mirror the prompts above describe.", + f"- Working clone (your cwd): {work}", + f"- Shared mirror (origin; durable; the human's push/pull target): {mirror_path}", + " Never edit its working tree by hand: an uncommitted tracked change there blocks every publish from this clone.", + "- Every commit is published to the shared mirror by a post-commit hook the moment it exists; the commit output", + " shows 'SUCODER: published'. A 'REJECTED' line means the human pushed first: `git pull --ff-only`, then commit", + " again. Never force-push to origin.", + f"- Uncommitted work (tracked or untracked, not ignored) is snapshotted to refs/sucoder/wip/{token} on the shared", + f" mirror {cadence}; the next launch restores it if no commit has landed since.", + f" Last snapshot: git -C {mirror_path} log -1 --format='%ci %s' refs/sucoder/wip/{token}", + f"- Ignored files (.venv, node_modules, caches) are never durable. Caches and $TMPDIR live under {local_root}", + " ($SUCODER_LOCAL_ROOT) and are rebuilt each job.", + f"- Deadline warnings: $HOME/.cache/sucoder/slurm-deadline-{token}.warn (30/15/5 minutes before the job's --time).", + "- Handoff notes go to .sucoder/handoff.org in this clone, committed.", + "- A job you dispatch to another node cannot see this clone: give it the shared mirror or a branch you have committed.", + ] + return "\n".join(lines) + def _agent_doc_block(self, ctx: MirrorContext) -> Optional[str]: """Inject ``AGENT.md`` / ``AGENT.org`` for non-Claude agents. diff --git a/tests/test_mirror.py b/tests/test_mirror.py index 6ae1c40..a4f7ddc 100644 --- a/tests/test_mirror.py +++ b/tests/test_mirror.py @@ -1034,6 +1034,79 @@ def fake_run_agent(args, **kwargs): assert not calls[-1]["args"][-1].startswith("export ") +def _staged_prelude(calls): + """The prelude text externalized over SSH stdin for a remote launch.""" + for c in calls: + a = c["args"] + if a[0] == "sh" and "prelude-" in a[2] and c["kwargs"].get("input"): + return c["kwargs"]["input"] + raise AssertionError("no prelude was staged") + + +def test_prelude_workspace_block_unconfined_local_tier(tmp_path, monkeypatch): + """With tiering on, the prelude tells the agent where it works, where + commits go, what survives, and how to check the last snapshot; the + unconfined path knows the job id, so paths are literal.""" + manager, ctx = _remote_launch_manager(tmp_path, monkeypatch, local_disk_root="/local") + manager.config.system_prompt = tmp_path / "sys.org" + manager.config.system_prompt.write_text("SYS\n") + calls = [] + + def fake_run_agent(args, **kwargs): + calls.append({"args": list(args), "kwargs": kwargs}) + return CommandResult(requested_args=list(args), executed_args=list(args), + stdout="", stderr="", returncode=0) + + monkeypatch.setattr(manager.executor, "run_agent", fake_run_agent) + manager.launch_agent(ctx, sync=False) + + prelude = _staged_prelude(calls) + assert "WORKSPACE (local-disk tiering)" in prelude + assert "Working clone (your cwd): /local/job1234567/mirrors/sample" in prelude + assert "Shared mirror (origin; durable; the human's push/pull target): /global/home/users/coder/mirrors/sample" in prelude + assert "refs/sucoder/wip/sample" in prelude + assert "git -C /global/home/users/coder/mirrors/sample log -1 --format='%ci %s' refs/sucoder/wip/sample" in prelude + assert "slurm-deadline-sample.warn" in prelude + assert "every 10 minutes and at each deadline warning" in prelude + assert ".sucoder/handoff.org in this clone" in prelude + # Ordering: after the system prompt, before the skill catalog. + assert prelude.index("SYSTEM PROMPT") < prelude.index("WORKSPACE (local-disk tiering)") + + +def test_prelude_workspace_block_absent_without_local_tier(tmp_path, monkeypatch): + manager, ctx = _remote_launch_manager(tmp_path, monkeypatch, local_disk_root=None) + manager.config.system_prompt = tmp_path / "sys.org" + manager.config.system_prompt.write_text("SYS\n") + calls = [] + + def fake_run_agent(args, **kwargs): + calls.append({"args": list(args), "kwargs": kwargs}) + return CommandResult(requested_args=list(args), executed_args=list(args), + stdout="", stderr="", returncode=0) + + monkeypatch.setattr(manager.executor, "run_agent", fake_run_agent) + manager.launch_agent(ctx, sync=False) + assert "WORKSPACE (local-disk tiering)" not in _staged_prelude(calls) + + +def test_prelude_workspace_block_confined_uses_runtime_job_id(tmp_path, monkeypatch): + """A confined launch renders the prelude before sbatch assigns the job + id, so the clone path is spelled with $SLURM_JOB_ID.""" + manager, ctx = _confined_manager(tmp_path, monkeypatch) + manager.executor.local_disk_root = "/local" + ctx.settings.remote.slurm.wip_snapshot_minutes = 0 + calls = [] + manager.executor.run_agent = _confined_responder(calls, sbatch_out="9") + + manager._launch_confined( + ctx, ["claude"], remote_prelude_text=manager._compose_context_prelude(ctx), + prelude_sentinel="__X__", env=None, detached=True, + ) + prelude = next(c["input"] for c in calls if c["args"][0] == "sh" and "prelude-" in c["args"][2]) + assert "Working clone (your cwd): /local/job$SLURM_JOB_ID/mirrors/sample" in prelude + assert "at each deadline warning only" in prelude + + def test_build_remote_agent_cmd_str_joins_and_appends_exec_bash(tmp_path: Path) -> None: """The extracted helper joins the command and appends ``; exec bash -l``. From 85607c58e47b29b5c8172fe7a492aad52b21824e Mon Sep 17 00:00:00 2001 From: Sue the Coder Date: Wed, 9 Sep 2026 12:26:46 -0700 Subject: [PATCH 2/2] fix(prelude): confined clone path via the exported $SUCODER_LOCAL_ROOT The confined WORKSPACE block spelled the clone as /job$SLURM_JOB_ID/mirrors/; an agent pasting that into a tool gets a literal "$SLURM_JOB_ID" segment. $SUCODER_LOCAL_ROOT is what the batch body actually exports before tmux starts, and it is the same directory, so spell the path with it and say it is exported. Adds an end-to-end launch_agent test for a confined target with tiering (executor has no job id yet): the block reaches the staged prelude in the confined spelling. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01StcQgXQDE4F6eJer1sEXRb --- sucoder/mirror.py | 13 +++++++++---- tests/test_mirror.py | 27 ++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/sucoder/mirror.py b/sucoder/mirror.py index f25000f..ccf8c9f 100644 --- a/sucoder/mirror.py +++ b/sucoder/mirror.py @@ -5329,7 +5329,8 @@ def _workspace_block(self, ctx: MirrorContext) -> Optional[str]: Rendered on the laptop before launch, so it cannot carry runtime state (last snapshot time); it gives the command that answers that instead. For a confined launch the job id is not known yet and the - path is spelled with ``$SLURM_JOB_ID``, which the batch body exports. + path is spelled with ``$SUCODER_LOCAL_ROOT``, which the batch body + exports before starting tmux (``local_tier.cache_exports_sh``). """ if not ctx.is_remote: return None @@ -5348,8 +5349,11 @@ def _workspace_block(self, ctx: MirrorContext) -> Optional[str]: work = work_path(local_disk_root, token, int(job_id)) local_root = f"{root}/job{job_id}" else: - work = f"{root}/job$SLURM_JOB_ID/mirrors/{token}" - local_root = f"{root}/job$SLURM_JOB_ID" + # Confined: the id is assigned by sbatch after this renders. + # $SUCODER_LOCAL_ROOT (= /job) is exported by the + # batch body, so it expands in the agent's shell and tools. + work = f"$SUCODER_LOCAL_ROOT/mirrors/{token}" + local_root = f"{root}/job" slurm = ctx.settings.remote.slurm if ctx.settings.remote else None minutes = slurm.wip_snapshot_minutes if slurm else 10 cadence = ( @@ -5359,7 +5363,8 @@ def _workspace_block(self, ctx: MirrorContext) -> Optional[str]: lines = [ "WORKSPACE (local-disk tiering)", "You are working in a node-local clone, not in the shared mirror the prompts above describe.", - f"- Working clone (your cwd): {work}", + f"- Working clone (your cwd): {work}" + + ("" if job_id else " ($SUCODER_LOCAL_ROOT is exported in your environment; `pwd` shows it resolved)"), f"- Shared mirror (origin; durable; the human's push/pull target): {mirror_path}", " Never edit its working tree by hand: an uncommitted tracked change there blocks every publish from this clone.", "- Every commit is published to the shared mirror by a post-commit hook the moment it exists; the commit output", diff --git a/tests/test_mirror.py b/tests/test_mirror.py index a4f7ddc..09806f1 100644 --- a/tests/test_mirror.py +++ b/tests/test_mirror.py @@ -1103,10 +1103,35 @@ def test_prelude_workspace_block_confined_uses_runtime_job_id(tmp_path, monkeypa prelude_sentinel="__X__", env=None, detached=True, ) prelude = next(c["input"] for c in calls if c["args"][0] == "sh" and "prelude-" in c["args"][2]) - assert "Working clone (your cwd): /local/job$SLURM_JOB_ID/mirrors/sample" in prelude + assert "Working clone (your cwd): $SUCODER_LOCAL_ROOT/mirrors/sample" in prelude + assert "$SUCODER_LOCAL_ROOT is exported in your environment" in prelude assert "at each deadline warning only" in prelude +def test_launch_agent_confined_local_tier_prelude_reaches_batch(tmp_path, monkeypatch): + """End to end through launch_agent: a confined target with tiering gets + the WORKSPACE block (confined spelling) in the prelude that the batch + job's agent reads, even though the executor has no job id yet.""" + manager, ctx = _confined_manager(tmp_path, monkeypatch) + manager.executor.local_disk_root = "/local" + manager.executor.slurm_job_id = None + manager.config.system_prompt = tmp_path / "sys.org" + manager.config.system_prompt.write_text("SYS\n") + monkeypatch.setattr( + MirrorManager, "_default_skills_catalog_path", lambda self: None, + ) + calls = [] + manager.executor.run_agent = _confined_responder(calls, sbatch_out="9") + + manager.launch_agent(ctx, sync=False, detached=True) + + assert ctx.confined is True + prelude = next(c["input"] for c in calls if c["args"][0] == "sh" and "prelude-" in c["args"][2]) + assert "WORKSPACE (local-disk tiering)" in prelude + assert "Working clone (your cwd): $SUCODER_LOCAL_ROOT/mirrors/sample" in prelude + assert "/local/job" not in prelude.split("WORKSPACE (local-disk tiering)")[1].split("Shared mirror")[0] + + def test_build_remote_agent_cmd_str_joins_and_appends_exec_bash(tmp_path: Path) -> None: """The extracted helper joins the command and appends ``; exec bash -l``.