@warpgogol/forge 2.21.8 → 4.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.
package/AGENTS.md CHANGED
@@ -81,6 +81,18 @@ The pinned-files protection system prevents accidental deletion, move, or modifi
81
81
  - **Archive pre-check:** All 6 archive handlers (`rfc.archive`, `adr.archive`, `plan.archive`, `audit.archive`, `session.archive`, `mission.archive`) load the pinned manifest once per invocation and skip pinned files/directories with a warning instead of moving them. When the manifest is missing, archive handlers behave as before (protection inactive). **Intra-directory moves are exempted:** if both source and destination are within the same pinned directory (e.g. `docs/rfcs/rfc-0076.md` → `docs/rfcs/archive/implemented/rfc-0076.md`), the move is allowed — the file hasn't left the protected directory.
82
82
  - **`.forge/` directory:** The `.forge/` directory is a forge-specific convention for project-local governance files. It sits alongside `forge.yaml` and contains `pinned.yaml` (manifest) and `pinned-audit.log` (override audit trail).
83
83
 
84
+ ## Agent-safety scripts in profiles (RFC-1019)
85
+
86
+ All Forge stack profiles include portable agent-safety scripts as embedded file content blocks. These scripts protect AI agents from destructive git operations and enforce session-end protocol.
87
+
88
+ - **`scripts/git-guard.sh`** — shell function intercepting `git stash`, `git reset --hard`, `git checkout --`, `git checkout -f`, `git switch -f`, `git clean -f`, `git restore`. Override: `ALLOW_DESTRUCTIVE_GIT=1`. Uses `FORGE_GIT_GUARD` marker (not `WERKSTATT_GIT_GUARD`).
89
+ - **`scripts/setup-git-guards.sh`** — installs git-guard.sh into `~/.zshenv`. Supports `install`, `--verify`, `--remove` modes.
90
+ - **`scripts/clean-stale-stashes.sh`** — detects and optionally drops stale git stash entries. Report mode (`--drop` to clean, `--drop-all` for all).
91
+ - **`scripts/check-clean-trees.sh`** — universal dirty-tree checker. Finds all `.git` directories up to 3 levels deep and checks each for dirty state.
92
+ - **`hooks/pre-user-prompt.sh`** — session-end protocol enforcement + stale-stash detection + git-guard verification. Requires `jq`. Exits with code 2 on session-end trigger phrases to inject protocol instructions.
93
+ - **`.windsurf/hooks.json`** — Windsurf hook registration for `pre_user_prompt`.
94
+ - **PREFERENCES.md** generated by `forge init` includes `formOfAddress` field and operational rules: plan confirmation vs implementation, skill invocation tracking, commit granularity, session-end protocol.
95
+
84
96
  ## Skills
85
97
 
86
98
  Skills live in `skills/` and are synced to `.agents/skills/` by `create`. Each skill has a `SKILL.md` with standardized frontmatter (name, description, category, concerns, dependsOn).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warpgogol/forge",
3
- "version": "2.21.8",
3
+ "version": "4.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -220,6 +220,314 @@ workspace:
220
220
  # Runs forge validation before commits
221
221
  echo "Pre-commit: running platform checks..."
222
222
  pnpm exec forge run forge.validate || exit 1
