devflow-kit 2.2.0 → 2.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.
@@ -23,6 +23,7 @@ source "$SCRIPT_DIR/hook-bootstrap" "pre-compact-memory"
23
23
 
24
24
  # JSON parsing (jq with node fallback) — silently no-op if neither available
25
25
  source "$SCRIPT_DIR/json-parse" || { echo "pre-compact-memory: failed to source json-parse" >&2; exit 1; }
26
+ source "$SCRIPT_DIR/is-hex-sha" || { echo "pre-compact-memory: failed to source is-hex-sha" >&2; exit 1; }
26
27
  if [ "$_JSON_AVAILABLE" = "false" ]; then exit 0; fi
27
28
 
28
29
  INPUT=$(cat)
@@ -64,6 +65,7 @@ source "$SCRIPT_DIR/ensure-devflow-init" "$CWD" || exit 0
64
65
  BACKUP_FILE="$MEMORY_DIR/backup.json"
65
66
 
66
67
  # Capture git state
68
+ GIT_HEAD_SHA=""
67
69
  GIT_BRANCH=""
68
70
  GIT_STATUS=""
69
71
  GIT_LOG=""
@@ -71,11 +73,12 @@ GIT_DIFF_STAT=""
71
73
  TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
72
74
 
73
75
  if cd "$CWD" 2>/dev/null && git rev-parse --git-dir >/dev/null 2>&1; then
76
+ GIT_HEAD_SHA=$(git rev-parse HEAD 2>/dev/null || echo "")
74
77
  GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
75
78
  GIT_STATUS=$(git status --porcelain 2>/dev/null | head -30 || echo "")
76
79
  GIT_LOG=$(git log --oneline -10 2>/dev/null || echo "")
77
80
  GIT_DIFF_STAT=$(git diff --stat HEAD 2>/dev/null || echo "")
78
- dbg "GIT_BRANCH=$GIT_BRANCH"
81
+ dbg "GIT_BRANCH=$GIT_BRANCH HEAD=$GIT_HEAD_SHA"
79
82
  fi
80
83
 
81
84
  # Snapshot current WORKING-MEMORY.md (preserves session context through compaction)
@@ -98,28 +101,43 @@ json_backup_construct \
98
101
  log "Wrote backup: $BACKUP_FILE"
99
102
  dbg "Wrote backup: $BACKUP_FILE"
100
103
 
101
- # Bootstrap minimal WORKING-MEMORY.md if none exists yet
102
- # This ensures SessionStart has context to inject after compaction
104
+ # Bootstrap minimal WORKING-MEMORY.md if absent; skip on detached HEAD, unborn branch,
105
+ # or malformed SHA. is_hex_sha 40 40: exactly 40 lowercase hex chars required.
106
+ # avoids REL-5: O_EXCL-style atomic create via noclobber so the existence test and the
107
+ # create are one operation — if the worker's CAS mv lands in the window, noclobber fails
108
+ # (file already exists) and we skip the bootstrap rather than truncating fresh memory.
103
109
  MEMORY_FILE="$MEMORY_DIR/WORKING-MEMORY.md"
104
- if [ ! -f "$MEMORY_FILE" ] && [ -n "$GIT_BRANCH" ]; then
105
- {
106
- echo "# Working Memory"
107
- echo ""
108
- echo "## Now"
109
- echo "- Session compacted before working memory was established"
110
- echo ""
111
- echo "## Context"
112
- echo "- Branch: $GIT_BRANCH"
113
- echo "$GIT_LOG" | head -3 | while IFS= read -r line; do
114
- [ -n "$line" ] && echo "- $line"
115
- done
116
- echo ""
117
- echo "## Modified Files"
118
- echo "$GIT_STATUS" | head -10 | while IFS= read -r line; do
119
- [ -n "$line" ] && echo "- $(echo "$line" | awk '{print $2}')"
120
- done
121
- } > "$MEMORY_FILE"
122
- dbg "Bootstrapped minimal WORKING-MEMORY.md"
110
+ if [ -n "$GIT_BRANCH" ] && is_hex_sha "$GIT_HEAD_SHA" 40 40; then
111
+ if (set -o noclobber; : > "$MEMORY_FILE") 2>/dev/null; then
112
+ {
113
+ echo "<!-- memory-head: $GIT_HEAD_SHA branch: $GIT_BRANCH -->"
114
+ echo ""
115
+ echo "## Now"
116
+ echo "- Session compacted before working memory was established"
117
+ echo ""
118
+ echo "## Progress"
119
+ echo "- (no history yet)"
120
+ echo ""
121
+ echo "## Decisions"
122
+ echo "- (none recorded)"
123
+ echo ""
124
+ echo "## Context"
125
+ echo "- Branch: $GIT_BRANCH"
126
+ echo "$GIT_LOG" | head -3 | while IFS= read -r line; do
127
+ [ -n "$line" ] && echo "- $line"
128
+ done
129
+ if [ -n "$GIT_STATUS" ]; then
130
+ echo "- Modified files:"
131
+ echo "$GIT_STATUS" | head -10 | while IFS= read -r line; do
132
+ [ -n "$line" ] && echo " - $(echo "$line" | awk '{print $2}')"
133
+ done
134
+ fi
135
+ echo ""
136
+ echo "## Session Log"
137
+ echo "- (no entries)"
138
+ } >> "$MEMORY_FILE"
139
+ dbg "Bootstrapped minimal WORKING-MEMORY.md with stamp and canonical sections"
140
+ fi
123
141
  fi
