@darkrei08/setup-ai 3.0.4 → 3.2.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/setup-ai.sh CHANGED
@@ -2,12 +2,12 @@
2
2
 
3
3
  # ==============================================================================
4
4
  # AI Dev Suite — Engineering Excellence Edition
5
- # Version: 3.0.4
5
+ # Version: 3.2.0
6
6
  #
7
7
  # Cross-platform (macOS + all major Linux distros) installer for an AI coding
8
8
  # toolchain. Windows is handled by the sibling setup-ai.ps1; the Node launcher
9
9
  # bin/setup-ai.mjs dispatches to the right script per OS and offers an
10
- # interactive, gentle-ai-style module menu.
10
+ # interactive arrow-key module menu.
11
11
  #
12
12
  # Design:
13
13
  # - modular: every tool is its own mod_* function, driven by an ordered
@@ -26,7 +26,7 @@
26
26
  set -Eeuo pipefail
27
27
  IFS=$'\n\t'
28
28
 
29
- SCRIPT_VERSION="3.0.4"
29
+ SCRIPT_VERSION="3.2.0"
30
30
 
31
31
  SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
32
32
  LOG_DIR="${SCRIPT_DIR}/logs"
@@ -47,19 +47,33 @@ PI_WORKFLOW_VERSION="${PI_WORKFLOW_VERSION:-}"
47
47
 
48
48
  DOTENV_REPO="${DOTENV_REPO:-https://github.com/vekexasia/dotenv.git}"
49
49
 
50
- ENGINEERING_EXCELLENCE_SLUG="${ENGINEERING_EXCELLENCE_SLUG:-micio86dev/Engineering-Excellence}"
50
+ # Gentle AI ecosystem configurator installer (macOS/Linux). `gentle-ai install`
51
+ # is the interactive per-agent/per-IDE selector that also wires each agent's MCP
52
+ # servers, so the tools show up under /mcp.
53
+ GENTLE_AI_INSTALL="${GENTLE_AI_INSTALL:-https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh}"
54
+
55
+ ENGINEERING_EXCELLENCE_SLUG="${ENGINEERING_EXCELLENCE_SLUG:-darkrei08/Engineering-Excellence}"
51
56
  ENGINEERING_EXCELLENCE_SKILL="engineering-excellence"
52
57
 
58
+ # Upstream agent-skill stack mirrored from vekexasia/dotenv setup_env.sh so the
59
+ # same skills land on every OS (dotenv itself is Linux-only). Each entry is
60
+ # "<source> <skill> [<skill>...]" installed via `npx skills add`.
61
+ UPSTREAM_SKILL_SOURCES=(
62
+ "herdrdev/herdr herdr"
63
+ "mattpocock/skills triage grill-me grilling wayfinder domain-modeling prototype research"
64
+ "https://github.com/pedronauck/skills typescript-advanced"
65
+ "humanlayer/skills show-me"
66
+ )
67
+ UPSTREAM_SKILL_NAMES=(herdr triage grill-me grilling wayfinder domain-modeling prototype research typescript-advanced show-me)
68
+
53
69
  PI_AGENT_DIR="${HOME}/.pi/agent"
54
70
  PI_EXTENSIONS_DIR="${PI_AGENT_DIR}/extensions"
55
71
  PI_NPM_DIR="${PI_AGENT_DIR}/npm"
56
- ENGINEERING_EXCELLENCE_DIR="${PI_AGENT_DIR}/skills/${ENGINEERING_EXCELLENCE_SKILL}"
57
72
 
58
73
  DOTENV_DIR="${HOME}/git/personale/dotenv"
59
74
  DOTENV_EXT_DIR="${DOTENV_DIR}/pi/agent/extensions/pi-ext-workflows"
60
75
 
61
76
  COCKPIT_REPO="jlcodes99/cockpit-tools"
62
- GENTLE_AI_INSTALL="https://raw.githubusercontent.com/Gentleman-Programming/gentle-ai/main/scripts/install.sh"
63
77
 
64
78
  # ------------------------------------------------------------------------------
65
79
  # Cleanup
@@ -221,6 +235,38 @@ capture_cmd() {
221
235
  log_event "INFO" "${phase}" "capture_success" "Output captured" 0 "${display}"
222
236
  }
223
237
 
