@sabaiway/agent-workflow-kit 1.48.0 → 1.49.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 CHANGED
@@ -4,6 +4,33 @@ Semantically versioned ([semver](https://semver.org)), newest first. The `versio
4
4
  is the current release. `upgrade` mode reads a project's `docs/ai/.workflow-version` and applies
5
5
  every `migrations/<version>-<slug>.md` newer than it, in semver order.
6
6
 
7
+ ## 1.49.0 — Honesty/robustness bundle: refresh EROFS stated skip · settings integer-overflow parity (AD-056)
8
+
9
+ A small **honesty/robustness** release (kit MINOR carrying the two bridge PATCH bumps in-tarball —
10
+ codex-cli-bridge 2.7.1 + antigravity-cli-bridge 2.6.1; engine 1.17.0 / memory 2.3.0 unchanged; lineage
11
+ head stays `2.0.0`). Two kit/bridge fixes ship here (a third, repo-only dispatcher fix, rides the same
12
+ commit); all three share one theme: **a blocked environment must produce a STATED degrade, never a
13
+ false red — and a real failure stays loud.**
14
+
15
+ - **Refresh under a read-only skills dir is a stated skip, not a false failure.** Under the harness
16
+ session sandbox `~/.claude/skills` is read-only, yet `--refresh-placed` re-syncs even at the current
17
+ version (repair-on-rerun). That write EROFSed into the generic catch → *"could not refresh — … recover
18
+ with setup"*, though both versions were already current AND `setup` hits the same read-only dir. It now
19
+ reports a new **`skipped-readonly`** outcome (exit 0): it names the current version, states the re-sync
20
+ was skipped/incomplete, and names the read-only cause — never claiming a re-sync ran or file integrity
21
+ (a partial copy may precede the block). Only a read-only-class write failure at the copy-write boundary,
22
+ at an equal version, degrades; a read-side / source-side / `linkWrappers` / real-I/O failure, or a
23
+ version-**behind** refresh, stays a loud *could not refresh* (its recovery pointing at a writable
24
+ rerun). The opt-in `setup` placement lane keeps its loud failure. Any drift persists until a writable
25
+ rerun (converge-on-re-run).
26
+ - **Settings integer validation is now shell↔JS exact (Issue-012, Resolved).** The four bridge wrappers'
27
+ shared `aw_settings_valid` integer arms did `(( 10#$v … ))`, which wraps modulo 2^64 on a 19+ digit
28
+ string — the shell **accepted** `18446744073709551916` (2^64+300) while the kit's `settingValueValid`
29
+ (safe-integer) **rejected** it. A shared overflow-safe `aw_int_in_range` helper (byte-identical across
30
+ all four wrappers) strips leading zeros then rejects on a digit count exceeding the max's — never
31
+ running arithmetic on a huge string. A leading-zero **in-range** value (e.g. `000…086400`) still
32
+ passes on both sides; the value is pinned by a new behavioral shell↔JS parity test.
33
+
7
34
  ## 1.48.0 — Family-owned neutral ack store + read-prompt-economy hook lane (AD-055, the CLAUDE-CODE-HARNESS-FRICTION cluster)
8
35
 
9
36
  A **feature** release (kit-only — engine 1.17.0, memory 2.3.0, bridges 2.7.0/2.6.0 unchanged) that
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: agent-workflow-kit
3
3
  description: Deploy or upgrade a portable AI-agent memory-and-workflow system in any project. Use when the user wants to bootstrap `docs/ai/` + an entry-point `AGENTS.md` (+ `CLAUDE.md` alias) + cap/archive/index enforcement in a new or existing repo, set up the Memory Map and session protocols, install the docs-rotation pre-commit hook, or run `/agent-workflow-kit` / `/agent-workflow-kit upgrade`. Triggers on phrases like "set up the memory system", "deploy the AI workflow here", "bootstrap docs/ai", "upgrade the workflow".
4
4
  disable-model-invocation: true
5
5
  metadata:
6
- version: '1.48.0'
6
+ version: '1.49.0'
7
7
  ---
8
8
 
9
9
  # agent-workflow-kit
@@ -2,7 +2,7 @@
2
2
  name: antigravity-cli-bridge
3
3
  description: Delegate work to Google's Antigravity CLI (`agy`) — the successor to Gemini CLI — to reach Gemini, Claude, and GPT-OSS models under a Google AI Pro/Ultra subscription from the terminal. Use when the user wants to run a headless `agy` prompt, hand a focused task or second-opinion review to `agy`, install or authenticate Antigravity CLI, check or economise its quota/models, bridge project context into `agy`, set up a second delegated-execution backend beside Codex, or troubleshoot `agy` flags, models, auth, conversations, or its no-JSON headless behaviour.
4
4
  metadata:
5
- version: '2.6.0'
5
+ version: '2.6.1'
6
6
  ---
7
7
 
8
8
  # antigravity-cli-bridge
@@ -132,12 +132,22 @@ aw_settings_known() {
132
132
  *) return 1 ;;
133
133
  esac
134
134
  }
135
+ aw_int_in_range() {
136
+ # All-digits $1 vs [min,max] WITHOUT 64-bit wrap (Issue-012): strip leading zeros, then a longer
137
+ # digit count than max's is unconditionally out of range — never do the arithmetic on a huge string.
138
+ # A leading-zero in-range value still passes (its stripped count is small); all-zeros collapses to
139
+ # "0" (below min>=1). Mirrors the JS settingValueValid safe-integer bound, verified by parity test.
140
+ local n="${1#"${1%%[!0]*}"}" min="$2" max="$3"
141
+ n="${n:-0}"
142
+ (( ${#n} > ${#max} )) && return 1
143
+ (( n >= min && n <= max ))
144
+ }
135
145
  aw_settings_valid() {
136
146
  local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
137
147
  case "$k" in
138
148
  CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
139
- CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
140
- CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
149
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 86400 ;;
150
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
141
151
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
142
152
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
143
153
  *) return 1 ;;
@@ -189,6 +199,14 @@ aw_apply_settings() {
189
199
  fi
190
200
  continue
191
201
  fi
202
+ # Normalize an all-digit (integer) value to DECIMAL before export: a leading-zero value the integer
203
+ # arms legitimately accept (000…086400 == 86400) would otherwise read as OCTAL in downstream Bash
204
+ # arithmetic ("value too great for base"). Strip leading zeros, floor "0"; enum/duration (non-digit)
205
+ # and boolean 0/1 are unaffected.
206
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
207
+ value="${value#"${value%%[!0]*}"}"
208
+ value="${value:-0}"
209
+ fi
192
210
  export "$key=$value"
193
211
  done
194
212
  return 0
@@ -199,7 +217,7 @@ DEFAULT_AGY_REVIEW_MODEL="Gemini 3.1 Pro (High)"
199
217
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
200
218
  # version (drift-guarded by agy-review.test.mjs against capability.json).
201
219
  AW_RECEIPT_BACKEND="agy"
202
- AW_BRIDGE_VERSION="2.6.0"
220
+ AW_BRIDGE_VERSION="2.6.1"
203
221
  # `-` not `:-` so an EXPLICIT empty AGY_MODEL= survives (drop --model, use settings.json — agy.sh:52).
204
222
  AGY_MODEL="${AGY_MODEL-$DEFAULT_AGY_REVIEW_MODEL}"
205
223
  # Frontier review models. ANY model is allowed; a sub-frontier one only earns a soft, silenceable warning.
@@ -994,8 +994,9 @@ describe('agy-review.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
994
994
  assert.ok(arm, `no validation arm for ${s.key}`);
995
995
  if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
996
996
  if (s.kind === 'integer') {
997
- assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
998
- assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
997
+ // Issue-012 refactor: min/max are pinned as the aw_int_in_range helper's positional bounds
998
+ // (`aw_int_in_range "$v" <min> <max>`) the overflow-safe range check replaced raw arithmetic.
999
+ assert.match(arm[1], new RegExp(`aw_int_in_range "\\$v" ${s.min} ${s.max}\\b`), `${s.key}: min/max ${s.min}/${s.max} not pinned as the aw_int_in_range bounds`);
999
1000
  }
1000
1001
  if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
1001
1002
  if (s.kind === 'duration') {
@@ -99,12 +99,22 @@ aw_settings_known() {
99
99
  *) return 1 ;;
100
100
  esac
101
101
  }
102
+ aw_int_in_range() {
103
+ # All-digits $1 vs [min,max] WITHOUT 64-bit wrap (Issue-012): strip leading zeros, then a longer
104
+ # digit count than max's is unconditionally out of range — never do the arithmetic on a huge string.
105
+ # A leading-zero in-range value still passes (its stripped count is small); all-zeros collapses to
106
+ # "0" (below min>=1). Mirrors the JS settingValueValid safe-integer bound, verified by parity test.
107
+ local n="${1#"${1%%[!0]*}"}" min="$2" max="$3"
108
+ n="${n:-0}"
109
+ (( ${#n} > ${#max} )) && return 1
110
+ (( n >= min && n <= max ))
111
+ }
102
112
  aw_settings_valid() {
103
113
  local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
104
114
  case "$k" in
105
115
  CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
106
- CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
107
- CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
116
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 86400 ;;
117
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
108
118
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
109
119
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
110
120
  *) return 1 ;;
@@ -156,6 +166,14 @@ aw_apply_settings() {
156
166
  fi
157
167
  continue
158
168
  fi
169
+ # Normalize an all-digit (integer) value to DECIMAL before export: a leading-zero value the integer
170
+ # arms legitimately accept (000…086400 == 86400) would otherwise read as OCTAL in downstream Bash
171
+ # arithmetic ("value too great for base"). Strip leading zeros, floor "0"; enum/duration (non-digit)
172
+ # and boolean 0/1 are unaffected.
173
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
174
+ value="${value#"${value%%[!0]*}"}"
175
+ value="${value:-0}"
176
+ fi
159
177
  export "$key=$value"
160
178
  done
161
179
  return 0
@@ -443,8 +443,9 @@ describe('agy.sh — settings surface ⟷ manifest (D6, manifest-pinned)', () =>
443
443
  assert.ok(arm, `no validation arm for ${s.key}`);
444
444
  if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
445
445
  if (s.kind === 'integer') {
446
- assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
447
- assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
446
+ // Issue-012 refactor: min/max are pinned as the aw_int_in_range helper's positional bounds
447
+ // (`aw_int_in_range "$v" <min> <max>`) the overflow-safe range check replaced raw arithmetic.
448
+ assert.match(arm[1], new RegExp(`aw_int_in_range "\\$v" ${s.min} ${s.max}\\b`), `${s.key}: min/max ${s.min}/${s.max} not pinned as the aw_int_in_range bounds`);
448
449
  }
449
450
  if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
450
451
  if (s.kind === 'duration') {
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "antigravity-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.6.0",
6
+ "version": "2.6.1",
7
7
  "provides": ["review", "probe"],
8
8
  "roles": {
9
9
  "review": {
@@ -2,7 +2,7 @@
2
2
  name: codex-cli-bridge
3
3
  description: Delegate work to the OpenAI Codex CLI (`codex`) under a ChatGPT subscription — run plan/instruction EXECUTION in a sandboxed workspace, or get a read-only ADVISORY review of a plan or working-tree diff — as a second delegated-execution backend beside Antigravity. Use when the user wants to hand a bounded coding task or plan to `codex exec`, get a second-opinion review from codex, install or authenticate Codex CLI, understand its sandbox/network/approval policy, drive codex efficiently from the main agent (exec vs review, resume, the commit boundary), bridge project context (`AGENTS.md`) into codex, or troubleshoot codex flags, models, auth, or its no-TTY headless behaviour.
4
4
  metadata:
5
- version: '2.7.0'
5
+ version: '2.7.1'
6
6
  ---
7
7
 
8
8
  # codex-cli-bridge
@@ -112,12 +112,22 @@ aw_settings_known() {
112
112
  *) return 1 ;;
113
113
  esac
114
114
  }
115
+ aw_int_in_range() {
116
+ # All-digits $1 vs [min,max] WITHOUT 64-bit wrap (Issue-012): strip leading zeros, then a longer
117
+ # digit count than max's is unconditionally out of range — never do the arithmetic on a huge string.
118
+ # A leading-zero in-range value still passes (its stripped count is small); all-zeros collapses to
119
+ # "0" (below min>=1). Mirrors the JS settingValueValid safe-integer bound, verified by parity test.
120
+ local n="${1#"${1%%[!0]*}"}" min="$2" max="$3"
121
+ n="${n:-0}"
122
+ (( ${#n} > ${#max} )) && return 1
123
+ (( n >= min && n <= max ))
124
+ }
115
125
  aw_settings_valid() {
116
126
  local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
117
127
  case "$k" in
118
128
  CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
119
- CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
120
- CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
129
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 86400 ;;
130
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
121
131
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
122
132
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
123
133
  *) return 1 ;;
@@ -169,6 +179,14 @@ aw_apply_settings() {
169
179
  fi
170
180
  continue
171
181
  fi
182
+ # Normalize an all-digit (integer) value to DECIMAL before export: a leading-zero value the integer
183
+ # arms legitimately accept (000…086400 == 86400) would otherwise read as OCTAL in downstream Bash
184
+ # arithmetic ("value too great for base"). Strip leading zeros, floor "0"; enum/duration (non-digit)
185
+ # and boolean 0/1 are unaffected.
186
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
187
+ value="${value#"${value%%[!0]*}"}"
188
+ value="${value:-0}"
189
+ fi
172
190
  export "$key=$value"
173
191
  done
174
192
  return 0
@@ -1080,8 +1080,9 @@ describe('codex-exec.sh — settings surface ⟷ manifest (D6, manifest-pinned)'
1080
1080
  assert.ok(arm, `no validation arm for ${s.key}`);
1081
1081
  if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
1082
1082
  if (s.kind === 'integer') {
1083
- assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
1084
- assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
1083
+ // Issue-012 refactor: min/max are pinned as the aw_int_in_range helper's positional bounds
1084
+ // (`aw_int_in_range "$v" <min> <max>`) the overflow-safe range check replaced raw arithmetic.
1085
+ assert.match(arm[1], new RegExp(`aw_int_in_range "\\$v" ${s.min} ${s.max}\\b`), `${s.key}: min/max ${s.min}/${s.max} not pinned as the aw_int_in_range bounds`);
1085
1086
  }
1086
1087
  if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
1087
1088
  if (s.kind === 'duration') {
@@ -104,12 +104,22 @@ aw_settings_known() {
104
104
  *) return 1 ;;
105
105
  esac
106
106
  }
107
+ aw_int_in_range() {
108
+ # All-digits $1 vs [min,max] WITHOUT 64-bit wrap (Issue-012): strip leading zeros, then a longer
109
+ # digit count than max's is unconditionally out of range — never do the arithmetic on a huge string.
110
+ # A leading-zero in-range value still passes (its stripped count is small); all-zeros collapses to
111
+ # "0" (below min>=1). Mirrors the JS settingValueValid safe-integer bound, verified by parity test.
112
+ local n="${1#"${1%%[!0]*}"}" min="$2" max="$3"
113
+ n="${n:-0}"
114
+ (( ${#n} > ${#max} )) && return 1
115
+ (( n >= min && n <= max ))
116
+ }
107
117
  aw_settings_valid() {
108
118
  local k="$1" v="$2" int_re='^[0-9]+$' dur_re='^[0-9]+(\.[0-9]+)?[smhd]$' zero_re='^0+(\.0+)?[smhd]$'
109
119
  case "$k" in
110
120
  CODEX_SERVICE_TIER) [[ "$v" == "priority" ]] ;;
111
- CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 86400 )) ;;
112
- CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && (( 10#$v >= 1 && 10#$v <= 100000000 )) ;;
121
+ CODEX_HARD_TIMEOUT) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 86400 ;;
122
+ CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] && aw_int_in_range "$v" 1 100000000 ;;
113
123
  AGY_HARD_TIMEOUT) [[ "$v" =~ $dur_re && ! "$v" =~ $zero_re ]] ;;
114
124
  AGY_REVIEW_ALLOW_ADDDIR) [[ "$v" == "0" || "$v" == "1" ]] ;;
115
125
  *) return 1 ;;
@@ -161,6 +171,14 @@ aw_apply_settings() {
161
171
  fi
162
172
  continue
163
173
  fi
174
+ # Normalize an all-digit (integer) value to DECIMAL before export: a leading-zero value the integer
175
+ # arms legitimately accept (000…086400 == 86400) would otherwise read as OCTAL in downstream Bash
176
+ # arithmetic ("value too great for base"). Strip leading zeros, floor "0"; enum/duration (non-digit)
177
+ # and boolean 0/1 are unaffected.
178
+ if [[ "$value" =~ ^[0-9]+$ ]]; then
179
+ value="${value#"${value%%[!0]*}"}"
180
+ value="${value:-0}"
181
+ fi
164
182
  export "$key=$value"
165
183
  done
166
184
  return 0
@@ -172,7 +190,7 @@ DEFAULT_CODEX_EFFORT="xhigh"
172
190
  # Review-receipt identity (AD-038). AW_BRIDGE_VERSION mirrors this bridge's SKILL.md/capability.json
173
191
  # version (drift-guarded by codex-review.test.mjs against capability.json).
174
192
  AW_RECEIPT_BACKEND="codex"
175
- AW_BRIDGE_VERSION="2.7.0"
193
+ AW_BRIDGE_VERSION="2.7.1"
176
194
  CODEX_MODEL="${CODEX_MODEL:-$DEFAULT_CODEX_MODEL}"
177
195
  CODEX_EFFORT="${CODEX_EFFORT:-$DEFAULT_CODEX_EFFORT}"
178
196
  # Generous hard cap for a slow xhigh review (subscription latency varies).
@@ -1020,8 +1020,9 @@ describe('codex-review.sh — settings surface ⟷ manifest (D6, manifest-pinned
1020
1020
  assert.ok(arm, `no validation arm for ${s.key}`);
1021
1021
  if (s.kind === 'enum') for (const v of s.values) assert.ok(arm[1].includes(`"${v}"`), `${s.key}: enum value '${v}' not pinned`);
1022
1022
  if (s.kind === 'integer') {
1023
- assert.match(arm[1], new RegExp(`>= ${s.min}\\b`), `${s.key}: min ${s.min} not pinned`);
1024
- assert.match(arm[1], new RegExp(`<= ${s.max}\\b`), `${s.key}: max ${s.max} not pinned`);
1023
+ // Issue-012 refactor: min/max are pinned as the aw_int_in_range helper's positional bounds
1024
+ // (`aw_int_in_range "$v" <min> <max>`) the overflow-safe range check replaced raw arithmetic.
1025
+ assert.match(arm[1], new RegExp(`aw_int_in_range "\\$v" ${s.min} ${s.max}\\b`), `${s.key}: min/max ${s.min}/${s.max} not pinned as the aw_int_in_range bounds`);
1025
1026
  }
1026
1027
  if (s.kind === 'boolean') assert.ok(arm[1].includes('"0"') && arm[1].includes('"1"'), `${s.key}: boolean 0/1 not pinned`);
1027
1028
  if (s.kind === 'duration') {
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "codex-cli-bridge",
5
5
  "kind": "execution-backend",
6
- "version": "2.7.0",
6
+ "version": "2.7.1",
7
7
  "provides": ["execute", "review"],
8
8
  "roles": {
9
9
  "execute": {
package/capability.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "schema": 1,
4
4
  "name": "agent-workflow-kit",
5
5
  "kind": "composition-root",
6
- "version": "1.48.0",
6
+ "version": "1.49.0",
7
7
  "provides": [],
8
8
  "roles": {},
9
9
  "detect": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sabaiway/agent-workflow-kit",
3
- "version": "1.48.0",
3
+ "version": "1.49.0",
4
4
  "description": "Portable, cross-agent memory & workflow for AI coding agents — Claude Code, Codex, Cursor, Devin Desktop. One command deploys an AGENTS.md entry point + docs/ai context with cap/archive/index enforcement into any repo.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -11,8 +11,13 @@ Run `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs [<backend>] [--bindir <pa
11
11
  runs as its fourth stamp-independent reconcile): refresh every bridge `setup` **already placed**
12
12
  from the kit's bundled copies + re-link its wrappers; an absent bridge is a stated skip (**never**
13
13
  placed), a placed bridge newer than the bundle is a stated skip naming the kit update (**never**
14
- downgraded), and every outcome line is composed by the tool — paste verbatim. Does not combine
15
- with `--dry-run`.
14
+ downgraded), and every outcome line is composed by the tool — paste verbatim. When the skills dir
15
+ is **read-only this session** and the placed bridge is already at the bundled version, the
16
+ equal-version re-sync it would run cannot write: that outcome is `skipped-readonly` — a **stated
17
+ skip** (exit 0, not a failure) naming the current version, the skipped/incomplete re-sync, and the
18
+ read-only cause; it never claims a re-sync ran, and any local drift persists until a writable rerun.
19
+ (A version-**behind** refresh blocked by the same read-only dir stays a loud `could not refresh`,
20
+ its recovery pointing at a writable rerun.) Does not combine with `--dry-run`.
16
21
  - `--help`, `-h` — usage.
17
22
 
18
23
  For each backend it:
@@ -31,14 +31,14 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
31
31
  **Placed-bridge refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.** Run
32
32
  `node ${CLAUDE_SKILL_DIR}/tools/setup-backends.mjs --refresh-placed` and **paste its per-bridge
33
33
  output lines verbatim** — every outcome line is composed by the tool (*refreshed* / *already
34
- current* / *skipped — not placed* / *could not refresh* + its recovery); you compose nothing
35
- factual. It is **refresh-only**: it refreshes a bridge **`setup` already placed** from this kit's
36
- bundled copies and re-links its wrappers; an **absent** bridge is a stated skip, **never a first
37
- placement** (placement stays the opt-in `${CLAUDE_SKILL_DIR}/references/modes/setup.md` — AD-009/AD-011 honesty intact), and a
38
- placed bridge **newer** than the bundled copy is a stated skip naming the kit update (**never a
39
- downgrade**). Like the other stamp-independent reconciles it runs on **every** upgrade
40
- including an equal-head onewith no lineage-head bump. A *could not refresh* line is
41
- non-fatal: relay it plainly with its recovery and continue the upgrade.
34
+ current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + its recovery). It
35
+ is **refresh-only**: it refreshes a bridge **`setup` already placed** from this kit's bundled copies
36
+ and re-links its wrappers; an **absent** bridge is a stated skip, **never a first placement**
37
+ (placement stays the opt-in `${CLAUDE_SKILL_DIR}/references/modes/setup.md` — AD-009/AD-011 honesty intact), a placed bridge
38
+ **newer** than the bundle is a stated skip naming the kit update (**never a downgrade**), and
39
+ `skipped-readonly` is an equal-version re-sync a **read-only** skills dir blocked this session (a
40
+ stated skip, exit 0not a failure). Runs on **every** upgrade (equal-head too), no lineage-head
41
+ bump; a *could not refresh* line is non-fatal relay it plainly with its recovery.
42
42
 
43
43
  **Agent-rules lens refresh — stamp-independent, same gate, BEFORE the equal-head short-circuit.**
44
44
  Run `node ${CLAUDE_SKILL_DIR}/tools/lens-region.mjs reconcile <project>/docs/ai/agent_rules.md`
@@ -56,7 +56,7 @@ Requires: ${CLAUDE_SKILL_DIR}/references/shared/report-footer.md · ${CLAUDE_SKI
56
56
  **NEVER writes** it (the file lives outside every kit tree — D2), so an unknown/retired key is
57
57
  flagged + preserved, never edited. Runs on **every** upgrade; exit 0 covers every outcome.
58
58
  4. **Equal-head exit — a real successful-exit report, not a bare stop.** If the stamp **equals** the head, the lineage is up to date — but step 3 (the stamp-independent reconciles) ran first and may have changed things, so this is a proper exit report, not a no-op:
59
- - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
59
+ - **Report step 3's outcome in plain language** — for **each** pointer (workflow-methodology, orchestration-recipes and autonomy-policy) whether it was *added*, was *already present* (nothing changed), or was *skipped* (the soft-skip from step 3, with its reason — over the line limit / engine too old / the autonomy pointer's anchor absent); whether the `docs/ai/orchestration.json` config was *seeded* (created from the template), had its onboarding note *refreshed*, was *already current*, or carried a *customized note that was preserved* (a user edit is never clobbered); whether the `docs/ai/gates.json` gate declaration was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/verification-profile.json` profile was *seeded* or was *already present* (preserved byte-for-byte); whether the `docs/ai/autonomy.json` declaration was *seeded* (the sparse defaults-equivalent note) or was *already present* (preserved byte-for-byte); whether the enforcement-script ensure *added* the `archive-decisions` pair to `scripts/`, found it *already present*, or found an *old ADR layout — migration instructed*; the **placed-bridge refresh** outcome — paste the tool's per-bridge lines verbatim (they are already plain: *refreshed* / *already current* / *skipped — not placed* / `skipped-readonly` / *could not refresh* + recovery); the **agent-rules lens** outcome (*refreshed* / *already current* / *custom edit preserved + note* / *file absent* / *engine too old* / *over the line cap*); the **bridge-settings reconcile** outcome (paste the tool's line verbatim); and, for a hidden deployment, whether the hidden-mode footprint was *moved to project-local*, was *already project-local* (nothing changed), or needed a question (ambiguous visibility / a leftover machine-wide block). Plain wording only — never the reconcile/slot/anchor/marker terms (the never-leak-kit-internals Gotcha — `${CLAUDE_SKILL_DIR}/references/shared/deploy-tail.md`).
60
60
  - **Never surface the structure number on this exit.** Whatever step 3 did, do **not** recite the `docs/ai` structure version, the internal versioning vocabulary, or the two-axes note here — the number is inert on an equal-head exit; it belongs to *Version disclosure* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md` (shown at the never-downgrade STOP, the explicit status view, or on an explicit ask). Frame the success itself per the final bullet: if step 3 changed anything, say **what changed** in plain human terms; only a pure zero-diff no-op is *settings already current — no update needed*.
61
61
  - **Render the mandatory Recommendations section — on this exit too, BEFORE the footer.** Run `node ${CLAUDE_SKILL_DIR}/tools/recommendations.mjs --cwd <project-root>` and PRESENT its output — from the `## Recommendations (agent-workflow)` header — in the user's conversational language: every fact, count and item from the tool, nothing added or dropped; commands, paths, hosts and rule strings byte-exact; show the raw tool block on request. The section is present-even-when-empty (with everything optimal the body is exactly `no recommendations — flow optimal.`) and VERDICT-FIRST — the composed verdict line renders from the frozen templates `{K} item(s) need attention` / `nothing is broken` / `{N} optional recommendation(s), apply any you want` / `optimality NOT attested — {M} probe check(s) skipped`. Then OFFER the consent-gated applies: the user picks items in plain language; surface each picked item's posture note, get the explicit confirm, then run EXACTLY the rendered one-liners (a HAND-APPLY item is never run by you) — the full lane in `${CLAUDE_SKILL_DIR}/references/modes/recommendations.md`. Pinned order on this exit: Recommendations block → optional applies → report footer → the commit ask (the advisor/apply lane never lands after the commit ask).
62
62
  - **Live host/session facts are tool-composed only.** Any claim this report makes about the current host or session state — prompts fired, sandbox scope, whether a bypass was needed, network reachability, approval counts — must trace to **live tool output** from **this session** (the lines you just composed, or a probe you ran this run); a memory/handover snapshot is **context, never report facts**, and a claim with no live signal is **omitted or explicitly marked unverified** — never asserted from recollection. Full clause: *Live host/session facts* in `${CLAUDE_SKILL_DIR}/references/shared/report-footer.md`.
@@ -36,6 +36,7 @@ import {
36
36
  VERDICT_SKIPS_TEMPLATE,
37
37
  ACKS_FILE,
38
38
  } from './recommendations.mjs';
39
+ import { SKIPPED_READONLY } from './setup-backends.mjs';
39
40
 
40
41
  const KIT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
41
42
 
@@ -45,6 +46,7 @@ const AUTONOMY_DOCTOR_DOC = 'references/modes/autonomy-doctor.md';
45
46
  const RECOMMENDATIONS_DOC = 'references/modes/recommendations.md';
46
47
  const UPGRADE_DOC = 'references/modes/upgrade.md';
47
48
  const VELOCITY_DOC = 'references/modes/velocity.md';
49
+ const SETUP_DOC = 'references/modes/setup.md';
48
50
 
49
51
  // A typed usage failure (exit 2) for the CLI parser — the codebase's typed-error idiom (no classes).
50
52
  const usageFail = (message) => Object.assign(new Error(message), { exitCode: 2 });
@@ -99,6 +101,11 @@ export const BINDINGS = Object.freeze([
99
101
  // (the incident's "mode-doc apply text stays in lockstep" acceptance as a mechanism, not prose).
100
102
  // Bound in BOTH docs that name the path (recommendations.md + velocity.md).
101
103
  valueBinding('acks-file', ACKS_FILE, ACKS_FILE, [RECOMMENDATIONS_DOC, VELOCITY_DOC]),
104
+ // The refresh read-only degrade outcome (REFRESH-EROFS-HONESTY / AD-056): the new skipped-readonly
105
+ // token must render in BOTH mode contracts that enumerate the placed-bridge refresh outcomes
106
+ // (setup.md owns --refresh-placed; upgrade.md pastes its lines) — a reworded doc dropping the
107
+ // outcome fails this pin plus the gate. The token tracks the exported SETUP constant.
108
+ valueBinding('refresh-skipped-readonly', SKIPPED_READONLY, SKIPPED_READONLY, [SETUP_DOC, UPGRADE_DOC]),
102
109
  ].map((b) => Object.freeze(b)));
103
110
 
104
111
  // ── the pure checker (readText is injectable for hermetic tests) ────────────────────────
package/tools/fs-safe.mjs CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
18
  import {
19
19
  lstatSync, existsSync, mkdirSync, readdirSync, copyFileSync, readlinkSync, symlinkSync,
20
- rmSync, unlinkSync,
20
+ rmSync, unlinkSync, readFileSync,
21
21
  } from 'node:fs';
22
22
  import { dirname, join, resolve, relative, sep, isAbsolute } from 'node:path';
23
23
 
@@ -68,6 +68,27 @@ export const assertContainedRealPath = (root, dest, deps = {}) => {
68
68
  rel.split(sep).filter(Boolean).reduce(walk, root);
69
69
  };
70
70
 
71
+ // Read-only write-boundary tagging (REFRESH-EROFS-HONESTY / AD-056). A DESTINATION-side write failure
72
+ // of the read-only class is TAGGED (err.readonlyWriteBoundary) so the refresh-only driver can classify
73
+ // an equal-version repair-on-rerun that cannot write as a STATED skip — never a false red. The tag is
74
+ // applied ONLY around the three write primitives, so a READ-side failure (bundle read / dir listing)
75
+ // is never absorbed: the degrade classifies at the write boundary, not by a broad err.code sniff over
76
+ // the whole copy. EROFS is destination-side by nature; mkdir/symlink write only the dest; an
77
+ // EACCES/EPERM at copyFile (which also READS the source) is destination-provable ONLY when the source
78
+ // is readable — else it is a source-side read failure that must stay loud.
79
+ const READONLY_WRITE_ERRNOS = new Set(['EROFS', 'EACCES', 'EPERM']);
80
+ export const isReadonlyWriteBoundary = (err) => Boolean(err && err.readonlyWriteBoundary);
81
+ const throwTaggedReadonly = (err, primitive, src, readFile) => {
82
+ if (err && READONLY_WRITE_ERRNOS.has(err.code)) {
83
+ let destinationSide = err.code === 'EROFS' || primitive !== 'copyFile';
84
+ if (!destinationSide) {
85
+ try { readFile(src); destinationSide = true; } catch { destinationSide = false; }
86
+ }
87
+ if (destinationSide) err.readonlyWriteBoundary = true;
88
+ }
89
+ throw err;
90
+ };
91
+
71
92
  // Recursive refresh copy. Guards every dest via assertContainedRealPath first, then:
72
93
  // symlink src → additive: skip if dest exists, else mirror the link target.
73
94
  // directory src → mkdir -p dest, recurse.
@@ -80,20 +101,22 @@ export const copyTreeRefresh = (src, dest, root, deps = {}) => {
80
101
  const copyFile = deps.copyFile ?? copyFileSync;
81
102
  const readlink = deps.readlink ?? readlinkSync;
82
103
  const symlink = deps.symlink ?? symlinkSync;
104
+ const readFile = deps.readFile ?? readFileSync;
83
105
 
84
106
  assertContainedRealPath(root, dest, deps);
85
107
  const stat = lstat(src);
86
108
  if (stat.isSymbolicLink()) {
87
109
  if (exists(dest)) return;
88
- symlink(readlink(src), dest);
110
+ const target = readlink(src); // read-side (a readlink failure is never tagged as a write boundary)
111
+ try { symlink(target, dest); } catch (err) { throwTaggedReadonly(err, 'symlink', src, readFile); }
89
112
  } else if (stat.isDirectory()) {
90
- mkdir(dest);
113
+ try { mkdir(dest); } catch (err) { throwTaggedReadonly(err, 'mkdir', src, readFile); }
91
114
  for (const entry of readdir(src)) {
92
115
  copyTreeRefresh(join(src, entry), join(dest, entry), root, deps);
93
116
  }
94
117
  } else {
95
- mkdir(dirname(dest));
96
- copyFile(src, dest);
118
+ try { mkdir(dirname(dest)); } catch (err) { throwTaggedReadonly(err, 'mkdir', src, readFile); }
119
+ try { copyFile(src, dest); } catch (err) { throwTaggedReadonly(err, 'copyFile', src, readFile); }
97
120
  }
98
121
  };
99
122
 
@@ -31,7 +31,7 @@ import { join, resolve, relative, dirname, isAbsolute } from 'node:path';
31
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
32
32
  import os from 'node:os';
33
33
  import { KNOWN_BACKENDS, detectBackend, detectBackends, resolveDir, guideFor, READY } from './detect-backends.mjs';
34
- import { copyTreeRefresh, linkManaged } from './fs-safe.mjs';
34
+ import { copyTreeRefresh, linkManaged, isReadonlyWriteBoundary } from './fs-safe.mjs';
35
35
  import { validateManifest, readAuthoritativeVersion, UNSUPPORTED, INVALID } from './manifest/validate.mjs';
36
36
  import { compareSemver } from './semver-lite.mjs';
37
37
 
@@ -54,6 +54,12 @@ export const SETUP_STOP = 'SETUP_STOP';
54
54
  const stop = (message, fields = {}) =>
55
55
  Object.assign(new Error(`[agent-workflow-kit] ${message}`), { name: 'SetupStop', code: SETUP_STOP, ...fields });
56
56
 
57
+ // A refresh-only outcome (REFRESH-EROFS-HONESTY / AD-056): an equal-version repair-on-rerun whose
58
+ // write hit a READ-ONLY skills dir. A STATED skip (exit 0), never a false "could not refresh" — the
59
+ // versions are already current and `setup` would hit the same read-only dir. Doc-parity binds this
60
+ // token into references/modes/setup.md + upgrade.md so the mode contracts track the constant.
61
+ export const SKIPPED_READONLY = 'skipped-readonly';
62
+
57
63
  // ── injectable fs ──────────────────────────────────────────────────────────────
58
64
 
59
65
  const fsDeps = (deps = {}) => ({
@@ -557,13 +563,42 @@ const refreshSkillOnly = (entry, deps = {}) => {
557
563
  const fresh = inspectSkillDir(entry, deps);
558
564
  if (fresh.action !== 'refresh') return { refreshed: false, drift: null };
559
565
  assertNoDowngrade(fresh, deps);
560
- const drift = copyBridgeWithHonesty('refresh', fresh.bundleDir, fresh.skillDir, deps);
561
- return { refreshed: true, drift };
566
+ // D1b: version equality for the read-only degrade is decided from the APPLY-TIME fresh inspection's
567
+ // fields (never the stale pre-apply plan) — both versions KNOWN and equal; two unknowns are not equal.
568
+ // assertNoDowngrade just read the bundled manifest, so this read cannot throw here; a versionless
569
+ // manifest yields null (never "equal" → no false skip).
570
+ const bundledManifest = readBundledManifest(fresh.bundleDir, deps);
571
+ const bundledVersion = typeof bundledManifest.version === 'string' ? bundledManifest.version : null;
572
+ const placedVersion = readPlacedVersionSafe(fresh.skillDir, deps);
573
+ const currentEqual = bundledVersion !== null && placedVersion === bundledVersion;
574
+ try {
575
+ const drift = copyBridgeWithHonesty('refresh', fresh.bundleDir, fresh.skillDir, deps);
576
+ return { refreshed: true, drift };
577
+ } catch (err) {
578
+ // D1/D1a: ONLY a read-only WRITE-boundary failure at an EQUAL version is a repair-on-rerun that
579
+ // cannot run — a STATED skip, never a false "could not refresh". Every other failure stays loud
580
+ // (a read-side / source-side / EIO failure, or a version-behind upgrade, re-throws here).
581
+ if (currentEqual && isReadonlyWriteBoundary(err)) return { refreshed: false, skippedReadonly: true, version: bundledVersion };
582
+ throw err;
583
+ }
562
584
  };
563
585
 
564
586
  const NOT_PLACED_LINE = 'skipped — not placed (placement is opt-in: /agent-workflow-kit setup)';
565
587
  const stripPrefix = (message) => message.replace('[agent-workflow-kit] ', '');
566
588
 
589
+ // The read-only degrade wording (D1). The STATED-skip line: version current + the re-sync
590
+ // skipped/incomplete + the read-only cause + the residual — it never claims a re-sync RAN (that is
591
+ // already-current's line) nor file integrity (a partial copy may have happened before the throw). The
592
+ // FAILED line (a version-behind upgrade blocked by the same read-only dir) stays loud, but its
593
+ // recovery points at a writable rerun — the in-session `setup` would hit the same read-only dir.
594
+ const READONLY_RERUN_HINT = 're-run the refresh from a writable session (e.g. outside the read-only sandbox)';
595
+ const skippedReadonlyLine = (name, version) =>
596
+ ` ${name}: already current${version ? ` (v${version})` : ''} — the re-sync was skipped/incomplete: ` +
597
+ `the skills directory is read-only this session (the tree may be PARTIALLY updated). Repair-on-rerun cannot ` +
598
+ `run here; any remaining drift persists until you ${READONLY_RERUN_HINT}.`;
599
+ const readonlyRefreshFailedLine = (name, message) =>
600
+ ` ${name}: could not refresh — ${stripPrefix(message)}; the skills directory is read-only this session — ${READONLY_RERUN_HINT}`;
601
+
567
602
  // Refresh every ALREADY-PLACED bridge from the kit's bundled copies and re-link its wrappers (a newer
568
603
  // bridge can add one). One reported outcome per backend — never a crash; a per-backend STOP/error
569
604
  // becomes a `failed` result. `line` is the tool-composed sentence callers print VERBATIM (the agent
@@ -592,6 +627,11 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
592
627
  return { name: plan.name, outcome: 'failed', line: ` ${plan.name}: could not refresh — ${stripPrefix(plan.reason)}; recover with /agent-workflow-kit setup` };
593
628
  }
594
629
  const refresh = refreshSkillOnly(registryEntry(plan.name), deps);
630
+ // A read-only skills dir at an EQUAL version is a STATED skip (repair-on-rerun cannot run here) —
631
+ // never a false "could not refresh" and never a failure exit (D1/AD-056).
632
+ if (refresh.skippedReadonly) {
633
+ return { name: plan.name, outcome: SKIPPED_READONLY, line: skippedReadonlyLine(plan.name, refresh.version) };
634
+ }
595
635
  if (!refresh.refreshed) {
596
636
  return { name: plan.name, outcome: 'not-placed', line: ` ${plan.name}: ${NOT_PLACED_LINE}` };
597
637
  }
@@ -615,6 +655,11 @@ export const refreshPlacedBridges = (deps = {}, names = KNOWN_BACKENDS.map((b) =
615
655
  if (err.wouldDowngrade) {
616
656
  return { name: canonical, outcome: 'kept-newer', line: ` ${canonical}: skipped — ${stripPrefix(err.message)}` };
617
657
  }
658
+ // A read-only WRITE failure on a version-BEHIND (upgrade) refresh stays a loud failure — but the
659
+ // in-session "recover with setup" would hit the same read-only dir, so point at a writable rerun.
660
+ if (isReadonlyWriteBoundary(err)) {
661
+ return { name: canonical, outcome: 'failed', line: readonlyRefreshFailedLine(canonical, err.message) };
662
+ }
618
663
  return { name: canonical, outcome: 'failed', line: ` ${canonical}: could not refresh — ${stripPrefix(err.message)}; recover with /agent-workflow-kit setup` };
619
664
  }
620
665
  });