devflow-kit 3.5.0 → 3.7.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/CHANGELOG.md +178 -1
- package/README.md +10 -6
- package/dist/agents/code.md +7 -11
- package/dist/agents/{diagnose.md → debug.md} +4 -4
- package/dist/agents/{code-fix.md → fix.md} +7 -8
- package/dist/agents/{scrutinize.md → harden.md} +6 -6
- package/dist/agents/{code-edit.md → refactor.md} +8 -9
- package/dist/agents/review.md +2 -0
- package/dist/cli/agents-view/render.js +1 -1
- package/dist/cli/commands/gates.js +276 -0
- package/dist/cli/commands/init-seed.js +9 -1
- package/dist/cli/commands/init.js +34 -11
- package/dist/cli/commands/learning.js +12 -12
- package/dist/cli/commands/legacy-hooks.js +1 -1
- package/dist/cli/commands/memory.js +18 -0
- package/dist/cli/commands/uninstall.js +111 -5
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +21 -17
- package/dist/commands/code-review.md +65 -66
- package/dist/commands/debug.md +17 -17
- package/dist/commands/dynamic-build.md +104 -116
- package/dist/commands/dynamic-plan.md +13 -11
- package/dist/commands/dynamic-profile.md +9 -7
- package/dist/commands/dynamic-tickets.md +13 -11
- package/dist/commands/explore.md +15 -15
- package/dist/commands/implement.md +80 -154
- package/dist/commands/plan.md +34 -66
- package/dist/commands/release.md +11 -7
- package/dist/commands/research.md +14 -10
- package/dist/commands/resolve.md +44 -88
- package/dist/commands/self-review.md +44 -44
- package/dist/core/agent-models.js +47 -11
- package/dist/core/{learning-tuning-config.js → curate-tuning-config.js} +14 -14
- package/dist/core/echo-value.js +42 -0
- package/dist/core/evidence-policy.js +17 -5
- package/dist/core/gate-level.js +213 -0
- package/dist/core/learning-queue-cleanup.js +1 -1
- package/dist/core/manifest.js +26 -10
- package/dist/core/migrations.js +63 -44
- package/dist/core/model-discovery.js +7 -1
- package/dist/core/plugins.js +14 -12
- package/dist/core/project-paths.js +3 -3
- package/dist/core/tracker.js +5 -33
- package/dist/hud/codex-usage.js +247 -0
- package/dist/hud/components/codex-quota.js +37 -0
- package/dist/hud/components/usage-quota.js +1 -1
- package/dist/hud/config.js +1 -0
- package/dist/hud/index.js +16 -3
- package/dist/hud/render.js +3 -1
- package/dist/learning-off/agents/code.md +7 -11
- package/dist/learning-off/agents/{diagnose.md → debug.md} +4 -4
- package/dist/learning-off/agents/{code-fix.md → fix.md} +7 -8
- package/dist/learning-off/agents/{scrutinize.md → harden.md} +6 -6
- package/dist/learning-off/agents/{code-edit.md → refactor.md} +8 -9
- package/dist/learning-off/agents/review.md +2 -0
- package/dist/learning-off/commands/bug-analysis.md +9 -9
- package/dist/learning-off/commands/code-review.md +54 -59
- package/dist/learning-off/commands/debug.md +6 -10
- package/dist/learning-off/commands/dynamic-build.md +98 -112
- package/dist/learning-off/commands/dynamic-plan.md +5 -5
- package/dist/learning-off/commands/dynamic-profile.md +1 -1
- package/dist/learning-off/commands/dynamic-tickets.md +5 -5
- package/dist/learning-off/commands/explore.md +4 -8
- package/dist/learning-off/commands/implement.md +68 -146
- package/dist/learning-off/commands/plan.md +23 -58
- package/dist/learning-off/commands/release.md +4 -2
- package/dist/learning-off/commands/research.md +2 -2
- package/dist/learning-off/commands/resolve.md +33 -81
- package/dist/learning-off/commands/self-review.md +32 -36
- package/dist/skills/git/references/pr/check-merge-readiness.md +2 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +4 -6
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +7 -9
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +7 -15
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +7 -15
- package/dist/targets/claude-code/installer.js +117 -84
- package/dist/targets/claude-code/learning-install.js +15 -3
- package/dist/targets/claude-code/tracker-servers.js +1 -1
- package/package.json +1 -1
- package/src/assets/agents/_partials/_code.mds +7 -8
- package/src/assets/agents/code.mds +4 -7
- package/src/assets/agents/{learning.md → curate.md} +8 -10
- package/src/assets/agents/{diagnose.mds → debug.mds} +4 -4
- package/src/assets/agents/evaluate.md +4 -4
- package/src/assets/agents/{code-fix.mds → fix.mds} +5 -5
- package/src/assets/agents/{scrutinize.mds → harden.mds} +6 -6
- package/src/assets/agents/{simplify.md → polish.md} +4 -4
- package/src/assets/agents/{code-edit.mds → refactor.mds} +6 -6
- package/src/assets/agents/review.mds +2 -0
- package/src/assets/agents/synthesize.md +2 -2
- package/src/assets/agents/test.md +2 -2
- package/src/assets/commands/_partials/_compliance.mds +1 -1
- package/src/assets/commands/_partials/_decisions.mds +26 -6
- package/src/assets/commands/_partials/_engine.mds +15 -15
- package/src/assets/commands/_partials/_evidence_policy.mds +0 -17
- package/src/assets/commands/_partials/_knowledge.mds +14 -9
- package/src/assets/commands/_partials/_preamble.mds +4 -2
- package/src/assets/commands/_partials/_roster.mds +4 -4
- package/src/assets/commands/_partials/_settings.mds +2 -2
- package/src/assets/commands/_partials/_wave.mds +10 -17
- package/src/assets/commands/bug-analysis.mds +7 -7
- package/src/assets/commands/code-review.mds +51 -56
- package/src/assets/commands/debug.mds +2 -2
- package/src/assets/commands/dynamic-build.mds +67 -74
- package/src/assets/commands/implement.mds +65 -127
- package/src/assets/commands/plan.mds +21 -61
- package/src/assets/commands/release.mds +5 -1
- package/src/assets/commands/resolve.mds +28 -76
- package/src/assets/commands/self-review.mds +27 -27
- package/src/assets/mds/git/_ops_steps.mds +3 -3
- package/src/assets/mds/git/_pr.mds +6 -6
- package/src/assets/mds/tracker/_common.mds +23 -12
- package/src/assets/mds/tracker/_github.mds +2 -9
- package/src/assets/mds/tracker/_jira.mds +2 -15
- package/src/assets/mds/tracker/_linear.mds +2 -15
- package/src/assets/scripts/ci-wait.cjs +22 -4
- package/src/assets/scripts/claude-md-audit.cjs +66 -51
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +404 -95
- package/src/assets/scripts/hooks/capture-prompt +1 -1
- package/src/assets/scripts/hooks/capture-question +1 -1
- package/src/assets/scripts/hooks/capture-turn +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +13 -2
- package/src/assets/scripts/hooks/json-parse +18 -3
- package/src/assets/scripts/hooks/lib/learning-store.cjs +1 -1
- package/src/assets/scripts/hooks/lib/project-paths.cjs +4 -4
- package/src/assets/scripts/hooks/memory-permission +358 -0
- package/src/assets/scripts/hooks/preamble +1 -1
- package/src/assets/scripts/hooks/session-start-context +60 -47
- package/src/assets/scripts/hooks/session-start-memory +26 -5
- package/src/assets/scripts/lib/project-config.cjs +19 -4
- package/src/assets/scripts/resolve-settings.cjs +200 -32
- package/src/assets/scripts/verify-evidence.cjs +726 -9
- package/src/assets/skills/agent-references/SKILL.md +14 -6
- package/src/assets/skills/agent-references/references/commands/code-review-levels.md +19 -0
- package/src/assets/skills/agent-references/references/commands/implement-evidence-asks.md +35 -0
- package/src/assets/skills/agent-references/references/commands/implement-revalidate.md +15 -0
- package/src/assets/skills/agent-references/references/commands/plan-levels.md +40 -0
- package/src/assets/skills/agent-references/references/synthesize/bug-analysis.md +1 -1
- package/src/assets/skills/agent-references/references/synthesize/planning.md +9 -22
- package/src/assets/skills/agent-references/references/synthesize/review.md +11 -11
- package/src/assets/skills/apply-decisions/SKILL.md +6 -4
- package/src/assets/skills/docs-framework/SKILL.md +3 -3
- package/src/assets/skills/qa/SKILL.md +1 -1
- package/src/assets/skills/quality-gates/SKILL.md +7 -7
- package/src/assets/skills/quality-gates/references/report-template.md +7 -7
- /package/src/assets/skills/agent-references/references/{learning → curate}/maintain.md +0 -0
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
# 120s throttle expires. Drains .pending-turns.jsonl → calls `claude -p` on the model
|
|
6
6
|
# and effort the `agents.memory` entry of ~/.devflow/agent-models.json names, else the
|
|
7
7
|
# shipped worker default, in a lean session (D-MEMORY-WORKER-LEAN) →
|
|
8
|
-
# rewrites WORKING-MEMORY.md with a git-reconciliation stamp on line 1.
|
|
8
|
+
# rewrites WORKING-MEMORY.md with a git-reconciliation stamp on line 1. The run's write
|
|
9
|
+
# is limited to its staged file, WORKING-MEMORY.md.new (see "the spawn" below).
|
|
9
10
|
#
|
|
10
11
|
# Applies the LLM-vs-plumbing principle: artifact content (WORKING-MEMORY.md) is
|
|
11
12
|
# authored by the LLM; this script only does plumbing (lock, queue drain, spawn).
|
|
@@ -26,6 +27,8 @@
|
|
|
26
27
|
# are correctly separated by the lock boundary.
|
|
27
28
|
# User-only: exits without an LLM run (no fabrication) and leaves the queue in place
|
|
28
29
|
# for the next run (D-QUEUE-NO-ORPHAN-DELETE)
|
|
30
|
+
# No parser: neither jq nor node on PATH → exits with no LLM run; the queue and any .processing batch are left in place
|
|
31
|
+
# Parser fails: jq or node exits non-zero while it reads the queue or the batch → exits with no LLM run; the turns are kept
|
|
29
32
|
|
|
30
33
|
set -e
|
|
31
34
|
|
|
@@ -64,12 +67,79 @@ source "$SCRIPT_DIR/is-hex-sha" || { echo "background-memory-update: failed to s
|
|
|
64
67
|
|
|
65
68
|
log "Starting (CWD=$CWD)"
|
|
66
69
|
|
|
70
|
+
# Every run ends with one `Run:` line that says what it was: the CLI version, the argv
|
|
71
|
+
# form it ran, how it ended, how many calls the permission rules denied and what the
|
|
72
|
+
# worker removed from the staged path. The exit trap writes it, so each way out of the
|
|
73
|
+
# script has one. The fields start as "no CLI read, no run" and the steps below fill them
|
|
74
|
+
# in; each is a keyword or digits, never text from the CLI, the queue or the prompt. The
|
|
75
|
+
# trap also removes RESULT_FILE (the captured JSON result, set at the spawn below) and,
|
|
76
|
+
# once this run holds it, the worker lock.
|
|
77
|
+
RUN_CLI="unknown" # X.Y.Z once the version probe has read one
|
|
78
|
+
RUN_ARGV="none" # lean or legacy, set where the run is spawned
|
|
79
|
+
RUN_RESULT="skip" # updated, conflict, failed, skip or below-floor
|
|
80
|
+
RUN_DENIALS="n/a" # digits or unknown, set where the run is spawned
|
|
81
|
+
RUN_REMOVED="none" # dir or link when a folder or a link was removed from the staged path
|
|
82
|
+
RUN_FLOOR="" # the version floor, named on a below-floor line only
|
|
83
|
+
RESULT_FILE=""
|
|
84
|
+
LOCK_HELD="false"
|
|
85
|
+
# Sets OWN_PID to a value that is the worker's own PID in the worker's shell and differs in
|
|
86
|
+
# every subshell. It is BASHPID when that is set (bash 4 and later, no fork). Otherwise it is
|
|
87
|
+
# the PID that a child, which execs sh, reports as its parent: that parent is this shell, and
|
|
88
|
+
# `$$` would be the worker's PID in every subshell. When that read fails or is not a number,
|
|
89
|
+
# BASH_SUBSHELL decides: 0 is the worker's own shell (`$$`), and above 0 is a subshell, which
|
|
90
|
+
# gets a non-numeric value that never equals the worker's PID.
|
|
91
|
+
own_pid() {
|
|
92
|
+
if [ -n "${BASHPID:-}" ]; then
|
|
93
|
+
OWN_PID="$BASHPID"
|
|
94
|
+
return 0
|
|
95
|
+
fi
|
|
96
|
+
OWN_PID=$(exec /bin/sh -c 'echo $PPID' 2>/dev/null) || OWN_PID=""
|
|
97
|
+
case "$OWN_PID" in
|
|
98
|
+
''|*[!0123456789]*)
|
|
99
|
+
if [ "${BASH_SUBSHELL:-0}" = "0" ]; then OWN_PID="$$"; else OWN_PID="subshell"; fi ;;
|
|
100
|
+
esac
|
|
101
|
+
}
|
|
102
|
+
own_pid
|
|
103
|
+
WORKER_PID="$OWN_PID"
|
|
104
|
+
finish_run() {
|
|
105
|
+
# Only the worker process itself ends the run. A subshell it forks (the watchdog, the
|
|
106
|
+
# claude spawn) that a signal reaches before bash resets its traps runs this trap too,
|
|
107
|
+
# and the PID check returns there: it writes no Run line and releases no lock.
|
|
108
|
+
own_pid
|
|
109
|
+
[ "$OWN_PID" = "$WORKER_PID" ] || return 0
|
|
110
|
+
local _floor=""
|
|
111
|
+
if [ -n "$RUN_FLOOR" ]; then _floor=" floor=${RUN_FLOOR}"; fi
|
|
112
|
+
log "Run: cli=${RUN_CLI} argv=${RUN_ARGV} result=${RUN_RESULT} denials=${RUN_DENIALS} removed=${RUN_REMOVED}${_floor}"
|
|
113
|
+
if [ -n "$RESULT_FILE" ]; then rm -f "$RESULT_FILE" 2>/dev/null || true; fi
|
|
114
|
+
if [ "$LOCK_HELD" = "true" ]; then rmdir "$LOCK_DIR" 2>/dev/null || true; fi
|
|
115
|
+
}
|
|
116
|
+
trap finish_run EXIT
|
|
117
|
+
|
|
118
|
+
# No JSON parser: stop before any step reads or moves the queue or the batch (#523).
|
|
119
|
+
if [ "${_JSON_AVAILABLE:-false}" != "true" ]; then
|
|
120
|
+
log "SKIP: neither jq nor node on PATH; the queue is kept for the next run; no LLM run"
|
|
121
|
+
exit 0
|
|
122
|
+
fi
|
|
123
|
+
|
|
67
124
|
# --- Resolve paths ---
|
|
68
125
|
# Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when the
|
|
69
126
|
# worker is spawned with a CWD inside .devflow/...). Empty → fall back to CWD.
|
|
70
127
|
source "$SCRIPT_DIR/resolve-project-root" 2>/dev/null || true
|
|
128
|
+
# The staged-path check, the settings JSON with the worker's Edit allow and deny rules, the
|
|
129
|
+
# result line and the one-time notice (needs df_no_symlink_below from resolve-project-root).
|
|
130
|
+
source "$SCRIPT_DIR/memory-permission" || { echo "background-memory-update: failed to source memory-permission" >&2; exit 1; }
|
|
71
131
|
PROJECT_ROOT="$(df_resolve_root "$CWD" 2>/dev/null || true)"
|
|
72
132
|
[ -n "$PROJECT_ROOT" ] || PROJECT_ROOT="$CWD"
|
|
133
|
+
# One canonical root feeds every path below, the staged path in the prompt and in the
|
|
134
|
+
# permission rule included: the rule matches a path as it resolves on disk, so a root
|
|
135
|
+
# reached through a link is resolved here once (`cd -P`, `pwd -P`) and never again.
|
|
136
|
+
PROJECT_ROOT_CANONICAL=$(cd -P -- "$PROJECT_ROOT" 2>/dev/null && pwd -P) || PROJECT_ROOT_CANONICAL=""
|
|
137
|
+
if [ -z "$PROJECT_ROOT_CANONICAL" ]; then
|
|
138
|
+
log "SKIP: project root could not be resolved; no LLM run"
|
|
139
|
+
memory_notice_write "$PROJECT_ROOT/.devflow/memory" "$PROJECT_ROOT" path
|
|
140
|
+
exit 0
|
|
141
|
+
fi
|
|
142
|
+
PROJECT_ROOT="$PROJECT_ROOT_CANONICAL"
|
|
73
143
|
PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
74
144
|
MEMORY_DIR="$PROJECT_DEVFLOW_DIR/memory"
|
|
75
145
|
QUEUE_FILE="$MEMORY_DIR/.pending-turns.jsonl"
|
|
@@ -120,11 +190,37 @@ if ! df_no_symlink_below "$PROJECT_ROOT" "$LOCK_DIR" "$QUEUE_FILE" "$PROCESSING_
|
|
|
120
190
|
exit 0
|
|
121
191
|
fi
|
|
122
192
|
|
|
193
|
+
# --- The worker's write is limited to its staged file ---
|
|
194
|
+
# The run gets an allow rule for $STAGED_FILE, a deny rule for every path under it, and a
|
|
195
|
+
# permission mode that denies every call no rule allows. The rules are plain text in a
|
|
196
|
+
# settings JSON, so the staged path is checked before the JSON is built: a character that
|
|
197
|
+
# has a meaning in a rule path, or a project inside a .git folder, ends the run here with
|
|
198
|
+
# no LLM call, and a one-time notice tells the user (memory-permission). The prompt, the
|
|
199
|
+
# rules and the checks below all use this one path.
|
|
200
|
+
if ! staged_path_safe "$STAGED_FILE"; then
|
|
201
|
+
if [ "$STAGED_PATH_REFUSAL" = "protected" ]; then
|
|
202
|
+
log "SKIP: the project lies inside a .git folder; no LLM run"
|
|
203
|
+
else
|
|
204
|
+
log "SKIP: the memory path holds characters the permission rule cannot carry; no LLM run"
|
|
205
|
+
fi
|
|
206
|
+
memory_notice_write "$MEMORY_DIR" "$PROJECT_ROOT" path
|
|
207
|
+
exit 0
|
|
208
|
+
fi
|
|
209
|
+
MEMORY_SETTINGS_JSON=$(memory_settings_json "$STAGED_FILE") || MEMORY_SETTINGS_JSON=""
|
|
210
|
+
if [ -z "$MEMORY_SETTINGS_JSON" ]; then
|
|
211
|
+
log "SKIP: the permission rule for the staged file could not be built; no LLM run"
|
|
212
|
+
exit 0
|
|
213
|
+
fi
|
|
214
|
+
|
|
123
215
|
# --- CLI version probe: choose the argv form (D-MEMORY-WORKER-LEAN) ---
|
|
124
216
|
# The lean argv (--effort, --safe-mode, --max-turns, ...) is verified from
|
|
125
217
|
# MEMORY_MIN_CLAUDE_VERSION up. A CLI older than that, or one whose version cannot be
|
|
126
218
|
# read, gets the legacy argv instead, because one flag a CLI does not know fails every
|
|
127
|
-
# run.
|
|
219
|
+
# run. A CLI whose version is read and is older than MEMORY_MIN_PERMISSION_VERSION, the
|
|
220
|
+
# lowest version on which the worker's write was measured to be limited to its staged
|
|
221
|
+
# file, gets no run at all: the queue is kept and the run's `Run:` line says below-floor.
|
|
222
|
+
# `devflow init` states this version; src/cli/commands/memory.ts holds the same value, and
|
|
223
|
+
# a test holds the two equal. The probe runs here, once $CLAUDE_BIN has resolved and the link check has
|
|
128
224
|
# passed, and before the lock is taken, so a slow or hung `--version` can never extend
|
|
129
225
|
# the lock's hold. It is bounded by DEVFLOW_BG_VERSION_PROBE_SECS (default 10, at most
|
|
130
226
|
# 60; tests override it as they do DEVFLOW_BG_WATCHDOG_SECS) with the watchdog's
|
|
@@ -134,6 +230,10 @@ fi
|
|
|
134
230
|
# aborts the caller, so each helper below ends in an explicit return status and is
|
|
135
231
|
# called inside an `if` condition.
|
|
136
232
|
MEMORY_MIN_CLAUDE_VERSION="2.1.286"
|
|
233
|
+
# The lowest version measured, 2.1.269 (2026-10-15, with 2.1.280, 2.1.286 and 2.1.299): on
|
|
234
|
+
# each, the staged file was written and every other path was refused. The cells are in
|
|
235
|
+
# docs/reference/platform-assumptions.md.
|
|
236
|
+
MEMORY_MIN_PERMISSION_VERSION="2.1.269"
|
|
137
237
|
|
|
138
238
|
# Sets CLAUDE_VERSION_TEXT to the first 512 bytes `claude --version` printed ("" on any
|
|
139
239
|
# failure). Returns 0 when it exited 0 inside the bound, else 1. A hung probe gets
|
|
@@ -161,7 +261,10 @@ probe_claude_version() {
|
|
|
161
261
|
) </dev/null >/dev/null 2>&1 &
|
|
162
262
|
_wd_pid=$!
|
|
163
263
|
if wait "$_pid" 2>/dev/null; then _status=0; fi
|
|
164
|
-
|
|
264
|
+
# SIGKILL, not SIGTERM: a fast probe ends while the watchdog subshell may not have run
|
|
265
|
+
# yet, and SIGTERM sent to a subshell that still carries the exit trap runs that trap
|
|
266
|
+
# there, which writes a second `Run:` line. SIGKILL runs no trap.
|
|
267
|
+
kill -KILL "$_wd_pid" 2>/dev/null || true
|
|
165
268
|
wait "$_wd_pid" 2>/dev/null || true
|
|
166
269
|
if [ "$_status" -eq 0 ]; then
|
|
167
270
|
CLAUDE_VERSION_TEXT=$(head -c 512 "$_out" 2>/dev/null || true)
|
|
@@ -170,21 +273,35 @@ probe_claude_version() {
|
|
|
170
273
|
return "$_status"
|
|
171
274
|
}
|
|
172
275
|
|
|
173
|
-
# Returns 0
|
|
174
|
-
#
|
|
175
|
-
#
|
|
176
|
-
#
|
|
276
|
+
# Returns 0 and sets CLAUDE_VERSION_PARSED to the first X.Y.Z in $1, each component as a
|
|
277
|
+
# base-10 number without leading zeros (a leading zero is not octal). Returns 1 when $1
|
|
278
|
+
# holds no X.Y.Z or a component longer than six digits. This is the one definition of a
|
|
279
|
+
# version: the run's CLI version and both floors read it. The digit set is spelled out: a
|
|
177
280
|
# range such as 0-9 collates by locale on bash 3.2.
|
|
178
|
-
|
|
179
|
-
local
|
|
180
|
-
local
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
[[ "$_text" =~ $_re ]] || return 1
|
|
281
|
+
claude_version_parsed() {
|
|
282
|
+
local _re='([0123456789]+)\.([0123456789]+)\.([0123456789]+)'
|
|
283
|
+
local _v1 _v2 _v3
|
|
284
|
+
CLAUDE_VERSION_PARSED=""
|
|
285
|
+
[[ "$1" =~ $_re ]] || return 1
|
|
184
286
|
_v1=${BASH_REMATCH[1]}; _v2=${BASH_REMATCH[2]}; _v3=${BASH_REMATCH[3]}
|
|
185
287
|
if [ "${#_v1}" -gt 6 ] || [ "${#_v2}" -gt 6 ] || [ "${#_v3}" -gt 6 ]; then return 1; fi
|
|
186
|
-
|
|
187
|
-
|
|
288
|
+
CLAUDE_VERSION_PARSED="$((10#$_v1)).$((10#$_v2)).$((10#$_v3))"
|
|
289
|
+
return 0
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
# Returns 0 when the version in $1 is at or above the version in $2, comparing the three
|
|
293
|
+
# components as numbers left to right (2.10.0 is above 2.1.286). Returns 1 when it is
|
|
294
|
+
# below, or when either text is no version for claude_version_parsed. It leaves
|
|
295
|
+
# CLAUDE_VERSION_PARSED holding the parse of $1 when both texts parse, and empty when either
|
|
296
|
+
# one does not parse.
|
|
297
|
+
claude_version_at_least() {
|
|
298
|
+
local _f1 _f2 _f3 _v1 _v2 _v3 _rest
|
|
299
|
+
claude_version_parsed "$2" || return 1
|
|
300
|
+
_f1=${CLAUDE_VERSION_PARSED%%.*}; _rest=${CLAUDE_VERSION_PARSED#*.}
|
|
301
|
+
_f2=${_rest%%.*}; _f3=${_rest#*.}
|
|
302
|
+
claude_version_parsed "$1" || return 1
|
|
303
|
+
_v1=${CLAUDE_VERSION_PARSED%%.*}; _rest=${CLAUDE_VERSION_PARSED#*.}
|
|
304
|
+
_v2=${_rest%%.*}; _v3=${_rest#*.}
|
|
188
305
|
if [ "$_v1" -ne "$_f1" ]; then
|
|
189
306
|
if [ "$_v1" -gt "$_f1" ]; then return 0; fi
|
|
190
307
|
return 1
|
|
@@ -197,13 +314,30 @@ claude_version_at_least() {
|
|
|
197
314
|
return 1
|
|
198
315
|
}
|
|
199
316
|
|
|
317
|
+
# The permission floor sits at or below the lean floor: a CLI the lean form runs on
|
|
318
|
+
# always has the permission flags too.
|
|
319
|
+
if ! claude_version_at_least "$MEMORY_MIN_CLAUDE_VERSION" "$MEMORY_MIN_PERMISSION_VERSION"; then
|
|
320
|
+
echo "[background-memory-update] FATAL: MEMORY_MIN_PERMISSION_VERSION ($MEMORY_MIN_PERMISSION_VERSION) must not exceed MEMORY_MIN_CLAUDE_VERSION ($MEMORY_MIN_CLAUDE_VERSION)" >&2
|
|
321
|
+
exit 1
|
|
322
|
+
fi
|
|
323
|
+
|
|
200
324
|
CLAUDE_ARGV_FORM="legacy"
|
|
201
325
|
if ! probe_claude_version; then
|
|
202
326
|
log "claude --version probe failed or timed out — using the legacy argv"
|
|
203
|
-
elif
|
|
204
|
-
|
|
327
|
+
elif ! claude_version_parsed "$CLAUDE_VERSION_TEXT"; then
|
|
328
|
+
log "claude --version could not be read — using the legacy argv"
|
|
205
329
|
else
|
|
206
|
-
|
|
330
|
+
RUN_CLI="$CLAUDE_VERSION_PARSED"
|
|
331
|
+
if ! claude_version_at_least "$RUN_CLI" "$MEMORY_MIN_PERMISSION_VERSION"; then
|
|
332
|
+
RUN_RESULT="below-floor"
|
|
333
|
+
RUN_FLOOR="$MEMORY_MIN_PERMISSION_VERSION"
|
|
334
|
+
exit 0
|
|
335
|
+
fi
|
|
336
|
+
if claude_version_at_least "$RUN_CLI" "$MEMORY_MIN_CLAUDE_VERSION"; then
|
|
337
|
+
CLAUDE_ARGV_FORM="lean"
|
|
338
|
+
else
|
|
339
|
+
log "claude --version is below ${MEMORY_MIN_CLAUDE_VERSION} — using the legacy argv"
|
|
340
|
+
fi
|
|
207
341
|
fi
|
|
208
342
|
|
|
209
343
|
# --- Worker-level lock (300s stale-break — much longer than learning-lock's 30s) ---
|
|
@@ -243,34 +377,46 @@ acquire_lock() {
|
|
|
243
377
|
return 0
|
|
244
378
|
}
|
|
245
379
|
|
|
246
|
-
cleanup_lock() {
|
|
247
|
-
rmdir "$LOCK_DIR" 2>/dev/null || true
|
|
248
|
-
}
|
|
249
|
-
trap cleanup_lock EXIT
|
|
250
|
-
|
|
251
380
|
break_stale_lock
|
|
252
381
|
|
|
382
|
+
# A worker that times out on the lock does not hold it, so finish_run leaves it alone.
|
|
253
383
|
if ! acquire_lock; then
|
|
254
384
|
log "SKIP: lock timeout after 90s — another worker is running"
|
|
255
|
-
trap - EXIT
|
|
256
385
|
exit 0
|
|
257
386
|
fi
|
|
387
|
+
LOCK_HELD="true"
|
|
258
388
|
|
|
259
|
-
# Clean up
|
|
260
|
-
# Without this, a stale staged file with a valid stamp would be mistakenly
|
|
261
|
-
#
|
|
389
|
+
# Clean up whatever stands at the staged path after a watchdog-killed or odd prior run.
|
|
390
|
+
# Without this, a stale staged file with a valid stamp would be mistakenly mv-ed to the
|
|
391
|
+
# real path on the NEXT run's CAS check (D-MEMORY-STAGED-CAS).
|
|
262
392
|
#
|
|
263
393
|
# D-MEMORY-WORKER-LEAN: the removal is also a CHECKED precondition. The model gets the
|
|
264
394
|
# Write tool alone, and Write refuses a file that exists and has not been read, which
|
|
265
395
|
# is how an earlier Write-only worker failed silently: every Write errored while the
|
|
266
396
|
# run exited 0. Once nothing stands at the staged path, the one Write is always a
|
|
267
|
-
# create, so Write-only is safe. A
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
397
|
+
# create, so Write-only is safe. A folder or a link there is removed first, by one
|
|
398
|
+
# guarded delete of exactly that path (memory_staged_remove: only
|
|
399
|
+
# <project>/.devflow/memory/WORKING-MEMORY.md.new, never through a link, a bounded
|
|
400
|
+
# listing, no pattern), then `rm -f` clears a stale regular file. Whatever still stands
|
|
401
|
+
# there stops the run here instead: no LLM call, the batch and the queue untouched
|
|
402
|
+
# (nothing has been claimed yet), the lock released by the EXIT trap, a one-time notice
|
|
403
|
+
# for the user, and a Run line that says `failed` and `removed=none`. A link there counts
|
|
404
|
+
# as present (path_occupied).
|
|
405
|
+
if ! memory_staged_remove "$MEMORY_DIR" "$PROJECT_ROOT" "$STAGED_FILE"; then
|
|
406
|
+
RUN_RESULT="failed"
|
|
407
|
+
log "SKIP: $STAGED_FILE is a folder or a link that was not removed (${STAGED_REMOVE_REFUSAL}); remove it to resume memory updates; no LLM run"
|
|
408
|
+
memory_notice_write "$MEMORY_DIR" "$PROJECT_ROOT" path
|
|
409
|
+
exit 0
|
|
410
|
+
fi
|
|
411
|
+
RUN_REMOVED="$STAGED_REMOVED"
|
|
412
|
+
if [ "$STAGED_REMOVED" != "none" ]; then
|
|
413
|
+
log "Removed a ${STAGED_REMOVED/dir/folder} from $STAGED_FILE before the run"
|
|
414
|
+
fi
|
|
271
415
|
rm -f "$STAGED_FILE" 2>/dev/null || true
|
|
272
|
-
if
|
|
416
|
+
if path_occupied "$STAGED_FILE"; then
|
|
417
|
+
RUN_RESULT="failed"
|
|
273
418
|
log "SKIP: $STAGED_FILE could not be removed — a Write to it would fail, so no LLM run; the batch and the queue are left as they are"
|
|
419
|
+
memory_notice_write "$MEMORY_DIR" "$PROJECT_ROOT" path
|
|
274
420
|
exit 0
|
|
275
421
|
fi
|
|
276
422
|
|
|
@@ -279,8 +425,6 @@ fi
|
|
|
279
425
|
# A qa row (captured Q&A pair) counts as content-bearing here too — it carries
|
|
280
426
|
# the same synthesis-worthy signal as an assistant turn (AC-F10). This check
|
|
281
427
|
# and the TURNS_TEXT extraction loop below must agree on that.
|
|
282
|
-
# When neither jq nor node is available (_JSON_AVAILABLE=false) we skip the check
|
|
283
|
-
# and allow the run to proceed — conservative: better to attempt than to skip blindly.
|
|
284
428
|
#
|
|
285
429
|
# D-QUEUE-NO-ORPHAN-DELETE: the queue is never deleted here. Claude Code runs a
|
|
286
430
|
# Stop event's hooks in parallel, so this worker can read the queue after
|
|
@@ -288,9 +432,13 @@ fi
|
|
|
288
432
|
# assistant row; deleting it then loses that turn's prompt. Leaving it costs
|
|
289
433
|
# nothing: the run is still skipped, the next run takes the whole turn once the
|
|
290
434
|
# assistant row lands, and queue-append caps the file (200 -> newest 100 lines).
|
|
291
|
-
if [ ! -f "$PROCESSING_FILE" ] && [ -f "$QUEUE_FILE" ] && [ -s "$QUEUE_FILE" ]
|
|
435
|
+
if [ ! -f "$PROCESSING_FILE" ] && [ -f "$QUEUE_FILE" ] && [ -s "$QUEUE_FILE" ]; then
|
|
436
|
+
# A parser that exits non-zero has not said that the queue holds no turn, so the status is
|
|
437
|
+
# read from the parser itself (no pipe after it) and the queue is kept as it is.
|
|
438
|
+
_PARSER_RC=0
|
|
292
439
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
293
|
-
_HAS_CONTENT=$(jq -r 'select(.role=="assistant" or .role=="qa") | .role' "$QUEUE_FILE" 2>/dev/null
|
|
440
|
+
_HAS_CONTENT=$(jq -R -r '(fromjson | select(.role=="assistant" or .role=="qa") | .role)?' "$QUEUE_FILE" 2>/dev/null) || _PARSER_RC=$?
|
|
441
|
+
_HAS_CONTENT="${_HAS_CONTENT%%$'\n'*}"
|
|
294
442
|
else
|
|
295
443
|
# SECURITY: pass path via argv, never interpolate into node -e source (avoids shell injection
|
|
296
444
|
# for repo paths containing quotes or special characters)
|
|
@@ -300,7 +448,11 @@ if [ ! -f "$PROCESSING_FILE" ] && [ -f "$QUEUE_FILE" ] && [ -s "$QUEUE_FILE" ] &
|
|
|
300
448
|
for (const l of lines) {
|
|
301
449
|
try { const o = JSON.parse(l); if (o.role === "assistant" || o.role === "qa") { process.stdout.write(o.role); break; } } catch {}
|
|
302
450
|
}
|
|
303
|
-
' "$QUEUE_FILE" 2>/dev/null ||
|
|
451
|
+
' "$QUEUE_FILE" 2>/dev/null) || _PARSER_RC=$?
|
|
452
|
+
fi
|
|
453
|
+
if [ "$_PARSER_RC" -ne 0 ]; then
|
|
454
|
+
log "SKIP: the JSON parser exited $_PARSER_RC on the queue; the queue is kept for the next run; no LLM run"
|
|
455
|
+
exit 0
|
|
304
456
|
fi
|
|
305
457
|
if [ -z "$_HAS_CONTENT" ]; then
|
|
306
458
|
log "User-only queue (no assistant/qa turn) — leaving it for the next run"
|
|
@@ -308,14 +460,29 @@ if [ ! -f "$PROCESSING_FILE" ] && [ -f "$QUEUE_FILE" ] && [ -s "$QUEUE_FILE" ] &
|
|
|
308
460
|
fi
|
|
309
461
|
fi
|
|
310
462
|
|
|
463
|
+
# Succeeds when the file is not empty and its last byte is not a line break (a row cut off
|
|
464
|
+
# before its break). The command substitution drops a trailing line break, so only a last
|
|
465
|
+
# byte of another kind is left in it.
|
|
466
|
+
ends_without_newline() {
|
|
467
|
+
[ -s "$1" ] && [ -n "$(tail -c 1 "$1" 2>/dev/null)" ]
|
|
468
|
+
}
|
|
469
|
+
|
|
311
470
|
# --- Claim queue atomically (D56c contract: .pending-turns.processing recovered at SessionStart) ---
|
|
312
471
|
# If a leftover .processing exists from a previous crash, merge new queue entries into it.
|
|
313
472
|
if [ -f "$PROCESSING_FILE" ]; then
|
|
314
473
|
log "Found leftover .processing from previous crash — merging"
|
|
315
474
|
if [ -f "$QUEUE_FILE" ]; then
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
475
|
+
# The queue is removed only after its rows are in the batch. A batch whose last row has
|
|
476
|
+
# no line break first gets one, so the first queue row does not run into that row and
|
|
477
|
+
# become one malformed line that the parser drops. A failed append leaves the queue for
|
|
478
|
+
# the next run; an append that stopped part-way may repeat rows then, and a line break
|
|
479
|
+
# added before a failed append only leaves a blank line, which the parser ignores.
|
|
480
|
+
if { ! ends_without_newline "$PROCESSING_FILE" || printf '\n' >> "$PROCESSING_FILE"; } 2>/dev/null && cat "$QUEUE_FILE" >> "$PROCESSING_FILE" 2>/dev/null; then
|
|
481
|
+
rm -f "$QUEUE_FILE" 2>/dev/null || true
|
|
482
|
+
log "Merged new queue entries into recovery batch"
|
|
483
|
+
else
|
|
484
|
+
log "Could not append the queue to the recovery batch — the queue is kept for the next run"
|
|
485
|
+
fi
|
|
319
486
|
fi
|
|
320
487
|
# Cap to prevent unbounded growth
|
|
321
488
|
PROC_OVERFLOW_CAP=200 # truncate when processing file exceeds this many lines
|
|
@@ -351,6 +518,11 @@ fi
|
|
|
351
518
|
touch "$PROCESSING_FILE" 2>/dev/null || true
|
|
352
519
|
|
|
353
520
|
TOTAL_LINES=$(wc -l < "$PROCESSING_FILE" | tr -d ' ')
|
|
521
|
+
# wc -l counts line breaks, so a last row with no break is not in its count. Count it, so a
|
|
522
|
+
# batch of one such row is a batch with a row, and only a batch with no byte is empty.
|
|
523
|
+
if ends_without_newline "$PROCESSING_FILE"; then
|
|
524
|
+
TOTAL_LINES=$(( TOTAL_LINES + 1 ))
|
|
525
|
+
fi
|
|
354
526
|
log "Processing $TOTAL_LINES queued entries"
|
|
355
527
|
|
|
356
528
|
if [ "$TOTAL_LINES" -eq 0 ]; then
|
|
@@ -372,24 +544,41 @@ else
|
|
|
372
544
|
fi
|
|
373
545
|
|
|
374
546
|
# Single-pass extraction: role + content as TSV (newlines collapsed)
|
|
375
|
-
#
|
|
547
|
+
# Each line is parsed alone (jq -R with fromjson, node per line): a malformed line drops only that line.
|
|
548
|
+
# A row cut off before its line break runs into the next row, so the two are one malformed line (#528).
|
|
549
|
+
# Both parsers emit a row only when its role is user, assistant or qa and its content is a
|
|
550
|
+
# string (or absent), so a line that is JSON but no turn is a skipped line on either backend.
|
|
551
|
+
# A parser that exits non-zero has read nothing it can vouch for: the batch is kept, with the
|
|
552
|
+
# queue merged into it, and no LLM runs. Exit 0 with no row still means "no readable turn".
|
|
553
|
+
_PARSER_RC=0
|
|
376
554
|
if [ "$_HAS_JQ" = "true" ]; then
|
|
377
|
-
EXTRACTED=$(printf '%s\n' "$ENTRIES" | jq -r '(.role
|
|
378
|
-
|
|
555
|
+
EXTRACTED=$(printf '%s\n' "$ENTRIES" | jq -R -r '(fromjson | select(.role=="user" or .role=="assistant" or .role=="qa") | .role + "\t" + ((.content // "") | split("\n") | join(" ")))?' 2>/dev/null) || _PARSER_RC=$?
|
|
556
|
+
else
|
|
379
557
|
EXTRACTED=$(printf '%s\n' "$ENTRIES" | node -e '
|
|
380
558
|
const lines = require("fs").readFileSync("/dev/stdin","utf8").split("\n");
|
|
381
559
|
for (const line of lines) {
|
|
382
560
|
if (!line.trim()) continue;
|
|
383
561
|
try {
|
|
384
562
|
const obj = JSON.parse(line);
|
|
385
|
-
const role = obj.role
|
|
386
|
-
|
|
387
|
-
|
|
563
|
+
const role = obj === null ? undefined : obj.role;
|
|
564
|
+
if (role !== "user" && role !== "assistant" && role !== "qa") continue;
|
|
565
|
+
const content = obj.content == null || obj.content === false ? "" : obj.content;
|
|
566
|
+
if (typeof content !== "string") continue;
|
|
567
|
+
process.stdout.write(role + "\t" + content.replace(/\n/g, " ") + "\n");
|
|
388
568
|
} catch {}
|
|
389
569
|
}
|
|
390
|
-
' 2>/dev/null ||
|
|
391
|
-
|
|
392
|
-
|
|
570
|
+
' 2>/dev/null) || _PARSER_RC=$?
|
|
571
|
+
fi
|
|
572
|
+
if [ "$_PARSER_RC" -ne 0 ]; then
|
|
573
|
+
log "SKIP: the JSON parser exited $_PARSER_RC on the batch; the batch is kept for the next run; no LLM run"
|
|
574
|
+
exit 0
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
_IN_LINES=$(printf '%s\n' "$ENTRIES" | LC_ALL=C grep -c '[^[:space:]]' || true)
|
|
578
|
+
_OUT_LINES=$(printf '%s\n' "$EXTRACTED" | LC_ALL=C grep -c . || true)
|
|
579
|
+
_SKIPPED=$(( ${_IN_LINES:-0} - ${_OUT_LINES:-0} ))
|
|
580
|
+
if [ "$_SKIPPED" -gt 0 ]; then
|
|
581
|
+
log "Skipped $_SKIPPED queue line(s) that hold no readable turn"
|
|
393
582
|
fi
|
|
394
583
|
|
|
395
584
|
TURNS_TEXT=""
|
|
@@ -633,7 +822,7 @@ MEMORY_EFFORT_CHARS="abcdefghijklmnopqrstuvwxyz"
|
|
|
633
822
|
# the `x` when the file, the field or a string there is missing. Always returns 0.
|
|
634
823
|
memory_agent_field() {
|
|
635
824
|
local _file="$HOME/.devflow/agent-models.json"
|
|
636
|
-
if [
|
|
825
|
+
if [ -f "$_file" ]; then
|
|
637
826
|
json_string_field_file "$_file" "agents.memory.$1" 2>/dev/null || true
|
|
638
827
|
fi
|
|
639
828
|
printf x
|
|
@@ -740,15 +929,18 @@ fi
|
|
|
740
929
|
# Job-control notifications go to stderr which is redirected to the log — suppress them.
|
|
741
930
|
#
|
|
742
931
|
# D-MEMORY-WORKER-LEAN — the spawn. The worker opens a headless session to make one
|
|
743
|
-
# Write, so the session is cut to what that Write needs
|
|
744
|
-
#
|
|
932
|
+
# Write, so the session is cut to what that Write needs, and the Write is limited to its
|
|
933
|
+
# staged file. The prompt goes on stdin, the JSON result to a temporary file and the
|
|
934
|
+
# errors to the log. Two argv forms, chosen by the version probe above:
|
|
745
935
|
#
|
|
746
936
|
# lean (CLI at or above MEMORY_MIN_CLAUDE_VERSION):
|
|
747
937
|
# -p --model M --effort E --safe-mode --tools Write --disable-slash-commands \
|
|
748
938
|
# --strict-mcp-config --mcp-config '{"mcpServers":{}}' --max-turns 3 \
|
|
749
|
-
# --
|
|
939
|
+
# --permission-mode dontAsk --setting-sources user --settings JSON \
|
|
940
|
+
# --output-format json
|
|
750
941
|
# legacy (older, unreadable or timed-out probe):
|
|
751
|
-
# -p --model M --tools Write --
|
|
942
|
+
# -p --model M --tools Write --permission-mode dontAsk --setting-sources user \
|
|
943
|
+
# --settings JSON --output-format json
|
|
752
944
|
#
|
|
753
945
|
# What each flag is for:
|
|
754
946
|
# --model, --effort the resolved agents.memory values above. --effort is the only
|
|
@@ -760,8 +952,7 @@ fi
|
|
|
760
952
|
# Auth, model selection and built-in tools work as usual, and
|
|
761
953
|
# settings `env` (the provider URL and key) still applies under
|
|
762
954
|
# it (measured on 2026-10-09), so a routed or keyed install works.
|
|
763
|
-
# --tools Write Write is the only tool.
|
|
764
|
-
# leaves every tool, listing and MCP server loaded.
|
|
955
|
+
# --tools Write Write is the only tool.
|
|
765
956
|
# --disable-slash-commands, --strict-mcp-config with an empty --mcp-config
|
|
766
957
|
# no slash-command or skill listing, no MCP server and none of the
|
|
767
958
|
# instructions they add to the context.
|
|
@@ -773,59 +964,123 @@ fi
|
|
|
773
964
|
# pattern). `--max-turns` is not listed in `claude --help` there
|
|
774
965
|
# but is registered (`--max-turns abc` errors "must be a number"),
|
|
775
966
|
# which is why it was exercised rather than read off the help.
|
|
776
|
-
# --
|
|
777
|
-
#
|
|
967
|
+
# --permission-mode dontAsk
|
|
968
|
+
# a call no allow rule covers is denied and never asked, so a
|
|
969
|
+
# headless run cannot wait on a prompt and the denial is the limit.
|
|
970
|
+
# --setting-sources user
|
|
971
|
+
# only the user's own settings load, never the project's or the
|
|
972
|
+
# local ones. The user's settings keep their `env` and their own
|
|
973
|
+
# allow rules, so a user allow rule that covers other paths covers
|
|
974
|
+
# the worker too. The user's ask and deny rules on Edit also apply
|
|
975
|
+
# to the run.
|
|
976
|
+
# --settings JSON an allow rule Edit(//<the canonical staged path>), a deny rule
|
|
977
|
+
# Edit(//<the canonical staged path>/*) for every path under it, and
|
|
978
|
+
# no hooks. Under dontAsk a path that no allow rule covers is refused;
|
|
979
|
+
# a folder or a link at the staged path is removed before the run
|
|
980
|
+
# and after it (memory_staged_remove). Only Edit rules cover the
|
|
981
|
+
# Write tool: a Write(path) rule is never checked, so it would deny
|
|
982
|
+
# every Write.
|
|
983
|
+
# The leading // makes the rule path absolute. The JSON is built by
|
|
984
|
+
# memory_settings_json, after the path check above, and its comment
|
|
985
|
+
# holds what was measured.
|
|
986
|
+
# --output-format json
|
|
987
|
+
# the result as one JSON object: its subtype, error flag, turn
|
|
988
|
+
# count and the list of denied calls. The hook reduces it to one
|
|
989
|
+
# fixed log line (memory_result_summary) and deletes the file; the
|
|
990
|
+
# denied calls' input, which can hold the memory body, is never
|
|
991
|
+
# read or logged.
|
|
778
992
|
# DEVFLOW_BG_UPDATER=1 the recursion guard stays on both forms, though --safe-mode
|
|
779
993
|
# also disables hooks. CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 and
|
|
780
994
|
# CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 keep the instruction files and
|
|
781
995
|
# auto-memory out on both forms, including the legacy one.
|
|
782
996
|
#
|
|
997
|
+
# The run starts in the project root, so a session that began in a subfolder runs from
|
|
998
|
+
# the same place. The worker enters the root before the fork; a root it cannot enter ends
|
|
999
|
+
# the run with its own log line and starts no claude, so the spawn's exit status is what
|
|
1000
|
+
# claude returned and nothing else. The spawn is a subshell that ends in `exec`, so the
|
|
1001
|
+
# claude process keeps the subshell's PID, which is its process group's leader (set -m
|
|
1002
|
+
# below), and the watchdog can signal the whole group.
|
|
1003
|
+
#
|
|
783
1004
|
# Why Write-only is safe: the staged path is empty before every run (the removal and
|
|
784
|
-
# the checked precondition after the lock above
|
|
785
|
-
# Write refuses an existing file that has not been
|
|
786
|
-
#
|
|
787
|
-
#
|
|
788
|
-
#
|
|
789
|
-
# read-before-write failures (the run exited 0 while every Write errored);
|
|
790
|
-
# baf02265 removed both flags, with no reason in its message;
|
|
791
|
-
# 6186645f raised the worker's model and kept `--allowedTools 'Read,Write'`.
|
|
1005
|
+
# the checked precondition after the lock above, then the check just before the spawn),
|
|
1006
|
+
# so the one Write is always a create. Write refuses an existing file that has not been
|
|
1007
|
+
# read, and a refused Write can leave the run's exit status at 0. That is why the staged
|
|
1008
|
+
# path is checked twice, and why a denied Write is counted from the JSON result and
|
|
1009
|
+
# logged, not left to look like a success.
|
|
792
1010
|
#
|
|
793
|
-
# The
|
|
794
|
-
#
|
|
795
|
-
#
|
|
1011
|
+
# The lean floor is 2.1.286, the lowest CLI on which the whole lean argv was run and its
|
|
1012
|
+
# flags, the settings `env` under --safe-mode and the effort override were checked
|
|
1013
|
+
# (2.1.286 and 2.1.292 to 2.1.294 behave identically; 2026-10-09, recorded in
|
|
796
1014
|
# docs/reference/platform-assumptions.md). The legacy form carries no --effort,
|
|
797
1015
|
# --safe-mode, --max-turns, --disable-slash-commands, --strict-mcp-config or
|
|
798
1016
|
# --mcp-config, none of which is verified on older CLIs, so on it effort is not
|
|
799
|
-
# applied, MCP servers stay loaded and there is no turn cap.
|
|
1017
|
+
# applied, MCP servers stay loaded and there is no turn cap. A CLI below
|
|
1018
|
+
# MEMORY_MIN_PERMISSION_VERSION gets no run at all: the limit was measured from that
|
|
1019
|
+
# version up.
|
|
800
1020
|
#
|
|
801
1021
|
# The turns input has no byte cap: the queue holds every turn whole by design, and the
|
|
802
1022
|
# prompt is bounded by the newest-20-rows window above and the 64 KB existing-memory
|
|
803
1023
|
# cap, not by cutting a turn.
|
|
804
1024
|
unset CLAUDE_CODE_EFFORT_LEVEL
|
|
805
|
-
|
|
1025
|
+
|
|
1026
|
+
# The JSON result goes to a temporary file the exit trap also removes. mktemp creates it
|
|
1027
|
+
# readable by the user alone; if it cannot be made, the result goes to /dev/null and the
|
|
1028
|
+
# run goes on without a count.
|
|
1029
|
+
RESULT_FILE=$(mktemp "${TMPDIR:-/tmp}/devflow-memory-result.XXXXXX" 2>/dev/null) || RESULT_FILE=""
|
|
1030
|
+
if [ -z "$RESULT_FILE" ]; then
|
|
1031
|
+
log "Result not captured: no temporary file could be created"
|
|
1032
|
+
fi
|
|
1033
|
+
|
|
1034
|
+
# Re-check the staged path just before the spawn: the lock is held and the path was
|
|
1035
|
+
# cleared, but the prompt took time to build. A file or a link there now, or a link
|
|
1036
|
+
# anywhere on its way, was put there since, so the run does not start; the batch stays
|
|
1037
|
+
# in .processing for recovery.
|
|
1038
|
+
if path_occupied "$STAGED_FILE" || ! df_no_symlink_below "$PROJECT_ROOT" "$STAGED_FILE"; then
|
|
1039
|
+
RUN_RESULT="failed"
|
|
1040
|
+
log "SKIP: the staged path changed before the run; leaving .processing for recovery"
|
|
1041
|
+
exit 0
|
|
1042
|
+
fi
|
|
1043
|
+
# Every check on the path has passed, so a path notice from an earlier run is stale.
|
|
1044
|
+
memory_notice_clear "$MEMORY_DIR" "$PROJECT_ROOT" path
|
|
1045
|
+
|
|
1046
|
+
# From here a way out other than the end of the script is a failed run.
|
|
1047
|
+
RUN_ARGV="$CLAUDE_ARGV_FORM"
|
|
1048
|
+
RUN_RESULT="failed"
|
|
1049
|
+
RUN_DENIALS="unknown"
|
|
1050
|
+
|
|
1051
|
+
if ! cd "$PROJECT_ROOT" 2>/dev/null; then
|
|
1052
|
+
log "FAIL: could not enter the project root"
|
|
1053
|
+
exit 0
|
|
1054
|
+
fi
|
|
1055
|
+
|
|
1056
|
+
# One argv for both forms, in the order the flags are passed: the model, the Write tool and
|
|
1057
|
+
# the four permission flags that limit the worker's write are stated here once; the lean
|
|
1058
|
+
# form adds its own flags. (`+=` needs bash 3.1; the array is never empty, so its
|
|
1059
|
+
# expansion below is safe on 3.2.)
|
|
1060
|
+
CLAUDE_ARGV=(
|
|
1061
|
+
-p
|
|
1062
|
+
--model "$MEMORY_MODEL"
|
|
1063
|
+
)
|
|
806
1064
|
if [ "$CLAUDE_ARGV_FORM" = "lean" ]; then
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
--disable-slash-commands \
|
|
813
|
-
--strict-mcp-config \
|
|
814
|
-
--mcp-config '{"mcpServers":{}}' \
|
|
815
|
-
--max-turns 3 \
|
|
816
|
-
--dangerously-skip-permissions \
|
|
817
|
-
--output-format text \
|
|
818
|
-
<<< "$PROMPT" \
|
|
819
|
-
>> "$LOG_FILE" 2>&1 &
|
|
820
|
-
else
|
|
821
|
-
DEVFLOW_BG_UPDATER=1 CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 "$CLAUDE_BIN" -p \
|
|
822
|
-
--model "$MEMORY_MODEL" \
|
|
823
|
-
--tools Write \
|
|
824
|
-
--dangerously-skip-permissions \
|
|
825
|
-
--output-format text \
|
|
826
|
-
<<< "$PROMPT" \
|
|
827
|
-
>> "$LOG_FILE" 2>&1 &
|
|
1065
|
+
CLAUDE_ARGV+=(--effort "$MEMORY_EFFORT" --safe-mode)
|
|
1066
|
+
fi
|
|
1067
|
+
CLAUDE_ARGV+=(--tools Write)
|
|
1068
|
+
if [ "$CLAUDE_ARGV_FORM" = "lean" ]; then
|
|
1069
|
+
CLAUDE_ARGV+=(--disable-slash-commands --strict-mcp-config --mcp-config '{"mcpServers":{}}' --max-turns 3)
|
|
828
1070
|
fi
|
|
1071
|
+
CLAUDE_ARGV+=(
|
|
1072
|
+
--permission-mode dontAsk
|
|
1073
|
+
--setting-sources user
|
|
1074
|
+
--settings "$MEMORY_SETTINGS_JSON"
|
|
1075
|
+
--output-format json
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
set -m
|
|
1079
|
+
(
|
|
1080
|
+
DEVFLOW_BG_UPDATER=1 CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 exec "$CLAUDE_BIN" "${CLAUDE_ARGV[@]}" \
|
|
1081
|
+
<<< "$PROMPT" \
|
|
1082
|
+
> "${RESULT_FILE:-/dev/null}" 2>> "$LOG_FILE"
|
|
1083
|
+
) &
|
|
829
1084
|
CLAUDE_PID=$!
|
|
830
1085
|
set +m
|
|
831
1086
|
|
|
@@ -848,10 +1103,52 @@ else
|
|
|
848
1103
|
CLAUDE_EXIT=$?
|
|
849
1104
|
fi
|
|
850
1105
|
|
|
851
|
-
# Cancel watchdog (avoids stray kill hitting a recycled PID after this process exits)
|
|
852
|
-
|
|
1106
|
+
# Cancel watchdog (avoids stray kill hitting a recycled PID after this process exits).
|
|
1107
|
+
# SIGKILL so no trap can run in the watchdog subshell, whatever the bash version.
|
|
1108
|
+
kill -KILL "$WD_PID" 2>/dev/null || true
|
|
853
1109
|
wait "$WD_PID" 2>/dev/null || true
|
|
854
1110
|
|
|
1111
|
+
# The JSON result, reduced to one fixed log line and deleted on every path out of here
|
|
1112
|
+
# (the exit trap removes it too). Only counts and flags are read from it: the denied
|
|
1113
|
+
# calls' input and the result text never reach the log.
|
|
1114
|
+
MEMORY_RESULT_DENIALS="unknown"
|
|
1115
|
+
if [ -n "$RESULT_FILE" ]; then
|
|
1116
|
+
memory_result_summary "$RESULT_FILE"
|
|
1117
|
+
log "$MEMORY_RESULT_LINE"
|
|
1118
|
+
rm -f "$RESULT_FILE" 2>/dev/null || true
|
|
1119
|
+
RESULT_FILE=""
|
|
1120
|
+
RUN_DENIALS="$MEMORY_RESULT_DENIALS"
|
|
1121
|
+
fi
|
|
1122
|
+
# 0 and an unreported count are both "no denial to report"; any other count is a refusal.
|
|
1123
|
+
RESULT_DENIED=""
|
|
1124
|
+
case "$MEMORY_RESULT_DENIALS" in
|
|
1125
|
+
unknown|0) ;;
|
|
1126
|
+
*) RESULT_DENIED="yes" ;;
|
|
1127
|
+
esac
|
|
1128
|
+
|
|
1129
|
+
# A folder or a link at the staged path now was put there during the run: the path was
|
|
1130
|
+
# empty at the spawn. It is removed the way it is before a run (memory_staged_remove), and
|
|
1131
|
+
# the run ends as failed: the checks below find no staged file, and the batch stays for
|
|
1132
|
+
# recovery. If the removal is refused, the folder stays, the log names the reason and the
|
|
1133
|
+
# next run stops at the same guard. The tests are builtins, so a normal run forks nothing.
|
|
1134
|
+
if [ -L "$STAGED_FILE" ] || [ -d "$STAGED_FILE" ]; then
|
|
1135
|
+
if memory_staged_remove "$MEMORY_DIR" "$PROJECT_ROOT" "$STAGED_FILE"; then
|
|
1136
|
+
RUN_REMOVED="$STAGED_REMOVED"
|
|
1137
|
+
log "FAIL: a ${STAGED_REMOVED/dir/folder} stood at $STAGED_FILE after the run and was removed; the run is failed"
|
|
1138
|
+
else
|
|
1139
|
+
log "FAIL: a folder or a link stands at $STAGED_FILE after the run and was not removed (${STAGED_REMOVE_REFUSAL}); remove it to resume memory updates"
|
|
1140
|
+
fi
|
|
1141
|
+
fi
|
|
1142
|
+
|
|
1143
|
+
# Logs a run whose Write the permission rules refused, with its closing text <$1>, and
|
|
1144
|
+
# counts the run toward the `denied` notice (two denied runs with no successful update
|
|
1145
|
+
# between them).
|
|
1146
|
+
record_denied_run() {
|
|
1147
|
+
log "FAIL: Write denied by permission rule (${MEMORY_RESULT_DENIALS})$1"
|
|
1148
|
+
memory_denied_seen "$MEMORY_DIR" "$PROJECT_ROOT"
|
|
1149
|
+
return 0
|
|
1150
|
+
}
|
|
1151
|
+
|
|
855
1152
|
if [ "$CLAUDE_EXIT" -gt 128 ]; then
|
|
856
1153
|
log "FAIL: claude -p killed by watchdog after ${WATCHDOG_SECS}s"
|
|
857
1154
|
# Leave .processing for crash recovery (session-start-memory's D56c cold path).
|
|
@@ -861,7 +1158,13 @@ if [ "$CLAUDE_EXIT" -gt 128 ]; then
|
|
|
861
1158
|
# keeping this plumbing script simple.
|
|
862
1159
|
exit 0
|
|
863
1160
|
elif [ "$CLAUDE_EXIT" -ne 0 ]; then
|
|
864
|
-
|
|
1161
|
+
if [ -n "$RESULT_DENIED" ]; then
|
|
1162
|
+
# A refused Write can end the run non-zero too (the lean form's turn cap), so the
|
|
1163
|
+
# count is named here as well as the exit code.
|
|
1164
|
+
record_denied_run ", exit $CLAUDE_EXIT"
|
|
1165
|
+
else
|
|
1166
|
+
log "FAIL: claude -p exited with code $CLAUDE_EXIT"
|
|
1167
|
+
fi
|
|
865
1168
|
# Leave .processing for crash recovery (see same rate-bound note above)
|
|
866
1169
|
exit 0
|
|
867
1170
|
fi
|
|
@@ -939,6 +1242,7 @@ verify_and_swap() {
|
|
|
939
1242
|
|
|
940
1243
|
OUTCOME="failed"
|
|
941
1244
|
verify_and_swap
|
|
1245
|
+
RUN_RESULT="$OUTCOME"
|
|
942
1246
|
|
|
943
1247
|
# Each stamp below is a `touch`, which follows a symbolic link, and its path was
|
|
944
1248
|
# checked before the LLM run, which can take minutes. So, like the swap's target,
|
|
@@ -946,6 +1250,8 @@ verify_and_swap
|
|
|
946
1250
|
# during the run, and the stamp is skipped and logged (D-HOOKS-NO-SYMLINK, git-marker).
|
|
947
1251
|
if [ "$OUTCOME" = "updated" ]; then
|
|
948
1252
|
rm -f "$PROCESSING_FILE" 2>/dev/null || true
|
|
1253
|
+
# A run that swapped the memory in ends any count of runs the write was refused in.
|
|
1254
|
+
memory_notice_clear "$MEMORY_DIR" "$PROJECT_ROOT" denied
|
|
949
1255
|
if df_no_symlink_below "$PROJECT_ROOT" "$OK_FILE"; then
|
|
950
1256
|
touch "$OK_FILE" 2>/dev/null || true
|
|
951
1257
|
log "SUCCESS: queue drained, .last-refresh-ok touched"
|
|
@@ -961,6 +1267,9 @@ elif [ "$OUTCOME" = "conflict" ]; then
|
|
|
961
1267
|
log "Heartbeat skipped: a symbolic link appeared on the path to $PROCESSING_FILE during the run"
|
|
962
1268
|
fi
|
|
963
1269
|
log "CONFLICT: queue retained in .processing, .last-refresh-ok untouched"
|
|
1270
|
+
elif [ -n "$RESULT_DENIED" ]; then
|
|
1271
|
+
# Leave .processing for session-start-memory's D56c cold path; do NOT touch .last-refresh-ok
|
|
1272
|
+
record_denied_run " — leaving .processing for recovery"
|
|
964
1273
|
else
|
|
965
1274
|
# Leave .processing for session-start-memory's D56c cold path; do NOT touch .last-refresh-ok
|
|
966
1275
|
log "FAIL: verification failed — leaving .processing for recovery"
|