devflow-kit 3.0.1 → 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 +49 -0
- package/README.md +1 -1
- package/dist/agents/git.md +2 -2
- package/dist/cli/agents-view/index.js +1 -1
- package/dist/cli/agents-view/render.js +71 -17
- package/dist/cli/agents-view/state.js +42 -16
- package/dist/cli/agents-view/terminal.js +5 -5
- package/dist/cli/commands/agents.js +142 -51
- package/dist/cli/commands/ambient.js +1 -1
- package/dist/cli/commands/attribution-prompts.js +8 -8
- package/dist/cli/commands/capture.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +8 -8
- package/dist/cli/commands/compliance.js +8 -7
- package/dist/cli/commands/flags.js +33 -31
- package/dist/cli/commands/hud.js +1 -1
- package/dist/cli/commands/init-seed.js +9 -9
- package/dist/cli/commands/init.js +162 -85
- package/dist/cli/commands/install-report.js +10 -10
- package/dist/cli/commands/learning.js +302 -136
- package/dist/cli/commands/memory.js +36 -15
- package/dist/cli/commands/proxy.js +23 -23
- package/dist/cli/commands/rules.js +6 -5
- package/dist/cli/commands/tracker-prompts.js +6 -6
- package/dist/cli/commands/tracker.js +9 -9
- package/dist/cli/commands/uninstall.js +183 -59
- package/dist/cli/flags-view/render.js +5 -5
- package/dist/cli/flags-view/state.js +9 -9
- package/dist/cli/flags-view/terminal.js +4 -4
- package/dist/cli/tui/cells.js +1 -1
- package/dist/cli/tui/terminal.js +6 -6
- package/dist/commands/code-review.md +0 -2
- package/dist/commands/debug.md +14 -11
- package/dist/commands/dynamic-build.md +51 -47
- package/dist/commands/dynamic-plan.md +27 -7
- package/dist/commands/dynamic-profile.md +17 -3
- package/dist/commands/dynamic-tickets.md +18 -4
- 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 +23 -3
- package/dist/commands/research.md +9 -3
- package/dist/commands/resolve.md +9 -12
- package/dist/commands/self-review.md +0 -2
- package/dist/core/agent-frontmatter.js +28 -3
- package/dist/core/agent-models.js +204 -42
- package/dist/core/agent-state.js +28 -6
- package/dist/core/ansi.js +2 -2
- package/dist/core/assets.js +1 -1
- package/dist/core/cache.js +7 -8
- package/dist/core/codex-auth-inspect.js +4 -4
- package/dist/core/compliance-compose.js +3 -3
- package/dist/core/compliance.js +3 -4
- package/dist/core/evidence-policy.js +14 -13
- package/dist/core/external-models.js +1 -1
- package/dist/core/feature-config.js +71 -13
- package/dist/core/feature-switch.js +3 -3
- package/dist/core/flags.js +49 -25
- package/dist/core/fs-atomic.js +6 -7
- package/dist/core/learning-queue-cleanup.js +16 -81
- package/dist/core/learning-store.js +61 -0
- package/dist/core/linked-path.js +46 -0
- package/dist/core/manifest.js +5 -5
- package/dist/core/mds-variants.js +13 -13
- package/dist/core/model-discovery.js +8 -8
- package/dist/core/observations.js +17 -101
- package/dist/core/orphan-sweep.js +4 -4
- package/dist/core/plugins.js +13 -8
- package/dist/core/project-paths.js +9 -13
- package/dist/core/proxy-log.js +8 -8
- package/dist/core/proxy-state.js +3 -3
- package/dist/core/queue-drain.js +31 -0
- package/dist/core/reference-sweep.js +6 -6
- package/dist/core/teammate-mode-cleanup.js +1 -1
- package/dist/core/tracker.js +14 -14
- package/dist/hud/colors.js +2 -2
- package/dist/hud/components/learning-counts.js +54 -22
- package/dist/hud/components/version-badge.js +1 -1
- package/dist/skills/git/references/pr/resolve-review-threads.md +2 -2
- 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/compliance-install.js +17 -15
- package/dist/targets/claude-code/hooks.js +2 -2
- package/dist/targets/claude-code/installer.js +59 -32
- package/dist/targets/claude-code/legacy.js +1 -1
- package/dist/targets/claude-code/post-install.js +135 -45
- package/dist/targets/claude-code/tracker-install.js +2 -2
- package/package.json +1 -1
- package/src/assets/agents/code.md +15 -21
- package/src/assets/agents/design.md +4 -2
- package/src/assets/agents/diagnose.md +3 -1
- package/src/assets/agents/evaluate.md +4 -0
- package/src/assets/agents/git.mds +2 -2
- package/src/assets/agents/knowledge.md +5 -3
- package/src/assets/agents/learning.md +281 -196
- package/src/assets/agents/research.md +3 -1
- package/src/assets/agents/review.md +5 -3
- package/src/assets/agents/scrutinize.md +5 -1
- package/src/assets/agents/simplify.md +4 -0
- package/src/assets/agents/skim.md +4 -2
- package/src/assets/agents/synthesize.md +6 -0
- package/src/assets/agents/test.md +18 -10
- package/src/assets/agents/triage.md +11 -9
- package/src/assets/agents/validate.md +14 -10
- package/src/assets/commands/_partials/_decisions.mds +8 -3
- package/src/assets/commands/_partials/_docs_root.mds +3 -3
- package/src/assets/commands/_partials/_engine.mds +16 -32
- package/src/assets/commands/_partials/_knowledge.mds +0 -2
- package/src/assets/commands/_partials/_preamble.mds +6 -2
- package/src/assets/commands/_partials/_settings.mds +2 -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 +18 -12
- package/src/assets/commands/dynamic-plan.mds +10 -4
- package/src/assets/commands/dynamic-profile.mds +1 -1
- package/src/assets/commands/dynamic-tickets.mds +2 -2
- 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 +23 -3
- package/src/assets/commands/research.mds +9 -3
- package/src/assets/commands/resolve.mds +9 -10
- package/src/assets/mds/git/_pr.mds +3 -3
- package/src/assets/mds/tracker/_common.mds +1 -1
- package/src/assets/mds/tracker/_github.mds +3 -3
- package/src/assets/mds/tracker/_jira.mds +3 -3
- package/src/assets/mds/tracker/_linear.mds +3 -3
- package/src/assets/mds/tracker/_mcp.mds +6 -5
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +4 -2
- package/src/assets/scripts/hooks/background-memory-update +97 -33
- 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 +5 -20
- package/src/assets/scripts/hooks/ensure-devflow-init +14 -2
- package/src/assets/scripts/hooks/ensure-proxy +5 -6
- package/src/assets/scripts/hooks/ensure-root-gitignore +123 -11
- package/src/assets/scripts/hooks/git-marker +71 -0
- package/src/assets/scripts/hooks/is-hex-sha +1 -1
- package/src/assets/scripts/hooks/json-helper.cjs +345 -944
- package/src/assets/scripts/hooks/json-parse +25 -129
- package/src/assets/scripts/hooks/lib/decisions-format.cjs +205 -156
- package/src/assets/scripts/hooks/lib/learning-store.cjs +3207 -0
- package/src/assets/scripts/hooks/lib/mkdir-lock.cjs +7 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +13 -19
- package/src/assets/scripts/hooks/lib/render-decisions.cjs +253 -226
- 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 +55 -23
- package/src/assets/scripts/hooks/resolve-project-root +3 -4
- package/src/assets/scripts/hooks/session-start-context +146 -45
- package/src/assets/scripts/hooks/session-start-memory +33 -11
- package/src/assets/scripts/lib/project-config.cjs +2 -2
- package/src/assets/scripts/pr-evidence.cjs +3 -3
- package/src/assets/scripts/redact-secrets.cjs +20 -20
- package/src/assets/scripts/release-trace.cjs +1 -1
- package/src/assets/scripts/resolve-evidence-policy.cjs +3 -3
- package/src/assets/scripts/resolve-settings.cjs +3 -3
- package/src/assets/scripts/verify-evidence.cjs +2 -2
- package/src/assets/skills/apply-decisions/SKILL.md +37 -17
- package/src/assets/skills/docs-framework/SKILL.md +2 -2
- package/src/assets/skills/feature-knowledge/SKILL.md +6 -5
- package/src/assets/skills/test-driven-development/SKILL.md +6 -4
- package/dist/core/observation-io.js +0 -50
- package/src/assets/scripts/hooks/decisions-usage-scan.cjs +0 -131
|
@@ -66,6 +66,16 @@ fi
|
|
|
66
66
|
# Auto-create .devflow/ and ensure .gitignore entries (idempotent after first run)
|
|
67
67
|
source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
|
|
68
68
|
|
|
69
|
+
# D-HOOKS-NO-SYMLINK (git-marker): every write below lands in the memory folder,
|
|
70
|
+
# so a linked folder, which would take the backup and the working memory into the
|
|
71
|
+
# folder it names, ends the hook here. The backup's own target is checked where the
|
|
72
|
+
# backup is renamed onto it.
|
|
73
|
+
if ! df_no_symlink_below "$PROJECT_ROOT" "$MEMORY_DIR"; then
|
|
74
|
+
log "SKIP: a symbolic link sits on the path to $MEMORY_DIR; no backup or working memory written"
|
|
75
|
+
dbg "EXIT: symbolic link on the memory folder path"
|
|
76
|
+
exit 0
|
|
77
|
+
fi
|
|
78
|
+
|
|
69
79
|
BACKUP_FILE="$MEMORY_DIR/backup.json"
|
|
70
80
|
|
|
71
81
|
# Capture git state
|
|
@@ -102,25 +112,60 @@ if cd "$CWD" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
|
|
|
102
112
|
dbg "GIT_BRANCH=$GIT_BRANCH HEAD=$GIT_HEAD_SHA"
|
|
103
113
|
fi
|
|
104
114
|
|
|
105
|
-
# Snapshot current WORKING-MEMORY.md (preserves session context through compaction)
|
|
115
|
+
# Snapshot current WORKING-MEMORY.md (preserves session context through compaction).
|
|
116
|
+
# The snapshot goes into the backup the next session start injects, so a working
|
|
117
|
+
# memory a symbolic link leads to is treated as absent (D-HOOKS-NO-SYMLINK).
|
|
106
118
|
MEMORY_SNAPSHOT=""
|
|
107
|
-
if
|
|
119
|
+
if df_file_below "$PROJECT_ROOT" "$MEMORY_DIR/WORKING-MEMORY.md"; then
|
|
108
120
|
MEMORY_SNAPSHOT=$(head -c 65536 "$MEMORY_DIR/WORKING-MEMORY.md")
|
|
109
121
|
dbg "MEMORY_SNAPSHOT_LENGTH=${#MEMORY_SNAPSHOT}"
|
|
110
122
|
fi
|
|
111
123
|
|
|
112
124
|
# Write backup JSON
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
# D-BACKUP-RENAME: the backup is written whole into a copy beside it, then
|
|
126
|
+
# renamed over it; it is never truncated and refilled in place. Reason:
|
|
127
|
+
# session-start-memory reads it twice, and a read landing between a truncate
|
|
128
|
+
# and the write that refills it would meet a partial file; a rename hands every
|
|
129
|
+
# read either the previous backup or the new one. SEC-2: the copy is created
|
|
130
|
+
# under umask 077, and only where nothing stands at its name: builtin tests come
|
|
131
|
+
# first, so an entry already there, a link to a FIFO or a device included, is
|
|
132
|
+
# never opened (noclobber alone would open one, as no regular file stands there),
|
|
133
|
+
# and noclobber then makes the create itself exclusive. The rename gives the
|
|
134
|
+
# backup the copy's inode and mode, so the backup, which holds the working
|
|
135
|
+
# memory, is 0600 whatever the caller's umask (as in queue-append). A write or
|
|
136
|
+
# rename that fails removes whatever holds the copy's name and keeps the
|
|
137
|
+
# previous backup; the bootstrap below still runs.
|
|
138
|
+
#
|
|
139
|
+
# The rename's target is checked first (D-HOOKS-NO-SYMLINK, git-marker): `mv`
|
|
140
|
+
# moves the copy into a folder a link at backup.json names, so a link there, to
|
|
141
|
+
# anything, skips the backup with nothing created, logged once, and the
|
|
142
|
+
# bootstrap below still runs.
|
|
143
|
+
#
|
|
144
|
+
# A run killed after creating its copy and before renaming it (by the hook's
|
|
145
|
+
# timeout, say) leaves the copy under its own PID, a name no later run writes,
|
|
146
|
+
# so each run first removes the copies nothing has written to for an hour. A
|
|
147
|
+
# write ends within the hook's 10 s timeout, so no live run's copy is that old
|
|
148
|
+
# and a concurrent run's copy is left alone.
|
|
149
|
+
find "$MEMORY_DIR" -maxdepth 1 -type f -name "${BACKUP_FILE##*/}.tmp.*" -mmin +60 -delete 2>/dev/null || true
|
|
150
|
+
BACKUP_TMP="$BACKUP_FILE.tmp.$$"
|
|
151
|
+
if ! df_no_symlink_below "$PROJECT_ROOT" "$BACKUP_FILE"; then
|
|
152
|
+
log "Backup not written: a symbolic link sits on the path to $BACKUP_FILE"
|
|
153
|
+
dbg "Backup not written: symbolic link at $BACKUP_FILE"
|
|
154
|
+
elif [ ! -e "$BACKUP_TMP" ] && [ ! -L "$BACKUP_TMP" ] && (umask 077 && set -o noclobber && json_backup_construct \
|
|
155
|
+
--arg ts "$TIMESTAMP" \
|
|
156
|
+
--arg branch "$GIT_BRANCH" \
|
|
157
|
+
--arg status "$GIT_STATUS" \
|
|
158
|
+
--arg log "$GIT_LOG" \
|
|
159
|
+
--arg diff "$GIT_DIFF_STAT" \
|
|
160
|
+
--arg memory "$MEMORY_SNAPSHOT" \
|
|
161
|
+
> "$BACKUP_TMP") && mv "$BACKUP_TMP" "$BACKUP_FILE"; then
|
|
162
|
+
log "Wrote backup: $BACKUP_FILE"
|
|
163
|
+
dbg "Wrote backup: $BACKUP_FILE"
|
|
164
|
+
else
|
|
165
|
+
rm -f "$BACKUP_TMP" 2>/dev/null || true
|
|
166
|
+
log "Backup not written; the previous one is kept: $BACKUP_FILE"
|
|
167
|
+
dbg "Backup not written: $BACKUP_FILE"
|
|
168
|
+
fi
|
|
124
169
|
|
|
125
170
|
# Bootstrap minimal WORKING-MEMORY.md if absent; skip on an unborn branch or a
|
|
126
171
|
# malformed SHA. is_hex_sha 40 40: exactly 40 lowercase hex chars required. A detached
|
|
@@ -131,9 +176,16 @@ dbg "Wrote backup: $BACKUP_FILE"
|
|
|
131
176
|
# avoids REL-5: O_EXCL-style atomic create via noclobber so the existence test and the
|
|
132
177
|
# create are one operation — if the worker's CAS mv lands in the window, noclobber fails
|
|
133
178
|
# (file already exists) and we skip the bootstrap rather than truncating fresh memory.
|
|
179
|
+
# D-HOOKS-NO-SYMLINK (git-marker): the create is made only where nothing stands, tested
|
|
180
|
+
# with builtins first, since noclobber alone opens a link to a FIFO or a device (no
|
|
181
|
+
# regular file stands there) and would wait on the FIFO or write the text into the
|
|
182
|
+
# device. A link there is left as it was and logged; the memory folder above it was
|
|
183
|
+
# checked at the top.
|
|
134
184
|
MEMORY_FILE="$MEMORY_DIR/WORKING-MEMORY.md"
|
|
135
185
|
if [ -n "$GIT_BRANCH" ] && is_hex_sha "$GIT_HEAD_SHA" 40 40; then
|
|
136
|
-
if
|
|
186
|
+
if [ -L "$MEMORY_FILE" ]; then
|
|
187
|
+
log "Working memory not bootstrapped: $MEMORY_FILE is a symbolic link"
|
|
188
|
+
elif [ ! -e "$MEMORY_FILE" ] && (set -o noclobber; : > "$MEMORY_FILE") 2>/dev/null; then
|
|
137
189
|
{
|
|
138
190
|
echo "<!-- memory-head: $GIT_HEAD_SHA branch: $GIT_BRANCH -->"
|
|
139
191
|
echo ""
|
|
@@ -62,7 +62,15 @@ if [ -z "$HEAD" ]; then
|
|
|
62
62
|
dbg "EXIT: empty prompt"
|
|
63
63
|
elif [[ "$HEAD" == "Implement the following plan:"* ]]; then
|
|
64
64
|
dbg "PLAN_HANDOFF detected — injecting devflow:implement directive"
|
|
65
|
-
|
|
65
|
+
# D-ARGS-ONCE: the directive passes the Skill call NO arguments. The handoff
|
|
66
|
+
# prompt IS the plan, so it is already in the conversation; handing it to the
|
|
67
|
+
# skill as an argument would put a second copy in the main-thread context for
|
|
68
|
+
# nothing. The skill's empty-input path takes the plan from the conversation.
|
|
69
|
+
# No plan body or plan path is parsed here: detection stays the literal prefix.
|
|
70
|
+
# This string is double-quoted bash: it must hold no dollar sign and no
|
|
71
|
+
# unescaped backtick, and it must equal HANDOFF_TEMPLATE in
|
|
72
|
+
# tests/fixtures/ambient-templates.ts byte for byte.
|
|
73
|
+
json_prompt_output "The user's prompt is a plan handoff (it begins with \`Implement the following plan:\`). In one short sentence, tell the user you're invoking \`devflow:implement\`. Then immediately invoke it with the Skill tool and no arguments: the plan is already in this conversation, so the skill needs no input. Do not pause to ask whether to proceed."
|
|
66
74
|
elif [[ "$HEAD" == "/"* ]]; then
|
|
67
75
|
dbg "EXIT: slash command — no reminder"
|
|
68
76
|
else
|
|
@@ -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
|
|
@@ -42,7 +49,7 @@
|
|
|
42
49
|
# `devflow memory|learning --enable/--disable` write. Only an
|
|
43
50
|
# explicit boolean `false` switches a feature off -- an absent,
|
|
44
51
|
# unreadable or malformed manifest, a missing key, or a non-boolean
|
|
45
|
-
# value leaves it ON (fail-open
|
|
52
|
+
# value leaves it ON (fail-open), the rule
|
|
46
53
|
# isMachineFeatureOn() applies in the CLI.
|
|
47
54
|
# project <root>/.devflow/project.json (team-committed) and
|
|
48
55
|
# personal <root>/.devflow/config.json (this worktree's own), read ONLY under
|
|
@@ -60,7 +67,7 @@
|
|
|
60
67
|
#
|
|
61
68
|
# D-LEARNING-LEGACY-DECISIONS: in the MANIFEST only, learning reads
|
|
62
69
|
# `features.learning` when it is a boolean, else the pre-rename
|
|
63
|
-
# `features.decisions`
|
|
70
|
+
# `features.decisions` -- readManifest's migration precedence
|
|
64
71
|
# exactly, so a legacy `decisions: false` switches learning off before any
|
|
65
72
|
# command has healed the file. Only a boolean `false` in whichever key decides
|
|
66
73
|
# is off, as above.
|
|
@@ -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
|
|
|
@@ -56,10 +56,9 @@ _df_nongit_root() {
|
|
|
56
56
|
# DF_LEDGER_ROOT — where the learning ledger and its queue live.
|
|
57
57
|
#
|
|
58
58
|
# D-LEDGER-MAIN-WORKTREE: a linked worktree (`git worktree add`, `claude
|
|
59
|
-
# --worktree`) has its own toplevel, so a ledger anchored there restarts
|
|
60
|
-
#
|
|
61
|
-
# with main's
|
|
62
|
-
# for good). The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
|
|
59
|
+
# --worktree`) has its own toplevel, so a ledger anchored there restarts its
|
|
60
|
+
# numbering at 001 — main's decisions are invisible in it, and the IDs it mints collide
|
|
61
|
+
# with main's. The ledger is one per REPOSITORY, not per checkout: DF_LEDGER_ROOT
|
|
63
62
|
# is the main worktree — the parent of `--git-common-dir` — when that common dir
|
|
64
63
|
# is a `<dir>/.git` directory AND `<dir>/.devflow` already exists; otherwise it is
|
|
65
64
|
# DF_ROOT. The existence test keeps a main checkout that never ran devflow from
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
# not the home directory (D-HOOKS-GIT-ONLY), and they read the learning ledger of
|
|
9
9
|
# the main worktree when this is a linked one (D-LEDGER-MAIN-WORKTREE).
|
|
10
10
|
#
|
|
11
|
-
# Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines)
|
|
11
|
+
# Section 1: Project decisions TL;DR (decisions.md / pitfalls.md header lines),
|
|
12
|
+
# then the path of the decisions index when it lists entries.
|
|
12
13
|
# Section 2: Learning maintenance directive — when captured turns are pending in
|
|
13
14
|
# the learning queue (or a crashed run left a stale .processing batch), instructs
|
|
14
15
|
# the main model to spawn the background Learning agent with the resolved model.
|
|
@@ -57,6 +58,11 @@ fi
|
|
|
57
58
|
devflow_debug_set_cwd "$CWD"
|
|
58
59
|
dbg "CWD=$CWD"
|
|
59
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
|
+
|
|
60
66
|
# Anchor .devflow/ to the project root (prevents a stray nested .devflow/ when this
|
|
61
67
|
# hook runs with a CWD inside .devflow/...). One git call yields both roots
|
|
62
68
|
# (resolve-project-root): PROJECT_ROOT is this checkout's toplevel, LEDGER_ROOT the
|
|
@@ -85,10 +91,18 @@ fi
|
|
|
85
91
|
# Ensure the project root .gitignore ignores .devflow/ wholesale. This runs on every
|
|
86
92
|
# git-project session regardless of feature toggles, so memory-off projects
|
|
87
93
|
# (learning/knowledge only) still get .devflow/ ignored — this is the
|
|
88
|
-
# memory-independent path that fixes the gitignore/memory coupling
|
|
94
|
+
# memory-independent path that fixes the gitignore/memory coupling. Single
|
|
89
95
|
# source of truth: ensure-root-gitignore. Soft-fail: a gitignore write must never
|
|
90
|
-
# block context injection. Marker keeps it O(1).
|
|
91
|
-
|
|
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
|
|
92
106
|
|
|
93
107
|
CONTEXT=""
|
|
94
108
|
|
|
@@ -101,7 +115,8 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
|
|
|
101
115
|
#
|
|
102
116
|
# Section 2 embeds $LEDGER_ROOT; Section 3 embeds $PROJECT_ROOT and
|
|
103
117
|
# $TRACKER_DEVFLOW_DIR — each inside a double-quoted `prompt: "..."` string the
|
|
104
|
-
# model reads out of additionalContext.
|
|
118
|
+
# model reads out of additionalContext. Section 1's index line embeds
|
|
119
|
+
# $LEDGER_ROOT too, as the path the model reads the decisions index from. The provider and model TOKENS in those
|
|
105
120
|
# directives are admitted by positive allowlists, and so are these: the gate is the
|
|
106
121
|
# POSITIVE shape ^[A-Za-z0-9/._+-]+$, expressed as "rejects if any character falls
|
|
107
122
|
# outside it".
|
|
@@ -134,16 +149,16 @@ TRACKER_DEVFLOW_DIR="$HOME/.devflow"
|
|
|
134
149
|
# admitted set is locale-dependent, and no rule here rests on it.
|
|
135
150
|
#
|
|
136
151
|
# Checked ONCE, here, where every value is resolved and above every section that
|
|
137
|
-
# interpolates them, so no sink can embed a value no gate saw (
|
|
152
|
+
# interpolates them, so no sink can embed a value no gate saw (the
|
|
138
153
|
# invariant belongs at the convergence point all callers pass through, not in
|
|
139
154
|
# whichever section someone remembered). `case` is a shell builtin, so the GitHub
|
|
140
155
|
# path still forks zero times [DR-10]. The empty arm is explicit: an unset root
|
|
141
156
|
# must not read as "no forbidden character, therefore safe".
|
|
142
157
|
#
|
|
143
158
|
# ONE gate per VALUE, applied to the root each section embeds, not one over their
|
|
144
|
-
# concatenation.
|
|
145
|
-
# root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value
|
|
146
|
-
# for
|
|
159
|
+
# concatenation. Sections 1 and 2 interpolate $LEDGER_ROOT alone — the main
|
|
160
|
+
# worktree's root in a linked worktree (D-LEDGER-MAIN-WORKTREE), so it is the value
|
|
161
|
+
# checked for them, not the checkout's own; only Section 3 interpolates
|
|
147
162
|
# $PROJECT_ROOT and $TRACKER_DEVFLOW_DIR. Gating values jointly made a rejected
|
|
148
163
|
# ~/.devflow shape suppress the Learning directive as well — a value Section 2
|
|
149
164
|
# never embeds, silently disabling the whole learning pipeline for any machine
|
|
@@ -178,9 +193,6 @@ PROJECT_DEVFLOW_DIR="$PROJECT_ROOT/.devflow"
|
|
|
178
193
|
LEDGER_DEVFLOW_DIR="$LEDGER_ROOT/.devflow"
|
|
179
194
|
LEARNING_DIR="$LEDGER_DEVFLOW_DIR/learning"
|
|
180
195
|
|
|
181
|
-
# Normal logging
|
|
182
|
-
source "$SCRIPT_DIR/hook-log-init" "session-start-context"
|
|
183
|
-
|
|
184
196
|
# --- Learning gate: the machine switch, narrowed by this checkout ---
|
|
185
197
|
# D-FEATURES-NARROW-ONLY (see queue-append): ~/.devflow/manifest.json's
|
|
186
198
|
# features.learning, narrowed by PROJECT_ROOT's project.json / config.json, is
|
|
@@ -197,24 +209,52 @@ LEARNING_ENABLED="$_QG_LEARNING"
|
|
|
197
209
|
|
|
198
210
|
# --- Section 1: Project Decisions TL;DR ---
|
|
199
211
|
if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
200
|
-
# 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).
|
|
201
214
|
if [ -d "$LEDGER_DEVFLOW_DIR" ] && [ ! -d "$LEARNING_DIR" ]; then
|
|
202
|
-
|
|
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
|
|
203
220
|
fi
|
|
204
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.
|
|
205
226
|
DECISIONS_TLDR=""
|
|
206
227
|
for kf in "$LEARNING_DIR"/decisions.md "$LEARNING_DIR"/pitfalls.md; do
|
|
207
|
-
if
|
|
228
|
+
if df_file_below "$LEDGER_ROOT" "$kf"; then
|
|
208
229
|
TLDR_LINE=$(sed -n '1s/<!-- TL;DR: \(.*\) -->/\1/p' "$kf")
|
|
209
230
|
if [ -n "$TLDR_LINE" ]; then
|
|
210
231
|
DECISIONS_TLDR="${DECISIONS_TLDR}${TLDR_LINE}\n"
|
|
211
232
|
fi
|
|
212
233
|
fi
|
|
213
234
|
done
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
235
|
+
# The index line names the decisions index, so the main model can pass it on
|
|
236
|
+
# as DECISIONS_CONTEXT. It interpolates $LEDGER_ROOT, so it needs that root's
|
|
237
|
+
# shape gate, and it is left out when the index lists no entry: an empty
|
|
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.
|
|
241
|
+
_SC_INDEX="$LEARNING_DIR/index.md"; DECISIONS_INDEX_LINE=""
|
|
242
|
+
if [ -n "$DIRECTIVE_LEDGER_SAFE" ] && df_file_below "$LEDGER_ROOT" "$_SC_INDEX" && [ -s "$_SC_INDEX" ]; then
|
|
243
|
+
_SC_IDX1=""; IFS= read -r _SC_IDX1 < "$_SC_INDEX" || true
|
|
244
|
+
[ "$_SC_IDX1" != "(none)" ] && DECISIONS_INDEX_LINE="Index: $_SC_INDEX"
|
|
245
|
+
fi
|
|
246
|
+
if [ -n "$DECISIONS_TLDR" ] || [ -n "$DECISIONS_INDEX_LINE" ]; then
|
|
247
|
+
dbg "Decisions TL;DR or index found"
|
|
248
|
+
DECISIONS_SECTION="--- PROJECT DECISIONS (TL;DR) ---"
|
|
249
|
+
if [ -n "$DECISIONS_TLDR" ]; then
|
|
250
|
+
DECISIONS_SECTION="${DECISIONS_SECTION}
|
|
217
251
|
$(printf '%b' "$DECISIONS_TLDR")"
|
|
252
|
+
fi
|
|
253
|
+
# The index line goes last.
|
|
254
|
+
if [ -n "$DECISIONS_INDEX_LINE" ]; then
|
|
255
|
+
DECISIONS_SECTION="${DECISIONS_SECTION}
|
|
256
|
+
${DECISIONS_INDEX_LINE}"
|
|
257
|
+
fi
|
|
218
258
|
if [ -n "$CONTEXT" ]; then
|
|
219
259
|
CONTEXT="${CONTEXT}
|
|
220
260
|
|
|
@@ -227,18 +267,23 @@ ${DECISIONS_SECTION}"
|
|
|
227
267
|
fi
|
|
228
268
|
|
|
229
269
|
# --- Section 2: Learning maintenance directive ---
|
|
230
|
-
# Emitted when captured turns are waiting: queue non-empty, or a
|
|
231
|
-
# .processing
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
270
|
+
# Emitted when captured turns are waiting: the queue is non-empty, or a
|
|
271
|
+
# .processing claim has had no heartbeat for PROCESSING_STALE_SECS and its batch
|
|
272
|
+
# needs a new owner. A FRESH .processing means a live Learning agent holds the
|
|
273
|
+
# claim, so the directive is suppressed even if new turns queued since. This
|
|
274
|
+
# check is advisory and decides only whether to spawn: the claim itself is taken
|
|
275
|
+
# by json-helper's claim-queue op under the learning lock, which applies the same
|
|
276
|
+
# threshold (D-OWNED-CLAIM, lib/learning-store.cjs), so a spawn that loses a race
|
|
277
|
+
# exits on `busy`.
|
|
235
278
|
if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
236
279
|
QUEUE_FILE="$LEARNING_DIR/.pending-turns.jsonl"
|
|
237
280
|
PROCESSING_FILE="$LEARNING_DIR/.pending-turns.processing"
|
|
238
281
|
PROCESSING_STALE_SECS=900
|
|
239
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).
|
|
240
285
|
LEARNING_WORK=""
|
|
241
|
-
if
|
|
286
|
+
if df_file_below "$LEDGER_ROOT" "$PROCESSING_FILE"; then
|
|
242
287
|
source "$SCRIPT_DIR/get-mtime" 2>/dev/null || true
|
|
243
288
|
_SC_PROC_MTIME=$(get_mtime "$PROCESSING_FILE" 2>/dev/null || true)
|
|
244
289
|
_SC_NOW=$(date +%s)
|
|
@@ -247,7 +292,7 @@ if [ "$LEARNING_ENABLED" = "true" ]; then
|
|
|
247
292
|
else
|
|
248
293
|
dbg "learning directive suppressed: fresh .processing (live agent owns the batch)"
|
|
249
294
|
fi
|
|
250
|
-
elif [ -s "$QUEUE_FILE" ]; then
|
|
295
|
+
elif df_file_below "$LEDGER_ROOT" "$QUEUE_FILE" && [ -s "$QUEUE_FILE" ]; then
|
|
251
296
|
LEARNING_WORK="queue"
|
|
252
297
|
fi
|
|
253
298
|
|
|
@@ -271,28 +316,83 @@ ${LEARNING_PAUSED_SECTION}"
|
|
|
271
316
|
fi
|
|
272
317
|
|
|
273
318
|
if [ -n "$LEARNING_WORK" ]; then
|
|
274
|
-
#
|
|
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.
|
|
275
354
|
LEARNING_MODEL=""
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
361
|
+
fi
|
|
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
|
|
278
380
|
fi
|
|
279
|
-
if [ -z "$
|
|
280
|
-
|
|
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\","
|
|
281
390
|
fi
|
|
282
|
-
LEARNING_MODEL="${LEARNING_MODEL:-opus}"
|
|
283
|
-
# Allowlist before interpolating into the injected directive (defense in depth --
|
|
284
|
-
# learning.json is user/config-controlled; a value with newlines/quotes must
|
|
285
|
-
# never inject arbitrary text into the SessionStart context). Fallback matches
|
|
286
|
-
# learning-tuning-config.ts DEFAULTS.model (duplicated-by-design, see feature KB).
|
|
287
|
-
case "$LEARNING_MODEL" in
|
|
288
|
-
opus|sonnet|haiku) ;;
|
|
289
|
-
*) LEARNING_MODEL="opus" ;;
|
|
290
|
-
esac
|
|
291
391
|
|
|
292
|
-
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})"
|
|
293
393
|
LEARNING_SECTION="--- LEARNING MAINTENANCE ---
|
|
294
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.
|
|
295
|
-
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\")
|
|
296
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."
|
|
297
397
|
if [ -n "$CONTEXT" ]; then
|
|
298
398
|
CONTEXT="${CONTEXT}
|
|
@@ -440,8 +540,8 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
|
440
540
|
tracker_gates_open() {
|
|
441
541
|
# Gate 1 — the attempt cap. Read with the `read` builtin: no fork.
|
|
442
542
|
#
|
|
443
|
-
# The counter's shape is ONE decimal integer line and nothing else
|
|
444
|
-
# document the shape of any file that gates an action, and keep absent and
|
|
543
|
+
# The counter's shape is ONE decimal integer line and nothing else
|
|
544
|
+
# (document the shape of any file that gates an action, and keep absent and
|
|
445
545
|
# malformed distinct from a value). Absent means "no attempt yet" = 0.
|
|
446
546
|
# Malformed self-heals to 0 and is overwritten with a well-formed count on
|
|
447
547
|
# emission below, so a stray byte can never recur: refusing forever would
|
|
@@ -571,7 +671,7 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
|
571
671
|
# allowlisted tokens. Decided once at the top of the file, where both values are
|
|
572
672
|
# resolved; consulted here because this is one of the two sinks that interpolate
|
|
573
673
|
# them, and a control stated once for the file is not a control at a sink that
|
|
574
|
-
# never consults it
|
|
674
|
+
# never consults it. The third path, the conventions file, is composed
|
|
575
675
|
# from the gated devflow directory and the allowlisted provider alone, so this
|
|
576
676
|
# gate covers it too.
|
|
577
677
|
if [ -z "$DIRECTIVE_PATHS_SAFE" ]; then
|
|
@@ -607,7 +707,8 @@ if [ -n "$TRACKER_PROVIDER" ] && [ ! -f "$TRACKER_CONVENTIONS" ]; then
|
|
|
607
707
|
# assertion of the closed domain rather than a sanitiser, and it is the single
|
|
608
708
|
# place the tier is validated, so a later config read cannot be wired in
|
|
609
709
|
# without passing through it. The literal must equal the Tracker agent's
|
|
610
|
-
# frontmatter `model:` (pinned against
|
|
710
|
+
# frontmatter `model:` (pinned against loadShippedAgentDefaults()['tracker'].model
|
|
711
|
+
# in shell-hooks-tracker).
|
|
611
712
|
TRACKER_MODEL="sonnet"
|
|
612
713
|
case "$TRACKER_MODEL" in
|
|
613
714
|
opus|sonnet|haiku) ;;
|