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
285 changes: 156 additions & 129 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 4 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -387,10 +387,7 @@ used_underscore_binding = "allow"
## sandbox dirs, mobile-layout detection, ArkUI native inputs), with the legacy
## `old/` crate copies removed so the repo resolves as a cargo git dependency.
[patch."https://github.com/makepad/makepad"]
## Pinned to ZhangHanDong/makepad dev at the merge of PR #6 (Splash-isolate
## shader identity, headless renderer correctness, ttf-parser gvar-alloc for
## Apple variable fonts, the CoreText outline fallback for hvgl-only system
## fonts like PingFang, and the closed-Modal overdraw fix), plus the
## selectable-Html fix from PR #5.
makepad-widgets = { git = "https://github.com/ZhangHanDong/makepad", rev = "1dd8d372cfb65b20160a3782586ed4d593cf8193", features = ["serde"] }
makepad-code-editor = { git = "https://github.com/ZhangHanDong/makepad", rev = "1dd8d372cfb65b20160a3782586ed4d593cf8193" }
## Isolated E2E integration of reviewed Dock visibility PR 8.
## Build Makepad Studio from this same commit for GUI E2E tests.
makepad-widgets = { git = "https://github.com/ZhangHanDong/makepad", rev = "45e826c2814fccd2c69d558b27775858444c907d" }
makepad-code-editor = { git = "https://github.com/ZhangHanDong/makepad", rev = "45e826c2814fccd2c69d558b27775858444c907d" }
7 changes: 7 additions & 0 deletions proptest-regressions/event_preview.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 0c081b28fcee5c8c4b5439561a52585269c1cfb4d75510d9001f584fb8ac3aea # shrinks to body = "", detail = ""
12 changes: 12 additions & 0 deletions resources/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@
"forward_modal.button.forward": "Forward",
"forward_modal.popup.submitting": "Forwarding message...",
"room_screen.popup.action_response.failed": "Failed to send action response.\n\nError: {error}",
"room_screen.popup.action_response.outcome_unknown": "The send outcome is unknown. Check the canonical approval status before trying again.\n\nError: {error}",
"room_screen.send_state.resend_modal.title": "Message not sent",
"room_screen.send_state.resend_modal.body": "This message could not be sent. Resend it?",
"room_screen.send_state.resend_modal.body_reason": "This message could not be sent: {reason}. Resend it?",
Expand All @@ -694,6 +695,17 @@
"room_screen.popup.approval_expired": "This approval request has expired. Ask the agent to create a new request.",
"room_screen.approval.pending": "Pending",
"room_screen.approval.expired": "Expired",
"room_screen.approval.approved": "Approved",
"room_screen.approval.denied": "Denied",
"room_screen.approval.consumed": "Consumed",
"room_screen.approval.unavailable": "Unavailable",
"room_screen.approval.submitting": "Submitting…",
"room_screen.approval.sent_awaiting_canonical": "Sent — awaiting status",
"room_screen.approval.outcome_unknown": "Send outcome unknown",
"room_screen.approval.task_result_unknown": "Approval consumption does not confirm that the task completed.",
"room_screen.approval.view_pending": "View pending approvals",
"room_screen.approval.navigation_unavailable": "The approval room is not available yet. Wait for room sync, then try again.",
"room_screen.approval.navigation_ambiguous": "More than one approval room matches this agent. Ask its owner to update the room binding.",
"room_screen.modal.delete_message.title": "Delete Message",
"room_screen.modal.delete_message.body": "Are you sure you want to delete this message? This cannot be undone.",
"room_screen.modal.delete_message.accept": "Delete",
Expand Down
12 changes: 12 additions & 0 deletions resources/i18n/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,7 @@
"forward_modal.button.forward": "转发",
"forward_modal.popup.submitting": "正在转发消息...",
"room_screen.popup.action_response.failed": "发送动作响应失败。\n\n错误:{error}",
"room_screen.popup.action_response.outcome_unknown": "发送结果未知。再次尝试前,请先检查 canonical 审批状态。\n\n错误:{error}",
"room_screen.send_state.resend_modal.title": "消息未发送",
"room_screen.send_state.resend_modal.body": "这条消息未能发出,是否重新发送?",
"room_screen.send_state.resend_modal.body_reason": "这条消息未能发出:{reason}。是否重新发送?",
Expand All @@ -694,6 +695,17 @@
"room_screen.popup.approval_expired": "该审批请求已过期,请让 agent 重新发起请求。",
"room_screen.approval.pending": "等待审批",
"room_screen.approval.expired": "已过期",
"room_screen.approval.approved": "已批准",
"room_screen.approval.denied": "已拒绝",
"room_screen.approval.consumed": "已消费",
"room_screen.approval.unavailable": "不可用",
"room_screen.approval.submitting": "正在提交…",
"room_screen.approval.sent_awaiting_canonical": "已发送,等待状态更新",
"room_screen.approval.outcome_unknown": "发送结果未知",
"room_screen.approval.task_result_unknown": "审批已消费不代表任务已经完成。",
"room_screen.approval.view_pending": "查看待审批",
"room_screen.approval.navigation_unavailable": "审批房间暂不可用。请等待房间同步后重试。",
"room_screen.approval.navigation_ambiguous": "此 Agent 对应多个审批房间,请联系所有者更新房间绑定。",
"room_screen.modal.delete_message.title": "删除消息",
"room_screen.modal.delete_message.body": "确认要删除这条消息吗?此操作无法撤销。",
"room_screen.modal.delete_message.accept": "删除",
Expand Down
41 changes: 11 additions & 30 deletions scripts/spec-guard.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
# `manual_test_*` selectors, which agent-spec reports as `skip`, and the
# boundary layer applies the whole change set to every spec, so a naive
# `agent-spec guard` fails on every PR. Instead:
# - specs changed in this change set are the active contracts: run
# `lifecycle` WITH the change set (boundaries + tests), fail on failed>0;
# - a single changed contract receives the full change set;
# - multiple active contracts collectively cover every changed path,
# then each runs `lifecycle` with its owned paths (boundaries + tests).
# Shared files go to every declaring task; explicit denies still fail;
# - all other specs are regression checks: run `verify` WITHOUT a change
# set (tests only), fail on failed>0. Skips are tolerated.
# set (tests only), fail on failed>0. Skips and legacy regression
# uncertainty remain nonblocking, explicitly reported as UNVERIFIED.
#
# Usage:
# scripts/spec-guard.sh [--change-scope staged|worktree] [--base <git-ref>] [--fast]
Expand Down Expand Up @@ -101,39 +104,17 @@ for cap in specs/capabilities/*.spec.md; do
done

# ---- 4. task specs ---------------------------------------------------------------
# 4a. Changed specs are the active contracts of this change: verify them WITH the
# change set (boundaries + bound tests). Manual `skip`s are tolerated; any
# `failed > 0` (test or boundary violation) fails the gate.
# 4a. Changed specs own the change collectively, not by intersecting unrelated
# allow-lists. The helper checks total path coverage and explicit denies,
# then runs the native lifecycle boundary verifier and tests for each scope.
# Manual skips stay visible; failed/uncertain active contracts fail the gate.
# 4b. Every other spec is a regression check: verify WITHOUT a change set (no
# boundary layer, since foreign files would trivially violate them) and fail
# only on `failed > 0`.
say "4/4 task specs: changed = contract check (with change set), others = regression (tests only)"
CHANGE_ARGS=()
for f in "${CHANGED[@]:-}"; do [ -n "$f" ] && [ -e "$f" ] && CHANGE_ARGS+=(--change "$f"); done
summary_failed() { grep -Eo '"failed": [0-9]+' | head -1 | grep -Eo '[0-9]+'; }
is_changed_spec() { local x; for x in "${CHANGED_SPECS[@]:-}"; do [ "$x" = "$1" ] && return 0; done; return 1; }
for spec in specs/*.spec.md; do
case "$spec" in specs/project.spec.md) continue;; esac
if is_changed_spec "$spec"; then
out="$(agent-spec lifecycle "$spec" --code . --run-log-dir .agent-spec/runs "${CHANGE_ARGS[@]}" --format json 2>&1 || true)"
n="$(echo "$out" | summary_failed || echo "?")"
if [ "$n" != "0" ]; then
echo "FAIL (contract): $spec — failed=$n"
echo "$out" | grep -E '"reason": "not covered|"verdict": "fail"' -B3 | grep -E 'reason|step_text|scenario_name' | head -12
fail=1
else
echo "ok (contract): $spec"
fi
else
out="$(agent-spec verify "$spec" --code . --format json 2>&1 || true)"
n="$(echo "$out" | summary_failed || echo "?")"
if [ "$n" != "0" ]; then
echo "FAIL (regression): $spec — failed=$n"
echo "$out" | grep -E '"verdict": "fail"' -B8 | grep -E 'scenario_name|reason' | head -8
fail=1
fi
fi
done
if ! python3 scripts/spec_guard.py "${CHANGE_ARGS[@]}"; then fail=1; fi

if [ "$fail" = "0" ]; then echo; echo "spec-guard: OK"; else echo; echo "spec-guard: FAILED"; fi
exit "$fail"
149 changes: 149 additions & 0 deletions scripts/spec_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
"""Task composition for spec-guard.sh. Native agent-spec still verifies each scope."""
import argparse
from functools import lru_cache
import json
from pathlib import Path
import re
import subprocess
import sys


def path_pattern(text):
# Match the path forms recognized by agent-spec; prose is not an allow-list.
text = text.strip()
if not (any(c in text for c in '/\\*') or text.endswith(('.rs', '.ts', '.js', '.py', '.md', '.spec'))):
return None
text = text.strip('`').replace('\\', '/')
while text.startswith('./'):
text = text[2:]
return text.strip('/')


def matches(pattern, path):
pattern = tuple(p for p in pattern.split('/') if p)
path = tuple(p for p in path.split('/') if p)

@lru_cache(None)
def visit(i, j):
if i == len(pattern):
return j == len(path)
if pattern[i] == '**':
return visit(i + 1, j) or (j < len(path) and visit(i, j + 1))
return (j < len(path)
and re.fullmatch(re.escape(pattern[i]).replace(r'\*', '.*'), path[j]) is not None
and visit(i + 1, j + 1))

return visit(0, 0)


def partition_changes(changes, documents):
"""All owners receive shared files; unowned paths and explicit denies fail."""
scopes = {spec: [] for spec in documents}
rules = {}
for spec, document in documents.items():
allow, deny = [], []
for section in document['sections']:
if section['kind'] != 'boundaries':
continue
for item in section['items']:
pattern = path_pattern(item['text'])
if pattern and item['category'] in ('allow', 'deny', 'general'):
(allow if item['category'] == 'allow' else deny).append(pattern)
rules[spec] = (allow, deny)
errors = []
for change in changes:
owned = False
for spec, (allow, deny) in rules.items():
if any(matches(pattern, change) for pattern in deny):
errors.append(f'{change}: forbidden by {spec}')
if any(matches(pattern, change) for pattern in allow):
scopes[spec].append(change)
owned = True
if not owned:
errors.append(f'{change}: not covered by any active task boundary')
if errors:
raise ValueError('\n'.join(errors))
return scopes


def cli_json(arguments):
result = subprocess.run(['agent-spec', *arguments], text=True, capture_output=True)
try:
document = json.loads(result.stdout)
except (ValueError, TypeError) as error:
raise ValueError(f'invalid agent-spec JSON (exit {result.returncode}): '
f'{(result.stderr or result.stdout)[:1200]}') from error
if not isinstance(document, dict):
raise ValueError('agent-spec report must be an object')
return result.returncode, document


def check_report(code, document, regression=False):
report = document.get('verification', document)
summary = report['summary']
counts = {key: summary[key] for key in ('total', 'passed', 'failed', 'skipped', 'uncertain')}
if any(type(value) is not int or value < 0 for value in counts.values()):
raise ValueError('invalid verification counts')
# Preserve the existing nonblocking uncertainty policy for unchanged tasks.
# Those results remain UNVERIFIED; active contracts cannot use this policy.
permitted_uncertain = regression and counts['uncertain'] > 0
unverified = counts['skipped'] > 0 or counts['uncertain'] > 0
ok = (code in (0, 1) and counts['total'] > 0 and counts['failed'] == 0
and (counts['uncertain'] == 0 or permitted_uncertain)
and (code == 0 or counts['skipped'] > 0 or permitted_uncertain))
detail = ', '.join(f'{key}={counts[key]}' for key in ('passed', 'failed', 'skipped', 'uncertain'))
if not ok or counts['uncertain'] > 0:
# Slice parsed diagnostics, never a live stdout pipe. Later tasks still run.
failures = [r for r in report.get('results', []) if r.get('verdict') in ('fail', 'uncertain')]
for result in failures[:12]:
print(' ' + result.get('scenario_name', '<unnamed>'))
for step in result.get('step_results', [])[:3]:
if step.get('verdict') in ('fail', 'uncertain'):
print(f" {step.get('step_text', '')}: {step.get('reason', '')}"[:1200])
return ok, detail, unverified


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--change', action='append', default=[])
args = parser.parse_args()
changes = list(dict.fromkeys(args.change))
specs = sorted(str(p) for p in Path('specs').glob('*.spec.md') if p.name != 'project.spec.md')
active = [spec for spec in specs if spec in changes]
scopes = {spec: changes for spec in active}
if len(active) > 1:
try:
documents = {}
for spec in active:
code, document = cli_json(['parse', spec, '--format', 'json'])
if code != 0:
raise ValueError(f'could not parse {spec}: exit {code}')
documents[spec] = document
scopes = partition_changes(changes, documents)
except (ValueError, KeyError, TypeError) as error:
print(f'FAIL (composition): {error}', file=sys.stderr)
return 1
failed = False
for spec in specs:
changed = spec in scopes
mode = 'contract' if changed else 'regression'
arguments = ['lifecycle' if changed else 'verify', spec, '--code', '.', '--format', 'json']
if changed:
arguments += ['--run-log-dir', '.agent-spec/runs']
for change in scopes[spec]:
arguments += ['--change', change]
try:
code, document = cli_json(arguments)
ok, detail, unverified = check_report(code, document, regression=not changed)
except (ValueError, KeyError, TypeError) as error:
ok, detail, unverified = False, str(error), False
status = ('UNVERIFIED' if unverified else 'ok') if ok else 'FAIL'
print(f'{status} ({mode}): {spec} — {detail}', flush=True)
if ok and unverified:
print(' Nonblocking under existing CI policy; skipped/uncertain scenarios are not passes.', flush=True)
failed |= not ok
return int(failed)


if __name__ == '__main__':
sys.exit(main())
Loading
Loading