From f9a8011622e97c6ddff963f91219feed2f45c7a9 Mon Sep 17 00:00:00 2001 From: KIM Date: Mon, 19 Oct 2026 01:26:22 -0600 Subject: [PATCH 1/2] feat: exact-engine offline user kits with recoverable installation --- .github/workflows/verification.yml | 7 + README.md | 2 + README_CN.md | 2 + docs/ACCEPTANCE_MATRIX.md | 2 +- docs/INDEPENDENT_USER_KIT_CN.md | 40 +++++ docs/PRODUCT_ACCEPTANCE_CN.md | 2 + docs/PRODUCT_PLAN.md | 2 +- docs/SUPPORT_AND_UPGRADES.md | 5 +- pyproject.toml | 2 +- src/mimo_harness/__init__.py | 2 +- src/mimo_harness/cli.py | 5 + src/mimo_harness/manifests.py | 8 +- src/mimo_harness/product_acceptance.py | 2 +- src/mimo_harness/study_bootstrap.py | 194 +++++++++++++++++++++++++ src/mimo_harness/user_kit.py | 132 +++++++++++++++++ tests/ci_user_kit.py | 68 +++++++++ tests/test_user_kit.py | 108 ++++++++++++++ 17 files changed, 574 insertions(+), 9 deletions(-) create mode 100644 docs/INDEPENDENT_USER_KIT_CN.md create mode 100644 src/mimo_harness/study_bootstrap.py create mode 100644 src/mimo_harness/user_kit.py create mode 100644 tests/ci_user_kit.py create mode 100644 tests/test_user_kit.py diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml index fa5831a..c9133ab 100644 --- a/.github/workflows/verification.yml +++ b/.github/workflows/verification.yml @@ -216,6 +216,11 @@ jobs: mimo --config examples/cli_agent/mimo-harness.json report --run examples/cli_agent/.mimo/runs/recovered-repair mimo export --run examples/cli_agent/.mimo/runs/recovered-repair --output cli-repair-evidence.zip mimo gate --bundle cli-repair-evidence.zip + - name: Prepare portable user-kit dependencies and rehearse clean installation + run: | + python -m pip download --only-binary=:all: --platform any --implementation py --abi none --dest .mimo/kit-wheels pip==25.0.1 pytest==8.4.2 iniconfig==2.1.0 packaging==25.0 pluggy==1.6.0 pygments==2.19.2 exceptiongroup==1.3.0 tomli==2.2.1 colorama==0.4.6 typing_extensions==4.15.0 + python -c "import pathlib,shutil; shutil.copy2(next(pathlib.Path('.mimo/dist').glob('*.whl')), '.mimo/kit-wheels')" + python tests/ci_user_kit.py --wheelhouse .mimo/kit-wheels --output .mimo/user-kit-rehearsal - uses: actions/upload-artifact@v4 if: always() with: @@ -223,6 +228,8 @@ jobs: path: | cli-pytest-results.xml .mimo/dist/* + .mimo/user-kit-rehearsal/kit/ + .mimo/user-kit-rehearsal/rehearsal.json include-hidden-files: true unit-and-smoke: diff --git a/README.md b/README.md index 7cf3041..ec3b5dc 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,8 @@ mimo readiness `readiness` returns `3` while external acceptance is pending. Use `trial-init`/`trial-run` for an explicitly budgeted live model trial and `walkthrough-init` for each independent new user. Blank templates and deterministic controls do not count as those observations. See the [acceptance guide](docs/PRODUCT_ACCEPTANCE_CN.md) and [support/upgrade boundaries](docs/SUPPORT_AND_UPGRADES.md). +v0.12 adds [`walkthrough-pack`](docs/INDEPENDENT_USER_KIT_CN.md): distribute one exact wheel, offline Python dependencies and three blank user-study slots. A cross-platform bootstrap creates a private environment, verifies the installed engine and preserves feedback when resuming interrupted setup. Actual user observations remain separate. + ### Continue an interrupted run ```bash diff --git a/README_CN.md b/README_CN.md index c27316e..bd94990 100644 --- a/README_CN.md +++ b/README_CN.md @@ -215,6 +215,8 @@ v0.11 已增加原生 Node.js 与通用 JUnit 的逐用例验证,可通过 `ms 真实模型试跑、新用户反馈和最终验收分别使用 `trial-init`/`trial-run`、`walkthrough-init` 和 `readiness`。执行 `mimo readiness` 可查看缺项;返回 `3` 表示尚未齐备,不会把测试模板当作真实试用。详见[中文产品验收指南](docs/PRODUCT_ACCEPTANCE_CN.md)和[支持范围与升级说明](docs/SUPPORT_AND_UPGRADES.md)。 +v0.12 新增 [`walkthrough-pack` 独立用户试用包](docs/INDEPENDENT_USER_KIT_CN.md):三位使用者共用同一份 wheel 和离线 Python 依赖,用一条命令创建独立环境;安装中断后可以恢复,已填写的反馈会保留。自动安装不算真人验收。 + ### 6.3 初始化并检查项目 先使用明确的离线模式。下面命令会创建 `mimo-harness.json` 和 `.mimo/models.json`,不会调用模型 API。 diff --git a/docs/ACCEPTANCE_MATRIX.md b/docs/ACCEPTANCE_MATRIX.md index 720e742..54b1fe5 100644 --- a/docs/ACCEPTANCE_MATRIX.md +++ b/docs/ACCEPTANCE_MATRIX.md @@ -59,7 +59,7 @@ The two actual-Codex tests require `MIMO_CODEX_BINARY` and the exact pinned exec - Signed/remote attestation, richer evidence migration and organization-level enforced workflow policies are outside the current unsigned bundle format. The opt-in PR template still requires maintainer-configured required checks/review protection. - Generic CLI Agent repair, the pinned Codex event adapter and two real Python defect controls have automated gates. Live paid-provider/model repair validation still requires credentials and an explicit spending allowance. Opaque external Agent tokens/costs remain unavailable; invocation/time caps are not billing caps. - Broader language/framework and defect diversity beyond the documented native Node/JUnit subset; Keil/firmware/hardware behavior remain separately scoped. -- Maintainer-selected license and at least three independent new-user walkthroughs. Wheel/sdist checks, upgrade/support notes, trial/feedback generation and source-bound CI collection are implemented; generated templates are not completed external evidence. +- Maintainer-selected license and at least three independent new-user walkthroughs. Wheel/sdist checks, upgrade/support notes, exact-wheel user kits, offline install/setup recovery, trial/feedback generation and source-bound CI collection are implemented; generated templates and automated installer rehearsals are not completed external evidence. - One final clean checkout/install run across the supported matrix, all failure-injection scenarios, evidence audit and scope review after the last functional change. Passing an intermediate batch is not the final completeness sign-off. Unavailable environments, skipped tests and external/manual gates must remain visible in the release record. diff --git a/docs/INDEPENDENT_USER_KIT_CN.md b/docs/INDEPENDENT_USER_KIT_CN.md new file mode 100644 index 0000000..ea09ad4 --- /dev/null +++ b/docs/INDEPENDENT_USER_KIT_CN.md @@ -0,0 +1,40 @@ +# 把同一份可验证安装包交给三位新用户 + +v0.12 beta 提供 `walkthrough-pack`,把当前引擎对应的 wheel、固定依赖、跨平台安装脚本、中文指南和三份空白反馈放在同一个目录。三位使用者安装完全相同的引擎文件,避免各自重建 wheel 带来身份差异。生成材料或自动演练不计为真人试用。 + +## 维护者准备 + +在准备交付的干净 checkout 中构建并安装 wheel: + +```bash +python -m pip install build==1.3.0 +python -m build --outdir .mimo/dist +python -m pip install --force-reinstall --no-deps .mimo/dist/mimo_codeharness-0.12.0-py3-none-any.whl +python -m pip download --only-binary=:all: --platform any --implementation py --abi none --dest .mimo/kit-wheels pip==25.0.1 pytest==8.4.2 iniconfig==2.1.0 packaging==25.0 pluggy==1.6.0 pygments==2.19.2 exceptiongroup==1.3.0 tomli==2.2.1 colorama==0.4.6 typing_extensions==4.15.0 +python -c "import shutil; shutil.copy2('.mimo/dist/mimo_codeharness-0.12.0-py3-none-any.whl', '.mimo/kit-wheels')" +mimo walkthrough-pack --wheelhouse .mimo/kit-wheels --output .mimo/user-kit +``` + +准备依赖需要联网;输出目录必须是新目录。wheel 必须与当前 CLI 的版本和引擎字节一致;缺依赖、重复版本、平台专用 wheel 和不同引擎都会被拒绝。安装时使用这些确切 wheel,不从在线索引解析新依赖。 + +CI 的 Linux/Windows `cli-install` 任务从真实构建产物生成试用包,并演练新环境安装、拒绝覆盖、半成品虚拟环境恢复、保留反馈、真实 boltons 对照验收、手动恢复和迁移后的 ZIP 门禁。对应 `cli-pytest-*` artifact 内包含 `.mimo/user-kit-rehearsal/kit/` 和 `rehearsal.json`。选择一份可信 CI 产出的包,原样分发给三位使用者;不要混合不同任务中重建的 wheel。 + +## 新用户操作 + +使用者只需预装 Python 3.10+ 和 Git,在试用包目录运行: + +```text +python bootstrap.py --participant user-01 --output my-study +``` + +另外两人选择 user-02、user-03。这些名称只是材料分配槽位,真实匿名 tester_id 由使用者自己填写。 + +安装脚本先校验传输文件,在新目录创建 `.venv`,仅离线安装包内依赖并核对实际安装的引擎;不修改系统 Python,不需要激活环境。随后进入 my-study,按照 GUIDE_CN.md 使用 `python mimo.py ...` 完成流程。Git 下载固定上游源码需要访问 GitHub,离线安装不等于整个真实案例都不需要网络。 + +安装失败或中断后,使用相同参数加 `--resume`。每次尝试保留独立安装日志,技术安装状态写入 setup.json。已有 feedback.json 和报告不会被自动填写或覆盖;无法恢复时保留原目录并使用新输出目录。 + +试用者提交自己填写的 feedback.json、自己运行生成的 evidence.zip 及必要日志。反馈仍要经过 [readiness](PRODUCT_ACCEPTANCE_CN.md) 检查和人工独立性复核。包中哈希和 CI JSON 均未签名,用于检查版本/传输一致性;请从可信维护者和 CI 取得材料。 + +汇总验收的维护者也应安装这份包中的同一 wheel,再运行 readiness。仅版本号相同、分别重建的代码文件未必具有相同的引擎身份。 + +本流程不调用模型、不设置模型额度,也不替维护者选项目许可证。[真实模型和最终发布验收](PRODUCT_ACCEPTANCE_CN.md) 仍单独进行。 diff --git a/docs/PRODUCT_ACCEPTANCE_CN.md b/docs/PRODUCT_ACCEPTANCE_CN.md index 40dfe89..a0d5fad 100644 --- a/docs/PRODUCT_ACCEPTANCE_CN.md +++ b/docs/PRODUCT_ACCEPTANCE_CN.md @@ -33,6 +33,8 @@ mimo trial-run --plan .mimo/live-trial-plan.json --output .mimo/live-trial-001 ## 2. 至少三位独立新用户试用 +推荐先按[独立用户试用包指南](INDEPENDENT_USER_KIT_CN.md)准备同一份 wheel 和离线依赖,让三位使用者获得一致的引擎。下面的 `walkthrough-init` 仍可用于已经安装好相同版本的环境。 + 为每位未参与实现的使用者生成一份独立材料: ```bash diff --git a/docs/PRODUCT_PLAN.md b/docs/PRODUCT_PLAN.md index 7701ff5..26b3838 100644 --- a/docs/PRODUCT_PLAN.md +++ b/docs/PRODUCT_PLAN.md @@ -16,7 +16,7 @@ Priorities are ordered by user dependency, with a concrete exit condition: | P2b · adapter implemented; live trial pending | Ready-to-use vendor adapter, then live model validation | Pinned Codex CLI adapter and config generator; actual-binary/local-server CI. A real model still requires available credentials, an explicit spending allowance and a separately recorded outcome | | P3a · implemented | Two independent real Python defect cases | boltons and more-itertools pinned source/fix commits; reproduced target assertions, reference acceptance, regression rejection, fresh recovery and portable evidence via `case-init`/`case-check` | | P3b · implemented | Native Node and generic JUnit case checks | Actual Node 22/24, typed JUnit contract, ms/boltons reference and regression controls, fixed acceptance, fresh recovery and exact portable proof; no zero-exit-only PASS | -| P4 · workflow prepared; real users pending | New-user workflow and actionable diagnosis | Chinese guide and blank feedback generation, duplicate/stale/unfinished evidence checks. At least three actual independent new users still need to complete the workflow and resolve observed issues | +| P4 · workflow prepared; real users pending | New-user workflow and actionable diagnosis | Exact-engine user kit, offline dependency installation, interrupted setup recovery, Chinese guides and three blank feedback slots; duplicate/stale/unfinished evidence checks. At least three actual independent new users still need to complete the workflow and resolve observed issues | | P5 · automation/docs prepared; final sign-off pending | Release and final completeness sign-off | Wheel/sdist, support/upgrade notes, exact-source CI record and readiness gate. Maintainer license choice, live trial, independent users and final manual review remain external gates | P2–P4 may be scoped into smaller batches, but a capability is not advertised as certified before its exit condition is met. Existing `mimo verify` already accepts patches produced by external tools without needing a tool-specific Agent integration. diff --git a/docs/SUPPORT_AND_UPGRADES.md b/docs/SUPPORT_AND_UPGRADES.md index cb2444c..ff9caaf 100644 --- a/docs/SUPPORT_AND_UPGRADES.md +++ b/docs/SUPPORT_AND_UPGRADES.md @@ -1,6 +1,6 @@ # Supported scope and upgrades -v0.11 remains a beta. Automated release evidence and a stable release sign-off are separate. The package has no mandatory third-party Python runtime dependencies; configured projects/runners need their own explicitly prepared dependencies. +v0.12 remains a beta. Automated release evidence and a stable release sign-off are separate. The package has no mandatory third-party Python runtime dependencies; configured projects/runners need their own explicitly prepared dependencies. | Component | Automated coverage | Boundary | |---|---|---| @@ -12,6 +12,7 @@ v0.11 remains a beta. Automated release evidence and a stable release sign-off a | Linux Docker | Actual non-root isolated baseline/candidate/Agent tests; Node/JUnit included | Prebuilt image, exact resolved image ID, explicit network/resources; no local fallback | | Codex | CLI 0.163.0 native-binary protocol on Linux/Windows | Explicit model/key; live model/tool quality and billing require observed trials | | Portable evidence | Relocation, strict gate, HTML/JSON/JUnit/log linkage; exact Node/JUnit identity | Unsigned integrity evidence, not hostile-party authentication | +| Independent user kit | One exact wheel, portable offline Python dependencies; actual Python 3.12 installation/recovery on Linux/Windows | Python/Git must already be installed; fetching pinned upstream cases needs GitHub; automated rehearsal is not a human trial | | Embedded/other languages | Existing scans and explicit commands | No claim of full Keil firmware build, hardware validity or certified Go/Rust/Jest/Vitest acceptance | ## Install and upgrade @@ -22,7 +23,7 @@ Keep the previous checkout/tag and all run directories before upgrading. Do not Recovery requires the exact source/test/configuration, task, engine, executable/environment and Docker identity that produced the original run. A version upgrade, changed reporter or changed Node binary can invalidate recovery. Restore the matching old environment to resume an old run, or start a new run; do not edit fingerprints/accounting to force reuse. Previously completed portable bundles remain available for inspection, subject to their evidence format and verifier compatibility. -v0.11 does not migrate cached tests or silently reuse old PASS. Resume recreates baseline/candidate checks; completed proposals are reused only when inputs match. Native Node executable bytes are included in local recovery identity; Docker runtime bytes are bound through image identity. +The current version does not migrate cached tests or silently reuse old PASS. Resume recreates baseline/candidate checks; completed proposals are reused only when inputs match. Native Node executable bytes are included in local recovery identity; Docker runtime bytes are bound through image identity. ## Final release record diff --git a/pyproject.toml b/pyproject.toml index d881bd4..3bd2869 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mimo-codeharness" -version = "0.11.0" +version = "0.12.0" description = "Evidence-backed repository and code-agent patch verification harness" readme = "README.md" requires-python = ">=3.10" diff --git a/src/mimo_harness/__init__.py b/src/mimo_harness/__init__.py index 05bc001..3897e26 100644 --- a/src/mimo_harness/__init__.py +++ b/src/mimo_harness/__init__.py @@ -1,4 +1,4 @@ """Installable command-line interface for MiMo-CodeHarness.""" -__version__ = "0.11.0" +__version__ = "0.12.0" diff --git a/src/mimo_harness/cli.py b/src/mimo_harness/cli.py index a43ac26..c72b0c7 100644 --- a/src/mimo_harness/cli.py +++ b/src/mimo_harness/cli.py @@ -1017,6 +1017,11 @@ def build_parser() -> argparse.ArgumentParser: walkthrough = sub.add_parser("walkthrough-init", help="Create an independent-user guide and blank feedback record.") walkthrough.add_argument("--output", required=True) walkthrough.set_defaults(handler=command_walkthrough_init) + from mimo_harness.user_kit import command_walkthrough_pack + pack = sub.add_parser("walkthrough-pack", help="Package one exact wheel and offline dependencies for three independent user walkthroughs.") + pack.add_argument("--wheelhouse", required=True, help="Directory containing the matching harness wheel and all portable pytest dependencies") + pack.add_argument("--output", required=True, help="New kit directory; existing files are preserved") + pack.set_defaults(handler=command_walkthrough_pack) readiness = sub.add_parser("readiness", help="Check source-bound product evidence; missing external gates remain pending.") readiness.add_argument("--repository", default=".") readiness.add_argument("--trial", help="Completed trial.json") diff --git a/src/mimo_harness/manifests.py b/src/mimo_harness/manifests.py index 28defa2..90f0044 100644 --- a/src/mimo_harness/manifests.py +++ b/src/mimo_harness/manifests.py @@ -182,7 +182,7 @@ def environment_identity(execution: dict, checks, salt: str, runtime_home: Path) "environment_sha256": digest([salt, env])} -def engine_identity() -> str: +def engine_inventory() -> dict: # A wheel lives in site-packages beside unrelated packages; a checkout may # contain legacy scripts that are not distributed. Hash the same executable # components in both layouts, using module names instead of install paths. @@ -191,7 +191,11 @@ def engine_identity() -> str: for name in ("run_mimo_codeharness_v02", "scoring_v03", "verification_v04"): spec = importlib.util.find_spec(name) code[name + ".py"] = file_digest(Path(spec.origin)) if spec and spec.origin else None - return digest(code) + return code + + +def engine_identity() -> str: + return digest(engine_inventory()) class RecoverySource: diff --git a/src/mimo_harness/product_acceptance.py b/src/mimo_harness/product_acceptance.py index de16306..0e20b6d 100644 --- a/src/mimo_harness/product_acceptance.py +++ b/src/mimo_harness/product_acceptance.py @@ -133,7 +133,7 @@ def command_walkthrough_init(args): 3. 执行 `mimo --config .mimo/user-case/mimo-harness.json doctor`。 4. 执行 `mimo case-check --directory .mimo/user-case --name first`。阅读参考 PASS、故意回归 FAIL 和恢复重跑的证据;整个对照验收应成功。 5. 再用相同名称运行一次,确认拒绝覆盖;记录提示是否足以解决问题。换一个新名称可重新验收。 -6. 将 `results/first/reference.zip` 复制到其他目录,执行 `mimo gate --bundle ZIP路径 --json`;解压后打开 report.html。 +6. 将 `.mimo/user-case/results/first/reference.zip` 复制到其他目录,执行 `mimo gate --bundle ZIP路径 --json`;解压后打开 report.html。 7. 填写匿名 tester_id、观察时间、环境、各步骤是否完成、遇到的问题。只在实际独立试用后设置 independence_confirmed=true。issues 中每个问题记录 description 和 resolved。 8. evidence_bundle 填写你自己运行产生的参考 ZIP 路径(相对 feedback.json 所在目录,或绝对路径)。不同试用者需要各自的运行证据。 diff --git a/src/mimo_harness/study_bootstrap.py b/src/mimo_harness/study_bootstrap.py new file mode 100644 index 0000000..e60eeb9 --- /dev/null +++ b/src/mimo_harness/study_bootstrap.py @@ -0,0 +1,194 @@ +"""Standalone offline installer copied into a user kit; no harness import required.""" +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path, PureWindowsPath + + +LAUNCHER = '''from pathlib import Path +import subprocess, sys +root = Path(__file__).resolve().parent +python = root / ".venv" / ("Scripts/python.exe" if sys.platform == "win32" else "bin/python") +raise SystemExit(subprocess.call([str(python), "-X", "utf8", "-I", "-m", "mimo_harness", *sys.argv[1:]], cwd=root)) +''' + + +def sha(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def read(path): + value = json.loads(path.read_text(encoding='utf-8')) + if not isinstance(value, dict): + raise ValueError("Expected an object: " + str(path)) + return value + + +def write(path, value): + fd, temp = tempfile.mkstemp(prefix='.setup-', dir=path.parent) + try: + with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as f: + json.dump(value, f, ensure_ascii=False, indent=2); f.write('\n') + os.replace(temp, path) + finally: + Path(temp).unlink(missing_ok=True) + + +def regular(root, name): + if (not isinstance(name, str) or not name or '\\' in name or Path(name).is_absolute() + or PureWindowsPath(name).drive or any(p in {'', '.', '..'} for p in name.split('/'))): + raise ValueError("Unsafe kit path.") + path = root / name + if any(p.is_symlink() for p in (path, *path.parents) if p != root.parent) or not path.is_file(): + raise ValueError("Missing or linked kit file: " + name) + path.resolve().relative_to(root.resolve()) + return path + + +def validate_kit(root): + manifest = regular(root, 'kit.json') + kit = read(manifest) + if (type(kit.get('schema_version')) is not int or kit['schema_version'] != 1 + or kit.get('kind') != 'independent_user_kit' or not isinstance(kit.get('files'), dict) + or kit.get('participants') != ['user-01', 'user-02', 'user-03'] + or not isinstance(kit.get('wheels'), list) or not kit['wheels'] + or not isinstance(kit.get('version'), str) or not isinstance(kit.get('engine_sha256'), str)): + raise ValueError("Invalid kit manifest.") + files = kit['files'] + required = {'bootstrap.py', 'GUIDE_CN.md'} | {'participants/' + p + '/feedback.json' for p in kit['participants']} + if not required.issubset(files): + raise ValueError("Incomplete kit inventory.") + for name, expected in files.items(): + if sha(regular(root, name)) != expected: + raise ValueError("Kit file changed: " + name) + wheels, names = [], set() + for row in kit['wheels']: + name = row['file'] + if name != Path(name).name or name in names or not name.endswith('.whl'): + raise ValueError("Invalid wheel entry.") + names.add(name) + path = regular(root, 'wheels/' + name) + if files.get('wheels/' + name) != row['sha256'] or sha(path) != row['sha256']: + raise ValueError("Wheel inventory changed.") + wheels.append(path) + if sum(row.get('package') == 'pip' for row in kit['wheels']) != 1: + raise ValueError("The kit must contain one reviewed offline pip installer wheel.") + return kit, sha(manifest), wheels + + +def child_environment(): + keys = {'PATH', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'PATHEXT', 'TEMP', 'TMP', 'TMPDIR', + 'HOME', 'USERPROFILE', 'APPDATA', 'LOCALAPPDATA', 'LANG', 'LC_ALL', 'LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH'} + return {k: v for k, v in os.environ.items() if k.upper() in keys} + + +def install(kit_root, output, participant, resume=False): + kit, kit_sha, wheels = validate_kit(kit_root) + if participant not in kit['participants']: + raise ValueError("Choose an assigned slot: user-01, user-02 or user-03.") + if sys.version_info < (3, 10) or not shutil.which('git'): + raise ValueError("Install Python 3.10+ and Git first; no model key is required.") + if output.is_symlink(): + raise ValueError("Study output cannot be a symlink.") + output = output.expanduser().resolve() + state_path = output / 'setup.json' + identity = {'schema_version': 1, 'kind': 'technical_installation', 'kit_sha256': kit_sha, + 'participant_slot': participant, 'engine_sha256': kit['engine_sha256'], + 'python': str(Path(sys.executable).resolve()), 'python_version': list(sys.version_info[:2])} + if output.exists(): + if not resume: + raise ValueError("Study directory already exists; preserve it and add --resume, or choose a new --output.") + state = read(regular(output, 'setup.json')) + if any(state.get(k) != v for k, v in identity.items()): + raise ValueError("Resume kit, slot or Python changed; preserve this study and choose a new --output.") + else: + if resume: + raise ValueError("No existing setup to resume; omit --resume.") + output.mkdir(parents=True, exist_ok=False) + state = {**identity, 'attempts': 0} + if type(state.get('attempts')) is not int or not 0 <= state['attempts'] < 100: + raise ValueError("Invalid setup attempt history.") + state.update(attempts=state['attempts'] + 1, status='INSTALLING', model_called=False, + note='Technical setup only. Human feedback remains independent and is never completed by this script.') + write(state_path, state) + log_path = output / f"install-{state['attempts']:03d}.log" + env_root = output / '.venv' + try: + if env_root.is_symlink(): + raise ValueError("Study environment cannot be a symlink.") + env = child_environment() + with log_path.open('x', encoding='utf-8', newline='\n') as log: + # Re-running venv without --clear repairs an interrupted creation. + # Only this setup's owned environment is used; study files survive. + subprocess.run([sys.executable, '-X', 'utf8', '-I', '-m', 'venv', '--without-pip', str(env_root)], env=env, + stdout=log, stderr=subprocess.STDOUT, check=True, timeout=180) + python = env_root / ('Scripts/python.exe' if sys.platform == 'win32' else 'bin/python') + pip_wheel = next(path for path, row in zip(wheels, kit['wheels']) if row['package'] == 'pip') + # A half-installed pip can retain metadata while lacking __main__. + # Load the verified wheel directly, never the damaged environment's + # pip, and reinstall exact files even when metadata claims success. + loader = "import runpy,sys;sys.path.insert(0,sys.argv.pop(1));runpy.run_module('pip',run_name='__main__',alter_sys=True)" + installer = [str(python), '-X', 'utf8', '-I', '-c', loader, str(pip_wheel), '--isolated'] + subprocess.run([*installer, 'install', '--no-index', '--no-deps', '--force-reinstall', + *map(str, wheels)], env=env, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=180) + subprocess.run([*installer, 'check'], env=env, + stdout=log, stderr=subprocess.STDOUT, check=True, timeout=30) + code = "import json;from mimo_harness import __version__;from mimo_harness.manifests import engine_identity;print(json.dumps([__version__,engine_identity()]))" + actual = subprocess.check_output([str(python), '-X', 'utf8', '-I', '-c', code], env=env, text=True, encoding='utf-8', timeout=30) + if json.loads(actual) != [kit['version'], kit['engine_sha256']]: + raise ValueError("Installed version/engine differs from the kit.") + for name, content in [('mimo.py', LAUNCHER)]: + path = output / name + if path.exists(): + if path.is_symlink() or path.read_bytes() != content.encode('utf-8'): + raise ValueError("Existing study launcher changed; it was preserved.") + else: + with path.open('x', encoding='utf-8', newline='\n') as f: f.write(content) + for name, source in [('feedback.json', kit_root / 'participants' / participant / 'feedback.json'), + ('GUIDE_CN.md', kit_root / 'GUIDE_CN.md')]: + path = output / name + if path.is_symlink(): raise ValueError("Study material cannot be a symlink.") + if not path.exists(): + with path.open('xb') as f: f.write(source.read_bytes()) + state['status'] = 'INSTALLED' + write(state_path, state) + except KeyboardInterrupt: + state['status'] = 'CANCELLED'; write(state_path, state) + raise + except (OSError, ValueError, subprocess.SubprocessError): + state['status'] = 'ERROR'; write(state_path, state) + print(f"Setup interrupted/failed. Retained log: {log_path}; retry with the same arguments and --resume.", file=sys.stderr) + raise + print(f"[INSTALLED] {output}\n[NEXT] Enter that directory, read GUIDE_CN.md, then run: python mimo.py --version\n[NOTE] No model call or human acceptance was recorded.") + return 0 + + +def main(argv=None): + # Windows redirected output can otherwise use a code page that cannot + # represent a Chinese study path. Child Python processes also request UTF-8. + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, 'reconfigure'): + stream.reconfigure(encoding='utf-8', errors='backslashreplace') + parser = argparse.ArgumentParser(description='Install an exact user kit in a fresh private environment; no model calls.') + parser.add_argument('--participant', required=True) + parser.add_argument('--output', required=True, type=Path) + parser.add_argument('--resume', action='store_true') + args = parser.parse_args(argv) + try: + return install(Path(__file__).resolve().parent, args.output, args.participant, args.resume) + except KeyboardInterrupt: + return 130 + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as exc: + print('[SETUP ERROR] ' + str(exc), file=sys.stderr) + return 4 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/src/mimo_harness/user_kit.py b/src/mimo_harness/user_kit.py new file mode 100644 index 0000000..3bf7312 --- /dev/null +++ b/src/mimo_harness/user_kit.py @@ -0,0 +1,132 @@ +"""Build a portable, exact-engine user-study kit; no model or user is simulated.""" +from __future__ import annotations + +import hashlib +import shutil +import zipfile +from email.parser import BytesParser +from pathlib import Path +from types import SimpleNamespace + +from mimo_harness import __version__ +from mimo_harness.manifests import atomic_json, engine_identity, engine_inventory, file_digest + +ALLOWED_PACKAGES = {"mimo-codeharness", "pip", "pytest", "iniconfig", "packaging", "pluggy", "pygments", + "exceptiongroup", "tomli", "colorama", "typing-extensions"} +REQUIRED_PACKAGES = ALLOWED_PACKAGES - {"typing-extensions"} + +GUIDE = """# MiMoHarness 独立用户试用 + +本包用于 v0.12 beta 的独立试用,不代表正式发布验收完成。安装、参考补丁和自动化演练均不算真人反馈,不会调用模型。 + +## 开始 + +先安装 Python 3.10+ 和 Git。三位使用者使用同一份试用包,各选一个不同的 participant 名称。不要混用不同平台重新构建的 wheel。 + +在试用包所在目录运行(把 user-01 换成分配给你的 user-02 或 user-03): + +```text +python bootstrap.py --participant user-01 --output my-study +``` + +脚本校验文件后,在新目录创建独立 Python 环境,仅从包内 wheels 安装依赖;不会修改系统 Python,也不需要激活虚拟环境。安装不联网。真实源码准备阶段需要 Git 能访问 GitHub。 + +中断或安装失败后,保留目录并添加 `--resume` 重试;setup.json 和 install-NNN.log 用于诊断。已有 feedback.json 不会被覆盖。不需要模型密钥,不要把密钥写入反馈。 + +## 实际操作 + +进入刚生成的 my-study 目录,依次执行: + +```text +python mimo.py --version +python mimo.py case-init --case boltons-write --output case +python mimo.py --config case/mimo-harness.json doctor +python mimo.py case-check --directory case --name first +``` + +查看 case/mimo-harness.json 和目标测试。最后一条命令验证参考修复 PASS、故意回归 FAIL、自动恢复及导出;这是参考对照,不是模型修复。 + +再运行一次相同的 case-check 命令,预期退出码 4 并拒绝覆盖。确认原有证据仍存在。新验收使用新名称。 + +独立练习恢复和导出: + +```text +python mimo.py resume --run case/runs/first-reference --name manual-recovery +python mimo.py export --run case/runs/manual-recovery --output evidence.zip +python mimo.py gate --bundle evidence.zip --json +``` + +恢复会重新运行基线和候选检查。把 evidence.zip 复制到另一个目录,再用 gate 检查;解压后打开 report.html。原始参考证据也位于 case/results/first/reference.zip。 + +## 如实反馈 + +实际完成后编辑本目录 feedback.json,填写匿名 tester_id、observed_at、environment 和遇到的问题,逐项记录步骤。只有独立操作后才填写 independence_confirmed=true。evidence_bundle 填 evidence.zip;每位使用者提交自己运行的 ZIP。 + +尚未完成或未解决的问题保持 false。不要把自动安装的 setup.json 当作反馈完成证明。将 feedback.json、evidence.zip 和必要的安装日志交还维护者;卡住时也应记录并反馈。 + +文件哈希用于检查传输损坏和版本一致性;包和反馈没有数字签名,请从可信维护者取得。代码、真实模型效果和真人独立性分别验收。 +""" + + +def inspect_wheel(path): + if path.is_symlink() or not path.is_file() or path.stat().st_size > 64 * 1024 * 1024: + raise ValueError("Kit wheels must be regular files smaller than 64 MiB.") + try: + with zipfile.ZipFile(path) as wheel: + names = wheel.namelist() + if len(names) != len(set(names)) or sum(i.file_size for i in wheel.infolist()) > 128 * 1024 * 1024: + raise ValueError("Invalid/oversized wheel inventory.") + metadata = [n for n in names if n.endswith('.dist-info/METADATA')] + headers = [n for n in names if n.endswith('.dist-info/WHEEL')] + if len(metadata) != 1 or len(headers) != 1: + raise ValueError("Wheel metadata missing or ambiguous.") + info = BytesParser().parsebytes(wheel.read(metadata[0])) + tags = BytesParser().parsebytes(wheel.read(headers[0])) + if tags.get('Root-Is-Purelib', '').lower() != 'true' or not any( + tag == 'py3-none-any' for tag in tags.get_all('Tag', [])): + raise ValueError("User kits require portable py3-none-any wheels.") + name = info.get('Name', '').lower().replace('_', '-') + if name not in ALLOWED_PACKAGES or not info.get('Version'): + raise ValueError("Unsupported kit dependency: " + name) + if name == 'mimo-codeharness': + actual = {n: hashlib.sha256(wheel.read(n)).hexdigest() for n in names + if (n.startswith('mimo_harness/') and n.count('/') == 1 and Path(n).suffix in {'.py', '.mjs'}) + or n in {'run_mimo_codeharness_v02.py', 'scoring_v03.py', 'verification_v04.py'}} + if info['Version'] != __version__ or actual != engine_inventory(): + raise ValueError("Harness wheel does not match the current version/engine; rebuild or use its matching installed CLI.") + return {"package": name, "version": info['Version'], "file": path.name, "sha256": file_digest(path)} + except (zipfile.BadZipFile, KeyError, RuntimeError, EOFError) as exc: + raise ValueError("Invalid wheel: " + path.name) from exc + + +def command_walkthrough_pack(args): + from mimo_harness.product_acceptance import command_walkthrough_init + wheelhouse = Path(args.wheelhouse).expanduser().resolve() + if not wheelhouse.is_dir(): + raise ValueError("Wheelhouse directory missing.") + paths = sorted(wheelhouse.iterdir()) + if not paths or any(p.suffix != '.whl' for p in paths): + raise ValueError("Wheelhouse must contain only the reviewed portable wheels.") + wheels = [inspect_wheel(p) for p in paths] + names = [row['package'] for row in wheels] + if len(names) != len(set(names)) or not REQUIRED_PACKAGES.issubset(names): + raise ValueError("Wheelhouse needs one harness/pytest wheel and complete Linux/Windows/Python 3.10 dependencies; see the user-kit guide.") + output = Path(args.output).expanduser().resolve() + output.mkdir(parents=True, exist_ok=False) + (output / 'wheels').mkdir() + for path in paths: + shutil.copy2(path, output / 'wheels' / path.name) + shutil.copyfile(Path(__file__).with_name('study_bootstrap.py'), output / 'bootstrap.py') + (output / 'GUIDE_CN.md').write_text(GUIDE, encoding='utf-8', newline='\n') + participants = ['user-01', 'user-02', 'user-03'] + for slot in participants: + folder = output / 'participants' / slot + command_walkthrough_init(SimpleNamespace(output=str(folder))) + (folder / 'GUIDE_CN.md').write_text(GUIDE, encoding='utf-8', newline='\n') + files = {p.relative_to(output).as_posix(): file_digest(p) for p in sorted(output.rglob('*')) if p.is_file()} + atomic_json(output / 'kit.json', {"schema_version": 1, "kind": "independent_user_kit", "version": __version__, + "engine_sha256": engine_identity(), "participants": participants, "wheels": wheels, "files": files, + "human_trials_completed": 0, "model_called": False, + "integrity_boundary": "Unsigned transfer/version integrity; participant labels are slots, not observed people."}) + print(f"[KIT] {output}\n[NEXT] Read GUIDE_CN.md; distribute this same kit to three independent users. No human/model acceptance recorded.") + return 0 diff --git a/tests/ci_user_kit.py b/tests/ci_user_kit.py new file mode 100644 index 0000000..ec747de --- /dev/null +++ b/tests/ci_user_kit.py @@ -0,0 +1,68 @@ +"""Actual wheel/venv rehearsal; never counted as an independent human trial.""" +import argparse +import hashlib +import json +import shutil +import subprocess +import sys +from pathlib import Path + + +def main(): + p = argparse.ArgumentParser() + p.add_argument('--wheelhouse', required=True, type=Path) + p.add_argument('--output', required=True, type=Path) + args = p.parse_args() + root = args.output.resolve(); root.mkdir(parents=True, exist_ok=False) + kit, study = root / 'kit', root / '试用 study with spaces' + + def call(argv, expect=0): + result = subprocess.run(list(map(str, argv)), capture_output=True, text=True, encoding='utf-8', timeout=240) + if result.returncode != expect: + raise AssertionError(f"Exit {result.returncode}, expected {expect}: {argv}\n{result.stdout}\n{result.stderr}") + return result + + call([sys.executable, '-I', '-m', 'mimo_harness', 'walkthrough-pack', '--wheelhouse', args.wheelhouse.resolve(), '--output', kit]) + command = [sys.executable, kit / 'bootstrap.py', '--participant', 'user-01', '--output', study] + call(command) + feedback = study / 'feedback.json' + data = json.loads(feedback.read_text(encoding='utf-8')) + assert data['tester_id'] == '' and data['independence_confirmed'] is False and not any(data['steps'].values()) + data['notes'] = 'AUTOMATED_REHEARSAL: preserve this diagnostic note; no human observation.' + feedback.write_text(json.dumps(data), encoding='utf-8') + saved = feedback.read_bytes() + call(command, expect=4) + # Keep metadata but remove entry files, reproducing a half-installed pip + # and harness that a normal "already satisfied" install cannot repair. + site = next((study / '.venv').glob('Lib/site-packages')) if sys.platform == 'win32' else next((study / '.venv').glob('lib/python*/site-packages')) + (site / 'pip/__main__.py').unlink() + (site / 'mimo_harness/cli.py').unlink() + call(command + ['--resume']) + assert feedback.read_bytes() == saved + assert len(list(study.glob('install-*.log'))) == 2 + assert json.loads((study / 'setup.json').read_text())['status'] == 'INSTALLED' + + cli = [sys.executable, study / 'mimo.py'] + call(cli + ['--version']) + call(cli + ['case-init', '--case', 'boltons-write', '--output', 'case']) + call(cli + ['--config', 'case/mimo-harness.json', 'doctor']) + call(cli + ['case-check', '--directory', 'case', '--name', 'first']) + reference = study / 'case/results/first/reference.zip' + original = hashlib.sha256(reference.read_bytes()).hexdigest() + call(cli + ['case-check', '--directory', 'case', '--name', 'first'], expect=4) + assert hashlib.sha256(reference.read_bytes()).hexdigest() == original + call(cli + ['resume', '--run', 'case/runs/first-reference', '--name', 'manual-recovery']) + call(cli + ['export', '--run', 'case/runs/manual-recovery', '--output', 'evidence.zip']) + moved = root / 'relocated.zip'; shutil.copyfile(study / 'evidence.zip', moved) + gate = json.loads(call(cli + ['gate', '--bundle', moved, '--json']).stdout) + assert gate['allowed'] is True + acceptance = json.loads((study / 'case/results/first/acceptance.json').read_text()) + assert acceptance['status'] == 'PASS' and acceptance['model_called'] is False + (root / 'rehearsal.json').write_text(json.dumps({'schema_version':1,'mode':'automated_installation_rehearsal', + 'status':'PASS','human_trials_completed':0,'model_called':False,'fresh_install':True,'partial_venv_recovery':True, + 'feedback_preserved':True,'reference_regression_recovery_gate':True,'relocated_bundle_gate':gate}, indent=2), encoding='utf-8') + print(f"User-kit installation/recovery/evidence rehearsal PASS: {root}; human trials 0; model calls 0.") + + +if __name__ == '__main__': + main() diff --git a/tests/test_user_kit.py b/tests/test_user_kit.py new file mode 100644 index 0000000..8059717 --- /dev/null +++ b/tests/test_user_kit.py @@ -0,0 +1,108 @@ +"""Synthetic wheel fixtures test kit integrity; actual installation runs in CI.""" +import hashlib +import json +import zipfile +from pathlib import Path + +import pytest + +from mimo_harness import __version__ +from mimo_harness.cli import main +from mimo_harness.manifests import engine_identity, atomic_json +from mimo_harness.study_bootstrap import validate_kit, install +from mimo_harness.user_kit import REQUIRED_PACKAGES + + +@pytest.fixture +def wheelhouse(tmp_path): + import importlib.util + import mimo_harness.user_kit as kit + folder = tmp_path / 'wheels'; folder.mkdir() + source = Path(kit.__file__).parent + for name in REQUIRED_PACKAGES: + stem = name.replace('-', '_') + version = __version__ if name == 'mimo-codeharness' else '1.0' + with zipfile.ZipFile(folder / f'{stem}-{version}-py3-none-any.whl', 'w') as wheel: + wheel.writestr(f'{stem}-{version}.dist-info/METADATA', f'Metadata-Version: 2.1\nName: {name}\nVersion: {version}\n') + wheel.writestr(f'{stem}-{version}.dist-info/WHEEL', 'Wheel-Version: 1.0\nRoot-Is-Purelib: true\nTag: py3-none-any\n') + if name == 'mimo-codeharness': + for path in source.iterdir(): + if path.suffix in {'.py', '.mjs'}: wheel.writestr('mimo_harness/' + path.name, path.read_bytes()) + for module in ('run_mimo_codeharness_v02', 'scoring_v03', 'verification_v04'): + wheel.writestr(module + '.py', Path(importlib.util.find_spec(module).origin).read_bytes()) + return folder + + +def make_kit(tmp_path, wheelhouse): + root = tmp_path / 'kit' + assert main(['walkthrough-pack', '--wheelhouse', str(wheelhouse), '--output', str(root)]) == 0 + return root + + +def test_three_blank_slots_share_exact_wheel_and_engine(tmp_path, wheelhouse): + root = make_kit(tmp_path, wheelhouse) + kit, _, wheels = validate_kit(root) + assert kit['engine_sha256'] == engine_identity() and kit['human_trials_completed'] == 0 and kit['model_called'] is False + assert len(wheels) == len(REQUIRED_PACKAGES) + for slot in kit['participants']: + feedback = json.loads((root / 'participants' / slot / 'feedback.json').read_text()) + assert feedback['engine_sha256'] == kit['engine_sha256'] + assert feedback['tester_id'] == '' and feedback['independence_confirmed'] is False and not any(feedback['steps'].values()) + saved = (root / 'kit.json').read_bytes() + assert main(['walkthrough-pack', '--wheelhouse', str(wheelhouse), '--output', str(root)]) == 4 + assert (root / 'kit.json').read_bytes() == saved + + +@pytest.mark.parametrize('kind', ['wheel', 'feedback', 'bootstrap']) +def test_corrupt_transfer_is_rejected_before_any_install_or_directory_creation(tmp_path, wheelhouse, kind, monkeypatch): + root = make_kit(tmp_path, wheelhouse) + path = {'wheel': next((root / 'wheels').iterdir()), 'feedback': root / 'participants/user-01/feedback.json', + 'bootstrap': root / 'bootstrap.py'}[kind] + path.write_bytes(path.read_bytes() + b'changed') + import subprocess + monkeypatch.setattr(subprocess, 'run', lambda *a, **kw: pytest.fail('Corrupt kit reached execution')) + output = tmp_path / 'study' + with pytest.raises(ValueError, match='changed'): + install(root, output, 'user-01') + assert not output.exists() + + +@pytest.mark.parametrize('kind', ['missing-dependency', 'wrong-engine', 'duplicate-wheel', 'platform-wheel']) +def test_incompatible_wheelhouse_is_rejected_without_partial_kit(tmp_path, wheelhouse, kind): + if kind == 'missing-dependency': + next(wheelhouse.glob('colorama*.whl')).unlink() + elif kind == 'wrong-engine': + p = next(wheelhouse.glob('mimo_*.whl')) + with zipfile.ZipFile(p, 'a') as w: w.writestr('mimo_harness/new_code.py', 'changed=True\n') + elif kind == 'duplicate-wheel': + p = next(wheelhouse.glob('pytest*.whl'));(wheelhouse / ('copy_' + p.name)).write_bytes(p.read_bytes()) + else: + p = next(wheelhouse.glob('pytest*.whl')) + with zipfile.ZipFile(p) as w: content = {n:w.read(n) for n in w.namelist()} + with zipfile.ZipFile(p, 'w') as w: + for n,v in content.items():w.writestr(n, v.replace(b'py3-none-any', b'cp312-cp312-win_amd64') if n.endswith('/WHEEL') else v) + output = tmp_path / 'kit' + assert main(['walkthrough-pack', '--wheelhouse', str(wheelhouse), '--output', str(output)]) == 4 + assert not output.exists() + + +def test_resume_rejects_other_kit_or_slot_before_install(tmp_path, wheelhouse, monkeypatch): + root = make_kit(tmp_path, wheelhouse) + output = tmp_path / 'study'; output.mkdir() + atomic_json(output / 'setup.json', {'schema_version':1,'kind':'technical_installation','kit_sha256':'wrong','attempts':1}) + import subprocess + monkeypatch.setattr(subprocess, 'run', lambda *a, **kw: pytest.fail('Mismatched setup reached execution')) + with pytest.raises(ValueError, match='changed'): + install(root, output, 'user-01', resume=True) + with pytest.raises(ValueError, match='already exists'): + install(root, output, 'user-01') + + +def test_unsafe_manifest_path_does_not_read_outside_kit(tmp_path, wheelhouse): + root = make_kit(tmp_path, wheelhouse) + outside = tmp_path / 'outside.txt'; outside.write_text('fixture') + manifest = json.loads((root / 'kit.json').read_text()) + manifest['files']['../outside.txt'] = hashlib.sha256(outside.read_bytes()).hexdigest() + atomic_json(root / 'kit.json', manifest) + with pytest.raises(ValueError, match='Unsafe'): + validate_kit(root) From 95b4d428bd32180b6d854c3fbf6ee683908452aa Mon Sep 17 00:00:00 2001 From: KIM Date: Mon, 19 Oct 2026 01:33:39 -0600 Subject: [PATCH 2/2] ci: publish directly usable independent user kit artifacts --- .github/workflows/verification.yml | 6 ++++++ docs/INDEPENDENT_USER_KIT_CN.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/verification.yml b/.github/workflows/verification.yml index c9133ab..de846aa 100644 --- a/.github/workflows/verification.yml +++ b/.github/workflows/verification.yml @@ -231,6 +231,12 @@ jobs: .mimo/user-kit-rehearsal/kit/ .mimo/user-kit-rehearsal/rehearsal.json include-hidden-files: true + - uses: actions/upload-artifact@v4 + with: + name: independent-user-kit-${{ matrix.os }} + path: .mimo/user-kit-rehearsal/kit/ + include-hidden-files: true + if-no-files-found: error unit-and-smoke: runs-on: ubuntu-latest diff --git a/docs/INDEPENDENT_USER_KIT_CN.md b/docs/INDEPENDENT_USER_KIT_CN.md index ea09ad4..e26ea17 100644 --- a/docs/INDEPENDENT_USER_KIT_CN.md +++ b/docs/INDEPENDENT_USER_KIT_CN.md @@ -17,7 +17,7 @@ mimo walkthrough-pack --wheelhouse .mimo/kit-wheels --output .mimo/user-kit 准备依赖需要联网;输出目录必须是新目录。wheel 必须与当前 CLI 的版本和引擎字节一致;缺依赖、重复版本、平台专用 wheel 和不同引擎都会被拒绝。安装时使用这些确切 wheel,不从在线索引解析新依赖。 -CI 的 Linux/Windows `cli-install` 任务从真实构建产物生成试用包,并演练新环境安装、拒绝覆盖、半成品虚拟环境恢复、保留反馈、真实 boltons 对照验收、手动恢复和迁移后的 ZIP 门禁。对应 `cli-pytest-*` artifact 内包含 `.mimo/user-kit-rehearsal/kit/` 和 `rehearsal.json`。选择一份可信 CI 产出的包,原样分发给三位使用者;不要混合不同任务中重建的 wheel。 +CI 的 Linux/Windows `cli-install` 任务从真实构建产物生成试用包,并演练新环境安装、拒绝覆盖、半成品虚拟环境恢复、保留反馈、真实 boltons 对照验收、手动恢复和迁移后的 ZIP 门禁。选择一份可信 CI 的 `independent-user-kit-ubuntu-latest` 或 `independent-user-kit-windows-latest` artifact 下载;解压后即可看到 bootstrap.py 和 GUIDE_CN.md。原样分发同一份包给三位使用者,不要混合不同任务中重建的 wheel。技术演练记录 rehearsal.json 另保留在 `cli-pytest-*` artifact 中。 ## 新用户操作