ordel-cli 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/PKG-INFO +1 -1
  2. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/context.py +14 -3
  3. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/explore.py +10 -0
  4. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/mcp_server.py +13 -3
  5. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/record.py +2 -0
  6. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/scenarios.py +11 -0
  7. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/session_driver.cjs +28 -1
  8. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/pyproject.toml +1 -1
  9. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/.gitignore +0 -0
  10. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/CATALOG.md +0 -0
  11. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/LICENSE +0 -0
  12. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/PRIVACY.md +0 -0
  13. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/README.md +0 -0
  14. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/SKILL.md +0 -0
  15. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/__init__.py +0 -0
  16. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/adopt.py +0 -0
  17. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/branding.py +0 -0
  18. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/capture.cjs +0 -0
  19. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/capture_extract.cjs +0 -0
  20. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/catalog.py +0 -0
  21. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/cli.py +0 -0
  22. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/coverage.py +0 -0
  23. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/doctor.py +0 -0
  24. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/heal_service.py +0 -0
  25. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/human_record.cjs +0 -0
  26. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/ingest.py +0 -0
  27. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/mcp_supervisor.cjs +0 -0
  28. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/ordel_config.cjs +0 -0
  29. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/ordel_evidence.cjs +0 -0
  30. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/pom.py +0 -0
  31. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/py.typed +0 -0
  32. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/qa_plan.py +0 -0
  33. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/record.cjs +0 -0
  34. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/references/communication-and-harnesses.md +0 -0
  35. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/references/execution-and-triage.md +0 -0
  36. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/references/intake-and-readiness.md +0 -0
  37. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/references/maintenance.md +0 -0
  38. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/references/scenarios-and-oracles.md +0 -0
  39. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/references/tools.md +0 -0
  40. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/runner.py +0 -0
  41. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/session.py +0 -0
  42. {ordel_cli-0.4.2 → ordel_cli-0.4.3}/ordel_cli/store.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ordel-cli
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Ordel free CLI — local QA-automation for individual devs. Drives the deterministic ordel-engine and exposes it to a BYO coding agent over MCP. No account, no DB, no Ordel LLM.
5
5
  Project-URL: Homepage, https://ordel.io
6
6
  Author: Ordel
@@ -97,6 +97,17 @@ def assess_context(store: OrdelStore) -> dict[str, Any]:
97
97
  if low.endswith(_SRC_SUFFIXES):
98
98
  source_files += 1
99
99
 