238
+ # Run a `grep` probe that distinguishes "no match" from a real failure.
239
+ # Writes matching lines to ${out_file}. Returns 0 when matches were found,
240
+ # 1 when there were none (grep exit 1), and hard-fails on any operational
241
+ # error (grep exit >1, e.g. an unreadable file) instead of hiding it.
242
+ grep_probe() {
243
+ local phase="$1" out_file="$2"; shift 2
244
+ local rc=0
245
+ grep "$@" >"${out_file}" 2>>"${HUMAN_LOG}" || rc=$?
246
+ case "${rc}" in
247
+ 0) return 0 ;;
248
+ 1) return 1 ;;
249
+ *)
250
+ log_event "ERROR" "${phase}" "grep_probe_failed" \
251
+ "grep probe failed with an operational error" "${rc}" "args=$*"
252
+ exit "${rc}"
253
+ ;;
254
+ esac
255
+ }
256
+
257
+ report_version() {
258
+ local output_var="$1" command_name="$2"; shift 2
259
+ if ! command -v "${command_name}" >/dev/null 2>&1; then
260
+ printf -v "${output_var}" '%s' "n/a"
261
+ return 0
262
+ fi
263
+ if ! capture_cmd "${output_var}" "report" "${command_name}" "$@"; then
264
+ printf -v "${output_var}" '%s' "n/a"
265
+ log_event "WARN" "report" "version_unavailable" \
266
+ "Could not read command version" 0 "command=${command_name}"
267
+ fi
268
+ }
269
+
224
270
  write_report() {
225
271
  local status="$1" rc="$2" line="${3:-n/a}" file="${4:-n/a}"
226
272
  local function_name="${5:-n/a}" command="${6:-n/a}"
@@ -250,44 +296,6 @@ write_report() {
250
296
  EOF
251
297
  }
252
298
 
253
- # ------------------------------------------------------------------------------
254
- # resolve_workflow_version — find the package.json that really owns
255
- # pi-extensible-workflows by walking up from where Node resolved its entry.
256
- # ------------------------------------------------------------------------------
257
-
258
- resolve_workflow_version() {
259
- local search_root="$1"
260
- node -e '
261
- const path = require("path");
262
- const fs = require("fs");
263
- const searchRoot = process.argv[1];
264
- let entry;
265
- try {
266
- entry = require.resolve("pi-extensible-workflows", { paths: [searchRoot] });
267
- } catch (err) {
268
- console.error(`pi-extensible-workflows is not resolvable from ${searchRoot}: ${err.message}`);
269
- process.exit(1);
270
- }
271
- let dir = path.dirname(entry);
272
- for (;;) {
273
- const candidate = path.join(dir, "package.json");
274
- if (fs.existsSync(candidate)) {
275
- const pkg = JSON.parse(fs.readFileSync(candidate, "utf8"));
276
- if (pkg.name === "pi-extensible-workflows") {
277
- process.stderr.write(`resolved_package_json=${candidate}\n`);
278
- console.log(pkg.version);
279
- process.exit(0);
280
- }
281
- }
282
- const parent = path.dirname(dir);
283
- if (parent === dir) break;
284
- dir = parent;
285
- }
286
- console.error(`Could not find pi-extensible-workflows package.json by walking up from ${entry}`);
287
- process.exit(1);
288
- ' "${search_root}"
289
- }
290
-
291
299
  # ==============================================================================
292
300
  # Module registry
293
301
  #
@@ -297,30 +305,32 @@ resolve_workflow_version() {
297
305
  # via --all or an explicit --only.
298
306
  # ==============================================================================
299
307
 
300
- MODULE_ORDER=(base node bun pi go dotenv ee pi-workflows herdr gentle-ai engram codex antigravity opencode cockpit)
301
-
302
- declare -A MODULE_DESC=(
303
- [base]="System packages (build tools, git, gh, python, neovim, jq, imagemagick)"
304
- [node]="Node.js via nvm (v22) + npm@latest + sudo-visible symlinks"
305
- [bun]="Bun runtime"
306
- [pi]="pi.dev coding agent CLI"
307
- [go]="Go toolchain"
308
- [dotenv]="vekexasia/dotenv dotfiles (Linux only: clones + runs setup_env.sh)"
309
- [ee]="Engineering Excellence skill (npx skills add, all detected agents)"
310
- [pi-workflows]="pi-extensible-workflows (fix module resolution for pi extensions)"
311
- [herdr]="herdr terminal multiplexer"
312
- [gentle-ai]="gentle-ai / gga (Gentleman's spec-driven agent runner) + gentle-pi package"
313
- [engram]="Engram persistent memory for pi (gentle-engram: /remember /recall /memory /forget)"
314
- [codex]="OpenAI Codex CLI"
315
- [antigravity]="Google Antigravity CLI (agy)"
316
- [opencode]="opencode agent CLI (opencode-ai)"
317
- [cockpit]="cockpit-tools desktop GUI app (optional, CC BY-NC-SA)"
318
- )
308
+ MODULE_ORDER=(base node bun pi go dotenv ee skills pi-workflows herdr gentle-ai codex antigravity opencode cockpit)
309
+
310
+ module_desc() {
311
+ case "$1" in
312
+ base) printf '%s\n' "System packages (build tools, git, gh, python, neovim, jq, imagemagick, go)" ;;
313
+ node) printf '%s\n' "Node.js v22 + npm@latest (nvm on Unix, winget on Windows)" ;;
314
+ bun) printf '%s\n' "Bun runtime" ;;
315
+ pi) printf '%s\n' "pi.dev coding agent CLI" ;;
316
+ go) printf '%s\n' "Go toolchain" ;;
317
+ dotenv) printf '%s\n' "vekexasia/dotenv dotfiles (Linux only: clones + runs setup_env.sh)" ;;
318
+ ee) printf '%s\n' "Engineering Excellence skill (npx skills add, all detected agents)" ;;
319
+ skills) printf '%s\n' "Upstream agent skills (herdr, grilling, research, typescript-advanced, show-me, ...) via npx skills add" ;;
320
+ pi-workflows) printf '%s\n' "pi-extensible-workflows (fix module resolution for pi extensions)" ;;
321
+ herdr) printf '%s\n' "herdr terminal multiplexer" ;;
322
+ gentle-ai) printf '%s\n' "gentle-ai / gga ecosystem configurator (per-agent select + MCP) + gentle-pi" ;;
323
+ codex) printf '%s\n' "OpenAI Codex CLI" ;;
324
+ antigravity) printf '%s\n' "Google Antigravity CLI (agy)" ;;
325
+ opencode) printf '%s\n' "opencode agent CLI (opencode-ai)" ;;
326
+ cockpit) printf '%s\n' "cockpit-tools desktop GUI app (optional, CC BY-NC-SA)" ;;
327
+ *) return 1 ;;
328
+ esac
329
+ }
319
330
 
320
- # Optional modules: excluded from the default/core run.
321
- declare -A MODULE_OPTIONAL=(
322
- [cockpit]=1
323
- )
331
+ module_is_optional() {
332
+ [[ "$1" == "cockpit" ]]
333
+ }
324
334
 
325
335
  # ------------------------------------------------------------------------------
326
336
  # OS / package-manager detection
