From 04d5dff5f0ccbfe5430321ff762778cf0dfa80d6 Mon Sep 17 00:00:00 2001 From: Rob Sherman Date: Thu, 30 Jul 2026 14:21:56 -0700 Subject: [PATCH 1/2] feat(engine,evals): expose rules-target so observe stops hardcoding CLAUDE.md Closes the second platform seam 0.24.0 created. The engine now writes a per-platform rules-block target, but evals/observe.py still hardcoded CLAUDE.md -- so for any platform whose block lands elsewhere it reported rules_block_present=false. A confident wrong answer, and it would have surfaced on the first Hermes/Codex run as a parity failure that is not real. Mirroring PLATFORM_RULES_CHAIN into Python was rejected: a second copy of platform knowledge only MOVES the drift rather than removing it, which is the same class of bug this epic was filed for. Instead the engine answers. New `rules-target` subcommand: read-only, prints the absolute path of the file the resolved platform's block belongs in, walking that platform's first-match-wins chain. Deliberately does NOT record a platform -- a query must not mutate state the way write-rules does, and there is a test pinning that. observe() now asks the engine and additionally reports `rules_block_file`. Naming the file makes a failure diagnosable without a re-run: "false" and "false, and we were looking at AGENTS.md" are very different findings. adapter's _failure() gained the key too, preserving the same-key-set invariant scorers depend on. MUTATION-TESTED: restoring the hardcoded CLAUDE.md fails the new regression test with exactly `- AGENTS.md / + CLAUDE.md`, so it pins the fix rather than decorating it. Also added a cannot-tell case -- observe must stay scoreable outside a pm project rather than raising, same principle as the adapter's infra guard. Docs per this repo's drift test, which requires every dispatch key in BOTH SKILL.md and README.md (its exclusion set is deliberately empty). VERIFIED WITH A REAL AGENT RUN against the blessed baseline: exit 0, all 5 checks STABLE, 0 blocking. rules_block_file=CLAUDE.md, present=true in the run record -- the new observable flows end to end. 10 turns, $1.64 notional (subscription auth). 287 node tests, 19 pytest, all passing. Claude-Session: https://claude.ai/code/session_01WBqyLnBbYEi5hp2noc3cMC --- .conductor/render-stamp.json | 4 +-- .conductor/state.json | 7 +++-- PROJECT.md | 10 +++---- README.md | 5 ++++ evals/adapter.py | 1 + evals/observe.py | 49 +++++++++++++++++++++++++++++-- evals/tests/test_observe.py | 49 +++++++++++++++++++++++++++++++ scripts/conductor.mjs | 14 +++++++-- scripts/test/platform.test.mjs | 53 ++++++++++++++++++++++++++++++++++ skills/conductor/SKILL.md | 4 +++ 10 files changed, 182 insertions(+), 14 deletions(-) diff --git a/.conductor/render-stamp.json b/.conductor/render-stamp.json index 8d438f8..14a0098 100644 --- a/.conductor/render-stamp.json +++ b/.conductor/render-stamp.json @@ -1,4 +1,4 @@ { - "renderedAt": "2026-07-30T15:40:51.660Z", - "stateMtimeMs": 1785426051639.6658 + "renderedAt": "2026-07-30T21:21:55.794Z", + "stateMtimeMs": 1785446515777.2825 } diff --git a/.conductor/state.json b/.conductor/state.json index c151ad0..bb795c8 100644 --- a/.conductor/state.json +++ b/.conductor/state.json @@ -1219,7 +1219,7 @@ "id": "edd-observe-hardcodes-claude-md", "title": "observe.py hardcodes CLAUDE.md -- a second, undeclared platform seam", "priority": "P1", - "status": "queued", + "status": "archived", "role": "epic", "lane": "claude-code", "links": [ @@ -1234,9 +1234,10 @@ "stories": [ { "title": "fold into the rules-block epic: once the engine writes a per-platform rules-block target, update observe() to match and prove both with one eval", - "done": false + "done": true } - ] + ], + "completedAt": "2026-07-30T21:21:55.677Z" }, { "id": "edd-user-memory-confound-control-run", diff --git a/PROJECT.md b/PROJECT.md index e57ef82..613594c 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -3,7 +3,7 @@ > GENERATED by the `pm` plugin β€” do not hand-edit. Source of truth is > `.conductor/state.json` (ordering, detours, links) + OpenSpec `tasks.md` (stories). > Regenerate: `/pm:status` (or `node scripts/conductor.mjs render`). -> Last rendered: 2026-07-30T15:40:51.653Z +> Last rendered: 2026-07-30T21:21:55.691Z ## Now @@ -23,7 +23,7 @@ _Empty β€” no work is paused._ | P1 | `df-hierarchy-executor-must-check-readme-for-user-facing-changes` | claude-code | epic | archived | β€” | - | | P1 | `df-live-infra-epic-session-continuity-warning` | claude-code | epic | archived πŸ€– | β€” | - | | P1 | `df-readme-stale-since-gate-guard` | claude-code | epic | archived | β€” | - | -| P1 | `edd-observe-hardcodes-claude-md` | claude-code | epic | queued | 0/1 stories | depends-onβ†’rules-block-hardcodes-claude-slash-commands | +| P1 | `edd-observe-hardcodes-claude-md` | claude-code | epic | archived | 1/1 stories | depends-onβ†’rules-block-hardcodes-claude-slash-commands | | P1 | `edd-user-memory-confound-control-run` | claude-code | epic | queued | 0/5 stories | blocksβ†’hermes-platform-support | | P1 | `gh-65-autodetour-false-positives` | claude-code | epic | queued | 2/4 stories | - | | P1 | `pm-fix-link-flag-validation-and-update` | claude-code | epic | archived | β€” | - | @@ -113,12 +113,12 @@ CONDUCTOR STATE β€” where we are and what's next NOW: (no active epic set) NEXT UP (by priority, then lane): - β€’ `edd-observe-hardcodes-claude-md` (P1, claude-code, queued) β€” 0/1 stories β€’ `edd-user-memory-confound-control-run` (P1, claude-code, queued) β€” 0/5 stories β€’ `gh-65-autodetour-false-positives` (P1, claude-code, queued) β€” 2/4 stories β€’ `autodetour-parser-misses-am-and-f` (P2, claude-code, queued) β€” 0/3 stories β€’ `edd-adapter-discards-agent-stdout` (P2, claude-code, queued) β€” β€” - (+7 more β€” see PROJECT.md) + β€’ `edd-measures-installed-plugin-not-worktree` (P2, claude-code, queued) β€” β€” + (+6 more β€” see PROJECT.md) lanes: openspec 2 Β· superpowers 5 Β· claude-code 55 Β· decision 7 planned: 2 β€” see PROJECT.md @@ -141,7 +141,7 @@ EPIC LINKS: β€’ `portfolio-architecture-consistency-scan` depends-on `epic-hierarchy-orchestration` β€” C presumes B's hierarchy execution exists to propagate cross-cutting decisions into TRACKER SYNC (github-issues): - ⚠ not yet in github-issues β€” create issues + record keys (update-epic): `edd-observe-hardcodes-claude-md`, `edd-user-memory-confound-control-run`, `autodetour-parser-misses-am-and-f`, `edd-adapter-discards-agent-stdout`, `edd-measures-installed-plugin-not-worktree`, `platform-switch-orphans-old-rules-block`, `multi-platform-agent-support` + ⚠ not yet in github-issues β€” create issues + record keys (update-epic): `edd-user-memory-confound-control-run`, `autodetour-parser-misses-am-and-f`, `edd-adapter-discards-agent-stdout`, `edd-measures-installed-plugin-not-worktree`, `platform-switch-orphans-old-rules-block`, `multi-platform-agent-support` πŸ’‘ 1 tracker configured (github-issues) β€” consider `/pm:sync` this session to pull in any new issues. diff --git a/README.md b/README.md index 6607b88..53afaa4 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,11 @@ pre-0.24.0 state file that predates the field β€” re-renders `PROJECT.md`, and s `pmVersion`. Idempotent β€” safe to run more than once. Requires `/reload-plugins` first if you just updated the plugin (the SessionStart briefing tells you when). +The engine also exposes `rules-target`, a read-only query printing the absolute path of the file +the recorded platform's rules block belongs in (resolving that platform's first-match-wins +chain). It exists so tooling around pm never has to mirror the chain β€” a second copy of platform +knowledge is drift waiting to happen. Unlike `write-rules`, it records nothing. + After showing the changelog delta ("What's new in pm"), the agent reviews each `Added` headline and recommends adopting any opt-in capability that's relevant to this repo's current `.conductor/state.json` (e.g. secondary trackers, `thorough` review mode) β€” one line, one diff --git a/evals/adapter.py b/evals/adapter.py index 39164e9..04517c6 100644 --- a/evals/adapter.py +++ b/evals/adapter.py @@ -43,6 +43,7 @@ def _failure(error: str) -> dict: "epic_ids": [], "detours": [], "rules_block_present": False, + "rules_block_file": None, "project_md_present": False, "new_epics": [], "exit_code": INFRA_FAILURE_EXIT_CODE, diff --git a/evals/observe.py b/evals/observe.py index 5c2e9a4..b362840 100644 --- a/evals/observe.py +++ b/evals/observe.py @@ -8,8 +8,15 @@ from __future__ import annotations import json +import os +import subprocess from pathlib import Path +ENGINE = Path(__file__).resolve().parent.parent / "scripts" / "conductor.mjs" + +# Only the stable PREFIX, never the full decorated marker. The engine's own detection keys on +# this same prefix precisely so the parenthetical can change without stranding older blocks -- +# matching the full string here would re-create that bug on the observer side. RULES_BEGIN = "