Skip to content

Commit fdf909e

Browse files
committed
merge: feat/tournament-orchestrator into reconcile/atlas-catchup
Brings atlas-coin-oracle-gate-cutover + reachability-gate + tournament orchestrator (stacked). cli.py/validation.py keep union of both sides; check-npm-auth.* resolve toward preflight canonical. # Conflicts: # prd_taskmaster/cli.py # prd_taskmaster/validation.py
2 parents 87a50ca + 35eed74 commit fdf909e

46 files changed

Lines changed: 14078 additions & 175 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Oracle Slice-1 DOGFOOD — cross-repo acceptance test
2+
3+
**Date:** 2026-06-16
4+
**Task:** 5.3 — cross-repo DOGFOOD acceptance test (capstone of Slice 1)
5+
**Test:** `tests/core/test_oracle_dogfood.py` (engine worktree)
6+
**Status:** GREEN — 3/3 passed (real podman, real CLI, real ship-check subprocess; no mocks)
7+
8+
## What was proven
9+
10+
The unfakable Atlas oracle gates a real `prd-taskmaster` ship-check end-to-end.
11+
For each DONE task, `skel/ship-check.py` (Gate 5) shells `atlas oracle grade`,
12+
which checks out the submitted commit into a throwaway worktree, **overlays the
13+
operator-held tests over the submitter's tree**, re-executes the card's grading
14+
command inside a digest-pinned podman sandbox, derives `PASS` iff exit 0, and
15+
appends a tamper-evident ledger event.
16+
17+
1. **Genuine pass ships.** Operator-held `grade.sh` = `exit 0`. Ship-check emits
18+
`SHIP_CHECK_OK`, returncode 0, and the ledger records `verdict == "PASS"`.
19+
2. **Reward hack blocked (the acceptance criterion).** The submitter ships a
20+
cheat — a committed `grade.sh` that always `exit 0` — while the operator-held
21+
`grade.sh` is `exit 1`. The cheat does **NOT** ship: the oracle overlays and
22+
re-executes the operator's copy, so the verdict is `FAIL`. `SHIP_CHECK_OK` is
23+
absent, returncode is non-zero, stderr names the oracle FAIL for task 1, and
24+
the ledger records `verdict == "FAIL"`. **Non-vacuous:** the only difference
25+
between case 1 and case 2 is which `grade.sh` the oracle re-runs — the
26+
submitter's committed copy never reaches the verdict.
27+
3. **Ledger integrity.** `atlas ledger verify <dir>` reports
28+
`{"ok": true, "eventCount": 1}` after the genuine pass.
29+
30+
## ATLAS_ORACLE_CMD used
31+
32+
```
33+
ATLAS_ORACLE_CMD="<spine>/node_modules/.bin/tsx <spine>/apps/cli/src/index.ts"
34+
```
35+
(`<spine>` = your local atlas-protocol checkout. The test reads `ATLAS_ORACLE_CMD` from the environment and skips when it is unset.)
36+
37+
`ship-check.py` shlex-splits `ATLAS_ORACLE_CMD` and appends
38+
`oracle grade --repo <root> --commit <HEAD> --card ... --held ... --evidence ... --ledger ...`.
39+
40+
**Why `tsx` on the CLI source and not `node dist/index.js`:** the spine
41+
monorepo's workspace packages (`@atlas-protocol/core|cards|evidence|executor`)
42+
declare `exports: "./src/index.ts"`, so the compiled `apps/cli/dist/index.js`
43+
resolves its workspace deps to TypeScript sources that bare `node` cannot load
44+
(`ERR_MODULE_NOT_FOUND: .../packages/executor/src/grade.js`). The `tsx`
45+
executable runs the identical CLI code path the spine's own vitest suite uses,
46+
with **no edits to the spine repo**. (Building the CLI — `pnpm --filter
47+
@atlas-protocol/cli build`, plus `pnpm -r build` for the workspace deps — was
48+
done as Step 0 and is required so the source resolves; the dist itself is not
49+
the run target.)
50+
51+
## Observed results
52+
53+
### Pass case (operator-held `grade.sh` = `exit 0`)
54+
55+
```
56+
$ ATLAS_ORACLE_CMD="$TSX $SRC" python3 skel/ship-check.py --cwd /tmp/ev_pass
57+
SHIP_CHECK_OK
58+
rc=0
59+
```
60+
61+
Ledger event payload (excerpt):
62+
63+
```json
64+
{
65+
"type": "verification.completed",
66+
"actor": { "kind": "executor", "id": "oracle" },
67+
"lifecycleState": "verification_passed",
68+
"payload": { "verdict": "PASS", "exitCode": 0, "overlayHash": "sha256:..." }
69+
}
70+
```
71+
72+
### Reward-hack case (submitter cheat `exit 0`, operator-held `exit 1`)
73+
74+
```
75+
$ ATLAS_ORACLE_CMD="$TSX $SRC" python3 skel/ship-check.py --cwd /tmp/ev_hack
76+
rc=1
77+
--- stdout (empty — no SHIP_CHECK_OK) ---
78+
--- stderr ---
79+
FAIL: task 1: oracle verdict FAIL
80+
```
81+
82+
The committed cheat (`exit 0`) was overlaid away by the operator-held `exit 1`
83+
and re-executed in the sandbox → verdict `FAIL` → ship blocked.
84+
85+
## pytest
86+
87+
```
88+
$ python3 -m pytest tests/core/test_oracle_dogfood.py -v
89+
tests/core/test_oracle_dogfood.py::test_genuine_pass_ships PASSED [ 33%]
90+
tests/core/test_oracle_dogfood.py::test_reward_hack_blocked PASSED [ 66%]
91+
tests/core/test_oracle_dogfood.py::test_ledger_integrity PASSED [100%]
92+
============================== 3 passed in 13.05s ==============================
93+
```
94+
95+
## Slice-2 hardening note
96+
97+
The Graded Card's `contentHash` is a syntactically valid placeholder.
98+
`gradeSubmission` does not re-verify `contentHash` against the card body in
99+
Slice 1 (it is only echoed into the ledger payload). Slice 2 should recompute
100+
and verify it before grading so a tampered card body cannot be graded under a
101+
stale hash. (Also tracked in the executor's `SLICE-1` deferral comments:
102+
infra-failure exit codes are currently recorded as `FAIL`, and `evidenceRef`
103+
records a path rather than a `sha256:`-prefixed content ref.)

