acstack 0.47.1 → 0.48.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "acstack",
3
3
  "description": "Agentic coding stack where planning grows only as far as needed: a map of decisions, tasks, checks, and handoffs behind a pluggable tracker.",
4
- "version": "0.47.1",
4
+ "version": "0.48.1",
5
5
  "author": {
6
6
  "name": "ac8318740"
7
7
  },
@@ -12,3 +12,31 @@ Skills for Real Engineers
12
12
  Copyright (c) 2026 Matt Pocock
13
13
  https://github.com/mattpocock/skills
14
14
  License: MIT (https://opensource.org/licenses/MIT)
15
+
16
+ The spechub CLI inlines the following libraries into every alignment pack it
17
+ generates (cli/src/lib/alignment-viewer.generated.ts). They are unmodified.
18
+
19
+ React and React DOM
20
+ Copyright (c) Meta Platforms, Inc. and affiliates.
21
+ https://github.com/facebook/react
22
+ License: MIT (https://opensource.org/licenses/MIT)
23
+
24
+ React Flow (@xyflow/react)
25
+ Copyright (c) 2019-2025 webkid GmbH
26
+ https://github.com/xyflow/xyflow
27
+ License: MIT (https://opensource.org/licenses/MIT)
28
+
29
+ Eclipse Layout Kernel (elkjs)
30
+ Copyright (c) 2017 Kiel University and others
31
+ https://github.com/kieler/elkjs (source code)
32
+ License: EPL-2.0 (https://www.eclipse.org/legal/epl-2.0/)
33
+
34
+ Lucide icons (lucide-react)
35
+ Copyright (c) Lucide Contributors
36
+ https://github.com/lucide-icons/lucide
37
+ License: ISC (https://opensource.org/licenses/ISC)
38
+
39
+ react-markdown
40
+ Copyright (c) Espen Hovlandsdal
41
+ https://github.com/remarkjs/react-markdown
42
+ License: MIT (https://opensource.org/licenses/MIT)
@@ -236,6 +236,19 @@ pi:
236
236
  # retro. Needs the agentic-retro plugin installed first, and apply says so
237
237
  # when it is not.
238
238
  agentic_retro: true
239
+ # Zentui supplies the accent-rail editor and Starship footer. The footer
240
+ # format keeps model/provider/thinking visible on the right, including in
241
+ # compact mode.
242
+ zentui: true
243
+ # Hide pi-lens's LSP status footer and diagnostics widget rows.
244
+ hide_lens_lsp_status: true
245
+ # Extra pi-lens tools installed onto PATH for ecosystems used across repos.
246
+ # These are machine-wide, not project dependencies.
247
+ lens_extra_tools:
248
+ python: true # black, mypy, pyright
249
+ docker: true # hadolint, Dockerfile language server
250
+ github_actions: true # actionlint, zizmor
251
+ terraform: true # terraform-ls, tflint, terragrunt
239
252
  # Written into ~/.pi/agent/settings.json. Empty leaves pi's own value alone.
240
253
  # Every other key in that file stays as you set it, and disable puts back
241
254
  # any value of yours that apply replaced.
@@ -247,8 +260,9 @@ pi:
247
260
  mermaid: streaming
248
261
  # pi's anonymous install and update reporting. Kept off by this setup.
249
262
  install_telemetry: false
250
- # A pi theme name. "system" takes its colours from the terminal.
251
- theme: system
263
+ # A pi theme name. alex-mid-grey is written by this setup and shades
264
+ # user messages.
265
+ theme: alex-mid-grey
252
266
 
253
267
  neovim:
254
268
  # The one component that is off by default, because it writes into a neovim
@@ -38,7 +38,13 @@ PI_SUBAGENT_CFG="$PI_DIR/extensions/subagent/config.json"
38
38
  PI_RETRO_EXT="${PI_HOME:-$HOME/.pi}/agent/extensions/agentic-retro.ts"
39
39
  PI_ACSTACK="npm:acstack"
40
40
  PI_BILLION="npm:billion-context"
41
+ BILLION_CONFIG="$HOME/.config/billion-context/billion-context.json"
41
42
  PI_SLASH_ANYWHERE="npm:pi-slash-anywhere"
43
+ PI_ZENTUI="npm:pi-zentui"
44
+ PI_ZENTUI_CFG="$PI_DIR/zentui.json"
45
+ PI_THEME_DIR="$PI_DIR/themes"
46
+ PI_THEME="$PI_THEME_DIR/alex-mid-grey.json"
47
+ PI_LENS_CFG="$HOME/.pi-lens/config.json"
42
48
  # A line starting with # is a heading in markdown, and the model reads it. An
43
49
  # HTML comment carries the same marker and renders as nothing.
44
50
  PI_MD_BEGIN="<!-- >>> acstack terminal-workspace >>> -->"
@@ -180,6 +186,57 @@ PY
180
186
  say "$name installed to $BIN/$name"
181
187
  }
182
188
 
189
+ install_plain_github_binary() { # install_plain_github_binary <name> <repo> <asset-match>
190
+ local name="$1" repo="$2" match="$3"
191
+ if [ "${INSTALL_FORCE:-0}" != "1" ] && have "$name"; then
192
+ say "$name already installed"; return 0
193
+ fi
194
+ local json url tmp
195
+ json=$(curl -sSf "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null)
196
+ [ -z "$json" ] && { say "$name: could not reach the GitHub release API"; return 1; }
197
+ url=$(MAPFLOW_JSON="$json" py "$match" <<'PY'
198
+ import json, os, sys
199
+ try:
200
+ d = json.loads(os.environ["MAPFLOW_JSON"])
201
+ except ValueError:
202
+ raise SystemExit
203
+ for a in d.get("assets", []):
204
+ if sys.argv[1] in a["name"]:
205
+ print(a["browser_download_url"]); break
206
+ PY
207
+ )
208
+ [ -z "$url" ] && { say "$name: no matching release asset, install manually"; return 1; }
209
+ tmp=$(mktemp)
210
+ if ! curl -fsSL "$url" -o "$tmp"; then
211
+ rm -f "$tmp"; say "$name: download failed"; return 1
212
+ fi
213
+ mkdir -p "$BIN"; cp "$tmp" "$BIN/$name"; chmod +x "$BIN/$name"
214
+ rm -f "$tmp"
215
+ say "$name installed to $BIN/$name"
216
+ }
217
+
218
+ install_npm_tool() { # install_npm_tool <command> <package>
219
+ local command="$1" package="$2"
220
+ if have "$command"; then say "$command already installed"; return 0; fi
221
+ have npm || { say "$command: npm not installed, skipping"; return 0; }
222
+ if npm install -g "$package" >/dev/null 2>&1; then
223
+ say "$command installed with npm"
224
+ else
225
+ say "$command install failed, run: npm install -g $package"
226
+ fi
227
+ }
228
+
229
+ install_uv_tool() { # install_uv_tool <command> <package>
230
+ local command="$1" package="$2"
231
+ if have "$command"; then say "$command already installed"; return 0; fi
232
+ have uv || { say "$command: uv not installed, skipping"; return 0; }
233
+ if uv tool install "$package" >/dev/null 2>&1; then
234
+ say "$command installed with uv"
235
+ else
236
+ say "$command install failed, run: uv tool install $package"
237
+ fi
238
+ }
239
+
183
240
  write_helpers() {
184
241
  mkdir -p "$BIN"
185
242
  # Helpers this script used to write and no longer does. Upgrading otherwise
@@ -3184,6 +3241,30 @@ apply_harlequin() {
3184
3241
  || say "harlequin install failed, run: uv tool install harlequin"
3185
3242
  }
3186
3243
 
3244
+ apply_lens_extra_tools() {
3245
+ # A small machine-wide pi-lens toolbelt for common project ecosystems.
3246
+ # Pi-lens can fall back to npx or prompt for some tools, but putting these
3247
+ # on PATH makes them available in any repo without per-project setup.
3248
+ if [ "$(cfg_get pi.lens_extra_tools.python false)" = "true" ]; then
3249
+ install_uv_tool black black
3250
+ install_uv_tool mypy mypy
3251
+ install_npm_tool pyright pyright
3252
+ fi
3253
+ if [ "$(cfg_get pi.lens_extra_tools.docker false)" = "true" ]; then
3254
+ install_plain_github_binary hadolint hadolint/hadolint Linux-x86_64
3255
+ install_npm_tool docker-langserver dockerfile-language-server-nodejs
3256
+ fi
3257
+ if [ "$(cfg_get pi.lens_extra_tools.github_actions false)" = "true" ]; then
3258
+ install_binary actionlint rhysd/actionlint linux_amd64
3259
+ install_uv_tool zizmor zizmor
3260
+ fi
3261
+ if [ "$(cfg_get pi.lens_extra_tools.terraform false)" = "true" ]; then
3262
+ install_binary terraform-ls hashicorp/terraform-ls linux_amd64
3263
+ install_binary tflint terraform-linters/tflint linux_amd64
3264
+ install_plain_github_binary terragrunt gruntwork-io/terragrunt linux_amd64
3265
+ fi
3266
+ }
3267
+
3187
3268
  apply_ghdash() {
3188
3269
  have gh || { say "gh not installed, skipping dashboard"; return 0; }
3189
3270
  gh extension list 2>/dev/null | grep -q gh-dash || gh extension install dlvhdr/gh-dash >/dev/null 2>&1
@@ -3495,7 +3576,7 @@ s = ((c.get("pi") or {}).get("settings") or {})
3495
3576
  KEYS = {"hide_thinking_block": ("hideThinkingBlock", True, (True, False)),
3496
3577
  "mermaid": ("markdown.mermaid", "streaming", ("off", "final", "streaming")),
3497
3578
  "install_telemetry": ("enableInstallTelemetry", False, (True, False)),
3498
- "theme": ("theme", "system", None)}
3579
+ "theme": ("theme", "alex-mid-grey", None)}
3499
3580
  out = {}
3500
3581
  for k, (key, default, allowed) in KEYS.items():
3501
3582
  v = s.get(k, default)
@@ -3514,6 +3595,153 @@ print(json.dumps(out))
3514
3595
  PY
3515
3596
  }
3516
3597
 
3598
+ pi_zentui_wanted() {
3599
+ cat <<'JSON'
3600
+ {
3601
+ "components.editor.enabled": true,
3602
+ "components.editor.style": "accent-rail",
3603
+ "components.editor.styles.accent-rail.transparent": false,
3604
+ "components.userMessages.enabled": false,
3605
+ "components.footer.style": "starship",
3606
+ "components.footer.modelLabel": "id",
3607
+ "components.footer.styles.starship.segments.modelInfo": true,
3608
+ "components.footer.styles.starship.extensionStatuses.defaultPlacement": "off",
3609
+ "components.extensionStatuses.defaultVisibility": "hide",
3610
+ "components.extensionStatuses.visibility.pi-lens": "hide",
3611
+ "components.extensionStatuses.visibility.pi-lens-lsp": "hide",
3612
+ "components.extensionStatuses.visibility.ponytail": "hide",
3613
+ "components.footer.styles.starship.responsive": true,
3614
+ "components.footer.styles.starship.format": "$cwd( in $session_name)( on $git_branch)( $git_status)( via $runtime)$fill$model($sep$provider)($sep$thinkingLevel)($sep$context)( $auto_compaction)($sep$tokens)( $cache_read)( $cache_write)($sep$cost)( $subscription)",
3615
+ "components.footer.styles.starship.compactFormat": "$cwd$wrap(in $session_name)$wrap(on $git_branch) $git_status$fill$model$wrap_sep($provider)$wrap_sep($thinkingLevel)$wrap_sep$context$wrap_sep$tokens"
3616
+ }
3617
+ JSON
3618
+ }
3619
+
3620
+ pi_zentui_release() {
3621
+ cat <<'JSON'
3622
+ {
3623
+ "components.editor.enabled": null,
3624
+ "components.editor.style": null,
3625
+ "components.editor.styles.accent-rail.transparent": null,
3626
+ "components.userMessages.enabled": null,
3627
+ "components.footer.style": null,
3628
+ "components.footer.modelLabel": null,
3629
+ "components.footer.styles.starship.segments.modelInfo": null,
3630
+ "components.footer.styles.starship.extensionStatuses.defaultPlacement": null,
3631
+ "components.extensionStatuses.defaultVisibility": null,
3632
+ "components.extensionStatuses.visibility.pi-lens": null,
3633
+ "components.extensionStatuses.visibility.pi-lens-lsp": null,
3634
+ "components.extensionStatuses.visibility.ponytail": null,
3635
+ "components.footer.styles.starship.responsive": null,
3636
+ "components.footer.styles.starship.format": null,
3637
+ "components.footer.styles.starship.compactFormat": null
3638
+ }
3639
+ JSON
3640
+ }
3641
+
3642
+ pi_write_theme() {
3643
+ if [ -e "$PI_THEME" ] && ! pi_state owns file "$PI_THEME"; then
3644
+ say "pi: $PI_THEME already exists, so the Mapflow theme was left alone"
3645
+ return 0
3646
+ fi
3647
+ MAPFLOW_PI_THEME="$PI_THEME" py <<'PY'
3648
+ import json, os
3649
+
3650
+ target = os.environ["MAPFLOW_PI_THEME"]
3651
+ theme = {
3652
+ "$schema": "https://pi.dev/schemas/theme.schema.json",
3653
+ "name": "alex-mid-grey",
3654
+ "vars": {
3655
+ "surfaceMuted": "#21262D",
3656
+ "border": "#30363D",
3657
+ "borderSubtle": "#21262D",
3658
+ "text": "#C9D1D9",
3659
+ "textBright": "#E6EDF3",
3660
+ "muted": "#8B949E",
3661
+ "dim": "#6E7681",
3662
+ "toolStatus": "#59616B",
3663
+ "blue": "#58A6FF",
3664
+ "blueSoft": "#79C0FF",
3665
+ "inlineCode": "#D2A8FF",
3666
+ "green": "#3FB950",
3667
+ "red": "#F85149",
3668
+ "yellow": "#E0A052",
3669
+ "orange": "#FFA657",
3670
+ "purple": "#D2A8FF",
3671
+ "toolAccent": "#95A7FF",
3672
+ "toolName": "#8B85D6",
3673
+ "toolText": "#98A1AC",
3674
+ "pink": "#FF7B72",
3675
+ "userPromptBg": "#24272e",
3676
+ },
3677
+ "colors": {
3678
+ "accent": "toolAccent",
3679
+ "border": "border",
3680
+ "borderAccent": "toolStatus",
3681
+ "borderMuted": "borderSubtle",
3682
+ "success": "green",
3683
+ "error": "red",
3684
+ "warning": "yellow",
3685
+ "muted": "muted",
3686
+ "dim": "dim",
3687
+ "text": "text",
3688
+ "thinkingText": "toolText",
3689
+ "selectedBg": "surfaceMuted",
3690
+ "searchMatchBg": "#3B2E10",
3691
+ "searchMatchText": "textBright",
3692
+ "userMessageBg": "userPromptBg",
3693
+ "userMessageText": "textBright",
3694
+ "customMessageBg": "",
3695
+ "customMessageText": "text",
3696
+ "customMessageLabel": "muted",
3697
+ "toolPendingBg": "",
3698
+ "toolSuccessBg": "",
3699
+ "toolErrorBg": "#211414",
3700
+ "toolTitle": "toolName",
3701
+ "toolOutput": "toolText",
3702
+ "scrollbarTrack": "borderSubtle",
3703
+ "scrollbarThumb": "border",
3704
+ "mdHeading": "textBright",
3705
+ "mdLink": "blue",
3706
+ "mdLinkUrl": "dim",
3707
+ "mdCode": "inlineCode",
3708
+ "mdCodeBlock": "text",
3709
+ "mdCodeBlockBorder": "#0D1117",
3710
+ "mdQuote": "toolText",
3711
+ "mdQuoteBorder": "border",
3712
+ "mdHr": "borderSubtle",
3713
+ "mdListBullet": "muted",
3714
+ "toolDiffAdded": "#45B85A",
3715
+ "toolDiffRemoved": "#E45D57",
3716
+ "toolDiffContext": "dim",
3717
+ "syntaxComment": "dim",
3718
+ "syntaxKeyword": "pink",
3719
+ "syntaxFunction": "purple",
3720
+ "syntaxVariable": "text",
3721
+ "syntaxString": "blueSoft",
3722
+ "syntaxNumber": "orange",
3723
+ "syntaxType": "orange",
3724
+ "syntaxOperator": "muted",
3725
+ "syntaxPunctuation": "muted",
3726
+ "thinkingOff": "borderSubtle",
3727
+ "thinkingMinimal": "border",
3728
+ "thinkingLow": "dim",
3729
+ "thinkingMedium": "muted",
3730
+ "thinkingHigh": "blue",
3731
+ "thinkingXhigh": "blueSoft",
3732
+ "thinkingMax": "#9FCFF8",
3733
+ "bashMode": "green",
3734
+ },
3735
+ "export": {"pageBg": "#0D1117", "cardBg": "#161B22", "infoBg": "#1C2128"},
3736
+ }
3737
+ os.makedirs(os.path.dirname(target), exist_ok=True)
3738
+ json.dump(theme, open(target, "w"), indent=2, ensure_ascii=False)
3739
+ open(target, "a").write("\n")
3740
+ PY
3741
+ pi_state own file "$PI_THEME"
3742
+ say "pi: Mapflow theme written to $PI_THEME"
3743
+ }
3744
+
3517
3745
  # npm installs pi into its global bin, which a fresh shell may not have on
3518
3746
  # PATH. Asking npm costs a process, so only when pi is not already found.
3519
3747
  find_pi() {
@@ -3762,10 +3990,16 @@ apply_pi() {
3762
3990
  bili_client remove codex
3763
3991
  bili_client remove claude
3764
3992
  if [ "$(cfg_get pi.billion_context true)" = "true" ]; then
3765
- pi_add "$PI_BILLION" "$PI_BILLION"
3766
- bili_client install pi
3993
+ pi_state merge "$BILLION_CONFIG" billion '{"pi.subagents": false}'
3994
+ if [ "$(json_field "$BILLION_CONFIG" pi.subagents)" = "False" ]; then
3995
+ pi_add "$PI_BILLION" "$PI_BILLION"
3996
+ bili_client install pi
3997
+ else
3998
+ say "pi: billion-context acp_delegate could not be disabled, so billion-context setup was skipped"
3999
+ fi
3767
4000
  else
3768
4001
  bili_client remove pi
4002
+ pi_state merge "$BILLION_CONFIG" billion '{"pi.subagents": null}'
3769
4003
  pi_drop "$PI_BILLION"
3770
4004
  fi
3771
4005
  if [ "$(cfg_get pi.question_tool true)" = "true" ]; then
@@ -3803,6 +4037,20 @@ apply_pi() {
3803
4037
  else
3804
4038
  pi_drop npm:pi-compact-tools
3805
4039
  fi
4040
+ [ "$(cfg_get pi.settings.theme alex-mid-grey)" = "alex-mid-grey" ] && pi_write_theme
4041
+ if [ "$(cfg_get pi.zentui true)" = "true" ]; then
4042
+ pi_add "$PI_ZENTUI" "$PI_ZENTUI"
4043
+ pi_state merge "$PI_ZENTUI_CFG" zentui "$(pi_zentui_wanted)"
4044
+ else
4045
+ pi_state merge "$PI_ZENTUI_CFG" zentui "$(pi_zentui_release)"
4046
+ pi_drop "$PI_ZENTUI"
4047
+ fi
4048
+ if [ "$(cfg_get pi.hide_lens_lsp_status true)" = "true" ]; then
4049
+ pi_state merge "$PI_LENS_CFG" pi_lens '{"ui.hideLspStatus": true, "widget.visible": false}'
4050
+ else
4051
+ pi_state merge "$PI_LENS_CFG" pi_lens '{"ui.hideLspStatus": null, "widget.visible": null}'
4052
+ fi
4053
+ apply_lens_extra_tools
3806
4054
 
3807
4055
  # agentic-retro writes its own extension file. This setup only asks it to,
3808
4056
  # and owns the file only when it was not there before.
@@ -3850,6 +4098,7 @@ remove_pi() {
3850
4098
  pi_drop npm:pi-plugin-manager
3851
4099
  pi_drop npm:@juicesharp/rpiv-todo
3852
4100
  pi_drop npm:pi-compact-tools
4101
+ pi_drop "$PI_ZENTUI"
3853
4102
  pi_drop npm:@juicesharp/rpiv-ask-user-question
3854
4103
  pi_drop npm:pi-web-access
3855
4104
  pi_drop npm:pi-lens
@@ -3858,6 +4107,7 @@ remove_pi() {
3858
4107
  else
3859
4108
  say "pi: $PI_SETTINGS does not parse, so its packages were left installed"
3860
4109
  fi
4110
+ pi_state merge "$BILLION_CONFIG" billion '{"pi.subagents": null}'
3861
4111
  if pi_state owns file "$PI_RETRO_EXT"; then
3862
4112
  rm -f "$PI_RETRO_EXT"
3863
4113
  pi_state disown file "$PI_RETRO_EXT"
@@ -3865,6 +4115,13 @@ remove_pi() {
3865
4115
  fi
3866
4116
  pi_state merge "$PI_SETTINGS" settings \
3867
4117
  '{"hideThinkingBlock": null, "markdown.mermaid": null, "enableInstallTelemetry": null, "theme": null}'
4118
+ pi_state merge "$PI_ZENTUI_CFG" zentui "$(pi_zentui_release)"
4119
+ pi_state merge "$PI_LENS_CFG" pi_lens '{"ui.hideLspStatus": null, "widget.visible": null}'
4120
+ if pi_state owns file "$PI_THEME"; then
4121
+ rm -f "$PI_THEME"
4122
+ pi_state disown file "$PI_THEME"
4123
+ say "pi: Mapflow theme removed"
4124
+ fi
3868
4125
  pi_state merge "$PI_KEYBINDINGS" keybindings '{"app.message.dequeue": null}'
3869
4126
  pi_state merge "$PI_SUBAGENT_CFG" subagent '{"toolActivation": null}'
3870
4127
  pi_state md strip
@@ -3899,6 +4156,12 @@ pi-web-access pi npm pi-web-access -
3899
4156
  pi-lens pi npm pi-lens -
3900
4157
  pi-slash-anywhere pi npm pi-slash-anywhere -
3901
4158
  pi-compact-tools pi npm pi-compact-tools -
4159
+ pi-zentui pi npm pi-zentui -
4160
+ actionlint pi github rhysd/actionlint linux_amd64
4161
+ hadolint pi github hadolint/hadolint Linux-x86_64
4162
+ terraform-ls pi github hashicorp/terraform-ls linux_amd64
4163
+ tflint pi github terraform-linters/tflint linux_amd64
4164
+ terragrunt pi github gruntwork-io/terragrunt linux_amd64
3902
4165
  T
3903
4166
  }
3904
4167
 
@@ -3929,8 +4192,18 @@ installed_version() { # installed_version <name> -> a bare version, or nothing
3929
4192
  harlequin) v=$(uv tool list 2>/dev/null | awk '$1=="harlequin"{print $2}') ;;
3930
4193
  # 1.1.0
3931
4194
  pi) find_pi; v=$(pi --version 2>/dev/null | awk '{print $NF}') ;;
4195
+ # actionlint version 1.7.7
4196
+ actionlint) v=$(actionlint -version 2>/dev/null | awk '{print $2}') ;;
4197
+ # Haskell Dockerfile Linter 2.12.0
4198
+ hadolint) v=$(hadolint --version 2>/dev/null | awk '{print $NF}') ;;
4199
+ # terraform-ls version 0.36.5
4200
+ terraform-ls) v=$(terraform-ls version 2>/dev/null | awk '{print $2}') ;;
4201
+ # TFLint version 0.59.1
4202
+ tflint) v=$(tflint --version 2>/dev/null | awk 'NR==1{print $3}') ;;
4203
+ # terragrunt version v0.91.2
4204
+ terragrunt) v=$(terragrunt --version 2>/dev/null | awk '{print $NF}') ;;
3932
4205
  # pi installs npm packages under its own directory, not npm's global one.
3933
- pi-subagents|pi-compact-tools|pi-web-access|pi-lens|pi-slash-anywhere|@juicesharp/rpiv-ask-user-question|billion-context|@juicesharp/rpiv-todo|pi-plugin-manager)
4206
+ pi-subagents|pi-compact-tools|pi-zentui|pi-web-access|pi-lens|pi-slash-anywhere|@juicesharp/rpiv-ask-user-question|billion-context|@juicesharp/rpiv-todo|pi-plugin-manager)
3934
4207
  v=$(json_field "$PI_DIR/npm/node_modules/$1/package.json" version) ;;
3935
4208
  esac
3936
4209
  printf '%s' "${v#v}"
@@ -4080,7 +4353,7 @@ upgrade_one() { # upgrade_one <name>
4080
4353
  else
4081
4354
  say "pi update failed"
4082
4355
  fi ;;
4083
- pi-subagents|pi-compact-tools|pi-web-access|pi-lens|pi-slash-anywhere|@juicesharp/rpiv-ask-user-question|billion-context|@juicesharp/rpiv-todo|pi-plugin-manager)
4356
+ pi-subagents|pi-compact-tools|pi-zentui|pi-web-access|pi-lens|pi-slash-anywhere|@juicesharp/rpiv-ask-user-question|billion-context|@juicesharp/rpiv-todo|pi-plugin-manager)
4084
4357
  if pi_npm_pin "$1"; then
4085
4358
  say "$1 is pinned to $(pi_state pkg "npm:$1") in pi, which pi update never moves"
4086
4359
  return 0
@@ -4097,6 +4370,10 @@ upgrade_one() { # upgrade_one <name>
4097
4370
  # here rather than behind a table lookup that will never find it.
4098
4371
  mermaid-ascii)
4099
4372
  INSTALL_FORCE=1 install_binary mermaid-ascii AlexanderGrooff/mermaid-ascii Linux_x86_64 ;;
4373
+ hadolint)
4374
+ INSTALL_FORCE=1 install_plain_github_binary hadolint hadolint/hadolint Linux-x86_64 ;;
4375
+ terragrunt)
4376
+ INSTALL_FORCE=1 install_plain_github_binary terragrunt gruntwork-io/terragrunt linux_amd64 ;;
4100
4377
  # The fork build comes from a local checkout, so a release archive would