124
142
 
125
143
  log "PreCompact complete"
@@ -68,6 +68,7 @@ fi
68
68
  # Non-clobber: only recovers when .pending-turns.jsonl does NOT already exist,
69
69
  # so a concurrent session's fresh queue is never overwritten.
70
70
  source "$SCRIPT_DIR/get-mtime" || { echo "session-start-memory: failed to source get-mtime" >&2; exit 1; }
71
+ source "$SCRIPT_DIR/is-hex-sha" || { echo "session-start-memory: failed to source is-hex-sha" >&2; exit 1; }
71
72
  _SSM_PT_PROC="$MEMORY_DIR/.pending-turns.processing"
72
73
  _SSM_PT_JSONL="$MEMORY_DIR/.pending-turns.jsonl"
73
74
  if [ -f "$_SSM_PT_PROC" ]; then
@@ -112,26 +113,11 @@ parse_and_validate_stamp() {
112
113
  STAMP_BRANCH=$(echo "$FIRST_LINE" | sed -n 's/<!-- memory-head: [^ ]* branch: \([^ ]*\) -->/\1/p')
113
114
  ;;
114
115
  esac
115
- # Hex-validation security gate — must stay before any git use of STAMP_SHA
116
- if [ -n "$STAMP_SHA" ]; then
117
- case "$STAMP_SHA" in
118
- [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*)
119
- # Looks like a hex string; verify length ≤ 40 and only hex chars via sed
120
- local _SHA_CLEAN
121
- _SHA_CLEAN=$(echo "$STAMP_SHA" | sed 's/[^0-9a-f]//g')
122
- local _SHA_LEN=${#STAMP_SHA}
123
- if [ "$_SHA_CLEAN" != "$STAMP_SHA" ] || [ "$_SHA_LEN" -gt 40 ]; then
124
- dbg "STAMP_SHA rejected (non-hex chars or >40): $STAMP_SHA"
125
- STAMP_SHA=""
126
- STAMP_BRANCH=""
127
- fi
128
- ;;
129
- *)
130
- dbg "STAMP_SHA rejected (bad prefix): $STAMP_SHA"
131
- STAMP_SHA=""
132
- STAMP_BRANCH=""
133
- ;;
134
- esac
116
+ # Hex-validation security gate — must stay before any git use of STAMP_SHA.
117
+ if [ -n "$STAMP_SHA" ] && ! is_hex_sha "$STAMP_SHA"; then
118
+ dbg "STAMP_SHA rejected (non-hex or out of range): $STAMP_SHA"
119
+ STAMP_SHA=""
120
+ STAMP_BRANCH=""
135
121
  fi
136
122
  dbg "STAMP_SHA=$STAMP_SHA STAMP_BRANCH=$STAMP_BRANCH"
137
123
  # Strip stamp line from injected body
@@ -147,16 +133,29 @@ parse_and_validate_stamp() {
147
133
 
148
134
  # --- detect_refresh_failing: sets REFRESH_FAILING in caller's scope.
149
135
  # Condition: queue non-empty AND (.last-refresh-ok missing OR >600s old)
136
+ # orphaned-processing depth fix: count both .pending-turns.jsonl AND
137
+ # .pending-turns.processing toward _queue_depth. Before this fix, an orphaned
138
+ # .processing (whose mtime is between 0s and the D56c 300s cold-path gate)
139
+ # was invisible to State-C — a crashed worker's batch would sit silently with
140
+ # no user-visible warning.
150
141
  detect_refresh_failing() {
151
142
  local _now="$1"
152
143
  local _memory_dir="$2"
153
144
  local _log_dir="$3"
154
145
  local _queue_file="$_memory_dir/.pending-turns.jsonl"
146
+ local _proc_file="$_memory_dir/.pending-turns.processing"
155
147
  local _ok_file="$_memory_dir/.last-refresh-ok"
156
148
  local _queue_depth=0
157
149
  if [ -f "$_queue_file" ] && [ -s "$_queue_file" ]; then
158
150
  _queue_depth=$(wc -l < "$_queue_file" | tr -d ' ')
159
151
  fi
152
+ # Also count lines in .processing — an orphaned batch left by a crashed worker
153
+ # is unprocessed content even if .jsonl is empty (applies B4 blind-spot fix)
154
+ if [ -f "$_proc_file" ] && [ -s "$_proc_file" ]; then
155
+ local _proc_depth
156
+ _proc_depth=$(wc -l < "$_proc_file" | tr -d ' ')
157
+ _queue_depth=$(( _queue_depth + _proc_depth ))
158
+ fi
160
159
  local _ok_age=9999999
161
160
  if [ -f "$_ok_file" ]; then
162
161
  local _ok_mtime