devlyn-cli 3.0.1 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/AGENTS.md +1 -1
  2. package/CLAUDE.md +3 -3
  3. package/README.md +20 -3
  4. package/bin/devlyn.js +69 -23
  5. package/config/skills/_shared/adapters/claude.md +3 -2
  6. package/config/skills/_shared/adapters/codex.md +1 -1
  7. package/config/skills/_shared/archive_run.py +5 -3
  8. package/config/skills/_shared/codex-config.md +13 -8
  9. package/config/skills/_shared/codex-monitored.sh +12 -19
  10. package/config/skills/_shared/collect-codex-findings.py +1 -0
  11. package/config/skills/_shared/engine-preflight.md +2 -0
  12. package/config/skills/_shared/finish-gate.py +4 -1
  13. package/config/skills/_shared/invocation-receipt.py +194 -5
  14. package/config/skills/_shared/judge-role-evidence.py +33 -6
  15. package/config/skills/_shared/phase-prompt-render.py +2 -0
  16. package/config/skills/_shared/platform-support.py +370 -0
  17. package/config/skills/_shared/process-evidence.py +4 -2
  18. package/config/skills/_shared/resolve-bootstrap.py +102 -59
  19. package/config/skills/_shared/resolve-stop-hook.py +3 -1
  20. package/config/skills/_shared/role-config.py +7 -5
  21. package/config/skills/_shared/run-bounded.py +35 -21
  22. package/config/skills/_shared/spec-verify-check.py +274 -157
  23. package/config/skills/_shared/state-phase-write.py +26 -9
  24. package/config/skills/_shared/task-complete.py +1173 -0
  25. package/config/skills/_shared/verify-merge-findings.py +9 -8
  26. package/config/skills/devlyn:queue/SKILL.md +3 -3
  27. package/config/skills/devlyn:resolve/SKILL.md +14 -7
  28. package/config/skills/devlyn:resolve/references/outer-loop.md +10 -2
  29. package/config/skills/devlyn:resolve/references/phases/verify.md +5 -3
  30. package/config/skills/devlyn:resolve/references/task-completion.md +117 -0
  31. package/package.json +1 -1
  32. package/scripts/lint-skills.sh +10 -6
package/AGENTS.md CHANGED
@@ -38,7 +38,7 @@ Each skill's `SKILL.md` is the source of truth for flags and workflow. Do not du
38
38
 
39
39
  Engine roles: orchestrator = whichever CLI the user opened (this contract is symmetric with CLAUDE.md. If you are Codex orchestrating `/devlyn:resolve`, the phase machinery is mandatory regardless of task size; if you will not run it, say so explicitly and stop — do not silently degrade to ad-hoc execution); legacy executor (with optional separate worker/primary/pair profiles) uses the canonical skill's orchestrator-supported default, overridable per run with `--engine <name>` or durably via machine-local `.devlyn/engines.json` `{"executor": "<name>", "pair_judge_priority": ["<name>", ...]}`; PLAN is orchestrator-fixed and never inherits `--engine` or the executor pin; pair judge = first available OTHER engine, default for VERIFY whenever available; risk probes remain conditional and --no-pair opts out. Pins fail closed (`BLOCKED:<engine>-unavailable` / `BLOCKED:invalid-engine-config`); new engines plug in by shipping `_shared/adapters/<name>.md`. `/devlyn:engines` (no args) shows the role table + detected engines; `executor <name>` / `pair <name>,...` / `clear` manage the pins. The executor pin binds the orchestrator in plain conversation too, not only inside a skill run: when you would implement directly and executor is pinned to a non-default engine, route that work through the pin — via `/devlyn:resolve` (reads it at PHASE 0) or by delegating to that engine; no pin / no file → use the canonical skill's orchestrator-supported default. The pair-judge pin stays pipeline-scoped. `/devlyn:engines role` configures explicit engine/model/effort for supported worker and judge routes; `/devlyn:resolve --role-config <path>` overrides roles for one run. Canonical skills define precedence, evidence and unsupported-route errors; absent profiles preserve existing defaults.
40
40
 
41
- Conversational entry: Before writes, inspect requested files and relevant callers/tests. Default to direct execution only for clear, local, reversible, low-risk work with a decisive existing or small task-specific acceptance check. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval.
41
+ Conversational entry: Before writes, inspect requested files and relevant callers/tests. Default to direct execution only for clear, local, reversible, low-risk work with a decisive existing or small task-specific acceptance check. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval. For direct/full/queue work, the outer owner reads `references/task-completion.md` relative to the installed `devlyn:resolve` skill's directory: prospective task-branch ownership (linked worktree optional), scoped commit acceptance, then PR/merge and recoverable owned-resource cleanup after required archive/queue commits. Local-only/no-push wins; delivery pending/failure stays separate from product verification.
42
42
 
43
43
  Use full `/devlyn:resolve` for material ambiguity, subsystem/design work, security/auth/payment/persistence/concurrency/public-API-contract changes, or inconclusive inspection/checks. Counts of lines, files or words alone do not establish low risk. Explicit resolve (even small), formal spec workflows and queue drains retain all canonical phases, independent verification, pins and failure handling. If new risk appears mid-edit, preserve the delta in the full run's scope/evidence; do not hide it in a new baseline or reconfirm existing authorization.
44
44
 
package/CLAUDE.md CHANGED
@@ -51,7 +51,7 @@ Each skill's `SKILL.md` is the source of truth for its flags and workflow — do
51
51
 
52
52
  ### Conversational entry + full-route handoff
53
53
 
54
- Before writes, inspect requested files and relevant callers/tests. Default to direct execution only for clear, local, reversible, low-risk work with a decisive existing or small task-specific acceptance check. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval.
54
+ Before writes, inspect requested files and relevant callers/tests. Default to direct execution only for clear, local, reversible, low-risk work with a decisive existing or small task-specific acceptance check. Preserve requested behavior, scope, explicit constraints and the executor pin; make scoped edits, run required checks, review the final diff, and report changes with evidence. One concise route/check explanation suffices; ordinary direct work needs no spec, pipeline state, phase workers or extra approval. For direct/full/queue work, the outer owner reads `references/task-completion.md` relative to the installed `devlyn:resolve` skill's directory: prospective task-branch ownership (linked worktree optional), scoped commit acceptance, then PR/merge and recoverable owned-resource cleanup after required archive/queue commits. Local-only/no-push wins; delivery pending/failure stays separate from product verification.
55
55
 