mcp-server/server.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,10 +184,29 @@ def claim_task(tag: str = "") -> dict:
184184

185185

186186
@mcp.tool()
187-
def set_task_status(id: str, status: str, tag: str = "") -> dict:
188-
"""Set a task or subtask status without terminating the MCP host."""
187+
def set_task_status(
188+
id: str,
189+
status: str,
190+
tag: str = "",
191+
evidence_ref: str | None = None,
192+
reachability: dict | None = None,
193+
) -> dict:
194+
"""Set a task or subtask status without terminating the MCP host.
195+
196+
For status != "done": evidence_ref and reachability are ignored.
197+
For status == "done" on a wired/live task: reachability must be provided
198+
with verdict in {WIRED, EXEMPT}. Pass the dict returned by the
199+
reachability sweep (mcp__atlas-engine__check_gate / sweep_task).
200+
Evidence is persisted on the task when provided (any tier).
201+
"""
189202
try:
190-
return TS.run_set_status(id_str=id, status=status, tag=tag or None)
203+
return TS.run_set_status(
204+
id_str=id,
205+
status=status,
206+
tag=tag or None,
207+
evidence_ref=evidence_ref,
208+
reachability=reachability,
209+
)
191210
except LIB.CommandError as exc:
192211
return {"ok": False, "error": exc.message, **exc.extra}
193212

