devlyn-cli 4.3.20 → 4.3.23
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 +2 -2
- package/CLAUDE.md +2 -2
- package/README.md +1 -1
- package/bin/devlyn.js +26 -1
- package/bin/instruction-templates.json +146 -0
- package/bin/skill-history.json +14 -1
- package/config/skills/_shared/bring-in.py +810 -0
- package/config/skills/_shared/harness-store.py +18 -6
- package/config/skills/_shared/runtime-principles.md +2 -2
- package/config/skills/_shared/storage-checkout.py +117 -0
- package/config/skills/_shared/storage-client.py +1180 -0
- package/config/skills/_shared/task-complete.py +900 -88
- package/config/skills/_shared/task-completion.md +78 -6
- package/config/skills/_shared/warm-build-dirs.py +53 -3
- package/config/skills/devlyn-ideate/SKILL.md +1 -1
- package/config/skills/devlyn-ideate/references/loop.md +2 -2
- package/config/skills/devlyn-ideate/scripts/queue.py +69 -18
- package/config/storage-client-contract.json +7 -0
- package/harness/AGENTS.md +3 -3
- package/harness/CLAUDE.md +3 -3
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/bring-in.py +810 -0
- package/native/claude/skills/_shared/harness-store.py +18 -6
- package/native/claude/skills/_shared/runtime-principles.md +2 -2
- package/native/claude/skills/_shared/storage-checkout.py +117 -0
- package/native/claude/skills/_shared/storage-client.py +1180 -0
- package/native/claude/skills/_shared/task-complete.py +900 -88
- package/native/claude/skills/_shared/task-completion.md +78 -6
- package/native/claude/skills/_shared/warm-build-dirs.py +53 -3
- package/native/claude/skills/devlyn-ideate/SKILL.md +1 -1
- package/native/claude/skills/devlyn-ideate/references/loop.md +2 -2
- package/native/claude/skills/devlyn-ideate/scripts/queue.py +69 -18
- 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 +810 -0
- package/native/codex/plugins/devlyn/skills/_shared/harness-store.py +18 -6
- package/native/codex/plugins/devlyn/skills/_shared/runtime-principles.md +2 -2
- package/native/codex/plugins/devlyn/skills/_shared/storage-checkout.py +117 -0
- package/native/codex/plugins/devlyn/skills/_shared/storage-client.py +1180 -0
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +900 -88
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +78 -6
- package/native/codex/plugins/devlyn/skills/_shared/warm-build-dirs.py +53 -3
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/SKILL.md +1 -1
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/references/loop.md +2 -2
- package/native/codex/plugins/devlyn/skills/devlyn-ideate/scripts/queue.py +69 -18
- package/optional-skills/better-auth-setup/SKILL.md +9 -3
- package/optional-skills/better-auth-setup/references/proxy-setup.md +10 -3
- package/package.json +1 -1
- package/release.json +91 -41
|
@@ -80,22 +80,88 @@ After remote delivery, fetch first. If the branch can fast-forward to the
|
|
|
80
80
|
delivered result, recheck the copied edits' current ownership and contents,
|
|
81
81
|
then remove them: `git restore --source=HEAD --staged --worktree -- <paths>`
|
|
82
82
|
for tracked paths holding only your edits, and delete copied untracked files.
|
|
83
|
-
Then
|
|
84
|
-
|
|
83
|
+
Then run `python3 "$DEVLYN_SHARED_DIR/bring-in.py" --repo . --branch <checked-out branch> --target <task branch> --ff-only`
|
|
84
|
+
for local delivery, or the same command with `--target <remote>/<base>` on the base branch after remote delivery;
|
|
85
85
|
a drain's `Bring into` command is its own. Once a local delivery is reconciled,
|
|
86
86
|
remove its worktree and task branch (`git worktree remove <worktree>`,
|
|
87
87
|
`git branch -d <task branch>`). Do not switch branches, create a reconciliation
|
|
88
|
-
merge commit or discard WIP to force this step. If reconciliation is unsafe,
|
|
88
|
+
merge commit, or discard WIP or ignored files to force this step. If reconciliation is unsafe,
|
|
89
89
|
retain and report the edits, delivered result and remaining bring-in action.
|
|
90
90
|
|
|
91
91
|
Save the returned receipt path under the common Gitdir. `reconciled` reports earlier accepted,
|
|
92
92
|
PR-delivered tasks whose merged resources were cleaned or retained; it is
|
|
93
93
|
informational, so never resume or release a receipt you do not own. Existing
|
|
94
94
|
branches/trees cannot be adopted, even when their names look generated. Nor
|
|
95
|
-
can
|
|
96
|
-
branch if present, delete its receipt directory, then allocate again.
|
|
95
|
+
can an interrupted standalone receipt that never reached `allocation: owned`:
|
|
96
|
+
remove its worktree and branch if present, delete its receipt directory, then allocate again.
|
|
97
97
|
Unreceipted tasks remain owner-managed.
|
|
98
98
|
|
|
99
|
+
New allocation and warming modes come from enrollment state alone:
|
|
100
|
+
|
|
101
|
+
| `~/.devlyn/storage-client.json` | recovery marker | unenroll journal | Mode |
|
|
102
|
+
|---|---|---|---|
|
|
103
|
+
| absent | absent | not read | standalone (never enrolled) |
|
|
104
|
+
| valid | any | not read | managed; a fenced authority refuses |
|
|
105
|
+
| invalid or unreadable | any | not read | refuse; repair registration |
|
|
106
|
+
| absent | present | verified `retired: true` | standalone (unenrollment complete) |
|
|
107
|
+
| absent | present | absent, unreadable or incomplete | refuse; Open Devlyn to repair its storage registration |
|
|
108
|
+
|
|
109
|
+
The journal `storage-client-unenroll.json` is read no-follow only on the last two
|
|
110
|
+
rows. Schema 1, canonical UUIDv7 operation, permanent `storage-client-fence.json`,
|
|
111
|
+
matching operation/root/lock identities and Rust's archive ordering/path
|
|
112
|
+
validation are required. If native root/lock identities or the home path changed,
|
|
113
|
+
completed retirement permits standalone only when `storage`, `control`,
|
|
114
|
+
`harness/state.json` and `storage-client.json` are all absent under the current
|
|
115
|
+
lifecycle root (checked without following links). Live inventory requires repair.
|
|
116
|
+
No repository-wide receipt scan selects mode. Other keys can allocate after
|
|
117
|
+
completed unenrollment; a leftover managed key retains its own recovery command.
|
|
118
|
+
|
|
119
|
+
Existing operations keep their managed identity. Receipts pin the control
|
|
120
|
+
identity; current enrollment verifies the helper before allocation. Recovery uses
|
|
121
|
+
the current verified `storage-client-recovery/pins.json`, including after uninstall
|
|
122
|
+
or unenrollment, and requires its authority binding to match the receipt. Missing
|
|
123
|
+
or invalid pins retain resources and require repair; saved artifacts are never a
|
|
124
|
+
fallback. Enrolled checkout preserves
|
|
125
|
+
`managed_pending`/`managed_owned` and Rust's `storage-allocation.json` subreceipt.
|
|
126
|
+
After an interrupted reply, use the returned `recover-storage` resume. Never
|
|
127
|
+
delete managed receipts or replace an unfinished operation ID.
|
|
128
|
+
|
|
129
|
+
Owner `recover-storage --receipt <receipt.json> --operation-id <saved ID>
|
|
130
|
+
--for-removal` fences allocation and reconciles closure and the exact cache set.
|
|
131
|
+
Never-published allocations are abandoned. Accepted LOCAL_ONLY, FAILED, and PRs
|
|
132
|
+
observed CLOSED without merge share custody/recovery-ref verification and fenced
|
|
133
|
+
removal, retaining task branches and a durable terminal delivery. Release writers
|
|
134
|
+
with `complete --writers-stopped` first. A queue-produced FAILED checkout is
|
|
135
|
+
advanced by `read-tree -m -u <source_sha> <publish_sha>` only when HEAD is at the
|
|
136
|
+
attached publish commit and the index and worktree are exactly at source; other
|
|
137
|
+
differences refuse as dirty.
|
|
138
|
+
|
|
139
|
+
Removal-finished receipts with final COMPLETE, LOCAL_ONLY, FAILED or CLOSED
|
|
140
|
+
delivery move under `devlyn-completion/retired/`, regardless of scratch status.
|
|
141
|
+
Retirement moves scratch and its saved cleanup record with the audit and deletes
|
|
142
|
+
nothing. `complete --writers-stopped` saves the writer release there and retries
|
|
143
|
+
pending scratch, including an interrupted cleanup with no saved result; the next
|
|
144
|
+
allocation retries completed deliveries with released writers. `clean-scratch`
|
|
145
|
+
remains available there and never downgrades CLEAN.
|
|
146
|
+
Saved managed `complete`, `recover-storage` and `clean-scratch` resumes carry an
|
|
147
|
+
operation ID validated under the receipt lock. Without an ID, a locator selects
|
|
148
|
+
its active receipt. An ID selects only that operation, including its verified
|
|
149
|
+
retired receipt; a mismatched ID refuses. Retired paths also remain usable.
|
|
150
|
+
A new allocation may reuse a retired key without disturbing its audit.
|
|
151
|
+
|
|
152
|
+
Retained submodule repositories in the admin directory block removal even when
|
|
153
|
+
the checkout is absent; preserve/publish their source and explicitly resolve
|
|
154
|
+
them. Manual checkout removal is advised only after portable source checks and
|
|
155
|
+
record preservation succeed; changed or new source requires retention. If native
|
|
156
|
+
Windows or unobservable writers then require retention, stop all
|
|
157
|
+
writers, remove the task checkout by hand, keep its branch and receipt, then run
|
|
158
|
+
the returned owner-removal resume. The absent-tree path completes the fenced
|
|
159
|
+
registration removal. Scratch cleanup requires positive allocation closure,
|
|
160
|
+
without a cache-removal fence; actual checkout removal owns that fence, including
|
|
161
|
+
empty cache sets. Missing helpers or uncertain closure retain resources with a
|
|
162
|
+
repair/resume action. Compatible recovery artifacts and Rust's LaunchGate remain
|
|
163
|
+
required while managed obligations exist.
|
|
164
|
+
|
|
99
165
|
Copy attributable task edits into the allocated worktree as a patch, never a
|
|
100
166
|
stash. Establish attribution from the pre-edit contents and starting Git state,
|
|
101
167
|
including staged, unstaged and untracked work. A path-scoped
|
|
@@ -113,6 +179,10 @@ require the affected checks again.
|
|
|
113
179
|
Keep the original edits through pending or failed delivery; only reconciliation
|
|
114
180
|
removes them. Preserve all other work and report anything left behind.
|
|
115
181
|
|
|
182
|
+
Managed allocations currently start cold; `build_dirs` retains each returned cache
|
|
183
|
+
and publication lease ID. Managed warming is deferred. Standalone allocations keep
|
|
184
|
+
the warming behavior below.
|
|
185
|
+
|
|
116
186
|
Build in the task worktree's own default build directory, never in one
|
|
117
187
|
another checkout builds in, even one build at a time: tools such as Cargo take
|
|
118
188
|
one checkout's outputs as fresh for the other. When the shared repository-local
|
|
@@ -142,6 +212,7 @@ release build caches when parking or after a failed run, use:
|
|
|
142
212
|
```sh
|
|
143
213
|
python3 "$DEVLYN_SHARED_DIR/task-complete.py" clean-scratch \
|
|
144
214
|
--receipt '<returned receipt.json>' --writers-stopped
|
|
215
|
+
# For managed receipts, use the saved resume with --operation-id <saved ID>.
|
|
145
216
|
```
|
|
146
217
|
|
|
147
218
|
This empties only the prospectively owned scratch, preserves its directory
|
|
@@ -246,7 +317,8 @@ the next allocation or resume after the restart.
|
|
|
246
317
|
On native Windows, locking and file durability are supported, but the helper cannot prove writer cessation and reports `writer observation unsupported on this platform; retain workspace`. Even `--writers-stopped` cannot authorize deletion without that observation: retain the workspace, its task refs and external receipt/custody, report delivery separately, and preserve the receipt-based resume command. A merged or on-base delivery therefore reports `CLEANUP_PENDING` (delivery `COMPLETE`) with `workspace_cleanup` naming the reason and resume. Do not kill unknown processes or force worktree removal to make completion pass.
|
|
247
318
|
|
|
248
319
|
The receipt directory holds byte-verified `custody/`, `manifest.json` and a guarded
|
|
249
|
-
`refs/devlyn/completed/<id>` recovery ref
|
|
320
|
+
`refs/devlyn/completed/<id>` recovery ref (new managed allocations use
|
|
321
|
+
`<id>.<operation>` so branch reuse preserves earlier recovery refs). Check evidence is copied there
|
|
250
322
|
before publication; removal rechecks it, the recovery ref and the merge or accepted commit on base.
|
|
251
323
|
Ignored build output and other ignored files are disposable under native
|
|
252
324
|
non-force worktree removal. The tree's `.devlyn/` files are byte/mode-verified
|
|
@@ -526,8 +526,15 @@ def warm_cargo(work, anchor, staging_root, deadline):
|
|
|
526
526
|
with contextlib.ExitStack() as stack:
|
|
527
527
|
staging = None
|
|
528
528
|
try:
|
|
529
|
-
|
|
530
|
-
|
|
529
|
+
# Decide managed cold start before Cargo, destination setup, donors
|
|
530
|
+
# or any seeding work. Standalone keeps its original path.
|
|
531
|
+
import runpy
|
|
532
|
+
storage = runpy.run_path(str(Path(__file__).with_name('storage-client.py')))
|
|
533
|
+
decision = storage['operation_enrollment']()
|
|
534
|
+
if decision['mode'] == 'managed':
|
|
535
|
+
return entry('cargo', 'cold', reason='managed warming deferred; cold start')
|
|
536
|
+
if shutil.which('cargo') is None:
|
|
537
|
+
raise Skip('cargo is not on PATH')
|
|
531
538
|
target, relative, parent = destination(work, deadline, stack)
|
|
532
539
|
candidates = donors(work, anchor, relative, deadline)
|
|
533
540
|
if not candidates:
|
|
@@ -678,6 +685,11 @@ class WarmTests(unittest.TestCase):
|
|
|
678
685
|
temporary = tempfile.TemporaryDirectory(prefix="warm-build-dirs-")
|
|
679
686
|
self.addCleanup(temporary.cleanup)
|
|
680
687
|
self.root = Path(temporary.name).resolve()
|
|
688
|
+
test_home = self.root / "home"
|
|
689
|
+
test_home.mkdir()
|
|
690
|
+
home_env = mock.patch.dict(os.environ, {"HOME": str(test_home), "USERPROFILE": str(test_home)})
|
|
691
|
+
home_env.start()
|
|
692
|
+
self.addCleanup(home_env.stop)
|
|
681
693
|
(self.root / "sitecustomize.py").write_text(
|
|
682
694
|
"import importlib.machinery, importlib.util, pathlib, sys\n"
|
|
683
695
|
"spec = importlib.machinery.PathFinder.find_spec('sitecustomize', [p for p in sys.path if pathlib.Path(p or '.').resolve() != pathlib.Path(__file__).resolve().parent])\n"
|
|
@@ -730,7 +742,7 @@ class WarmTests(unittest.TestCase):
|
|
|
730
742
|
def copy(source, target, deadline):
|
|
731
743
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
732
744
|
subprocess.run(["cp", "-a", "--", str(source), str(target)], check=True)
|
|
733
|
-
patch = mock.patch(
|
|
745
|
+
patch = mock.patch.dict(warm_cargo.__globals__, clone=copy)
|
|
734
746
|
patch.start()
|
|
735
747
|
self.addCleanup(patch.stop)
|
|
736
748
|
|
|
@@ -832,6 +844,44 @@ class WarmTests(unittest.TestCase):
|
|
|
832
844
|
self.assertEqual([], list(self.staging.iterdir()))
|
|
833
845
|
self.assertEqual("", self.git(self.work, "status", "--porcelain", "--untracked-files=all"))
|
|
834
846
|
|
|
847
|
+
@unittest.skipIf(os.name == 'nt', 'Windows keeps its existing skip')
|
|
848
|
+
def test_managed_skip_precedes_donor_traversal_and_reports_cold_start(self):
|
|
849
|
+
from unittest import mock
|
|
850
|
+
import runpy
|
|
851
|
+
real = runpy.run_path
|
|
852
|
+
def modules(path, *args, **kwargs):
|
|
853
|
+
if Path(path).name == 'storage-client.py':
|
|
854
|
+
return {'operation_enrollment': lambda: {'mode': 'managed'}}
|
|
855
|
+
return real(path, *args, **kwargs)
|
|
856
|
+
with mock.patch.object(runpy, 'run_path', modules), \
|
|
857
|
+
mock.patch.dict(warm_cargo.__globals__, donors=mock.Mock(side_effect=AssertionError('donor traversal')),
|
|
858
|
+
destination=mock.Mock(side_effect=AssertionError('destination setup')),
|
|
859
|
+
clone=mock.Mock(side_effect=AssertionError('seed')), publish=mock.Mock(side_effect=AssertionError('publish'))):
|
|
860
|
+
result = self.warm()[0]
|
|
861
|
+
self.assertEqual(result['status'], 'cold')
|
|
862
|
+
self.assertIn('cold start', result['reason'])
|
|
863
|
+
self.assertFalse((self.work / 'target').exists())
|
|
864
|
+
self.assert_nothing_left()
|
|
865
|
+
|
|
866
|
+
@unittest.skipIf(os.name == 'nt', 'warming is POSIX-only')
|
|
867
|
+
def test_standalone_warm_never_registers_destination(self):
|
|
868
|
+
from unittest import mock
|
|
869
|
+
import runpy
|
|
870
|
+
real = runpy.run_path
|
|
871
|
+
def modules(path, *args, **kwargs):
|
|
872
|
+
module = real(path, *args, **kwargs)
|
|
873
|
+
if Path(path).name == 'storage-client.py':
|
|
874
|
+
module['Client'] = mock.Mock(side_effect=AssertionError('standalone authority call'))
|
|
875
|
+
module['operation_id'] = mock.Mock(side_effect=AssertionError('standalone operation'))
|
|
876
|
+
return module
|
|
877
|
+
common = Path(self.git(self.work, 'rev-parse', '--path-format=absolute', '--git-common-dir').strip())
|
|
878
|
+
before = sorted(p.name for p in common.iterdir())
|
|
879
|
+
with mock.patch.object(runpy, 'run_path', modules):
|
|
880
|
+
result = self.warm()[0]
|
|
881
|
+
self.assertEqual(result['status'], 'seeded', result)
|
|
882
|
+
self.assertEqual(sorted(p.name for p in common.iterdir()), before)
|
|
883
|
+
self.assertFalse((Path(os.environ['HOME']) / '.devlyn').exists())
|
|
884
|
+
|
|
835
885
|
@unittest.skipIf(os.name == "nt", "warming is POSIX-only")
|
|
836
886
|
def test_local_git_true_seeds_without_cargo_opt_in(self):
|
|
837
887
|
self.write_fixture(workspace_metadata=None)
|
|
@@ -123,7 +123,7 @@ Execute the devlyn loop task packet {packet}: read its workerPrompt when present
|
|
|
123
123
|
python3 "$DEVLYN_SKILL_DIR/scripts/queue.py" drain --repo . [--local-only] -- <executor argv>
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
A drain can run for hours. Do not end the turn until it exits: run it in the foreground with the longest allowed timeout, or in the background and await its completion notice, because
|
|
126
|
+
A drain can run for hours. Do not end the turn until it exits: run it in the foreground with the longest allowed timeout, or in the background and await its completion notice, because reporting the drain's result requires observing its exit.
|
|
127
127
|
|
|
128
128
|
- `WAITING` on legacy rows: plan and materialize each under the autonomous policy, then drain again; a row whose planning stops on material ambiguity stays pending and its question is reported.
|
|
129
129
|
- Other `WAITING`: report each task’s reason and recovery command. Task-local failures leave dependents and barrier-held siblings waiting while independent work continues; the next drain retries eligible work once. An interrupted executor is observed once per drain and cannot be adopted or respawned until it stops. `BLOCKED` means authoritative queue reconstruction or preflight failed, including a held drain lock. Never edit receipts, refs or queue rows to get past either result.
|
|
@@ -37,7 +37,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
|
|
|
37
37
|
|
|
38
38
|
| Interrupted | Resume |
|
|
39
39
|
|---|---|
|
|
40
|
-
| During allocation |
|
|
40
|
+
| During allocation | Replay `managed_pending` with `recover-storage --receipt`; `managed_owned` is owned. Managed removal uses `recover-storage --receipt --for-removal` and preserves receipts. An interrupted standalone allocation is never adopted: its task waits, naming the recovery (remove its worktree and branch if present, delete the receipt directory, drain again), while independent work continues. |
|
|
41
41
|
| Inputs commit | A branch head equal to the expected inputs tree is adopted; anything else leaves the task waiting for inspection, preserving the unexpected commits. |
|
|
42
42
|
| During execution | Each attempt's start is recorded before its spawn and its exit after it. A start without an exit gets one writer observation per drain. A live writer leaves the task waiting while independent work continues; a later drain adopts the submission or runs the executor again only after cessation is established; where writers cannot be observed (native Windows) the task becomes `[F] interrupted-unobservable` with its workspace retained, its dependents become prerequisite-blocked and independent work continues. Exactly-once execution is not promised. |
|
|
43
43
|
| During checks | Acceptance reruns; incomplete evidence stays unreferenced. |
|
|
@@ -49,7 +49,7 @@ The host supplies the executor and maps its configured engine to the argv; the l
|
|
|
49
49
|
|
|
50
50
|
Expected task-local operational failures leave the selected task waiting with `<phase> blocked: <detail>` and recovery guidance. It is attempted at most once per drain; dependents, local siblings behind an active task, and siblings behind an undelivered package carrier wait too. Independent work continues and the drain ends `WAITING`. Inputs, acceptance, custody and terminal-attachment failures resume only their missing effects; bound results and accepted work never replay. The controller boundary contains `LoopError` and `OSError`; other programming errors propagate there. The inherited task-complete wrapper and delivery-repository lookup also contain helper `TypeError`, `KeyError` and `ValueError`, converting them to task waits.
|
|
51
51
|
|
|
52
|
-
11. **Report** to `<common Gitdir>/devlyn-loops/<loop-id>/drain-report.md` after every drain: per task the product result and reason, receipt, evidence custody and recovery ref, allocation base, accepted (or unaccepted) source, terminal commit, delivery status, PR URL and resume command, assumptions and unresolved questions, the retained worktree and branch, and workspace and scratch cleanup. Include the package's decisions and assumptions once, excluding the manifest and separately from executor assumptions; read the captured package when an add record exists, otherwise use the checkout, and report an unreadable package explicitly. Whole-loop acceptance is reported separately over the manifest's complete task set: every task needs receipt-backed acceptance, including the integration task's assembled-product check on the final frontier; a manifest task missing from the queue is reported as not yet run. `Bring into` names the
|
|
52
|
+
11. **Report** to `<common Gitdir>/devlyn-loops/<loop-id>/drain-report.md` after every drain: per task the product result and reason, receipt, evidence custody and recovery ref, allocation base, accepted (or unaccepted) source, terminal commit, delivery status, PR URL and resume command, assumptions and unresolved questions, the retained worktree and branch, and workspace and scratch cleanup. Include the package's decisions and assumptions once, excluding the manifest and separately from executor assumptions; read the captured package when an add record exists, otherwise use the checkout, and report an unreadable package explicitly. Whole-loop acceptance is reported separately over the manifest's complete task set: every task needs receipt-backed acceptance, including the integration task's assembled-product check on the final frontier; a manifest task missing from the queue is reported as not yet run. `Bring into` names the shared `bring-in.py` helper, targeting the local frontier branch or the delivered `<remote>/<base>`. It fast-forwards when it can; otherwise it prepares the merge without touching the checkout and lands it by a protected fast-forward, so several local loops still apply in either order. A file collision, conflict, or unsupported merge setting stops before the branch moves and names the next step.
|
|
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
|
|
|
@@ -47,7 +47,7 @@ FRONTMATTER_KEYS = {"id", "loop_id", "title", "kind", "review_requirements", "co
|
|
|
47
47
|
KINDS = {"feature", "spike", "prototype"}
|
|
48
48
|
UNSUPPORTED = {"process_evidence": "phase process obligations", "required_risk_probe_requirements": "risk-probe obligations",
|
|
49
49
|
"tier_a_waivers": "benchmark scope waivers", "spec_output_files": "benchmark scope surfaces"}
|
|
50
|
-
SETTLED = {"LOCAL_ONLY", "COMPLETE", "FAILED"}
|
|
50
|
+
SETTLED = {"LOCAL_ONLY", "COMPLETE", "FAILED", "CLOSED"}
|
|
51
51
|
OBLIGATIONS = [
|
|
52
52
|
"Work only in this worktree on its owned branch under the installed methodology; never edit the loop package or docs/specs/queue.md.",
|
|
53
53
|
"Commit any source changes on the owned branch; when none are needed, use inputs_sha without creating an empty commit. Leave no uncommitted or untracked source and remove residue the change created.",
|
|
@@ -170,10 +170,10 @@ def lock(common, name, *, blocking):
|
|
|
170
170
|
yield
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
def harness_call(name, *args):
|
|
173
|
+
def harness_call(name, *args, **kwargs):
|
|
174
174
|
helper = shared("harness-store")
|
|
175
175
|
try:
|
|
176
|
-
return helper[name](*args)
|
|
176
|
+
return helper[name](*args, **kwargs)
|
|
177
177
|
except helper["HarnessError"] as exc:
|
|
178
178
|
raise LoopError(str(exc)) from exc
|
|
179
179
|
|
|
@@ -542,7 +542,23 @@ def branch_of(identity):
|
|
|
542
542
|
|
|
543
543
|
|
|
544
544
|
def receipt_path(common, identity):
|
|
545
|
-
|
|
545
|
+
key = hashlib.sha256(branch_of(identity).encode()).hexdigest()[:24]
|
|
546
|
+
root = common / 'devlyn-completion'
|
|
547
|
+
active = root / key / 'receipt.json'
|
|
548
|
+
if active.exists():
|
|
549
|
+
return active
|
|
550
|
+
retired = sorted((root / 'retired').glob(key + '.*/receipt.json'))
|
|
551
|
+
if len(retired) > 1:
|
|
552
|
+
delivered = []
|
|
553
|
+
for path in retired:
|
|
554
|
+
receipt = read_json(path)
|
|
555
|
+
require(isinstance(receipt, dict), f'{path}: malformed retired receipt')
|
|
556
|
+
if receipt.get('delivery') == 'COMPLETE':
|
|
557
|
+
delivered.append(path)
|
|
558
|
+
require(len(delivered) <= 1, f'conflicting delivered receipts for {identity}: {delivered}')
|
|
559
|
+
if delivered:
|
|
560
|
+
return delivered[0]
|
|
561
|
+
return retired[-1] if retired else active
|
|
546
562
|
|
|
547
563
|
|
|
548
564
|
def added_path(common, loop):
|
|
@@ -671,7 +687,9 @@ def task_complete(action, *, exclude_receipts=(), warnings=None, **values):
|
|
|
671
687
|
return helper[action](argparse.Namespace(**values), **options)
|
|
672
688
|
except (helper["CompletionError"], OSError, KeyError, TypeError, ValueError) as exc:
|
|
673
689
|
# Inherited helper containment includes these three programming-error types; the controller boundary is narrower.
|
|
674
|
-
|
|
690
|
+
details = getattr(exc, "storage_error", {})
|
|
691
|
+
context = f"; receipt={details.get('receipt')}; resume={details.get('resume')}" if details else ""
|
|
692
|
+
raise LoopError(f"task-complete {action}: {exc}{context}") from exc
|
|
675
693
|
|
|
676
694
|
|
|
677
695
|
def derive_state(anchor, common, row, claims, rev):
|
|
@@ -690,7 +708,7 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
690
708
|
require(receipt.get("task") == identity and receipt.get("branch") == branch_of(identity) and path == receipt_path(common, identity)
|
|
691
709
|
and path == path.resolve(),
|
|
692
710
|
f"receipt {path} claims {identity} from branch {receipt.get('branch')!r}; conflicting ownership, inspect it")
|
|
693
|
-
require(receipt.get("allocation") in ("owned", "allocating"),
|
|
711
|
+
require(receipt.get("allocation") in ("owned", "allocating", "managed_pending", "managed_owned"),
|
|
694
712
|
f"receipt {path}: missing or malformed allocation checkpoint")
|
|
695
713
|
if receipt["allocation"] == "allocating":
|
|
696
714
|
# Never adopted, and no executor ran: the task waits until the user removes what the allocation left.
|
|
@@ -700,9 +718,18 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
700
718
|
return state
|
|
701
719
|
require(all(isinstance(receipt.get(key), str) and receipt[key] and "\0" not in receipt[key] for key in
|
|
702
720
|
("id", "baseline", "worktree", "common_gitdir", "recovery_ref")), f"receipt {path}: malformed allocation fields")
|
|
703
|
-
|
|
721
|
+
archived = path.parent.parent == common / 'devlyn-completion/retired'
|
|
722
|
+
directory_id = receipt['id'] + '.' + receipt.get('storage', {}).get('request', {}).get('operation_id', '') if archived else receipt['id']
|
|
723
|
+
require(not archived or receipt.get('storage', {}).get('removal_finished') and (
|
|
724
|
+
receipt.get('status') == 'ABANDONED' or receipt.get('delivery') in {'COMPLETE', 'LOCAL_ONLY', 'FAILED', 'CLOSED'}),
|
|
725
|
+
f'{path}: nonterminal retired receipt')
|
|
726
|
+
require(receipt["common_gitdir"] == str(common) and directory_id == path.parent.name,
|
|
704
727
|
f"receipt {path}: conflicting common Gitdir/id binding")
|
|
705
|
-
|
|
728
|
+
recovery = "refs/devlyn/completed/" + receipt["id"]
|
|
729
|
+
allowed = {recovery}
|
|
730
|
+
if receipt['allocation'] in ('managed_pending', 'managed_owned'):
|
|
731
|
+
allowed.add(recovery + '.' + receipt.get('storage', {}).get('request', {}).get('operation_id', ''))
|
|
732
|
+
require(receipt["recovery_ref"] in allowed, f"receipt {path}: conflicting recovery ref ownership")
|
|
706
733
|
for key in ("delivery", "product", "pr_url", "base", "remote"):
|
|
707
734
|
require(key not in receipt or receipt[key] is None or isinstance(receipt[key], str), f"receipt {path}: malformed {key}")
|
|
708
735
|
for key in ("acceptance", "queue", "merge", "workspace_cleanup", "scratch_cleanup"):
|
|
@@ -753,7 +780,8 @@ def derive_state(anchor, common, row, claims, rev):
|
|
|
753
780
|
require(bound["worktree"] == receipt["worktree"] and bound["task"] == identity and bound["branch"] == receipt["branch"]
|
|
754
781
|
and bound["allocation_base"] == receipt["baseline"],
|
|
755
782
|
f"{packet}: conflicting packet ownership")
|
|
756
|
-
|
|
783
|
+
original = common / 'devlyn-completion' / receipt['id'] / 'receipt.json' if archived else path
|
|
784
|
+
for key, expected in (("receipt", original), ("scratch", original.parent / "scratch"), ("submission", original.parent / "submission.json"),
|
|
757
785
|
("evidence_dir", Path(receipt["worktree"]) / ".devlyn/loop")):
|
|
758
786
|
require(bound.get(key) == str(expected), f"{packet}: malformed or conflicting {key} path")
|
|
759
787
|
require(isinstance(bound["inputs_sha"], str) and SHA_RE.fullmatch(bound["inputs_sha"]), f"{packet}: malformed inputs commit")
|
|
@@ -788,7 +816,8 @@ def view(anchor, common, local_only=False, *, pure=False):
|
|
|
788
816
|
claims, unreadable, damaged = {}, [], {}
|
|
789
817
|
canonical = {receipt_path(common, row["identity"]): row["identity"] for row in rows if row["identity"]}
|
|
790
818
|
branches = {branch_of(identity): identity for identity in canonical.values()}
|
|
791
|
-
for path in sorted((common / "devlyn-completion").glob("*/receipt.json")
|
|
819
|
+
for path in sorted([*(common / "devlyn-completion").glob("*/receipt.json"),
|
|
820
|
+
*(path for path in canonical if path.parent.parent.name == "retired")]):
|
|
792
821
|
try:
|
|
793
822
|
receipt = read_json(path)
|
|
794
823
|
require(isinstance(receipt, dict), f"{path}: malformed receipt identity")
|
|
@@ -1322,8 +1351,22 @@ def advance(v, row, opts):
|
|
|
1322
1351
|
if not path.exists() and (refused := allocate(v, row, opts)):
|
|
1323
1352
|
return refused
|
|
1324
1353
|
receipt = read_json(path)
|
|
1354
|
+
if receipt.get('status') == 'ABANDONED':
|
|
1355
|
+
if refused := allocate(v, row, opts):
|
|
1356
|
+
return refused
|
|
1357
|
+
path = receipt_path(v['common'], identity)
|
|
1358
|
+
receipt = read_json(path)
|
|
1359
|
+
accepted = receipt.get('acceptance') or receipt.get('pushed') or receipt.get('pr_number')
|
|
1360
|
+
if receipt.get('allocation') in ('managed_pending', 'managed_owned') and not accepted:
|
|
1361
|
+
require('removal_request' not in receipt.get('storage', {}),
|
|
1362
|
+
'allocation fenced; run ' + shared('task-complete')['receipt_resume'](
|
|
1363
|
+
'recover-storage', path, receipt, flags=['--for-removal']))
|
|
1364
|
+
if receipt.get('allocation') == 'managed_pending' and not accepted:
|
|
1365
|
+
task_complete('recover_storage', receipt=str(path), for_removal=False,
|
|
1366
|
+
operation_id=receipt.get('storage', {}).get('request', {}).get('operation_id'))
|
|
1367
|
+
receipt = read_json(path)
|
|
1325
1368
|
if receipt.get("acceptance") and (path.parent / "packet.json").exists():
|
|
1326
|
-
harness_call("verify_packet", read_json(path.parent / "packet.json"), path.parent / "packet.json", SKILLS)
|
|
1369
|
+
harness_call("verify_packet", read_json(path.parent / "packet.json"), path.parent / "packet.json", SKILLS, accepted_receipt=receipt)
|
|
1327
1370
|
if not receipt.get("acceptance"):
|
|
1328
1371
|
state["phase"] = "inputs"
|
|
1329
1372
|
failure = v["states"][identity].get("inputs_changed")
|
|
@@ -1350,6 +1393,7 @@ def advance(v, row, opts):
|
|
|
1350
1393
|
packet = read_json(path.parent / "packet.json")
|
|
1351
1394
|
# The plan's mode seeds the first delivery call; a later per-task --mode persists in the receipt.
|
|
1352
1395
|
result = task_complete("complete", warnings=opts.warnings, receipt=str(path), acceptance=None,
|
|
1396
|
+
operation_id=receipt.get("storage", {}).get("request", {}).get("operation_id"),
|
|
1353
1397
|
mode=None if local or receipt.get("mode_override") else packet["delivery"], local_only=local, writers_stopped=True)
|
|
1354
1398
|
progress(identity, f"delivery {result['status']}")
|
|
1355
1399
|
if result.get("merge_refused"):
|
|
@@ -1379,6 +1423,8 @@ def summary(v, row):
|
|
|
1379
1423
|
binding = harness_call("read_binding", v["common"])
|
|
1380
1424
|
skills = Path(binding["release"]["root"]) / "config/skills" if binding else SKILLS
|
|
1381
1425
|
argv = [sys.executable, str(skills / "_shared/task-complete.py"), "complete", "--receipt", str(state["path"])]
|
|
1426
|
+
if operation := receipt.get("storage", {}).get("request", {}).get("operation_id"):
|
|
1427
|
+
argv += ["--operation-id", operation]
|
|
1382
1428
|
if receipt.get("writers_released"):
|
|
1383
1429
|
argv.append("--writers-stopped")
|
|
1384
1430
|
if not receipt.get("local_only") and not receipt.get("mode_override"):
|
|
@@ -1438,13 +1484,18 @@ def bring_in(v, loop):
|
|
|
1438
1484
|
return None
|
|
1439
1485
|
receipt = tip["receipt"]
|
|
1440
1486
|
if receipt.get("local_only"):
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1487
|
+
if loop not in v["records"]:
|
|
1488
|
+
return None
|
|
1489
|
+
branch, target = v["records"][loop]["branch"], receipt["branch"]
|
|
1490
|
+
else:
|
|
1491
|
+
if not delivered(receipt):
|
|
1492
|
+
return None
|
|
1493
|
+
branch, target = receipt["base"], f"{receipt['remote']}/{receipt['base']}"
|
|
1494
|
+
# Prepare any merge without touching the anchor, then land by protected fast-forward.
|
|
1495
|
+
# Several loops still apply in either order, including after merge, squash or rebase delivery.
|
|
1496
|
+
command = shlex.join(["python3", str(SKILLS / "_shared" / "bring-in.py"), "--repo", str(v["anchor"]),
|
|
1497
|
+
"--branch", branch, "--target", target])
|
|
1498
|
+
return f"{branch}: {command}"
|
|
1448
1499
|
|
|
1449
1500
|
|
|
1450
1501
|
def write_reports(v, status, reason, warnings):
|
package/harness/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- devlyn:instructions:begin sha256=
|
|
1
|
+
<!-- devlyn:instructions:begin sha256=25ed8e19648642bab459f945a6608ea1d7fe217497c29840d49d3d65a6de1092 -->
|
|
2
2
|
Project-specific instructions outside this managed block take precedence over these defaults.
|
|
3
3
|
|
|
4
4
|
# Project Instructions
|
|
@@ -14,9 +14,9 @@ This contract serves one goal: any capable engine — Claude, GPT/Codex, or a fu
|
|
|
14
14
|
Seven rules govern every change. Cite them by name when a decision touches one.
|
|
15
15
|
|
|
16
16
|
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.
|
|
17
|
-
2. **No overengineering** — smallest change
|
|
17
|
+
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.
|
|
18
18
|
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.
|
|
19
|
-
4. **Worldclass** —
|
|
19
|
+
4. **Worldclass** — zero CRITICAL, zero HIGH security/design findings on the shippable path. Any review is one pass in which every reviewer, including each one the user requires, reads the source the checks ran on; fix each finding or reject it with a reason, re-check the fixes and the invariants they touch, and repeat the full review only while a CRITICAL or HIGH finding remains.
|
|
20
20
|
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
21
21
|
6. **Optimized** — use efficient code and avoid unnecessary work without sacrificing correctness or the user's requirements.
|
|
22
22
|
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|
package/harness/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- devlyn:instructions:begin sha256=
|
|
1
|
+
<!-- devlyn:instructions:begin sha256=25ed8e19648642bab459f945a6608ea1d7fe217497c29840d49d3d65a6de1092 -->
|
|
2
2
|
Project-specific instructions outside this managed block take precedence over these defaults.
|
|
3
3
|
|
|
4
4
|
# Project Instructions
|
|
@@ -14,9 +14,9 @@ This contract serves one goal: any capable engine — Claude, GPT/Codex, or a fu
|
|
|
14
14
|
Seven rules govern every change. Cite them by name when a decision touches one.
|
|
15
15
|
|
|
16
16
|
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.
|
|
17
|
-
2. **No overengineering** — smallest change
|
|
17
|
+
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.
|
|
18
18
|
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.
|
|
19
|
-
4. **Worldclass** —
|
|
19
|
+
4. **Worldclass** — zero CRITICAL, zero HIGH security/design findings on the shippable path. Any review is one pass in which every reviewer, including each one the user requires, reads the source the checks ran on; fix each finding or reject it with a reason, re-check the fixes and the invariants they touch, and repeat the full review only while a CRITICAL or HIGH finding remains.
|
|
20
20
|
5. **Best practice** — idiomatic for the language and framework. Use standard primitives; do not hand-roll what the library already provides.
|
|
21
21
|
6. **Optimized** — use efficient code and avoid unnecessary work without sacrificing correctness or the user's requirements.
|
|
22
22
|
7. **Production ready** — error states are explicit and visible; behavior under failure is what the user expects, not silent corruption.
|