56
56
  Use full `/devlyn:resolve` for material ambiguity, subsystem/design work, security/auth/payment/persistence/concurrency/public-API-contract changes, or inconclusive inspection/checks. Counts of lines, files or words alone do not establish low risk. Explicit resolve (even small), formal spec workflows and queue drains retain all canonical phases, independent verification, pins and failure handling. If new risk appears mid-edit, preserve the delta in the full run's scope/evidence; do not hide it in a new baseline or reconfirm existing authorization.
57
57
 
@@ -60,9 +60,9 @@ The user does not invoke skills manually; the orchestrating model does. Large ta
60
60
  1. Write the agreed contract to `docs/specs/<id>/spec.md` (+ `spec.expected.json` when mechanical verifications exist). Always a spec file for large work — a written spec is the user's reviewed contract; free-form large now assumes-and-logs instead of halting (zero-scope-signal goals still halt to ideate).
61
61
  2. Present a one-screen plan-contract summary — the user's single review checkpoint, BEFORE the pipeline starts.
62
62
  3. On go-ahead, commit the scoped queue-item delta (when applicable) and linked spec bundle, then invoke `/devlyn:resolve --spec <path>` hands-free to completion.
63
- 4. **Per-task outer loop**: read the terminal verdict. PASS → done. Verdicts backed by spec/verification findings (NEEDS_WORK, verify/build-gate exhaustion) → adjudicate the findings, amend the spec (recorded in the spec file; spec stays read-only inside a run), commit that scoped amendment, then re-invoke — at most 3 outer iterations, then surface with the findings trail. Infrastructure, invalid-input, engine-availability, and implement-empty BLOCKED verdicts are not spec-amendable: surface immediately. Every iteration re-enters through committed durable artifacts (spec, findings, run archive), never conversation memory.
63
+ 4. **Per-task outer loop**: read the terminal verdict. Successful normal-run archive → outer-owner completion per the canonical task-completion reference, after any terminal queue commit. Verdicts backed by spec/verification findings (NEEDS_WORK, verify/build-gate exhaustion) → adjudicate the findings, amend the spec (recorded in the spec file; spec stays read-only inside a run), commit that scoped amendment, then re-invoke — at most 3 outer iterations, then surface with the findings trail. Infrastructure, invalid-input, engine-availability, and implement-empty BLOCKED verdicts are not spec-amendable: surface immediately. Every iteration re-enters through committed durable artifacts (spec, findings, run archive), never conversation memory.
64
64
 
65
- **Intent queue (unattended drain)**: the user stacks intents in `docs/specs/queue.md` (ordered checklist; `/devlyn:queue` is the front-end — no args shows status, `add <intent>` appends, `drain` starts the serial drain). Drain strictly serially — per item: spec it, commit the scoped owner baseline, run the outer loop, mark `[x]` done or `[F]` blocked with reason, commit that terminal transition, then continue; a blocked item never halts the queue. The queue entry is the user's go-ahead, so assume-and-log replaces the interactive checkpoint — but unattended assumptions may only take scope-narrowing, reversible, non-user-visible defaults. Material ambiguity (user-visible behavior, data/state semantics, new files/scripts/flags, implementation surface) → mark `[F] needs-review`, commit it, and move on. End the drain with a per-item verdict + assumptions report. The canonical commit order is `config/skills/devlyn:resolve/references/outer-loop.md`.
65
+ **Intent queue (unattended drain)**: the user stacks intents in `docs/specs/queue.md` (ordered checklist; `/devlyn:queue` is the front-end — no args shows status, `add <intent>` appends, `drain` starts the serial drain). Drain strictly serially — per item: spec it, commit the scoped owner baseline, run the outer loop, mark `[x]` done or `[F]` blocked with reason, commit that terminal transition, then complete eligible delivery once. Retain PR/pending workspaces; unavailable separate safe placement stops the drain with remaining items pending. A product-blocked item otherwise never halts the queue. The queue entry is the user's go-ahead, so assume-and-log replaces the interactive checkpoint — but unattended assumptions may only take scope-narrowing, reversible, non-user-visible defaults. Material ambiguity (user-visible behavior, data/state semantics, new files/scripts/flags, implementation surface) → mark `[F] needs-review`, commit it, and move on. End the drain with a per-item product verdict, separate delivery status/URL/resume and assumptions report. The canonical commit order is `config/skills/devlyn:resolve/references/outer-loop.md`.
66
66
 
67
67
  ## Subtractive-first editing — perfection = nothing left to remove
68
68
  <!-- runtime-principles:section=subtractive-first:begin -->
package/README.md CHANGED
@@ -62,7 +62,7 @@ Skip ideate entirely if you have a spec or just want to describe the work — `/
62
62
 
63
63
  ### Step 2 — Resolve with `/devlyn:resolve`
64
64
 
65
- Before writes, inspect requested files and relevant callers/tests. Clear, local, reversible, low-risk conversational edits with a decisive acceptance check run directly, honoring scope, constraints and executor pins; finish required checks and diff review, then report evidence. Material ambiguity, subsystem/design work, security/auth/payment/persistence/concurrency/public-API-contract changes or no decisive check use full `/devlyn:resolve`. Explicit resolve (including small tasks), formal specs and queue drains keep the full workflow below. Pass a spec, a free-form goal, or a diff to verify.
65
+ Before writes, inspect requested files and relevant callers/tests. Clear, local, reversible, low-risk conversational edits with a decisive acceptance check run directly, honoring scope, constraints and executor pins; finish required checks and diff review, then accept the scoped commit for owner completion below. Material ambiguity, subsystem/design work, security/auth/payment/persistence/concurrency/public-API-contract changes or no decisive check use full `/devlyn:resolve`. Explicit resolve (including small tasks), formal specs and queue drains keep the full workflow below. Pass a spec, a free-form goal, or a diff to verify.
66
66
 
67
67
  ```
68
68
  /devlyn:resolve "fix the login bug" # free-form
@@ -97,9 +97,21 @@ golden fixture, risk-probe, or pair-evidence candidate must also include
97
97
  difference from rejected or solo-saturated controls such as `S2`-`S6`; without