prd_taskmaster/backend.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def rate(self, tag=None, research=True) -> dict: ...
4040
"status": "pending",
4141
"dependencies": [],
4242
"priority": "high",
43+
"tier": "domain-model",
44+
"reachableVia": "",
4345
"subtasks": [
4446
{
4547
"id": 1,
@@ -64,7 +66,12 @@ def rate(self, tag=None, research=True) -> dict: ...
6466
task must include id, title, description, details, testStrategy, status,
6567
dependencies, priority, and at least 2 subtasks; dependencies must reference
6668
existing task or sibling subtask IDs; use only priority high, medium, or low;
67-
do not include placeholders, generic tasks, or empty testStrategy fields."""
69+
do not include placeholders, generic tasks, or empty testStrategy fields;
70+
tier ∈ {spike|domain-model|wired|live}: the altitude of the claim — spike=research,
71+
domain-model=pure logic, wired=integration, live=user-visible; wired/live require
72+
reachability evidence (the deterministic enrich step will set this if omitted);
73+
reachableVia names the existing route/component/CLI/tool/API the new code wires into;
74+
required for wired/live tasks (a task naming no consumer is an orphan by design)."""
6875

6976

7077
PARALLEL_RESULT_SCHEMA_HINT = """{
@@ -228,6 +235,7 @@ def _task_summaries(tasks: list[dict]) -> list[dict]:
228235
"dependencies": task.get("dependencies") or [],
229236
"status": task.get("status", "pending"),
230237
"subtask_count": len(task.get("subtasks") or []),
238+
"reachableVia": task.get("reachableVia", ""),
231239
})
232240
return summaries
233241

@@ -360,7 +368,10 @@ def parse_prd(self, prd_path, num_tasks, tag=None) -> dict:
360368
prompt = (
361369
f"Parse this PRD into exactly {num_tasks} TaskMaster-compatible tasks.\n"
362370
f"Target tag: {tag or parallel.current_tag(None)}.\n"
363-
"Return only the tasks JSON object.\n\n"
371+
"Return only the tasks JSON object.\n"
372+
"For wired/live tier tasks, set reachableVia to the existing route, component, CLI, "
373+
"tool, or API that this task's code wires into (e.g. 'route:/api/v1/orders', "
374+
"'cli:prd-taskmaster', 'component:OrdersTable').\n\n"
364375
f"PRD PATH: {path}\n"
365376
f"PRD:\n{prd_text}"
366377
)

prd_taskmaster/cli.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from prd_taskmaster.context_pack import build_context_pack
2626
from prd_taskmaster import fleet, parallel, task_state
2727
from prd_taskmaster.lib import _detect_taskmaster_method
28+
from prd_taskmaster.reachability_cmd import cmd_reachability_sweep
29+
from prd_taskmaster.tournament.cmd import cmd_tournament_run, cmd_tournament_status
2830

2931

3032
def _backend_source() -> str:
@@ -361,6 +363,61 @@ def build_parser() -> argparse.ArgumentParser:
361363
p.add_argument("--id", required=True)
362364
p.add_argument("--status", required=True)
363365
p.add_argument("--tag")
366+
p.add_argument(
367+
"--evidence-ref",
368+
default=None,
369+
help="Path or ref to the CDD evidence card for this task",
370+
)
371+
p.add_argument(
372+
"--reachability",
373+
default=None,
374+
help=(
375+
"Reachability verdict: bare string (WIRED|EXEMPT|ORPHAN) or a JSON dict. "
376+
"When omitted and marking done, the verdict is auto-read from the task's "
377+
"CDD card .atlas-ai/cdd/task-<id>.json if present."
378+
),
379+
)
380+
381+
# reachability-sweep
382+
p = sub.add_parser(
383+
"reachability-sweep",
384+
help="Run the reachability sweep for a task and write the verdict into its CDD card",
385+
)
386+
p.add_argument("--task", required=True, help="Task id (e.g. 1 or 1.2)")
387+
p.add_argument(
388+
"--start-commit",
389+
required=True,
390+
help="Git SHA recorded when work on this task began (git rev-parse HEAD at task start)",
391+
)
392+
p.add_argument(
393+
"--cwd",
394+
default=None,
395+
help="Explicit repo root (defaults to the current working directory)",
396+
)
397+
398+
# tournament-run — run a full tournament job (spawn→collect→adjudicate→settle→reputation)
399+
p = sub.add_parser(
400+
"tournament-run",
401+
help="Run a full tournament job: spawn racers, collect commit-reveals, adjudicate, settle, record reputation",
402+
)
403+
p.add_argument("--card", required=True, help="Path to CDD card JSON")
404+
p.add_argument("--task", required=True, help="Task id (e.g. 7 or 1.2)")
405+
p.add_argument("--base-ref", required=True, help="Fork-point git SHA all worktrees branch from")
406+
p.add_argument("--models", required=True, help="Comma-separated model strings (e.g. claude:sonnet,claude:haiku)")
407+
p.add_argument("--job-id", required=True, help="Unique tournament job identifier")
408+
p.add_argument("--bounty", required=True, type=int, help="Bounty amount in coin units")
409+
p.add_argument("--job-poster", required=True, help="Identity of the bounty poster")
410+
p.add_argument("--window", type=float, default=120.0, help="Commit-reveal window in seconds (default 120)")
411+
p.add_argument("--enforce-slash", action="store_true", help="Pass --enforce-slash to the settle CLI")
412+
p.add_argument("--task-class", default="coding", help="Reputation bucket (default: coding)")
413+
414+
# tournament-status — read reputation snapshot + active operator count
415+
p = sub.add_parser(
416+
"tournament-status",
417+
help="Show reputation snapshot and active operator slot count",
418+
)
419+
p.add_argument("--reputation-path", default=None, help="Path to reputation.jsonl (default: .atlas-ai/reputation.jsonl)")
420+
p.add_argument("--operators-path", default=None, help="Path to operators.json (default: .atlas-ai/tournament/operators.json)")
364421

365422
# status — render progress panels
366423
p = sub.add_parser("status", help="Render Atlas progress panels for the current phase")
@@ -416,6 +473,9 @@ def cmd_status(args) -> None:
416473
"next-task": task_state.cmd_next_task,
417474
"claim-task": task_state.cmd_claim_task,
418475
"set-status": task_state.cmd_set_status,
476+
"reachability-sweep": cmd_reachability_sweep,
477+
"tournament-run": cmd_tournament_run,
478+
"tournament-status": cmd_tournament_status,
419479
"economy-report": cmd_economy_report,
420480
"context-pack": cmd_context_pack,
421481
"feedback-add": cmd_feedback_add,

prd_taskmaster/lib.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,13 @@ def atomic_write(path: Path, content: str) -> None:
7777

7878
def locked_update(path: Path, transform: Callable[[str], str]) -> str:
7979
"""Read-modify-write under flock. transform takes current content, returns new content.
80-
Returns the new content for convenience."""
80+
Returns the new content for convenience.
81+
82+
The write is skipped when the transform returns the same string as ``current``
83+
(identity check: ``new is current`` or ``new == current``) to avoid creating
84+
ghost empty files when the transform signals a no-op / error abort by returning
85+
the unchanged input.
86+
"""
8187
path = Path(path)
8288
path.parent.mkdir(parents=True, exist_ok=True)
8389
lock_path = path.with_suffix(path.suffix + ".lock")
@@ -86,7 +92,8 @@ def locked_update(path: Path, transform: Callable[[str], str]) -> str:
8692
try:
8793
current = path.read_text() if path.exists() else ""
8894
new = transform(current)
89-
atomic_write(path, new)
95+
if new is not current and new != current:
96+
atomic_write(path, new)
9097
return new
9198
finally:
9299
fcntl.flock(lock_f, fcntl.LOCK_UN)

0 commit comments

Comments
 (0)