4101
4378
  # replace it with the stock binary and silently drop the patches it carries.
4102
4379
  # outdated already skips it; naming it here has to skip it too.
@@ -4165,7 +4442,7 @@ case "$ACTION" in
4165
4442
  if have pi || [ "$(cfg_get pi.enabled false)" = "true" ]; then
4166
4443
  echo
4167
4444
  have pi && say "pi: $(installed_version pi)" || say "pi: not installed"
4168
- for id in npm:pi-subagents "$PI_ACSTACK" "$PI_BILLION" npm:@juicesharp/rpiv-todo npm:pi-plugin-manager npm:@juicesharp/rpiv-ask-user-question npm:pi-web-access npm:pi-lens "$PI_SLASH_ANYWHERE" npm:pi-compact-tools; do
4445
+ for id in npm:pi-subagents "$PI_ACSTACK" "$PI_BILLION" npm:@juicesharp/rpiv-todo npm:pi-plugin-manager npm:@juicesharp/rpiv-ask-user-question npm:pi-web-access npm:pi-lens "$PI_SLASH_ANYWHERE" npm:pi-compact-tools "$PI_ZENTUI"; do
4169
4446
  src=$(pi_state pkg "$id") && say "pi package: $src" || say "pi package: $id not installed"
4170
4447
  done
4171
4448
  [ -e "$PI_RETRO_EXT" ] && say "pi agentic-retro extension: present" \