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
173 changes: 104 additions & 69 deletions .sopify-skills/blueprint/tasks.md

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ Format: Summary → Changed → Plan Packages. File-level details live in `git l

## [Unreleased]

## [2026-05-13.111757] - 2026-05-13

### Summary

- Changes across: Runtime, Tests, Changed.

### Changed

- **Runtime**: Updated runtime internals (2 files)
- **Tests**: Updated automated coverage (3 files)
- **Changed**: Updated project files (1 files)

## [2026-05-11.202509] - 2026-05-11

### Summary
Expand Down
2 changes: 1 addition & 1 deletion Claude/Skills/CN/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- bootstrap: lang=zh-CN; encoding=UTF-8 -->
<!-- SOPIFY_VERSION: 2026-05-11.202509 -->
<!-- SOPIFY_VERSION: 2026-05-13.111757 -->
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->

# Sopify - 自适应 AI 编程助手
Expand Down
2 changes: 1 addition & 1 deletion Claude/Skills/EN/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- bootstrap: lang=en-US; encoding=UTF-8 -->
<!-- SOPIFY_VERSION: 2026-05-11.202509 -->
<!-- SOPIFY_VERSION: 2026-05-13.111757 -->
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->

# Sopify - Adaptive AI Programming Assistant
Expand Down
2 changes: 1 addition & 1 deletion Codex/Skills/CN/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- bootstrap: lang=zh-CN; encoding=UTF-8 -->
<!-- SOPIFY_VERSION: 2026-05-11.202509 -->
<!-- SOPIFY_VERSION: 2026-05-13.111757 -->
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->

# Sopify - 自适应 AI 编程助手
Expand Down
2 changes: 1 addition & 1 deletion Codex/Skills/EN/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!-- bootstrap: lang=en-US; encoding=UTF-8 -->
<!-- SOPIFY_VERSION: 2026-05-11.202509 -->
<!-- SOPIFY_VERSION: 2026-05-13.111757 -->
<!-- ARCHITECTURE: Adaptive Workflow + Layered Rules -->

# Sopify - Adaptive AI Programming Assistant
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
[![Docs](https://img.shields.io/badge/docs-CC%20BY%204.0-green.svg)](./LICENSE-docs)
[![Version](https://img.shields.io/badge/version-2026--05--11.202509-orange.svg)](#version-history)
[![Version](https://img.shields.io/badge/version-2026--05--13.111757-orange.svg)](#version-history)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING.md)

English · [简体中文](./README.zh-CN.md) · [Quick Start](#quick-start) · [Contributors](./CONTRIBUTORS.md)
Expand Down Expand Up @@ -50,6 +50,14 @@ Sopify uses project-level conventions to make critical steps visible. The basic
| Decisions are invisible and non-auditable | Plan changes force re-confirmation — the AI cannot silently proceed |
| Each session's learning is disposable | Plans, decisions, and reviews persist as reusable project assets |

## Architecture

<div align="center">
<img src="./assets/sopify-architecture.svg" width="800" alt="Sopify Architecture — Evidence & Authorization Layer" />
</div>

User input flows through a host adapter (Codex, Claude, etc.) into the Core Protocol, where every action is proposed, validated, gated, and receipted. The Validator is the sole authorizer — the host LLM is only a proposal source. Knowledge layers (blueprint, plan, history) persist across sessions and hosts.

## Installation

Two-step install (recommended for first-time review):
Expand Down
10 changes: 9 additions & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

[![许可证](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
[![文档](https://img.shields.io/badge/docs-CC%20BY%204.0-green.svg)](./LICENSE-docs)
[![版本](https://img.shields.io/badge/version-2026--05--11.202509-orange.svg)](#版本历史)
[![版本](https://img.shields.io/badge/version-2026--05--13.111757-orange.svg)](#版本历史)
[![欢迎PR](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](./CONTRIBUTING_CN.md)

[English](./README.md) · 简体中文 · [快速开始](#快速开始) · [贡献者](./CONTRIBUTORS.md)
Expand Down Expand Up @@ -50,6 +50,14 @@ Sopify 用项目级约定把关键节点变成可见流程。基础过程记录
| 决策不可见、不可审计 | 方案变更后必须重新确认 — AI 不能静默继续 |
| 每个 session 的学习都是一次性的 | 方案、决策、审查结论沉淀为可复用的项目资产 |

## 架构

<div align="center">
<img src="./assets/sopify-architecture.svg" width="800" alt="Sopify 架构 — 证据与授权层" />
</div>

用户输入经过宿主适配器(Codex、Claude 等)进入核心协议层,每个操作都经历提议、校验、闸门、收据四步。Validator 是唯一授权者 — 宿主 LLM 只是提议来源。知识层(蓝图、方案、历史)跨 session 和宿主持久保留。

## 安装说明

两步安装(推荐首次使用时先审查再执行):
Expand Down
Binary file added assets/sopify-architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions assets/sopify-architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions installer/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ def _build_bundle_smoke_env(*, payload_manifest_path: Path | None) -> dict[str,
env = dict(os.environ)
if payload_manifest_path is not None:
env["SOPIFY_PAYLOAD_MANIFEST"] = str(payload_manifest_path)
# Keep bundle smoke focused on bundle/runtime assets instead of inheriting
# arbitrary user-level skills from the current machine.
isolated_home = Path(env.get("TMPDIR") or "/tmp") / "sopify-bundle-smoke-home"
isolated_home.mkdir(parents=True, exist_ok=True)
env["HOME"] = str(isolated_home)
return env


Expand Down
69 changes: 67 additions & 2 deletions runtime/_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,14 @@ def _parse_mapping(lines: Sequence[_Line], index: int, indent: int) -> Tuple[dic
if line.content.startswith("- "):
break
key, remainder = _split_key_value(line)
if remainder == "":
if _is_block_scalar_marker(remainder):
value, index = _parse_block_scalar(
lines,
index + 1,
parent_indent=indent,
style=remainder,
)
elif remainder == "":
index += 1
if index < len(lines) and lines[index].indent > indent:
value, index = _parse_block(lines, index, lines[index].indent)
Expand Down Expand Up @@ -136,7 +143,15 @@ def _parse_list(lines: Sequence[_Line], index: int, indent: int) -> Tuple[list[A
if _looks_like_mapping_entry(item_text):
key, remainder = _split_key_value(_Line(indent=indent + 2, content=item_text, line_number=line.line_number))
item: dict[str, Any] = {}
if remainder == "":
if _is_block_scalar_marker(remainder):
value, index = _parse_block_scalar(
lines,
index,
parent_indent=indent,
style=remainder,
)
item[key] = value
elif remainder == "":
if has_child:
value, index = _parse_block(lines, index, lines[index].indent)
else:
Expand Down Expand Up @@ -197,3 +212,53 @@ def _parse_scalar(value: str) -> Any:
inner = value[1:-1]
return inner.replace(r"\'", "'").replace(r'\"', '"')
return value


def _is_block_scalar_marker(value: str) -> bool:
return value in {"|", "|-", ">", ">-"}


def _parse_block_scalar(
lines: Sequence[_Line],
index: int,
*,
parent_indent: int,
style: str,
) -> Tuple[str, int]:
if index >= len(lines) or lines[index].indent <= parent_indent:
return "", index

block_indent = lines[index].indent
chunks: list[str] = []
while index < len(lines):
line = lines[index]
if line.indent < block_indent:
break
if line.indent == parent_indent:
break
if line.indent < block_indent:
raise YamlParseError(f"Unexpected indentation at line {line.line_number}")
relative_indent = line.indent - block_indent
chunks.append((" " * relative_indent) + line.content)
index += 1

if style.startswith("|"):
text = "\n".join(chunks)
else:
paragraphs: list[str] = []
current: list[str] = []
for chunk in chunks:
if chunk == "":
if current:
paragraphs.append(" ".join(current))
current = []
paragraphs.append("")
else:
current.append(chunk)
if current:
paragraphs.append(" ".join(current))
text = "\n".join(paragraphs)

if not style.endswith("-"):
text += "\n"
return text, index
13 changes: 9 additions & 4 deletions runtime/skill_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from typing import Dict, Iterable, Mapping, Optional, Sequence
import re

from ._yaml import load_yaml
from ._yaml import YamlParseError, load_yaml
from .builtin_catalog import load_builtin_skills
from .models import RuntimeConfig, SkillMeta
from .skill_schema import SkillManifestError, normalize_skill_manifest
Expand Down Expand Up @@ -77,7 +77,7 @@ def _discover_under_root(self, root: Path, source: str) -> Iterable[SkillMeta]:

def _read_skill(self, skill_file: Path, source: str) -> Optional[SkillMeta]:
text = skill_file.read_text(encoding="utf-8")
front_matter = _parse_front_matter(text)
front_matter = _parse_front_matter(text, fail_closed=(source == "builtin"))
skill_dir = skill_file.parent
raw_manifest = _load_manifest(skill_dir / "skill.yaml")
try:
Expand Down Expand Up @@ -131,11 +131,16 @@ def _read_skill(self, skill_file: Path, source: str) -> Optional[SkillMeta]:
)


def _parse_front_matter(text: str) -> dict[str, object]:
def _parse_front_matter(text: str, *, fail_closed: bool = False) -> dict[str, object]:
match = _FRONT_MATTER_RE.match(text)
if not match:
return {}
data = load_yaml(match.group(1))
try:
data = load_yaml(match.group(1))
except YamlParseError:
if fail_closed:
raise
return {}
return data if isinstance(data, dict) else {}


Expand Down
2 changes: 2 additions & 0 deletions tests/test_installer_validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ def test_smoke_uses_explicit_payload_manifest_env(self) -> None:
self.assertEqual(output, "ok")
env = mock_run.call_args.kwargs.get("env") or {}
self.assertEqual(env.get("SOPIFY_PAYLOAD_MANIFEST"), str(payload_manifest))
self.assertIn("HOME", env)
self.assertTrue(env["HOME"].endswith("sopify-bundle-smoke-home"))

def test_failure_details_always_include_exit_status_and_command(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
Expand Down
Loading
Loading