diff --git a/plugins/repo-dreamer/skills/repository-skill-forge/SKILL.md b/plugins/repo-dreamer/skills/repository-skill-forge/SKILL.md index 5afcb09..1744f76 100644 --- a/plugins/repo-dreamer/skills/repository-skill-forge/SKILL.md +++ b/plugins/repo-dreamer/skills/repository-skill-forge/SKILL.md @@ -293,13 +293,13 @@ Cluster eligible candidates by repository subject. Assign a stable Assign deterministic non-negative `rank` values. Validate and independently review every proposal. Compare it with repository -skills and the complete Forge PR catalog. Every promoted proposal body must -contain the exact marker produced by: +skills and the complete Forge PR catalog. Store each promoted proposal's marker +with that proposal rather than in a shared run-level file: ```bash python3 "$SKILL_DIR/scripts/proposal-ledger.py" marker \ --proposal "$PROPOSAL_JSON" \ - --out "$RUN_DIR/proposal-marker.md" + --out "$RUN_DIR/proposals/$PROPOSAL_KEY/proposal-marker.md" ``` The marker is persistent PR metadata. Do not edit or remove it when updating a @@ -317,6 +317,10 @@ python3 "$SKILL_DIR/scripts/proposal-ledger.py" select \ --out "$RUN_DIR/proposal-selection.json" ``` +The selected entry contains `selection.marker`, generated directly from the +selected proposal. Pass that exact value unchanged in the PR body; never use a +shared marker file or a marker belonging to another proposal. + Reconciliation rules: - same `proposalKey` and `proposalVersion` in any open, closed, or merged PR: @@ -346,6 +350,11 @@ TARGET_SHA="$( If no proposal is selected, missing target identity must not block the run. If selected and target identity cannot be resolved, block before publication. +For a create action, stage only the selected proposal. Before copying, reject +symlinks in its source tree, every existing destination path component, and +every existing destination entry mapped from a selected source entry. Compare +the pending checkout paths with the exact selected source-file manifest and +block on any additional path. After publication, apply `skills-forge` when the label exists and the available GitHub tools support it. Label lookup, creation, or application failure is non-blocking because the persistent marker is the authoritative Forge identity. diff --git a/plugins/repo-dreamer/skills/repository-skill-forge/prompts/author-proposal.md b/plugins/repo-dreamer/skills/repository-skill-forge/prompts/author-proposal.md index 6ac3d2a..f2ffb67 100644 --- a/plugins/repo-dreamer/skills/repository-skill-forge/prompts/author-proposal.md +++ b/plugins/repo-dreamer/skills/repository-skill-forge/prompts/author-proposal.md @@ -21,9 +21,11 @@ proposal manifest's `candidateIds` array. Set `proposalVersion` from the complete proposal, including the sorted candidate IDs and versions plus the generated skill content, so materially changed evidence or instructions produce a new version. -The pull request body must contain the exact machine-readable marker generated -by `proposal-ledger.py marker`. Keep that marker unchanged for the lifetime of -the PR; it is the stateless workflow's cross-run identity and rejection memory. +Store the machine-readable marker generated by `proposal-ledger.py marker` +inside this proposal's run-local directory, never in a shared marker file. +Publication must use the exact `selection.marker` generated from the selected +proposal and keep it unchanged for the lifetime of the PR; it is the stateless +workflow's cross-run identity and rejection memory. Copy the extraction coverage into the proposal manifest. Use `extraction.status: complete` for complete runs. For partial runs include diff --git a/plugins/repo-dreamer/skills/repository-skill-forge/scripts/extraction-controller.py b/plugins/repo-dreamer/skills/repository-skill-forge/scripts/extraction-controller.py index 4a89626..ab6b068 100644 --- a/plugins/repo-dreamer/skills/repository-skill-forge/scripts/extraction-controller.py +++ b/plugins/repo-dreamer/skills/repository-skill-forge/scripts/extraction-controller.py @@ -1013,11 +1013,15 @@ def record_failure( recovered = split_partition(state, partition, reason) else: batch = find_batch(partition, action["batchId"]) - recovered = ( - split_tool_batch(state, partition, batch, reason) - if batch["status"] == "tools" - else split_batch(state, partition, batch, reason) - ) + if batch["status"] == "tools": + recovered = split_tool_batch( + state, + partition, + batch, + reason, + ) or split_batch(state, partition, batch, reason) + else: + recovered = split_batch(state, partition, batch, reason) if recovered: state["handledActionIds"].append(action["actionId"]) return diff --git a/plugins/repo-dreamer/skills/repository-skill-forge/scripts/proposal-ledger.py b/plugins/repo-dreamer/skills/repository-skill-forge/scripts/proposal-ledger.py index 15b7fa3..6715cff 100755 --- a/plugins/repo-dreamer/skills/repository-skill-forge/scripts/proposal-ledger.py +++ b/plugins/repo-dreamer/skills/repository-skill-forge/scripts/proposal-ledger.py @@ -218,6 +218,11 @@ def select( ), None, ) + if selected is not None: + selected = { + **selected, + "marker": render_marker(selected["proposal"]), + } return { "selection": selected, "mutationCount": 1 if selected else 0, diff --git a/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_extraction_controller.py b/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_extraction_controller.py index dad4433..6646e40 100644 --- a/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_extraction_controller.py +++ b/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_extraction_controller.py @@ -137,6 +137,35 @@ def test_handoff_failure_splits_without_artifact_blocker(self) -> None: self.assertIn(action["actionId"], state["handledActionIds"]) self.assertEqual(1, state["workCounters"]["failedQueries"]) + def test_single_session_tool_overflow_reduces_page_size(self) -> None: + with tempfile.TemporaryDirectory() as run_dir: + state = controller.initialize(arguments(run_dir, tool_page_size=8)) + partition = state["partitions"][0] + batch = controller.make_batches(["session-1"], 1, 8)[0] + batch["status"] = "tools" + partition["batches"] = [batch] + action = { + "actionId": "tools-batch-1", + "kind": "tool-calls", + "partitionId": partition["partitionId"], + "batchId": batch["batchId"], + } + state["issuedActions"] = [action] + + controller.record_failure( + state, + action, + "row_limit_exceeded", + count_attempt=False, + allow_retry=False, + ) + + self.assertEqual("running", state["status"]) + self.assertEqual(4, batch["pageSize"]) + self.assertEqual([], state["blockers"]) + self.assertEqual([], state["omittedUnits"]) + self.assertEqual("reduce_evidence_page", state["retryHistory"][-1]["kind"]) + def test_cli_uses_fast_safe_defaults(self) -> None: with tempfile.TemporaryDirectory() as run_dir: state_path = Path(run_dir) / "state.json" diff --git a/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_stateless_forge.py b/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_stateless_forge.py index 01085d8..1c5254a 100644 --- a/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_stateless_forge.py +++ b/plugins/repo-dreamer/skills/repository-skill-forge/tests/test_stateless_forge.py @@ -265,6 +265,10 @@ def test_selection_allows_at_most_one_mutation(self) -> None: self.assertEqual(1, result["mutationCount"]) self.assertEqual("first", result["selection"]["proposal"]["proposalKey"]) + self.assertEqual( + ledger.render_marker(proposal(key="first", rank=1)), + result["selection"]["marker"], + ) def test_selection_rejects_invalid_rank(self) -> None: invalid = proposal()