design-playbook 0.25.0 → 0.25.2
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.
- package/README.md +21 -6
- package/codex/AGENTS.md +1 -1
- package/codex/install_skills.py +9 -0
- package/commands/run-status.md +84 -0
- package/mcp/evidence/README.md +16 -5
- package/mcp/evidence/capture_runtime.py +90 -7
- package/mcp/evidence/disclosure.py +2 -3
- package/mcp/evidence/evidence_preflight.py +22 -2
- package/mcp/evidence/path_syntax.py +26 -0
- package/mcp/evidence/server.py +24 -1
- package/mcp/preview/control.css +6 -1
- package/mcp/preview/server.py +9 -0
- package/mcp/run_console/snapshot_builder.py +2 -1
- package/package.json +2 -2
- package/scripts/adapter_matrix.py +2 -2
- package/scripts/audit_preferences.py +9 -0
- package/scripts/component_candidates.py +51 -11
- package/scripts/context_budget.py +9 -0
- package/scripts/contract_v1.py +10 -0
- package/scripts/doctor.py +10 -1
- package/scripts/eval_decisions.py +9 -0
- package/scripts/evidence_manifest.py +9 -0
- package/scripts/frontend_review.py +495 -0
- package/scripts/g10_design_decisions.py +9 -0
- package/scripts/g11_coverage.py +23 -0
- package/scripts/g1_spec.py +88 -0
- package/scripts/g2_g4_pointback.py +9 -0
- package/scripts/g5_preview.py +9 -0
- package/scripts/g6_evidence.py +64 -6
- package/scripts/g6_records.py +47 -0
- package/scripts/g6_warnings.py +12 -3
- package/scripts/g7_contract_drift.py +9 -0
- package/scripts/g8_run_registry.py +9 -0
- package/scripts/g9_shaping.py +9 -0
- package/scripts/generate_adapter.py +16 -1
- package/scripts/method_semantics.py +7 -1
- package/scripts/run_console.py +9 -0
- package/scripts/run_handoff.py +9 -0
- package/scripts/run_profile.py +10 -0
- package/scripts/run_status.py +45 -0
- package/scripts/shaping_log.py +10 -0
- package/scripts/stages.py +11 -0
- package/scripts/stdio_encoding.py +42 -0
- package/scripts/validate_run.py +9 -7
- package/skills/component-distill/SKILL.md +5 -2
- package/skills/craft-guard/SKILL.md +1 -1
- package/skills/design-baseline/SKILL.md +8 -4
- package/skills/design-baseline/scripts/design_baseline.py +30 -6
- package/skills/design-playbook/SKILL.md +14 -4
- package/skills/design-playbook/references/observe-ops.md +2 -1
- package/skills/ui-evaluator/SKILL.md +1 -1
- package/skills/ui-evaluator/references/a11y-tree.md +4 -0
- package/skills/ux-spec/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# design-playbook
|
|
2
2
|
|
|
3
|
-
Agent plugin
|
|
3
|
+
Agent plugin for **evidence-backed UI delivery** in existing Web products
|
|
4
|
+
(Claude Code / Codex). **Design I/O** is the declaration and contract mechanism.
|
|
5
|
+
|
|
6
|
+
The project is in maintainer self-use and maintenance. Catalog submissions
|
|
7
|
+
and recruitment are paused; installed paths remain available. Missing required
|
|
8
|
+
proof stays `blocked`; an explicitly skipped evaluator records `audited: false`,
|
|
9
|
+
not an audited Pass. Evaluator review does not replace human semantic approval.
|
|
4
10
|
|
|
5
11
|
Declarations + contracts — not a style CSV pack. Compose with [ui-ux-pro-max](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill) and Anthropic `frontend-design` for aesthetics; this package owns pipeline and acceptance.
|
|
6
12
|
|
|
@@ -52,6 +58,7 @@ After install, skills and commands are **namespaced** by the plugin name:
|
|
|
52
58
|
| `/design-playbook:craft-guard` | Craft / anti-slop skill |
|
|
53
59
|
| `/design-playbook:native-craft` | Native-feel desktop declaration skill |
|
|
54
60
|
| `/design-playbook:ui-evaluator` | Point-back acceptance skill |
|
|
61
|
+
| `/design-playbook:component-distill` | Cross-run component/token proposal skill and command; report-only, promotion requires a user decision |
|
|
55
62
|
| `/design-playbook:design-io` | Full pipeline command |
|
|
56
63
|
| `/design-playbook:ux-spec` | Spec-only command |
|
|
57
64
|
| `/design-playbook:ui-review` | Review command |
|
|
@@ -75,8 +82,8 @@ pi has no plugin namespace — skills are `/skill:<name>`, commands are bare `/<
|
|
|
75
82
|
| Invoke | Role |
|
|
76
83
|
| --- | --- |
|
|
77
84
|
| `/skill:design-playbook` | Orchestrator skill (model-invoked) |
|
|
78
|
-
| `/skill:ux-spec` … `/skill:
|
|
79
|
-
| `/design-io` · `/ux-spec` · `/ui-review` · `/run-review` · `/run-status` · `/run-handoff` · `/doctor` | Pipeline / spec-only / review / cross-run / status / handoff / health commands |
|
|
85
|
+
| `/skill:ux-spec` … `/skill:component-distill` | Same nine skills as above |
|
|
86
|
+
| `/design-io` · `/ux-spec` · `/ui-review` · `/run-review` · `/run-status` · `/run-handoff` · `/doctor` · `/component-distill` | Pipeline / spec-only / review / cross-run / status / handoff / health / proposal commands |
|
|
80
87
|
|
|
81
88
|
pi ships no built-in MCP, so `preview*` and `observe*` skip by default (ADR-0009 absent→skip; the pipeline still runs spec → picker → fill → craft → accept). To enable both gates, install an MCP adapter and register the bundled servers in your project `.mcp.json`:
|
|
82
89
|
|
|
@@ -113,7 +120,8 @@ npx design-playbook init <agent> # e.g. cursor, gemini-cli, windsurf
|
|
|
113
120
|
npx design-playbook --list # all 30 agents, shows which have renderers
|
|
114
121
|
```
|
|
115
122
|
|
|
116
|
-
See the root [README](
|
|
123
|
+
See the root [README](https://github.com/Bandersnatch0x/design-playbook#-install-on-other-agents)
|
|
124
|
+
for the tier table and capability notes.
|
|
117
125
|
|
|
118
126
|
## Stack with other skills
|
|
119
127
|
|
|
@@ -131,7 +139,7 @@ See the root [README](../../README.md#install-on-other-agents) for the tier tabl
|
|
|
131
139
|
.mcp.json ← bundled MCP servers, launched via ${CLAUDE_PLUGIN_ROOT} (ADR-0009)
|
|
132
140
|
mcp/{preview,evidence}/← MCP adapter runtimes (preview_prototype / execute_capture_plan)
|
|
133
141
|
skills/<name>/SKILL.md ← model-invoked skills
|
|
134
|
-
commands/<name>.md ← slash commands
|
|
142
|
+
commands/<name>.md ← slash commands; see invocation tables above
|
|
135
143
|
codex/AGENTS.md ← Codex bridge notes
|
|
136
144
|
examples/ ← self-authored onboarding samples
|
|
137
145
|
LICENSE · NOTICE ← authored-only scope
|
|
@@ -159,6 +167,13 @@ python <pkg>/scripts/run_status.py --list # newest runs under .
|
|
|
159
167
|
|
|
160
168
|
The status command reuses the packaged validator’s G5 confirm rules. It is part of the installed package — not monorepo-only tooling. For an eligible run it also reports an explicit `open-console` continuation command for the local Run Console (it never starts a server itself), with the blocking reason and a safe fallback when the run or Console prerequisites are ineligible. The Console’s current claim is **local, experimental, and trial-gated** — `run-status --json` reports the same capability receipt (`publicClaim: experimental`) — and no authorized external trial or public release is claimed until the separately authorized read-only trial gate passes (ADR-0043).
|
|
161
169
|
|
|
170
|
+
The source checkout adds a bounded self-use report:
|
|
171
|
+
`python <pkg>/scripts/run_status.py <run> --scope path:P1 --json`.
|
|
172
|
+
It links explicit declarations, summarizes evidence gaps, and retains the original
|
|
173
|
+
owner's re-verification requirements. Unknown impact cannot safely narrow that
|
|
174
|
+
scope. See [scope inputs, source refresh, and limits](commands/run-status.md).
|
|
175
|
+
This is not a release or measured productivity claim.
|
|
176
|
+
|
|
162
177
|
### Static handoff
|
|
163
178
|
|
|
164
179
|
```text
|
|
@@ -176,7 +191,7 @@ python <pkg>/scripts/doctor.py --json
|
|
|
176
191
|
|
|
177
192
|
One packaged diagnosis for interpreter, package surface, optional Playwright, and run-root configuration. Distinguishes `ok` / `degraded` / `broken` with repair actions. Those three states describe **install and runtime health** of what is present locally — they are not a public capability-maturity verdict; maturity vocabulary (`stable` / `experimental` / `blocked-by-gate` / `not-shipped`) stays with the `run-status` capability receipt, and doctor reads existing facts rather than adding a new health or capability-state authority.
|
|
178
193
|
|
|
179
|
-
**Bundled MCP (v0.3+):** Preview (`mcp/preview/`) and Evidence (`mcp/evidence/`) runtimes ship inside this package and are registered by `.mcp.json` (`${CLAUDE_PLUGIN_ROOT}`). Sibling monorepo dirs remain compatibility launchers/docs. The orchestrator still **probes** MCP `tools/list` and skips `preview*` / `observe*` when tools are absent. Evidence provider writes artifacts only — never the manifest. **`DESIGN_PLAYBOOK_RUN_ROOT`:**
|
|
194
|
+
**Bundled MCP (v0.3+):** Preview (`mcp/preview/`) and Evidence (`mcp/evidence/`) runtimes ship inside this package and are registered by `.mcp.json` (`${CLAUDE_PLUGIN_ROOT}`). Sibling monorepo dirs remain compatibility launchers/docs. The orchestrator still **probes** MCP `tools/list` and skips `preview*` / `observe*` when tools are absent. Evidence provider writes artifacts only — never the manifest. **`DESIGN_PLAYBOOK_RUN_ROOT`:** `.mcp.json` passes this variable through without pinning a default. With no explicit root, artifacts resolve under the **MCP process cwd**, not necessarily the chat workspace. For a host-app run, use an absolute `.scratch/<run>/` root; per-call overrides and marker requirements are documented in [`mcp/evidence/README.md`](mcp/evidence/README.md). Capture responses include `written_path` (absolute) so mis-rooted writes are visible without a filesystem search.
|
|
180
195
|
|
|
181
196
|
What is **deterministically enforced** today: repository install/structure CI checks and the run-artifact shape (`scripts/validate_run.py` — L1–L6 present; every top-level L6 item ordered `Given -> When -> Then`; one non-empty four-field evidence ledger row per `L6.<n>` with allowed results; four non-empty finding fields with non-empty source; exactly one explicit `## Verdict` of `Pass` or `Recirculate`; Pass requires every evidence result to be `pass` and exactly one issue-linked `0 blocking` closure per blocking finding; exit 0/`RUN OK`, exit 1/`RUN INVALID`, exit 2/`RUN ERROR`; regression-tested by `tests/test_validate_run.py`, which also validates the showcase artifacts directly; **G5** is a *conditional* preview-confirm gate — enforced only when preview artifacts exist / `--preview-dir` is used; **G6** is a *conditional* evidence-binding gate — enforced only when a ledger `observed` references an `evidence/` artifact / `--evidence-dir` is used; opt-in **strict mode** via `--require-preview` / `--require-evidence` / `--strict`). The `observe*` step probes MCP tool `execute_capture_plan` and is skipped when absent. Everything else in the pipeline is agent-executed craft judgment, not a machine gate.
|
|
182
197
|
|
package/codex/AGENTS.md
CHANGED
package/codex/install_skills.py
CHANGED
|
@@ -66,4 +66,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
if __name__ == "__main__":
|
|
69
|
+
# One pipe-encoding seam (T-105): UTF-8 on piped stdout/stderr
|
|
70
|
+
# regardless of the host code page. See scripts/stdio_encoding.py.
|
|
71
|
+
for _candidate in Path(__file__).resolve().parents:
|
|
72
|
+
if (_candidate / "design_playbook.py").is_file():
|
|
73
|
+
sys.path.insert(0, str(_candidate))
|
|
74
|
+
break
|
|
75
|
+
from design_playbook.scripts.stdio_encoding import configure_piped_utf8
|
|
76
|
+
|
|
77
|
+
configure_piped_utf8()
|
|
69
78
|
sys.exit(main())
|
package/commands/run-status.md
CHANGED
|
@@ -18,6 +18,90 @@ python <plugin>/scripts/run_status.py [.scratch/<run>] [--json] [--list] [--scra
|
|
|
18
18
|
|
|
19
19
|
For an eligible run the continuation names an explicit `open-console` command for the local, experimental, trial-gated Run Console — no authorized external trial or public release is claimed until the separately authorized read-only trial gate passes (ADR-0043). `run-status` never starts a server, daemon, or background process itself; an ineligible run reports the blocking reason and a safe fallback instead. On Windows the emitted command line is PowerShell syntax (single-quoted literals behind the `&` call operator) — paste it into PowerShell, not cmd.exe.
|
|
20
20
|
|
|
21
|
+
## Explicit frontend scope review
|
|
22
|
+
|
|
23
|
+
The source checkout also provides a bounded, read-only report for self-use. It
|
|
24
|
+
has no separate Console panel or persistent mapping, and no measured time-saving
|
|
25
|
+
or released-package claim.
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
python <plugin>/scripts/run_status.py <run> --scope path:P1 --scope page:checkout
|
|
29
|
+
python <plugin>/scripts/run_status.py <run> --scope component:Retry --json
|
|
30
|
+
python <plugin>/scripts/run_status.py <run> --scope path:P1 --git-root <repo> --base-revision <base> --head-revision <head> --json
|
|
31
|
+
python <plugin>/scripts/run_status.py <run> --scope path:P1 --git-root <repo> --base-revision <base> --worktree --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
- A run path and at least one `--scope` are required. Scope mode does not discover
|
|
35
|
+
runs and cannot combine with `--list`; `--scratch` only applies to discovery.
|
|
36
|
+
Its JSON is a scope report, not the legacy status JSON or Run Snapshot v1. It
|
|
37
|
+
carries `schemaVersion: 1`; every member of `evidence_gaps[].bindings[]` has the
|
|
38
|
+
same keys `integrity`, `reasons`, `source`, `content_hash`, and an unbound or
|
|
39
|
+
unreadable entry reports `source: null` / `content_hash: null` rather than
|
|
40
|
+
dropping those keys.
|
|
41
|
+
- Repeated scopes accept `path:P1`, `page:<id>`, `component:<id>`, and
|
|
42
|
+
`file:<relative/path>`. Paths must stay within the explicit Git root, or the
|
|
43
|
+
selected run if no Git root was given. Absolute paths and traversal are rejected.
|
|
44
|
+
- Known links require an L3 `Path | Steps` row and an L6 `(path: P1)` reference.
|
|
45
|
+
Page links additionally require an exact L2 `Page | Duty` entry and an exact
|
|
46
|
+
step token separated by `->` or an arrow. Free-form prose is not a code
|
|
47
|
+
dependency map. Assumptions stay `assumed`; component and file clues currently
|
|
48
|
+
remain `unknown`. No association means unknown, never unaffected.
|
|
49
|
+
- Git is optional. When requested, supply the actual repository root, a base,
|
|
50
|
+
and exactly one of a head revision or `--worktree`. Worktree mode includes
|
|
51
|
+
untracked paths. The report neither guesses a base nor scans source code.
|
|
52
|
+
|
|
53
|
+
Evidence requirements are read only where an L6 item already declares a precise
|
|
54
|
+
continuation, for example:
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
- Given checkout fails When retry is offered Then recovery is visible (path: P1)
|
|
58
|
+
Required evidence: screenshot; state=error; viewport=390x844
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The recognized proof values are `screenshot`, `a11y_tree`, and
|
|
62
|
+
`interaction_trace`; `state` and `viewport` are optional and appear in that order.
|
|
63
|
+
This optional read convention adds no mandatory spec field. Missing or
|
|
64
|
+
unrecognized prose stays `unknown`; conflicting declarations stay `inconsistent`.
|
|
65
|
+
The separate sampling summary enumerates only nonblank L5 state cells.
|
|
66
|
+
`reported` means a sampling claim, not a verified proof binding.
|
|
67
|
+
|
|
68
|
+
The report separates source availability, binding integrity, evidence gaps, and
|
|
69
|
+
the original evaluator result. Missing required proof is `blocked`; skipping the
|
|
70
|
+
evaluator remains `unaudited`. An owner ledger that exists but cannot be
|
|
71
|
+
projected is never read as an absent one: every gap gains the
|
|
72
|
+
`pointback-malformed` reason, and sources that are still current report the
|
|
73
|
+
evaluation as `inconsistent` rather than `unknown`. Binding timestamps order
|
|
74
|
+
by instant, so mixed `Z` / `+HH:MM` stamps pick the real latest capture: a
|
|
75
|
+
stamp that is missing, offset-less, or not ISO-8601 reports
|
|
76
|
+
`invalid-binding-timestamp`, and two distinct entries sharing the latest
|
|
77
|
+
instant report `conflicting-bindings`; both leave the binding `inconsistent`.
|
|
78
|
+
Not-applicable and unreviewed reasons are referenced at their source, not
|
|
79
|
+
copied as private prose.
|
|
80
|
+
It does not start a Provider, read browser login state, emit capture URLs or
|
|
81
|
+
source code, write run artifacts, or grant semantic approval. Treat scope names
|
|
82
|
+
and relative file paths as local data.
|
|
83
|
+
|
|
84
|
+
Reverification candidates retain the original Repair Packet's invalidated set,
|
|
85
|
+
owner, resume stage, and recapture requirement. **They cannot safely narrow the
|
|
86
|
+
original owner scope**: declaration links do not prove exhaustive code impact.
|
|
87
|
+
No repair or acceptance command is executed.
|
|
88
|
+
|
|
89
|
+
Before copying an owner command from an earlier report, repeat the same inputs
|
|
90
|
+
with its `source_hash`:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
python <plugin>/scripts/run_status.py <run> --scope path:P1 --expected-source-hash sha256:<previous-hash> --json
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Changed or owner-unverified sources make the report `stale` and remove the copyable command.
|
|
97
|
+
Unavailable contracts also suppress it. Refresh the report and use the existing
|
|
98
|
+
owner workflow; a source check does not lock files against later edits.
|
|
99
|
+
Exit 0 means a report was produced, not that the run passed acceptance.
|
|
100
|
+
Invalid inputs or unreadable/out-of-root sources exit 2.
|
|
101
|
+
|
|
21
102
|
## Done when
|
|
22
103
|
|
|
23
104
|
The command names completed stage markers, any active blocker (preview floor, baseline gate, recirculate verdict), and the single next valid resume action. It reuses `validate_run` judgments for G5 confirm validity rather than inventing a second state machine. Stale, partial, hash-mismatched, malformed, or inconsistent runs stay visible as those states and are never replaced by an older successful snapshot.
|
|
105
|
+
|
|
106
|
+
In scope mode, known links are traceable to their declaration and hash, uncertain
|
|
107
|
+
links remain explicit, and diagnostic gaps never replace the evaluator verdict.
|
package/mcp/evidence/README.md
CHANGED
|
@@ -6,11 +6,21 @@ Runtime for the optional **observe\*** step. Writes capture artifacts only — *
|
|
|
6
6
|
|
|
7
7
|
| Setting | Meaning |
|
|
8
8
|
| --- | --- |
|
|
9
|
-
| Unset | Artifact paths resolve under the **MCP process cwd** |
|
|
10
|
-
| `"."` (
|
|
9
|
+
| Unset | Artifact paths resolve under the **MCP process cwd** (the packaged `.mcp.json` passes the variable through, it does not pin a value) |
|
|
10
|
+
| `"."` (a host config that pins it) | Same — relative to process cwd, **not** the chat workspace root |
|
|
11
11
|
| Absolute path | Preferred for cross-repo dogfood: set to the run root (e.g. `/path/to/host-app/.scratch/playbook-smoke/<run>`) so `evidence/L6.*.png` lands next to `manifest.jsonl` |
|
|
12
12
|
|
|
13
|
-
Relative values are resolved with `Path(value).resolve()` at process start semantics (cwd-relative). If captures appear under the plugin monorepo instead of the host run, check cwd and this env — the tool also returns **`written_path`** (absolute) so mis-roots are obvious without a filesystem search.
|
|
13
|
+
Relative values are resolved with `Path(value).resolve()` at process start semantics (cwd-relative). If captures appear under the plugin monorepo instead of the host run, check cwd and this env — the tool also returns **`written_path`** (absolute) plus a `warnings` entry so mis-roots are obvious without a filesystem search.
|
|
14
|
+
|
|
15
|
+
**Cannot restart the server (`--plugin-dir` dev load)?** Pass `run_root` on the tool call — an absolute `.scratch/<run>/` that already carries a run marker (`plan.md` / `point-back.md`). Resolution order is `run_root` → `DESIGN_PLAYBOOK_RUN_ROOT` → process cwd, and the argument binds only that one call, so a mistyped or markerless root fails the capture instead of scattering evidence.
|
|
16
|
+
|
|
17
|
+
## What each capture type writes
|
|
18
|
+
|
|
19
|
+
| `type` | Bytes | Name it |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| `screenshot` | PNG (plus a `<stem>.probe.json` sidecar when the adapter can probe) | `evidence/<leaf>.png` |
|
|
22
|
+
| `a11y tree` | JSON envelope `{"format": "aria_snapshot", "tree": "…"}` — `tree` is Playwright's indentation text, not a node/role tree | `evidence/<leaf>.json` |
|
|
23
|
+
| `interaction trace` | Playwright trace **ZIP** (actions + snapshots inside) | `evidence/<leaf>.trace.zip` — the Provider refuses a non-`.zip` name |
|
|
14
24
|
|
|
15
25
|
Example (host run):
|
|
16
26
|
|
|
@@ -20,8 +30,9 @@ Example (host run):
|
|
|
20
30
|
}
|
|
21
31
|
```
|
|
22
32
|
|
|
23
|
-
Plugin auto-load: [
|
|
24
|
-
|
|
33
|
+
Plugin auto-load: [`packages/design-playbook/.mcp.json`](https://github.com/Bandersnatch0x/design-playbook/blob/main/packages/design-playbook/.mcp.json)
|
|
34
|
+
(a repository file, not shipped in the npm tarball).
|
|
35
|
+
Codex / manual: sibling [`mcp.example.toml`](https://github.com/Bandersnatch0x/design-playbook/blob/main/packages/design-playbook-evidence/mcp.example.toml).
|
|
25
36
|
|
|
26
37
|
## Return shape
|
|
27
38
|
|
|
@@ -11,7 +11,8 @@ from __future__ import annotations
|
|
|
11
11
|
import json
|
|
12
12
|
import math
|
|
13
13
|
import os
|
|
14
|
-
from
|
|
14
|
+
from contextvars import ContextVar
|
|
15
|
+
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
15
16
|
from typing import Any, Protocol
|
|
16
17
|
|
|
17
18
|
from design_playbook.mcp.evidence import containment
|
|
@@ -22,6 +23,7 @@ from design_playbook.mcp.evidence.action_params import (
|
|
|
22
23
|
from design_playbook.mcp.evidence.capture_contract import parse_capture_contract
|
|
23
24
|
from design_playbook.mcp.evidence.path_syntax import (
|
|
24
25
|
probe_sidecar_rel,
|
|
26
|
+
trace_artifact_error,
|
|
25
27
|
trimmed_relpath,
|
|
26
28
|
)
|
|
27
29
|
from design_playbook.mcp.evidence.disclosure import (
|
|
@@ -50,6 +52,7 @@ ALLOWED_ARGUMENTS = frozenset(
|
|
|
50
52
|
"viewport",
|
|
51
53
|
"freeze",
|
|
52
54
|
"storage_state",
|
|
55
|
+
"run_root",
|
|
53
56
|
}
|
|
54
57
|
)
|
|
55
58
|
RUN_ROOT_ENV = "DESIGN_PLAYBOOK_RUN_ROOT"
|
|
@@ -278,30 +281,78 @@ def _apply_freeze(page: Any, freeze: dict[str, Any]) -> None:
|
|
|
278
281
|
_RUN_MARKERS = ("plan.md", "point-back.md")
|
|
279
282
|
_warned_run_root = False
|
|
280
283
|
|
|
284
|
+
# Per-call run root (DEF-4): a `--plugin-dir` dev host cannot edit the shipped
|
|
285
|
+
# DESIGN_PLAYBOOK_RUN_ROOT after the MCP process starts, so
|
|
286
|
+
# `execute_capture_plan` accepts a `run_root` argument that binds the
|
|
287
|
+
# resolution for the duration of one call and nothing longer.
|
|
288
|
+
_CALL_RUN_ROOT: ContextVar["Path | None"] = ContextVar(
|
|
289
|
+
"design_playbook_call_run_root", default=None
|
|
290
|
+
)
|
|
281
291
|
|
|
282
292
|
_MISROOTED_WARNING = (
|
|
283
293
|
"run root resolved to a markerless cwd (DESIGN_PLAYBOOK_RUN_ROOT "
|
|
284
|
-
"unset or '.'); written_path is outside the run tree —
|
|
285
|
-
"
|
|
286
|
-
"
|
|
294
|
+
"unset or '.'); written_path is outside the run tree — pass "
|
|
295
|
+
"run_root=<abs .scratch/<run>/> on the recapture (no server restart "
|
|
296
|
+
"needed) or set DESIGN_PLAYBOOK_RUN_ROOT before launch"
|
|
287
297
|
)
|
|
288
298
|
|
|
289
299
|
|
|
300
|
+
def _has_run_marker(root: Path) -> bool:
|
|
301
|
+
"""True when ``root`` carries one of the run's own marker artifacts."""
|
|
302
|
+
return any((root / marker).is_file() for marker in _RUN_MARKERS)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _validated_call_run_root(value: object) -> Path:
|
|
306
|
+
"""Normalize an explicit ``run_root`` argument.
|
|
307
|
+
|
|
308
|
+
Absolute, existing, and marker-carrying: a mistyped root must fail the
|
|
309
|
+
call rather than scatter evidence into a directory that is not a run.
|
|
310
|
+
"""
|
|
311
|
+
if not isinstance(value, str) or not value.strip():
|
|
312
|
+
raise ValueError("run_root must be a non-empty absolute path string")
|
|
313
|
+
raw = value.strip()
|
|
314
|
+
if not (
|
|
315
|
+
Path(raw).is_absolute()
|
|
316
|
+
or PureWindowsPath(raw).is_absolute()
|
|
317
|
+
or PurePosixPath(raw).is_absolute()
|
|
318
|
+
):
|
|
319
|
+
raise ValueError(f"run_root must be an absolute path (got {raw!r})")
|
|
320
|
+
root = Path(raw).expanduser().resolve()
|
|
321
|
+
if not root.is_dir():
|
|
322
|
+
raise ValueError(
|
|
323
|
+
f"run_root {root} is not an existing directory; pass the run root "
|
|
324
|
+
".scratch/<run>/"
|
|
325
|
+
)
|
|
326
|
+
if not _has_run_marker(root):
|
|
327
|
+
raise ValueError(
|
|
328
|
+
f"run_root {root} carries no run marker "
|
|
329
|
+
f"({' / '.join(_RUN_MARKERS)}); pass the run root, not the "
|
|
330
|
+
"workspace root or the evidence/ directory"
|
|
331
|
+
)
|
|
332
|
+
return root
|
|
333
|
+
|
|
334
|
+
|
|
290
335
|
def _run_root_misrooted() -> bool:
|
|
291
336
|
"""True when the run root fell back to a markerless cwd.
|
|
292
337
|
|
|
293
338
|
The shipped .mcp.json default (DESIGN_PLAYBOOK_RUN_ROOT=".") makes the
|
|
294
339
|
server resolve artifacts under its process cwd; in a host workspace that
|
|
295
340
|
cwd is the repo root, so captures silently land outside the run tree.
|
|
341
|
+
A per-call ``run_root`` is marker-validated before use, so it is never
|
|
342
|
+
misrooted.
|
|
296
343
|
"""
|
|
344
|
+
if _CALL_RUN_ROOT.get() is not None:
|
|
345
|
+
return False
|
|
297
346
|
configured = os.environ.get(RUN_ROOT_ENV)
|
|
298
347
|
if configured and configured != ".":
|
|
299
348
|
return False
|
|
300
|
-
|
|
301
|
-
return not any((root / marker).is_file() for marker in _RUN_MARKERS)
|
|
349
|
+
return not _has_run_marker(Path.cwd().resolve())
|
|
302
350
|
|
|
303
351
|
|
|
304
352
|
def _run_root() -> Path:
|
|
353
|
+
explicit = _CALL_RUN_ROOT.get()
|
|
354
|
+
if explicit is not None:
|
|
355
|
+
return explicit
|
|
305
356
|
configured = os.environ.get(RUN_ROOT_ENV)
|
|
306
357
|
if not configured or configured == ".":
|
|
307
358
|
# Warn only when cwd does not look like a run dir (no run marker
|
|
@@ -318,7 +369,8 @@ def _run_root() -> Path:
|
|
|
318
369
|
f"({' / '.join(_RUN_MARKERS)}); artifacts resolve under "
|
|
319
370
|
f"{root}/evidence/. Set DESIGN_PLAYBOOK_RUN_ROOT to the run "
|
|
320
371
|
"root when the host workspace is not the intended run "
|
|
321
|
-
"directory
|
|
372
|
+
"directory, or pass run_root=<abs run root> on the capture "
|
|
373
|
+
"call when the server cannot be restarted."
|
|
322
374
|
)
|
|
323
375
|
return root
|
|
324
376
|
return Path(configured).resolve()
|
|
@@ -792,6 +844,34 @@ def _validate_runtime_object(
|
|
|
792
844
|
def execute_capture_plan(
|
|
793
845
|
args: dict[str, Any],
|
|
794
846
|
browser_adapter: BrowserAdapter | None = None,
|
|
847
|
+
) -> dict[str, Any]:
|
|
848
|
+
"""Capture one artifact, then report the result (never a verdict).
|
|
849
|
+
|
|
850
|
+
Run root resolution order: an explicit ``run_root`` argument (bound to
|
|
851
|
+
this call only) → ``DESIGN_PLAYBOOK_RUN_ROOT`` → process cwd. The explicit
|
|
852
|
+
root is marker-validated before use, so a dev host that cannot restart the
|
|
853
|
+
MCP server still lands evidence inside the run tree (DEF-4).
|
|
854
|
+
"""
|
|
855
|
+
raw_run_root = args.get("run_root")
|
|
856
|
+
if raw_run_root is None:
|
|
857
|
+
return _capture(args, browser_adapter)
|
|
858
|
+
artifact = args.get("artifact_path")
|
|
859
|
+
label = artifact if isinstance(artifact, str) else ""
|
|
860
|
+
try:
|
|
861
|
+
root = _validated_call_run_root(raw_run_root)
|
|
862
|
+
except (ValueError, OSError) as exc:
|
|
863
|
+
# OSError is in range: Path.resolve() rejects host-illegal characters.
|
|
864
|
+
return _failed(label, str(exc))
|
|
865
|
+
token = _CALL_RUN_ROOT.set(root)
|
|
866
|
+
try:
|
|
867
|
+
return _capture(args, browser_adapter)
|
|
868
|
+
finally:
|
|
869
|
+
_CALL_RUN_ROOT.reset(token)
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _capture(
|
|
873
|
+
args: dict[str, Any],
|
|
874
|
+
browser_adapter: BrowserAdapter | None,
|
|
795
875
|
) -> dict[str, Any]:
|
|
796
876
|
unknown = sorted(set(args) - ALLOWED_ARGUMENTS)
|
|
797
877
|
if unknown:
|
|
@@ -824,6 +904,9 @@ def execute_capture_plan(
|
|
|
824
904
|
raise ValueError("storage_state must be a string path when provided")
|
|
825
905
|
|
|
826
906
|
rel = artifact_path.strip()
|
|
907
|
+
trace_error = trace_artifact_error(cap_type, rel)
|
|
908
|
+
if trace_error:
|
|
909
|
+
return _failed(rel, trace_error, request=request)
|
|
827
910
|
try:
|
|
828
911
|
out_path = _resolve_artifact_path(rel)
|
|
829
912
|
except ValueError as exc:
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Static-handoff disclosure review builder (Stage 9 evidence).
|
|
2
2
|
|
|
3
3
|
Produces the ``disclosure-review.json`` delivery credential defined by the
|
|
4
|
-
Static Handoff
|
|
5
|
-
-implementation-plan.md §4.2): a single authoritative payload binding the run
|
|
4
|
+
Static Handoff contract (ADR-0034): a single authoritative payload binding the run
|
|
6
5
|
identity, verdict, profile, decision authority, the five standard viewport
|
|
7
6
|
layout metrics (``sw`` / ``innerH`` / ``hOverflow`` / ``disclosure.inFold``),
|
|
8
7
|
and the G1–G8 gate count — so a front-end/QA consumer can reproduce and audit
|
|
@@ -16,7 +15,7 @@ Two seams keep the contract builder pure and testable without a browser:
|
|
|
16
15
|
The probe JS string is also exposed (``LAYOUT_PROBE_JS``) for a static
|
|
17
16
|
syntax/structure check.
|
|
18
17
|
* ``build_disclosure(...)`` — deterministic pure builder: no I/O, no browser.
|
|
19
|
-
It only normalizes caller-supplied facts into the
|
|
18
|
+
It only normalizes caller-supplied facts into the disclosure shape.
|
|
20
19
|
|
|
21
20
|
``build_handoff_zip()`` packages the disclosure credential plus any caller-
|
|
22
21
|
supplied snapshot artifacts into a single ZIP. The Evidence-side builder
|
|
@@ -25,6 +25,7 @@ import json
|
|
|
25
25
|
import re
|
|
26
26
|
import sys
|
|
27
27
|
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
28
29
|
from typing import Any
|
|
29
30
|
|
|
30
31
|
try:
|
|
@@ -37,8 +38,10 @@ try:
|
|
|
37
38
|
parse_capture_contract,
|
|
38
39
|
)
|
|
39
40
|
from design_playbook.mcp.evidence.path_syntax import ( # noqa: E402
|
|
41
|
+
TRACE_SUFFIX,
|
|
40
42
|
lexical_posix_key,
|
|
41
43
|
probe_sidecar_rel,
|
|
44
|
+
trace_artifact_error,
|
|
42
45
|
trimmed_relpath,
|
|
43
46
|
)
|
|
44
47
|
except ImportError: # standalone execution: same-dir seam (rules_registry pattern)
|
|
@@ -52,8 +55,10 @@ except ImportError: # standalone execution: same-dir seam (rules_registry patte
|
|
|
52
55
|
)
|
|
53
56
|
from capture_contract import parse_capture_contract # noqa: E402
|
|
54
57
|
from path_syntax import ( # noqa: E402
|
|
58
|
+
TRACE_SUFFIX,
|
|
55
59
|
lexical_posix_key,
|
|
56
60
|
probe_sidecar_rel,
|
|
61
|
+
trace_artifact_error,
|
|
57
62
|
trimmed_relpath,
|
|
58
63
|
)
|
|
59
64
|
|
|
@@ -133,6 +138,14 @@ def preflight_entry(request: object, entry: int) -> list[PreflightFact]:
|
|
|
133
138
|
expected="relative path starting with "
|
|
134
139
|
f"{ARTIFACT_PREFIX!r}",
|
|
135
140
|
actual=artifact))
|
|
141
|
+
elif isinstance(capture_type, str) and (
|
|
142
|
+
name_error := trace_artifact_error(capture_type, artifact)
|
|
143
|
+
):
|
|
144
|
+
# Same rule the Provider rejects with (path_syntax), reported here
|
|
145
|
+
# before a browser starts (DEF-6).
|
|
146
|
+
facts.append(_error("bad_artifact_extension", name_error, entry,
|
|
147
|
+
expected=f"name ending in {TRACE_SUFFIX}",
|
|
148
|
+
actual=artifact))
|
|
136
149
|
|
|
137
150
|
actions = request.get("actions")
|
|
138
151
|
if actions is not None:
|
|
@@ -291,8 +304,15 @@ def preflight_plan(plan: object) -> list[PreflightFact]:
|
|
|
291
304
|
|
|
292
305
|
|
|
293
306
|
def main(argv: list[str] | None = None) -> int:
|
|
294
|
-
|
|
295
|
-
|
|
307
|
+
# One pipe-encoding seam (T-105): UTF-8 on piped stdout/stderr
|
|
308
|
+
# regardless of the host code page. See scripts/stdio_encoding.py.
|
|
309
|
+
for _candidate in Path(__file__).resolve().parents:
|
|
310
|
+
if (_candidate / "design_playbook.py").is_file():
|
|
311
|
+
sys.path.insert(0, str(_candidate))
|
|
312
|
+
break
|
|
313
|
+
from design_playbook.scripts.stdio_encoding import configure_piped_utf8
|
|
314
|
+
|
|
315
|
+
configure_piped_utf8()
|
|
296
316
|
args = sys.argv[1:] if argv is None else argv
|
|
297
317
|
md = "--md" in args
|
|
298
318
|
paths = [a for a in args if a != "--md"]
|
|
@@ -31,3 +31,29 @@ def probe_sidecar_rel(artifact_rel: str) -> str:
|
|
|
31
31
|
if "." in artifact_rel.rsplit("/", 1)[-1]:
|
|
32
32
|
return artifact_rel.rsplit(".", 1)[0] + ".probe.json"
|
|
33
33
|
return artifact_rel + ".probe.json"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# On-disk shape of an interaction trace (DEF-6): ``tracing.stop(path=...)``
|
|
37
|
+
# writes a Playwright trace ZIP, so any other extension mislabels binary bytes.
|
|
38
|
+
TRACE_SUFFIX = ".zip"
|
|
39
|
+
TRACE_EXAMPLE = "evidence/<criterion>.trace.zip"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def trace_artifact_error(capture_type: str, artifact_rel: str) -> str:
|
|
43
|
+
"""Naming error when a trace capture is not named ``.zip``; "" otherwise.
|
|
44
|
+
|
|
45
|
+
One definition for the capture runtime (hard reject) and the static
|
|
46
|
+
preflight (early fact), so a plan never passes preflight only to fail there.
|
|
47
|
+
An empty name is left to the required-field check at each call site.
|
|
48
|
+
"""
|
|
49
|
+
if (
|
|
50
|
+
capture_type == "interaction trace"
|
|
51
|
+
and artifact_rel
|
|
52
|
+
and not artifact_rel.casefold().endswith(TRACE_SUFFIX)
|
|
53
|
+
):
|
|
54
|
+
return (
|
|
55
|
+
"interaction trace artifacts are Playwright trace ZIP files — name "
|
|
56
|
+
f"artifact_path with a {TRACE_SUFFIX} extension (e.g. {TRACE_EXAMPLE}); "
|
|
57
|
+
f"got {artifact_rel!r}"
|
|
58
|
+
)
|
|
59
|
+
return ""
|
package/mcp/evidence/server.py
CHANGED
|
@@ -49,7 +49,10 @@ def _tool_schema() -> dict[str, Any]:
|
|
|
49
49
|
'v1: "screenshot" | "a11y tree" | "interaction trace". '
|
|
50
50
|
"A screenshot capture also writes a sibling "
|
|
51
51
|
".probe.json page-probe artifact (facts, not a "
|
|
52
|
-
"judgment) when the adapter can probe."
|
|
52
|
+
"judgment) when the adapter can probe. An a11y tree "
|
|
53
|
+
"is a JSON envelope {format, tree} whose tree is "
|
|
54
|
+
"Playwright aria_snapshot text; an interaction trace "
|
|
55
|
+
"is a Playwright trace ZIP (name it .zip)."
|
|
53
56
|
),
|
|
54
57
|
"enum": ["screenshot", "a11y tree", "interaction trace"],
|
|
55
58
|
},
|
|
@@ -79,6 +82,17 @@ def _tool_schema() -> dict[str, Any]:
|
|
|
79
82
|
"'evidence/'). Provider only writes this file."
|
|
80
83
|
),
|
|
81
84
|
},
|
|
85
|
+
"run_root": {
|
|
86
|
+
"type": "string",
|
|
87
|
+
"description": (
|
|
88
|
+
"Optional absolute run root (.scratch/<run>/) for THIS "
|
|
89
|
+
"call only, overriding DESIGN_PLAYBOOK_RUN_ROOT / "
|
|
90
|
+
"process cwd. Use it when the MCP process was started "
|
|
91
|
+
"without the env var and cannot be restarted; the "
|
|
92
|
+
"directory must exist and carry a run marker "
|
|
93
|
+
"(plan.md or point-back.md)."
|
|
94
|
+
),
|
|
95
|
+
},
|
|
82
96
|
"overwrite": {
|
|
83
97
|
"type": "boolean",
|
|
84
98
|
"description": (
|
|
@@ -110,6 +124,15 @@ def _tool_schema() -> dict[str, Any]:
|
|
|
110
124
|
|
|
111
125
|
|
|
112
126
|
if __name__ == "__main__":
|
|
127
|
+
# One pipe-encoding seam (T-105): UTF-8 on piped stdout/stderr
|
|
128
|
+
# regardless of the host code page. See scripts/stdio_encoding.py.
|
|
129
|
+
for _candidate in Path(__file__).resolve().parents:
|
|
130
|
+
if (_candidate / "design_playbook.py").is_file():
|
|
131
|
+
sys.path.insert(0, str(_candidate))
|
|
132
|
+
break
|
|
133
|
+
from design_playbook.scripts.stdio_encoding import configure_piped_utf8
|
|
134
|
+
|
|
135
|
+
configure_piped_utf8()
|
|
113
136
|
serve_stdio(
|
|
114
137
|
SERVER_NAME,
|
|
115
138
|
SERVER_VERSION,
|
package/mcp/preview/control.css
CHANGED
|
@@ -113,6 +113,8 @@ body.dpb-shell { overflow: hidden; height: 100vh; }
|
|
|
113
113
|
}
|
|
114
114
|
#dpb-root kbd, .dpb-toasts kbd {
|
|
115
115
|
display: inline-flex; align-items: center; justify-content: center;
|
|
116
|
+
/* OBS-1: a shortcut badge is a fixed glyph run — never squeeze or fold it. */
|
|
117
|
+
flex: 0 0 auto; white-space: nowrap;
|
|
116
118
|
font: 600 9px/1.2 ui-monospace, "JetBrains Mono", Consolas, monospace;
|
|
117
119
|
padding: 1px 5px; border-radius: 4px;
|
|
118
120
|
background: var(--dpb-surface); color: var(--dpb-soft);
|
|
@@ -548,10 +550,13 @@ body.dpb-tool-ruler .dpb-draw-layer { pointer-events: auto; cursor: crosshair; }
|
|
|
548
550
|
border-color: var(--dpb-danger) !important;
|
|
549
551
|
box-shadow: 0 0 0 3px var(--dpb-ring-danger) !important;
|
|
550
552
|
}
|
|
551
|
-
.dpb-foot-actions { display: flex; justify-content: flex-end; gap: 10px; }
|
|
553
|
+
.dpb-foot-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 10px; }
|
|
552
554
|
.dpb-btn {
|
|
553
555
|
appearance: none; cursor: pointer; height: 32px; padding: 0 14px; border-radius: 8px;
|
|
554
556
|
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
|
|
557
|
+
/* OBS-1: a fixed-height pill must keep one text line; long locale labels
|
|
558
|
+
(EN "Approve (2 notes)") wrap onto a second row of the actions instead. */
|
|
559
|
+
flex: 0 0 auto; white-space: nowrap;
|
|
555
560
|
border: 1px solid var(--dpb-line-strong); background: transparent; color: var(--dpb-soft);
|
|
556
561
|
font: 600 12px/1 inherit; transition: all var(--dpb-duration-spring) var(--dpb-ease-spring);
|
|
557
562
|
}
|
package/mcp/preview/server.py
CHANGED
|
@@ -155,6 +155,15 @@ def handle_preview_prototype(args: dict[str, Any]) -> dict[str, Any]:
|
|
|
155
155
|
|
|
156
156
|
|
|
157
157
|
if __name__ == "__main__":
|
|
158
|
+
# One pipe-encoding seam (T-105): UTF-8 on piped stdout/stderr
|
|
159
|
+
# regardless of the host code page. See scripts/stdio_encoding.py.
|
|
160
|
+
for _candidate in Path(__file__).resolve().parents:
|
|
161
|
+
if (_candidate / "design_playbook.py").is_file():
|
|
162
|
+
sys.path.insert(0, str(_candidate))
|
|
163
|
+
break
|
|
164
|
+
from design_playbook.scripts.stdio_encoding import configure_piped_utf8
|
|
165
|
+
|
|
166
|
+
configure_piped_utf8()
|
|
158
167
|
if len(sys.argv) > 1 and sys.argv[1] == "--self-check":
|
|
159
168
|
self_check_floor()
|
|
160
169
|
else:
|
|
@@ -119,7 +119,8 @@ _RETAINED_FIELDS = (
|
|
|
119
119
|
# Capture-contract quality findings (G6.capture_*) do not unbind evidence
|
|
120
120
|
# at this boundary.
|
|
121
121
|
_UNBINDABLE_RULES = frozenset(
|
|
122
|
-
{"G6.artifact_missing", "G6.escape", "G6.no_binding", "G6.unknown_criterion"
|
|
122
|
+
{"G6.artifact_missing", "G6.escape", "G6.no_binding", "G6.unknown_criterion",
|
|
123
|
+
"G6.binding_conflict"}
|
|
123
124
|
)
|
|
124
125
|
_DEGRADING_RULES = frozenset({"G6.artifact_missing", "G6.escape"})
|
|
125
126
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "design-playbook",
|
|
3
|
-
"version": "0.25.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.25.2",
|
|
4
|
+
"description": "Evidence-backed UI delivery for coding agents working on existing Web products: declared criteria, criterion-bound artifacts, findings linked to declarations, and repair guidance. Evaluator review does not replace human semantic approval.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -10,8 +10,8 @@ ADR-0042 amendment): the tier encodes the capability class, each renderer
|
|
|
10
10
|
documents its own output surface, and the published counts are derived
|
|
11
11
|
from this matrix by the validate.py gate.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
Decision authority: docs/adr/0042-multi-platform-adapter-generator.md.
|
|
14
|
+
This module owns the current adapter inventory.
|
|
15
15
|
"""
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
@@ -597,4 +597,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
597
597
|
|
|
598
598
|
|
|
599
599
|
if __name__ == "__main__":
|
|
600
|
+
# One pipe-encoding seam (T-105): UTF-8 on piped stdout/stderr
|
|
601
|
+
# regardless of the host code page. See scripts/stdio_encoding.py.
|
|
602
|
+
for _candidate in Path(__file__).resolve().parents:
|
|
603
|
+
if (_candidate / "design_playbook.py").is_file():
|
|
604
|
+
sys.path.insert(0, str(_candidate))
|
|
605
|
+
break
|
|
606
|
+
from design_playbook.scripts.stdio_encoding import configure_piped_utf8
|
|
607
|
+
|
|
608
|
+
configure_piped_utf8()
|
|
600
609
|
raise SystemExit(main())
|