devflow-kit 3.1.0 → 3.3.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 +52 -0
- package/README.md +2 -2
- package/dist/cli/agents-view/render.js +69 -15
- package/dist/cli/agents-view/state.js +40 -14
- package/dist/cli/commands/agents.js +135 -45
- package/dist/cli/commands/init.js +128 -53
- package/dist/cli/commands/learning.js +61 -13
- package/dist/cli/commands/memory.js +35 -14
- package/dist/cli/commands/uninstall.js +163 -39
- package/dist/commands/code-review.md +1 -3
- package/dist/commands/debug.md +15 -12
- package/dist/commands/dynamic-build.md +172 -135
- package/dist/commands/dynamic-plan.md +9 -3
- package/dist/commands/explore.md +10 -4
- package/dist/commands/implement.md +149 -145
- package/dist/commands/plan.md +13 -9
- package/dist/commands/release.md +8 -2
- package/dist/commands/research.md +8 -2
- package/dist/commands/resolve.md +28 -19
- package/dist/commands/self-review.md +16 -13
- package/dist/core/agent-frontmatter.js +25 -0
- package/dist/core/agent-models.js +201 -36
- package/dist/core/agent-state.js +27 -5
- package/dist/core/assets.js +1 -1
- package/dist/core/feature-config.js +68 -10
- package/dist/core/flags.js +24 -0
- package/dist/core/learning-queue-cleanup.js +10 -11
- package/dist/core/learning-tuning-config.js +8 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/plugins.js +16 -5
- package/dist/core/queue-drain.js +31 -0
- package/dist/hud/components/learning-counts.js +54 -8
- package/dist/skills/git/references/tracker/github/create-release.md +2 -2
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +1 -1
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +1 -1
- package/dist/targets/claude-code/installer.js +36 -9
- package/dist/targets/claude-code/post-install.js +128 -38
- package/package.json +1 -1
- package/src/assets/agents/code.md +85 -35
- package/src/assets/agents/design.md +12 -0
- package/src/assets/agents/diagnose.md +18 -11
- package/src/assets/agents/evaluate.md +17 -24
- package/src/assets/agents/knowledge.md +7 -3
- package/src/assets/agents/learning.md +4 -6
- package/src/assets/agents/research.md +21 -0
- package/src/assets/agents/review.md +12 -0
- package/src/assets/agents/scrutinize.md +37 -9
- package/src/assets/agents/simplify.md +24 -0
- package/src/assets/agents/skim.md +6 -2
- package/src/assets/agents/synthesize.md +18 -0
- package/src/assets/agents/test.md +19 -11
- package/src/assets/agents/triage.md +8 -0
- package/src/assets/agents/validate.md +20 -11
- package/src/assets/commands/_partials/_engine.mds +36 -55
- package/src/assets/commands/_partials/_knowledge.mds +1 -3
- package/src/assets/commands/_partials/_plan_contract.mds +1 -1
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/_partials/_wave.mds +8 -6
- package/src/assets/commands/code-review.mds +1 -3
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +126 -72
- package/src/assets/commands/dynamic-plan.mds +7 -1
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +147 -141
- package/src/assets/commands/plan.mds +12 -8
- package/src/assets/commands/release.md +8 -2
- package/src/assets/commands/research.mds +8 -2
- package/src/assets/commands/resolve.mds +27 -16
- package/src/assets/commands/self-review.mds +15 -10
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +2 -2
- package/src/assets/mds/tracker/_jira.mds +2 -2
- package/src/assets/mds/tracker/_linear.mds +2 -2
- package/src/assets/scripts/ci-wait.cjs +636 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -3
- package/src/assets/scripts/hooks/background-memory-update +356 -17
- package/src/assets/scripts/hooks/capture-prompt +4 -3
- package/src/assets/scripts/hooks/capture-question +4 -3
- package/src/assets/scripts/hooks/capture-turn +4 -3
- package/src/assets/scripts/hooks/ensure-devflow-init +13 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +122 -10
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/json-helper.cjs +12 -145
- package/src/assets/scripts/hooks/json-parse +24 -129
- package/src/assets/scripts/hooks/lib/learning-store.cjs +169 -64
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +10 -0
- package/src/assets/scripts/hooks/pre-compact-memory +66 -14
- package/src/assets/scripts/hooks/preamble +9 -1
- package/src/assets/scripts/hooks/queue-append +53 -21
- package/src/assets/scripts/hooks/session-start-context +108 -29
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/release-trace.cjs +27 -10
- package/src/assets/skills/accessibility/SKILL.md +1 -1
- package/src/assets/skills/apply-decisions/SKILL.md +12 -82
- package/src/assets/skills/apply-feature-knowledge/SKILL.md +8 -42
- package/src/assets/skills/architecture/SKILL.md +1 -1
- package/src/assets/skills/boundary-validation/SKILL.md +1 -1
- package/src/assets/skills/complexity/SKILL.md +1 -1
- package/src/assets/skills/compliance/SKILL.md +1 -1
- package/src/assets/skills/consistency/SKILL.md +1 -1
- package/src/assets/skills/database/SKILL.md +1 -1
- package/src/assets/skills/dependencies/SKILL.md +1 -1
- package/src/assets/skills/dependency-research/SKILL.md +3 -6
- package/src/assets/skills/design-review/SKILL.md +1 -1
- package/src/assets/skills/docs-framework/SKILL.md +1 -1
- package/src/assets/skills/documentation/SKILL.md +1 -1
- package/src/assets/skills/gap-analysis/SKILL.md +1 -1
- package/src/assets/skills/git/SKILL.md +1 -1
- package/src/assets/skills/go/SKILL.md +1 -1
- package/src/assets/skills/java/SKILL.md +1 -1
- package/src/assets/skills/patterns/SKILL.md +1 -1
- package/src/assets/skills/performance/SKILL.md +1 -1
- package/src/assets/skills/python/SKILL.md +1 -1
- package/src/assets/skills/qa/SKILL.md +1 -3
- package/src/assets/skills/quality-gates/SKILL.md +9 -12
- package/src/assets/skills/quality-gates/references/report-template.md +20 -20
- package/src/assets/skills/react/SKILL.md +1 -1
- package/src/assets/skills/regression/SKILL.md +1 -1
- package/src/assets/skills/reliability/SKILL.md +1 -1
- package/src/assets/skills/research-codebase/SKILL.md +1 -1
- package/src/assets/skills/research-competitor/SKILL.md +1 -1
- package/src/assets/skills/research-external/SKILL.md +1 -1
- package/src/assets/skills/research-technology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/rust/SKILL.md +1 -1
- package/src/assets/skills/security/SKILL.md +1 -1
- package/src/assets/skills/software-design/SKILL.md +1 -1
- package/src/assets/skills/test-driven-development/SKILL.md +15 -33
- package/src/assets/skills/testing/SKILL.md +1 -1
- package/src/assets/skills/typescript/SKILL.md +1 -1
- package/src/assets/skills/ui-design/SKILL.md +1 -1
- package/src/assets/skills/worktree-support/SKILL.md +3 -55
- package/src/assets/skills/worktree-support/references/discovery.md +48 -0
- package/src/assets/skills/worktree-support/references/roots.md +2 -2
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# background-memory-update — Eager working-memory refresh worker
|
|
4
4
|
# Spawned as a detached background process by memory-worker (Stop hook) after the
|
|
5
|
-
# 120s throttle expires. Drains .pending-turns.jsonl → calls `claude -p`
|
|
5
|
+
# 120s throttle expires. Drains .pending-turns.jsonl → calls `claude -p` on the model
|
|
6
|
+
# and effort the `agents.memory` entry of ~/.devflow/agent-models.json names, else the
|
|
7
|
+
# shipped worker default, in a lean session (D-MEMORY-WORKER-LEAN) →
|
|
6
8
|
# rewrites WORKING-MEMORY.md with a git-reconciliation stamp on line 1.
|
|
7
9
|
#
|
|
8
10
|
# Applies the LLM-vs-plumbing principle: artifact content (WORKING-MEMORY.md) is
|
|
@@ -106,6 +108,104 @@ if ! command -v cksum >/dev/null 2>&1; then
|
|
|
106
108
|
exit 0
|
|
107
109
|
fi
|
|
108
110
|
|
|
111
|
+
# --- Never write through a symbolic link (D-HOOKS-NO-SYMLINK, git-marker) ---
|
|
112
|
+
# The lock (`mkdir`), the batch merge (`>>`), the claim heartbeat and the success
|
|
113
|
+
# stamp (`touch`) write these paths in place; the claim and the trim rename onto the
|
|
114
|
+
# batch and the swap onto the working memory, and `mv` moves a file into the folder
|
|
115
|
+
# a link at its target names. The trim's copy is created only where nothing stands.
|
|
116
|
+
# So the run is skipped, before it takes the lock or starts an LLM run, when any of
|
|
117
|
+
# these paths or a folder above them is a link.
|
|
118
|
+
if ! df_no_symlink_below "$PROJECT_ROOT" "$LOCK_DIR" "$QUEUE_FILE" "$PROCESSING_FILE" "$OK_FILE" "$MEMORY_FILE"; then
|
|
119
|
+
log "SKIP: a symbolic link sits on the path to the memory files in $MEMORY_DIR; nothing written"
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- CLI version probe: choose the argv form (D-MEMORY-WORKER-LEAN) ---
|
|
124
|
+
# The lean argv (--effort, --safe-mode, --max-turns, ...) is verified from
|
|
125
|
+
# MEMORY_MIN_CLAUDE_VERSION up. A CLI older than that, or one whose version cannot be
|
|
126
|
+
# read, gets the legacy argv instead, because one flag a CLI does not know fails every
|
|
127
|
+
# run. The probe runs here, once $CLAUDE_BIN has resolved and the link check has
|
|
128
|
+
# passed, and before the lock is taken, so a slow or hung `--version` can never extend
|
|
129
|
+
# the lock's hold. It is bounded by DEVFLOW_BG_VERSION_PROBE_SECS (default 10, at most
|
|
130
|
+
# 60; tests override it as they do DEVFLOW_BG_WATCHDOG_SECS) with the watchdog's
|
|
131
|
+
# background-and-kill idiom. Its output is parsed, never executed.
|
|
132
|
+
#
|
|
133
|
+
# Errexit safety: the worker runs under `set -e`, and a helper's last non-zero status
|
|
134
|
+
# aborts the caller, so each helper below ends in an explicit return status and is
|
|
135
|
+
# called inside an `if` condition.
|
|
136
|
+
MEMORY_MIN_CLAUDE_VERSION="2.1.286"
|
|
137
|
+
|
|
138
|
+
# Sets CLAUDE_VERSION_TEXT to the first 512 bytes `claude --version` printed ("" on any
|
|
139
|
+
# failure). Returns 0 when it exited 0 inside the bound, else 1. A hung probe gets
|
|
140
|
+
# SIGTERM to its own process group (set -m, as for the spawn below) after the bound
|
|
141
|
+
# and SIGKILL 2s later; the watchdog subshell has its descriptors closed so a sleep it
|
|
142
|
+
# leaves behind holds no pipe open.
|
|
143
|
+
probe_claude_version() {
|
|
144
|
+
local _secs _out _pid _wd_pid _status=1
|
|
145
|
+
CLAUDE_VERSION_TEXT=""
|
|
146
|
+
_secs="${DEVFLOW_BG_VERSION_PROBE_SECS:-10}"
|
|
147
|
+
case "$_secs" in
|
|
148
|
+
''|*[!0123456789]*|????*) _secs=10 ;;
|
|
149
|
+
esac
|
|
150
|
+
if [ "$_secs" -lt 1 ] || [ "$_secs" -gt 60 ]; then _secs=10; fi
|
|
151
|
+
_out=$(mktemp "${TMPDIR:-/tmp}/devflow-claude-version.XXXXXX" 2>/dev/null) || return 1
|
|
152
|
+
set -m
|
|
153
|
+
"$CLAUDE_BIN" --version </dev/null >"$_out" 2>/dev/null &
|
|
154
|
+
_pid=$!
|
|
155
|
+
set +m
|
|
156
|
+
(
|
|
157
|
+
sleep "$_secs"
|
|
158
|
+
kill -TERM -"$_pid" 2>/dev/null || true
|
|
159
|
+
sleep 2
|
|
160
|
+
kill -KILL -"$_pid" 2>/dev/null || true
|
|
161
|
+
) </dev/null >/dev/null 2>&1 &
|
|
162
|
+
_wd_pid=$!
|
|
163
|
+
if wait "$_pid" 2>/dev/null; then _status=0; fi
|
|
164
|
+
kill "$_wd_pid" 2>/dev/null || true
|
|
165
|
+
wait "$_wd_pid" 2>/dev/null || true
|
|
166
|
+
if [ "$_status" -eq 0 ]; then
|
|
167
|
+
CLAUDE_VERSION_TEXT=$(head -c 512 "$_out" 2>/dev/null || true)
|
|
168
|
+
fi
|
|
169
|
+
rm -f "$_out" 2>/dev/null || true
|
|
170
|
+
return "$_status"
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Returns 0 when the first X.Y.Z in $1 is at or above the first X.Y.Z in $2, comparing
|
|
174
|
+
# the three components as base-10 numbers left to right (2.10.0 is above 2.1.286, and
|
|
175
|
+
# a leading zero is not octal). Returns 1 when it is below, or when either text holds
|
|
176
|
+
# no X.Y.Z or a component longer than six digits. The digit set is spelled out: a
|
|
177
|
+
# range such as 0-9 collates by locale on bash 3.2.
|
|
178
|
+
claude_version_at_least() {
|
|
179
|
+
local _text="$1" _floor="$2" _re='([0123456789]+)\.([0123456789]+)\.([0123456789]+)'
|
|
180
|
+
local _f1 _f2 _f3 _v1 _v2 _v3
|
|
181
|
+
[[ "$_floor" =~ $_re ]] || return 1
|
|
182
|
+
_f1=${BASH_REMATCH[1]}; _f2=${BASH_REMATCH[2]}; _f3=${BASH_REMATCH[3]}
|
|
183
|
+
[[ "$_text" =~ $_re ]] || return 1
|
|
184
|
+
_v1=${BASH_REMATCH[1]}; _v2=${BASH_REMATCH[2]}; _v3=${BASH_REMATCH[3]}
|
|
185
|
+
if [ "${#_v1}" -gt 6 ] || [ "${#_v2}" -gt 6 ] || [ "${#_v3}" -gt 6 ]; then return 1; fi
|
|
186
|
+
_v1=$((10#$_v1)); _v2=$((10#$_v2)); _v3=$((10#$_v3))
|
|
187
|
+
_f1=$((10#$_f1)); _f2=$((10#$_f2)); _f3=$((10#$_f3))
|
|
188
|
+
if [ "$_v1" -ne "$_f1" ]; then
|
|
189
|
+
if [ "$_v1" -gt "$_f1" ]; then return 0; fi
|
|
190
|
+
return 1
|
|
191
|
+
fi
|
|
192
|
+
if [ "$_v2" -ne "$_f2" ]; then
|
|
193
|
+
if [ "$_v2" -gt "$_f2" ]; then return 0; fi
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
if [ "$_v3" -ge "$_f3" ]; then return 0; fi
|
|
197
|
+
return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
CLAUDE_ARGV_FORM="legacy"
|
|
201
|
+
if ! probe_claude_version; then
|
|
202
|
+
log "claude --version probe failed or timed out — using the legacy argv"
|
|
203
|
+
elif claude_version_at_least "$CLAUDE_VERSION_TEXT" "$MEMORY_MIN_CLAUDE_VERSION"; then
|
|
204
|
+
CLAUDE_ARGV_FORM="lean"
|
|
205
|
+
else
|
|
206
|
+
log "claude --version is below ${MEMORY_MIN_CLAUDE_VERSION} or unparseable — using the legacy argv"
|
|
207
|
+
fi
|
|
208
|
+
|
|
109
209
|
# --- Worker-level lock (300s stale-break — much longer than learning-lock's 30s) ---
|
|
110
210
|
# This prevents a second worker (spawned 121s later) from double-writing WORKING-MEMORY.md
|
|
111
211
|
# while the first worker's claude -p call (up to 120s) is still in flight.
|
|
@@ -159,7 +259,20 @@ fi
|
|
|
159
259
|
# Clean up any staged file left by a watchdog-killed prior run.
|
|
160
260
|
# Without this, a stale staged file with a valid stamp would be mistakenly
|
|
161
261
|
# mv-ed to the real path on the NEXT run's CAS check (D-MEMORY-STAGED-CAS).
|
|
262
|
+
#
|
|
263
|
+
# D-MEMORY-WORKER-LEAN: the removal is also a CHECKED precondition. The model gets the
|
|
264
|
+
# Write tool alone, and Write refuses a file that exists and has not been read, which
|
|
265
|
+
# is how an earlier Write-only worker failed silently: every Write errored while the
|
|
266
|
+
# run exited 0. Once nothing stands at the staged path, the one Write is always a
|
|
267
|
+
# create, so Write-only is safe. A path `rm -f` cannot clear (a directory, say) would
|
|
268
|
+
# make every Write fail, so the run stops here instead: no LLM call, the batch and
|
|
269
|
+
# the queue untouched (nothing has been claimed yet), the lock released by the EXIT
|
|
270
|
+
# trap. `-e` or `-L`, so a link there counts as present.
|
|
162
271
|
rm -f "$STAGED_FILE" 2>/dev/null || true
|
|
272
|
+
if [ -e "$STAGED_FILE" ] || [ -L "$STAGED_FILE" ]; then
|
|
273
|
+
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"
|
|
274
|
+
exit 0
|
|
275
|
+
fi
|
|
163
276
|
|
|
164
277
|
# --- Orphan-only skip: if queue has no assistant/qa turn, exit and leave the queue ---
|
|
165
278
|
# This prevents fabrication-prone LLM runs with only user turns in the queue.
|
|
@@ -210,8 +323,20 @@ if [ -f "$PROCESSING_FILE" ]; then
|
|
|
210
323
|
_PROC_LINES=$(wc -l < "$PROCESSING_FILE" | tr -d ' ')
|
|
211
324
|
if [ "$_PROC_LINES" -gt "$PROC_OVERFLOW_CAP" ]; then
|
|
212
325
|
_PROC_TMP="$PROCESSING_FILE.tmp.$$"
|
|
213
|
-
|
|
214
|
-
|
|
326
|
+
# SEC-2: the batch is the claimed queue, 0600 like the queue itself, and the
|
|
327
|
+
# cold path at session start moves it back to the queue. mv gives it the
|
|
328
|
+
# trimmed copy's inode and mode, so the copy is created under umask 077, and
|
|
329
|
+
# only where nothing stands at its name: builtin tests come first, so an entry
|
|
330
|
+
# already there, a link to a FIFO or a device included, is never opened
|
|
331
|
+
# (noclobber alone would open one, as no regular file stands there), and
|
|
332
|
+
# noclobber then makes the create itself exclusive. Such an entry is removed
|
|
333
|
+
# instead, and the batch is left untrimmed.
|
|
334
|
+
if [ ! -e "$_PROC_TMP" ] && [ ! -L "$_PROC_TMP" ] && (umask 077 && set -o noclobber && tail -"$PROC_OVERFLOW_TARGET" "$PROCESSING_FILE" > "$_PROC_TMP") && mv "$_PROC_TMP" "$PROCESSING_FILE"; then
|
|
335
|
+
log "Processing overflow: truncated from $_PROC_LINES to $PROC_OVERFLOW_TARGET lines"
|
|
336
|
+
else
|
|
337
|
+
rm -f "$_PROC_TMP" 2>/dev/null || true
|
|
338
|
+
log "Processing overflow: not truncated, its copy could not be written ($_PROC_LINES lines)"
|
|
339
|
+
fi
|
|
215
340
|
fi
|
|
216
341
|
elif [ -f "$QUEUE_FILE" ]; then
|
|
217
342
|
mv "$QUEUE_FILE" "$PROCESSING_FILE" 2>/dev/null || { log "SKIP: failed to claim queue (race condition — another worker got it)"; exit 0; }
|
|
@@ -340,15 +465,22 @@ log "Built $TURN_COUNT turns from queue"
|
|
|
340
465
|
# on either side, the CAS must treat it as CONFLICT rather than a match — separating
|
|
341
466
|
# the "cksum invocation failure" sentinel from "file absent" keeps the "resolves toward
|
|
342
467
|
# false-conflict, never false-success" invariant true even when cksum errors.
|
|
468
|
+
#
|
|
469
|
+
# D-HOOKS-NO-SYMLINK (git-marker): the existing memory goes into the prompt, so a
|
|
470
|
+
# working memory a symbolic link leads to is treated as absent. A link at its own
|
|
471
|
+
# path has already skipped the run, since the swap renames onto it (above), so
|
|
472
|
+
# only one that appeared since can reach this read.
|
|
473
|
+
MEMORY_PRESENT=""
|
|
474
|
+
if df_file_below "$PROJECT_ROOT" "$MEMORY_FILE"; then MEMORY_PRESENT="yes"; fi
|
|
343
475
|
CKSUM_FAILED="false"
|
|
344
476
|
PRE_RUN_CKSUM="ABSENT"
|
|
345
|
-
if [ -
|
|
477
|
+
if [ -n "$MEMORY_PRESENT" ]; then
|
|
346
478
|
PRE_RUN_CKSUM=$(cksum "$MEMORY_FILE" 2>/dev/null) || CKSUM_FAILED="true"
|
|
347
479
|
fi
|
|
348
480
|
|
|
349
481
|
EXISTING_MEMORY=""
|
|
350
482
|
MEMORY_READ_LIMIT=65536 # bytes — keep prompt under context limit
|
|
351
|
-
if [ -
|
|
483
|
+
if [ -n "$MEMORY_PRESENT" ]; then
|
|
352
484
|
EXISTING_MEMORY=$(head -c "$MEMORY_READ_LIMIT" "$MEMORY_FILE")
|
|
353
485
|
fi
|
|
354
486
|
|
|
@@ -469,7 +601,107 @@ Instructions:
|
|
|
469
601
|
EOF
|
|
470
602
|
)
|
|
471
603
|
|
|
472
|
-
|
|
604
|
+
# --- Model and effort (D-MEMORY-WORKER-LEAN) ---
|
|
605
|
+
# One authority: WORKER_AGENTS.memory in src/core/agent-models.ts, which `devflow
|
|
606
|
+
# agents` shows as the worker's default. The two defaults below are that row spelled
|
|
607
|
+
# out for bash, held equal to it by tests/agent-models-worker.test.ts, as are the
|
|
608
|
+
# alias and effort lists and the name rules. A personal `agents.memory` entry in
|
|
609
|
+
# $HOME/.devflow/agent-models.json (the machine root; nothing relocates it) overrides
|
|
610
|
+
# them. Each field is validated on its own against the worker domain, so a bad model
|
|
611
|
+
# keeps a good effort and the reverse:
|
|
612
|
+
# model = `default` (the shipped one), one of the aliases, or a `claude-` name;
|
|
613
|
+
# a name starts with a letter or digit, continues with those and . _ -,
|
|
614
|
+
# and is 64 characters at most (MODEL_NAME_RE in src/core/agent-frontmatter.ts)
|
|
615
|
+
# effort = `default` (the shipped one) or one of the levels
|
|
616
|
+
# `inherit` is outside both, since a background worker has no session to inherit from.
|
|
617
|
+
# Only a value that passed its match reaches argv. A value that is absent, empty or
|
|
618
|
+
# not a JSON string reads as absent and takes the shipped one silently; a string that
|
|
619
|
+
# fails its match takes it with one log line naming the field, never the value.
|
|
620
|
+
# The character sets are spelled out because a range such as A-Za-z matches accented
|
|
621
|
+
# letters in a UTF-8 locale on bash 3.2.
|
|
622
|
+
MEMORY_MODEL_DEFAULT="claude-sonnet-5-5"
|
|
623
|
+
MEMORY_EFFORT_DEFAULT="high"
|
|
624
|
+
MEMORY_MODEL_ALIASES="haiku sonnet opus fable"
|
|
625
|
+
MEMORY_MODEL_PREFIX="claude-"
|
|
626
|
+
MEMORY_MODEL_MAX_LEN=64
|
|
627
|
+
MEMORY_EFFORT_LEVELS="low medium high xhigh max"
|
|
628
|
+
MEMORY_NAME_HEAD="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
629
|
+
MEMORY_EFFORT_CHARS="abcdefghijklmnopqrstuvwxyz"
|
|
630
|
+
|
|
631
|
+
# Prints the string value of agents.memory.<field> followed by one `x` (the caller strips
|
|
632
|
+
# it, so a value's own trailing newline survives the command substitution). Prints just
|
|
633
|
+
# the `x` when the file, the field or a string there is missing. Always returns 0.
|
|
634
|
+
memory_agent_field() {
|
|
635
|
+
local _file="$HOME/.devflow/agent-models.json"
|
|
636
|
+
if [ "$_JSON_AVAILABLE" = "true" ] && [ -f "$_file" ]; then
|
|
637
|
+
json_string_field_file "$_file" "agents.memory.$1" 2>/dev/null || true
|
|
638
|
+
fi
|
|
639
|
+
printf x
|
|
640
|
+
return 0
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
# Sets MEMORY_MODEL. Always returns 0.
|
|
644
|
+
resolve_memory_model() {
|
|
645
|
+
local _raw _tail="${MEMORY_NAME_HEAD}._-" _ok=""
|
|
646
|
+
MEMORY_MODEL="$MEMORY_MODEL_DEFAULT"
|
|
647
|
+
_raw=$(memory_agent_field model)
|
|
648
|
+
_raw=${_raw%x}
|
|
649
|
+
case "$_raw" in
|
|
650
|
+
''|default) return 0 ;;
|
|
651
|
+
[!$MEMORY_NAME_HEAD]*|*[!$_tail]*) ;;
|
|
652
|
+
*)
|
|
653
|
+
if [ "${#_raw}" -le "$MEMORY_MODEL_MAX_LEN" ]; then
|
|
654
|
+
case " $MEMORY_MODEL_ALIASES " in
|
|
655
|
+
*" $_raw "*) _ok="yes" ;;
|
|
656
|
+
esac
|
|
657
|
+
case "$_raw" in
|
|
658
|
+
"$MEMORY_MODEL_PREFIX"?*) _ok="yes" ;;
|
|
659
|
+
esac
|
|
660
|
+
fi
|
|
661
|
+
;;
|
|
662
|
+
esac
|
|
663
|
+
if [ -n "$_ok" ]; then
|
|
664
|
+
MEMORY_MODEL="$_raw"
|
|
665
|
+
else
|
|
666
|
+
log "agents.memory.model: not a model the worker accepts (default, an alias or a ${MEMORY_MODEL_PREFIX} name) — using the shipped model"
|
|
667
|
+
fi
|
|
668
|
+
return 0
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
# Sets MEMORY_EFFORT. Always returns 0.
|
|
672
|
+
resolve_memory_effort() {
|
|
673
|
+
local _raw _ok=""
|
|
674
|
+
MEMORY_EFFORT="$MEMORY_EFFORT_DEFAULT"
|
|
675
|
+
_raw=$(memory_agent_field effort)
|
|
676
|
+
_raw=${_raw%x}
|
|
677
|
+
case "$_raw" in
|
|
678
|
+
''|default) return 0 ;;
|
|
679
|
+
*[!$MEMORY_EFFORT_CHARS]*) ;;
|
|
680
|
+
*)
|
|
681
|
+
case " $MEMORY_EFFORT_LEVELS " in
|
|
682
|
+
*" $_raw "*) _ok="yes" ;;
|
|
683
|
+
esac
|
|
684
|
+
;;
|
|
685
|
+
esac
|
|
686
|
+
if [ -n "$_ok" ]; then
|
|
687
|
+
MEMORY_EFFORT="$_raw"
|
|
688
|
+
else
|
|
689
|
+
log "agents.memory.effort: not an effort level the worker accepts (default or a level) — using the shipped effort"
|
|
690
|
+
fi
|
|
691
|
+
return 0
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
resolve_memory_model
|
|
695
|
+
resolve_memory_effort
|
|
696
|
+
|
|
697
|
+
if [ "$CLAUDE_ARGV_FORM" = "lean" ]; then
|
|
698
|
+
EFFORT_LOG="effort ${MEMORY_EFFORT}"
|
|
699
|
+
ARGV_LOG="lean argv"
|
|
700
|
+
else
|
|
701
|
+
EFFORT_LOG="no effort passed"
|
|
702
|
+
ARGV_LOG="legacy argv"
|
|
703
|
+
fi
|
|
704
|
+
log "Spawning claude -p (model ${MEMORY_MODEL}, ${EFFORT_LOG}, ${ARGV_LOG}, ${TURN_COUNT} turns)"
|
|
473
705
|
# SECURITY: never log PROMPT — it contains turn content which may include secrets
|
|
474
706
|
|
|
475
707
|
# --- Watchdog timeout (overridable for tests: DEVFLOW_BG_WATCHDOG_SECS=2) ---
|
|
@@ -506,14 +738,94 @@ fi
|
|
|
506
738
|
# Restore set +m immediately after capturing CLAUDE_PID so the watchdog subshell and
|
|
507
739
|
# other subsequent background operations stay in the worker's group.
|
|
508
740
|
# Job-control notifications go to stderr which is redirected to the log — suppress them.
|
|
741
|
+
#
|
|
742
|
+
# 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. Prompt on stdin, output and
|
|
744
|
+
# errors to the log, as before. Two argv forms, chosen by the version probe above:
|
|
745
|
+
#
|
|
746
|
+
# lean (CLI at or above MEMORY_MIN_CLAUDE_VERSION):
|
|
747
|
+
# -p --model M --effort E --safe-mode --tools Write --disable-slash-commands \
|
|
748
|
+
# --strict-mcp-config --mcp-config '{"mcpServers":{}}' --max-turns 3 \
|
|
749
|
+
# --dangerously-skip-permissions --output-format text
|
|
750
|
+
# legacy (older, unreadable or timed-out probe):
|
|
751
|
+
# -p --model M --tools Write --dangerously-skip-permissions --output-format text
|
|
752
|
+
#
|
|
753
|
+
# What each flag is for:
|
|
754
|
+
# --model, --effort the resolved agents.memory values above. --effort is the only
|
|
755
|
+
# effort source: an inherited CLAUDE_CODE_EFFORT_LEVEL beats the
|
|
756
|
+
# flag (measured on 2026-10-09), so it is unset below, for both
|
|
757
|
+
# forms, and the log's "no effort passed" is then true.
|
|
758
|
+
# --safe-mode starts with the user's customisations off: instruction files,
|
|
759
|
+
# skills, plugins, hooks, MCP servers, custom commands and agents.
|
|
760
|
+
# Auth, model selection and built-in tools work as usual, and
|
|
761
|
+
# settings `env` (the provider URL and key) still applies under
|
|
762
|
+
# it (measured on 2026-10-09), so a routed or keyed install works.
|
|
763
|
+
# --tools Write Write is the only tool. `--allowedTools` only pre-approves, it
|
|
764
|
+
# leaves every tool, listing and MCP server loaded.
|
|
765
|
+
# --disable-slash-commands, --strict-mcp-config with an empty --mcp-config
|
|
766
|
+
# no slash-command or skill listing, no MCP server and none of the
|
|
767
|
+
# instructions they add to the context.
|
|
768
|
+
# --max-turns 3 the worker's own pattern is one Write and then the end of the
|
|
769
|
+
# turn, two requests; the third is margin. A run that reaches the
|
|
770
|
+
# cap exits non-zero, so its batch stays for the retry; the Write
|
|
771
|
+
# on the capped turn still runs. 3 was verified on claude 2.1.294
|
|
772
|
+
# on 2026-10-09 (cap 1 exits 1, caps 2 and 3 exit 0 on that
|
|
773
|
+
# pattern). `--max-turns` is not listed in `claude --help` there
|
|
774
|
+
# but is registered (`--max-turns abc` errors "must be a number"),
|
|
775
|
+
# which is why it was exercised rather than read off the help.
|
|
776
|
+
# --dangerously-skip-permissions headless: nothing could answer a prompt.
|
|
777
|
+
# --output-format text
|
|
778
|
+
# DEVFLOW_BG_UPDATER=1 the recursion guard stays on both forms, though --safe-mode
|
|
779
|
+
# also disables hooks. CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 and
|
|
780
|
+
# CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 keep the instruction files and
|
|
781
|
+
# auto-memory out on both forms, including the legacy one.
|
|
782
|
+
#
|
|
783
|
+
# Why Write-only is safe: the staged path is empty before every run (the removal and
|
|
784
|
+
# the checked precondition after the lock above), so the one Write is always a create.
|
|
785
|
+
# Write refuses an existing file that has not been read, and that is the whole history
|
|
786
|
+
# of this flag set:
|
|
787
|
+
# a472a490 passed `--tools "Write"` with `--allowedTools` for two memory files;
|
|
788
|
+
# e701cecc widened the tools to `Read,Write,Bash` after counting the silent
|
|
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'`.
|
|
792
|
+
#
|
|
793
|
+
# The version floor is 2.1.286, the lowest CLI on which the whole lean argv was run
|
|
794
|
+
# and its flags, the settings `env` under --safe-mode and the effort override were
|
|
795
|
+
# checked (2.1.286 and 2.1.292 to 2.1.294 behave identically; 2026-10-09, recorded in
|
|
796
|
+
# docs/reference/platform-assumptions.md). The legacy form carries no --effort,
|
|
797
|
+
# --safe-mode, --max-turns, --disable-slash-commands, --strict-mcp-config or
|
|
798
|
+
# --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.
|
|
800
|
+
#
|
|
801
|
+
# The turns input has no byte cap: the queue holds every turn whole by design, and the
|
|
802
|
+
# prompt is bounded by the newest-20-rows window above and the 64 KB existing-memory
|
|
803
|
+
# cap, not by cutting a turn.
|
|
804
|
+
unset CLAUDE_CODE_EFFORT_LEVEL
|
|
509
805
|
set -m
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
806
|
+
if [ "$CLAUDE_ARGV_FORM" = "lean" ]; then
|
|
807
|
+
DEVFLOW_BG_UPDATER=1 CLAUDE_CODE_DISABLE_CLAUDE_MDS=1 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 "$CLAUDE_BIN" -p \
|
|
808
|
+
--model "$MEMORY_MODEL" \
|
|
809
|
+
--effort "$MEMORY_EFFORT" \
|
|
810
|
+
--safe-mode \
|
|
811
|
+
--tools Write \
|
|
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 &
|
|
828
|
+
fi
|
|
517
829
|
CLAUDE_PID=$!
|
|
518
830
|
set +m
|
|
519
831
|
|
|
@@ -572,13 +884,28 @@ fi
|
|
|
572
884
|
# Each state is assigned exactly once at the point it is decided (single OUTCOME
|
|
573
885
|
# variable, three states).
|
|
574
886
|
verify_and_swap() {
|
|
575
|
-
|
|
887
|
+
# A symbolic link at the staged path counts as no staged file (D-HOOKS-NO-SYMLINK,
|
|
888
|
+
# git-marker): `head` below would read the file it names, and the swap's `mv`
|
|
889
|
+
# would rename the link itself into the working memory's place. The worker
|
|
890
|
+
# removed that path before the LLM run, so a link there was planted during it.
|
|
891
|
+
# df_file_below checks it before both, and logs the refusal once.
|
|
892
|
+
df_file_below "$PROJECT_ROOT" "$STAGED_FILE" && [ -s "$STAGED_FILE" ] || {
|
|
576
893
|
log "WARN: staged file missing or empty after claude -p run"; OUTCOME="failed"; return; }
|
|
577
894
|
case "$(head -1 "$STAGED_FILE" 2>/dev/null || echo "")" in
|
|
578
895
|
"<!-- memory-head:"*) : ;;
|
|
579
896
|
*) log "WARN: staged file exists but stamp missing on line 1 — treating as failure"
|
|
580
897
|
rm -f "$STAGED_FILE" 2>/dev/null || true; OUTCOME="failed"; return ;;
|
|
581
898
|
esac
|
|
899
|
+
# A symbolic link on the working memory's path refuses the swap
|
|
900
|
+
# (D-HOOKS-NO-SYMLINK, git-marker): the run began without one, so a link there now
|
|
901
|
+
# is a change made during the run, and `mv` would move the staged memory into a
|
|
902
|
+
# folder it names. When the run began with no working memory, the cksum
|
|
903
|
+
# comparison below would take the link for that absent file.
|
|
904
|
+
if ! df_no_symlink_below "$PROJECT_ROOT" "$MEMORY_FILE"; then
|
|
905
|
+
rm -f "$STAGED_FILE" 2>/dev/null || true
|
|
906
|
+
log "CONFLICT: a symbolic link appeared on the path to $MEMORY_FILE during the run — not swapped, leaving .processing for retry"
|
|
907
|
+
OUTCOME="conflict"; return
|
|
908
|
+
fi
|
|
582
909
|
# Re-check real file cksum before swapping.
|
|
583
910
|
# Residual TOCTOU: a human edit landing in the O(ms) window between this
|
|
584
911
|
# cksum re-check and the mv would be clobbered; this is accepted — the
|
|
@@ -613,14 +940,26 @@ verify_and_swap() {
|
|
|
613
940
|
OUTCOME="failed"
|
|
614
941
|
verify_and_swap
|
|
615
942
|
|
|
943
|
+
# Each stamp below is a `touch`, which follows a symbolic link, and its path was
|
|
944
|
+
# checked before the LLM run, which can take minutes. So, like the swap's target,
|
|
945
|
+
# it is checked again just before the touch: a link there now is a change made
|
|
946
|
+
# during the run, and the stamp is skipped and logged (D-HOOKS-NO-SYMLINK, git-marker).
|
|
616
947
|
if [ "$OUTCOME" = "updated" ]; then
|
|
617
948
|
rm -f "$PROCESSING_FILE" 2>/dev/null || true
|
|
618
|
-
|
|
619
|
-
|
|
949
|
+
if df_no_symlink_below "$PROJECT_ROOT" "$OK_FILE"; then
|
|
950
|
+
touch "$OK_FILE" 2>/dev/null || true
|
|
951
|
+
log "SUCCESS: queue drained, .last-refresh-ok touched"
|
|
952
|
+
else
|
|
953
|
+
log "SUCCESS: queue drained; .last-refresh-ok not touched, a symbolic link appeared on its path during the run"
|
|
954
|
+
fi
|
|
620
955
|
elif [ "$OUTCOME" = "conflict" ]; then
|
|
621
956
|
# Heartbeat: this batch is still owned by the retry cycle, not orphaned.
|
|
622
957
|
# Keeps session-start-memory's 300s cold path from reclaiming a live retry.
|
|
623
|
-
|
|
958
|
+
if df_no_symlink_below "$PROJECT_ROOT" "$PROCESSING_FILE"; then
|
|
959
|
+
touch "$PROCESSING_FILE" 2>/dev/null || true
|
|
960
|
+
else
|
|
961
|
+
log "Heartbeat skipped: a symbolic link appeared on the path to $PROCESSING_FILE during the run"
|
|
962
|
+
fi
|
|
624
963
|
log "CONFLICT: queue retained in .processing, .last-refresh-ok untouched"
|
|
625
964
|
else
|
|
626
965
|
# Leave .processing for session-start-memory's D56c cold path; do NOT touch .last-refresh-ok
|
|
@@ -84,11 +84,12 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
|
|
|
84
84
|
source "$SCRIPT_DIR/get-mtime" || { echo "capture-prompt: failed to source get-mtime" >&2; exit 1; }
|
|
85
85
|
source "$SCRIPT_DIR/learning-lock" || { echo "capture-prompt: failed to source learning-lock" >&2; exit 1; }
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
# Each queue is checked from the root it lies below, and its folder created there,
|
|
88
|
+
# by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
|
|
89
89
|
TS=$(date +%s)
|
|
90
90
|
queue_append_both \
|
|
91
|
-
"$
|
|
91
|
+
"$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
|
|
92
|
+
"$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
|
|
92
93
|
"$MEMORY_ENABLED" "$LEARNING_ENABLED" \
|
|
93
94
|
"user" "$PROMPT" "$TS"
|
|
94
95
|
|
|
@@ -140,8 +140,8 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
|
|
|
140
140
|
source "$SCRIPT_DIR/get-mtime" || { echo "capture-question: failed to source get-mtime" >&2; exit 1; }
|
|
141
141
|
source "$SCRIPT_DIR/learning-lock" || { echo "capture-question: failed to source learning-lock" >&2; exit 1; }
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
# Each queue is checked from the root it lies below, and its folder created there,
|
|
144
|
+
# by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
|
|
145
145
|
_QUESTION_COUNT=0
|
|
146
146
|
while IFS=$'\t' read -r Q A; do
|
|
147
147
|
[ -z "$Q" ] && continue
|
|
@@ -150,7 +150,8 @@ while IFS=$'\t' read -r Q A; do
|
|
|
150
150
|
A: ${A}"
|
|
151
151
|
TS=$(date +%s)
|
|
152
152
|
queue_append_both \
|
|
153
|
-
"$
|
|
153
|
+
"$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
|
|
154
|
+
"$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
|
|
154
155
|
"$MEMORY_ENABLED" "$LEARNING_ENABLED" \
|
|
155
156
|
"qa" "$CONTENT" "$TS"
|
|
156
157
|
_QUESTION_COUNT=$(( _QUESTION_COUNT + 1 ))
|
|
@@ -90,11 +90,12 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
|
|
|
90
90
|
source "$SCRIPT_DIR/get-mtime" || { echo "capture-turn: failed to source get-mtime" >&2; exit 1; }
|
|
91
91
|
source "$SCRIPT_DIR/learning-lock" || { echo "capture-turn: failed to source learning-lock" >&2; exit 1; }
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
# Each queue is checked from the root it lies below, and its folder created there,
|
|
94
|
+
# by queue_append_row (D-HOOKS-NO-SYMLINK, git-marker).
|
|
95
95
|
TS=$(date +%s)
|
|
96
96
|
queue_append_both \
|
|
97
|
-
"$
|
|
97
|
+
"$PROJECT_ROOT" "$MEMORY_DIR/.pending-turns.jsonl" \
|
|
98
|
+
"$LEDGER_ROOT" "$LEARNING_DIR/.pending-turns.jsonl" \
|
|
98
99
|
"$MEMORY_ENABLED" "$LEARNING_ENABLED" \
|
|
99
100
|
"assistant" "$ASSISTANT_MSG" "$TS"
|
|
100
101
|
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
# Ensures .devflow/ and all subdirectories exist and the project root .gitignore
|
|
3
3
|
# ignores .devflow/. Called from capture-prompt, capture-turn, capture-question,
|
|
4
4
|
# memory-worker, and pre-compact-memory. Idempotent.
|
|
5
|
-
# Returns non-zero, having written nothing, outside a git project
|
|
5
|
+
# Returns non-zero, having written nothing, outside a git project, at HOME, or when
|
|
6
|
+
# .devflow is a symbolic link.
|
|
6
7
|
# Usage: source ensure-devflow-init "$CWD"
|
|
7
8
|
|
|
8
9
|
[ -z "$1" ] && return 1
|
|
@@ -33,6 +34,17 @@ df_is_project_root "$_EDI_ROOT" 2>/dev/null || return 1
|
|
|
33
34
|
|
|
34
35
|
_DEVFLOW_DIR="$_EDI_ROOT/.devflow"
|
|
35
36
|
|
|
37
|
+
# D-HOOKS-NO-SYMLINK (git-marker): a `.devflow` that is itself a symbolic link
|
|
38
|
+
# would take every folder below and the carve-out marker into the folder it names,
|
|
39
|
+
# so nothing is written and the caller stops, as at any root it may not write to.
|
|
40
|
+
# The skip is logged once, through log() where the caller defines it. A folder below
|
|
41
|
+
# `.devflow` that is a link is left to the write under it: `mkdir -p` creates
|
|
42
|
+
# nothing through a link it ends at.
|
|
43
|
+
if ! df_no_symlink_below "$_EDI_ROOT" "$_DEVFLOW_DIR"; then
|
|
44
|
+
declare -F log >/dev/null 2>&1 && log "Skipped: $_DEVFLOW_DIR is a symbolic link; nothing written under it"
|
|
45
|
+
return 1
|
|
46
|
+
fi
|
|
47
|
+
|
|
36
48
|
# learning/ is scaffolded only where the ledger lives. In a linked worktree whose
|
|
37
49
|
# main checkout holds the ledger (D-LEDGER-MAIN-WORKTREE, resolve-project-root) a
|
|
38
50
|
# learning/ here would be an empty directory nothing writes to; the capture hooks
|