devlyn-cli 4.2.17 → 4.2.18

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.
package/README.md CHANGED
@@ -231,6 +231,8 @@ Only `__pycache__` directories containing exclusively regular `*.pyc` files coun
231
231
 
232
232
  On native Windows, use native Node/npm and Python plus Git for Windows Bash for the shipped shell wrapper. Run `npx devlyn-cli -y` in the project (add `--claude` for Claude Code). Skill folders follow the Agent Skills naming standard (for example `devlyn-ideate`), so a project that commits them checks out on Windows. Harness text is UTF-8 without requiring `PYTHONUTF8`.
233
233
 
234
+ New default drain roots disable Windows content indexing for new children; names stay indexed and Microsoft Defender scanning is unaffected. For Defender performance, use a Dev Drive via `--worktree-root` or an administrator's exclusion for the root; see [loop options](config/skills/devlyn-ideate/references/loop.md).
235
+
234
236
  Windows completion preserves the workspace, owned refs and recovery receipt when writer cessation cannot be proved, even after merge; resume guidance and delivery status remain separate from product verification. The portability workflow checks out the repository on native Windows and tests the POSIX-packed npm artifact there. A POSIX pass alone does not establish Windows support: acceptance requires the passing Windows job for the exact source SHA/artifact hashes.
235
237
 
236
238
  ## Contributing
@@ -111,6 +111,7 @@
111
111
  "a9c5242608e8b6f660f3ae9daf3fdefb210b6e4603bddee3d26ba59a55be4085",
112
112
  "aa0d6fad740ead835720a528f197b29e6cd0b209d3b9db1b312b9895f991b037",
113
113
  "ac3d475156af89e2e887118e92b972dbfefde6f0a07bbbdb68ba88fce5f7904d",
114
+ "ac404e4e846557b4c7b32050dee5f7c83648311b7d1cecdbdcae9eeda1b8eaf7",
114
115
  "ac5485ee9218c7038c6d329a3877925995c5afcb2dbd16b1da4495ed769d2083",
115
116
  "add41e1257ed525d6f225d9eb976a3978f126e604ca7c0f5aa1b223af78f1902",
116
117
  "afb478a981c4fd91f66fbe10d04481cd4562825fd6785ba0aadc6ce3ee3da173",
@@ -322,6 +323,7 @@
322
323
  "5954e371659ae1010757f0eec4b80dd65a899f9fcf76d4d84d9a75476a963218",
323
324
  "65770e3f20c3a3afd2822e9acdcfc80b827f84a7737010b79332da70d431b05d",
324
325
  "65c781764633b912e5947362eba0837c8027461a904975401d79ad4fe8714084",
326
+ "694c8920e50d92cba46dbb27eb44dd88035a89ed625d3cbf9f2bce9366d44c96",
325
327
  "6c3e2e4b84b0b4f81aed3759704da7c14f3cbabf3beeb0d1654feda3a15a8675",
326
328
  "6d9df58c117165e9d297aa6be0df8d076bac1ed49dc29ae93906af1969798ab1",
327
329
  "6e7228610628fc4bca31dc0b0aaf5eb4445e1f123f2c54bd13921c5cd01129fe",
@@ -115,6 +115,15 @@ def system_exit_code(code):
115
115
  return code - 0x100000000 if os.name == "nt" and code >= 0x80000000 else code
116
116
 
117
117
 
118
+ def mark_not_content_indexed(path):
119
+ """Disable Windows content indexing for a directory and children created afterwards."""
120
+ if os.name != "nt":
121
+ return
122
+ attrs = _kernel.GetFileAttributesW(str(path))
123
+ _checked(attrs != 0xFFFFFFFF, _kernel.GetFileAttributesW, (str(path),)) # INVALID_FILE_ATTRIBUTES.
124
+ _kernel.SetFileAttributesW(str(path), attrs | stat.FILE_ATTRIBUTE_NOT_CONTENT_INDEXED)
125
+
126
+
118
127
  if os.name == "nt":
119
128
  import _winapi
