diff --git a/AGENTS.md b/AGENTS.md index aea5573d1b..fa006ceb6b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,12 @@ before using the task picker; its coverage fields identify prior checks and remaining obligations. Explicit user tasks take precedence over that default assignment; do not reroll an already assigned task. +For a fresh assignment, run `python3 -B scripts/choose_current_research_task.py`. +This entry point resolves generic baseline commands and retains each frozen +card beside the current command. The historical picker and root guide stay +unchanged because research packages pin their bytes. The current command +examples are in [the standing procedure](docs/research/agent-procedure.md). + For repeated/overnight loops, follow the [upstream refresh checkpoint](autoresearch.md#looping-agents-refresh-upstream-at-each-research-checkpoint) at every new round, before a major pivot and before submission. Fetch canonical diff --git a/docs/research/agent-procedure.md b/docs/research/agent-procedure.md index 9294c540bf..9a2d1b819e 100644 --- a/docs/research/agent-procedure.md +++ b/docs/research/agent-procedure.md @@ -102,15 +102,15 @@ Run this standard-library helper: ```sh # Python 3.10 or later; no third-party dependencies. -python3 scripts/choose_research_task.py +python3 scripts/choose_current_research_task.py # To reproduce a selection, pass the emitted 32-hex-digit nonce: -python3 scripts/choose_research_task.py --seed YOUR_NONCE +python3 scripts/choose_current_research_task.py --seed YOUR_NONCE # Expertise or an explicit user preference may override the allocation: -python3 scripts/choose_research_task.py --track complex-mechanisms +python3 scripts/choose_current_research_task.py --track complex-mechanisms # Retrieve a specific card after assignment: -python3 scripts/choose_research_task.py --task connector-alphabet-change +python3 scripts/choose_current_research_task.py --task connector-alphabet-change # Optional: surface structured handoffs/claims from an existing overlap cache. -python3 scripts/choose_research_task.py --coordination-cache /tmp/overlap.json +python3 scripts/choose_current_research_task.py --coordination-cache /tmp/overlap.json ``` It reads [the task catalogue](agent-task-catalog.json), draws a diff --git a/docs/research/selected-baseline.md b/docs/research/selected-baseline.md index 78655e806f..ee6c992bf2 100644 --- a/docs/research/selected-baseline.md +++ b/docs/research/selected-baseline.md @@ -1,6 +1,6 @@ # Productive-helper baseline orientation -For the selected #4359 productive-helper certificate, run from the repository root: +For the current selected productive-helper certificate, run from the repository root: ```sh python3 -B scripts/research_selected_baseline.py @@ -56,8 +56,8 @@ python3 -B scripts/research_baseline_sparse.py ``` Inspect its suggested sparse-checkout additions and materialize the inputs. -Then use `research_selected_baseline.py` for the productive-helper summary, -in place of the historical baseline command in its `next` field. Diagnosis +Then run the current command in its `next` field. It uses +`research_selected_baseline.py` for the productive-helper summary. Diagnosis does not verify the inputs; the successor's source check remains mandatory. ## Compatibility and validation @@ -85,13 +85,41 @@ The test filename is included by the existing research onboarding discovery command. The full selected cold replay is a separate obligation and was not run for this reporting adapter. -## Open next questions +## Current task-picker commands + +The current task-picker CLI, `scripts/choose_current_research_task.py`, resolves the exact generic command +`python3 -B scripts/research_baseline.py` to the current selected baseline. +Each affected suggestion retains its complete original entry in +`catalog_first_step` and explains the change in `first_step_resolution`. +Only the command changes. The inspection text, scope, task, method, scope key +and acceptance gate stay the same. Other scientific first-step commands are +not changed. The CLI checks that the current helper exists, or is an exact +HEAD file omitted by a sparse checkout, before it emits the resolved command. +A sparse result names the missing helper for materialization. + +The historical `scripts/choose_research_task.py`, catalogue bytes, `select()` +result and package `TASK.json` original-card receipts remain exact. The current +entry point runs the historical CLI with unchanged arguments and resolves its +JSON output. Help and error output keep the child status and text. This separate +entry point is necessary because a frozen package also pins the picker script +as a runtime input. It must not be changed or silently repinned. This distinction is deliberate: the CLI gives current +orientation, while the stored card records the historical assignment. Do not +copy the resolved command into an old package or repin its frozen inputs. +[PR4906](https://github.com/CrocSwap/integer-mult-bounds/pull/4906) found 57 +runtime consumers of the catalogue and requested this receipt-preserving +migration. [PR5359](https://github.com/CrocSwap/integer-mult-bounds/pull/5359) +provides the separate authenticated historical complex-ceiling replay. +That scientific command is outside this generic-baseline resolution rule. + +The displayed scientific starting points still need a current overlap and +scope check. Command resolution is not a strategic reassessment or scientific +acceptance. Tests cover all 16 generic cards, unchanged other cards, exact +command matching, missing/deleted/sparse helpers and actual CLI output. +Run them with `python3 -B -m unittest discover -s tests -p test_research_current_assignment.py`. +Current CLI resolution and these controls: Ali Romman with substantial OpenAI +Codex assistance; Apache-2.0. -The active catalogue and pinned brief still name the historical entry point. -A maintainer can migrate those references after choosing how to preserve the -dependent frozen packages' source receipts. The first step is to inventory -their `VERIFY.json` and `SOURCES.json` dependencies; changing the helper bytes -or silently repinning old inputs is not part of this contribution. +## Open next questions Future selected certificate schemas need an explicit budget-stage adapter. Start from the new selected record and reviewed source schema, then add a diff --git a/scripts/choose_current_research_task.py b/scripts/choose_current_research_task.py new file mode 100644 index 0000000000..9836d57d46 --- /dev/null +++ b/scripts/choose_current_research_task.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +"""Show current first steps and preserve the frozen task-picker output. + +Ali Romman with substantial OpenAI Codex assistance. Apache-2.0. +Arguments and selection are delegated to the historical picker CLI. +""" +import json +from pathlib import Path +import runpy +import subprocess +import sys + +ROOT = Path(__file__).resolve().parents[1] +LEGACY = ROOT / "scripts/choose_research_task.py" +check_catalog_files = runpy.run_path(str(LEGACY))["check_catalog_files"] + + +def resolve_current_first_steps(result, root): + """Resolve generic orientation at the CLI boundary; keep the source card.""" + old = ["python3", "-B", "scripts/research_baseline.py"] + current = "scripts/research_selected_baseline.py" + cards = [card for card in result["suggestions"] + if card["first_step"]["command"] == old] + if not cards: + return [] + omitted = check_catalog_files(root, [ + (current, "missing current first-step script: " + current), + ]) + for card in cards: + original = card["first_step"] + card["catalog_first_step"] = original + card["first_step"] = {**original, "command": ["python3", "-B", current]} + card["first_step_resolution"] = ( + "The current CLI uses the selected-certificate baseline for generic " + "orientation. catalog_first_step is the exact frozen card entry. " + "The task, scientific checks and acceptance gate are unchanged." + ) + return omitted + + +def main(): + child = subprocess.run([sys.executable, "-B", str(LEGACY), *sys.argv[1:]], + capture_output=True, text=True, encoding="utf-8") + if child.returncode or any(arg in ("-h", "--help") for arg in sys.argv[1:]): + sys.stdout.write(child.stdout) + sys.stderr.write(child.stderr) + return child.returncode + try: + result = json.loads(child.stdout) + omitted = resolve_current_first_steps(result, ROOT) + if omitted: + sparse = result.setdefault("sparse_checkout", {"omitted_catalogue_paths": []}) + sparse["omitted_current_first_step_paths"] = omitted + sparse["scope"] = ( + "These absent regular files match HEAD and skip-worktree index entries. " + "Materialize the chosen task's inputs before running it; this validates " + "first-step references, not replay dependencies." + ) + except (OSError, ValueError, KeyError, TypeError) as exc: + sys.stderr.write("current first-step resolution failed: " + str(exc) + "\n") + return 2 + print(json.dumps(result, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_research_coordination.py b/tests/test_research_coordination.py index af573f1291..95d7dda49a 100644 --- a/tests/test_research_coordination.py +++ b/tests/test_research_coordination.py @@ -121,7 +121,7 @@ def test_root_entry_and_priorities_links(self): self.assertIn('docs/research/current-priorities.md',root) self.assertIn('docs/research/agent-procedure.md',root) procedure=(ROOT/'docs/research/agent-procedure.md').read_text() - self.assertLess(procedure.index('**Before drawing a card**'),procedure.index('python3 scripts/choose_research_task.py')) + self.assertLess(procedure.index('**Before drawing a card**'),procedure.index('python3 scripts/choose_current_research_task.py')) if __name__=='__main__':unittest.main() diff --git a/tests/test_research_current_assignment.py b/tests/test_research_current_assignment.py new file mode 100644 index 0000000000..49f7c86969 --- /dev/null +++ b/tests/test_research_current_assignment.py @@ -0,0 +1,99 @@ +"""Current orientation must preserve frozen task-selection evidence.""" +import copy +import importlib.util +import json +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("current_picker", ROOT / "scripts/choose_current_research_task.py") +current = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(current) +LEGACY = current.LEGACY +legacy = current.runpy.run_path(str(LEGACY)) +CATALOG = json.loads((ROOT / legacy["CATALOG"]).read_text(encoding="utf-8")) + + +class CurrentResearchAssignmentTests(unittest.TestCase): + def test_every_frozen_card_and_other_fields_stay_exact(self): + before = copy.deepcopy(CATALOG) + resolved = 0 + for track in CATALOG["tracks"]: + for task in track["tasks"]: + result = legacy["select"](CATALOG, "a1" * 16, task_id=task["id"]) + original = copy.deepcopy(result) + current.resolve_current_first_steps(result, ROOT) + card = result["suggestions"][0] + if task["first_step"]["command"] == ["python3", "-B", "scripts/research_baseline.py"]: + resolved += 1 + self.assertEqual(card["catalog_first_step"], task["first_step"]) + self.assertEqual(card["first_step"]["command"], + ["python3", "-B", "scripts/research_selected_baseline.py"]) + self.assertTrue(card.pop("first_step_resolution")) + card["first_step"] = card.pop("catalog_first_step") + self.assertEqual(result, original) + self.assertEqual(resolved, 16) + self.assertEqual(CATALOG, before) + + def test_only_exact_generic_command_is_resolved(self): + result = copy.deepcopy(legacy["select"](CATALOG, "0" * 32, task_id="portable-replay")) + result["suggestions"][0]["first_step"]["command"].append("--historical-fixture") + original = copy.deepcopy(result) + self.assertEqual(current.resolve_current_first_steps(result, ROOT), []) + self.assertEqual(result, original) + + def test_missing_current_helper_does_not_change_receipt(self): + result = legacy["select"](CATALOG, "0" * 32, task_id="portable-replay") + original = copy.deepcopy(result) + with tempfile.TemporaryDirectory() as directory: + with self.assertRaisesRegex(ValueError, "missing current first-step script"): + current.resolve_current_first_steps(result, Path(directory)) + self.assertEqual(result, original) + + def cli(self, script, args): + return subprocess.run([sys.executable, "-B", str(script), *args], cwd=ROOT, + capture_output=True, text=True, encoding="utf-8") + + def test_cli_preserves_legacy_output_and_emits_both_commands(self): + args = ["--seed", "0" * 32, "--task", "portable-replay"] + original = self.cli(LEGACY, args) + output = self.cli(ROOT / "scripts/choose_current_research_task.py", args) + self.assertEqual(original.returncode, 0) + self.assertEqual(output.returncode, 0) + old, new = json.loads(original.stdout), json.loads(output.stdout) + card = new["suggestions"][0] + self.assertEqual(card["first_step"]["command"], + ["python3", "-B", "scripts/research_selected_baseline.py"]) + self.assertEqual(card.pop("catalog_first_step"), old["suggestions"][0]["first_step"]) + self.assertTrue(card.pop("first_step_resolution")) + card["first_step"] = old["suggestions"][0]["first_step"] + self.assertEqual(new, old) + self.assertEqual(self.cli(LEGACY, args).stdout, original.stdout) + + def test_help_and_error_preserve_child_status_and_output(self): + for args, code in ((["--help"], 0), (["--unknown-option"], 2), (["--seed", "bad"], 2)): + original = self.cli(LEGACY, args) + output = self.cli(ROOT / "scripts/choose_current_research_task.py", args) + self.assertEqual(output.returncode, code) + self.assertEqual((output.stdout, output.stderr), (original.stdout, original.stderr)) + + def test_sparse_helper_is_allowed_but_deletion_is_rejected(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + name = "scripts/research_selected_baseline.py" + path = root / name; path.parent.mkdir(); path.write_text("print('fixture')\n") + def git(*args): + subprocess.run(["git", *args], cwd=root, check=True, capture_output=True) + git("init", "-q"); git("add", name) + git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", + "-c", "core.hooksPath=/dev/null", "commit", "-qm", "helper") + git("update-index", "--skip-worktree", name); path.unlink() + result = legacy["select"](CATALOG, "0" * 32, task_id="portable-replay") + self.assertEqual(current.resolve_current_first_steps(result, root), [name]) + git("update-index", "--no-skip-worktree", name) + result = legacy["select"](CATALOG, "0" * 32, task_id="portable-replay") + with self.assertRaisesRegex(ValueError, "missing current first-step script"): + current.resolve_current_first_steps(result, root)