devlyn-cli 4.3.30 → 4.3.32
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/AGENTS.md +1 -1
- package/CLAUDE.md +1 -1
- package/README.md +3 -1
- package/bin/devlyn.js +33 -8
- package/bin/harness-activation.js +4 -1
- package/bin/harness-status.js +15 -8
- package/bin/instruction-templates.json +2 -0
- package/bin/skill-history.json +12 -1
- package/config/skills/_shared/bring-in.py +10 -0
- package/config/skills/_shared/runtime-principles.md +1 -1
- package/config/skills/_shared/task-complete.py +319 -54
- package/config/skills/_shared/task-completion.md +5 -5
- package/config/skills/_shared/warm-build-dirs.py +13 -0
- package/config/skills/devlyn-ideate/references/executor.md +1 -1
- package/config/skills/devlyn-ideate/references/loop.md +1 -1
- package/config/skills/devlyn-ideate/scripts/acceptance.py +11 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +17 -1
- package/harness/AGENTS.md +2 -2
- package/harness/CLAUDE.md +2 -2
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/bring-in.py +10 -0
- package/native/claude/skills/_shared/runtime-principles.md +1 -1
- package/native/claude/skills/_shared/task-complete.py +319 -54
- package/native/claude/skills/_shared/task-completion.md +5 -5
- package/native/claude/skills/_shared/warm-build-dirs.py +13 -0
- package/native/claude/skills/devlyn-ideate/references/executor.md +1 -1
- package/native/claude/skills/devlyn-ideate/references/loop.md +1 -1
- package/native/claude/skills/devlyn-ideate/scripts/acceptance.py +11 -0
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +17 -1
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/bring-in.py +10 -0
- package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +319 -54
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +5 -5
- package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +13 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/executor.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/acceptance.py +11 -0
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +17 -1
- package/optional-skills/code-review-standards/SKILL.md +10 -6
- package/package.json +1 -1
- package/release.json +45 -45
|
@@ -201,14 +201,14 @@ publication mode pushes no task commit and opens no PR. Delivery is `COMPLETE` w
|
|
|
201
201
|
`pr` pushes the exact accepted task ref and creates/reuses its exact repository,
|
|
202
202
|
head and base PR. While OPEN, it cancels owned auto-merge, reports `PR` with its URL
|
|
203
203
|
and retains the checkout. Once MERGED by any method, either mode cleans released
|
|
204
|
-
resources in that call, on resume or at the next allocation. Default `auto` also
|
|
205
|
-
|
|
206
|
-
checks/reviews and merge policy remain authoritative: no admin bypass, strategy
|
|
204
|
+
resources in that call, on resume or at the next allocation. Default `auto` also
|
|
205
|
+
merges the accepted commit. Merge policy remains authoritative: no admin bypass, strategy
|
|
207
206
|
fallback or repository setting changes. When the repository disallows merge
|
|
208
|
-
commits or refuses the
|
|
207
|
+
commits or refuses the merge, delivery reports `PR` with `merge_refused` and
|
|
209
208
|
the PR waits for a person. Command success alone never proves merge.
|
|
210
209
|
|
|
211
|
-
`PENDING` retains the workspace
|
|
210
|
+
`PENDING` retains the workspace; its receipt-based resume command merges once
|
|
211
|
+
required checks or reviews pass, or a person merges it.
|
|
212
212
|
`BLOCKED` names the cause and retry action; repair that condition before retrying.
|
|
213
213
|
Resume reobserves Git/PR state under a per-receipt lock, reuses the original
|
|
214
214
|
acceptance and performs only missing eligible effects.
|
|
@@ -706,9 +706,14 @@ class WarmTests(unittest.TestCase):
|
|
|
706
706
|
(bin_dir / "cargo").write_text(FAKE_CARGO)
|
|
707
707
|
(bin_dir / "cargo").chmod(0o755)
|
|
708
708
|
self.fixture, self.log = self.root / "fixture.json", self.root / "cargo.log"
|
|
709
|
+
# A commit's detached automatic maintenance would still write into .git while a test reads the tree. Appended to
|
|
710
|
+
# the inherited GIT_CONFIG_COUNT entries, which this environment keeps.
|
|
711
|
+
count = int(os.environ.get("GIT_CONFIG_COUNT") or 0)
|
|
709
712
|
self.environment = mock.patch.dict(os.environ, {
|
|
710
713
|
"PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "FAKE_CARGO": str(self.fixture),
|
|
711
714
|
"FAKE_CARGO_LOG": str(self.log), "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1",
|
|
715
|
+
"GIT_CONFIG_COUNT": str(count + 1), f"GIT_CONFIG_KEY_{count}": "maintenance.auto",
|
|
716
|
+
f"GIT_CONFIG_VALUE_{count}": "false",
|
|
712
717
|
"PYTHONPATH": str(self.root) + os.pathsep + os.environ.get("PYTHONPATH", ""),
|
|
713
718
|
"PYTHONDONTWRITEBYTECODE": "1",
|
|
714
719
|
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t"})
|
|
@@ -749,6 +754,14 @@ class WarmTests(unittest.TestCase):
|
|
|
749
754
|
def git(self, work, *args):
|
|
750
755
|
return subprocess.run(["git", "-C", str(work), *args], check=True, capture_output=True, text=True).stdout
|
|
751
756
|
|
|
757
|
+
def test_fixture_commit_starts_no_automatic_maintenance(self):
|
|
758
|
+
# Its detached child would still write into .git while a test reads the tree or cleanup removes it.
|
|
759
|
+
trace = self.root / "trace.json"
|
|
760
|
+
os.environ["GIT_TRACE2_EVENT"] = str(trace) # setUp's patch restores the environment
|
|
761
|
+
self.git(self.anchor, "commit", "-q", "--allow-empty", "-m", "traced")
|
|
762
|
+
children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
|
|
763
|
+
self.assertEqual(children, [])
|
|
764
|
+
|
|
752
765
|
def write_fixture(self, **extra):
|
|
753
766
|
packages = [
|
|
754
767
|
{"name": "app", "source": None, "manifest_path": str(self.work / "Cargo.toml"), "targets": [{"kind": ["lib"], "name": "LIB"}]},
|
|
@@ -59,4 +59,4 @@ Each `review_requirements` ID needs review evidence: a JSON record under `<workt
|
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
A record counts only when it binds this task, the candidate source and both contract digests; any other well-formed record is reported under `ignored_reviews` (a source change invalidates earlier reviews). A listed review or runner record that is unreadable or not this shape fails acceptance with its path named; keys a review record or finding adds beyond this shape are ignored. The counted records' `requirements` must cover `review_requirements`. Each finding's `disposition` is `open`, `resolved` or `rejected` (`rejected` needs a `reason`); an open binding finding blocks acceptance. The loop requires no particular engine or number of reviewers.
|
|
62
|
+
A record counts only when it binds this task, the candidate source and both contract digests; any other well-formed record is reported under `ignored_reviews` (a source change invalidates earlier reviews; the record for the changed source may come from a pass that reads only the change and the invariants it touches). A listed review or runner record that is unreadable or not this shape fails acceptance with its path named; keys a review record or finding adds beyond this shape are ignored. The counted records' `requirements` must cover `review_requirements`. Each finding's `disposition` is `open`, `resolved` or `rejected` (`rejected` needs a `reason`); an open binding finding blocks acceptance. The loop requires no particular engine or number of reviewers.
|
|
@@ -32,7 +32,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
|
|
|
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.
|
|
35
|
-
9. **Deliver** after waiting for the executor, with `task-complete.py complete --receipt <receipt> --writers-stopped`, plus `--local-only` or, until the receipt records a mode, the packet's `--mode auto|pr`, so a per-task `--mode` persists. Local-only keeps source, custody and the recovery ref with no remote effect. `auto`/`pr` publish the terminal commit under the completion contract; a pending or refused delivery keeps product acceptance, resources and a resume command.
|
|
35
|
+
9. **Deliver** after waiting for the executor, with `task-complete.py complete --receipt <receipt> --writers-stopped`, plus `--local-only` or, until the receipt records a mode, the packet's `--mode auto|pr`, so a per-task `--mode` persists. Local-only keeps source, custody and the recovery ref with no remote effect. `auto`/`pr` publish the terminal commit under the completion contract; a pending or refused delivery keeps product acceptance, resources and a resume command. A merged delivery whose cleanup is refused settles (`COMPLETE`) with what the refusal guards retained and reported under workspace cleanup; retained cleanup never blocks the queue. Failed products are never published. The receipt's `delivery` field checkpoints the latest outcome. A local chain is never turned into stacked PRs; publishing it requires integrated acceptance of the actual publication candidate.
|
|
36
36
|
10. **Recover** from durable state only:
|
|
37
37
|
|
|
38
38
|
| Interrupted | Resume |
|
|
@@ -515,6 +515,8 @@ class AcceptanceTests(unittest.TestCase):
|
|
|
515
515
|
self.addCleanup(self.temp.cleanup)
|
|
516
516
|
self.root = Path(self.temp.name).resolve()
|
|
517
517
|
self.work = self.root / "work tree"
|
|
518
|
+
# A commit's detached automatic maintenance would still write into .git while a test reads the tree.
|
|
519
|
+
(self.root / "gitconfig").write_text("[maintenance]\n\tauto = false\n", encoding="utf-8")
|
|
518
520
|
env = {"GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": str(self.root / "gitconfig"), "GIT_AUTHOR_NAME": "Fixture",
|
|
519
521
|
"GIT_AUTHOR_EMAIL": "fixture@example.invalid", "GIT_COMMITTER_NAME": "Fixture", "GIT_COMMITTER_EMAIL": "fixture@example.invalid"}
|
|
520
522
|
self.saved = {key: os.environ.get(key) for key in env}
|
|
@@ -543,6 +545,15 @@ class AcceptanceTests(unittest.TestCase):
|
|
|
543
545
|
self.g("commit", "-q", "-m", message)
|
|
544
546
|
return self.g("rev-parse", "HEAD")
|
|
545
547
|
|
|
548
|
+
def test_fixture_commit_starts_no_automatic_maintenance(self):
|
|
549
|
+
# Its detached child would still write into .git while a test reads the tree or cleanup removes it.
|
|
550
|
+
from unittest import mock
|
|
551
|
+
trace = self.root / "trace.json"
|
|
552
|
+
with mock.patch.dict(os.environ, GIT_TRACE2_EVENT=str(trace)):
|
|
553
|
+
self.g("commit", "-q", "--allow-empty", "-m", "traced")
|
|
554
|
+
children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
|
|
555
|
+
self.assertEqual(children, [])
|
|
556
|
+
|
|
546
557
|
def inputs(self, expected=None, review=()):
|
|
547
558
|
base = self.g("rev-parse", "HEAD")
|
|
548
559
|
files = {"docs/specs/l/meta.md": "# meta\n", "docs/specs/l/t/spec.md": "# task R1 R2\n",
|
|
@@ -1429,6 +1429,8 @@ def advance(v, row, opts):
|
|
|
1429
1429
|
progress(identity, f"delivery {result['status']}")
|
|
1430
1430
|
if result.get("merge_refused"):
|
|
1431
1431
|
return f"merge refused: {result['merge_refused']}; a person merges the PR"
|
|
1432
|
+
if waits := result.get("merge_waits"):
|
|
1433
|
+
return f"merge waits: PR is {waits} ({'GitHub has not computed its mergeability yet' if waits == 'UNKNOWN' else 'required checks or reviews not passing yet'}); drain again later, or merge it"
|
|
1432
1434
|
return None
|
|
1433
1435
|
|
|
1434
1436
|
|
|
@@ -1657,11 +1659,15 @@ def status_summary(v):
|
|
|
1657
1659
|
row = next_task(v, set())
|
|
1658
1660
|
annotate(v, {})
|
|
1659
1661
|
tasks = [summary(v, r) for r in v["rows"] if r["identity"]]
|
|
1662
|
+
# A task without a resume is no pending delivery, so a cleanup it retained is named here with its own resume.
|
|
1663
|
+
retained = [f"{t['identity']}: {label} {t[key]}" for t in tasks if not t.get("resume")
|
|
1664
|
+
for key, field, label in (("cleanup", "workspace_cleanup", "workspace cleanup"), ("scratch", "scratch_cleanup", "scratch cleanup"))
|
|
1665
|
+
if ((v["states"][t["identity"]]["receipt"] or {}).get(field) or {}).get("reason")]
|
|
1660
1666
|
return {"status": "OK", **skill_diagnostics(anchor), "counts": counts(v), "next": row["identity"] if row else None,
|
|
1661
1667
|
"blockers": [f"{t['identity']}: {t['reason']}" for t in tasks if t.get("reason") and t["result"] in {"pending", "active", "blocked"}]
|
|
1662
1668
|
+ [f"legacy row {r['index'] + 1} needs planning: {r['text']}" for r in v["rows"] if not r["identity"] and r["mark"] == " "]
|
|
1663
1669
|
+ [f"unreadable receipt {item}" for item in v["unreadable"]],
|
|
1664
|
-
"delivery": [t for t in tasks if t.get("resume")], "cleanup": v["cleanup"]}
|
|
1670
|
+
"delivery": [t for t in tasks if t.get("resume")], "cleanup": v["cleanup"] + retained}
|
|
1665
1671
|
|
|
1666
1672
|
|
|
1667
1673
|
def status(args):
|
|
@@ -1897,6 +1903,8 @@ class QueueTests(unittest.TestCase):
|
|
|
1897
1903
|
"import shutil\nfrom types import SimpleNamespace\n"
|
|
1898
1904
|
"shutil.disk_usage = lambda path: SimpleNamespace(total=100 * 1024**3, free=100 * 1024**3)\n",
|
|
1899
1905
|
encoding="utf-8")
|
|
1906
|
+
# A commit's detached automatic maintenance would still write into .git while a test reads the tree.
|
|
1907
|
+
(self.root / "gitconfig").write_text("[maintenance]\n\tauto = false\n", encoding="utf-8")
|
|
1900
1908
|
self.env = {**os.environ, "GIT_CONFIG_NOSYSTEM": "1", "GIT_CONFIG_GLOBAL": str(self.root / "gitconfig"),
|
|
1901
1909
|
"GIT_AUTHOR_NAME": "Fixture", "GIT_AUTHOR_EMAIL": "fixture@example.invalid",
|
|
1902
1910
|
"GIT_COMMITTER_NAME": "Fixture", "GIT_COMMITTER_EMAIL": "fixture@example.invalid"}
|
|
@@ -1940,6 +1948,14 @@ class QueueTests(unittest.TestCase):
|
|
|
1940
1948
|
path.write_text(text.replace(old, new, 1), encoding="utf-8")
|
|
1941
1949
|
return mutate
|
|
1942
1950
|
|
|
1951
|
+
def test_fixture_commit_starts_no_automatic_maintenance(self):
|
|
1952
|
+
# Its detached child would still write into .git while a test reads the tree or cleanup removes it.
|
|
1953
|
+
trace = self.root / "trace.json"
|
|
1954
|
+
subprocess.run(["git", "-C", str(self.anchor), "commit", "-q", "--allow-empty", "-m", "traced"], check=True,
|
|
1955
|
+
env={**self.env, "GIT_TRACE2_EVENT": str(trace)})
|
|
1956
|
+
children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
|
|
1957
|
+
self.assertEqual(children, [])
|
|
1958
|
+
|
|
1943
1959
|
def test_package_validation(self):
|
|
1944
1960
|
self.assertEqual(self.cli("check", self.meta)["tasks"], ["inv.t1", "inv.t2"])
|
|
1945
1961
|
manifest = json.loads(JSON_FENCE_RE.search((self.anchor / "docs/specs/inv/meta.md").read_text(encoding="utf-8")).group(1))
|
|
@@ -363,6 +363,8 @@ class BringInTests(unittest.TestCase):
|
|
|
363
363
|
self.env = {key: value for key, value in os.environ.items()
|
|
364
364
|
if not key.startswith("GIT_")}
|
|
365
365
|
self.env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_GLOBAL=str(self.root / "no-global"),
|
|
366
|
+
# A commit's detached automatic maintenance would still write into .git while a test copies it.
|
|
367
|
+
GIT_CONFIG_COUNT="1", GIT_CONFIG_KEY_0="maintenance.auto", GIT_CONFIG_VALUE_0="false",
|
|
366
368
|
GIT_AUTHOR_NAME="Fixture", GIT_AUTHOR_EMAIL="fixture@example.invalid",
|
|
367
369
|
GIT_COMMITTER_NAME="Fixture", GIT_COMMITTER_EMAIL="fixture@example.invalid",
|
|
368
370
|
LC_ALL="C", GIT_EDITOR=":", GIT_MERGE_AUTOEDIT="no")
|
|
@@ -415,6 +417,14 @@ class BringInTests(unittest.TestCase):
|
|
|
415
417
|
path.write_bytes(("#!/bin/sh\n" + body).encode("utf-8"))
|
|
416
418
|
path.chmod(0o755)
|
|
417
419
|
|
|
420
|
+
def test_fixture_commit_starts_no_automatic_maintenance(self):
|
|
421
|
+
# Its detached child would still write .git/objects/maintenance.lock while copytree or cleanup reads the tree.
|
|
422
|
+
trace = self.root / "trace.json"
|
|
423
|
+
self.env["GIT_TRACE2_EVENT"] = str(trace)
|
|
424
|
+
self.commit("traced", "traced\n")
|
|
425
|
+
children = [line for line in trace.read_text(encoding="utf-8").splitlines() if '"event":"child_start"' in line]
|
|
426
|
+
self.assertEqual(children, [])
|
|
427
|
+
|
|
418
428
|
def test_fast_forward_and_paths_with_spaces(self):
|
|
419
429
|
self.cli()
|
|
420
430
|
self.assertEqual(self.g("rev-parse", "HEAD").strip(), self.target)
|
|
@@ -15,7 +15,7 @@ Seven rules govern every change. Cite them by name when a decision touches one.
|
|
|
15
15
|
1. **No workaround** — fix the root cause, never the symptom. No `any`, no `@ts-ignore`, no silent `catch`, no hardcoded fallback that hides a broken contract. No config bypasses.
|
|
16
16
|
2. **No overengineering** — smallest sufficient change, one responsibility per module. New abstractions need an observed failure. Subtractive-first: ask "what can I delete instead?" before writing anything new. Remove code your change makes unused; only report unrelated pre-existing dead code.
|
|
17
17
|
3. **No guesswork** — verify with the actual files, logs, diffs, and run output before forming conclusions. State the falsifiable prediction BEFORE the experiment; record raw results AFTER. Retroactive prediction edits are dishonest.
|
|
18
|
-
4. **Worldclass** — zero CRITICAL, zero HIGH
|
|
18
|
+
4. **Worldclass** — zero CRITICAL, zero HIGH findings on the shippable path; each must name a concrete case of wrong behavior or results, a security hole, or lost or corrupted data. A review is one pass on the source the checks ran on, by the reviewers the user requires, else by one reviewer who has not seen the author's reasoning. Fix each finding or reject it with a reason, and verify every edit and the invariants it touches. Only a CRITICAL or HIGH calls for another reviewer pass, which reads the fixes, the rejections and the invariants they touch. If one survives that pass, or its fix caused a new one, repair once more; if the next pass finds either again, stop: the work does not ship and is reported blocked.
|
|
19
19
|
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
20
20
|
6. **Optimized** — efficient on the resource that matters (the user's time, tokens, attention, runtime) without weakening correctness or requirements. An extra check or process step must beat the simpler path.
|
|
21
21
|
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|