@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 +27 -0
- package/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/SKILL.md +1 -1
- package/bridges/antigravity-cli-bridge/bin/agy-review.sh +21 -3
- package/bridges/antigravity-cli-bridge/bin/agy-review.test.mjs +3 -2
- package/bridges/antigravity-cli-bridge/bin/agy.sh +20 -2
- package/bridges/antigravity-cli-bridge/bin/agy.test.mjs +3 -2
- package/bridges/antigravity-cli-bridge/capability.json +1 -1
- package/bridges/codex-cli-bridge/SKILL.md +1 -1
- package/bridges/codex-cli-bridge/bin/codex-exec.sh +20 -2
- package/bridges/codex-cli-bridge/bin/codex-exec.test.mjs +3 -2
- package/bridges/codex-cli-bridge/bin/codex-review.sh +21 -3
- package/bridges/codex-cli-bridge/bin/codex-review.test.mjs +3 -2
- package/bridges/codex-cli-bridge/capability.json +1 -1
- package/capability.json +1 -1
- package/package.json +1 -1
- package/references/modes/setup.md +7 -2
- package/references/modes/upgrade.md +9 -9
- package/tools/doc-parity.mjs +7 -0
- package/tools/fs-safe.mjs +28 -5
- package/tools/setup-backends.mjs +48 -3
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.
|
|
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.
|
|
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 ]] &&
|
|
140
|
-
CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] &&
|
|
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.
|
|
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
|
-
|
|
998
|
-
|
|
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 ]] &&
|
|
107
|
-
CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] &&
|
|
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
|
-
|
|
447
|
-
|
|
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') {
|
|
@@ -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.
|
|
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 ]] &&
|
|
120
|
-
CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] &&
|
|
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
|
-
|
|
1084
|
-
|
|
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 ]] &&
|
|
112
|
-
CODEX_REVIEW_MAX_TOTAL_BYTES) [[ "$v" =~ $int_re ]] &&
|
|
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.
|
|
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
|
-
|
|
1024
|
-
|
|
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') {
|
package/capability.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sabaiway/agent-workflow-kit",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
15
|
-
|
|
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)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
non-fatal
|
|
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 0 — not 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`.
|
package/tools/doc-parity.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/tools/setup-backends.mjs
CHANGED
|
@@ -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
|
-
|
|
561
|
-
|
|
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
|
});
|