maestro-case-kit 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,9 @@
1
+ """Maestro Case Kit — agent-native knowledge + offline validators for UiPath Maestro Case.
2
+
3
+ A living, version-stamped knowledge layer over the undocumented Maestro Case /
4
+ Data Fabric / Action Center footguns surfaced while building a multi-stakeholder
5
+ crisis case, plus credential-free static validators that run in CI. No UiPath
6
+ login required for the v1 surface.
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,178 @@
1
+ """`maestro-case` CLI — agent-native, offline access to the knowledge layer.
2
+
3
+ v1 surface: `explain`. Validators (`lint`, `check-spawn`, `check-df`) attach to the
4
+ same parser in later slices. Every subcommand supports ``--json`` for agent/CI use
5
+ and exits non-zero when it has a finding to report.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+ from typing import IO
15
+
16
+ from . import __version__, contribution, knowledge, validators
17
+
18
+
19
+ def _print_human(entries: list[knowledge.KnowledgeEntry], stream: IO[str]) -> None:
20
+ for e in entries:
21
+ signals = ", ".join(e.error_signatures) or "(no error code — silent behavior)"
22
+ resolved = f" resolved_in: {e.resolved_in}" if e.resolved_in else ""
23
+ print(f"[{e.id}] {e.title}", file=stream)
24
+ print(f" surface: {e.surface}", file=stream)
25
+ print(f" signals: {signals}", file=stream)
26
+ print(f" cause: {e.cause}", file=stream)
27
+ print(f" fix: {e.fix}", file=stream)
28
+ print(f" proven_on: {e.proven_on}{resolved}", file=stream)
29
+ if e.references:
30
+ print(f" refs: {', '.join(e.references)}", file=stream)
31
+ print("", file=stream)
32
+
33
+
34
+ def _cmd_explain(args: argparse.Namespace) -> int:
35
+ hits = knowledge.find(args.query, include_resolved=args.include_resolved)
36
+ if args.json:
37
+ print(json.dumps([e.to_dict() for e in hits], indent=2))
38
+ return 0 if hits else 1
39
+ if not hits:
40
+ print(
41
+ f"No known Maestro Case footgun matches {args.query!r}. "
42
+ f"Try an error code (e.g. 400300) or a keyword (e.g. underscore, gate, deploy).",
43
+ file=sys.stderr,
44
+ )
45
+ return 1
46
+ _print_human(hits, sys.stdout)
47
+ return 0
48
+
49
+
50
+ def _emit_findings(findings: list[validators.Finding], as_json: bool, ok_label: str) -> int:
51
+ if as_json:
52
+ print(json.dumps([f.to_dict() for f in findings], indent=2))
53
+ return 1 if findings else 0
54
+ if not findings:
55
+ print(f"OK — {ok_label}")
56
+ return 0
57
+ for f in findings:
58
+ suffix = f" (explain: {f.entry_id})" if f.entry_id else ""
59
+ where = f" @ {f.location}" if f.location else ""
60
+ print(f"[{f.severity.upper()}] {f.rule_id}: {f.message}{where}{suffix}")
61
+ print(f"\n{len(findings)} finding(s).")
62
+ return 1
63
+
64
+
65
+ def _cmd_lint(args: argparse.Namespace) -> int:
66
+ findings = validators.lint_caseplan(args.caseplan_dir)
67
+ return _emit_findings(findings, args.json, f"no Maestro Case footguns in {args.caseplan_dir}")
68
+
69
+
70
+ def _cmd_check_spawn(args: argparse.Namespace) -> int:
71
+ findings = validators.check_spawn_fanout(args.caseplan_dir)
72
+ return _emit_findings(findings, args.json, f"no qem spawn-fanout issues in {args.caseplan_dir}")
73
+
74
+
75
+ def _cmd_check_df(args: argparse.Namespace) -> int:
76
+ findings = validators.validate_df_entity(args.spec)
77
+ return _emit_findings(findings, args.json, f"no Data Fabric field-name traps in {args.spec}")
78
+
79
+
80
+ def _cmd_validate_knowledge(args: argparse.Namespace) -> int:
81
+ denylist: list[str] = []
82
+ if args.denylist_file:
83
+ for line in Path(args.denylist_file).read_text(encoding="utf-8").splitlines():
84
+ token = line.strip()
85
+ if token and not token.startswith("#"):
86
+ denylist.append(token)
87
+ data: object = args.file or {"entries": [e.to_dict() for e in knowledge.load_entries()]}
88
+ problems = contribution.validate_knowledge(data, denylist)
89
+ if args.json:
90
+ print(json.dumps(problems, indent=2))
91
+ return 1 if problems else 0
92
+ if not problems:
93
+ print("OK — knowledge passes the contribution gate.")
94
+ return 0
95
+ for problem in problems:
96
+ print(f"[GATE] {problem}")
97
+ print(f"\n{len(problems)} problem(s).")
98
+ return 1
99
+
100
+
101
+ def build_parser() -> argparse.ArgumentParser:
102
+ parser = argparse.ArgumentParser(
103
+ prog="maestro-case",
104
+ description="Knowledge + offline validators for UiPath Maestro Case footguns.",
105
+ )
106
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
107
+ sub = parser.add_subparsers(dest="command")
108
+
109
+ explain = sub.add_parser(
110
+ "explain",
111
+ help="Explain a UiPath Maestro Case error code or footgun from the knowledge layer.",
112
+ )
113
+ explain.add_argument(
114
+ "query",
115
+ help="An error code/signature (e.g. 400300, 160009) or a keyword (e.g. underscore, deploy).",
116
+ )
117
+ explain.add_argument("--json", action="store_true", help="Emit structured JSON.")
118
+ explain.add_argument(
119
+ "--include-resolved",
120
+ action="store_true",
121
+ help="Include entries marked resolved in a later platform version.",
122
+ )
123
+ explain.set_defaults(func=_cmd_explain)
124
+
125
+ lint = sub.add_parser(
126
+ "lint",
127
+ help="Statically lint a caseplan directory for known footguns (offline, no login).",
128
+ )
129
+ lint.add_argument(
130
+ "caseplan_dir",
131
+ help="Path to a directory containing caseplan.json (and ideally caseplan.json.bpmn).",
132
+ )
133
+ lint.add_argument("--json", action="store_true", help="Emit structured JSON findings.")
134
+ lint.set_defaults(func=_cmd_lint)
135
+
136
+ check_spawn = sub.add_parser(
137
+ "check-spawn",
138
+ help="Flag =datafabric.qem expressions in spawn inputs (fail at runtime, 400300).",
139
+ )
140
+ check_spawn.add_argument("caseplan_dir", help="Path to a directory containing caseplan.json.")
141
+ check_spawn.add_argument("--json", action="store_true", help="Emit structured JSON findings.")
142
+ check_spawn.set_defaults(func=_cmd_check_spawn)
143
+
144
+ check_df = sub.add_parser(
145
+ "check-df",
146
+ help="Lint a Data Fabric entity/field spec for silent-drop and reserved-name traps.",
147
+ )
148
+ check_df.add_argument("spec", help="Path to a JSON entity spec ({\"fields\": [...]}).")
149
+ check_df.add_argument("--json", action="store_true", help="Emit structured JSON findings.")
150
+ check_df.set_defaults(func=_cmd_check_df)
151
+
152
+ vk = sub.add_parser(
153
+ "validate-knowledge",
154
+ help="Validate a knowledge file against the schema + an optional IP-safety denylist.",
155
+ )
156
+ vk.add_argument("--file", help="Path to a knowledge JSON file (default: the bundled layer).")
157
+ vk.add_argument(
158
+ "--denylist-file",
159
+ help="Optional newline-delimited file of forbidden tokens (# comments allowed).",
160
+ )
161
+ vk.add_argument("--json", action="store_true", help="Emit structured JSON problems.")
162
+ vk.set_defaults(func=_cmd_validate_knowledge)
163
+ return parser
164
+
165
+
166
+ def main(argv: list[str] | None = None) -> int:
167
+ parser = build_parser()
168
+ args = parser.parse_args(argv)
169
+ if not getattr(args, "command", None):
170
+ parser.print_help(sys.stderr)
171
+ return 2
172
+ func = args.func
173
+ assert callable(func)
174
+ return int(func(args))
175
+
176
+
177
+ if __name__ == "__main__": # pragma: no cover
178
+ raise SystemExit(main())
@@ -0,0 +1,83 @@
1
+ """Contribution gate — validates knowledge entries against the schema and an
2
+ optional IP-safety denylist, so curation (not the distribution channel) is the moat.
3
+
4
+ A contributed entry must be well-formed and free of any caller-supplied forbidden
5
+ tokens (e.g. real company names). The denylist is a parameter, not hardcoded, so the
6
+ toolkit stays a general public artifact; a host repo passes its own denylist in CI.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from collections.abc import Iterable
13
+ from pathlib import Path
14
+
15
+ REQUIRED_FIELDS: tuple[str, ...] = (
16
+ "id",
17
+ "kind",
18
+ "title",
19
+ "surface",
20
+ "symptom",
21
+ "cause",
22
+ "fix",
23
+ "proven_on",
24
+ "severity",
25
+ )
26
+ VALID_SEVERITY: frozenset[str] = frozenset({"high", "medium", "low"})
27
+ _LIST_FIELDS: tuple[str, ...] = ("error_signatures", "references")
28
+
29
+
30
+ def _entry_text(entry: dict) -> str:
31
+ parts: list[str] = []
32
+ for value in entry.values():
33
+ if isinstance(value, str):
34
+ parts.append(value)
35
+ elif isinstance(value, list):
36
+ parts.extend(str(item) for item in value)
37
+ return " ".join(parts).lower()
38
+
39
+
40
+ def validate_entry(entry: dict, denylist: Iterable[str] = ()) -> list[str]:
41
+ """Return a list of problems with one entry; empty means it passes the gate."""
42
+ problems: list[str] = []
43
+ for field in REQUIRED_FIELDS:
44
+ if not entry.get(field):
45
+ problems.append(f"missing or empty required field: {field}")
46
+ severity = entry.get("severity")
47
+ if severity is not None and severity not in VALID_SEVERITY:
48
+ problems.append(f"invalid severity {severity!r} (expected one of {sorted(VALID_SEVERITY)})")
49
+ for field in _LIST_FIELDS:
50
+ if field in entry and not isinstance(entry[field], list):
51
+ problems.append(f"{field} must be a list")
52
+ text = _entry_text(entry)
53
+ entry_id = entry.get("id", "<unknown>")
54
+ for token in denylist:
55
+ if token and token.lower() in text:
56
+ problems.append(f"forbidden token {token!r} in entry {entry_id}")
57
+ return problems
58
+
59
+
60
+ def _coerce_entries(data: object) -> list[dict]:
61
+ if isinstance(data, (str, Path)):
62
+ data = json.loads(Path(data).read_text(encoding="utf-8"))
63
+ if isinstance(data, dict):
64
+ entries = data.get("entries", [])
65
+ else:
66
+ entries = data
67
+ return [e for e in entries if isinstance(e, dict)] if isinstance(entries, list) else []
68
+
69
+
70
+ def validate_knowledge(data: object, denylist: Iterable[str] = ()) -> list[str]:
71
+ """Validate a knowledge collection (path, {"entries": [...]}, or a list)."""
72
+ entries = _coerce_entries(data)
73
+ denylist = tuple(denylist)
74
+ problems: list[str] = []
75
+ ids: list[str] = []
76
+ for entry in entries:
77
+ problems.extend(validate_entry(entry, denylist))
78
+ entry_id = entry.get("id")
79
+ if isinstance(entry_id, str):
80
+ ids.append(entry_id)
81
+ for dup in sorted({i for i in ids if ids.count(i) > 1}):
82
+ problems.append(f"duplicate id: {dup}")
83
+ return problems
@@ -0,0 +1,187 @@
1
+ {
2
+ "schema_version": 1,
3
+ "entries": [
4
+ {
5
+ "id": "MC-SPAWN-QEM-400300",
6
+ "kind": "runtime",
7
+ "title": "datafabric.qem expression fails in case spawn inputs",
8
+ "surface": "Maestro Case spawn JobArguments",
9
+ "symptom": "A case spawn whose JobArguments use a =datafabric.qem: query expression fails at runtime instead of fanning out.",
10
+ "error_signatures": ["400300", "Syntax error at index 4"],
11
+ "cause": "=datafabric.qem: query expressions are not evaluated in case-spawn JobArguments; they raise a runtime syntax error rather than resolving to data.",
12
+ "fix": "Pass a literal stakeholder slug instead (e.g. StakeholderId: \"gamma\"). Do not attempt data-driven fan-out via qem in spawn inputs; resolve the values at authoring time.",
13
+ "proven_on": "1.0.21",
14
+ "resolved_in": null,
15
+ "severity": "high",
16
+ "references": ["docs/submission/PRODUCT-FEEDBACK.md", "scripts/merge-canvas-download.py"]
17
+ },
18
+ {
19
+ "id": "MC-BPMN-STALE",
20
+ "kind": "runtime",
21
+ "title": "Stale caseplan.json.bpmn runs old logic; only the canvas recompiles it",
22
+ "surface": "Maestro Case build/deploy",
23
+ "symptom": "Edits to caseplan.json have no runtime effect; the case keeps running old logic with no error.",
24
+ "error_signatures": [],
25
+ "cause": "The engine executes the compiled caseplan.json.bpmn, not caseplan.json. Only opening and saving the case in the Studio Web canvas regenerates the .bpmn; CLI upload/pack do not recompile it.",
26
+ "fix": "After any caseplan edit: open the case in the canvas, run uip solution download --extract, commit the regenerated .bpmn, then pack/deploy. Detect staleness by comparing mtimes (caseplan.json newer than .bpmn = stale).",
27
+ "proven_on": "1.0.31",
28
+ "resolved_in": null,
29
+ "severity": "high",
30
+ "references": ["scripts/merge-canvas-download.py", "docs/submission/PRODUCT-FEEDBACK.md"]
31
+ },
32
+ {
33
+ "id": "MC-JOB-CASE-SYNC",
34
+ "kind": "runtime",
35
+ "title": "Completed case instance leaves its Orchestrator job Running",
36
+ "surface": "Orchestrator jobs vs Maestro Case instances",
37
+ "symptom": "A case instance shows Completed but its backing Orchestrator job stays Running for hours or days, breaking job-based completion polling.",
38
+ "error_signatures": [],
39
+ "cause": "Completed Maestro Case instances do not flip their backing job to Successful (only Cancelled instances flip the job, to Stopped). The two views are not reconciled.",
40
+ "fix": "Treat case instances as the source of truth: verify completion with uip maestro case instance get/list, not the Jobs view. Sweep zombie Running jobs separately.",
41
+ "proven_on": "1.0.23",
42
+ "resolved_in": null,
43
+ "severity": "medium",
44
+ "references": ["docs/submission/PRODUCT-FEEDBACK.md", "agents/case-job-janitor"]
45
+ },
46
+ {
47
+ "id": "DF-RESERVED-ID",
48
+ "kind": "data-fabric",
49
+ "title": "Data Fabric rejects a field named id",
50
+ "surface": "Data Fabric entity creation",
51
+ "symptom": "Creating an entity with a field named id is rejected.",
52
+ "error_signatures": ["Invalid field name 'id'", "field name 'id'"],
53
+ "cause": "id collides with the system Id UUID primary key.",
54
+ "fix": "Use an alternate name such as slug instead of id for your business key.",
55
+ "proven_on": "1.0.x",
56
+ "resolved_in": null,
57
+ "severity": "medium",
58
+ "references": ["scripts/seed_data_fabric.py", "docs/submission/PRODUCT-FEEDBACK.md"]
59
+ },
60
+ {
61
+ "id": "DF-UNDERSCORE-DROP",
62
+ "kind": "data-fabric",
63
+ "title": "Underscore field names are silently dropped on insert",
64
+ "surface": "Data Fabric records insert",
65
+ "symptom": "Field names containing underscores (provider_id, display_name) are accepted by the schema and the insert reports success, but the values never persist. No error is surfaced.",
66
+ "error_signatures": [],
67
+ "cause": "uip df records insert silently drops fields whose names contain underscores; only the live-layer camelCase form persists.",
68
+ "fix": "camelCase every field name on insert (provider_id -> providerId, display_name -> displayName). Keep snake_case only in schema docs, never in the inserted payload.",
69
+ "proven_on": "1.0.x",
70
+ "resolved_in": null,
71
+ "severity": "high",
72
+ "references": ["scripts/seed_data_fabric.py", "docs/submission/PRODUCT-FEEDBACK.md"]
73
+ },
74
+ {
75
+ "id": "DF-ENTITY-NAME-NOT-GUID",
76
+ "kind": "data-fabric",
77
+ "title": "records insert needs the entity GUID, not its name",
78
+ "surface": "Data Fabric records insert",
79
+ "symptom": "Passing an entity name to records insert is rejected.",
80
+ "error_signatures": ["is not valid", "not valid"],
81
+ "cause": "uip df records insert expects the entity GUID returned by entities create/list, not the human-readable entity name.",
82
+ "fix": "Run uip df entities list first, build a name->GUID map, and pass the GUID to every insert.",
83
+ "proven_on": "1.0.x",
84
+ "resolved_in": null,
85
+ "severity": "low",
86
+ "references": ["scripts/seed_data_fabric.py"]
87
+ },
88
+ {
89
+ "id": "HITL-GATE-DELETE-160009",
90
+ "kind": "hitl",
91
+ "title": "Deleting a pending HITL gate faults the case (160009)",
92
+ "surface": "Action Center HITL gate tasks",
93
+ "symptom": "Deleting (instead of actioning) a pending gate task faults the backing case instance.",
94
+ "error_signatures": ["160009"],
95
+ "cause": "A deleted gate task is recorded as a user-caused incident (ErrorCode 160009, IncidentType User) pinned to the gate element; the case transitions to Faulted.",
96
+ "fix": "Always ACTION gate tasks (Approve/Deny, File/Withdraw) — never delete them. Add a confirmation/guard in any operator UI that exposes delete.",
97
+ "proven_on": "1.0.32",
98
+ "resolved_in": null,
99
+ "severity": "medium",
100
+ "references": ["docs/submission/PRODUCT-FEEDBACK.md"]
101
+ },
102
+ {
103
+ "id": "HITL-APPTASK-405",
104
+ "kind": "hitl",
105
+ "title": "AppTask completion returns 405 on the obvious endpoints",
106
+ "surface": "Action Center AppTask completion (REST)",
107
+ "symptom": "Completing an AppTask via the obvious OData/form endpoints returns 405 Method Not Allowed.",
108
+ "error_signatures": ["405", "Method Not Allowed"],
109
+ "cause": "The documented OData Complete action and form endpoints do not complete AppTasks; the correct route was undocumented.",
110
+ "fix": "Use POST /tasks/AppTasks/CompleteAppTask (the route behind uip tasks complete --type AppTask). Assign the task first; unassigned tasks fail.",
111
+ "proven_on": "1.0.x",
112
+ "resolved_in": null,
113
+ "severity": "low",
114
+ "references": ["demo_autocomplete.py", "docs/submission/PRODUCT-FEEDBACK.md"]
115
+ },
116
+ {
117
+ "id": "HITL-APPTASK-UNASSIGNED",
118
+ "kind": "hitl",
119
+ "title": "AppTask completion fails when the task is unassigned",
120
+ "surface": "Action Center AppTask completion",
121
+ "symptom": "Completing an AppTask that is not assigned to the caller fails.",
122
+ "error_signatures": ["This action is no longer assigned to you", "no longer assigned"],
123
+ "cause": "AppTask completion requires the task to be assigned to the acting identity first.",
124
+ "fix": "Assign the task before completing it (uip tasks assign <id> --user <email>), then complete.",
125
+ "proven_on": "1.0.x",
126
+ "resolved_in": null,
127
+ "severity": "low",
128
+ "references": ["demo_autocomplete.py"]
129
+ },
130
+ {
131
+ "id": "HITL-APPTASK-ORPHANED",
132
+ "kind": "hitl",
133
+ "title": "AppTasks on a stopped case are orphaned and uncompletable",
134
+ "surface": "Action Center AppTask completion",
135
+ "symptom": "An AppTask whose backing case instance was stopped or swept cannot be completed.",
136
+ "error_signatures": ["This action has been already deleted", "already deleted"],
137
+ "cause": "Stopping or sweeping the backing case instance orphans its AppTasks; they are dead in both CLI and the Action Center UI.",
138
+ "fix": "Complete AppTasks before stopping or sweeping their backing jobs. For a demo, action all gates within the janitor window before any sweep, or start a fresh run.",
139
+ "proven_on": "1.0.x",
140
+ "resolved_in": null,
141
+ "severity": "medium",
142
+ "references": ["demo_autocomplete.py", "docs/submission/PRODUCT-FEEDBACK.md"]
143
+ },
144
+ {
145
+ "id": "CLI-CODEDAPP-INDEXING-HANG",
146
+ "kind": "cli",
147
+ "title": "uip codedapp deploy hangs forever with -v/--path-name",
148
+ "surface": "uip codedapp deploy",
149
+ "symptom": "Deploying a coded app with explicit -v/--path-name hangs indefinitely on an indexing status check and never returns.",
150
+ "error_signatures": ["still being indexed", "has not been published yet"],
151
+ "cause": "The explicit-version deploy path triggers a broken indexing status-check that never completes.",
152
+ "fix": "Use the bare form: uip codedapp deploy -n <name> --folder-key <key>. It reads app.config.json and upgrades in place. Confirm via stdout 'App upgraded successfully' + HTTP 200, not the local config file.",
153
+ "proven_on": "1.0.7",
154
+ "resolved_in": null,
155
+ "severity": "low",
156
+ "references": ["docs/submission/PRODUCT-FEEDBACK.md", "CLAUDE.md"]
157
+ },
158
+ {
159
+ "id": "PKG-ENTRYPOINT-REGEN",
160
+ "kind": "packaging",
161
+ "title": "solution pack regenerates entry-point IDs and breaks upgrades",
162
+ "surface": "uip solution pack / deploy upgrade",
163
+ "symptom": "Upgrading an existing deployment fails because the solution tries to preserve old entry-point IDs that no longer exist.",
164
+ "error_signatures": ["Cannot set entry point", "was not found in package version"],
165
+ "cause": "uip solution pack regenerates entry-point IDs on every repack, so an in-place upgrade cannot match the previously deployed IDs.",
166
+ "fix": "Build a mixed-version package: keep already-deployed Agent-type processes at their stable version and bump only non-Agent projects, swapping payloads and bumping packageVersion + a fresh packageVersionKey.",
167
+ "proven_on": "1.0.32",
168
+ "resolved_in": null,
169
+ "severity": "high",
170
+ "references": ["docs/submission/PRODUCT-FEEDBACK.md"]
171
+ },
172
+ {
173
+ "id": "CG-MD-SKIP",
174
+ "kind": "context-grounding",
175
+ "title": "Context Grounding silently skips .md files",
176
+ "surface": "Context Grounding ingestion",
177
+ "symptom": "Ingesting a Markdown corpus reports Successful, but searches return zero hits.",
178
+ "error_signatures": [],
179
+ "cause": "Markdown is not a supported ingestion format; .md files are skipped silently while the job still reports success.",
180
+ "fix": "Convert the corpus to .txt (or .pdf/.docx) before ingestion, and verify retrieval returns hits after seeding.",
181
+ "proven_on": "1.0.x",
182
+ "resolved_in": null,
183
+ "severity": "medium",
184
+ "references": ["scripts/seed_data_fabric.py", "docs/submission/PRODUCT-FEEDBACK.md"]
185
+ }
186
+ ]
187
+ }
@@ -0,0 +1,150 @@
1
+ """The versioned knowledge layer: load, query, and freshness-filter footgun entries.
2
+
3
+ Each entry records the platform/CLI version it was proven on and an optional
4
+ ``resolved_in`` version. When UiPath fixes a footgun, set ``resolved_in`` and the
5
+ entry self-deprecates into history instead of misinforming — the freshness risk
6
+ becomes a feature (see the requirements doc, KD6).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from dataclasses import dataclass, replace
13
+ from importlib.resources import files
14
+ from pathlib import Path
15
+
16
+ _DATA_PACKAGE = "maestro_case_kit.data"
17
+ _DATA_FILE = "knowledge.json"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class KnowledgeEntry:
22
+ """One discovered Maestro Case / Data Fabric / Action Center footgun."""
23
+
24
+ id: str
25
+ kind: str
26
+ title: str
27
+ surface: str
28
+ symptom: str
29
+ error_signatures: tuple[str, ...]
30
+ cause: str
31
+ fix: str
32
+ proven_on: str
33
+ severity: str
34
+ references: tuple[str, ...]
35
+ resolved_in: str | None = None
36
+
37
+ @property
38
+ def is_active(self) -> bool:
39
+ """True until UiPath ships a fix and ``resolved_in`` is set."""
40
+ return self.resolved_in is None
41
+
42
+ def to_dict(self) -> dict[str, object]:
43
+ return {
44
+ "id": self.id,
45
+ "kind": self.kind,
46
+ "title": self.title,
47
+ "surface": self.surface,
48
+ "symptom": self.symptom,
49
+ "error_signatures": list(self.error_signatures),
50
+ "cause": self.cause,
51
+ "fix": self.fix,
52
+ "proven_on": self.proven_on,
53
+ "resolved_in": self.resolved_in,
54
+ "severity": self.severity,
55
+ "references": list(self.references),
56
+ }
57
+
58
+
59
+ def _normalize(text: str) -> str:
60
+ return " ".join(text.casefold().split())
61
+
62
+
63
+ def _read_raw(path: Path | None) -> dict[str, object]:
64
+ if path is None:
65
+ text = files(_DATA_PACKAGE).joinpath(_DATA_FILE).read_text(encoding="utf-8")
66
+ else:
67
+ text = Path(path).read_text(encoding="utf-8")
68
+ return json.loads(text)
69
+
70
+
71
+ def load_entries(path: Path | None = None) -> list[KnowledgeEntry]:
72
+ """Load every knowledge entry from the bundled data file (or a given path)."""
73
+ raw = _read_raw(path)
74
+ items = raw["entries"]
75
+ assert isinstance(items, list)
76
+ entries: list[KnowledgeEntry] = []
77
+ for item in items:
78
+ entries.append(
79
+ KnowledgeEntry(
80
+ id=item["id"],
81
+ kind=item["kind"],
82
+ title=item["title"],
83
+ surface=item["surface"],
84
+ symptom=item["symptom"],
85
+ error_signatures=tuple(item.get("error_signatures", [])),
86
+ cause=item["cause"],
87
+ fix=item["fix"],
88
+ proven_on=item["proven_on"],
89
+ severity=item["severity"],
90
+ references=tuple(item.get("references", [])),
91
+ resolved_in=item.get("resolved_in"),
92
+ )
93
+ )
94
+ return entries
95
+
96
+
97
+ def replace_resolved(entry: KnowledgeEntry, version: str) -> KnowledgeEntry:
98
+ """Return a copy of ``entry`` marked resolved in ``version`` (test/curation helper)."""
99
+ return replace(entry, resolved_in=version)
100
+
101
+
102
+ def active_entries(entries: list[KnowledgeEntry] | None = None) -> list[KnowledgeEntry]:
103
+ """Entries that still bite the current platform (``resolved_in`` unset)."""
104
+ pool = load_entries() if entries is None else entries
105
+ return [e for e in pool if e.is_active]
106
+
107
+
108
+ def find_by_error(
109
+ query: str,
110
+ entries: list[KnowledgeEntry] | None = None,
111
+ include_resolved: bool = False,
112
+ ) -> list[KnowledgeEntry]:
113
+ """Match a raw error signature against entries' ``error_signatures``."""
114
+ pool = load_entries() if entries is None else entries
115
+ q = _normalize(query)
116
+ if not q:
117
+ return []
118
+ hits: list[KnowledgeEntry] = []
119
+ for e in pool:
120
+ if not include_resolved and not e.is_active:
121
+ continue
122
+ for sig in e.error_signatures:
123
+ ns = _normalize(sig)
124
+ if ns and (q in ns or ns in q):
125
+ hits.append(e)
126
+ break
127
+ return hits
128
+
129
+
130
+ def find(
131
+ query: str,
132
+ entries: list[KnowledgeEntry] | None = None,
133
+ include_resolved: bool = False,
134
+ ) -> list[KnowledgeEntry]:
135
+ """Error-signature matches first, then a keyword fallback across entry text."""
136
+ pool = load_entries() if entries is None else entries
137
+ q = _normalize(query)
138
+ if not q:
139
+ return []
140
+ candidates = pool if include_resolved else [e for e in pool if e.is_active]
141
+ sig_hits = find_by_error(query, candidates, include_resolved=include_resolved)
142
+ seen = {e.id for e in sig_hits}
143
+ keyword_hits: list[KnowledgeEntry] = []
144
+ for e in candidates:
145
+ if e.id in seen:
146
+ continue
147
+ blob = _normalize(" ".join([e.id, e.title, e.symptom, e.cause, e.fix, e.surface]))
148
+ if q in blob:
149
+ keyword_hits.append(e)
150
+ return sig_hits + keyword_hits
@@ -0,0 +1,94 @@
1
+ """A dependency-free MCP server over stdio (newline-delimited JSON-RPC 2.0).
2
+
3
+ Implements the three methods an agent host needs — initialize, tools/list,
4
+ tools/call — over the shared tool registry. No third-party MCP SDK required, so
5
+ the server runs anywhere Python does and stays testable in CI. Register with a
6
+ Claude Code / OpenClaw host as: `maestro-case-mcp` (stdio).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import sys
13
+ from typing import IO
14
+
15
+ from . import __version__, tools
16
+
17
+ PROTOCOL_VERSION = "2025-06-18"
18
+ SERVER_NAME = "maestro-case-kit"
19
+
20
+
21
+ def _result(request_id: object, result: dict) -> dict:
22
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
23
+
24
+
25
+ def _error(request_id: object, code: int, message: str) -> dict:
26
+ return {"jsonrpc": "2.0", "id": request_id, "error": {"code": code, "message": message}}
27
+
28
+
29
+ def handle_request(request: dict) -> dict | None:
30
+ """Dispatch one JSON-RPC request. Returns None for notifications (no id)."""
31
+ method = request.get("method")
32
+ request_id = request.get("id")
33
+ if request_id is None:
34
+ return None # notification — no response
35
+
36
+ if method == "initialize":
37
+ return _result(
38
+ request_id,
39
+ {
40
+ "protocolVersion": PROTOCOL_VERSION,
41
+ "capabilities": {"tools": {}},
42
+ "serverInfo": {"name": SERVER_NAME, "version": __version__},
43
+ },
44
+ )
45
+
46
+ if method == "tools/list":
47
+ listed = [
48
+ {"name": t.name, "description": t.description, "inputSchema": t.input_schema}
49
+ for t in tools.TOOLS
50
+ ]
51
+ return _result(request_id, {"tools": listed})
52
+
53
+ if method == "tools/call":
54
+ params = request.get("params") or {}
55
+ name = params.get("name", "")
56
+ arguments = params.get("arguments") or {}
57
+ try:
58
+ output = tools.run_tool(name, arguments)
59
+ return _result(
60
+ request_id,
61
+ {"content": [{"type": "text", "text": json.dumps(output, indent=2)}], "isError": False},
62
+ )
63
+ except Exception as exc: # tool-level failure -> isError, not a protocol error
64
+ return _result(
65
+ request_id,
66
+ {"content": [{"type": "text", "text": f"{type(exc).__name__}: {exc}"}], "isError": True},
67
+ )
68
+
69
+ return _error(request_id, -32601, f"method not found: {method}")
70
+
71
+
72
+ def serve(stdin: IO[str], stdout: IO[str]) -> None:
73
+ """Read newline-delimited JSON-RPC from stdin; write responses to stdout."""
74
+ for line in stdin:
75
+ line = line.strip()
76
+ if not line:
77
+ continue
78
+ try:
79
+ request = json.loads(line)
80
+ except json.JSONDecodeError:
81
+ continue
82
+ response = handle_request(request)
83
+ if response is not None:
84
+ stdout.write(json.dumps(response) + "\n")
85
+ stdout.flush()
86
+
87
+
88
+ def main() -> int:
89
+ serve(sys.stdin, sys.stdout)
90
+ return 0
91
+
92
+
93
+ if __name__ == "__main__": # pragma: no cover
94
+ raise SystemExit(main())
@@ -0,0 +1,106 @@
1
+ """Agent-native tool registry — one definition of the tool surface, shared by the
2
+ CLI and the MCP server. Each tool has a JSON input schema and a handler that
3
+ returns a list of plain dicts (knowledge entries or lint findings).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass
9
+ from typing import Callable
10
+
11
+ from . import knowledge, validators
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class Tool:
16
+ name: str
17
+ description: str
18
+ input_schema: dict
19
+ handler: Callable[[dict], list[dict]]
20
+
21
+
22
+ def _explain(args: dict) -> list[dict]:
23
+ hits = knowledge.find(str(args["query"]), include_resolved=bool(args.get("include_resolved", False)))
24
+ return [e.to_dict() for e in hits]
25
+
26
+
27
+ def _lint(args: dict) -> list[dict]:
28
+ return [f.to_dict() for f in validators.lint_caseplan(str(args["caseplan_dir"]))]
29
+
30
+
31
+ def _check_spawn(args: dict) -> list[dict]:
32
+ return [f.to_dict() for f in validators.check_spawn_fanout(str(args["caseplan_dir"]))]
33
+
34
+
35
+ def _check_df(args: dict) -> list[dict]:
36
+ return [f.to_dict() for f in validators.validate_df_entity(str(args["spec_path"]))]
37
+
38
+
39
+ def _caseplan_dir_schema() -> dict:
40
+ return {
41
+ "type": "object",
42
+ "properties": {
43
+ "caseplan_dir": {
44
+ "type": "string",
45
+ "description": "Path to a directory containing caseplan.json.",
46
+ }
47
+ },
48
+ "required": ["caseplan_dir"],
49
+ }
50
+
51
+
52
+ TOOLS: list[Tool] = [
53
+ Tool(
54
+ "maestro_case_explain",
55
+ "Explain a UiPath Maestro Case error code or footgun from the offline, "
56
+ "version-stamped knowledge layer. Accepts an error code/signature or a keyword.",
57
+ {
58
+ "type": "object",
59
+ "properties": {
60
+ "query": {"type": "string", "description": "Error code/signature or keyword."},
61
+ "include_resolved": {
62
+ "type": "boolean",
63
+ "description": "Include entries marked resolved in a later platform version.",
64
+ },
65
+ },
66
+ "required": ["query"],
67
+ },
68
+ _explain,
69
+ ),
70
+ Tool(
71
+ "maestro_case_lint",
72
+ "Statically lint a caseplan directory for known footguns (stale .bpmn, missing "
73
+ "start event, duplicate output vars, bad expression prefixes). Offline, no login.",
74
+ _caseplan_dir_schema(),
75
+ _lint,
76
+ ),
77
+ Tool(
78
+ "maestro_case_check_spawn",
79
+ "Flag =datafabric.qem expressions in spawn inputs, which fail at runtime (400300).",
80
+ _caseplan_dir_schema(),
81
+ _check_spawn,
82
+ ),
83
+ Tool(
84
+ "maestro_case_check_df",
85
+ "Lint a Data Fabric entity/field spec for the underscore silent-drop and the "
86
+ "reserved 'id' field traps.",
87
+ {
88
+ "type": "object",
89
+ "properties": {
90
+ "spec_path": {"type": "string", "description": "Path to a JSON entity spec."}
91
+ },
92
+ "required": ["spec_path"],
93
+ },
94
+ _check_df,
95
+ ),
96
+ ]
97
+
98
+ _BY_NAME: dict[str, Tool] = {t.name: t for t in TOOLS}
99
+
100
+
101
+ def get_tool(name: str) -> Tool:
102
+ return _BY_NAME[name]
103
+
104
+
105
+ def run_tool(name: str, args: dict) -> list[dict]:
106
+ return get_tool(name).handler(args)
@@ -0,0 +1,264 @@
1
+ """Static, credential-free validators for a UiPath Maestro Case caseplan.
2
+
3
+ These run offline against a caseplan directory (caseplan.json + the compiled
4
+ caseplan.json.bpmn) and are safe in CI — no UiPath login. The rules generalize the
5
+ deterministic canvas-round-trip drops that bite Maestro Case authors; rule ids map
6
+ to knowledge-layer entries so `maestro-case explain <rule>` gives the full recipe.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import re
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ # Allowed V20 caseplan expression prefixes. Seeded from the documented conventions
17
+ # and reconciled against the prefixes that actually appear in live caseplans
18
+ # (=jsonString is real and was missing from the docs). =datafabric is a valid
19
+ # family even though the =datafabric.qem: spawn form fails at runtime — that
20
+ # runtime trap is caught by check_spawn_fanout, not by a prefix rule.
21
+ ALLOWED_EXPR_PREFIXES: tuple[str, ...] = (
22
+ "=vars",
23
+ "=js:",
24
+ "=metadata",
25
+ "=bindings",
26
+ "=datafabric",
27
+ "=response",
28
+ "=string",
29
+ "=jsonString",
30
+ )
31
+
32
+ _LEGACY_EXPR = re.compile(r"^\$\w")
33
+ _LOOKS_LIKE_EXPR = re.compile(r"^=[A-Za-z]")
34
+
35
+
36
+ @dataclass(frozen=True)
37
+ class Finding:
38
+ rule_id: str
39
+ severity: str
40
+ message: str
41
+ location: str | None = None
42
+ entry_id: str | None = None
43
+
44
+ def to_dict(self) -> dict[str, object]:
45
+ return {
46
+ "rule_id": self.rule_id,
47
+ "severity": self.severity,
48
+ "message": self.message,
49
+ "location": self.location,
50
+ "entry_id": self.entry_id,
51
+ }
52
+
53
+
54
+ def _walk_strings(node: object, path: str = "$"):
55
+ """Yield (json_path, string_value) for every string leaf in the structure."""
56
+ if isinstance(node, str):
57
+ yield path, node
58
+ elif isinstance(node, dict):
59
+ for key, value in node.items():
60
+ yield from _walk_strings(value, f"{path}.{key}")
61
+ elif isinstance(node, list):
62
+ for index, value in enumerate(node):
63
+ yield from _walk_strings(value, f"{path}[{index}]")
64
+
65
+
66
+ def _walk_output_lists(node: object, path: str = "$"):
67
+ """Yield (json_path, outputs_list) for every list found under an 'outputs' key."""
68
+ if isinstance(node, dict):
69
+ for key, value in node.items():
70
+ child_path = f"{path}.{key}"
71
+ if key == "outputs" and isinstance(value, list):
72
+ yield child_path, value
73
+ yield from _walk_output_lists(value, child_path)
74
+ elif isinstance(node, list):
75
+ for index, value in enumerate(node):
76
+ yield from _walk_output_lists(value, f"{path}[{index}]")
77
+
78
+
79
+ def _check_bpmn(caseplan_path: Path, bpmn_path: Path) -> list[Finding]:
80
+ findings: list[Finding] = []
81
+ if not bpmn_path.is_file():
82
+ findings.append(
83
+ Finding(
84
+ "MC-NO-BPMN",
85
+ "medium",
86
+ "No compiled caseplan.json.bpmn next to caseplan.json — the engine "
87
+ "runs the compiled .bpmn, so edits may be inert until the canvas "
88
+ "regenerates it. Cannot verify compiled state.",
89
+ location=str(bpmn_path),
90
+ entry_id="MC-BPMN-STALE",
91
+ )
92
+ )
93
+ return findings
94
+ if caseplan_path.stat().st_mtime > bpmn_path.stat().st_mtime:
95
+ findings.append(
96
+ Finding(
97
+ "MC-BPMN-STALE",
98
+ "high",
99
+ "caseplan.json is newer than its compiled caseplan.json.bpmn — edits "
100
+ "are likely inert at runtime. Regenerate the .bpmn via the Studio Web "
101
+ "canvas before pack/deploy.",
102
+ location=str(bpmn_path),
103
+ entry_id="MC-BPMN-STALE",
104
+ )
105
+ )
106
+ if bpmn_path.read_text(encoding="utf-8").count("<bpmn:startEvent") == 0:
107
+ findings.append(
108
+ Finding(
109
+ "MC-NO-START-EVENT",
110
+ "high",
111
+ "Compiled .bpmn has no <bpmn:startEvent> — the case will not auto-walk "
112
+ "from its first stage. The canvas commonly drops the mainline start "
113
+ "event on round-trip; restore it before deploy.",
114
+ location=str(bpmn_path),
115
+ entry_id="MC-BPMN-STALE",
116
+ )
117
+ )
118
+ return findings
119
+
120
+
121
+ def _check_outputs(caseplan: object) -> list[Finding]:
122
+ findings: list[Finding] = []
123
+ for path, outputs in _walk_output_lists(caseplan):
124
+ seen: set[str] = set()
125
+ for output in outputs:
126
+ if not isinstance(output, dict):
127
+ continue
128
+ var = output.get("var")
129
+ if isinstance(var, str) and var:
130
+ if var in seen:
131
+ findings.append(
132
+ Finding(
133
+ "MC-DUP-OUTPUT-VAR",
134
+ "high",
135
+ f"Duplicate output variable {var!r} on one task — V20 rejects "
136
+ f"duplicate output vars (the canvas auto-adds a dup 'error' "
137
+ f"output on round-trip). De-duplicate or suffix from 2.",
138
+ location=path,
139
+ )
140
+ )
141
+ seen.add(var)
142
+ return findings
143
+
144
+
145
+ # Output/variable binding keys hold =<FieldName> references (e.g. source="=Error",
146
+ # target="=reviewerId"), not value expressions — exempt them from the prefix rule.
147
+ _BINDING_KEYS = (".source", ".target")
148
+
149
+
150
+ def _check_expressions(caseplan: object) -> list[Finding]:
151
+ findings: list[Finding] = []
152
+ for path, value in _walk_strings(caseplan):
153
+ if _LEGACY_EXPR.match(value):
154
+ findings.append(
155
+ Finding(
156
+ "MC-LEGACY-EXPR",
157
+ "medium",
158
+ f"Legacy $-prefixed expression {value!r} — V20 uses =-prefixed "
159
+ f"expressions (e.g. =js:vars.x), not $vars.x.",
160
+ location=path,
161
+ )
162
+ )
163
+ elif path.endswith(_BINDING_KEYS):
164
+ continue
165
+ elif _LOOKS_LIKE_EXPR.match(value) and not value.startswith(ALLOWED_EXPR_PREFIXES):
166
+ findings.append(
167
+ Finding(
168
+ "MC-BAD-EXPR-PREFIX",
169
+ "medium",
170
+ f"Expression {value!r} does not start with an allowed V20 prefix "
171
+ f"({', '.join(ALLOWED_EXPR_PREFIXES)}).",
172
+ location=path,
173
+ )
174
+ )
175
+ return findings
176
+
177
+
178
+ def _load_caseplan(caseplan_dir: Path | str) -> tuple[object, Path]:
179
+ directory = Path(caseplan_dir)
180
+ caseplan_path = directory / "caseplan.json"
181
+ if not caseplan_path.is_file():
182
+ raise FileNotFoundError(f"no caseplan.json in {directory}")
183
+ return json.loads(caseplan_path.read_text(encoding="utf-8")), caseplan_path
184
+
185
+
186
+ def lint_caseplan(caseplan_dir: Path | str) -> list[Finding]:
187
+ """Lint a caseplan directory. Raises FileNotFoundError if caseplan.json is absent."""
188
+ caseplan, caseplan_path = _load_caseplan(caseplan_dir)
189
+ bpmn_path = caseplan_path.parent / "caseplan.json.bpmn"
190
+ findings: list[Finding] = []
191
+ findings.extend(_check_bpmn(caseplan_path, bpmn_path))
192
+ findings.extend(_check_outputs(caseplan))
193
+ findings.extend(_check_expressions(caseplan))
194
+ return findings
195
+
196
+
197
+ def check_spawn_fanout(caseplan_dir: Path | str) -> list[Finding]:
198
+ """Flag =datafabric.qem expressions in spawn inputs — they fail at runtime (400300)."""
199
+ caseplan, _ = _load_caseplan(caseplan_dir)
200
+ findings: list[Finding] = []
201
+ for path, value in _walk_strings(caseplan):
202
+ if "=datafabric.qem" in value:
203
+ findings.append(
204
+ Finding(
205
+ "MC-SPAWN-QEM-400300",
206
+ "high",
207
+ f"=datafabric.qem expression {value!r} fails at runtime in spawn "
208
+ f"inputs (400300, 'Syntax error at index 4'). Resolve the value at "
209
+ f"authoring time and pass a literal slug instead.",
210
+ location=path,
211
+ entry_id="MC-SPAWN-QEM-400300",
212
+ )
213
+ )
214
+ return findings
215
+
216
+
217
+ def _to_camel(name: str) -> str:
218
+ parts = name.split("_")
219
+ return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:] if p)
220
+
221
+
222
+ def _extract_field_names(spec: object) -> list[str]:
223
+ fields = spec.get("fields", []) if isinstance(spec, dict) else spec
224
+ names: list[str] = []
225
+ if isinstance(fields, list):
226
+ for field in fields:
227
+ if isinstance(field, str):
228
+ names.append(field)
229
+ elif isinstance(field, dict) and isinstance(field.get("name"), str):
230
+ names.append(field["name"])
231
+ return names
232
+
233
+
234
+ def validate_df_entity(spec_path: Path | str) -> list[Finding]:
235
+ """Lint a Data Fabric entity/field spec for silent-drop and reserved-name traps."""
236
+ path = Path(spec_path)
237
+ if not path.is_file():
238
+ raise FileNotFoundError(f"no spec file at {path}")
239
+ spec = json.loads(path.read_text(encoding="utf-8"))
240
+ findings: list[Finding] = []
241
+ for name in _extract_field_names(spec):
242
+ if name == "id":
243
+ findings.append(
244
+ Finding(
245
+ "DF-RESERVED-ID",
246
+ "medium",
247
+ "Field 'id' collides with the system Id primary key and is rejected; "
248
+ "use 'slug' (or another business-key name) instead.",
249
+ location=name,
250
+ entry_id="DF-RESERVED-ID",
251
+ )
252
+ )
253
+ if "_" in name:
254
+ findings.append(
255
+ Finding(
256
+ "DF-UNDERSCORE-DROP",
257
+ "high",
258
+ f"Field {name!r} contains an underscore and is silently dropped on "
259
+ f"insert (no error surfaced). Use {_to_camel(name)!r} instead.",
260
+ location=name,
261
+ entry_id="DF-UNDERSCORE-DROP",
262
+ )
263
+ )
264
+ return findings
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: maestro-case-kit
3
+ Version: 0.1.0
4
+ Summary: Agent-native knowledge layer + offline, credential-free static validators for UiPath Maestro Case footguns.
5
+ License: MIT
6
+ Keywords: agent-skills,case-management,linter,maestro,mcp,uipath
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Maestro Case Kit
11
+
12
+ Offline, credential-free **knowledge + static validators** for UiPath **Maestro Case /
13
+ Data Fabric / Action Center** footguns. One define-once source, four artifacts: a Go-free
14
+ Python **CLI**, an **MCP server**, a Claude Code **skill**, and an OpenClaw **skill**.
15
+
16
+ > Built from behaviors discovered while running a multi-stakeholder crisis case end-to-end
17
+ > on UiPath Automation Cloud. The orchestration tier *above* the canvas is unserved by
18
+ > official tooling; this kit makes the hard-won knowledge installable and agent-native.
19
+
20
+ ## Why
21
+
22
+ - UiPath's coding-agent MCP is a single catch-all `run_command` shell — not typed tools.
23
+ - Maestro Case error codes (`400300`, `160009`, `170015`, ...) return zero search results.
24
+ - Caseplan edits can be silently inert; Data Fabric fields can silently vanish on insert.
25
+
26
+ This kit encodes those footguns as a **version-stamped knowledge layer** + **CI linters**
27
+ that run with **no UiPath login**.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pipx install maestro-case-kit # CLI: maestro-case ; MCP: maestro-case-mcp
33
+ ```
34
+
35
+ ## Use
36
+
37
+ ```bash
38
+ maestro-case explain 400300 # error code -> proven cause + fix (offline)
39
+ maestro-case lint path/to/caseplan-dir # static V20 lint (stale .bpmn, no start event, ...)
40
+ maestro-case check-spawn path/to/caseplan-dir # =datafabric.qem in spawn inputs (400300)
41
+ maestro-case check-df entity-spec.json # Data Fabric underscore-drop / reserved id
42
+ ```
43
+
44
+ Every command takes `--json` and exits non-zero when it has a finding, so it drops
45
+ straight into CI. See [`SKILL.md`](SKILL.md) for agent-host usage and recipes.
46
+
47
+ ## MCP server
48
+
49
+ `maestro-case-mcp` speaks newline-delimited JSON-RPC over stdio (no third-party MCP SDK
50
+ dependency) and exposes typed tools: `maestro_case_explain`, `maestro_case_lint`,
51
+ `maestro_case_check_spawn`, `maestro_case_check_df`. Register it with any MCP host.
52
+
53
+ ## One source, many harnesses
54
+
55
+ `SKILL.md` is the define-once skill source. Fan it out to other coding-agent runtimes
56
+ (Cursor, Codex, Gemini, Copilot, OpenClaw/ClawHub) with a skills converter — e.g.
57
+ `/polyskill` or `npx skills add`. The CLI and MCP server are generated from the same
58
+ shared tool registry (`maestro_case_kit/tools.py`), so a fix lands once and every surface
59
+ inherits it.
60
+
61
+ ## Knowledge entries & contributions
62
+
63
+ Entries live in `maestro_case_kit/data/knowledge.json`, each stamped with the version it
64
+ was proven on and an optional `resolved_in`. Contributions run through an automated
65
+ IP-safety + schema gate (see `CONTRIBUTING.md`). License: MIT.
@@ -0,0 +1,12 @@
1
+ maestro_case_kit/__init__.py,sha256=XRvRISS5KvcGp0qkmmWwgyPQx-GVdq0Qg3B8gT3-_Yo,394
2
+ maestro_case_kit/cli.py,sha256=7ZzoOHE7GkseM2ZBdy1IthowkU1btQVx6I2ykQHx_SE,6880
3
+ maestro_case_kit/contribution.py,sha256=LkRodpLoATWdtLmb2km9vcCrQmsY7jKwOSMPToZVZIo,3005
4
+ maestro_case_kit/knowledge.py,sha256=FWUN-samNj8HtkxWQzlJJhV9OJxcWoJNN9kKAV7Sq9U,4923
5
+ maestro_case_kit/mcp_server.py,sha256=-ajM2ujCfEv3rkh0JTjeTyDgvoUuRkGCHzjzsn8KTlw,3041
6
+ maestro_case_kit/tools.py,sha256=Ymuq7BgTQMOB4VGsHcjSADhDNrsHKAWGohhIzkklAhE,3190
7
+ maestro_case_kit/validators.py,sha256=7Y9_Nfc0-dnb5CwmfaOMZYnK4983ocXKkWHkllfkbbc,10005
8
+ maestro_case_kit/data/knowledge.json,sha256=hvu6Ey3XWVz0_8P9ebf3JulPGhmod652Bl90axJTqkg,10938
9
+ maestro_case_kit-0.1.0.dist-info/METADATA,sha256=16As3wzE6fAYJUjr4ieWMLYS1aDiWJ-Df4OPvgmUoWc,2868
10
+ maestro_case_kit-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ maestro_case_kit-0.1.0.dist-info/entry_points.txt,sha256=VBEKF4kwpfhvMQyTz-prBmszS6kM5rZfV44MbvIPrrM,111
12
+ maestro_case_kit-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ maestro-case = maestro_case_kit.cli:main
3
+ maestro-case-mcp = maestro_case_kit.mcp_server:main