devlyn-cli 4.2.26 → 4.3.3
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/bin/devlyn.js +510 -0
- package/bin/engines.js +20 -15
- package/bin/harness-activation.js +1 -2
- package/bin/release.js +16 -3
- package/bin/skill-history.json +5 -0
- package/config/skills/_shared/harness-store.py +202 -0
- package/config/skills/_shared/task-completion.md +6 -3
- package/config/skills/_shared/warm-build-dirs.py +97 -2
- package/config/skills/devlyn-ideate/SKILL.md +5 -1
- package/config/skills/devlyn-ideate/references/loop.md +12 -2
- package/config/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +5 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +165 -15
- package/harness/AGENTS.md +37 -0
- package/harness/CLAUDE.md +37 -0
- package/native/claude/.claude-plugin/plugin.json +6 -0
- package/native/claude/skills/_shared/.ruff.toml +2 -0
- package/native/claude/skills/_shared/adapters/claude.md +15 -0
- package/native/claude/skills/_shared/adapters/codex.md +14 -0
- package/native/claude/skills/_shared/adapters/omp.md +6 -0
- package/native/claude/skills/_shared/codex-monitored.sh +263 -0
- package/native/claude/skills/_shared/engine-doctor.sh +70 -0
- package/native/claude/skills/_shared/engine-preflight.md +29 -0
- package/native/claude/skills/_shared/expected-contract.py +242 -0
- package/native/claude/skills/_shared/expected.schema.json +120 -0
- package/native/claude/skills/_shared/harness-store.py +202 -0
- package/native/claude/skills/_shared/invocation-receipt.py +499 -0
- package/native/claude/skills/_shared/platform-support.py +395 -0
- package/native/claude/skills/_shared/role-config.py +247 -0
- package/native/claude/skills/_shared/runtime-principles.md +35 -0
- package/native/claude/skills/_shared/task-complete.py +2865 -0
- package/native/claude/skills/_shared/task-completion.md +249 -0
- package/native/claude/skills/_shared/warm-build-dirs.py +1065 -0
- package/native/claude/skills/devlyn-engines/SKILL.md +63 -0
- package/native/claude/skills/devlyn-ideate/SKILL.md +132 -0
- package/native/claude/skills/devlyn-ideate/references/elicitation.md +29 -0
- package/native/claude/skills/devlyn-ideate/references/loop.md +123 -0
- package/native/claude/skills/devlyn-ideate/references/package-format.md +128 -0
- package/native/claude/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +882 -0
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +2191 -0
- package/native/codex/.agents/plugins/marketplace.json +12 -0
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +6 -0
- package/native/codex/plugins/devlyn/skills/_shared/.ruff.toml +2 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/claude.md +15 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/codex.md +14 -0
- package/native/codex/plugins/devlyn/skills/_shared/adapters/omp.md +6 -0
- package/native/codex/plugins/devlyn/skills/_shared/codex-monitored.sh +263 -0
- package/native/codex/plugins/devlyn/skills/_shared/engine-doctor.sh +70 -0
- package/native/codex/plugins/devlyn/skills/_shared/engine-preflight.md +29 -0
- package/native/codex/plugins/devlyn/skills/_shared/expected-contract.py +242 -0
- package/native/codex/plugins/devlyn/skills/_shared/expected.schema.json +120 -0
- package/native/codex/plugins/devlyn/skills/_shared/harness-store.py +202 -0
- package/native/codex/plugins/devlyn/skills/_shared/invocation-receipt.py +499 -0
- package/native/codex/plugins/devlyn/skills/_shared/platform-support.py +395 -0
- package/native/codex/plugins/devlyn/skills/_shared/role-config.py +247 -0
- package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +35 -0
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +2865 -0
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +249 -0
- package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +1065 -0
- package/native/codex/plugins/devlyn/skills/devlyn-engines/SKILL.md +63 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +132 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/elicitation.md +29 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +123 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/package-format.md +128 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/plan-add.md +12 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +882 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +2191 -0
- package/package.json +3 -2
- package/release.json +1630 -0
package/bin/skill-history.json
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"3be2ec1243eb28f779b170e469dfb65555a92dc08b3d927b8973cbd49308125d",
|
|
44
44
|
"3c58989be2f943896810ef709fb115171420a918dfaadc43ea47940fa8ebc7ed",
|
|
45
45
|
"3c81c575cc43d70488865b440204cb399091405d9b7f81dbf047a32d3d5ac045",
|
|
46
|
+
"3cb043970432281f3c2c3c7ec813c4721221ca18ff227e94ef204f2ef2178044",
|
|
46
47
|
"3cd398df22e666fdd2e0f253c5a7464bca62bceefe3737d28a5ef95b75cb3a33",
|
|
47
48
|
"3e39fbe29d0bd607f4ad940f4ca40f9abf82d468e418d0362fb0fd4fa6812475",
|
|
48
49
|
"3e5135877e733f1dcff3b747444228f5eece7cb047956c87a3af02a485cfaad9",
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
"69a3fa5c1e7c5358cf7571936c8c09f5cc1d13999c8490ac10533af2091c4226",
|
|
80
81
|
"6a523474b70ba99aff5472020cdde9ca1e7c91f8ee65db03c9e712adfac6d31d",
|
|
81
82
|
"6d1a5458b23196b6f6fde6b7e9dc097ed173cb5914de439e512a032ca1892812",
|
|
83
|
+
"6e70044cab5a3fb1f78b7148c2111e4298ce8f5f8e902220430ca316bb61255d",
|
|
82
84
|
"6fa2b5e089dcd00613c6e9800d2e763da9e5b2c80e6a7ef5d54d5ad7113e8399",
|
|
83
85
|
"71fb125e20420d6f3fcd3396093fcdc8d552bab9626c3cbd962351e0d19adac8",
|
|
84
86
|
"725395f70eec7e2ceae1f3aa122893433923b098a1e0d6202dd410979ef4eea0",
|
|
@@ -145,6 +147,7 @@
|
|
|
145
147
|
"d3b40b9f0e15bf90ca557c0198a8fe5ff1d046b9aaa54b181c743d8e006f9c34",
|
|
146
148
|
"d424feceffc005f220e33a19cac73f33bd40d120cc14da33725b8a990704bcf6",
|
|
147
149
|
"d7388e36d76647a89ce52de615271928cf9fc78f980df399d2bc2e6d6675ec82",
|
|
150
|
+
"d85fa24b9b2c439742c9a62d5d4e07a031a1cfe6024bbc06f98eff7063424179",
|
|
148
151
|
"dac3da11ea5686382f5c9892a0b8e4ec25e625e5946283d8e2ad944aa84e1df2",
|
|
149
152
|
"db867d1fd98058bb20e41e942abd95f08fbaa9465928025f69f0a0308fa71514",
|
|
150
153
|
"dd0bfdfd2db0b5f9ddef75d1b9ce3247698739fc551d292366dccd506fef6a36",
|
|
@@ -319,6 +322,7 @@
|
|
|
319
322
|
"0dc69c20434966015176d83baa6f95b3421f9e13d0b1bd608a22f0560eae1049",
|
|
320
323
|
"0fe9b270d670f70da9d2535969b6117684b7c1517806b3029ebaf9624da8006c",
|
|
321
324
|
"1582c202d361af927c2d0843d4bcb72ccc98a0a2a9d8344d118d680f70f7e1e4",
|
|
325
|
+
"19207315f23c9a12bf2ced0e4e79bb7dfdc7cc5d0c9dd94095d9c6dfe1e8fea8",
|
|
322
326
|
"1d8c3bd31c8b755acd0c3c9f416acb5024dfa99fbf5e2c14d8d6df00a24fe9b4",
|
|
323
327
|
"300d1af60cc53285935dc4b5b370e157707218d844a4303b96d7826eef36ed43",
|
|
324
328
|
"302b1a6c109087dce02ed6af184d6aeeca6cecfd40ebe638beecf8bdf21cf59c",
|
|
@@ -357,6 +361,7 @@
|
|
|
357
361
|
"d6433bf0f96dd9dd485c3623885255738654674b3e23cfcc1a2cfd24f3cda96e",
|
|
358
362
|
"db8d921aabf0d04811b55465a3757c62c4b39631aebaf6eecc994ca241cfbe56",
|
|
359
363
|
"e25da746dc37b2e82c658a28d9481fd69741151045683eee2119582ddaf731cd",
|
|
364
|
+
"e47a2ce766ae81fa0870e0a8dfbfe737311de69ce620e92217abbe4c532e3c8b",
|
|
360
365
|
"e929f76c15349f1b9bc1e84e0a3e9e48e916168d79270e0f4a36ea8eab9b5388",
|
|
361
366
|
"f2243cc4150e225961825f6097b0cf38166c291dcf58127516ceee841f7ed4c8",
|
|
362
367
|
"fa536198d539a3c85f54dda74d0d78716ac6e6849e733bdadecc8605511eac37",
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Release identity and queue resource checks. Readers never write or create locks.
|
|
2
|
+
|
|
3
|
+
The release verifier is CLI-1's bin/release.js, not a second manifest verifier.
|
|
4
|
+
A Codex projection has .devlyn-harness-projection.json at its plugin root:
|
|
5
|
+
{schemaVersion: 1, release: ReleaseRef, source: "native/codex/plugins/devlyn"}.
|
|
6
|
+
Its complete regular-file tree (except that marker) must match that release subtree.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
PROJECTION = ".devlyn-harness-projection.json"
|
|
19
|
+
CODEX_SOURCE = "native/codex/plugins/devlyn"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HarnessError(Exception):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def require(condition, message):
|
|
27
|
+
if not condition:
|
|
28
|
+
raise HarnessError(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_json(path):
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(Path(path).read_bytes())
|
|
34
|
+
except (OSError, ValueError) as exc:
|
|
35
|
+
raise HarnessError(f"{path}: {exc}") from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def file_identity(path):
|
|
39
|
+
path = Path(path)
|
|
40
|
+
info = path.lstat()
|
|
41
|
+
require(path.is_file() and not path.is_symlink() and info.st_nlink == 1, f"not a regular resource: {path}")
|
|
42
|
+
return {"path": str(path), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def release_ref(value):
|
|
46
|
+
require(isinstance(value, dict) and set(value) == {"package", "version", "digest", "root"}
|
|
47
|
+
and value["package"] == "devlyn-cli"
|
|
48
|
+
and all(isinstance(value[k], str) and value[k] and "\0" not in value[k] for k in ("version", "digest", "root"))
|
|
49
|
+
and re.fullmatch(r"[a-f0-9]{64}", value["digest"]) and Path(value["root"]).is_absolute(), "malformed release reference")
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify_release(root):
|
|
54
|
+
root = Path(root)
|
|
55
|
+
# As harness-status verifies it: an npm-installed package keeps npm's own .bin shims, which are checked.
|
|
56
|
+
script = "const r=require(process.argv[1]);r.runJson(()=>r.verifyRelease(process.argv[2],{npmInstall:true}));"
|
|
57
|
+
result = subprocess.run(["node", "-e", script, str(root / "bin/release.js"), str(root)], capture_output=True, text=True, encoding="utf-8")
|
|
58
|
+
require(result.returncode == 0, f"release verification failed: {root}: {result.stderr.strip()}")
|
|
59
|
+
try:
|
|
60
|
+
release = release_ref(json.loads(result.stdout)["data"]["release"])
|
|
61
|
+
except (KeyError, ValueError, TypeError) as exc:
|
|
62
|
+
raise HarnessError(f"invalid release verifier result: {root}: {exc}") from exc
|
|
63
|
+
manifest = read_json(root / "release.json")
|
|
64
|
+
defaults = file_identity(root / manifest["principles"]["defaults"])
|
|
65
|
+
return {"schemaVersion": 1, "release": release, "defaults": defaults}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def walk_error(error):
|
|
69
|
+
raise error # os.walk otherwise silently skips unreadable projection directories.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def resolve(skills):
|
|
73
|
+
"""Resolve only this invoking skill's real ancestry; never search another install."""
|
|
74
|
+
skill = (Path(skills) / "devlyn-ideate").resolve()
|
|
75
|
+
for parent in (skill, *skill.parents):
|
|
76
|
+
marker = parent / PROJECTION
|
|
77
|
+
if marker.exists() or marker.is_symlink():
|
|
78
|
+
file_identity(marker)
|
|
79
|
+
projection = read_json(marker)
|
|
80
|
+
require(isinstance(projection, dict) and set(projection) == {"schemaVersion", "release", "source"}
|
|
81
|
+
and type(projection["schemaVersion"]) is int and projection["schemaVersion"] == 1
|
|
82
|
+
and projection["source"] == CODEX_SOURCE, f"malformed projection: {marker}")
|
|
83
|
+
ref = release_ref(projection["release"])
|
|
84
|
+
binding = verify_release(ref["root"])
|
|
85
|
+
require(binding["release"] == ref, f"projection release changed: {marker}")
|
|
86
|
+
source = Path(ref["root"]) / CODEX_SOURCE
|
|
87
|
+
require(skill == parent / "skills/devlyn-ideate", f"unexpected projection skill: {skill}")
|
|
88
|
+
expected = {p.relative_to(source) for p in source.rglob("*") if p.is_file()}
|
|
89
|
+
actual = set()
|
|
90
|
+
for directory, dirs, files in os.walk(parent, onerror=walk_error):
|
|
91
|
+
for name in dirs:
|
|
92
|
+
require(not (Path(directory) / name).is_symlink(), f"projection directory link: {directory}/{name}")
|
|
93
|
+
for name in files:
|
|
94
|
+
file = Path(directory) / name
|
|
95
|
+
if file == marker:
|
|
96
|
+
continue
|
|
97
|
+
rel = file.relative_to(parent)
|
|
98
|
+
actual.add(rel)
|
|
99
|
+
require(rel in expected and file_identity(file)["sha256"] == file_identity(source / rel)["sha256"]
|
|
100
|
+
and (os.name == "nt" or bool(file.stat().st_mode & 0o111) == bool((source / rel).stat().st_mode & 0o111)),
|
|
101
|
+
f"projection resource changed: {file}")
|
|
102
|
+
require(actual == expected, f"projection file list changed: {parent}")
|
|
103
|
+
return binding
|
|
104
|
+
manifest = parent / "release.json"
|
|
105
|
+
if (manifest.exists() or manifest.is_symlink()) and skill in [parent / p / "devlyn-ideate" for p in
|
|
106
|
+
("config/skills", "native/claude/skills", CODEX_SOURCE + "/skills")]:
|
|
107
|
+
return verify_release(parent)
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def read_binding(common):
|
|
112
|
+
path = Path(common) / "devlyn-loops/harness.json"
|
|
113
|
+
if not path.exists() and not path.is_symlink():
|
|
114
|
+
return None
|
|
115
|
+
file_identity(path)
|
|
116
|
+
binding = read_json(path)
|
|
117
|
+
require(isinstance(binding, dict) and set(binding) == {"schemaVersion", "release", "defaults"}
|
|
118
|
+
and type(binding["schemaVersion"]) is int and binding["schemaVersion"] == 1, f"incompatible queue binding: {path}")
|
|
119
|
+
release_ref(binding["release"])
|
|
120
|
+
resource(binding["defaults"])
|
|
121
|
+
return binding
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def resource(value):
|
|
125
|
+
require(isinstance(value, dict) and set(value) == {"path", "sha256"}
|
|
126
|
+
and isinstance(value["path"], str) and "\0" not in value["path"] and Path(value["path"]).is_absolute()
|
|
127
|
+
and isinstance(value["sha256"], str) and re.fullmatch(r"[a-f0-9]{64}", value["sha256"]), "malformed resource binding")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def recovery_argv(binding, argv):
|
|
131
|
+
return [sys.executable, str(Path(binding["release"]["root"]) / "config/skills/devlyn-ideate/scripts/queue.py"), *argv]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def check_binding(binding, invoking):
|
|
135
|
+
if binding is None:
|
|
136
|
+
return
|
|
137
|
+
require(invoking is not None and binding["release"] == invoking["release"], "queue-release-mismatch")
|
|
138
|
+
require(binding == invoking, "queue defaults changed; restore the bound release")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def packet_schema(packet):
|
|
142
|
+
require(isinstance(packet, dict), "task packet must be an object")
|
|
143
|
+
version = packet.get("schema_version", 1)
|
|
144
|
+
require(type(version) is int and version in (1, 2), f"incompatible task packet version: {version!r}")
|
|
145
|
+
if version == 1:
|
|
146
|
+
return
|
|
147
|
+
require("harness" in packet and isinstance(packet.get("methodology"), dict), "malformed packet v2 harness/methodology")
|
|
148
|
+
methodology = packet["methodology"]
|
|
149
|
+
require(set(methodology) == {"defaults", "projectInstructions"} and isinstance(methodology["projectInstructions"], list),
|
|
150
|
+
"malformed packet v2 methodology")
|
|
151
|
+
for entry in methodology["projectInstructions"]:
|
|
152
|
+
resource(entry)
|
|
153
|
+
if packet["harness"] is None:
|
|
154
|
+
defaults = methodology["defaults"]
|
|
155
|
+
require(packet.get("workerPrompt") is None and (defaults is None or (isinstance(defaults, dict) and defaults.get("source") == "skill")),
|
|
156
|
+
"legacy packet has release resources")
|
|
157
|
+
if defaults is not None:
|
|
158
|
+
resource({k: v for k, v in defaults.items() if k != "source"})
|
|
159
|
+
return
|
|
160
|
+
release_ref(packet["harness"])
|
|
161
|
+
defaults = methodology["defaults"]
|
|
162
|
+
require(isinstance(defaults, dict) and defaults.get("source") == "release", "packet has no verified release defaults source")
|
|
163
|
+
resource({k: v for k, v in defaults.items() if k != "source"})
|
|
164
|
+
resource(packet.get("workerPrompt"))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def verify_packet(packet, path, skills):
|
|
168
|
+
packet_schema(packet)
|
|
169
|
+
if packet.get("schema_version", 1) == 1:
|
|
170
|
+
return # Preserve v1 execution without upgrading its resources.
|
|
171
|
+
receipt = packet.get("receipt")
|
|
172
|
+
require(isinstance(receipt, str) and "\0" not in receipt and Path(receipt).is_absolute()
|
|
173
|
+
and Path(receipt).name == "receipt.json" and Path(receipt).parent.parent.name == "devlyn-completion"
|
|
174
|
+
and Path(receipt) == Path(path).with_name("receipt.json"),
|
|
175
|
+
"malformed packet receipt path")
|
|
176
|
+
binding = read_binding(Path(receipt).parent.parent.parent)
|
|
177
|
+
if packet["harness"] is None:
|
|
178
|
+
require(binding is None, "bound task packet has no harness/defaults source")
|
|
179
|
+
# An unbound installation's own defaults change with its normal updates: never gate acceptance or delivery on
|
|
180
|
+
# their bytes (queue.py checks only their presence, before a spawn).
|
|
181
|
+
return
|
|
182
|
+
require(binding is not None, "packet queue binding is missing; restore harness.json")
|
|
183
|
+
invoking = resolve(skills)
|
|
184
|
+
ref = packet["harness"]
|
|
185
|
+
require(invoking is not None and invoking["release"] == ref,
|
|
186
|
+
f"packet-release-mismatch; use {Path(ref['root']) / 'config/skills/devlyn-ideate/scripts/queue.py'}")
|
|
187
|
+
check_binding(binding, invoking)
|
|
188
|
+
defaults = {"source": "release", **binding["defaults"]}
|
|
189
|
+
require(packet["methodology"]["defaults"] == defaults, "packet defaults differ from queue binding")
|
|
190
|
+
expected_runner = [str(Path(ref["root"]) / "config/skills/devlyn-ideate/scripts/acceptance.py"),
|
|
191
|
+
"run", "--packet", str(path)]
|
|
192
|
+
require(isinstance(packet.get("runner"), list) and packet["runner"][1:] == expected_runner, "packet runner differs from bound release")
|
|
193
|
+
prompt = packet["workerPrompt"]
|
|
194
|
+
require(prompt["path"] == str(Path(path).parent / "worker-prompt.md") and file_identity(prompt["path"]) == prompt,
|
|
195
|
+
"worker prompt changed or missing; restore the bound resource")
|
|
196
|
+
for entry in packet["methodology"]["projectInstructions"]:
|
|
197
|
+
name = Path(entry["path"]).name
|
|
198
|
+
require(name in ("CLAUDE.md", "AGENTS.md") and entry["path"] == str(Path(packet["worktree"]) / name),
|
|
199
|
+
"project instruction path escapes packet worktree")
|
|
200
|
+
committed = subprocess.run(["git", "-C", packet["worktree"], "cat-file", "blob", f"{packet['inputs_sha']}:{name}"], capture_output=True)
|
|
201
|
+
require(committed.returncode == 0 and hashlib.sha256(committed.stdout).hexdigest() == entry["sha256"],
|
|
202
|
+
f"project instruction differs from inputs commit: {name}")
|
|
@@ -108,13 +108,16 @@ removes them. Preserve all other work and report anything left behind.
|
|
|
108
108
|
|
|
109
109
|
Build in the task worktree's own default build directory, never in one
|
|
110
110
|
another checkout builds in, even one build at a time: tools such as Cargo take
|
|
111
|
-
one checkout's outputs as fresh for the other. When
|
|
111
|
+
one checkout's outputs as fresh for the other. When the shared repository-local
|
|
112
|
+
Git config `devlyn.warmBuildDirs` is true, or it is unset and the Cargo workspace's
|
|
112
113
|
Cargo.toml sets `[workspace.metadata.devlyn] warm-build-dirs = true`,
|
|
113
114
|
allocation seeds that directory from a built checkout of the repository, so
|
|
114
115
|
the first build does not rebuild every dependency; `build_dirs` in its result
|
|
115
116
|
and receipt says what it did, and a skip means an ordinary cold build. The
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
Git setting takes precedence on this machine: false disables and invalid values
|
|
118
|
+
skip; global, system, include and per-worktree values are not consulted. Seeding
|
|
119
|
+
keeps dependency outputs that may hold the other checkout's paths, and those
|
|
120
|
+
follow that checkout's later rebuilds. `warm-build-dirs.py <worktree>` does the same for a
|
|
118
121
|
worktree made without allocation. A warmed directory leaves with the worktree
|
|
119
122
|
after merge, so a pending, failed or local-only task keeps it.
|
|
120
123
|
|
|
@@ -31,6 +31,9 @@ a crate that compiled a path in where the reading above does not see it) still n
|
|
|
31
31
|
donor's files: when the donor rebuilds or removes them, the recipient can change behavior
|
|
32
32
|
without a warning. That is why a workspace opts in, with `[workspace.metadata.devlyn]
|
|
33
33
|
warm-build-dirs = true` in its Cargo.toml, once its dependencies do not use such paths.
|
|
34
|
+
The repository's shared local Git config `devlyn.warmBuildDirs` overrides that opt-in on
|
|
35
|
+
this machine: true enables, false disables, unset uses Cargo.toml, and invalid values skip.
|
|
36
|
+
Global, system, include and per-worktree config values are not consulted.
|
|
34
37
|
|
|
35
38
|
The donor is only read: its dev build lock is taken from an existing lock file opened read-only,
|
|
36
39
|
without waiting, while the layouts are cloned. The whole operation has a time limit. Every skip
|
|
@@ -89,6 +92,14 @@ class Skip(Exception):
|
|
|
89
92
|
"""An expected reason not to warm; the build starts cold."""
|
|
90
93
|
|
|
91
94
|
|
|
95
|
+
class CommandFailed(Skip):
|
|
96
|
+
"""A command failed; retain its exit code and diagnostics for callers."""
|
|
97
|
+
|
|
98
|
+
def __init__(self, argv, result):
|
|
99
|
+
self.result = result
|
|
100
|
+
super().__init__(f"{' '.join(map(str, argv[:3]))} failed: {(result.stderr or result.stdout).strip()[-400:]}")
|
|
101
|
+
|
|
102
|
+
|
|
92
103
|
class Unsafe(Exception):
|
|
93
104
|
"""A safety check failed; nothing was published and the donor was not written."""
|
|
94
105
|
|
|
@@ -120,7 +131,7 @@ def run(argv, *, cwd, deadline, env=None):
|
|
|
120
131
|
except subprocess.TimeoutExpired as exc:
|
|
121
132
|
raise TimeUp(f"{' '.join(map(str, argv[:3]))} timed out") from exc
|
|
122
133
|
if result.returncode != 0:
|
|
123
|
-
raise
|
|
134
|
+
raise CommandFailed(argv, result)
|
|
124
135
|
return result.stdout
|
|
125
136
|
|
|
126
137
|
|
|
@@ -166,7 +177,19 @@ def no_symlink_between(root, path):
|
|
|
166
177
|
def destination(work, deadline, stack):
|
|
167
178
|
"""The worktree's own Cargo target directory and a descriptor for its parent, when it can be warmed."""
|
|
168
179
|
found = metadata(work, deadline)
|
|
169
|
-
|
|
180
|
+
setting = "git config --local devlyn.warmBuildDirs"
|
|
181
|
+
try:
|
|
182
|
+
enabled = git(work, "config", "--local", "--type=bool", "--get", "devlyn.warmBuildDirs", deadline=deadline)
|
|
183
|
+
except CommandFailed as exc:
|
|
184
|
+
if exc.result.returncode != 1:
|
|
185
|
+
error = (exc.result.stderr or exc.result.stdout or str(exc)).splitlines()[0]
|
|
186
|
+
raise Skip(f"cannot read {setting}: {error}") from exc
|
|
187
|
+
enabled = None
|
|
188
|
+
except OSError as exc:
|
|
189
|
+
raise Skip(f"cannot read {setting}: {exc}") from exc
|
|
190
|
+
if enabled == "false":
|
|
191
|
+
raise Skip(f"turned off: {setting} is false")
|
|
192
|
+
if enabled != "true" and ((found.get("metadata") or {}).get("devlyn") or {}).get("warm-build-dirs") is not True:
|
|
170
193
|
raise Skip("not enabled: the workspace's Cargo.toml has no [workspace.metadata.devlyn] warm-build-dirs = true")
|
|
171
194
|
if any(key in os.environ for key in REDIRECTS) and metadata(work, deadline, env=without_redirects()) != found:
|
|
172
195
|
raise Skip("the target directory comes from this environment, which later commands may not share")
|
|
@@ -799,6 +822,78 @@ class WarmTests(unittest.TestCase):
|
|
|
799
822
|
self.assertEqual([], list(self.staging.iterdir()))
|
|
800
823
|
self.assertEqual("", self.git(self.work, "status", "--porcelain", "--untracked-files=all"))
|
|
801
824
|
|
|
825
|
+
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
826
|
+
def test_local_git_true_seeds_without_cargo_opt_in(self):
|
|
827
|
+
self.write_fixture(workspace_metadata=None)
|
|
828
|
+
self.git(self.anchor, "config", "--local", "devlyn.warmBuildDirs", "true")
|
|
829
|
+
[result] = self.warm()
|
|
830
|
+
self.assertEqual("seeded", result["status"], result)
|
|
831
|
+
self.assertTrue((self.work / "target" / "debug" / "deps" / f"libserde-{'1' * 16}.rlib").is_file())
|
|
832
|
+
self.assert_nothing_left()
|
|
833
|
+
|
|
834
|
+
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
835
|
+
def test_local_git_false_overrides_cargo_opt_in(self):
|
|
836
|
+
self.git(self.anchor, "config", "--local", "devlyn.warmBuildDirs", "false")
|
|
837
|
+
[result] = self.warm()
|
|
838
|
+
self.assertEqual("skipped", result["status"], result)
|
|
839
|
+
self.assertEqual("turned off: git config --local devlyn.warmBuildDirs is false", result["reason"])
|
|
840
|
+
self.assertFalse((self.work / "target").exists())
|
|
841
|
+
self.assert_nothing_left()
|
|
842
|
+
|
|
843
|
+
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
844
|
+
def test_invalid_local_git_value_skips_with_git_error(self):
|
|
845
|
+
self.git(self.anchor, "config", "--local", "devlyn.warmBuildDirs", "invalid")
|
|
846
|
+
[result] = self.warm()
|
|
847
|
+
self.assertEqual("skipped", result["status"], result)
|
|
848
|
+
self.assertIn("git config --local devlyn.warmBuildDirs", result["reason"])
|
|
849
|
+
self.assertIn("fatal: bad boolean config value 'invalid' for 'devlyn.warmbuilddirs'", result["reason"])
|
|
850
|
+
self.assertFalse((self.work / "target").exists())
|
|
851
|
+
self.assert_nothing_left()
|
|
852
|
+
|
|
853
|
+
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
854
|
+
def test_unset_local_git_value_uses_cargo_opt_in(self):
|
|
855
|
+
for opted_in in (False, True):
|
|
856
|
+
with self.subTest(opted_in=opted_in):
|
|
857
|
+
self.write_fixture(workspace_metadata={"devlyn": {"warm-build-dirs": opted_in}})
|
|
858
|
+
[result] = self.warm()
|
|
859
|
+
self.assertEqual("seeded" if opted_in else "skipped", result["status"], result)
|
|
860
|
+
if not opted_in:
|
|
861
|
+
self.assertIn("not enabled", result["reason"])
|
|
862
|
+
self.assert_nothing_left()
|
|
863
|
+
|
|
864
|
+
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
865
|
+
def test_other_git_config_failures_skip_with_first_error_line(self):
|
|
866
|
+
config = self.anchor / ".git" / "config"
|
|
867
|
+
config.write_text(config.read_text() + "\n[broken\nsecond bad line\n")
|
|
868
|
+
error = subprocess.run(["git", "-C", str(self.work), "config", "--local", "--type=bool",
|
|
869
|
+
"--get", "devlyn.warmBuildDirs"], capture_output=True, text=True)
|
|
870
|
+
self.assertNotIn(error.returncode, (0, 1))
|
|
871
|
+
[result] = self.warm()
|
|
872
|
+
self.assertEqual("skipped", result["status"], result)
|
|
873
|
+
self.assertEqual(f"cannot read git config --local devlyn.warmBuildDirs: {error.stderr.splitlines()[0]}",
|
|
874
|
+
result["reason"])
|
|
875
|
+
self.assertFalse((self.work / "target").exists())
|
|
876
|
+
self.assertEqual([], list(self.staging.iterdir()))
|
|
877
|
+
|
|
878
|
+
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
879
|
+
def test_global_include_and_per_worktree_config_do_not_override_cargo(self):
|
|
880
|
+
global_config, included = self.root / "global-config", self.root / "included-config"
|
|
881
|
+
os.environ["GIT_CONFIG_GLOBAL"] = str(global_config)
|
|
882
|
+
self.git(self.anchor, "config", "--local", "include.path", str(included))
|
|
883
|
+
self.git(self.anchor, "config", "--local", "extensions.worktreeConfig", "true")
|
|
884
|
+
for opted_in in (False, True):
|
|
885
|
+
with self.subTest(opted_in=opted_in):
|
|
886
|
+
value = "false" if opted_in else "true"
|
|
887
|
+
for config in (global_config, included):
|
|
888
|
+
config.write_text(f"[devlyn]\n\twarmBuildDirs = {value}\n")
|
|
889
|
+
self.git(self.work, "config", "--worktree", "devlyn.warmBuildDirs", value)
|
|
890
|
+
self.write_fixture(workspace_metadata={"devlyn": {"warm-build-dirs": opted_in}})
|
|
891
|
+
[result] = self.warm()
|
|
892
|
+
self.assertEqual("seeded" if opted_in else "skipped", result["status"], result)
|
|
893
|
+
if not opted_in:
|
|
894
|
+
self.assertIn("not enabled", result["reason"])
|
|
895
|
+
self.assert_nothing_left()
|
|
896
|
+
|
|
802
897
|
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
803
898
|
def test_seeds_only_what_carries_over_and_never_writes_the_donor(self):
|
|
804
899
|
before = self.snapshot(self.donor)
|
|
@@ -94,10 +94,14 @@ A removed flag stops with its instruction and selects no other behavior.
|
|
|
94
94
|
|
|
95
95
|
An absolute path to a package's `meta.md` is added as it is; anything else is planned first. Add with `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" add '<absolute meta.md>'`, the only writer of a loop's queue file `docs/specs/<loop-id>/queue.md`; never edit one by hand. In every delivery mode add makes no commit on the checked-out branch and leaves the branch, the index and tracked files as they are: it holds the package and its rows at `refs/devlyn/captures/<loop-id>`, records the add and removes the package copies equal to that capture. Report the capture ref and its `git show` command, any `cleanup` entry, and that a revision needs a new loop id. A retry of the same add reports it as added, also once the copies are gone. Tasks start from committed state, a local loop from the branch it was added on and `auto`/`pr` from the remote base, so commit, and for `auto`/`pr` push, whatever the plan depends on. A pending legacy raw-intent row of `docs/specs/queue.md` is replaced in place by a package whose `## Intent` reproduces the row verbatim, added with `--materialize <line>`.
|
|
96
96
|
|
|
97
|
+
Release-backed queues retain their verified release in `<git-common-dir>/devlyn-loops/harness.json` until all tasks, recovery and delivery settle. A different release refuses with the exact recovery argv; use it. Existing unfinished unbound queues and standalone installations remain legacy.
|
|
98
|
+
|
|
97
99
|
## status
|
|
98
100
|
|
|
99
101
|
Run `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" status --repo .` and report its counts, `next`, `blockers`, `warnings`, pending deliveries with their resume commands and any `cleanup` entry.
|
|
100
102
|
|
|
103
|
+
For read-only consumers, `python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" inspect --repo . --json` returns the status summary plus `binding` and `compatibility`, without cleanup, locks or writes.
|
|
104
|
+
|
|
101
105
|
## drain
|
|
102
106
|
|
|
103
107
|
`status`, `add` and `drain` write the repository's Git directory, and auto/pr delivery needs network access, so a sandboxed host runs them with escalation or full access.
|
|
@@ -112,7 +116,7 @@ Follow [loop.md](references/loop.md). The executor is the configured route, the
|
|
|
112
116
|
Another engine needs an argv that starts one fresh non-interactive session able, without prompts, to edit, run commands and commit in its task worktree and to write the packet's submission file. `<prompt>` carries the absolute binding values, while `{packet}` stays literal for the drain to fill:
|
|
113
117
|
|
|
114
118
|
```text
|
|
115
|
-
Execute the devlyn loop task packet {packet}: work only in its owned worktree under the installed instructions; meet its obligations, then write its submission and any review records as "Executor exchange" and "Review records" in <DEVLYN_SKILL_DIR>/references/loop.md specify.
|
|
119
|
+
Execute the devlyn loop task packet {packet}: read its workerPrompt when present; read methodology.defaults.path when named and apply those verified defaults, with explicit user and native owner/project instructions taking precedence; work only in its owned worktree under the installed instructions; meet its obligations, then write its submission and any review records as "Executor exchange" and "Review records" in <DEVLYN_SKILL_DIR>/references/loop.md specify. Use the literal DEVLYN_SKILL_DIR and DEVLYN_SHARED_DIR bindings in workerPrompt when present; otherwise set DEVLYN_SKILL_DIR=<DEVLYN_SKILL_DIR> and DEVLYN_SHARED_DIR=<DEVLYN_SHARED_DIR> from these literal values. Never use inherited environment values. Never push, open a pull request or merge: the drain delivers.
|
|
116
120
|
```
|
|
117
121
|
|
|
118
122
|
```sh
|
|
@@ -28,7 +28,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
|
|
|
28
28
|
- Local: the first task uses `--local-base <HEAD of the branch the loop was added on>`, the add record's branch, never whichever branch is checked out; that HEAD must descend from the manifest `base_sha`, or the task waits naming it. Instructions committed there before this first allocation are therefore used. Its receipt keeps that start, and a later task with no accepted predecessor reuses it, also after the first task failed; a fixed start is never recalculated. Later tasks use `--from-receipt <latest accepted receipt of the loop>`, which starts from that receipt's exact `source_sha`, never its terminal commit, current HEAD or workspace. Every prerequisite's accepted source must be an ancestor of that frontier; divergent dependencies need a planned integration task, because the driver never merges. A failure leaves the frontier unchanged.
|
|
29
29
|
- `auto`/`pr`: `--start` the refreshed remote base, never an anchor-local commit, so no unpushed anchor commit reaches a task PR; push whatever the plan depends on. A task allocated while that base lacks `docs/specs/<loop-id>/` is the loop's carrier; a carrier that fails is never published, and the next task allocated becomes the carrier. The start must contain each prerequisite's merge commit, checked before allocation and again before every execution or resume. A squash merge needs no original-source ancestry. A refresh failure waits for restored remote access or the base ref. A missing merge before allocation waits for that merge to be restored on the remote base; an allocated baseline is immutable and needs a new loop or verified recovery of a never-executed allocation.
|
|
30
30
|
4. **Commit scoped inputs** on the owned branch before any executor write, only what the allocation base lacks. A base without `docs/specs/<loop-id>/` gets the package directory from the capture with the loop's queue file, its settled rows keeping their marks (receipt-proven `[x]` and `[F]`, prerequisite-blocked `[F]`): a local loop's first task, or an `auto`/`pr` carrier, whose PR lands the package before tasks diverge. Otherwise a local task commits the base's queue file with the loop's receipt-proven terminal rows (typically a predecessor's `[x]`), every other byte unchanged, and an `auto`/`pr` task commits nothing, so its PR changes only its own row and its product. A base that already carries them gets no inputs commit; unrelated product commits stay out.
|
|
31
|
-
5. **Exchange one packet.** Write `<receipt dir>/packet.json`, then run the executor. An executor that exits without writing its submission leaves its task active, waiting with the exit code and `executor.stderr`, and the next drain runs it again; one that cannot start keeps its allocation, packet and durable start/not-started log, waits for executable, permissions or workspace repair, and retries on the next drain.
|
|
31
|
+
5. **Exchange one packet.** Verify the bound release, defaults, packet and worker prompt before execution or recovery; changed or missing resources refuse dispatch. Write `<receipt dir>/packet.json`, then run the executor. An executor that exits without writing its submission leaves its task active, waiting with the exit code and `executor.stderr`, and the next drain runs it again; one that cannot start keeps its allocation, packet and durable start/not-started log, waits for executable, permissions or workspace repair, and retries on the next drain.
|
|
32
32
|
6. **Derive acceptance** with `acceptance.py accept`. It binds the committed contract to the packet digests; requires the candidate to be the owned branch head with HEAD checked out on that branch, to descend from the inputs, to leave the package (its queue file included) and `docs/specs/queue.md` untouched and to have a clean worktree; executes each declared command on that source, recording raw stdout/stderr and exit, timeout or spawn outcomes (a runner result is reused only when it is wholly clean — no reasons, so no failed check and no source change during its checks — for the same source and contract digests, with unchanged streams); evaluates the file and diff guards against the inputs; confirms the source is unchanged; checks review records; and writes `.devlyn/loop/acceptance.json`. Missing checks, missing review coverage and open binding findings cannot produce `[x]`, and acceptance weakening is not repair. This establishes command outcomes and evidence completeness, not a reviewer's semantic judgment; deliberate evidence forgery is outside the trust model.
|
|
33
33
|
7. **Bind before terminal metadata.** `task-complete.py accept --receipt <receipt> --acceptance <worktree>/.devlyn/loop/acceptance.json` takes evidence custody and points the recovery ref at the source. Failed results are bound the same way; they never become a frontier or a publishable product.
|
|
34
34
|
8. **Commit the terminal transition** under the queue lock: one commit whose sole parent is the bound source and whose only change is this task's row in `docs/specs/<loop-id>/queue.md` (`[x]`, or `[F] — <first reason> (receipt <id>)`), validated as the only legal transition. Then `task-complete.py attach --receipt <receipt> --commit <terminal> --file docs/specs/<loop-id>/queue.md` records it separately from the source and moves the recovery ref to it.
|
|
@@ -53,6 +53,14 @@ Expected task-local operational failures leave the selected task waiting with `<
|
|
|
53
53
|
|
|
54
54
|
Package cleanup, parent pruning and report failures appear as diagnostics under `cleanup`, with retained paths and repair guidance; they do not change product acceptance or completed delivery. The next drain retries them.
|
|
55
55
|
|
|
56
|
+
## Release binding and read-only consumers
|
|
57
|
+
|
|
58
|
+
`<git-common-dir>/devlyn-loops/harness.json` is authoritative across linked worktrees: `{schemaVersion: 1, release: {package, version, digest, root}, defaults: {path, sha256}}`. Paths are absolute and hashes cover exact bytes. The queue lock covers binding before the first capture. An interrupted first add, unfinished tasks, recovery conflicts and pending delivery retain it; settlement removes it. A different release refuses with `recovery argv` as a JSON argv array, preserving the caller’s arguments. No caller substitutes another implementation.
|
|
59
|
+
|
|
60
|
+
`queue.py inspect --repo <dir> --json` reads without cleanup, locks or writes. It returns the status summary plus `binding` (null for legacy) and `compatibility`: `compatible`, `legacy`, `reasons`, `recoveryArgv`, `taskPacket` ranges and `queueBinding` ranges. Incompatible or unreadable state is reported, never migrated. Apps should use this reader instead of `status`, which still reconciles cleanup. For a mismatch, `recoveryArgv` names the bound queue’s inspect invocation; use the same bound queue entry point for authorized add/drain operations.
|
|
61
|
+
|
|
62
|
+
Codex projection writers place `.devlyn-harness-projection.json` at the copied plugin root, beside `skills` and `.codex-plugin`: `{schemaVersion: 1, release: <ReleaseRef>, source: "native/codex/plugins/devlyn"}`. The reader verifies the named release and every projected file’s bytes/mode against that subtree (excluding only the marker). Symlinked skill invocations resolve through their real path. Standalone installs and existing unfinished unbound queues retain legacy behavior; new legacy packets use v2 with a null release, and their `defaults` name the invoking installation's own `_shared/runtime-principles.md` (`source: "skill"`). No engine applicability is inferred from instruction filenames: bound queues use verified release defaults with native instructions taking precedence. Worker and runner paths point to the bound release, independent of a projection cache’s lifetime.
|
|
63
|
+
|
|
56
64
|
## Executor exchange
|
|
57
65
|
|
|
58
66
|
The packet (`<receipt dir>/packet.json`) carries everything a fresh executor needs:
|
|
@@ -66,7 +74,9 @@ The packet (`<receipt dir>/packet.json`) carries everything a fresh executor nee
|
|
|
66
74
|
| `requirements`, `review_requirements` | Requirement IDs and those needing review evidence. |
|
|
67
75
|
| `shared_constraints` | The meta-prompt's `Constraints and exclusions`. |
|
|
68
76
|
| `dependencies` | `{task, receipt, source_sha}` of each accepted prerequisite. |
|
|
69
|
-
| `
|
|
77
|
+
| `schema_version`, `harness` | New packets use version 2 and the bound release reference, or null for legacy unbound queues. Existing v1 packets execute unchanged. |
|
|
78
|
+
| `methodology` | `projectInstructions`: `{path, sha256}` of instruction bytes at the inputs commit. `defaults`: `{source: "release", path, sha256}` from the verified binding; for an unbound queue `{source: "skill", path, sha256}` of the installation's own `_shared/runtime-principles.md`. A task whose executor would get no principles at all (no defaults and no committed CLAUDE.md/AGENTS.md) waits with that reason instead of running. Read the named defaults; explicit user and native owner/project instructions take precedence. No native engine applicability is inferred from filenames. |
|
|
79
|
+
| `workerPrompt` | `{path, sha256}` of the task’s worker instructions, or null for legacy unbound queues. Read it before working. |
|
|
70
80
|
| `delivery` | `local-only`, `auto` or `pr`. |
|
|
71
81
|
| `evidence_dir`, `runner` | Where loop evidence lives (`<worktree>/.devlyn/loop`, ignored by Git) and the runner argv. |
|
|
72
82
|
| `submission`, `obligations` | Where to write the submission, and the completion obligations. |
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Planning and capture only
|
|
2
|
+
|
|
3
|
+
This reference grants plan and add operations only. Never drain, implement, run an executor or deliver code from this planning session.
|
|
4
|
+
|
|
5
|
+
Resolve this reference’s actual containing skill directory (the parent of `references`), resolving symlinks; bind it as `DEVLYN_SKILL_DIR`. Resolve its sibling `_shared` as `DEVLYN_SHARED_DIR`. Verify the loaded files; missing or conflicting source identity is `BLOCKED:skill-source-unresolved`. Never substitute another installation or inherited environment values.
|
|
6
|
+
|
|
7
|
+
1. Inspect project facts and follow [elicitation.md](elicitation.md). Ask only when the unresolved answer changes authorized behavior, scope, data semantics, acceptance or delivery. Existing authorization remains effective. Under autonomous planning, use conventional, reversible defaults for ordinary details and record each assumption; material ambiguity stops affected work as needs-review with a concrete question. Independent authorized planning may continue.
|
|
8
|
+
2. Write `docs/specs/<loop-id>/meta.md` and self-contained task contracts per [package-format.md](package-format.md). Preserve an input document unchanged and carry every substantive requirement into the package. Use this execution policy in the package: for ordinary details within authorized scope, follow established project conventions, otherwise the narrowest literal or conventional reading consistent with the request; choices must be low-consequence and reversible, recorded with their reasons. Material ambiguity means competing intended outcomes, unresolved persistent data/state semantics or public surface beyond the request; stop affected work as needs-review with a concrete question. Independent authorized work may continue. Never omit requested work or weaken acceptance. Set delivery from explicit restrictions or the project completion policy; without one GitHub origin, record local-only as an assumption, or ask when the user explicitly requested a PR/merge.
|
|
9
|
+
3. Run `python3 "<DEVLYN_SKILL_DIR>/scripts/queue.py" check '<absolute meta.md>'` and repair until `VALID`. Plan stops here: report the path, tasks, dependencies and assumptions.
|
|
10
|
+
4. When add is authorized, run `python3 "<DEVLYN_SKILL_DIR>/scripts/queue.py" add '<absolute meta.md>'`. It alone writes queue state, captures without committing on the checked-out branch and removes matching untracked package copies. Report the capture ref, its `git show` command and cleanup diagnostics. Revisions need a new loop id. A legacy pending row is materialized with `--materialize <line>` and its verbatim intent in the package.
|
|
11
|
+
|
|
12
|
+
Report capture and stop. A separate authorized execution session owns drain.
|
|
@@ -89,6 +89,11 @@ def load_packet(path):
|
|
|
89
89
|
packet = read_json(path)
|
|
90
90
|
if not isinstance(packet, dict) or PACKET_KEYS - set(packet):
|
|
91
91
|
raise AcceptanceError(f"{path}: task packet lacks {sorted(PACKET_KEYS - set(packet if isinstance(packet, dict) else {}))}")
|
|
92
|
+
helper = shared("harness-store")
|
|
93
|
+
try:
|
|
94
|
+
helper["verify_packet"](packet, path, SKILLS)
|
|
95
|
+
except (helper["HarnessError"], OSError) as exc:
|
|
96
|
+
raise AcceptanceError(str(exc)) from exc
|
|
92
97
|
return packet
|
|
93
98
|
|
|
94
99
|
|