98
98
  that, `/devlyn:resolve` stops with `BLOCKED:solo-ceiling-avoidance-required`.
99
99
 
100
+ Accepted tasks default to owner completion: scoped commit → push → PR → normal
101
+ protected merge → recoverable cleanup of prospectively owned resources. Set
102
+ `git config --local devlyn.completionMode pr` to stop at a PR, or `auto` to restore
103
+ the default; `task-complete.py complete --mode auto|pr` overrides one task.
104
+ Local-only/no-push instructions take precedence. Linked worktrees are optional;
105
+ existing branches cannot be adopted. Full runs require successful archive before
106
+ delivery; direct tasks use their actual checks and root acceptance. Verify-only
107
+ never publishes. Pending checks or unsupported merge policy retain resources and
108
+ report a receipt-based resume command separately from product verification.
109
+ See [task completion](config/skills/devlyn:resolve/references/task-completion.md)
110
+ for allocation, acceptance, writer cessation and recovery.
111
+
100
112
  ### Queue multiple intents for unattended drain — `/devlyn:queue`
101
113
 
102
- Stack tasks to run back-to-back without supervision. `/devlyn:queue add "<intent>"` appends to `docs/specs/queue.md` (an ordered checklist); `/devlyn:queue drain` runs each item serially — spec it, run the resolve outer loop, mark `[x]` done or `[F]` blocked with a reason, then move on. A blocked item never halts the queue, and unattended runs only take scope-narrowing, reversible defaults — anything user-visible or ambiguous is marked `[F] needs-review` for you to adjudicate afterward. `/devlyn:queue` with no args shows status.
114
+ Stack tasks to run back-to-back without supervision. `/devlyn:queue add "<intent>"` appends to `docs/specs/queue.md` (an ordered checklist); `/devlyn:queue drain` runs each item serially — spec it, run the resolve outer loop, commit `[x]` done or `[F]` blocked with a reason, then complete eligible delivery once. Product verdict and delivery status are separate. A blocked product otherwise never halts the queue; pending delivery retains its branch, and the next item requires separate safe placement. Unattended runs only take scope-narrowing, reversible defaults — anything user-visible or ambiguous is marked `[F] needs-review` for you to adjudicate afterward. `/devlyn:queue` with no args shows status.
103
115
 
104
116
  ### Engine roles — auto-detected, pinnable with `/devlyn:engines`
105
117
 
@@ -293,9 +305,14 @@ Selected during install. Run `npx devlyn-cli` again to add more.
293
305
 
294
306
  ## Requirements
295
307
 
296
- - **Node.js 18+**
308
+ - **Node.js 18+** and npm
309
+ - **Python 3.11+** available as `python3`, and Git for the resolve harness
297
310
  - **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** installed and configured
298
311
 
312
+ 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 Codex with `npx devlyn-cli agents codex`. npm extraction and installed directories use the legal U+F03A colon alias (for example `devlyn\uf03aresolve`); logical skill names remain `devlyn:resolve`. Do not rename them by hand. Harness text is UTF-8 without requiring `PYTHONUTF8`.
313
+
314
+ 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 tests a POSIX-packed npm artifact on native Windows without checking out colon paths. A POSIX pass alone does not establish Windows support: acceptance requires the passing Windows job for the exact source SHA/artifact hashes.
315
+
299
316
  ## Contributing
300
317
 
301
318
  - **Add a skill** — directory in `config/skills/` with `SKILL.md`
package/bin/devlyn.js CHANGED
@@ -18,6 +18,33 @@ const DEVLYN_CORE_SKILLS = ['devlyn:resolve', 'devlyn:ideate', 'devlyn:design-ui
18
18
  const DEVLYN_SKILL_DIR_STAMP = '__DEVLYN_SKILL_DIR__';
19
19
  const DEVLYN_INSTALL_MARKER = '.devlyn-install.json';
20
20
 
21
+ function logicalSkillName(name) {
22
+ return name.replace(/\uF03A/g, ':');
23
+ }
24
+
25
+ function physicalSkillName(name) {
26
+ return process.platform === 'win32' ? name.replace(/:/g, '\uF03A') : name;
27
+ }
28
+
29
+ function skillPath(root, name) {
30
+ name = logicalSkillName(name);
31
+ const aliases = [...new Set([name, name.replace(/:/g, '\uF03A')])];
32
+ const entries = fs.existsSync(root) ? fs.readdirSync(root) : [];
33
+ const found = aliases.filter((alias) => entries.includes(alias));
34
+ if (found.length > 1) throw new Error(`Ambiguous skill aliases in ${root}: ${found.join(', ')}`);
35
+ return path.join(root, found[0] || physicalSkillName(name));
36
+ }
37
+
38
+ function preflightSkillDirs(source, target, names) {
39
+ for (const name of names) {
40
+ skillPath(source, name);
41
+ skillPath(target, name);
42
+ }
43
+ for (const relative of DEPRECATED_DIRS) {
44
+ skillPath(target, path.basename(relative));
45
+ }
46
+ }
47
+
21
48
  // Cross-agent shared skills directory read by BOTH oh-my-pi and Pi. Verified
22
49
  // from the omp binary's skill-provider strings ("skills from .agents/skills —
23
50
  // project walk-up + user home") and Pi's docs ("~/.agents/skills/"). Installing
@@ -313,6 +340,8 @@ function listContents() {
313
340
 
314
341
  function cleanupDeprecated(targetDir) {
315
342
  let removed = 0;
343
+ const deprecated = DEPRECATED_DIRS.map((relPath) =>
344
+ [relPath, skillPath(path.join(targetDir, 'skills'), path.basename(relPath))]);
316
345
  for (const relPath of DEPRECATED_FILES) {
317
346
  const fullPath = path.join(targetDir, relPath);
318
347
  if (fs.existsSync(fullPath)) {
@@ -321,8 +350,7 @@ function cleanupDeprecated(targetDir) {
321
350
  removed++;
322
351
  }
323
352
  }
324
- for (const relPath of DEPRECATED_DIRS) {
325
- const fullPath = path.join(targetDir, relPath);
353
+ for (const [relPath, fullPath] of deprecated) {
326
354
  if (fs.existsSync(fullPath)) {
327
355
  fs.rmSync(fullPath, { recursive: true });
328
356
  log(` ✕ ${relPath}/ (removed)`, 'dim');
@@ -337,13 +365,14 @@ function copyRecursive(src, dest, baseDir) {
337
365
 
338
366
  if (stats.isDirectory()) {
339
367
  // Never install dev workspaces, even when running from source repo.
340
- if (UNSHIPPED_SKILL_DIRS.has(path.basename(src))) return;
368
+ if (UNSHIPPED_SKILL_DIRS.has(logicalSkillName(path.basename(src)))) return;
341
369
  if (!fs.existsSync(dest)) {
342
370
  fs.mkdirSync(dest, { recursive: true });
343
371
  }
344
372
 
345
373
  for (const item of fs.readdirSync(src)) {
346
- copyRecursive(path.join(src, item), path.join(dest, item), baseDir);
374
+ const name = src === path.join(CONFIG_SOURCE, 'skills') ? physicalSkillName(logicalSkillName(item)) : item;
375
+ copyRecursive(path.join(src, item), path.join(dest, name), baseDir);
347
376
  }
348
377
  } else {
349
378
  const destDir = path.dirname(dest);
@@ -360,9 +389,19 @@ function clearInstallMarker(skillsDir) {
360
389
  }
361
390
 
362
391
  function assertCompleteSkillInstall(sourceSkillsDir, skillsDir, skillNames) {
363
- const missing = skillNames.filter((skillName) =>
364
- !fs.existsSync(path.join(sourceSkillsDir, skillName))
365
- || !fs.existsSync(path.join(skillsDir, skillName)));
392
+ function complete(src, dest) {
393
+ if (!fs.existsSync(src) || !fs.existsSync(dest)) return false;
394
+ if (fs.statSync(src).isDirectory()) {
395
+ return fs.statSync(dest).isDirectory() && fs.readdirSync(src).every((name) =>
396
+ complete(path.join(src, name), path.join(dest, name)));
397
+ }
398
+ return fs.statSync(dest).isFile();
399
+ }
400
+ const missing = skillNames.filter((name) => {
401
+ const src = skillPath(sourceSkillsDir, name);
402
+ const dest = skillPath(skillsDir, name);
403
+ return !complete(src, dest) || (name !== '_shared' && !fs.existsSync(path.join(src, 'SKILL.md')));
404
+ });
366
405
  if (missing.length > 0) {
367
406
  throw new Error(`Incomplete devlyn skill install; missing: ${missing.join(', ')}`);
368
407
  }
@@ -440,11 +479,12 @@ const UNSHIPPED_SKILL_DIRS = new Set([
440
479
  // no counterpart in source. Dev workspaces are skipped entirely.
441
480
  function cleanManagedSkillDirs(sourceSkillsDir, targetSkillsDir) {
442
481
  if (!fs.existsSync(sourceSkillsDir) || !fs.existsSync(targetSkillsDir)) return 0;
482
+ preflightSkillDirs(sourceSkillsDir, targetSkillsDir, fs.readdirSync(sourceSkillsDir));
443
483
  let cleaned = 0;
444
484
  for (const entry of fs.readdirSync(sourceSkillsDir, { withFileTypes: true })) {
445
485
  if (!entry.isDirectory()) continue;
446
- if (UNSHIPPED_SKILL_DIRS.has(entry.name)) continue;
447
- const targetPath = path.join(targetSkillsDir, entry.name);
486
+ if (UNSHIPPED_SKILL_DIRS.has(logicalSkillName(entry.name))) continue;
487
+ const targetPath = skillPath(targetSkillsDir, entry.name);
448
488
  if (fs.existsSync(targetPath)) {
449
489
  fs.rmSync(targetPath, { recursive: true, force: true });
450
490
  cleaned++;
@@ -547,9 +587,11 @@ function multiSelect(items, preselectedIndices = []) {
547
587
  }
548
588
 
549
589
  function installLocalSkill(skillName) {
550
- const src = path.join(OPTIONAL_SKILLS_SOURCE, skillName);
590
+ const src = skillPath(OPTIONAL_SKILLS_SOURCE, skillName);
551
591
  const targetDir = getTargetDir();
552
- const dest = path.join(targetDir, 'skills', skillName);
592
+ const targets = new Set([path.join(targetDir, 'skills'), ...Object.values(CLI_TARGETS)
593
+ .map((cli) => cli.skillsDir).filter((dir) => dir && fs.existsSync(dir))]);
594
+ for (const target of targets) skillPath(target, skillName);
553
595
 
554
596
  if (!fs.existsSync(src)) {
555
597
  log(` ⚠️ Skill "${skillName}" not found`, 'yellow');
@@ -557,7 +599,6 @@ function installLocalSkill(skillName) {
557
599
  }
558
600
 
559
601
  log(`\n🛠️ Installing ${skillName}...`, 'cyan');
560
- copyRecursive(src, dest, targetDir);
561
602
 
562
603
  // Mirror to every CLI skill-loader directory that already exists so optional
563
604
  // skills are picked up by Codex/omp/Pi (and any future CLI with a skillsDir)
@@ -565,13 +606,12 @@ function installLocalSkill(skillName) {
565
606
  // create an agent install just because someone opted into a Claude-side skill.
566
607
  // De-dup by directory: omp and Pi share ~/.agents/skills, so the mirror runs
567
608
  // once per unique destination.
568
- const mirrored = new Set();
569
- for (const cli of Object.values(CLI_TARGETS)) {
570
- if (!cli.skillsDir || mirrored.has(cli.skillsDir) || !fs.existsSync(cli.skillsDir)) continue;
571
- mirrored.add(cli.skillsDir);
572
- const cliDest = path.join(cli.skillsDir, skillName);
573
- if (fs.existsSync(cliDest)) fs.rmSync(cliDest, { recursive: true, force: true });
574
- copyRecursive(src, cliDest, cli.skillsDir);
609
+ for (const target of targets) {
610
+ fs.rmSync(skillPath(target, skillName), { recursive: true, force: true });
611
+ const dest = path.join(target, physicalSkillName(skillName));
612
+ copyRecursive(src, dest, target);
613
+ stampInstalledSkillDir(dest, dest);
614
+ assertCompleteSkillInstall(OPTIONAL_SKILLS_SOURCE, target, [skillName]);
575
615
  }
576
616
  return true;
577
617
  }
@@ -632,6 +672,7 @@ function installSkillsForCLI(cliKey) {
632
672
  fs.mkdirSync(cli.skillsDir, { recursive: true });
633
673
  }
634
674
  clearInstallMarker(cli.skillsDir);
675
+ preflightSkillDirs(sourceSkillsDir, cli.skillsDir, cli.skillsToInstall);
635
676
 
636
677
  const removed = cleanupDeprecated(path.dirname(cli.skillsDir));
637
678
  if (removed > 0) {
@@ -640,13 +681,14 @@ function installSkillsForCLI(cliKey) {
640
681
 
641
682
  let copied = 0;
642
683
  for (const skillName of cli.skillsToInstall) {
643
- const src = path.join(sourceSkillsDir, skillName);
644
- const dest = path.join(cli.skillsDir, skillName);
684
+ const src = skillPath(sourceSkillsDir, skillName);
685
+ const previous = skillPath(cli.skillsDir, skillName);
686
+ const dest = path.join(cli.skillsDir, physicalSkillName(skillName));
645
687
  if (!fs.existsSync(src)) continue;
646
688
  // Full replace per cleanManagedSkillDirs semantics: stale files in the
647
689
  // installed mirror would otherwise persist forever.
648
- if (fs.existsSync(dest)) {
649
- fs.rmSync(dest, { recursive: true, force: true });
690
+ if (fs.existsSync(previous)) {
691
+ fs.rmSync(previous, { recursive: true, force: true });
650
692
  }
651
693
  copyRecursive(src, dest, cli.skillsDir);
652
694
  stampInstalledSkillDir(dest, dest);
@@ -777,6 +819,10 @@ function installClaudeCore() {
777
819
  }
778
820
  copyRecursive(CONFIG_SOURCE, targetDir, targetDir);
779
821
  assertCompleteSkillInstall(path.join(CONFIG_SOURCE, 'skills'), skillsDir, DEVLYN_CORE_SKILLS);
822
+ for (const name of DEVLYN_CORE_SKILLS) {
823
+ const dest = skillPath(skillsDir, name);
824
+ stampInstalledSkillDir(dest, dest);
825
+ }
780
826
  writeInstallMarker(skillsDir);
781
827
 
782
828
  // Remove deprecated files from previous versions
@@ -22,7 +22,7 @@ two targeted probes; the invoking phase sets `--effort`, pair-JUDGE uses
22
22
  `medium`):
23
23
 
24
24
  ```bash
25
- python3 "$DEVLYN_SHARED_DIR/run-bounded.py" 600 -- claude -p "<judge prompt>" \
25
+ python3 "$DEVLYN_SHARED_DIR/run-bounded.py" 600 --stdin-file "<judge-prompt-file>" --record-transport -- claude -p \
26
26
  --permission-mode dontAsk \
27
27
  --tools "Read,Grep,Glob" --allowedTools "Read,Grep,Glob" \
28
28
  --setting-sources project --strict-mcp-config --mcp-config '{"mcpServers":{}}' \
@@ -30,6 +30,7 @@ python3 "$DEVLYN_SHARED_DIR/run-bounded.py" 600 -- claude -p "<judge prompt>" \
30
30
  > .devlyn/claude-judge.stdout 2> .devlyn/claude-judge.stderr
31
31
  ```
32
32
 
33
+ - Write the complete prompt as exact UTF-8 file bytes; no positional prompt or shell substitution. With `--record-transport`, the runner snapshots stdin and exclusively creates `<judge-prompt-file>.transport.json` to bind actual native argv and delivered bytes; an existing carrier rejects dispatch. Preserve it for explicit judge-role authentication. Ordinary `--stdin-file` reads require no output or writable input directory; without it, stdin remains DEVNULL.
33
34
  - Omit `--model` — the CLI's configured default is used (zero-touch, same
34
35
  rule as codex-config.md's "omit `-m`").
35
36
  - `dontAsk` denies anything not allowlisted (official guide: "denies
@@ -46,7 +47,7 @@ python3 "$DEVLYN_SHARED_DIR/run-bounded.py" 600 -- claude -p "<judge prompt>" \
46
47
  `.devlyn/verify.pair.findings.jsonl`. Raw stdout stays diagnostic at
47
48
  `.devlyn/claude-judge.stdout`; `verify-merge-findings.py` blocks the run
48
49
  if stdout contains findings the canonical file lacks.
49
- - Exit 124 is a wall-budget abort (kills the process group) → the orchestrator
50
+ - Exit 124 is a wall-budget abort (terminates the native child process tree; POSIX process group) → the orchestrator
50
51
  writes `.devlyn/verify.pair.timeout.json`; budget abort ≠ availability failure
51
52
  and the fail-closed availability rules above are unchanged.
52
53
  - Do not pipe stdout (`| tail`, `| grep`); capture to file. Non-zero exit other
@@ -14,7 +14,7 @@ Your default is efficient, direct, task-oriented. The canonical body specifies t
14
14
 
15
15
  Resolve the request in the fewest useful tool loops without sacrificing correctness. For retrieval tasks: start with one broad search using short discriminative keywords; make another retrieval call only when the top results don't answer the core question or a required fact / parameter / source is missing. For tool-heavy tasks, start with a brief preamble: a one-line acknowledgment of the request and the first step you'll take.
16
16
 
17
- For VERIFY JUDGE retrieval, batch related reads. After sealed parity proves a
17
+ For a constrained-read VERIFY packet, judge the supplied material without tools; missing or unbound inputs require a verdict-binding BLOCKED finding. Keep the read-only sandbox and bounded review. Otherwise, for VERIFY JUDGE retrieval, batch related reads. After sealed parity proves a
18
18
  canonical file and its `.agents` mirror are byte-identical, inspect the
19
19
  canonical file only. Open self-test bodies or MECHANICAL raw streams only for a
20
20
  named clause that remains unresolved after the broad pass.
@@ -535,7 +535,7 @@ def completion_self_test() -> None:
535
535
  for i, final in enumerate(invalid):
536
536
  path = runs / f"indeterminate-{i:02d}" / "pipeline.state.json"
537
537
  path.parent.mkdir()
538
- path.write_text(json.dumps({"run_id": path.parent.name, "phases": {"final_report": final}}))
538
+ path.write_text(json.dumps({"run_id": path.parent.name, "phases": {"final_report": final}}), encoding="utf-8")
539
539
  preserved.append(path)
540
540
  for i, raw in enumerate((b"{", b"\xff", b"null", b'{"phases":{}}', b'{"run_id":[]}')):
541
541
  path = runs / f"malformed-{i}" / "pipeline.state.json"
@@ -545,14 +545,14 @@ def completion_self_test() -> None:
545
545
  for i, pattern in enumerate(BOOTSTRAP_TEMP_PATTERNS):
546
546
  path = runs / f"residue-{i}" / "pipeline.state.json"
547
547
  path.parent.mkdir()
548
- path.write_text(json.dumps({"run_id": path.parent.name, "phases": {"final_report": valid[0]}}))
548
+ path.write_text(json.dumps({"run_id": path.parent.name, "phases": {"final_report": valid[0]}}), encoding="utf-8")
549
549
  residue = path.with_name(pattern.replace("*", "interrupted"))
550
550
  residue.write_bytes(b"interrupted initialization")
551
551
  preserved.extend((path, residue))
552
552
  for i in range(12):
553
553
  path = runs / f"complete-{i:02d}" / "pipeline.state.json"
554
554
  path.parent.mkdir()
555
- path.write_text(json.dumps({"run_id": path.parent.name, "phases": {"final_report": valid[i % 8]}}))
555
+ path.write_text(json.dumps({"run_id": path.parent.name, "phases": {"final_report": valid[i % 8]}}), encoding="utf-8")
556
556
  before = {p: p.read_bytes() for p in preserved}
557
557
  assert prune(runs) == 2
558
558
  assert {p: p.read_bytes() for p in preserved} == before
@@ -901,6 +901,7 @@ def self_test() -> int:
901
901
  [sys.executable, str(pathlib.Path(__file__).resolve()), "--devlyn-dir", str(bad)],
902
902
  capture_output=True,
903
903
  text=True,
904
+ encoding="utf-8",
904
905
  )
905
906
  assert blocked_cli.returncode == 1
906
907
  assert "error: archive blocked:" in blocked_cli.stderr
@@ -948,4 +949,5 @@ def main() -> int:
948
949
 
949
950
 
950
951
  if __name__ == "__main__":
952
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
951
953
  raise SystemExit(main())
@@ -4,7 +4,7 @@ Single source of truth for how every skill calls Codex. **MCP is not used.** Ski
4
4
 
5
5
  ## Canonical invocations
6
6
 
7
- All long-running Codex calls go through `codex-monitored.sh` — a thin wrapper that closes stdin (codex 0.124.0 hangs when both stdin is open and a prompt arg is given), streams Codex stdout fully (no `tail -n` truncation), and prints a `[codex-monitored] heartbeat` line every 30s so the outer `claude -p` byte-watchdog stays fed during long reasoning gaps. The wrapper passes its arguments through verbatim to the underlying CLI, so the canonical flag set is unchanged from a raw call — only the launcher differs.
7
+ All long-running Codex calls go through `codex-monitored.sh`. It streams full stdout and emits a `[codex-monitored] heartbeat` every 30s on stderr. Write multiline prompts as exact UTF-8 bytes to a task-local file; set `DEVLYN_CODEX_PROMPT_FILE` and pass the sole prompt argument `-`. The wrapper snapshots those bytes before dispatch and seals actual argv, prompt digest and completion in `<prompt-file>.transport.json`. Missing/unreadable files, competing prompt arguments and receipt/prompt mismatches fail before launch. Without file transport, stdin remains DEVNULL (avoiding the open-stdin plus argument-prompt hang).
8
8
 
9
9
  Before the first Codex call, resolve the wrapper from the invoked skill directory:
10
10
 
@@ -25,22 +25,22 @@ fi
25
25
  **Read-only critique / adversarial review / debate** (`/devlyn:resolve` VERIFY pair-mode, plus any future ideate read-only critique). Security review stays native to Claude Code BUILD_GATE. Codex returns findings on stdout; the orchestrator writes files.
26
26
 
27
27
  ```bash
28
- CODEX_MONITORED_ISOLATED=1 bash "$CODEX_MONITORED_PATH" \
28
+ DEVLYN_CODEX_PROMPT_FILE="<prompt-file>" CODEX_MONITORED_ISOLATED=1 CODEX_MONITORED_TIMEOUT_SEC=600 bash "$CODEX_MONITORED_PATH" \
29
29
  -C <project-root> \
30
30
  -s read-only \
31
31
  -c model_reasoning_effort=xhigh \
32
- "<inlined-prompt>"
32
+ -
33
33
  ```
34
34
 
35
35
  **Workspace-write implementation** (`/devlyn:resolve` IMPLEMENT phase when `--engine codex` or `--engine auto` routes to Codex, plus codex-routed `/devlyn:ideate` phases):
36
36
 
37
37
  ```bash
38
- bash "$CODEX_MONITORED_PATH" \
38
+ DEVLYN_CODEX_PROMPT_FILE="<prompt-file>" bash "$CODEX_MONITORED_PATH" \
39
39
  -C <project-root> \
40
40
  -s workspace-write \
41
41
  -c sandbox_workspace_write.network_access=false \
42
42
  -c model_reasoning_effort=xhigh \
43
- "<inlined-prompt>"
43
+ -
44
44
  ```
45
45
 
46
46
  **CI-equivalent BUILD_GATE** keeps the same write sandbox and enables general
@@ -48,15 +48,16 @@ outbound network access inside that sandbox, including the loopback servers and
48
48
  network-backed test gates that CI may exercise:
49
49
 
50
50
  ```bash
51
- bash "$CODEX_MONITORED_PATH" \
51
+ DEVLYN_CODEX_PROMPT_FILE="<prompt-file>" bash "$CODEX_MONITORED_PATH" \
52
52
  -C <project-root> \
53
53
  -s workspace-write \
54
54
  -c sandbox_workspace_write.network_access=true \
55
55
  -c model_reasoning_effort=xhigh \
56
- "<inlined-prompt>"
56
+ -
57
57
  ```
58
58
 
59
59
  Notes:
60
+ - `DEVLYN_CODEX_PROMPT_FILE` — use the same file as `DEVLYN_INVOCATION_PROMPT_FILE` for receipt-bound calls. Retain the generated transport carrier with the prompt/session/argv evidence; a path or claimed environment value alone does not prove delivery. Do not use shell command substitution for prompt bytes.
60
61
  - `-C` — project root so Codex's working directory matches.
61
62
  - `-s read-only` / `-s workspace-write` — sandbox policy. Use workspace-write for implementation/probe phases that write tracked files or `.devlyn` artifacts.
62
63
  - `-c sandbox_workspace_write.network_access=<true|false>` — required and receipt-bound for mutation phases: `true` only for BUILD_GATE and, per call, PROBE-DERIVE when the probe's visible Verification command requires a localhost service; `false` for PLAN, IMPLEMENT, CLEANUP, and PROBE-DERIVE by default. This allows CI-equivalent loopback/network tests without widening to `danger-full-access` and prevents user configuration from silently changing other phases.
@@ -66,6 +67,10 @@ Notes:
66
67
  - Wrapper calls are **foreground-blocking**. Never launch them via a backgrounded shell (`run_in_background`, `&`, `nohup`) and never end the orchestrator message while one runs: a headless print-mode session kills backgrounded children at wind-down (observed 2026-07-07: an FS1 A-arm IMPLEMENT codex call was killed at turn end → 0-byte delivery). The heartbeat stream is the observability channel; block on the call. Interactive Claude Code caps a foreground Bash call at `BASH_MAX_TIMEOUT_MS` (the installer sets at least 3600000 ms, IMPLEMENT's effective outer ceiling), so every foreground Codex wrapper call must pass the Bash `timeout` explicitly at that ceiling or the phase's own budget (for example, 600s judges) — never rely on the 120 s default.
67
68
  - Raw `codex exec ...` invocations are **forbidden** in skill prompts. The benchmark variant arm runs a PATH shim (`scripts/codex-shim/codex`) that transparently re-routes any raw `codex exec` to the wrapper as a safety net, but skills should always emit the wrapper form directly so the orchestrator's first-attempt has the right shape. Two prior iterations (iter-0006 universal foreground ban, iter-0008 prompt-level kill-shape contract) failed because the orchestrator picked starvation-prone shapes (`codex exec ... 2>&1 | tail -200`) from its own pattern prior — the wrapper plus the shim is the runtime binding layer those iters lacked. See `autoresearch/iterations/0009-wrapper-and-hook.md`.
68
69
 
70
+ ## Constrained Windows judge reads
71
+
72
+ Codex supports [native Windows sandboxing](https://learn.chatgpt.com/docs/windows/windows-sandbox). A policy-denied read selects a constrained-read route for that judge; it does not establish that Windows lacks sandbox support or denies every read. The orchestrator supplies the complete spec, sibling expected contract, accepted source SHA and cumulative diff, relevant source/tests with file:line locators, and validated sealed MECHANICAL results, manifests and required raw streams inline in the prompt file. Tell the fresh judge: "Judge only the supplied evidence; run no tools. Missing, truncated or unbound inputs require a verdict-binding BLOCKED finding." Keep `-s read-only`, isolation, freshness, selected model/effort, the 600s budget and normal findings/timeout handling. Never widen sandbox permissions or invent evidence to complete the packet.
73
+
69
74
  ## Availability check
70
75
 
71
76
  Before the first Codex call in a run, verify the CLI is on PATH:
@@ -84,4 +89,4 @@ The local Codex CLI, fronted by `codex-monitored.sh`, is the integration boundar
84
89
 
85
90
  Skills write the invocation as a Bash command the runtime executes. Example shape from `/devlyn:resolve` PHASE 2 IMPLEMENT when routed to Codex:
86
91
 
87
- > Run `bash "$CODEX_MONITORED_PATH" --json -C <state.base_ref.repo_root> -s workspace-write -m <model_requested> -c sandbox_workspace_write.network_access=false -c model_reasoning_effort=xhigh "<IMPLEMENT prompt>"`. Capture stdout as the IMPLEMENT reply; non-zero exit → treat as subagent failure. The wrapper emits `[codex-monitored]` heartbeat and lifecycle lines on **stderr** — stdout stays clean for Codex output, so the orchestrator can parse the reply without filtering. Heartbeat-on-stderr keeps the orchestrator's combined-output stream non-silent (defeats the iter-0008 byte-watchdog kill) without polluting the codex-reply view of stdout. Do not pipe the wrapper; direct capture or file redirection preserves streaming and avoids the pipe-refusal exit.
92
+ > Run `DEVLYN_CODEX_PROMPT_FILE="<IMPLEMENT-prompt-file>" bash "$CODEX_MONITORED_PATH" --json -C <state.base_ref.repo_root> -s workspace-write -m <model_requested> -c sandbox_workspace_write.network_access=false -c model_reasoning_effort=xhigh -`. Capture stdout as the IMPLEMENT reply; non-zero exit → treat as subagent failure. The wrapper emits `[codex-monitored]` heartbeat and lifecycle lines on **stderr** — stdout stays clean for Codex output, so the orchestrator can parse the reply without filtering. Heartbeat-on-stderr keeps the orchestrator's combined-output stream non-silent (defeats the iter-0008 byte-watchdog kill) without polluting the codex-reply view of stdout. Do not pipe the wrapper; direct capture or file redirection preserves streaming and avoids the pipe-refusal exit.
@@ -21,7 +21,7 @@
21
21
  # clear message so the orchestrator can self-correct on retry.
22
22
  # (Round 2 finding #1 fix: shim alone does not defeat `| tail`; the
23
23
  # wrapper must reject the pipe shape directly.)
24
- # 2. Closes stdin (`< /dev/null`) — kills the codex 0.124.0 stdin hang.
24
+ # 2. Closes stdin unless DEVLYN_CODEX_PROMPT_FILE supplies the sole prompt.
25
25
  # 3. Streams codex stdout to OUR stdout line-by-line — the orchestrator reads
26
26
  # stdout as the subagent reply (per `_shared/codex-config.md`) so we MUST
27
27
  # NOT swallow it (e.g. `tail -n 200`). codex stderr forwards to OUR stderr.
@@ -54,6 +54,7 @@
54
54
  # CODEX_MONITORED_ALLOW_PIPED — set non-empty to skip the pipe-stdout
55
55
  # refusal. Reserved for tests; don't use
56
56
  # in skill prompts.
57
+ # DEVLYN_CODEX_PROMPT_FILE — exact binary stdin prompt; requires sole `-`.
57
58
  # DEVLYN_INVOCATION_* — run/phase/round/workdir/prompt/session/
58
59
  # receipt identity. When any is set, all
59
60
  # are required and the wrapper seals a
@@ -269,22 +270,17 @@ if [ -n "${CODEX_MONITORED_ISOLATED:-}" ]; then
269
270
  printf '[codex-monitored] isolated=1\n' >&2
270
271
  fi
271
272
 
272
- if [ -n "$RECEIPT_ENABLED" ]; then
273
- RECEIPT_HELPER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/invocation-receipt.py"
274
- python3 "$RECEIPT_HELPER" start \
275
- --workdir "$DEVLYN_INVOCATION_WORKDIR" \
276
- --receipt "$DEVLYN_INVOCATION_RECEIPT" \
277
- --run-id "$DEVLYN_INVOCATION_RUN_ID" \
278
- --phase "$DEVLYN_INVOCATION_PHASE" \
279
- --round "$DEVLYN_INVOCATION_ROUND" \
280
- --prompt-file "$DEVLYN_INVOCATION_PROMPT_FILE" \
281
- --session-file "$DEVLYN_INVOCATION_SESSION_FILE" \
282
- -- "${CODEX_ARGS[@]}" || exit 64
273
+ RECEIPT_HELPER="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/invocation-receipt.py"
274
+ # Native Python owns Windows PIDs and the timeout tree; MSYS PIDs are different.
275
+ if [ "${OS:-}" = Windows_NT ]; then
276
+ exec python3 "$RECEIPT_HELPER" dispatch --binary "$CODEX_BIN" \
277
+ --timeout "$TIMEOUT_SEC" --heartbeat "$HEARTBEAT_SEC" -- "${CODEX_ARGS[@]}" < /dev/null
283
278
  fi
284
279
 
285
- # Launch codex with stdin closed; output streams directly to OUR stdout/stderr.
280
+ # The dispatcher snapshots file input, seals actual argv, then execs in this group.
286
281
  set -m
287
- "$CODEX_BIN" exec "${CODEX_ARGS[@]}" < /dev/null &
282
+ python3 "$RECEIPT_HELPER" dispatch --binary "$CODEX_BIN" \
283
+ --timeout "$TIMEOUT_SEC" --heartbeat "$HEARTBEAT_SEC" -- "${CODEX_ARGS[@]}" < /dev/null &
288
284
  CODEX_PID=$!
289
285
  printf '[codex-monitored] codex pid=%d\n' "$CODEX_PID" >&2
290
286
 
@@ -308,11 +304,8 @@ if [ -n "$TIMEOUT_FLAG" ] && [ -f "$TIMEOUT_FLAG" ]; then
308
304
  EXIT=124
309
305
  fi
310
306
 
311
- if [ -n "$RECEIPT_ENABLED" ]; then
312
- python3 "$RECEIPT_HELPER" finish \
313
- --workdir "$DEVLYN_INVOCATION_WORKDIR" \
314
- --receipt "$DEVLYN_INVOCATION_RECEIPT" \
315
- --exit-code "$EXIT" || exit 64
307
+ if [ -n "$RECEIPT_ENABLED" ] || [ -n "${DEVLYN_CODEX_PROMPT_FILE:-}" ]; then
308
+ python3 "$RECEIPT_HELPER" complete-dispatch --exit-code "$EXIT" || exit 64
316
309
  fi
317
310
 
318
311
  printf '[codex-monitored] codex exited: code=%d elapsed=%ds\n' \
@@ -501,4 +501,5 @@ def main() -> int:
501
501
 
502
502
 
503
503
  if __name__ == "__main__":
504
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
504
505
  raise SystemExit(main())
@@ -49,6 +49,8 @@ Do not report a downgraded successful run when a required engine is missing.
49
49
 
50
50
  ## Canonical cross-engine invocations
51
51
 
52
+ For native Windows, use native Node/npm and Python (`python3` on PATH), with Git for Windows Bash for the shipped shell wrapper. Supported npm engine shims resolve to native argv; unsupported/malformed shims fail visibly. Deliver multiline prompts from files: Codex uses `DEVLYN_CODEX_PROMPT_FILE` with explicit `-`; Claude uses `run-bounded.py 600 --stdin-file <prompt-file> --record-transport -- claude -p`. Retain generated transport evidence. A policy-denied Codex judge read uses the complete inline no-tools packet in `_shared/codex-config.md#constrained-windows-judge-reads`, keeping read-only isolation and the selected role.
53
+
52
54
  After the availability check passes, Codex keeps the monitored `codex-monitored.sh`
53
55
  path; every other resolved OTHER engine follows `_shared/adapters/<name>.md`
54
56
  `## Invocation`. Capture `.devlyn/<name>-judge.stdout`, then normalize it with
@@ -2,6 +2,7 @@
2
2
  """Deterministic PHASE 6 final-diff gate for /devlyn:resolve."""
3
3
  from __future__ import annotations
4
4
 
5
+ import runpy
5
6
  import argparse
6
7
  import importlib.util
7
8
  import json
@@ -43,6 +44,7 @@ def git(work: pathlib.Path, *args: str) -> subprocess.CompletedProcess[str]:
43
44
  cwd=str(work),
44
45
  capture_output=True,
45
46
  text=True,
47
+ encoding="utf-8",
46
48
  )
47
49
 
48
50
 
@@ -329,7 +331,7 @@ def write_state(devlyn: pathlib.Path, state: dict) -> None:
329
331
 
330
332
  def read_findings(devlyn: pathlib.Path) -> list[dict]:
331
333
  path = devlyn / FINDINGS_NAME
332
- return [SPEC_VERIFY.loads_strict_json(line) for line in path.read_text().splitlines() if line.strip()]
334
+ return [SPEC_VERIFY.loads_strict_json(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip()]
333
335
 
334
336
 
335
337
  def read_summary(devlyn: pathlib.Path) -> dict:
@@ -515,4 +517,5 @@ def main() -> int:
515
517
 
516
518
 
517
519
  if __name__ == "__main__":
520
+ runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
518
521
  raise SystemExit(main())