@imdeadpool/guardex 7.0.41 → 7.1.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.
Files changed (118) hide show
  1. package/README.md +94 -13
  2. package/package.json +3 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/skills/gx-act/SKILL.md +82 -0
  6. package/src/agents/cleanup-sessions.js +126 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +202 -0
  9. package/src/agents/launch.js +249 -0
  10. package/src/agents/registry.js +133 -0
  11. package/src/agents/selection-panel.js +571 -0
  12. package/src/agents/sessions.js +151 -0
  13. package/src/agents/start.js +591 -0
  14. package/src/agents/status.js +146 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +344 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +357 -3
  19. package/src/cli/commands/agents.js +364 -0
  20. package/src/cli/commands/bootstrap.js +92 -0
  21. package/src/cli/commands/branch.js +127 -0
  22. package/src/cli/commands/claude.js +674 -0
  23. package/src/cli/commands/doctor.js +268 -0
  24. package/src/cli/commands/finish.js +26 -0
  25. package/src/cli/commands/mcp.js +122 -0
  26. package/src/cli/commands/misc.js +304 -0
  27. package/src/cli/commands/pr.js +439 -0
  28. package/src/cli/commands/prompt.js +92 -0
  29. package/src/cli/commands/release.js +305 -0
  30. package/src/cli/commands/report.js +244 -0
  31. package/src/cli/commands/review.js +32 -0
  32. package/src/cli/commands/setup.js +242 -0
  33. package/src/cli/commands/status.js +338 -0
  34. package/src/cli/commands/watch.js +234 -0
  35. package/src/cli/main.js +85 -3613
  36. package/src/cli/shared/repo-env.js +161 -0
  37. package/src/cli/shared/sandbox.js +417 -0
  38. package/src/cli/shared/scaffolding.js +535 -0
  39. package/src/cli/shared/toolchain-shims.js +420 -0
  40. package/src/cockpit/action-runner.js +3 -0
  41. package/src/cockpit/actions.js +80 -0
  42. package/src/cockpit/control.js +1121 -0
  43. package/src/cockpit/index.js +426 -0
  44. package/src/cockpit/kitty-layout.js +549 -0
  45. package/src/cockpit/kitty-tree.js +144 -0
  46. package/src/cockpit/logs-reader.js +182 -0
  47. package/src/cockpit/menu.js +204 -0
  48. package/src/cockpit/pane-actions.js +597 -0
  49. package/src/cockpit/pane-menu.js +387 -0
  50. package/src/cockpit/projects-finder.js +178 -0
  51. package/src/cockpit/render.js +215 -0
  52. package/src/cockpit/settings-render.js +128 -0
  53. package/src/cockpit/settings.js +124 -0
  54. package/src/cockpit/shortcuts.js +24 -0
  55. package/src/cockpit/sidebar.js +311 -0
  56. package/src/cockpit/state.js +72 -0
  57. package/src/cockpit/theme.js +128 -0
  58. package/src/cockpit/welcome.js +266 -0
  59. package/src/context.js +304 -43
  60. package/src/core/runtime.js +6 -1
  61. package/src/doctor/index.js +45 -15
  62. package/src/finish/index.js +186 -7
  63. package/src/finish/preflight.js +177 -0
  64. package/src/finish/review-gate.js +182 -0
  65. package/src/git/index.js +511 -4
  66. package/src/hooks/index.js +0 -64
  67. package/src/kitty/command.js +101 -0
  68. package/src/kitty/runtime.js +250 -0
  69. package/src/mcp/collect.js +370 -0
  70. package/src/mcp/server.js +157 -0
  71. package/src/output/index.js +68 -2
  72. package/src/pr-review.js +264 -0
  73. package/src/pr.js +381 -0
  74. package/src/sandbox/index.js +13 -2
  75. package/src/scaffold/agent-worktree-prep.js +213 -0
  76. package/src/scaffold/index.js +127 -10
  77. package/src/speckit/index.js +226 -0
  78. package/src/submodule/index.js +288 -0
  79. package/src/terminal/index.js +45 -0
  80. package/src/terminal/kitty.js +622 -0
  81. package/src/terminal/tmux.js +125 -0
  82. package/src/tmux/command.js +27 -0
  83. package/src/tmux/session.js +89 -0
  84. package/src/toolchain/index.js +20 -0
  85. package/templates/AGENTS.monorepo-apps.md +26 -0
  86. package/templates/AGENTS.multiagent-safety.md +63 -323
  87. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  88. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  89. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  90. package/templates/githooks/pre-commit +44 -20
  91. package/templates/github/workflows/README.md +87 -0
  92. package/templates/github/workflows/ci-full.yml +55 -0
  93. package/templates/github/workflows/ci.yml +56 -0
  94. package/templates/github/workflows/cr.yml +20 -1
  95. package/templates/scripts/agent-branch-finish.sh +519 -23
  96. package/templates/scripts/agent-branch-merge.sh +4 -1
  97. package/templates/scripts/agent-branch-start.sh +176 -24
  98. package/templates/scripts/agent-preflight.sh +115 -0
  99. package/templates/scripts/agent-worktree-prune.sh +96 -5
  100. package/templates/scripts/codex-agent.sh +41 -97
  101. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  102. package/templates/scripts/review-bot-watch.sh +31 -2
  103. package/templates/scripts/agent-session-state.js +0 -171
  104. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  105. package/templates/vscode/guardex-active-agents/README.md +0 -34
  106. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  107. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  108. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  109. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  110. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  111. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  112. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  113. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  114. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  115. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  116. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  117. package/templates/vscode/guardex-active-agents/package.json +0 -169
  118. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
@@ -281,7 +281,10 @@ if [[ -z "$TARGET_BRANCH" ]]; then
281
281
  start_output=""
282
282
  if ! start_output="$(
283
283
  cd "$repo_root"
284
- GUARDEX_OPENSPEC_AUTO_INIT=1 run_guardex_cli branch start "$TASK_NAME" "$AGENT_NAME" "$BASE_BRANCH" 2>&1
284
+ # An integration lane is inherently cross-cutting, so pin it to T3 (full
285
+ # change + plan workspace) regardless of the branch-start default (now T1).
286
+ GUARDEX_OPENSPEC_AUTO_INIT=1 GUARDEX_OPENSPEC_TIER="${GUARDEX_OPENSPEC_TIER:-T3}" \
287
+ run_guardex_cli branch start "$TASK_NAME" "$AGENT_NAME" "$BASE_BRANCH" 2>&1
285
288
  )"; then
286
289
  printf '%s\n' "$start_output" >&2
287
290
  exit 1
@@ -9,13 +9,19 @@ WORKTREE_ROOT_REL=""
9
9
  WORKTREE_ROOT_EXPLICIT=0
10
10
  NODE_BIN="${GUARDEX_NODE_BIN:-node}"
11
11
  CLI_ENTRY="${GUARDEX_CLI_ENTRY:-}"
12
- OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-false}"
12
+ OPENSPEC_AUTO_INIT_RAW="${GUARDEX_OPENSPEC_AUTO_INIT:-true}"
13
13
  OPENSPEC_PLAN_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_PLAN_SLUG:-}"
14
14
  OPENSPEC_CHANGE_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CHANGE_SLUG:-}"
15
15
  OPENSPEC_CAPABILITY_SLUG_OVERRIDE="${GUARDEX_OPENSPEC_CAPABILITY_SLUG:-}"
16
16
  OPENSPEC_MASTERPLAN_LABEL_RAW="${GUARDEX_OPENSPEC_MASTERPLAN_LABEL-masterplan}"
17
- OPENSPEC_TIER_RAW="${GUARDEX_OPENSPEC_TIER:-T3}"
17
+ # Default tier is T1 (notes.md only, minimal scaffold): most tasks are small,
18
+ # and a full T3 plan workspace costs thousands of tokens an agent never reads.
19
+ # Escalate explicitly with --tier T2 (behavior change) or T3 (plan-driven).
20
+ OPENSPEC_TIER_RAW="${GUARDEX_OPENSPEC_TIER:-T1}"
18
21
  REUSE_EXISTING_RAW="${GUARDEX_BRANCH_START_REUSE_EXISTING:-true}"
22
+ AUTO_TRANSFER_ENABLED_RAW="${GUARDEX_AUTO_TRANSFER:-true}"
23
+ AUTO_TRANSFER_EXCLUDE_DEFAULT='.omc/**:.omx/state/**:.dev-ports.json:apps/logs/**:.codex/settings.local.json:.claude/settings.local.json:.codex/state/**:.claude/state/**'
24
+ AUTO_TRANSFER_EXCLUDE_RAW="${GUARDEX_AUTO_TRANSFER_EXCLUDE-$AUTO_TRANSFER_EXCLUDE_DEFAULT}"
19
25
  PRINT_NAME_ONLY=0
20
26
  POSITIONAL_ARGS=()
21
27
 
@@ -36,8 +42,40 @@ run_guardex_cli() {
36
42
  return 127
37
43
  }
38
44
 
45
+ print_usage() {
46
+ cat <<'USAGE'
47
+ Usage: agent-branch-start [task] [agent] [base] [options]
48
+
49
+ Start an isolated agent/* branch + worktree for a task.
50
+
51
+ Positional:
52
+ task Task name/slug (default: "task")
53
+ agent Agent name (default: "agent")
54
+ base Base branch to fork from (default: repo default)
55
+
56
+ Options:
57
+ --task <name> Task name/slug
58
+ --agent <name> Agent name
59
+ --base <branch> Base branch to fork from
60
+ --worktree-root <p> Worktree root dir (default: .omx/agent-worktrees)
61
+ --reuse-existing Reuse an existing matching worktree (default)
62
+ --new Force a fresh worktree instead of reusing
63
+ --tier <T1|T2|T3> OpenSpec tier for scaffolding (default T1; T2 for a
64
+ behavior change, T3 for plan-driven work)
65
+ --transfer Auto-transfer uncommitted changes into the worktree (default)
66
+ --no-transfer Do not auto-transfer uncommitted changes
67
+ --transfer-exclude <globs> Colon-separated globs to exclude from transfer
68
+ --print-name-only Print the computed branch name and exit
69
+ -h, --help Show this help and exit
70
+ USAGE
71
+ }
72
+
39
73
  while [[ $# -gt 0 ]]; do
40
74
  case "$1" in
75
+ -h|--help)
76
+ print_usage
77
+ exit 0
78
+ ;;
41
79
  --task)
42
80
  TASK_NAME="${2:-task}"
43
81
  shift 2
@@ -67,6 +105,18 @@ while [[ $# -gt 0 ]]; do
67
105
  REUSE_EXISTING_RAW="false"
68
106
  shift
69
107
  ;;
108
+ --no-transfer)
109
+ AUTO_TRANSFER_ENABLED_RAW="false"
110
+ shift
111
+ ;;
112
+ --transfer)
113
+ AUTO_TRANSFER_ENABLED_RAW="true"
114
+ shift
115
+ ;;
116
+ --transfer-exclude)
117
+ AUTO_TRANSFER_EXCLUDE_RAW="${2:-}"
118
+ shift 2
119
+ ;;
70
120
  --in-place|--allow-in-place)
71
121
  echo "[agent-branch-start] In-place branch mode is disabled." >&2
72
122
  echo "[agent-branch-start] This command always creates an isolated worktree to keep your active checkout unchanged." >&2
@@ -277,7 +327,7 @@ normalize_tier() {
277
327
  esac
278
328
  }
279
329
 
280
- if ! OPENSPEC_TIER="$(normalize_tier "$OPENSPEC_TIER_RAW" "T3")"; then
330
+ if ! OPENSPEC_TIER="$(normalize_tier "$OPENSPEC_TIER_RAW" "T1")"; then
281
331
  echo "[agent-branch-start] Unsupported OpenSpec tier: ${OPENSPEC_TIER_RAW}" >&2
282
332
  exit 1
283
333
  fi
@@ -389,11 +439,41 @@ print_reused_agent_worktree() {
389
439
  echo "[agent-branch-start] OpenSpec tier: ${OPENSPEC_TIER}"
390
440
  echo "[agent-branch-start] OpenSpec change: existing worktree"
391
441
  echo "[agent-branch-start] OpenSpec plan: existing worktree"
392
- echo "[agent-branch-start] Next steps:"
393
- echo " cd \"${worktree_path}\""
394
- echo " gx locks claim --branch \"${branch_name}\" <file...>"
395
- echo " # continue work in this existing sandbox"
396
- echo " gx branch finish --branch \"${branch_name}\" --via-pr --wait-for-merge"
442
+ print_agent_next_steps "$branch_name" "$worktree_path" "continue work in this existing sandbox" "$BASE_BRANCH"
443
+ }
444
+
445
+ print_agent_next_steps() {
446
+ local branch_name="$1"
447
+ local worktree_path="$2"
448
+ local work_step="$3"
449
+ local base_branch="${4:-main}"
450
+
451
+ if [[ -z "$base_branch" ]]; then
452
+ base_branch="main"
453
+ fi
454
+
455
+ # Pre-`Ready:` post-condition check. `git worktree add` has been observed
456
+ # to return 0 with the worktree dir still missing on disk (race condition
457
+ # during the OpenSpec / dependency-symlink phase between `worktree add`
458
+ # and now). If we print `Ready:` in that state, callers cd into a
459
+ # vanished directory and lose subsequent edits silently. Surface it as
460
+ # an exit-1 error instead — operator can retry + `git worktree prune`.
461
+ if [[ ! -d "$worktree_path" || ! -e "$worktree_path/.git" ]]; then
462
+ printf '[agent-branch-start] ERROR: worktree did not materialize on disk before Ready:\n' >&2
463
+ printf '[agent-branch-start] branch: %s\n' "$branch_name" >&2
464
+ printf '[agent-branch-start] expected: %s\n' "$worktree_path" >&2
465
+ printf '[agent-branch-start] Run `git worktree prune` to clear ghost entries, then retry.\n' >&2
466
+ exit 1
467
+ fi
468
+
469
+ echo "[agent-branch-start] Ready:"
470
+ echo " branch: ${branch_name}"
471
+ echo " worktree: ${worktree_path}"
472
+ echo " next:"
473
+ echo " cd \"${worktree_path}\""
474
+ echo " gx locks claim --branch \"${branch_name}\" <file...>"
475
+ echo " # ${work_step}"
476
+ echo " gx branch finish --branch \"${branch_name}\" --base ${base_branch} --via-pr --wait-for-merge --cleanup"
397
477
  }
398
478
 
399
479
  has_local_changes() {
@@ -419,7 +499,7 @@ meaningful_slug_tokens() {
419
499
  | awk '
420
500
  length($0) < 4 { next }
421
501
  $0 ~ /^[0-9]+$/ { next }
422
- $0 ~ /^(agent|agents|branch|codex|claude|continue|dirty|existing|fix|from|implement|make|matching|reuse|start|task|that|this|update|with|worktree|worktrees)$/ { next }
502
+ $0 ~ /^(agent|agents|branch|codex|claude|continue|dirty|existing|fix|from|implement|make|matching|openspec|reuse|start|task|that|this|update|with|worktree|worktrees)$/ { next }
423
503
  !seen[$0]++ { print }
424
504
  '
425
505
  }
@@ -463,6 +543,25 @@ managed_worktree_roots() {
463
543
  done
464
544
  }
465
545
 
546
+ branch_published_then_remote_pruned() {
547
+ # Detect the post-`gx branch finish --via-pr --cleanup` state: the agent
548
+ # branch was published (so upstream config is set), but the remote-tracking
549
+ # ref no longer exists (because `push origin --delete` ran during cleanup).
550
+ # That combination only arises after a finish that successfully merged the
551
+ # PR and pruned the remote branch — a freshly-created agent branch never
552
+ # has an upstream until publish, so this never false-positives on the
553
+ # "started, dirty, no commits yet" case that we want to keep reusable.
554
+ local repo="$1"
555
+ local branch="$2"
556
+ local upstream
557
+ upstream="$(git -C "$repo" config --get "branch.${branch}.remote" 2>/dev/null || true)"
558
+ [[ -n "$upstream" ]] || return 1
559
+ if git -C "$repo" show-ref --verify --quiet "refs/remotes/${upstream}/${branch}"; then
560
+ return 1
561
+ fi
562
+ return 0
563
+ }
564
+
466
565
  find_matching_dirty_agent_worktree() {
467
566
  local repo="$1"
468
567
  local worktree_root_rel="$2"
@@ -483,6 +582,13 @@ find_matching_dirty_agent_worktree() {
483
582
  fi
484
583
  [[ "$branch" == "agent/${agent_slug}/"* ]] || continue
485
584
  has_local_changes "$entry" || continue
585
+ # Skip merged-and-cleaned worktrees that happen to still be on disk
586
+ # (e.g. operator's shell is cwd'd inside; cleanup deferred). Reusing
587
+ # such a worktree would silently hand the next agent a stale HEAD.
588
+ if branch_published_then_remote_pruned "$repo" "$branch"; then
589
+ echo "[agent-branch-start] Skipping merged-and-cleaned worktree: ${entry} (branch ${branch} has no remote tracking ref)" >&2
590
+ continue
591
+ fi
486
592
 
487
593
  descriptor="${branch#agent/${agent_slug}/}"
488
594
  score="$(token_match_score "$task_slug" "$descriptor")"
@@ -774,18 +880,54 @@ restore_auto_transfer_stash_on_failure() {
774
880
 
775
881
  trap 'restore_auto_transfer_stash_on_failure "$?"' EXIT
776
882
 
883
+ auto_transfer_enabled_lc="$(printf '%s' "$AUTO_TRANSFER_ENABLED_RAW" | tr '[:upper:]' '[:lower:]')"
884
+ case "$auto_transfer_enabled_lc" in
885
+ 0|false|no|off) AUTO_TRANSFER_ENABLED=0 ;;
886
+ *) AUTO_TRANSFER_ENABLED=1 ;;
887
+ esac
888
+
889
+ build_auto_transfer_stash_argv() {
890
+ # Emit NUL-separated argv: --include-untracked --message <msg> [-- :/ :(exclude,glob)PAT ...]
891
+ local msg="$1"
892
+ printf '%s\0' --include-untracked --message "$msg"
893
+ if [[ -z "${AUTO_TRANSFER_EXCLUDE_RAW:-}" ]]; then
894
+ return 0
895
+ fi
896
+ printf '%s\0' '--' ':/'
897
+ local -a patterns=()
898
+ IFS=':' read -ra patterns <<< "$AUTO_TRANSFER_EXCLUDE_RAW"
899
+ local pattern
900
+ for pattern in "${patterns[@]}"; do
901
+ [[ -z "$pattern" ]] && continue
902
+ printf '%s\0' ":(exclude,glob)${pattern}"
903
+ done
904
+ }
905
+
777
906
  current_branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
778
907
  protected_branches_raw="$(resolve_protected_branches "$repo_root")"
779
908
  if [[ -n "$current_branch" && "$current_branch" != "HEAD" ]] && is_protected_branch_name "$current_branch" "$protected_branches_raw"; then
780
909
  if has_local_changes "$repo_root"; then
781
- auto_transfer_message="guardex-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
782
- if git -C "$repo_root" stash push --include-untracked --message "$auto_transfer_message" >/dev/null 2>&1; then
783
- auto_transfer_stash_ref="$(resolve_stash_ref_by_message "$repo_root" "$auto_transfer_message")"
784
- if [[ -n "$auto_transfer_stash_ref" ]]; then
785
- auto_transfer_source_branch="$current_branch"
786
- echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}'..."
787
- if ! maybe_fail_after_auto_transfer_stash; then
788
- exit 1
910
+ if [[ "$AUTO_TRANSFER_ENABLED" -eq 0 ]]; then
911
+ echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'; --no-transfer set, leaving them in place." >&2
912
+ echo "[agent-branch-start] If you intended those changes for '${branch_name}', stash them manually and apply inside the worktree." >&2
913
+ else
914
+ auto_transfer_message="guardex-auto-transfer-${timestamp}-${agent_slug}-${task_slug}"
915
+ stash_argv=()
916
+ while IFS= read -r -d '' arg; do
917
+ stash_argv+=("$arg")
918
+ done < <(build_auto_transfer_stash_argv "$auto_transfer_message")
919
+ if git -C "$repo_root" stash push "${stash_argv[@]}" >/dev/null 2>&1; then
920
+ auto_transfer_stash_ref="$(resolve_stash_ref_by_message "$repo_root" "$auto_transfer_message")"
921
+ if [[ -n "$auto_transfer_stash_ref" ]]; then
922
+ auto_transfer_source_branch="$current_branch"
923
+ echo "[agent-branch-start] Detected local changes on protected branch '${current_branch}'. Moving them to '${branch_name}' (state-file globs excluded)..."
924
+ if ! maybe_fail_after_auto_transfer_stash; then
925
+ exit 1
926
+ fi
927
+ else
928
+ if has_local_changes "$repo_root"; then
929
+ echo "[agent-branch-start] Local changes on '${current_branch}' all match the auto-transfer exclude list; leaving them in place on '${current_branch}'." >&2
930
+ fi
789
931
  fi
790
932
  fi
791
933
  fi
@@ -797,6 +939,13 @@ if ! worktree_add_output="$(git -C "$repo_root" worktree add -b "$branch_name" "
797
939
  printf '%s\n' "$worktree_add_output" >&2
798
940
  exit 1
799
941
  fi
942
+ # git worktree add has been observed to exit 0 while the target dir is
943
+ # missing — verify before downstream init runs against a phantom path.
944
+ if [[ ! -d "$worktree_path" || ! -e "$worktree_path/.git" ]]; then
945
+ printf '[agent-branch-start] ERROR: git worktree add reported success but %s is not a valid worktree.\n' "$worktree_path" >&2
946
+ printf '%s\n' "$worktree_add_output" >&2
947
+ exit 1
948
+ fi
800
949
  git -C "$repo_root" config "branch.${branch_name}.guardexBase" "$BASE_BRANCH" >/dev/null 2>&1 || true
801
950
  git -C "$repo_root" config "branch.${branch_name}.guardexWorktreeRoot" "$WORKTREE_ROOT_REL" >/dev/null 2>&1 || true
802
951
  # Fresh agent branches should start unpublished; clear any inherited base-branch tracking.
@@ -832,18 +981,21 @@ fi
832
981
  echo "[agent-branch-start] Created branch: ${branch_name}"
833
982
  echo "[agent-branch-start] Worktree: ${worktree_path}"
834
983
  echo "[agent-branch-start] OpenSpec tier: ${OPENSPEC_TIER}"
835
- if [[ "$OPENSPEC_SKIP_CHANGE" -eq 1 ]]; then
984
+ if [[ "$OPENSPEC_TIER" == "T1" ]]; then
985
+ echo "[agent-branch-start] T1 minimal scaffold (notes.md). Escalate: --tier T2 for a behavior change, T3 for plan-driven work."
986
+ fi
987
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
988
+ echo "[agent-branch-start] OpenSpec change: skipped (GUARDEX_OPENSPEC_AUTO_INIT disabled)"
989
+ elif [[ "$OPENSPEC_SKIP_CHANGE" -eq 1 ]]; then
836
990
  echo "[agent-branch-start] OpenSpec change: skipped by tier ${OPENSPEC_TIER}"
837
991
  else
838
992
  echo "[agent-branch-start] OpenSpec change: openspec/changes/${openspec_change_slug}"
839
993
  fi
840
- if [[ "$OPENSPEC_SKIP_PLAN" -eq 1 ]]; then
994
+ if [[ "$OPENSPEC_AUTO_INIT" -ne 1 ]]; then
995
+ echo "[agent-branch-start] OpenSpec plan: skipped (GUARDEX_OPENSPEC_AUTO_INIT disabled)"
996
+ elif [[ "$OPENSPEC_SKIP_PLAN" -eq 1 ]]; then
841
997
  echo "[agent-branch-start] OpenSpec plan: skipped by tier ${OPENSPEC_TIER}"
842
998
  else
843
999
  echo "[agent-branch-start] OpenSpec plan: openspec/plan/${openspec_plan_slug}"
844
1000
  fi
845
- echo "[agent-branch-start] Next steps:"
846
- echo " cd \"${worktree_path}\""
847
- echo " gx locks claim --branch \"${branch_name}\" <file...>"
848
- echo " # implement + commit"
849
- echo " gx branch finish --branch \"${branch_name}\" --base ${BASE_BRANCH} --via-pr --wait-for-merge"
1001
+ print_agent_next_steps "$branch_name" "$worktree_path" "implement + commit" "$BASE_BRANCH"
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env bash
2
+ # Pre-flight verification gate for gitguardex-managed projects.
3
+ #
4
+ # Runs in the agent's worktree from `gx branch finish` BEFORE the push
5
+ # happens. Returns non-zero to refuse the push so a broken commit
6
+ # never reaches the PR / CI / merge funnel.
7
+ #
8
+ # Auto-detects the project's stack and runs conventional verification:
9
+ # - Node/pnpm: pnpm typecheck && pnpm lint && pnpm test (each only
10
+ # if the script exists in package.json)
11
+ # - Node/npm: npm test (only if defined)
12
+ # - Rust: cargo check
13
+ # - Python: ruff check (only if ruff is installed)
14
+ #
15
+ # Override per-project by replacing this file (delete the symlink under
16
+ # scripts/agent-preflight.sh and write your own).
17
+ #
18
+ # Skip a single run with `gx branch finish --no-preflight`.
19
+
20
+ set -euo pipefail
21
+
22
+ repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
23
+ cd "$repo_root"
24
+
25
+ ran=0 # steps that PASSED
26
+ attempted=0 # steps that RAN (pass or fail) — drives stack detection
27
+ fail=0
28
+ # Quiet by default: a green `npm test` run can be hundreds of lines of TAP that
29
+ # floods the agent's context on every `gx branch finish`. Capture each step's
30
+ # output, print a one-line summary on success, and surface only the tail on
31
+ # failure (where it is actually useful). Stream full output live with
32
+ # GUARDEX_PREFLIGHT_VERBOSE=1.
33
+ GUARDEX_PREFLIGHT_FAIL_TAIL="${GUARDEX_PREFLIGHT_FAIL_TAIL:-40}"
34
+ run_step() {
35
+ local label="$1"
36
+ shift
37
+ echo "[agent-preflight] -> $label"
38
+ attempted=$((attempted + 1))
39
+ if [[ "${GUARDEX_PREFLIGHT_VERBOSE:-0}" == "1" ]]; then
40
+ if "$@"; then
41
+ ran=$((ran + 1))
42
+ echo "[agent-preflight] ok"
43
+ else
44
+ echo "[agent-preflight] FAIL: $label" >&2
45
+ fail=1
46
+ fi
47
+ return 0
48
+ fi
49
+ local out rc
50
+ # `if` keeps `set -e` from aborting on a failing step before we capture rc.
51
+ if out="$("$@" 2>&1)"; then
52
+ rc=0
53
+ else
54
+ rc=$?
55
+ fi
56
+ if [[ "$rc" -eq 0 ]]; then
57
+ ran=$((ran + 1))
58
+ echo "[agent-preflight] ok ($(printf '%s\n' "$out" | wc -l | tr -d ' ') lines suppressed; GUARDEX_PREFLIGHT_VERBOSE=1 to show)"
59
+ else
60
+ echo "[agent-preflight] FAIL: $label (exit $rc) — last ${GUARDEX_PREFLIGHT_FAIL_TAIL} lines:" >&2
61
+ printf '%s\n' "$out" | tail -n "$GUARDEX_PREFLIGHT_FAIL_TAIL" >&2
62
+ fail=1
63
+ fi
64
+ }
65
+
66
+ has_package_script() {
67
+ local script_name="$1"
68
+ [[ -f package.json ]] || return 1
69
+ grep -E "\"${script_name}\"\\s*:" package.json >/dev/null 2>&1
70
+ }
71
+
72
+ # Node detection
73
+ if [[ -f package.json ]]; then
74
+ pkg_manager=""
75
+ if command -v pnpm >/dev/null 2>&1 && [[ -f pnpm-lock.yaml ]]; then
76
+ pkg_manager="pnpm"
77
+ elif command -v npm >/dev/null 2>&1 && [[ -f package-lock.json ]]; then
78
+ pkg_manager="npm"
79
+ fi
80
+
81
+ case "$pkg_manager" in
82
+ pnpm)
83
+ has_package_script typecheck && run_step "pnpm typecheck" pnpm typecheck
84
+ has_package_script lint && run_step "pnpm lint" pnpm lint
85
+ has_package_script test && run_step "pnpm test" pnpm test
86
+ ;;
87
+ npm)
88
+ has_package_script test && run_step "npm test" npm test
89
+ ;;
90
+ esac
91
+ fi
92
+
93
+ # Rust detection
94
+ if [[ -f Cargo.toml ]] && command -v cargo >/dev/null 2>&1; then
95
+ run_step "cargo check" cargo check --quiet
96
+ fi
97
+
98
+ # Python detection (ruff if available; pytest is too project-specific to default)
99
+ if [[ -f pyproject.toml ]] && command -v ruff >/dev/null 2>&1; then
100
+ run_step "ruff check" ruff check .
101
+ fi
102
+
103
+ if [[ "$attempted" -eq 0 ]]; then
104
+ echo "[agent-preflight] No recognized project stack detected; skipping checks." >&2
105
+ exit 0
106
+ fi
107
+
108
+ if [[ "$fail" -ne 0 ]]; then
109
+ echo "[agent-preflight] Verification failed; refusing push." >&2
110
+ echo "[agent-preflight] Fix the issues, or re-run with: gx branch finish --no-preflight ..." >&2
111
+ exit 1
112
+ fi
113
+
114
+ echo "[agent-preflight] ${ran} step(s) passed."
115
+ exit 0
@@ -82,7 +82,12 @@ if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
82
82
  fi
83
83
 
84
84
  repo_root="$(git rev-parse --show-toplevel)"
85
- current_pwd="$(pwd -P)"
85
+ current_pwd="${GUARDEX_PRUNE_ACTIVE_CWD:-$(pwd -P)}"
86
+ if [[ -d "$current_pwd" ]]; then
87
+ current_pwd="$(cd "$current_pwd" && pwd -P)"
88
+ else
89
+ current_pwd=""
90
+ fi
86
91
  repo_common_dir="$(
87
92
  git -C "$repo_root" rev-parse --git-common-dir \
88
93
  | awk -v root="$repo_root" '{ if ($0 ~ /^\//) { print $0 } else { print root "/" $0 } }'
@@ -244,11 +249,71 @@ branch_has_worktree() {
244
249
  git -C "$repo_root" worktree list --porcelain | grep -q "^branch refs/heads/${branch}$"
245
250
  }
246
251
 
252
+ # Globs treated as agent state, not real work. Worktrees whose only "dirty"
253
+ # content matches these are considered clean for prune purposes. Mirrors the
254
+ # auto-transfer + auto-resolve allowlist from PRs #546/#547 (state-file globs
255
+ # never carry authoritative content out of an agent branch).
256
+ WORKTREE_STATE_EXCLUDE_GLOBS_DEFAULT='.omc/**:.omx/state/**:.dev-ports.json:apps/logs/**:.codex/settings.local.json:.claude/settings.local.json:.codex/state/**:.claude/state/**'
257
+ WORKTREE_STATE_EXCLUDE_GLOBS_RAW="${GUARDEX_PRUNE_STATE_EXCLUDE_GLOBS-$WORKTREE_STATE_EXCLUDE_GLOBS_DEFAULT}"
258
+
259
+ build_state_exclude_pathspec_args() {
260
+ # Emit one ':(exclude,glob)<pat>' arg per non-empty pattern.
261
+ if [[ -z "$WORKTREE_STATE_EXCLUDE_GLOBS_RAW" ]]; then
262
+ return 0
263
+ fi
264
+ local -a globs=()
265
+ IFS=':' read -ra globs <<< "$WORKTREE_STATE_EXCLUDE_GLOBS_RAW"
266
+ local pattern
267
+ for pattern in "${globs[@]}"; do
268
+ [[ -z "$pattern" ]] && continue
269
+ printf '%s\0' ":(exclude,glob)${pattern}"
270
+ done
271
+ }
272
+
273
+ # Capture the state-exclude pathspecs once; reused by is_clean_worktree and
274
+ # the dirt-summary logger below.
275
+ STATE_EXCLUDE_PATHSPEC_ARGS=()
276
+ while IFS= read -r -d '' arg; do
277
+ STATE_EXCLUDE_PATHSPEC_ARGS+=("$arg")
278
+ done < <(build_state_exclude_pathspec_args)
279
+
247
280
  is_clean_worktree() {
248
281
  local wt="$1"
249
- git -C "$wt" diff --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
250
- && git -C "$wt" diff --cached --quiet -- . ":(exclude).omx/state/agent-file-locks.json" \
251
- && [[ -z "$(git -C "$wt" ls-files --others --exclude-standard)" ]]
282
+ git -C "$wt" diff --quiet -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}" \
283
+ && git -C "$wt" diff --cached --quiet -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}" \
284
+ && [[ -z "$(git -C "$wt" ls-files --others --exclude-standard -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}")" ]]
285
+ }
286
+
287
+ # Returns a short, human-readable summary of why a worktree is considered dirty:
288
+ # up to 3 modified-tracked paths + up to 3 untracked paths + a "(+N more)" tail.
289
+ # Used to surface actionable context next to "Skipping dirty worktree" log lines
290
+ # (previously gave no clue what was actually dirty).
291
+ summarize_worktree_dirt() {
292
+ local wt="$1"
293
+ local modified_paths untracked_paths
294
+ modified_paths="$(git -C "$wt" diff --name-only --diff-filter=AMDR -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}" 2>/dev/null | head -3)"
295
+ local modified_count
296
+ modified_count="$(git -C "$wt" diff --name-only --diff-filter=AMDR -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}" 2>/dev/null | wc -l | tr -d ' ')"
297
+ untracked_paths="$(git -C "$wt" ls-files --others --exclude-standard -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}" 2>/dev/null | head -3)"
298
+ local untracked_count
299
+ untracked_count="$(git -C "$wt" ls-files --others --exclude-standard -- . "${STATE_EXCLUDE_PATHSPEC_ARGS[@]}" 2>/dev/null | wc -l | tr -d ' ')"
300
+
301
+ if [[ -n "$modified_paths" ]]; then
302
+ while IFS= read -r p; do
303
+ [[ -n "$p" ]] && echo " modified: ${p}"
304
+ done <<< "$modified_paths"
305
+ if [[ "$modified_count" -gt 3 ]]; then
306
+ echo " modified: (+$((modified_count - 3)) more)"
307
+ fi
308
+ fi
309
+ if [[ -n "$untracked_paths" ]]; then
310
+ while IFS= read -r p; do
311
+ [[ -n "$p" ]] && echo " untracked: ${p}"
312
+ done <<< "$untracked_paths"
313
+ if [[ "$untracked_count" -gt 3 ]]; then
314
+ echo " untracked: (+$((untracked_count - 3)) more)"
315
+ fi
316
+ fi
252
317
  }
253
318
 
254
319
  resolve_worktree_common_dir() {
@@ -317,6 +382,26 @@ read_branch_activity_epoch() {
317
382
 
318
383
  skipped_recent=0
319
384
 
385
+ has_live_process_in_worktree() {
386
+ local wt="$1"
387
+ local proc_cwd=""
388
+
389
+ [[ -d /proc ]] || return 1
390
+
391
+ for proc_cwd in /proc/[0-9]*/cwd; do
392
+ [[ -e "$proc_cwd" ]] || continue
393
+ local live_cwd=""
394
+ live_cwd="$(readlink "$proc_cwd" 2>/dev/null || true)"
395
+ [[ -n "$live_cwd" ]] || continue
396
+ live_cwd="${live_cwd% (deleted)}"
397
+ if [[ "$live_cwd" == "$wt" || "$live_cwd" == "${wt}"/* ]]; then
398
+ return 0
399
+ fi
400
+ done
401
+
402
+ return 1
403
+ }
404
+
320
405
  branch_idle_gate() {
321
406
  local branch="$1"
322
407
  local wt="$2"
@@ -431,11 +516,16 @@ process_entry() {
431
516
  return
432
517
  fi
433
518
 
434
- if [[ "$wt" == "$current_pwd" ]]; then
519
+ if [[ -n "$current_pwd" && ( "$wt" == "$current_pwd" || "$current_pwd" == "${wt}"/* ) ]]; then
435
520
  skipped_active=$((skipped_active + 1))
436
521
  echo "[agent-worktree-prune] Skipping active cwd worktree: ${wt}"
437
522
  return
438
523
  fi
524
+ if has_live_process_in_worktree "$wt"; then
525
+ skipped_active=$((skipped_active + 1))
526
+ echo "[agent-worktree-prune] Skipping live process worktree: ${wt}"
527
+ return
528
+ fi
439
529
 
440
530
  local remove_reason=""
441
531
  local branch_delete_mode="safe"
@@ -477,6 +567,7 @@ process_entry() {
477
567
  if [[ "$FORCE_DIRTY" -ne 1 ]] && ! is_clean_worktree "$wt"; then
478
568
  skipped_dirty=$((skipped_dirty + 1))
479
569
  echo "[agent-worktree-prune] Skipping dirty worktree (${remove_reason}): ${wt}"
570
+ summarize_worktree_dirt "$wt"
480
571
  return
481
572
  fi
482
573