Continue-research mode: two-stage adopt-and-optimize an existing repo - #162
Continue-research mode: two-stage adopt-and-optimize an existing repo#162AndrewRqy wants to merge 4 commits into
Conversation
Frankbest18
left a comment
There was a problem hiding this comment.
Thanks for reworking this into a clearer two-stage design. I think the overall direction is better, but a few parts of the workflow and trust boundary still need attention:
[P1] The advertised Docker workflow is incomplete.
There are two separate breaks in the normal local-repository path:
./neurico continue-research <repo> <intention.md>is advertised, butcontinue-researchis not registered in the Docker command wrapper.- After submission, the Docker mount list includes local datasets, functions, and papers, but not
continuation.source_repo. The Stage 1 container therefore cannot access a local repository that exists only on the host.
[P1] Mechanical continuation invariants can fail open.
The PR presents check and protected_path invariants as hard guardrails, but their enforcement currently depends too heavily on the generated evaluator.
First, the protected-path hashes are stored in scoring/protected_baseline.json, which remains writable during optimization. An iteration can change both a protected file and its recorded hash, causing the comparison to pass.
Second, the evaluator verifier does not review continuation invariants. It verifies metrics, result formats, and required evaluation functions, but it does not confirm that every protected path and check command was correctly included. A generated evaluator can therefore omit or incorrectly implement an invariant without Stage 1 detecting it.
[P1] sealed: true does not reliably isolate held-out data that originated inside the source repository.
The staged copy in data/.test is hidden correctly, and removing the adopted workspace’s old Git history is useful. The remaining concern is the original repository, which can still contain the same held-out data.
In native execution, the optimizing agent runs on the same host and can still access the original local repository. For a remote repository, the original URL remains visible, so the agent may be able to fetch the repository and recover the held-out file or its history again. The new workspace is sanitized, but access to the original source is not consistently removed.
[P1] GitHub privacy settings are not preserved through continuation preparation.
The runner accepts --private and --no-hash, but those values are dropped before repository adoption. Repository creation therefore falls back to its defaults. In particular, a user can request --private while the continuation repository is still created publicly when GitHub integration is active. --no-github is not affected.
[P2] Held-out staging can silently use the wrong or incomplete data.
Staging treats an existing data/.test/<name> destination as proof that the resource is already complete.
This causes two related problems:
- Two held-out resources with the same basename are rewritten to the same destination, so one silently replaces the other.
- A partial, stale, or otherwise incorrect existing destination is accepted without checking that it still matches the declared source.
Because this data is used to establish the baseline and evaluate candidates, an incorrect staged copy can invalidate later AutoResearch decisions.
[P2] --force-fresh is ignored for continuation runs.
Continuation dispatch does not carry the flag into its preparation logic. Once an adoption record and current-best checkpoint exist, Stage 1 reuses the old repository, held-out data, evaluator, and baseline even when the user explicitly requests a fresh run. This can make a seemingly fresh experiment continue from stale state.
[P2] Protected-path hashing does not cover every relevant filesystem change.
The current hash detects regular-file path and content changes, but it does not reliably cover permission changes, empty directories, or symlink identity. These changes can affect repository behavior and enter an accepted checkpoint while protected_paths_unchanged still reports success.
…protected baseline, verify invariants, redact source_repo, honor private/force-fresh, dedup held-out staging, complete protected hash
|
Thanks for the detailed reply, all seven are addressed on this new commit :) P1, Docker workflow incomplete. Both breaks fixed. continue-research is now a registered command in docker/run.sh (a cmd_continue_research mirroring submit-local: it mounts the intention file and, for a local repo, the repo read-only at its identical host path, then runs the converter in-container), and it is listed in the wrapper help and the neurico usage. Running stays a separate ./neurico run , so --run is not handled by the wrapper (it prints that guidance). Separately, collect_host_paths now adds continuation.source_repo to the mount sidecar, so a local source repo is mounted into the Stage 1 prepare container; remote URLs are skipped since they are cloned at adoption. P1, invariant baseline writable. scoring/protected_baseline.json is now in SEALED_PATHS. It is relocated out of the workspace while the optimizing agent runs (so an iteration can no longer rewrite a protected file and its recorded hash together) and copied into the isolated scorer tree so eval.py still reads it. Same protection eval.py already gets. P1, verifier does not review invariants. Rather than extend the LLM verifier, Stage 1 now runs a mechanical, behavioral cross-check after the baseline is scored (verify_invariant_guardrails): every declared protected_path invariant must have produced a protected_paths_unchanged property, every check invariant must have produced its guardrail property matched by command text, and each must pass at baseline (nothing has changed yet, and the checks are expected to pass on the adopted repo). A missing or failing guardrail fails Stage 1 loudly instead of letting an unenforced invariant reach optimization. This does not depend on the generated evaluator being trustworthy. P1, source repo not isolated. The leak is closed and the boundary is made explicit. continuation.source_repo is now redacted from the agent-visible .neurico/idea.yaml (via workspace_contract_copy) and from the adoption record (name only, for a remote URL as well as a local path), so an agent is no longer handed the original path or a re-cloneable URL in its task description or in the committed backup. The native case is unchanged by design: native runs share the host and are a trusted single process, so an agent with full filesystem access could still reach a local origin through OS introspection. The redaction removes the easy, incidental signpost; the container boundary remains the real isolation, and I have kept that as the documented trust model rather than adding native sandboxing in this PR. P2, held-out staging wrong or incomplete data. Both parts fixed in stage_held_out_data. A per-pass name set disambiguates same-basename sources (the second becomes name-2, so no source silently overwrites another), and an existing destination is accepted only when it is verified against the resolved source by size and content hash. A partial, stale, or mismatched destination is re-staged rather than trusted on existence. P2, --force-fresh ignored. _run_continuation now takes fo passes it. When set, any prepared or in-progresscontinuation workspace is moved aside before the already_prepared check, so Stage 1 re-adopts from scratch instead of continuing from stale repo/held-out/evaluator/baseline state. P2, protected-path hashing incomplete. _hash_protected_pa bits, symlink identity (the target string, not thetarget's bytes), and directory entries including empty ones, so a chmod, a repointed symlink, or an added or removed empty directory under a protected prefix all change the hash. The rule-maker tempact algorithm so the generated eval.py recomputes it thesame way; the P2 cross-check above will fail Stage 1 if a generated evaluator ever disagrees. |
Frankbest18
left a comment
There was a problem hiding this comment.
Thanks for the update. There are several issues that may affect the ordinary continuation workflow:
[P1] Stage 1 completion state is inconsistent
After the baseline checkpoint is created, Stage 1 writes .neurico/continuation_prepared.json. That file is neither committed nor ignored, so the normal continuation validator sees a dirty workspace and can reject Stage 2 before the first iteration.
At the same time, the runner does not actually use this marker to determine whether preparation completed. It only checks for the adoption record and current_best. This creates the opposite failure on retry: if invariant verification fails after current_best has been recorded, the next run treats the workspace as prepared, skips verification, and continues with the evaluator that was previously rejected.
[P1] In-repository sealed datasets fail during preparation
Held-out staging runs once during repository adoption and then immediately runs again afterward. The first call moves an in-repository sealed file into data/.test and removes the original. The second call prioritizes the recorded original source_path, cannot find it because it was intentionally removed, and fails preparation even though the sealed copy was staged successfully.
[P1] Docker applies --force-fresh in both containers
The preparation container receives --force-fresh, moves the old workspace aside, and creates a new prepared workspace. The research container then receives the same flag and moves that newly prepared workspace aside as well. For a local source repository, it cannot prepare again because the source is intentionally not mounted in the research container.
[P1] Invariant verification still depends on the generated evaluator’s claims
verify_invariant_guardrails() checks that the expected properties appear in targets.json and that results.json reports them as satisfied. It does not independently establish that eval.py actually hashes the protected paths or executes the declared check commands.
An evaluator that always reports protected_paths_unchanged = 1.0 and the check property as satisfied would pass this verification without enforcing either invariant. The current check validates the successful baseline output, but not the guardrails’ failure behavior.
[P1] The original remote source remains discoverable in the research container
Removing source_repo from the workspace’s .neurico/idea.yaml is helpful, but the research container still mounts the complete submitted-ideas directory at /app/ideas. The original submitted YAML there retains the remote repository URL.
An optimizing agent that inspects this directory can recover the URL and potentially clone the original repository, including held-out files or historical versions that were removed from the adopted workspace. The runtime readability check does not cover remote URLs.
[P2] Protected-path hashing still misses some filesystem changes
The expanded hash now covers regular file contents, descendant permissions, empty descendant directories, and symlink retargeting. However:
- Permissions on the protected directory root itself are not included.
- For a directory symlink, only the target string is hashed; changes to content behind the unchanged symlink are not detected.
These cases can change the behavior of a protected path while protected_paths_unchanged still reports success.
…ion bootstrap path Live-validated end to end with three Docker continue-research runs.
|
Thanks, all six are addressed on the branch. P1, Stage 1 completion state inconsistent. .neurico/continuation_prepared.json is added to the adoption .gitignore, so it no longer dirties the workspace. It is also load-bearing now: already_prepared requires the marker (written last, only after invariant verification passes) in addition to the adoption record and current_best. A run whose verification failed after current_best was recorded leaves no marker, so the next run re-runs Stage 1 and re-verifies instead of continuing with the rejected evaluator. P1, in-repository sealed datasets fail during preparation. Held-out staging runs twice per prepare, and the first pass moves an in-repo source out and removes the original. The second pass now accepts the already-staged destination when the source is gone: a move removes the source only after a complete copy, so source-absent plus destination-present means the move finished. Source verification (size plus content hash) still applies when the source is present, so a partial or stale copy is re-staged. P1, Docker applies --force-fresh in both containers. --force-fresh is stripped from the research container invocation. It belongs to the prepare stage, which already moved the old workspace aside and re-adopted; the research container then finds the prepared workspace and runs only Stage 2. P1, invariant verification still depends on the evaluator's claims. The eval_verifier now reviews continuation invariants: a new invariants check reads the generated eval.py and confirms it actually reads protected_baseline.json, recomputes the hashes, and runs each check command, rather than trusting a passing baseline. A rigged eval.py that hardcodes the property is a fail. It runs through the verifier plus one-retry loop and is applicable whenever the contract declares invariants. The verifier is now also invoked on the continuation path: Stage 1 runs in bootstrap mode, and the bootstrap rule maker previously did not call the verifier, so I wired the verifier and its retry loop into the bootstrap pipeline, with the rule maker regenerating from the findings on a failed verdict. The earlier mechanical baseline check remains as a behavioral backstop. P1, original remote source discoverable in the research container. The research container no longer mounts the host ideas/ directory (whose submitted YAML kept the URL). Stage 2 loads the redacted .neurico/idea.yaml from the workspace instead, and continuation is detected there via the prepared marker rather than source_repo, so the research container has no path to the original URL. Native runs remain a documented trust boundary. P2, protected-path hashing misses some changes. The root directory's own permission bits are now folded into the hash, and a directory symlink contributes both its target string and a content signature of the target, so a change behind an unchanged link is detected. The rule-maker template is updated in lockstep so the generated eval.py mirrors the algorithm. |
Frankbest18
left a comment
There was a problem hiding this comment.
Thanks for the update. I think most of the previous issues has been addressed. Theree are only three remaining issues of smaller scale:
P1 — The prepared marker can still leave common Python repositories dirty
The new .gitignore entry for .neurico/continuation_prepared.json is added only when the repository does not already contain __pycache__/.
Many Python repositories already ignore __pycache__/. In that case, NeuriCo skips the entire block and never adds the prepared-marker rule. After Stage 1 writes the marker, Git reports it as untracked, and the normal continuation validator can reject Stage 2 because the workspace is dirty.
P2 — The research container can no longer update the host-side idea status
Removing the /app/ideas mount from the research container closes the source-URL leak. However, finalization still uses IdeaManager.update_status() inside that container.
Stage 1 can move the host idea to in_progress, but Stage 2 no longer has access to the host idea file and therefore cannot move it to completed. A successful continuation can consequently remain marked as in_progress, leaving NeuriCo’s status tracking inconsistent with the actual run result.
P2 — Directory-symlink protection still misses some changes
The updated protected-path hash detects changes to the symlink target string and regular-file contents behind a directory symlink. The target signature, however, only includes regular-file paths and bytes.
Changes such as adding an empty directory, changing permissions, or changing nested symlink identity behind the directory symlink can still leave the hash unchanged. Therefore, the symlink target is not covered by the same complete filesystem fingerprint used for a directly protected directory.
|
Thanks. All three addressed in b43e29b.
|
Frankbest18
left a comment
There was a problem hiding this comment.
Thanks for the update. I still see four concerns with the new Git-based protection and status flow:
[P1] Protected paths can expose ignored or sealed data through Git history
Protected paths are force-added to the Git baseline even when they are intentionally ignored. This can place files such as .env, credentials, or private local datasets into repository history. It also affects sealed data when a protected parent directory contains the sealed location. Although these files may later disappear from the working tree, their contents remain recoverable from the baseline commit and could be included when the repository is pushed.
[P1] Git blobs can disagree with an unchanged working file
The baseline stores the Git representation of each file, while verification compares that blob directly with the working-tree bytes. Git attributes can transform content while staging—for example, by normalizing line endings or applying a clean filter. The resulting blob can therefore differ from a file that has not changed since preparation, causing the initial invariant check to fail incorrectly.
[P2] The Git baseline does not represent the complete protected filesystem state
A Git tree does not record empty directories, directory permissions, or most file-permission changes. For symlinks, it records only the link target rather than the contents reached through it. Changes involving these properties can therefore occur inside a protected path without being detected, despite the stronger expectation that the protected path remains unchanged.
[P2] Failed research can be marked as completed
The host-side finalizer treats a zero Docker exit code as successful research. However, the Python runner can return an unsuccessful research result without exiting with a nonzero code. Docker then reports success, and the idea is moved to completed even though the research result says it failed. This leaves the durable idea status inconsistent with the actual run outcome.
Supersedes #143. Same feature, redesigned. The single-stage version accumulated a class of held-out-data leaks and scoring-tamper bugs that all traced to one root: the optimizing agent shared a filesystem with the trusted evaluation setup (the raw materials, the scoring-protocol generation, the baseline). This version separates those in time instead of trying to defend them in place.
What this adds
Continue-research starts NeuriCo from an existing repository and improves it toward a user goal, instead of building a solution from scratch. The user supplies a repo (local path or GitHub URL) and an intention file declaring a continuation contract: a goal, invariants, and bring-your-own evaluation materials including held-out data.
Two stages, one command
./neurico runon a continuation idea executes two stages back to back:Stage 1 (prepare). Trusted setup, no optimizing agent runs. It adopts the repo with fresh git history (source remotes scrubbed, nested
.gitremoved), moves declared held-out materials into a gitignoreddata/.testbefore the anchor commit, records a trusted baseline of protected-path hashes, then runs the bootstrap rule maker to generatescoring/{eval.py,targets.json,interface.md}and scores the baseline. The output is an ordinary scored NeuriCo workspace.Stage 2. A plain
continue_from_current_bestrun on that workspace, with no continue-research-specific code, no scoring-protocol regeneration, and no re-baseline. Stage 2 is indistinguishable from an ordinary--continue-autoresearch.Because all trusted setup finishes before the optimizing agent exists, the agent never coexists with the raw source materials or with scorer/baseline establishment. That removes the tampering and leak surface the single-stage design kept hitting, rather than patching each instance.
The continuation contract
The intention file declares a goal plus invariants. Each invariant kind is enforced concretely:
protected_patheval.pyemits aprotected_paths_unchangedproperty that fails the iteration on any change.checkstatementHeld-out evaluation data is declared as a material and staged into
data/.test, which is already in NeuriCo'sSEALED_PATHS, so Stage 2 hides it from the optimizing agent and from checkpoints exactly as any scored run does.Docker
Native runs are a single process. Under Docker the same one command runs as two containers so the source materials never reach the optimizing agent: a prepare container that mounts the repo and host materials and runs Stage 1 with
--prepare-only, then a research container that mounts only the workspace (no materials) and runs Stage 2. A runtime backstop in_run_continuationrefuses to run Stage 2 in a container while the source repo or a declared material path is still readable, so a misrouted single-container run cannot expose materials.Files
src/core/continuation_prepare.py(new)src/core/repo_adoption.py(new)src/cli/continue_research.py(new)source_repoto the CLI argument and gates on conversion faithfulness.src/cli/idea_conversion.py(new)src/core/runner.py_run_continuationdispatch (Stage 1 then Stage 2),--prepare-only, idempotent skip when already prepared, the container isolation backstop.src/core/local_resources.pyvalidate_continuation, protected-path normalization and hashing, held-out declaration).src/core/idea_manager.pyvalidate_continuationinto idea validation.templates/agents/rule_maker_bootstrap.txtprotected_pathinvariants as theprotected_paths_unchangedguardrail against the Stage 1 baseline.docker/run.shRoughly half the size of #143, with the whole entangled layer removed: the encryption-at-rest store, the durable protocol store, the materials fingerprint, in-loop regeneration, re-baselining, content-hash restaging, and the old two-phase mount sidecar are all gone.
Testing
--prepare-only, idempotent resume, held-out never entering git, and the protected-path baseline. Suite green.protected_pathinvariant and the Stage 1 baseline, the generatedeval.pyreportedprotected_paths_unchanged= 1.0 with the file unchanged and 0.0 after a simulated agent modification, so the iteration is rejected on any change..gitare gone, anddata/.testis gitignored.A full live continue-research replication (Stage 1 baseline through several Stage 2 iterations) on the cluster is the next step before merge.