Skip to content

feat(leadgen): unify lead_profile + lead_sync into a single leadgen action#74

Merged
eldonm merged 14 commits into
mainfrom
feat/leadgen
Jul 8, 2026
Merged

feat(leadgen): unify lead_profile + lead_sync into a single leadgen action#74
eldonm merged 14 commits into
mainfrom
feat/leadgen

Conversation

@eldonm

@eldonm eldonm commented Jul 7, 2026

Copy link
Copy Markdown
Member

Consolidates the separate lead_profile and lead_sync_action packages (and their skills) into one cohesive jvagent/action/leadgen harness.

What's in it

  • Unified actionengine, spec, store, sync, hooks, validators, contract validation (_validate_contract), and a thin-harness pattern.
  • DocsREADME + docs/ (frontmatter-schema, thin-harness, extending, troubleshooting).
  • Exampleexamples/example_leadgen skill template + an example leadgen_agent.
  • CLIjvagent skill create-leadgen <agent> <skill> scaffolds a leadgen skill from the template.
  • Tests — capture, spec, sync-templates, thin-harness guards, validators.
  • Removes the retired lead_profile / lead_sync_action packages + skills (−2586 lines).

Test plan

  • pytest tests/action/leadgen — 15 passed.
  • pre-commit run clean (black, isort, flake8, mypy, detect-secrets).

