devrites 6.1.3 → 6.1.4

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to DevRites are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and DevRites adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Releases are generated automatically by [semantic-release](https://semantic-release.gitbook.io/) from Conventional Commits on `main`.
4
4
 
5
+ ## [6.1.4](https://github.com/ViktorsBaikers/DevRites/compare/v6.1.3...v6.1.4) (2026-10-16)
6
+
7
+ ### Fixed
8
+
9
+ * **devrites:** free dead-session claims and surface writer-safe rule ([#110](https://github.com/ViktorsBaikers/DevRites/issues/110)) ([3d431b1](https://github.com/ViktorsBaikers/DevRites/commit/3d431b12cab83d4aaff7aaa4ced2c75210416a90))
10
+
5
11
  ## [6.1.3](https://github.com/ViktorsBaikers/DevRites/compare/v6.1.2...v6.1.3) (2026-10-16)
6
12
 
7
13
  ### Changed
package/README.md CHANGED
@@ -31,7 +31,7 @@ history can't lose it. After you clear the conversation, switch hosts, or hand
31
31
  the feature to another engineer, the next session reads the same plan,
32
32
  decisions, and evidence before it continues.
33
33
 
34
- **Status:** [`v6.1.3`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.1.3): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
34
+ **Status:** [`v6.1.4`](https://github.com/ViktorsBaikers/DevRites/releases/tag/v6.1.4): see [`CHANGELOG.md`](CHANGELOG.md) for release notes.
35
35
 
36
36
  This is the latest published release; `main` may contain unreleased work.
37
37
 
@@ -55,6 +55,9 @@ type claimRecord struct {
55
55
  // so a later session can prove that holder exited (see holderProbe).
56
56
  Host string `json:"host,omitempty"`
57
57
  Anchors []claimProc `json:"anchors,omitempty"`
58
+ // Holder is the anchor that is the claiming agent session itself; its
59
+ // exit releases the claim even while the shell that launched it lives.
60
+ Holder *claimProc `json:"holder,omitempty"`
58
61
  }
59
62
 
60
63
  // RunClaim manages the advisory file-claim ledger.
@@ -318,6 +321,7 @@ func claimAddLocked(path, session, reason string, ttl int, paths []string, stdou
318
321
  ExpiresAt: now.Add(time.Duration(ttl) * time.Minute).Format(time.RFC3339),
319
322
  Host: probe.host,
320
323
  Anchors: probe.anchors(now),
324
+ Holder: probe.holder(now),
321
325
  }
322
326
  if err := appendClaim(path, rec); err != nil {
323
327
  fmt.Fprintf(stderr, "claim: %v\n", err)
@@ -3,6 +3,7 @@ package lib
3
3
  import (
4
4
  "os"
5
5
  "os/exec"
6
+ "path/filepath"
6
7
  "slices"
7
8
  "strconv"
8
9
  "strings"
@@ -23,6 +24,14 @@ type claimProc struct {
23
24
  type procInfo struct {
24
25
  ppid int
25
26
  start string
27
+ comm string
28
+ }
29
+
30
+ // shells outlive the agent sessions they launch (a terminal pane, a
31
+ // multiplexer's login shell), so a shell never stands for the holder.
32
+ var shells = map[string]bool{
33
+ "sh": true, "bash": true, "zsh": true, "fish": true, "dash": true, "ksh": true,
34
+ "tcsh": true, "csh": true, "nu": true, "pwsh": true, "powershell": true, "cmd": true,
26
35
  }
27
36
 
28
37
  // processTable lists running processes keyed by pid, or nil when the host
@@ -30,7 +39,7 @@ type procInfo struct {
30
39
  var processTable = psProcessTable
31
40
 
32
41
  func psProcessTable() map[int]procInfo {
33
- cmd := exec.Command("ps", "-A", "-o", "pid=,ppid=,lstart=")
42
+ cmd := exec.Command("ps", "-A", "-o", "pid=,ppid=,lstart=,comm=")
34
43
  cmd.Env = append(os.Environ(), "LC_ALL=C")
35
44
  out, err := cmd.Output()
36
45
  if err != nil {
@@ -39,16 +48,16 @@ func psProcessTable() map[int]procInfo {
39
48
  table := map[int]procInfo{}
40
49
  for _, line := range strings.Split(string(out), "\n") {
41
50
  f := strings.Fields(line)
42
- if len(f) != 7 {
51
+ if len(f) < 8 {
43
52
  continue
44
53
  }
45
54
  pid, perr := strconv.Atoi(f[0])
46
55
  ppid, pperr := strconv.Atoi(f[1])
47
- start, serr := time.ParseInLocation("Mon Jan 2 15:04:05 2006", strings.Join(f[2:], " "), time.Local)
56
+ start, serr := time.ParseInLocation("Mon Jan 2 15:04:05 2006", strings.Join(f[2:7], " "), time.Local)
48
57
  if perr != nil || pperr != nil || serr != nil {
49
58
  continue
50
59
  }
51
- table[pid] = procInfo{ppid: ppid, start: start.UTC().Format(time.RFC3339)}
60
+ table[pid] = procInfo{ppid: ppid, start: start.UTC().Format(time.RFC3339), comm: strings.Join(f[7:], " ")}
52
61
  }
53
62
  if len(table) == 0 {
54
63
  return nil
@@ -99,12 +108,33 @@ func (p holderProbe) anchors(now time.Time) []claimProc {
99
108
  return out
100
109
  }
101
110
 
111
+ // holder returns the nearest anchor that is not a shell: the agent session
112
+ // process that owns the claim. A shell or terminal multiplexer above it can
113
+ // outlive the session, so only the session's own exit proves release.
114
+ func (p holderProbe) holder(now time.Time) *claimProc {
115
+ for _, proc := range p.anchors(now) {
116
+ name := strings.TrimPrefix(filepath.Base(p.table[proc.PID].comm), "-")
117
+ if name != "" && !shells[name] {
118
+ return &proc
119
+ }
120
+ }
121
+ return nil
122
+ }
123
+
102
124
  // gone reports whether rec's holder is proven exited: the claim was taken on
103
- // this host and every anchor it does not share with the caller has exited.
125
+ // this host and its recorded holder session has exited, or, for a claim with
126
+ // no recorded holder, every anchor it does not share with the caller has.
104
127
  func (p holderProbe) gone(rec *claimRecord) bool {
105
128
  if p.table == nil || rec.Host == "" || rec.Host != p.host {
106
129
  return false
107
130
  }
131
+ if h := rec.Holder; h != nil {
132
+ if slices.Contains(p.self, *h) {
133
+ return false
134
+ }
135
+ proc, ok := p.table[h.PID]
136
+ return !ok || proc.start != h.Start
137
+ }
108
138
  unique := 0
109
139
  for _, a := range rec.Anchors {
110
140
  if slices.Contains(p.self, a) {
@@ -441,6 +441,9 @@ func admitWrightRounds(root, slug string, f *dispatchFile, w *dispatchWave, revi
441
441
  lines = append(lines, fmt.Sprintf("dispatch: WARNING: %s earlier wave(s) %s never returned; record `dispatch return` or `dispatch abandon --reason`", id, strings.Join(unreturned, ",")))
442
442
  }
443
443
  }
444
+ if kind == "wright" {
445
+ lines = append(lines, "dispatch: wright contract: name the writer-safe checks the wright runs (syntax/type/lint, unit and self-check suites, a narrow local smoke) and never forbid them; only artifact-producing, consumptive, or full canonical gates stay root-owned")
446
+ }
444
447
  if len(overCap) == 0 {
445
448
  if review != "" {
446
449
  fmt.Fprintf(stderr, "dispatch: --reslice-review given but no slice has reached its %d-round cap\n", wrightRoundsPerReview)
@@ -27,6 +27,9 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md)
27
27
  ([`one-slice-cycle.md`](reference/one-slice-cycle.md),
28
28
  [`afk-discipline.md`](reference/afk-discipline.md)); dispatch uses
29
29
  [`reference/wright-dispatch.md`](reference/wright-dispatch.md).
30
+ Every wright contract names the writer-safe checks the wright runs (syntax/type/lint,
31
+ unit and self-check suites, a narrow local smoke) and never forbids them; only
32
+ artifact-producing, consumptive, or full canonical gates stay root-owned.
30
33
 
31
34
  ## Required rules
32
35
 
@@ -27,6 +27,9 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md)
27
27
  ([`one-slice-cycle.md`](reference/one-slice-cycle.md),
28
28
  [`afk-discipline.md`](reference/afk-discipline.md)); dispatch uses
29
29
  [`reference/wright-dispatch.md`](reference/wright-dispatch.md).
30
+ Every wright contract names the writer-safe checks the wright runs (syntax/type/lint,
31
+ unit and self-check suites, a narrow local smoke) and never forbids them; only
32
+ artifact-producing, consumptive, or full canonical gates stay root-owned.
30
33
 
31
34
  ## Required rules
32
35
 
@@ -27,6 +27,9 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md)
27
27
  ([`one-slice-cycle.md`](reference/one-slice-cycle.md),
28
28
  [`afk-discipline.md`](reference/afk-discipline.md)); dispatch uses
29
29
  [`reference/wright-dispatch.md`](reference/wright-dispatch.md).
30
+ Every wright contract names the writer-safe checks the wright runs (syntax/type/lint,
31
+ unit and self-check suites, a narrow local smoke) and never forbids them; only
32
+ artifact-producing, consumptive, or full canonical gates stay root-owned.
30
33
 
31
34
  ## Required rules
32
35
 
@@ -26,6 +26,9 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md)
26
26
  ([`one-slice-cycle.md`](reference/one-slice-cycle.md),
27
27
  [`afk-discipline.md`](reference/afk-discipline.md)); dispatch uses
28
28
  [`reference/wright-dispatch.md`](reference/wright-dispatch.md).
29
+ Every wright contract names the writer-safe checks the wright runs (syntax/type/lint,
30
+ unit and self-check suites, a narrow local smoke) and never forbids them; only
31
+ artifact-producing, consumptive, or full canonical gates stay root-owned.
29
32
 
30
33
  ## Required rules
31
34
 
@@ -27,6 +27,9 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md)
27
27
  ([`one-slice-cycle.md`](reference/one-slice-cycle.md),
28
28
  [`afk-discipline.md`](reference/afk-discipline.md)); dispatch uses
29
29
  [`reference/wright-dispatch.md`](reference/wright-dispatch.md).
30
+ Every wright contract names the writer-safe checks the wright runs (syntax/type/lint,
31
+ unit and self-check suites, a narrow local smoke) and never forbids them; only
32
+ artifact-producing, consumptive, or full canonical gates stay root-owned.
30
33
 
31
34
  ## Required rules
32
35
 
@@ -27,6 +27,9 @@ Execute [`reference/phase-contract.md`](reference/phase-contract.md)
27
27
  ([`one-slice-cycle.md`](reference/one-slice-cycle.md),
28
28
  [`afk-discipline.md`](reference/afk-discipline.md)); dispatch uses
29
29
  [`reference/wright-dispatch.md`](reference/wright-dispatch.md).
30
+ Every wright contract names the writer-safe checks the wright runs (syntax/type/lint,
31
+ unit and self-check suites, a narrow local smoke) and never forbids them; only
32
+ artifact-producing, consumptive, or full canonical gates stay root-owned.
30
33
 
31
34
  ## Required rules
32
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devrites",
3
- "version": "6.1.3",
3
+ "version": "6.1.4",
4
4
  "description": "DevRites: a disciplined senior-engineer workflow pack for Claude Code, Codex, omp, pi, and Devin CLI",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "homepage": "https://github.com/ViktorsBaikers/DevRites#readme",