agentic-devtools 0.2.332__py3-none-any.whl → 0.2.334__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.
- agentic_devtools/_version.py +2 -2
- agentic_devtools/adapters/__init__.py +8 -0
- agentic_devtools/adapters/idempotency_query_provider.py +64 -0
- agentic_devtools/adapters/issue_provider.py +65 -0
- agentic_devtools/adapters/jira_provider.py +140 -75
- agentic_devtools/adapters/operation_plan.py +111 -0
- agentic_devtools/adapters/orchestration_key.py +39 -15
- agentic_devtools/adapters/plan_manifest.py +437 -0
- agentic_devtools/orchestration/graph_builder.py +15 -5
- agentic_devtools/orchestration/nodes/__init__.py +31 -0
- agentic_devtools/orchestration/nodes/_helpers.py +261 -0
- agentic_devtools/orchestration/nodes/checklist_creation.py +139 -0
- agentic_devtools/orchestration/nodes/commit.py +169 -0
- agentic_devtools/orchestration/nodes/completion.py +163 -0
- agentic_devtools/orchestration/nodes/implementation.py +395 -0
- agentic_devtools/orchestration/nodes/implementation_review.py +126 -0
- agentic_devtools/orchestration/nodes/initiate.py +136 -0
- agentic_devtools/orchestration/nodes/planning.py +221 -0
- agentic_devtools/orchestration/nodes/pull_request.py +256 -0
- agentic_devtools/orchestration/nodes/retrieve.py +198 -0
- agentic_devtools/orchestration/nodes/setup.py +123 -0
- agentic_devtools/orchestration/nodes/verification.py +96 -0
- agentic_devtools/orchestration/pilot_workflow.py +53 -21
- agentic_devtools/orchestration/runner.py +62 -9
- agentic_devtools/orchestration/state_schema.py +24 -0
- {agentic_devtools-0.2.332.dist-info → agentic_devtools-0.2.334.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.332.dist-info → agentic_devtools-0.2.334.dist-info}/RECORD +30 -14
- {agentic_devtools-0.2.332.dist-info → agentic_devtools-0.2.334.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.332.dist-info → agentic_devtools-0.2.334.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.332.dist-info → agentic_devtools-0.2.334.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""Manifest planning and execution for dry-run and idempotent issue creation.
|
|
2
|
+
|
|
3
|
+
Provides ``plan_manifest()`` for assembling a dependency-safe ``OperationPlan``
|
|
4
|
+
from a JSON manifest, and ``execute_manifest()`` as a convenience wrapper for
|
|
5
|
+
real execution mode.
|
|
6
|
+
|
|
7
|
+
The manifest describes a tree of issues with parent-child and blocking
|
|
8
|
+
relationships. The orchestrator traverses the manifest in topological order,
|
|
9
|
+
computes orchestration keys for each operation, and either previews (dry-run)
|
|
10
|
+
or executes (real) the operations through an ``IssueProvider``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from agentic_devtools.adapters.cycle_detection import CycleDetectedError, detect_cycles
|
|
18
|
+
from agentic_devtools.adapters.idempotency_query_provider import IdempotencyQueryProvider
|
|
19
|
+
from agentic_devtools.adapters.issue_provider import VALID_ISSUE_TYPES, IssueProvider
|
|
20
|
+
from agentic_devtools.adapters.operation_plan import OperationDescriptor, OperationPlan
|
|
21
|
+
from agentic_devtools.adapters.orchestration_key import embed_orchestration_key, generate_orchestration_key
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def plan_manifest(
|
|
25
|
+
manifest: dict[str, Any],
|
|
26
|
+
provider: IssueProvider,
|
|
27
|
+
*,
|
|
28
|
+
dry_run: bool = True,
|
|
29
|
+
check_existing: bool = False,
|
|
30
|
+
query_provider: IdempotencyQueryProvider | None = None,
|
|
31
|
+
) -> OperationPlan:
|
|
32
|
+
"""Assemble a dependency-safe operation plan from a manifest.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
manifest: JSON manifest with "nodes" list. Each node has "ref",
|
|
36
|
+
"title", "body", "issue_type", and optionally "parent_ref"
|
|
37
|
+
and "blocked_by" (list of refs).
|
|
38
|
+
provider: IssueProvider for dry-run param extraction / real execution.
|
|
39
|
+
dry_run: When True, no mutations. When False, execute operations.
|
|
40
|
+
check_existing: When True (dry-run only), query provider for
|
|
41
|
+
existing entities. Requires query_provider.
|
|
42
|
+
query_provider: IdempotencyQueryProvider for existence checks.
|
|
43
|
+
Required when check_existing=True.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
OperationPlan with ordered OperationDescriptor entries.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If check_existing=True but query_provider is None.
|
|
50
|
+
ValueError: If check_existing=True but dry_run=False.
|
|
51
|
+
ValueError: If check_existing=True but query_provider does not implement
|
|
52
|
+
the IdempotencyQueryProvider protocol.
|
|
53
|
+
ValueError: If manifest is not a dict, manifest['nodes'] is not a list,
|
|
54
|
+
any node is not a dict, a node is missing a required key
|
|
55
|
+
('ref', 'title', 'issue_type'), a required key value is not a
|
|
56
|
+
non-empty string after trimming, issue_type is unsupported,
|
|
57
|
+
'body' is present but is neither a string nor null,
|
|
58
|
+
'blocked_by' or 'labels' is present but not a list,
|
|
59
|
+
or duplicate refs are detected.
|
|
60
|
+
ValueError: If a circular dependency is detected among manifest nodes.
|
|
61
|
+
ValueError: If a node's parent_ref or blocked_by ref cannot be resolved during real execution.
|
|
62
|
+
Any provider error: Propagated from adapter calls (FR-009).
|
|
63
|
+
"""
|
|
64
|
+
if check_existing and query_provider is None:
|
|
65
|
+
raise ValueError("check_existing=True requires a query_provider")
|
|
66
|
+
if check_existing and not dry_run:
|
|
67
|
+
raise ValueError("check_existing=True is only valid with dry_run=True")
|
|
68
|
+
if check_existing and not isinstance(query_provider, IdempotencyQueryProvider):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"query_provider must implement the IdempotencyQueryProvider protocol, got {type(query_provider).__name__}"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
if not isinstance(manifest, dict):
|
|
74
|
+
raise ValueError(f"manifest must be a dict, got {type(manifest).__name__}")
|
|
75
|
+
|
|
76
|
+
nodes = manifest.get("nodes", [])
|
|
77
|
+
|
|
78
|
+
# Validate manifest structure up front for deterministic, actionable errors.
|
|
79
|
+
if not isinstance(nodes, list):
|
|
80
|
+
raise ValueError(f"manifest['nodes'] must be a list, got {type(nodes).__name__}")
|
|
81
|
+
validated_nodes: list[dict[str, Any]] = []
|
|
82
|
+
seen_refs: set[str] = set()
|
|
83
|
+
for i, node in enumerate(nodes):
|
|
84
|
+
if not isinstance(node, dict):
|
|
85
|
+
raise ValueError(f"manifest['nodes'][{i}] must be a dict, got {type(node).__name__}")
|
|
86
|
+
normalized_node = dict(node)
|
|
87
|
+
for key in ("ref", "title", "issue_type"):
|
|
88
|
+
if key not in node:
|
|
89
|
+
raise ValueError(f"manifest['nodes'][{i}] missing required key {key!r}")
|
|
90
|
+
value = node[key]
|
|
91
|
+
if not isinstance(value, str) or not value.strip():
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"manifest['nodes'][{i}][{key!r}] must be a non-empty string, got {type(value).__name__!r}"
|
|
94
|
+
)
|
|
95
|
+
if key == "issue_type":
|
|
96
|
+
normalized_value = value.strip().lower()
|
|
97
|
+
if normalized_value not in VALID_ISSUE_TYPES:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Unsupported issue_type {normalized_value!r}. Valid types: {sorted(VALID_ISSUE_TYPES)}"
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
normalized_value = value.strip()
|
|
103
|
+
normalized_node[key] = normalized_value
|
|
104
|
+
if "body" in node and node["body"] is not None and not isinstance(node["body"], str):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
f"manifest['nodes'][{i}]['body'] must be a string or null, got {type(node['body']).__name__!r}"
|
|
107
|
+
)
|
|
108
|
+
if "parent_ref" in node:
|
|
109
|
+
parent_ref_val = node["parent_ref"]
|
|
110
|
+
if not isinstance(parent_ref_val, str) or not parent_ref_val.strip():
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"manifest['nodes'][{i}]['parent_ref'] must be a non-empty string, "
|
|
113
|
+
f"got {type(parent_ref_val).__name__!r}"
|
|
114
|
+
)
|
|
115
|
+
normalized_node["parent_ref"] = parent_ref_val.strip()
|
|
116
|
+
for list_key in ("blocked_by", "labels"):
|
|
117
|
+
if list_key not in node:
|
|
118
|
+
continue
|
|
119
|
+
value = node[list_key]
|
|
120
|
+
if not isinstance(value, list):
|
|
121
|
+
raise ValueError(f"manifest['nodes'][{i}][{list_key!r}] must be a list, got {type(value).__name__!r}")
|
|
122
|
+
normalized_elems: list[str] = []
|
|
123
|
+
for j, elem in enumerate(value):
|
|
124
|
+
if not isinstance(elem, str) or not elem.strip():
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"manifest['nodes'][{i}][{list_key!r}][{j}] must be a non-empty string, "
|
|
127
|
+
f"got {type(elem).__name__!r}"
|
|
128
|
+
)
|
|
129
|
+
normalized_elems.append(elem.strip())
|
|
130
|
+
normalized_node[list_key] = normalized_elems
|
|
131
|
+
node_ref = normalized_node["ref"]
|
|
132
|
+
if node_ref in seen_refs:
|
|
133
|
+
raise ValueError(f"Duplicate ref {node_ref!r} in manifest['nodes']")
|
|
134
|
+
seen_refs.add(node_ref)
|
|
135
|
+
validated_nodes.append(normalized_node)
|
|
136
|
+
|
|
137
|
+
# Build node lookup and edges for topological sort
|
|
138
|
+
node_map: dict[str, dict[str, Any]] = {}
|
|
139
|
+
for node in validated_nodes:
|
|
140
|
+
node_map[node["ref"]] = node
|
|
141
|
+
|
|
142
|
+
# Determine topological order based on blocked_by relationships
|
|
143
|
+
edges: list[tuple[str, str]] = []
|
|
144
|
+
for node in validated_nodes:
|
|
145
|
+
for blocker_ref in node.get("blocked_by", []):
|
|
146
|
+
# blocker must be created before this node
|
|
147
|
+
edges.append((blocker_ref, node["ref"]))
|
|
148
|
+
|
|
149
|
+
# Also ensure parents are created before children
|
|
150
|
+
for node in validated_nodes:
|
|
151
|
+
parent_ref = node.get("parent_ref")
|
|
152
|
+
if parent_ref:
|
|
153
|
+
edges.append((parent_ref, node["ref"]))
|
|
154
|
+
|
|
155
|
+
if edges:
|
|
156
|
+
try:
|
|
157
|
+
sorted_refs = detect_cycles(edges)
|
|
158
|
+
except CycleDetectedError as exc:
|
|
159
|
+
raise ValueError(str(exc)) from exc
|
|
160
|
+
# Include any nodes not in the edge graph (isolated nodes)
|
|
161
|
+
sorted_refs_set = set(sorted_refs)
|
|
162
|
+
remaining = [r for r in node_map if r not in sorted_refs_set]
|
|
163
|
+
sorted_refs = sorted_refs + remaining
|
|
164
|
+
else:
|
|
165
|
+
sorted_refs = list(node_map.keys())
|
|
166
|
+
|
|
167
|
+
# In real execution, validate all cross-node references before any mutation.
|
|
168
|
+
if not dry_run:
|
|
169
|
+
for ref in sorted_refs:
|
|
170
|
+
if ref not in node_map:
|
|
171
|
+
continue
|
|
172
|
+
node = node_map[ref]
|
|
173
|
+
parent_ref = node.get("parent_ref")
|
|
174
|
+
if parent_ref and parent_ref not in node_map:
|
|
175
|
+
raise ValueError(f"parent_ref {parent_ref!r} for manifest ref {ref!r} could not be resolved")
|
|
176
|
+
for blocker_ref in node.get("blocked_by", []):
|
|
177
|
+
if blocker_ref not in node_map:
|
|
178
|
+
raise ValueError(f"blocked_by ref {blocker_ref!r} for manifest ref {ref!r} could not be resolved")
|
|
179
|
+
|
|
180
|
+
# Phase 1: Generate create operations in topological order
|
|
181
|
+
descriptors: list[OperationDescriptor] = []
|
|
182
|
+
ref_bindings: dict[str, str] = {} # manifest ref → provider identifier
|
|
183
|
+
|
|
184
|
+
for ref in sorted_refs:
|
|
185
|
+
if ref not in node_map:
|
|
186
|
+
continue
|
|
187
|
+
node = node_map[ref]
|
|
188
|
+
orch_key = generate_orchestration_key("create_issue", ref)
|
|
189
|
+
body = node.get("body", "")
|
|
190
|
+
if body is None:
|
|
191
|
+
body = ""
|
|
192
|
+
body_with_key = embed_orchestration_key(body, orch_key)
|
|
193
|
+
|
|
194
|
+
provider_params: dict[str, Any] = {
|
|
195
|
+
"title": node["title"],
|
|
196
|
+
"body": body_with_key,
|
|
197
|
+
"issue_type": node["issue_type"],
|
|
198
|
+
}
|
|
199
|
+
if node.get("parent_ref"):
|
|
200
|
+
provider_params["parent_ref"] = node["parent_ref"]
|
|
201
|
+
if node.get("labels"):
|
|
202
|
+
provider_params["labels"] = node["labels"]
|
|
203
|
+
|
|
204
|
+
if dry_run and not check_existing:
|
|
205
|
+
# Mode 1: planning-only dry-run — assemble descriptors only.
|
|
206
|
+
descriptors.append(
|
|
207
|
+
OperationDescriptor(
|
|
208
|
+
operation_type="create_issue",
|
|
209
|
+
orchestration_key=orch_key,
|
|
210
|
+
refs=(ref,),
|
|
211
|
+
status="dry-run",
|
|
212
|
+
provider_params=provider_params,
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
elif dry_run and check_existing:
|
|
216
|
+
# Mode 2: dry-run with existence checks
|
|
217
|
+
assert query_provider is not None
|
|
218
|
+
existing = query_provider.find_existing_issue(orch_key)
|
|
219
|
+
if existing is not None:
|
|
220
|
+
ref_bindings[ref] = existing.identifier
|
|
221
|
+
descriptors.append(
|
|
222
|
+
OperationDescriptor(
|
|
223
|
+
operation_type="create_issue",
|
|
224
|
+
orchestration_key=orch_key,
|
|
225
|
+
refs=(ref,),
|
|
226
|
+
status="existing",
|
|
227
|
+
provider_params=provider_params,
|
|
228
|
+
result=existing,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
else:
|
|
232
|
+
descriptors.append(
|
|
233
|
+
OperationDescriptor(
|
|
234
|
+
operation_type="create_issue",
|
|
235
|
+
orchestration_key=orch_key,
|
|
236
|
+
refs=(ref,),
|
|
237
|
+
status="dry-run",
|
|
238
|
+
provider_params=provider_params,
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
else:
|
|
242
|
+
# Mode 3: real execution
|
|
243
|
+
parent_id: str | None = None
|
|
244
|
+
if node.get("parent_ref"):
|
|
245
|
+
parent_id = ref_bindings[node["parent_ref"]]
|
|
246
|
+
|
|
247
|
+
result = provider.create_issue(
|
|
248
|
+
title=node["title"],
|
|
249
|
+
body=body_with_key,
|
|
250
|
+
issue_type=node["issue_type"],
|
|
251
|
+
parent_id=parent_id,
|
|
252
|
+
labels=node.get("labels"),
|
|
253
|
+
idempotency_key=orch_key,
|
|
254
|
+
dry_run=False,
|
|
255
|
+
)
|
|
256
|
+
ref_bindings[ref] = result.identifier
|
|
257
|
+
descriptors.append(
|
|
258
|
+
OperationDescriptor(
|
|
259
|
+
operation_type="create_issue",
|
|
260
|
+
orchestration_key=orch_key,
|
|
261
|
+
refs=(ref,),
|
|
262
|
+
status=result.status,
|
|
263
|
+
provider_params=provider_params,
|
|
264
|
+
result=result,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Phase 2: Generate link_subissue operations
|
|
269
|
+
for ref in sorted_refs:
|
|
270
|
+
if ref not in node_map:
|
|
271
|
+
continue
|
|
272
|
+
node = node_map[ref]
|
|
273
|
+
parent_ref = node.get("parent_ref")
|
|
274
|
+
if not parent_ref:
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
orch_key = generate_orchestration_key("link_subissue", parent_ref, ref)
|
|
278
|
+
provider_params = {"parent_ref": parent_ref, "child_ref": ref}
|
|
279
|
+
|
|
280
|
+
if dry_run and not check_existing:
|
|
281
|
+
descriptors.append(
|
|
282
|
+
OperationDescriptor(
|
|
283
|
+
operation_type="link_subissue",
|
|
284
|
+
orchestration_key=orch_key,
|
|
285
|
+
refs=(parent_ref, ref),
|
|
286
|
+
status="dry-run",
|
|
287
|
+
provider_params=provider_params,
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
elif dry_run and check_existing:
|
|
291
|
+
assert query_provider is not None
|
|
292
|
+
parent_id = ref_bindings.get(parent_ref)
|
|
293
|
+
child_id = ref_bindings.get(ref)
|
|
294
|
+
if parent_id and child_id:
|
|
295
|
+
link_existing = query_provider.find_existing_link(parent_id, child_id)
|
|
296
|
+
if link_existing is not None:
|
|
297
|
+
descriptors.append(
|
|
298
|
+
OperationDescriptor(
|
|
299
|
+
operation_type="link_subissue",
|
|
300
|
+
orchestration_key=orch_key,
|
|
301
|
+
refs=(parent_ref, ref),
|
|
302
|
+
status="already-linked",
|
|
303
|
+
provider_params=provider_params,
|
|
304
|
+
result=link_existing,
|
|
305
|
+
)
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
descriptors.append(
|
|
309
|
+
OperationDescriptor(
|
|
310
|
+
operation_type="link_subissue",
|
|
311
|
+
orchestration_key=orch_key,
|
|
312
|
+
refs=(parent_ref, ref),
|
|
313
|
+
status="dry-run",
|
|
314
|
+
provider_params=provider_params,
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
descriptors.append(
|
|
319
|
+
OperationDescriptor(
|
|
320
|
+
operation_type="link_subissue",
|
|
321
|
+
orchestration_key=orch_key,
|
|
322
|
+
refs=(parent_ref, ref),
|
|
323
|
+
status="dry-run",
|
|
324
|
+
provider_params=provider_params,
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
else:
|
|
328
|
+
# Real execution
|
|
329
|
+
parent_id = ref_bindings[parent_ref]
|
|
330
|
+
child_id = ref_bindings[ref]
|
|
331
|
+
link_result = provider.link_subissue(parent_id, child_id, dry_run=False)
|
|
332
|
+
descriptors.append(
|
|
333
|
+
OperationDescriptor(
|
|
334
|
+
operation_type="link_subissue",
|
|
335
|
+
orchestration_key=orch_key,
|
|
336
|
+
refs=(parent_ref, ref),
|
|
337
|
+
status=link_result.status,
|
|
338
|
+
provider_params=provider_params,
|
|
339
|
+
result=link_result,
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Phase 3: Generate add_blocked_by operations
|
|
344
|
+
for ref in sorted_refs:
|
|
345
|
+
if ref not in node_map:
|
|
346
|
+
continue
|
|
347
|
+
node = node_map[ref]
|
|
348
|
+
for blocker_ref in node.get("blocked_by", []):
|
|
349
|
+
orch_key = generate_orchestration_key("add_blocked_by", ref, blocker_ref)
|
|
350
|
+
provider_params = {"issue_ref": ref, "blocked_by_ref": blocker_ref}
|
|
351
|
+
|
|
352
|
+
if dry_run and not check_existing:
|
|
353
|
+
descriptors.append(
|
|
354
|
+
OperationDescriptor(
|
|
355
|
+
operation_type="add_blocked_by",
|
|
356
|
+
orchestration_key=orch_key,
|
|
357
|
+
refs=(ref, blocker_ref),
|
|
358
|
+
status="dry-run",
|
|
359
|
+
provider_params=provider_params,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
elif dry_run and check_existing:
|
|
363
|
+
assert query_provider is not None
|
|
364
|
+
issue_id = ref_bindings.get(ref)
|
|
365
|
+
blocker_id = ref_bindings.get(blocker_ref)
|
|
366
|
+
if issue_id and blocker_id:
|
|
367
|
+
dep_existing = query_provider.find_existing_dependency(issue_id, blocker_id)
|
|
368
|
+
if dep_existing is not None:
|
|
369
|
+
descriptors.append(
|
|
370
|
+
OperationDescriptor(
|
|
371
|
+
operation_type="add_blocked_by",
|
|
372
|
+
orchestration_key=orch_key,
|
|
373
|
+
refs=(ref, blocker_ref),
|
|
374
|
+
status="already-linked",
|
|
375
|
+
provider_params=provider_params,
|
|
376
|
+
result=dep_existing,
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
descriptors.append(
|
|
381
|
+
OperationDescriptor(
|
|
382
|
+
operation_type="add_blocked_by",
|
|
383
|
+
orchestration_key=orch_key,
|
|
384
|
+
refs=(ref, blocker_ref),
|
|
385
|
+
status="dry-run",
|
|
386
|
+
provider_params=provider_params,
|
|
387
|
+
)
|
|
388
|
+
)
|
|
389
|
+
else:
|
|
390
|
+
descriptors.append(
|
|
391
|
+
OperationDescriptor(
|
|
392
|
+
operation_type="add_blocked_by",
|
|
393
|
+
orchestration_key=orch_key,
|
|
394
|
+
refs=(ref, blocker_ref),
|
|
395
|
+
status="dry-run",
|
|
396
|
+
provider_params=provider_params,
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
# Real execution
|
|
401
|
+
issue_id = ref_bindings[ref]
|
|
402
|
+
blocker_id = ref_bindings[blocker_ref]
|
|
403
|
+
dep_result = provider.add_blocked_by(issue_id, blocker_id, dry_run=False)
|
|
404
|
+
descriptors.append(
|
|
405
|
+
OperationDescriptor(
|
|
406
|
+
operation_type="add_blocked_by",
|
|
407
|
+
orchestration_key=orch_key,
|
|
408
|
+
refs=(ref, blocker_ref),
|
|
409
|
+
status=dep_result.status,
|
|
410
|
+
provider_params=provider_params,
|
|
411
|
+
result=dep_result,
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
return OperationPlan(
|
|
416
|
+
operations=tuple(descriptors),
|
|
417
|
+
dry_run=dry_run,
|
|
418
|
+
check_existing=check_existing,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def execute_manifest(
|
|
423
|
+
manifest: dict[str, Any],
|
|
424
|
+
provider: IssueProvider,
|
|
425
|
+
) -> OperationPlan:
|
|
426
|
+
"""Execute a manifest against a provider (real mutations).
|
|
427
|
+
|
|
428
|
+
Convenience wrapper for ``plan_manifest(dry_run=False)``.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
manifest: JSON manifest with nodes and edges.
|
|
432
|
+
provider: IssueProvider for real execution.
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
OperationPlan with results from the executed operations.
|
|
436
|
+
"""
|
|
437
|
+
return plan_manifest(manifest, provider, dry_run=False)
|
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
from langgraph.graph import END, StateGraph
|
|
4
4
|
from langgraph.graph.state import CompiledStateGraph
|
|
5
5
|
|
|
6
|
-
from .
|
|
6
|
+
from .nodes import (
|
|
7
7
|
checklist_creation_node,
|
|
8
8
|
commit_node,
|
|
9
9
|
completion_node,
|
|
10
|
-
error_handler_node,
|
|
11
10
|
implementation_node,
|
|
12
11
|
implementation_review_node,
|
|
13
12
|
initiate_node,
|
|
14
13
|
planning_node,
|
|
15
14
|
pull_request_node,
|
|
15
|
+
retrieve_node,
|
|
16
|
+
setup_node,
|
|
17
|
+
verification_node,
|
|
18
|
+
)
|
|
19
|
+
from .pilot_workflow import (
|
|
20
|
+
error_handler_node,
|
|
16
21
|
route_after_checklist_creation,
|
|
17
22
|
route_after_commit,
|
|
18
23
|
route_after_implementation,
|
|
@@ -20,10 +25,9 @@ from .pilot_workflow import (
|
|
|
20
25
|
route_after_initiate,
|
|
21
26
|
route_after_plan,
|
|
22
27
|
route_after_pull_request,
|
|
28
|
+
route_after_retrieve,
|
|
23
29
|
route_after_setup,
|
|
24
30
|
route_after_verify,
|
|
25
|
-
setup_node,
|
|
26
|
-
verification_node,
|
|
27
31
|
)
|
|
28
32
|
from .state_schema import WorkOnIssueState
|
|
29
33
|
|
|
@@ -44,6 +48,7 @@ def build_work_on_issue_graph(checkpointer=None) -> CompiledStateGraph:
|
|
|
44
48
|
# -- nodes ---------------------------------------------------------------
|
|
45
49
|
graph.add_node("initiate", initiate_node)
|
|
46
50
|
graph.add_node("setup", setup_node)
|
|
51
|
+
graph.add_node("retrieve", retrieve_node)
|
|
47
52
|
graph.add_node("planning", planning_node)
|
|
48
53
|
graph.add_node("checklist_creation", checklist_creation_node)
|
|
49
54
|
graph.add_node("implementation", implementation_node)
|
|
@@ -61,11 +66,16 @@ def build_work_on_issue_graph(checkpointer=None) -> CompiledStateGraph:
|
|
|
61
66
|
graph.add_conditional_edges(
|
|
62
67
|
"initiate",
|
|
63
68
|
route_after_initiate,
|
|
64
|
-
{"setup": "setup", "
|
|
69
|
+
{"setup": "setup", "error_handler": "error_handler"},
|
|
65
70
|
)
|
|
66
71
|
graph.add_conditional_edges(
|
|
67
72
|
"setup",
|
|
68
73
|
route_after_setup,
|
|
74
|
+
{"retrieve": "retrieve", "error_handler": "error_handler"},
|
|
75
|
+
)
|
|
76
|
+
graph.add_conditional_edges(
|
|
77
|
+
"retrieve",
|
|
78
|
+
route_after_retrieve,
|
|
69
79
|
{"planning": "planning", "error_handler": "error_handler"},
|
|
70
80
|
)
|
|
71
81
|
graph.add_conditional_edges(
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Real node implementations for the work-on-issue LangGraph workflow.
|
|
2
|
+
|
|
3
|
+
Re-exports all node functions so they can be imported from a single location:
|
|
4
|
+
``from agentic_devtools.orchestration.nodes import initiate_node, ...``
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from agentic_devtools.orchestration.nodes.checklist_creation import checklist_creation_node
|
|
8
|
+
from agentic_devtools.orchestration.nodes.commit import commit_node
|
|
9
|
+
from agentic_devtools.orchestration.nodes.completion import completion_node
|
|
10
|
+
from agentic_devtools.orchestration.nodes.implementation import implementation_node
|
|
11
|
+
from agentic_devtools.orchestration.nodes.implementation_review import implementation_review_node
|
|
12
|
+
from agentic_devtools.orchestration.nodes.initiate import initiate_node
|
|
13
|
+
from agentic_devtools.orchestration.nodes.planning import planning_node
|
|
14
|
+
from agentic_devtools.orchestration.nodes.pull_request import pull_request_node
|
|
15
|
+
from agentic_devtools.orchestration.nodes.retrieve import retrieve_node
|
|
16
|
+
from agentic_devtools.orchestration.nodes.setup import setup_node
|
|
17
|
+
from agentic_devtools.orchestration.nodes.verification import verification_node
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"checklist_creation_node",
|
|
21
|
+
"commit_node",
|
|
22
|
+
"completion_node",
|
|
23
|
+
"implementation_node",
|
|
24
|
+
"implementation_review_node",
|
|
25
|
+
"initiate_node",
|
|
26
|
+
"planning_node",
|
|
27
|
+
"pull_request_node",
|
|
28
|
+
"retrieve_node",
|
|
29
|
+
"setup_node",
|
|
30
|
+
"verification_node",
|
|
31
|
+
]
|