harnessops 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harnessops/__init__.py +4 -0
- harnessops/__main__.py +5 -0
- harnessops/adapters/__init__.py +2 -0
- harnessops/adapters/base.py +39 -0
- harnessops/adapters/generic_code.py +6 -0
- harnessops/adapters/harnessops_core.py +17 -0
- harnessops/adapters/paper_harness_project.py +21 -0
- harnessops/adapters/paper_harness_upstream.py +15 -0
- harnessops/adapters/python_package.py +14 -0
- harnessops/adapters/runops_project.py +21 -0
- harnessops/adapters/runops_upstream.py +17 -0
- harnessops/cli/__init__.py +1 -0
- harnessops/cli/add_failure.py +85 -0
- harnessops/cli/add_feedback.py +4 -0
- harnessops/cli/agent.py +81 -0
- harnessops/cli/decide.py +50 -0
- harnessops/cli/detect.py +28 -0
- harnessops/cli/doctor.py +43 -0
- harnessops/cli/eval.py +58 -0
- harnessops/cli/feedback.py +116 -0
- harnessops/cli/init.py +80 -0
- harnessops/cli/lab.py +49 -0
- harnessops/cli/link.py +4 -0
- harnessops/cli/main.py +34 -0
- harnessops/cli/migrate.py +36 -0
- harnessops/cli/profiles.py +37 -0
- harnessops/cli/propose.py +47 -0
- harnessops/cli/report.py +17 -0
- harnessops/cli/route.py +42 -0
- harnessops/core/__init__.py +2 -0
- harnessops/core/agent_bridge.py +45 -0
- harnessops/core/detect.py +67 -0
- harnessops/core/evaluation.py +75 -0
- harnessops/core/lock.py +57 -0
- harnessops/core/manifest.py +60 -0
- harnessops/core/migration.py +40 -0
- harnessops/core/overlay.py +207 -0
- harnessops/core/paths.py +19 -0
- harnessops/core/project.py +51 -0
- harnessops/core/records.py +422 -0
- harnessops/core/render.py +33 -0
- harnessops/core/routing.py +38 -0
- harnessops/core/sanitize.py +41 -0
- harnessops/core/validation.py +123 -0
- harnessops/core/yamlio.py +42 -0
- harnessops/migrations/__init__.py +2 -0
- harnessops/migrations/registry.py +2 -0
- harnessops/migrations/v0_1_to_v0_2.py +3 -0
- harnessops/profiles/__init__.py +2 -0
- harnessops/profiles/builtins/__init__.py +2 -0
- harnessops/profiles/builtins/generic-code.yml +35 -0
- harnessops/profiles/builtins/harnessops-core.yml +49 -0
- harnessops/profiles/builtins/paper-harness-project.yml +67 -0
- harnessops/profiles/builtins/paper-harness-upstream.yml +53 -0
- harnessops/profiles/builtins/python-package.yml +40 -0
- harnessops/profiles/builtins/runops-project.yml +79 -0
- harnessops/profiles/builtins/runops-upstream.yml +49 -0
- harnessops/profiles/builtins/target-harness.yml +37 -0
- harnessops/profiles/loader.py +4 -0
- harnessops/profiles/registry.py +53 -0
- harnessops/schemas/__init__.py +2 -0
- harnessops/schemas/json/__init__.py +2 -0
- harnessops/schemas/json/decision.schema.json +19 -0
- harnessops/schemas/json/eval-case.schema.json +13 -0
- harnessops/schemas/json/experiment.schema.json +10 -0
- harnessops/schemas/json/failure-record.schema.json +13 -0
- harnessops/schemas/json/feedback-record.schema.json +17 -0
- harnessops/schemas/json/harness-manifest.schema.json +20 -0
- harnessops/schemas/json/harnessops-project.schema.json +15 -0
- harnessops/schemas/json/hypothesis.schema.json +23 -0
- harnessops/schemas/json/profile.schema.json +23 -0
- harnessops/schemas/loader.py +11 -0
- harnessops-0.1.0.dist-info/METADATA +75 -0
- harnessops-0.1.0.dist-info/RECORD +77 -0
- harnessops-0.1.0.dist-info/WHEEL +4 -0
- harnessops-0.1.0.dist-info/entry_points.txt +3 -0
- harnessops-0.1.0.dist-info/licenses/LICENSE +2 -0
harnessops/__init__.py
ADDED
harnessops/__main__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class DetectionResult:
|
|
9
|
+
profile_id: str | None
|
|
10
|
+
repository_kind: str
|
|
11
|
+
confidence: float
|
|
12
|
+
markers: list[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class CheckResult:
|
|
17
|
+
name: str
|
|
18
|
+
ok: bool
|
|
19
|
+
message: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Adapter:
|
|
23
|
+
id = "base"
|
|
24
|
+
|
|
25
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
26
|
+
return DetectionResult(None, "unknown", 0.0, [])
|
|
27
|
+
|
|
28
|
+
def default_profile_id(self, root: Path) -> str | None:
|
|
29
|
+
return self.detect(root).profile_id
|
|
30
|
+
|
|
31
|
+
def doctor_checks(self, root: Path) -> list[CheckResult]:
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
def routing_hints(self, text: str) -> list[str]:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
def eval_case_templates(self) -> list[str]:
|
|
38
|
+
return []
|
|
39
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from harnessops.adapters.base import Adapter, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HarnessOpsCoreAdapter(Adapter):
|
|
9
|
+
id = "harnessops_core"
|
|
10
|
+
|
|
11
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
12
|
+
markers = [m for m in ["pyproject.toml", "src/harnessops", "src/harnessops/profiles", "src/harnessops/schemas"] if (root / m).exists()]
|
|
13
|
+
pyproject = (root / "pyproject.toml").read_text(encoding="utf-8") if (root / "pyproject.toml").exists() else ""
|
|
14
|
+
if 'name = "harnessops"' in pyproject and len(markers) >= 2:
|
|
15
|
+
return DetectionResult("harnessops-core", "harnessops-repository", 0.95, markers)
|
|
16
|
+
return DetectionResult(None, "unknown", 0.0, markers)
|
|
17
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from harnessops.adapters.base import Adapter, CheckResult, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PaperHarnessProjectAdapter(Adapter):
|
|
9
|
+
id = "paper_harness_project"
|
|
10
|
+
|
|
11
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
12
|
+
markers = [m for m in ["manuscript", "notes/claim-evidence-map.md", "refs", "submission"] if (root / m).exists()]
|
|
13
|
+
profile = "paper-harness-project" if len(markers) >= 2 else None
|
|
14
|
+
return DetectionResult(profile, "project-repository", len(markers) / 4.0, markers)
|
|
15
|
+
|
|
16
|
+
def doctor_checks(self, root: Path) -> list[CheckResult]:
|
|
17
|
+
return [
|
|
18
|
+
CheckResult("manuscript", (root / "manuscript").exists(), "manuscript exists"),
|
|
19
|
+
CheckResult("notes", (root / "notes").exists(), "notes exists"),
|
|
20
|
+
]
|
|
21
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from harnessops.adapters.base import Adapter, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PaperHarnessUpstreamAdapter(Adapter):
|
|
9
|
+
id = "paper_harness_upstream"
|
|
10
|
+
|
|
11
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
12
|
+
markers = [m for m in ["template", "scripts/publish-scaffold.sh", "template/manuscript"] if (root / m).exists()]
|
|
13
|
+
profile = "paper-harness-upstream" if len(markers) >= 2 else None
|
|
14
|
+
return DetectionResult(profile, "target-repository", len(markers) / 3.0, markers)
|
|
15
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from harnessops.adapters.base import Adapter, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PythonPackageAdapter(Adapter):
|
|
9
|
+
id = "python_package"
|
|
10
|
+
|
|
11
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
12
|
+
markers = ["pyproject.toml"] if (root / "pyproject.toml").exists() else []
|
|
13
|
+
return DetectionResult("python-package" if markers else None, "python-package", 0.4, markers)
|
|
14
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from harnessops.adapters.base import Adapter, CheckResult, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RunopsProjectAdapter(Adapter):
|
|
9
|
+
id = "runops_project"
|
|
10
|
+
|
|
11
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
12
|
+
markers = [m for m in [".runops/harness.lock", "campaign.toml", "cases", "runs"] if (root / m).exists()]
|
|
13
|
+
profile = "runops-project" if len(markers) >= 2 else None
|
|
14
|
+
return DetectionResult(profile, "project-repository", len(markers) / 4.0, markers)
|
|
15
|
+
|
|
16
|
+
def doctor_checks(self, root: Path) -> list[CheckResult]:
|
|
17
|
+
checks = []
|
|
18
|
+
for marker in ["campaign.toml", "cases", "runs"]:
|
|
19
|
+
checks.append(CheckResult(marker, (root / marker).exists(), f"{marker} exists"))
|
|
20
|
+
return checks
|
|
21
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from harnessops.adapters.base import Adapter, DetectionResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RunopsUpstreamAdapter(Adapter):
|
|
9
|
+
id = "runops_upstream"
|
|
10
|
+
|
|
11
|
+
def detect(self, root: Path) -> DetectionResult:
|
|
12
|
+
markers = [m for m in ["pyproject.toml", "src/runops", "src/runops/templates"] if (root / m).exists()]
|
|
13
|
+
pyproject = (root / "pyproject.toml").read_text(encoding="utf-8") if (root / "pyproject.toml").exists() else ""
|
|
14
|
+
if 'name = "runops"' in pyproject and len(markers) >= 2:
|
|
15
|
+
return DetectionResult("runops-upstream", "target-repository", 0.95, markers)
|
|
16
|
+
return DetectionResult(None, "unknown", 0.0, markers)
|
|
17
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HarnessOps CLI パッケージ。"""
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from harnessops.core.paths import find_root
|
|
8
|
+
from harnessops.core.project import load_project
|
|
9
|
+
from harnessops.core.records import create_failure, create_feedback_from_failure, find_record, read_record
|
|
10
|
+
from harnessops.core.render import refresh_views
|
|
11
|
+
from harnessops.core.routing import classify_text
|
|
12
|
+
from harnessops.core.routing import DISPOSITIONS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def add_failure_command(
|
|
16
|
+
title: str = typer.Option(..., "--title"),
|
|
17
|
+
target: Optional[str] = typer.Option(None, "--target"),
|
|
18
|
+
context: str = typer.Option("", "--context"),
|
|
19
|
+
what_happened: str = typer.Option("", "--what-happened"),
|
|
20
|
+
why_matters: str = typer.Option("", "--why-matters"),
|
|
21
|
+
desired_behavior: str = typer.Option("", "--desired-behavior"),
|
|
22
|
+
local_workaround: str = typer.Option("", "--local-workaround"),
|
|
23
|
+
disposition: Optional[str] = typer.Option(None, "--disposition"),
|
|
24
|
+
from_file: Optional[str] = typer.Option(None, "--from-file"),
|
|
25
|
+
interactive: bool = typer.Option(False, "--interactive"),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""プロジェクト側の失敗レコードを作成します。"""
|
|
28
|
+
del interactive
|
|
29
|
+
root = find_root()
|
|
30
|
+
project = load_project(root)
|
|
31
|
+
if project.overlay_mode not in {"feedback-source", "local-and-feedback"}:
|
|
32
|
+
typer.echo("add-failure には feedback-source または local-and-feedback mode が必要です")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
file_text = ""
|
|
35
|
+
if from_file:
|
|
36
|
+
file_text = (root / from_file).read_text(encoding="utf-8")
|
|
37
|
+
routing = classify_text(" ".join([title, context, what_happened, file_text]), target=target)
|
|
38
|
+
if disposition is not None and disposition not in DISPOSITIONS:
|
|
39
|
+
typer.echo(f"disposition が不正です: {disposition}")
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
path = create_failure(
|
|
42
|
+
project,
|
|
43
|
+
title=title,
|
|
44
|
+
target=target,
|
|
45
|
+
context=context or file_text,
|
|
46
|
+
what_happened=what_happened or file_text,
|
|
47
|
+
why_matters=why_matters,
|
|
48
|
+
desired_behavior=desired_behavior,
|
|
49
|
+
local_workaround=local_workaround,
|
|
50
|
+
disposition_type=disposition or routing["type"],
|
|
51
|
+
)
|
|
52
|
+
refresh_views(root, project.overlay_path)
|
|
53
|
+
typer.echo(path.relative_to(root).as_posix())
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def add_feedback_command(
|
|
57
|
+
from_id: str = typer.Option(..., "--from"),
|
|
58
|
+
target: Optional[str] = typer.Option(None, "--target"),
|
|
59
|
+
feedback_type: Optional[str] = typer.Option(None, "--type"),
|
|
60
|
+
title: Optional[str] = typer.Option(None, "--title"),
|
|
61
|
+
summary: str = typer.Option("", "--summary"),
|
|
62
|
+
) -> None:
|
|
63
|
+
"""既存の失敗からフィードバック下書きを作成します。
|
|
64
|
+
|
|
65
|
+
下書きは `hops feedback export --sanitize` でエクスポートされるまで非公開です。
|
|
66
|
+
"""
|
|
67
|
+
root = find_root()
|
|
68
|
+
project = load_project(root)
|
|
69
|
+
failure_frontmatter, _ = read_record(find_record(project, from_id))
|
|
70
|
+
resolved_target = target or failure_frontmatter.get("disposition", {}).get("target") or "harnessops"
|
|
71
|
+
path = create_feedback_from_failure(
|
|
72
|
+
project,
|
|
73
|
+
failure_ref=from_id,
|
|
74
|
+
target=resolved_target,
|
|
75
|
+
feedback_type=feedback_type,
|
|
76
|
+
title=title,
|
|
77
|
+
summary=summary,
|
|
78
|
+
)
|
|
79
|
+
refresh_views(root, project.overlay_path)
|
|
80
|
+
typer.echo(path.relative_to(root).as_posix())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register(app: typer.Typer) -> None:
|
|
84
|
+
app.command("add-failure")(add_failure_command)
|
|
85
|
+
app.command("add-feedback")(add_feedback_command)
|
harnessops/cli/agent.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from harnessops.core.agent_bridge import write_bridge
|
|
10
|
+
from harnessops.core.paths import find_root
|
|
11
|
+
|
|
12
|
+
agent_app = typer.Typer(help="エージェントブリッジ/プラグイン成果物をインストールまたは検証します。")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@agent_app.command("bridge")
|
|
16
|
+
def bridge(codex: bool = typer.Option(False, "--codex"), claude: bool = typer.Option(False, "--claude"), force: bool = typer.Option(False, "--force")) -> None:
|
|
17
|
+
"""薄いリポジトリローカルブリッジスキルを生成します。"""
|
|
18
|
+
if not codex and not claude:
|
|
19
|
+
codex = True
|
|
20
|
+
root = find_root()
|
|
21
|
+
paths = write_bridge(root, codex=codex, claude=claude, force=force)
|
|
22
|
+
typer.echo(json.dumps([path.relative_to(root).as_posix() for path in paths], indent=2))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@agent_app.command("install")
|
|
26
|
+
def install(
|
|
27
|
+
codex: bool = typer.Option(False, "--codex"),
|
|
28
|
+
claude: bool = typer.Option(False, "--claude"),
|
|
29
|
+
scope: str = typer.Option("repo", "--scope"),
|
|
30
|
+
force: bool = typer.Option(False, "--force"),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""リポジトリローカルブリッジをインストールするか、同梱プラグインをユーザープラグインディレクトリへコピーします。"""
|
|
33
|
+
if not codex and not claude:
|
|
34
|
+
codex = True
|
|
35
|
+
root = find_root()
|
|
36
|
+
if scope == "repo":
|
|
37
|
+
bridge(codex=codex, claude=claude, force=force)
|
|
38
|
+
return
|
|
39
|
+
if scope != "user":
|
|
40
|
+
typer.echo("scope は repo または user で指定してください")
|
|
41
|
+
raise typer.Exit(1)
|
|
42
|
+
home = Path.home()
|
|
43
|
+
installed = []
|
|
44
|
+
for enabled, host, plugin_dir in [
|
|
45
|
+
(codex, "codex", home / ".codex" / "plugins" / "harnessops"),
|
|
46
|
+
(claude, "claude", home / ".claude" / "plugins" / "harnessops"),
|
|
47
|
+
]:
|
|
48
|
+
if not enabled:
|
|
49
|
+
continue
|
|
50
|
+
source = root / "plugins" / host / "harnessops"
|
|
51
|
+
if not source.exists():
|
|
52
|
+
source = Path(__file__).resolve().parents[3] / "plugins" / host / "harnessops"
|
|
53
|
+
if not source.exists():
|
|
54
|
+
typer.echo(f"同梱 {host} プラグインソースが見つかりません")
|
|
55
|
+
raise typer.Exit(1)
|
|
56
|
+
if plugin_dir.exists():
|
|
57
|
+
if not force:
|
|
58
|
+
typer.echo(f"プラグインは既に存在します: {plugin_dir}")
|
|
59
|
+
raise typer.Exit(2)
|
|
60
|
+
shutil.rmtree(plugin_dir)
|
|
61
|
+
plugin_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
shutil.copytree(source, plugin_dir)
|
|
63
|
+
installed.append(plugin_dir.as_posix())
|
|
64
|
+
typer.echo(json.dumps(installed, indent=2))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@agent_app.command("verify")
|
|
68
|
+
def verify() -> None:
|
|
69
|
+
"""リポジトリローカルブリッジまたは同梱プラグイン成果物を検証します。"""
|
|
70
|
+
root = find_root()
|
|
71
|
+
expected = root / ".agents" / "skills" / "harnessops-bridge" / "SKILL.md"
|
|
72
|
+
packaged = root / "plugins" / "codex" / "harnessops" / ".codex-plugin" / "plugin.json"
|
|
73
|
+
if expected.exists() or packaged.exists():
|
|
74
|
+
typer.echo("ok")
|
|
75
|
+
else:
|
|
76
|
+
typer.echo("ブリッジまたは同梱プラグインが見つかりません")
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def register(app: typer.Typer) -> None:
|
|
81
|
+
app.add_typer(agent_app, name="agent")
|
harnessops/cli/decide.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from harnessops.core.paths import find_root
|
|
6
|
+
from harnessops.core.project import load_project
|
|
7
|
+
from harnessops.core.records import create_decision
|
|
8
|
+
|
|
9
|
+
STATUSES = {"adopted", "rejected", "parked", "needs-more-evidence", "merged-into-other", "not-upstreamable"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def decide_command(
|
|
13
|
+
experiment: str | None = typer.Option(None, "--experiment"),
|
|
14
|
+
from_id: str | None = typer.Option(None, "--from"),
|
|
15
|
+
status: str = typer.Option(..., "--status"),
|
|
16
|
+
reason: str = typer.Option("", "--reason"),
|
|
17
|
+
evidence: str = typer.Option("", "--evidence"),
|
|
18
|
+
regression_risk: str = typer.Option("", "--regression-risk"),
|
|
19
|
+
follow_up: str = typer.Option("", "--follow-up"),
|
|
20
|
+
guard_path: str | None = typer.Option(None, "--guard-path"),
|
|
21
|
+
) -> None:
|
|
22
|
+
"""判断レコードを作成します。"""
|
|
23
|
+
if status not in STATUSES:
|
|
24
|
+
typer.echo(f"status が不正です: {status}")
|
|
25
|
+
raise typer.Exit(1)
|
|
26
|
+
source = experiment or from_id
|
|
27
|
+
if not source:
|
|
28
|
+
typer.echo("--experiment または --from を指定してください")
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
if status == "adopted" and (not evidence or not regression_risk or not guard_path):
|
|
31
|
+
typer.echo("adopted の判断には --evidence、--regression-risk、--guard-path が必要です")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
root = find_root()
|
|
34
|
+
project = load_project(root)
|
|
35
|
+
path = create_decision(
|
|
36
|
+
project,
|
|
37
|
+
source=source,
|
|
38
|
+
status=status,
|
|
39
|
+
title=f"{status} {source}",
|
|
40
|
+
reason=reason or f"`{source}` に対して `{status}` の判断を記録しました。",
|
|
41
|
+
evidence=evidence or "証拠は指定されていません。この判断は採用可能ではありません。",
|
|
42
|
+
regression_risk=regression_risk or "回帰リスクは評価されていません。",
|
|
43
|
+
follow_up=follow_up or "変更を昇格する前にこの判断をレビューしてください。",
|
|
44
|
+
guard_path=guard_path,
|
|
45
|
+
)
|
|
46
|
+
typer.echo(path.relative_to(root).as_posix())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def register(app: typer.Typer) -> None:
|
|
50
|
+
app.command("decide")(decide_command)
|
harnessops/cli/detect.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from harnessops.core.detect import detect_repository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def detect(json_output: bool = typer.Option(False, "--json")) -> None:
|
|
12
|
+
"""リポジトリ種別と推奨プロファイルを推定します。"""
|
|
13
|
+
root = Path.cwd().resolve()
|
|
14
|
+
result = detect_repository(root)
|
|
15
|
+
result["root"] = str(root)
|
|
16
|
+
if json_output:
|
|
17
|
+
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
18
|
+
return
|
|
19
|
+
typer.echo(f"ルート: {root}")
|
|
20
|
+
typer.echo(f"プロファイル: {result.get('profile')}")
|
|
21
|
+
typer.echo(f"リポジトリ種別: {result.get('repository_kind')}")
|
|
22
|
+
typer.echo(f"検出元: {result.get('source')}")
|
|
23
|
+
if result.get("markers"):
|
|
24
|
+
typer.echo("マーカー: " + ", ".join(result["markers"]))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def register(app: typer.Typer) -> None:
|
|
28
|
+
app.command("detect")(detect)
|
harnessops/cli/doctor.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from harnessops.core.paths import find_root
|
|
8
|
+
from harnessops.core.project import load_project
|
|
9
|
+
from harnessops.core.validation import doctor as doctor_project
|
|
10
|
+
from harnessops.core.migration import check_migrations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def doctor_command(
|
|
14
|
+
json_output: bool = typer.Option(False, "--json"),
|
|
15
|
+
provider: bool = typer.Option(False, "--provider"),
|
|
16
|
+
check_overlay: bool = typer.Option(False, "--check-overlay"),
|
|
17
|
+
check_records: bool = typer.Option(False, "--check-records"),
|
|
18
|
+
allow_pending: bool = typer.Option(False, "--allow-pending"),
|
|
19
|
+
) -> None:
|
|
20
|
+
"""HarnessOps リンク、オーバーレイ、プロファイル、レコードを検証します。"""
|
|
21
|
+
del check_overlay
|
|
22
|
+
project = load_project(find_root())
|
|
23
|
+
result = doctor_project(project, check_records=check_records)
|
|
24
|
+
migration = check_migrations(project)
|
|
25
|
+
if not migration["ok"] and not allow_pending:
|
|
26
|
+
result["ok"] = False
|
|
27
|
+
result["errors"].extend(f"未適用マイグレーション: {item}" for item in migration["pending"])
|
|
28
|
+
if provider:
|
|
29
|
+
result["provider"] = {"checked": False, "message": "MVP doctor ではプロバイダコマンド実行は有効化されていません"}
|
|
30
|
+
if json_output:
|
|
31
|
+
typer.echo(json.dumps(result, indent=2, sort_keys=True))
|
|
32
|
+
else:
|
|
33
|
+
typer.echo("ok" if result["ok"] else "失敗")
|
|
34
|
+
for warning in result["warnings"]:
|
|
35
|
+
typer.echo(f"警告: {warning}")
|
|
36
|
+
for error in result["errors"]:
|
|
37
|
+
typer.echo(f"エラー: {error}")
|
|
38
|
+
if not result["ok"]:
|
|
39
|
+
raise typer.Exit(1)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register(app: typer.Typer) -> None:
|
|
43
|
+
app.command("doctor")(doctor_command)
|
harnessops/cli/eval.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from harnessops.core.evaluation import parse_scores, write_manual_eval
|
|
6
|
+
from harnessops.core.paths import find_root
|
|
7
|
+
from harnessops.core.project import load_project
|
|
8
|
+
from harnessops.core.records import find_record, read_record
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def eval_command(
|
|
12
|
+
case: str | None = typer.Option(None, "--case"),
|
|
13
|
+
manual: bool = typer.Option(False, "--manual"),
|
|
14
|
+
all_cases: bool = typer.Option(False, "--all"),
|
|
15
|
+
experiment: str | None = typer.Option(None, "--experiment"),
|
|
16
|
+
score: list[str] = typer.Option(None, "--score"),
|
|
17
|
+
notes: str = typer.Option("", "--notes"),
|
|
18
|
+
) -> None:
|
|
19
|
+
"""評価ケースの多軸手動スコアカードを保存します。"""
|
|
20
|
+
root = find_root()
|
|
21
|
+
project = load_project(root)
|
|
22
|
+
cases = []
|
|
23
|
+
if all_cases:
|
|
24
|
+
cases = [path.stem.split("-", 1)[0] for path in sorted((project.overlay_dir / "records/eval-cases").glob("E*.md"))]
|
|
25
|
+
elif experiment:
|
|
26
|
+
try:
|
|
27
|
+
experiment_path = find_record(project, experiment)
|
|
28
|
+
frontmatter, _ = read_record(experiment_path)
|
|
29
|
+
cases = [str(item) for item in frontmatter.get("eval_cases", [])]
|
|
30
|
+
except FileNotFoundError:
|
|
31
|
+
typer.echo(f"experiment が見つかりません: {experiment}")
|
|
32
|
+
raise typer.Exit(1)
|
|
33
|
+
elif case:
|
|
34
|
+
cases = [case]
|
|
35
|
+
else:
|
|
36
|
+
typer.echo("--case または --all を指定してください")
|
|
37
|
+
raise typer.Exit(1)
|
|
38
|
+
if not cases:
|
|
39
|
+
typer.echo("評価ケースが見つかりません")
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
if not manual:
|
|
42
|
+
typer.echo("このコマンドには手動採点が必要です。--manual を指定してください")
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
try:
|
|
45
|
+
scores = parse_scores(score or [])
|
|
46
|
+
except ValueError as exc:
|
|
47
|
+
typer.echo(str(exc))
|
|
48
|
+
raise typer.Exit(1) from exc
|
|
49
|
+
outputs = []
|
|
50
|
+
for case_id in cases:
|
|
51
|
+
yml_path, md_path = write_manual_eval(project, case_id=case_id, scores=scores, notes=notes, experiment=experiment)
|
|
52
|
+
outputs.append(yml_path.relative_to(root).as_posix())
|
|
53
|
+
outputs.append(md_path.relative_to(root).as_posix())
|
|
54
|
+
typer.echo("\n".join(outputs))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def register(app: typer.Typer) -> None:
|
|
58
|
+
app.command("eval")(eval_command)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from harnessops.core.paths import find_root
|
|
10
|
+
from harnessops.core.project import load_project
|
|
11
|
+
from harnessops.core.records import create_imported_feedback, next_id, read_record
|
|
12
|
+
from harnessops.core.render import refresh_views
|
|
13
|
+
from harnessops.core.sanitize import sanitize_text
|
|
14
|
+
from harnessops.profiles.registry import load_profile
|
|
15
|
+
|
|
16
|
+
feedback_app = typer.Typer(help="フィードバックバンドルをエクスポート/インポートします。")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@feedback_app.command("export")
|
|
20
|
+
def export_feedback(
|
|
21
|
+
target: Optional[str] = typer.Option(None, "--target"),
|
|
22
|
+
sanitize: bool = typer.Option(False, "--sanitize"),
|
|
23
|
+
format: str = typer.Option("markdown", "--format"), # noqa: A002
|
|
24
|
+
allow_private: bool = typer.Option(False, "--allow-private"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""プロジェクト側レコードからサニタイズ済み上流/メタフィードバックバンドルを生成します。"""
|
|
27
|
+
root = find_root()
|
|
28
|
+
project = load_project(root)
|
|
29
|
+
if not sanitize and not allow_private:
|
|
30
|
+
typer.echo("--allow-private なしの未サニタイズエクスポートは拒否します")
|
|
31
|
+
raise typer.Exit(1)
|
|
32
|
+
profile = load_profile(project.profile_id)
|
|
33
|
+
records = []
|
|
34
|
+
target_filter = target or "all"
|
|
35
|
+
for path in sorted((project.overlay_dir / "records/failures").glob("*.md")):
|
|
36
|
+
frontmatter, body = read_record(path)
|
|
37
|
+
disposition = frontmatter.get("disposition", {})
|
|
38
|
+
if disposition.get("type") not in {"target-upstream-candidate", "meta-harness-candidate", "protocol-candidate"}:
|
|
39
|
+
continue
|
|
40
|
+
if disposition.get("target") == target_filter or target_filter == "all":
|
|
41
|
+
records.append((path, frontmatter, body))
|
|
42
|
+
for rel in ["records/upstream-feedback", "records/meta-feedback"]:
|
|
43
|
+
for path in sorted((project.overlay_dir / rel).glob("*.md")):
|
|
44
|
+
frontmatter, body = read_record(path)
|
|
45
|
+
if frontmatter.get("target") == target_filter or target_filter == "all":
|
|
46
|
+
records.append((path, frontmatter, body))
|
|
47
|
+
if not records:
|
|
48
|
+
typer.echo("一致するフィードバックレコードがありません")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
resolved_targets = sorted({str(record[1].get("target") or record[1].get("disposition", {}).get("target") or "unknown") for record in records})
|
|
51
|
+
export_target = target or ("mixed" if len(resolved_targets) != 1 else resolved_targets[0])
|
|
52
|
+
prefix = "MF" if export_target == "harnessops" else "UF"
|
|
53
|
+
out_dir = project.overlay_dir / "views" / "exported-feedback"
|
|
54
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
export_id = next_id(out_dir, prefix)
|
|
56
|
+
title = f"{export_target} へのフィードバック"
|
|
57
|
+
sections = []
|
|
58
|
+
for path, frontmatter, body in records:
|
|
59
|
+
sections.append(f"## 送信元 {frontmatter.get('id')}: {path.name}\n\n{body.strip()}\n")
|
|
60
|
+
bundle_body = "\n".join(sections)
|
|
61
|
+
if sanitize:
|
|
62
|
+
bundle_body = sanitize_text(bundle_body, root=root, profile=profile, allow_private=allow_private)
|
|
63
|
+
if format in {"issue", "github-issue"}:
|
|
64
|
+
bundle_body = "## Issue下書き\n\n" + bundle_body + "\n\n## 確認\n\nこれは下書きのみです。HarnessOps はリモートIssueを自動作成しません。\n"
|
|
65
|
+
frontmatter = {
|
|
66
|
+
"id": export_id,
|
|
67
|
+
"record_type": "meta_feedback" if prefix == "MF" else "upstream_feedback",
|
|
68
|
+
"created_at": records[0][1].get("created_at"),
|
|
69
|
+
"status": "draft",
|
|
70
|
+
"target": export_target,
|
|
71
|
+
"source_failure": records[0][1].get("id"),
|
|
72
|
+
"sanitized": bool(sanitize),
|
|
73
|
+
"visibility": "sanitized" if sanitize else "private-until-sanitized",
|
|
74
|
+
"format": format,
|
|
75
|
+
"included_targets": resolved_targets,
|
|
76
|
+
}
|
|
77
|
+
text = "---\n" + json.dumps(frontmatter, indent=2) + "\n---\n\n# " + title + "\n\n" + bundle_body
|
|
78
|
+
out_path = out_dir / f"{export_id}-{export_target}-feedback.md"
|
|
79
|
+
out_path.write_text(text, encoding="utf-8")
|
|
80
|
+
refresh_views(root, project.overlay_path)
|
|
81
|
+
typer.echo(out_path.relative_to(root).as_posix())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@feedback_app.command("import")
|
|
85
|
+
def import_feedback(
|
|
86
|
+
path: Optional[Path] = typer.Argument(None),
|
|
87
|
+
issue: Optional[int] = typer.Option(None, "--issue"),
|
|
88
|
+
repo: Optional[str] = typer.Option(None, "--repo"),
|
|
89
|
+
) -> None:
|
|
90
|
+
"""フィードバックバンドルをターゲット側 harness-lab にインポートします。"""
|
|
91
|
+
root = find_root()
|
|
92
|
+
project = load_project(root)
|
|
93
|
+
if project.overlay_mode not in {"upstream-lab", "meta-lab"}:
|
|
94
|
+
typer.echo("feedback import には upstream-lab または meta-lab mode が必要です")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
if issue is not None:
|
|
97
|
+
source = {"id": f"ISSUE-{issue}", "record_type": "upstream_feedback", "issue": {"url": f"https://github.com/{repo or 'unknown'}/issues/{issue}"}}
|
|
98
|
+
body = f"{repo or 'unknown'} から GitHub issue {issue} をインポートしました。"
|
|
99
|
+
title = f"GitHub issue {issue}"
|
|
100
|
+
elif path is not None:
|
|
101
|
+
source_path = path if path.is_absolute() else root / path
|
|
102
|
+
source, body = read_record(source_path)
|
|
103
|
+
if source.get("record_type") not in {"upstream_feedback", "meta_feedback"} or not source.get("sanitized", False):
|
|
104
|
+
typer.echo("import にはサニタイズ済み upstream_feedback または meta_feedback バンドルが必要です")
|
|
105
|
+
raise typer.Exit(1)
|
|
106
|
+
title = source_path.stem
|
|
107
|
+
else:
|
|
108
|
+
typer.echo("バンドルパスまたは --issue を指定してください")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
out_path = create_imported_feedback(project, source_record=source, body=body, title=title)
|
|
111
|
+
refresh_views(root, project.overlay_path)
|
|
112
|
+
typer.echo(out_path.relative_to(root).as_posix())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def register(app: typer.Typer) -> None:
|
|
116
|
+
app.add_typer(feedback_app, name="feedback")
|