120
129
  import ctypes
@@ -164,6 +173,7 @@ if os.name == "nt":
164
173
  ("DuplicateHandle", [wintypes.HANDLE, wintypes.HANDLE, wintypes.HANDLE, ctypes.POINTER(wintypes.HANDLE), wintypes.DWORD, wintypes.BOOL, wintypes.DWORD], wintypes.BOOL),
165
174
  ("CloseHandle", [wintypes.HANDLE], wintypes.BOOL),
166
175
  ("PeekNamedPipe", [wintypes.HANDLE, ctypes.c_void_p, wintypes.DWORD, ctypes.c_void_p, ctypes.POINTER(wintypes.DWORD), ctypes.c_void_p], wintypes.BOOL),
176
+ ("SetFileAttributesW", [wintypes.LPCWSTR, wintypes.DWORD], wintypes.BOOL),
167
177
  ):
168
178
  _function = getattr(_kernel, _name)
169
179
  _function.argtypes, _function.restype, _function.errcheck = _args, _result, _checked
@@ -171,6 +181,9 @@ if os.name == "nt":
171
181
  _kernel.WaitForSingleObject.argtypes = [wintypes.HANDLE, wintypes.DWORD]
172
182
  _kernel.WaitForSingleObject.restype = wintypes.DWORD # WAIT_OBJECT_0 is successful zero, not BOOL.
173
183
 
184
+ _kernel.GetFileAttributesW.argtypes = [wintypes.LPCWSTR]
185
+ _kernel.GetFileAttributesW.restype = wintypes.DWORD # Failure is INVALID_FILE_ATTRIBUTES, not zero.
186
+
174
187
  def _read_control(fd, count):
175
188
  deadline = time.monotonic() + 10
176
189
  data = b""
@@ -52,10 +52,12 @@ not write to the shared checkout. Queue drains retain their allocation order.
52
52
  ```sh
53
53
  python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
54
54
  --task '<task identity>' --branch '<absent task branch>' \
55
- --worktree '<absent path>' --repository '<owner/repo>' --remote origin --base main
55
+ --worktree '<repo parent>/<repo name>.devlyn.noindex/<task directory>' \
56
+ --repository '<owner/repo>' --remote origin --base main
56
57
  ```
57
58
 
58
- Every delivery allocation owns a linked worktree; `--worktree` is required.
59
+ Every delivery allocation owns a linked worktree; `--worktree` is required and
60
+ must name an absent path.
59
61
  A publication allocation normally starts at the exact fetched remote base,
60
62
  independent of the anchor's branch or dirty state, and leaves the anchor's
61
63
  HEAD, index and files untouched. For a commit request, local-only work or
