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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ patch version.
- Pinned `skills@1.5.22` distribution discovery plus a separate
`skills@latest` forward-compatibility smoke.

### Fixed

- Reject non-integer manifest schema versions and blank item titles or source
references.
- Fail closed when a local ticket omits its `Blocked by` declaration; tickets
without blockers must state `None` explicitly.

### Boundaries

- The public release, anonymous-install verification, clean-home Codex and
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ collected only on a frozen release candidate.

| Source | V0 support |
| --- | --- |
| `to-tickets` local one-ticket-per-file Markdown | Supported; attested to upstream `v1.2.2` |
| `to-tickets` local one-ticket-per-file Markdown | Supported; attested to upstream `v1.2.2`; every ticket must explicitly declare `Blocked by`, using `None` when empty |
| GitHub native sub-issues and dependencies | Supported |
| GitHub task-list children and `## Blocked by` / `Blocked by: #N` fallback | Supported |
| Normalized work-graph JSON | Supported |
Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# First Tree Skills

> 英文 [README.md](README.md) 是规范来源。本译文同步于 2026-08-06;若内容不一致,以英文版为准。
> 英文 [README.md](README.md) 是规范来源。本译文同步于 2026-08-10;若内容不一致,以英文版为准。

用于把结构化 agent 工作安全移入
[First Tree](https://first-tree.ai/?utm_source=first-tree-skills&utm_medium=github&utm_campaign=readme) 的 companion skills。
Expand Down Expand Up @@ -56,7 +56,7 @@ Use the run-with-first-tree skill to inspect and run the tickets under

## V0 输入与边界

支持:`to-tickets` v1.2.2 的一票一文件 Markdown、GitHub 原生子 issue 与依赖、正文中的 `Blocked by` fallback,以及 normalized work-graph JSON。
支持:`to-tickets` v1.2.2 的一票一文件 Markdown(每张票必须显式声明 `Blocked by`,无依赖时写 `None`)、GitHub 原生子 issue 与依赖、正文中的 `Blocked by` fallback,以及 normalized work-graph JSON。

不支持:直接消费 Wayfinder map、GitLab/custom tracker 原生 API、后台监控、自动改写源 tracker,以及把派发权限扩展到用户未授权的代码、生产或外部系统。

Expand Down
19 changes: 13 additions & 6 deletions skills/run-with-first-tree/references/work-graph-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,12 @@ Use UTF-8 JSON:
}
```

The validator requires `schema_version`, `source`, and `items`. Every item
requires every field shown above. Its `graph_digest` identifies the stable
source, item identities, dispatchability, and blocking topology; mutable status,
claim, and acceptance fields do not change the digest.
The validator requires `schema_version`, `source`, and `items`.
`schema_version` must be the JSON integer `1`; booleans and numeric strings are
invalid. Every item requires every field shown above, and `title` and `source`
must be non-empty strings. Its `graph_digest` identifies the stable source, item
identities, dispatchability, and blocking topology; mutable status, claim, and
acceptance fields do not change the digest.

## Item semantics

Expand All @@ -49,8 +51,9 @@ claim, and acceptance fields do not change the digest.
or `-` so it can be reused safely in dispatch metadata.
`AMBIGUOUS:` and `UNRESOLVED:` are reserved legacy dependency-marker
prefixes and cannot be used as item ids.
- `title` — human-readable ticket name.
- `source` — absolute or project-relative path, or canonical issue URL.
- `title` — non-empty human-readable ticket name.
- `source` — non-empty absolute or project-relative path, or canonical issue
URL.
- `status` — `open`, `completed`, or `cancelled`. A GitHub issue closed as
`not_planned`, `duplicate`, or another non-completion reason is cancelled and
does not release dependents.
Expand Down Expand Up @@ -115,6 +118,10 @@ The local builder recognizes the one-ticket-per-file shape used by
- `**Status:** ready-for-agent` as the readiness signal; and
- Markdown task-list items as acceptance criteria.

Every local ticket must contain exactly one `Blocked by` declaration. Tickets
without blockers must state `None` explicitly. A missing or repeated declaration
is unresolved and validation fails closed.

An atom that is exactly `#<item-id>` (optionally followed by one terminal `.`
or `!`) resolves an exact-case item id before case-folded ids or aliases.
Formatting or list-marker prefixes do not participate in that identity rule.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,10 @@ def identity_syntax_is_valid(original: str, reference_id: str) -> bool:
f"{UNRESOLVED_DEPENDENCY_PREFIX}oversized-blocker-value:{size}"
for size in oversized_blocker_sizes
]
if blocker_declaration_count == 0:
resolved.append(
f"{UNRESOLVED_DEPENDENCY_PREFIX}missing-blocked-by-declaration"
)
for original_blocker, blocker, syntax_valid in raw_blockers:
identity_candidates = [blocker]
if blocker.endswith((".", "!")):
Expand Down
14 changes: 12 additions & 2 deletions skills/run-with-first-tree/scripts/validate_work_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ def _load_manifest(path: Path) -> tuple[dict[str, Any] | None, list[str]]:

def _validate_shape(manifest: dict[str, Any]) -> list[str]:
errors: list[str] = []
if manifest.get("schema_version") != 1:
errors.append("schema_version must be 1")
if (
type(manifest.get("schema_version")) is not int
or manifest.get("schema_version") != 1
):
errors.append("schema_version must be the integer 1")

source = manifest.get("source")
if not isinstance(source, dict):
Expand Down Expand Up @@ -77,6 +80,13 @@ def _validate_shape(manifest: dict[str, Any]) -> list[str]:
elif not isinstance(item[field], expected_type):
errors.append(f"{prefix}.{field} must be {expected_type.__name__}")

title = item.get("title")
if isinstance(title, str) and not title.strip():
errors.append(f"{prefix}.title must be a non-empty string")
source_reference = item.get("source")
if isinstance(source_reference, str) and not source_reference.strip():
errors.append(f"{prefix}.source must be a non-empty string")

item_id = item.get("id")
if isinstance(item_id, str):
if not item_id.strip():
Expand Down
Loading
Loading