wup 0.2.79__tar.gz → 0.2.80__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {wup-0.2.79 → wup-0.2.80}/PKG-INFO +60 -7
  2. {wup-0.2.79 → wup-0.2.80}/README.md +59 -6
  3. {wup-0.2.79 → wup-0.2.80}/pyproject.toml +1 -1
  4. wup-0.2.80/tests/test_intent_monitor.py +208 -0
  5. {wup-0.2.79 → wup-0.2.80}/tests/test_multi_project.py +42 -0
  6. {wup-0.2.79 → wup-0.2.80}/wup/__init__.py +1 -1
  7. {wup-0.2.79 → wup-0.2.80}/wup/config.py +84 -2
  8. {wup-0.2.79 → wup-0.2.80}/wup/core.py +29 -2
  9. wup-0.2.80/wup/intent_monitor.py +260 -0
  10. {wup-0.2.79 → wup-0.2.80}/wup/models/config.py +28 -0
  11. {wup-0.2.79 → wup-0.2.80}/wup/testql_watcher.py +23 -0
  12. {wup-0.2.79 → wup-0.2.80}/wup.egg-info/PKG-INFO +60 -7
  13. {wup-0.2.79 → wup-0.2.80}/wup.egg-info/SOURCES.txt +2 -0
  14. {wup-0.2.79 → wup-0.2.80}/LICENSE +0 -0
  15. {wup-0.2.79 → wup-0.2.80}/setup.cfg +0 -0
  16. {wup-0.2.79 → wup-0.2.80}/tests/test_aql.py +0 -0
  17. {wup-0.2.79 → wup-0.2.80}/tests/test_assistant.py +0 -0
  18. {wup-0.2.79 → wup-0.2.80}/tests/test_auto_detection.py +0 -0
  19. {wup-0.2.79 → wup-0.2.80}/tests/test_bootstrap.py +0 -0
  20. {wup-0.2.79 → wup-0.2.80}/tests/test_cli_bridge.py +0 -0
  21. {wup-0.2.79 → wup-0.2.80}/tests/test_cli_filtering.py +0 -0
  22. {wup-0.2.79 → wup-0.2.80}/tests/test_control.py +0 -0
  23. {wup-0.2.79 → wup-0.2.80}/tests/test_discovery_adapters.py +0 -0
  24. {wup-0.2.79 → wup-0.2.80}/tests/test_e2e.py +0 -0
  25. {wup-0.2.79 → wup-0.2.80}/tests/test_endpoints_init_cli.py +0 -0
  26. {wup-0.2.79 → wup-0.2.80}/tests/test_genericity.py +0 -0
  27. {wup-0.2.79 → wup-0.2.80}/tests/test_health_summary_passed.py +0 -0
  28. {wup-0.2.79 → wup-0.2.80}/tests/test_monitoring_manifest.py +0 -0
  29. {wup-0.2.79 → wup-0.2.80}/tests/test_oql.py +0 -0
  30. {wup-0.2.79 → wup-0.2.80}/tests/test_planfile_reporter_dedupe.py +0 -0
  31. {wup-0.2.79 → wup-0.2.80}/tests/test_probe_mutex.py +0 -0
  32. {wup-0.2.79 → wup-0.2.80}/tests/test_service_inference.py +0 -0
  33. {wup-0.2.79 → wup-0.2.80}/tests/test_status_data.py +0 -0
  34. {wup-0.2.79 → wup-0.2.80}/tests/test_sync.py +0 -0
  35. {wup-0.2.79 → wup-0.2.80}/tests/test_testql_monitor.py +0 -0
  36. {wup-0.2.79 → wup-0.2.80}/tests/test_testql_watcher.py +0 -0
  37. {wup-0.2.79 → wup-0.2.80}/tests/test_visual_diff_periodic_skip.py +0 -0
  38. {wup-0.2.79 → wup-0.2.80}/tests/test_visual_diff_progress.py +0 -0
  39. {wup-0.2.79 → wup-0.2.80}/tests/test_watch_exclude.py +0 -0
  40. {wup-0.2.79 → wup-0.2.80}/tests/test_watch_no_paths_exit.py +0 -0
  41. {wup-0.2.79 → wup-0.2.80}/tests/test_web_client.py +0 -0
  42. {wup-0.2.79 → wup-0.2.80}/tests/test_wup.py +0 -0
  43. {wup-0.2.79 → wup-0.2.80}/tests/test_wup_generate.py +0 -0
  44. {wup-0.2.79 → wup-0.2.80}/wup/_ast_detector.py +0 -0
  45. {wup-0.2.79 → wup-0.2.80}/wup/_base_detector.py +0 -0
  46. {wup-0.2.79 → wup-0.2.80}/wup/_hash_detector.py +0 -0
  47. {wup-0.2.79 → wup-0.2.80}/wup/_yaml_detector.py +0 -0
  48. {wup-0.2.79 → wup-0.2.80}/wup/anomaly_detector.py +0 -0
  49. {wup-0.2.79 → wup-0.2.80}/wup/anomaly_models.py +0 -0
  50. {wup-0.2.79 → wup-0.2.80}/wup/aql.py +0 -0
  51. {wup-0.2.79 → wup-0.2.80}/wup/assistant.py +0 -0
  52. {wup-0.2.79 → wup-0.2.80}/wup/assistant_discovery.py +0 -0
  53. {wup-0.2.79 → wup-0.2.80}/wup/assistant_validator.py +0 -0
  54. {wup-0.2.79 → wup-0.2.80}/wup/bootstrap.py +0 -0
  55. {wup-0.2.79 → wup-0.2.80}/wup/bus.py +0 -0
  56. {wup-0.2.79 → wup-0.2.80}/wup/cli.py +0 -0
  57. {wup-0.2.79 → wup-0.2.80}/wup/cli_bridge.py +0 -0
  58. {wup-0.2.79 → wup-0.2.80}/wup/cli_config_generator.py +0 -0
  59. {wup-0.2.79 → wup-0.2.80}/wup/cli_scanner.py +0 -0
  60. {wup-0.2.79 → wup-0.2.80}/wup/control.py +0 -0
  61. {wup-0.2.79 → wup-0.2.80}/wup/dependency_mapper.py +0 -0
  62. {wup-0.2.79 → wup-0.2.80}/wup/discovery.py +0 -0
  63. {wup-0.2.79 → wup-0.2.80}/wup/endpoints.py +0 -0
  64. {wup-0.2.79 → wup-0.2.80}/wup/event_store.py +0 -0
  65. {wup-0.2.79 → wup-0.2.80}/wup/file_watcher/events/file_events.py +0 -0
  66. {wup-0.2.79 → wup-0.2.80}/wup/generate.py +0 -0
  67. {wup-0.2.79 → wup-0.2.80}/wup/init_cli.py +0 -0
  68. {wup-0.2.79 → wup-0.2.80}/wup/models/__init__.py +0 -0
  69. {wup-0.2.79 → wup-0.2.80}/wup/models/target.py +0 -0
  70. {wup-0.2.79 → wup-0.2.80}/wup/monitoring_manifest.py +0 -0
  71. {wup-0.2.79 → wup-0.2.80}/wup/multi.py +0 -0
  72. {wup-0.2.79 → wup-0.2.80}/wup/oql.py +0 -0
  73. {wup-0.2.79 → wup-0.2.80}/wup/paths.py +0 -0
  74. {wup-0.2.79 → wup-0.2.80}/wup/planfile_reporter.py +0 -0
  75. {wup-0.2.79 → wup-0.2.80}/wup/status_data.py +0 -0
  76. {wup-0.2.79 → wup-0.2.80}/wup/sync.py +0 -0
  77. {wup-0.2.79 → wup-0.2.80}/wup/testing/events/health_events.py +0 -0
  78. {wup-0.2.79 → wup-0.2.80}/wup/testing/events/test_results.py +0 -0
  79. {wup-0.2.79 → wup-0.2.80}/wup/testing/handlers/event_handlers.py +0 -0
  80. {wup-0.2.79 → wup-0.2.80}/wup/testing/handlers/health_handlers.py +0 -0
  81. {wup-0.2.79 → wup-0.2.80}/wup/testing/queries/health_queries.py +0 -0
  82. {wup-0.2.79 → wup-0.2.80}/wup/testql_cli_generator.py +0 -0
  83. {wup-0.2.79 → wup-0.2.80}/wup/testql_discovery.py +0 -0
  84. {wup-0.2.79 → wup-0.2.80}/wup/testql_monitor.py +0 -0
  85. {wup-0.2.79 → wup-0.2.80}/wup/validate.py +0 -0
  86. {wup-0.2.79 → wup-0.2.80}/wup/visual_diff.py +0 -0
  87. {wup-0.2.79 → wup-0.2.80}/wup/web_client.py +0 -0
  88. {wup-0.2.79 → wup-0.2.80}/wup.egg-info/dependency_links.txt +0 -0
  89. {wup-0.2.79 → wup-0.2.80}/wup.egg-info/entry_points.txt +0 -0
  90. {wup-0.2.79 → wup-0.2.80}/wup.egg-info/requires.txt +0 -0
  91. {wup-0.2.79 → wup-0.2.80}/wup.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wup
