Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/prompts/orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ Read all run-scoped artifacts, enforce autonomy gates, and return the owned `ver

## Decision space
- `publish_pr`
- `local_complete`
- `await_approval`
- `reject`
- `no_changes`

## Rules
- Follow `.agent-policy.yaml` as the source of truth for risk-class autonomy, project publication rules, protected paths, and human approval gates.
- If risk is low or medium, no hard blockers remain, and policy allows publication, choose `publish_pr`.
- If the user explicitly requires the changes to remain local and uncommitted, choose `local_complete` with `execution_status=completed`; keep publication limitations in warnings and next actions, not as blockers.
- If risk is high, choose `await_approval`.
- Never auto-act on protected paths.
- Never auto-merge, deploy, force-push, rewrite history, or access production credentials.
Expand Down Expand Up @@ -70,7 +72,7 @@ If the profile is missing or inconsistent, choose `await_approval` or `reject` a
## Required JSON shape
```json
{
"decision": "publish_pr|await_approval|reject|no_changes",
"decision": "publish_pr|local_complete|await_approval|reject|no_changes",
"execution_status": "planned|running|completed|blocked|failed",
"task": "",
"project_profile": "agent_workspace|django|nextjs_web",
Expand Down
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,10 @@ A task is not done until:
- Keep local agent memory private; do not copy private issue journals into public PR text unless explicitly approved and sanitized.
- Route every model-backed role through the provider-neutral `Runtime` contract. In Step 2 only local-subscription `codex-cli` is a production runtime; direct provider SDK/CLI calls from Harness code and Model Router behavior are forbidden.
- Treat `.agent/project.yaml` as local execution identity only. It must never grant publication, merge, deployment, credential, network, protected-path, or provider-routing authority.

## Mobilistics preflight
- For work in `mobilistics-artificial-intelligence`, `webcrawler`, `mai-nextjs`, or `vpagent/frontend`, every planning, implementation, repair, and review role must use the user-level `$naivy-preflight-review` skill.
- Run the skill before editing to build an impact map and after verification to audit the complete diff. Load every applicable backend, crawler, or frontend profile.
- Record the impact-map decisions in the role-owned plan artifact and the final evidence in the role-owned review or verdict artifact; do not introduce a parallel workflow-state location.
- Reject `local_complete` when an applicable evidence field is missing, a required realistic integration check was replaced only by mocks, or an unresolved P1/P2 finding remains.
- Treat the skill as a quality gate, not publication authority. Continue to follow `.agent-policy.yaml`, the active user instruction, and the existing publication and approval lifecycle.
2 changes: 2 additions & 0 deletions ai_harness/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,8 @@ def attention_items(
}:
continue
run_id = str(task.get("run_id", ""))
if run_id and run_id in seen_runs:
continue
run = runs_by_id.get(run_id, {})
attention = run.get("attention", {}) if isinstance(run, dict) else {}
summary = str(task.get("exception_reason", "")).strip()
Expand Down
2 changes: 1 addition & 1 deletion schemas/verdict.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"lessons_updated"
],
"enums": {
"decision": ["publish_pr", "await_approval", "reject", "no_changes"],
"decision": ["publish_pr", "local_complete", "await_approval", "reject", "no_changes"],
"execution_status": ["planned", "running", "completed", "blocked", "failed"],
"project_profile": ["agent_workspace", "django", "nextjs_web"],
"risk_class": ["low", "medium", "high"]
Expand Down
42 changes: 41 additions & 1 deletion scripts/approval_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import Any

from runtime_contracts import load_json as load_schema, validate_contract
from security_approval import security_scope
from task_queue import DEFAULT_DB, TaskQueue, TaskRecord
from ai_harness.recovery.policy import load_recovery_policy

Expand Down Expand Up @@ -104,9 +105,22 @@ def append_error(run_dir: Path, *, code: str, message: str) -> None:
def canonical_scope(scope: dict[str, Any]) -> dict[str, Any]:
actions = sorted({str(item) for item in scope.get("actions", []) if isinstance(item, str)})
paths = sorted({str(item) for item in scope.get("paths", []) if isinstance(item, str)})
finding_ids = sorted(
{str(item) for item in scope.get("finding_ids", []) if isinstance(item, str)}
)
gate = str(scope.get("gate", ""))
risk_class = str(scope.get("risk_class", ""))
return {"actions": actions, "paths": paths, "gate": gate, "risk_class": risk_class}
security_fingerprint = str(scope.get("security_fingerprint", ""))
verifier_fingerprint = str(scope.get("verifier_fingerprint", ""))
return {
"actions": actions,
"paths": paths,
"gate": gate,
"risk_class": risk_class,
"finding_ids": finding_ids,
"security_fingerprint": security_fingerprint,
"verifier_fingerprint": verifier_fingerprint,
}


def scope_covers(requested: dict[str, Any], approved: dict[str, Any]) -> bool:
Expand Down Expand Up @@ -154,12 +168,36 @@ def default_scope(workflow: dict[str, Any], role: str) -> dict[str, Any]:
security = read_json(security_path)
if security.get("status") in {"fail", "blocked"} or security.get("verdict") == "broken":
actions.append("accept_security_finding")
security_details = security_scope(security)
else:
security_details = {}
else:
security_details = {}
verifier_artifacts = {
"architecture-consistency-agent": "architecture_consistency.json",
"semantic-conflict-agent": "semantic_conflict.json",
"reviewer": "review.json",
}
verifier_name = verifier_artifacts.get(role)
verifier_path = Path(str(workflow.get("artifacts_dir", ""))) / str(verifier_name or "")
if verifier_name and verifier_path.is_file():
actions.append("accept_unavailable_verification")
verifier = read_json(verifier_path)
verifier_details = {
"verifier_fingerprint": hashlib.sha256(
json.dumps(verifier, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
}
else:
verifier_details = {}
return canonical_scope(
{
"actions": actions,
"paths": paths,
"gate": role,
"risk_class": risk_class,
**security_details,
**verifier_details,
}
)

Expand Down Expand Up @@ -367,6 +405,7 @@ def _prepare_resume_locked(run_dir: Path) -> dict[str, Any]:
"approval_id": approval["approval_id"],
"gate": role,
"scope": approval["approved_scope"],
"checkpoint_fingerprint": approval["checkpoint_fingerprint"],
"granted_at": approval["decided_at"],
"reason": approval["reason"],
}
Expand Down Expand Up @@ -409,6 +448,7 @@ def resume_run(run_dir: Path, *, queue: TaskQueue) -> tuple[dict[str, Any], Task
priority=100,
max_retries=2,
run_id=run_dir.name,
supersede_awaiting_approval=True,
)
if not result["already_consumed"]:
append_event(run_dir, "workflow.resume_queued", approval)
Expand Down
38 changes: 35 additions & 3 deletions scripts/task_queue.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
import sqlite3
import sys
import time
from contextlib import contextmanager
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
from typing import Any, Iterator


ROOT = Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -110,12 +111,17 @@ def __init__(self, path: Path = DEFAULT_DB) -> None:
self.path.parent.mkdir(parents=True, exist_ok=True)
self.initialize()

def connect(self) -> sqlite3.Connection:
@contextmanager
def connect(self) -> Iterator[sqlite3.Connection]:
connection = sqlite3.connect(self.path, timeout=1, isolation_level=None)
connection.row_factory = sqlite3.Row
connection.execute("PRAGMA busy_timeout = 1000")
connection.execute("PRAGMA foreign_keys = ON")
return connection
try:
with connection:
yield connection
finally:
connection.close()

@staticmethod
def _begin_immediate(connection: sqlite3.Connection) -> None:
Expand Down Expand Up @@ -274,6 +280,7 @@ def enqueue(
priority: int = 0,
max_retries: int = 2,
run_id: str = "",
supersede_awaiting_approval: bool = False,
) -> TaskRecord:
if not task_key.strip():
raise ValueError("task_key is required")
Expand All @@ -297,6 +304,31 @@ def enqueue(
raise RuntimeError("failed to enqueue task")
if cursor.rowcount == 1:
self.event(connection, int(row["id"]), "enqueued", details={"priority": priority}, now=now)
if supersede_awaiting_approval and run_id:
superseded = connection.execute(
"""
SELECT id FROM tasks
WHERE run_id=? AND status='awaiting_approval' AND id<>?
ORDER BY id
""",
(run_id, int(row["id"])),
).fetchall()
connection.execute(
"""
UPDATE tasks SET status='completed',updated_at=?,requires_human=0,
exception_reason='',recovery_action=''
WHERE run_id=? AND status='awaiting_approval' AND id<>?
""",
(now, run_id, int(row["id"])),
)
for previous in superseded:
self.event(
connection,
int(previous["id"]),
"superseded_by_resume",
details={"successor_task_id": int(row["id"]), "run_id": run_id},
now=now,
)
connection.commit()
return self.record(row)

Expand Down
Loading
Loading