@@ -14,7 +14,7 @@ Both print one JSON object; exit 1 is `BLOCKED` with a `reason`.
14
14
  - `status` takes `queue.lock` in the repository's Git directory and finishes `add`'s cleanup (step 1); it reports the running `skill` root and version, any `warnings`, reconciled counts (`pending`, `active`, `accepted`, `failed`, `blocked`, `legacy_pending`), the `next` task, `blockers`, deliveries still pending with their resume (drain again until the terminal commit is attached, then the printed completion command; add `--writers-stopped` only after stopping the task's writers), and under `cleanup` the package copies `add` kept or could not remove.
15
15
  - `drain` ends `DRAINED` (no runnable task remains; open deliveries show per task), `WAITING` (a task waits with its reason, or legacy rows need planning) or `BLOCKED` (unreadable or ambiguous authoritative queue, invalid invocation, preflight refusal or held drain lock). Progress lines `devlyn-loop: <task>: <event>` go to stderr.
16
16
  - `--local-only` (alias `--no-push`) keeps the loop local. A manifest `delivery: local-only` and any earlier local receipt of the loop keep that restriction for later drains. A local loop needs no remote; nothing is fetched or pushed. An `auto`/`pr` loop none of whose tasks has a pushed PR runs as a local loop from its capture and the branch it was added on. Once one has, that PR is never rewritten to local and the loop is never run locally: its tasks wait, naming the PR, while other work continues.
17
- - Worktrees default to `<repo parent>/<repo name>.devlyn/<loop-id>/<task-id>`.
17
+ - Worktrees default to `<repo parent>/<repo name>.devlyn.noindex/<loop-id>/<task-id>`; tasks already allocated under an earlier default resume where their receipts record them. On Windows, a default root newly created by the drain gets the not-content-indexed attribute, inherited by new children (content only; names stay indexed). Microsoft Defender scanning is unaffected; use a Dev Drive via `--worktree-root` or an administrator's Defender exclusion for the root.
18
18
 
19
19
  The host supplies the executor and maps its configured engine to the argv; the loop has no model preference. Each `{packet}` in the argv is replaced by the task packet path, and each `{worktree_git_dir}` by the task worktree's absolute Git directory. The executor runs with the task worktree as its working directory and the null device as stdin; its stdout and stderr are appended to `executor.stdout` and `executor.stderr` in the packet's `evidence_dir`, never a pipe.
20
20
 
@@ -1062,6 +1062,17 @@ def allocate(v, row, opts):
1062
1062
  if values["start"] and (drift := instruction_drift(anchor, start)):
1063
1063
  return (f"the installed instructions ({drift}) in this checkout differ from those in its start commit {start}, so it would run "
1064
1064
  f"without them; commit them and push them to origin/{manifest['base_ref']}")
1065
+ if os.name == "nt" and opts.default_worktree_root:
1066
+ try:
1067
+ opts.worktree_root.mkdir(exist_ok=False)
1068
+ except FileExistsError:
1069
+ pass # An existing root belongs to its owner; never change its attributes.
1070
+ else:
1071
+ try:
1072
+ shared("platform-support")["mark_not_content_indexed"](opts.worktree_root)
1073
+ except OSError as exc:
1074
+ opts.root_diagnostics.append(
1075
+ f"content indexing unchanged: {opts.worktree_root}: could not set FILE_ATTRIBUTE_NOT_CONTENT_INDEXED: {exc}; continuing")
1065
1076
  result = task_complete("allocate", exclude_receipts=v["invalid_receipts"], **values)
1066
1077
  progress(identity, f"allocated {result['worktree']}")
1067
1078
  return None
@@ -1426,7 +1437,9 @@ def drain(args):
1426
1437
  args.repo = anchor # Preserve the repository root for failures reported by main.
1427
1438
  diagnostics = skill_diagnostics(anchor)
1428
1439
  require(any("{packet}" in part for part in args.executor), "executor argv must contain {packet}, replaced by the task packet path")
1429
- args.worktree_root = Path(args.worktree_root).resolve() if args.worktree_root else anchor.parent / f"{anchor.name}.devlyn"
1440
+ args.default_worktree_root = args.worktree_root is None
1441
+ args.root_diagnostics = []
1442
+ args.worktree_root = Path(args.worktree_root).resolve() if args.worktree_root else anchor.parent / f"{anchor.name}.devlyn.noindex"
1430
1443
  with lock(common, "drain.lock", blocking=False):
1431
1444
  attempted, last, status, reason, refused = set(), None, "DRAINED", None, {}
1432
1445
  try:
@@ -1455,6 +1468,7 @@ def drain(args):
1455
1468
  last = view(anchor, common, args.local_only)
1456
1469
  if last is None:
1457
1470
  return {"status": status, "reason": reason, **diagnostics}
1471
+ last["cleanup"].extend(args.root_diagnostics)
1458
1472
  annotate(last, refused)
1459
1473
  prune_parents(last, args.worktree_root)
1460
1474
  return {"status": status, "reason": reason, **diagnostics,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devlyn-cli",
3
- "version": "4.2.17",
3
+ "version": "4.2.18",
4
4
  "description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — core engineering principles for CLAUDE.md/AGENTS.md and a loop designer that splits one intent into tasks agents drain hands-free",
5
5
  "homepage": "https://github.com/fysoul17/devlyn-cli#readme",
6
6
  "bin": {