@warpgogol/forge 2.21.7 → 3.0.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.
@@ -76,6 +76,327 @@ workspace:
76
76
  .turbo/
77
77
  .cache/
78
78
  operator-profile.md
79
+ - path: scripts/git-guard.sh
80
+ content: |
81
+ #!/bin/bash
82
+ # git-guard.sh — shell function that intercepts destructive git commands.
83
+ # Sourced by ~/.zshenv (via setup-git-guards.sh).
84
+ # Guards (only active in repos containing scripts/git-guard.sh):
85
+ # git stash, git reset --hard, git checkout --, git checkout -f,
86
+ # git switch -f, git clean -f, git restore
87
+ # Override: ALLOW_DESTRUCTIVE_GIT=1 git stash
88
+
89
+ export FORGE_GIT_GUARD=1
90
+
91
+ _git_guard_find_root() {
92
+ local _repo_root _dir
93
+ _repo_root="$(command git rev-parse --show-toplevel 2>/dev/null || echo "")"
94
+ if [ -z "$_repo_root" ]; then echo ""; return; fi
95
+ if [ -f "$_repo_root/scripts/git-guard.sh" ]; then echo "$_repo_root"; return; fi
96
+ _dir="$(dirname "$_repo_root")"
97
+ while [ -n "$_dir" ] && [ "$_dir" != "/" ]; do
98
+ if [ -f "$_dir/scripts/git-guard.sh" ]; then echo "$_dir"; return; fi
99
+ _dir="$(dirname "$_dir")"
100
+ done
101
+ echo ""
102
+ }
103
+
104
+ git() {
105
+ local _guard_root
106
+ _guard_root="$(_git_guard_find_root)"
107
+ if [ -z "$_guard_root" ] || [ -n "${ALLOW_DESTRUCTIVE_GIT:-}" ]; then
108
+ command git "$@"; return $?
109
+ fi
110
+ local _all_args=("$@") _i=0 _cmd="" _cmd_index=0
111
+ while [ "$_i" -lt "${#_all_args[@]}" ]; do
112
+ local _arg="${_all_args[$_i]}"
113
+ case "$_arg" in
114
+ -C|-c|--git-dir|--work-tree|--namespace) _i=$((_i + 2)); continue ;;
115
+ -*) _i=$((_i + 1)); continue ;;
116
+ esac
117
+ _cmd="$_arg"; _cmd_index="$_i"; break
118
+ done
119
+ local _sub_args=() _j=$((_cmd_index + 1))
120
+ while [ "$_j" -lt "${#_all_args[@]}" ]; do
121
+ _sub_args+=("${_all_args[$_j]}"); _j=$((_j + 1))
122
+ done
123
+ case "$_cmd" in
124
+ stash)
125
+ echo "BLOCKED: git stash is disabled in agent sessions." >&2
126
+ echo "Set ALLOW_DESTRUCTIVE_GIT=1 to override." >&2; return 1 ;;
127
+ reset)
128
+ for _arg in "${_sub_args[@]}"; do
129
+ if [ "$_arg" = "--hard" ] || [[ "$_arg" == --hard=* ]]; then
130
+ echo "BLOCKED: git reset --hard is disabled." >&2; return 1
131
+ fi
132
+ done; command git "$@"; return $? ;;
133
+ checkout)
134
+ for _arg in "${_sub_args[@]}"; do
135
+ if [ "$_arg" = "--" ]; then
136
+ echo "BLOCKED: git checkout -- is disabled." >&2; return 1
137
+ fi
138
+ case "$_arg" in -f*|--force*)
139
+ echo "BLOCKED: git checkout -f is disabled." >&2; return 1 ;;
140
+ esac
141
+ done; command git "$@"; return $? ;;
142
+ switch)
143
+ for _arg in "${_sub_args[@]}"; do
144
+ case "$_arg" in -f*|--force*)
145
+ echo "BLOCKED: git switch -f is disabled." >&2; return 1 ;;
146
+ esac
147
+ done; command git "$@"; return $? ;;
148
+ restore)
149
+ echo "BLOCKED: git restore is disabled." >&2; return 1 ;;
150
+ clean)
151
+ for _arg in "${_sub_args[@]}"; do
152
+ case "$_arg" in -f*|--force*)
153
+ echo "BLOCKED: git clean -f is disabled." >&2; return 1 ;;
154
+ esac
155
+ done; command git "$@"; return $? ;;
156
+ *) command git "$@"; return $? ;;
157
+ esac
158
+ }
159
+ - path: scripts/setup-git-guards.sh
160
+ content: |
161
+ #!/bin/bash
162
+ # setup-git-guards.sh — install shell function guards for destructive git ops.
163
+ # Auto-detects shell: zsh → ~/.zshenv, bash → ~/.bashrc (Git Bash on Windows).
164
+ set -euo pipefail
165
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
166
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
167
+ guard_script="$repo_root/scripts/git-guard.sh"
168
+ marker="# forge-git-guard"
169
+ # Auto-detect target profile file based on current shell
170
+ case "${SHELL:-}" in
171
+ *zsh*) profile_file="${HOME}/.zshenv" ;;
172
+ *bash*) profile_file="${HOME}/.bashrc" ;;
173
+ *) profile_file="${HOME}/.bashrc" ;;
174
+ esac
175
+ mode="${1:-install}"
176
+ case "$mode" in
177
+ install)
178
+ if [ ! -f "$guard_script" ]; then echo "ERROR: $guard_script not found" >&2; exit 1; fi
179
+ chmod +x "$guard_script" 2>/dev/null || true
180
+ if [ -f "$profile_file" ]; then
181
+ tmp="$(mktemp)"
182
+ awk -v marker="$marker" '$0 == marker { skip=2; next } skip > 0 { skip--; next } { print }' "$profile_file" > "$tmp" || true
183
+ mv "$tmp" "$profile_file" 2>/dev/null || true
184
+ fi
185
+ echo "" >> "$profile_file"
186
+ echo "$marker" >> "$profile_file"
187
+ echo "[ -f \"$guard_script\" ] && source \"$guard_script\"" >> "$profile_file"
188
+ echo "Git guards installed into $profile_file. Restart your shell or run: source $guard_script" ;;
189
+ --verify)
190
+ if [ -z "${FORGE_GIT_GUARD:-}" ]; then echo "MISSING: git guard not loaded" >&2; exit 1; fi
191
+ if ! grep -qF "$marker" "$profile_file" 2>/dev/null; then echo "MISSING: guard not in $profile_file" >&2; exit 1; fi
192
+ echo "OK: git guards installed and loaded" ;;
193
+ --remove)
194
+ if [ -f "$profile_file" ]; then
195
+ tmp="$(mktemp)"
196
+ awk -v marker="$marker" '$0 == marker { skip=2; next } skip > 0 { skip--; next } { print }' "$profile_file" > "$tmp" || true
197
+ mv "$tmp" "$profile_file" 2>/dev/null || true
198
+ fi
199
+ echo "Git guards removed from $profile_file" ;;
200
+ *) echo "Usage: bash scripts/setup-git-guards.sh [install|--verify|--remove]" >&2; exit 1 ;;
201
+ esac
202
+ - path: scripts/clean-stale-stashes.sh
203
+ content: |
204
+ #!/bin/bash
205
+ # clean-stale-stashes.sh — detect and optionally drop stale git stash entries.
206
+ # Usage: bash scripts/clean-stale-stashes.sh [--drop|--drop-all]
207
+ set -euo pipefail
208
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
209
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
210
+ mode="report"
211
+ if [ "${1:-}" = "--drop" ]; then mode="drop"; elif [ "${1:-}" = "--drop-all" ]; then mode="drop-all"; fi
212
+ stash_count="$(git -C "$repo_root" stash list 2>/dev/null | wc -l)"
213
+ if [ "$stash_count" -eq 0 ]; then exit 0; fi
214
+ stale_indices=() total="$stash_count"
215
+ for ((i = 0; i < total; i++)); do
216
+ ref="stash@{$i}"
217
+ if [ "$mode" = "drop-all" ]; then stale_indices+=("$i"); continue; fi
218
+ tracked_changes="$(git -C "$repo_root" stash show "$ref" --stat 2>/dev/null || true)"
219
+ untracked_files="$(git -C "$repo_root" stash show --include-untracked "$ref" --stat 2>/dev/null || true)"
220
+ if [ -z "$tracked_changes" ] && [ -z "$untracked_files" ]; then stale_indices+=("$i"); continue; fi
221
+ if [ -z "$tracked_changes" ] && [ -n "$untracked_files" ]; then
222
+ all_committed=true
223
+ while IFS= read -r filepath; do
224
+ [ -z "$filepath" ] && continue
225
+ if ! git -C "$repo_root" cat-file -e "HEAD:$filepath" 2>/dev/null; then all_committed=false; break; fi
226
+ done < <(git -C "$repo_root" stash show --include-untracked "$ref" --name-only 2>/dev/null || true)
227
+ if [ "$all_committed" = true ]; then stale_indices+=("$i"); fi
228
+ fi
229
+ done
230
+ if [ ${#stale_indices[@]} -eq 0 ]; then exit 0; fi
231
+ if [ "$mode" = "report" ]; then
232
+ echo "STALE STASH ENTRIES DETECTED (${#stale_indices[@]} of $stash_count):"
233
+ for idx in "${stale_indices[@]}"; do
234
+ echo " stash@{$idx}: $(git -C "$repo_root" stash list | sed -n "$((idx + 1))p")"
235
+ done
236
+ echo "Clean with: bash scripts/clean-stale-stashes.sh --drop"; exit 1
237
+ fi
238
+ if [ "$mode" = "drop-all" ]; then git -C "$repo_root" stash clear; echo "Dropped all $stash_count entries."; exit 0; fi
239
+ for ((i = ${#stale_indices[@]} - 1; i >= 0; i--)); do
240
+ idx="${stale_indices[$i]}"
241
+ git -C "$repo_root" stash drop "stash@{$idx}" 2>/dev/null || true
242
+ done
243
+ echo "Dropped ${#stale_indices[@]} stale entry/entries."; exit 0
244
+ - path: scripts/check-clean-trees.sh
245
+ content: |
246
+ #!/bin/bash
247
+ # check-clean-trees.sh — verify all git trees are clean.
248
+ # Checks repo root + all nested .git directories up to 3 levels deep.
249
+ set -euo pipefail
250
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
251
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
252
+ dirty=0 output=""
253
+ main_status="$(git -C "$repo_root" status --short 2>/dev/null || true)"
254
+ if [ -n "$main_status" ]; then
255
+ output+="DIRTY: $repo_root\n$(printf '%s\n' "$main_status" | sed 's/^/ /')\n"
256
+ dirty=1
257
+ fi
258
+ while IFS= read -r gitdir; do
259
+ nested_root="$(dirname "$gitdir")"
260
+ [ "$nested_root" = "$repo_root" ] && continue
261
+ nested_status="$(git -C "$nested_root" status --short 2>/dev/null || true)"
262
+ if [ -n "$nested_status" ]; then
263
+ output+="DIRTY: $nested_root\n$(printf '%s\n' "$nested_status" | sed 's/^/ /')\n"
264
+ dirty=1
265
+ fi
266
+ done < <(find "$repo_root" -maxdepth 3 -name ".git" -type d 2>/dev/null | head -20)
267
+ if [ "$dirty" -eq 1 ]; then
268
+ echo -e "$output" | head -40
269
+ echo "---"
270
+ echo "Uncommitted changes detected. Commit before session end."
271
+ exit 1
272
+ fi
273
+ exit 0
274
+ - path: hooks/pre-user-prompt.sh
275
+ content: |
276
+ #!/bin/bash
277
+ # Pre-user-prompt hook: session-end protocol + stale-stash + git-guard checks.
278
+ # Requires jq for JSON payload parsing.
279
+ set -euo pipefail
280
+ payload="$(cat)"
281
+ user_prompt="$(printf '%s' "$payload" | jq -r '.tool_info.user_prompt // empty' 2>/dev/null || true)"
282
+ if [ -z "$user_prompt" ]; then exit 0; fi
283
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
284
+ if [ -n "$repo_root" ] && [ -x "$repo_root/scripts/clean-stale-stashes.sh" ]; then
285
+ stash_output="$("$repo_root/scripts/clean-stale-stashes.sh" 2>&1 || true)"
286
+ if [ -n "$stash_output" ]; then
287
+ echo "⚠️ STALE GIT STASH DETECTED" >&2
288
+ echo "$stash_output" >&2
289
+ echo "Clean with: bash scripts/clean-stale-stashes.sh --drop" >&2
290
+ echo "" >&2
291
+ fi
292
+ fi
293
+ if [ -n "$repo_root" ] && [ -f "$repo_root/scripts/git-guard.sh" ]; then
294
+ if [ -z "${FORGE_GIT_GUARD:-}" ]; then
295
+ echo "⚠️ GIT GUARD NOT LOADED — destructive git operations are NOT blocked." >&2
296
+ echo "Run: source $repo_root/scripts/git-guard.sh" >&2
297
+ echo "" >&2
298
+ fi
299
+ fi
300
+ session_end_phrases=("Завершаем эту сессию" "Завершаем сессию" "Заканчиваем сессию" "Завершить сессию" "End session" "Wrap up" "Session end" "/session-end")
301
+ matched=""
302
+ for phrase in "${session_end_phrases[@]}"; do
303
+ if printf '%s' "$user_prompt" | grep -qiF "$phrase"; then matched="$phrase"; break; fi
304
+ done
305
+ if [ -z "$matched" ]; then exit 0; fi
306
+ cat >&2 <<EOF
307
+ SESSION-END PROTOCOL TRIGGERED (matched: "$matched")
308
+ The user's original message was:
309
+ > $user_prompt
310
+ You MUST invoke the fo-session-retro skill via the skill tool BEFORE producing
311
+ any other output. This is a NON-NEGOTIABLE BLOCKED GATE per PREFERENCES.md.
312
+ DO NOT produce a closing summary or ad-hoc output. The closing block must come
313
+ from fo-session-retro's report.
314
+ Protocol steps:
315
+ 1. Verify clean working trees — run: bash scripts/check-clean-trees.sh
316
+ 2. Invoke fo-session-retro via the skill tool
317
+ 3. The retro skill's report IS the session-end output
318
+ EOF
319
+ if [ -n "$repo_root" ] && [ -x "$repo_root/scripts/check-clean-trees.sh" ]; then
320
+ tree_output="$("$repo_root/scripts/check-clean-trees.sh" 2>&1 || true)"
321
+ if [ -n "$tree_output" ]; then echo "" >&2; echo "$tree_output" >&2; fi
322
+ fi
323
+ exit 2
324
+ - path: hooks/pre-user-prompt-wrapper.mjs
325
+ content: |
326
+ #!/usr/bin/env node
327
+ // pre-user-prompt-wrapper.mjs — cross-platform wrapper for pre-user-prompt.sh.
328
+ // Detects bash availability, prints one-time warning if bash not found,
329
+ // then delegates to bash hooks/pre-user-prompt.sh with stdin piped.
330
+ import { execFileSync, spawn } from "node:child_process";
331
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
332
+ import { dirname, join } from "node:path";
333
+ import { fileURLToPath } from "node:url";
334
+
335
+ const __dirname = dirname(fileURLToPath(import.meta.url));
336
+ const scriptPath = join(__dirname, "pre-user-prompt.sh");
337
+ const cacheDir = join(__dirname, "..", ".cache");
338
+ const warningFlag = join(cacheDir, "git-guard-warning-sent");
339
+
340
+ // If pre-user-prompt.sh doesn't exist, silently exit
341
+ if (!existsSync(scriptPath)) {
342
+ process.exit(0);
343
+ }
344
+
345
+ // Check if bash is available
346
+ let bashAvailable = false;
347
+ try {
348
+ execFileSync("bash", ["--version"], { stdio: "ignore" });
349
+ bashAvailable = true;
350
+ } catch {
351
+ bashAvailable = false;
352
+ }
353
+
354
+ if (!bashAvailable) {
355
+ // Print one-time warning if not already sent
356
+ if (!existsSync(warningFlag)) {
357
+ console.error("⚠️ bash not found — pre-user-prompt hook is disabled.");
358
+ console.error("Install Git Bash (Git for Windows) or WSL to enable agent-safety hooks.");
359
+ console.error("");
360
+ try {
361
+ mkdirSync(cacheDir, { recursive: true });
362
+ writeFileSync(warningFlag, new Date().toISOString(), "utf8");
363
+ } catch {
364
+ // Best-effort — don't crash if cache dir is not writable
365
+ }
366
+ }
367
+ process.exit(0);
368
+ }
369
+
370
+ // Delegate to bash script with stdin piped
371
+ const child = spawn("bash", [scriptPath], { stdio: ["inherit", "inherit", "inherit"] });
372
+ child.on("exit", (code) => {
373
+ process.exit(code ?? 0);
374
+ });
375
+ - path: .windsurf/hooks.json
376
+ content: |
377
+ {
378
+ "hooks": {
379
+ "pre_user_prompt": [
380
+ {
381
+ "command": "node $ROOT_WORKSPACE_PATH/hooks/pre-user-prompt-wrapper.mjs",
382
+ "show_output": true
383
+ }
384
+ ]
385
+ }
386
+ }
387
+ - path: .forge/pinned.yaml
388
+ content: |
389
+ # Pinned foundation files (DNA-62, RFC-0733)
390
+ # Protect mode: warns on delete/move. Freeze mode: blocks modify too.
391
+ entries:
392
+ - path: scripts/git-guard.sh
393
+ mode: protect
394
+ - path: scripts/setup-git-guards.sh
395
+ mode: protect
396
+ - path: hooks/pre-user-prompt.sh
397
+ mode: protect
398
+ - path: hooks/pre-user-prompt-wrapper.mjs
399
+ mode: protect
79
400
  - path: knowledge/manifest.yaml
80
401
  content: |
81
402
  # Knowledge manifest — canonical dataset identity