devflow-toolkit 0.3.0__py3-none-any.whl
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.
- devflow/__init__.py +0 -0
- devflow/adapters/__init__.py +47 -0
- devflow/adapters/agents.py +49 -0
- devflow/adapters/base.py +81 -0
- devflow/adapters/claude_code.py +120 -0
- devflow/adapters/codex.py +111 -0
- devflow/adapters/detector.py +118 -0
- devflow/adapters/instructions.py +263 -0
- devflow/adapters/kiro.py +211 -0
- devflow/adapters/opencode.py +237 -0
- devflow/adapters/registry.py +41 -0
- devflow/cli.py +39 -0
- devflow/commands/__init__.py +6 -0
- devflow/commands/approve.py +98 -0
- devflow/commands/harness.py +98 -0
- devflow/commands/init.py +544 -0
- devflow/commands/spec.py +514 -0
- devflow/commands/status.py +156 -0
- devflow/core/__init__.py +15 -0
- devflow/core/architecture.py +469 -0
- devflow/core/artifacts/__init__.py +29 -0
- devflow/core/artifacts/errors.py +19 -0
- devflow/core/artifacts/models.py +82 -0
- devflow/core/artifacts/store.py +130 -0
- devflow/core/clock.py +67 -0
- devflow/core/config.py +706 -0
- devflow/core/constitution.py +166 -0
- devflow/core/discovery.py +1304 -0
- devflow/core/ports.py +159 -0
- devflow/core/profile_manager.py +311 -0
- devflow/core/project.py +533 -0
- devflow/core/spec_workflow.py +932 -0
- devflow/core/specification.py +2443 -0
- devflow/core/state/__init__.py +75 -0
- devflow/core/state/errors.py +39 -0
- devflow/core/state/events.py +725 -0
- devflow/core/state/locator.py +116 -0
- devflow/core/state/lock.py +69 -0
- devflow/core/state/manager.py +960 -0
- devflow/core/state/models.py +164 -0
- devflow/core/state/transitions.py +114 -0
- devflow/models/__init__.py +6 -0
- devflow/models/profile.py +63 -0
- devflow/models/project.py +47 -0
- devflow/profiles/ai-rag/constitution/ai-rag-standards.md +49 -0
- devflow/profiles/ai-rag/profile.yaml +28 -0
- devflow/profiles/base/constitution/architecture.md +58 -0
- devflow/profiles/base/constitution/code-standards.md +85 -0
- devflow/profiles/base/constitution/constitution.md +85 -0
- devflow/profiles/base/profile.yaml +15 -0
- devflow/profiles/django-backend/constitution/django-standards.md +64 -0
- devflow/profiles/django-backend/profile.yaml +24 -0
- devflow/profiles/fastapi-backend/constitution/fastapi-standards.md +59 -0
- devflow/profiles/fastapi-backend/profile.yaml +23 -0
- devflow/prompts/__init__.py +200 -0
- devflow/prompts/discovery.md +119 -0
- devflow/prompts/spec.greenfield.md +149 -0
- devflow/prompts/spec.md +116 -0
- devflow_toolkit-0.3.0.dist-info/METADATA +628 -0
- devflow_toolkit-0.3.0.dist-info/RECORD +63 -0
- devflow_toolkit-0.3.0.dist-info/WHEEL +4 -0
- devflow_toolkit-0.3.0.dist-info/entry_points.txt +2 -0
- devflow_toolkit-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
"""Harness-independent specification workflow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from devflow.core.architecture import (
|
|
9
|
+
ArchitectureManifest,
|
|
10
|
+
ArchitectureManifestLoader,
|
|
11
|
+
)
|
|
12
|
+
from devflow.core.artifacts.store import ArtifactStore
|
|
13
|
+
from devflow.core.clock import Clock, SystemClock, format_timestamp
|
|
14
|
+
from devflow.core.config import (
|
|
15
|
+
EXECUTION_MODES,
|
|
16
|
+
ConfigError,
|
|
17
|
+
DevFlowConfig,
|
|
18
|
+
)
|
|
19
|
+
from devflow.core.constitution import Constitution, ConstitutionLoader
|
|
20
|
+
from devflow.core.discovery import (
|
|
21
|
+
DiscoveryDocument,
|
|
22
|
+
DiscoveryError,
|
|
23
|
+
DiscoveryManager,
|
|
24
|
+
DiscoveryPlanner,
|
|
25
|
+
)
|
|
26
|
+
from devflow.core.ports import (
|
|
27
|
+
HarnessExecutionError,
|
|
28
|
+
HarnessPort,
|
|
29
|
+
Invocation,
|
|
30
|
+
InvocationExecutor,
|
|
31
|
+
RunContext,
|
|
32
|
+
SetupResult,
|
|
33
|
+
build_setup_context,
|
|
34
|
+
)
|
|
35
|
+
from devflow.core.profile_manager import ProfileManager
|
|
36
|
+
from devflow.core.project import ProjectDetector
|
|
37
|
+
from devflow.core.specification import (
|
|
38
|
+
GeneratedSpec,
|
|
39
|
+
PreparedSpec,
|
|
40
|
+
SpecificationManager,
|
|
41
|
+
strip_ansi,
|
|
42
|
+
)
|
|
43
|
+
from devflow.core.state import WorkflowLocator
|
|
44
|
+
from devflow.models.project import (
|
|
45
|
+
PROJECT_MODES,
|
|
46
|
+
Project,
|
|
47
|
+
canonical_project_mode,
|
|
48
|
+
)
|
|
49
|
+
from devflow.models.profile import Profile
|
|
50
|
+
from devflow.prompts import (
|
|
51
|
+
build_discovery_prompt,
|
|
52
|
+
build_spec_prompt,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class SpecWorkflowRequest:
|
|
58
|
+
"""Everything the workflow needs to generate one specification."""
|
|
59
|
+
|
|
60
|
+
description: str
|
|
61
|
+
config: DevFlowConfig
|
|
62
|
+
execution_mode: str
|
|
63
|
+
adapter: HarnessPort | None = None
|
|
64
|
+
warnings: tuple[str, ...] = ()
|
|
65
|
+
project_mode: str = "existing"
|
|
66
|
+
continue_spec_id: str | None = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class SpecWorkflowResult:
|
|
71
|
+
"""Outcome of a specification workflow run."""
|
|
72
|
+
|
|
73
|
+
spec: GeneratedSpec | None
|
|
74
|
+
harness_name: str | None
|
|
75
|
+
execution_mode: str
|
|
76
|
+
warnings: tuple[str, ...]
|
|
77
|
+
run_warnings: tuple[str, ...]
|
|
78
|
+
constitution_warnings: tuple[str, ...]
|
|
79
|
+
setup_result: SetupResult | None
|
|
80
|
+
constitution_names: tuple[str, ...]
|
|
81
|
+
project_mode: str = "existing"
|
|
82
|
+
architecture_digest: str | None = None
|
|
83
|
+
architecture: ArchitectureManifest | None = None
|
|
84
|
+
discovery: DiscoveryDocument | None = None
|
|
85
|
+
discovery_digest: str | None = None
|
|
86
|
+
discovery_path: Path | None = None
|
|
87
|
+
discovery_source: str = "deterministic"
|
|
88
|
+
discovery_created: bool = False
|
|
89
|
+
blocked: bool = False
|
|
90
|
+
spec_written: bool = False
|
|
91
|
+
spec_reused: bool = False
|
|
92
|
+
continuation: bool = False
|
|
93
|
+
directory: Path | None = None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class SpecWorkflow:
|
|
97
|
+
"""Prepare, run and validate a specification independently of any harness."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
executor: InvocationExecutor,
|
|
102
|
+
*,
|
|
103
|
+
manager: SpecificationManager | None = None,
|
|
104
|
+
profile_manager: ProfileManager | None = None,
|
|
105
|
+
project_detector: ProjectDetector | None = None,
|
|
106
|
+
clock: Clock | None = None,
|
|
107
|
+
discovery_planner: DiscoveryPlanner | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
self.executor = executor
|
|
110
|
+
self.manager = manager or SpecificationManager()
|
|
111
|
+
self.profile_manager = profile_manager or ProfileManager()
|
|
112
|
+
self.project_detector = project_detector or ProjectDetector()
|
|
113
|
+
self.constitution_loader = ConstitutionLoader(self.profile_manager)
|
|
114
|
+
self.clock = clock or SystemClock()
|
|
115
|
+
self.discovery_planner = discovery_planner or DiscoveryPlanner()
|
|
116
|
+
|
|
117
|
+
def run(self, request: SpecWorkflowRequest) -> SpecWorkflowResult:
|
|
118
|
+
"""Run the specification workflow and return its outcome."""
|
|
119
|
+
description = request.description.strip()
|
|
120
|
+
continue_id = (request.continue_spec_id or "").strip() or None
|
|
121
|
+
if continue_id and description:
|
|
122
|
+
msg = (
|
|
123
|
+
"Pass either a new change description or --continue, not both. "
|
|
124
|
+
"Continuing reuses the discovery request of the existing change."
|
|
125
|
+
)
|
|
126
|
+
raise ConfigError(msg)
|
|
127
|
+
if not description and continue_id is None:
|
|
128
|
+
msg = "The change description cannot be empty."
|
|
129
|
+
raise ConfigError(msg)
|
|
130
|
+
if request.execution_mode not in EXECUTION_MODES:
|
|
131
|
+
msg = (
|
|
132
|
+
f"Unknown execution mode: {request.execution_mode}. "
|
|
133
|
+
f"Supported values: {', '.join(EXECUTION_MODES)}."
|
|
134
|
+
)
|
|
135
|
+
raise ConfigError(msg)
|
|
136
|
+
|
|
137
|
+
requested_mode = canonical_project_mode(request.project_mode)
|
|
138
|
+
if requested_mode not in PROJECT_MODES:
|
|
139
|
+
msg = (
|
|
140
|
+
f"Unknown project mode: {request.project_mode}. "
|
|
141
|
+
f"Supported values: {', '.join(PROJECT_MODES)}."
|
|
142
|
+
)
|
|
143
|
+
raise ConfigError(msg)
|
|
144
|
+
|
|
145
|
+
config = request.config
|
|
146
|
+
profiles = self.profile_manager.resolve(list(config.profiles))
|
|
147
|
+
constitution = self.constitution_loader.load(config, profiles)
|
|
148
|
+
|
|
149
|
+
project = self.project_detector.detect(config.root)
|
|
150
|
+
project.profiles = list(config.profiles)
|
|
151
|
+
|
|
152
|
+
warnings = self._profile_warnings(config, project)
|
|
153
|
+
discovery_manager = DiscoveryManager(config.root, clock=self.clock)
|
|
154
|
+
continuation = continue_id is not None
|
|
155
|
+
project_mode = requested_mode
|
|
156
|
+
location_directory: Path | None = None
|
|
157
|
+
location_spec_id: str | None = None
|
|
158
|
+
discovery: DiscoveryDocument | None = None
|
|
159
|
+
|
|
160
|
+
if continue_id is not None:
|
|
161
|
+
location = WorkflowLocator(config.specifications_path).find(
|
|
162
|
+
continue_id
|
|
163
|
+
)
|
|
164
|
+
location_directory = location.directory
|
|
165
|
+
location_spec_id = location.spec_id
|
|
166
|
+
discovery = discovery_manager.load(location.directory, required=True)
|
|
167
|
+
description = discovery.request.strip()
|
|
168
|
+
if not description:
|
|
169
|
+
msg = "The discovery record has an empty request."
|
|
170
|
+
raise ConfigError(msg)
|
|
171
|
+
project_mode = discovery.project_mode
|
|
172
|
+
if requested_mode != project_mode:
|
|
173
|
+
warnings.append(
|
|
174
|
+
f"the discovery record uses project mode '{project_mode}'. "
|
|
175
|
+
f"Ignoring the configured mode '{requested_mode}' for this "
|
|
176
|
+
"change."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
architecture = ArchitectureManifestLoader().load(
|
|
180
|
+
config.root,
|
|
181
|
+
required=project_mode == "greenfield",
|
|
182
|
+
)
|
|
183
|
+
if architecture is not None:
|
|
184
|
+
warnings.extend(architecture.warnings)
|
|
185
|
+
|
|
186
|
+
prepared = self.manager.prepare(
|
|
187
|
+
description=description,
|
|
188
|
+
config=config,
|
|
189
|
+
project=project,
|
|
190
|
+
constitution=constitution,
|
|
191
|
+
profiles=profiles,
|
|
192
|
+
project_mode=project_mode,
|
|
193
|
+
architecture=architecture,
|
|
194
|
+
discovery=discovery,
|
|
195
|
+
discovery_digest=(
|
|
196
|
+
self._discovery_digest(discovery_manager, location_directory)
|
|
197
|
+
if continuation and location_directory is not None
|
|
198
|
+
else None
|
|
199
|
+
),
|
|
200
|
+
directory=location_directory,
|
|
201
|
+
spec_id=location_spec_id,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
discovery_created = False
|
|
205
|
+
discovery_source = "deterministic"
|
|
206
|
+
if continuation:
|
|
207
|
+
assert discovery is not None
|
|
208
|
+
assert location_directory is not None
|
|
209
|
+
discovery_path = discovery_manager.path(location_directory)
|
|
210
|
+
discovery_digest = self._discovery_digest(
|
|
211
|
+
discovery_manager,
|
|
212
|
+
location_directory,
|
|
213
|
+
)
|
|
214
|
+
else:
|
|
215
|
+
now = format_timestamp(self.clock.now())
|
|
216
|
+
discovery = self.discovery_planner.build(
|
|
217
|
+
request=description,
|
|
218
|
+
spec_id=prepared.spec_id,
|
|
219
|
+
change=prepared.directory.name,
|
|
220
|
+
project_mode=project_mode,
|
|
221
|
+
project=project,
|
|
222
|
+
profiles=tuple(profile.name for profile in profiles),
|
|
223
|
+
reference_paths=tuple(
|
|
224
|
+
reference.path for reference in prepared.analysis.references
|
|
225
|
+
),
|
|
226
|
+
category_paths={
|
|
227
|
+
category: tuple(
|
|
228
|
+
reference.path for reference in references
|
|
229
|
+
)
|
|
230
|
+
for category, references in prepared.analysis.categories.items()
|
|
231
|
+
},
|
|
232
|
+
has_tests=prepared.analysis.has_tests,
|
|
233
|
+
architecture=architecture,
|
|
234
|
+
now=now,
|
|
235
|
+
)
|
|
236
|
+
discovery_path, discovery_digest = discovery_manager.write(
|
|
237
|
+
prepared.directory,
|
|
238
|
+
discovery,
|
|
239
|
+
)
|
|
240
|
+
discovery_created = True
|
|
241
|
+
discovery = discovery_manager.load(
|
|
242
|
+
prepared.directory,
|
|
243
|
+
required=True,
|
|
244
|
+
)
|
|
245
|
+
discovery_digest = discovery_manager.store.digest_file(
|
|
246
|
+
discovery_path
|
|
247
|
+
)
|
|
248
|
+
prepared = self.manager.prepare(
|
|
249
|
+
description=description,
|
|
250
|
+
config=config,
|
|
251
|
+
project=project,
|
|
252
|
+
constitution=constitution,
|
|
253
|
+
profiles=profiles,
|
|
254
|
+
project_mode=project_mode,
|
|
255
|
+
architecture=architecture,
|
|
256
|
+
discovery=discovery,
|
|
257
|
+
discovery_digest=discovery_digest,
|
|
258
|
+
directory=prepared.directory,
|
|
259
|
+
spec_id=prepared.spec_id,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
warnings.extend(request.warnings)
|
|
263
|
+
if (
|
|
264
|
+
request.execution_mode == "interactive"
|
|
265
|
+
and not config.permissions.allow_spec_write
|
|
266
|
+
):
|
|
267
|
+
warnings.append(
|
|
268
|
+
"Specification writes are disabled for the agent. The interactive "
|
|
269
|
+
"run cannot persist artifacts, so DevFlow will use deterministic "
|
|
270
|
+
"output unless the harness returns a valid document another way."
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
adapter = request.adapter
|
|
274
|
+
setup_result: SetupResult | None = None
|
|
275
|
+
if (
|
|
276
|
+
not discovery.is_complete
|
|
277
|
+
and self._can_run_discovery(adapter, request, config)
|
|
278
|
+
):
|
|
279
|
+
assert adapter is not None
|
|
280
|
+
setup_result = self._setup_adapter(
|
|
281
|
+
adapter=adapter,
|
|
282
|
+
config=config,
|
|
283
|
+
project=project,
|
|
284
|
+
profiles=profiles,
|
|
285
|
+
constitution=constitution,
|
|
286
|
+
warnings=warnings,
|
|
287
|
+
)
|
|
288
|
+
discovery, discovery_digest, discovery_changed = (
|
|
289
|
+
self._run_discovery(
|
|
290
|
+
prepared=prepared,
|
|
291
|
+
discovery=discovery,
|
|
292
|
+
discovery_manager=discovery_manager,
|
|
293
|
+
adapter=adapter,
|
|
294
|
+
execution_mode=request.execution_mode,
|
|
295
|
+
config=config,
|
|
296
|
+
project=project,
|
|
297
|
+
constitution=constitution,
|
|
298
|
+
profiles=profiles,
|
|
299
|
+
warnings=warnings,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
if discovery_changed:
|
|
303
|
+
discovery_source = "agent"
|
|
304
|
+
discovery = discovery_manager.load(
|
|
305
|
+
prepared.directory,
|
|
306
|
+
required=True,
|
|
307
|
+
)
|
|
308
|
+
discovery_digest = self._discovery_digest(
|
|
309
|
+
discovery_manager,
|
|
310
|
+
prepared.directory,
|
|
311
|
+
)
|
|
312
|
+
prepared = self.manager.prepare(
|
|
313
|
+
description=description,
|
|
314
|
+
config=config,
|
|
315
|
+
project=project,
|
|
316
|
+
constitution=constitution,
|
|
317
|
+
profiles=profiles,
|
|
318
|
+
project_mode=project_mode,
|
|
319
|
+
architecture=architecture,
|
|
320
|
+
discovery=discovery,
|
|
321
|
+
discovery_digest=discovery_digest,
|
|
322
|
+
directory=prepared.directory,
|
|
323
|
+
spec_id=prepared.spec_id,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
self.manager.materialize_context(prepared)
|
|
327
|
+
|
|
328
|
+
if not discovery.is_complete:
|
|
329
|
+
return self._build_result(
|
|
330
|
+
spec=None,
|
|
331
|
+
harness_name=None,
|
|
332
|
+
execution_mode=request.execution_mode,
|
|
333
|
+
warnings=warnings,
|
|
334
|
+
run_warnings=[],
|
|
335
|
+
setup_result=setup_result,
|
|
336
|
+
constitution=constitution,
|
|
337
|
+
project_mode=project_mode,
|
|
338
|
+
architecture=architecture,
|
|
339
|
+
discovery=discovery,
|
|
340
|
+
discovery_digest=discovery_digest,
|
|
341
|
+
discovery_path=discovery_path,
|
|
342
|
+
discovery_source=discovery_source,
|
|
343
|
+
discovery_created=discovery_created,
|
|
344
|
+
blocked=True,
|
|
345
|
+
spec_written=False,
|
|
346
|
+
spec_reused=False,
|
|
347
|
+
continuation=continuation,
|
|
348
|
+
directory=prepared.directory,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
reused_spec = self._reusable_spec(
|
|
352
|
+
prepared=prepared,
|
|
353
|
+
continuation=continuation,
|
|
354
|
+
project_mode=project_mode,
|
|
355
|
+
architecture=architecture,
|
|
356
|
+
discovery_digest=discovery_digest,
|
|
357
|
+
)
|
|
358
|
+
if reused_spec is not None:
|
|
359
|
+
return self._build_result(
|
|
360
|
+
spec=reused_spec,
|
|
361
|
+
harness_name=None,
|
|
362
|
+
execution_mode=request.execution_mode,
|
|
363
|
+
warnings=warnings,
|
|
364
|
+
run_warnings=[],
|
|
365
|
+
setup_result=setup_result,
|
|
366
|
+
constitution=constitution,
|
|
367
|
+
project_mode=project_mode,
|
|
368
|
+
architecture=architecture,
|
|
369
|
+
discovery=discovery,
|
|
370
|
+
discovery_digest=discovery_digest,
|
|
371
|
+
discovery_path=discovery_path,
|
|
372
|
+
discovery_source=discovery_source,
|
|
373
|
+
discovery_created=discovery_created,
|
|
374
|
+
blocked=False,
|
|
375
|
+
spec_written=False,
|
|
376
|
+
spec_reused=True,
|
|
377
|
+
continuation=continuation,
|
|
378
|
+
directory=prepared.directory,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
archive_existing = prepared.path.is_file()
|
|
382
|
+
|
|
383
|
+
if adapter is None:
|
|
384
|
+
return self._fallback(
|
|
385
|
+
prepared=prepared,
|
|
386
|
+
warnings=warnings,
|
|
387
|
+
constitution=constitution,
|
|
388
|
+
execution_mode=request.execution_mode,
|
|
389
|
+
project_mode=project_mode,
|
|
390
|
+
architecture=architecture,
|
|
391
|
+
discovery=discovery,
|
|
392
|
+
discovery_digest=discovery_digest,
|
|
393
|
+
discovery_path=discovery_path,
|
|
394
|
+
discovery_source=discovery_source,
|
|
395
|
+
discovery_created=discovery_created,
|
|
396
|
+
continuation=continuation,
|
|
397
|
+
archive=archive_existing,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if not config.permissions.allow_project_read:
|
|
401
|
+
warnings.append(
|
|
402
|
+
"Project reads are disabled for the selected harness. DevFlow "
|
|
403
|
+
"cannot provide project context to the agent, so deterministic "
|
|
404
|
+
"DevFlow output will be used."
|
|
405
|
+
)
|
|
406
|
+
return self._fallback(
|
|
407
|
+
prepared=prepared,
|
|
408
|
+
warnings=warnings,
|
|
409
|
+
constitution=constitution,
|
|
410
|
+
execution_mode=request.execution_mode,
|
|
411
|
+
project_mode=project_mode,
|
|
412
|
+
architecture=architecture,
|
|
413
|
+
discovery=discovery,
|
|
414
|
+
discovery_digest=discovery_digest,
|
|
415
|
+
discovery_path=discovery_path,
|
|
416
|
+
discovery_source=discovery_source,
|
|
417
|
+
discovery_created=discovery_created,
|
|
418
|
+
continuation=continuation,
|
|
419
|
+
archive=archive_existing,
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
if not adapter.is_available():
|
|
423
|
+
warnings.append(
|
|
424
|
+
f"Agent harness '{adapter.name}' is no longer available on "
|
|
425
|
+
"PATH. DevFlow will use deterministic output."
|
|
426
|
+
)
|
|
427
|
+
return self._fallback(
|
|
428
|
+
prepared=prepared,
|
|
429
|
+
warnings=warnings,
|
|
430
|
+
constitution=constitution,
|
|
431
|
+
execution_mode=request.execution_mode,
|
|
432
|
+
project_mode=project_mode,
|
|
433
|
+
architecture=architecture,
|
|
434
|
+
discovery=discovery,
|
|
435
|
+
discovery_digest=discovery_digest,
|
|
436
|
+
discovery_path=discovery_path,
|
|
437
|
+
discovery_source=discovery_source,
|
|
438
|
+
discovery_created=discovery_created,
|
|
439
|
+
continuation=continuation,
|
|
440
|
+
archive=archive_existing,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
if setup_result is None:
|
|
444
|
+
setup_result = self._setup_adapter(
|
|
445
|
+
adapter=adapter,
|
|
446
|
+
config=config,
|
|
447
|
+
project=project,
|
|
448
|
+
profiles=profiles,
|
|
449
|
+
constitution=constitution,
|
|
450
|
+
warnings=warnings,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
context_path = self.manager.materialize_context(prepared)
|
|
454
|
+
prompt = build_spec_prompt(
|
|
455
|
+
request=description,
|
|
456
|
+
project_name=project.name,
|
|
457
|
+
context_path=self._relative(config, context_path),
|
|
458
|
+
spec_path=self._relative(config, prepared.path),
|
|
459
|
+
constitution_paths=[
|
|
460
|
+
self._relative(
|
|
461
|
+
config,
|
|
462
|
+
config.constitution.path / document.name,
|
|
463
|
+
)
|
|
464
|
+
for document in constitution.documents
|
|
465
|
+
],
|
|
466
|
+
harness_name=adapter.display_name,
|
|
467
|
+
execution_mode=request.execution_mode,
|
|
468
|
+
permissions=config.permissions,
|
|
469
|
+
project_mode=project_mode,
|
|
470
|
+
architecture_path=(
|
|
471
|
+
architecture.relative_path(config.root)
|
|
472
|
+
if architecture is not None
|
|
473
|
+
else ""
|
|
474
|
+
),
|
|
475
|
+
architecture_digest=(
|
|
476
|
+
architecture.digest if architecture is not None else ""
|
|
477
|
+
),
|
|
478
|
+
architecture_style=(
|
|
479
|
+
architecture.style if architecture is not None else ""
|
|
480
|
+
),
|
|
481
|
+
discovery_path=(
|
|
482
|
+
self._relative(config, discovery_path)
|
|
483
|
+
if discovery_path is not None
|
|
484
|
+
else ""
|
|
485
|
+
),
|
|
486
|
+
discovery_digest=discovery_digest,
|
|
487
|
+
discovery_decisions=list(discovery.decisions()),
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
output_path = (
|
|
491
|
+
prepared.directory / ".devflow-agent-output.md"
|
|
492
|
+
if request.execution_mode == "non-interactive"
|
|
493
|
+
else None
|
|
494
|
+
)
|
|
495
|
+
run_context = RunContext(
|
|
496
|
+
root=config.root,
|
|
497
|
+
prompt=prompt,
|
|
498
|
+
spec_directory=prepared.directory,
|
|
499
|
+
spec_path=prepared.path,
|
|
500
|
+
mode=request.execution_mode,
|
|
501
|
+
permissions=config.permissions,
|
|
502
|
+
output_path=output_path,
|
|
503
|
+
)
|
|
504
|
+
warnings.extend(adapter.permission_warnings(run_context))
|
|
505
|
+
|
|
506
|
+
invocation = adapter.build_run(run_context)
|
|
507
|
+
spec, run_warnings = self._run_agent(
|
|
508
|
+
prepared=prepared,
|
|
509
|
+
adapter=adapter,
|
|
510
|
+
invocation=invocation,
|
|
511
|
+
execution_mode=request.execution_mode,
|
|
512
|
+
output_path=output_path,
|
|
513
|
+
project_mode=project_mode,
|
|
514
|
+
architecture=architecture,
|
|
515
|
+
discovery_digest=discovery_digest,
|
|
516
|
+
archive=archive_existing,
|
|
517
|
+
)
|
|
518
|
+
return self._build_result(
|
|
519
|
+
spec=spec,
|
|
520
|
+
harness_name=adapter.display_name,
|
|
521
|
+
execution_mode=request.execution_mode,
|
|
522
|
+
warnings=warnings,
|
|
523
|
+
run_warnings=run_warnings,
|
|
524
|
+
setup_result=setup_result,
|
|
525
|
+
constitution=constitution,
|
|
526
|
+
project_mode=project_mode,
|
|
527
|
+
architecture=architecture,
|
|
528
|
+
discovery=discovery,
|
|
529
|
+
discovery_digest=discovery_digest,
|
|
530
|
+
discovery_path=discovery_path,
|
|
531
|
+
discovery_source=discovery_source,
|
|
532
|
+
discovery_created=discovery_created,
|
|
533
|
+
blocked=False,
|
|
534
|
+
spec_written=True,
|
|
535
|
+
spec_reused=False,
|
|
536
|
+
continuation=continuation,
|
|
537
|
+
directory=prepared.directory,
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
def _can_run_discovery(
|
|
541
|
+
self,
|
|
542
|
+
adapter: HarnessPort | None,
|
|
543
|
+
request: SpecWorkflowRequest,
|
|
544
|
+
config: DevFlowConfig,
|
|
545
|
+
) -> bool:
|
|
546
|
+
return (
|
|
547
|
+
adapter is not None
|
|
548
|
+
and request.execution_mode == "interactive"
|
|
549
|
+
and config.permissions.allow_project_read
|
|
550
|
+
and config.permissions.allow_spec_write
|
|
551
|
+
and adapter.is_available()
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
def _setup_adapter(
|
|
555
|
+
self,
|
|
556
|
+
*,
|
|
557
|
+
adapter: HarnessPort,
|
|
558
|
+
config: DevFlowConfig,
|
|
559
|
+
project: Project,
|
|
560
|
+
profiles: list[Profile],
|
|
561
|
+
constitution: Constitution,
|
|
562
|
+
warnings: list[str],
|
|
563
|
+
) -> SetupResult:
|
|
564
|
+
setup_result = adapter.setup(
|
|
565
|
+
build_setup_context(
|
|
566
|
+
config=config,
|
|
567
|
+
project=project,
|
|
568
|
+
profiles=profiles,
|
|
569
|
+
constitution=constitution,
|
|
570
|
+
)
|
|
571
|
+
)
|
|
572
|
+
warnings.extend(setup_result.warnings)
|
|
573
|
+
return setup_result
|
|
574
|
+
|
|
575
|
+
def _run_discovery(
|
|
576
|
+
self,
|
|
577
|
+
*,
|
|
578
|
+
prepared: PreparedSpec,
|
|
579
|
+
discovery: DiscoveryDocument,
|
|
580
|
+
discovery_manager: DiscoveryManager,
|
|
581
|
+
adapter: HarnessPort,
|
|
582
|
+
execution_mode: str,
|
|
583
|
+
config: DevFlowConfig,
|
|
584
|
+
project: Project,
|
|
585
|
+
constitution: Constitution,
|
|
586
|
+
profiles: list[Profile],
|
|
587
|
+
warnings: list[str],
|
|
588
|
+
) -> tuple[DiscoveryDocument, str, bool]:
|
|
589
|
+
context_path = self.manager.materialize_context(prepared)
|
|
590
|
+
discovery_path = discovery_manager.path(prepared.directory)
|
|
591
|
+
original_digest = self._discovery_digest(
|
|
592
|
+
discovery_manager,
|
|
593
|
+
prepared.directory,
|
|
594
|
+
)
|
|
595
|
+
spec_existed = prepared.path.exists()
|
|
596
|
+
prompt = build_discovery_prompt(
|
|
597
|
+
request=discovery.request,
|
|
598
|
+
project_name=project.name,
|
|
599
|
+
context_path=self._relative(config, context_path),
|
|
600
|
+
discovery_path=self._relative(config, discovery_path),
|
|
601
|
+
harness_name=adapter.display_name,
|
|
602
|
+
execution_mode=execution_mode,
|
|
603
|
+
permissions=config.permissions,
|
|
604
|
+
)
|
|
605
|
+
run_context = RunContext(
|
|
606
|
+
root=config.root,
|
|
607
|
+
prompt=prompt,
|
|
608
|
+
spec_directory=prepared.directory,
|
|
609
|
+
spec_path=prepared.path,
|
|
610
|
+
mode=execution_mode,
|
|
611
|
+
permissions=config.permissions,
|
|
612
|
+
output_path=None,
|
|
613
|
+
)
|
|
614
|
+
warnings.extend(adapter.permission_warnings(run_context))
|
|
615
|
+
invocation = adapter.build_run(run_context)
|
|
616
|
+
try:
|
|
617
|
+
self.executor(invocation)
|
|
618
|
+
except HarnessExecutionError as error:
|
|
619
|
+
warnings.append(
|
|
620
|
+
f"discovery agent run failed: {error}. Discovery remains "
|
|
621
|
+
"awaiting clarification."
|
|
622
|
+
)
|
|
623
|
+
return discovery, original_digest, False
|
|
624
|
+
if not spec_existed and prepared.path.exists():
|
|
625
|
+
warnings.append(
|
|
626
|
+
"the discovery agent wrote spec.md although Discovery was "
|
|
627
|
+
"incomplete. The file was archived and will not be recorded."
|
|
628
|
+
)
|
|
629
|
+
store = ArtifactStore(prepared.root)
|
|
630
|
+
store.archive(prepared.path)
|
|
631
|
+
prepared.path.unlink(missing_ok=True)
|
|
632
|
+
try:
|
|
633
|
+
updated = discovery_manager.load(
|
|
634
|
+
prepared.directory,
|
|
635
|
+
required=True,
|
|
636
|
+
)
|
|
637
|
+
except DiscoveryError as error:
|
|
638
|
+
warnings.append(
|
|
639
|
+
f"the discovery agent produced invalid discovery data: {error}. "
|
|
640
|
+
"Discovery remains unchanged."
|
|
641
|
+
)
|
|
642
|
+
return discovery, original_digest, False
|
|
643
|
+
digest = self._discovery_digest(
|
|
644
|
+
discovery_manager,
|
|
645
|
+
prepared.directory,
|
|
646
|
+
)
|
|
647
|
+
if digest == original_digest:
|
|
648
|
+
warnings.append(
|
|
649
|
+
"the discovery agent did not record any answer or assumption "
|
|
650
|
+
"acceptance."
|
|
651
|
+
)
|
|
652
|
+
return updated, digest, digest != original_digest
|
|
653
|
+
|
|
654
|
+
def _discovery_digest(
|
|
655
|
+
self,
|
|
656
|
+
discovery_manager: DiscoveryManager,
|
|
657
|
+
directory: Path | None,
|
|
658
|
+
) -> str:
|
|
659
|
+
if directory is None:
|
|
660
|
+
return ""
|
|
661
|
+
path = discovery_manager.path(directory)
|
|
662
|
+
if not path.is_file():
|
|
663
|
+
return ""
|
|
664
|
+
return discovery_manager.store.digest_file(path)
|
|
665
|
+
|
|
666
|
+
def _reusable_spec(
|
|
667
|
+
self,
|
|
668
|
+
*,
|
|
669
|
+
prepared: PreparedSpec,
|
|
670
|
+
continuation: bool,
|
|
671
|
+
project_mode: str,
|
|
672
|
+
architecture: ArchitectureManifest | None,
|
|
673
|
+
discovery_digest: str,
|
|
674
|
+
) -> GeneratedSpec | None:
|
|
675
|
+
if not continuation or not prepared.path.is_file():
|
|
676
|
+
return None
|
|
677
|
+
try:
|
|
678
|
+
content = prepared.path.read_text(encoding="utf-8")
|
|
679
|
+
except (OSError, UnicodeDecodeError):
|
|
680
|
+
return None
|
|
681
|
+
valid, _reason = self.manager.validate(
|
|
682
|
+
content,
|
|
683
|
+
project_mode=project_mode,
|
|
684
|
+
architecture=architecture,
|
|
685
|
+
discovery_digest=discovery_digest,
|
|
686
|
+
)
|
|
687
|
+
if not valid:
|
|
688
|
+
return None
|
|
689
|
+
questions = self.manager.extract_open_questions(content)
|
|
690
|
+
return GeneratedSpec(
|
|
691
|
+
spec_id=prepared.spec_id,
|
|
692
|
+
title=prepared.title,
|
|
693
|
+
path=prepared.path,
|
|
694
|
+
open_questions=tuple(questions),
|
|
695
|
+
source="existing",
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
def _run_agent(
|
|
699
|
+
self,
|
|
700
|
+
*,
|
|
701
|
+
prepared: PreparedSpec,
|
|
702
|
+
adapter: HarnessPort,
|
|
703
|
+
invocation: Invocation,
|
|
704
|
+
execution_mode: str,
|
|
705
|
+
output_path: Path | None,
|
|
706
|
+
project_mode: str,
|
|
707
|
+
architecture: ArchitectureManifest | None,
|
|
708
|
+
discovery_digest: str,
|
|
709
|
+
archive: bool,
|
|
710
|
+
) -> tuple[GeneratedSpec, list[str]]:
|
|
711
|
+
source = f"{adapter.display_name} ({execution_mode})"
|
|
712
|
+
run_warnings: list[str] = []
|
|
713
|
+
try:
|
|
714
|
+
run_result = self.executor(invocation)
|
|
715
|
+
except HarnessExecutionError as error:
|
|
716
|
+
run_warnings.append(str(error))
|
|
717
|
+
return (
|
|
718
|
+
self.manager.write(prepared, archive=archive),
|
|
719
|
+
run_warnings,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
try:
|
|
723
|
+
if execution_mode == "interactive":
|
|
724
|
+
if prepared.path.is_symlink():
|
|
725
|
+
run_warnings.append(
|
|
726
|
+
"the interactive agent wrote the specification as a "
|
|
727
|
+
"symbolic link. Using deterministic DevFlow output."
|
|
728
|
+
)
|
|
729
|
+
return (
|
|
730
|
+
self.manager.write(prepared, archive=archive),
|
|
731
|
+
run_warnings,
|
|
732
|
+
)
|
|
733
|
+
if prepared.path.is_file():
|
|
734
|
+
content = prepared.path.read_text(encoding="utf-8")
|
|
735
|
+
valid, reason = self.manager.validate(
|
|
736
|
+
content,
|
|
737
|
+
project_mode=project_mode,
|
|
738
|
+
architecture=architecture,
|
|
739
|
+
discovery_digest=discovery_digest,
|
|
740
|
+
)
|
|
741
|
+
if valid:
|
|
742
|
+
return (
|
|
743
|
+
self.manager.write(
|
|
744
|
+
prepared,
|
|
745
|
+
content,
|
|
746
|
+
source=source,
|
|
747
|
+
archive=archive,
|
|
748
|
+
),
|
|
749
|
+
run_warnings,
|
|
750
|
+
)
|
|
751
|
+
run_warnings.append(
|
|
752
|
+
"agent specification was invalid: "
|
|
753
|
+
f"{reason}. Using deterministic DevFlow output."
|
|
754
|
+
)
|
|
755
|
+
return (
|
|
756
|
+
self.manager.write(prepared, archive=archive),
|
|
757
|
+
run_warnings,
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
run_warnings.append(
|
|
761
|
+
"the interactive agent did not write the specification. "
|
|
762
|
+
"Using deterministic DevFlow output."
|
|
763
|
+
)
|
|
764
|
+
return (
|
|
765
|
+
self.manager.write(prepared, archive=archive),
|
|
766
|
+
run_warnings,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
if run_result.returncode != 0:
|
|
770
|
+
detail = strip_ansi(run_result.stderr).strip().splitlines()
|
|
771
|
+
reason = (
|
|
772
|
+
detail[-1]
|
|
773
|
+
if detail
|
|
774
|
+
else f"exit code {run_result.returncode}"
|
|
775
|
+
)
|
|
776
|
+
run_warnings.append(
|
|
777
|
+
f"agent run failed: {reason}. "
|
|
778
|
+
"Using deterministic DevFlow output."
|
|
779
|
+
)
|
|
780
|
+
return (
|
|
781
|
+
self.manager.write(prepared, archive=archive),
|
|
782
|
+
run_warnings,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
content = run_result.file_output or run_result.stdout
|
|
786
|
+
valid, reason = self.manager.validate(
|
|
787
|
+
content,
|
|
788
|
+
project_mode=project_mode,
|
|
789
|
+
architecture=architecture,
|
|
790
|
+
discovery_digest=discovery_digest,
|
|
791
|
+
)
|
|
792
|
+
if valid:
|
|
793
|
+
return (
|
|
794
|
+
self.manager.write(
|
|
795
|
+
prepared,
|
|
796
|
+
content,
|
|
797
|
+
source=source,
|
|
798
|
+
archive=archive,
|
|
799
|
+
),
|
|
800
|
+
run_warnings,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
run_warnings.append(
|
|
804
|
+
f"agent output was invalid: {reason}. "
|
|
805
|
+
"Using deterministic DevFlow output."
|
|
806
|
+
)
|
|
807
|
+
return (
|
|
808
|
+
self.manager.write(prepared, archive=archive),
|
|
809
|
+
run_warnings,
|
|
810
|
+
)
|
|
811
|
+
finally:
|
|
812
|
+
if output_path is not None:
|
|
813
|
+
output_path.unlink(missing_ok=True)
|
|
814
|
+
|
|
815
|
+
def _profile_warnings(
|
|
816
|
+
self,
|
|
817
|
+
config: DevFlowConfig,
|
|
818
|
+
project: Project,
|
|
819
|
+
) -> list[str]:
|
|
820
|
+
detected_profiles = {
|
|
821
|
+
match.profile.name
|
|
822
|
+
for match in self.profile_manager.detect(project)
|
|
823
|
+
}
|
|
824
|
+
configured_profiles = set(config.profiles)
|
|
825
|
+
new_profiles = sorted(detected_profiles - configured_profiles)
|
|
826
|
+
stale_profiles = sorted(configured_profiles - detected_profiles)
|
|
827
|
+
|
|
828
|
+
warnings: list[str] = []
|
|
829
|
+
for profile_name in new_profiles:
|
|
830
|
+
warnings.append(
|
|
831
|
+
f"detected profile '{profile_name}' is not configured. Current "
|
|
832
|
+
"analysis uses the profiles in .devflow/config.yaml."
|
|
833
|
+
)
|
|
834
|
+
for profile_name in stale_profiles:
|
|
835
|
+
warnings.append(
|
|
836
|
+
f"configured profile '{profile_name}' was not detected in the "
|
|
837
|
+
"current project. Review .devflow/config.yaml if this changed."
|
|
838
|
+
)
|
|
839
|
+
return warnings
|
|
840
|
+
|
|
841
|
+
def _fallback(
|
|
842
|
+
self,
|
|
843
|
+
*,
|
|
844
|
+
prepared: PreparedSpec,
|
|
845
|
+
warnings: list[str],
|
|
846
|
+
constitution: Constitution,
|
|
847
|
+
execution_mode: str,
|
|
848
|
+
project_mode: str,
|
|
849
|
+
architecture: ArchitectureManifest | None,
|
|
850
|
+
discovery: DiscoveryDocument,
|
|
851
|
+
discovery_digest: str,
|
|
852
|
+
discovery_path: Path,
|
|
853
|
+
discovery_source: str,
|
|
854
|
+
discovery_created: bool,
|
|
855
|
+
continuation: bool,
|
|
856
|
+
archive: bool,
|
|
857
|
+
) -> SpecWorkflowResult:
|
|
858
|
+
self.manager.materialize_context(prepared)
|
|
859
|
+
return self._build_result(
|
|
860
|
+
spec=self.manager.write(prepared, archive=archive),
|
|
861
|
+
harness_name=None,
|
|
862
|
+
execution_mode=execution_mode,
|
|
863
|
+
warnings=warnings,
|
|
864
|
+
run_warnings=[],
|
|
865
|
+
setup_result=None,
|
|
866
|
+
constitution=constitution,
|
|
867
|
+
project_mode=project_mode,
|
|
868
|
+
architecture=architecture,
|
|
869
|
+
discovery=discovery,
|
|
870
|
+
discovery_digest=discovery_digest,
|
|
871
|
+
discovery_path=discovery_path,
|
|
872
|
+
discovery_source=discovery_source,
|
|
873
|
+
discovery_created=discovery_created,
|
|
874
|
+
blocked=False,
|
|
875
|
+
spec_written=True,
|
|
876
|
+
spec_reused=False,
|
|
877
|
+
continuation=continuation,
|
|
878
|
+
directory=prepared.directory,
|
|
879
|
+
)
|
|
880
|
+
|
|
881
|
+
def _build_result(
|
|
882
|
+
self,
|
|
883
|
+
*,
|
|
884
|
+
spec: GeneratedSpec | None,
|
|
885
|
+
harness_name: str | None,
|
|
886
|
+
execution_mode: str,
|
|
887
|
+
warnings: list[str],
|
|
888
|
+
run_warnings: list[str],
|
|
889
|
+
setup_result: SetupResult | None,
|
|
890
|
+
constitution: Constitution,
|
|
891
|
+
project_mode: str,
|
|
892
|
+
architecture: ArchitectureManifest | None,
|
|
893
|
+
discovery: DiscoveryDocument | None,
|
|
894
|
+
discovery_digest: str,
|
|
895
|
+
discovery_path: Path | None,
|
|
896
|
+
discovery_source: str,
|
|
897
|
+
discovery_created: bool,
|
|
898
|
+
blocked: bool,
|
|
899
|
+
spec_written: bool,
|
|
900
|
+
spec_reused: bool,
|
|
901
|
+
continuation: bool,
|
|
902
|
+
directory: Path,
|
|
903
|
+
) -> SpecWorkflowResult:
|
|
904
|
+
return SpecWorkflowResult(
|
|
905
|
+
spec=spec,
|
|
906
|
+
harness_name=harness_name,
|
|
907
|
+
execution_mode=execution_mode,
|
|
908
|
+
warnings=tuple(warnings),
|
|
909
|
+
run_warnings=tuple(run_warnings),
|
|
910
|
+
constitution_warnings=constitution.warnings,
|
|
911
|
+
setup_result=setup_result,
|
|
912
|
+
constitution_names=constitution.names,
|
|
913
|
+
project_mode=project_mode,
|
|
914
|
+
architecture_digest=(
|
|
915
|
+
architecture.digest if architecture is not None else None
|
|
916
|
+
),
|
|
917
|
+
architecture=architecture,
|
|
918
|
+
discovery=discovery,
|
|
919
|
+
discovery_digest=discovery_digest or None,
|
|
920
|
+
discovery_path=discovery_path,
|
|
921
|
+
discovery_source=discovery_source,
|
|
922
|
+
discovery_created=discovery_created,
|
|
923
|
+
blocked=blocked,
|
|
924
|
+
spec_written=spec_written,
|
|
925
|
+
spec_reused=spec_reused,
|
|
926
|
+
continuation=continuation,
|
|
927
|
+
directory=directory,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
@staticmethod
|
|
931
|
+
def _relative(config: DevFlowConfig, path: Path) -> str:
|
|
932
|
+
return path.relative_to(config.root).as_posix()
|