223
+ - path: scripts/git-guard.sh
224
+ content: |
225
+ #!/bin/bash
226
+ # git-guard.sh — shell function that intercepts destructive git commands.
227
+ # Sourced by ~/.zshenv (via setup-git-guards.sh).
228
+ # Guards (only active in repos containing scripts/git-guard.sh):
229
+ # git stash, git reset --hard, git checkout --, git checkout -f,
230
+ # git switch -f, git clean -f, git restore
231
+ # Override: ALLOW_DESTRUCTIVE_GIT=1 git stash
232
+
233
+ export FORGE_GIT_GUARD=1
234
+
235
+ _git_guard_find_root() {
236
+ local _repo_root _dir
237
+ _repo_root="$(command git rev-parse --show-toplevel 2>/dev/null || echo "")"
238
+ if [ -z "$_repo_root" ]; then echo ""; return; fi
239
+ if [ -f "$_repo_root/scripts/git-guard.sh" ]; then echo "$_repo_root"; return; fi
240
+ _dir="$(dirname "$_repo_root")"
241
+ while [ -n "$_dir" ] && [ "$_dir" != "/" ]; do
242
+ if [ -f "$_dir/scripts/git-guard.sh" ]; then echo "$_dir"; return; fi
243
+ _dir="$(dirname "$_dir")"
244
+ done
245
+ echo ""
246
+ }
247
+
248
+ git() {
249
+ local _guard_root
250
+ _guard_root="$(_git_guard_find_root)"
251
+ if [ -z "$_guard_root" ] || [ -n "${ALLOW_DESTRUCTIVE_GIT:-}" ]; then
252
+ command git "$@"; return $?
253
+ fi
254
+ local _all_args=("$@") _i=0 _cmd="" _cmd_index=0
255
+ while [ "$_i" -lt "${#_all_args[@]}" ]; do
256
+ local _arg="${_all_args[$_i]}"
257
+ case "$_arg" in
258
+ -C|-c|--git-dir|--work-tree|--namespace) _i=$((_i + 2)); continue ;;
259
+ -*) _i=$((_i + 1)); continue ;;
260
+ esac
261
+ _cmd="$_arg"; _cmd_index="$_i"; break
262
+ done
263
+ local _sub_args=() _j=$((_cmd_index + 1))
264
+ while [ "$_j" -lt "${#_all_args[@]}" ]; do
265
+ _sub_args+=("${_all_args[$_j]}"); _j=$((_j + 1))
266
+ done
267
+ case "$_cmd" in
268
+ stash)
269
+ echo "BLOCKED: git stash is disabled in agent sessions." >&2
270
+ echo "Set ALLOW_DESTRUCTIVE_GIT=1 to override." >&2; return 1 ;;
271
+ reset)
272
+ for _arg in "${_sub_args[@]}"; do
273
+ if [ "$_arg" = "--hard" ] || [[ "$_arg" == --hard=* ]]; then
274
+ echo "BLOCKED: git reset --hard is disabled." >&2; return 1
275
+ fi
276
+ done; command git "$@"; return $? ;;
277
+ checkout)
278
+ for _arg in "${_sub_args[@]}"; do
279
+ if [ "$_arg" = "--" ]; then
280
+ echo "BLOCKED: git checkout -- is disabled." >&2; return 1
281
+ fi
282
+ case "$_arg" in -f*|--force*)
283
+ echo "BLOCKED: git checkout -f is disabled." >&2; return 1 ;;
284
+ esac
285
+ done; command git "$@"; return $? ;;
286
+ switch)
287
+ for _arg in "${_sub_args[@]}"; do
288
+ case "$_arg" in -f*|--force*)
289
+ echo "BLOCKED: git switch -f is disabled." >&2; return 1 ;;
290
+ esac
291
+ done; command git "$@"; return $? ;;
292
+ restore)
293
+ echo "BLOCKED: git restore is disabled." >&2; return 1 ;;
294
+ clean)
295
+ for _arg in "${_sub_args[@]}"; do
296
+ case "$_arg" in -f*|--force*)
297
+ echo "BLOCKED: git clean -f is disabled." >&2; return 1 ;;
298
+ esac
299
+ done; command git "$@"; return $? ;;
300
+ *) command git "$@"; return $? ;;
301
+ esac
302
+ }
303
+ - path: scripts/setup-git-guards.sh
304
+ content: |
305
+ #!/bin/bash
306
+ # setup-git-guards.sh — install shell function guards for destructive git ops.
307
+ # Auto-detects shell: zsh → ~/.zshenv, bash → ~/.bashrc (Git Bash on Windows).
308
+ set -euo pipefail
309
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
310
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
311
+ guard_script="$repo_root/scripts/git-guard.sh"
312
+ marker="# forge-git-guard"
313
+ # Auto-detect target profile file based on current shell
314
+ case "${SHELL:-}" in
315
+ *zsh*) profile_file="${HOME}/.zshenv" ;;
316
+ *bash*) profile_file="${HOME}/.bashrc" ;;
317
+ *) profile_file="${HOME}/.bashrc" ;;
318
+ esac
319
+ mode="${1:-install}"
320
+ case "$mode" in
321
+ install)
322
+ if [ ! -f "$guard_script" ]; then echo "ERROR: $guard_script not found" >&2; exit 1; fi
323
+ chmod +x "$guard_script" 2>/dev/null || true
324
+ if [ -f "$profile_file" ]; then
325
+ tmp="$(mktemp)"
326
+ awk -v marker="$marker" '$0 == marker { skip=2; next } skip > 0 { skip--; next } { print }' "$profile_file" > "$tmp" || true
327
+ mv "$tmp" "$profile_file" 2>/dev/null || true
328
+ fi
329
+ echo "" >> "$profile_file"
330
+ echo "$marker" >> "$profile_file"
331
+ echo "[ -f \"$guard_script\" ] && source \"$guard_script\"" >> "$profile_file"
332
+ echo "Git guards installed into $profile_file. Restart your shell or run: source $guard_script" ;;
333
+ --verify)
334
+ if [ -z "${FORGE_GIT_GUARD:-}" ]; then echo "MISSING: git guard not loaded" >&2; exit 1; fi
335
+ if ! grep -qF "$marker" "$profile_file" 2>/dev/null; then echo "MISSING: guard not in $profile_file" >&2; exit 1; fi
336
+ echo "OK: git guards installed and loaded" ;;
337
+ --remove)
338
+ if [ -f "$profile_file" ]; then
339
+ tmp="$(mktemp)"
340
+ awk -v marker="$marker" '$0 == marker { skip=2; next } skip > 0 { skip--; next } { print }' "$profile_file" > "$tmp" || true
341
+ mv "$tmp" "$profile_file" 2>/dev/null || true
342
+ fi
343
+ echo "Git guards removed from $profile_file" ;;
344
+ *) echo "Usage: bash scripts/setup-git-guards.sh [install|--verify|--remove]" >&2; exit 1 ;;
345
+ esac
346
+ - path: scripts/clean-stale-stashes.sh
347
+ content: |
348
+ #!/bin/bash
349
+ # clean-stale-stashes.sh — detect and optionally drop stale git stash entries.
350
+ # Usage: bash scripts/clean-stale-stashes.sh [--drop|--drop-all]
351
+ set -euo pipefail
352
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
353
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
354
+ mode="report"
355
+ if [ "${1:-}" = "--drop" ]; then mode="drop"; elif [ "${1:-}" = "--drop-all" ]; then mode="drop-all"; fi
356
+ stash_count="$(git -C "$repo_root" stash list 2>/dev/null | wc -l)"
357
+ if [ "$stash_count" -eq 0 ]; then exit 0; fi
358
+ stale_indices=() total="$stash_count"
359
+ for ((i = 0; i < total; i++)); do
360
+ ref="stash@{$i}"
361
+ if [ "$mode" = "drop-all" ]; then stale_indices+=("$i"); continue; fi
362
+ tracked_changes="$(git -C "$repo_root" stash show "$ref" --stat 2>/dev/null || true)"
363
+ untracked_files="$(git -C "$repo_root" stash show --include-untracked "$ref" --stat 2>/dev/null || true)"
364
+ if [ -z "$tracked_changes" ] && [ -z "$untracked_files" ]; then stale_indices+=("$i"); continue; fi
365
+ if [ -z "$tracked_changes" ] && [ -n "$untracked_files" ]; then
366
+ all_committed=true
367
+ while IFS= read -r filepath; do
368
+ [ -z "$filepath" ] && continue
369
+ if ! git -C "$repo_root" cat-file -e "HEAD:$filepath" 2>/dev/null; then all_committed=false; break; fi
370
+ done < <(git -C "$repo_root" stash show --include-untracked "$ref" --name-only 2>/dev/null || true)
371
+ if [ "$all_committed" = true ]; then stale_indices+=("$i"); fi
372
+ fi
373
+ done
374
+ if [ ${#stale_indices[@]} -eq 0 ]; then exit 0; fi
375
+ if [ "$mode" = "report" ]; then
376
+ echo "STALE STASH ENTRIES DETECTED (${#stale_indices[@]} of $stash_count):"
377
+ for idx in "${stale_indices[@]}"; do
378
+ echo " stash@{$idx}: $(git -C "$repo_root" stash list | sed -n "$((idx + 1))p")"
379
+ done
380
+ echo "Clean with: bash scripts/clean-stale-stashes.sh --drop"; exit 1
381
+ fi
382
+ if [ "$mode" = "drop-all" ]; then git -C "$repo_root" stash clear; echo "Dropped all $stash_count entries."; exit 0; fi
383
+ for ((i = ${#stale_indices[@]} - 1; i >= 0; i--)); do
384
+ idx="${stale_indices[$i]}"
385
+ git -C "$repo_root" stash drop "stash@{$idx}" 2>/dev/null || true
386
+ done
387
+ echo "Dropped ${#stale_indices[@]} stale entry/entries."; exit 0
388
+ - path: scripts/check-clean-trees.sh
389
+ content: |
390
+ #!/bin/bash
391
+ # check-clean-trees.sh — verify all git trees are clean.
392
+ # Checks repo root + all nested .git directories up to 3 levels deep.
393
+ set -euo pipefail
394
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
395
+ if [ -z "$repo_root" ]; then echo "ERROR: not inside a git repository" >&2; exit 1; fi
396
+ dirty=0 output=""
397
+ main_status="$(git -C "$repo_root" status --short 2>/dev/null || true)"
398
+ if [ -n "$main_status" ]; then
399
+ output+="DIRTY: $repo_root\n$(printf '%s\n' "$main_status" | sed 's/^/ /')\n"
400
+ dirty=1
401
+ fi
402
+ while IFS= read -r gitdir; do
403
+ nested_root="$(dirname "$gitdir")"
404
+ [ "$nested_root" = "$repo_root" ] && continue
405
+ nested_status="$(git -C "$nested_root" status --short 2>/dev/null || true)"
406
+ if [ -n "$nested_status" ]; then
407
+ output+="DIRTY: $nested_root\n$(printf '%s\n' "$nested_status" | sed 's/^/ /')\n"
408
+ dirty=1
409
+ fi
410
+ done < <(find "$repo_root" -maxdepth 3 -name ".git" -type d 2>/dev/null | head -20)
411
+ if [ "$dirty" -eq 1 ]; then
412
+ echo -e "$output" | head -40
413
+ echo "---"
414
+ echo "Uncommitted changes detected. Commit before session end."
415
+ exit 1
416
+ fi
417
+ exit 0
418
+ - path: hooks/pre-user-prompt.sh
419
+ content: |
420
+ #!/bin/bash
421
+ # Pre-user-prompt hook: session-end protocol + stale-stash + git-guard checks.
422
+ # Requires jq for JSON payload parsing.
423
+ set -euo pipefail
424
+ payload="$(cat)"
425
+ user_prompt="$(printf '%s' "$payload" | jq -r '.tool_info.user_prompt // empty' 2>/dev/null || true)"
426
+ if [ -z "$user_prompt" ]; then exit 0; fi
427
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || echo "")"
428
+ if [ -n "$repo_root" ] && [ -x "$repo_root/scripts/clean-stale-stashes.sh" ]; then
429
+ stash_output="$("$repo_root/scripts/clean-stale-stashes.sh" 2>&1 || true)"
430
+ if [ -n "$stash_output" ]; then
431
+ echo "⚠️ STALE GIT STASH DETECTED" >&2
432
+ echo "$stash_output" >&2
433
+ echo "Clean with: bash scripts/clean-stale-stashes.sh --drop" >&2
434
+ echo "" >&2
435
+ fi
436
+ fi
437
+ if [ -n "$repo_root" ] && [ -f "$repo_root/scripts/git-guard.sh" ]; then
438
+ if [ -z "${FORGE_GIT_GUARD:-}" ]; then
439
+ echo "⚠️ GIT GUARD NOT LOADED — destructive git operations are NOT blocked." >&2
440
+ echo "Run: source $repo_root/scripts/git-guard.sh" >&2
441
+ echo "" >&2
442
+ fi
443
+ fi
444
+ session_end_phrases=("Завершаем эту сессию" "Завершаем сессию" "Заканчиваем сессию" "Завершить сессию" "End session" "Wrap up" "Session end" "/session-end")
445
+ matched=""
446
+ for phrase in "${session_end_phrases[@]}"; do
447
+ if printf '%s' "$user_prompt" | grep -qiF "$phrase"; then matched="$phrase"; break; fi
448
+ done
449
+ if [ -z "$matched" ]; then exit 0; fi
450
+ cat >&2 <<EOF
451
+ SESSION-END PROTOCOL TRIGGERED (matched: "$matched")
452
+ The user's original message was:
453
+ > $user_prompt
454
+ You MUST invoke the fo-session-retro skill via the skill tool BEFORE producing
455
+ any other output. This is a NON-NEGOTIABLE BLOCKED GATE per PREFERENCES.md.
456
+ DO NOT produce a closing summary or ad-hoc output. The closing block must come
457
+ from fo-session-retro's report.
458
+ Protocol steps:
459
+ 1. Verify clean working trees — run: bash scripts/check-clean-trees.sh
460
+ 2. Invoke fo-session-retro via the skill tool
461
+ 3. The retro skill's report IS the session-end output
462
+ EOF
463
+ if [ -n "$repo_root" ] && [ -x "$repo_root/scripts/check-clean-trees.sh" ]; then
464
+ tree_output="$("$repo_root/scripts/check-clean-trees.sh" 2>&1 || true)"
465
+ if [ -n "$tree_output" ]; then echo "" >&2; echo "$tree_output" >&2; fi
466
+ fi
467
+ exit 2
468
+ - path: hooks/pre-user-prompt-wrapper.mjs
469
+ content: |
470
+ #!/usr/bin/env node
471
+ // pre-user-prompt-wrapper.mjs — cross-platform wrapper for pre-user-prompt.sh.
472
+ // Detects bash availability, prints one-time warning if bash not found,
473
+ // then delegates to bash hooks/pre-user-prompt.sh with stdin piped.
474
+ import { execFileSync, spawn } from "node:child_process";
475
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
476
+ import { dirname, join } from "node:path";
477
+ import { fileURLToPath } from "node:url";
478
+
479
+ const __dirname = dirname(fileURLToPath(import.meta.url));
480
+ const scriptPath = join(__dirname, "pre-user-prompt.sh");
481
+ const cacheDir = join(__dirname, "..", ".cache");
482
+ const warningFlag = join(cacheDir, "git-guard-warning-sent");
483
+
484
+ // If pre-user-prompt.sh doesn't exist, silently exit
485
+ if (!existsSync(scriptPath)) {
486
+ process.exit(0);
487
+ }
488
+
489
+ // Check if bash is available
490
+ let bashAvailable = false;
491
+ try {
492
+ execFileSync("bash", ["--version"], { stdio: "ignore" });
493
+ bashAvailable = true;
494
+ } catch {
495
+ bashAvailable = false;
496
+ }
497
+
498
+ if (!bashAvailable) {
499
+ // Print one-time warning if not already sent
500
+ if (!existsSync(warningFlag)) {
501
+ console.error("⚠️ bash not found — pre-user-prompt hook is disabled.");
502
+ console.error("Install Git Bash (Git for Windows) or WSL to enable agent-safety hooks.");
503
+ console.error("");
504
+ try {
505
+ mkdirSync(cacheDir, { recursive: true });
506
+ writeFileSync(warningFlag, new Date().toISOString(), "utf8");
507
+ } catch {
508
+ // Best-effort — don't crash if cache dir is not writable
509
+ }
510
+ }
511
+ process.exit(0);
512
+ }
513
+
514
+ // Delegate to bash script with stdin piped
515
+ const child = spawn("bash", [scriptPath], { stdio: ["inherit", "inherit", "inherit"] });
516
+ child.on("exit", (code) => {
517
+ process.exit(code ?? 0);
518
+ });
519
+ - path: .windsurf/hooks.json
520
+ content: |
521
+ {
522
+ "hooks": {
523
+ "pre_user_prompt": [
524
+ {
525
+ "command": "node $ROOT_WORKSPACE_PATH/hooks/pre-user-prompt-wrapper.mjs",
526
+ "show_output": true
527
+ }
528
+ ]
529
+ }
530
+ }
223
531
  - path: systems-cache/.gitkeep
224
532
  content: |
225
533
  # Sternsystem cache directory (RFC-0790)
@@ -243,6 +551,14 @@ workspace:
243
551
  mode: protect
244
552
  - path: systems-cache/.gitkeep
245
553
  mode: protect
554
+ - path: scripts/git-guard.sh
555
+ mode: protect
556
+ - path: scripts/setup-git-guards.sh
557
+ mode: protect
558
+ - path: hooks/pre-user-prompt.sh
559
+ mode: protect
560
+ - path: hooks/pre-user-prompt-wrapper.mjs
561
+ mode: protect
246
562
  - path: README.md
247
563
  content: |
248
564
  # __PROJECT_NAME__
@@ -253,6 +569,8 @@ workspace:
253
569
 
254
570
  - Node.js 24+
255
571
  - pnpm 10+
572
+ - jq (for pre-user-prompt hook)
573
+ - On Windows: Git Bash or WSL (for shell scripts)
256
574
 
257
575
  ## Setup
258
576
 
@@ -270,7 +588,19 @@ workspace:
270
588
  pnpm install
271
589
  ```
272
590
 
273
- ### 3. Verify the workshop
591
+ ### 3. Install git guards (recommended)
592
+
593
+ Git guards prevent AI agents from running destructive git commands (`git stash`, `git reset --hard`, `git checkout --`, `git restore`, `git clean -f`):
594
+
595
+ ```sh
596
+ bash scripts/setup-git-guards.sh
597
+ ```
598
+
599
+ This installs a shell function into your shell profile (`~/.zshenv` for zsh, `~/.bashrc` for bash/Git Bash). Restart your shell or run `source scripts/git-guard.sh`.
600
+
601
+ > **Windows**: Git guards require Git Bash (included with Git for Windows). Run `bash scripts/setup-git-guards.sh` from Git Bash. If bash is not in PATH, the pre-user-prompt hook wrapper prints a one-time warning. Install `jq` via `winget install jqlang.jq` or `choco install jq`.
602
+
603
+ ### 4. Verify the workshop
274
604
 
275
605
  ```sh
276
606
  pnpm exec forge run forge.doctor