@@ -444,7 +454,7 @@ mod_node() {
444
454
  # nvm refuses to operate when npm_config_prefix is set (e.g. to /usr/local),
445
455
  # aborting with "nvm is not compatible with the npm_config_prefix ...".
446
456
  # It is only needed by nvm's own shims, so drop it for this process.
447
- unset npm_config_prefix NPM_CONFIG_PREFIX 2>/dev/null || true
457
+ unset npm_config_prefix NPM_CONFIG_PREFIX 2>/dev/null || log_event "WARN" "node" "unset_prefix_failed" "Could not unset npm prefix vars" 0
448
458
 
449
459
  # shellcheck disable=SC1090
450
460
  source "${NVM_DIR}/nvm.sh"
@@ -453,13 +463,7 @@ mod_node() {
453
463
  run_cmd "node" nvm alias default 22
454
464
  run_cmd "node" nvm use 22
455
465
 
456
- node - <<'NODE'
457
- const [major, minor] = process.versions.node.split('.').map(Number);
458
- if (major < 22 || (major === 22 && minor < 19)) {
459
- console.error(`Node.js ${process.versions.node} is too old; pi-extensible-workflows needs >= 22.19.`);
460
- process.exit(1);
461
- }
462
- NODE
466
+ assert_node_minimum
463
467
 
464
468
  # npm@latest (NOT npm@12 — that version does not exist).
465
469
  run_cmd "node" npm install -g npm@latest
@@ -474,6 +478,28 @@ NODE
474
478
  run_optional "node" sudo ln -sf "${npm_bin}" /usr/local/bin/npm
475
479
  }
476
480
 
481
+ assert_node_minimum() {
482
+ require_command node
483
+
484
+ local node_version major minor
485
+ capture_cmd node_version "node" node --version
486
+ if [[ "${node_version}" =~ ^v([0-9]+)\.([0-9]+) ]]; then
487
+ major="${BASH_REMATCH[1]}"
488
+ minor="${BASH_REMATCH[2]}"
489
+ else
490
+ log_event "ERROR" "node" "version_invalid" "Could not parse Node.js version" 1 "version=${node_version}"
491
+ return 1
492
+ fi
493
+
494
+ if (( major < 22 || (major == 22 && minor < 19) )); then
495
+ log_event "ERROR" "node" "version_unsupported" \
496
+ "Node.js ${node_version} is too old; pi-extensible-workflows needs >= 22.19" 1
497
+ return 1
498
+ fi
499
+ log_event "INFO" "node" "runtime_validated" "Node.js version satisfies workflow requirement" 0 \
500
+ "version=${node_version};minimum=22.19"
501
+ }
502
+
477
503
  # --- bun --------------------------------------------------------------------
478
504
  mod_bun() {
479
505
  section "Bun"
@@ -485,7 +511,12 @@ mod_bun() {
485
511
  fi
486
512
  export PATH="${BUN_INSTALL}/bin:${PATH}"
487
513
  require_command bun
488
- log_event "INFO" "bun" "runtime_ready" "Bun runtime validated" 0 "version=$(bun --version)"
514
+ local bun_version
515
+ if capture_cmd bun_version "bun" bun --version; then
516
+ log_event "INFO" "bun" "runtime_ready" "Bun runtime validated" 0 "version=${bun_version}"
517
+ else
518
+ return 1
519
+ fi
489
520
  }
490
521
 
491
522
  # --- pi ---------------------------------------------------------------------
@@ -499,7 +530,10 @@ mod_pi() {
499
530
  fi
500
531
  export PATH="${HOME}/.pi/bin:${HOME}/.local/bin:${PATH}"
501
532
  require_command pi
502
- PI_VERSION="$(pi --version 2>/dev/null || true)"
533
+ if ! PI_VERSION="$(pi --version 2>/dev/null)"; then
534
+ PI_VERSION="unknown"
535
+ log_event "WARN" "pi" "version_unavailable" "Could not read pi --version" 0
536
+ fi
503
537
  log_event "INFO" "pi" "cli_ready" "Pi CLI detected" 0 "version=${PI_VERSION}"
504
538
  mkdir -p "${PI_AGENT_DIR}" "${PI_EXTENSIONS_DIR}" "${PI_NPM_DIR}" "${PI_AGENT_DIR}/skills"
505
539
  }
@@ -508,8 +542,12 @@ mod_pi() {
508
542
  mod_go() {
509
543
  section "Go toolchain"
510
544
  if command -v go >/dev/null 2>&1; then
511
- log_event "INFO" "go" "already_present" "Go already installed" 0 "version=$(go version)"
512
- return
545
+ local go_version
546
+ if capture_cmd go_version "go" go version; then
547
+ log_event "INFO" "go" "already_present" "Go already installed" 0 "version=${go_version}"
548
+ return
549
+ fi
550
+ return 1
513
551
  fi
514
552
  if [[ "${OS_FAMILY}" == "macos" ]]; then
515
553
  run_cmd "go" brew install go
@@ -542,27 +580,37 @@ mod_dotenv() {
542
580
  fi
543
581
 
544
582
  # Idempotent upstream-quirk patches (no-ops once upstream merges the fixes).
545
- if grep -RIl --exclude-dir=.git "gthelding/monokai-pro.nvim" "${DOTENV_DIR}" \
546
- >"${TMP_DIR}/monokai_hits" 2>/dev/null; then
583
+ if grep_probe "dotenv" "${TMP_DIR}/monokai_hits" \
584
+ -RIl --exclude-dir=.git "gthelding/monokai-pro.nvim" "${DOTENV_DIR}"; then
547
585
  while IFS= read -r file; do
548
- sed -i 's|gthelding/monokai-pro.nvim|loctvl842/monokai-pro.nvim|g' "${file}"
586
+ run_cmd "dotenv" sed -i 's|gthelding/monokai-pro.nvim|loctvl842/monokai-pro.nvim|g' "${file}"
549
587
  log_event "INFO" "dotenv" "reference_patched" "Updated stale monokai-pro reference" 0 "file=${file}"
550
588
  done < "${TMP_DIR}/monokai_hits"
551
589
  fi
552
590
 
553
- if grep -RIl --exclude-dir=.git 'sudo npm install -g --prefix /usr/local bun' "${DOTENV_DIR}" \
554
- >"${TMP_DIR}/sudo_npm_hits" 2>/dev/null; then
591
+ if grep_probe "dotenv" "${TMP_DIR}/sudo_npm_hits" \
592
+ -RIl --exclude-dir=.git 'sudo npm install -g --prefix /usr/local bun' "${DOTENV_DIR}"; then
555
593
  while IFS= read -r file; do
556
- sed -i 's|sudo npm install -g --prefix /usr/local bun|sudo "$(command -v npm)" install -g --prefix /usr/local bun|g' "${file}"
594
+ run_cmd "dotenv" sed -i 's|sudo npm install -g --prefix /usr/local bun|sudo "$(command -v npm)" install -g --prefix /usr/local bun|g' "${file}"
557
595
  log_event "INFO" "dotenv" "reference_patched" "Patched sudo npm call to absolute path" 0 "file=${file}"
558
596
  done < "${TMP_DIR}/sudo_npm_hits"
559
597
  fi
560
598
 
561
- if grep -RIl --exclude-dir=.git -e 'npm install -g --prefix "\$HOME/.local" tree-sitter-cli' "${DOTENV_DIR}" \
562
- >"${TMP_DIR}/treesitter_hits" 2>/dev/null; then
599
+ if grep_probe "dotenv" "${TMP_DIR}/treesitter_hits" \
600
+ -RIl --exclude-dir=.git -e 'npm install -g --prefix "\$HOME/.local" tree-sitter-cli' "${DOTENV_DIR}"; then
563
601
  while IFS= read -r file; do
564
- grep -q 'AI_DEV_TS_CLI_PATCH' "${file}" && continue
565
- python3 - "${file}" <<'PYPATCH'
602
+ local marker_rc=0
603
+ grep -q 'AI_DEV_TS_CLI_PATCH' "${file}" 2>>"${HUMAN_LOG}" || marker_rc=$?
604
+ case "${marker_rc}" in
605
+ 0) continue ;; # already patched
606
+ 1) ;; # not patched yet; apply below
607
+ *)
608
+ log_event "ERROR" "dotenv" "marker_probe_failed" \
609
+ "grep marker probe failed with an operational error" "${marker_rc}" "file=${file}"
610
+ exit "${marker_rc}"
611
+ ;;
612
+ esac
613
+ run_cmd "dotenv" python3 - "${file}" <<'PYPATCH'
566
614
  import sys
567
615
  path = sys.argv[1]
568
616
  with open(path, "r", newline="") as fh:
@@ -591,10 +639,55 @@ PYPATCH
591
639
  if [[ -x "${DOTENV_DIR}/setup_env.sh" ]]; then
592
640
  run_cmd "dotenv" bash "${DOTENV_DIR}/setup_env.sh"
593
641
  else
594
- log_event "WARN" "dotenv" "setup_script_missing" "dotenv/setup_env.sh missing or not executable"
642
+ log_event "ERROR" "dotenv" "setup_script_missing" \
643
+ "dotenv/setup_env.sh missing or not executable" 1 \
644
+ "expected=${DOTENV_DIR}/setup_env.sh"
645
+ return 1
595
646
  fi
596
647
  }
597
648
 
649
+ # --- skill target helpers ----------------------------------------------------
650
+ agent_config_dir() {
651
+ case "$1" in
652
+ pi) printf '%s\n' "${HOME}/.pi" ;;
653
+ claude-code) printf '%s\n' "${HOME}/.claude" ;;
654
+ gemini-cli) printf '%s\n' "${HOME}/.gemini" ;;
655
+ cursor) printf '%s\n' "${HOME}/.cursor" ;;
656
+ antigravity) printf '%s\n' "${HOME}/.antigravity" ;;
657
+ codex) printf '%s\n' "${HOME}/.codex" ;;
658
+ opencode) printf '%s\n' "${HOME}/.config/opencode" ;;
659
+ *) return 1 ;;
660
+ esac
661
+ }
662
+
663
+ agent_skill_root() {
664
+ case "$1" in
665
+ pi) printf '%s\n' "${PI_AGENT_DIR}/skills" ;;
666
+ claude-code) printf '%s\n' "${HOME}/.claude/skills" ;;
667
+ gemini-cli) printf '%s\n' "${HOME}/.gemini/skills" ;;
668
+ cursor) printf '%s\n' "${HOME}/.cursor/skills" ;;
669
+ antigravity) printf '%s\n' "${HOME}/.antigravity/skills" ;;
670
+ codex) printf '%s\n' "${HOME}/.codex/skills" ;;
671
+ opencode) printf '%s\n' "${HOME}/.config/opencode/skills" ;;
672
+ *) return 1 ;;
673
+ esac
674
+ }
675
+
676
+ verify_skill_for_agents() {
677
+ local phase="$1" skill="$2"; shift 2
678
+ local agent root
679
+ for agent in "$@"; do
680
+ root="$(agent_skill_root "${agent}")"
681
+ if [[ ! -f "${root}/${skill}/SKILL.md" ]]; then
682
+ log_event "ERROR" "${phase}" "skill_missing" \
683
+ "Skill SKILL.md missing for targeted agent" 1 \
684
+ "agent=${agent};skill=${skill};expected=${root}/${skill}/SKILL.md"
685
+ return 1
686
+ fi
687
+ done
688
+ return 0
689
+ }
690
+
598
691
  # --- engineering-excellence -------------------------------------------------
599
692
  mod_ee() {
600
693
  section "Engineering Excellence"
@@ -604,41 +697,72 @@ mod_ee() {
604
697
  # (replaces the old git-clone-and-move). Agents are detected by their config dir.
605
698
  # Keys are the `skills` CLI agent names (claude-code, gemini-cli, ...), which
606
699
  # differ from the config-dir basename; values are the dir we detect them by.
607
- local agent dir
608
- local -A agent_dir=(
609
- [pi]="${HOME}/.pi"
610
- [claude-code]="${HOME}/.claude"
611
- [gemini-cli]="${HOME}/.gemini"
612
- [cursor]="${HOME}/.cursor"
613
- [antigravity]="${HOME}/.antigravity"
614
- [codex]="${HOME}/.codex"
615
- [opencode]="${HOME}/.config/opencode"
616
- )
700
+ local agent
617
701
  local installed_any=0
702
+ local -a target_agents=()
618
703
  for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
619
- dir="${agent_dir[$agent]}"
620
- [[ -d "${dir}" ]] || continue
621
- run_optional "engineering-excellence" \
704
+ [[ -d "$(agent_config_dir "${agent}")" ]] || continue
705
+ run_cmd "engineering-excellence" \
622
706
  npx --yes skills@latest add "${ENGINEERING_EXCELLENCE_SLUG}" \
623
707
  --skill "${ENGINEERING_EXCELLENCE_SKILL}" --global --agent "${agent}" --copy --yes
708
+ target_agents+=("${agent}")
624
709
  installed_any=1
625
710
  done
626
711
 
627
712
  if (( installed_any == 0 )); then
628
713
  # No agent detected yet — install at least for pi (created by mod_pi).
629
- run_optional "engineering-excellence" \
714
+ run_cmd "engineering-excellence" \
630
715
  npx --yes skills@latest add "${ENGINEERING_EXCELLENCE_SLUG}" \
631
716
  --skill "${ENGINEERING_EXCELLENCE_SKILL}" --global --agent pi --copy --yes
717
+ target_agents=(pi)
632
718
  fi
633
719
 
634
- if [[ -f "${ENGINEERING_EXCELLENCE_DIR}/SKILL.md" ]]; then
635
- log_event "INFO" "engineering-excellence" "skill_installed" "EE skill present for pi" 0 \
636
- "path=${ENGINEERING_EXCELLENCE_DIR}"
637
- else
638
- log_event "WARN" "engineering-excellence" "skill_path_unverified" \
639
- "EE SKILL.md not found at pi path; check other agents' skill dirs" 0 \
640
- "expected=${ENGINEERING_EXCELLENCE_DIR}"
641
- fi
720
+ verify_skill_for_agents "engineering-excellence" "${ENGINEERING_EXCELLENCE_SKILL}" "${target_agents[@]}"
721
+ log_event "INFO" "engineering-excellence" "skills_verified" \
722
+ "Engineering Excellence skill verified for every targeted agent" 0 \
723
+ "agents=$(IFS=,; printf '%s' "${target_agents[*]}")"
724
+ }
725
+
726
+ # --- upstream agent skills --------------------------------------------------
727
+ # Installs vekexasia/dotenv's skill stack on every OS via `npx skills add`.
728
+ # On Linux the dotenv module may already install these; skills add --copy is
729
+ # idempotent, so a re-run is safe.
730
+ mod_skills() {
731
+ section "Agent skills (upstream stack)"
732
+ require_command npx
733
+
734
+ # Agent keys are the `skills` CLI names (claude-code, gemini-cli, ...),
735
+ # detected by their config dir — same mapping as mod_ee.
736
+ local agent
737
+ local -a agents=()
738
+ for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
739
+ [[ -d "$(agent_config_dir "${agent}")" ]] && agents+=("${agent}")
740
+ done
741
+ # pi is created by mod_pi; guarantee at least pi so the stack always lands.
742
+ (( ${#agents[@]} == 0 )) && agents=(pi)
743
+
744
+ local source_spec source skill_csv
745
+ local -a skill_list
746
+ for source_spec in "${UPSTREAM_SKILL_SOURCES[@]}"; do
747
+ source="${source_spec%% *}"
748
+ skill_csv="${source_spec#* }"
749
+ # The script keeps global IFS at newline/tab for safe line reads;
750
+ # explicitly restore spaces for this space-separated skill list.
751
+ IFS=' ' read -r -a skill_list <<< "${skill_csv}"
752
+ for agent in "${agents[@]}"; do
753
+ run_cmd "skills" \
754
+ npx --yes skills@latest add "${source}" \
755
+ --skill "${skill_list[@]}" --global --agent "${agent}" --copy --yes
756
+ done
757
+ done
758
+
759
+ local sk
760
+ for sk in "${UPSTREAM_SKILL_NAMES[@]}"; do
761
+ verify_skill_for_agents "skills" "${sk}" "${agents[@]}"
762
+ done
763
+ log_event "INFO" "skills" "skills_verified" \
764
+ "Upstream skills verified for every targeted agent" 0 \
765
+ "agents=$(IFS=,; printf '%s' "${agents[*]}")"
642
766
  }
643
767
 
644
768
  # --- pi-extensible-workflows ------------------------------------------------
@@ -646,6 +770,7 @@ mod_pi_workflows() {
646
770
  section "pi-extensible-workflows"
647
771
  require_command pi
648
772
  require_command node
773
+ assert_node_minimum
649
774
 
650
775
  if [[ -z "${PI_WORKFLOW_VERSION}" ]]; then
651
776
  capture_cmd PI_WORKFLOW_VERSION "pi-workflows" npm view pi-extensible-workflows version
@@ -661,16 +786,33 @@ mod_pi_workflows() {
661
786
  mkdir -p "${PI_EXTENSIONS_DIR}"
662
787
  pushd "${PI_EXTENSIONS_DIR}" >/dev/null
663
788
  printf '%s\n' 'ignore-scripts=false' > .npmrc
789
+ # Mark this directory as an npm project root so `npm install` lands HERE and
790
+ # cannot walk up the tree into an ancestor project. This matters when
791
+ # ~/.pi/agent is symlinked into another repo (e.g. the dotenv dotfiles):
792
+ # without a local package.json, npm would install into that repo's
793
+ # node_modules and verification would then pick up a stale, shadowing copy.
794
+ if [[ ! -f package.json ]]; then
795
+ printf '%s\n' '{"name":"pi-extensions","private":true}' > package.json
796
+ fi
664
797
  run_cmd "pi-workflows-node" npm install --save-exact --no-audit --no-fund \
665
798
  "pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
666
799
  popd >/dev/null
667
800
 
668
- local resolved
669
- resolved="$(node -e 'console.log(require.resolve("pi-extensible-workflows",{paths:[process.argv[1]]}))' "${PI_EXTENSIONS_DIR}")"
670
- log_event "INFO" "pi-workflows-node" "module_resolved" "Resolvable from pi extensions dir" 0 "resolved=${resolved}"
801
+ # Verify the version that actually landed in THIS directory by reading its
802
+ # local package.json directly. Do NOT use require.resolve here: it ascends
803
+ # the directory tree and can resolve a shadowing copy in an ancestor
804
+ # node_modules, producing a false version_mismatch.
805
+ local local_pkg="${PI_EXTENSIONS_DIR}/node_modules/pi-extensible-workflows/package.json"
806
+ if [[ ! -f "${local_pkg}" ]]; then
807
+ log_event "ERROR" "pi-workflows-node" "install_missing" "pi-extensible-workflows not installed in extensions dir" 1 \
808
+ "expected_path=${local_pkg}"
809
+ exit 1
810
+ fi
811
+ log_event "INFO" "pi-workflows-node" "module_resolved" "Installed in pi extensions dir" 0 "resolved=${local_pkg}"
671
812
 
672
813
  local installed
673
- installed="$(resolve_workflow_version "${PI_EXTENSIONS_DIR}")"
814
+ capture_cmd installed "pi-workflows-node" node -e \
815
+ 'console.log(require(process.argv[1]).version)' "${local_pkg}"
674
816
  if [[ "${installed}" != "${PI_WORKFLOW_VERSION}" ]]; then
675
817
  log_event "ERROR" "pi-workflows-node" "version_mismatch" "Installed version mismatch" 1 \
676
818
  "expected=${PI_WORKFLOW_VERSION};actual=${installed}"
@@ -680,9 +822,31 @@ mod_pi_workflows() {
680
822
  if [[ -d "${DOTENV_EXT_DIR}" ]]; then
681
823
  pushd "${DOTENV_EXT_DIR}" >/dev/null
682
824
  printf '%s\n' 'ignore-scripts=false' > .npmrc
825
+ if [[ ! -f package.json ]]; then
826
+ printf '%s\n' '{"name":"pi-ext-workflows","private":true}' > package.json
827
+ fi
683
828
  run_cmd "dotenv-workflows" npm install --save-exact --no-audit --no-fund \
684
829
  "pi-extensible-workflows@${PI_WORKFLOW_VERSION}"
685
830
  popd >/dev/null
831
+
832
+ local dotenv_workflow_pkg="${DOTENV_EXT_DIR}/node_modules/pi-extensible-workflows/package.json"
833
+ if [[ ! -f "${dotenv_workflow_pkg}" ]]; then
834
+ log_event "ERROR" "dotenv-workflows" "install_missing" \
835
+ "pi-extensible-workflows not installed in dotenv extensions dir" 1 \
836
+ "expected_path=${dotenv_workflow_pkg}"
837
+ exit 1
838
+ fi
839
+ local dotenv_installed
840
+ capture_cmd dotenv_installed "dotenv-workflows" node -e \
841
+ 'console.log(require(process.argv[1]).version)' "${dotenv_workflow_pkg}"
842
+ if [[ "${dotenv_installed}" != "${PI_WORKFLOW_VERSION}" ]]; then
843
+ log_event "ERROR" "dotenv-workflows" "version_mismatch" \
844
+ "Installed dotenv workflow version mismatch" 1 \
845
+ "expected=${PI_WORKFLOW_VERSION};actual=${dotenv_installed}"
846
+ exit 1
847
+ fi
848
+ log_event "INFO" "dotenv-workflows" "module_verified" \
849
+ "Verified installed dotenv workflow package" 0 "path=${dotenv_workflow_pkg}"
686
850
  fi
687
851
  }
688
852
 
@@ -690,90 +854,126 @@ mod_pi_workflows() {
690
854
  mod_herdr() {
691
855
  section "herdr"
692
856
  if command -v herdr >/dev/null 2>&1; then
693
- log_event "INFO" "herdr" "already_present" "herdr already installed" 0 "version=$(herdr --version 2>/dev/null || true)"
694
- return
857
+ local herdr_version
858
+ if capture_cmd herdr_version "herdr" herdr --version; then
859
+ log_event "INFO" "herdr" "already_present" "herdr already installed" 0 "version=${herdr_version}"
860
+ return
861
+ fi
862
+ return 1
695
863
  fi
696
864
  if [[ "${OS_FAMILY}" == "macos" ]]; then
697
- run_optional "herdr" brew install herdr
865
+ run_cmd "herdr" brew install herdr
698
866
  else
699
867
  local installer="${TMP_DIR}/install-herdr.sh"
700
- run_optional "herdr" curl -fsSL https://herdr.dev/install.sh -o "${installer}"
701
- [[ -s "${installer}" ]] && run_optional "herdr" sh "${installer}"
868
+ run_cmd "herdr" curl -fsSL https://herdr.dev/install.sh -o "${installer}"
869
+ [[ -s "${installer}" ]] || {
870
+ log_event "ERROR" "herdr" "installer_missing" "herdr installer is empty" 1 "path=${installer}"
871
+ return 1
872
+ }
873
+ run_cmd "herdr" sh "${installer}"
702
874
  fi
875
+ require_command herdr
703
876
  }
704
877
 
705
878
  # --- gentle-ai --------------------------------------------------------------
879
+ # Reinstates the Gentle AI ecosystem configurator. `gentle-ai install` is the
880
+ # per-agent/per-IDE selector (Pi, Claude Code, Cursor, Codex, ...) that also
881
+ # wires each selected agent's MCP servers, so tools appear under /mcp. The
882
+ # configurator is a CORE step: it always runs (interactively with a TTY, or
883
+ # non-interactively over the detected agents in CI/pipes) and a failure fails
884
+ # the module. For pi we additionally guarantee + verify the first-class
885
+ # gentle-pi harness and pi-mcp-adapter. Idempotent: safe to re-run.
706
886
  mod_gentle_ai() {
707
887
  section "gentle-ai"
708
- if ! command -v gentle-ai >/dev/null 2>&1 && ! command -v gga >/dev/null 2>&1; then
888
+
889
+ # Installers drop the CLI into a PATH dir; make sure the usual ones resolve
890
+ # in this live process so the post-install verification can find it.
891
+ export PATH="${HOME}/.local/bin:${HOME}/go/bin:${PATH}"
892
+
893
+ # The per-agent selector and the pi harness both require the gentle-ai CLI
894
+ # itself; a legacy standalone `gga` is NOT enough, so install whenever the
895
+ # gentle-ai CLI is missing even if an old gga is on PATH.
896
+ if ! command -v gentle-ai >/dev/null 2>&1; then
709
897
  if [[ "${OS_FAMILY}" == "macos" ]]; then
710
- run_optional "gentle-ai" brew tap gentleman-programming/tap
711
- run_optional "gentle-ai" brew install gentle-ai
898
+ run_cmd "gentle-ai" brew tap gentleman-programming/tap
899
+ run_cmd "gentle-ai" brew install gentle-ai
712
900
  else
713
901
  local installer="${TMP_DIR}/install-gentle-ai.sh"
714
- run_optional "gentle-ai" curl -fsSL "${GENTLE_AI_INSTALL}" -o "${installer}"
715
- [[ -s "${installer}" ]] && run_optional "gentle-ai" bash "${installer}"
902
+ run_cmd "gentle-ai" curl -fsSL "${GENTLE_AI_INSTALL}" -o "${installer}"
903
+ [[ -s "${installer}" ]] || {
904
+ log_event "ERROR" "gentle-ai" "installer_missing" "gentle-ai installer is empty" 1 "path=${installer}"
905
+ return 1
906
+ }
907
+ run_cmd "gentle-ai" bash "${installer}"
908
+ fi
909
+ fi
910
+
911
+ # Verify the CLI is present (a binary, not an npm tree — command -v + --version
912
+ # is the correct check). Required for both the selector and the pi harness.
913
+ command -v gentle-ai >/dev/null 2>&1 || {
914
+ log_event "ERROR" "gentle-ai" "binary_missing" "gentle-ai CLI not found on PATH after install" 1
915
+ return 1
916
+ }
917
+ run_cmd "gentle-ai" gentle-ai --version
918
+
919
+ # Detect the agents/IDEs present on this machine (same mapping as mod_ee).
920
+ local agent
921
+ local -a detected_agents=()
922
+ for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
923
+ [[ -d "$(agent_config_dir "${agent}")" ]] && detected_agents+=("${agent}")
924
+ done
925
+ (( ${#detected_agents[@]} == 0 )) && detected_agents=(pi)
926
+
927
+ # Per-agent / per-IDE selection + MCP wiring, owned by gentle-ai. This is a
928
+ # core step and must actually run: with a real TTY we launch the interactive
929
+ # selector (run directly — run_cmd would redirect stdout and hide prompts);
930
+ # otherwise we run it non-interactively over the detected agents so CI/pipes
931
+ # never hang. A failure fails the module (no silent downgrade).
932
+ if [[ -t 0 && -t 1 ]]; then
933
+ log_event "INFO" "gentle-ai" "configurator_start" "Launching gentle-ai install (choose agents/IDEs + MCP)" 0
934
+ if ! gentle-ai install --scope global; then
935
+ log_event "ERROR" "gentle-ai" "configurator_failed" "gentle-ai install failed" 1
936
+ return 1
716
937
  fi
938
+ else
939
+ local agents_csv; agents_csv="$(IFS=,; printf '%s' "${detected_agents[*]}")"
940
+ log_event "INFO" "gentle-ai" "configurator_noninteractive" \
941
+ "No TTY; installing gentle-ai for detected agents" 0 "agents=${agents_csv}"
942
+ run_cmd "gentle-ai" gentle-ai install --scope global --agents "${agents_csv}"
717
943
  fi
944
+ log_event "INFO" "gentle-ai" "configurator_done" "gentle-ai install completed" 0
718
945
 
719
- # Enable gentle-ai INSIDE pi as packages (the standalone gga binary alone
720
- # does not register anything in pi this is why it wasn't visible there).
946
+ # Guarantee pi reads gentle-ai in its MCP list (/mcp): install the first-class
947
+ # gentle-pi harness and the pi-mcp-adapter bridge, then verify the exact
948
+ # target (pi's own settings file), not a walked resolution.
721
949
  if command -v pi >/dev/null 2>&1; then
722
- run_optional "gentle-ai" pi install npm:gentle-pi
723
- run_optional "gentle-ai" pi install npm:pi-mcp-adapter
724
- log_event "INFO" "gentle-ai" "pi_enabled" "gentle-pi registered in pi (verify: /gentle-ai:status)" 0
950
+ run_cmd "gentle-ai" pi install npm:gentle-pi
951
+ run_cmd "gentle-ai" pi install npm:pi-mcp-adapter
952
+ local pi_settings="${PI_AGENT_DIR}/settings.json"
953
+ if [[ -f "${pi_settings}" ]] \
954
+ && grep -q '"npm:gentle-pi"' "${pi_settings}" \
955
+ && grep -q '"npm:pi-mcp-adapter"' "${pi_settings}"; then
956
+ log_event "INFO" "gentle-ai" "pi_enabled" \
957
+ "gentle-pi + pi-mcp-adapter registered in pi (verify: /mcp, /gentle-ai:status)" 0
958
+ else
959
+ log_event "ERROR" "gentle-ai" "pi_enable_failed" \
960
+ "gentle-pi and/or pi-mcp-adapter not present in pi settings after install" 1 "expected=${pi_settings}"
961
+ return 1
962
+ fi
725
963
  fi
726
964
 
727
- # Surface gentle-ai's own next-steps (do NOT run — they are per-repo).
728
965
  log_event "INFO" "gentle-ai" "next_steps" "gentle-ai post-install hints" 0
729
966
  cat <<'HINT' | tee -a "${HUMAN_LOG}"
730
967
  gentle-ai next steps (run yourself, per project):
731
968
  1) Set your API keys
732
969
  2) Run your selected agent
733
- 3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models)
970
+ 3) Try: /sdd-new my-feature (in pi: /gentle-ai:status, /gentleman:models, /mcp)
734
971
  GGA (per project):
735
972
  gga init # inside each repo
736
973
  gga install
737
974
  HINT
738
975
  }
739
976
 
740
- # --- engram (pi persistent memory) ------------------------------------------
741
- # The gentle-engram pi extension auto-starts `engram serve`, so the Engram Go
742
- # binary MUST be on PATH first — otherwise the extension loads but silently
743
- # fails (the "engram doesn't work in pi" symptom). Install binary, then the pi
744
- # packages, then init, then the user restarts pi.
745
- mod_engram() {
746
- section "Engram memory (pi)"
747
-
748
- # 1. Engram binary (Go).
749
- if ! command -v engram >/dev/null 2>&1; then
750
- if [[ "${OS_FAMILY}" == "macos" ]]; then
751
- run_optional "engram" brew install gentleman-programming/tap/engram
752
- fi
753
- if ! command -v engram >/dev/null 2>&1 && command -v go >/dev/null 2>&1; then
754
- run_optional "engram" go install github.com/Gentleman-Programming/engram/cmd/engram@latest
755
- fi
756
- fi
757
- # go installs into $GOPATH/bin (default ~/go/bin), which may not be on PATH yet.
758
- [[ -x "${HOME}/go/bin/engram" ]] && export PATH="${HOME}/go/bin:${PATH}"
759
-
760
- if ! command -v engram >/dev/null 2>&1; then
761
- log_event "WARN" "engram" "binary_missing" \
762
- "engram binary not found after install; the pi extension needs it on PATH" 1
763
- fi
764
-
765
- # 2. pi integration (in-process extension is the primary path; MCP adapter optional).
766
- if command -v pi >/dev/null 2>&1; then
767
- run_optional "engram" pi install npm:gentle-engram
768
- run_optional "engram" pi install npm:pi-mcp-adapter
769
- run_optional "engram" npm exec --yes --package gentle-engram@latest -- pi-engram init
770
- log_event "INFO" "engram" "enabled" \
771
- "Engram enabled — RESTART pi, then verify with mem_current_project / mem_doctor / 'engram tui'" 0
772
- else
773
- log_event "WARN" "engram" "pi_missing" "pi not found; Engram pi integration skipped"
774
- fi
775
- }
776
-
777
977
  # --- codex ------------------------------------------------------------------
778
978
  mod_codex() {
779
979
  section "Codex CLI"
@@ -782,13 +982,18 @@ mod_codex() {
782
982
  return
783
983
  fi
784
984
  if [[ "${OS_FAMILY}" == "macos" ]] && command -v brew >/dev/null 2>&1; then
785
- run_optional "codex" brew install --cask codex
985
+ run_cmd "codex" brew install --cask codex
786
986
  fi
787
987
  if ! command -v codex >/dev/null 2>&1; then
788
988
  local installer="${TMP_DIR}/install-codex.sh"
789
- run_optional "codex" curl -fsSL https://chatgpt.com/codex/install.sh -o "${installer}"
790
- [[ -s "${installer}" ]] && run_optional "codex" sh "${installer}"
989
+ run_cmd "codex" curl -fsSL https://chatgpt.com/codex/install.sh -o "${installer}"
990
+ [[ -s "${installer}" ]] || {
991
+ log_event "ERROR" "codex" "installer_missing" "codex installer is empty" 1 "path=${installer}"
992
+ return 1
993
+ }
994
+ run_cmd "codex" sh "${installer}"
791
995
  fi
996
+ require_command codex
792
997
  }
793
998
 
794
999
  # --- antigravity ------------------------------------------------------------
@@ -799,8 +1004,13 @@ mod_antigravity() {
799
1004
  return
800
1005
  fi
801
1006
  local installer="${TMP_DIR}/install-antigravity.sh"
802
- run_optional "antigravity" curl -fsSL https://antigravity.google/cli/install.sh -o "${installer}"
803
- [[ -s "${installer}" ]] && run_optional "antigravity" bash "${installer}"
1007
+ run_cmd "antigravity" curl -fsSL https://antigravity.google/cli/install.sh -o "${installer}"
1008
+ [[ -s "${installer}" ]] || {
1009
+ log_event "ERROR" "antigravity" "installer_missing" "Antigravity installer is empty" 1 "path=${installer}"
1010
+ return 1
1011
+ }
1012
+ run_cmd "antigravity" bash "${installer}"
1013
+ require_command agy
804
1014
  }
805
1015
 
806
1016
  # --- opencode ---------------------------------------------------------------
@@ -811,12 +1021,17 @@ mod_opencode() {
811
1021
  return
812
1022
  fi
813
1023
  if [[ "${OS_FAMILY}" == "macos" ]]; then
814
- run_optional "opencode" brew install anomalyco/tap/opencode
1024
+ run_cmd "opencode" brew install anomalyco/tap/opencode
815
1025
  else
816
1026
  local installer="${TMP_DIR}/install-opencode.sh"
817
- run_optional "opencode" curl -fsSL https://opencode.ai/install -o "${installer}"
818
- [[ -s "${installer}" ]] && run_optional "opencode" bash "${installer}"
1027
+ run_cmd "opencode" curl -fsSL https://opencode.ai/install -o "${installer}"
1028
+ [[ -s "${installer}" ]] || {
1029
+ log_event "ERROR" "opencode" "installer_missing" "opencode installer is empty" 1 "path=${installer}"
1030
+ return 1
1031
+ }
1032
+ run_cmd "opencode" bash "${installer}"
819
1033
  fi
1034
+ require_command opencode
820
1035
 
821
1036
  log_event "INFO" "opencode" "zen_hint" "OpenCode Go / Zen provider hint" 0
822
1037
  cat <<'HINT' | tee -a "${HUMAN_LOG}"
@@ -862,8 +1077,11 @@ mod_cockpit() {
862
1077
  esac
863
1078
 
864
1079
  local url
865
- url="$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "${release_json}" \
866
- | cut -d '"' -f4 | grep -iE "${asset_pat}" | head -n1 || true)"
1080
+ if ! url="$(grep -oE '"browser_download_url":[[:space:]]*"[^"]+"' "${release_json}" \
1081
+ | cut -d '"' -f4 | grep -iE "${asset_pat}" | sed -n '1p')"; then
1082
+ log_event "WARN" "cockpit" "asset_parse_failed" "Could not parse cockpit-tools release assets"
1083
+ return 0
1084
+ fi
867
1085
 
868
1086
  if [[ -z "${url}" ]]; then
869
1087
  log_event "WARN" "cockpit" "no_matching_asset" \
@@ -873,7 +1091,10 @@ mod_cockpit() {
873
1091
 
874
1092
  local installer="${TMP_DIR}/cockpit-asset"
875
1093
  run_optional "cockpit" curl -fsSL "${url}" -o "${installer}"
876
- [[ -s "${installer}" ]] || return
1094
+ if [[ ! -s "${installer}" ]]; then
1095
+ log_event "WARN" "cockpit" "download_unavailable" "Could not download the selected cockpit-tools asset"
1096
+ return 0
1097
+ fi
877
1098
 
878
1099
  case "${PM}" in
879
1100
  apt-get) run_optional "cockpit" sudo apt-get install -y "${installer}" ;;
@@ -928,17 +1149,75 @@ quality_gates() {
928
1149
  run_cmd "quality" bash -n "${BASH_SOURCE[0]}"
929
1150
 
930
1151
  is_selected node && { run_cmd "quality" node --version; run_cmd "quality" npm --version; }
931
- is_selected bun && run_optional "quality" bun --version
932
- is_selected pi && run_optional "quality" pi --no-extensions --version
1152
+ is_selected bun && run_cmd "quality" bun --version
1153
+ if is_selected pi; then
1154
+ require_command pi
1155
+ run_cmd "quality" pi --no-extensions --version
1156
+ fi
1157
+
1158
+ if is_selected node || is_selected pi-workflows; then
1159
+ assert_node_minimum
1160
+ fi
933
1161
 
934
1162
  if is_selected pi-workflows; then
1163
+ local workflow_pkg="${PI_EXTENSIONS_DIR}/node_modules/pi-extensible-workflows/package.json"
1164
+ if [[ ! -f "${workflow_pkg}" ]]; then
1165
+ log_event "ERROR" "quality" "workflow_package_missing" \
1166
+ "pi-extensible-workflows package.json missing from extensions dir" 1 \
1167
+ "expected_path=${workflow_pkg}"
1168
+ exit 1
1169
+ fi
935
1170
  run_cmd "quality" node -e \
936
- 'const root=process.argv[1]; console.log("RESOLVED="+require.resolve("pi-extensible-workflows",{paths:[root]}))' \
937
- "${PI_EXTENSIONS_DIR}"
1171
+ 'const fs=require("fs"); const path=process.argv[1]; const pkg=JSON.parse(fs.readFileSync(path,"utf8")); console.log("VERSION="+pkg.version)' \
1172
+ "${workflow_pkg}"
938
1173
  fi
939
1174
 
940
- if is_selected ee && [[ -f "${ENGINEERING_EXCELLENCE_DIR}/SKILL.md" ]]; then
941
- log_event "INFO" "quality" "ee_gate_passed" "Engineering Excellence SKILL.md present"
1175
+ local -a target_agents=()
1176
+ local agent sk
1177
+ for agent in pi claude-code gemini-cli cursor antigravity codex opencode; do
1178
+ [[ -d "$(agent_config_dir "${agent}")" ]] && target_agents+=("${agent}")
1179
+ done
1180
+ (( ${#target_agents[@]} == 0 )) && target_agents=(pi)
1181
+
1182
+ if is_selected ee; then
1183
+ verify_skill_for_agents "quality" "${ENGINEERING_EXCELLENCE_SKILL}" "${target_agents[@]}"
1184
+ log_event "INFO" "quality" "ee_gate_passed" \
1185
+ "Engineering Excellence skill verified for every targeted agent" 0
1186
+ fi
1187
+
1188
+ if is_selected skills; then
1189
+ for sk in "${UPSTREAM_SKILL_NAMES[@]}"; do
1190
+ verify_skill_for_agents "quality" "${sk}" "${target_agents[@]}"
1191
+ done
1192
+ log_event "INFO" "quality" "skills_gate_passed" \
1193
+ "All upstream skills verified for every targeted agent" 0
1194
+ fi
1195
+
1196
+ if is_selected gentle-ai; then
1197
+ # The module requires the gentle-ai CLI specifically; a legacy standalone
1198
+ # gga cannot prove the per-agent/MCP configuration ran.
1199
+ if ! command -v gentle-ai >/dev/null 2>&1; then
1200
+ log_event "ERROR" "quality" "gentle_ai_missing" \
1201
+ "gentle-ai CLI not found on PATH after install" 1
1202
+ exit 1
1203
+ fi
1204
+ if command -v pi >/dev/null 2>&1; then
1205
+ local gentle_settings="${PI_AGENT_DIR}/settings.json"
1206
+ if [[ ! -f "${gentle_settings}" ]] \
1207
+ || ! grep -q '"npm:gentle-pi"' "${gentle_settings}" \
1208
+ || ! grep -q '"npm:pi-mcp-adapter"' "${gentle_settings}"; then
1209
+ log_event "ERROR" "quality" "gentle_pi_missing" \
1210
+ "gentle-pi and/or pi-mcp-adapter not registered in pi settings" 1 "expected=${gentle_settings}"
1211
+ exit 1
1212
+ fi
1213
+ log_event "INFO" "quality" "gentle_ai_gate_passed" \
1214
+ "gentle-ai verified (CLI present; gentle-pi + pi-mcp-adapter registered in pi)" 0
1215
+ else
1216
+ # No pi on PATH: pi MCP wiring is genuinely not applicable, so do not
1217
+ # claim it was registered.
1218
+ log_event "INFO" "quality" "gentle_ai_gate_passed" \
1219
+ "gentle-ai CLI verified; pi not present, so pi MCP wiring was not required" 0
1220
+ fi
942
1221
  fi
943
1222
 
944
1223
  log_event "INFO" "quality" "gates_done" "Quality gates completed for selected modules"
@@ -963,8 +1242,8 @@ print_list() {
963
1242
  printf 'AI Dev Suite %s — modules (core = installed by default):\n\n' "${SCRIPT_VERSION}"
964
1243
  local m tag
965
1244
  for m in "${MODULE_ORDER[@]}"; do
966
- if [[ -n "${MODULE_OPTIONAL[$m]:-}" ]]; then tag="optional"; else tag="core "; fi
967
- printf ' [%s] %-14s %s\n' "${tag}" "${m}" "${MODULE_DESC[$m]}"
1245
+ if module_is_optional "${m}"; then tag="optional"; else tag="core "; fi
1246
+ printf ' [%s] %-14s %s\n' "${tag}" "${m}" "$(module_desc "${m}")"
968
1247
  done
969
1248
  printf '\nUse: --only <csv> | --all | (default = core)\n'
970
1249
  }
@@ -976,11 +1255,14 @@ print_help() {
976
1255
  }
977
1256
 
978
1257
  parse_args() {
979
- local mode="default" only_csv=""
1258
+ local mode="default" only_csv="" all_requested=0
980
1259
  while [[ $# -gt 0 ]]; do
981
1260
  case "$1" in
982
- --all) mode="all"; shift ;;
983
- --only) mode="only"; only_csv="${2:-}"; shift 2 ;;
1261
+ --all) mode="all"; all_requested=1; shift ;;
1262
+ --only)
1263
+ [[ $# -ge 2 ]] || { printf '%s\n' '--only requires a non-empty comma-separated module list.' >&2; exit 2; }
1264
+ mode="only"; only_csv="$2"; shift 2
1265
+ ;;
984
1266
  --only=*) mode="only"; only_csv="${1#*=}"; shift ;;
985
1267
  --yes|-y) shift ;; # accepted for launcher parity
986
1268
  --list) print_list; exit 0 ;;
@@ -989,26 +1271,37 @@ parse_args() {
989
1271
  esac
990
1272
  done
991
1273
 
1274
+ # Match PowerShell: -All wins whenever it is present, regardless of
1275
+ # where it appears relative to --only.
1276
+ (( all_requested == 1 )) && mode="all"
1277
+
992
1278
  local requested=()
993
1279
  case "${mode}" in
994
1280
  all) requested=("${MODULE_ORDER[@]}") ;;
995
1281
  only)
996
- local IFS_SAVE="${IFS}"; IFS=','
997
- # shellcheck disable=SC2206
998
- local raw=(${only_csv})
999
- IFS="${IFS_SAVE}"
1282
+ # Split on commas without word-splitting or glob expansion.
1283
+ local raw=()
1284
+ IFS=',' read -r -a raw <<< "${only_csv}"
1000
1285
  local r
1001
1286
  for r in "${raw[@]}"; do
1002
- r="$(printf '%s' "${r}" | tr -d '[:space:]')"
1287
+ # Trim only surrounding whitespace (match PowerShell .Trim());
1288
+ # internal whitespace makes the token invalid instead of being
1289
+ # silently collapsed (e.g. "co dex" must not become "codex").
1290
+ r="${r#"${r%%[![:space:]]*}"}"
1291
+ r="${r%"${r##*[![:space:]]}"}"
1003
1292
  [[ -z "${r}" ]] && continue
1004
- [[ -n "${MODULE_DESC[$r]:-}" ]] || { printf 'Unknown module: %s\n' "${r}" >&2; exit 2; }
1293
+ module_desc "${r}" >/dev/null || { printf 'Unknown module: %s\n' "${r}" >&2; exit 2; }
1005
1294
  requested+=("${r}")
1006
1295
  done
1296
+ (( ${#requested[@]} > 0 )) || {
1297
+ printf '%s\n' '--only requires a non-empty comma-separated module list.' >&2
1298
+ exit 2
1299
+ }
1007
1300
  ;;
1008
1301
  default)
1009
1302
  local m
1010
1303
  for m in "${MODULE_ORDER[@]}"; do
1011
- [[ -n "${MODULE_OPTIONAL[$m]:-}" ]] && continue
1304
+ module_is_optional "${m}" && continue
1012
1305
  requested+=("${m}")
1013
1306
  done
1014
1307
  ;;
@@ -1051,10 +1344,15 @@ parse_args "$@"
1051
1344
 
1052
1345
  section "Preflight"
1053
1346
  require_command bash
1054
- require_command curl
1055
- require_command git
1056
1347
  detect_os
1057
1348
 
1349
+ # The base module owns bootstrap tools on a clean machine. For explicit
1350
+ # selections that skip base, fail early because later installers need them.
1351
+ if ! is_selected base; then
1352
+ require_command curl
1353
+ require_command git
1354
+ fi
1355
+
1058
1356
  if [[ "${OS_FAMILY}" == "linux" ]]; then
1059
1357
  require_command sudo
1060
1358
  fi
@@ -1071,6 +1369,11 @@ quality_gates
1071
1369
 
1072
1370
  write_report "SUCCESS" 0 "n/a" "n/a" "n/a" "n/a"
1073
1371
 
1372
+ report_version REPORT_NODE_VERSION node --version
1373
+ report_version REPORT_NPM_VERSION npm --version
1374
+ report_version REPORT_BUN_VERSION bun --version
1375
+ report_version REPORT_GO_VERSION go version
1376
+
1074
1377
  cat >> "${REPORT_FILE}" <<EOF
1075
1378
 
1076
1379
  ## Installed (selected modules)
@@ -1079,11 +1382,11 @@ ${SELECTED_DISPLAY}
1079
1382
 
1080
1383
  ## Versions
1081
1384
 
1082
- - Node.js: \`$(command -v node >/dev/null 2>&1 && node --version || echo n/a)\`
1083
- - npm: \`$(command -v npm >/dev/null 2>&1 && npm --version || echo n/a)\`
1084
- - Bun: \`$(command -v bun >/dev/null 2>&1 && bun --version || echo n/a)\`
1385
+ - Node.js: \`${REPORT_NODE_VERSION}\`
1386
+ - npm: \`${REPORT_NPM_VERSION}\`
1387
+ - Bun: \`${REPORT_BUN_VERSION}\`
1085
1388
  - Pi: \`${PI_VERSION:-n/a}\`
1086
- - Go: \`$(command -v go >/dev/null 2>&1 && go version || echo n/a)\`
1389
+ - Go: \`${REPORT_GO_VERSION}\`
1087
1390
  EOF
1088
1391
 
1089
1392
  log_event "INFO" "bootstrap" "completed" "AI Dev Suite setup completed successfully" 0 \