diff --git a/ROADMAP.md b/ROADMAP.md index 711f6ad..5b54686 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,6 +33,7 @@ Current capability snapshot: - Scheduled trigger boundary: deterministic one-shot local schedule definitions, due-run CLI, trigger-boundary execution, audit metadata, and snapshot inspection - Release automation: read-only release preflight checks, CI dry-run coverage, and maintainer release-process docs - Pilot scenario pack: local customer-support, sales-renewal, and risk-exception smokes that reuse trigger input, body-only mapping, credential handles, manual gates, audit, snapshots, and LiteGraph overlays +- Product connector pilot scenario: sales renewal risk workflow that uses the out-of-core Lark/Feishu task dry-run connector after a manual control gate, with webhook trigger, audit, snapshot, and LiteGraph overlay artifacts Important boundaries: @@ -44,13 +45,12 @@ Important boundaries: ## Current Priority Snapshot -- Active loop: Loop 37, Product Connector Pilot Scenario. -- Active question: can the Lark/Feishu task dry-run connector package be used inside one concrete local pilot workflow without turning it into a live SaaS integration? -- Required evidence: a scenario workflow, explicit connector loading, publish/trigger execution, operator snapshot, compact audit metadata, and no resolved credential or raw task-payload leakage in connector output/audit. -- First PR shape: add one local pilot scenario that uses the existing `lark_task` dry-run package; no live Lark API call, OAuth, hosted callback, automatic discovery, queue, scheduler expansion, or marketplace behavior. -- Selection result: Loop 36 proved the first product-shaped connector package boundary with `examples/connectors/lark_task_connector.py`. -- Scenario direction: start with a sales renewal risk follow-up flow, because it already has clear owner accountability and a natural task handoff after a control decision. -- Decision gate: only consider live Lark API behavior after the dry-run package works in at least one local pilot workflow with useful operator evidence. +- Active loop: Loop 38, Live Connector Readiness Review. +- Active question: is live Lark/Feishu API behavior justified now that the selected connector has passed both package-level and pilot-workflow dry-run evidence? +- Required evidence: a decision note covering API scope, credential model, idempotency, failure modes, audit redaction, test strategy, and rollback boundaries before any live implementation work starts. +- First PR shape: documentation and tests for a readiness decision only; no live Lark API call, OAuth, hosted callback, automatic discovery, queue, scheduler expansion, or marketplace behavior. +- Selection result: Loop 37 proved the `lark_task` dry-run package inside a sales renewal risk pilot workflow with manual control, webhook trigger, audit, snapshot, and LiteGraph overlay artifacts. +- Decision gate: live Lark API work may start only after the readiness review scopes one narrow action and its safety requirements. - Deferred: connector marketplace work, dynamic discovery, hosted ingress, production scheduling, OAuth, token refresh systems, and broad SaaS connector catalogs. ## Real Team Pilot Readiness @@ -82,14 +82,15 @@ Ready now: - Review the first product connector candidate decision for a Lark/Feishu task connector. - Run the selected Lark/Feishu task connector as an explicitly loaded out-of-core dry-run package. - Inspect compact Lark/Feishu task connector audit metadata without exposing resolved credentials or raw mapped task payloads. +- Run a sales renewal risk pilot workflow that uses the Lark/Feishu task dry-run package after a manual gate. Still needed before serious pilots: -- A concrete local pilot workflow that uses the Lark/Feishu task dry-run package in a realistic business scenario. -- Operator-facing evidence that the product connector package remains understandable in snapshots and audit trails. +- A live connector readiness review that decides whether any Lark/Feishu API behavior should be implemented next. +- A scoped live-action safety plan covering credential model, idempotency, API errors, audit redaction, and rollback boundaries. - Production-grade recurring schedulers, hosted ingress, and real SaaS integrations remain out of scope until local pilot and connector-packaging evidence is stronger. -Pilot sequencing rule: use the selected Lark/Feishu task connector in one local dry-run pilot scenario before any live SaaS behavior. Trigger input is durable, but credential material must stay outside trigger input and immutable workflow artifacts. +Pilot sequencing rule: complete the live connector readiness review before any live SaaS behavior. Trigger input is durable, but credential material must stay outside trigger input and immutable workflow artifacts. ## Completed Loops @@ -131,86 +132,81 @@ Pilot sequencing rule: use the selected Lark/Feishu task connector in one local | Loop 34: Connector Packaging Boundary | Complete | Repeatable local connector package layout, explicit-loading smoke contract, compatibility notes, and stability boundaries | | Loop 35: First Product Connector Candidate | Complete | Lark/Feishu task connector selected, alternatives compared, package boundary and dry-run smoke plan documented | | Loop 36: First Product Connector Package Smoke | Complete | Lark/Feishu task connector dry-run package fixture, explicit-loading smoke, credential-handle evidence, and compact connector metadata | +| Loop 37: Product Connector Pilot Scenario | Complete | Sales renewal risk workflow using the Lark/Feishu task dry-run connector after a manual gate, with webhook trigger, audit, snapshot, and LiteGraph overlay artifacts | ## Active Roadmap Future work should stay in small closed loops. A loop is complete only when it has a CLI path, tests, documentation, and a merged PR. -Post-`v0.1.0` work now has one active priority after Loop 36 proved the first product-shaped connector package smoke: +Post-`v0.1.0` work now has one active priority after Loop 37 proved the first product connector pilot scenario: -1. use the Lark/Feishu task dry-run connector package inside one concrete local pilot workflow without adding live SaaS infrastructure. +1. decide whether live Lark/Feishu API behavior is safe and valuable enough to implement, before writing live connector code. -### Loop 37: Product Connector Pilot Scenario +### Loop 38: Live Connector Readiness Review -Goal: prove the Lark/Feishu task dry-run connector package works inside one realistic local pilot workflow, with operator-facing audit and snapshot evidence. +Goal: decide whether the project should implement any live Lark/Feishu API behavior next, and define the smallest safe action surface if the answer is yes. -Why this is next: Loop 36 proved the connector package can stay out-of-core and run through the existing control plane. The next risk is whether that package is useful when embedded in a business workflow rather than isolated as a connector-only smoke. +Why this is next: Loop 36 proved the connector package can stay out-of-core, and Loop 37 proved it remains useful inside a controlled local pilot workflow. The next risk is no longer local mechanics; it is whether live SaaS behavior has a clear safety boundary. Status: next engineering loop. -Start condition: Loop 36 has merged with `examples/connectors/lark_task_connector.py` and `python3 scripts/lark_task_connector_smoke.py --work-dir /tmp/skill2workflow-lark-task-connector`. +Start condition: Loop 37 has merged with `python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot`. -Recommended first scenario: sales renewal risk follow-up. The workflow should accept durable trigger input for account, renewal risk, owner, and due date; pass through a controlled decision point; then invoke the `lark_task` dry-run connector to create an owner follow-up task. The pilot should prove the business handoff and operator evidence, not live task creation. +Recommended first review scope: Lark/Feishu `create_task` only. The review should decide whether that live action is safe to implement, what credential model it requires, what idempotency key should protect against duplicate task creation, and what audit evidence is allowed. Initial PR boundary: -- Add one generated local pilot workflow that uses `lark_task` to create a dry-run task after a controlled business decision point. -- Expose a deterministic smoke command, expected shape: `python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot`. -- Reuse explicit connector loading through `load_external_connector(...)`; do not add registry auto-discovery. -- Keep `operation: create_task` and `mode: dry_run`; do not call the live Lark/Feishu API. -- Preserve credential-handle behavior and compact audit metadata from Loop 36. -- Write workflow, run, audit, connector, trigger, and control-plane snapshot artifacts for the pilot scenario. -- Document what the pilot scenario proves and what remains out of scope. -- Defer OAuth, token refresh, hosted callbacks, production scheduling, marketplace indexing, queues, and broad connector catalogs. +- Add a decision note under `docs/` that evaluates live Lark/Feishu `create_task` readiness. +- Cover API action surface, credential model, idempotency, duplicate prevention, failure handling, audit redaction, local test strategy, and rollback boundaries. +- Update Roadmap only after the decision is explicit. +- Keep `examples/connectors/lark_task_connector.py` dry-run-only during the review. +- Do not add live API calls, OAuth, token refresh, hosted callbacks, production scheduling, marketplace indexing, queues, or broad connector catalogs. Recommended first-cut order: -1. Choose the smallest scenario where a task should be created only after a gate or policy decision. -2. Add a focused smoke test for the scenario artifact and no-secret/no-payload connector evidence. -3. Implement the scenario helper using the existing control plane and explicit connector runtime. -4. Verify snapshot and audit output remain operator-readable. -5. Update docs only where the new pilot path changes contributor behavior. +1. Add a docs contract test for the readiness note and Roadmap state. +2. Write the decision note with explicit yes/no criteria for live implementation. +3. Decide the next loop from the evidence: live create-task implementation, more dry-run hardening, or defer live behavior. +4. Update docs and Roadmap with the decision. -Loop 37 acceptance evidence: +Loop 38 acceptance evidence: -- The pilot scenario executes a published workflow through the control plane. -- The scenario explicitly loads `lark_task` and keeps the default registry unchanged. -- The workflow demonstrates a business control point before the dry-run task action. -- Credential values remain outside Workflow DSL, connector output, audit events, and committed fixtures. -- Connector metadata remains compact and useful in audit/snapshot artifacts. +- A decision note exists and is covered by tests. +- The note names the live API action surface or explicitly defers live behavior. +- Credential handling and audit redaction rules are explicit before implementation starts. +- Idempotency and duplicate task prevention are explicitly addressed. - The work does not add automatic discovery, installer, marketplace, OAuth, hosted callbacks, queues, live API behavior, or production schedulers. -Loop 37 verification commands: +Loop 38 verification commands: - `PYTHONPATH=src python3 -m unittest discover -s tests -v` -- `python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot` - `python3 -m py_compile src/skill2workflow/*.py` - `python3 scripts/secret_hygiene.py examples/workflows` - `git diff --check` -Loop 37 done means: +Loop 38 done means: -- The selected Lark/Feishu task connector has graduated from an isolated package smoke into one local pilot workflow. -- The implementation remains a local dry-run package, not a marketplace, hosted integration platform, or live Lark API client. +- The project has a reviewed decision on whether live Lark/Feishu task creation is next. +- The implementation remains dry-run-only until that decision produces a scoped live implementation loop. ## Near-Term Loop Queue This queue is ordered by what most improves open-source adoption after the first release. Treat it as a planning queue, not a commitment to implement all items without review. -Loops 24-36 are now historical execution evidence and are tracked in the Completed Loops table above. The near-term queue starts with the current pilot scenario and only lists work that should still be scoped or implemented. +Loops 24-37 are now historical execution evidence and are tracked in the Completed Loops table above. The near-term queue starts with the live-readiness review and only lists work that should still be scoped or implemented. | Loop | Status | Goal | Expected artifact | | --- | --- | --- | --- | -| Loop 37: Product Connector Pilot Scenario | Next | Use the first connector package in one local pilot scenario without hosted callbacks or marketplace behavior | sales renewal risk workflow, `lark_task_pilot_smoke.py`, operator snapshot, compact audit evidence | -| Loop 38: Live Connector Readiness Review | Candidate | Decide whether live Lark/Feishu API behavior is justified after dry-run pilot evidence | decision note, risk checklist, scoped implementation plan | +| Loop 38: Live Connector Readiness Review | Next | Decide whether live Lark/Feishu API behavior is justified after dry-run pilot evidence | decision note, risk checklist, scoped implementation plan | +| Loop 39: Live Connector Implementation Or Deferral | Candidate | Implement the scoped live action only if Loop 38 approves it, otherwise add the next dry-run hardening loop | implementation PR or deferral note | Loop selection rules: - Pick the next loop only after the previous loop is merged or explicitly deferred. - Keep implementation local-first and dependency-light unless a spec-backed capability requires otherwise. - Prefer examples and guardrails that make the current runtime easier to trust before adding new platform surface area. -- Do not add live SaaS connector behavior until the dry-run connector has proven useful inside at least one local pilot scenario. -- Keep Loop 38 tentative until Loop 37 has merged. +- Do not add live SaaS connector behavior until the Loop 38 readiness review approves a scoped action. +- Keep Loop 39 tentative until Loop 38 has merged. ## Release Tag Plan @@ -252,7 +248,7 @@ Status: delivered by Loops 1-9. ### v0.2: Connector Runtime -Status: first MVP shipped in Loop 10. Runtime hardening shipped in Loop 17. Retry execution shipped in Loop 21. Credential fixture hygiene shipped in Loop 22. Local credential handles shipped in Loop 25. Body-only trigger input mapping shipped in Loop 30. Connector extension contract shipped in Loop 31. Pilot scenario pack evidence shipped in Loop 32. Local connector extension prototype shipped in Loop 33. Connector packaging boundary shipped in Loop 34. Lark/Feishu task connector selection shipped in Loop 35. First product connector package smoke shipped in Loop 36. Future work should use the selected connector package in a local pilot scenario before live SaaS behavior. +Status: first MVP shipped in Loop 10. Runtime hardening shipped in Loop 17. Retry execution shipped in Loop 21. Credential fixture hygiene shipped in Loop 22. Local credential handles shipped in Loop 25. Body-only trigger input mapping shipped in Loop 30. Connector extension contract shipped in Loop 31. Pilot scenario pack evidence shipped in Loop 32. Local connector extension prototype shipped in Loop 33. Connector packaging boundary shipped in Loop 34. Lark/Feishu task connector selection shipped in Loop 35. First product connector package smoke shipped in Loop 36. Product connector pilot scenario shipped in Loop 37. Future work should complete a live-readiness review before live SaaS behavior. - Connector manifests - Connector binding validation @@ -267,7 +263,8 @@ Status: first MVP shipped in Loop 10. Runtime hardening shipped in Loop 17. Retr - Repeatable connector package boundary before product-specific connector packages - Selected first product connector candidate: Lark/Feishu task connector - First product connector package smoke for Lark/Feishu `create_task` dry-run behavior -- Future: product connector pilot scenario using the dry-run package +- Product connector pilot scenario using the dry-run package +- Future: live connector readiness review before any live API behavior ### v0.3: Authoring Experience @@ -324,7 +321,7 @@ Status: trigger API shipped in Loop 23; input runtime shipped in Loop 24; local ### v0.7: Pilot Integration Boundary -Status: local trigger, input, credential, webhook, scheduled trigger, visual inspection, body-only input mapping, pilot playbook, scenario pack, connector extension contract semantics, one explicit external connector prototype, the connector package boundary, first product connector candidate selection, and Lark/Feishu task dry-run package smoke are stable enough for local evaluation. The next step is using that dry-run connector package in one local pilot workflow. +Status: local trigger, input, credential, webhook, scheduled trigger, visual inspection, body-only input mapping, pilot playbook, scenario pack, connector extension contract semantics, one explicit external connector prototype, the connector package boundary, first product connector candidate selection, Lark/Feishu task dry-run package smoke, and one product connector pilot scenario are stable enough for local evaluation. The next step is a live-readiness review before any live Lark/Feishu API implementation. - Credential provider interface - Secret-handle documentation without secret storage in Workflow DSL @@ -339,7 +336,8 @@ Status: local trigger, input, credential, webhook, scheduled trigger, visual ins - Connector packaging boundary - First product connector candidate: Lark/Feishu task connector - First product connector package smoke for Lark/Feishu `create_task` dry-run behavior -- Next: product connector pilot scenario +- Product connector pilot scenario using the Lark/Feishu task dry-run package +- Next: live connector readiness review - Future: product-specific connector packages and hosted control-plane integrations ### v1.0: Production Baseline @@ -377,7 +375,7 @@ These are intentionally deferred until the local open-source runtime is stronger - Distributed scheduling - Automatic connector package discovery or installation - OAuth flows, hosted connector callbacks, and token refresh systems -- Live product-specific SaaS connector behavior before dry-run pilot scenario evidence +- Live product-specific SaaS connector behavior before the Loop 38 readiness review - Complex enterprise connector marketplace - Guaranteed automatic conversion of arbitrary SOP documents diff --git a/docs/connectors.md b/docs/connectors.md index 4757492..432d16a 100644 --- a/docs/connectors.md +++ b/docs/connectors.md @@ -1,7 +1,7 @@ # Connector Runtime `skill2workflow` currently ships a minimal local connector runtime. It is designed to make connector-bound workflow nodes testable and auditable without adding external services, SDK dependencies, secret storage, or a connector marketplace. -Loop 33 adds one explicitly loaded local external connector fixture to prove the extension boundary. Loop 36 adds the first product-shaped connector package fixture, a Lark/Feishu task `create_task` dry-run connector. Neither loop adds automatic discovery, live SaaS calls, OAuth, or marketplace behavior. +Loop 33 adds one explicitly loaded local external connector fixture to prove the extension boundary. Loop 36 adds the first product-shaped connector package fixture, a Lark/Feishu task `create_task` dry-run connector. Loop 37 proves that connector inside a sales renewal risk pilot workflow. These loops do not add automatic discovery, live SaaS calls, OAuth, or marketplace behavior. Workflow DSL remains the execution truth source. Connector bindings live on workflow nodes, and the local executor records connector lifecycle events in run state and control-plane audit logs. @@ -275,6 +275,14 @@ python3 scripts/lark_task_connector_smoke.py --work-dir /tmp/skill2workflow-lark The smoke explicitly loads `examples/connectors/lark_task_connector.py`, publishes a generated workflow, triggers it with non-secret task input, resolves `lark_bot_access_token` through a temporary local credential provider, and writes workflow, run, audit, connector, trigger, and control-plane snapshot artifacts under the work directory. +Run the sales renewal risk pilot smoke from a source checkout: + +```bash +python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot +``` + +The pilot uses the same explicitly loaded package inside a workflow that starts through the local webhook trigger boundary, waits at a manual gate, resumes with approval, and then invokes the connector. It proves business handoff and operator evidence, not live Lark/Feishu task creation. + HTTP connector bindings may also reference local credential handles: ```json diff --git a/docs/examples.md b/docs/examples.md index 7f6c554..77a9299 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -14,6 +14,7 @@ The example pack shows how standard Agent `SKILL.md` files become controlled Wor | HTTP connector | n/a | `examples/workflows/http-connector.workflow.json` | `examples/workflows/http-connector.litegraph.json` | Editable HTTP connector request and body input-mapping fixture | | Local external connector | `examples/connectors/local_echo_connector.py` | runtime-generated | n/a | Explicitly loaded external connector fixture with credential and audit redaction | | Lark/Feishu task connector | `examples/connectors/lark_task_connector.py` | runtime-generated | n/a | Explicitly loaded product-shaped dry-run connector package with compact audit metadata | +| Lark/Feishu task pilot | `examples/connectors/lark_task_connector.py` | runtime-generated | runtime-generated | Sales renewal risk workflow with manual gate before dry-run owner task handoff | ## Inspecting Examples @@ -101,6 +102,16 @@ python3 scripts/lark_task_connector_smoke.py --work-dir /tmp/skill2workflow-lark Use it to evaluate the first product-shaped connector package boundary. It is not a live Lark/Feishu API client, OAuth flow, hosted callback, connector installer, marketplace entry, queue, or production scheduler. +### Lark/Feishu Task Pilot + +The Lark/Feishu task pilot is generated at runtime and uses the `lark_task` dry-run connector inside a sales renewal risk workflow. It starts through the local webhook trigger boundary, waits at a manual review gate, resumes with approval, then invokes the explicitly loaded connector to validate an owner follow-up task request: + +```bash +python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot +``` + +The pilot writes workflow, trigger response, run state, audit, connector list, control-plane snapshot, and LiteGraph overlay artifacts. Use it to inspect whether a product connector package remains understandable in a business workflow with a control point. It is still dry-run only and does not create live Lark/Feishu tasks. + ### Local Pilot Scenario Pack The pilot scenario pack is generated at runtime rather than committed as static fixtures. It runs customer support escalation, sales renewal follow-up, and risk exception review through local-only workflows and HTTP receivers: diff --git a/docs/superpowers/plans/2026-07-09-lark-task-pilot-scenario.md b/docs/superpowers/plans/2026-07-09-lark-task-pilot-scenario.md new file mode 100644 index 0000000..be81518 --- /dev/null +++ b/docs/superpowers/plans/2026-07-09-lark-task-pilot-scenario.md @@ -0,0 +1,119 @@ +# Lark Task Pilot Scenario Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Implement Loop 37 by running the Lark/Feishu task dry-run connector inside a sales renewal risk pilot workflow. + +**Architecture:** The pilot helper publishes a generated Workflow DSL through the existing local control plane, starts it through the local webhook trigger boundary, pauses at a manual control gate, resumes the run, and then invokes the explicitly loaded `lark_task` connector package. The helper writes workflow, trigger, run, audit, connectors, snapshot, and LiteGraph overlay artifacts under a caller-provided work directory. + +**Tech Stack:** Python 3.9 standard library, existing `LocalControlPlane`, `handle_webhook_request`, `ConnectorRuntime`, `load_external_connector`, `workflow_to_litegraph`, and `unittest`. + +## Global Constraints + +- Workflow DSL remains the execution source of truth. +- The Lark/Feishu task connector stays outside the built-in connector registry. +- Loop 37 uses `operation: create_task` with `mode: dry_run` only. +- No live Lark API call, OAuth, hosted callback, automatic discovery, marketplace behavior, queue, production scheduler, or token refresh. +- Credential values must remain outside Workflow DSL, connector output, connector audit metadata, committed fixtures, and smoke summaries. +- Trigger input may persist in run context by existing trigger design, but raw mapped task values must not be duplicated into connector output or audit metadata. + +--- + +### Task 1: Sales Renewal Lark Task Pilot Smoke + +**Files:** +- Create: `tests/test_lark_task_pilot.py` +- Create: `src/skill2workflow/lark_task_pilot.py` +- Create: `scripts/lark_task_pilot_smoke.py` + +**Interfaces:** +- Consumes: `load_external_connector(path)`, `ConnectorRuntime([external_connector])`, `LocalControlPlane`, `handle_webhook_request`, `workflow_to_litegraph` +- Produces: `run_lark_task_pilot(repo_root: Path, work_dir: Path, reset: bool = True) -> Dict[str, object]` + +- [x] **Step 1: Write failing pilot smoke test** + +Run: + +```bash +PYTHONPATH=src python3 -m unittest tests.test_lark_task_pilot -v +``` + +Expected: fail because `skill2workflow.lark_task_pilot` does not exist. + +- [x] **Step 2: Implement pilot helper and CLI wrapper** + +Create `run_lark_task_pilot(...)` and `scripts/lark_task_pilot_smoke.py`. The helper must explicitly load `examples/connectors/lark_task_connector.py`, publish `workflow_lark_task_pilot`, trigger it through `/webhooks/workflow_lark_task_pilot/0.1.0`, resume the manual gate, write artifacts, and return a compact summary. + +- [x] **Step 3: Verify focused pilot test and CLI smoke** + +Run: + +```bash +PYTHONPATH=src python3 -m unittest tests.test_lark_task_pilot -v +python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot +``` + +Expected: pass. + +### Task 2: Documentation And Roadmap + +**Files:** +- Modify: `docs/examples.md` +- Modify: `docs/connectors.md` +- Modify: `ROADMAP.md` +- Modify: `tests/test_product_connector_pilot_roadmap.py` + +**Interfaces:** +- Consumes: `scripts/lark_task_pilot_smoke.py` +- Produces: documented Loop 37 smoke command and roadmap transition to Loop 38 candidate work + +- [x] **Step 1: Update docs** + +Document the sales renewal risk pilot smoke in `docs/examples.md` and reference it from the Lark/Feishu connector docs. Keep live API and hosted integration behavior out of scope. + +- [x] **Step 2: Advance Roadmap** + +Move Loop 37 to complete, set Loop 38 as next, and keep live connector work behind a readiness review rather than implementation. + +- [x] **Step 3: Verify docs contracts** + +Run: + +```bash +PYTHONPATH=src python3 -m unittest tests.test_product_connector_pilot_roadmap -v +``` + +Expected: pass. + +### Task 3: Full Verification And PR + +**Files:** +- Modify: `docs/superpowers/plans/2026-07-09-lark-task-pilot-scenario.md` + +**Interfaces:** +- Consumes: Task 1 and Task 2 implementation +- Produces: draft PR for Loop 37 + +- [x] **Step 1: Run full verification** + +Run: + +```bash +PYTHONPATH=src python3 -m unittest discover -s tests -v +python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot +python3 -m py_compile src/skill2workflow/*.py +python3 scripts/secret_hygiene.py examples/workflows +git diff --check +``` + +Expected: all commands exit 0. + +- [x] **Step 2: Commit and open draft PR** + +Run: + +```bash +git add ROADMAP.md docs/connectors.md docs/examples.md docs/superpowers/plans/2026-07-09-lark-task-pilot-scenario.md scripts/lark_task_pilot_smoke.py src/skill2workflow/lark_task_pilot.py tests/test_lark_task_pilot.py tests/test_product_connector_pilot_roadmap.py +git commit -m "feat: add lark task pilot scenario" +git push -u origin loop-37-lark-task-pilot +``` diff --git a/scripts/lark_task_pilot_smoke.py b/scripts/lark_task_pilot_smoke.py new file mode 100644 index 0000000..5007d5c --- /dev/null +++ b/scripts/lark_task_pilot_smoke.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +"""Run the skill2workflow local Lark/Feishu task pilot from a source checkout.""" + +from pathlib import Path +import sys + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SRC_ROOT = REPO_ROOT / "src" +if str(SRC_ROOT) not in sys.path: + sys.path.insert(0, str(SRC_ROOT)) + +from skill2workflow.lark_task_pilot import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/skill2workflow/lark_task_pilot.py b/src/skill2workflow/lark_task_pilot.py new file mode 100644 index 0000000..b4efa88 --- /dev/null +++ b/src/skill2workflow/lark_task_pilot.py @@ -0,0 +1,304 @@ +"""Deterministic local sales renewal pilot using the Lark/Feishu task dry-run connector.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import tempfile +from pathlib import Path +from typing import Dict + +from .compiler import validate_workflow +from .connectors import ConnectorRuntime +from .control_plane import LocalControlPlane +from .credentials import StaticCredentialProvider +from .dashboard import build_control_snapshot +from .external_connectors import load_external_connector +from .pilot import DEFAULT_UI_URL +from .visualizer import workflow_to_litegraph +from .webhooks import handle_webhook_request + + +DEFAULT_WORK_DIR = Path(tempfile.gettempdir()) / "skill2workflow-lark-task-pilot" +LOCAL_SECRET = "local-lark-secret" + + +def run_lark_task_pilot( + repo_root: Path, + work_dir: Path = DEFAULT_WORK_DIR, + reset: bool = True, +) -> Dict[str, object]: + """Run the sales renewal risk pilot through a manual gate and Lark task dry-run.""" + repo_root = Path(repo_root).resolve() + work_dir = Path(work_dir).resolve() + if reset: + _reset_work_dir(repo_root, work_dir) + + artifacts_dir = work_dir / "artifacts" + state_dir = work_dir / "state" + artifacts_dir.mkdir(parents=True, exist_ok=True) + state_dir.mkdir(parents=True, exist_ok=True) + + external_connector = load_external_connector(repo_root / "examples" / "connectors" / "lark_task_connector.py") + default_connector_ids = [manifest["id"] for manifest in ConnectorRuntime().list_connectors()] + runtime = ConnectorRuntime([external_connector]) + connector_ids = [manifest["id"] for manifest in runtime.list_connectors()] + + workflow = _lark_task_pilot_workflow() + errors = validate_workflow(workflow) + if errors: + raise ValueError("; ".join(errors)) + + control = LocalControlPlane( + state_dir, + credential_provider=StaticCredentialProvider({"lark_bot_access_token": LOCAL_SECRET}), + connector_runtime=runtime, + ) + control.publish_workflow(workflow) + trigger_payload = { + "source": "local-webhook", + "idempotency_key": "sales-renewal-risk-001", + "input": _pilot_trigger_input(), + } + trigger_response = handle_webhook_request( + control, + "POST", + "/webhooks/workflow_lark_task_pilot/0.1.0", + json.dumps(trigger_payload).encode("utf-8"), + ) + run_id = str(trigger_response["run_id"]) + waiting_state = control.get_run(run_id) + gate_summary = _gate_summary(waiting_state, approved=False) + run_state = waiting_state + if waiting_state.get("status") == "waiting": + run_state = control.resume_published_run(run_id, approved=True) + gate_summary = _gate_summary(run_state, approved=True) + + audit_events = control.list_audit_events(run_id=run_id) + completed_events = [event for event in audit_events if event.get("type") == "connector_completed"] + connector_summary = _connector_summary(completed_events[0] if completed_events else {}) + snapshot = build_control_snapshot(state_dir, connector_runtime=runtime) + litegraph_overlay = workflow_to_litegraph(workflow, run_state=run_state, audit_events=audit_events) + + workflow_path = artifacts_dir / "workflow.json" + trigger_path = artifacts_dir / "trigger-response.json" + run_path = artifacts_dir / "run.json" + audit_path = artifacts_dir / "audit.json" + snapshot_path = artifacts_dir / "control-plane-snapshot.json" + overlay_path = artifacts_dir / "workflow.overlay.litegraph.json" + connectors_path = artifacts_dir / "connectors.json" + + _write_json(workflow_path, workflow) + _write_json(trigger_path, trigger_response) + _write_json(run_path, run_state) + _write_json(audit_path, audit_events) + _write_json(snapshot_path, snapshot) + _write_json(overlay_path, litegraph_overlay) + _write_json(connectors_path, runtime.list_connectors()) + + return { + "ok": True, + "work_dir": str(work_dir), + "state_dir": str(state_dir), + "scenario": { + "id": "sales_renewal_risk_followup", + "control": "manual_gate_before_lark_task", + }, + "workflow_id": "workflow_lark_task_pilot", + "workflow_version": "0.1.0", + "run_id": run_id, + "run_status": run_state.get("status", ""), + "default_connector_ids": default_connector_ids, + "connector_ids": connector_ids, + "trigger_response": trigger_response, + "trigger_summary": _trigger_summary(trigger_payload), + "gate_summary": gate_summary, + "connector_summary": connector_summary, + "snapshot_summary": snapshot.get("summary", {}), + "artifacts": { + "workflow": str(workflow_path), + "trigger_response": str(trigger_path), + "run": str(run_path), + "audit": str(audit_path), + "snapshot": str(snapshot_path), + "litegraph_overlay": str(overlay_path), + "connectors": str(connectors_path), + }, + "commands": { + "run_lark_task_pilot": f"python3 scripts/lark_task_pilot_smoke.py --work-dir {work_dir}", + "serve_ui": "python3 -m http.server 4173", + "ui_url": DEFAULT_UI_URL, + }, + } + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser( + prog="lark_task_pilot_smoke", + description="Generate and run the local sales renewal Lark/Feishu task pilot.", + ) + parser.add_argument("--repo-root", type=Path, default=Path(__file__).resolve().parents[2]) + parser.add_argument("--work-dir", type=Path, default=DEFAULT_WORK_DIR) + parser.add_argument("--no-reset", action="store_true", help="Keep existing Lark task pilot artifacts.") + args = parser.parse_args(argv) + + result = run_lark_task_pilot(args.repo_root, args.work_dir, reset=not args.no_reset) + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +def _pilot_trigger_input() -> Dict[str, object]: + return { + "account_id": "acct_123", + "account_name": "ACME Global", + "renewal_risk": "High renewal risk because executive sponsor changed", + "owner_open_id": "ou_lark_task_owner", + "due_at": "2026-08-15T09:00:00Z", + } + + +def _lark_task_pilot_workflow() -> Dict[str, object]: + return { + "schema_version": "0.1.0", + "workflow": { + "id": "workflow_lark_task_pilot", + "name": "lark-task-sales-renewal-pilot", + "description": "Local sales renewal risk pilot using the Lark/Feishu task dry-run connector.", + "version": "0.1.0", + "status": "draft", + }, + "entry": "start", + "nodes": [ + { + "id": "start", + "type": "start", + "title": "Receive renewal risk", + "description": "Receive durable sales renewal risk trigger input.", + "on_success": "review_renewal_risk", + }, + { + "id": "review_renewal_risk", + "type": "human_gate", + "title": "Review renewal risk", + "description": "Manual control point before creating an owner follow-up task.", + "action": { + "kind": "human_approval", + "prompt": "Approve the renewal risk follow-up task for the account owner.", + }, + "connector": {"id": "manual", "kind": "manual"}, + "on_success": "create_lark_task", + "on_failure": "failure", + }, + { + "id": "create_lark_task", + "type": "tool_call", + "title": "Create owner follow-up task", + "description": "Validate a Lark/Feishu owner follow-up task request without calling the live API.", + "action": { + "kind": "tool_call", + "instruction": "Create a dry-run Lark/Feishu task for the account owner.", + }, + "retry": {"max_attempts": 0}, + "connector": { + "id": "lark_task", + "kind": "lark_task", + "operation": "create_task", + "mode": "dry_run", + "request": { + "body": { + "source": "skill2workflow-lark-task-pilot", + "scenario": "sales_renewal_risk_followup", + }, + "input_mapping": [ + {"from": "/input/account_name", "to": "/body/title", "required": True}, + {"from": "/input/renewal_risk", "to": "/body/description", "required": True}, + {"from": "/input/owner_open_id", "to": "/body/assignee_open_id", "required": True}, + {"from": "/input/due_at", "to": "/body/due_at", "required": True}, + ], + }, + "credentials": [ + { + "target": "header", + "name": "Authorization", + "handle": "lark_bot_access_token", + "prefix": "Bearer ", + } + ], + }, + "on_success": "end", + "on_failure": "failure", + }, + {"id": "failure", "type": "failure", "title": "Failure"}, + {"id": "end", "type": "end", "title": "Renewal follow-up captured"}, + ], + "edges": [ + {"id": "edge_start_review", "from": "start", "to": "review_renewal_risk", "label": "next"}, + {"id": "edge_review_task", "from": "review_renewal_risk", "to": "create_lark_task", "label": "next"}, + {"id": "edge_review_failure", "from": "review_renewal_risk", "to": "failure", "label": "failure"}, + {"id": "edge_task_end", "from": "create_lark_task", "to": "end", "label": "next"}, + {"id": "edge_task_failure", "from": "create_lark_task", "to": "failure", "label": "failure"}, + ], + "state_schema": {}, + "guards": [], + "checkpoints": [], + "policies": {"default_retry": {"max_attempts": 0}, "default_timeout_ms": 300000}, + } + + +def _trigger_summary(trigger_payload: Dict[str, object]) -> Dict[str, object]: + trigger_input = trigger_payload.get("input", {}) + input_keys = sorted(str(key) for key in trigger_input.keys()) if isinstance(trigger_input, dict) else [] + return { + "source": str(trigger_payload.get("source", "")), + "idempotency_key": str(trigger_payload.get("idempotency_key", "")), + "input_keys": input_keys, + } + + +def _gate_summary(run_state: Dict[str, object], approved: bool) -> Dict[str, object]: + node_results = run_state.get("node_results", {}) + result = node_results.get("review_renewal_risk", {}) if isinstance(node_results, dict) else {} + return { + "node_id": "review_renewal_risk", + "resumed": bool(result), + "approved": bool(result.get("approved")) if isinstance(result, dict) else approved, + } + + +def _connector_summary(event: Dict[str, object]) -> Dict[str, object]: + metadata = event.get("connector_metadata", {}) + if not isinstance(metadata, dict): + metadata = {} + return { + "connector_id": str(event.get("connector_id", "")), + "connector_status": str(event.get("connector_status", "")), + "credential_status": str(event.get("credential_status", "")), + "credential_handles": [str(handle) for handle in event.get("credential_handles", [])] + if isinstance(event.get("credential_handles"), list) + else [], + "input_mapping_status": str(event.get("input_mapping_status", "")), + "input_mapping_keys": [str(key) for key in event.get("input_mapping_keys", [])] + if isinstance(event.get("input_mapping_keys"), list) + else [], + "operation": str(metadata.get("operation", "")), + "mode": str(metadata.get("mode", "")), + "task_title_present": bool(metadata.get("task_title_present")), + "task_description_present": bool(metadata.get("task_description_present")), + "assignee_present": bool(metadata.get("assignee_present")), + "due_at_present": bool(metadata.get("due_at_present")), + } + + +def _reset_work_dir(repo_root: Path, work_dir: Path) -> None: + if work_dir == repo_root or repo_root in work_dir.parents: + raise ValueError("Lark task pilot work_dir must be outside the repository when reset is enabled") + if work_dir == Path(work_dir.anchor): + raise ValueError("Lark task pilot work_dir cannot be a filesystem root") + if work_dir.exists(): + shutil.rmtree(work_dir) + + +def _write_json(path: Path, value: object) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8") diff --git a/tests/test_first_product_connector_candidate_docs.py b/tests/test_first_product_connector_candidate_docs.py index 7e296c9..3fcc5f1 100644 --- a/tests/test_first_product_connector_candidate_docs.py +++ b/tests/test_first_product_connector_candidate_docs.py @@ -45,9 +45,10 @@ def test_loop_35_lark_task_candidate_decision_is_documented(self): self.assertIn("| Loop 35: First Product Connector Candidate | Complete |", roadmap) self.assertIn("| Loop 36: First Product Connector Package Smoke | Complete |", roadmap) - self.assertIn("| Loop 37: Product Connector Pilot Scenario | Next |", roadmap) + self.assertIn("| Loop 37: Product Connector Pilot Scenario | Complete |", roadmap) + self.assertIn("| Loop 38: Live Connector Readiness Review | Next |", roadmap) self.assertIn("Lark/Feishu task connector", roadmap) - self.assertIn("first product-shaped connector package boundary", roadmap) + self.assertIn("package-level and pilot-workflow dry-run evidence", roadmap) def _read(path: str) -> str: diff --git a/tests/test_lark_task_pilot.py b/tests/test_lark_task_pilot.py new file mode 100644 index 0000000..aa99412 --- /dev/null +++ b/tests/test_lark_task_pilot.py @@ -0,0 +1,88 @@ +import json +from pathlib import Path +from tempfile import TemporaryDirectory +from unittest import TestCase + +from skill2workflow.lark_task_pilot import run_lark_task_pilot + + +class LarkTaskPilotTests(TestCase): + def test_lark_task_pilot_runs_sales_renewal_flow_with_control_gate(self): + repo_root = Path(__file__).resolve().parents[1] + + with TemporaryDirectory() as tmp: + work_dir = Path(tmp) / "lark-task-pilot" + result = run_lark_task_pilot(repo_root=repo_root, work_dir=work_dir, reset=True) + + workflow_path = Path(result["artifacts"]["workflow"]) + trigger_path = Path(result["artifacts"]["trigger_response"]) + run_path = Path(result["artifacts"]["run"]) + audit_path = Path(result["artifacts"]["audit"]) + snapshot_path = Path(result["artifacts"]["snapshot"]) + overlay_path = Path(result["artifacts"]["litegraph_overlay"]) + connectors_path = Path(result["artifacts"]["connectors"]) + self.assertTrue(workflow_path.exists()) + self.assertTrue(trigger_path.exists()) + self.assertTrue(run_path.exists()) + self.assertTrue(audit_path.exists()) + self.assertTrue(snapshot_path.exists()) + self.assertTrue(overlay_path.exists()) + self.assertTrue(connectors_path.exists()) + + workflow = json.loads(workflow_path.read_text(encoding="utf-8")) + run_state = json.loads(run_path.read_text(encoding="utf-8")) + audit_events = json.loads(audit_path.read_text(encoding="utf-8")) + + self.assertTrue(result["ok"]) + self.assertEqual(result["scenario"]["id"], "sales_renewal_risk_followup") + self.assertEqual(result["workflow_id"], "workflow_lark_task_pilot") + self.assertEqual(result["run_status"], "completed") + self.assertEqual(result["default_connector_ids"], ["manual", "http"]) + self.assertEqual(result["connector_ids"], ["manual", "http", "lark_task"]) + + self.assertEqual(result["gate_summary"], {"node_id": "review_renewal_risk", "resumed": True, "approved": True}) + self.assertEqual(result["trigger_summary"]["source"], "local-webhook") + self.assertEqual( + result["trigger_summary"]["input_keys"], + ["account_id", "account_name", "due_at", "owner_open_id", "renewal_risk"], + ) + + self.assertEqual(result["connector_summary"]["connector_id"], "lark_task") + self.assertEqual(result["connector_summary"]["operation"], "create_task") + self.assertEqual(result["connector_summary"]["mode"], "dry_run") + self.assertEqual(result["connector_summary"]["credential_handles"], ["lark_bot_access_token"]) + self.assertEqual( + result["connector_summary"]["input_mapping_keys"], + ["account_name", "due_at", "owner_open_id", "renewal_risk"], + ) + self.assertTrue(result["connector_summary"]["task_title_present"]) + self.assertTrue(result["connector_summary"]["task_description_present"]) + self.assertTrue(result["connector_summary"]["assignee_present"]) + self.assertTrue(result["connector_summary"]["due_at_present"]) + self.assertEqual(result["snapshot_summary"]["run_status_counts"], {"completed": 1}) + self.assertEqual(result["snapshot_summary"]["connector_count"], 3) + + node_ids = [node["id"] for node in workflow["nodes"]] + self.assertEqual(node_ids, ["start", "review_renewal_risk", "create_lark_task", "failure", "end"]) + create_node = next(node for node in workflow["nodes"] if node["id"] == "create_lark_task") + self.assertEqual(create_node["connector"]["id"], "lark_task") + self.assertEqual(create_node["connector"]["operation"], "create_task") + self.assertEqual(create_node["connector"]["mode"], "dry_run") + + self.assertEqual(run_state["node_results"]["review_renewal_risk"]["status"], "approved") + connector_node_result = run_state["node_results"]["create_lark_task"] + encoded_connector_result = json.dumps(connector_node_result, ensure_ascii=False) + encoded_audit = json.dumps(audit_events, ensure_ascii=False) + for raw_value in ( + "ACME Global", + "High renewal risk because executive sponsor changed", + "ou_lark_task_owner", + "2026-08-15T09:00:00Z", + "local-lark-secret", + ): + self.assertNotIn(raw_value, encoded_connector_result) + self.assertNotIn(raw_value, encoded_audit) + + self.assertIn("run_waiting", [event["type"] for event in audit_events]) + self.assertIn("run_resumed", [event["type"] for event in audit_events]) + self.assertIn("connector_completed", [event["type"] for event in audit_events]) diff --git a/tests/test_product_connector_pilot_roadmap.py b/tests/test_product_connector_pilot_roadmap.py index e4425e2..a2c4610 100644 --- a/tests/test_product_connector_pilot_roadmap.py +++ b/tests/test_product_connector_pilot_roadmap.py @@ -9,16 +9,18 @@ class ProductConnectorPilotRoadmapTests(TestCase): def test_loop_37_product_connector_pilot_is_scoped(self): roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8") - self.assertIn("Active loop: Loop 37, Product Connector Pilot Scenario", roadmap) + self.assertIn("Active loop: Loop 38, Live Connector Readiness Review", roadmap) self.assertIn("| Loop 36: First Product Connector Package Smoke | Complete |", roadmap) - self.assertIn("| Loop 37: Product Connector Pilot Scenario | Next |", roadmap) - self.assertIn("| Loop 38: Live Connector Readiness Review | Candidate |", roadmap) + self.assertIn("| Loop 37: Product Connector Pilot Scenario | Complete |", roadmap) + self.assertIn("| Loop 38: Live Connector Readiness Review | Next |", roadmap) + self.assertIn("| Loop 39: Live Connector Implementation Or Deferral | Candidate |", roadmap) - self.assertIn("sales renewal risk follow-up", roadmap) - self.assertIn("controlled decision point", roadmap) + self.assertIn("sales renewal risk workflow", roadmap) + self.assertIn("manual control gate", roadmap) self.assertIn( "python3 scripts/lark_task_pilot_smoke.py --work-dir /tmp/skill2workflow-lark-task-pilot", roadmap, ) - self.assertIn("do not call the live Lark/Feishu API", roadmap) - self.assertIn("Credential values remain outside Workflow DSL", roadmap) + self.assertIn("live-readiness review before any live Lark/Feishu API implementation", roadmap) + self.assertIn("Credential handling and audit redaction rules are explicit", roadmap) + self.assertIn("Loop 38 readiness review approves a scoped action", roadmap)