From 48b965ec4e792b16a8588af07d8fb03237311da3 Mon Sep 17 00:00:00 2001 From: Myfro9 <36755290+Myfro9@users.noreply.github.com> Date: Mon, 25 May 2026 13:13:00 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=BA=86langgraph-plan-view?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81windows=E4=B8=8B=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=20langgraph=20cli=EF=BC=9B=E6=9B=B4=E6=96=B0=E4=BA=86langgraph?= =?UTF-8?q?-vivado-install=EF=BC=8C=E8=A6=81=E6=B1=82=E5=AE=89=E8=A3=85lan?= =?UTF-8?q?ggraph=20cli=EF=BC=8C=E7=A1=AE=E5=AE=9Aconda=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E7=9A=84=E5=90=8D=E7=A7=B0=E4=B8=BAlanggraph?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 347 ++++++++++++++++ .stfolder/syncthing-folder-594de7.txt | 5 - langgraph-plan-review/SKILL.md | 65 ++- langgraph-plan-review/langgraph.json | 9 + langgraph-plan-review/scripts/graph.py | 19 + .../scripts/manage_dev_server.ps1 | 193 +++++++++ langgraph-plan-review/scripts/run_workflow.py | 369 ++++++++++++------ langgraph-plan-review/scripts/workflow.py | 23 +- langgraph-vivado-install/SKILL.md | 95 ++++- 9 files changed, 980 insertions(+), 145 deletions(-) create mode 100644 .gitignore delete mode 100644 .stfolder/syncthing-folder-594de7.txt create mode 100644 langgraph-plan-review/langgraph.json create mode 100644 langgraph-plan-review/scripts/graph.py create mode 100644 langgraph-plan-review/scripts/manage_dev_server.ps1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e32840 --- /dev/null +++ b/.gitignore @@ -0,0 +1,347 @@ +.system/.codex-system-skills.marker +.system/imagegen/agents/openai.yaml +.system/imagegen/assets/imagegen-small.svg +.system/imagegen/assets/imagegen.png +.system/imagegen/LICENSE.txt +.system/imagegen/references/cli.md +.system/imagegen/references/codex-network.md +.system/imagegen/references/image-api.md +.system/imagegen/references/prompting.md +.system/imagegen/references/sample-prompts.md +.system/imagegen/scripts/image_gen.py +.system/imagegen/scripts/remove_chroma_key.py +.system/imagegen/SKILL.md +.system/openai-docs/agents/openai.yaml +.system/openai-docs/assets/openai-small.svg +.system/openai-docs/assets/openai.png +.system/openai-docs/LICENSE.txt +.system/openai-docs/references/latest-model.md +.system/openai-docs/references/prompting-guide.md +.system/openai-docs/references/upgrade-guide.md +.system/openai-docs/scripts/resolve-latest-model-info.js +.system/openai-docs/SKILL.md +.system/plugin-creator/agents/openai.yaml +.system/plugin-creator/assets/plugin-creator-small.svg +.system/plugin-creator/assets/plugin-creator.png +.system/plugin-creator/references/plugin-json-spec.md +.system/plugin-creator/scripts/create_basic_plugin.py +.system/plugin-creator/scripts/validate_plugin.py +.system/plugin-creator/SKILL.md +.system/skill-creator/agents/openai.yaml +.system/skill-creator/assets/skill-creator-small.svg +.system/skill-creator/assets/skill-creator.png +.system/skill-creator/license.txt +.system/skill-creator/references/openai_yaml.md +.system/skill-creator/scripts/generate_openai_yaml.py +.system/skill-creator/scripts/init_skill.py +.system/skill-creator/scripts/quick_validate.py +.system/skill-creator/SKILL.md +.system/skill-installer/agents/openai.yaml +.system/skill-installer/assets/skill-installer-small.svg +.system/skill-installer/assets/skill-installer.png +.system/skill-installer/LICENSE.txt +.system/skill-installer/scripts/github_utils.py +.system/skill-installer/scripts/install-skill-from-github.py +.system/skill-installer/scripts/list-skills.py +.system/skill-installer/SKILL.md +deploy-to-vercel/resources/deploy-codex.sh +deploy-to-vercel/resources/deploy.sh +deploy-to-vercel/SKILL.md +claude-hud-statusline/SKILL.md +find-skills/SKILL.md +vercel-cli-with-tokens/SKILL.md +vercel-composition-patterns/AGENTS.md +vercel-composition-patterns/metadata.json +vercel-composition-patterns/README.md +vercel-composition-patterns/rules/_sections.md +vercel-composition-patterns/rules/_template.md +vercel-composition-patterns/rules/architecture-avoid-boolean-props.md +vercel-composition-patterns/rules/architecture-compound-components.md +vercel-composition-patterns/rules/patterns-children-over-render-props.md +vercel-composition-patterns/rules/patterns-explicit-variants.md +vercel-composition-patterns/rules/react19-no-forwardref.md +vercel-composition-patterns/rules/state-context-interface.md +vercel-composition-patterns/rules/state-decouple-implementation.md +vercel-composition-patterns/rules/state-lift-state.md +vercel-composition-patterns/SKILL.md +vercel-optimize/AGENTS.md +vercel-optimize/CONTRIBUTING.md +vercel-optimize/lib/auth-route.mjs +vercel-optimize/lib/budget-summary.mjs +vercel-optimize/lib/citations.mjs +vercel-optimize/lib/cost-coverage.mjs +vercel-optimize/lib/dedup-recs.mjs +vercel-optimize/lib/deep-dive.mjs +vercel-optimize/lib/display-labels.mjs +vercel-optimize/lib/extract-claims.mjs +vercel-optimize/lib/framework-support.mjs +vercel-optimize/lib/gates/build-minutes-fanout.mjs +vercel-optimize/lib/gates/cold-start.mjs +vercel-optimize/lib/gates/contract.mjs +vercel-optimize/lib/gates/cwv-poor.mjs +vercel-optimize/lib/gates/external-api-slow.mjs +vercel-optimize/lib/gates/hard-gates.mjs +vercel-optimize/lib/gates/index.mjs +vercel-optimize/lib/gates/isr-overrevalidation.mjs +vercel-optimize/lib/gates/middleware-heavy.mjs +vercel-optimize/lib/gates/observability-events-attribution.mjs +vercel-optimize/lib/gates/platform-bot-protection.mjs +vercel-optimize/lib/gates/platform-fluid-compute.mjs +vercel-optimize/lib/gates/region-misconfig.mjs +vercel-optimize/lib/gates/route-errors.mjs +vercel-optimize/lib/gates/scanner-driven.mjs +vercel-optimize/lib/gates/select-candidates.mjs +vercel-optimize/lib/gates/slow-route.mjs +vercel-optimize/lib/gates/types.d.ts +vercel-optimize/lib/gates/uncached-route.mjs +vercel-optimize/lib/gates/usage-spike-triage.mjs +vercel-optimize/lib/grade-recommendation.mjs +vercel-optimize/lib/impact-label.mjs +vercel-optimize/lib/impact-magnitude.mjs +vercel-optimize/lib/investigation-brief.mjs +vercel-optimize/lib/project-facts.mjs +vercel-optimize/lib/queries.mjs +vercel-optimize/lib/reconcile-candidates.mjs +vercel-optimize/lib/render-report.mjs +vercel-optimize/lib/repo-root.mjs +vercel-optimize/lib/route-normalize.mjs +vercel-optimize/lib/sanitizers/bot-protection-certainty.mjs +vercel-optimize/lib/sanitizers/cache-tag-invalidation-certainty.mjs +vercel-optimize/lib/sanitizers/count-correct.mjs +vercel-optimize/lib/sanitizers/function-duration-invocations.mjs +vercel-optimize/lib/sanitizers/index.mjs +vercel-optimize/lib/sanitizers/middleware-conflict.mjs +vercel-optimize/lib/sanitizers/missing-citation.mjs +vercel-optimize/lib/sanitizers/pre-release.mjs +vercel-optimize/lib/sanitizers/rate-limit.mjs +vercel-optimize/lib/sanitizers/rendering-mode-mislabel.mjs +vercel-optimize/lib/sanitizers/undeclared-dep.mjs +vercel-optimize/lib/sanitizers/vercel-directive-strip.mjs +vercel-optimize/lib/sanitizers/window-units.mjs +vercel-optimize/lib/scanners/cache-components-suspense-dedupe.mjs +vercel-optimize/lib/scanners/edge-heavy-import.mjs +vercel-optimize/lib/scanners/force-dynamic.mjs +vercel-optimize/lib/scanners/headers-in-page.mjs +vercel-optimize/lib/scanners/index.mjs +vercel-optimize/lib/scanners/large-static-asset.mjs +vercel-optimize/lib/scanners/max-age-without-s-maxage.mjs +vercel-optimize/lib/scanners/middleware-broad-matcher.mjs +vercel-optimize/lib/scanners/missing-cache-headers.mjs +vercel-optimize/lib/scanners/prisma-include-tree.mjs +vercel-optimize/lib/scanners/region-pin-in-config.mjs +vercel-optimize/lib/scanners/source-maps-production.mjs +vercel-optimize/lib/scanners/sveltekit-prerender-missing.mjs +vercel-optimize/lib/scanners/turbo-force-bypass.mjs +vercel-optimize/lib/scanners/unoptimized-image.mjs +vercel-optimize/lib/scanners/use-cache-date-stamp.mjs +vercel-optimize/lib/support-topics.mjs +vercel-optimize/lib/throttle.mjs +vercel-optimize/lib/util.mjs +vercel-optimize/lib/vercel.mjs +vercel-optimize/lib/verify-claim.mjs +vercel-optimize/lib/workspace-resolver.mjs +vercel-optimize/metadata.json +vercel-optimize/README.md +vercel-optimize/references/candidates.md +vercel-optimize/references/data-collection.md +vercel-optimize/references/docs-library.json +vercel-optimize/references/doctrine.md +vercel-optimize/references/observability-plus.md +vercel-optimize/references/playbooks/ai-application.md +vercel-optimize/references/playbooks/api-service.md +vercel-optimize/references/playbooks/content-site.md +vercel-optimize/references/playbooks/ecommerce.md +vercel-optimize/references/playbooks/marketing.md +vercel-optimize/references/playbooks/README.md +vercel-optimize/references/playbooks/saas.md +vercel-optimize/references/playbooks/sveltekit.md +vercel-optimize/references/recommendations.md +vercel-optimize/references/scanner-patterns.md +vercel-optimize/references/scoring.md +vercel-optimize/references/support-topics/astro-edge-middleware-scope.md +vercel-optimize/references/support-topics/astro-output-mode-and-isr.md +vercel-optimize/references/support-topics/auth-preserving-parallelization.md +vercel-optimize/references/support-topics/bot-protection-product-guardrails.md +vercel-optimize/references/support-topics/build-minutes-monorepo-fanout.md +vercel-optimize/references/support-topics/cache-components-static-shell-boundaries.md +vercel-optimize/references/support-topics/cache-components-suspense-dedupe-pitfall.md +vercel-optimize/references/support-topics/cdn-cache-auth-safety.md +vercel-optimize/references/support-topics/cold-start-initialization-bundle.md +vercel-optimize/references/support-topics/core-web-vitals-client-bottlenecks.md +vercel-optimize/references/support-topics/database-egress-pooling-region.md +vercel-optimize/references/support-topics/dynamic-rendering-traps.md +vercel-optimize/references/support-topics/external-api-critical-path-platform.md +vercel-optimize/references/support-topics/external-api-critical-path.md +vercel-optimize/references/support-topics/fast-data-transfer-payloads.md +vercel-optimize/references/support-topics/fluid-compute-caveats.md +vercel-optimize/references/support-topics/function-duration-io-and-after.md +vercel-optimize/references/support-topics/function-invocation-reduction.md +vercel-optimize/references/support-topics/function-region-misconfiguration-ttfb.md +vercel-optimize/references/support-topics/image-optimization-cost-control.md +vercel-optimize/references/support-topics/isr-revalidation-static-generation.md +vercel-optimize/references/support-topics/middleware-proxy-edge-cost.md +vercel-optimize/references/support-topics/next-fetch-revalidate-floor.md +vercel-optimize/references/support-topics/next-font-cls-self-hosting.md +vercel-optimize/references/support-topics/next-heavy-ui-lazy-load-boundaries.md +vercel-optimize/references/support-topics/next-image-lcp-preload-sizes.md +vercel-optimize/references/support-topics/next-route-handler-get-cache-defaults.md +vercel-optimize/references/support-topics/next-script-third-party-strategy.md +vercel-optimize/references/support-topics/nextjs-version-cache-semantics.md +vercel-optimize/references/support-topics/not-found-catchall-request-waste.md +vercel-optimize/references/support-topics/nuxt-route-rules-cache-isr.md +vercel-optimize/references/support-topics/observability-events-cost-attribution.md +vercel-optimize/references/support-topics/post-response-work-waituntil.md +vercel-optimize/references/support-topics/README.md +vercel-optimize/references/support-topics/route-error-durable-offload.md +vercel-optimize/references/support-topics/route-error-runtime-limits.md +vercel-optimize/references/support-topics/runtime-cache-reusable-data.md +vercel-optimize/references/support-topics/sveltekit-isr-prerender-safety.md +vercel-optimize/references/support-topics/sveltekit-split-cold-start-tradeoff.md +vercel-optimize/references/support-topics/usage-spike-triage.md +vercel-optimize/references/support-topics/use-cache-date-stamp-isr-write-amplifier.md +vercel-optimize/references/support-topics/use-cache-remote-shared-origin-data.md +vercel-optimize/references/verification.md +vercel-optimize/references/voice.md +vercel-optimize/scripts/budget-summary.mjs +vercel-optimize/scripts/build-docs.mjs +vercel-optimize/scripts/check-citations.mjs +vercel-optimize/scripts/check-docs-fresh.mjs +vercel-optimize/scripts/collect-signals.mjs +vercel-optimize/scripts/collect-sub-agent-outputs.mjs +vercel-optimize/scripts/deep-dive.mjs +vercel-optimize/scripts/gate-investigations.mjs +vercel-optimize/scripts/merge-signals.mjs +vercel-optimize/scripts/prepare-investigation-brief.mjs +vercel-optimize/scripts/reconcile-candidates.mjs +vercel-optimize/scripts/render-report.mjs +vercel-optimize/scripts/scan-codebase.mjs +vercel-optimize/scripts/verify-and-regen.mjs +vercel-optimize/scripts/verify-finding.mjs +vercel-optimize/SKILL.md +vercel-react-best-practices/AGENTS.md +vercel-react-best-practices/metadata.json +vercel-react-best-practices/README.md +vercel-react-best-practices/rules/_sections.md +vercel-react-best-practices/rules/_template.md +vercel-react-best-practices/rules/advanced-effect-event-deps.md +vercel-react-best-practices/rules/advanced-event-handler-refs.md +vercel-react-best-practices/rules/advanced-init-once.md +vercel-react-best-practices/rules/advanced-use-latest.md +vercel-react-best-practices/rules/async-api-routes.md +vercel-react-best-practices/rules/async-cheap-condition-before-await.md +vercel-react-best-practices/rules/async-defer-await.md +vercel-react-best-practices/rules/async-dependencies.md +vercel-react-best-practices/rules/async-parallel.md +vercel-react-best-practices/rules/async-suspense-boundaries.md +vercel-react-best-practices/rules/bundle-analyzable-paths.md +vercel-react-best-practices/rules/bundle-barrel-imports.md +vercel-react-best-practices/rules/bundle-conditional.md +vercel-react-best-practices/rules/bundle-defer-third-party.md +vercel-react-best-practices/rules/bundle-dynamic-imports.md +vercel-react-best-practices/rules/bundle-preload.md +vercel-react-best-practices/rules/client-event-listeners.md +vercel-react-best-practices/rules/client-localstorage-schema.md +vercel-react-best-practices/rules/client-passive-event-listeners.md +vercel-react-best-practices/rules/client-swr-dedup.md +vercel-react-best-practices/rules/js-batch-dom-css.md +vercel-react-best-practices/rules/js-cache-function-results.md +vercel-react-best-practices/rules/js-cache-property-access.md +vercel-react-best-practices/rules/js-cache-storage.md +vercel-react-best-practices/rules/js-combine-iterations.md +vercel-react-best-practices/rules/js-early-exit.md +vercel-react-best-practices/rules/js-flatmap-filter.md +vercel-react-best-practices/rules/js-hoist-regexp.md +vercel-react-best-practices/rules/js-index-maps.md +vercel-react-best-practices/rules/js-length-check-first.md +vercel-react-best-practices/rules/js-min-max-loop.md +vercel-react-best-practices/rules/js-request-idle-callback.md +vercel-react-best-practices/rules/js-set-map-lookups.md +vercel-react-best-practices/rules/js-tosorted-immutable.md +vercel-react-best-practices/rules/rendering-activity.md +vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +vercel-react-best-practices/rules/rendering-conditional-render.md +vercel-react-best-practices/rules/rendering-content-visibility.md +vercel-react-best-practices/rules/rendering-hoist-jsx.md +vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md +vercel-react-best-practices/rules/rendering-resource-hints.md +vercel-react-best-practices/rules/rendering-script-defer-async.md +vercel-react-best-practices/rules/rendering-svg-precision.md +vercel-react-best-practices/rules/rendering-usetransition-loading.md +vercel-react-best-practices/rules/rerender-defer-reads.md +vercel-react-best-practices/rules/rerender-dependencies.md +vercel-react-best-practices/rules/rerender-derived-state-no-effect.md +vercel-react-best-practices/rules/rerender-derived-state.md +vercel-react-best-practices/rules/rerender-functional-setstate.md +vercel-react-best-practices/rules/rerender-lazy-state-init.md +vercel-react-best-practices/rules/rerender-memo-with-default-value.md +vercel-react-best-practices/rules/rerender-memo.md +vercel-react-best-practices/rules/rerender-move-effect-to-event.md +vercel-react-best-practices/rules/rerender-no-inline-components.md +vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md +vercel-react-best-practices/rules/rerender-split-combined-hooks.md +vercel-react-best-practices/rules/rerender-transitions.md +vercel-react-best-practices/rules/rerender-use-deferred-value.md +vercel-react-best-practices/rules/rerender-use-ref-transient-values.md +vercel-react-best-practices/rules/server-after-nonblocking.md +vercel-react-best-practices/rules/server-auth-actions.md +vercel-react-best-practices/rules/server-cache-lru.md +vercel-react-best-practices/rules/server-cache-react.md +vercel-react-best-practices/rules/server-dedup-props.md +vercel-react-best-practices/rules/server-hoist-static-io.md +vercel-react-best-practices/rules/server-no-shared-module-state.md +vercel-react-best-practices/rules/server-parallel-fetching.md +vercel-react-best-practices/rules/server-parallel-nested-fetching.md +vercel-react-best-practices/rules/server-serialization.md +vercel-react-best-practices/SKILL.md +vercel-react-native-skills/AGENTS.md +vercel-react-native-skills/metadata.json +vercel-react-native-skills/README.md +vercel-react-native-skills/rules/_sections.md +vercel-react-native-skills/rules/_template.md +vercel-react-native-skills/rules/animation-derived-value.md +vercel-react-native-skills/rules/animation-gesture-detector-press.md +vercel-react-native-skills/rules/animation-gpu-properties.md +vercel-react-native-skills/rules/design-system-compound-components.md +vercel-react-native-skills/rules/fonts-config-plugin.md +vercel-react-native-skills/rules/imports-design-system-folder.md +vercel-react-native-skills/rules/js-hoist-intl.md +vercel-react-native-skills/rules/list-performance-callbacks.md +vercel-react-native-skills/rules/list-performance-function-references.md +vercel-react-native-skills/rules/list-performance-images.md +vercel-react-native-skills/rules/list-performance-inline-objects.md +vercel-react-native-skills/rules/list-performance-item-expensive.md +vercel-react-native-skills/rules/list-performance-item-memo.md +vercel-react-native-skills/rules/list-performance-item-types.md +vercel-react-native-skills/rules/list-performance-virtualize.md +vercel-react-native-skills/rules/monorepo-native-deps-in-app.md +vercel-react-native-skills/rules/monorepo-single-dependency-versions.md +vercel-react-native-skills/rules/navigation-native-navigators.md +vercel-react-native-skills/rules/react-compiler-destructure-functions.md +vercel-react-native-skills/rules/react-compiler-reanimated-shared-values.md +vercel-react-native-skills/rules/react-state-dispatcher.md +vercel-react-native-skills/rules/react-state-fallback.md +vercel-react-native-skills/rules/react-state-minimize.md +vercel-react-native-skills/rules/rendering-no-falsy-and.md +vercel-react-native-skills/rules/rendering-text-in-text-component.md +vercel-react-native-skills/rules/scroll-position-no-state.md +vercel-react-native-skills/rules/state-ground-truth.md +vercel-react-native-skills/rules/ui-expo-image.md +vercel-react-native-skills/rules/ui-image-gallery.md +vercel-react-native-skills/rules/ui-measure-views.md +vercel-react-native-skills/rules/ui-menus.md +vercel-react-native-skills/rules/ui-native-modals.md +vercel-react-native-skills/rules/ui-pressable.md +vercel-react-native-skills/rules/ui-safe-area-scroll.md +vercel-react-native-skills/rules/ui-scrollview-content-inset.md +vercel-react-native-skills/rules/ui-styling.md +vercel-react-native-skills/SKILL.md +vercel-react-view-transitions/AGENTS.md +vercel-react-view-transitions/metadata.json +vercel-react-view-transitions/README.md +vercel-react-view-transitions/references/css-recipes.md +vercel-react-view-transitions/references/implementation.md +vercel-react-view-transitions/references/nextjs.md +vercel-react-view-transitions/references/patterns.md +vercel-react-view-transitions/SKILL.md +web-design-guidelines/SKILL.md diff --git a/.stfolder/syncthing-folder-594de7.txt b/.stfolder/syncthing-folder-594de7.txt deleted file mode 100644 index 93dcc42..0000000 --- a/.stfolder/syncthing-folder-594de7.txt +++ /dev/null @@ -1,5 +0,0 @@ -# This directory is a Syncthing folder marker. -# Do not delete. - -folderID: ggxeg-zoyv2 -created: 2026-05-09T01:23:05-05:00 diff --git a/langgraph-plan-review/SKILL.md b/langgraph-plan-review/SKILL.md index df2aaa2..faf0814 100644 --- a/langgraph-plan-review/SKILL.md +++ b/langgraph-plan-review/SKILL.md @@ -7,6 +7,29 @@ description: Use the LangGraph plan-review workflow from the current Claude Code Use this skill to run the LangGraph plan-review workflow. All scripts are bundled under the skill's own `scripts\` directory (`C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\`) — no dependency on `E:\LangGraph_Design\lab00`. +## Pre-flight: Environment Setup + +Before running the workflow for the first time (or when unsure the environment is ready), verify that the required packages are installed in the `langgraph` conda env: + +```powershell +# Install langgraph dev server, SDK, and HTTP client +conda run -n langgraph pip install "langgraph-cli[inmem]" langgraph-sdk httpx + +# Verify the SDK is importable +conda run -n langgraph python -c "from langgraph_sdk import get_sync_client; print('SDK OK')" + +# Verify the langgraph dev command is available +conda run -n langgraph langgraph --version +``` + +| Package | Purpose | +|---|---| +| `langgraph-cli[inmem]` | Provides the `langgraph dev` server command | +| `langgraph-sdk` | Python client used by `run_workflow.py` to submit runs | +| `httpx` | HTTP health-check in `run_workflow.py` | + +**Codex auth** (if not already set up): `C:\Users\bowei\.codex\auth.json` must contain a valid ChatGPT OAuth token. Run `codex auth login` if the file is missing. + ## Invariants - Use the current Claude Code session for all Claude Code work. Do not launch another `claude` process. @@ -22,13 +45,40 @@ Use this skill to run the LangGraph plan-review workflow. All scripts are bundle - Keep all workflow files under `\Notes` unless the user explicitly overrides `-NotesDir`. - `TaskId` is the numeric suffix, such as `01`; write the plan to `Notes\Task01.md`. - `discussxx-yy.md` numbering is managed by the LangGraph app for Codex review files. If Claude rejects a review comment, create the next numbered discuss file manually before rerunning review. -- The workflow uses LangGraph `interrupt()` and persists state in `workflow_state.db`. +- The workflow uses LangGraph `interrupt()` and persists state via the langgraph dev server. - Each `run_langgraph.ps1` invocation advances the graph to the next interrupt or completion. Use `-Resume` to continue the same thread after completing the requested Claude Code action. +## Dev Server Lifecycle + +The `langgraph dev` server must be running for the entire duration of a workflow session. +Use `manage_dev_server.ps1` to start it before the first `run_langgraph.ps1` call and stop it after the workflow completes. + +```powershell +# Start (idempotent — safe to call even if already running) +C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\manage_dev_server.ps1 start + +# Stop (call once when workflow returns {"status":"done"}) +C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\manage_dev_server.ps1 stop + +# Status check +C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\manage_dev_server.ps1 status +``` + +- `start` waits up to 45 s for `http://127.0.0.1:2024/ok` to respond, then prints the Studio URL. +- `stop` kills the process by PID file and removes it; falls back to killing by port 2024 if the PID is stale. +- If the server is already running, `start` is a no-op. +- Server stdout/stderr logs are written to `%TEMP%\langgraph_dev_server.stdout.log` and `%TEMP%\langgraph_dev_server.stderr.log` for debugging. +- **Studio URL**: `https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024` — open in browser to track workflow execution in real time. + ## Workflow 1. Determine `TaskId` from the user. If missing, ask for it. -2. Start or resume the LangGraph app: +2. Start the dev server (idempotent): + ```powershell + C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\manage_dev_server.ps1 start + ``` + Wait for the "Ready" confirmation before proceeding. Open the Studio URL in a browser for real-time tracking. +3. Start or resume the LangGraph app: ```powershell # Use double backslashes (\\) in the JSON string so Windows paths are valid JSON. # $task_path / $latest_discuss_path / etc. are Python Template placeholders — keep them literal. @@ -39,7 +89,7 @@ Use this skill to run the LangGraph plan-review workflow. All scripts are bundle -CodexReviewCommand $codexCmd ` -Timeout 300 ``` -3. Inspect the returned JSON. If `status` is `waiting`, perform `action_required` in the current Claude Code session: +4. Inspect the returned JSON. If `status` is `waiting`, perform `action_required` in the current Claude Code session: - `write_plan`: write `Task.md` with project background, task objective, and concrete implementation plan, then resume with `-Resume 'true'`. - `decide`: read the discuss file and resume with `-Resume '{"accepts":true}'` or `-Resume '{"accepts":false,"reason":"..."}'`. - `revise`: revise `Task.md`, then resume with `-Resume 'true'`. @@ -48,10 +98,13 @@ Use this skill to run the LangGraph plan-review workflow. All scripts are bundle **Why `review_task.py`**: Using the shim keeps all codex-specific logic in Python, and the JSON array stays simple. `run_langgraph.ps1` base64-encodes the JSON command and resume payload before passing them through `conda run`, so normal PowerShell single-quoted JSON can be used. **Windows paths inside the JSON string must use `\\` (double backslash) to produce valid JSON**, e.g. `C:\\Users\\bowei\\...`. `run_langgraph.ps1` automatically sets `PYTHONIOENCODING=utf-8` so prompts containing Unicode (Chinese, etc.) pass through correctly. -4. Inspect the command output after each resume. +5. Inspect the command output after each resume. - Check `"status"` to determine next action: `"waiting"` means perform the next `action_required`; `"done"` means the workflow is complete. - If a new `latest_discuss_path` is present in the output, read that discuss file for context. -5. Stop when the workflow returns `{"status":"done", ...}`. +6. Stop when the workflow returns `{"status":"done", ...}`: + ```powershell + C:\Users\bowei\.claude\skills\langgraph-plan-review\scripts\manage_dev_server.ps1 stop + ``` ## Rejection File Rule @@ -82,3 +135,5 @@ Write the rejection content directly to `action_required.output_path`: - `review_task.py` persists the Codex session id through LangGraph state and uses `codex exec resume` on later review passes. - `review_task.py` runs Codex with the original Claude Code launch working directory as cwd. - Codex review output is normalized to JSON for `agents.py`: `{"has_serious_issue":true/false,"comment":"...","codex_session_id":"..."}`. +- If the dev server restarts, `run_workflow.py` detects that the stored thread no longer exists and creates a new thread automatically. +- `run_workflow.py` passes the codex command and timeout as `config["configurable"]` per run — no server restart needed when switching projects. diff --git a/langgraph-plan-review/langgraph.json b/langgraph-plan-review/langgraph.json new file mode 100644 index 0000000..a64c0dc --- /dev/null +++ b/langgraph-plan-review/langgraph.json @@ -0,0 +1,9 @@ +{ + "dependencies": [ + "langgraph", + "langgraph-checkpoint-sqlite" + ], + "graphs": { + "plan_review": "./scripts/graph.py:graph" + } +} diff --git a/langgraph-plan-review/scripts/graph.py b/langgraph-plan-review/scripts/graph.py new file mode 100644 index 0000000..2ba17a9 --- /dev/null +++ b/langgraph-plan-review/scripts/graph.py @@ -0,0 +1,19 @@ +"""Module-level graph export for `langgraph dev`. + +The Codex review command is injected per-run via config["configurable"], +so no runner is needed at build time. The server loads this module once; +every submitted run brings its own command through the LangGraph SDK client. +""" +from __future__ import annotations + +import sys +from pathlib import Path + +# Ensure sibling modules (agents, workflow) are importable when langgraph dev +# loads this file from the skill root via ./scripts/graph.py:graph. +sys.path.insert(0, str(Path(__file__).parent)) + +from workflow import build_workflow # noqa: E402 + +# langgraph dev manages its own persistence — do not pass a checkpointer here. +graph = build_workflow() diff --git a/langgraph-plan-review/scripts/manage_dev_server.ps1 b/langgraph-plan-review/scripts/manage_dev_server.ps1 new file mode 100644 index 0000000..1c55c86 --- /dev/null +++ b/langgraph-plan-review/scripts/manage_dev_server.ps1 @@ -0,0 +1,193 @@ +<# +.SYNOPSIS + Manage the langgraph dev server on Windows. +.DESCRIPTION + Start, stop, or check the status of the langgraph dev server. + The server starts from the skill root (where langgraph.json lives) so the + graph module is discovered correctly. +.PARAMETER Command + start | stop | status +.EXAMPLE + manage_dev_server.ps1 start # idempotent — safe to call if already running + manage_dev_server.ps1 stop # call once workflow returns {"status":"done"} + manage_dev_server.ps1 status # print running/stopped + Studio URL +#> +param([string]$Command = "status") + +$SkillDir = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path +$PidFile = Join-Path $env:TEMP "langgraph_dev_server.pid" +$StdoutLog = Join-Path $env:TEMP "langgraph_dev_server.stdout.log" +$StderrLog = Join-Path $env:TEMP "langgraph_dev_server.stderr.log" +$ApiUrl = "http://127.0.0.1:2024" +$MaxWait = 45 # seconds to wait for the server to become ready + +# --- helpers ----------------------------------------------------------- + +function Test-HttpOk([string]$Url) { + try { + $r = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 3 -ErrorAction Stop + return ($r.StatusCode -ge 200 -and $r.StatusCode -lt 300) + } catch { + return $false + } +} + +function Get-StoredPid { + if (-not (Test-Path $PidFile)) { return 0 } + $raw = (Get-Content $PidFile -Raw -ErrorAction SilentlyContinue).Trim() + if (-not $raw) { return 0 } + [int]$raw +} + +function Test-ServerRunning { + $pid = Get-StoredPid + if ($pid -eq 0) { return $false } + $proc = Get-Process -Id $pid -ErrorAction SilentlyContinue + if (-not $proc) { return $false } + return (Test-HttpOk "$ApiUrl/ok") +} + +function Find-LangGraphExe { + $up = $env:USERPROFILE + $candidates = @( + "$up\.conda\envs\langgraph\Scripts\langgraph.exe", + "$up\miniconda3\envs\langgraph\Scripts\langgraph.exe", + "$up\anaconda3\envs\langgraph\Scripts\langgraph.exe", + "$up\miniforge3\envs\langgraph\Scripts\langgraph.exe", + "$up\AppData\Local\miniconda3\envs\langgraph\Scripts\langgraph.exe", + "C:\ProgramData\miniconda3\envs\langgraph\Scripts\langgraph.exe", + "C:\ProgramData\anaconda3\envs\langgraph\Scripts\langgraph.exe" + ) + foreach ($c in $candidates) { + if (Test-Path $c) { return $c } + } + # Try PATH + $cmd = Get-Command langgraph -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + # Try resolving via conda + $condaCandidates = @($env:CONDA_EXE, "C:\ProgramData\miniconda3\Scripts\conda.exe", + "$up\AppData\Local\miniconda3\Scripts\conda.exe") + foreach ($conda in $condaCandidates) { + if ($conda -and (Test-Path $conda)) { + try { + $lgPath = (& $conda run -n langgraph python -c "import shutil; print(shutil.which('langgraph') or '')" 2>$null).Trim() + if ($lgPath -and (Test-Path $lgPath)) { return $lgPath } + } catch {} + } + } + throw ("Cannot find langgraph.exe in the 'langgraph' conda env. " + + "Install it: conda run -n langgraph pip install 'langgraph-cli[inmem]'") +} + +function Stop-ServerProcess([int]$PidValue) { + $stopped = $false + if ($PidValue -gt 0) { + $proc = Get-Process -Id $PidValue -ErrorAction SilentlyContinue + if ($proc) { + Stop-Process -Id $PidValue -Force -ErrorAction SilentlyContinue + $stopped = $true + } + } + # Fallback: kill whoever is listening on port 2024 + if (-not $stopped) { + $conn = Get-NetTCPConnection -LocalPort 2024 -State Listen -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($conn) { + Stop-Process -Id $conn.OwningProcess -Force -ErrorAction SilentlyContinue + Write-Host "[dev-server] Stopped process on port 2024 (PID $($conn.OwningProcess))" + $stopped = $true + } + } + return $stopped +} + +# --- command dispatch --------------------------------------------------- + +switch ($Command) { + + "start" { + if (Test-ServerRunning) { + $pid = Get-StoredPid + Write-Host "[dev-server] Already running (PID $pid) at $ApiUrl" + Write-Host "[dev-server] Studio: https://smith.langchain.com/studio/?baseUrl=$ApiUrl" + exit 0 + } + + $lgExe = Find-LangGraphExe + Write-Host "[dev-server] Starting langgraph dev server..." + Write-Host "[dev-server] exe: $lgExe" + Write-Host "[dev-server] dir: $SkillDir" + + # Remove stale logs + Remove-Item $StdoutLog -ErrorAction SilentlyContinue + Remove-Item $StderrLog -ErrorAction SilentlyContinue + + $proc = Start-Process -FilePath $lgExe ` + -ArgumentList "dev", "--no-browser" ` + -WorkingDirectory $SkillDir ` + -RedirectStandardOutput $StdoutLog ` + -RedirectStandardError $StderrLog ` + -WindowStyle Hidden -PassThru + + if (-not $proc) { + Write-Error "[dev-server] Failed to launch process" + exit 1 + } + $proc.Id | Out-File -FilePath $PidFile -Encoding ascii -NoNewline + + # Wait for server to respond + $elapsed = 0 + while (-not (Test-HttpOk "$ApiUrl/ok") -and $elapsed -lt $MaxWait) { + Start-Sleep -Seconds 1 + $elapsed++ + if ($proc.HasExited) { + Write-Error "[dev-server] Process exited unexpectedly (code $($proc.ExitCode))" + Write-Host "--- stdout ($StdoutLog) ---" + if (Test-Path $StdoutLog) { Get-Content $StdoutLog -Tail 30 } + Write-Host "--- stderr ($StderrLog) ---" + if (Test-Path $StderrLog) { Get-Content $StderrLog -Tail 30 } + Remove-Item $PidFile -ErrorAction SilentlyContinue + exit 1 + } + } + + if ($elapsed -ge $MaxWait) { + Write-Error "[dev-server] Server did not become ready within ${MaxWait}s" + Write-Host "--- stdout ($StdoutLog) ---" + if (Test-Path $StdoutLog) { Get-Content $StdoutLog -Tail 30 } + Write-Host "--- stderr ($StderrLog) ---" + if (Test-Path $StderrLog) { Get-Content $StderrLog -Tail 30 } + exit 1 + } + + Write-Host "[dev-server] Ready in ${elapsed}s" + Write-Host "[dev-server] API: $ApiUrl" + Write-Host "[dev-server] Studio: https://smith.langchain.com/studio/?baseUrl=$ApiUrl" + Write-Host "[dev-server] Logs: $StdoutLog / $StderrLog" + } + + "stop" { + $pid = Get-StoredPid + if ((Stop-ServerProcess -PidValue $pid)) { + Write-Host "[dev-server] Stopped (PID $pid)" + } else { + Write-Host "[dev-server] Not running (nothing to stop)" + } + Remove-Item $PidFile -ErrorAction SilentlyContinue + } + + "status" { + if (Test-ServerRunning) { + $pid = Get-StoredPid + Write-Host "[dev-server] Running (PID $pid) at $ApiUrl" + Write-Host "[dev-server] Studio: https://smith.langchain.com/studio/?baseUrl=$ApiUrl" + } else { + Write-Host "[dev-server] Not running" + } + } + + default { + Write-Error "Usage: manage_dev_server.ps1 {start|stop|status}" + exit 1 + } +} diff --git a/langgraph-plan-review/scripts/run_workflow.py b/langgraph-plan-review/scripts/run_workflow.py index d1f2c01..490c401 100644 --- a/langgraph-plan-review/scripts/run_workflow.py +++ b/langgraph-plan-review/scripts/run_workflow.py @@ -1,3 +1,12 @@ +"""run_workflow.py — submit workflow runs to the local langgraph dev server. + +Instead of running the graph directly in-process, every invocation sends a +run to the langgraph dev server (http://127.0.0.1:2024) via the LangGraph +SDK sync client. This lets LangGraph Studio track execution in real time. + +The Codex review command is forwarded in the run's config so the server does +not need to be restarted when the command changes between projects. +""" from __future__ import annotations import argparse @@ -5,63 +14,68 @@ import base64 import json import logging import sys +import time from pathlib import Path -from langgraph.checkpoint.sqlite import SqliteSaver -from langgraph.types import Command - -from agents import AgentRunner -from workflow import build_workflow, resolve_notes_dir, resolve_work_dir +SERVER_URL = "http://127.0.0.1:2024" +ASSISTANT_ID = "plan_review" +THREAD_MAP_FILENAME = "workflow_threads.json" -DB_FILENAME = "workflow_state.db" +# ── path helpers ────────────────────────────────────────────────────────────── + +def _resolve_work_dir(value: str) -> Path: + return Path(value or ".").resolve() -def parse_command(value: str | None) -> list[str] | None: - if not value: - return None - try: - parsed = json.loads(value) - except json.JSONDecodeError: - # Fallback: the string may contain Windows paths with unescaped backslashes - # that make it invalid JSON. Re-escape backslashes and retry before giving up. +def _resolve_notes_dir(work_dir: Path, value: str) -> Path: + notes_dir = Path(value or "Notes") + if not notes_dir.is_absolute(): + notes_dir = work_dir / notes_dir + return notes_dir.resolve() + + +# ── thread map (task_id → server thread_id) ────────────────────────────────── + +def _load_thread_map(notes_dir: Path) -> dict: + path = notes_dir / THREAD_MAP_FILENAME + if path.exists(): try: - import re - # Replace bare backslashes (not already doubled) with \\ - re_escaped = re.sub(r'\\(?![\\"/bfnrtu])', r'\\\\', value) - parsed = json.loads(re_escaped) - except json.JSONDecodeError: - stripped = value.strip() - if stripped.startswith("[") and stripped.endswith("]"): - # Last-resort comma split; strip surrounding quotes from each item. - parsed = [item.strip().strip('"').strip("'") - for item in stripped[1:-1].split(",")] - else: - raise - if not isinstance(parsed, list) or not all(isinstance(item, str) for item in parsed): - raise argparse.ArgumentTypeError("command must be a JSON string array") - return parsed + return json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return {} + return {} -def parse_command_b64(value: str | None) -> list[str] | None: - if not value: - return None - try: - decoded = base64.b64decode(value).decode("utf-8") - except Exception as exc: - raise argparse.ArgumentTypeError(f"invalid base64 command: {exc}") from exc - return parse_command(decoded) +def _save_thread_map(notes_dir: Path, mapping: dict) -> None: + path = notes_dir / THREAD_MAP_FILENAME + path.write_text(json.dumps(mapping, indent=2, ensure_ascii=False), encoding="utf-8") -def parse_json_b64(value: str | None): - if not value: - return None - try: - decoded = base64.b64decode(value).decode("utf-8") - except Exception as exc: - raise argparse.ArgumentTypeError(f"invalid base64 JSON: {exc}") from exc - return json.loads(decoded) +# ── interrupt extraction ────────────────────────────────────────────────────── +def _extract_interrupt(state: dict): + """Return the first interrupt value from a thread-state dict, or None.""" + # Primary: tasks[].interrupts (langgraph-api >= 0.5) + for task in state.get("tasks", []): + for intr in task.get("interrupts", []): + if isinstance(intr, dict): + return intr.get("value", intr) + return intr + # Fallback: __interrupt__ channel value + interrupt_list = state.get("values", {}).get("__interrupt__", []) + if interrupt_list: + intr = interrupt_list[0] + return intr.value if hasattr(intr, "value") else intr + return None + + +def _is_interrupted(state: dict) -> bool: + """Return True when the thread is paused at an interrupt.""" + return bool(state.get("next")) and _extract_interrupt(state) is not None + + +# ── core workflow runner ────────────────────────────────────────────────────── def run_workflow( *, @@ -69,113 +83,210 @@ def run_workflow( user_prompt: str = "", notes_dir: str = "Notes", work_dir: str = "", - runner: AgentRunner, + codex_review_command: list[str], thread_id: str | None = None, resume_payload=None, + timeout: int = 600, + server_url: str = SERVER_URL, ) -> dict: - effective_thread = thread_id or task_id - resolved_work_dir = resolve_work_dir(work_dir) - resolved_notes_dir = resolve_notes_dir(resolved_work_dir, notes_dir) + try: + from langgraph_sdk import get_sync_client + except ImportError: + raise ImportError( + "langgraph_sdk not installed. Run: " + "conda run -n langgraph pip install langgraph-sdk" + ) + + import httpx + + effective_task_id = thread_id or task_id + resolved_work_dir = _resolve_work_dir(work_dir) + resolved_notes_dir = _resolve_notes_dir(resolved_work_dir, notes_dir) resolved_notes_dir.mkdir(parents=True, exist_ok=True) - db_path = resolved_notes_dir / DB_FILENAME - with SqliteSaver.from_conn_string(str(db_path)) as checkpointer: - app = build_workflow(runner, checkpointer=checkpointer) - config = {"configurable": {"thread_id": effective_thread}} + # ── verify server is up ─────────────────────────────────────────────────── + try: + httpx.get(f"{server_url}/ok", timeout=5).raise_for_status() + except Exception as exc: + raise RuntimeError( + f"langgraph dev server not reachable at {server_url}.\n" + f"Start it first:\n" + f" PowerShell -File C:\\Users\\bowei\\.claude\\skills\\langgraph-plan-review\\scripts\\manage_dev_server.ps1 start\n" + f"Error: {exc}" + ) - if resume_payload is not None: - result = app.invoke(Command(resume=resume_payload), config=config) - else: - result = app.invoke( - { - "task_id": task_id, - "user_prompt": user_prompt, - "notes_dir": str(resolved_notes_dir), - "work_dir": str(resolved_work_dir), - }, - config=config, + client = get_sync_client(url=server_url) + + # ── resolve server thread_id ────────────────────────────────────────────── + thread_map = _load_thread_map(resolved_notes_dir) + server_thread_id: str | None = thread_map.get(effective_task_id) + + # Verify the stored thread still exists on the server (server may have restarted) + if server_thread_id: + try: + client.threads.get(server_thread_id) + except Exception: + logging.warning( + "Stored thread %s no longer exists on server (server restarted?); " + "creating a new thread.", + server_thread_id, ) + server_thread_id = None - interrupts = result.get("__interrupt__", []) - if interrupts: - intr = interrupts[0] - action_required = intr.value if hasattr(intr, "value") else intr + if not server_thread_id: + thread = client.threads.create( + metadata={"task_id": effective_task_id} + ) + server_thread_id = thread["thread_id"] + thread_map[effective_task_id] = server_thread_id + _save_thread_map(resolved_notes_dir, thread_map) + + # ── submit run ──────────────────────────────────────────────────────────── + run_config = { + "configurable": { + "codex_review_command": codex_review_command, + "codex_timeout": timeout, + } + } + + if resume_payload is not None: + run = client.runs.create( + server_thread_id, + ASSISTANT_ID, + command={"resume": resume_payload}, + config=run_config, + ) + else: + run = client.runs.create( + server_thread_id, + ASSISTANT_ID, + input={ + "task_id": effective_task_id, + "user_prompt": user_prompt, + "notes_dir": str(resolved_notes_dir), + "work_dir": str(resolved_work_dir), + }, + config=run_config, + ) + + run_id = run["run_id"] + logging.info("Submitted run %s on thread %s", run_id, server_thread_id) + + # ── wait for completion or interrupt ────────────────────────────────────── + ACTIVE = {"pending", "running"} + deadline = time.monotonic() + timeout + run_status = run.get("status", "pending") + + while run_status in ACTIVE and time.monotonic() < deadline: + time.sleep(1) + try: + final_run = client.runs.get(server_thread_id, run_id) + run_status = final_run.get("status", run_status) + except Exception as exc: + logging.debug("runs.get error (will retry): %s", exc) + + if run_status in ACTIVE: + raise TimeoutError( + f"Run {run_id} did not complete within {timeout}s (status={run_status})" + ) + + # ── detect interrupt from thread state ──────────────────────────────────── + state = client.threads.get_state(server_thread_id) + + if _is_interrupted(state): + interrupt_value = _extract_interrupt(state) return { "status": "waiting", - "thread_id": effective_thread, - "db_path": str(db_path), - "action_required": action_required, + "thread_id": effective_task_id, + "server_thread_id": server_thread_id, + "action_required": interrupt_value, } + if run_status not in ("success",): + error = final_run.get("error") or run_status + raise RuntimeError(f"Run ended with status '{run_status}': {error}") + + # ── extract final state ─────────────────────────────────────────────────── + values = state.get("values", {}) + return { "status": "done", - "thread_id": effective_thread, - "db_path": str(db_path), - "task_path": result.get("task_path", ""), - "latest_discuss_path": result.get("latest_discuss_path", ""), - "work_dir": result.get("work_dir", ""), - "codex_session_id": result.get("codex_session_id", ""), - "has_serious_issue": result.get("has_serious_issue", False), - "should_exit": result.get("should_exit", True), + "thread_id": effective_task_id, + "server_thread_id": server_thread_id, + "task_path": values.get("task_path", ""), + "latest_discuss_path": values.get("latest_discuss_path", ""), + "work_dir": values.get("work_dir", ""), + "codex_session_id": values.get("codex_session_id", ""), + "has_serious_issue": values.get("has_serious_issue", False), + "should_exit": values.get("should_exit", True), } +# ── CLI ─────────────────────────────────────────────────────────────────────── + +def _parse_command(value: str | None) -> list[str] | None: + if not value: + return None + try: + parsed = json.loads(value) + except json.JSONDecodeError: + import re + try: + parsed = json.loads(re.sub(r'\\(?![\\"/bfnrtu])', r'\\\\', value)) + except json.JSONDecodeError: + stripped = value.strip() + if stripped.startswith("[") and stripped.endswith("]"): + parsed = [i.strip().strip('"').strip("'") for i in stripped[1:-1].split(",")] + else: + raise + if not isinstance(parsed, list) or not all(isinstance(i, str) for i in parsed): + raise argparse.ArgumentTypeError("command must be a JSON string array") + return parsed + + +def _parse_b64(value: str | None) -> list[str] | None: + if not value: + return None + return _parse_command(base64.b64decode(value).decode("utf-8")) + + +def _parse_json_b64(value: str | None): + if not value: + return None + return json.loads(base64.b64decode(value).decode("utf-8")) + + def main() -> int: parser = argparse.ArgumentParser( - description="Run the LangGraph Claude/Codex review workflow." - ) - parser.add_argument("--task-id", required=True, help="Task id, for example 02") - parser.add_argument("--prompt", default="", help="User request recorded for traceability") - parser.add_argument("--notes-dir", default="Notes", help="Directory for Task/discuss files") - parser.add_argument("--work-dir", default="", help="Claude Code launch working directory") - parser.add_argument( - "--codex-review-command", - type=parse_command, - default=None, - help="JSON argv for Codex review step.", - ) - parser.add_argument( - "--codex-review-command-b64", - type=parse_command_b64, - default=None, - help="Base64-encoded JSON argv for Codex review step.", + description="Submit a plan-review workflow run to the local langgraph dev server." ) + parser.add_argument("--task-id", required=True) + parser.add_argument("--prompt", default="") + parser.add_argument("--notes-dir", default="Notes") + parser.add_argument("--work-dir", default="") + parser.add_argument("--codex-review-command", type=_parse_command, default=None) + parser.add_argument("--codex-review-command-b64", type=_parse_b64, default=None) parser.add_argument("--timeout", type=int, default=600) - parser.add_argument( - "--thread-id", - default="", - help="Thread ID for resuming an existing run (defaults to task-id)", - ) - parser.add_argument( - "--resume", - default="", - help=( - "JSON payload for Command(resume=...). " - "Use true for plan/revise/rejection steps; {\"accepts\":true} for decision step." - ), - ) - parser.add_argument( - "--resume-b64", - default="", - help="Base64-encoded JSON payload for Command(resume=...).", - ) - parser.add_argument("--verbose", "-v", action="store_true", help="Enable debug logging to stderr.") + parser.add_argument("--thread-id", default="") + parser.add_argument("--resume", default="") + parser.add_argument("--resume-b64", default="") + parser.add_argument("--server-url", default=SERVER_URL) + parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() + if args.verbose: - logging.basicConfig(level=logging.DEBUG, stream=sys.stderr, - format="%(asctime)s %(levelname)s %(message)s") - codex_review_command = args.codex_review_command_b64 or args.codex_review_command - if not codex_review_command: + logging.basicConfig( + level=logging.DEBUG, stream=sys.stderr, + format="%(asctime)s %(levelname)s %(message)s", + ) + + codex_cmd = args.codex_review_command_b64 or args.codex_review_command + if not codex_cmd: parser.error("--codex-review-command or --codex-review-command-b64 is required") - runner = AgentRunner( - codex_review_command=codex_review_command, - timeout=args.timeout, - ) - # resume=True tells LangGraph to resume from the last interrupt without a specific value. - # Callers should pass 'true' (JSON boolean) for plan/revise/rejection steps. - resume_payload = parse_json_b64(args.resume_b64) if args.resume_b64 else ( - json.loads(args.resume) if args.resume else None + resume_payload = ( + _parse_json_b64(args.resume_b64) if args.resume_b64 + else (json.loads(args.resume) if args.resume else None) ) output = run_workflow( @@ -183,9 +294,11 @@ def main() -> int: user_prompt=args.prompt, notes_dir=args.notes_dir, work_dir=args.work_dir, - runner=runner, + codex_review_command=codex_cmd, thread_id=args.thread_id or None, resume_payload=resume_payload, + timeout=args.timeout, + server_url=args.server_url, ) print(json.dumps(output, ensure_ascii=False, indent=2)) diff --git a/langgraph-plan-review/scripts/workflow.py b/langgraph-plan-review/scripts/workflow.py index ce2dc69..cc881de 100644 --- a/langgraph-plan-review/scripts/workflow.py +++ b/langgraph-plan-review/scripts/workflow.py @@ -1,15 +1,30 @@ from __future__ import annotations +import sys from pathlib import Path import re from typing import TypedDict +from langchain_core.runnables import RunnableConfig from langgraph.graph import END, START, StateGraph from langgraph.types import interrupt from agents import AgentRunner +def _default_review_cmd() -> list[str]: + """Return the real review_task.py command using the current interpreter.""" + review_py = Path(__file__).parent / "review_task.py" + return [ + sys.executable, + str(review_py), + "$task_path", + "$latest_discuss_path", + "$codex_session_id", + "$work_dir", + ] + + class WorkflowState(TypedDict): task_id: str user_prompt: str @@ -129,7 +144,7 @@ def values_from_state(state: WorkflowState) -> dict[str, str]: } -def build_workflow(runner: AgentRunner, *, checkpointer=None): +def build_workflow(*, checkpointer=None): graph = StateGraph(WorkflowState) def initialize(state: WorkflowState) -> WorkflowState: @@ -163,7 +178,11 @@ def build_workflow(runner: AgentRunner, *, checkpointer=None): validate_task_file(state["task_path"]) return state - def review(state: WorkflowState) -> WorkflowState: + def review(state: WorkflowState, config: RunnableConfig) -> WorkflowState: + cfg = config.get("configurable", {}) + codex_cmd: list[str] = cfg.get("codex_review_command") or _default_review_cmd() + timeout: int = int(cfg.get("codex_timeout", 600)) + runner = AgentRunner(codex_review_command=codex_cmd, timeout=timeout) result = runner.review(values_from_state(state)) updates = { **state, diff --git a/langgraph-vivado-install/SKILL.md b/langgraph-vivado-install/SKILL.md index cc6cb7f..b082447 100644 --- a/langgraph-vivado-install/SKILL.md +++ b/langgraph-vivado-install/SKILL.md @@ -114,16 +114,40 @@ conda env list | grep "^langgraph" ``` ### 2b. 安装所需 Python 包 + +一条命令安装全部依赖(包含 `/langgraph-plan-review` skill 所需包): + ```bash -conda run -n langgraph pip install langgraph langchain-anthropic pandas +conda run -n langgraph pip install \ + langgraph \ + langchain-anthropic \ + pandas \ + "langgraph-cli[inmem]" \ + langgraph-sdk \ + httpx \ + langgraph-checkpoint-sqlite ``` +> **为什么这些包?** +> | 包 | 用途 | +> |---|---| +> | `langgraph` | 核心图执行引擎 | +> | `langchain-anthropic` | Anthropic LLM 集成 | +> | `pandas` | 数据处理 | +> | `langgraph-cli[inmem]` | 提供 `langgraph dev` 服务器命令(`/langgraph-plan-review` 必需) | +> | `langgraph-sdk` | `run_workflow.py` 通过 SDK 向 dev server 提交 run(`/langgraph-plan-review` 必需) | +> | `httpx` | `run_workflow.py` 健康检查(`/langgraph-plan-review` 必需) | +> | `langgraph-checkpoint-sqlite` | dev server 状态持久化(`/langgraph-plan-review` 必需) | + 验证安装: ```bash conda run -n langgraph python -c "import langgraph; print('LangGraph OK:', langgraph.__version__)" +conda run -n langgraph python -c "from langgraph_sdk import get_sync_client; print('langgraph-sdk OK')" +conda run -n langgraph python -c "import httpx; print('httpx OK')" +conda run -n langgraph langgraph --version ``` -若导入失败,报告错误并停止,请用户检查网络或 pip 源配置。 +若任一导入失败,报告错误并停止,请用户检查网络或 pip 源配置。 --- @@ -273,7 +297,15 @@ conda run -n langgraph vivado -version "VIVADO_BIN_PATH": "", "conda_env": "langgraph", "python_version": "3.11", - "packages": ["langgraph", "langchain-anthropic", "pandas"] + "packages": [ + "langgraph", + "langchain-anthropic", + "pandas", + "langgraph-cli[inmem]", + "langgraph-sdk", + "httpx", + "langgraph-checkpoint-sqlite" + ] } ``` @@ -302,6 +334,55 @@ def test_langgraph(): return False +def test_langgraph_plan_review_deps(): + """Verify packages required by the /langgraph-plan-review skill.""" + ok = True + checks = [ + ("langgraph_sdk", "from langgraph_sdk import get_sync_client"), + ("httpx", "import httpx"), + ("langgraph_checkpoint_sqlite", "from langgraph.checkpoint.sqlite import SqliteSaver"), + ] + for name, stmt in checks: + try: + exec(stmt) + import importlib.metadata + try: + ver = importlib.metadata.version(name.replace("_", "-")) + print(f"[OK] {name} OK (version: {ver})") + except importlib.metadata.PackageNotFoundError: + print(f"[OK] {name} importable") + except Exception as e: + print(f"[FAIL] {name}: {e}") + ok = False + + # Check langgraph dev CLI + try: + result = subprocess.run( + [sys.executable, "-m", "langgraph", "--version"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0: + print(f"[OK] langgraph-cli OK: {result.stdout.strip() or result.stderr.strip()}") + else: + # Try via entry point script + import shutil + lg = shutil.which("langgraph") + if lg: + r2 = subprocess.run([lg, "--version"], capture_output=True, text=True, timeout=10) + if r2.returncode == 0: + print(f"[OK] langgraph dev CLI OK: {r2.stdout.strip()}") + else: + print(f"[WARN] langgraph CLI found but --version failed: {r2.stderr.strip()}") + else: + print("[FAIL] langgraph CLI not found on PATH — install: pip install 'langgraph-cli[inmem]'") + ok = False + except Exception as e: + print(f"[FAIL] langgraph CLI check error: {e}") + ok = False + + return ok + + def test_vivado(): config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "env_config.json") env = os.environ.copy() @@ -342,10 +423,14 @@ if __name__ == "__main__": print("LangGraph + Vivado smoke test") print("=" * 50) ok_lg = test_langgraph() + print("-" * 50) + print("langgraph-plan-review deps:") + ok_pr = test_langgraph_plan_review_deps() + print("-" * 50) ok_vv = test_vivado() print("=" * 50) - if ok_lg and ok_vv: - print("[PASS] All checks passed. LangGraph + Vivado pipeline OK!") + if ok_lg and ok_pr and ok_vv: + print("[PASS] All checks passed. LangGraph + Vivado + plan-review pipeline OK!") sys.exit(0) else: print("[FAIL] Some checks failed. See above.")