init: llm-wiki-haness 하네스 설계
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,667 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate every platform adapter from repository-neutral workflow/role sources.
|
||||
|
||||
The generator intentionally uses only the Python standard library. Neutral
|
||||
source bodies live below ``harness/source``; platform-only execution metadata
|
||||
(tool names, model selection, and sandbox mode) lives beside this adapter.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
|
||||
MANIFEST_REL = Path("harness/source/generation-manifest.json")
|
||||
PLATFORM_METADATA_REL = Path("harness/adapters/platform-metadata.json")
|
||||
EXECUTION_PROFILES_REL = Path("harness/source/execution-profiles.json")
|
||||
MARKDOWN_MARKER = "<!-- GENERATED from {source} sha256:{digest}; DO NOT EDIT -->"
|
||||
COMMENT_MARKER = "# GENERATED from {source} sha256:{digest}; DO NOT EDIT"
|
||||
JSON_MARKER = "GENERATED from {source} sha256:{digest}; DO NOT EDIT"
|
||||
|
||||
SUPPORTED_SOURCE_KINDS = {"workflow", "agent"}
|
||||
SUPPORTED_TARGET_KINDS = {
|
||||
"agent-skill",
|
||||
"agent-workflow",
|
||||
"claude-command",
|
||||
"plugin-agent-md",
|
||||
"claude-agent-md",
|
||||
"codex-agent-md",
|
||||
"codex-agent-toml",
|
||||
"antigravity-agent-json",
|
||||
}
|
||||
SUPPORTED_HEADING_LOCALES = {"en", "ko-KR", "mixed"}
|
||||
TARGET_KIND_PREFIXES = {
|
||||
"agent-skill": ".agents/skills/",
|
||||
"agent-workflow": ".agents/workflows/",
|
||||
"claude-command": ".claude/commands/",
|
||||
"plugin-agent-md": ".agents/plugins/wiki-superpowers/agents/",
|
||||
"claude-agent-md": ".claude/agents/",
|
||||
"codex-agent-md": ".codex/agents/",
|
||||
"codex-agent-toml": ".codex/agents/",
|
||||
"antigravity-agent-json": ".agents/agents/",
|
||||
}
|
||||
EXECUTION_KINDS = {"orchestrated", "deterministic"}
|
||||
EXECUTION_PROFILES = {"capture", "design", "audit", "publish"}
|
||||
EXECUTION_RISKS = {"low", "medium", "high", "critical"}
|
||||
|
||||
sys.path.insert(0, str(DEFAULT_ROOT / "harness/runtime"))
|
||||
from fs_transaction import replace_many # noqa: E402
|
||||
|
||||
|
||||
class GenerationError(ValueError):
|
||||
"""Raised when the neutral-source contract is internally inconsistent."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Target:
|
||||
kind: str
|
||||
path: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Source:
|
||||
kind: str
|
||||
identifier: str
|
||||
description: str
|
||||
argument_hint: str | None
|
||||
heading_locale: str
|
||||
metadata_path: str
|
||||
body_path: str
|
||||
body: str
|
||||
digest: str
|
||||
execution_contract: dict[str, Any] | None
|
||||
targets: tuple[Target, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Catalog:
|
||||
root: Path
|
||||
manifest: dict[str, Any]
|
||||
platform_metadata: dict[str, Any]
|
||||
sources: tuple[Source, ...]
|
||||
|
||||
@property
|
||||
def targets(self) -> tuple[tuple[Source, Target], ...]:
|
||||
return tuple((source, target) for source in self.sources for target in source.targets)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenerationResult:
|
||||
stale: tuple[str, ...] = ()
|
||||
missing: tuple[str, ...] = ()
|
||||
extra: tuple[str, ...] = ()
|
||||
written: tuple[str, ...] = ()
|
||||
|
||||
@property
|
||||
def drift(self) -> tuple[str, ...]:
|
||||
return self.stale + self.missing
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
return not (self.stale or self.missing or self.extra)
|
||||
|
||||
|
||||
def _read_json(path: Path) -> dict[str, Any]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise GenerationError(f"JSON root must be an object: {path}")
|
||||
return data
|
||||
|
||||
|
||||
def _safe_rel(value: Any, *, field: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise GenerationError(f"{field} must be a non-empty path")
|
||||
path = Path(value)
|
||||
if path.is_absolute() or ".." in path.parts:
|
||||
raise GenerationError(f"{field} escapes repository: {value}")
|
||||
return path.as_posix()
|
||||
|
||||
|
||||
def _non_empty_string(data: dict[str, Any], key: str, *, source: str) -> str:
|
||||
value = data.get(key)
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise GenerationError(f"{source}: missing non-empty {key}")
|
||||
return value
|
||||
|
||||
|
||||
def _composite_digest(metadata_raw: bytes, body_path: str, body_raw: bytes) -> str:
|
||||
digest = hashlib.sha256()
|
||||
digest.update(metadata_raw)
|
||||
digest.update(b"\0")
|
||||
digest.update(body_path.encode("utf-8"))
|
||||
digest.update(b"\0")
|
||||
digest.update(body_raw)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def _expand_globs(root: Path, patterns: Iterable[str], excludes: Iterable[str] = ()) -> set[str]:
|
||||
excluded = set(excludes)
|
||||
found: set[str] = set()
|
||||
for pattern in patterns:
|
||||
found.update(
|
||||
path.relative_to(root).as_posix()
|
||||
for path in root.glob(pattern)
|
||||
if path.is_file()
|
||||
)
|
||||
return found - excluded
|
||||
|
||||
|
||||
def _validate_source_inventory(root: Path, manifest: dict[str, Any]) -> None:
|
||||
inventory = manifest.get("source_inventory")
|
||||
if not isinstance(inventory, dict):
|
||||
raise GenerationError("manifest source_inventory must be an object")
|
||||
patterns = inventory.get("include")
|
||||
if not isinstance(patterns, list) or not all(isinstance(item, str) for item in patterns):
|
||||
raise GenerationError("manifest source_inventory.include must be a string list")
|
||||
declared_entries = manifest.get("sources")
|
||||
if not isinstance(declared_entries, list) or not declared_entries:
|
||||
raise GenerationError("manifest sources must be a non-empty list")
|
||||
|
||||
declared_metadata: list[str] = []
|
||||
declared_bodies: list[str] = []
|
||||
for entry in declared_entries:
|
||||
if not isinstance(entry, dict):
|
||||
raise GenerationError("manifest source entries must be objects")
|
||||
declared_metadata.append(_safe_rel(entry.get("metadata"), field="source metadata"))
|
||||
declared_bodies.append(_safe_rel(entry.get("body"), field="source body"))
|
||||
|
||||
duplicates = sorted(
|
||||
path for path in set(declared_metadata) if declared_metadata.count(path) > 1
|
||||
)
|
||||
if duplicates:
|
||||
raise GenerationError("duplicate source mapping: " + ", ".join(duplicates))
|
||||
|
||||
declared = set(declared_metadata) | set(declared_bodies)
|
||||
actual = _expand_globs(root, patterns)
|
||||
missing = sorted(declared - actual)
|
||||
extra = sorted(actual - declared)
|
||||
if missing:
|
||||
raise GenerationError("missing source files: " + ", ".join(missing))
|
||||
if extra:
|
||||
raise GenerationError("extra source files: " + ", ".join(extra))
|
||||
|
||||
|
||||
def _load_source(root: Path, entry: dict[str, Any]) -> Source:
|
||||
metadata_path = _safe_rel(entry.get("metadata"), field="source metadata")
|
||||
body_path = _safe_rel(entry.get("body"), field="source body")
|
||||
metadata_abs = root / metadata_path
|
||||
body_abs = root / body_path
|
||||
metadata_raw = metadata_abs.read_bytes()
|
||||
body_raw = body_abs.read_bytes()
|
||||
data = json.loads(metadata_raw.decode("utf-8"))
|
||||
if not isinstance(data, dict):
|
||||
raise GenerationError(f"{metadata_path}: JSON root must be an object")
|
||||
kind = data.get("source_kind")
|
||||
if kind not in SUPPORTED_SOURCE_KINDS:
|
||||
raise GenerationError(f"{metadata_path}: unsupported source_kind: {kind}")
|
||||
expected_schema = 2 if kind == "workflow" else 1
|
||||
if data.get("schema_version") != expected_schema:
|
||||
raise GenerationError(f"{metadata_path}: expected schema_version {expected_schema}")
|
||||
identifier = _non_empty_string(data, "id", source=metadata_path)
|
||||
description = _non_empty_string(data, "description", source=metadata_path)
|
||||
heading_locale = data.get("heading_locale")
|
||||
if heading_locale not in SUPPORTED_HEADING_LOCALES:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: heading_locale must be one of {sorted(SUPPORTED_HEADING_LOCALES)}"
|
||||
)
|
||||
if data.get("body_path") != body_path:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: body_path {data.get('body_path')!r} does not match manifest {body_path!r}"
|
||||
)
|
||||
argument_hint: str | None = None
|
||||
execution_contract: dict[str, Any] | None = None
|
||||
if kind == "workflow":
|
||||
argument_hint = _non_empty_string(data, "argument_hint", source=metadata_path)
|
||||
execution_contract = _validate_execution_contract(root, data, metadata_path)
|
||||
elif "execution_contract" in data:
|
||||
raise GenerationError(f"{metadata_path}: agents cannot declare execution_contract")
|
||||
|
||||
raw_targets = data.get("targets")
|
||||
if not isinstance(raw_targets, list) or not raw_targets:
|
||||
raise GenerationError(f"{metadata_path}: targets must be a non-empty list")
|
||||
targets: list[Target] = []
|
||||
for raw_target in raw_targets:
|
||||
if not isinstance(raw_target, dict):
|
||||
raise GenerationError(f"{metadata_path}: targets must be objects")
|
||||
target_kind = raw_target.get("kind")
|
||||
if target_kind not in SUPPORTED_TARGET_KINDS:
|
||||
raise GenerationError(f"{metadata_path}: unsupported target kind: {target_kind}")
|
||||
target_path = _safe_rel(raw_target.get("path"), field="target path")
|
||||
if not target_path.startswith(TARGET_KIND_PREFIXES[target_kind]):
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: {target_path} does not match target kind {target_kind}"
|
||||
)
|
||||
if kind == "workflow" and target_kind not in {
|
||||
"agent-skill",
|
||||
"agent-workflow",
|
||||
"claude-command",
|
||||
}:
|
||||
raise GenerationError(f"{metadata_path}: workflow cannot generate {target_kind}")
|
||||
if kind == "agent" and target_kind in {
|
||||
"agent-skill",
|
||||
"agent-workflow",
|
||||
"claude-command",
|
||||
}:
|
||||
raise GenerationError(f"{metadata_path}: agent cannot generate {target_kind}")
|
||||
targets.append(Target(target_kind, target_path))
|
||||
|
||||
body = body_raw.decode("utf-8")
|
||||
if not body.strip():
|
||||
raise GenerationError(f"{body_path}: source body must not be empty")
|
||||
if not body.endswith("\n"):
|
||||
raise GenerationError(f"{body_path}: source body must end with a newline")
|
||||
digest = _composite_digest(metadata_raw, body_path, body_raw)
|
||||
return Source(
|
||||
kind=kind,
|
||||
identifier=identifier,
|
||||
description=description,
|
||||
argument_hint=argument_hint,
|
||||
heading_locale=heading_locale,
|
||||
metadata_path=metadata_path,
|
||||
body_path=body_path,
|
||||
body=body,
|
||||
digest=digest,
|
||||
execution_contract=execution_contract,
|
||||
targets=tuple(targets),
|
||||
)
|
||||
|
||||
|
||||
def _validate_execution_contract(
|
||||
root: Path,
|
||||
data: dict[str, Any],
|
||||
metadata_path: str,
|
||||
) -> dict[str, Any]:
|
||||
if "profile" in data or "default_risk" in data:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: profile/default_risk must be nested under execution_contract"
|
||||
)
|
||||
contract = data.get("execution_contract")
|
||||
if not isinstance(contract, dict):
|
||||
raise GenerationError(f"{metadata_path}: execution_contract must be an object")
|
||||
allowed = {
|
||||
"kind",
|
||||
"profile",
|
||||
"default_risk",
|
||||
"entrypoint",
|
||||
"dry_run_first",
|
||||
"result_schema",
|
||||
"design_bearing",
|
||||
}
|
||||
extra = sorted(set(contract) - allowed)
|
||||
if extra:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: unsupported execution_contract fields: {', '.join(extra)}"
|
||||
)
|
||||
kind = contract.get("kind")
|
||||
profile = contract.get("profile")
|
||||
default_risk = contract.get("default_risk")
|
||||
if kind not in EXECUTION_KINDS:
|
||||
raise GenerationError(f"{metadata_path}: unsupported execution kind: {kind}")
|
||||
if profile not in EXECUTION_PROFILES:
|
||||
raise GenerationError(f"{metadata_path}: unsupported execution profile: {profile}")
|
||||
if default_risk not in EXECUTION_RISKS:
|
||||
raise GenerationError(f"{metadata_path}: unsupported default_risk: {default_risk}")
|
||||
if not isinstance(contract.get("design_bearing"), bool):
|
||||
raise GenerationError(f"{metadata_path}: design_bearing must be boolean")
|
||||
|
||||
deterministic_fields = {"entrypoint", "dry_run_first", "result_schema"}
|
||||
present_deterministic = deterministic_fields.intersection(contract)
|
||||
if kind == "deterministic":
|
||||
missing = sorted(deterministic_fields - present_deterministic)
|
||||
if missing:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: deterministic contract missing: {', '.join(missing)}"
|
||||
)
|
||||
entrypoint = _safe_rel(contract.get("entrypoint"), field="execution entrypoint")
|
||||
if not entrypoint.endswith(".py") or not (root / entrypoint).is_file():
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: deterministic entrypoint is not executable source: {entrypoint}"
|
||||
)
|
||||
if contract.get("dry_run_first") is not True:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: deterministic workflow must set dry_run_first=true"
|
||||
)
|
||||
result_schema = contract.get("result_schema")
|
||||
if not isinstance(result_schema, str) or not result_schema.strip():
|
||||
raise GenerationError(f"{metadata_path}: result_schema must be non-empty")
|
||||
elif present_deterministic:
|
||||
raise GenerationError(
|
||||
f"{metadata_path}: orchestrated workflow cannot declare deterministic fields"
|
||||
)
|
||||
return dict(contract)
|
||||
|
||||
|
||||
def load_catalog(root: Path = DEFAULT_ROOT) -> Catalog:
|
||||
root = root.resolve()
|
||||
manifest_path = root / MANIFEST_REL
|
||||
platform_path = root / PLATFORM_METADATA_REL
|
||||
manifest = _read_json(manifest_path)
|
||||
if manifest.get("schema_version") != 1:
|
||||
raise GenerationError("generation manifest has unsupported schema_version")
|
||||
_validate_source_inventory(root, manifest)
|
||||
|
||||
execution_profiles = _read_json(root / EXECUTION_PROFILES_REL)
|
||||
if execution_profiles.get("schema_version") != "execution-profiles/v1":
|
||||
raise GenerationError("execution profile source has unsupported schema_version")
|
||||
if set(execution_profiles.get("profiles", {})) != EXECUTION_PROFILES:
|
||||
raise GenerationError("generator workflow profiles drift from execution profile SSOT")
|
||||
if set(execution_profiles.get("risk_levels", [])) != EXECUTION_RISKS:
|
||||
raise GenerationError("generator workflow risks drift from execution profile SSOT")
|
||||
|
||||
platform_metadata = _read_json(platform_path)
|
||||
if platform_metadata.get("schema_version") != 1:
|
||||
raise GenerationError("platform metadata has unsupported schema_version")
|
||||
sources = tuple(_load_source(root, entry) for entry in manifest["sources"])
|
||||
|
||||
identifiers: set[tuple[str, str]] = set()
|
||||
mapped_targets: dict[str, str] = {}
|
||||
for source in sources:
|
||||
source_key = (source.kind, source.identifier)
|
||||
if source_key in identifiers:
|
||||
raise GenerationError(f"duplicate logical source: {source.kind}:{source.identifier}")
|
||||
identifiers.add(source_key)
|
||||
for target in source.targets:
|
||||
previous = mapped_targets.get(target.path)
|
||||
if previous is not None:
|
||||
raise GenerationError(
|
||||
f"duplicate target mapping: {target.path} ({previous}, {source.metadata_path})"
|
||||
)
|
||||
mapped_targets[target.path] = source.metadata_path
|
||||
|
||||
expected_count = manifest.get("expected_target_count")
|
||||
if not isinstance(expected_count, int) or expected_count <= 0:
|
||||
raise GenerationError("manifest expected_target_count must be a positive integer")
|
||||
if len(mapped_targets) != expected_count:
|
||||
raise GenerationError(
|
||||
f"mapped target count {len(mapped_targets)} != expected_target_count {expected_count}"
|
||||
)
|
||||
|
||||
configured_agents = platform_metadata.get("agents")
|
||||
if not isinstance(configured_agents, dict):
|
||||
raise GenerationError("platform metadata agents must be an object")
|
||||
agent_ids = {source.identifier for source in sources if source.kind == "agent"}
|
||||
extra_agent_metadata = sorted(set(configured_agents) - agent_ids)
|
||||
if extra_agent_metadata:
|
||||
raise GenerationError("extra platform agent metadata: " + ", ".join(extra_agent_metadata))
|
||||
for source in sources:
|
||||
if source.kind != "agent":
|
||||
continue
|
||||
required_platforms = {
|
||||
"claude" if target.kind == "claude-agent-md" else
|
||||
"codex" if target.kind.startswith("codex-agent-") else
|
||||
"antigravity" if target.kind == "antigravity-agent-json" else
|
||||
"plugin"
|
||||
for target in source.targets
|
||||
}
|
||||
metadata = configured_agents.get(source.identifier)
|
||||
if not isinstance(metadata, dict):
|
||||
raise GenerationError(f"missing platform metadata for agent {source.identifier}")
|
||||
missing_platforms = sorted(required_platforms - set(metadata))
|
||||
if missing_platforms:
|
||||
raise GenerationError(
|
||||
f"missing platform metadata for {source.identifier}: {', '.join(missing_platforms)}"
|
||||
)
|
||||
return Catalog(root, manifest, platform_metadata, sources)
|
||||
|
||||
|
||||
def _yaml_string(value: str) -> str:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
|
||||
|
||||
def _toml_string(value: str) -> str:
|
||||
return json.dumps(value, ensure_ascii=False)
|
||||
|
||||
|
||||
def _markdown_marker(source: Source) -> str:
|
||||
return MARKDOWN_MARKER.format(source=source.metadata_path, digest=source.digest)
|
||||
|
||||
|
||||
def _workflow_policy_block(workflow: str) -> str:
|
||||
return f"""## 실행 정책 해석
|
||||
|
||||
1. 작업을 시작하기 전에 다음 baseline resolver를 실행하고 `status: PLANNED`, `phase: baseline`을 확인한다.
|
||||
|
||||
```bash
|
||||
python3 harness/runtime/workflow_dispatch.py {workflow} --root . --phase baseline
|
||||
```
|
||||
|
||||
2. `mandatory_gates`에서 `required`인 gate는 risk와 무관하게 생략하지 않는다. review dispatch와 응답 형식은 resolver의 `dispatch`, `review_intensity`, `output_contract`만 따른다.
|
||||
3. finding과 claim 집계가 끝나면 최종 응답 전에 같은 workflow를 `--phase final --finding-count <N>`으로 다시 resolve한다. claim이 있으면 `--claims-present`, 공개 claim이면 `--public-claims-present`를 argv 항목으로 추가한다.
|
||||
4. final resolver의 필수 gate·review·output contract를 반영하기 전에는 완료를 선언하지 않는다."""
|
||||
|
||||
|
||||
def _render_workflow(source: Source, target: Target) -> str:
|
||||
assert source.argument_hint is not None
|
||||
assert source.execution_contract is not None
|
||||
contract = json.dumps(source.execution_contract, ensure_ascii=False, separators=(",", ":"))
|
||||
if target.kind == "claude-command":
|
||||
frontmatter = (
|
||||
"---\n"
|
||||
f"description: {_yaml_string(source.description)}\n"
|
||||
f"argument-hint: {source.argument_hint}\n"
|
||||
f"execution_contract: {contract}\n"
|
||||
"---"
|
||||
)
|
||||
arguments = "$ARGUMENTS"
|
||||
prefix = ""
|
||||
elif target.kind == "agent-skill":
|
||||
frontmatter = (
|
||||
"---\n"
|
||||
f"name: {source.identifier}\n"
|
||||
f"description: {_yaml_string(f'{source.description} (입력: {source.argument_hint})')}\n"
|
||||
f"execution_contract: {contract}\n"
|
||||
"---"
|
||||
)
|
||||
arguments = source.argument_hint
|
||||
prefix = ""
|
||||
elif target.kind == "agent-workflow":
|
||||
frontmatter = (
|
||||
"---\n"
|
||||
f"description: {_yaml_string(source.description)}\n"
|
||||
f"execution_contract: {contract}\n"
|
||||
"---"
|
||||
)
|
||||
arguments = source.argument_hint
|
||||
prefix = (
|
||||
f"사용자가 `/{source.identifier} {source.argument_hint}` 를 입력하면 아래 절차를 수행한다.\n\n"
|
||||
)
|
||||
else: # pragma: no cover - source validation prevents this path
|
||||
raise GenerationError(f"unsupported workflow target kind: {target.kind}")
|
||||
body = source.body.replace("{{arguments}}", arguments)
|
||||
policy = _workflow_policy_block(source.identifier)
|
||||
return f"{frontmatter}\n{_markdown_marker(source)}\n\n{prefix}{policy}\n\n{body}".rstrip() + "\n"
|
||||
|
||||
|
||||
def _agent_platform(catalog: Catalog, source: Source, platform: str) -> dict[str, Any]:
|
||||
metadata = catalog.platform_metadata["agents"][source.identifier].get(platform)
|
||||
if not isinstance(metadata, dict):
|
||||
raise GenerationError(f"missing {platform} metadata for {source.identifier}")
|
||||
return metadata
|
||||
|
||||
|
||||
def _render_agent(catalog: Catalog, source: Source, target: Target) -> str:
|
||||
marker = _markdown_marker(source)
|
||||
if target.kind in {"plugin-agent-md", "codex-agent-md"}:
|
||||
frontmatter = (
|
||||
"---\n"
|
||||
f"name: {source.identifier}\n"
|
||||
f"description: {_yaml_string(source.description)}\n"
|
||||
"---"
|
||||
)
|
||||
return f"{frontmatter}\n{marker}\n\n{source.body}".rstrip() + "\n"
|
||||
|
||||
if target.kind == "claude-agent-md":
|
||||
metadata = _agent_platform(catalog, source, "claude")
|
||||
tools = metadata.get("tools")
|
||||
model = metadata.get("model")
|
||||
if not isinstance(tools, str) or not tools or not isinstance(model, str) or not model:
|
||||
raise GenerationError(f"invalid Claude metadata for {source.identifier}")
|
||||
frontmatter = (
|
||||
"---\n"
|
||||
f"name: {source.identifier}\n"
|
||||
f"description: {_yaml_string(source.description)}\n"
|
||||
f"tools: {tools}\n"
|
||||
f"model: {model}\n"
|
||||
"---"
|
||||
)
|
||||
return f"{frontmatter}\n{marker}\n\n{source.body}".rstrip() + "\n"
|
||||
|
||||
if target.kind == "codex-agent-toml":
|
||||
metadata = _agent_platform(catalog, source, "codex")
|
||||
sandbox_mode = metadata.get("sandbox_mode")
|
||||
if not isinstance(sandbox_mode, str) or not sandbox_mode:
|
||||
raise GenerationError(f"invalid Codex metadata for {source.identifier}")
|
||||
if "'''" in source.body:
|
||||
raise GenerationError(f"{source.body_path}: cannot embed triple single quote in TOML")
|
||||
comment = COMMENT_MARKER.format(source=source.metadata_path, digest=source.digest)
|
||||
return (
|
||||
f"{comment}\n"
|
||||
f"name = {_toml_string(source.identifier)}\n"
|
||||
f"description = {_toml_string(source.description)}\n"
|
||||
f"sandbox_mode = {_toml_string(sandbox_mode)}\n"
|
||||
"developer_instructions = '''\n"
|
||||
f"{source.body.rstrip()}\n"
|
||||
"'''\n"
|
||||
)
|
||||
|
||||
if target.kind == "antigravity-agent-json":
|
||||
metadata = _agent_platform(catalog, source, "antigravity")
|
||||
hidden = metadata.get("hidden")
|
||||
tool_names = metadata.get("tool_names")
|
||||
include_sections = metadata.get("include_sections")
|
||||
if not isinstance(hidden, bool):
|
||||
raise GenerationError(f"invalid Antigravity hidden flag for {source.identifier}")
|
||||
if not isinstance(tool_names, list) or not all(isinstance(item, str) for item in tool_names):
|
||||
raise GenerationError(f"invalid Antigravity tool_names for {source.identifier}")
|
||||
if not isinstance(include_sections, list) or not all(
|
||||
isinstance(item, str) for item in include_sections
|
||||
):
|
||||
raise GenerationError(f"invalid Antigravity include_sections for {source.identifier}")
|
||||
payload = {
|
||||
"_generated": JSON_MARKER.format(
|
||||
source=source.metadata_path,
|
||||
digest=source.digest,
|
||||
),
|
||||
"name": source.identifier,
|
||||
"description": source.description,
|
||||
"hidden": hidden,
|
||||
"config": {
|
||||
"customAgent": {
|
||||
"systemPromptSections": [
|
||||
{"title": "Agent System Instructions", "content": source.body.rstrip("\n")}
|
||||
],
|
||||
"toolNames": tool_names,
|
||||
"systemPromptConfig": {"includeSections": include_sections},
|
||||
}
|
||||
},
|
||||
}
|
||||
return json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
||||
|
||||
raise GenerationError(f"unsupported agent target kind: {target.kind}")
|
||||
|
||||
|
||||
def render_target(catalog: Catalog, source: Source, target: Target) -> str:
|
||||
if source.kind == "workflow":
|
||||
return _render_workflow(source, target)
|
||||
return _render_agent(catalog, source, target)
|
||||
|
||||
|
||||
def _target_inventory(catalog: Catalog) -> set[str]:
|
||||
inventory = catalog.manifest.get("target_inventory")
|
||||
if not isinstance(inventory, dict):
|
||||
raise GenerationError("manifest target_inventory must be an object")
|
||||
include = inventory.get("include")
|
||||
exclude = inventory.get("exclude", [])
|
||||
if not isinstance(include, list) or not all(isinstance(item, str) for item in include):
|
||||
raise GenerationError("target_inventory.include must be a string list")
|
||||
if not isinstance(exclude, list) or not all(isinstance(item, str) for item in exclude):
|
||||
raise GenerationError("target_inventory.exclude must be a string list")
|
||||
return _expand_globs(catalog.root, include, exclude)
|
||||
|
||||
|
||||
def _render_inventory(catalog: Catalog) -> dict[Path, bytes]:
|
||||
rendered: dict[Path, bytes] = {}
|
||||
for source, target in catalog.targets:
|
||||
path = (catalog.root / target.path).resolve()
|
||||
if path in rendered:
|
||||
raise GenerationError(f"duplicate rendered target: {target.path}")
|
||||
rendered[path] = render_target(catalog, source, target).encode("utf-8")
|
||||
if len(rendered) != catalog.manifest["expected_target_count"]:
|
||||
raise GenerationError("rendered target inventory count drift")
|
||||
return rendered
|
||||
|
||||
|
||||
def generate(root: Path = DEFAULT_ROOT, *, check: bool = True) -> GenerationResult:
|
||||
catalog = load_catalog(root)
|
||||
declared = {target.path for _, target in catalog.targets}
|
||||
rendered = _render_inventory(catalog)
|
||||
rendered_rel = {path.relative_to(catalog.root).as_posix() for path in rendered}
|
||||
if rendered_rel != declared:
|
||||
raise GenerationError("rendered target inventory differs from declared inventory")
|
||||
actual = _target_inventory(catalog)
|
||||
extra = tuple(sorted(actual - declared))
|
||||
missing = tuple(sorted(declared - actual))
|
||||
if extra:
|
||||
return GenerationResult(missing=missing, extra=extra)
|
||||
|
||||
stale: list[str] = []
|
||||
for path, expected in rendered.items():
|
||||
relative = path.relative_to(catalog.root).as_posix()
|
||||
actual_bytes = path.read_bytes() if path.exists() else None
|
||||
if actual_bytes != expected and path.exists():
|
||||
stale.append(relative)
|
||||
|
||||
if check:
|
||||
return GenerationResult(
|
||||
stale=tuple(sorted(stale)),
|
||||
missing=missing,
|
||||
extra=extra,
|
||||
)
|
||||
if stale or missing:
|
||||
# One transaction owns the complete generated inventory. Passing all
|
||||
# rendered bytes (not only drifted targets) prevents mixed revisions if
|
||||
# the declaration or render order changes during future maintenance.
|
||||
replace_many(rendered)
|
||||
return GenerationResult(written=tuple(sorted(declared)))
|
||||
return GenerationResult()
|
||||
|
||||
|
||||
def _print_result(result: GenerationResult, *, check: bool) -> None:
|
||||
if result.stale:
|
||||
print("stale generated targets: " + ", ".join(result.stale), file=sys.stderr)
|
||||
if result.missing:
|
||||
print("missing generated targets: " + ", ".join(result.missing), file=sys.stderr)
|
||||
if result.extra:
|
||||
print("extra unmapped targets: " + ", ".join(result.extra), file=sys.stderr)
|
||||
if not check:
|
||||
for path in result.written:
|
||||
print(f"generated {path}")
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--check", action="store_true", help="fail on mapping or content drift")
|
||||
mode.add_argument("--write", action="store_true", help="write every missing or stale target")
|
||||
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT, help=argparse.SUPPRESS)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
result = generate(args.root, check=args.check)
|
||||
except (OSError, GenerationError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
||||
print(f"adapter generation failed: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
_print_result(result, check=args.check)
|
||||
if result.extra or (args.check and not result.ok):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate platform rule slices from repository-neutral root rule SSOT files."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
|
||||
CONFIG = Path("harness/source/rule-adapters.json")
|
||||
sys.path.insert(0, str(DEFAULT_ROOT / "harness/runtime"))
|
||||
from fs_transaction import replace_many # noqa: E402
|
||||
|
||||
|
||||
class RuleGenerationError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _safe_path(value: Any) -> Path:
|
||||
if not isinstance(value, str) or not value:
|
||||
raise RuleGenerationError("path must be a non-empty string")
|
||||
path = Path(value)
|
||||
if path.is_absolute() or ".." in path.parts:
|
||||
raise RuleGenerationError(f"path escapes repository: {value}")
|
||||
return path
|
||||
|
||||
|
||||
def _slice(text: str, start: str, end: str | None, source: Path) -> str:
|
||||
lines = text.splitlines(keepends=True)
|
||||
try:
|
||||
start_index = next(index for index, line in enumerate(lines) if line.rstrip("\n") == start)
|
||||
except StopIteration as exc:
|
||||
raise RuleGenerationError(f"{source}: missing start heading {start!r}") from exc
|
||||
end_index = len(lines)
|
||||
if end is not None:
|
||||
try:
|
||||
end_index = next(
|
||||
index for index, line in enumerate(lines[start_index + 1 :], start_index + 1)
|
||||
if line.rstrip("\n") == end
|
||||
)
|
||||
except StopIteration as exc:
|
||||
raise RuleGenerationError(f"{source}: missing end heading {end!r}") from exc
|
||||
return "".join(lines[start_index:end_index]).rstrip() + "\n"
|
||||
|
||||
|
||||
def render(root: Path) -> dict[Path, str]:
|
||||
config_path = root / CONFIG
|
||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||
if config.get("schema_version") != "rule-adapters/v1":
|
||||
raise RuleGenerationError("expected rule-adapters/v1")
|
||||
groups = config.get("groups")
|
||||
if not isinstance(groups, list) or not groups:
|
||||
raise RuleGenerationError("groups must be a non-empty list")
|
||||
rendered: dict[Path, str] = {}
|
||||
for group in groups:
|
||||
if not isinstance(group, dict):
|
||||
raise RuleGenerationError("group must be an object")
|
||||
source_rel = _safe_path(group.get("source"))
|
||||
target_dir = _safe_path(group.get("target_dir"))
|
||||
source = root / source_rel
|
||||
source_text = source.read_text(encoding="utf-8")
|
||||
digest = hashlib.sha256(source_text.encode("utf-8")).hexdigest()
|
||||
slices = group.get("slices")
|
||||
if not isinstance(slices, list) or not slices:
|
||||
raise RuleGenerationError(f"{source_rel}: slices must be non-empty")
|
||||
links: list[str] = []
|
||||
for item in slices:
|
||||
target_name = _safe_path(item.get("target"))
|
||||
if len(target_name.parts) != 1:
|
||||
raise RuleGenerationError("rule slice target must be a filename")
|
||||
start = item.get("start")
|
||||
end = item.get("end")
|
||||
if not isinstance(start, str) or (end is not None and not isinstance(end, str)):
|
||||
raise RuleGenerationError("slice start/end must be headings")
|
||||
target = target_dir / target_name
|
||||
if target in rendered:
|
||||
raise RuleGenerationError(f"duplicate rule target: {target}")
|
||||
marker = f"<!-- GENERATED from {source_rel.as_posix()} sha256:{digest}; DO NOT EDIT -->"
|
||||
rendered[target] = (
|
||||
f"{marker}\n\n"
|
||||
f"Root SSOT: [`{source_rel.as_posix()}`](../../../../../{source_rel.as_posix()})\n\n"
|
||||
f"{_slice(source_text, start, end, source_rel)}"
|
||||
)
|
||||
links.append(f"- [`{target_name.as_posix()}`]({target_name.as_posix()}): `{start}`")
|
||||
index_name = _safe_path(group.get("index"))
|
||||
index = target_dir / index_name
|
||||
marker = f"<!-- GENERATED from {source_rel.as_posix()} sha256:{digest}; DO NOT EDIT -->"
|
||||
rendered[index] = (
|
||||
f"{marker}\n\n"
|
||||
f"# 생성된 rule index\n\n"
|
||||
f"Root SSOT: [`{source_rel.as_posix()}`](../../../../../{source_rel.as_posix()})\n\n"
|
||||
"아래 파일은 root SSOT의 heading 구간에서 생성된다. 직접 편집하지 않는다.\n\n"
|
||||
+ "\n".join(links)
|
||||
+ "\n"
|
||||
)
|
||||
return rendered
|
||||
|
||||
|
||||
def generate(root: Path, check: bool) -> tuple[list[str], list[str]]:
|
||||
expected = render(root)
|
||||
stale = [path.as_posix() for path, text in expected.items() if not (root / path).is_file() or (root / path).read_text(encoding="utf-8") != text]
|
||||
written: list[str] = []
|
||||
if not check and stale:
|
||||
changes = {root / path: expected[path].encode("utf-8") for path in expected if path.as_posix() in stale}
|
||||
replace_many(changes)
|
||||
written = sorted(stale)
|
||||
return sorted(stale), written
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("--check", action="store_true")
|
||||
mode.add_argument("--write", action="store_true")
|
||||
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
root = args.root.resolve(strict=True)
|
||||
stale, written = generate(root, args.check)
|
||||
result = {
|
||||
"schema_version": "rule-adapter-result/v1",
|
||||
"status": "DRIFT" if args.check and stale else "UPDATED" if written else "CURRENT",
|
||||
"stale": stale,
|
||||
"written": written,
|
||||
"target_count": len(render(root)),
|
||||
}
|
||||
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
|
||||
sys.stdout.write("\n")
|
||||
return 1 if args.check and stale else 0
|
||||
except (OSError, UnicodeError, json.JSONDecodeError, RuleGenerationError) as exc:
|
||||
json.dump({"schema_version": "rule-adapter-result/v1", "status": "FAIL", "error": str(exc)}, sys.stdout, ensure_ascii=False, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compatibility wrapper for the repository-wide neutral adapter generator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ADAPTER_DIR = Path(__file__).resolve().parent
|
||||
if str(ADAPTER_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ADAPTER_DIR))
|
||||
|
||||
from generate import ( # noqa: E402,F401
|
||||
DEFAULT_ROOT,
|
||||
MANIFEST_REL,
|
||||
MARKDOWN_MARKER,
|
||||
GenerationError,
|
||||
GenerationResult,
|
||||
load_catalog,
|
||||
main,
|
||||
render_target,
|
||||
)
|
||||
from generate import generate as _generate # noqa: E402
|
||||
|
||||
|
||||
# Kept for callers of the original one-workflow module.
|
||||
SOURCE_REL = Path("harness/source/workflows/branch-from-project.json")
|
||||
MARKER = MARKDOWN_MARKER
|
||||
|
||||
|
||||
def generate(root: Path, check: bool = False) -> list[str]:
|
||||
"""Return changed paths using the legacy list-shaped API."""
|
||||
|
||||
result = _generate(root, check=check)
|
||||
return list(result.drift if check else result.written)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,355 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"agents": {
|
||||
"branch-depth-auditor": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob",
|
||||
"model": "opus"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"coverage-auditor": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "sonnet"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"extraction-broker": {
|
||||
"claude": {
|
||||
"tools": "Read, Bash, Grep, Glob",
|
||||
"model": "haiku"
|
||||
}
|
||||
},
|
||||
"project-readiness-auditor": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob",
|
||||
"model": "opus"
|
||||
}
|
||||
},
|
||||
"wiki-adversarial-reviewer": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "opus"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-consistency-auditor": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "opus"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-semantic-coherence-auditor": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "opus"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-decision-researcher": {
|
||||
"claude": {
|
||||
"tools": "Read, Bash, Grep, Glob, WebSearch, WebFetch",
|
||||
"model": "sonnet"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command",
|
||||
"read_url_content",
|
||||
"search_web"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-diagram-reviewer": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "sonnet"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-doc-author": {
|
||||
"claude": {
|
||||
"tools": "Read, Edit, Write, Bash, Grep, Glob",
|
||||
"model": "sonnet"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "workspace-write"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"write_to_file",
|
||||
"replace_file_content",
|
||||
"multi_replace_file_content",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-link-verifier": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "haiku"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-research-lane": {
|
||||
"claude": {
|
||||
"tools": "Read, Grep, Glob, Bash",
|
||||
"model": "sonnet"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "read-only"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"run_command"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
},
|
||||
"wiki-source-summarizer": {
|
||||
"claude": {
|
||||
"tools": "Read, Edit, Write, Bash, Grep, Glob, WebFetch",
|
||||
"model": "sonnet"
|
||||
},
|
||||
"plugin": {},
|
||||
"codex": {
|
||||
"sandbox_mode": "workspace-write"
|
||||
},
|
||||
"antigravity": {
|
||||
"hidden": true,
|
||||
"tool_names": [
|
||||
"send_message",
|
||||
"view_file",
|
||||
"find_by_name",
|
||||
"grep_search",
|
||||
"list_dir",
|
||||
"write_to_file",
|
||||
"replace_file_content",
|
||||
"multi_replace_file_content",
|
||||
"run_command",
|
||||
"read_url_content"
|
||||
],
|
||||
"include_sections": [
|
||||
"user_information",
|
||||
"mcp_servers",
|
||||
"skills",
|
||||
"subagent_reminder",
|
||||
"messaging",
|
||||
"artifacts",
|
||||
"user_rules"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user