diff --git a/.github/workflows/opc-lab.yml b/.github/workflows/opc-lab.yml new file mode 100644 index 0000000..a481a31 --- /dev/null +++ b/.github/workflows/opc-lab.yml @@ -0,0 +1,35 @@ +name: OPC Lab + +on: + pull_request: + paths: + - 'labs/opc-company/**' + - 'scripts/verify.sh' + - 'README.md' + - 'README.en.md' + - '.github/workflows/opc-lab.yml' + workflow_dispatch: + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify QuietHarness repository + run: ./scripts/verify.sh + - name: Verify OPC fixtures + run: python3 labs/opc-company/tests/run.py + - name: Parse OPC JSON + run: | + find labs/opc-company -name '*.json' -print0 | while IFS= read -r -d '' file; do + python3 -m json.tool "$file" >/dev/null + done + - name: Reject private or production identifiers + run: | + hits="$(grep -REn '(/(Users|home)/[^/]+|leo[-]vault|_[i]nventory|active[-]tasks|T[0-9]{3,}|thread[_ -]?id|session[_ -]?id|0x[a-fA-F0-9]{40})' labs/opc-company 2>/dev/null || true)" + if [ -n "$hits" ]; then + echo 'OPC_VERIFY_FAIL private or production pattern detected' >&2 + echo "$hits" >&2 + exit 1 + fi + echo OPC_PRIVATE_SCAN_OK diff --git a/labs/opc-company/README.en.md b/labs/opc-company/README.en.md new file mode 100644 index 0000000..231e362 --- /dev/null +++ b/labs/opc-company/README.en.md @@ -0,0 +1,30 @@ +# OPC Company Layer (experimental) + +This is QuietHarness's company-scale governance lab, not another multi-agent framework. + +QuietHarness focuses on reliable execution by one or a few agents. The OPC Company Layer focuses on durable ownership, canonical state, handoffs, acceptance, semantic ACKs, and worker failover across multiple long-lived agents. + +## 10-minute first success + +```bash +cd labs/opc-company +python3 scripts/opc.py status examples/synthetic-company + +python3 scripts/opc.py verify \ + --task fixtures/false-completion/task.json \ + --receipt fixtures/false-completion/receipt.json +# expected: rejected + +python3 scripts/opc.py verify \ + --task examples/synthetic-company/tasks/product-release.json \ + --receipt examples/synthetic-company/receipts/product-release.valid.json +# expected: PASS + +python3 scripts/opc.py failover \ + --before fixtures/quota-failover/task.before.json \ + --after fixtures/quota-failover/task.after.json +# expected: same task, same owner, new worker, PASS +``` + +Public: protocols, schemas, synthetic fixtures, verifier. +Private: live business tasks, production topology, account bindings, schedulers, exact quota routing, trading alpha, credentials. diff --git a/labs/opc-company/README.md b/labs/opc-company/README.md new file mode 100644 index 0000000..89e406d --- /dev/null +++ b/labs/opc-company/README.md @@ -0,0 +1,37 @@ +# OPC Company Layer(实验性) + +这是 QuietHarness 的公司级治理实验层,不是另一个“多 Agent 框架”。 + +QuietHarness 解决单个或少量 Agent 的可靠执行;OPC Company Layer 解决多个长期 Owner / Worker 如何共享事实、交接、验收,并在换模型或额度耗尽后继续同一任务。 + +## 10 分钟 First Success + +```bash +cd labs/opc-company + +python3 scripts/opc.py status examples/synthetic-company + +# 1) Agent 自述完成,但没有 artifact / validation / owner writeback +python3 scripts/opc.py verify \ + --task fixtures/false-completion/task.json \ + --receipt fixtures/false-completion/receipt.json +# 预期:拒绝 + +# 2) 真实 receipt +python3 scripts/opc.py verify \ + --task examples/synthetic-company/tasks/product-release.json \ + --receipt examples/synthetic-company/receipts/product-release.valid.json +# 预期:PASS + +# 3) Worker 额度耗尽,但任务不迁移 +python3 scripts/opc.py failover \ + --before fixtures/quota-failover/task.before.json \ + --after fixtures/quota-failover/task.after.json +# 预期:同 task / 同 owner / 新 worker,PASS +``` + +## 公开边界 + +公开的是协议、schema、synthetic fixture 和 verifier。 + +不公开真实业务任务、生产 Owner 拓扑、账号绑定、scheduler、MCPX production 配置、额度路由、交易策略和其他 operational alpha。 diff --git a/labs/opc-company/examples/synthetic-company/organization.json b/labs/opc-company/examples/synthetic-company/organization.json new file mode 100644 index 0000000..d74fb8f --- /dev/null +++ b/labs/opc-company/examples/synthetic-company/organization.json @@ -0,0 +1 @@ +{"schema_version":1,"organization_id":"synthetic-opc","roles":["ops","research","content","product"]} \ No newline at end of file diff --git a/labs/opc-company/examples/synthetic-company/receipts/product-release.valid.json b/labs/opc-company/examples/synthetic-company/receipts/product-release.valid.json new file mode 100644 index 0000000..9ea67ef --- /dev/null +++ b/labs/opc-company/examples/synthetic-company/receipts/product-release.valid.json @@ -0,0 +1 @@ +{"schema_version":1,"receipt_id":"receipt-product-release-1","task_id":"product-release","worker":"worker-b","outcome":"completed","artifacts":["artifacts/product-change.diff"],"validation":["tests: PASS"],"owner_writeback":true,"downstream_ack":null,"created_at":"2026-01-01T10:00:00Z"} \ No newline at end of file diff --git a/labs/opc-company/examples/synthetic-company/roles.json b/labs/opc-company/examples/synthetic-company/roles.json new file mode 100644 index 0000000..cd8788c --- /dev/null +++ b/labs/opc-company/examples/synthetic-company/roles.json @@ -0,0 +1 @@ +[{"schema_version":1,"role_id":"ops","name":"Ops Owner","owner":"ops-owner","responsibilities":["governance","acceptance","recovery"]},{"schema_version":1,"role_id":"research","name":"Research Owner","owner":"research-owner","responsibilities":["research","source verification"]},{"schema_version":1,"role_id":"content","name":"Content Owner","owner":"content-owner","responsibilities":["drafting","release readiness"]},{"schema_version":1,"role_id":"product","name":"Product Owner","owner":"product-owner","responsibilities":["implementation","tests"]}] \ No newline at end of file diff --git a/labs/opc-company/examples/synthetic-company/tasks/product-release.json b/labs/opc-company/examples/synthetic-company/tasks/product-release.json new file mode 100644 index 0000000..7f5efcf --- /dev/null +++ b/labs/opc-company/examples/synthetic-company/tasks/product-release.json @@ -0,0 +1 @@ +{"schema_version":1,"id":"product-release","title":"Implement and verify a small product change","status":"active","owner":"product","primary_worker":"worker-a","claimed_by":"worker-a","claimed_at":"2026-01-01T09:00:00Z","next_action":"Implement the change and produce validation evidence.","updated_at":"2026-01-01T09:00:00Z","artifacts":[],"decision_gate":null,"hard_gates_pending":[],"downstream_requires_ack":false} \ No newline at end of file diff --git a/labs/opc-company/examples/synthetic-company/tasks/research-to-content.json b/labs/opc-company/examples/synthetic-company/tasks/research-to-content.json new file mode 100644 index 0000000..2df9357 --- /dev/null +++ b/labs/opc-company/examples/synthetic-company/tasks/research-to-content.json @@ -0,0 +1 @@ +{"schema_version":1,"id":"research-to-content","title":"Turn verified research into a content draft","status":"active","owner":"research","primary_worker":"worker-a","claimed_by":"worker-a","claimed_at":"2026-01-01T09:00:00Z","next_action":"Produce verified research artifact and hand it to Content Owner.","updated_at":"2026-01-01T09:00:00Z","artifacts":[],"decision_gate":null,"hard_gates_pending":[],"downstream_requires_ack":true} \ No newline at end of file diff --git a/labs/opc-company/fixtures/false-completion/receipt.json b/labs/opc-company/fixtures/false-completion/receipt.json new file mode 100644 index 0000000..aae5e1a --- /dev/null +++ b/labs/opc-company/fixtures/false-completion/receipt.json @@ -0,0 +1 @@ +{"schema_version":1,"receipt_id":"receipt-false-done","task_id":"product-release","worker":"worker-a","outcome":"completed","artifacts":[],"validation":[],"owner_writeback":false,"downstream_ack":null,"created_at":"2026-01-01T09:30:00Z"} \ No newline at end of file diff --git a/labs/opc-company/fixtures/false-completion/task.json b/labs/opc-company/fixtures/false-completion/task.json new file mode 100644 index 0000000..7f5efcf --- /dev/null +++ b/labs/opc-company/fixtures/false-completion/task.json @@ -0,0 +1 @@ +{"schema_version":1,"id":"product-release","title":"Implement and verify a small product change","status":"active","owner":"product","primary_worker":"worker-a","claimed_by":"worker-a","claimed_at":"2026-01-01T09:00:00Z","next_action":"Implement the change and produce validation evidence.","updated_at":"2026-01-01T09:00:00Z","artifacts":[],"decision_gate":null,"hard_gates_pending":[],"downstream_requires_ack":false} \ No newline at end of file diff --git a/labs/opc-company/fixtures/missing-ack/receipt.json b/labs/opc-company/fixtures/missing-ack/receipt.json new file mode 100644 index 0000000..b5a6456 --- /dev/null +++ b/labs/opc-company/fixtures/missing-ack/receipt.json @@ -0,0 +1 @@ +{"schema_version":1,"receipt_id":"receipt-research-1","task_id":"research-to-content","worker":"worker-a","outcome":"completed","artifacts":["artifacts/research.md"],"validation":["sources: verified"],"owner_writeback":true,"downstream_ack":null,"created_at":"2026-01-01T09:30:00Z"} \ No newline at end of file diff --git a/labs/opc-company/fixtures/missing-ack/task.json b/labs/opc-company/fixtures/missing-ack/task.json new file mode 100644 index 0000000..2df9357 --- /dev/null +++ b/labs/opc-company/fixtures/missing-ack/task.json @@ -0,0 +1 @@ +{"schema_version":1,"id":"research-to-content","title":"Turn verified research into a content draft","status":"active","owner":"research","primary_worker":"worker-a","claimed_by":"worker-a","claimed_at":"2026-01-01T09:00:00Z","next_action":"Produce verified research artifact and hand it to Content Owner.","updated_at":"2026-01-01T09:00:00Z","artifacts":[],"decision_gate":null,"hard_gates_pending":[],"downstream_requires_ack":true} \ No newline at end of file diff --git a/labs/opc-company/fixtures/quota-failover/failover.json b/labs/opc-company/fixtures/quota-failover/failover.json new file mode 100644 index 0000000..969884e --- /dev/null +++ b/labs/opc-company/fixtures/quota-failover/failover.json @@ -0,0 +1 @@ +{"reason":"worker_quota_exhausted","from_worker":"worker-a","to_worker":"worker-b","same_task_id":true,"new_task_created":false,"canonical_state_preserved":true} \ No newline at end of file diff --git a/labs/opc-company/fixtures/quota-failover/task.after.json b/labs/opc-company/fixtures/quota-failover/task.after.json new file mode 100644 index 0000000..5929ceb --- /dev/null +++ b/labs/opc-company/fixtures/quota-failover/task.after.json @@ -0,0 +1 @@ +{"schema_version":1,"id":"product-release","title":"Implement and verify a small product change","status":"active","owner":"product","primary_worker":"worker-b","claimed_by":"worker-b","claimed_at":"2026-01-01T09:45:00Z","next_action":"Resume the same implementation batch from canonical task state; do not repeat completed work.","updated_at":"2026-01-01T09:45:00Z","artifacts":[],"decision_gate":null,"hard_gates_pending":[],"downstream_requires_ack":false} \ No newline at end of file diff --git a/labs/opc-company/fixtures/quota-failover/task.before.json b/labs/opc-company/fixtures/quota-failover/task.before.json new file mode 100644 index 0000000..7f5efcf --- /dev/null +++ b/labs/opc-company/fixtures/quota-failover/task.before.json @@ -0,0 +1 @@ +{"schema_version":1,"id":"product-release","title":"Implement and verify a small product change","status":"active","owner":"product","primary_worker":"worker-a","claimed_by":"worker-a","claimed_at":"2026-01-01T09:00:00Z","next_action":"Implement the change and produce validation evidence.","updated_at":"2026-01-01T09:00:00Z","artifacts":[],"decision_gate":null,"hard_gates_pending":[],"downstream_requires_ack":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/disposition.schema.json b/labs/opc-company/schemas/disposition.schema.json new file mode 100644 index 0000000..7d91710 --- /dev/null +++ b/labs/opc-company/schemas/disposition.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/disposition.schema.json","title":"OPC Semantic Disposition","type":"object","required":["schema_version","message_id","consumer","status","decided_at"],"properties":{"schema_version":{"const":1},"message_id":{"type":"string","minLength":1},"consumer":{"type":"string","minLength":1},"status":{"enum":["ADOPTED","REJECTED","DEFERRED","DUPLICATE","UNKNOWN"]},"reason":{"type":"string"},"decided_at":{"type":"string","minLength":1}},"additionalProperties":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/gate.schema.json b/labs/opc-company/schemas/gate.schema.json new file mode 100644 index 0000000..9ebb987 --- /dev/null +++ b/labs/opc-company/schemas/gate.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/gate.schema.json","title":"OPC Gate","type":"object","required":["schema_version","gate_id","kind","status"],"properties":{"schema_version":{"const":1},"gate_id":{"type":"string","minLength":1},"kind":{"enum":["human_approval","account","funds","publish","production","unknown"]},"status":{"enum":["OPEN","PASS","BLOCKED","UNKNOWN"]},"evidence":{"type":"array","items":{"type":"string"}}},"additionalProperties":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/handoff.schema.json b/labs/opc-company/schemas/handoff.schema.json new file mode 100644 index 0000000..4a71391 --- /dev/null +++ b/labs/opc-company/schemas/handoff.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/handoff.schema.json","title":"OPC Handoff","type":"object","required":["schema_version","handoff_id","task_id","from","to","body","created_at"],"properties":{"schema_version":{"const":1},"handoff_id":{"type":"string","minLength":1},"task_id":{"type":"string","minLength":1},"from":{"type":"string","minLength":1},"to":{"type":"string","minLength":1},"body":{"type":"string","minLength":1},"created_at":{"type":"string","minLength":1},"receipt_ref":{"type":["string","null"]}},"additionalProperties":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/organization.schema.json b/labs/opc-company/schemas/organization.schema.json new file mode 100644 index 0000000..80b9f0e --- /dev/null +++ b/labs/opc-company/schemas/organization.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/organization.schema.json","title":"OPC Organization","type":"object","required":["schema_version","organization_id","roles"],"properties":{"schema_version":{"const":1},"organization_id":{"type":"string","minLength":1},"roles":{"type":"array","minItems":1,"items":{"type":"string","minLength":1},"uniqueItems":true}},"additionalProperties":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/receipt.schema.json b/labs/opc-company/schemas/receipt.schema.json new file mode 100644 index 0000000..a05925b --- /dev/null +++ b/labs/opc-company/schemas/receipt.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/receipt.schema.json","title":"OPC Completion Receipt","type":"object","required":["schema_version","receipt_id","task_id","worker","outcome","artifacts","validation","owner_writeback","created_at"],"properties":{"schema_version":{"const":1},"receipt_id":{"type":"string","minLength":1},"task_id":{"type":"string","minLength":1},"worker":{"type":"string","minLength":1},"outcome":{"enum":["completed","blocked","negative_result","partial"]},"artifacts":{"type":"array","items":{"type":"string"}},"validation":{"type":"array","items":{"type":"string"}},"owner_writeback":{"type":"boolean"},"downstream_ack":{"type":["object","null"]},"created_at":{"type":"string","minLength":1}},"additionalProperties":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/role.schema.json b/labs/opc-company/schemas/role.schema.json new file mode 100644 index 0000000..ae3c835 --- /dev/null +++ b/labs/opc-company/schemas/role.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/role.schema.json","title":"OPC Role","type":"object","required":["schema_version","role_id","name","owner","responsibilities"],"properties":{"schema_version":{"const":1},"role_id":{"type":"string","minLength":1},"name":{"type":"string","minLength":1},"owner":{"type":"string","minLength":1},"responsibilities":{"type":"array","minItems":1,"items":{"type":"string","minLength":1}}},"additionalProperties":false} \ No newline at end of file diff --git a/labs/opc-company/schemas/task.schema.json b/labs/opc-company/schemas/task.schema.json new file mode 100644 index 0000000..9495649 --- /dev/null +++ b/labs/opc-company/schemas/task.schema.json @@ -0,0 +1 @@ +{"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://example.invalid/opc/task.schema.json","title":"OPC Task","type":"object","required":["schema_version","id","status","owner","primary_worker","next_action","updated_at","artifacts"],"properties":{"schema_version":{"const":1},"id":{"type":"string","minLength":1},"title":{"type":"string"},"status":{"enum":["active","waiting","monitoring","parked","archived"]},"owner":{"type":"string","minLength":1},"primary_worker":{"type":["string","null"]},"claimed_by":{"type":["string","null"]},"claimed_at":{"type":["string","null"]},"next_action":{"type":"string","minLength":1},"updated_at":{"type":"string","minLength":1},"artifacts":{"type":"array","items":{"type":"string"}},"decision_gate":{"type":["object","null"]},"hard_gates_pending":{"type":"array","items":{"type":"string"}},"downstream_requires_ack":{"type":"boolean","default":false}},"additionalProperties":true} \ No newline at end of file diff --git a/labs/opc-company/scripts/opc.py b/labs/opc-company/scripts/opc.py new file mode 100644 index 0000000..dfabc57 --- /dev/null +++ b/labs/opc-company/scripts/opc.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +from __future__ import annotations +import argparse, json +from pathlib import Path + +VALID_STATUSES = {"active","waiting","monitoring","parked","archived"} + +def read_json(path): + return json.loads(Path(path).read_text(encoding="utf-8")) + +def lint_task(task): + errors=[] + for k in ("schema_version","id","status","owner","primary_worker","next_action","updated_at","artifacts"): + if k not in task: errors.append(f"missing:{k}") + if task.get("status") not in VALID_STATUSES: errors.append("invalid:status") + if task.get("claimed_by") and not task.get("claimed_at"): errors.append("claim_without_time") + if task.get("status") == "archived" and task.get("claimed_by"): errors.append("archived_but_claimed") + return errors + +def cmd_lint(args): + errors=[] + for p in args.files: + obj=read_json(p) + if isinstance(obj, dict) and "status" in obj and "id" in obj: + errors += [f"{p}:{e}" for e in lint_task(obj)] + if errors: + for e in errors: print("OPC_LINT_ERROR", e) + return 1 + print(f"OPC_LINT_OK files={len(args.files)}") + return 0 + +def cmd_status(args): + d=Path(args.company); tasks=[] + for p in sorted((d/"tasks").glob("*.json")): tasks.append(read_json(p)) + active=[t for t in tasks if t.get("status")=="active"] + out={"task_count":len(tasks),"active_tasks":[t["id"] for t in active],"active_owners":sorted(set(t.get("owner") for t in active)),"pending_ack_tasks":[t["id"] for t in tasks if t.get("downstream_requires_ack")],"work_continuing":bool(active)} + print(json.dumps(out, indent=2)); return 0 + +def cmd_verify(args): + task=read_json(args.task); receipt=read_json(args.receipt); errors=[] + if receipt.get("task_id") != task.get("id"): errors.append("task_id_mismatch") + if receipt.get("outcome") == "completed": + if not receipt.get("artifacts"): errors.append("missing_artifact") + if not receipt.get("validation"): errors.append("missing_validation") + if receipt.get("owner_writeback") is not True: errors.append("missing_owner_writeback") + if task.get("downstream_requires_ack"): + ack=receipt.get("downstream_ack") + if not isinstance(ack, dict) or ack.get("status") not in {"ADOPTED","REJECTED","DEFERRED","DUPLICATE"}: errors.append("missing_semantic_ack") + if errors: + for e in errors: print("OPC_VERIFY_REJECT", e) + return 1 + print("OPC_VERIFY_PASS"); return 0 + +def cmd_failover(args): + before=read_json(args.before); after=read_json(args.after); errors=[] + if before.get("id") != after.get("id"): errors.append("task_id_changed") + if before.get("owner") != after.get("owner"): errors.append("owner_changed") + if before.get("artifacts") != after.get("artifacts"): errors.append("artifacts_changed_during_failover") + if before.get("primary_worker") == after.get("primary_worker"): errors.append("worker_not_changed") + if errors: + for e in errors: print("OPC_FAILOVER_REJECT", e) + return 1 + print(f"OPC_FAILOVER_PASS task={before.get('id')} from={before.get('primary_worker')} to={after.get('primary_worker')}"); return 0 + +def main(): + ap=argparse.ArgumentParser(prog="opc"); sp=ap.add_subparsers(dest="cmd",required=True) + p=sp.add_parser("lint"); p.add_argument("files",nargs="+"); p.set_defaults(fn=cmd_lint) + p=sp.add_parser("status"); p.add_argument("company"); p.set_defaults(fn=cmd_status) + p=sp.add_parser("verify"); p.add_argument("--task",required=True); p.add_argument("--receipt",required=True); p.set_defaults(fn=cmd_verify) + p=sp.add_parser("failover"); p.add_argument("--before",required=True); p.add_argument("--after",required=True); p.set_defaults(fn=cmd_failover) + args=ap.parse_args(); raise SystemExit(args.fn(args)) + +if __name__=="__main__": main() diff --git a/labs/opc-company/tests/run.py b/labs/opc-company/tests/run.py new file mode 100644 index 0000000..aaf146f --- /dev/null +++ b/labs/opc-company/tests/run.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +from pathlib import Path +import subprocess, sys, json +ROOT=Path(__file__).resolve().parents[1]; OPC=ROOT/"scripts/opc.py" +def run(args, expected): + p=subprocess.run([sys.executable,str(OPC),*args],text=True,capture_output=True) + if p.returncode != expected: + print("TEST_FAIL",args,"expected",expected,"got",p.returncode); print(p.stdout,p.stderr); raise SystemExit(1) + return p.stdout +for p in ROOT.rglob("*.json"): json.loads(p.read_text()) +out=run(["status",str(ROOT/"examples/synthetic-company")],0); assert '"work_continuing": true' in out +out=run(["verify","--task",str(ROOT/"fixtures/false-completion/task.json"),"--receipt",str(ROOT/"fixtures/false-completion/receipt.json")],1); assert "missing_artifact" in out and "missing_validation" in out and "missing_owner_writeback" in out +out=run(["verify","--task",str(ROOT/"fixtures/missing-ack/task.json"),"--receipt",str(ROOT/"fixtures/missing-ack/receipt.json")],1); assert "missing_semantic_ack" in out +out=run(["verify","--task",str(ROOT/"examples/synthetic-company/tasks/product-release.json"),"--receipt",str(ROOT/"examples/synthetic-company/receipts/product-release.valid.json")],0); assert "OPC_VERIFY_PASS" in out +out=run(["failover","--before",str(ROOT/"fixtures/quota-failover/task.before.json"),"--after",str(ROOT/"fixtures/quota-failover/task.after.json")],0); assert "OPC_FAILOVER_PASS" in out +task_files=[str(p) for p in (ROOT/"examples/synthetic-company/tasks").glob("*.json")]; out=run(["lint",*task_files],0); assert "OPC_LINT_OK" in out +print("OPC_TEST_OK")