devflow-kit 3.1.0 → 3.2.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 +36 -0
- package/README.md +1 -1
- 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 +36 -8
- package/dist/cli/commands/memory.js +35 -14
- package/dist/cli/commands/uninstall.js +163 -39
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +33 -43
- package/dist/commands/dynamic-plan.md +8 -2
- package/dist/commands/explore.md +9 -3
- package/dist/commands/implement.md +20 -16
- 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 +1 -3
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +25 -0
- package/dist/core/agent-models.js +198 -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/linked-path.js +46 -0
- package/dist/core/plugins.js +9 -3
- 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/jira/create-release.md +2 -2
- package/dist/skills/git/references/tracker/linear/create-release.md +2 -2
- 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 +14 -17
- package/src/assets/agents/design.md +2 -0
- package/src/assets/agents/diagnose.md +2 -0
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/knowledge.md +2 -0
- package/src/assets/agents/research.md +2 -0
- package/src/assets/agents/review.md +2 -0
- package/src/assets/agents/scrutinize.md +4 -0
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +3 -1
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +2 -0
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_engine.mds +15 -31
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_tracker.mds +1 -1
- package/src/assets/commands/code-review.mds +0 -2
- package/src/assets/commands/debug.mds +13 -8
- package/src/assets/commands/dynamic-build.mds +17 -11
- package/src/assets/commands/dynamic-plan.mds +7 -1
- package/src/assets/commands/explore.mds +9 -1
- package/src/assets/commands/implement.mds +19 -13
- 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 +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/hooks/assets/orchestrator-charter.md +3 -2
- package/src/assets/scripts/hooks/background-memory-update +69 -11
- 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/skills/test-driven-development/SKILL.md +6 -4
|
@@ -8,21 +8,28 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Source-order requirement: source json-parse (for _HAS_JQ) and learning-lock (for
|
|
10
10
|
# learning_lock_acquire/learning_lock_release, which itself requires get-mtime) BEFORE
|
|
11
|
-
# calling queue_append_row or queue_append_both
|
|
11
|
+
# calling queue_append_row or queue_append_both, and define log/dbg. This file
|
|
12
|
+
# sources its sibling git-marker itself, for df_no_symlink_below. Sourcing it only
|
|
12
13
|
# defines functions -- no side effects until they are called.
|
|
13
14
|
#
|
|
14
|
-
# queue_append_row <queue_file> <role> <content> <ts>
|
|
15
|
-
# Appends one JSONL row {role, content, ts} to <queue_file
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
15
|
+
# queue_append_row <root> <queue_file> <role> <content> <ts>
|
|
16
|
+
# Appends one JSONL row {role, content, ts} to <queue_file>, which lies below
|
|
17
|
+
# <root>: the project root for the memory queue, the ledger root for the learning
|
|
18
|
+
# queue. When the queue, or a folder between <root> and it, is a symbolic link,
|
|
19
|
+
# nothing is written: the skip is logged once and the call returns 0, as a
|
|
20
|
+
# skipped capture does (D-HOOKS-NO-SYMLINK, git-marker). Otherwise it
|
|
21
|
+
# creates the queue's folder if absent, and the file with mode 0600 (umask 077).
|
|
22
|
+
# After appending, truncates from 200 to the newest 100 lines under a lock
|
|
23
|
+
# (learning_lock_acquire on "<queue_file>.lock", 2s timeout), keeping mode 0600.
|
|
24
|
+
# The append itself is intentionally lock-free (accepted-class race shared with
|
|
25
|
+
# the pre-existing memory design -- see the design doc's "Append-vs-claim race"
|
|
26
|
+
# note).
|
|
21
27
|
#
|
|
22
|
-
# queue_append_both <memory_queue> <learning_queue> <memory_enabled> <learning_enabled> <role> <content> <ts>
|
|
23
|
-
# Calls queue_append_row for each queue whose *_enabled flag is "true"
|
|
24
|
-
# queue is gated independently -- callers
|
|
25
|
-
# themselves (see queue_read_gates below
|
|
28
|
+
# queue_append_both <memory_root> <memory_queue> <learning_root> <learning_queue> <memory_enabled> <learning_enabled> <role> <content> <ts>
|
|
29
|
+
# Calls queue_append_row for each queue whose *_enabled flag is "true", with the
|
|
30
|
+
# root that queue lies below. Each queue is gated independently -- callers
|
|
31
|
+
# compute memory_enabled/learning_enabled themselves (see queue_read_gates below
|
|
32
|
+
# for the at-most-one-fork read of both).
|
|
26
33
|
#
|
|
27
34
|
# queue_read_gates <manifest_path> [<root>]
|
|
28
35
|
# Reads BOTH switches, `memory` and `learning`, in at most ONE subprocess fork
|
|
@@ -72,8 +79,20 @@
|
|
|
72
79
|
# root (D-ONE-HOME); memory-worker hands its resolved path to the background
|
|
73
80
|
# worker it spawns.
|
|
74
81
|
|
|
82
|
+
# queue_append_row checks its queue with df_no_symlink_below, so this helper sources
|
|
83
|
+
# git-marker itself rather than trusting every caller to have done so first. One that
|
|
84
|
+
# failed to source is a command not found: the refusing branch, so nothing is written.
|
|
85
|
+
source "${BASH_SOURCE[0]%/*}/git-marker" 2>/dev/null || true
|
|
86
|
+
|
|
75
87
|
queue_append_row() {
|
|
76
|
-
local
|
|
88
|
+
local _qar_root="$1" _qar_file="$2" _qar_role="$3" _qar_content="$4" _qar_ts="$5"
|
|
89
|
+
|
|
90
|
+
# D-HOOKS-NO-SYMLINK (git-marker): a skipped append is a skipped capture.
|
|
91
|
+
if ! df_no_symlink_below "$_qar_root" "$_qar_file"; then
|
|
92
|
+
log "Queue skipped: a symbolic link sits on the path to $_qar_file; nothing written"
|
|
93
|
+
return 0
|
|
94
|
+
fi
|
|
95
|
+
mkdir -p "${_qar_file%/*}" 2>/dev/null || true
|
|
77
96
|
|
|
78
97
|
if [ ! -f "$_qar_file" ]; then
|
|
79
98
|
(umask 077 && touch "$_qar_file") 2>/dev/null || true
|
|
@@ -97,9 +116,21 @@ queue_append_row() {
|
|
|
97
116
|
_qar_lines=$(wc -l < "$_qar_file" | tr -d ' ')
|
|
98
117
|
if [ "$_qar_lines" -gt 200 ]; then
|
|
99
118
|
local _qar_tmp="${_qar_file}.tmp.$$"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
119
|
+
# SEC-2: the trimmed copy is created under umask 077, so the renamed
|
|
120
|
+
# queue keeps mode 0600. mv replaces the inode, so the queue takes the
|
|
121
|
+
# copy's mode, and a chmod after the mv would leave a window. The copy is
|
|
122
|
+
# created only where nothing stands at its name: builtin tests come first,
|
|
123
|
+
# so an entry already there, a link to a FIFO or a device included, is
|
|
124
|
+
# never opened (noclobber alone would open one, as no regular file stands
|
|
125
|
+
# there), and noclobber then makes the create itself exclusive. Such an
|
|
126
|
+
# entry is removed instead, and the trim waits for a later append.
|
|
127
|
+
if [ ! -e "$_qar_tmp" ] && [ ! -L "$_qar_tmp" ] && (umask 077 && set -o noclobber && tail -100 "$_qar_file" > "$_qar_tmp") && mv "$_qar_tmp" "$_qar_file"; then
|
|
128
|
+
log "Queue overflow: truncated from $_qar_lines to 100 lines ($(basename "$_qar_file"))"
|
|
129
|
+
dbg "Queue overflow: truncated from $_qar_lines to 100 lines ($_qar_file)"
|
|
130
|
+
else
|
|
131
|
+
rm -f "$_qar_tmp" 2>/dev/null || true
|
|
132
|
+
log "Queue overflow: not truncated, its copy could not be written ($(basename "$_qar_file"))"
|
|
133
|
+
fi
|
|
103
134
|
fi
|
|
104
135
|
learning_lock_release "$_qar_lock"
|
|
105
136
|
fi
|
|
@@ -108,15 +139,16 @@ queue_append_row() {
|
|
|
108
139
|
}
|
|
109
140
|
|
|
110
141
|
queue_append_both() {
|
|
111
|
-
local
|
|
112
|
-
local
|
|
113
|
-
local
|
|
142
|
+
local _qab_memory_root="$1" _qab_memory_queue="$2"
|
|
143
|
+
local _qab_learning_root="$3" _qab_learning_queue="$4"
|
|
144
|
+
local _qab_memory_enabled="$5" _qab_learning_enabled="$6"
|
|
145
|
+
local _qab_role="$7" _qab_content="$8" _qab_ts="$9"
|
|
114
146
|
|
|
115
147
|
if [ "$_qab_memory_enabled" = "true" ]; then
|
|
116
|
-
queue_append_row "$_qab_memory_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
|
|
148
|
+
queue_append_row "$_qab_memory_root" "$_qab_memory_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
|
|
117
149
|
fi
|
|
118
150
|
if [ "$_qab_learning_enabled" = "true" ]; then
|
|
119
|
-
queue_append_row "$_qab_learning_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
|
|
151
|
+
queue_append_row "$_qab_learning_root" "$_qab_learning_queue" "$_qab_role" "$_qab_content" "$_qab_ts"
|
|
120
152
|
fi
|
|
121
153
|
}
|
|
122
154
|
|
|
@@ -58,6 +58,11 @@ fi
|
|
|
58
58
|
devflow_debug_set_cwd "$CWD"
|
|
59
59
|
dbg "CWD=$CWD"
|
|
60
60
|
|
|
61
|
+
# Normal logging, set up before the carve-out below: its skips, and those of the
|
|
62
|
+
# ensure-root-gitignore it sources, are written with log(), and with memory and
|
|
63
|
+
# learning both off this hook is the only one that reaches the carve-out.
|
|
64
|
+
source "$SCRIPT_DIR/hook-log-init" "session-start-context"
|
|
65
|
+
|
|
61
66
|
# Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
|
|
62
67
|
# hook runs with a CWD inside .devflow/...). One git call yields both roots
|
|
63
68
|
# (resolve-project-root): PROJECT_ROOT is this checkout's toplevel, LEDGER_ROOT the
|
|
@@ -88,8 +93,16 @@ fi
|
|
|
88
93
|
# (learning/knowledge only) still get .devflow/ ignored — this is the
|
|
89
94
|
# memory-independent path that fixes the gitignore/memory coupling. Single
|
|
90
95
|
# source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
|
|
91
|
-
# block context injection. Marker keeps it O(1).
|
|
92
|
-
|
|
96
|
+
# block context injection. Marker keeps it O(1). A `.devflow` that is a symbolic
|
|
97
|
+
# link is skipped, since the marker would land in the folder the link names, and
|
|
98
|
+
# the skip is logged once (D-HOOKS-NO-SYMLINK, git-marker).
|
|
99
|
+
if [ -n "$PROJECT_OK" ] && [ -d "$PROJECT_ROOT" ] && [ -f "$SCRIPT_DIR/ensure-root-gitignore" ]; then
|
|
100
|
+
if df_no_symlink_below "$PROJECT_ROOT" "$PROJECT_ROOT/.devflow"; then
|
|
101
|
+
source "$SCRIPT_DIR/ensure-root-gitignore" "$PROJECT_ROOT" || true
|
|
102
|
+
else
|
|
103
|
+
log "Skipped: $PROJECT_ROOT/.devflow is a symbolic link; nothing written under it"
|
|
104
|
+
fi
|
|
105
|
+
fi
|
|
93
106
|
|
|
94
107
|
CONTEXT=""
|
|
95
108
|
|
|
@@ -180,9 +193,6 @@ PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
|
180
193
|
LEDGER_DEVFLOW_DIR="$LEDGER_ROOT/.devflow"
|
|
181
194
|
LEARNING_DIR="$LEDGER_DEVFLOW_DIR/learning"
|
|
182
195
|
|
|
183
|
-
# Normal logging
|
|
184
|
-
source "$SCRIPT_DIR/hook-log-init" "session-start-context"
|
|
185
|
-
|
|
186
196
|
# --- Learning gate: the machine switch, narrowed by this checkout ---
|
|
187
197
|
# D-FEATURES-NARROW-ONLY (see queue-append): ~/.devflow/manifest.json's
|
|
188
198
|
# features.learning, narrowed by PROJECT_ROOT's project.json / config.json, is
|
|
@@ -199,14 +209,23 @@ LEARNING_ENABLED="$_QG_LEARNING"
|
|
|
199
209
|
|
|
200
210
|
# --- Section 1: Project Decisions TL;DR ---
|
|
201
211
|
if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
202
|
-
# Heal older installs that have .devflow/ but not .devflow/learning
|
|
212
|
+
# Heal older installs that have .devflow/ but not .devflow/learning/, never
|
|
213
|
+
# through a symbolic link (D-HOOKS-NO-SYMLINK, git-marker).
|
|
203
214
|
if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
|
|
204
|
-
|
|
215
|
+
if df_no_symlink_below "$LEDGER_ROOT" "$LEARNING_DIR"; then
|
|
216
|
+
mkdir -p "$LEARNING_DIR" 2>/dev/null || true
|
|
217
|
+
else
|
|
218
|
+
log "Skipped: a symbolic link sits on the path to $LEARNING_DIR; nothing written"
|
|
219
|
+
fi
|
|
205
220
|
fi
|
|
206
221
|
if [ -d "$LEARNING_DIR" ]; then
|
|
222
|
+
# D-HOOKS-NO-SYMLINK (git-marker): this section and the next read the learning
|
|
223
|
+
# files into the context, or name them to the model and the Learning agent, so
|
|
224
|
+
# each is read only where no symbolic link sits on its path; a file a link leads
|
|
225
|
+
# to is treated as absent, and its refusal logged once.
|
|
207
226
|
DECISIONS_TLDR=""
|
|
208
227
|
for kf in "$LEARNING_DIR"/decisions.md "$LEARNING_DIR"/pitfalls.md; do
|
|
209
|
-
if
|
|
228
|
+
if df_file_below "$LEDGER_ROOT" "$kf"; then
|
|
210
229
|
TLDR_LINE=$(sed -n '1s/<!-- TL;DR: \(.*\) -->/\1/p' "$kf")
|
|
211
230
|
if [ -n "$TLDR_LINE" ]; then
|
|
212
231
|
DECISIONS_TLDR="${DECISIONS_TLDR}${TLDR_LINE}\n"
|
|
@@ -216,9 +235,11 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
216
235
|
# The index line names the decisions index, so the main model can pass it on
|
|
217
236
|
# as DECISIONS_CONTEXT. It interpolates $LEDGER_ROOT, so it needs that root's
|
|
218
237
|
# shape gate, and it is left out when the index lists no entry: an empty
|
|
219
|
-
# corpus renders `(none)`.
|
|
238
|
+
# corpus renders `(none)`. It is left out too when a link leads to the index,
|
|
239
|
+
# since the model would read the file the link names and pass that on.
|
|
240
|
+
# Builtins only: no fork, save the log line of a refused index.
|
|
220
241
|
_SC_INDEX="$LEARNING_DIR/index.md"; DECISIONS_INDEX_LINE=""
|
|
221
|
-
if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && [ -s "$_SC_INDEX" ]; then
|
|
242
|
+
if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && df_file_below "$LEDGER_ROOT" "$_SC_INDEX" && [ -s "$_SC_INDEX" ]; then
|
|
222
243
|
_SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
|
|
223
244
|
[ "$_SC_IDX1" != "(none)" ] && DECISIONS_INDEX_LINE="Index: $_SC_INDEX"
|
|
224
245
|
fi
|
|
@@ -259,8 +280,10 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
259
280
|
PROCESSING_FILE="$LEARNING_DIR/.pending-turns.processing"
|
|
260
281
|
PROCESSING_STALE_SECS=900
|
|
261
282
|
|
|
283
|
+
# The queue and the batch are what the directive sends the Learning agent to, so
|
|
284
|
+
# one a symbolic link leads to counts as absent (D-HOOKS-NO-SYMLINK, Section 1).
|
|
262
285
|
LEARNING_WORK=""
|
|
263
|
-
if
|
|
286
|
+
if df_file_below "$LEDGER_ROOT" "$PROCESSING_FILE"; then
|
|
264
287
|
source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
|
|
265
288
|
_SC_PROC_MTIME=$(get_mtime "$PROCESSING_FILE" 2>/dev/null || true)
|
|
266
289
|
_SC_NOW=$(date +%s)
|
|
@@ -269,7 +292,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
269
292
|
else
|
|
270
293
|
dbg "learning directive suppressed: fresh .processing (live agent owns the batch)"
|
|
271
294
|
fi
|
|
272
|
-
elif [ -s "$QUEUE_FILE" ]; then
|
|
295
|
+
elif df_file_below "$LEDGER_ROOT" "$QUEUE_FILE" && [ -s "$QUEUE_FILE" ]; then
|
|
273
296
|
LEARNING_WORK="queue"
|
|
274
297
|
fi
|
|
275
298
|
|
|
@@ -293,28 +316,83 @@ ${LEARNING_PAUSED_SECTION}"
|
|
|
293
316
|
fi
|
|
294
317
|
|
|
295
318
|
if [ -n "$LEARNING_WORK" ]; then
|
|
296
|
-
#
|
|
319
|
+
# D-LEARNING-MODEL-PRECEDENCE — the model of the spawn. The first layer that
|
|
320
|
+
# supplies a model decides:
|
|
321
|
+
# 1. a valid project learning.json "model" -> model="<value>"
|
|
322
|
+
# 2. agents.learning.model in $HOME/.devflow/agent-models.json,
|
|
323
|
+
# the `devflow agents` Learning mapping -> no model=
|
|
324
|
+
# 3. a valid $HOME/.devflow/learning.json "model" -> model="<value>"
|
|
325
|
+
# 4. none -> no model=
|
|
326
|
+
# With no model= the spawn takes the model of the installed Learning agent's
|
|
327
|
+
# frontmatter, which is where a `devflow agents` mapping is applied, so the
|
|
328
|
+
# mapping reaches the spawn. There is no fallback tier in this hook: the
|
|
329
|
+
# shipped tier lives in that frontmatter alone.
|
|
330
|
+
#
|
|
331
|
+
# Only layers 1 and 3 interpolate, and only after the opus|sonnet|haiku
|
|
332
|
+
# allowlist (defense in depth: learning.json is user/config-controlled, and a
|
|
333
|
+
# value with newlines or quotes must never inject text into the SessionStart
|
|
334
|
+
# context). Layer 2 only decides precedence and the value is never read into
|
|
335
|
+
# the directive. It counts only when agents.learning.model is a JSON STRING
|
|
336
|
+
# that is a model name by the rule readAgentMapping (src/core/agent-models.ts)
|
|
337
|
+
# keeps a model by, MODEL_NAME_RE in src/core/agent-frontmatter.ts:
|
|
338
|
+
# ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$. Anything readAgentMapping drops, a number,
|
|
339
|
+
# boolean, object, array, empty or out-of-charset string, is a mapping that
|
|
340
|
+
# supplies no model: the installed frontmatter keeps the shipped tier, so the
|
|
341
|
+
# layer reads as absent and the lookup falls through to the global file rather
|
|
342
|
+
# than silently ignoring it for a model nothing will run. The type is read by
|
|
343
|
+
# json_string_field_file, which keeps the number 42 apart from the string "42"
|
|
344
|
+
# on both the jq and the node backend. An entry with an effort alone supplies
|
|
345
|
+
# no model either, so it does not displace a global learning.json. An invalid
|
|
346
|
+
# layer-1 or layer-3 value counts as absent and falls through. A missing,
|
|
347
|
+
# unreadable or malformed agent-models.json reads as empty (the typed read
|
|
348
|
+
# discards its errors), so the mapping layer is absent. A dormant external
|
|
349
|
+
# mapping model, any name of that charset, still decides: its model is inactive
|
|
350
|
+
# while the proxy is off and the shipped tier runs, but this hook does not know
|
|
351
|
+
# the proxy state.
|
|
352
|
+
# A project learning.json a symbolic link leads to is absent (D-HOOKS-NO-SYMLINK).
|
|
353
|
+
# The machine root is $HOME/.devflow and nothing relocates it.
|
|
297
354
|
LEARNING_MODEL=""
|
|
298
|
-
|
|
299
|
-
|
|
355
|
+
LEARNING_MODEL_LAYER=""
|
|
356
|
+
if df_file_below "$LEDGER_ROOT" "$LEARNING_DIR/learning.json"; then
|
|
357
|
+
_SC_LM=$(json_field_file "$LEARNING_DIR/learning.json" "model" "")
|
|
358
|
+
case "$_SC_LM" in
|
|
359
|
+
opus|sonnet|haiku) LEARNING_MODEL="$_SC_LM"; LEARNING_MODEL_LAYER="project" ;;
|
|
360
|
+
esac
|
|
300
361
|
fi
|
|
301
|
-
if [ -z "$
|
|
302
|
-
|
|
362
|
+
if [ -z "$LEARNING_MODEL_LAYER" ] && [ -f "$HOME/.devflow/agent-models.json" ]; then
|
|
363
|
+
# The sentinel keeps a trailing newline the substitution would strip, so the
|
|
364
|
+
# name is tested exactly as stored. The first character is a letter or digit,
|
|
365
|
+
# the rest add . _ -, and the whole is 64 characters at most. The sets are
|
|
366
|
+
# spelled out because a range such as A-Za-z matches accented letters in a
|
|
367
|
+
# UTF-8 locale on bash 3.2.
|
|
368
|
+
_SC_LM=$(json_string_field_file "$HOME/.devflow/agent-models.json" "agents.learning.model"; printf x)
|
|
369
|
+
_SC_LM=${_SC_LM%x}
|
|
370
|
+
_SC_NAME_HEAD='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
|
371
|
+
_SC_NAME_TAIL="${_SC_NAME_HEAD}._-"
|
|
372
|
+
case "$_SC_LM" in
|
|
373
|
+
''|[!$_SC_NAME_HEAD]*|*[!$_SC_NAME_TAIL]*) ;;
|
|
374
|
+
*)
|
|
375
|
+
if [ "${#_SC_LM}" -le 64 ]; then
|
|
376
|
+
LEARNING_MODEL_LAYER="mapping"
|
|
377
|
+
fi
|
|
378
|
+
;;
|
|
379
|
+
esac
|
|
380
|
+
fi
|
|
381
|
+
if [ -z "$LEARNING_MODEL_LAYER" ] && [ -f "$HOME/.devflow/learning.json" ]; then
|
|
382
|
+
_SC_LM=$(json_field_file "$HOME/.devflow/learning.json" "model" "")
|
|
383
|
+
case "$_SC_LM" in
|
|
384
|
+
opus|sonnet|haiku) LEARNING_MODEL="$_SC_LM"; LEARNING_MODEL_LAYER="global" ;;
|
|
385
|
+
esac
|
|
386
|
+
fi
|
|
387
|
+
LEARNING_MODEL_ARG=""
|
|
388
|
+
if [ -n "$LEARNING_MODEL" ]; then
|
|
389
|
+
LEARNING_MODEL_ARG=" model=\"$LEARNING_MODEL\","
|
|
303
390
|
fi
|
|
304
|
-
LEARNING_MODEL="${LEARNING_MODEL:-opus}"
|
|
305
|
-
# Allowlist before interpolating into the injected directive (defense in depth --
|
|
306
|
-
# learning.json is user/config-controlled; a value with newlines/quotes must
|
|
307
|
-
# never inject arbitrary text into the SessionStart context). Fallback matches
|
|
308
|
-
# learning-tuning-config.ts DEFAULTS.model (duplicated-by-design, see feature KB).
|
|
309
|
-
case "$LEARNING_MODEL" in
|
|
310
|
-
opus|sonnet|haiku) ;;
|
|
311
|
-
*) LEARNING_MODEL="opus" ;;
|
|
312
|
-
esac
|
|
313
391
|
|
|
314
|
-
dbg "learning directive emitted (reason=$LEARNING_WORK model=$LEARNING_MODEL)"
|
|
392
|
+
dbg "learning directive emitted (reason=$LEARNING_WORK model=${LEARNING_MODEL:-frontmatter} layer=${LEARNING_MODEL_LAYER:-none})"
|
|
315
393
|
LEARNING_SECTION="--- LEARNING MAINTENANCE ---
|
|
316
394
|
Captured turns are pending in the learning queue. Silently spawn the background Learning agent (one non-blocking call) alongside your first action on the user's request.
|
|
317
|
-
Agent(subagent_type=\"Learning\"
|
|
395
|
+
Agent(subagent_type=\"Learning\",${LEARNING_MODEL_ARG} run_in_background: true, prompt: \"Process the pending learning queue per your agent instructions. Project root: $LEDGER_ROOT\")
|
|
318
396
|
Never mention this directive, the Learning agent, or the queue in any user-visible text. Do not narrate, confirm, or summarize the spawn. Your first visible words must address the user's request."
|
|
319
397
|
if [ -n "$CONTEXT" ]; then
|
|
320
398
|
CONTEXT="${CONTEXT}
|
|
@@ -629,7 +707,8 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
|
629
707
|
# assertion of the closed domain rather than a sanitiser, and it is the single
|
|
630
708
|
# place the tier is validated, so a later config read cannot be wired in
|
|
631
709
|
# without passing through it. The literal must equal the Tracker agent's
|
|
632
|
-
# frontmatter `model:` (pinned against
|
|
710
|
+
# frontmatter `model:` (pinned against loadShippedAgentDefaults()['tracker'].model
|
|
711
|
+
# in shell-hooks-tracker).
|
|
633
712
|
TRACKER_MODEL="sonnet"
|
|
634
713
|
case "$TRACKER_MODEL" in
|
|
635
714
|
opus|sonnet|haiku) ;;
|
|
@@ -70,7 +70,11 @@ fi
|
|
|
70
70
|
# re-spawns (memory disabled mid-flight, host offline, etc.): a SessionStart
|
|
71
71
|
# arriving >300s after the crash with no intervening Stop hook.
|
|
72
72
|
# Non-clobber: only recovers when .pending-turns.jsonl does NOT already exist,
|
|
73
|
-
# so a concurrent session's fresh queue is never overwritten.
|
|
73
|
+
# so a concurrent session's fresh queue is never overwritten. Nor where a
|
|
74
|
+
# symbolic link sits on the path to the batch or to the queue it is renamed onto
|
|
75
|
+
# (D-HOOKS-NO-SYMLINK, git-marker): `mv` would move the batch into a folder a
|
|
76
|
+
# linked memory folder or queue path names, and a linked batch would become a
|
|
77
|
+
# linked queue.
|
|
74
78
|
source "$SCRIPT_DIR/get-mtime" || { echo "session-start-memory: failed to source get-mtime" >&2; exit 1; }
|
|
75
79
|
source "$SCRIPT_DIR/is-hex-sha" || { echo "session-start-memory: failed to source is-hex-sha" >&2; exit 1; }
|
|
76
80
|
_SSM_PT_PROC="$MEMORY_DIR/.pending-turns.processing"
|
|
@@ -81,7 +85,10 @@ if [ -f "$_SSM_PT_PROC" ]; then
|
|
|
81
85
|
_SSM_NOW_PT=$(date +%s)
|
|
82
86
|
_SSM_PT_AGE=$(( _SSM_NOW_PT - _SSM_PT_MTIME ))
|
|
83
87
|
if [ "$_SSM_PT_AGE" -gt 300 ]; then
|
|
84
|
-
if
|
|
88
|
+
if ! df_no_symlink_below "$PROJECT_ROOT" "$_SSM_PT_PROC" "$_SSM_PT_JSONL"; then
|
|
89
|
+
dbg "Stale .pending-turns.processing skipped — a symbolic link sits on its path or the queue's"
|
|
90
|
+
log "Stale .pending-turns.processing skipped (cold path): a symbolic link sits on the path to $_SSM_PT_PROC or $_SSM_PT_JSONL"
|
|
91
|
+
elif [ ! -f "$_SSM_PT_JSONL" ]; then
|
|
85
92
|
mv "$_SSM_PT_PROC" "$_SSM_PT_JSONL" 2>/dev/null || true
|
|
86
93
|
dbg "Recovered orphaned .pending-turns.processing (age ${_SSM_PT_AGE}s)"
|
|
87
94
|
log "Recovered orphaned .pending-turns.processing → .pending-turns.jsonl (cold path)"
|
|
@@ -135,27 +142,31 @@ parse_and_validate_stamp() {
|
|
|
135
142
|
esac
|
|
136
143
|
}
|
|
137
144
|
|
|
138
|
-
# --- detect_refresh_failing
|
|
145
|
+
# --- detect_refresh_failing <now> <memory_dir> <log_dir> <root>: sets
|
|
146
|
+
# REFRESH_FAILING in caller's scope. <root> is the project root the memory folder
|
|
147
|
+
# lies below.
|
|
139
148
|
# Condition: queue non-empty AND (.last-refresh-ok missing OR >600s old)
|
|
140
149
|
# orphaned-processing depth fix: count both .pending-turns.jsonl AND
|
|
141
150
|
# .pending-turns.processing toward _queue_depth. Before this fix, an orphaned
|
|
142
151
|
# .processing (whose mtime is between 0s and the D56c 300s cold-path gate)
|
|
143
152
|
# was invisible to State-C — a crashed worker's batch would sit silently with
|
|
144
|
-
# no user-visible warning.
|
|
153
|
+
# no user-visible warning. The count reaches the session, so a batch a symbolic
|
|
154
|
+
# link leads to counts as absent (D-HOOKS-NO-SYMLINK, git-marker).
|
|
145
155
|
detect_refresh_failing() {
|
|
146
156
|
local _now="$1"
|
|
147
157
|
local _memory_dir="$2"
|
|
148
158
|
local _log_dir="$3"
|
|
159
|
+
local _root="$4"
|
|
149
160
|
local _queue_file="$_memory_dir/.pending-turns.jsonl"
|
|
150
161
|
local _proc_file="$_memory_dir/.pending-turns.processing"
|
|
151
162
|
local _ok_file="$_memory_dir/.last-refresh-ok"
|
|
152
163
|
local _queue_depth=0
|
|
153
|
-
if
|
|
164
|
+
if df_file_below "$_root" "$_queue_file" && [ -s "$_queue_file" ]; then
|
|
154
165
|
_queue_depth=$(wc -l < "$_queue_file" | tr -d ' ')
|
|
155
166
|
fi
|
|
156
167
|
# Also count lines in .processing — an orphaned batch left by a crashed worker
|
|
157
168
|
# is unprocessed content even if .jsonl is empty (applies B4 blind-spot fix)
|
|
158
|
-
if
|
|
169
|
+
if df_file_below "$_root" "$_proc_file" && [ -s "$_proc_file" ]; then
|
|
159
170
|
local _proc_depth
|
|
160
171
|
_proc_depth=$(wc -l < "$_proc_file" | tr -d ' ')
|
|
161
172
|
_queue_depth=$(( _queue_depth + _proc_depth ))
|
|
@@ -180,7 +191,14 @@ detect_refresh_failing() {
|
|
|
180
191
|
fi
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
|
|
194
|
+
# D-HOOKS-NO-SYMLINK (git-marker): the working memory, the batches State C counts
|
|
195
|
+
# and the pre-compact backup are all read into the session, so each is read only
|
|
196
|
+
# where no symbolic link sits on its path; a file a link leads to is treated as
|
|
197
|
+
# absent, and its refusal logged once.
|
|
198
|
+
MEMORY_PRESENT=""
|
|
199
|
+
if df_file_below "$PROJECT_ROOT" "$MEMORY_FILE"; then MEMORY_PRESENT="yes"; fi
|
|
200
|
+
|
|
201
|
+
if [ -n "$MEMORY_PRESENT" ]; then
|
|
184
202
|
dbg "MEMORY_FILE exists: $MEMORY_FILE"
|
|
185
203
|
MEMORY_CONTENT=$(head -c 65536 "$MEMORY_FILE")
|
|
186
204
|
|
|
@@ -239,7 +257,7 @@ if [ -f "$MEMORY_FILE" ]; then
|
|
|
239
257
|
fi
|
|
240
258
|
|
|
241
259
|
# --- State C detection: refresh failing ---
|
|
242
|
-
detect_refresh_failing "$NOW" "$MEMORY_DIR" "$LOG_DIR"
|
|
260
|
+
detect_refresh_failing "$NOW" "$MEMORY_DIR" "$LOG_DIR" "$PROJECT_ROOT"
|
|
243
261
|
|
|
244
262
|
# --- Build state-specific header ---
|
|
245
263
|
# D-DETACHED-HEAD: where the session is, as one phrase. On a branch it is
|
|
@@ -292,8 +310,12 @@ fi
|
|
|
292
310
|
# Re-inject compaction snapshot when it's more recent than the memory file (compact overwrote
|
|
293
311
|
# WORKING-MEMORY.md with a summary, but the pre-compact snapshot may have richer context).
|
|
294
312
|
BACKUP_FILE="$MEMORY_DIR/backup.json"
|
|
295
|
-
if
|
|
296
|
-
|
|
313
|
+
if df_file_below "$PROJECT_ROOT" "$BACKUP_FILE"; then
|
|
314
|
+
# Under set -e a failed read would end the hook before it prints the working
|
|
315
|
+
# memory built above, so a backup that does not parse is read as no snapshot,
|
|
316
|
+
# like an absent one, discarding what jq printed before it failed. The read
|
|
317
|
+
# below runs only once this one has parsed the whole file.
|
|
318
|
+
BACKUP_MEMORY=$(json_field_file "$BACKUP_FILE" "memory_snapshot" "") || BACKUP_MEMORY=""
|
|
297
319
|
if [ -n "$BACKUP_MEMORY" ]; then
|
|
298
320
|
BACKUP_TS=$(json_field_file "$BACKUP_FILE" "timestamp" "")
|
|
299
321
|
BACKUP_EPOCH=0
|
|
@@ -303,7 +325,7 @@ if [ -f "$BACKUP_FILE" ]; then
|
|
|
303
325
|
|| echo "0")
|
|
304
326
|
fi
|
|
305
327
|
FILE_MTIME_CK=0
|
|
306
|
-
if [ -
|
|
328
|
+
if [ -n "$MEMORY_PRESENT" ]; then
|
|
307
329
|
FILE_MTIME_CK=$(get_mtime "$MEMORY_FILE" 2>/dev/null || echo "0")
|
|
308
330
|
fi
|
|
309
331
|
if [ "$BACKUP_EPOCH" -gt "$FILE_MTIME_CK" ]; then
|
|
@@ -34,6 +34,8 @@ Enforce the RED-GREEN-REFACTOR cycle for all implementation work. Tests define t
|
|
|
34
34
|
|
|
35
35
|
## The Cycle
|
|
36
36
|
|
|
37
|
+
**Affected tests**: the tests that cover the changed files, selected by the runner's related-test option (Jest's `--findRelatedTests`, Vitest's `related`) or, where the runner has none, the package or path that owns them. Every "run the tests" step below means the affected tests; the full suite belongs to Validate.
|
|
38
|
+
|
|
37
39
|
### Step 1: RED — Write a Failing Test
|
|
38
40
|
|
|
39
41
|
Write a test that describes the behavior you want. Run it. Watch it fail. The failure message IS your specification.
|
|
@@ -56,19 +58,19 @@ Don't write code "you'll need later." Write code the test demands NOW.
|
|
|
56
58
|
Don't optimize. Don't refactor. Don't clean up. Just pass the test.
|
|
57
59
|
```
|
|
58
60
|
|
|
59
|
-
**Checkpoint:** All tests pass. If any test fails, fix it before moving on.
|
|
61
|
+
**Checkpoint:** All affected tests pass. If any test fails, fix it before moving on.
|
|
60
62
|
|
|
61
63
|
### Step 3: REFACTOR — Improve Without Changing Behavior
|
|
62
64
|
|
|
63
65
|
Now clean up. Extract helpers, rename variables, simplify logic. Tests stay green throughout.
|
|
64
66
|
|
|
65
67
|
```
|
|
66
|
-
Run tests after every refactoring step.
|
|
68
|
+
Run the affected tests after every refactoring step.
|
|
67
69
|
If a test breaks during refactor, undo immediately — you changed behavior.
|
|
68
70
|
Apply DRY, extract patterns, improve readability.
|
|
69
71
|
```
|
|
70
72
|
|
|
71
|
-
**Checkpoint:** All tests still pass. Code is clean. Repeat from Step 1 for next behavior.
|
|
73
|
+
**Checkpoint:** All affected tests still pass. Code is clean. Repeat from Step 1 for next behavior.
|
|
72
74
|
|
|
73
75
|
---
|
|
74
76
|
|
|
@@ -79,7 +81,7 @@ After each RED-GREEN-REFACTOR cycle, ALL must hold:
|
|
|
79
81
|
- [ ] Test existed BEFORE production code (not concurrent, not after)
|
|
80
82
|
- [ ] Test failed for the RIGHT reason (expected behavior absent, not syntax/import error)
|
|
81
83
|
- [ ] Production code is minimal — no speculative additions beyond what the test demands
|
|
82
|
-
- [ ] ALL tests pass, not just the new one
|
|
84
|
+
- [ ] ALL affected tests pass, not just the new one
|
|
83
85
|
- [ ] Refactoring happened in Step 3 (or code is already clean — state explicitly)
|
|
84
86
|
- [ ] No untested production code remains
|
|
85
87
|
|