design-playbook 0.24.3 → 0.25.0
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/codex/AGENTS.md +1 -1
- package/commands/component-distill.md +8 -0
- package/cordis.patch.yml +19 -0
- package/lib/index.js +4 -2
- package/mcp/evidence/capture_runtime.py +35 -8
- package/mcp/evidence/containment.py +91 -0
- package/mcp/evidence/server.py +2 -1
- package/mcp/preview/control.css +172 -144
- package/mcp/preview/control.html +80 -86
- package/mcp/preview/control.js +286 -63
- package/mcp/preview/control.py +10 -4
- package/mcp/preview/control.review.js +64 -77
- package/mcp/preview/i18n.py +49 -43
- package/mcp/preview/integrity.py +2 -0
- package/mcp/preview/ledger.py +65 -0
- package/mcp/preview/pin_bridge.py +11 -1
- package/mcp/preview/review_session.py +29 -3
- package/mcp/preview/server.py +25 -3
- package/mcp/preview/transaction.py +50 -1
- package/mcp/run_console/actions.py +133 -7
- package/mcp/run_console/app.css +87 -0
- package/mcp/run_console/app.js +275 -15
- package/mcp/run_console/diagnostic_export.py +403 -0
- package/mcp/run_console/export_transaction.py +208 -0
- package/mcp/run_console/http_server.py +106 -6
- package/mcp/run_console/request_security.py +19 -2
- package/mcp/run_console/session.py +22 -2
- package/mcp/run_console/source_registry.py +23 -1
- package/package.json +10 -2
- package/scripts/component_candidates.py +380 -0
- package/scripts/contract_v1.py +5 -0
- package/scripts/eval_decisions.py +303 -0
- package/scripts/evidence_manifest.py +271 -0
- package/scripts/g10_design_decisions.py +28 -14
- package/scripts/g1_spec.py +43 -2
- package/scripts/g2_g4_pointback.py +68 -4
- package/scripts/g5_preview.py +93 -3
- package/scripts/g6_evidence.py +82 -6
- package/scripts/g6_warnings.py +24 -14
- package/scripts/g8_run_registry.py +20 -7
- package/scripts/g9_shaping.py +42 -11
- package/scripts/governance_jsonl.py +42 -0
- package/scripts/promotion_governance.py +185 -0
- package/scripts/run_continuation.py +21 -4
- package/scripts/run_metadata.py +0 -7
- package/scripts/shaping_log.py +38 -2
- package/scripts/stages.py +5 -0
- package/scripts/validate_run.py +16 -0
- package/skills/component-distill/SKILL.md +49 -0
- package/skills/craft-guard/SKILL.md +1 -1
- package/skills/craft-guard/references/detectors.md +1 -0
- package/skills/design-baseline/SKILL.md +2 -0
- package/skills/design-baseline/scripts/design_baseline.py +224 -0
- package/skills/design-playbook/SKILL.md +12 -2
- package/skills/design-playbook/references/observe-ops.md +2 -1
- package/skills/design-playbook/references/preview-ops.md +2 -1
- package/skills/ui-evaluator/SKILL.md +1 -1
- package/skills/ui-picker/SKILL.md +1 -1
- package/skills/ux-spec/SKILL.md +5 -3
- package/skills/ux-spec/references/spec-template.md +2 -1
- package/mcp/evidence/test_capture_contract.py +0 -355
- package/mcp/evidence/test_containment.py +0 -733
- package/mcp/evidence/test_delivery_matrix.py +0 -159
- package/mcp/evidence/test_disclosure.py +0 -309
- package/mcp/evidence/test_evidence_preflight.py +0 -288
- package/mcp/evidence/test_handoff.py +0 -672
- package/mcp/evidence/test_handoff_i18n.py +0 -125
- package/mcp/evidence/test_ledger_syntax.py +0 -251
- package/mcp/evidence/test_page_defects.py +0 -90
- package/mcp/evidence/test_server_stdio.py +0 -821
- package/mcp/preview/test_browser_control.py +0 -1284
- package/mcp/preview/test_i18n_labels.py +0 -92
- package/mcp/preview/test_integrity.py +0 -237
- package/mcp/preview/test_preview_usability.py +0 -247
- package/mcp/preview/test_server_stdio.py +0 -200
- package/mcp/preview/test_transaction.py +0 -810
- package/mcp/preview/test_versions.py +0 -625
- package/mcp/preview/test_versions_freeze.py +0 -176
- package/mcp/run_console/test_actions.py +0 -723
- package/mcp/run_console/test_contract.py +0 -668
- package/mcp/run_console/test_diagnostic_export_gate.py +0 -1540
- package/mcp/run_console/test_http_server.py +0 -1041
- package/mcp/run_console/test_parity.py +0 -1129
- package/mcp/run_console/test_read_only_trial.py +0 -583
- package/mcp/run_console/test_repair_packet.py +0 -840
- package/mcp/run_console/test_request_security.py +0 -325
- package/mcp/run_console/test_role_attestation_gate.py +0 -945
- package/mcp/run_console/test_session.py +0 -336
- package/mcp/run_console/test_snapshot_builder.py +0 -912
- package/mcp/run_console/test_source_registry.py +0 -950
- package/mcp/run_console/test_ui.py +0 -444
- package/mcp/run_console/test_ui_actions.py +0 -295
- package/mcp/run_console/test_ui_browser.py +0 -951
package/codex/AGENTS.md
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Cross-run component backflow — derive recurring components from iterated pages and render a propose-only DESIGN.md promotion proposal (report-only, user-gated)
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Cross-run **component backflow** over `.scratch/<run>/` decision reports in the user project. Not a step of a single Design I/O run. Run skill **component-distill**. Proposal header **`component-distill/v1`**; report-only — never writes `DESIGN.md` or any authority; promotion is a user decision recorded in the governance log and executed only by `design_baseline.py promote`.
|
|
6
|
+
|
|
7
|
+
Output path:
|
|
8
|
+
$ARGUMENTS
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# design-playbook dsh bundle patch
|
|
2
|
+
#
|
|
3
|
+
# Mounts the design-playbook plugin (this package's lib/index.js), which
|
|
4
|
+
# registers the bundled skills as a provider on ctx.skills. The plugin
|
|
5
|
+
# locates its own skills/ directory via __dirname, so no path expression is
|
|
6
|
+
# needed and no cwd-dependent resolution is involved.
|
|
7
|
+
#
|
|
8
|
+
# Note: the Cordis `!!js` evaluation scope provides no `require` (only Node
|
|
9
|
+
# globals plus ctx-provided values like dshHomePath), so pointing a
|
|
10
|
+
# skill-filesystem customSkillDirs row at package resources via
|
|
11
|
+
# `require.resolve` does not work. The plugin route is the supported way for
|
|
12
|
+
# a package to contribute its own skills.
|
|
13
|
+
#
|
|
14
|
+
# P2 (MCP + commands) will append rows here.
|
|
15
|
+
|
|
16
|
+
- insert:
|
|
17
|
+
- id: design-playbook
|
|
18
|
+
name: 'design-playbook'
|
|
19
|
+
disabled: false
|
package/lib/index.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* `skills/` directory. The plugin locates the directory via `__dirname`,
|
|
9
9
|
* so no `!!js` expression and no cwd-dependent resolution is involved.
|
|
10
10
|
*
|
|
11
|
-
* 2. Slash commands (ctx.commands) — `
|
|
12
|
-
* `run-handoff`, `run-review`, `run-status`, `ui-review`,
|
|
11
|
+
* 2. Slash commands (ctx.commands) — `component-distill`, `design-io`,
|
|
12
|
+
* `doctor`, `run-handoff`, `run-review`, `run-status`, `ui-review`,
|
|
13
|
+
* `ux-spec` —
|
|
13
14
|
* that load the matching `commands/<name>.md` prompt, substitute
|
|
14
15
|
* `$ARGUMENTS` with the raw trailing input, and inject it as a
|
|
15
16
|
* user-role follow-up turn via `agent.followup()`.
|
|
@@ -87,6 +88,7 @@ exports.createUserMessageFromPrompt = createUserMessageFromPrompt
|
|
|
87
88
|
* substituted) is injected as a user follow-up turn.
|
|
88
89
|
*/
|
|
89
90
|
const COMMAND_NAMES = [
|
|
91
|
+
'component-distill',
|
|
90
92
|
'design-io',
|
|
91
93
|
'doctor',
|
|
92
94
|
'run-handoff',
|
|
@@ -110,6 +110,10 @@ def _failed(
|
|
|
110
110
|
}
|
|
111
111
|
if request is not None:
|
|
112
112
|
payload["request"] = request
|
|
113
|
+
if written_path and _run_root_misrooted():
|
|
114
|
+
# Same misroot warning as the success payload: a failed write outside
|
|
115
|
+
# the run tree is exactly where the orchestrator needs the hint.
|
|
116
|
+
payload["warnings"] = [_MISROOTED_WARNING]
|
|
113
117
|
return payload
|
|
114
118
|
|
|
115
119
|
|
|
@@ -138,6 +142,10 @@ def _captured(
|
|
|
138
142
|
"written_path": written_path,
|
|
139
143
|
"request": request,
|
|
140
144
|
}
|
|
145
|
+
if _run_root_misrooted():
|
|
146
|
+
# The stderr warning fires once per process and may never reach the
|
|
147
|
+
# model; the payload is what the orchestrator actually reads.
|
|
148
|
+
payload["warnings"] = [_MISROOTED_WARNING]
|
|
141
149
|
if probe_artifact:
|
|
142
150
|
payload["probe_artifact"] = probe_artifact
|
|
143
151
|
return payload
|
|
@@ -271,19 +279,38 @@ _RUN_MARKERS = ("plan.md", "point-back.md")
|
|
|
271
279
|
_warned_run_root = False
|
|
272
280
|
|
|
273
281
|
|
|
282
|
+
_MISROOTED_WARNING = (
|
|
283
|
+
"run root resolved to a markerless cwd (DESIGN_PLAYBOOK_RUN_ROOT "
|
|
284
|
+
"unset or '.'); written_path is outside the run tree — set "
|
|
285
|
+
"DESIGN_PLAYBOOK_RUN_ROOT to the run root (.scratch/<run>/) "
|
|
286
|
+
"before binding this artifact"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _run_root_misrooted() -> bool:
|
|
291
|
+
"""True when the run root fell back to a markerless cwd.
|
|
292
|
+
|
|
293
|
+
The shipped .mcp.json default (DESIGN_PLAYBOOK_RUN_ROOT=".") makes the
|
|
294
|
+
server resolve artifacts under its process cwd; in a host workspace that
|
|
295
|
+
cwd is the repo root, so captures silently land outside the run tree.
|
|
296
|
+
"""
|
|
297
|
+
configured = os.environ.get(RUN_ROOT_ENV)
|
|
298
|
+
if configured and configured != ".":
|
|
299
|
+
return False
|
|
300
|
+
root = Path.cwd().resolve()
|
|
301
|
+
return not any((root / marker).is_file() for marker in _RUN_MARKERS)
|
|
302
|
+
|
|
303
|
+
|
|
274
304
|
def _run_root() -> Path:
|
|
275
305
|
configured = os.environ.get(RUN_ROOT_ENV)
|
|
276
306
|
if not configured or configured == ".":
|
|
277
|
-
# cwd
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
# misconfig — and only once per process to avoid per-capture spam.
|
|
307
|
+
# Warn only when cwd does not look like a run dir (no run marker
|
|
308
|
+
# file) — the shipped default resolving to a real run dir is correct
|
|
309
|
+
# usage, not a misconfig — and only once per process to avoid
|
|
310
|
+
# per-capture spam.
|
|
282
311
|
root = Path.cwd().resolve()
|
|
283
312
|
global _warned_run_root
|
|
284
|
-
if not _warned_run_root and
|
|
285
|
-
(root / marker).is_file() for marker in _RUN_MARKERS
|
|
286
|
-
):
|
|
313
|
+
if not _warned_run_root and _run_root_misrooted():
|
|
287
314
|
_warned_run_root = True
|
|
288
315
|
_log(
|
|
289
316
|
"WARNING: DESIGN_PLAYBOOK_RUN_ROOT is unset or '.' "
|
|
@@ -23,6 +23,11 @@ consume it instead of mirroring the escape classes. The ``evidence/``
|
|
|
23
23
|
operations remain the ADR-0026 contract surface - same reason codes, same
|
|
24
24
|
existence timing - now expressed as specializations of the one resolver.
|
|
25
25
|
|
|
26
|
+
ADR-0044 adds the Diagnostic export write boundary as a third specialization:
|
|
27
|
+
``trial_export_write_target`` confines one bare filename under
|
|
28
|
+
``<run_root>/trial-export/`` (the trial-export subtree), with the same
|
|
29
|
+
reason-code discipline and TOCTOU limit.
|
|
30
|
+
|
|
26
31
|
Threat-model limit (ADR-0026, explicit): this module resolves and validates
|
|
27
32
|
the path; it does NOT perform the write. Path resolution alone cannot close
|
|
28
33
|
the TOCTOU gap - a concurrent untrusted filesystem actor that replaces a
|
|
@@ -50,6 +55,9 @@ REASON_RESOLUTION_FAILURE = "resolution_failure"
|
|
|
50
55
|
REASON_CANONICAL_ESCAPE = "canonical_escape"
|
|
51
56
|
REASON_SYMLINK_ESCAPE = "symlink_escape"
|
|
52
57
|
REASON_NOT_REGULAR_FILE = "not_regular_file"
|
|
58
|
+
# Trial-export targets (ADR-0044) accept bare filenames only; a name that
|
|
59
|
+
# carries any separator or reserved form is rejected before resolution.
|
|
60
|
+
REASON_RESERVED_NAME = "reserved_name"
|
|
53
61
|
|
|
54
62
|
# Every resolution-time escape reason (the classes the ADR requires both
|
|
55
63
|
# operations to reject at resolution time). The Provider treats all of these
|
|
@@ -203,3 +211,86 @@ def read_artifact(artifact_path: str, run_root: Path) -> ContainmentResult:
|
|
|
203
211
|
must not bind a directory or a missing path).
|
|
204
212
|
"""
|
|
205
213
|
return _resolve(artifact_path, run_root, require_existing_file=True)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# The Diagnostic export write boundary (ADR-0044): one subtree, sibling of
|
|
217
|
+
# evidence/, owned here so the export transaction cannot disagree with the
|
|
218
|
+
# one containment authority on where trial exports may land.
|
|
219
|
+
TRIAL_EXPORT_SUBDIR = "trial-export"
|
|
220
|
+
|
|
221
|
+
# Names the export subtree may never carry. ``manifest.jsonl`` is reserved
|
|
222
|
+
# across the run tree (the Evidence Manifest authority); the current-directory
|
|
223
|
+
# name is a no-op write and is refused as malformed rather than silently
|
|
224
|
+
# permitted.
|
|
225
|
+
_TRIAL_EXPORT_RESERVED_NAMES = frozenset({"manifest.jsonl", "", ".", ".."})
|
|
226
|
+
|
|
227
|
+
# Win32 name quirks that CreateFile folds but Path.resolve does not: a
|
|
228
|
+
# trailing dot or space vanishes on write (the on-disk name would diverge
|
|
229
|
+
# from the reviewed one), and the reserved device names are never regular
|
|
230
|
+
# files. The export writes exactly the reviewed pair, so both classes are
|
|
231
|
+
# rejected as reserved.
|
|
232
|
+
_WIN32_DEVICE_NAMES = frozenset(
|
|
233
|
+
{"CON", "PRN", "AUX", "NUL",
|
|
234
|
+
*(f"COM{i}" for i in range(1, 10)),
|
|
235
|
+
*(f"LPT{i}" for i in range(1, 10))},
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def trial_export_write_target(filename: str, run_root: Path) -> ContainmentResult:
|
|
240
|
+
"""Resolve a Diagnostic export write target under ``trial-export/``.
|
|
241
|
+
|
|
242
|
+
``filename`` must be a bare filename - no directory separators (native,
|
|
243
|
+
POSIX, or Windows), no drive form, no ``..`` segment, and not one of the
|
|
244
|
+
reserved names, Win32 fold forms (trailing dot or space), or reserved
|
|
245
|
+
device stems - anything the platform would write under a different name
|
|
246
|
+
than the one reviewed. Same resolution-time escape
|
|
247
|
+
rejection and TOCTOU limit as every operation in this module; the
|
|
248
|
+
transaction performs the staged write and rollback around this resolution.
|
|
249
|
+
"""
|
|
250
|
+
if not isinstance(filename, str) or filename == "":
|
|
251
|
+
return ContainmentResult(None, REASON_RESERVED_NAME)
|
|
252
|
+
if filename in _TRIAL_EXPORT_RESERVED_NAMES:
|
|
253
|
+
return ContainmentResult(None, REASON_RESERVED_NAME)
|
|
254
|
+
# Bare-filename precondition: any separator, drive, or traversal form
|
|
255
|
+
# fails before the generic resolver can even see it.
|
|
256
|
+
if (
|
|
257
|
+
PurePosixPath(filename).is_absolute()
|
|
258
|
+
or PureWindowsPath(filename).is_absolute()
|
|
259
|
+
or "/" in filename
|
|
260
|
+
or "\\" in filename
|
|
261
|
+
or any(part == ".." for part in PurePosixPath(filename).parts)
|
|
262
|
+
or any(part == ".." for part in PureWindowsPath(filename).parts)
|
|
263
|
+
or ":" in filename
|
|
264
|
+
):
|
|
265
|
+
return ContainmentResult(None, REASON_ABSOLUTE_PATH)
|
|
266
|
+
# Win32 fold classes: a trailing dot/space or a reserved device stem
|
|
267
|
+
# would make the on-disk name differ from the reviewed name.
|
|
268
|
+
if filename != filename.rstrip(" ."):
|
|
269
|
+
return ContainmentResult(None, REASON_RESERVED_NAME)
|
|
270
|
+
if filename.split(".", 1)[0].upper() in _WIN32_DEVICE_NAMES:
|
|
271
|
+
return ContainmentResult(None, REASON_RESERVED_NAME)
|
|
272
|
+
# The boundary subtree itself must be a real child of the run root: a
|
|
273
|
+
# ``trial-export`` symlink pointing outside the run root would make the
|
|
274
|
+
# generic under-boundary check pass while writing outside the selected
|
|
275
|
+
# run, so the resolved boundary is required to stay inside the resolved
|
|
276
|
+
# run root before anything else is resolved against it.
|
|
277
|
+
try:
|
|
278
|
+
resolved_root = run_root.resolve(strict=False)
|
|
279
|
+
boundary = (run_root / TRIAL_EXPORT_SUBDIR).resolve(strict=False)
|
|
280
|
+
Path(os.path.realpath(boundary)).relative_to(
|
|
281
|
+
Path(os.path.realpath(resolved_root))
|
|
282
|
+
)
|
|
283
|
+
except (OSError, ValueError):
|
|
284
|
+
return ContainmentResult(None, REASON_SYMLINK_ESCAPE)
|
|
285
|
+
result = _resolve_candidate(
|
|
286
|
+
run_root,
|
|
287
|
+
f"{TRIAL_EXPORT_SUBDIR}/{filename}",
|
|
288
|
+
run_root / TRIAL_EXPORT_SUBDIR,
|
|
289
|
+
require_existing_file=False,
|
|
290
|
+
)
|
|
291
|
+
# The generic resolver also folds "." segments; a name like "a/." or
|
|
292
|
+
# "a.." is a file name here, but a name that Path normalizes to
|
|
293
|
+
# something other than itself inside the subtree must not pass.
|
|
294
|
+
if result.ok and result.path is not None and result.path.name != filename:
|
|
295
|
+
return ContainmentResult(None, REASON_RESERVED_NAME)
|
|
296
|
+
return result
|
package/mcp/evidence/server.py
CHANGED
|
@@ -32,7 +32,8 @@ def _tool_schema() -> dict[str, Any]:
|
|
|
32
32
|
"schemaVersion=1 plus explicit viewport, apply deterministic freeze "
|
|
33
33
|
"by default, write one artifact (screenshot / a11y tree / "
|
|
34
34
|
"interaction trace). Returns capture result only (artifact, "
|
|
35
|
-
"observed_state, result, error, written_path, request
|
|
35
|
+
"observed_state, result, error, written_path, request; plus a "
|
|
36
|
+
"warnings list when the run root looks misconfigured) — never "
|
|
36
37
|
"writes manifest; never judges criteria."
|
|
37
38
|
),
|
|
38
39
|
"inputSchema": {
|