100
- return _assess(ContextScan(pages=pages, flows=flows, runs=runs, functional_specs=functional,
101
- ordel_specs=ordel, unit_specs=unit, unknown_specs=unknown,
102
- page_objects=page_objects, has_ci=has_ci, source_files=source_files))
100
+ res = _assess(ContextScan(pages=pages, flows=flows, runs=runs, functional_specs=functional,
101
+ ordel_specs=ordel, unit_specs=unit, unknown_specs=unknown,
102
+ page_objects=page_objects, has_ci=has_ci, source_files=source_files))
103
+ # FINDINGS-042 #3: get_app_context said initialized:false while this said "ok, MAP next" in
104
+ # the same folder, and mapping then failed. Both now say the same thing.
105
+ res["initialized"] = store.exists
106
+ if not store.exists:
107
+ before = str(res.get("required_next_phase", ""))
108
+ res["required_next_phase"] = ("INIT — this folder has no .ordel/ yet: run `ordel init` "
109
+ "here first (it creates .ordel/ and ORDEL.md); nothing can be "
110
+ "mapped, recorded or run until then")
111
+ if isinstance(res.get("ascii"), str) and before:
112
+ res["ascii"] = res["ascii"].replace(before, res["required_next_phase"])
113
+ return res
@@ -140,6 +140,16 @@ def persist_capture(store: OrdelStore, doc: dict[str, Any], page_id: str,
140
140
  The fingerprint transform is the pure ``fingerprints_from_capture``; this adapter only
141
141
  writes the result to the store."""
142
142
  fps, url, title = fingerprints_from_capture(doc, fallback_url=fallback_url)
143
+ # FINDINGS-042 #1: after a failed navigation the browser sits on chrome-error://chromewebdata/;
144
+ # mapping it under the agent's page_id took that id from the real page, which was then saved
145
+ # under a site-qualified name, and coverage counted an error page.
146
+ scheme = url.split(":", 1)[0].lower() if ":" in url else ""
147
+ if scheme not in ("http", "https", "file"):
148
+ return {"status": "not_mapped", "page_id": page_id, "url": url, "title": title,
149
+ "elements": 0,
150
+ "note": (f"nothing was mapped: the browser is on {url or 'no page'}, not a page of "
151
+ "the app (a navigation failed, or nothing is loaded yet); goto a working "
152
+ "address and capture again")}
143
153
  # A capture replaces its page's elements. When the id already belongs to ANOTHER site (a
144
154
  # second app's root is HomePage too), replacing it would silently discard that site's map:
145
155
  # save under a site-qualified id and say so. The same site re-captured still refreshes.
@@ -89,7 +89,7 @@ _FAILURE_STATUSES: frozenset[str] = frozenset({
89
89
  "unknown_element", "record_failed", "capture_failed", "http_error", "no_playwright",
90
90
  "no_node", "timeout", "no_session", "session_error", "nav_failed", "failed",
91
91
  "circuit_open", "error", "exists", "action_failed", "ambiguous", "not_actionable",
92
- "invalid_target", "flow_rejected",
92
+ "invalid_target", "flow_rejected", "not_mapped",
93
93
  })
94
94
 
95
95
 
@@ -911,6 +911,9 @@ def build_server(root: str | Path = ".") -> FastMCP:
911
911
  Returns {page_id, url, title, elements}. No LLM."""
912
912
  from ordel_cli.explore import page_id_from_url, persist_capture
913
913
  def _do() -> dict[str, Any]:
914
+ if not store.exists:
915
+ return {"status": "not_initialized",
916
+ "note": "this folder has no .ordel/ yet: run `ordel init` here first"}
914
917
  doc = _sess().capture()
915
918
  if not doc.get("ok", True):
916
919
  return {"status": "session_error", "note": doc.get("error", "capture failed")}
@@ -963,14 +966,21 @@ def build_server(root: str | Path = ".") -> FastMCP:
963
966
  if isinstance(results, tuple) and len(results) == 2 and isinstance(results[1], dict):
964
967
  content, structured = results
965
968
  blocks = list(content)
969
+ failed = _is_failure(name, structured)
970
+ if failed and not structured.get("ascii"):
971
+ # FINDINGS-042 #4: a failure is a milestone the person must see too; the skill's
972
+ # checkpoints paste `ascii`, which only successes carried.
973
+ why = str(structured.get("note") or structured.get("error") or "").strip()
974
+ first = why.splitlines()[0][:300] if why else ""
975
+ structured = {**structured, "ascii": f"✗ {name}: {structured.get('status') or 'failed'}"
976
+ + (f" - {first}" if first else "")}
966
977
  summary = structured.get("ascii")
967
978
  if isinstance(summary, str) and summary:
968
979
  # The step summary as its own first, plain-text block: a host that shows tool
969
980
  # output shows the map or the verdicts, not an escaped JSON line (the 0.4.0
970
981
  # evaluation's F5: the summary reached the agent but not the person).
971
982
  blocks = [TextContent(type="text", text=summary), *blocks]
972
- return CallToolResult(content=blocks, structuredContent=structured,
973
- isError=_is_failure(name, structured))
983
+ return CallToolResult(content=blocks, structuredContent=structured, isError=failed)
974
984
  return results
975
985
 
976
986
  mcp._mcp_server.call_tool(validate_input=False)(_wire_call_tool)
@@ -484,6 +484,8 @@ def record_human(store: OrdelStore, name: str, url: str, headed: bool = True,
484
484
  # the id the page was actually saved under (another site may already hold the plain one)
485
485
  saved = persist_capture(store, {**page, "url": page_url}, page_id_from_url(page_url),
486
486
  fallback_url=page_url)
487
+ if saved.get("status") == "not_mapped":
488
+ continue
487
489
  page_ids[page_url] = str(saved["page_id"])
488
490
  for s in steps:
489
491
  u = strip_query(str(s.get("url", "")))
@@ -81,6 +81,17 @@ def _load_flow_doc(store: OrdelStore, flow_name: str) -> tuple[dict[str, Any] |
81
81
  return None, {"status": "invalid_flow",
82
82
  "note": f"flow '{flow_name}': step {i} field '{key}' must be a "
83
83
  f"string, number or boolean, got {type(value).__name__}"}
84
+ # FINDINGS-042 #5: a hand-written or 0.3-era flow with "transitions": 4 crashed with
85
+ # "'int' object is not iterable" when the scenarios read each step's page.
86
+ transitions = doc.get("transitions")
87
+ if transitions is not None and (not isinstance(transitions, list)
88
+ or not all(isinstance(t, dict) for t in transitions)):
89
+ return None, {"status": "invalid_flow",
90
+ "note": f"flow '{flow_name}': 'transitions' must be a list of step records "
91
+ f"(got {type(transitions).__name__}); record the flow again"}
92
+ if not isinstance(doc.get("final_url", ""), str):
93
+ return None, {"status": "invalid_flow",
94
+ "note": f"flow '{flow_name}': 'final_url' must be a string"}
84
95
  return doc, None
85
96
 
86
97
 
@@ -563,6 +563,26 @@ function requirePage(msg) {
563
563
  if (needsPage && !page) throw new Error("no open session — call 'open' first");
564
564
  }
565
565
 
566
+ // FINDINGS-042 #2: a click whose request never answered held the chain for as long as the
567
+ // server took (forever, on a hung login POST), so every later command - current_url included -
568
+ // timed out behind it and the session was wedged. Each page command now answers within this
569
+ // deadline (under the MCP side's 60 s) and frees the chain; open and close keep their own.
570
+ const STEP_DEADLINE_MS = 45000;
571
+ function withDeadline(work, cmd) {
572
+ work.catch(() => {}); // an abandoned step may still fail later: never an unhandled rejection
573
+ let timer;
574
+ const deadline = new Promise((_, reject) => {
575
+ timer = setTimeout(() => {
576
+ const e = new Error(`'${cmd}' did not finish within ${STEP_DEADLINE_MS / 1000}s: the page is still `
577
+ + 'waiting (a request that has not answered, or a page that never settles). The session is '
578
+ + 'still usable: current_url, goto another address, or browser_close.');
579
+ e.deadline = true;
580
+ reject(e);
581
+ }, STEP_DEADLINE_MS);
582
+ });
583
+ return Promise.race([work, deadline]).finally(() => clearTimeout(timer));
584
+ }
585
+
566
586
  // Serialize commands so overlapping stdin lines can't race the single page.
567
587
  let chain = Promise.resolve();
568
588
  const rl = readline.createInterface({ input: process.stdin });
@@ -574,10 +594,17 @@ rl.on('line', (line) => {
574
594
  try { msg = JSON.parse(text); } catch (e) { write({ ok: false, error: 'bad json' }); return; }
575
595
  try {
576
596
  requirePage(msg);
577
- const res = await handle(msg);
597
+ const bounded = msg.cmd !== 'open' && msg.cmd !== 'close';
598
+ const res = bounded ? await withDeadline(handle(msg), msg.cmd) : await handle(msg);
578
599
  write({ id: msg.id, ...res });
579
600
  if (msg.cmd === 'close') await shutdown(0);
580
601
  } catch (e) {
602
+ if (e && e.deadline) { // no element diagnosis: probing the busy page could hang again
603
+ let url = '';
604
+ try { url = page ? page.url() : ''; } catch (_) { /* page gone */ }
605
+ write({ id: msg.id, ok: false, error_kind: 'no_response', url, error: e.message });
606
+ return;
607
+ }
581
608
  const res = ELEMENT_CMDS.has(msg.cmd) && page ? await diagnose(msg, e)
582
609
  : { ok: false, error: String((e && e.message) || e).replace(ANSI, '') };
583
610
  write({ id: msg.id, ...res });
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ordel-cli"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Ordel free CLI — local QA-automation for individual devs. Drives the deterministic ordel-engine and exposes it to a BYO coding agent over MCP. No account, no DB, no Ordel LLM."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes