fdsx 0.2.2__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fdsx-0.2.2 → fdsx-0.2.4}/PKG-INFO +37 -13
- {fdsx-0.2.2 → fdsx-0.2.4}/README.md +37 -13
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/cli/main.py +31 -35
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/batch.py +0 -160
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/__init__.py +0 -2
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/tasks_dir.py +38 -10
- fdsx-0.2.4/src/fdsx/core/mode.py +36 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/selector.py +20 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/data/skills/fdsx/SKILL.md +47 -6
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/data/skills/fdsx/references/yaml-schema.md +76 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/display/terminal.py +12 -8
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx.egg-info/PKG-INFO +37 -13
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx.egg-info/SOURCES.txt +3 -4
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_batch_full_pipeline.py +0 -11
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_wait_and_resume.py +3 -2
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_auto_init_cli.py +23 -16
- fdsx-0.2.4/tests/integration/test_ci_cli.py +178 -0
- fdsx-0.2.4/tests/integration/test_ci_mode.py +292 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_single_task_confirm.py +2 -2
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_tasks_dir.py +77 -57
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_workflow_persistence.py +5 -5
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_batch.py +0 -371
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_selector.py +90 -53
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_spinner.py +45 -38
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_terminal.py +136 -101
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_workflow_cui.py +16 -16
- fdsx-0.2.2/src/fdsx/core/engine/batch.py +0 -132
- fdsx-0.2.2/tests/e2e/test_batch_backward_compat.py +0 -73
- fdsx-0.2.2/tests/e2e/test_cli_batch_split.py +0 -263
- fdsx-0.2.2/tests/e2e/test_cli_batch_tasks.py +0 -112
- {fdsx-0.2.2 → fdsx-0.2.4}/.github/dependabot.yml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/.github/workflows/publish.yml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/.github/workflows/test.yml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/.pre-commit-config.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/AGENTS.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/CLAUDE.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/GEMINI.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/LICENSE +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/MANIFEST.in +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/examples/self-improve/README.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/examples/self-improve/analyze.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/examples/self-improve/collect_data.sh +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/examples/self-improve/research.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/examples/self-improve/workflow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/examples/self-improve/write_lessons.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/pyproject.toml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/setup.cfg +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/checkpoint/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/checkpoint/manager.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/cli/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/cli/init_interactive.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/aggregation.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/compile.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/execution.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/helpers.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/map_iteration.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/nodes.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/parallel.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/compiler/routing.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/config.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/interrupts.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/results.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/resume.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/run.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/signals.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/engine/validate.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/extraction.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/graph_utils.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/hooks.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/init.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/loader.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/paths.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/profiles.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/thread_id.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/core/variables.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/data/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/data/skills/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/display/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/finalize.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/fix.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/implement.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/plan.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/replan.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/review-code-quality.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/review-security.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/full-impl/workflow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/self-improve/README.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/self-improve/analyze.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/self-improve/collect_data.sh +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/self-improve/research.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/self-improve/workflow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/self-improve/write_lessons.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/finalize.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/fix.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/implement.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/plan.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/replan.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/review-general.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/examples/workflows/simple-impl/workflow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/logging/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/logging/recorder.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/logging/stream_logger.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/models/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/models/flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/models/init.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/models/task.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/models/validators.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/notify/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/notify/webhook.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/base.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/claude.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/codex.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/gemini.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/opencode.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/providers/system.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx/py.typed +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx.egg-info/dependency_links.txt +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx.egg-info/entry_points.txt +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx.egg-info/requires.txt +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/src/fdsx.egg-info/top_level.txt +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/conftest.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/cli_test_utils.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/conftest.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_add_cli.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_auto_init.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_batch_edge_cases.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_batch_error_messages.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_flow_types.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_map_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_signal_handling.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_thread_id.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_validation_and_run.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/e2e/test_cli_workflow_name.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/batch_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/checkpoint_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/choice_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/choice_flow_default.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/claude_stream.ndjson +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/codex_stream.jsonl +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/extraction_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/input_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/invalid_flows/bad_next_ref.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/invalid_flows/missing_start_at.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/invalid_flows/mutual_exclusive.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/json_codeblock_extraction_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/json_extraction_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/loop_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/map_basic.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/map_empty_items.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/map_fail_fast_false.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/map_inside_parallel.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/max_iterations_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/max_iterations_wait_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/parallel_min_success.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/parallel_review.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/profile_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/profile_parallel_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/prompt_file_test/flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/prompt_file_test/prompt.txt +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/regex_extraction_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/sample_tasks.md +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/self_improve_flow/collect_data.sh +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/self_improve_flow/workflow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/simple_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/wait_approval.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/wait_resume_flow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/wait_webhook.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/workflows_name_display/alpha.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/workflows_name_display/beta/workflow.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/fixtures/workflows_name_display/gamma.yaml +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_add_single_task.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_auto_init.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_auto_select.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_checkpoint_resume.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_choice_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_claude_streaming.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_codex_stream_lifecycle.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_codex_streaming.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_default_tasks_dir.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_extraction_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_gemini_provider.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_hook_streaming_interaction.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_hooks_integration.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_inactivity_timeout.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_init.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_init_cli.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_init_full_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_init_interactive.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_init_templates.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_iteration_logs.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_large_command.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_linear_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_lock_atomicity.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_loop_enforcement.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_loop_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_map_checkpoint.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_map_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_max_iterations_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_parallel_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_profile_assignment.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_profile_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_provider_options.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_quiet_mode.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_result_file.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_resume_interrupt.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_scaffold_gitignore.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_scenario_flows.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_self_improve_flow.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_skill_install.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_split.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_split_spinner.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_stream_output.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_subprocess_completion.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_system_prompt.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_tool_use_streaming.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_universal_scaffold_trigger.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/integration/test_wait_resume.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/__init__.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_aggregation.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_backoff.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_checkpoint.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_claude_options.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_claude_stream_parser.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_cli_version.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_codex_stream_parser.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_compiler_merge.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_config.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_config_generation.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_engine.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_execution.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_extraction.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_format_tool_input_summary.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_gemini_options.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_gemini_stream.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_gitignore.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_graph_utils.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_hooks.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_loader.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_map_model.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_max_iterations.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_models.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_paths.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_profile_fixtures.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_profiles.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_prompt_file.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_provider_options.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_provider_stdin_fallback.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_recorder.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_result_file.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_resume_display.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_selector_name.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_signal_handler.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_stream_callback.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_stream_logger.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_stream_logger_iteration.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_subprocess_completion.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_subprocess_realtime_streaming.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_subprocess_stdin.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_task_model.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_task_splitter_prompt.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_thread_id.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_variables.py +0 -0
- {fdsx-0.2.2 → fdsx-0.2.4}/tests/unit/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fdsx
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Declarative AI agent workflow execution framework
|
|
5
5
|
Author: kenfdev
|
|
6
6
|
License-Expression: MIT
|
|
@@ -50,9 +50,11 @@ fdsx enables you to define AI agent workflows in YAML, combining the durability
|
|
|
50
50
|
|
|
51
51
|
**Key features:**
|
|
52
52
|
- Declarative YAML-based workflow definition
|
|
53
|
+
- Interactive project initialization and scaffolding
|
|
53
54
|
- Stateful execution with checkpoint/resume
|
|
54
55
|
- Parallel execution with branch aggregation
|
|
55
|
-
-
|
|
56
|
+
- Map state for iterating over arrays with sub-workflows
|
|
57
|
+
- Persistent batch task processing with crash-resilient resume
|
|
56
58
|
- Multiple LLM provider support (Claude, Codex, Gemini, OpenCode, and system commands)
|
|
57
59
|
- Named profiles for reusable provider/model configuration
|
|
58
60
|
- Webhook notifications on wait states
|
|
@@ -74,6 +76,14 @@ uv tool install fdsx
|
|
|
74
76
|
|
|
75
77
|
## Quick Start
|
|
76
78
|
|
|
79
|
+
Initialize a new project:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
fdsx init
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This interactively scaffolds a `.fdsx/` directory with configuration and example workflows.
|
|
86
|
+
|
|
77
87
|
Create a simple YAML workflow file:
|
|
78
88
|
|
|
79
89
|
```yaml
|
|
@@ -184,6 +194,8 @@ states:
|
|
|
184
194
|
type: llm_classify # (literal, REQUIRED) only "llm_classify" supported
|
|
185
195
|
provider: claude # (string, REQUIRED) LLM provider for classification
|
|
186
196
|
prompt: "Classify as APPROVED or NEEDS_FIX" # (string, REQUIRED)
|
|
197
|
+
# Alternatively, use a profile reference (mutually exclusive with provider):
|
|
198
|
+
# profile: smarty
|
|
187
199
|
|
|
188
200
|
# --- Execution control ---
|
|
189
201
|
retry: 3 # (int, default: 3) retry attempts on failure
|
|
@@ -234,8 +246,10 @@ states:
|
|
|
234
246
|
parallel_review:
|
|
235
247
|
type: parallel # (REQUIRED) literal "parallel"
|
|
236
248
|
branches: # (list, REQUIRED) each branch is an independent execution
|
|
237
|
-
- provider: claude # same provider rules as task
|
|
249
|
+
- provider: claude # same provider rules as task
|
|
238
250
|
model: claude-sonnet-4-6
|
|
251
|
+
# Alternatively, use a profile reference (mutually exclusive with provider/model):
|
|
252
|
+
# profile: smarty
|
|
239
253
|
prompt_template: |
|
|
240
254
|
Review code quality: {implementation}
|
|
241
255
|
# prompt_file: review.md # alternative to prompt_template
|
|
@@ -286,7 +300,7 @@ states:
|
|
|
286
300
|
result_path: $.iter.step2
|
|
287
301
|
retry: 0
|
|
288
302
|
fail_fast: true # (bool, default: true) stop all iterations on first failure
|
|
289
|
-
result_path: $.map_results # (string,
|
|
303
|
+
result_path: $.map_results # (string, REQUIRED) JSONPath for the results array
|
|
290
304
|
max_iterations: 10 # (int, optional) max times this state can be re-entered
|
|
291
305
|
hooks: # (optional)
|
|
292
306
|
next: after_map # next / end — same rules as task
|
|
@@ -446,6 +460,10 @@ workflows_dir: .fdsx/workflows # (string, default: ".fdsx/workflows")
|
|
|
446
460
|
# must be relative, no ".." components
|
|
447
461
|
# where `fdsx run --tasks-dir` discovers workflows
|
|
448
462
|
|
|
463
|
+
# --- Default tasks directory ---
|
|
464
|
+
default_tasks_dir: .fdsx/tasks # (string, optional) default directory for bare `fdsx run`
|
|
465
|
+
# when no workflow, --tasks, or --tasks-dir is given
|
|
466
|
+
|
|
449
467
|
# --- Auto-workflow selection ---
|
|
450
468
|
auto_workflow: false # (bool, default: false) skip interactive confirmation UI
|
|
451
469
|
|
|
@@ -457,7 +475,7 @@ workflow_selector:
|
|
|
457
475
|
extra_instructions: | # (string, optional) appended to the selection prompt
|
|
458
476
|
Prefer simple-impl for small tasks.
|
|
459
477
|
|
|
460
|
-
# --- Task splitter: LLM used by `fdsx split` ---
|
|
478
|
+
# --- Task splitter: LLM used by `fdsx add --split` ---
|
|
461
479
|
task_splitter:
|
|
462
480
|
profile: smarty # (string, optional) profile ref — mutually exclusive with provider/model
|
|
463
481
|
# provider: claude # (string, default: "claude")
|
|
@@ -477,6 +495,8 @@ providers:
|
|
|
477
495
|
dangerously_skip_permissions: true # (bool, default: false)
|
|
478
496
|
allowed_tools: [] # (list of strings, default: []) tool allowlist
|
|
479
497
|
disallowed_tools: [] # (list of strings, default: []) tool denylist
|
|
498
|
+
system_prompt: "Custom system prompt" # (string, optional) override the default system prompt
|
|
499
|
+
append_system_prompt: "Extra instructions" # (string, optional) append to the default system prompt
|
|
480
500
|
inactivity_timeout: 600 # (int, optional) seconds before killing inactive subprocess
|
|
481
501
|
|
|
482
502
|
codex:
|
|
@@ -519,27 +539,31 @@ hooks:
|
|
|
519
539
|
| Flag | Description |
|
|
520
540
|
|------|-------------|
|
|
521
541
|
| `--version` | Show version and exit |
|
|
522
|
-
| `--ci` | Run in CI mode (non-interactive) |
|
|
523
|
-
| `--interactive` | Force interactive mode |
|
|
542
|
+
| `--ci` | Run in CI mode (non-interactive, mutually exclusive with `--interactive`). Also auto-detected from `CI` and `GITHUB_ACTIONS` environment variables |
|
|
543
|
+
| `--interactive` | Force interactive mode (mutually exclusive with `--ci`) |
|
|
524
544
|
|
|
525
545
|
### Commands
|
|
526
546
|
|
|
527
547
|
| Command | Description |
|
|
528
548
|
|---------|-------------|
|
|
549
|
+
| `fdsx init` | Initialize a new fdsx project with interactive setup |
|
|
550
|
+
| `fdsx init --skill` | Install the /fdsx Claude Code skill only (skip scaffold) |
|
|
551
|
+
| `fdsx run` | Execute tasks from default tasks directory (`default_tasks_dir` or `.fdsx/tasks/`) |
|
|
529
552
|
| `fdsx run <workflow.yaml>` | Execute a workflow |
|
|
530
553
|
| `fdsx run <workflow.yaml> --input key=value` | Pass input variables |
|
|
531
|
-
| `fdsx run <workflow.yaml> --tasks tasks.yaml` | In-memory batch execution |
|
|
532
554
|
| `fdsx run --tasks-dir <dir>` | Persistent batch execution (workflow optional) |
|
|
533
555
|
| `fdsx run ... --quiet` | Suppress stderr streaming output |
|
|
534
556
|
| `fdsx run ... --auto-workflow` | Skip workflow confirmation UI |
|
|
535
|
-
| `fdsx run ... --confirm-workflow` | Show workflow confirmation UI |
|
|
557
|
+
| `fdsx run ... --confirm-workflow` | Show workflow confirmation UI (requires interactive mode) |
|
|
558
|
+
| `fdsx run ... --continue-on-error` | Continue processing remaining entries on error in tasks-dir mode |
|
|
536
559
|
| `fdsx resume --thread-id <id>` | Resume from checkpoint |
|
|
537
560
|
| `fdsx resume --thread-id <id> --base-dir <dir>` | Resume with custom base directory |
|
|
538
561
|
| `fdsx validate <workflow.yaml>` | Validate YAML syntax |
|
|
539
562
|
| `fdsx list` | List recent runs |
|
|
540
563
|
| `fdsx list --base-dir <dir>` | List runs from custom base directory |
|
|
541
|
-
| `fdsx
|
|
542
|
-
| `fdsx
|
|
564
|
+
| `fdsx add <task_file>` | Add a task file to the batch execution queue (single task) |
|
|
565
|
+
| `fdsx add <task_file> --split` | Split a task file into individual task files |
|
|
566
|
+
| `fdsx add <task_file> --split --force` | Clear existing tasks directory before splitting |
|
|
543
567
|
|
|
544
568
|
## Example Workflow
|
|
545
569
|
|
|
@@ -610,8 +634,8 @@ states:
|
|
|
610
634
|
|
|
611
635
|
Run this example:
|
|
612
636
|
```bash
|
|
613
|
-
#
|
|
614
|
-
fdsx
|
|
637
|
+
# Initialize the project (creates .fdsx/ with config and example workflows):
|
|
638
|
+
fdsx init
|
|
615
639
|
|
|
616
640
|
# Then run the scaffolded example workflow:
|
|
617
641
|
fdsx run .fdsx/workflows/plan-implement-review/workflow.yaml --input task="Build a web calculator"
|
|
@@ -10,9 +10,11 @@ fdsx enables you to define AI agent workflows in YAML, combining the durability
|
|
|
10
10
|
|
|
11
11
|
**Key features:**
|
|
12
12
|
- Declarative YAML-based workflow definition
|
|
13
|
+
- Interactive project initialization and scaffolding
|
|
13
14
|
- Stateful execution with checkpoint/resume
|
|
14
15
|
- Parallel execution with branch aggregation
|
|
15
|
-
-
|
|
16
|
+
- Map state for iterating over arrays with sub-workflows
|
|
17
|
+
- Persistent batch task processing with crash-resilient resume
|
|
16
18
|
- Multiple LLM provider support (Claude, Codex, Gemini, OpenCode, and system commands)
|
|
17
19
|
- Named profiles for reusable provider/model configuration
|
|
18
20
|
- Webhook notifications on wait states
|
|
@@ -34,6 +36,14 @@ uv tool install fdsx
|
|
|
34
36
|
|
|
35
37
|
## Quick Start
|
|
36
38
|
|
|
39
|
+
Initialize a new project:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
fdsx init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This interactively scaffolds a `.fdsx/` directory with configuration and example workflows.
|
|
46
|
+
|
|
37
47
|
Create a simple YAML workflow file:
|
|
38
48
|
|
|
39
49
|
```yaml
|
|
@@ -144,6 +154,8 @@ states:
|
|
|
144
154
|
type: llm_classify # (literal, REQUIRED) only "llm_classify" supported
|
|
145
155
|
provider: claude # (string, REQUIRED) LLM provider for classification
|
|
146
156
|
prompt: "Classify as APPROVED or NEEDS_FIX" # (string, REQUIRED)
|
|
157
|
+
# Alternatively, use a profile reference (mutually exclusive with provider):
|
|
158
|
+
# profile: smarty
|
|
147
159
|
|
|
148
160
|
# --- Execution control ---
|
|
149
161
|
retry: 3 # (int, default: 3) retry attempts on failure
|
|
@@ -194,8 +206,10 @@ states:
|
|
|
194
206
|
parallel_review:
|
|
195
207
|
type: parallel # (REQUIRED) literal "parallel"
|
|
196
208
|
branches: # (list, REQUIRED) each branch is an independent execution
|
|
197
|
-
- provider: claude # same provider rules as task
|
|
209
|
+
- provider: claude # same provider rules as task
|
|
198
210
|
model: claude-sonnet-4-6
|
|
211
|
+
# Alternatively, use a profile reference (mutually exclusive with provider/model):
|
|
212
|
+
# profile: smarty
|
|
199
213
|
prompt_template: |
|
|
200
214
|
Review code quality: {implementation}
|
|
201
215
|
# prompt_file: review.md # alternative to prompt_template
|
|
@@ -246,7 +260,7 @@ states:
|
|
|
246
260
|
result_path: $.iter.step2
|
|
247
261
|
retry: 0
|
|
248
262
|
fail_fast: true # (bool, default: true) stop all iterations on first failure
|
|
249
|
-
result_path: $.map_results # (string,
|
|
263
|
+
result_path: $.map_results # (string, REQUIRED) JSONPath for the results array
|
|
250
264
|
max_iterations: 10 # (int, optional) max times this state can be re-entered
|
|
251
265
|
hooks: # (optional)
|
|
252
266
|
next: after_map # next / end — same rules as task
|
|
@@ -406,6 +420,10 @@ workflows_dir: .fdsx/workflows # (string, default: ".fdsx/workflows")
|
|
|
406
420
|
# must be relative, no ".." components
|
|
407
421
|
# where `fdsx run --tasks-dir` discovers workflows
|
|
408
422
|
|
|
423
|
+
# --- Default tasks directory ---
|
|
424
|
+
default_tasks_dir: .fdsx/tasks # (string, optional) default directory for bare `fdsx run`
|
|
425
|
+
# when no workflow, --tasks, or --tasks-dir is given
|
|
426
|
+
|
|
409
427
|
# --- Auto-workflow selection ---
|
|
410
428
|
auto_workflow: false # (bool, default: false) skip interactive confirmation UI
|
|
411
429
|
|
|
@@ -417,7 +435,7 @@ workflow_selector:
|
|
|
417
435
|
extra_instructions: | # (string, optional) appended to the selection prompt
|
|
418
436
|
Prefer simple-impl for small tasks.
|
|
419
437
|
|
|
420
|
-
# --- Task splitter: LLM used by `fdsx split` ---
|
|
438
|
+
# --- Task splitter: LLM used by `fdsx add --split` ---
|
|
421
439
|
task_splitter:
|
|
422
440
|
profile: smarty # (string, optional) profile ref — mutually exclusive with provider/model
|
|
423
441
|
# provider: claude # (string, default: "claude")
|
|
@@ -437,6 +455,8 @@ providers:
|
|
|
437
455
|
dangerously_skip_permissions: true # (bool, default: false)
|
|
438
456
|
allowed_tools: [] # (list of strings, default: []) tool allowlist
|
|
439
457
|
disallowed_tools: [] # (list of strings, default: []) tool denylist
|
|
458
|
+
system_prompt: "Custom system prompt" # (string, optional) override the default system prompt
|
|
459
|
+
append_system_prompt: "Extra instructions" # (string, optional) append to the default system prompt
|
|
440
460
|
inactivity_timeout: 600 # (int, optional) seconds before killing inactive subprocess
|
|
441
461
|
|
|
442
462
|
codex:
|
|
@@ -479,27 +499,31 @@ hooks:
|
|
|
479
499
|
| Flag | Description |
|
|
480
500
|
|------|-------------|
|
|
481
501
|
| `--version` | Show version and exit |
|
|
482
|
-
| `--ci` | Run in CI mode (non-interactive) |
|
|
483
|
-
| `--interactive` | Force interactive mode |
|
|
502
|
+
| `--ci` | Run in CI mode (non-interactive, mutually exclusive with `--interactive`). Also auto-detected from `CI` and `GITHUB_ACTIONS` environment variables |
|
|
503
|
+
| `--interactive` | Force interactive mode (mutually exclusive with `--ci`) |
|
|
484
504
|
|
|
485
505
|
### Commands
|
|
486
506
|
|
|
487
507
|
| Command | Description |
|
|
488
508
|
|---------|-------------|
|
|
509
|
+
| `fdsx init` | Initialize a new fdsx project with interactive setup |
|
|
510
|
+
| `fdsx init --skill` | Install the /fdsx Claude Code skill only (skip scaffold) |
|
|
511
|
+
| `fdsx run` | Execute tasks from default tasks directory (`default_tasks_dir` or `.fdsx/tasks/`) |
|
|
489
512
|
| `fdsx run <workflow.yaml>` | Execute a workflow |
|
|
490
513
|
| `fdsx run <workflow.yaml> --input key=value` | Pass input variables |
|
|
491
|
-
| `fdsx run <workflow.yaml> --tasks tasks.yaml` | In-memory batch execution |
|
|
492
514
|
| `fdsx run --tasks-dir <dir>` | Persistent batch execution (workflow optional) |
|
|
493
515
|
| `fdsx run ... --quiet` | Suppress stderr streaming output |
|
|
494
516
|
| `fdsx run ... --auto-workflow` | Skip workflow confirmation UI |
|
|
495
|
-
| `fdsx run ... --confirm-workflow` | Show workflow confirmation UI |
|
|
517
|
+
| `fdsx run ... --confirm-workflow` | Show workflow confirmation UI (requires interactive mode) |
|
|
518
|
+
| `fdsx run ... --continue-on-error` | Continue processing remaining entries on error in tasks-dir mode |
|
|
496
519
|
| `fdsx resume --thread-id <id>` | Resume from checkpoint |
|
|
497
520
|
| `fdsx resume --thread-id <id> --base-dir <dir>` | Resume with custom base directory |
|
|
498
521
|
| `fdsx validate <workflow.yaml>` | Validate YAML syntax |
|
|
499
522
|
| `fdsx list` | List recent runs |
|
|
500
523
|
| `fdsx list --base-dir <dir>` | List runs from custom base directory |
|
|
501
|
-
| `fdsx
|
|
502
|
-
| `fdsx
|
|
524
|
+
| `fdsx add <task_file>` | Add a task file to the batch execution queue (single task) |
|
|
525
|
+
| `fdsx add <task_file> --split` | Split a task file into individual task files |
|
|
526
|
+
| `fdsx add <task_file> --split --force` | Clear existing tasks directory before splitting |
|
|
503
527
|
|
|
504
528
|
## Example Workflow
|
|
505
529
|
|
|
@@ -570,8 +594,8 @@ states:
|
|
|
570
594
|
|
|
571
595
|
Run this example:
|
|
572
596
|
```bash
|
|
573
|
-
#
|
|
574
|
-
fdsx
|
|
597
|
+
# Initialize the project (creates .fdsx/ with config and example workflows):
|
|
598
|
+
fdsx init
|
|
575
599
|
|
|
576
600
|
# Then run the scaffolded example workflow:
|
|
577
601
|
fdsx run .fdsx/workflows/plan-implement-review/workflow.yaml --input task="Build a web calculator"
|
|
@@ -592,4 +616,4 @@ fdsx list
|
|
|
592
616
|
|
|
593
617
|
## License
|
|
594
618
|
|
|
595
|
-
MIT License.
|
|
619
|
+
MIT License.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import sys
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
@@ -33,14 +34,13 @@ from fdsx.core.init import (
|
|
|
33
34
|
needs_init,
|
|
34
35
|
scaffold,
|
|
35
36
|
)
|
|
37
|
+
from fdsx.core.mode import is_interactive, set_interactive_mode
|
|
36
38
|
from fdsx.core.thread_id import generate_thread_id
|
|
37
39
|
from fdsx.display.terminal import Spinner, _sanitize_output, display_resume_command
|
|
38
40
|
from fdsx.models.init import InitConfig
|
|
39
41
|
|
|
40
42
|
app = typer.Typer(help="fdsx - Declarative AI agent workflow execution framework")
|
|
41
43
|
|
|
42
|
-
_interactive_mode: bool | None = None
|
|
43
|
-
|
|
44
44
|
|
|
45
45
|
def _validate_tasks_dir(tasks_dir: Path) -> None:
|
|
46
46
|
if not tasks_dir.exists():
|
|
@@ -82,7 +82,6 @@ def main(
|
|
|
82
82
|
help="Run in interactive mode (enables TTY detection if not explicitly set).",
|
|
83
83
|
),
|
|
84
84
|
) -> None:
|
|
85
|
-
global _interactive_mode
|
|
86
85
|
if ci and interactive:
|
|
87
86
|
typer.echo(
|
|
88
87
|
"Error: --ci and --interactive are mutually exclusive",
|
|
@@ -90,11 +89,16 @@ def main(
|
|
|
90
89
|
)
|
|
91
90
|
raise typer.Exit(code=2)
|
|
92
91
|
if interactive:
|
|
93
|
-
|
|
92
|
+
set_interactive_mode(True)
|
|
94
93
|
elif ci:
|
|
95
|
-
|
|
94
|
+
set_interactive_mode(False)
|
|
96
95
|
else:
|
|
97
|
-
|
|
96
|
+
ci_env = os.environ.get("CI", "").lower() in ("true", "1", "yes")
|
|
97
|
+
gh_actions = os.environ.get("GITHUB_ACTIONS", "").lower() == "true"
|
|
98
|
+
if ci_env or gh_actions:
|
|
99
|
+
set_interactive_mode(False)
|
|
100
|
+
else:
|
|
101
|
+
set_interactive_mode(sys.stdin.isatty())
|
|
98
102
|
if ctx.invoked_subcommand != "init" and needs_init(Path.cwd()):
|
|
99
103
|
typer.echo(
|
|
100
104
|
"No .fdsx/ directory found. Run 'fdsx init' to set up your project.",
|
|
@@ -102,7 +106,7 @@ def main(
|
|
|
102
106
|
)
|
|
103
107
|
raise typer.Exit(code=0)
|
|
104
108
|
elif (
|
|
105
|
-
|
|
109
|
+
is_interactive()
|
|
106
110
|
and not needs_init(Path.cwd())
|
|
107
111
|
and not (Path.cwd() / ".fdsx" / ".gitignore").exists()
|
|
108
112
|
):
|
|
@@ -122,11 +126,6 @@ def run(
|
|
|
122
126
|
input_vars: list[str] | None = typer.Option(
|
|
123
127
|
None, "--input", help="Input variable as KEY=VALUE"
|
|
124
128
|
),
|
|
125
|
-
tasks_file: Path | None = typer.Option(
|
|
126
|
-
None,
|
|
127
|
-
"--tasks",
|
|
128
|
-
help="Batch task file for in-memory splitting and execution (requires workflow argument)",
|
|
129
|
-
),
|
|
130
129
|
tasks_dir: Path | None = typer.Option(
|
|
131
130
|
None,
|
|
132
131
|
"--tasks-dir",
|
|
@@ -147,8 +146,13 @@ def run(
|
|
|
147
146
|
"--quiet",
|
|
148
147
|
help="Suppress stderr streaming output from providers. Log files are still written and completion summary is still shown.",
|
|
149
148
|
),
|
|
149
|
+
continue_on_error: bool = typer.Option(
|
|
150
|
+
False,
|
|
151
|
+
"--continue-on-error",
|
|
152
|
+
help="Continue processing remaining entries when an error occurs in tasks-dir mode.",
|
|
153
|
+
),
|
|
150
154
|
) -> None:
|
|
151
|
-
"""Run a workflow. Supports single execution
|
|
155
|
+
"""Run a workflow. Supports single execution and persistent batch (--tasks-dir) modes.
|
|
152
156
|
|
|
153
157
|
Shows an animated spinner during workflow auto-selection for tasks-dir mode.
|
|
154
158
|
Displays an interactive numbered-list CUI for workflow confirmation (in interactive terminals).
|
|
@@ -156,28 +160,20 @@ def run(
|
|
|
156
160
|
In non-interactive (non-TTY) terminals, auto-confirms without prompting."""
|
|
157
161
|
config = load_config()
|
|
158
162
|
if tasks_dir is not None:
|
|
159
|
-
if input_vars is not None
|
|
163
|
+
if input_vars is not None:
|
|
160
164
|
typer.echo(
|
|
161
|
-
"Error: --tasks-dir is mutually exclusive with --input
|
|
165
|
+
"Error: --tasks-dir is mutually exclusive with --input",
|
|
162
166
|
err=True,
|
|
163
167
|
)
|
|
164
168
|
raise typer.Exit(code=2)
|
|
165
169
|
_validate_tasks_dir(tasks_dir)
|
|
166
|
-
elif input_vars and
|
|
170
|
+
elif input_vars is not None and workflow is None:
|
|
167
171
|
typer.echo(
|
|
168
|
-
"Error:
|
|
172
|
+
"Error: workflow argument is required when using --input",
|
|
169
173
|
err=True,
|
|
170
174
|
)
|
|
171
175
|
raise typer.Exit(code=2)
|
|
172
|
-
elif
|
|
173
|
-
typer.echo(
|
|
174
|
-
"Error: workflow argument is required when using --tasks",
|
|
175
|
-
err=True,
|
|
176
|
-
)
|
|
177
|
-
raise typer.Exit(code=2)
|
|
178
|
-
elif (
|
|
179
|
-
workflow is None and tasks_dir is None and tasks_file is None and not input_vars
|
|
180
|
-
):
|
|
176
|
+
elif workflow is None and tasks_dir is None and not input_vars:
|
|
181
177
|
resolved_tasks_dir = Path(
|
|
182
178
|
config.default_tasks_dir if config.default_tasks_dir else ".fdsx/tasks/"
|
|
183
179
|
).expanduser()
|
|
@@ -191,6 +187,13 @@ def run(
|
|
|
191
187
|
)
|
|
192
188
|
raise typer.Exit(code=2)
|
|
193
189
|
|
|
190
|
+
if confirm_workflow and not is_interactive():
|
|
191
|
+
typer.echo(
|
|
192
|
+
"Error: --confirm-workflow requires interactive mode and cannot be used with --ci or in CI environments",
|
|
193
|
+
err=True,
|
|
194
|
+
)
|
|
195
|
+
raise typer.Exit(code=2)
|
|
196
|
+
|
|
194
197
|
inputs = None
|
|
195
198
|
if input_vars:
|
|
196
199
|
inputs = {}
|
|
@@ -222,20 +225,13 @@ def run(
|
|
|
222
225
|
base_dir,
|
|
223
226
|
auto_workflow=effective_auto_workflow,
|
|
224
227
|
quiet=quiet,
|
|
228
|
+
continue_on_error=continue_on_error,
|
|
225
229
|
)
|
|
226
230
|
has_failure = any(r.get("status") == "failed" for r in results)
|
|
227
231
|
if has_failure:
|
|
228
232
|
raise typer.Exit(code=1)
|
|
229
233
|
else:
|
|
230
234
|
raise typer.Exit(code=0)
|
|
231
|
-
elif tasks_file is not None:
|
|
232
|
-
assert workflow is not None
|
|
233
|
-
results = engine.run_batch(workflow, tasks_file, base_dir, quiet=quiet)
|
|
234
|
-
has_failure = any(r.get("status") == "failed" for r in results)
|
|
235
|
-
if has_failure:
|
|
236
|
-
raise typer.Exit(code=1)
|
|
237
|
-
else:
|
|
238
|
-
raise typer.Exit(code=0)
|
|
239
235
|
else:
|
|
240
236
|
assert workflow is not None
|
|
241
237
|
if current_thread_id is None:
|
|
@@ -368,7 +364,7 @@ def _run_skill_only_install() -> None:
|
|
|
368
364
|
|
|
369
365
|
def _prompt_and_install_skill(cwd: Path) -> None:
|
|
370
366
|
"""Prompt for skill install after scaffold completes. Handles decline gracefully."""
|
|
371
|
-
if not
|
|
367
|
+
if not is_interactive():
|
|
372
368
|
return
|
|
373
369
|
|
|
374
370
|
try:
|
|
@@ -10,7 +10,6 @@ import structlog
|
|
|
10
10
|
|
|
11
11
|
from fdsx.core.config import TaskSplitterConfig
|
|
12
12
|
from fdsx.display.terminal import _sanitize_output
|
|
13
|
-
from fdsx.models.flow import Flow
|
|
14
13
|
from fdsx.models.task import (
|
|
15
14
|
TaskEntry,
|
|
16
15
|
TaskFile,
|
|
@@ -36,47 +35,6 @@ def _slugify(text: str, max_length: int = 40) -> str:
|
|
|
36
35
|
return slug or "task"
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
def split_tasks(
|
|
40
|
-
task_content: str, flow: Flow, task_splitter: TaskSplitterConfig
|
|
41
|
-
) -> list[str]:
|
|
42
|
-
"""Invoke the task_splitter LLM to split the task file content into individual tasks.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
task_content: The content of the task file
|
|
46
|
-
flow: The flow definition
|
|
47
|
-
task_splitter: The task splitter configuration
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
List of task description strings
|
|
51
|
-
"""
|
|
52
|
-
provider = get_provider(task_splitter.provider)
|
|
53
|
-
|
|
54
|
-
state_names = list(flow.states.keys())
|
|
55
|
-
input_vars = _extract_input_variables(flow)
|
|
56
|
-
|
|
57
|
-
prompt = _build_task_split_prompt(
|
|
58
|
-
task_content,
|
|
59
|
-
state_names,
|
|
60
|
-
input_vars,
|
|
61
|
-
extra_instructions=task_splitter.extra_instructions,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
result = provider.execute(
|
|
65
|
-
prompt=prompt,
|
|
66
|
-
model=task_splitter.model,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
if result.exit_code != 0:
|
|
70
|
-
raise RuntimeError(f"Task splitter failed: {result.stderr}")
|
|
71
|
-
|
|
72
|
-
try:
|
|
73
|
-
groups = _parse_structured_tasks(result.stdout)
|
|
74
|
-
flattened = [entry.description for group in groups for entry in group]
|
|
75
|
-
return flattened
|
|
76
|
-
except ValueError:
|
|
77
|
-
return _parse_task_list(result.stdout)
|
|
78
|
-
|
|
79
|
-
|
|
80
38
|
def _invoke_splitter_and_parse(
|
|
81
39
|
provider: "ProviderBase", prompt: str, model: str | None
|
|
82
40
|
) -> tuple[list[list[TaskEntry]], str]:
|
|
@@ -340,54 +298,6 @@ IMPORTANT: Output ONLY the JSON array, no additional text, explanations, or mark
|
|
|
340
298
|
return prompt
|
|
341
299
|
|
|
342
300
|
|
|
343
|
-
def _extract_input_variables(flow: Flow) -> set[str]:
|
|
344
|
-
"""Extract expected input variables from the flow.
|
|
345
|
-
|
|
346
|
-
Scans prompt_template fields for {variable} references to identify actual inputs.
|
|
347
|
-
Also includes 'task' as the standard batch input variable.
|
|
348
|
-
"""
|
|
349
|
-
import re
|
|
350
|
-
|
|
351
|
-
from fdsx.models.flow import ParallelState, TaskState
|
|
352
|
-
|
|
353
|
-
input_vars: set[str] = {"task"}
|
|
354
|
-
# Matches {var}, {var.field}, {var[0]} etc.
|
|
355
|
-
var_pattern = r"\{(\w+(?:\.\w+)*(?:\[\d+\])?)\}"
|
|
356
|
-
|
|
357
|
-
for _state_name, state in flow.states.items():
|
|
358
|
-
if isinstance(state, TaskState) and state.prompt_template:
|
|
359
|
-
for match in re.findall(var_pattern, state.prompt_template):
|
|
360
|
-
root = match.split(".")[0].split("[")[0]
|
|
361
|
-
input_vars.add(root)
|
|
362
|
-
elif isinstance(state, ParallelState):
|
|
363
|
-
for branch in state.branches:
|
|
364
|
-
if branch.prompt_template:
|
|
365
|
-
for match in re.findall(var_pattern, branch.prompt_template):
|
|
366
|
-
root = match.split(".")[0].split("[")[0]
|
|
367
|
-
input_vars.add(root)
|
|
368
|
-
|
|
369
|
-
return input_vars
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
def _parse_task_list(response: str) -> list[str]:
|
|
373
|
-
"""Parse the LLM response into a list of task strings."""
|
|
374
|
-
tasks: list[str] = []
|
|
375
|
-
lines = response.strip().split("\n")
|
|
376
|
-
|
|
377
|
-
for line in lines:
|
|
378
|
-
line = line.strip()
|
|
379
|
-
if not line:
|
|
380
|
-
continue
|
|
381
|
-
|
|
382
|
-
if line[0].isdigit() and ". " in line:
|
|
383
|
-
task = line.split(". ", 1)[1].strip()
|
|
384
|
-
tasks.append(task)
|
|
385
|
-
else:
|
|
386
|
-
tasks.append(line)
|
|
387
|
-
|
|
388
|
-
return tasks
|
|
389
|
-
|
|
390
|
-
|
|
391
301
|
def _parse_structured_tasks(response: str) -> list[list[TaskEntry]]:
|
|
392
302
|
"""Parse the LLM JSON response into a list of file groups.
|
|
393
303
|
|
|
@@ -551,76 +461,6 @@ def move_task_to_completed(file_path: Path) -> None:
|
|
|
551
461
|
shutil.move(str(file_path), str(dest))
|
|
552
462
|
|
|
553
463
|
|
|
554
|
-
def display_task_list(tasks: list[str]) -> bool:
|
|
555
|
-
"""Display the split tasks in numbered format and prompt for confirmation.
|
|
556
|
-
|
|
557
|
-
Args:
|
|
558
|
-
tasks: List of task description strings
|
|
559
|
-
|
|
560
|
-
Returns:
|
|
561
|
-
True if user approves, False if rejected
|
|
562
|
-
"""
|
|
563
|
-
print("The following tasks will be executed:", file=sys.stderr)
|
|
564
|
-
print("-" * 60, file=sys.stderr)
|
|
565
|
-
|
|
566
|
-
for i, task in enumerate(tasks, 1):
|
|
567
|
-
task_preview = task[:70] + "..." if len(task) > 70 else task
|
|
568
|
-
print(f" {i}. {_sanitize_output(task_preview)}", file=sys.stderr)
|
|
569
|
-
|
|
570
|
-
print("-" * 60, file=sys.stderr)
|
|
571
|
-
|
|
572
|
-
while True:
|
|
573
|
-
response = input("Approve task list? (y/n): ").strip().lower()
|
|
574
|
-
if response == "y":
|
|
575
|
-
return True
|
|
576
|
-
elif response == "n":
|
|
577
|
-
return False
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
def display_batch_summary(results: list[dict[str, Any]]) -> None:
|
|
581
|
-
"""Display a summary table of all task results.
|
|
582
|
-
|
|
583
|
-
Args:
|
|
584
|
-
results: List of result dicts with task_index, task_description, thread_id, status, error
|
|
585
|
-
"""
|
|
586
|
-
print("\n" + "=" * 80, file=sys.stderr)
|
|
587
|
-
print("BATCH EXECUTION SUMMARY", file=sys.stderr)
|
|
588
|
-
print("=" * 80, file=sys.stderr)
|
|
589
|
-
print(
|
|
590
|
-
f"{'#':<4} {'STATUS':<12} {'THREAD_ID':<36} {'TASK':<25}",
|
|
591
|
-
file=sys.stderr,
|
|
592
|
-
)
|
|
593
|
-
print("-" * 80, file=sys.stderr)
|
|
594
|
-
|
|
595
|
-
for result in results:
|
|
596
|
-
task_idx = result.get("task_index", 0) + 1
|
|
597
|
-
status = result.get("status", "unknown")
|
|
598
|
-
thread_id = result.get("thread_id", "")[:36]
|
|
599
|
-
task_desc = result.get("task_description", "")[:25]
|
|
600
|
-
|
|
601
|
-
status_symbol = "✓" if status == "completed" else "✗"
|
|
602
|
-
|
|
603
|
-
print(
|
|
604
|
-
f"{task_idx:<4} {status_symbol} {status:<10} {_sanitize_output(thread_id):<36} {_sanitize_output(task_desc):<25}",
|
|
605
|
-
file=sys.stderr,
|
|
606
|
-
)
|
|
607
|
-
|
|
608
|
-
if result.get("error"):
|
|
609
|
-
error_preview = result["error"][:60]
|
|
610
|
-
print(f" Error: {_sanitize_output(error_preview)}", file=sys.stderr)
|
|
611
|
-
|
|
612
|
-
print("-" * 80, file=sys.stderr)
|
|
613
|
-
|
|
614
|
-
total = len(results)
|
|
615
|
-
succeeded = sum(1 for r in results if r.get("status") == "completed")
|
|
616
|
-
failed = total - succeeded
|
|
617
|
-
|
|
618
|
-
print(
|
|
619
|
-
f"Total: {total} | Succeeded: {succeeded} | Failed: {failed}", file=sys.stderr
|
|
620
|
-
)
|
|
621
|
-
print("=" * 80, file=sys.stderr)
|
|
622
|
-
|
|
623
|
-
|
|
624
464
|
def display_tasks_dir_summary(results: list[dict[str, Any]]) -> None:
|
|
625
465
|
"""Display a summary of tasks-dir execution results.
|
|
626
466
|
|
|
@@ -5,7 +5,6 @@ existing imports like ``from fdsx.core.engine import run_flow`` continue
|
|
|
5
5
|
to work without changes.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .batch import run_batch
|
|
9
8
|
from .results import (
|
|
10
9
|
_calc_elapsed,
|
|
11
10
|
_detect_abort_status,
|
|
@@ -36,7 +35,6 @@ __all__ = [
|
|
|
36
35
|
"_workflow_persist_id",
|
|
37
36
|
"load_tasks_dir",
|
|
38
37
|
"resume_flow",
|
|
39
|
-
"run_batch",
|
|
40
38
|
"run_flow",
|
|
41
39
|
"run_tasks_dir",
|
|
42
40
|
"validate_flow",
|