agent-workflow-ui 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_workflow_ui-1.0.0/PKG-INFO +64 -0
- agent_workflow_ui-1.0.0/README.md +37 -0
- agent_workflow_ui-1.0.0/pyproject.toml +69 -0
- agent_workflow_ui-1.0.0/setup.cfg +4 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/SKILL.md +258 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/__init__.py +2 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/__main__.py +64 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/browser.py +70 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/config.py +74 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/http_endpoint.py +412 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/opencode_config.py +475 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/render/__init__.py +2 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/render/default_templates/ack.html.j2 +39 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/render/default_templates/increment-planning.html.j2 +254 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/render/default_templates/project-setup.html.j2 +851 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/render/engine.py +71 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/render/frontmatter.py +56 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/roles_processor.py +237 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/server.py +66 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/skill_installer.py +72 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/state.py +318 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/tools/__init__.py +1 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/tools/awf.py +1112 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/tools/forms.py +440 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui/tools/templates.py +57 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui.egg-info/PKG-INFO +64 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui.egg-info/SOURCES.txt +29 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui.egg-info/dependency_links.txt +1 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui.egg-info/entry_points.txt +2 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui.egg-info/requires.txt +8 -0
- agent_workflow_ui-1.0.0/src/agent_workflow_ui.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-workflow-ui
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: MCP plugin that gives opencode agents tools for visual interaction (HTML forms, dashboards) with users
|
|
5
|
+
Author: EnerJize
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/EnerJizeIT/agentic-workflow
|
|
8
|
+
Project-URL: Repository, https://github.com/EnerJizeIT/agentic-workflow
|
|
9
|
+
Project-URL: Issues, https://github.com/EnerJizeIT/agentic-workflow/issues
|
|
10
|
+
Keywords: opencode,mcp,ai-agent,pipeline,dashboard,forms
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
Requires-Dist: mcp>=1.0
|
|
21
|
+
Requires-Dist: jinja2>=3.1
|
|
22
|
+
Requires-Dist: pyyaml>=6.0
|
|
23
|
+
Requires-Dist: awf>=1.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
27
|
+
|
|
28
|
+
# agent-workflow-ui
|
|
29
|
+
|
|
30
|
+
MCP plugin для opencode. 29 typed tools (5 UI + 24 workflow).
|
|
31
|
+
|
|
32
|
+
## Установка
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install -e ".[dev]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Plugin автоматически регистрируется в opencode через `awf init`.
|
|
39
|
+
|
|
40
|
+
## Tools
|
|
41
|
+
|
|
42
|
+
### UI (5)
|
|
43
|
+
`open_form`, `read_submit`, `cancel_form`, `list_pending_forms`, `list_templates`
|
|
44
|
+
|
|
45
|
+
### Workflow (24)
|
|
46
|
+
`awf_init`, `awf_status`, `awf_start`, `awf_continue`, `awf_kill`, `awf_retry_stage`, `awf_baseline`, `awf_rollback`, `awf_approve`, `awf_reject`, `awf_report`, `awf_reset`, `awf_add_role`, `awf_analyze_roles`, `awf_dispatch_todo`, `awf_load_supervisor_context`, `awf_open_project_setup_form`, `awf_open_increment_planning_form`, `awf_open_pipeline_dashboard`, `awf_wait_for_event`, `awf_check_model_config`, `awf_current_step`, `awf_set_goal`, `awf_confirm_normalized`
|
|
47
|
+
|
|
48
|
+
Все workflow tools возвращают `next_action` — guide для supervisor.
|
|
49
|
+
|
|
50
|
+
## Структура
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
src/agent_workflow_ui/
|
|
54
|
+
├── server.py # MCP server, 29 tools
|
|
55
|
+
├── tools/awf.py # 24 awf wrappers (asyncio.to_thread + next_action)
|
|
56
|
+
├── tools/forms.py # 5 UI wrappers
|
|
57
|
+
├── http_endpoint.py # Form HTTP server
|
|
58
|
+
├── opencode_config.py # Model discovery
|
|
59
|
+
├── roles_processor.py # Role setup materialization
|
|
60
|
+
├── state.py # Form state persistence
|
|
61
|
+
└── render/ # Jinja2 templates
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Все workflow tools — thin async wrappers над `awf.api.*()`. Бизнес-логика в `awf`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# agent-workflow-ui
|
|
2
|
+
|
|
3
|
+
MCP plugin для opencode. 29 typed tools (5 UI + 24 workflow).
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Plugin автоматически регистрируется в opencode через `awf init`.
|
|
12
|
+
|
|
13
|
+
## Tools
|
|
14
|
+
|
|
15
|
+
### UI (5)
|
|
16
|
+
`open_form`, `read_submit`, `cancel_form`, `list_pending_forms`, `list_templates`
|
|
17
|
+
|
|
18
|
+
### Workflow (24)
|
|
19
|
+
`awf_init`, `awf_status`, `awf_start`, `awf_continue`, `awf_kill`, `awf_retry_stage`, `awf_baseline`, `awf_rollback`, `awf_approve`, `awf_reject`, `awf_report`, `awf_reset`, `awf_add_role`, `awf_analyze_roles`, `awf_dispatch_todo`, `awf_load_supervisor_context`, `awf_open_project_setup_form`, `awf_open_increment_planning_form`, `awf_open_pipeline_dashboard`, `awf_wait_for_event`, `awf_check_model_config`, `awf_current_step`, `awf_set_goal`, `awf_confirm_normalized`
|
|
20
|
+
|
|
21
|
+
Все workflow tools возвращают `next_action` — guide для supervisor.
|
|
22
|
+
|
|
23
|
+
## Структура
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
src/agent_workflow_ui/
|
|
27
|
+
├── server.py # MCP server, 29 tools
|
|
28
|
+
├── tools/awf.py # 24 awf wrappers (asyncio.to_thread + next_action)
|
|
29
|
+
├── tools/forms.py # 5 UI wrappers
|
|
30
|
+
├── http_endpoint.py # Form HTTP server
|
|
31
|
+
├── opencode_config.py # Model discovery
|
|
32
|
+
├── roles_processor.py # Role setup materialization
|
|
33
|
+
├── state.py # Form state persistence
|
|
34
|
+
└── render/ # Jinja2 templates
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Все workflow tools — thin async wrappers над `awf.api.*()`. Бизнес-логика в `awf`.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-workflow-ui"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "MCP plugin that gives opencode agents tools for visual interaction (HTML forms, dashboards) with users"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "EnerJize" }]
|
|
13
|
+
keywords = ["opencode", "mcp", "ai-agent", "pipeline", "dashboard", "forms"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"mcp>=1.0",
|
|
25
|
+
"jinja2>=3.1",
|
|
26
|
+
"pyyaml>=6.0",
|
|
27
|
+
"awf>=1.0.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/EnerJizeIT/agentic-workflow"
|
|
32
|
+
Repository = "https://github.com/EnerJizeIT/agentic-workflow"
|
|
33
|
+
Issues = "https://github.com/EnerJizeIT/agentic-workflow/issues"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=7.0",
|
|
38
|
+
"pytest-cov>=4.0",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
agent-workflow-ui = "agent_workflow_ui.__main__:main"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
include = ["agent_workflow_ui*"]
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
agent_workflow_ui = ["render/default_templates/*.j2", "SKILL.md"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
line-length = 100
|
|
53
|
+
target-version = "py310"
|
|
54
|
+
|
|
55
|
+
[tool.ruff.lint]
|
|
56
|
+
# F: pyflakes (real bugs)
|
|
57
|
+
# E/W: pycodestyle (PEP 8)
|
|
58
|
+
# I: isort (import sorting)
|
|
59
|
+
# UP: pyupgrade (modernize syntax)
|
|
60
|
+
select = ["F", "E", "W", "I", "UP"]
|
|
61
|
+
ignore = [
|
|
62
|
+
"E501", # line too long — handled by formatter, not linter
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint.per-file-ignores]
|
|
66
|
+
"tests/**" = [
|
|
67
|
+
"F841", # unused local variables (test fixtures often have them)
|
|
68
|
+
"E501",
|
|
69
|
+
]
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# Agent Workflow UI
|
|
2
|
+
|
|
3
|
+
MCP plugin for [opencode](https://opencode.ai/) that gives agents tools for visual
|
|
4
|
+
interaction with users: HTML forms for structured input, dashboards for monitoring.
|
|
5
|
+
|
|
6
|
+
Use forms when chat is inefficient for the task at hand. Forms are a supplement
|
|
7
|
+
to chat, not a replacement.
|
|
8
|
+
|
|
9
|
+
## When to use a form
|
|
10
|
+
|
|
11
|
+
**For project setup** (configuring pipeline + roles + models + supervisor):
|
|
12
|
+
use `awf_open_project_setup_form(project_dir)` — dedicated shortcut tool.
|
|
13
|
+
Plugin auto-populates roles/models/skills. Do NOT call `open_form`
|
|
14
|
+
directly for project setup, do NOT study template structure.
|
|
15
|
+
|
|
16
|
+
**For increment planning** (choosing decomposition strategy):
|
|
17
|
+
use `awf_open_increment_planning_form(variants, project_dir)` — user
|
|
18
|
+
picks from supervisor-generated variants.
|
|
19
|
+
|
|
20
|
+
**For pipeline monitoring** (watching agents work):
|
|
21
|
+
use `awf_open_pipeline_dashboard(project_dir)` — live dashboard with
|
|
22
|
+
auto-refresh 5s. Ask user before opening.
|
|
23
|
+
|
|
24
|
+
**For supervisor wake-up** (no more polling):
|
|
25
|
+
use `awf_wait_for_event(project_dir, timeout=120)` — ONE call blocks
|
|
26
|
+
until verify/blocked/checkpoint/done. Replaces sleep+status loops.
|
|
27
|
+
|
|
28
|
+
Call `open_form` directly when:
|
|
29
|
+
|
|
30
|
+
- **A file upload** is needed (ТЗ, product-vision, custom role `.md`).
|
|
31
|
+
- **Ad-hoc decision forks** with custom template you created.
|
|
32
|
+
- Monitoring a **long-running pipeline** (→ dashboard, see above).
|
|
33
|
+
|
|
34
|
+
## When NOT to use a form (use chat instead)
|
|
35
|
+
|
|
36
|
+
- A simple **Y/N** answer.
|
|
37
|
+
- Choosing between **2-3 short options**.
|
|
38
|
+
- **Clarifying questions** during work.
|
|
39
|
+
- **Tone calibration** or approach discussion.
|
|
40
|
+
- Anything that can be answered in one line of text.
|
|
41
|
+
|
|
42
|
+
## Usage pattern — NON-BLOCKING
|
|
43
|
+
|
|
44
|
+
**Always use `open_form` (non-blocking). Never use `open_form_and_wait` (it is not registered as MCP tool anyway).**
|
|
45
|
+
|
|
46
|
+
1. Agent decides a form is needed.
|
|
47
|
+
2. Call `open_form(template="...", data={...})` — opens form, returns `form_id` immediately.
|
|
48
|
+
3. Tell the user in CLI: **"I opened a form in your browser. Fill it out, click Submit, then tell me here when you're done."**
|
|
49
|
+
4. Agent is now **free** — can continue other work, answer questions, or wait for the user.
|
|
50
|
+
5. When the user types something in CLI (e.g., "done", "submitted", "I filled it"):
|
|
51
|
+
- Call `read_submit(form_id)` to retrieve the submitted data.
|
|
52
|
+
- Analyze `data` and proceed.
|
|
53
|
+
|
|
54
|
+
**Why non-blocking?**
|
|
55
|
+
|
|
56
|
+
The agent stays responsive. The user controls the pace — they fill the form
|
|
57
|
+
when ready, then explicitly tell the agent to proceed. This avoids the agent
|
|
58
|
+
"hanging" silently while the user is still thinking.
|
|
59
|
+
|
|
60
|
+
**Critical:** You MUST tell the user to come back to CLI and notify you after
|
|
61
|
+
submitting. The form's "Submitted!" page also reminds them, but you should
|
|
62
|
+
say it in your message too.
|
|
63
|
+
|
|
64
|
+
### What to tell the user
|
|
65
|
+
|
|
66
|
+
After calling `open_form`, ALWAYS say something like:
|
|
67
|
+
|
|
68
|
+
> "I've opened a form in your browser (Form ID: FORM-...).
|
|
69
|
+
> Please fill it out and click Submit.
|
|
70
|
+
> When done, come back here and tell me — I'll pick up your answers."
|
|
71
|
+
|
|
72
|
+
### When the user says "done" / "submitted" / "filled it"
|
|
73
|
+
|
|
74
|
+
Call `read_submit(form_id)` immediately to get the data. Don't make the user
|
|
75
|
+
repeat themselves. If `read_submit` returns `submitted: false`, tell the user
|
|
76
|
+
the form hasn't been submitted yet and ask them to check.
|
|
77
|
+
|
|
78
|
+
## Tool reference
|
|
79
|
+
|
|
80
|
+
### `open_form(template, data, project_dir?, ttl_seconds?)` — PRIMARY TOOL
|
|
81
|
+
|
|
82
|
+
Render and open an HTML form in the user's browser. Non-blocking — returns immediately.
|
|
83
|
+
|
|
84
|
+
**Arguments:**
|
|
85
|
+
- `template` (str, required) — Template name without extension (e.g., "project-setup").
|
|
86
|
+
- `data` (dict, optional) — Variables to render in the template.
|
|
87
|
+
- `project_dir` (str, optional but **required for project-setup**) — Absolute path to awf project root. When provided, submit triggers automatic materialization via `awf.api.apply_project_setup` (writes pipeline.yaml, patches config.yaml + supervisor.md). Without it, submit is saved to `inputs/` only — agent must materialize manually. MCP subprocess runs in $HOME, so cwd-based detection does NOT work — pass explicitly.
|
|
88
|
+
- `ttl_seconds` (int, optional) — Auto-expire form after N seconds.
|
|
89
|
+
|
|
90
|
+
**Returns:** `{form_id, browser_opened, submit_url, expires_at?, error?}`
|
|
91
|
+
|
|
92
|
+
**MUST for project-setup:** Always pass `project_dir` explicitly:
|
|
93
|
+
```python
|
|
94
|
+
open_form(template="project-setup", data={...}, project_dir="/abs/path/to/project")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `read_submit(form_id)` — USE WHEN USER SAYS "DONE"
|
|
98
|
+
|
|
99
|
+
Check if user submitted the form. Non-blocking — returns immediately.
|
|
100
|
+
|
|
101
|
+
**Returns:** `{submitted, form_id, status, data?, submitted_at?, error?}`
|
|
102
|
+
|
|
103
|
+
**Status values:** `pending`, `submitted`, `cancelled`, `expired`, `unknown`, `error`
|
|
104
|
+
|
|
105
|
+
### `cancel_form(form_id)`
|
|
106
|
+
|
|
107
|
+
Cancel a pending form. Future HTTP submits for this form_id are ignored.
|
|
108
|
+
|
|
109
|
+
**Returns:** `{cancelled, form_id, reason?}`
|
|
110
|
+
|
|
111
|
+
### `list_pending_forms()`
|
|
112
|
+
|
|
113
|
+
List forms opened but not yet submitted, cancelled, or expired.
|
|
114
|
+
|
|
115
|
+
**Returns:** `{pending: [{form_id, template, opened_at, age_seconds}], count}`
|
|
116
|
+
|
|
117
|
+
### `list_templates()`
|
|
118
|
+
|
|
119
|
+
List available form templates with metadata.
|
|
120
|
+
|
|
121
|
+
**Returns:** `{templates: [{name, source, description, required_data_keys, optional_data_keys}]}`
|
|
122
|
+
|
|
123
|
+
## Available templates
|
|
124
|
+
|
|
125
|
+
Call `list_templates` to see what's available at runtime.
|
|
126
|
+
|
|
127
|
+
### Primary (MVP)
|
|
128
|
+
|
|
129
|
+
| Template | Description | Required data keys |
|
|
130
|
+
|---|---|---|
|
|
131
|
+
| `project-setup` | Composite form: context + ТЗ files + supervisor + team (with models). Models pulled from opencode automatically. | `available_roles` |
|
|
132
|
+
|
|
133
|
+
Plugin injects automatically into every template:
|
|
134
|
+
- `form_id`, `submit_url`, `template_name`
|
|
135
|
+
- `available_models`, `recent_models` (from opencode CLI + SQLite history)
|
|
136
|
+
- `custom_supervisor_roles`, `custom_agents` (from `~/.config/awf/roles/`)
|
|
137
|
+
- `existing_supervisor_slugs`, `existing_agent_slugs` (for client-side conflict detection)
|
|
138
|
+
|
|
139
|
+
### Future templates
|
|
140
|
+
|
|
141
|
+
Templates for scenarios 2-6 (decision forks, blockage recovery, dashboards, priority planning, onboarding wizard) will be added as those scenarios are implemented. Currently only `project-setup` ships as a default template.
|
|
142
|
+
|
|
143
|
+
**Until future scenarios are implemented:** Use `project-setup` for project configuration. Use chat for everything else.
|
|
144
|
+
|
|
145
|
+
## Creating new templates (agent-driven)
|
|
146
|
+
|
|
147
|
+
**Only the agent (you, LLM) can create new templates.** The user does not edit `.agentic/templates/` manually — if a custom form is needed, you create it.
|
|
148
|
+
|
|
149
|
+
### How to create a new template
|
|
150
|
+
|
|
151
|
+
1. **Front (HTML form):** write a Jinja2 template at `.agentic/templates/<name>.html.j2`. Structure:
|
|
152
|
+
```jinja2
|
|
153
|
+
---
|
|
154
|
+
description: What this form does
|
|
155
|
+
required_data_keys:
|
|
156
|
+
- some_required_var
|
|
157
|
+
optional_data_keys:
|
|
158
|
+
- some_optional_var
|
|
159
|
+
---
|
|
160
|
+
<!DOCTYPE html>
|
|
161
|
+
<html>
|
|
162
|
+
<head><title>...</title></head>
|
|
163
|
+
<body>
|
|
164
|
+
<form action="{{ submit_url }}" method="POST">
|
|
165
|
+
<input type="hidden" name="form_id" value="{{ form_id }}">
|
|
166
|
+
<!-- form fields here -->
|
|
167
|
+
<button type="submit">Submit</button>
|
|
168
|
+
</form>
|
|
169
|
+
</body>
|
|
170
|
+
</html>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
2. **Back (interpretation):** describe in supervisor.md (or in your response to the user) how to interpret the submit data. Plugin's HTTP endpoint saves the raw form data to `inputs/<form_id>.yaml` — you read it via `read_submit` and parse the fields yourself.
|
|
174
|
+
|
|
175
|
+
3. **Use:** call `open_form(template="<name>", data={...})`. Plugin's `ChoiceLoader` finds project templates first, then defaults — so your new template is available immediately.
|
|
176
|
+
|
|
177
|
+
### Template variables available
|
|
178
|
+
|
|
179
|
+
Every template gets these injected by plugin:
|
|
180
|
+
- `{{ form_id }}` — current form ID
|
|
181
|
+
- `{{ submit_url }}` — POST endpoint
|
|
182
|
+
- `{{ template_name }}` — current template name
|
|
183
|
+
- `{{ available_models }}`, `{{ recent_models }}` — opencode models
|
|
184
|
+
- `{{ custom_supervisor_roles }}`, `{{ custom_agents }}` — saved roles
|
|
185
|
+
- Plus any keys you pass in `data` argument
|
|
186
|
+
|
|
187
|
+
### Important rules
|
|
188
|
+
|
|
189
|
+
- Always use `<form action="{{ submit_url }}" method="POST">` — that's how submits reach the plugin.
|
|
190
|
+
- Always include `<input type="hidden" name="form_id" value="{{ form_id }}">`.
|
|
191
|
+
- Plugin uses Jinja2 with autoescape — user input is safe.
|
|
192
|
+
- YAML frontmatter is stripped from output automatically.
|
|
193
|
+
|
|
194
|
+
## Mistakes to avoid
|
|
195
|
+
|
|
196
|
+
- **DO NOT** use blocking wait tools — they are not registered. Use `open_form` + user notification pattern.
|
|
197
|
+
- **DO NOT** forget to tell the user to come back to CLI after submitting.
|
|
198
|
+
- **DO NOT** open a form for Y/N questions — use chat.
|
|
199
|
+
- **DO NOT** open more than 3 forms simultaneously — the user will be confused.
|
|
200
|
+
- **DO NOT** use forms as a chat replacement — they're a supplement.
|
|
201
|
+
- **DO NOT** invent template names — only `project-setup` ships as default. Future scenarios will add more.
|
|
202
|
+
|
|
203
|
+
## Example workflow
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
Agent: "I need to configure your project. Let me open a form."
|
|
207
|
+
|
|
208
|
+
→ open_form(template="project-setup", project_dir="/abs/path/to/project", data={"available_roles": [
|
|
209
|
+
{"id": "backend-dev", "title": "Backend Developer"},
|
|
210
|
+
{"id": "frontend-dev", "title": "Frontend Developer"},
|
|
211
|
+
{"id": "tester", "title": "Tester"}
|
|
212
|
+
]})
|
|
213
|
+
Returns: {form_id: "FORM-20260726143022-a1b2", browser_opened: true, submit_url: "..."}
|
|
214
|
+
|
|
215
|
+
Agent (to user): "I've opened project setup form in your browser.
|
|
216
|
+
Fill context, pick supervisor, build your team, click Submit.
|
|
217
|
+
When done, tell me here."
|
|
218
|
+
|
|
219
|
+
[Agent is free — can answer other questions, do other work, or wait]
|
|
220
|
+
|
|
221
|
+
User (in CLI): "Done!"
|
|
222
|
+
|
|
223
|
+
→ read_submit("FORM-20260726143022-a1b2")
|
|
224
|
+
Returns: {submitted: true, data: {
|
|
225
|
+
context_message: "...",
|
|
226
|
+
team_config: '[{"type":"default","agent":"backend-dev","model":"anthropic/claude-3.5"}]',
|
|
227
|
+
spec_files_json: '[...]',
|
|
228
|
+
...
|
|
229
|
+
}}
|
|
230
|
+
|
|
231
|
+
After submit, plugin writes:
|
|
232
|
+
- Custom roles to global + project (BD-3-B, BD-5).
|
|
233
|
+
- Pipeline stages to .agentic/pipelines/default.yaml (BD-9).
|
|
234
|
+
- Per-role models to .agentic/config.yaml (BD-32).
|
|
235
|
+
|
|
236
|
+
Next `awf start` runs the pipeline. Supervisor (interactive session or
|
|
237
|
+
subprocess in --auto mode) creates TODO from phases/plan.md, agents
|
|
238
|
+
execute, supervisor verifies.
|
|
239
|
+
|
|
240
|
+
Optional: run `awf analyze-roles` (BD-31) before `awf start` if roles
|
|
241
|
+
have overlapping zones — adds disambiguation patches to role files.
|
|
242
|
+
|
|
243
|
+
Agent (to user): "Got it. Generating .agentic/config.yaml and roles/..."
|
|
244
|
+
[Agent writes project files using its own tools — plugin does NOT generate them]
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Troubleshooting
|
|
248
|
+
|
|
249
|
+
- **Browser didn't open:** The `open_form` result may have `browser_opened: false`
|
|
250
|
+
with an error message. Tell the user the form HTML is at a local file path, or
|
|
251
|
+
offer to collect the answer via chat instead.
|
|
252
|
+
- **User can't access browser (SSH/remote):** Offer a CLI fallback — ask the user
|
|
253
|
+
to type their choice in the chat. Call `cancel_form` to clean up.
|
|
254
|
+
- **User says "done" but read_submit returns `submitted: false`:** The user may
|
|
255
|
+
have closed the form without submitting. Ask them to check and submit again.
|
|
256
|
+
- **Form expired:** If `read_submit` returns `status: "expired"`, open a new form
|
|
257
|
+
with a longer TTL or ask via chat.
|
|
258
|
+
- **Conflict on save:** If the user is saving a custom role with a name that already exists, the form will show a JS confirm dialog. If they confirm — overwrite; if cancel — they change the name or uncheck save.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Entry point: python -m agent_workflow_ui"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .config import detect_project_dir, ensure_directories, load
|
|
9
|
+
from .http_endpoint import start_http_server
|
|
10
|
+
from .opencode_config import read_opencode_models, read_recent_models
|
|
11
|
+
from .render.engine import create_env
|
|
12
|
+
from .server import create_server
|
|
13
|
+
from .skill_installer import ensure_skill_installed
|
|
14
|
+
from .state import get_registry, set_config, set_http_port, set_jinja_env, set_project_dir
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
DEFAULT_TEMPLATES_DIR = Path(__file__).parent / "render" / "default_templates"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def main() -> int:
|
|
22
|
+
"""Run the MCP server on stdio transport, with HTTP endpoint in background."""
|
|
23
|
+
logging.basicConfig(
|
|
24
|
+
level=logging.INFO,
|
|
25
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
26
|
+
stream=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# Lazy skill install: copy SKILL.md to ~/.config/opencode/skills/agent-workflow-ui/
|
|
31
|
+
# Idempotent — overwrites if bundled version differs. No-op if already current.
|
|
32
|
+
ensure_skill_installed()
|
|
33
|
+
|
|
34
|
+
config = load()
|
|
35
|
+
ensure_directories(config)
|
|
36
|
+
set_config(config)
|
|
37
|
+
set_project_dir(detect_project_dir())
|
|
38
|
+
|
|
39
|
+
env = create_env([config.templates_dir, DEFAULT_TEMPLATES_DIR])
|
|
40
|
+
env.globals["available_models"] = read_opencode_models()
|
|
41
|
+
env.globals["recent_models"] = read_recent_models()
|
|
42
|
+
set_jinja_env(env)
|
|
43
|
+
|
|
44
|
+
registry = get_registry()
|
|
45
|
+
server, port = start_http_server(config, registry)
|
|
46
|
+
set_http_port(port)
|
|
47
|
+
log.info("agent_workflow_ui started (HTTP on port %d)", port)
|
|
48
|
+
|
|
49
|
+
mcp = create_server()
|
|
50
|
+
mcp.run() # blocks; stdio transport
|
|
51
|
+
|
|
52
|
+
server.shutdown()
|
|
53
|
+
return 0
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
log.info("agent_workflow_ui interrupted by user")
|
|
56
|
+
return 130
|
|
57
|
+
except Exception:
|
|
58
|
+
log.exception("agent_workflow_ui crashed during startup/runtime")
|
|
59
|
+
print("FATAL: agent_workflow_ui failed to start", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
sys.exit(main())
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Cross-platform browser open via stdlib subprocess."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import platform
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def detect_open_command(configured: str = "auto") -> str:
|
|
11
|
+
"""Detect the platform-appropriate command to open a file in the browser.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
configured: "auto" for platform detection, or an explicit command name.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Command string: "xdg-open" (Linux), "open" (macOS), "explorer" (Windows).
|
|
18
|
+
"""
|
|
19
|
+
if configured != "auto":
|
|
20
|
+
return configured
|
|
21
|
+
|
|
22
|
+
system = platform.system()
|
|
23
|
+
if system == "Linux":
|
|
24
|
+
return "xdg-open"
|
|
25
|
+
if system == "Darwin":
|
|
26
|
+
return "open"
|
|
27
|
+
if system == "Windows":
|
|
28
|
+
return "explorer"
|
|
29
|
+
# Fallback
|
|
30
|
+
return "xdg-open"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def open_path(target: Path | str, command: str = "auto") -> tuple[bool, str]:
|
|
34
|
+
"""Open a file path or URL in the default browser.
|
|
35
|
+
|
|
36
|
+
Fire-and-forget: does not block the calling process beyond a short timeout.
|
|
37
|
+
stdout/stderr are captured so they don't interfere with MCP stdio transport.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
target: File path or URL to open.
|
|
41
|
+
command: "auto" (detect by platform), or explicit command name.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
Tuple (success, message). On success: (True, "opened via <cmd>").
|
|
45
|
+
On failure: (False, error description).
|
|
46
|
+
"""
|
|
47
|
+
cmd = detect_open_command(command)
|
|
48
|
+
|
|
49
|
+
if shutil.which(cmd) is None:
|
|
50
|
+
return False, f"Browser open command '{cmd}' not found on PATH"
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
result = subprocess.run(
|
|
54
|
+
[cmd, str(target)],
|
|
55
|
+
capture_output=True,
|
|
56
|
+
timeout=5,
|
|
57
|
+
check=False,
|
|
58
|
+
)
|
|
59
|
+
if result.returncode == 0:
|
|
60
|
+
return True, f"opened via {cmd}"
|
|
61
|
+
|
|
62
|
+
stderr = result.stderr.decode("utf-8", errors="replace").strip()
|
|
63
|
+
return (
|
|
64
|
+
False,
|
|
65
|
+
f"'{cmd}' exited with code {result.returncode}: {stderr or 'unknown error'}",
|
|
66
|
+
)
|
|
67
|
+
except subprocess.TimeoutExpired:
|
|
68
|
+
return False, f"'{cmd}' timed out after 5s"
|
|
69
|
+
except FileNotFoundError:
|
|
70
|
+
return False, f"Browser open command '{cmd}' not found"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Configuration via environment variables."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Config:
|
|
12
|
+
"""Resolved plugin configuration."""
|
|
13
|
+
inputs_dir: Path
|
|
14
|
+
templates_dir: Path
|
|
15
|
+
dashboards_dir: Path
|
|
16
|
+
http_port: int
|
|
17
|
+
open_browser_cmd: str
|
|
18
|
+
temp_dir: Path
|
|
19
|
+
default_ttl_seconds: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load() -> Config:
|
|
23
|
+
"""Load config from environment variables.
|
|
24
|
+
|
|
25
|
+
Paths are resolved against the current working directory (which is the
|
|
26
|
+
opencode project root when the plugin runs).
|
|
27
|
+
"""
|
|
28
|
+
cwd = Path.cwd()
|
|
29
|
+
|
|
30
|
+
def _path(env_var: str, default: str) -> Path:
|
|
31
|
+
value = os.environ.get(env_var, default)
|
|
32
|
+
p = Path(value)
|
|
33
|
+
if not p.is_absolute():
|
|
34
|
+
p = (cwd / p).resolve()
|
|
35
|
+
return p
|
|
36
|
+
|
|
37
|
+
return Config(
|
|
38
|
+
inputs_dir=_path("AWF_INPUTS_DIR", ".agentic/inputs"),
|
|
39
|
+
templates_dir=_path("AWF_TEMPLATES_DIR", ".agentic/templates"),
|
|
40
|
+
dashboards_dir=_path("AWF_DASHBOARDS_DIR", ".agentic/dashboards"),
|
|
41
|
+
http_port=int(os.environ.get("AWF_HTTP_PORT", "0")),
|
|
42
|
+
open_browser_cmd=os.environ.get("AWF_OPEN_BROWSER_CMD", "auto"),
|
|
43
|
+
temp_dir=_path("AWF_TEMP_DIR", tempfile.gettempdir()),
|
|
44
|
+
default_ttl_seconds=int(os.environ.get("AWF_DEFAULT_TTL_SECONDS", "86400")),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def detect_project_dir() -> Path | None:
|
|
49
|
+
"""Detect active awf project directory.
|
|
50
|
+
|
|
51
|
+
Returns Path.cwd() if it contains .agentic/ directory, None otherwise.
|
|
52
|
+
|
|
53
|
+
.. note::
|
|
54
|
+
When opencode supports setting env vars for MCP subprocess, switch to
|
|
55
|
+
``AWF_PROJECT_DIR`` env var as primary source. Until then,
|
|
56
|
+
agent-passed ``project_dir`` in ``open_form(data)`` is canonical.
|
|
57
|
+
"""
|
|
58
|
+
import logging
|
|
59
|
+
|
|
60
|
+
log = logging.getLogger(__name__)
|
|
61
|
+
cwd = Path.cwd()
|
|
62
|
+
agentic = cwd / ".agentic"
|
|
63
|
+
if agentic.is_dir():
|
|
64
|
+
log.debug("Detected awf project directory: %s", cwd)
|
|
65
|
+
return cwd
|
|
66
|
+
log.debug("No .agentic/ found at %s — not inside an awf project", cwd)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def ensure_directories(config: Config) -> None:
|
|
71
|
+
"""Create runtime directories if missing. Idempotent."""
|
|
72
|
+
config.inputs_dir.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
config.templates_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
config.dashboards_dir.mkdir(parents=True, exist_ok=True)
|