devlyn-cli 2.9.3 → 2.10.0
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 +3 -3
- package/CLAUDE.md +4 -4
- package/README.md +4 -4
- package/benchmark/auto-resolve/fixtures/SCHEMA.md +1 -1
- package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +1 -0
- package/bin/devlyn.js +55 -7
- package/config/skills/_shared/adapters/README.md +2 -2
- package/config/skills/_shared/adapters/grok.md +77 -0
- package/config/skills/_shared/archive_run.py +35 -1
- package/config/skills/_shared/collect-codex-findings.py +15 -10
- package/config/skills/_shared/engine-doctor.sh +6 -3
- package/config/skills/_shared/engine-preflight.md +12 -5
- package/config/skills/_shared/resolve-bootstrap.py +630 -0
- package/config/skills/_shared/resolve-stop-hook.py +325 -0
- package/config/skills/_shared/state-phase-write.py +2264 -13
- package/config/skills/_shared/terminal-claim-check.py +537 -0
- package/config/skills/_shared/verify-merge-findings.py +363 -16
- package/config/skills/devlyn:engines/SKILL.md +1 -1
- package/config/skills/devlyn:queue/SKILL.md +12 -0
- package/config/skills/devlyn:resolve/SKILL.md +54 -73
- package/config/skills/devlyn:resolve/references/phases/surface-close.md +34 -0
- package/config/skills/devlyn:resolve/references/phases/verify.md +65 -96
- package/config/skills/devlyn:resolve/references/state-schema.md +25 -18
- package/optional-skills/devlyn:reap/SKILL.md +1 -1
- package/optional-skills/devlyn:reap/scripts/reap.sh +25 -13
- package/optional-skills/devlyn:reap/scripts/scan.sh +15 -1
- package/package.json +1 -1
- package/scripts/lint-skills.sh +131 -67
|
@@ -6,7 +6,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
6
6
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
9
|
-
"version": "
|
|
9
|
+
"version": "3.0",
|
|
10
10
|
"run_id": "rs-<UTC-timestamp>-<12-hex>",
|
|
11
11
|
"started_at": "2026-04-30T12:00:00Z",
|
|
12
12
|
"engine": "claude",
|
|
@@ -33,6 +33,7 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
33
33
|
"plan": null,
|
|
34
34
|
"probe_derive": null,
|
|
35
35
|
"implement": null,
|
|
36
|
+
"surface_close": null,
|
|
36
37
|
"build_gate": null,
|
|
37
38
|
"cleanup": null,
|
|
38
39
|
"verify": null,
|
|
@@ -45,27 +46,29 @@ Single authoritative verdict source for `/devlyn:resolve`. The orchestrator bran
|
|
|
45
46
|
## Field rules
|
|
46
47
|
|
|
47
48
|
- **version** — string. Bump major on a breaking schema change.
|
|
49
|
+
- **session_id** — optional string or null stamped from `CLAUDE_CODE_SESSION_ID`; a missing/null value leaves Stop-hook pressure inert.
|
|
48
50
|
- **mode** — `"free-form" | "spec" | "verify-only"`.
|
|
49
|
-
- **pair_verify** — boolean.
|
|
51
|
+
- **pair_verify** — boolean. True only for `--pair-verify`. Pairing is already default-when-available; this flag makes OTHER-engine availability an explicit fail-closed promise and adds `mode.pair-verify` telemetry. It is mutually exclusive with `risk_profile.pair_default_enabled == false` from `--no-pair`.
|
|
50
52
|
- **complexity** — `null | "trivial" | "medium" | "large"`. Free-form mode populates this; spec/verify-only mode leaves it null.
|
|
51
53
|
- **engine** — any engine name with a shipped adapter (`_shared/adapters/<name>.md`); `"claude"` and `"codex"` today. A required unavailable engine stops the run with `BLOCKED:<engine>-unavailable`.
|
|
52
54
|
- **engine_source** — `"flag" | "engines.json" | "default"` — provenance of the resolved executor engine (`_shared/engine-preflight.md#role-resolution`). Optional on archived pre-iter-0038 runs; absent means `default`.
|
|
53
|
-
- **source** —
|
|
54
|
-
- **risk_profile** — PHASE 0
|
|
55
|
+
- **source** — downstream contract provenance. Spec/verify-only set `type: "spec"`, `spec_path`, and `spec_sha256`. Free-form sets `type: "generated"`, `goal_path: ".devlyn/goal.raw.txt"` + exact-byte `goal_sha256`, and `criteria_path: ".devlyn/criteria.generated.md"` + raw-byte `criteria_sha256`. VERIFY re-checks its contract hash.
|
|
56
|
+
- **risk_profile** — PHASE 0 routing state. `high_risk` controls automatic risk probes; `risk_probes_enabled` / `risk_probes_explicit` preserve their current semantics; `pair_default_enabled` is false only for explicit `--no-pair`. `risk_profile` must remain an object with boolean `high_risk`, `risk_probes_enabled`, `risk_probes_explicit`, and `pair_default_enabled` fields when present, plus `reasons` as a string list. Malformed state blocks VERIFY because it can hide required routes.
|
|
55
57
|
- **risk_probes_digest** — top-level sha256 written by PHASE 1.5 after probe validation; BUILD_GATE/VERIFY replay semantics live in `phases/build-gate.md` step 4.
|
|
56
58
|
- **rounds.global** — incremented every fix-loop pass (BUILD_GATE → fix-loop, VERIFY → fix-loop, OR a phase-gate fix respawn inside phase-gated IMPLEMENT).
|
|
57
59
|
- **phases.implement.exec** — only on phase-gated large runs (plan.md has `## Execution phases` with >1 phase): `{ "total": N, "current": k, "statuses": ["PASS"|"FAIL"|null, ...], "commits": ["<sha>", ...] }`. Phase definitions live in plan.md (immutable contract); progress lives here (routing truth — never route on plan.md checkboxes). Absent on single-phase runs. `cumulative.patch` for such runs is `git diff <base_ref.sha>...HEAD`.
|
|
58
60
|
- **phases.probe_derive** — optional PHASE 1.5 entry when `--risk-probes` is enabled. Artifacts include `.devlyn/risk-probes.jsonl`. Probe failures later surface through BUILD_GATE/VERIFY as `correctness.risk-probe-failed`.
|
|
61
|
+
- **phases.surface_close** — generated trivial/medium one-shot: `pre_sha`, `input_patch_sha256`, assembled-byte `prompt_sha256`, `untracked_before`; spawn requires Claude plus a requested model, and `surface-check` gates PASS. `.devlyn/surface-close.output.json` retains the Claude JSON wrapper used for completion attestation while the round JSONL remains the execution-audit backstop. Automatic Claude unavailability alone records null verdict plus `skipped_reason: "auto_surface_close_claude_unavailable"`. After an adjudication-malformed reply only, successful rollback plus transcript write-audit records the two nonterminal fields `skipped_reason: "surface_close_rolled_back_adjudication_malformed"` and `continued_after_block: true`; neither creates a terminal floor. Other failures store bare `"BLOCKED"`. `durability[]` is append-only: `{round, origin_phase, receipt_path, receipt_sha256, restore_commit_sha}` per `.devlyn/closure-durability.round-<n>.json`.
|
|
59
62
|
- **bypasses** — array of phase names from `--bypass`. Valid: `"build-gate" | "cleanup"`. PLAN, IMPLEMENT, VERIFY are non-bypassable (orchestrator rejects at parse time).
|
|
60
|
-
- **implement_passed_sha** — captured at end of PHASE 2; null until then. Activates the post-implement invariant for CLEANUP and VERIFY.
|
|
63
|
+
- **implement_passed_sha** — captured at end of PHASE 2; null until then. Activates the post-implement invariant for SURFACE_CLOSE, CLEANUP, and VERIFY.
|
|
61
64
|
- **criteria** — generated from spec's `## Requirements` checklist (one per `- [ ]`). `status: pending → implemented` is the legal transition. `failed_by_finding_ids` populates when VERIFY surfaces a finding tied to a criterion.
|
|
62
65
|
- **`.devlyn/untracked.baseline` artifact** — written at PHASE 0 (`spec-verify-check.py --write-untracked-baseline`); BUILD_GATE flags created-during-run unauthorized untracked files against it (`.devlyn/` exempt; semantics in `phases/build-gate.md` step 4).
|
|
63
|
-
- **verify.coverage_failed** — set
|
|
64
|
-
- **verify.pair_trigger** —
|
|
66
|
+
- **verify.coverage_failed** — outcome-dependent telemetry set when JUDGE cannot exercise a spec axis. It no longer gates pair dispatch: after MECHANICAL passes, `pair.default` dispatches both judges concurrently whenever `pair_default_enabled != false` and the OTHER engine is available. An orchestrator without foreground parallel dispatch runs the same two required judges sequentially; a primary blocker never skips the pair. A verdict-binding MECHANICAL blocker still skips both. Legacy complexity values remain accepted only for archived compatibility.
|
|
67
|
+
- **verify.pair_trigger** — strict decision state: `{ "eligible": boolean, "reasons": string[], "skipped_reason": string|null }`. Schema v3.0 eligible state requires `pair.default` plus every applicable telemetry reason and `skipped_reason: null`; outcome-dependent reasons are appended after both judges return. Canonical reasons are `pair.default`, `mode.verify-only`, `mode.pair-verify`, `complexity.high`, `complexity.large`, `spec.complexity.high`, `spec.complexity.large`, `spec.solo_headroom_hypothesis`, `risk.high`, `risk_probes.enabled`, `risk_probes.present`, `coverage.failed`, `mechanical.warning`, and `judge.warning`. Ineligible new-run state has empty reasons and only `user_no_pair`, `mechanical_blocker`, `auto_pair_other_engine_unavailable`, or null; `primary_judge_blocker` remains parser-recognized only for archived v2.0 replay and retains its existing pre-known-reason rejection. Explicit `mode.pair-verify` cannot use the unavailable-engine skip. Missing, contradictory, incomplete, or unknown trigger state BLOCKs VERIFY.
|
|
65
68
|
|
|
66
69
|
## Per-phase shape
|
|
67
70
|
|
|
68
|
-
Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `build_gate`, `cleanup`, `verify`, `final_report`):
|
|
71
|
+
Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `surface_close`, `build_gate`, `cleanup`, `verify`, `final_report`):
|
|
69
72
|
|
|
70
73
|
```json
|
|
71
74
|
{
|
|
@@ -76,7 +79,8 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
76
79
|
"triggered_by": null,
|
|
77
80
|
"verdict": "PASS",
|
|
78
81
|
"engine": "claude",
|
|
79
|
-
"
|
|
82
|
+
"model_requested": "<orchestrator-selected model id or null>",
|
|
83
|
+
"model_effective": "<session-attested model id or null>",
|
|
80
84
|
"pre_sha": null,
|
|
81
85
|
"post_sha": null,
|
|
82
86
|
"artifacts": { "findings_file": null, "log_file": null },
|
|
@@ -87,10 +91,12 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
87
91
|
`history` is absent until a phase is re-entered. Immediately before re-entry overwrites the live `started_at`, `verdict`, `completed_at`, and `duration_ms`, `state-phase-write.py` appends those prior values as one object to `history[]`, creating the append-only array on first re-entry.
|
|
88
92
|
|
|
89
93
|
- `verdict` — `"PASS" | "PASS_WITH_ISSUES" | "FAIL" | "NEEDS_WORK" | "BLOCKED"`. PHASE 6 (FINAL_REPORT) writes its own verdict per the terminal-verdict precedence.
|
|
94
|
+
- `model_requested` / `model_effective` — `model_requested` is supplied by the orchestrator through `--model`; `model_effective` is parsed only from completion-time engine session evidence. Codex evidence accepts the `model: <id>` exec header or, for `.jsonl`, rollout `turn_context.payload.model`; Claude JSON output accepts the sole `modelUsage` key. A supplied but unreadable, unparseable, ambiguous, or requested/effective-mismatched session log makes the phase verdict `BLOCKED` and the completion command fail. For mutation phases, omission of `--engine-session-log` also fails closed when `.devlyn/<artifact-phase>.worker-session.<round>.jsonl` exists; the error names that retained file. Otherwise omission records `model_effective: null` and attests that the engine class exposed no session log for that invocation.
|
|
90
95
|
- `triggered_by` — null on first run; one of `"build_gate" | "verify"` when the phase is a fix-loop respawn.
|
|
91
|
-
- `pre_sha` — captured
|
|
92
|
-
- `post_sha` — captured
|
|
96
|
+
- `pre_sha` — captured before SURFACE_CLOSE or CLEANUP. It bounds that phase's post-spawn diff and rollback.
|
|
97
|
+
- `post_sha` — captured when SURFACE_CLOSE or CLEANUP completes; finish-gate subtracts CLEANUP's `pre_sha..post_sha` window.
|
|
93
98
|
- `sub_verdicts` — only populated for VERIFY: `{ "mechanical": "PASS|FAIL", "judge": "PASS|...", "pair_judge": "PASS|..." | "TIMEOUT" | null }`. Values are normalized by `verify-merge-findings.py`; model prose verdicts cannot upgrade or downgrade the deterministic findings-derived verdict. `sub_verdicts.pair_judge` is `"TIMEOUT"` when a pair judge exceeded its wall budget and a valid `.devlyn/verify.pair.timeout.json` marker was read — semantics in `references/phases/verify.md` (pair budget section).
|
|
99
|
+
- `judge_durations_ms` — only populated for VERIFY: `{ "judge": <non-negative int>, "pair_judge": <non-negative int|null> }`. The orchestrator writes each wall duration when it collects that judge's result; values remain a sibling of `sub_verdicts`, whose values stay normalized strings. VERIFY spawn resets this key to null so re-entry cannot retain prior-round timings.
|
|
94
100
|
- `merged` — only populated for VERIFY after `verify-merge-findings.py --write-state`: `{ "verdict": "...", "findings_file": ".devlyn/verify-merged.findings.jsonl", "summary_file": ".devlyn/verify-merge.summary.json" }`.
|
|
95
101
|
- `pair_trigger` — only populated for VERIFY; same shape as top-level `verify.pair_trigger` when the phase stores it locally.
|
|
96
102
|
- `correctness.risk-probe-failed` — emitted by `spec-verify-check.py --include-risk-probes` when an executable probe derived from the visible `## Verification` section fails.
|
|
@@ -99,9 +105,10 @@ Each entry under `phases.<name>` (for `plan`, `probe_derive`, `implement`, `buil
|
|
|
99
105
|
|
|
100
106
|
Phase lifecycle (`started_at`/`completed_at`/`duration_ms`/`round`/`triggered_by`/`verdict`) is written by a deterministic script, never hand-edited JSON — a prior hand-edited fix-loop respawn left `started_at` stale, corrupting cross-phase ordering because `completed_at`/`round`/`triggered_by` advanced to the new round while `started_at` didn't. Phase workers report their verdict and artifact paths in their reply; they never edit `pipeline.state.json` themselves.
|
|
101
107
|
|
|
102
|
-
1. **Spawn
|
|
103
|
-
2. **
|
|
104
|
-
3. **
|
|
108
|
+
1. **Spawn**: `state-phase-write.py --devlyn-dir .devlyn --phase <name> spawn --round <N> [--triggered-by build_gate|verify] [--pre-sha <sha>] [--engine <e>] [--model <requested-model>]`. Re-entry preserves lifecycle in `history[]`, resets owned fields, and leaves fields such as `phases.implement.exec`; VERIFY also clears prior-round files. SURFACE_CLOSE additionally requires `--model <requested-model> --input-patch-sha256 <hex> --prompt-sha256 <hex> --untracked-before-json <array>`; use `surface-check --authorized-surface-json <array>`, `surface-adjudication-recover --authorized-surface-json <array>` only after its adjudication-malformed result, `surface-rollback` for other failures, or `surface-skip` only for automatic Claude unavailability.
|
|
109
|
+
2. **Transition** (normal direct handoff): `state-phase-write.py ... --phase <current> transition` takes the same completion/attestation arguments plus caller-specified `--next-phase`, `--next-round`, `--next-triggered-by`, `--next-engine`, `--next-model`, and next-phase metadata. It validates a fixed legal edge, applies complete plus spawn to a detached candidate, and replaces state once; either both lifecycle writes land or neither. The verb returns JSON paths/digests/state facts only and never chooses a phase, engine, branch, prompt, or agent.
|
|
110
|
+
3. **Complete** (after the orchestrator reads the phase's reply and determines the verdict, when no next phase opens): `python3 "$DEVLYN_SHARED_DIR/state-phase-write.py" --devlyn-dir .devlyn --phase <name> complete --verdict <V> [--post-sha <sha>] [--findings-file <path>] [--log-file <path>] [--engine-session-log <path>]`. Pass the invocation's own session evidence whenever it exists; SURFACE_CLOSE passes `.devlyn/surface-close.output.json` as `--engine-session-log` so its `modelUsage` attests completion. Omission records `model_effective: null` only when the current phase/round has no retained worker-session file. Before completing a mutation phase (`implement`, `surface_close` when present, or `cleanup`), copy the exact session JSONL identified by that phase's dispatch receipt to `.devlyn/<artifact-phase>.worker-session.<round>.jsonl`, where `<artifact-phase>` is `implement`, `surface-close`, or `cleanup`; multiple rounds remain separate. Never discover or copy sessions by scanning an engine-global session directory. If that current-round file exists but `--engine-session-log` is omitted, completion fails closed and names the file. `duration_ms` is derived from the phase's own recorded `started_at` — it cannot drift from `completed_at`. `--verdict` is required for every phase except VERIFY: `verify-merge-findings.py --write-state` is the sole normal writer of `phases.verify.verdict`, so `complete verify` is always called without `--verdict` (an explicit one is rejected) and preserves what's already on disk; model-attestation failure still forces the phase to `BLOCKED`.
|
|
111
|
+
4. **Before archive** (PHASE 6 step 5): `phases.final_report.verdict` must be non-null. Archive prune skips runs whose final_report verdict is null (treated as in-flight).
|
|
105
112
|
|
|
106
113
|
## Terminal verdict (PHASE 6)
|
|
107
114
|
|
|
@@ -117,12 +124,12 @@ Precedence:
|
|
|
117
124
|
|
|
118
125
|
Header: `run_id | engine | mode | complexity | verdict | wall_time_s`.
|
|
119
126
|
|
|
120
|
-
Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by | findings_count
|
|
127
|
+
Per-phase summary table: `phase | verdict | duration_ms | round | triggered_by | findings_count`; include SURFACE_CLOSE when run or skipped.
|
|
121
128
|
|
|
122
|
-
Findings table
|
|
129
|
+
Findings table: each emitted finding's `severity | rule_id | file:line | message | confidence`.
|
|
123
130
|
|
|
124
|
-
Follow-up notes: any large-mode assumptions
|
|
131
|
+
Follow-up notes: when `phases.surface_close.continued_after_block` is true, exactly one explicit `pipeline continued to BUILD_GATE — surface_close_rolled_back_adjudication_malformed` line (never a terminal floor); any large-mode assumptions; any pair-judge TIMEOUT surfaced as `solo verdict after pair TIMEOUT`; pair/risk-probe opt-out state; engine setup guidance for `BLOCKED:<engine>-unavailable`; `/devlyn:ideate` guidance for `BLOCKED:solo-headroom-hypothesis-required` that asks for the visible behavior `solo_claude` is expected to miss; `/devlyn:ideate` guidance for `BLOCKED:solo-ceiling-avoidance-required` that asks for the concrete difference from rejected or solo-saturated controls such as `S2`-`S6`; and any `state.verify.coverage_failed` axes.
|
|
125
132
|
|
|
126
133
|
## Archive contract
|
|
127
134
|
|
|
128
|
-
PHASE 6 step 4 moves
|
|
135
|
+
PHASE 6 step 4 moves `PER_RUN_PATTERNS` into `.devlyn/runs/<run_id>/`, including Goal, SURFACE_CLOSE patch/prompt/reply, and mutation worker-session JSONL; it never scans engine-global sessions. Machine config stays; the last 10 completed runs remain. Archive/prune failure does not change verdict.
|
|
@@ -95,7 +95,7 @@ These are the ONLY categories reap.sh will touch:
|
|
|
95
95
|
|
|
96
96
|
| Category | Match | Why safe |
|
|
97
97
|
|---|---|---|
|
|
98
|
-
| `telegram-bun` | `bun server.ts` **AND** cwd
|
|
98
|
+
| `telegram-bun` | worker `bun server.ts` **AND** cwd under `…/claude-plugins-official/…telegram` (matches both the old `plugins/cache/` and current `plugins/marketplaces/…/external_plugins/` layouts); PLUS the runner root `bun run --cwd …/claude-plugins-official/…telegram … start`, verified by its own cmdline (its cwd is the session dir). Workers ignore SIGTERM (measured 2026-07-17: 23/23 survived TERM, died on KILL) — reap.sh sends SIGKILL for this category by default. | Telegram MCP plugin leaks one runner+worker tree per Claude session; at ~24 trees the workers (21-43% CPU each) saturated an 11-core machine (loadavg 100+). |
|
|
99
99
|
| `superset-codex-bash` | `/bin/bash .*/.superset/bin/codex` with PPID=1 | `.superset/bin/codex` wrapper exits without killing its tail child; bash copies left behind. |
|
|
100
100
|
| `superset-codex-tail` | `tail -F .*superset-codex-session-*.jsonl` with PPID=1 | Log tail from the same wrapper, always safe to stop. |
|
|
101
101
|
| `workerd` (opt-in) | `@cloudflare/workerd-darwin-*/bin/workerd serve ` with PPID=1 | moonmaker-engine dev server that survives tab close. Opt-in because the user may have an active dev session. |
|
|
@@ -47,16 +47,23 @@ collect_pids() {
|
|
|
47
47
|
local category="$1"
|
|
48
48
|
case "$category" in
|
|
49
49
|
telegram-bun)
|
|
50
|
-
# cwd-verified — same logic
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
50
|
+
# Worker form: cwd-verified — same logic (and same both-layouts pattern)
|
|
51
|
+
# as scan.sh. Runner form: cmdline names the plugin path = verification
|
|
52
|
+
# (its cwd is the session dir, not the plugin dir).
|
|
53
|
+
{
|
|
54
|
+
printf '%s\n' "$SNAPSHOT" \
|
|
55
|
+
| grep -E '/bun[^ ]* server\.ts( |$)' \
|
|
56
|
+
| awk '{print $1}' \
|
|
57
|
+
| while read -r pid; do
|
|
58
|
+
cwd="$(lsof -a -d cwd -p "$pid" 2>/dev/null | awk 'NR==2 {for(i=9;i<=NF;i++) printf "%s ", $i; print ""}')"
|
|
59
|
+
case "$cwd" in
|
|
60
|
+
*"/claude-plugins-official/"*telegram*) printf '%s\n' "$pid" ;;
|
|
61
|
+
esac
|
|
62
|
+
done
|
|
63
|
+
printf '%s\n' "$SNAPSHOT" \
|
|
64
|
+
| grep -E '( |/)bun[^ ]* run --cwd [^ ]*/claude-plugins-official/[^ ]*telegram[^ ]*( |$)' \
|
|
65
|
+
| awk '{print $1}'
|
|
66
|
+
} | sort -u
|
|
60
67
|
;;
|
|
61
68
|
superset-codex-bash)
|
|
62
69
|
printf '%s\n' "$SNAPSHOT" | grep -E '/bin/bash .*/\.superset/bin/codex( |$)' | awk '{print $1}' ;;
|
|
@@ -108,11 +115,16 @@ for cat in "${CATS_ARR[@]}"; do
|
|
|
108
115
|
TOTAL_SKIPPED=$((TOTAL_SKIPPED+1))
|
|
109
116
|
continue
|
|
110
117
|
fi
|
|
118
|
+
# telegram-bun workers IGNORE SIGTERM (measured 2026-07-17: 23/23 survived
|
|
119
|
+
# TERM, all died on KILL — the leak's root behavior is "never exits").
|
|
120
|
+
# TERM against them is a proven no-op, so this category defaults to KILL.
|
|
121
|
+
sig="$SIGNAL"
|
|
122
|
+
[ "$cat" = "telegram-bun" ] && sig="KILL"
|
|
111
123
|
if [ "$DRY" -eq 1 ]; then
|
|
112
|
-
printf '[%s] %s would SIG%s\n' "$cat" "$pid" "$
|
|
124
|
+
printf '[%s] %s would SIG%s\n' "$cat" "$pid" "$sig"
|
|
113
125
|
else
|
|
114
|
-
if kill -s "$
|
|
115
|
-
printf '[%s] %s SIG%s sent\n' "$cat" "$pid" "$
|
|
126
|
+
if kill -s "$sig" "$pid" 2>/dev/null; then
|
|
127
|
+
printf '[%s] %s SIG%s sent\n' "$cat" "$pid" "$sig"
|
|
116
128
|
TOTAL_KILLED=$((TOTAL_KILLED+1))
|
|
117
129
|
else
|
|
118
130
|
printf '[%s] %s kill failed\n' "$cat" "$pid"
|
|
@@ -21,6 +21,10 @@ SNAPSHOT="$(ps -eo pid=,ppid=,user=,etime=,command= 2>/dev/null | awk -v me="$ME
|
|
|
21
21
|
# Conservative by design — if unsure, leave it UNKNOWN.
|
|
22
22
|
# -----------------------------------------------------------------------------
|
|
23
23
|
match_telegram_bun() { grep -E '/bun[^ ]* server\.ts( |$)'; }
|
|
24
|
+
# Runner form of the same leak (2026-07-17: ~24 found at loadavg 100+): the
|
|
25
|
+
# `bun run --cwd <plugin> … start` root outlives its session; its cmdline
|
|
26
|
+
# NAMES the plugin path, so the cmdline itself is the verification.
|
|
27
|
+
match_telegram_run() { grep -E '( |/)bun[^ ]* run --cwd [^ ]*/claude-plugins-official/[^ ]*telegram[^ ]*( |$)'; }
|
|
24
28
|
match_superset_codex_sh() { grep -E '/bin/bash .*/\.superset/bin/codex( |$)'; }
|
|
25
29
|
match_superset_codex_tl() { grep -E 'tail .*superset-codex-session-.*\.jsonl'; }
|
|
26
30
|
match_workerd_dev() { grep -E '@cloudflare/workerd-darwin-[^/]+/bin/workerd serve '; }
|
|
@@ -50,12 +54,22 @@ if [ -n "$SNAPSHOT" ]; then
|
|
|
50
54
|
BUN_CANDIDATES="$(printf '%s\n' "$SNAPSHOT" | match_telegram_bun | awk '{print $1}')"
|
|
51
55
|
for pid in $BUN_CANDIDATES; do
|
|
52
56
|
cwd="$(lsof -a -d cwd -p "$pid" 2>/dev/null | awk 'NR==2 {for(i=9;i<=NF;i++) printf "%s ", $i; print ""}')"
|
|
57
|
+
# One pattern covers both plugin layouts: old plugins/cache/…/telegram/
|
|
58
|
+
# and current plugins/marketplaces/…/external_plugins/telegram (the old
|
|
59
|
+
# cache-only pattern missed the live layout — 2026-07-17).
|
|
53
60
|
case "$cwd" in
|
|
54
|
-
*"/
|
|
61
|
+
*"/claude-plugins-official/"*telegram*)
|
|
55
62
|
TELEGRAM_PIDS="${TELEGRAM_PIDS}${pid}
|
|
56
63
|
" ;;
|
|
57
64
|
esac
|
|
58
65
|
done
|
|
66
|
+
# Runner roots verify by cmdline (their cwd is the SESSION dir, not the
|
|
67
|
+
# plugin dir — `bun run --cwd X` sets X for the child, not for itself).
|
|
68
|
+
RUN_CANDIDATES="$(printf '%s\n' "$SNAPSHOT" | match_telegram_run | awk '{print $1}')"
|
|
69
|
+
for pid in $RUN_CANDIDATES; do
|
|
70
|
+
TELEGRAM_PIDS="${TELEGRAM_PIDS}${pid}
|
|
71
|
+
"
|
|
72
|
+
done
|
|
59
73
|
fi
|
|
60
74
|
|
|
61
75
|
if [ -n "$TELEGRAM_PIDS" ]; then
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devlyn-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Loop & harness engineering for AI coding agents (Claude Code, Codex, and any AGENTS.md-driven agent) — take intent to shipped, engineer-quality software hands-free with phase-gated pipelines and agent orchestration",
|
|
5
5
|
"homepage": "https://github.com/fysoul17/devlyn-cli#readme",
|
|
6
6
|
"bin": {
|