Independent of the outstanding dependency PRs (#67, #71, #72).

🤖 Generated with Claude Code

eldonm pushed a commit that referenced this pull request Jul 7, 2026
…ck-yaml

- Regenerate package-lock.json cleanly (rm + npm install) so it's fully in sync
  — the incremental install left it missing a transitive dep, so `npm ci` (what
  CI runs) failed with EUSAGE. `npm ci` now installs clean.
- Exclude auto-generated pnpm-lock.yaml from check-yaml (mirrors #74): main's
  pnpm-lock carries a duplicate key from the #69 merge that trips the strict
  parser; lockfiles shouldn't be yaml-linted. jvchat uses npm ci, not pnpm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eldonm added a commit that referenced this pull request Jul 7, 2026
* chore(jvchat): upgrade to React 19

Bump react / react-dom / @types/react / @types/react-dom to ^19 and regenerate
package-lock.json. Fixes the one React 19 breaking change in the tree: the
global `JSX` namespace was removed, so JsonViewer.tsx's `JSX.Element` return
types now import the type explicitly (`import type { JSX } from "react"`).

The @assistant-ui/*, radix-ui, @testing-library/react, lucide, etc. ecosystem
all dedupe cleanly onto react@19. jvchat build (tsc && vite build), lint
(eslint --max-warnings 0), and tests (vitest, 55 passing) are green under 19.

Supersedes dependabot #65 (partial bump — it missed @types/react-dom, causing the
install-time ERESOLVE). Closes #73.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react-19): sync package-lock (npm ci); exclude pnpm-lock from check-yaml

- Regenerate package-lock.json cleanly (rm + npm install) so it's fully in sync
  — the incremental install left it missing a transitive dep, so `npm ci` (what
  CI runs) failed with EUSAGE. `npm ci` now installs clean.
- Exclude auto-generated pnpm-lock.yaml from check-yaml (mirrors #74): main's
  pnpm-lock carries a duplicate key from the #69 merge that trips the strict
  parser; lockfiles shouldn't be yaml-linted. jvchat uses npm ci, not pnpm.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Eldon Marks <eldon.marks@v75inc.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Eldon Marks and others added 2 commits July 7, 2026 03:36
…ction

Consolidate the separate lead_profile and lead_sync_action packages (and their
skills) into one cohesive jvagent/action/leadgen harness: engine, spec, store,
sync, hooks, validators, contract validation, and a thin-harness pattern, with
README + docs (frontmatter-schema, thin-harness, extending, troubleshooting),
an example_leadgen skill template, an example leadgen_agent, and full tests.

Adds `jvagent skill create-leadgen` CLI to scaffold a leadgen skill from the
template. Removes the retired lead_profile / lead_sync_action packages and
skills (-2586 lines).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-lock from check-yaml

- Rename tests/action/leadgen/test_thin_harness_guards.py ->
  test_leadgen_thin_harness_guards.py: pytest imports test modules by basename
  (no __init__), so it collided with tests/action/interview/ of the same name
  and aborted full-suite collection ("import file mismatch"). Passed locally
  only because the leadgen dir was run in isolation.
- black/isort the leadgen test files (missed in the first pass — only source +
  cli were formatted).
- Exclude pnpm-lock.yaml from check-yaml: main's jvchat/pnpm-lock.yaml carries a
  duplicate key (@codemirror/state@6.7.0) from the #69 lockfile merge that trips
  the strict YAML parser; auto-generated lockfiles shouldn't be yaml-linted
  (detect-secrets already excludes it). Unblocks check-yaml across all PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Eldon Marks and others added 12 commits July 7, 2026 04:17
Refine leadgen sync.py and extend test_sync_templates.py; touch the example
product_inquiry_leads SKILL.md. pre-commit clean; leadgen tests (17) pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… intro coexistence

Adapt the leadgen reference agent into a multi-skill storefront and harden the
leadgen action.

Agent (examples/jvagent_app/.../leadgen_agent):
- Add contrib/storefront mock action (dummy FAQ + product catalog) and the
  product_faq / product_search skills, coexisting with leadgen on one
  orchestrator surface (gpt-4.1).
- Add jvagent/intro_interact_action opener; configure sync on the leadgen
  action in agent.yaml rather than the skill frontmatter.

Intro coexistence (jvagent/action/intro):
- Contribute the first-message greeting as a response-shaping *parameter* (HOW)
  instead of a *directive* (WHAT), so ReplyAction weaves it into the same reply
  as the substantive answer instead of emitting a second, disjoint paragraph.
  Fixes intro + orchestrator "responding in parallel".

Leadgen action (jvagent/action/leadgen):
- Sync digest dedup fix: compute_digest hashed internal keys, so the stored
  digest changed the hash and every capture re-synced (duplicate rows on
  append destinations). Exclude _-prefixed keys from the digest and from
  {profile_json} so internal state never re-syncs or leaks to a destination.
- Sync is destination-agnostic via the standard MCP interface; drop the
  google_sheets-specific framing from example and docs.
- Move sync config onto the action (sync_mode / sync_min_fields /
  sync_require_any / sync_destinations); a skill may still self-contain sync by
  declaring its own destinations.
- More aggressive contact capture: STANDING GOAL in the capture/retrieve tool
  descriptions plus a next_ask hint in tool results; value-tied gap-fill each
  turn, easing off once required fields are captured.

Tests: sync dispatch/dedup/resync/internal-key + action-sync merge, intro
parameter contract, storefront search. pre-commit + affected slices green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nce; drop stale docs

- Rewrite jvagent/action/intro/README.md: was stale (described a directive +
  PersonaAction + wrong weights + Jac comparison). Now documents the
  parameter-based first-message intro, weight -300, new_user gating, ReplyAction
  egress.
- Leadgen README/frontmatter-schema/troubleshooting: sync config lives on the
  action (agent.yaml) via sync_mode/min_fields/require_any/destinations, standard
  MCP interface (no Sheets-specific framing), proactive next_ask gap-fill,
  graceful "connector not configured" skip, digest/internal-key notes.
- Add examples/.../leadgen_agent/README.md documenting the storefront demo
  (leadgen + FAQ + product-search + intro + sync).
- actions-catalog: refresh intro (parameter/-300) and leadgen (destination-agnostic
  sync) entries.
- CHANGELOG: Added storefront demo; Changed generic sync + action-level config +
  aggressive capture + intro parameter; Fixed digest re-sync + internal-key leak.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_healthcheck_requires_directive used asyncio.get_event_loop().run_until_complete,
which raises "no current event loop" on CI's Python — passed locally by luck. Use
@pytest.mark.asyncio + await like the other tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- task_store: set_plan persisted via bare conversation.save(), losing the
  plan (parsed Task detached from raw dicts); _persist_step re-parsed the
  stale raw entry, dropping StepHandle mutations. Both now write the
  mutated object back into conversation.tasks by id.
- orchestrator: repeat guard ran AFTER dispatch, so an identical
  side-effecting call executed up to 4x; now short-circuits pre-dispatch
  (one retry allowed after tool error/timeout).
- orchestrator: a locked flow that keeps raising trapped the user behind
  the turn-lock every turn; two consecutive errors now cancel the owning
  control-task (note_locked_flow_error/clear_locked_flow_error).
- core: app_loader wrote App._cached_app directly, bypassing the
  _set_cached_app guard (core/CLAUDE.md contract #2).
- core: CacheManager._lock mutated _loop_locks unguarded and never
  evicted closed-loop entries — unbounded growth on serverless warm
  starts (mirror of App._get_lock's eviction).
- core: failed startup re-ran full action init on every request; now
  backs off 30s between attempts.
- core: unset ${VAR} placeholders now WARN by default (silently-empty
  credentials); JVAGENT_WARN_EMPTY_PLACEHOLDERS=false opts out.

Regression tests added for every fix (tests/memory, tests/core,
tests/action/orchestrator).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- core/benchmark.py: zero importers
- core/graph_traversal.py: exported but never called anywhere
- action/lead_profile/: ghost dir (only __pycache__ left after the
  leadgen unification); action/__deltest: empty stray file
- orchestrator._finalize_directives: superseded by _egress (ADR-0024);
  only its own tests called it — tests removed, latch coverage lives in
  test_single_egress/test_adapter_no_double_send
- orchestrator._has_runnable_work: production never called it; tests
  now assert pick_top_runnable directly
- dependency_installer: drop pass-through action_path param (never used)
- intro: docstrings referenced retired InteractRouter/BridgeInteractAction

max_concurrent_tools kept: documented reserved config key (ADR-0015).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gates were configured to catch nothing: flake8 ignored the dead-code
detectors (F401/F811/F841) and bare-except (E722); mypy ignore_errors
blanketed core, memory, cli, and nearly every action family; the
pre-commit pytest hook was manual-stage only.

- .flake8: enforce E722/F401/F811/F841; __init__.py re-exports get a
  per-file ignore. Fallout: ~150 dead imports removed (autoflake),
  duplicate imports and unused locals dropped by hand. Two re-exports
  consumed by tests kept with explicit noqa (whatsapp
  TypingStateManager, pageindex _ssrf_guard_url).
- mypy: enforce jvagent.core.*, jvagent.memory.*, jvagent.cli,
  jvagent.action framework + most families; ignore_errors now covers
  only six provider clusters (model/whatsapp/stt/tts/pageindex/
  vectorstore). Fixes: App.now() @overload (fmt=None -> datetime),
  StepStatus Literal for normalize_step_status, User.memory widened to
  Dict[str, Any] (stores nested user_model), ConfigSchema._keys
  ClassVar, various annotations.
- pre-commit: pytest hook now runs at pre-push (was manual-only,
  contradicting the documented mandatory gate); CLAUDE.md notes it.
- pyproject: requires-python >=3.10 (mcp needs it; 3.8/3.9 untested),
  mypy python_version 3.10, unconditional mcp dependency.
- sentdm_broadcast/test_cli.py renamed broadcast_tester.py — an
  interactive HTTP tester, not a pytest test; pytest never collected it.
- tests/logging/ added (endpoint filter/time parsing, logs-DB contract,
  INTERACTION level registration) — subsystem previously untested.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Conversation.get_interactions: batch-load by conversation_id (one
  query on the conv_timestamp index, ordered in memory) instead of a
  node-by-node chain walk — the per-turn LLM-history path paid N
  sequential DB fetches, unbounded at the default interaction_limit=0.
- Memory.get_user: debounce the last_seen bump (60s window) — every
  inbound message paid a full user.save() just to refresh a timestamp.
- check_pip_dependency_installed: importlib.metadata + packaging
  version-spec matching — the old probe __import__ed the package (side
  effects, slow) and shelled out to 'pip list' per package at boot, and
  ignored version specifiers entirely.
- repair_scratch.scratch_count: backend count() instead of loading all
  rows and len()ing them (fallback scan kept for backends without it).
- distributed_conversation_lock (DynamoDB): one boto3 client per
  acquisition, not one per poll iteration plus one per release.
- orchestrator._ingest_uploads: per-image interpretations (a model
  round-trip each) now run concurrently; artifact writes stay serial.
- tool_decorator.collect_tools: per-class discovery cache — docstring/
  signature introspection is static per class, and the orchestrator
  calls get_tools() on every enabled action each turn.

Skipped deliberately: App.get() cache-hit context verification (the
extra round-trip is the AUDIT-core C-1 multi-tenant safety check) and
TaskStore incremental persist (invasive; step/plan writes fixed in
70161c3).

New characterization tests: get_interactions order/limit/reverse,
last_seen debounce, version-aware dependency probe (the probe test
demonstrated the old side-effect import by printing the Zen of Python).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CLAUDE.md / core/CLAUDE.md: repoint drifted source anchors (App/Agent
  class lines, pruning at conversation.py:490, set_app_update_mode at
  app.py:596 — previous refs landed on unrelated lines).
- use_case_loader: actually validate documents against
  use-case-v1.schema.json (the schema was loaded but never applied).
- orchestrator/continuation: order concurrent active tasks by parsed
  datetime instead of lexicographic str(updated_at) — naive/aware or
  offset-bearing timestamps from external writers sorted wrong.
- env.py: collapse two copy-paste dotenv readers into one helper; the
  swallowed exception is now logged at debug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add skill_spec base, registry, contract modules for shared parsing
- Refactor interview and leadgen specs to import shared primitives
- Resolve leadgen merge conflicts blocking test collection

Co-authored-by: Cursor <cursoragent@cursor.com>
…gress

Consolidate shared channel/webhook/OAuth/spreadsheet helpers; split orchestrator
into egress/loop/uploads modules; retire router/converse/retrieval/long_memory
rails actions in favor of orchestrator skills. Fix ReplyAction.gather() and
orchestrator _send_reply so intro parameters-only shaping emits on first
engagement. Drop docling from default test/dev deps (keep jvagent[pageindex]).
Pin jvspatial 0.0.12; update tests and docs for removed actions and new layout.

Co-authored-by: Cursor <cursoragent@cursor.com>
…plit

Trailing whitespace in ADRs, black/isort on orchestrator/cli/embed modules,
and remove unused imports that CI flake8 caught but local hooks had missed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@eldonm eldonm self-assigned this Jul 8, 2026
@eldonm eldonm merged commit bd2e536 into main Jul 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant