devlyn-cli 3.1.0 → 3.1.2
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 +6 -1
- package/bin/devlyn.js +69 -23
- package/config/skills/_shared/adapters/claude.md +3 -2
- package/config/skills/_shared/adapters/codex.md +1 -1
- package/config/skills/_shared/archive_run.py +5 -3
- package/config/skills/_shared/codex-config.md +13 -8
- package/config/skills/_shared/codex-monitored.sh +12 -19
- package/config/skills/_shared/collect-codex-findings.py +1 -0
- package/config/skills/_shared/engine-preflight.md +2 -0
- package/config/skills/_shared/finish-gate.py +4 -1
- package/config/skills/_shared/invocation-receipt.py +194 -5
- package/config/skills/_shared/judge-role-evidence.py +33 -6
- package/config/skills/_shared/phase-prompt-render.py +2 -0
- package/config/skills/_shared/platform-support.py +370 -0
- package/config/skills/_shared/process-evidence.py +20 -3
- package/config/skills/_shared/resolve-bootstrap.py +131 -63
- package/config/skills/_shared/resolve-stop-hook.py +3 -1
- package/config/skills/_shared/role-config.py +7 -5
- package/config/skills/_shared/run-bounded.py +35 -21
- package/config/skills/_shared/spec-verify-check.py +393 -167
- package/config/skills/_shared/state-phase-write.py +26 -9
- package/config/skills/_shared/task-complete.py +337 -97
- package/config/skills/_shared/verify-merge-findings.py +9 -8
- package/config/skills/devlyn:ideate/references/spec-template.md +1 -1
- package/config/skills/devlyn:resolve/SKILL.md +10 -7
- package/config/skills/devlyn:resolve/references/free-form-mode.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
- package/config/skills/devlyn:resolve/references/phases/verify.md +14 -10
- package/config/skills/devlyn:resolve/references/state-schema.md +1 -1
- package/config/skills/devlyn:resolve/references/task-completion.md +28 -1
- package/package.json +1 -1
- package/scripts/lint-skills.sh +6 -5
package/README.md
CHANGED
|
@@ -305,9 +305,14 @@ Selected during install. Run `npx devlyn-cli` again to add more.
|
|
|
305
305
|
|
|
306
306
|
## Requirements
|
|
307
307
|
|
|
308
|
-
- **Node.js 18+**
|
|
308
|
+
- **Node.js 18+** and npm
|
|
309
|
+
- **Python 3.11+** available as `python3`, and Git for the resolve harness
|
|
309
310
|
- **[Claude Code](https://docs.anthropic.com/en/docs/claude-code)** installed and configured
|
|
310
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
|
+
|
|
311
316
|
## Contributing
|
|
312
317
|
|
|
313
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
|
|
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
|
-
|
|
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
|
-
|
|
364
|
-
!fs.existsSync(
|
|
365
|
-
|
|
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 =
|
|
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 =
|
|
590
|
+
const src = skillPath(OPTIONAL_SKILLS_SOURCE, skillName);
|
|
551
591
|
const targetDir = getTargetDir();
|
|
552
|
-
const
|
|
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
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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 =
|
|
644
|
-
const
|
|
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(
|
|
649
|
-
fs.rmSync(
|
|
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 --
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
--
|
|
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
|
-
#
|
|
280
|
+
# The dispatcher snapshots file input, seals actual argv, then execs in this group.
|
|
286
281
|
set -m
|
|
287
|
-
"$
|
|
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"
|
|
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' \
|
|
@@ -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())
|