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
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
},
"metadata": {
"description": "Structured agent-user communication: validated forms, decision cards with recommendations, structured pushback, and progress reports \u2014 batch questions instead of asking one at a time.",
"version": "0.8.0"
"version": "0.9.0"
},
"plugins": [
{
"name": "attune-forms",
"description": "The communication grammar for AI agents: batch independent questions into ONE validated form; offer recommendations as decision cards with rationales and per-option tradeoffs; disagree constructively via pushback cards; report multi-step progress with a blocked-item picker. Renders rich HTML where the host supports widgets and degrades cleanly to plain questions everywhere else. Powered by the attune-forms PyPI package via a bundled MCP server.",
"source": "./plugin",
"version": "0.8.0",
"version": "0.9.0",
"author": {
"name": "Smart AI Memory",
"email": "admin@smartaimemory.com"
Expand Down
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,41 @@ follow [SemVer](https://semver.org/).

## [Unreleased]

## [0.9.0] — 2026-08-29

### Added
- **Fix-first command workspace grammar** — four portable state views
(`intake`, `preview`, `execution`, `receipt`) compose the existing
validated `FormSchema` with a closed display-block vocabulary and stable,
host-dispatched actions. Widget and Markdown renderers preserve the same
view/action return contract without accepting executable callbacks or
arbitrary HTML.
- **Provider-neutral semantic token artifact** — versioned light/dark color
roles, typography, spacing, radius, motion, and control targets are loaded
from packaged JSON and exposed as a recursively immutable mapping. Shared
form CSS and separately-budgeted workspace CSS project from that source.
- **Workspace showcase and hostile-boundary receipts** — all four views,
every form construct, and every display block are exercised. Tests parse
emitted action JavaScript with Node, reject script-context action values,
calculate WCAG AA dark-action contrast, pin explicit confirmation parity,
and enforce independent form/workspace CSS budgets.

### Changed
- Form widget and portable Markdown renderers accept optional stable action
and view context, action-specific labels, workspace-owned titles, and
explicit-action consequences while retaining their existing defaults for
standalone callers.

### Fixed
- Display-action widgets now emit valid JavaScript, disable actions after
dispatch, announce success through a live region, and send the same fenced
sentinel grammar as form-backed views.
- Dark workspace tokens retain host-variable fallbacks, embedded forms inherit
the workspace profile, and primary-action foregrounds meet WCAG AA.
- Runtime enum guards, Markdown structural escaping, code-language validation,
evidence-table scopes, stable instance ids, and recursive token freezing
close the contract and accessibility gaps found by cross-review and the
three-seat release-readiness roundtable.
- **Multi-line item `detail` kept its shape on both rendering surfaces**
(round table `q-forms-hunk-review-001`, 2026-08-28). A `detail`
carrying more than one line — a diff hunk, a log excerpt — was
Expand Down
2 changes: 1 addition & 1 deletion plugin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "attune-forms",
"version": "0.8.0",
"version": "0.9.0",
"description": "Structured agent-user communication \u2014 validated forms, decision cards, pushback, progress reports, deliberation, triage boards, confirm gates, rankings, and assumption reviews via the attune-forms MCP server.",
"author": {
"name": "Smart AI Memory",
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "attune-forms"
version = "0.8.0"
version = "0.9.0"
description = "Dynamic forms library: declarative FormSchema, multi-surface renderers (widget HTML, AskUserQuestion, MCP elicitation), and template-driven intake generation"
readme = "README.md"
requires-python = ">=3.10"
Expand Down Expand Up @@ -45,7 +45,7 @@ Repository = "https://github.com/Smart-AI-Memory/attune-forms"
where = ["src"]

[tool.setuptools.package-data]
attune_forms = ["templates/*.json"]
attune_forms = ["templates/*.json", "semantic_tokens.json"]

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
27 changes: 27 additions & 0 deletions src/attune_forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,21 @@
)
from attune_forms.reference_form import EXAMPLE_ANSWERS, REFERENCE_FORM
from attune_forms.template_store import form_from_template, list_templates
from attune_forms.tokens import SEMANTIC_TOKENS, token
from attune_forms.widget import WIDGET_RESPONSE_MARKER, form_to_widget_html
from attune_forms.workspace import (
WorkspaceAction,
WorkspaceActionIntent,
WorkspaceBlock,
WorkspaceBlockKind,
WorkspaceItem,
WorkspaceSection,
WorkspaceTone,
WorkspaceView,
WorkspaceViewId,
workspace_to_markdown,
workspace_to_widget_html,
)

__all__ = [
"EXAMPLE_ANSWERS",
Expand All @@ -103,6 +117,7 @@
"validate_template",
"REFERENCE_FORM",
"WIDGET_RESPONSE_MARKER",
"SEMANTIC_TOKENS",
"FormValidationError",
"collect_form_response",
"form_from_dict",
Expand All @@ -122,6 +137,18 @@
"problems_to_markdown",
"select_form_surface",
"set_keyboard_mode",
"token",
"WorkspaceAction",
"WorkspaceActionIntent",
"WorkspaceBlock",
"WorkspaceBlockKind",
"WorkspaceItem",
"WorkspaceSection",
"WorkspaceTone",
"WorkspaceView",
"WorkspaceViewId",
"workspace_to_markdown",
"workspace_to_widget_html",
"ASSUMPTION_RULINGS",
"ranking_slot_count",
"triage_item_key",
Expand Down
40 changes: 32 additions & 8 deletions src/attune_forms/markdown_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,12 @@ def _skeleton_value(q: FormQuestion) -> Any:
return q.recommended if q.recommended else None


def reply_skeleton(form: FormSchema, questions: list[FormQuestion] | None = None) -> dict[str, Any]:
def reply_skeleton(
form: FormSchema,
questions: list[FormQuestion] | None = None,
action: str | None = None,
view: str | None = None,
) -> dict[str, Any]:
"""The sentinel-marked reply skeleton for a form (or a subset of it).

``questions`` restricts the ``answers`` map to those fields — the
Expand All @@ -333,14 +338,26 @@ def reply_skeleton(form: FormSchema, questions: list[FormQuestion] | None = None
ingested as a reply (confirmation pass 2, 2026-08-20).
"""
chosen = questions if questions is not None else form.questions
return {
payload = {
WIDGET_RESPONSE_MARKER: True,
"title": form.title,
"answers": {q.id: _skeleton_value(q) for q in chosen},
}


def form_to_markdown(form: FormSchema, message: str = "") -> str:
if action is not None:
payload["action"] = action
if view is not None:
payload["view"] = view
return payload


def form_to_markdown(
form: FormSchema,
message: str = "",
action: str | None = None,
submit_label: str | None = None,
include_title: bool = True,
view: str | None = None,
) -> str:
"""Render a declarative form as portable markdown (S4).

For hosts that render neither HTML widgets nor a question tool: the
Expand All @@ -354,11 +371,18 @@ def form_to_markdown(form: FormSchema, message: str = "") -> str:
form: The validated form to render (build it with
:func:`form_from_dict` first).
message: Optional prompt shown above the form.
action: Optional stable host action id added to the answer
skeleton.
submit_label: Optional action-specific instruction label.
include_title: Whether to emit the form's level-two heading.
Workspace renderers disable it because their shell already
owns the view heading.
view: Optional workspace view id added to the answer skeleton.

Returns:
A markdown string ready to relay to any text host.
"""
lines = [f"## {form.title}"]
lines = [f"## {form.title}"] if include_title else []
if message:
lines += ["", message]
if form.description:
Expand All @@ -373,13 +397,13 @@ def form_to_markdown(form: FormSchema, message: str = "") -> str:
lines += [
"",
"---",
"Reply by filling the `answers` values below, or with shorthand "
f"{submit_label or 'Reply'} by filling the `answers` values below, or with shorthand "
"lines — `field_id: value` or `N: value` (field number); a triage "
"row is `field_id.item_id: disposition`; a ranking is a comma list "
"in order (`field_id: b, a, c`) or one slot per line "
"(`field_id.1: b`); an assumption row is `field_id.item_id: accept`, "
"`field_id.item_id: reject`, or `field_id.item_id: edit: <text>`:",
"",
*_skeleton_block(reply_skeleton(form)),
*_skeleton_block(reply_skeleton(form, action=action, view=view)),
]
return "\n".join(lines)
55 changes: 55 additions & 0 deletions src/attune_forms/semantic_tokens.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"version": 1,
"color": {
"light": {
"action": "#004ac6",
"action_hover": "#003ea8",
"success": "#006c49",
"warning": "#a1571c",
"danger": "#ba1a1a",
"recommendation": "#7c3aed",
"neutral_text": "#0b1c30",
"neutral_muted": "#5f6470",
"surface": "#f8f9ff",
"surface_raised": "#eff4ff",
"border": "#c3c6d7",
"focus": "#2563eb"
},
"dark": {
"action": "#8db2ff",
"action_hover": "#b4c9ff",
"success": "#4edea3",
"warning": "#ffb77d",
"danger": "#ffb4ab",
"recommendation": "#c5b4ff",
"neutral_text": "#f8f9ff",
"neutral_muted": "#b7c8e1",
"surface": "#0b1c30",
"surface_raised": "#1a2d42",
"border": "#38485d",
"focus": "#b4c9ff"
}
},
"radius": {
"control": "8px",
"panel": "12px"
},
"spacing": {
"xs": "0.25rem",
"sm": "0.5rem",
"md": "1rem",
"lg": "1.5rem"
},
"motion": {
"fast": "120ms",
"normal": "200ms"
},
"control": {
"minimum_target": "2.5rem"
},
"typography": {
"body": "Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",
"heading": "Manrope, Inter, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",
"mono": "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
}
}
Loading
Loading