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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codex/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"hooks": {
"SessionStart": [
{
"matcher": "startup|resume|clear|compact",
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
Expand Down
11 changes: 8 additions & 3 deletions .evozeus-wrapper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ Wrapper harness migrations are recorded under `.evozeus-wrapper/docs/migrations/

### Skill changes

- None yet.
- Upgraded the embedded EvoZeus-wrapper harness from v0.9.1 to v0.10.0.
- Clarified that the project hook covers canonical-repository maintenance while Skill-entry preflight remains prompt-enforced.

### Feedback / Issues

- None yet.
- MetaInFLow/EvoZeus-wrapper#12: correct hook capability scope and add the global session dispatcher lifecycle.
- MetaInFLow/EvoZeus-wrapper#14: preserve target headings and accurate layout labels during harness refresh.

### Verification

- None yet.
- `python3 .evozeus-wrapper/scripts/evozeus_wrapper_preflight.py structure`
- `python3 .evozeus-wrapper/scripts/evozeus_wrapper_preflight.py doctor --repo MetaInFLow/metainflow-dev-tasks`
- Skill format validation and repository hook adapter smoke test.


## [v0.1.0] - 2026-07-20

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Upgrade EvoZeus-wrapper Harness to v0.10.0

## Related Issue

- MetaInFLow/EvoZeus-wrapper#12 defines the corrected hook capability scopes and global session dispatcher.
- MetaInFLow/EvoZeus-wrapper#14 tracks the migration regression found while applying this upgrade.

## Optimization Goal

Upgrade the target harness from v0.9.1 to v0.10.0 without changing the development-task management rules. The resulting contract must distinguish canonical-repository maintenance hooks, the global session dispatcher, and prompt-enforced Skill-entry preflight.

## Direction

- Refresh only EvoZeus-wrapper-managed files.
- Preserve the target Skill title and all business instruction sections.
- Record the actual `consolidated-v2 -> consolidated-v2` version refresh.
- Keep the Skill release version independent from the wrapper harness version.
- Do not broadly upgrade other registered Skills until wrapper issue #14 is resolved.

## Implementation Plan

1. Apply the official v0.10.0 harness migration after a conflict-free dry-run.
2. Restore target-owned heading content removed by the migrator.
3. Correct the generated layout label and retain the wrapper migration record.
4. Refresh the manifest, preflight checker, onboarding guide, project hook adapter, and Skill status prelude.
5. Install the user-level global dispatcher separately with backup and trust-pending state.

## Verification Plan

- Run target structure and source-contract preflight checks.
- Validate the Skill format.
- Execute the repository hook adapter with synthetic `SessionStart` input.
- Check the staged diff for whitespace errors and confirm business sections remain present.
- Require the GitHub pull-request gate to pass before merge.

## Release Plan

Merge the harness-only PR into `main` without changing Skill release v0.1.0. The wrapper migration remains independently versioned as v0.10.0. Roll back by reverting the migration commit and uninstalling the global dispatcher if required.
26 changes: 26 additions & 0 deletions .evozeus-wrapper/docs/migrations/2026-07-20-v0.9.1-to-v0.10.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# EvoZeus-wrapper Harness Refresh:v0.9.1 -> v0.10.0

- 日期:2026-07-20
- Wrapper 版本:v0.9.1 -> v0.10.0
- 新事实源:`.evozeus-wrapper/wrapper.json`
- 目标:修复 consolidated-v2 harness 并刷新 wrapper-managed contract。

## 移动记录

- 无文件移动;刷新 consolidated-v2 managed files。

## 保留的宿主接点

- `.codex/hooks.json`
- `.github/ISSUE_TEMPLATE/`
- `.github/pull_request_template.md`
- `.github/workflows/evozeus-wrapper-preflight.yml`

## 验证

- `python3 .evozeus-wrapper/scripts/evozeus_wrapper_preflight.py structure`
- `python3 .evozeus-wrapper/scripts/evozeus_wrapper_preflight.py runtime`

## 回滚

- 回滚包含本次迁移的 Git commit。
9 changes: 9 additions & 0 deletions .evozeus-wrapper/docs/onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@
1. 在 EvoZeus-wrapper repo 中运行 `onboarding.installation.command`,把 runtime install 建成指向 canonical repo 的 symlink。
2. 真实目录不会被删除。先用 `--dry-run` 查看计划;确认归档后显式增加 `--approve-archive`。原目录归档到 `~/.evozeus/archives/runtime-installs/`。
3. 运行 `onboarding.installation.verification`,确认 runtime pointer 和 canonical repo 一致。
4. runtime Skill 安装与全局 hook 安装是两个状态;软链接成功不代表 `global_session_dispatcher` 已安装或已信任。

## 全局 Dispatcher

- 在 EvoZeus-wrapper repo 先运行 `python3 scripts/evozeus_wrapper.py hook global plan --json`。
- 用户明确确认后运行 `hook global install --approve --json`,结构化合并 `~/.codex/hooks.json`。
- 安装后通过 `/hooks` 审核,再用 `hook global trust --status trusted --approve --json` 记录审核结果。
- global dispatcher 在 `SessionStart` 聚合检查全部 registered wrapped Skills,不精确绑定随后被选中的某个 Skill。

## 调用

- 调用触发词和业务入口归目标 Skill 的 canonical `SKILL.md` 所有,wrapper 不猜测业务调用方式。
- 按 `onboarding.invocation.instruction` 在新的消费项目会话中调用 Skill。
- 运行 `onboarding.invocation.verification`,确认宿主加载的是 canonical repo,并通过 consumer-project smoke test。
- Skill 被选中后,instruction surface 的 `skill_entry_preflight` 在业务主链路前检查当前 Skill;该步骤不是 native hook。

## 初始化

Expand Down
51 changes: 49 additions & 2 deletions .evozeus-wrapper/hooks/evozeus_wrapper_start_check.py
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import re
import sys
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
Expand All @@ -16,6 +17,8 @@
LATEST_VERSION_ENV = "EVOZEUS_WRAPPER_LATEST_VERSION"
ENFORCEMENT_ENV = "EVOZEUS_WRAPPER_HOOK_ENFORCEMENT"
LATEST_RELEASE_URL = "https://api.github.com/repos/MetaInFLow/EvoZeus-wrapper/releases/latest"
GLOBAL_CACHE_PATH = ".evozeus/cache/evozeus-wrapper-latest.json"
GLOBAL_CACHE_TTL_SECONDS = 3600


def repo_root() -> Path:
Expand All @@ -32,6 +35,24 @@ def read_json(path: Path) -> dict[str, Any] | None:
return data if isinstance(data, dict) else None


def write_latest_cache(home: Path, version: str, checked_at_epoch: int) -> None:
path = home / GLOBAL_CACHE_PATH
try:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(f".{path.name}.tmp")
temporary.write_text(
json.dumps(
{"version": version, "checked_at_epoch": checked_at_epoch},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
temporary.replace(path)
except OSError:
pass


def version_key(tag: str) -> tuple[int, int, int] | None:
match = re.fullmatch(r"v(\d+)\.(\d+)\.(\d+)", tag)
if not match:
Expand Down Expand Up @@ -64,6 +85,8 @@ def resolve_latest_version(
current: str,
environment: dict[str, str] | None = None,
fetcher=None,
home: Path | None = None,
now_epoch: int | None = None,
) -> dict[str, str | None]:
environment = os.environ if environment is None else environment
checked_at = datetime.now(timezone.utc).isoformat()
Expand All @@ -76,8 +99,27 @@ def resolve_latest_version(
"url": None,
"error": None,
}
home = Path.home() if home is None else home.expanduser().resolve()
now_epoch = int(time.time()) if now_epoch is None else now_epoch
cache = read_json(home / GLOBAL_CACHE_PATH) or {}
cached_version = cache.get("version")
cached_at = cache.get("checked_at_epoch")
if (
isinstance(cached_version, str)
and version_key(cached_version)
and isinstance(cached_at, int)
and max(0, now_epoch - cached_at) <= GLOBAL_CACHE_TTL_SECONDS

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject cache timestamps that are in the future

When the system clock moves backward or the shared dispatcher cache contains a future checked_at_epoch, max(0, now_epoch - cached_at) treats the entry as zero seconds old. A sufficiently future timestamp can therefore keep a stale wrapper version valid far beyond the one-hour TTL, preventing this startup check from discovering newer releases. Require the computed age to be nonnegative as well as within the TTL.

Useful? React with 👍 / 👎.

):
return {
"version": cached_version,
"source": "global_dispatcher_cache",
"checked_at": checked_at,
"url": None,
"error": None,
}
release = (fetcher or fetch_latest_release)()
if release.get("version"):
write_latest_cache(home, release["version"], now_epoch)
return {
"version": release["version"],
"source": "github_latest_release",
Expand Down Expand Up @@ -107,7 +149,10 @@ def block(reason: str, next_action: str) -> int:
"systemMessage": reason,
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": f"EvoZeus-wrapper blocked start. next_action={next_action}",
"additionalContext": (
"EvoZeus-wrapper capability=repo_maintenance_hook; "
f"scope=canonical_repository; blocked start; next_action={next_action}"
),
},
}
)
Expand All @@ -121,7 +166,9 @@ def allow(level: str, message: str, next_action: str) -> int:
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": (
f"EvoZeus-wrapper hook_start_check level={level}; next_action={next_action}; {message}"
"EvoZeus-wrapper capability=repo_maintenance_hook; "
f"scope=canonical_repository; hook_start_check level={level}; "
f"next_action={next_action}; {message}"
),
},
}
Expand Down
95 changes: 83 additions & 12 deletions .evozeus-wrapper/scripts/evozeus_wrapper_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,25 +267,80 @@ def detected_plugin_manifests(target: Path) -> list[str]:

def check_integration_contract(target: Path, manifest: dict | None) -> None:
integration = (manifest or {}).get("integration") or {}
registration = ((manifest or {}).get("hook_registration") or {}).get("codex") or {}
mode = integration.get("mode")
capabilities = integration.get("capabilities") or {}
repo_hook = capabilities.get("repo_maintenance_hook") or {}
global_dispatcher = capabilities.get("global_session_dispatcher") or {}
skill_entry = capabilities.get("skill_entry_preflight") or {}
invocation_hook = capabilities.get("skill_invocation_hook") or {}

if repo_hook.get("covers_skill_invocation"):
fail("repo_maintenance_hook cannot claim Skill-invocation coverage")
if global_dispatcher.get("covers_skill_invocation"):
fail("global_session_dispatcher cannot claim per-Skill invocation coverage")
if global_dispatcher.get("installed") or global_dispatcher.get("native_enforced"):
fail("portable manifest cannot persist user-level global dispatcher installation state")
if global_dispatcher.get("trust_status") not in {None, "not_installed"}:
fail("portable manifest cannot persist user-level global dispatcher trust state")
if skill_entry.get("native_enforced"):
fail("skill_entry_preflight is prompt-compliance based, not native enforcement")
if skill_entry.get("installed"):
surface_rel = (manifest or {}).get("instruction_surface") or integration.get("root_entry")
if not isinstance(surface_rel, str) or not surface_rel:
fail("skill_entry_preflight installation requires an instruction_surface")
relative_surface = Path(surface_rel)
if relative_surface.is_absolute() or ".." in relative_surface.parts:
fail("skill_entry_preflight instruction_surface must stay inside target")
surface = target / relative_surface
cursor = surface
while cursor != target:
if cursor.is_symlink():
fail("skill_entry_preflight instruction_surface cannot use symlink components")
cursor = cursor.parent
try:
surface.resolve(strict=True).relative_to(target.resolve())
except (OSError, ValueError):
fail("skill_entry_preflight instruction_surface is missing or outside target")
if not surface.is_file():
fail("skill_entry_preflight instruction_surface is missing")
content = content_after_frontmatter(surface.read_text(encoding="utf-8")).lstrip()
lines = content.splitlines()
if content.startswith("## EvoZeus-wrapper 状态检查"):
has_status_prelude = True
else:
has_status_prelude = bool(
lines
and lines[0].startswith("# ")
and "\n".join(lines[1:]).lstrip().startswith(
"## EvoZeus-wrapper 状态检查"
)
)
if not has_status_prelude:
fail("skill_entry_preflight installation requires the status prelude")
if registration.get("capability") == "repo_maintenance_hook":
if registration.get("scope") != "canonical_repository":
fail("repo_maintenance_hook registration scope must be canonical_repository")
if registration.get("covers_skill_invocation"):
fail("repo_maintenance_hook registration cannot claim Skill-invocation coverage")

if not mode:
warn("wrapper manifest has no integration.mode; treating runtime checks as prompt/manual fallback")
return

if mode == "native_host_hook":
hooks = detected_hook_files(target)
plugins = detected_plugin_manifests(target)
codex_project_hook = CODEX_HOOKS_CONFIG in hooks and CODEX_START_HOOK_SCRIPT in hooks
plugin_lifecycle_hook = bool(hooks and plugins)
if not codex_project_hook and not plugin_lifecycle_hook:
fail(
"integration.mode is native_host_hook but host hook evidence is missing: "
f"hooks={hooks or 'none'}, plugin_manifests={plugins or 'none'}"
)
ok("integration contract has host hook evidence")
if not integration.get("native_skill_invocation_hook_installed"):
fail("native Skill-invocation coverage requires explicit invocation-hook evidence")
if not invocation_hook.get("supported") or not invocation_hook.get("installed"):
fail("native Skill-invocation coverage requires a supported installed SkillInvoke hook")
ok("integration contract has native Skill-invocation hook evidence")
return

if mode in {"bootstrap_skill", "prompt_runtime_check", "manual_only"}:
if integration.get("native_skill_invocation_hook_installed"):
fail(f"integration.mode={mode} conflicts with native Skill-invocation hook installation")
if integration.get("native_host_hook_installed"):
fail("deprecated native_host_hook_installed cannot overstate Skill-invocation coverage")
ok(f"integration contract declares non-native mode: {mode}")
return

Expand Down Expand Up @@ -479,8 +534,24 @@ def check_status_prelude(skill_text: str, label: str = "SKILL.md") -> None:
def root_entry_path(target: Path) -> Path:
manifest = load_wrapper_manifest(target)
if manifest and manifest.get("instruction_surface"):
manifest_surface = target / manifest["instruction_surface"]
if manifest_surface.exists():
raw_surface = manifest["instruction_surface"]
relative = Path(raw_surface) if isinstance(raw_surface, str) else Path("")
if relative.is_absolute() or ".." in relative.parts:
fail(f"manifest instruction_surface must stay inside target: {raw_surface}")
manifest_surface = target / relative
cursor = manifest_surface
while cursor != target:
if cursor.is_symlink():
fail(
"manifest instruction_surface cannot use symlink components: "
f"{raw_surface}"
)
cursor = cursor.parent
try:
manifest_surface.resolve(strict=True).relative_to(target.resolve())
except (OSError, ValueError):
fail(f"manifest instruction_surface is missing or outside target: {raw_surface}")
if manifest_surface.is_file():
return manifest_surface
fail(f"manifest instruction_surface is missing: {manifest['instruction_surface']}")
skill = target / "SKILL.md"
Expand Down
Loading
Loading