3
- Version: 0.2.79
3
+ Version: 0.2.80
4
4
  Summary: WUP (What's Up) - Intelligent file watcher for regression testing in large projects
5
5
  Author-email: Tom Sapletta <tom@sapletta.com>
6
6
  License-Expression: Apache-2.0
@@ -47,17 +47,17 @@ Dynamic: license-file
47
47
 
48
48
  ## AI Cost Tracking
49
49
 
50
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
50
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
51
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.74-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-55.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
52
52
 
53
- - 🤖 **LLM usage:** $7.6978 (109 commits)
54
- - 👤 **Human dev:** ~$5461 (54.6h @ $100/h, 30min dedup)
53
+ - 🤖 **LLM usage:** $7.7365 (111 commits)
54
+ - 👤 **Human dev:** ~$5566 (55.7h @ $100/h, 30min dedup)
55
55
 
56
- Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
56
+ Generated on 2026-08-01 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
57
57
 
58
58
  ---
59
59
 
60
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
60
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
61
61
 
62
62
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
63
63
 
@@ -157,6 +157,13 @@ wup testql-endpoints /path/to/scenarios --output testql-deps.json
157
157
  Pass several project roots to watch them all at once in a single process. Each
158
158
  project keeps its own `wup.yaml`, dependency map, file observer and test queue.
159
159
 
160
+ When the current directory is a workspace containing projects in immediate
161
+ subfolders, plain `wup watch .` auto-detects their common source directories,
162
+ for example `api/src`, `worker/services` and `frontend/app`. This also repairs
163
+ the runtime behaviour of older auto-generated configs that still point only to
164
+ missing root-level `app`, `src` and `routes` directories. The config file is not
165
+ rewritten by this fallback.
166
+
160
167
  ```bash
161
168
  # Watch several projects at the same time
162
169
  wup watch ./service-a ./service-b ./service-c
@@ -176,6 +183,50 @@ path is resolved per project, so each project uses its own `deps.json`.
176
183
  > The live `--dashboard` and single `--config` options apply to single-project
177
184
  > runs only; when watching multiple projects each uses its own `wup.yaml`.
178
185
 
186
+ ### Continuous Intent Monitoring with todo2code
187
+
188
+ WUP can run todo2code on startup, periodically and after debounced source-file
189
+ changes. Diagnostics are projected into the regular service-health stream as
190
+ `<project>:intent`: blocking findings produce `down`, review-required findings
191
+ produce `degraded`, and a clean audit produces `up`.
192
+
193
+ The integration is opt-in and deterministic by default, so merely installing
194
+ WUP never starts paid LLM requests:
195
+
196
+ ```yaml
197
+ intent_monitoring:
198
+ enabled: true
199
+ runner: cli
200
+ command:
201
+ - node
202
+ - /path/to/todo2code/dist/src/cli.js
203
+ interval_s: 300
204
+ debounce_s: 10
205
+ run_on_start: true
206
+ run_on_change: true
207
+ mode: deterministic
208
+ task_file: TASK.md
209
+ todo_file: TODO.md
210
+ changelog_file: CHANGELOG.md
211
+ docs:
212
+ - README.md
213
+ - docs/**/*.md
214
+ output_dir: .wup/intent
215
+ fail_severities:
216
+ - blocking
217
+ - review_required
218
+ ```
219
+
220
+ Set `runner: python` to use the dependency-free `todo2code-sdk` bridge. Install
221
+ it from todo2code's `sdk/python` directory and set `cli_path` to the built
222
+ `dist/src/cli.js`. The Python package still delegates semantic processing to
223
+ the canonical Node/TypeScript runtime.
224
+
225
+ `mode: prefer-llm` or `mode: require-llm` enables LLM enrichment for NL and
226
+ TODO/CHANGELOG. Documentation and final-summary LLM calls remain separately
227
+ controlled by `docs_llm` and `summary_llm`. Use these modes deliberately: an
228
+ audit may run after every debounce interval and therefore incur provider cost.
229
+
179
230
  ### Initialize Configuration
180
231
 
181
232
  ```bash
@@ -294,6 +345,8 @@ The generated `wup.yaml` includes:
294
345
  - **Metadata header**: Version, generation date, documentation links
295
346
  - **Dependencies info**: WUP version and optional wupbro dashboard
296
347
  - **Quick start guide**: Common commands to get started
348
+ - **Detected watch paths**: Existing common source directories at the project
349
+ root and one level below it
297
350
 
298
351
  Example `wup.yaml`:
299
352
 
@@ -3,17 +3,17 @@
3
3
 
4
4
  ## AI Cost Tracking
5
5
 
6
- ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
- ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.70-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-54.6h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
6
+ ![PyPI](https://img.shields.io/badge/pypi-costs-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
7
+ ![AI Cost](https://img.shields.io/badge/AI%20Cost-$7.74-orange) ![Human Time](https://img.shields.io/badge/Human%20Time-55.7h-blue) ![Model](https://img.shields.io/badge/Model-openrouter%2Fqwen%2Fqwen3--coder--next-lightgrey)
8
8
 
9
- - 🤖 **LLM usage:** $7.6978 (109 commits)
10
- - 👤 **Human dev:** ~$5461 (54.6h @ $100/h, 30min dedup)
9
+ - 🤖 **LLM usage:** $7.7365 (111 commits)
10
+ - 👤 **Human dev:** ~$5566 (55.7h @ $100/h, 30min dedup)
11
11
 
12
- Generated on 2026-07-29 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
12
+ Generated on 2026-08-01 using [openrouter/qwen/qwen3-coder-next](https://openrouter.ai/qwen/qwen3-coder-next)
13
13
 
14
14
  ---
15
15
 
16
- ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.79-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
16
+ ![PyPI](https://img.shields.io/badge/pypi-wup-blue) ![Version](https://img.shields.io/badge/version-0.2.80-blue) ![Python](https://img.shields.io/badge/python-3.9+-blue) ![License](https://img.shields.io/badge/license-Apache--2.0-green)
17
17
 
18
18
  **WUP (What's Up)** - Intelligent file watcher for regression testing in large projects.
19
19
 
@@ -113,6 +113,13 @@ wup testql-endpoints /path/to/scenarios --output testql-deps.json
113
113
  Pass several project roots to watch them all at once in a single process. Each
114
114
  project keeps its own `wup.yaml`, dependency map, file observer and test queue.
115
115
 
116
+ When the current directory is a workspace containing projects in immediate
117
+ subfolders, plain `wup watch .` auto-detects their common source directories,
118
+ for example `api/src`, `worker/services` and `frontend/app`. This also repairs
119
+ the runtime behaviour of older auto-generated configs that still point only to
120
+ missing root-level `app`, `src` and `routes` directories. The config file is not
121
+ rewritten by this fallback.
122
+
116
123
  ```bash
117
124
  # Watch several projects at the same time
118
125
  wup watch ./service-a ./service-b ./service-c
@@ -132,6 +139,50 @@ path is resolved per project, so each project uses its own `deps.json`.
132
139
  > The live `--dashboard` and single `--config` options apply to single-project
133
140
  > runs only; when watching multiple projects each uses its own `wup.yaml`.
134
141
 
142
+ ### Continuous Intent Monitoring with todo2code
143
+
144
+ WUP can run todo2code on startup, periodically and after debounced source-file
145
+ changes. Diagnostics are projected into the regular service-health stream as
146
+ `<project>:intent`: blocking findings produce `down`, review-required findings
147
+ produce `degraded`, and a clean audit produces `up`.
148
+
149
+ The integration is opt-in and deterministic by default, so merely installing
150
+ WUP never starts paid LLM requests:
151
+
152
+ ```yaml
153
+ intent_monitoring:
154
+ enabled: true
155
+ runner: cli
156
+ command:
157
+ - node
158
+ - /path/to/todo2code/dist/src/cli.js
159
+ interval_s: 300
160
+ debounce_s: 10
161
+ run_on_start: true
162
+ run_on_change: true
163
+ mode: deterministic
164
+ task_file: TASK.md
165
+ todo_file: TODO.md
166
+ changelog_file: CHANGELOG.md
167
+ docs:
168
+ - README.md
169
+ - docs/**/*.md
170
+ output_dir: .wup/intent
171
+ fail_severities:
172
+ - blocking
173
+ - review_required
174
+ ```
175
+
176
+ Set `runner: python` to use the dependency-free `todo2code-sdk` bridge. Install
177
+ it from todo2code's `sdk/python` directory and set `cli_path` to the built
178
+ `dist/src/cli.js`. The Python package still delegates semantic processing to
179
+ the canonical Node/TypeScript runtime.
180
+
181
+ `mode: prefer-llm` or `mode: require-llm` enables LLM enrichment for NL and
182
+ TODO/CHANGELOG. Documentation and final-summary LLM calls remain separately
183
+ controlled by `docs_llm` and `summary_llm`. Use these modes deliberately: an
184
+ audit may run after every debounce interval and therefore incur provider cost.
185
+
135
186
  ### Initialize Configuration
136
187
 
137
188
  ```bash
@@ -250,6 +301,8 @@ The generated `wup.yaml` includes:
250
301
  - **Metadata header**: Version, generation date, documentation links
251
302
  - **Dependencies info**: WUP version and optional wupbro dashboard
252
303
  - **Quick start guide**: Common commands to get started
304
+ - **Detected watch paths**: Existing common source directories at the project
305
+ root and one level below it
253
306
 
254
307
  Example `wup.yaml`:
255
308
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wup"
7
- version = "0.2.79"
7
+ version = "0.2.80"
8
8
  description = "WUP (What's Up) - Intelligent file watcher for regression testing in large projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,208 @@
1
+ """Tests for continuous todo2code Intent-vs-Reality monitoring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ import types
8
+ from pathlib import Path
9
+ from types import SimpleNamespace
10
+
11
+ from wup.config import load_config, save_config
12
+ from wup.intent_monitor import IntentAuditResult, Todo2CodeIntentMonitor
13
+ from wup.models.config import IntentMonitoringConfig, ProjectConfig, WupConfig
14
+ from wup.testql_watcher import TestQLWatcher
15
+
16
+
17
+ def _diagnostics(tmp_path: Path, items: list[dict]) -> Path:
18
+ path = tmp_path / "diagnostics.json"
19
+ path.write_text(json.dumps({"diagnostics": items}), encoding="utf-8")
20
+ return path
21
+
22
+
23
+ def test_config_round_trip_preserves_intent_monitoring(tmp_path: Path) -> None:
24
+ config = WupConfig(
25
+ project=ProjectConfig(name="demo"),
26
+ intent_monitoring=IntentMonitoringConfig(
27
+ enabled=True,
28
+ runner="python",
29
+ cli_path="/opt/todo2code/dist/src/cli.js",
30
+ interval_s=120,
31
+ mode="require-llm",
32
+ docs_llm=True,
33
+ fail_codes=["PLANNED_NOT_IMPLEMENTED"],
34
+ ),
35
+ )
36
+ save_config(config, tmp_path / "wup.yaml")
37
+
38
+ loaded = load_config(tmp_path)
39
+
40
+ assert loaded.intent_monitoring.enabled is True
41
+ assert loaded.intent_monitoring.runner == "python"
42
+ assert loaded.intent_monitoring.interval_s == 120
43
+ assert loaded.intent_monitoring.mode == "require-llm"
44
+ assert loaded.intent_monitoring.docs_llm is True
45
+ assert loaded.intent_monitoring.fail_codes == ["PLANNED_NOT_IMPLEMENTED"]
46
+
47
+
48
+ def test_cli_monitor_maps_blocking_diagnostics_to_down(tmp_path: Path, monkeypatch) -> None:
49
+ path = _diagnostics(
50
+ tmp_path,
51
+ [{"severity": "blocking", "code": "PLANNED_NOT_IMPLEMENTED"}],
52
+ )
53
+ calls = []
54
+
55
+ def fake_run(command, **kwargs):
56
+ calls.append((command, kwargs))
57
+ payload = {"diagnosticsPath": str(path), "runDirectory": str(tmp_path / "run")}
58
+ return SimpleNamespace(returncode=0, stdout=json.dumps(payload), stderr="")
59
+
60
+ monkeypatch.setattr("wup.intent_monitor.subprocess.run", fake_run)
61
+ results = []
62
+ config = IntentMonitoringConfig(
63
+ enabled=True,
64
+ command=["node", "/opt/todo2code/cli.js"],
65
+ mode="deterministic",
66
+ )
67
+ monitor = Todo2CodeIntentMonitor(tmp_path, config, results.append)
68
+
69
+ result = monitor.run_once()
70
+
71
+ assert result.status == "down"
72
+ assert len(result.diagnostics) == 1
73
+ assert results == [result]
74
+ command, kwargs = calls[0]
75
+ assert command[:3] == ["node", "/opt/todo2code/cli.js", "pipeline"]
76
+ assert "--no-docs-llm" in command
77
+ assert "--no-summary-llm" in command
78
+ assert kwargs.get("shell", False) is False
79
+
80
+
81
+ def test_cli_monitor_resolves_single_javascript_launcher(tmp_path: Path, monkeypatch) -> None:
82
+ path = _diagnostics(tmp_path, [])
83
+ cli_path = tmp_path / "cli.js"
84
+ cli_path.write_text("", encoding="utf-8")
85
+ observed = {}
86
+
87
+ monkeypatch.setattr("wup.intent_monitor.shutil.which", lambda command: str(cli_path))
88
+
89
+ def fake_run(command, **kwargs):
90
+ observed["command"] = command
91
+ return SimpleNamespace(
92
+ returncode=0,
93
+ stdout=json.dumps({"diagnosticsPath": str(path)}),
94
+ stderr="",
95
+ )
96
+
97
+ monkeypatch.setattr("wup.intent_monitor.subprocess.run", fake_run)
98
+ monitor = Todo2CodeIntentMonitor(
99
+ tmp_path,
100
+ IntentMonitoringConfig(enabled=True, command=["t2c"]),
101
+ lambda result: None,
102
+ )
103
+
104
+ result = monitor.run_once()
105
+
106
+ assert result.status == "up"
107
+ assert observed["command"][:2] == ["node", str(cli_path.resolve())]
108
+
109
+
110
+ def test_monitor_filters_codes_and_maps_review_required_to_degraded(tmp_path: Path) -> None:
111
+ path = _diagnostics(
112
+ tmp_path,
113
+ [
114
+ {"severity": "review_required", "code": "CHANGELOG_WITHOUT_IMPLEMENTATION"},
115
+ {"severity": "blocking", "code": "UNRELATED"},
116
+ ],
117
+ )
118
+ config = IntentMonitoringConfig(
119
+ fail_codes=["CHANGELOG_WITHOUT_IMPLEMENTATION"]
120
+ )
121
+ monitor = Todo2CodeIntentMonitor(tmp_path, config, lambda result: None)
122
+
123
+ result = monitor._result_from_payload({"diagnosticsPath": str(path)})
124
+
125
+ assert result.status == "degraded"
126
+ assert [item["code"] for item in result.diagnostics] == [
127
+ "CHANGELOG_WITHOUT_IMPLEMENTATION"
128
+ ]
129
+
130
+
131
+ def test_monitor_reports_runner_failure_as_down(tmp_path: Path, monkeypatch) -> None:
132
+ monkeypatch.setattr(
133
+ "wup.intent_monitor.subprocess.run",
134
+ lambda *args, **kwargs: SimpleNamespace(
135
+ returncode=2, stdout="", stderr="structured output invalid"
136
+ ),
137
+ )
138
+ results = []
139
+ monitor = Todo2CodeIntentMonitor(
140
+ tmp_path, IntentMonitoringConfig(enabled=True), results.append
141
+ )
142
+
143
+ result = monitor.run_once()
144
+
145
+ assert result.status == "down"
146
+ assert "structured output invalid" in result.message
147
+ assert results == [result]
148
+
149
+
150
+ def test_python_runner_uses_todo2code_sdk_bridge(tmp_path: Path, monkeypatch) -> None:
151
+ path = _diagnostics(tmp_path, [])
152
+ observed = {}
153
+
154
+ class FakeRuntime:
155
+ def __init__(self, root, **kwargs):
156
+ observed["root"] = root
157
+ observed["init"] = kwargs
158
+
159
+ def invoke(self, arguments):
160
+ observed["arguments"] = arguments
161
+ payload = {"diagnosticsPath": str(path), "runDirectory": "python-run"}
162
+ return SimpleNamespace(stdout=json.dumps(payload))
163
+
164
+ module = types.ModuleType("todo2code")
165
+ module.TypeScriptRuntime = FakeRuntime
166
+ monkeypatch.setitem(sys.modules, "todo2code", module)
167
+ config = IntentMonitoringConfig(
168
+ enabled=True,
169
+ runner="python",
170
+ cli_path="/opt/todo2code/dist/src/cli.js",
171
+ mode="prefer-llm",
172
+ docs_llm=True,
173
+ )
174
+ monitor = Todo2CodeIntentMonitor(tmp_path, config, lambda result: None)
175
+
176
+ result = monitor.run_once()
177
+
178
+ assert result.status == "up"
179
+ assert observed["root"] == tmp_path.resolve()
180
+ assert observed["init"]["cli_path"] == config.cli_path
181
+ assert observed["arguments"][:2] == ["pipeline", str(tmp_path.resolve())]
182
+ assert observed["arguments"][observed["arguments"].index("--markdown-mode") + 1] == "prefer-llm"
183
+ assert "--no-docs-llm" not in observed["arguments"]
184
+ assert "--no-communication" in observed["arguments"]
185
+
186
+
187
+ def test_testql_watcher_projects_intent_result_as_project_health() -> None:
188
+ watcher = object.__new__(TestQLWatcher)
189
+ watcher.config = SimpleNamespace(project=SimpleNamespace(name="workspace"))
190
+ transitions = []
191
+ watcher._record_health_transition = lambda **values: transitions.append(values)
192
+ result = IntentAuditResult(
193
+ status="down",
194
+ message="todo2code found 3 intent issue(s), 1 blocking",
195
+ diagnostics_path="/tmp/diagnostics.json",
196
+ )
197
+
198
+ watcher._record_intent_audit(result)
199
+
200
+ assert transitions == [
201
+ {
202
+ "service": "workspace:intent",
203
+ "status": "down",
204
+ "stage": "intent",
205
+ "message": result.message,
206
+ "track_file": result.diagnostics_path,
207
+ }
208
+ ]
@@ -58,6 +58,29 @@ def test_detect_watch_paths_backend_frontend(tmp_path: Path) -> None:
58
58
  assert "app/**" not in paths
59
59
 
60
60
 
61
+ def test_detect_watch_paths_finds_source_dirs_in_project_subfolders(
62
+ tmp_path: Path,
63
+ ) -> None:
64
+ (tmp_path / "api" / "src").mkdir(parents=True)
65
+ (tmp_path / "worker" / "services").mkdir(parents=True)
66
+
67
+ assert detect_watch_paths(tmp_path) == ["api/src/**", "worker/services/**"]
68
+
69
+
70
+ def test_detect_watch_paths_combines_root_and_project_subfolders(tmp_path: Path) -> None:
71
+ (tmp_path / "src").mkdir()
72
+ (tmp_path / "worker" / "src").mkdir(parents=True)
73
+
74
+ assert detect_watch_paths(tmp_path) == ["src/**", "worker/src/**"]
75
+
76
+
77
+ def test_detect_watch_paths_skips_nested_vendor_and_hidden_dirs(tmp_path: Path) -> None:
78
+ (tmp_path / "node_modules" / "package" / "src").mkdir(parents=True)
79
+ (tmp_path / ".cache" / "src").mkdir(parents=True)
80
+
81
+ assert detect_watch_paths(tmp_path) == ["app/**", "src/**", "routes/**"]
82
+
83
+
61
84
  def test_default_config_watches_only_real_dirs(tmp_path: Path) -> None:
62
85
  (tmp_path / "services").mkdir()
63
86
  cfg = get_default_config(tmp_path)
@@ -122,6 +145,25 @@ def test_prepare_observer_none_when_no_valid_paths(tmp_path: Path) -> None:
122
145
  assert watcher.prepare_observer() is None
123
146
 
124
147
 
148
+ def test_watcher_falls_back_to_source_dirs_in_project_subfolders(
149
+ tmp_path: Path,
150
+ ) -> None:
151
+ nested_src = tmp_path / "api" / "src"
152
+ nested_services = tmp_path / "worker" / "services"
153
+ nested_src.mkdir(parents=True)
154
+ nested_services.mkdir(parents=True)
155
+ watcher = _watcher(tmp_path, paths=["app/**", "src/**", "routes/**"])
156
+
157
+ assert watcher.build_watched_paths() == [str(nested_src), str(nested_services)]
158
+
159
+
160
+ def test_watcher_does_not_replace_invalid_custom_paths(tmp_path: Path) -> None:
161
+ (tmp_path / "src").mkdir()
162
+ watcher = _watcher(tmp_path, paths=["misspelled-source/**"])
163
+
164
+ assert watcher.build_watched_paths() == []
165
+
166
+
125
167
  def test_multi_watcher_returns_false_when_all_invalid(tmp_path: Path) -> None:
126
168
  w1 = _watcher(tmp_path, paths=["missing-a/**"])
127
169
  w2 = _watcher(tmp_path, paths=["missing-b/**"])
@@ -7,7 +7,7 @@ WUP monitors file changes and runs intelligent regression tests using a 3-layer
7
7
  3. Detail Layer: Full tests with blame reports (only on failure)
8
8
  """
9
9
 
10
- __version__ = "0.2.79"
10
+ __version__ = "0.2.80"
11
11
  __author__ = "Tom Sapletta"
12
12
 
13
13
  from .config import load_config, save_config, get_default_config
@@ -13,6 +13,7 @@ import yaml
13
13
 
14
14
  from .models.config import (
15
15
  AnomalyDetectionConfig,
16
+ IntentMonitoringConfig,
16
17
  NotifyConfig,
17
18
  PlanfileConfig,
18
19
  ProjectConfig,
@@ -438,6 +439,52 @@ def _parse_semcod_tools_config(raw: dict) -> SemcodToolsConfig:
438
439
  )
439
440
 
440
441
 
442
+ def _parse_intent_monitoring_config(raw: dict) -> IntentMonitoringConfig:
443
+ intent_raw = raw.get("intent_monitoring", {})
444
+ if not isinstance(intent_raw, dict):
445
+ return IntentMonitoringConfig()
446
+
447
+ defaults = IntentMonitoringConfig()
448
+ command_raw = intent_raw.get("command", defaults.command)
449
+ if isinstance(command_raw, str):
450
+ command = [command_raw]
451
+ elif isinstance(command_raw, list):
452
+ command = [str(part) for part in command_raw if str(part)]
453
+ else:
454
+ command = defaults.command
455
+
456
+ docs_raw = intent_raw.get("docs", defaults.docs)
457
+ severities_raw = intent_raw.get("fail_severities", defaults.fail_severities)
458
+ codes_raw = intent_raw.get("fail_codes", defaults.fail_codes)
459
+ return IntentMonitoringConfig(
460
+ enabled=bool(intent_raw.get("enabled", defaults.enabled)),
461
+ runner=str(intent_raw.get("runner", defaults.runner)),
462
+ command=command or defaults.command,
463
+ cli_path=str(intent_raw.get("cli_path", defaults.cli_path)),
464
+ interval_s=max(0, int(intent_raw.get("interval_s", defaults.interval_s))),
465
+ debounce_s=max(0, int(intent_raw.get("debounce_s", defaults.debounce_s))),
466
+ timeout_s=max(1, int(intent_raw.get("timeout_s", defaults.timeout_s))),
467
+ run_on_start=bool(intent_raw.get("run_on_start", defaults.run_on_start)),
468
+ run_on_change=bool(intent_raw.get("run_on_change", defaults.run_on_change)),
469
+ mode=str(intent_raw.get("mode", defaults.mode)),
470
+ docs_llm=bool(intent_raw.get("docs_llm", defaults.docs_llm)),
471
+ summary_llm=bool(intent_raw.get("summary_llm", defaults.summary_llm)),
472
+ task_file=str(intent_raw.get("task_file", defaults.task_file)),
473
+ todo_file=str(intent_raw.get("todo_file", defaults.todo_file)),
474
+ changelog_file=str(intent_raw.get("changelog_file", defaults.changelog_file)),
475
+ docs=[str(item) for item in docs_raw]
476
+ if isinstance(docs_raw, list)
477
+ else defaults.docs,
478
+ output_dir=str(intent_raw.get("output_dir", defaults.output_dir)),
479
+ fail_severities=[str(item) for item in severities_raw]
480
+ if isinstance(severities_raw, list)
481
+ else defaults.fail_severities,
482
+ fail_codes=[str(item) for item in codes_raw]
483
+ if isinstance(codes_raw, list)
484
+ else defaults.fail_codes,
485
+ )
486
+
487
+
441
488
  def validate_config(
442
489
  raw: dict, *, environ: Mapping[str, str] | None = None
443
490
  ) -> WupConfig:
@@ -465,6 +512,7 @@ def validate_config(
465
512
  planfile=_parse_planfile_config(raw, effective_environ),
466
513
  anomaly_detection=_parse_anomaly_detection_config(raw),
467
514
  semcod_tools=_parse_semcod_tools_config(raw),
515
+ intent_monitoring=_parse_intent_monitoring_config(raw),
468
516
  )
469
517
 
470
518
 
@@ -485,18 +533,52 @@ _DEFAULT_SOURCE_DIRS = [
485
533
  "cmd",
486
534
  ]
487
535
 
536
+ _WATCH_DISCOVERY_SKIP_DIRS = {
537
+ ".git",
538
+ ".venv",
539
+ "venv",
540
+ "node_modules",
541
+ "__pycache__",
542
+ "dist",
543
+ "build",
544
+ }
545
+
488
546
 
489
547
  def detect_watch_paths(project_root: Path) -> list:
490
548
  """
491
549
  Pick watch-path globs for a project by probing for common source directories.
492
550
 
493
551
  Returns ``["<dir>/**", ...]`` for every well-known source directory that
494
- exists under ``project_root``. Falls back to the classic app/src/routes
495
- guess only when none are found, so a brand-new project still gets a config.
552
+ exists directly under ``project_root``. For a directory containing several
553
+ projects, the same probe is also performed one level down (for example,
554
+ ``api/src/**`` and ``worker/app/**``). Hidden and vendor/build directories
555
+ are skipped. Falls back to the classic app/src/routes guess only when none
556
+ are found, so a brand-new project still gets a config.
496
557
  """
497
558
  found = [
498
559
  f"{name}/**" for name in _DEFAULT_SOURCE_DIRS if (project_root / name).is_dir()
499
560
  ]
561
+
562
+ # A common workspace layout is ``root/project/src``. WUP used to only
563
+ # inspect ``root/src``, producing an unusable app/src/routes config for such
564
+ # workspaces. Limit discovery to one project level so we do not accidentally
565
+ # select vendored source trees deep inside dependencies.
566
+ try:
567
+ children = sorted(project_root.iterdir(), key=lambda path: path.name)
568
+ except OSError:
569
+ children = []
570
+
571
+ for child in children:
572
+ if (
573
+ not child.is_dir()
574
+ or child.name.startswith(".")
575
+ or child.name in _WATCH_DISCOVERY_SKIP_DIRS
576
+ ):
577
+ continue
578
+ for name in _DEFAULT_SOURCE_DIRS:
579
+ if (child / name).is_dir():
580
+ found.append(f"{child.name}/{name}/**")
581
+
500
582
  return found or ["app/**", "src/**", "routes/**"]
501
583
 
502
584
 
@@ -22,7 +22,7 @@ from watchdog.events import FileSystemEventHandler
22
22
  from watchdog.observers import Observer
23
23
  from watchdog.observers.polling import PollingObserver
24
24
 
25
- from .config import load_config
25
+ from .config import detect_watch_paths, load_config
26
26
  from .dependency_mapper import DependencyMapper
27
27
  from .models.config import WupConfig, ServiceConfig
28
28
  from .models.target import ServiceTestTarget
@@ -534,10 +534,12 @@ class WupWatcher:
534
534
  if self.config.watch.paths:
535
535
  # Use paths from config
536
536
  watch_paths = []
537
+ configured_paths = []
537
538
  for pattern in self.config.watch.paths:
538
539
  # Handle exclusion patterns (starting with !)
539
540
  if pattern.startswith("!"):
540
541
  continue
542
+ configured_paths.append(pattern.rstrip("/"))
541
543
  # Convert to absolute path
542
544
  if "**" in pattern:
543
545
  base_path = pattern.replace("**", "")
@@ -547,7 +549,32 @@ class WupWatcher:
547
549
  abs_path = str(self.project_root / base_path)
548
550
  if Path(abs_path).exists():
549
551
  watch_paths.append(abs_path)
550
- return watch_paths
552
+ if watch_paths:
553
+ return watch_paths
554
+
555
+ # Older auto-generated configs always contained app/src/routes.
556
+ # When the supplied root is a workspace and its projects live one
557
+ # level down, all three paths are invalid. Re-detect at runtime so
558
+ # ``wup watch .`` starts successfully without rewriting user config.
559
+ # Preserve the configuration error for deliberate custom paths;
560
+ # silently replacing a misspelled path with an unrelated detected
561
+ # directory would hide the user's mistake.
562
+ if set(configured_paths) != {"app/**", "src/**", "routes/**"}:
563
+ return []
564
+
565
+ detected_paths = []
566
+ for pattern in detect_watch_paths(self.project_root):
567
+ base_path = pattern.replace("**", "")
568
+ abs_path = self.project_root / base_path
569
+ if abs_path.exists():
570
+ detected_paths.append(str(abs_path))
571
+ if detected_paths:
572
+ self.console.print(
573
+ "[yellow]Configured watch paths do not exist; "
574
+ "using auto-detected project subfolders.[/yellow]"
575
+ )
576
+ return detected_paths
577
+ return []
551
578
 
552
579
  # Fallback to default paths
553
580
  return [