Skip to content

Commit 1f3897a

Browse files
cobusgreylingTHRISHAL12345Cobus Greyling
authored
feat(loop-worktree): wait queue + deadlock detection (#292) (#295)
* feat(loop-worktree): add lease/lock coordinator with deadlock detection * feat(loop-worktree): wait queue + deadlock detection (#292) Cherry-pick from @THRISHAL12345 — adds --wait TTL queueing and wait-for-graph deadlock detection on advisory path locks. - Bump loop-worktree to 1.2.0 - Trim RELEASE_NOTES_DRAFT for next drafter run - Link Discussion #294 in README; update STATE.md publish queue --------- Co-authored-by: THRISHAL12345 <domathrishal123@gmail.com> Co-authored-by: Cobus Greyling <cobusgreyling@Cobuss-MacBook-Pro-2.local>
1 parent 2aae80c commit 1f3897a

14 files changed

Lines changed: 460 additions & 157 deletions

File tree

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ For developers using Grok, Claude Code, Codex, Cursor, and other AI coding agent
100100
| [Contributor quickstart](https://github.com/cobusgreyling/loop-engineering/discussions/123) | **Help wanted:** 21 scoped `good first issues` — comment *I'll take this* to get assigned |
101101
| [Community update](https://github.com/cobusgreyling/loop-engineering/discussions/145) | **July 4:** 5.5k stars, traffic sources, contributor merges |
102102
| [Community week (Jul 8)](https://github.com/cobusgreyling/loop-engineering/discussions/219) | loop-worktree npm, MCP quickstart, tool appendices |
103+
| [npm update (Jul 16)](https://github.com/cobusgreyling/loop-engineering/discussions/294) | `loop-context` 1.2.0 + `loop-worktree` 1.1.0 — daily budget, path locks |
103104
| [Maintenance (Jul 10)](https://github.com/cobusgreyling/loop-engineering/discussions/241) | Doc sync, branch prune, loop-audit 1.6.0 follow-up |
104105
| [Prior release notes](https://github.com/cobusgreyling/loop-engineering/discussions/89) | v1.5.0 — loop-sync, constraints, MCP server |
105106
| [Add your project](https://github.com/cobusgreyling/loop-engineering/discussions/92) | **Pinned:** Loop Ready badge + adopters list |

‎RELEASE_NOTES_DRAFT.md‎

Lines changed: 11 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,29 @@
1-
# Release notes draft — since `loop-context-v1.1.0`
1+
# Release notes draft — since `loop-context-v1.2.0`
22

3-
**Status:** Ready for human review (release prep [#272](https://github.com/cobusgreyling/loop-engineering/issues/272), 2026-07-15).
3+
**Status:** Stub for next changelog-drafter run.
44

5-
**Window:** 2026-07-13 → 2026-07-15 (tag `loop-context-v1.1.0` through maintainer batch)
5+
**Last published:** [Discussion #294](https://github.com/cobusgreyling/loop-engineering/discussions/294) (2026-07-16) — `loop-context` 1.2.0, `loop-worktree` 1.1.0.
6+
7+
**Window:** 2026-07-16 → (next tag)
68

79
---
810

911
## Highlights
1012

11-
### Published Jul 13 — npm batch
12-
13-
| Package | Version | Tag | What shipped |
14-
|---------|---------|-----|--------------|
15-
| `@cobusgreyling/loop-cost` | **1.1.0** | `loop-cost-v1.1.0` | Multi-agent orchestration cost on the action path ([#256](https://github.com/cobusgreyling/loop-engineering/pull/256), @KhaiTrang1995) |
16-
| `@cobusgreyling/loop-context` | **1.1.0** | `loop-context-v1.1.0` | `--budget-from-pattern` resolves per-run token caps from `loop-cost` ([#258](https://github.com/cobusgreyling/loop-engineering/pull/258), @KhaiTrang1995) |
17-
| `@cobusgreyling/loop-init` | **1.4.0** | `loop-init-v1.4.0` | Dependency sync + maintenance publish ([#271](https://github.com/cobusgreyling/loop-engineering/pull/271)) |
18-
19-
```bash
20-
npx @cobusgreyling/loop-context --check --ledger run.json \
21-
--budget-from-pattern ci-sweeper --budget-level L2
22-
npx @cobusgreyling/loop-cost --pattern ci-sweeper --level L2 --json
23-
```
13+
_(changelog-drafter will fill from merges since `loop-context-v1.2.0`)_
2414

25-
### Next npm publish (merged 2026-07-15)
15+
### Pending npm publish
2616

2717
| Package | PR | What ships |
2818
|---------|-----|------------|
29-
| `@cobusgreyling/loop-context` **1.2.0** | [#273](https://github.com/cobusgreyling/loop-engineering/pull/273) | Daily token spend tracking (`--daily-budget-from-pattern`), `--on-exceed` hook for `loop-budget.md` automation (@KhaiTrang1995) |
30-
| `@cobusgreyling/loop-worktree` **1.1.0** | [#274](https://github.com/cobusgreyling/loop-engineering/pull/274) | Advisory path locking (`lock` / `unlock` / `locks`) for multi-loop collision prevention (@KhaiTrang1995) |
31-
32-
```bash
33-
# Daily cap across separate runs
34-
npx @cobusgreyling/loop-context --check --ledger run.json \
35-
--daily-budget-from-pattern ci-sweeper --on-exceed ./scripts/on-budget-exceed.sh
36-
37-
# Multi-loop path coordination
38-
npx @cobusgreyling/loop-worktree lock --paths 'package-lock.json,pnpm-lock.yaml' --owner ci-sweeper
39-
npx @cobusgreyling/loop-worktree locks --json
40-
```
41-
42-
### Infra
43-
44-
- **Star-history CI** — `update-star-history.yml` now requires `STAR_HISTORY_TOKEN` (fine-grained PAT). `GITHUB_TOKEN` cannot list stargazers under GitHub's Jul 2026 API restriction. SVGs refreshed to ~7,928 stars.
45-
46-
---
47-
48-
## Docs & examples (since prior draft)
49-
50-
| PR | Contributor | What shipped |
51-
|----|-------------|--------------|
52-
| [#268](https://github.com/cobusgreyling/loop-engineering/pull/268) | @Mahizhan-S | Cursor post-merge cleanup example |
53-
| [#251](https://github.com/cobusgreyling/loop-engineering/pull/251) | @Mahizhan-S | Cursor dependency sweeper example |
54-
| [#247](https://github.com/cobusgreyling/loop-engineering/pull/247) | @AshayK003 | Hermes PR Babysitter example |
55-
| [#257](https://github.com/cobusgreyling/loop-engineering/pull/257) | @k-anushka14 | dependency-vs-ci-sweeper collision story |
56-
| [#249](https://github.com/cobusgreyling/loop-engineering/pull/249) | @Mahizhan-S | Codeium (Windsurf) primitives appendix |
57-
| [#263](https://github.com/cobusgreyling/loop-engineering/pull/263) | maintainer | `loop-sync` QUICKSTART subsection |
19+
| `@cobusgreyling/loop-worktree` **1.2.0** | [#292](https://github.com/cobusgreyling/loop-engineering/pull/292) | Wait queue (`--wait`) + deadlock detection on path locks (@THRISHAL12345) |
20+
| `@cobusgreyling/loop-gate` **1.0.0** | [#291](https://github.com/cobusgreyling/loop-engineering/pull/291) | Mechanical denylist + auto-merge allowlist from `gate.yaml` (@KhaiTrang1995) |
5821

5922
---
6023

61-
## npm packages (current on main)
62-
63-
| Package | Version | Notes |
64-
|---------|---------|-------|
65-
| `@cobusgreyling/loop-context` | 1.1.0 → **1.2.0** pending tag | Daily budget + on-exceed hook after #273 publish |
66-
| `@cobusgreyling/loop-worktree` | 1.0.0 → **1.1.0** pending tag | Path locking after #274 publish |
67-
| `@cobusgreyling/loop-cost` | **1.1.0** | Multi-agent orchestration estimates |
68-
| `@cobusgreyling/loop-init` | **1.4.0** | |
69-
| `@cobusgreyling/loop-audit` | **1.6.0** | No change this window |
70-
| `@cobusgreyling/loop-mcp-server` | 1.0.0 | No change |
71-
| `@cobusgreyling/loop-sync` | 1.0.0 | No change |
72-
| `@cobusgreyling/goal-audit` | 1.0.2 | Companion stack |
73-
74-
---
75-
76-
## Suggested publish sequence
77-
78-
After review, tag and push in dependency order:
79-
80-
```bash
81-
git tag loop-context-v1.2.0 && git push origin loop-context-v1.2.0
82-
git tag loop-worktree-v1.1.0 && git push origin loop-worktree-v1.1.0
83-
```
84-
85-
Then publish a GitHub Discussion (see prior drafts [#219](https://github.com/cobusgreyling/loop-engineering/discussions/219), [#241](https://github.com/cobusgreyling/loop-engineering/discussions/241)) and close [#272](https://github.com/cobusgreyling/loop-engineering/issues/272).
86-
87-
---
88-
89-
## Try it in 5 minutes
24+
## Try it
9025

9126
```bash
9227
npx @cobusgreyling/loop-init . --pattern daily-triage --tool grok
9328
npx @cobusgreyling/loop-audit . --suggest
94-
npx @cobusgreyling/loop-context --check --ledger run.json --budget-from-pattern daily-triage
95-
```
96-
97-
Week two (L2): pair `loop-worktree` path locks with `loop-context --daily-budget-from-pattern` — see [QUICKSTART](docs/QUICKSTART.md).
29+
```

‎STATE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Last run: 2026-07-15T09:44:51Z (automated daily-triage workflow)
55
## High Priority (loop is acting or waiting on human)
66

77
- Maintain loop readiness score ≥ 58 (current: **100**, level **L3**).
8-
- Keep npm packages current after tool changes (tag `loop-audit-v*`, `loop-init-v*`, `loop-cost-v*`, `loop-context-v*`, `loop-worktree-v*` — see docs/RELEASE.md). **Pending publish:** `loop-context` 1.2.0, `loop-worktree` 1.1.0 (merged #273, #274).
8+
- Keep npm packages current after tool changes (tag `loop-audit-v*`, `loop-init-v*`, `loop-cost-v*`, `loop-context-v*`, `loop-worktree-v*`, `loop-gate-v*` — see docs/RELEASE.md). **Pending publish:** `loop-worktree` 1.2.0 (wait queue + deadlock detection, #292), `loop-gate` 1.0.0 (#291).
99

1010

1111
## Watch List

‎tools/loop-worktree/README.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,15 @@ loop-worktree gc [--force] [--json]
3939

4040
loop-worktree list [--status active] [--json]
4141

42-
loop-worktree lock --paths <glob1,glob2,...> --owner <name> [--ttl 6h]
42+
loop-worktree lock --paths <glob1,glob2,...> --owner <name> [--ttl 6h] [--wait 15m]
4343
# advisory lock: fails if another (non-expired) owner already holds an overlapping path
44+
# --wait queues instead of failing immediately; detects wait-for-graph deadlocks
4445

4546
loop-worktree unlock --owner <name>
4647
# releases that owner's lock; no-op if it didn't hold one
4748

4849
loop-worktree locks [--sweep] [--force] [--json]
49-
# lists active locks; --sweep reports expired ones (report-only; --force deletes them)
50+
# lists active locks and wait intents; --sweep reports expired ones (report-only; --force deletes them)
5051
```
5152

5253
## Status

‎tools/loop-worktree/dist/cli.js‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import { createWorktree, markWorktree, cleanupWorktrees, gc, listWorktrees, VALID_STATUSES, } from './worktree.js';
3-
import { lockPaths, unlockOwner, listLocks, sweepExpiredLocks, isExpired } from './lock.js';
3+
import { lockPaths, unlockOwner, listLocks, sweepExpiredLocks, isExpired, listWaits, isWaitExpired } from './lock.js';
44
function parseFlags(argv) {
55
const flags = { root: process.cwd(), force: false, json: false, sweep: false };
66
for (let i = 0; i < argv.length; i++) {
@@ -27,6 +27,8 @@ function parseFlags(argv) {
2727
flags.owner = argv[++i];
2828
else if (a === '--ttl')
2929
flags.ttl = argv[++i];
30+
else if (a === '--wait')
31+
flags.wait = argv[++i];
3032
else if (a === '--sweep')
3133
flags.sweep = true;
3234
}
@@ -49,7 +51,7 @@ Usage:
4951
loop-worktree cleanup [--status rejected,escalated] [--older-than 24h] [--force]
5052
loop-worktree gc [--force] [--json]
5153
loop-worktree list [--status <s>] [--json]
52-
loop-worktree lock --paths <glob1,glob2,...> --owner <name> [--ttl 6h]
54+
loop-worktree lock --paths <glob1,glob2,...> --owner <name> [--ttl 6h] [--wait 15m]
5355
loop-worktree unlock --owner <name>
5456
loop-worktree locks [--sweep] [--force] [--json]
5557
@@ -63,6 +65,7 @@ Locking (advisory, not enforced by create -- pair it in your control script):
6365
--paths <csv> Comma-separated path globs this owner is about to touch
6466
--owner <name> Lock/unlock identity, typically the pattern name
6567
--ttl <dur> Optional expiry (e.g. 30m, 6h, 1d); omit for no auto-expiry
68+
--wait <dur> Wait up to this long for locks to clear instead of failing immediately (detects deadlocks)
6669
locks --sweep Report expired locks (report-only; --force deletes them)
6770
6871
Worktrees live under .loop-worktrees/, tracked in .loop-worktrees/manifest.json.
@@ -154,7 +157,7 @@ async function main() {
154157
throw new Error('lock requires --paths and --owner.');
155158
}
156159
const paths = flags.paths.split(',').map((p) => p.trim()).filter(Boolean);
157-
const entry = await lockPaths({ root: flags.root, owner: flags.owner, paths, ttl: flags.ttl });
160+
const entry = await lockPaths({ root: flags.root, owner: flags.owner, paths, ttl: flags.ttl, wait: flags.wait });
158161
console.log(`locked ${entry.paths.join(', ')} for ${entry.owner}` + (entry.expiresAt ? ` (expires ${entry.expiresAt})` : ''));
159162
return 0;
160163
}
@@ -176,23 +179,36 @@ async function main() {
176179
for (const l of result.expired) {
177180
console.log(result.removed.includes(l.owner) ? `removed expired lock ${l.owner}` : `expired lock ${l.owner}`);
178181
}
179-
console.log(`locks --sweep: ${result.expired.length} expired` +
180-
(flags.force ? `, ${result.removed.length} removed` : ' (report-only; use --force to remove)'));
182+
const expiredWaits = result.expiredWaits || [];
183+
const removedWaits = result.removedWaits || [];
184+
for (const w of expiredWaits) {
185+
console.log(removedWaits.includes(w.owner) ? `removed expired wait ${w.owner}` : `expired wait ${w.owner}`);
186+
}
187+
console.log(`locks --sweep: ${result.expired.length} lock(s), ${expiredWaits.length} wait(s) expired` +
188+
(flags.force ? `, ${result.removed.length + removedWaits.length} removed` : ' (report-only; use --force to remove)'));
181189
return 0;
182190
}
183191
const locks = await listLocks(flags.root);
192+
const waits = await listWaits(flags.root);
184193
if (flags.json) {
185-
console.log(JSON.stringify(locks.map((l) => ({ ...l, expired: isExpired(l) })), null, 2));
194+
console.log(JSON.stringify({
195+
locks: locks.map((l) => ({ ...l, expired: isExpired(l) })),
196+
waits: waits.map((w) => ({ ...w, expired: isWaitExpired(w) }))
197+
}, null, 2));
186198
return 0;
187199
}
188-
if (locks.length === 0) {
189-
console.log('no active locks');
200+
if (locks.length === 0 && waits.length === 0) {
201+
console.log('no active locks or waits');
190202
return 0;
191203
}
192204
for (const l of locks) {
193205
const expiryNote = l.expiresAt ? `, expires ${l.expiresAt}${isExpired(l) ? ' (expired)' : ''}` : '';
194206
console.log(`${l.owner} ${l.paths.join(', ')} (locked ${l.lockedAt}${expiryNote})`);
195207
}
208+
for (const w of waits) {
209+
const expiryNote = w.expiresAt ? `, wait expires ${w.expiresAt}${isWaitExpired(w) ? ' (expired)' : ''}` : '';
210+
console.log(`${w.owner} [WAITING ON: ${w.waitingOn.join(', ')}] ${w.paths.join(', ')} (requested ${w.requestedAt}${expiryNote})`);
211+
}
196212
return 0;
197213
}
198214
default:

‎tools/loop-worktree/dist/lock.d.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ export interface LockEntry {
66
/** Absent means the lock never expires automatically; must be explicitly unlocked. */
77
expiresAt?: string;
88
}
9+
export interface WaitEntry {
10+
owner: string;
11+
paths: string[];
12+
waitingOn: string[];
13+
requestedAt: string;
14+
expiresAt?: string;
15+
}
16+
export declare function listWaits(root: string): Promise<WaitEntry[]>;
17+
export declare function isWaitExpired(wait: WaitEntry, now?: number): boolean;
918
export declare function isExpired(lock: LockEntry, now?: number): boolean;
1019
/**
1120
* Two path globs "overlap" if, segment by segment, every position where both
@@ -25,6 +34,8 @@ export interface LockPathsInput {
2534
paths: string[];
2635
/** e.g. "6h" -- if omitted, the lock never expires automatically. */
2736
ttl?: string;
37+
/** e.g. "15m" -- wait duration if paths are locked. */
38+
wait?: string;
2839
}
2940
/**
3041
* Acquire an advisory lock on `paths` for `owner`. Fails if any *other*,
@@ -37,6 +48,8 @@ export declare function unlockOwner(root: string, owner: string): Promise<boolea
3748
export interface SweepExpiredLocksResult {
3849
expired: LockEntry[];
3950
removed: string[];
51+
expiredWaits?: WaitEntry[];
52+
removedWaits?: string[];
4053
}
4154
/** Report (and, with force, delete) locks past their own TTL. Never touches an active lock. */
4255
export declare function sweepExpiredLocks(root: string, opts?: {

0 commit comments

Comments
 (0)