agentic-devtools 0.2.152__py3-none-any.whl → 0.2.154__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/cli/copilot/auto_start.py +100 -22
- agentic_devtools/cli/copilot/session.py +16 -4
- agentic_devtools/cli/workflows/commands.py +11 -0
- agentic_devtools/cli/workflows/worktree_setup.py +222 -9
- {agentic_devtools-0.2.152.dist-info → agentic_devtools-0.2.154.dist-info}/METADATA +1 -1
- {agentic_devtools-0.2.152.dist-info → agentic_devtools-0.2.154.dist-info}/RECORD +10 -10
- {agentic_devtools-0.2.152.dist-info → agentic_devtools-0.2.154.dist-info}/WHEEL +0 -0
- {agentic_devtools-0.2.152.dist-info → agentic_devtools-0.2.154.dist-info}/entry_points.txt +0 -0
- {agentic_devtools-0.2.152.dist-info → agentic_devtools-0.2.154.dist-info}/licenses/LICENSE +0 -0
agentic_devtools/_version.py
CHANGED
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.2.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 2,
|
|
21
|
+
__version__ = version = '0.2.154'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 154)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -49,11 +49,20 @@ _STATE_KEY = "copilot"
|
|
|
49
49
|
_TRIGGERED_RUNS_KEY = "auto_start_triggered_runs"
|
|
50
50
|
_MODEL_KEY = "model_id"
|
|
51
51
|
|
|
52
|
-
# Retry constants for transient Windows file-lock errors (WinError 32)
|
|
52
|
+
# Retry constants for transient Windows file-lock errors (WinError 32) and
|
|
53
|
+
# rapid non-zero exit codes (batch wrapper fails because copilot.ps1 is locked).
|
|
53
54
|
_RETRY_MAX_ATTEMPTS = 5 # retries (6 total tries)
|
|
54
55
|
_RETRY_INITIAL_DELAY_S = 0.5 # first backoff delay in seconds
|
|
55
56
|
_RETRY_BACKOFF_FACTOR = 2.0 # doubling
|
|
56
57
|
_RETRY_MAX_DELAY_S = 4.0 # cap per delay in seconds
|
|
58
|
+
_RAPID_FAILURE_THRESHOLD_S = 5.0 # exit within this = transient failure
|
|
59
|
+
|
|
60
|
+
# Cleanup retry constants for transient Windows delete/write locks.
|
|
61
|
+
_CLEANUP_RETRYABLE_WINERRORS = {5, 32, 110}
|
|
62
|
+
_CLEANUP_MAX_RETRIES = 3
|
|
63
|
+
_CLEANUP_INITIAL_DELAY_S = 0.1
|
|
64
|
+
_CLEANUP_BACKOFF_FACTOR = 2.0
|
|
65
|
+
_CLEANUP_MAX_DELAY_S = 0.8
|
|
57
66
|
|
|
58
67
|
|
|
59
68
|
def _read_model_from_state(state_file_path: Path) -> str | None:
|
|
@@ -200,14 +209,35 @@ def _is_retryable_win_error(exc: OSError) -> bool:
|
|
|
200
209
|
return getattr(exc, "winerror", None) == 32
|
|
201
210
|
|
|
202
211
|
|
|
212
|
+
def _is_retryable_cleanup_win_error(exc: OSError) -> bool:
|
|
213
|
+
"""Return ``True`` for transient Windows cleanup lock errors.
|
|
214
|
+
|
|
215
|
+
Covers common transient lock/access-denied cleanup failures:
|
|
216
|
+
- 5 (``ERROR_ACCESS_DENIED``)
|
|
217
|
+
- 32 (``ERROR_SHARING_VIOLATION``)
|
|
218
|
+
- 110 (``ERROR_OPEN_FAILED``)
|
|
219
|
+
"""
|
|
220
|
+
return getattr(exc, "winerror", None) in _CLEANUP_RETRYABLE_WINERRORS
|
|
221
|
+
|
|
222
|
+
|
|
203
223
|
def _run_copilot_with_retry(
|
|
204
224
|
copilot_args: list[str],
|
|
205
225
|
cwd: str,
|
|
206
226
|
) -> subprocess.CompletedProcess:
|
|
207
227
|
"""Run the Copilot CLI with retry logic for transient Windows file locks.
|
|
208
228
|
|
|
209
|
-
Wraps ``subprocess.run`` with exponential backoff retries
|
|
210
|
-
|
|
229
|
+
Wraps ``subprocess.run`` with exponential backoff retries for two failure
|
|
230
|
+
modes:
|
|
231
|
+
|
|
232
|
+
1. ``OSError`` with ``winerror == 32`` (Windows ``ERROR_SHARING_VIOLATION``)
|
|
233
|
+
raised directly by ``subprocess.run`` when the binary itself is locked.
|
|
234
|
+
2. Rapid non-zero exit codes from Windows batch wrappers (process exits within
|
|
235
|
+
``_RAPID_FAILURE_THRESHOLD_S`` seconds). This handles the case where
|
|
236
|
+
a ``.bat`` wrapper starts successfully but the underlying script
|
|
237
|
+
(``copilot.ps1``) is locked by another process (e.g., the VS Code
|
|
238
|
+
Copilot Chat extension during activation). The batch exits non-zero
|
|
239
|
+
almost immediately, which is distinguishable from a legitimate session
|
|
240
|
+
failure (which would take several seconds at minimum).
|
|
211
241
|
|
|
212
242
|
Non-retryable errors (``FileNotFoundError``, other ``OSError`` variants)
|
|
213
243
|
are re-raised immediately without retry.
|
|
@@ -217,7 +247,9 @@ def _run_copilot_with_retry(
|
|
|
217
247
|
cwd: Working directory for the subprocess.
|
|
218
248
|
|
|
219
249
|
Returns:
|
|
220
|
-
The ``CompletedProcess``
|
|
250
|
+
The ``CompletedProcess`` result from the final attempt. This may have
|
|
251
|
+
a non-zero ``returncode`` when retries are exhausted for rapid
|
|
252
|
+
non-zero exits from Windows batch wrappers.
|
|
221
253
|
|
|
222
254
|
Raises:
|
|
223
255
|
FileNotFoundError: If the binary or cwd is missing (not retried).
|
|
@@ -232,7 +264,36 @@ def _run_copilot_with_retry(
|
|
|
232
264
|
|
|
233
265
|
for attempt in range(1, total_tries + 1):
|
|
234
266
|
try:
|
|
235
|
-
|
|
267
|
+
t0 = time.monotonic()
|
|
268
|
+
result = subprocess.run(copilot_args, cwd=cwd) # noqa: S603
|
|
269
|
+
elapsed = time.monotonic() - t0
|
|
270
|
+
|
|
271
|
+
command_name = copilot_args[0].lower() if copilot_args else ""
|
|
272
|
+
is_batch_wrapper = command_name.endswith((".bat", ".cmd"))
|
|
273
|
+
# A rapid non-zero exit is only treated as transient for Windows
|
|
274
|
+
# batch wrappers, where .bat -> .ps1 handoff can fail while
|
|
275
|
+
# copilot.ps1 is temporarily locked by VS Code's extension.
|
|
276
|
+
if result.returncode != 0 and elapsed < _RAPID_FAILURE_THRESHOLD_S and is_batch_wrapper:
|
|
277
|
+
if attempt == total_tries:
|
|
278
|
+
print(
|
|
279
|
+
f"agdt-copilot-auto-start: error: retry budget exhausted "
|
|
280
|
+
f"after {total_tries} attempts (rapid exit code "
|
|
281
|
+
f"{result.returncode} in {elapsed:.1f}s)",
|
|
282
|
+
file=sys.stderr,
|
|
283
|
+
)
|
|
284
|
+
return result
|
|
285
|
+
print(
|
|
286
|
+
f"agdt-copilot-auto-start: warning: copilot exited rapidly "
|
|
287
|
+
f"(code={result.returncode}, {elapsed:.1f}s < {_RAPID_FAILURE_THRESHOLD_S}s, "
|
|
288
|
+
f"attempt {attempt}/{total_tries}), "
|
|
289
|
+
f"retrying in {delay:.1f}s (likely transient batch-wrapper file lock)",
|
|
290
|
+
file=sys.stderr,
|
|
291
|
+
)
|
|
292
|
+
time.sleep(delay)
|
|
293
|
+
delay = min(delay * _RETRY_BACKOFF_FACTOR, _RETRY_MAX_DELAY_S)
|
|
294
|
+
continue
|
|
295
|
+
|
|
296
|
+
return result
|
|
236
297
|
except FileNotFoundError:
|
|
237
298
|
raise
|
|
238
299
|
except OSError as exc:
|
|
@@ -272,8 +333,9 @@ def _cleanup_auto_start_task(
|
|
|
272
333
|
Delegates to :func:`~agentic_devtools.cli.vscode_tasks.remove_auto_start_task`.
|
|
273
334
|
|
|
274
335
|
Errors are caught so that cleanup failure never changes the caller's exit
|
|
275
|
-
code.
|
|
276
|
-
|
|
336
|
+
code. Transient Windows cleanup lock errors (``winerror`` in ``[5, 32, 110]``)
|
|
337
|
+
are retried up to 3 times with exponential backoff; all failures are
|
|
338
|
+
otherwise silently ignored.
|
|
277
339
|
|
|
278
340
|
Args:
|
|
279
341
|
worktree_path: Absolute path to the worktree directory.
|
|
@@ -288,25 +350,41 @@ def _cleanup_auto_start_task(
|
|
|
288
350
|
"""
|
|
289
351
|
vscode_dir = os.path.join(worktree_path, ".vscode")
|
|
290
352
|
tasks_path = os.path.join(vscode_dir, "tasks.json")
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
353
|
+
if _CLEANUP_MAX_RETRIES < 0:
|
|
354
|
+
return
|
|
355
|
+
attempts = _CLEANUP_MAX_RETRIES + 1
|
|
356
|
+
delay = _CLEANUP_INITIAL_DELAY_S
|
|
357
|
+
attempt = 1
|
|
358
|
+
while attempt <= attempts: # pragma: no branch
|
|
359
|
+
try:
|
|
360
|
+
remove_auto_start_task(
|
|
361
|
+
tasks_path,
|
|
362
|
+
vscode_dir,
|
|
363
|
+
task_label,
|
|
364
|
+
delete_if_empty=created_new,
|
|
365
|
+
)
|
|
366
|
+
return
|
|
367
|
+
except OSError as exc:
|
|
368
|
+
if not _is_retryable_cleanup_win_error(exc) or attempt == attempts:
|
|
369
|
+
# Best-effort cleanup: any failure must not affect caller exit code.
|
|
370
|
+
return
|
|
300
371
|
print(
|
|
301
372
|
f"agdt-copilot-auto-start: warning: cleanup encountered transient "
|
|
302
|
-
f"file lock (
|
|
373
|
+
f"file lock (attempt {attempt}/{attempts}, "
|
|
374
|
+
f"winerror={getattr(exc, 'winerror', '?')}), "
|
|
375
|
+
f"retrying in {delay:.1f}s: {exc}",
|
|
303
376
|
file=sys.stderr,
|
|
304
377
|
)
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
378
|
+
try:
|
|
379
|
+
time.sleep(delay)
|
|
380
|
+
except KeyboardInterrupt:
|
|
381
|
+
return
|
|
382
|
+
delay = min(delay * _CLEANUP_BACKOFF_FACTOR, _CLEANUP_MAX_DELAY_S)
|
|
383
|
+
attempt += 1
|
|
384
|
+
except Exception:
|
|
385
|
+
# Best-effort cleanup: any failure must be silently ignored so it cannot
|
|
386
|
+
# affect the caller's exit code.
|
|
387
|
+
return
|
|
310
388
|
|
|
311
389
|
|
|
312
390
|
def copilot_auto_start_cmd(argv: list[str] | None = None) -> None:
|
|
@@ -156,6 +156,8 @@ class CopilotSessionResult:
|
|
|
156
156
|
when this object is returned).
|
|
157
157
|
process: The :class:`subprocess.Popen` handle for non-interactive
|
|
158
158
|
sessions; ``None`` for interactive sessions.
|
|
159
|
+
log_file: Absolute path to the session log file for non-interactive
|
|
160
|
+
sessions; ``None`` for interactive sessions.
|
|
159
161
|
"""
|
|
160
162
|
|
|
161
163
|
session_id: str
|
|
@@ -164,6 +166,7 @@ class CopilotSessionResult:
|
|
|
164
166
|
start_time: str
|
|
165
167
|
pid: int | None = field(default=None)
|
|
166
168
|
process: subprocess.Popen | None = field(default=None, repr=False) # type: ignore[type-arg]
|
|
169
|
+
log_file: str | None = field(default=None)
|
|
167
170
|
|
|
168
171
|
|
|
169
172
|
# ---------------------------------------------------------------------------
|
|
@@ -175,17 +178,25 @@ def _get_copilot_binary() -> str | None:
|
|
|
175
178
|
"""Return the path to the copilot binary, or ``None`` if not found.
|
|
176
179
|
|
|
177
180
|
Checks (in order):
|
|
178
|
-
1. The
|
|
179
|
-
2. The
|
|
181
|
+
1. The managed install at ``~/.agdt/bin/copilot[.exe]``.
|
|
182
|
+
2. The standalone ``copilot`` binary on the system ``PATH``.
|
|
183
|
+
|
|
184
|
+
The managed install is preferred because it is a direct executable that
|
|
185
|
+
avoids the ``.bat`` → ``copilot.ps1`` indirection used by the VS Code
|
|
186
|
+
Copilot Chat extension's bundled CLI. On Windows, that ``.ps1`` file can
|
|
187
|
+
be temporarily locked by the extension during activation, causing transient
|
|
188
|
+
``ERROR_SHARING_VIOLATION`` failures that are invisible to
|
|
189
|
+
``subprocess.run`` (they manifest as non-zero exit codes from the batch
|
|
190
|
+
wrapper rather than ``OSError``).
|
|
180
191
|
|
|
181
192
|
Returns:
|
|
182
193
|
Absolute path string when found, ``None`` otherwise.
|
|
183
194
|
"""
|
|
195
|
+
if _MANAGED_COPILOT.is_file():
|
|
196
|
+
return str(_MANAGED_COPILOT)
|
|
184
197
|
system_path = shutil.which("copilot")
|
|
185
198
|
if system_path:
|
|
186
199
|
return system_path
|
|
187
|
-
if _MANAGED_COPILOT.is_file():
|
|
188
|
-
return str(_MANAGED_COPILOT)
|
|
189
200
|
return None
|
|
190
201
|
|
|
191
202
|
|
|
@@ -921,6 +932,7 @@ def start_copilot_session(
|
|
|
921
932
|
start_time=start_time,
|
|
922
933
|
pid=process.pid,
|
|
923
934
|
process=process,
|
|
935
|
+
log_file=str(log_file_path),
|
|
924
936
|
)
|
|
925
937
|
|
|
926
938
|
_persist_session_state(result, model=model)
|
|
@@ -394,6 +394,17 @@ Examples:
|
|
|
394
394
|
clear_state_for_workflow_initiation(preserve_run_id=bool(skip_copilot_session))
|
|
395
395
|
set_value("copilot.model_id", model)
|
|
396
396
|
|
|
397
|
+
# Remove any stale VS Code auto-start task from a previous workflow to
|
|
398
|
+
# prevent a duplicate Copilot session from firing on folderOpen/reload.
|
|
399
|
+
# This is belt-and-suspenders alongside the cleanup in
|
|
400
|
+
# _start_copilot_session_for_workflow — the earlier we remove the stale
|
|
401
|
+
# task, the smaller the window for a race where VS Code reloads and fires
|
|
402
|
+
# the old task before we start the new session.
|
|
403
|
+
repo_root_early = get_git_repo_root() or os.getcwd()
|
|
404
|
+
from .worktree_setup import _cleanup_stale_auto_start_task_for_worktree
|
|
405
|
+
|
|
406
|
+
_cleanup_stale_auto_start_task_for_worktree(repo_root_early)
|
|
407
|
+
|
|
397
408
|
# Set provided values in state using the NORMALIZED identifiers. When both issue_key
|
|
398
409
|
# and pull_request_id are provided, write jira.issue_key FIRST so that the engine-side
|
|
399
410
|
# priority guard in set_value() (which checks the loaded state dict) sees the issue key
|
|
@@ -14,6 +14,7 @@ import platform
|
|
|
14
14
|
import shutil
|
|
15
15
|
import subprocess
|
|
16
16
|
import sys
|
|
17
|
+
import threading
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from pathlib import Path
|
|
19
20
|
|
|
@@ -1455,6 +1456,27 @@ def _remove_stale_auto_start_task(
|
|
|
1455
1456
|
remove_auto_start_task(tasks_path, vscode_dir, task_label, delete_if_empty=delete_if_empty)
|
|
1456
1457
|
|
|
1457
1458
|
|
|
1459
|
+
def _cleanup_stale_auto_start_task_for_worktree(worktree_path: str) -> None:
|
|
1460
|
+
"""Remove any stale auto-start task from a worktree's ``.vscode/tasks.json``.
|
|
1461
|
+
|
|
1462
|
+
Best-effort helper that derives the paths from *worktree_path* and
|
|
1463
|
+
delegates to :func:`_remove_stale_auto_start_task`. Called before
|
|
1464
|
+
starting a new Copilot session to prevent a leftover ``runOn: folderOpen``
|
|
1465
|
+
task from a previous workflow from spawning a duplicate session when
|
|
1466
|
+
VS Code reloads.
|
|
1467
|
+
|
|
1468
|
+
All errors are silently caught so this never prevents the caller from
|
|
1469
|
+
proceeding.
|
|
1470
|
+
"""
|
|
1471
|
+
try:
|
|
1472
|
+
vscode_dir = os.path.join(worktree_path, ".vscode")
|
|
1473
|
+
tasks_path = os.path.join(vscode_dir, "tasks.json")
|
|
1474
|
+
if os.path.isfile(tasks_path):
|
|
1475
|
+
_remove_stale_auto_start_task(tasks_path, vscode_dir, _AUTO_START_TASK_LABEL)
|
|
1476
|
+
except Exception:
|
|
1477
|
+
pass
|
|
1478
|
+
|
|
1479
|
+
|
|
1458
1480
|
class WorktreeStateContext:
|
|
1459
1481
|
"""Context manager for cross-worktree state resolution.
|
|
1460
1482
|
|
|
@@ -2347,18 +2369,26 @@ def _start_copilot_session_for_workflow(
|
|
|
2347
2369
|
# where stdin/stdout are redirected to DEVNULL/log files).
|
|
2348
2370
|
has_tty = getattr(sys.stdin, "isatty", lambda: False)() and getattr(sys.stdout, "isatty", lambda: False)()
|
|
2349
2371
|
|
|
2350
|
-
# --- Auto-start injection confirmed:
|
|
2372
|
+
# --- Auto-start injection confirmed: delayed verification fallback ------
|
|
2351
2373
|
# When the caller confirms the auto-start task was successfully injected
|
|
2352
|
-
# into tasks.json, trust that VS Code will execute it
|
|
2353
|
-
#
|
|
2354
|
-
#
|
|
2355
|
-
#
|
|
2356
|
-
#
|
|
2374
|
+
# into tasks.json, we trust that VS Code *will probably* execute it.
|
|
2375
|
+
# However, the task can still fail (e.g., copilot binary locked, permission
|
|
2376
|
+
# dialog dismissed, VS Code crash). Instead of blindly returning True,
|
|
2377
|
+
# we spawn a non-daemon thread that waits for the run ID to appear in state
|
|
2378
|
+
# (proving the task ran). If it doesn't appear within the grace period,
|
|
2379
|
+
# the thread starts a non-interactive background session as a safety net.
|
|
2357
2380
|
if autostart_injected and not has_tty:
|
|
2358
2381
|
print(
|
|
2359
2382
|
"\n--- VS Code auto-start task was successfully injected. "
|
|
2360
|
-
"
|
|
2361
|
-
"
|
|
2383
|
+
f"A delayed verification (up to {_AUTOSTART_VERIFICATION_DELAY_S:.0f}s) "
|
|
2384
|
+
"will confirm it ran and may keep this task running; if not, a "
|
|
2385
|
+
"background fallback session will be started automatically. ---"
|
|
2386
|
+
)
|
|
2387
|
+
_spawn_delayed_autostart_verification(
|
|
2388
|
+
worktree_path=worktree_path,
|
|
2389
|
+
start_prompt=start_prompt,
|
|
2390
|
+
workflow_name=workflow_name,
|
|
2391
|
+
model=model,
|
|
2362
2392
|
)
|
|
2363
2393
|
return True
|
|
2364
2394
|
|
|
@@ -2460,6 +2490,12 @@ def _start_copilot_session_for_workflow(
|
|
|
2460
2490
|
|
|
2461
2491
|
effective_interactive = interactive and is_vscode_available() and has_tty
|
|
2462
2492
|
|
|
2493
|
+
# Remove any stale auto-start task from tasks.json BEFORE starting a new
|
|
2494
|
+
# session. A leftover runOn:folderOpen task from a previous workflow
|
|
2495
|
+
# invocation can fire when VS Code reloads, causing a duplicate Copilot
|
|
2496
|
+
# session alongside the one we are about to start. See #1742.
|
|
2497
|
+
_cleanup_stale_auto_start_task_for_worktree(worktree_path)
|
|
2498
|
+
|
|
2463
2499
|
print(
|
|
2464
2500
|
f"\n--- Starting gh copilot session for {workflow_name} "
|
|
2465
2501
|
f"(mode: {'interactive' if effective_interactive else 'non-interactive'}) ---"
|
|
@@ -2475,12 +2511,24 @@ def _start_copilot_session_for_workflow(
|
|
|
2475
2511
|
|
|
2476
2512
|
state_dir = get_state_dir()
|
|
2477
2513
|
os.environ["AGENTIC_DEVTOOLS_STATE_DIR"] = str(state_dir)
|
|
2478
|
-
start_copilot_session(
|
|
2514
|
+
session_result = start_copilot_session(
|
|
2479
2515
|
prompt=start_prompt,
|
|
2480
2516
|
working_directory=worktree_path,
|
|
2481
2517
|
interactive=effective_interactive,
|
|
2482
2518
|
model=model,
|
|
2483
2519
|
)
|
|
2520
|
+
# Open the log file in VS Code for non-interactive sessions so the
|
|
2521
|
+
# user can watch Copilot output in real time. Skip in CI (no VS Code
|
|
2522
|
+
# or no TTY on the *original* caller — here we check is_vscode_available
|
|
2523
|
+
# which is False in headless CI).
|
|
2524
|
+
if (
|
|
2525
|
+
not effective_interactive
|
|
2526
|
+
and session_result is not None
|
|
2527
|
+
and session_result.log_file
|
|
2528
|
+
and is_vscode_available()
|
|
2529
|
+
and not _in_test_environment()
|
|
2530
|
+
):
|
|
2531
|
+
_open_log_in_vscode(session_result.log_file, worktree_path)
|
|
2484
2532
|
return True
|
|
2485
2533
|
|
|
2486
2534
|
|
|
@@ -2758,6 +2806,128 @@ def _start_copilot_session_for_update_jira_issue(
|
|
|
2758
2806
|
_PENDING_AUTO_START_FILENAME = "pending-auto-start.json"
|
|
2759
2807
|
_FOCUS_SETTLE_SECONDS: float = 2.0
|
|
2760
2808
|
|
|
2809
|
+
# Grace period (seconds) for the delayed auto-start verification.
|
|
2810
|
+
# VS Code may show a workspace trust or task permission dialog before
|
|
2811
|
+
# executing runOn:folderOpen, so the window must be generous.
|
|
2812
|
+
_AUTOSTART_VERIFICATION_DELAY_S: float = 45.0
|
|
2813
|
+
_AUTOSTART_VERIFICATION_POLL_S: float = 3.0
|
|
2814
|
+
|
|
2815
|
+
|
|
2816
|
+
def _spawn_delayed_autostart_verification(
|
|
2817
|
+
worktree_path: str,
|
|
2818
|
+
start_prompt: str,
|
|
2819
|
+
workflow_name: str,
|
|
2820
|
+
model: str | None = None,
|
|
2821
|
+
) -> None:
|
|
2822
|
+
"""Spawn a non-daemon thread that verifies the VS Code auto-start task ran.
|
|
2823
|
+
|
|
2824
|
+
After a grace period, the thread checks whether the auto-start task
|
|
2825
|
+
wrote its run ID into ``copilot.auto_start_triggered_runs``. If not,
|
|
2826
|
+
it starts a non-interactive background Copilot session as a fallback
|
|
2827
|
+
and opens the log file in VS Code (when available).
|
|
2828
|
+
|
|
2829
|
+
The thread is non-daemon so it keeps the parent process alive until
|
|
2830
|
+
verification completes (at most ``_AUTOSTART_VERIFICATION_DELAY_S``
|
|
2831
|
+
seconds). If a fallback session is started, the session's non-daemon
|
|
2832
|
+
tee-thread keeps the process alive for the duration of the Copilot
|
|
2833
|
+
subprocess.
|
|
2834
|
+
|
|
2835
|
+
**Safety guard**: This function is a no-op inside test environments
|
|
2836
|
+
(``PYTEST_CURRENT_TEST`` set) to prevent threads from outliving test
|
|
2837
|
+
cases and accidentally spawning real Copilot sessions.
|
|
2838
|
+
"""
|
|
2839
|
+
if _in_test_environment():
|
|
2840
|
+
return
|
|
2841
|
+
|
|
2842
|
+
state_file_path, state_run_id = _resolve_state_context_in_worktree(worktree_path, include_run_id=True)
|
|
2843
|
+
|
|
2844
|
+
def _verify_and_fallback() -> None:
|
|
2845
|
+
import time
|
|
2846
|
+
|
|
2847
|
+
from agentic_devtools.cli.copilot.auto_start import _is_run_triggered
|
|
2848
|
+
|
|
2849
|
+
# Prefer the run_id from state when available; fall back to the pending marker written during injection.
|
|
2850
|
+
current_run_id = state_run_id.strip()
|
|
2851
|
+
if not current_run_id:
|
|
2852
|
+
marker_path = os.path.join(worktree_path, ".vscode", _PENDING_AUTO_START_FILENAME)
|
|
2853
|
+
try:
|
|
2854
|
+
if os.path.isfile(marker_path):
|
|
2855
|
+
with open(marker_path, encoding="utf-8") as fh:
|
|
2856
|
+
marker = json.load(fh)
|
|
2857
|
+
if isinstance(marker, dict):
|
|
2858
|
+
run_id_value = marker.get("run_id")
|
|
2859
|
+
if isinstance(run_id_value, str):
|
|
2860
|
+
current_run_id = run_id_value.strip()
|
|
2861
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
2862
|
+
current_run_id = ""
|
|
2863
|
+
|
|
2864
|
+
if state_file_path is None or not current_run_id:
|
|
2865
|
+
# Cannot verify — fall through to start fallback immediately
|
|
2866
|
+
print(
|
|
2867
|
+
"\n--- Delayed verification: no state file or run ID available. "
|
|
2868
|
+
"Starting background fallback Copilot session. ---",
|
|
2869
|
+
file=sys.stderr,
|
|
2870
|
+
)
|
|
2871
|
+
else:
|
|
2872
|
+
# Poll for the run ID until the grace period expires.
|
|
2873
|
+
elapsed = 0.0
|
|
2874
|
+
while elapsed < _AUTOSTART_VERIFICATION_DELAY_S:
|
|
2875
|
+
if _is_run_triggered(state_file_path, current_run_id):
|
|
2876
|
+
# Auto-start task confirmed running — no fallback needed.
|
|
2877
|
+
return
|
|
2878
|
+
time.sleep(_AUTOSTART_VERIFICATION_POLL_S)
|
|
2879
|
+
elapsed += _AUTOSTART_VERIFICATION_POLL_S
|
|
2880
|
+
|
|
2881
|
+
print(
|
|
2882
|
+
f"\n--- Delayed verification: VS Code auto-start task did NOT run "
|
|
2883
|
+
f"within {_AUTOSTART_VERIFICATION_DELAY_S:.0f}s. "
|
|
2884
|
+
f"Starting background fallback Copilot session for {workflow_name}. ---",
|
|
2885
|
+
file=sys.stderr,
|
|
2886
|
+
)
|
|
2887
|
+
|
|
2888
|
+
# Start the fallback non-interactive session.
|
|
2889
|
+
try:
|
|
2890
|
+
from ..copilot.session import start_copilot_session
|
|
2891
|
+
|
|
2892
|
+
# Pin state writes to the target worktree state dir when available.
|
|
2893
|
+
target_state_dir = os.path.dirname(state_file_path) if state_file_path else None
|
|
2894
|
+
previous_state_dir = os.environ.get("AGENTIC_DEVTOOLS_STATE_DIR")
|
|
2895
|
+
if target_state_dir:
|
|
2896
|
+
os.environ["AGENTIC_DEVTOOLS_STATE_DIR"] = target_state_dir
|
|
2897
|
+
try:
|
|
2898
|
+
session_result = start_copilot_session(
|
|
2899
|
+
prompt=start_prompt,
|
|
2900
|
+
working_directory=worktree_path,
|
|
2901
|
+
interactive=False,
|
|
2902
|
+
model=model,
|
|
2903
|
+
)
|
|
2904
|
+
finally:
|
|
2905
|
+
if target_state_dir:
|
|
2906
|
+
if previous_state_dir is None:
|
|
2907
|
+
os.environ.pop("AGENTIC_DEVTOOLS_STATE_DIR", None)
|
|
2908
|
+
else:
|
|
2909
|
+
os.environ["AGENTIC_DEVTOOLS_STATE_DIR"] = previous_state_dir
|
|
2910
|
+
# Open log in VS Code so the user can monitor.
|
|
2911
|
+
if (
|
|
2912
|
+
session_result is not None
|
|
2913
|
+
and session_result.log_file
|
|
2914
|
+
and is_vscode_available()
|
|
2915
|
+
and not _in_test_environment()
|
|
2916
|
+
):
|
|
2917
|
+
_open_log_in_vscode(session_result.log_file, worktree_path)
|
|
2918
|
+
except Exception as exc:
|
|
2919
|
+
print(
|
|
2920
|
+
f"Warning: delayed fallback Copilot session failed: {exc}",
|
|
2921
|
+
file=sys.stderr,
|
|
2922
|
+
)
|
|
2923
|
+
|
|
2924
|
+
thread = threading.Thread(
|
|
2925
|
+
target=_verify_and_fallback,
|
|
2926
|
+
name="autostart-verification",
|
|
2927
|
+
daemon=False,
|
|
2928
|
+
)
|
|
2929
|
+
thread.start()
|
|
2930
|
+
|
|
2761
2931
|
|
|
2762
2932
|
def _write_pending_auto_start_marker(
|
|
2763
2933
|
worktree_path: str,
|
|
@@ -2847,6 +3017,49 @@ def _focus_vscode_window(worktree_path: str) -> bool:
|
|
|
2847
3017
|
return True
|
|
2848
3018
|
|
|
2849
3019
|
|
|
3020
|
+
def _open_log_in_vscode(log_file_path: str, worktree_path: str) -> None:
|
|
3021
|
+
"""Open the Copilot session log file in VS Code for live monitoring.
|
|
3022
|
+
|
|
3023
|
+
Uses ``code <log_file_path>`` to request opening the file in VS Code.
|
|
3024
|
+
The CLI decides which window receives the file (often the most recently
|
|
3025
|
+
active window). Best-effort: errors are printed to stderr but never raised.
|
|
3026
|
+
|
|
3027
|
+
Args:
|
|
3028
|
+
log_file_path: Absolute path to the ``.log`` file.
|
|
3029
|
+
worktree_path: The worktree root (used for context in messages).
|
|
3030
|
+
"""
|
|
3031
|
+
try:
|
|
3032
|
+
log_file_path = os.path.abspath(log_file_path)
|
|
3033
|
+
use_shell = platform.system() == "Windows"
|
|
3034
|
+
if use_shell and any(ch in log_file_path for ch in "&|<>^%"):
|
|
3035
|
+
print(
|
|
3036
|
+
"Warning: refusing to open log file in VS Code on Windows because "
|
|
3037
|
+
f"path contains cmd.exe metacharacters: {log_file_path!r}",
|
|
3038
|
+
file=sys.stderr,
|
|
3039
|
+
)
|
|
3040
|
+
return
|
|
3041
|
+
proc = subprocess.run(
|
|
3042
|
+
["code", log_file_path],
|
|
3043
|
+
stdout=subprocess.DEVNULL,
|
|
3044
|
+
stderr=subprocess.DEVNULL,
|
|
3045
|
+
timeout=10,
|
|
3046
|
+
shell=use_shell,
|
|
3047
|
+
)
|
|
3048
|
+
if proc.returncode == 0:
|
|
3049
|
+
print(f"--- Opened Copilot session log in VS Code: {log_file_path} (worktree: {worktree_path}) ---")
|
|
3050
|
+
else:
|
|
3051
|
+
print(
|
|
3052
|
+
f"Warning: 'code' exited with {proc.returncode} when opening log file "
|
|
3053
|
+
f"{log_file_path!r} (worktree: {worktree_path}).",
|
|
3054
|
+
file=sys.stderr,
|
|
3055
|
+
)
|
|
3056
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
3057
|
+
print(
|
|
3058
|
+
f"Warning: could not open log file in VS Code: {exc} (worktree: {worktree_path}, file: {log_file_path!r})",
|
|
3059
|
+
file=sys.stderr,
|
|
3060
|
+
)
|
|
3061
|
+
|
|
3062
|
+
|
|
2850
3063
|
def _try_terminal_send_fallback(worktree_path: str, expected_run_id: str | None = None) -> bool:
|
|
2851
3064
|
"""Attempt to start the Copilot session via VS Code terminal sendSequence.
|
|
2852
3065
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
agentic_devtools/__init__.py,sha256=J_Zw_vWKghk-cLmqI83hXQmSiS8zMhGIHM5WPLDkZuo,242
|
|
2
|
-
agentic_devtools/_version.py,sha256=
|
|
2
|
+
agentic_devtools/_version.py,sha256=a3-860ghbqgShvATariWvr3uQTtn4SgO05rdw4MFYhE,524
|
|
3
3
|
agentic_devtools/agdt_gitignore.py,sha256=Jcc9BUiCIqCG9tUuHc5Jua5tPT_QtTnEK6Gr0v_RrKQ,1559
|
|
4
4
|
agentic_devtools/background_tasks.py,sha256=QCiCSTcU8HSFa_g_fa2NWXhhRZU4zo6YgsPSIVVmdOU,16971
|
|
5
5
|
agentic_devtools/config.py,sha256=X6zv8Twviv9LMiV7TYy_6SQeNezJHV8lBNvIxLkOqLY,8049
|
|
@@ -145,8 +145,8 @@ agentic_devtools/cli/ci/tracker/renderer.py,sha256=6LhId0Ehfcmn_BAtrH69z9Cl4xBfQ
|
|
|
145
145
|
agentic_devtools/cli/config/__init__.py,sha256=1KFPaXnhgP1ZBvIxxLAethaCeY5RzTM2GwU-xnt69ro,271
|
|
146
146
|
agentic_devtools/cli/config/project_config.py,sha256=NjFW3Ds0vUxotGSJwp7Z4zktFItTvRp2pQ0PDdWwYIE,3193
|
|
147
147
|
agentic_devtools/cli/copilot/__init__.py,sha256=FFlmM67FbXNTKvWDm4ySOUXL6DvZMnyMWNrGa_ALiq4,562
|
|
148
|
-
agentic_devtools/cli/copilot/auto_start.py,sha256=
|
|
149
|
-
agentic_devtools/cli/copilot/session.py,sha256=
|
|
148
|
+
agentic_devtools/cli/copilot/auto_start.py,sha256=0T3tIFw3R18_KPZ-00i9HsyG5Z1dccHZmLXqYEiB8xM,33522
|
|
149
|
+
agentic_devtools/cli/copilot/session.py,sha256=7v6Wx24Edf3pAkEu4eFbxrp_vIJFAQ4xLPZq73m_ghI,39821
|
|
150
150
|
agentic_devtools/cli/git/__init__.py,sha256=ACoWYDgm8AMjk2wJ_Ycc5CY7xCXJQtfB55HzM-yx8WA,2538
|
|
151
151
|
agentic_devtools/cli/git/agdt_branch.py,sha256=4qHH1dxYr3LraAB2Q6_fjgLrI3KrPCvXu-uOsJryN7M,37373
|
|
152
152
|
agentic_devtools/cli/git/async_commands.py,sha256=DdvoMsqcVh5T78wXoFQO2m1B61QC-QPDsy6bwZ9VBDk,9883
|
|
@@ -258,11 +258,11 @@ agentic_devtools/cli/workflows/advancement.py,sha256=bxrF4L0r6LTu9Di6Ll6PUEFLAYA
|
|
|
258
258
|
agentic_devtools/cli/workflows/applied_suggestions.py,sha256=vRDYk5nokfMtmiQzCanBTBmRRFWIPNZILJDQ4hIb7nE,9306
|
|
259
259
|
agentic_devtools/cli/workflows/base.py,sha256=pgdzP5M3CKXTg1mVMqUz2t-C_NMCKbptmiM-iUgbsBg,10966
|
|
260
260
|
agentic_devtools/cli/workflows/checklist.py,sha256=3JMY-pvqb6wUpG2XXSFNnYiGYO9aM9O96Z6GW-4qb_s,9998
|
|
261
|
-
agentic_devtools/cli/workflows/commands.py,sha256=
|
|
261
|
+
agentic_devtools/cli/workflows/commands.py,sha256=Bto2FM7oFTKlYkP2mT0o20ARS9R4ivPD_mOOccRDk1E,143166
|
|
262
262
|
agentic_devtools/cli/workflows/engine_resolution.py,sha256=HUYMGERXCyJJgc0RgWg5w7JdpHGPjdXfnPkZAqMn5iI,2309
|
|
263
263
|
agentic_devtools/cli/workflows/manager.py,sha256=BxSDix8IV6QQhawIMvfIN0iOKbkFFxAauwONlkse9uU,28721
|
|
264
264
|
agentic_devtools/cli/workflows/preflight.py,sha256=vljErnXSvtgbN9m5zArGjZ4iemk6LajPj2mqGkyqGbs,12233
|
|
265
|
-
agentic_devtools/cli/workflows/worktree_setup.py,sha256=
|
|
265
|
+
agentic_devtools/cli/workflows/worktree_setup.py,sha256=gpGwpqUlR41JvEiUNZsIzZ2dP1KV0pdh18Hdc-Ub75k,166019
|
|
266
266
|
agentic_devtools/context/__init__.py,sha256=Rrk10DQHFnkFiFd1w9EFwBPo36tV-HJqjgaANm_3mg0,427
|
|
267
267
|
agentic_devtools/context/models.py,sha256=tsKM6x_yAHkWcJOzNrgMaqkXyDxR1mVZyC5Dbu_rIdE,1518
|
|
268
268
|
agentic_devtools/context/nodes.py,sha256=cexQ92RbPddTWV8zwLTK4EPEfVGZZk4Sp-nzblgE3wo,4951
|
|
@@ -600,8 +600,8 @@ agentic_devtools/_bundled_skills/prompts/speckit.plan.prompt.md,sha256=IJja5r2Sd
|
|
|
600
600
|
agentic_devtools/_bundled_skills/prompts/speckit.specify.prompt.md,sha256=eyzE3GRi2hyW30a6xPYOU7q6MJf0skrD-baEGURYqpg,31
|
|
601
601
|
agentic_devtools/_bundled_skills/prompts/speckit.tasks.prompt.md,sha256=iPxXwon5nV6dNcJV8-JoP3PssKUVXctNiG-C9SsRhB8,29
|
|
602
602
|
agentic_devtools/_bundled_skills/prompts/speckit.taskstoissues.prompt.md,sha256=L5Y21PMSoUcPAAdHy2Jnf-wGVdi04jV_pPvyOJZfpm0,37
|
|
603
|
-
agentic_devtools-0.2.
|
|
604
|
-
agentic_devtools-0.2.
|
|
605
|
-
agentic_devtools-0.2.
|
|
606
|
-
agentic_devtools-0.2.
|
|
607
|
-
agentic_devtools-0.2.
|
|
603
|
+
agentic_devtools-0.2.154.dist-info/METADATA,sha256=wE2Q9F0IF2bwtMB211fHD4BN9T6hpTFu14bPxXvygXY,29269
|
|
604
|
+
agentic_devtools-0.2.154.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
605
|
+
agentic_devtools-0.2.154.dist-info/entry_points.txt,sha256=CDSdq-zuDsv3AHT2rcY6EBRdtL1tsV_Q3shNW1AP7Ec,9329
|
|
606
|
+
agentic_devtools-0.2.154.dist-info/licenses/LICENSE,sha256=yBEDdICksxhBYLWoERKp9MTqwGnUF6Ryj9BTLwXTc6k,1082
|
|
607
|
+
agentic_devtools-0.2.154.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|