Skip to content

Continue-research mode: two-stage adopt-and-optimize an existing repo - #162

Open
AndrewRqy wants to merge 4 commits into
ChicagoHAI:mainfrom
AndrewRqy:feat/continue-research-v2
Open

Continue-research mode: two-stage adopt-and-optimize an existing repo#162
AndrewRqy wants to merge 4 commits into
ChicagoHAI:mainfrom
AndrewRqy:feat/continue-research-v2

Conversation

@AndrewRqy

Copy link
Copy Markdown
Contributor

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.

./neurico continue-research <repo> <intention.md>   # convert intention to an idea with a continuation contract
./neurico run <idea>                                # adopt the repo and optimize it

Two stages, one command

./neurico run on 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 .git removed), moves declared held-out materials into a gitignored data/.test before the anchor commit, records a trusted baseline of protected-path hashes, then runs the bootstrap rule maker to generate scoring/{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_best run 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:

Kind Meaning Enforcement
protected_path these files/dirs must not change Hard scoring guardrail. Stage 1 records a sha256 baseline; the generated eval.py emits a protected_paths_unchanged property that fails the iteration on any change.
check this command must keep passing Guardrail property that runs the command verbatim and requires exit 0.
statement a prose constraint Surfaced to the agents, not mechanically checkable.

Held-out evaluation data is declared as a material and staged into data/.test, which is already in NeuriCo's SEALED_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_continuation refuses 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

File Purpose
src/core/continuation_prepare.py (new) Stage 1 flow: adopt, stage held-out, write protected-path baseline, generate scorer + score baseline.
src/core/repo_adoption.py (new) Adopt a repo into the workspace with fresh git history; a pre-commit hook stages held-out data before the anchor commit so no plaintext enters history.
src/cli/continue_research.py (new) Intention-to-idea conversion CLI with the continuation contract; pins source_repo to the CLI argument and gates on conversion faithfulness.
src/cli/idea_conversion.py (new) Shared conversion helpers used by the continuation converter.
src/core/runner.py _run_continuation dispatch (Stage 1 then Stage 2), --prepare-only, idempotent skip when already prepared, the container isolation backstop.
src/core/local_resources.py Continuation contract and invariant helpers (validate_continuation, protected-path normalization and hashing, held-out declaration).
src/core/idea_manager.py Wires validate_continuation into idea validation.
templates/agents/rule_maker_bootstrap.txt Instructs the rule maker to encode protected_path invariants as the protected_paths_unchanged guardrail against the Stage 1 baseline.
docker/run.sh Two-container prepare then research split for a continuation idea; forward runs are unchanged.

Roughly 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

  • Local unit coverage of the prepare flow, adoption fresh-history scrub, the two-stage dispatch order, --prepare-only, idempotent resume, held-out never entering git, and the protected-path baseline. Suite green.
  • The protected-path guardrail was validated with a live bootstrap rule maker end to end: given a protected_path invariant and the Stage 1 baseline, the generated eval.py reported protected_paths_unchanged = 1.0 with the file unchanged and 0.0 after a simulated agent modification, so the iteration is rejected on any change.
  • Adoption verified on real repos: after prepare, an in-repo held-out file is absent from the working tree and from every git object, source remotes and nested .git are gone, and data/.test is gitignored.

A full live continue-research replication (Stage 1 baseline through several Stage 2 iterations) on the cluster is the next step before merge.

@Frankbest18 Frankbest18 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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, but continue-research is 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
@AndrewRqy

Copy link
Copy Markdown
Contributor Author

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 Frankbest18 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.
@AndrewRqy

Copy link
Copy Markdown
Contributor Author

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 Frankbest18 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@AndrewRqy

Copy link
Copy Markdown
Contributor Author

Thanks. All three addressed in b43e29b.

  1. Marker gitignore nesting. The .neurico/continuation_prepared.json rule sat inside the if "pycache/" not in ... block, so an adopted repo that already ignored pycache/ skipped it and left the prepared marker untracked, which the continuation validator reads as a dirty tree. Each rule is now added independently, so the marker is ignored regardless of the source .gitignore.

  2. Protected-path symlink hashing. Replaced the hand-rolled hash rather than patch the symlink branch. The baseline is now anchored to a git commit written in Stage 1, and eval.py compares protected paths against it using immutable objects only (git ls-tree + git cat-file blob, which cannot be forged at a fixed sha) read against the real filesystem (open().read(), os.readlink, os.lstat, os.walk). It never uses git diff/ls-files/status/index, which the Stage 2 agent could subvert (assume-unchanged/skip-worktree, a clean filter, core.fileMode=false, or .gitignore all hide a real change). This removes both the symlink gap and the byte-for-byte mirroring eval.py had to reproduce. Four regression tests cover the evasions.

  3. update_status no-op in the research container. Stage 2 drops the ideas/ mount, so the runner's in-container "completed" update did nothing and successful ideas stayed in_progress. The two-container run now finalizes status host-side after the research container exits 0, in a throwaway container that mounts only ideas/, so the source URL stays unreadable to the agent.

@Frankbest18 Frankbest18 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

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.

2 participants