@pareto4/pp-core 1.22.0 → 1.23.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.
Files changed (119) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/install.js +205 -28
  3. package/commands/pp-help.md +2 -1
  4. package/gemini-extension.json +1 -1
  5. package/hooks/dist/pp-check-update-worker.js +1 -1
  6. package/hooks/dist/pp-context-monitor.js +3 -3
  7. package/hooks/dist/pp-statusline.js +88 -14
  8. package/hooks/dist/pp-update-banner.js +44 -4
  9. package/hooks/dist/pp-workflow-guard.js +3 -3
  10. package/hooks/pp-check-update-worker.js +1 -1
  11. package/hooks/pp-context-monitor.js +3 -3
  12. package/hooks/pp-statusline.js +88 -14
  13. package/hooks/pp-update-banner.js +44 -4
  14. package/hooks/pp-workflow-guard.js +3 -3
  15. package/package.json +1 -1
  16. package/pp-core/bin/lib/audience-command.cjs +271 -0
  17. package/pp-core/bin/lib/audience.cjs +293 -0
  18. package/pp-core/bin/lib/commands.cjs +26 -17
  19. package/pp-core/bin/lib/discovery-charter.cjs +13 -1
  20. package/pp-core/bin/lib/discovery-input-check.cjs +11 -11
  21. package/pp-core/bin/lib/discovery-session-check.cjs +7 -7
  22. package/pp-core/bin/lib/discovery-status.cjs +419 -0
  23. package/pp-core/bin/lib/explain-command.cjs +48 -0
  24. package/pp-core/bin/lib/gate-card.cjs +97 -0
  25. package/pp-core/bin/lib/hook-messages.cjs +132 -0
  26. package/pp-core/bin/lib/init-contract.cjs +116 -76
  27. package/pp-core/bin/lib/init-interactive.cjs +110 -49
  28. package/pp-core/bin/lib/init.cjs +44 -3
  29. package/pp-core/bin/lib/io.cjs +33 -0
  30. package/pp-core/bin/lib/message-labels.cjs +75 -0
  31. package/pp-core/bin/lib/messages/core.cjs +20 -0
  32. package/pp-core/bin/lib/messages/discovery.cjs +267 -0
  33. package/pp-core/bin/lib/messages/gate.cjs +257 -0
  34. package/pp-core/bin/lib/messages/hook.cjs +114 -0
  35. package/pp-core/bin/lib/messages/init.cjs +513 -0
  36. package/pp-core/bin/lib/messages/progress.cjs +110 -0
  37. package/pp-core/bin/lib/messages.cjs +208 -0
  38. package/pp-core/bin/lib/vault-command-router.cjs +16 -3
  39. package/pp-core/bin/lib/violation-render.cjs +78 -0
  40. package/pp-core/bin/pp-tools.cjs +68 -23
  41. package/pp-core/bin/shared/config-schema.manifest.json +1 -0
  42. package/pp-core/references/audience-moments.md +119 -0
  43. package/pp-core/references/audience.md +114 -0
  44. package/pp-core/references/discovery-charter.md +2 -2
  45. package/pp-core/templates/project-charter.md +3 -4
  46. package/pp-core/workflows/add-phase.md +2 -0
  47. package/pp-core/workflows/add-tests.md +2 -0
  48. package/pp-core/workflows/add-todo.md +2 -0
  49. package/pp-core/workflows/ai-integration-phase.md +2 -0
  50. package/pp-core/workflows/audit-milestone.md +2 -0
  51. package/pp-core/workflows/autonomous.md +2 -0
  52. package/pp-core/workflows/check-todos.md +2 -0
  53. package/pp-core/workflows/code-review-fix.md +2 -0
  54. package/pp-core/workflows/code-review.md +2 -0
  55. package/pp-core/workflows/complete-milestone.md +10 -1
  56. package/pp-core/workflows/discovery-sprint/charter.md +3 -2
  57. package/pp-core/workflows/discovery-sprint/prefill.md +3 -0
  58. package/pp-core/workflows/discovery-sprint/prototype.md +14 -0
  59. package/pp-core/workflows/discovery-sprint/sessions/01-kick-off.md +4 -1
  60. package/pp-core/workflows/discovery-sprint/sessions/13-wrap-up.md +1 -1
  61. package/pp-core/workflows/discovery-sprint/vault-context.md +15 -0
  62. package/pp-core/workflows/discovery-sprint.md +14 -0
  63. package/pp-core/workflows/discovery-to-build.md +8 -2
  64. package/pp-core/workflows/discuss-phase-assumptions.md +2 -0
  65. package/pp-core/workflows/discuss-phase.md +1 -1
  66. package/pp-core/workflows/edit-phase.md +2 -0
  67. package/pp-core/workflows/eval-review.md +2 -0
  68. package/pp-core/workflows/execute-phase/steps/check-query-gate.md +25 -1
  69. package/pp-core/workflows/execute-phase/steps/vault-fetch.md +1 -0
  70. package/pp-core/workflows/execute-phase/steps/vault-load-gate.md +2 -0
  71. package/pp-core/workflows/execute-phase.md +1 -1
  72. package/pp-core/workflows/execute-plan.md +2 -0
  73. package/pp-core/workflows/extract-learnings.md +2 -0
  74. package/pp-core/workflows/help/modes/business.md +56 -0
  75. package/pp-core/workflows/help.md +12 -2
  76. package/pp-core/workflows/import.md +10 -1
  77. package/pp-core/workflows/ingest-docs.md +2 -0
  78. package/pp-core/workflows/init.md +17 -1
  79. package/pp-core/workflows/insert-phase.md +2 -0
  80. package/pp-core/workflows/list-workspaces.md +2 -0
  81. package/pp-core/workflows/manager.md +2 -0
  82. package/pp-core/workflows/map-codebase.md +2 -0
  83. package/pp-core/workflows/milestone-summary.md +2 -0
  84. package/pp-core/workflows/new-milestone.md +10 -1
  85. package/pp-core/workflows/new-project.md +12 -2
  86. package/pp-core/workflows/new-workspace.md +2 -0
  87. package/pp-core/workflows/next.md +8 -0
  88. package/pp-core/workflows/plan-milestone-gaps.md +10 -1
  89. package/pp-core/workflows/plan-phase.md +2 -1
  90. package/pp-core/workflows/plan-review-convergence.md +1 -1
  91. package/pp-core/workflows/progress.md +10 -0
  92. package/pp-core/workflows/quick.md +11 -1
  93. package/pp-core/workflows/remove-phase.md +2 -0
  94. package/pp-core/workflows/remove-workspace.md +2 -0
  95. package/pp-core/workflows/resume-project.md +25 -0
  96. package/pp-core/workflows/review.md +2 -0
  97. package/pp-core/workflows/scan.md +2 -0
  98. package/pp-core/workflows/secure-phase.md +2 -0
  99. package/pp-core/workflows/settings.md +41 -0
  100. package/pp-core/workflows/ship.md +2 -0
  101. package/pp-core/workflows/spec-phase.md +1 -1
  102. package/pp-core/workflows/ui-phase.md +2 -0
  103. package/pp-core/workflows/ui-review.md +2 -0
  104. package/pp-core/workflows/ultraplan-phase.md +2 -0
  105. package/pp-core/workflows/validate-phase.md +2 -0
  106. package/pp-core/workflows/vault-review.md +2 -0
  107. package/pp-core/workflows/verify-phase.md +2 -0
  108. package/pp-core/workflows/verify-work.md +2 -0
  109. package/scripts/affected-tests-lib.cjs +26 -12
  110. package/scripts/ci-release-only.cjs +158 -0
  111. package/scripts/live-runs/driver.cjs +326 -0
  112. package/scripts/live-runs/run.cjs +906 -0
  113. package/scripts/live-runs/scenarios.cjs +326 -0
  114. package/scripts/live-runs/scratch-project.cjs +132 -0
  115. package/scripts/live-runs/scrub.cjs +291 -0
  116. package/scripts/live-runs/turn-guard.cjs +107 -0
  117. package/scripts/live-runs/walk-setup.cjs +198 -0
  118. package/scripts/run-tests.cjs +151 -135
  119. package/scripts/test-env.cjs +64 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pp-core",
3
3
  "displayName": "PP Core",
4
- "version": "1.22.0",
4
+ "version": "1.23.1",
5
5
  "description": "PP Core is a meta-prompting, context engineering, and spec-driven development system for AI coding agents.",
6
6
  "author": {
7
7
  "name": "pareto4",
package/bin/install.js CHANGED
@@ -349,6 +349,9 @@ const {
349
349
  const {
350
350
  updateCacheFileName,
351
351
  } = require(path.join(__dirname, '..', 'pp-core', 'bin', 'lib', 'package-identity.cjs'));
352
+ // Phase 39 (AUD-02): the per-person audience leaf is the only reader/writer of the
353
+ // person file; the installer never builds that path itself (it honours GSD_HOME).
354
+ const audienceLib = require(path.join(__dirname, '..', 'pp-core', 'bin', 'lib', 'audience.cjs'));
352
355
 
353
356
  // Parse args
354
357
  const args = process.argv.slice(2);
@@ -375,6 +378,9 @@ const _profileArgRaw = (() => {
375
378
  const _profileIsCore = _profileArgRaw === 'core';
376
379
  const _requestedProfileName = (hasMinimal || _profileIsCore) ? 'core' : (_profileArgRaw || null);
377
380
 
381
+ // Phase 39 AUD-02: pure parse here; the main block validates it before any install branch runs.
382
+ const audienceArg = parseAudienceArg(args);
383
+
378
384
  if (hasMinimal && _profileArgRaw) {
379
385
  console.error(` ${yellow}Cannot specify both --minimal/--core-only and --profile${reset}`);
380
386
  process.exit(1);
@@ -585,7 +591,7 @@ if (hasUninstall) {
585
591
 
586
592
  // Show help if requested
587
593
  if (hasHelp) {
588
- console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall pp-core (remove all pp-core files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`pp update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall pp-core from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, pp-core creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
594
+ console.log(` ${yellow}Usage:${reset} npx ${pkg.name} [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--kilo${reset} Install for Kilo only\n ${cyan}--codex${reset} Install for Codex only\n ${cyan}--kimi${reset} Install for Kimi CLI only\n ${cyan}--copilot${reset} Install for Copilot only\n ${cyan}--antigravity${reset} Install for Antigravity only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--windsurf${reset} Install for Windsurf only\n ${cyan}--augment${reset} Install for Augment only\n ${cyan}--trae${reset} Install for Trae only\n ${cyan}--qwen${reset} Install for Qwen Code only\n ${cyan}--hermes${reset} Install for Hermes Agent only\n ${cyan}--cline${reset} Install for Cline only\n ${cyan}--codebuddy${reset} Install for CodeBuddy only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall pp-core (remove all pp-core files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n ${cyan}--portable-hooks${reset} Emit \$HOME-relative hook paths in settings.json\n (for WSL/Docker bind-mount setups; also GSD_PORTABLE_HOOKS=1)\n ${cyan}--profile=<name>${reset} Install a named skill profile. Profiles:\n core — ${PROFILES.core.length} main-loop skills incl. phase (~130 desc tokens)\n standard — ${PROFILES.standard.length} skills incl. phase, review, config (~700)\n full — all skills (default)\n Composable: --profile=core,audit installs union of closures.\n Profile is persisted and respected by \`pp update\`.\n ${cyan}--minimal${reset} Alias for --profile=core (back-compat).\n Cuts cold-start overhead from ~12k tokens to ~700.\n Alias: --core-only.\n ${cyan}--audience <who>${reset} Who you are: business, engineer or developer (default developer).\n Saved once for you, not per project; skips the first-install question.\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx ${pkg.name}\n\n ${dim}# Install for Claude Code globally${reset}\n npx ${pkg.name} --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx ${pkg.name} --gemini --global\n\n ${dim}# Install for Kilo globally${reset}\n npx ${pkg.name} --kilo --global\n\n ${dim}# Install for Codex globally${reset}\n npx ${pkg.name} --codex --global\n\n ${dim}# Install for Kimi CLI globally${reset}\n npx ${pkg.name} --kimi --global\n\n ${dim}# Install for Kimi CLI under ~/.kimi-code${reset}\n npx ${pkg.name} --kimi --global --config-dir ~/.kimi-code\n\n ${dim}# Install for Copilot globally${reset}\n npx ${pkg.name} --copilot --global\n\n ${dim}# Install for Copilot locally${reset}\n npx ${pkg.name} --copilot --local\n\n ${dim}# Install for Antigravity globally${reset}\n npx ${pkg.name} --antigravity --global\n\n ${dim}# Install for Antigravity locally${reset}\n npx ${pkg.name} --antigravity --local\n\n ${dim}# Install for Cursor globally${reset}\n npx ${pkg.name} --cursor --global\n\n ${dim}# Install for Cursor locally${reset}\n npx ${pkg.name} --cursor --local\n\n ${dim}# Install for Windsurf globally${reset}\n npx ${pkg.name} --windsurf --global\n\n ${dim}# Install for Windsurf locally${reset}\n npx ${pkg.name} --windsurf --local\n\n ${dim}# Install for Augment globally${reset}\n npx ${pkg.name} --augment --global\n\n ${dim}# Install for Augment locally${reset}\n npx ${pkg.name} --augment --local\n\n ${dim}# Install for Trae globally${reset}\n npx ${pkg.name} --trae --global\n\n ${dim}# Install for Trae locally${reset}\n npx ${pkg.name} --trae --local\n\n ${dim}# Install for Hermes Agent globally${reset}\n npx ${pkg.name} --hermes --global\n\n ${dim}# Install for Hermes Agent locally${reset}\n npx ${pkg.name} --hermes --local\n\n ${dim}# Install for Cline globally${reset}\n npx ${pkg.name} --cline --global\n\n ${dim}# Install for Cline locally${reset}\n npx ${pkg.name} --cline --local\n\n ${dim}# Install for CodeBuddy globally${reset}\n npx ${pkg.name} --codebuddy --global\n\n ${dim}# Install for CodeBuddy locally${reset}\n npx ${pkg.name} --codebuddy --local\n\n ${dim}# Install for all runtimes globally${reset}\n npx ${pkg.name} --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx ${pkg.name} --kilo --global --config-dir ~/.kilo-work\n\n ${dim}# Install to current project only${reset}\n npx ${pkg.name} --claude --local\n\n ${dim}# Uninstall pp-core from Cursor globally${reset}\n npx ${pkg.name} --cursor --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / OPENCODE_CONFIG_DIR / GEMINI_CONFIG_DIR / KILO_CONFIG_DIR / CODEX_HOME / KIMI_CONFIG_DIR / COPILOT_CONFIG_DIR / COPILOT_HOME / ANTIGRAVITY_CONFIG_DIR / CURSOR_CONFIG_DIR / WINDSURF_CONFIG_DIR / AUGMENT_CONFIG_DIR / TRAE_CONFIG_DIR / QWEN_CONFIG_DIR / HERMES_HOME / CLINE_CONFIG_DIR / CODEBUDDY_CONFIG_DIR environment variables.\n Kimi CLI defaults to the first existing generic skills root: ${cyan}~/.config/agents/skills${reset}, then ${cyan}~/.agents/skills${reset}; if neither exists, pp-core creates ${cyan}~/.config/agents${reset}.\n Use ${cyan}--config-dir ~/.kimi-code${reset} or ${cyan}KIMI_CONFIG_DIR=~/.kimi-code${reset} for brand-specific Kimi installs.\n`);
589
595
  process.exit(0);
590
596
  }
591
597
 
@@ -9842,17 +9848,13 @@ function reportLocalPatches(configDir, runtime = 'claude') {
9842
9848
  try { meta = JSON.parse(fs.readFileSync(metaPath, 'utf8')); } catch { return []; }
9843
9849
 
9844
9850
  if (meta.files && meta.files.length > 0) {
9845
- const reapplyCommand = (runtime === 'opencode' || runtime === 'kilo' || runtime === 'copilot')
9846
- ? '/gsd-update --reapply'
9847
- : runtime === 'gemini'
9848
- ? '/gsd:update --reapply'
9849
- : runtime === 'codex'
9850
- ? '$gsd-update --reapply'
9851
- : runtime === 'cursor'
9852
- ? 'gsd-update --reapply (mention the skill name)'
9851
+ const reapplyCommand = runtime === 'codex'
9852
+ ? '$pp-update --reapply'
9853
+ : runtime === 'cursor'
9854
+ ? 'pp-update --reapply (mention the skill name)'
9853
9855
  : runtime === 'kimi'
9854
- ? '/skill:gsd-update --reapply'
9855
- : '/gsd-update --reapply';
9856
+ ? '/skill:pp-update --reapply'
9857
+ : '/pp-update --reapply';
9856
9858
  console.log('');
9857
9859
  console.log(' ' + yellow + 'Local patches detected' + reset + ' (from v' + meta.from_version + '):');
9858
9860
  for (const f of meta.files) {
@@ -11471,7 +11473,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11471
11473
  }
11472
11474
 
11473
11475
  persistActiveProfileMarker();
11474
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
11476
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, priorInstallExisted };
11475
11477
  }
11476
11478
 
11477
11479
  if (plan.installSurface === 'copilot-instructions') {
@@ -11501,7 +11503,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11501
11503
  writeCopilotHookConfig(targetDir);
11502
11504
  console.log(` ${green}✓${reset} Configured Copilot lifecycle hook (sessionStart)`);
11503
11505
  persistActiveProfileMarker();
11504
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
11506
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, priorInstallExisted };
11505
11507
  }
11506
11508
 
11507
11509
  if (plan.installSurface === 'cursor-hooks-json') {
@@ -11516,13 +11518,13 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11516
11518
  // Re-run the manifest pass so the hook scripts + hooks.json are hash-tracked.
11517
11519
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
11518
11520
  persistActiveProfileMarker();
11519
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
11521
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, priorInstallExisted };
11520
11522
  }
11521
11523
 
11522
11524
  if (plan.installSurface === 'profile-marker-only') {
11523
11525
  // Windsurf/Trae/Kimi use artifact-only surfaces — no config.toml or settings.json hooks needed.
11524
11526
  persistActiveProfileMarker();
11525
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
11527
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, priorInstallExisted };
11526
11528
  }
11527
11529
 
11528
11530
  if (plan.installSurface === 'cline-rules') {
@@ -11534,7 +11536,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11534
11536
  // writeManifest() call, so a second pass is needed to hash-track them.
11535
11537
  writeManifest(targetDir, runtime, { mode: _effectiveInstallMode });
11536
11538
  persistActiveProfileMarker();
11537
- return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir };
11539
+ return { settingsPath: null, settings: null, statuslineCommand: null, updateBannerCommand: null, runtime, configDir: targetDir, priorInstallExisted };
11538
11540
  }
11539
11541
 
11540
11542
  // Configure statusline and hooks in settings.json (or settings.local.json for local Claude installs).
@@ -11836,6 +11838,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
11836
11838
  updateBannerCommand,
11837
11839
  runtime,
11838
11840
  configDir: targetDir,
11841
+ priorInstallExisted,
11839
11842
  rollbackInstallerMigrations,
11840
11843
  };
11841
11844
  }
@@ -12177,6 +12180,163 @@ function promptRuntime(callback) {
12177
12180
  });
12178
12181
  }
12179
12182
 
12183
+ // ─── Audience (Phase 39 AUD-02, D-07, D-09) ─────────────────────────────────
12184
+
12185
+ /**
12186
+ * Parse `--audience <value>` / `--audience=<value>` from argv. Pure.
12187
+ * @param {string[]} argv
12188
+ * @returns {{ value: string|null, error: string|null }}
12189
+ */
12190
+ function parseAudienceArg(argv) {
12191
+ const list = Array.isArray(argv) ? argv : [];
12192
+ const allowed = 'business, engineer, developer';
12193
+ let raw = null;
12194
+ for (let i = 0; i < list.length; i++) {
12195
+ const arg = list[i];
12196
+ if (typeof arg !== 'string') continue;
12197
+ if (arg === '--audience') {
12198
+ const next = list[i + 1];
12199
+ if (typeof next !== 'string' || next.startsWith('--')) {
12200
+ return { value: null, error: `--audience needs a value: ${allowed}` };
12201
+ }
12202
+ raw = next;
12203
+ break;
12204
+ }
12205
+ if (arg.startsWith('--audience=')) {
12206
+ raw = arg.slice('--audience='.length);
12207
+ break;
12208
+ }
12209
+ }
12210
+ if (raw === null) return { value: null, error: null };
12211
+ const value = raw.trim().toLowerCase();
12212
+ if (!audienceLib.isAudience(value)) {
12213
+ return { value: null, error: `--audience "${raw.slice(0, 40)}" is not one of ${allowed}` };
12214
+ }
12215
+ return { value, error: null };
12216
+ }
12217
+
12218
+ /** The first-run question, identical to the one the workflows show (D-09). Pure. */
12219
+ function buildAudiencePromptText() {
12220
+ return audienceLib.FIRST_RUN_PROMPT;
12221
+ }
12222
+
12223
+ /**
12224
+ * Turn a typed answer into an audience. Enter keeps developer (D-07); an
12225
+ * unknown answer gives no audience (`recognized: false`), so a typo is never
12226
+ * saved as a permanent developer (WR-02). Pure.
12227
+ */
12228
+ function resolveInstallAudienceAnswer(answer) {
12229
+ const parsed = audienceLib.parseAudienceInput(answer == null ? '' : answer);
12230
+ if (parsed === null) return { audience: null, recognized: false };
12231
+ return { audience: parsed, recognized: true };
12232
+ }
12233
+
12234
+ const AUDIENCE_RETRY_NOTICE = ' Not recognised — choose 1, 2 or 3.';
12235
+ const AUDIENCE_GIVE_UP_NOTICE = ' Not recognised — nothing saved; /pp-init and /pp-discovery will ask again.';
12236
+ const AUDIENCE_NO_ANSWER_NOTICE = ' No answer — nothing saved; /pp-init and /pp-discovery will ask again.';
12237
+
12238
+ /**
12239
+ * True only on an interactive first install with nothing set (D-07): all four
12240
+ * conditions must hold. Pure.
12241
+ */
12242
+ function shouldAskAudience({ flagValue, isInteractive, hasValidPersonSetting, priorInstallExisted }) {
12243
+ return !flagValue && !!isInteractive && !hasValidPersonSetting && !priorInstallExisted;
12244
+ }
12245
+
12246
+ function saveInstallAudience(audience, env) {
12247
+ // CR-01 (D-10): no language key, so a stored language override is kept.
12248
+ const result = audienceLib.writePerson({ audience }, env);
12249
+ if (result && result.ok) {
12250
+ console.log(` ${green}✓${reset} Audience set to ${audience} ${dim}(saved in ${result.path})${reset}`);
12251
+ } else {
12252
+ const why = result && result.error ? `: ${result.error}` : '';
12253
+ console.log(` ${yellow}Could not save the audience${why}. Set it later: pp-tools audience set <business|engineer|developer>${reset}`);
12254
+ }
12255
+ }
12256
+
12257
+ /**
12258
+ * Provision the per-person audience during install. `--audience` always
12259
+ * writes and never asks; otherwise ask once, on a true first install only.
12260
+ * Never throws. The callback runs unless the user presses Ctrl-C at the
12261
+ * prompt, which exits 130 before finalize (WR-09); a re-run completes it.
12262
+ */
12263
+ function handleAudience({ flagValue, isInteractive, priorInstallExisted, env = process.env, ask,
12264
+ createInterface = () => readline.createInterface({ input: process.stdin, output: process.stdout }) }, callback) {
12265
+ if (flagValue) {
12266
+ saveInstallAudience(flagValue, env);
12267
+ callback();
12268
+ return;
12269
+ }
12270
+ let hasValidPersonSetting = false;
12271
+ try {
12272
+ hasValidPersonSetting = audienceLib.readPerson(env).audience !== null;
12273
+ } catch {
12274
+ hasValidPersonSetting = false;
12275
+ }
12276
+ if (!shouldAskAudience({ flagValue, isInteractive, hasValidPersonSetting, priorInstallExisted })) {
12277
+ callback();
12278
+ return;
12279
+ }
12280
+ // An unknown answer is asked once more; still unknown, nothing is saved and
12281
+ // the first-run question is left for /pp-init and /pp-discovery (WR-02).
12282
+ // One readline interface serves both attempts: a second interface on the
12283
+ // same stdin would lose lines the first one already buffered.
12284
+ //
12285
+ // WR-03: on Ctrl-D or a closed stdin the question callback never fires;
12286
+ // without the close handler the event loop drains, the process exits 0 and
12287
+ // finalize (settings.json: hooks, permissions) never runs. Treat it as
12288
+ // "save nothing, continue". `done` is set before rl.close(), which emits
12289
+ // 'close' synchronously, so the callback runs exactly once.
12290
+ let attempts = 0;
12291
+ let done = false;
12292
+ let rl = null;
12293
+ const askOnce = () => {
12294
+ attempts += 1;
12295
+ if (typeof ask === 'function') {
12296
+ ask(buildAudiencePromptText(), finish);
12297
+ return;
12298
+ }
12299
+ if (!rl) {
12300
+ rl = createInterface();
12301
+ // WR-09: on a TTY, readline turns Ctrl-C into rl.close() unless a
12302
+ // 'SIGINT' listener exists, which the close handler below would read as
12303
+ // "no answer, continue". Ctrl-C cancels, as at the other prompts.
12304
+ rl.on('SIGINT', () => {
12305
+ done = true;
12306
+ console.log(`\n ${yellow}Installation cancelled${reset}\n`);
12307
+ rl.close();
12308
+ process.exit(130);
12309
+ });
12310
+ rl.on('close', () => {
12311
+ if (done) return;
12312
+ done = true;
12313
+ console.log('\n' + AUDIENCE_NO_ANSWER_NOTICE);
12314
+ callback();
12315
+ });
12316
+ }
12317
+ rl.question(' > ', finish);
12318
+ };
12319
+ const finish = (answer) => {
12320
+ const { audience, recognized } = resolveInstallAudienceAnswer(answer);
12321
+ if (!recognized && attempts < 2) {
12322
+ console.log(AUDIENCE_RETRY_NOTICE);
12323
+ askOnce();
12324
+ return;
12325
+ }
12326
+ done = true;
12327
+ if (rl) rl.close();
12328
+ if (!recognized) {
12329
+ console.log(AUDIENCE_GIVE_UP_NOTICE);
12330
+ callback();
12331
+ return;
12332
+ }
12333
+ saveInstallAudience(audience, env);
12334
+ callback();
12335
+ };
12336
+ console.log(buildAudiencePromptText());
12337
+ askOnce();
12338
+ }
12339
+
12180
12340
  // ─── Update banner (#2795) ──────────────────────────────────────────────────
12181
12341
 
12182
12342
  /**
@@ -12575,6 +12735,8 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
12575
12735
  throw error;
12576
12736
  }
12577
12737
 
12738
+ const priorInstallExisted = results.some((r) => r && r.priorInstallExisted === true);
12739
+
12578
12740
  const statuslineRuntimes = ['claude', 'gemini'];
12579
12741
  const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
12580
12742
 
@@ -12640,18 +12802,24 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
12640
12802
  });
12641
12803
  };
12642
12804
 
12643
- if (primaryStatuslineResult) {
12644
- handleStatusline(primaryStatuslineResult.settings, isInteractive, continueAfterStatusline);
12645
- } else if (canInstallBanner) {
12646
- // No statusline-capable runtime, but at least one runtime can host the
12647
- // banner — still offer it.
12648
- handleUpdateBanner(isInteractive, (shouldInstallBanner) => {
12649
- finalize(false, shouldInstallBanner);
12650
- });
12651
- } else {
12652
- // Nothing to prompt about — no statusline, no banner-capable runtime.
12653
- finalize(false, false);
12654
- }
12805
+ const runRemainingPrompts = () => {
12806
+ if (primaryStatuslineResult) {
12807
+ handleStatusline(primaryStatuslineResult.settings, isInteractive, continueAfterStatusline);
12808
+ } else if (canInstallBanner) {
12809
+ // No statusline-capable runtime, but at least one runtime can host the
12810
+ // banner — still offer it.
12811
+ handleUpdateBanner(isInteractive, (shouldInstallBanner) => {
12812
+ finalize(false, shouldInstallBanner);
12813
+ });
12814
+ } else {
12815
+ // Nothing to prompt about — no statusline, no banner-capable runtime.
12816
+ finalize(false, false);
12817
+ }
12818
+ };
12819
+
12820
+ // Phase 39 AUD-02 (D-07): provision the per-person audience before the
12821
+ // statusline/banner prompts. Non-interactive paths call back immediately.
12822
+ handleAudience({ flagValue: audienceArg.value, isInteractive, priorInstallExisted }, runRemainingPrompts);
12655
12823
  }
12656
12824
 
12657
12825
  // Always export so runtime-artifact-layout.cjs's lazy loader can access
@@ -12793,6 +12961,11 @@ module.exports = {
12793
12961
  buildRuntimePromptText,
12794
12962
  buildUpdateBannerPromptText,
12795
12963
  parseUpdateBannerInput,
12964
+ parseAudienceArg,
12965
+ buildAudiencePromptText,
12966
+ resolveInstallAudienceAnswer,
12967
+ shouldAskAudience,
12968
+ handleAudience,
12796
12969
  buildUpdateBannerHookEntry,
12797
12970
  buildHookCommand,
12798
12971
  normalizeNodePath,
@@ -12824,6 +12997,10 @@ module.exports = {
12824
12997
 
12825
12998
  // Main logic — only run when not loaded as a module for testing
12826
12999
  if (require.main === module && !process.env.GSD_TEST_MODE) {
13000
+ if (audienceArg.error) {
13001
+ console.error(' ' + yellow + audienceArg.error + reset);
13002
+ process.exit(1);
13003
+ }
12827
13004
  if (hasDryRun) {
12828
13005
  // --dry-run: preview legacy cleanup and exit without installing.
12829
13006
  if (hasUninstall) {
@@ -4,11 +4,12 @@ description: Show available pp-core commands and usage guide
4
4
  argument-hint: "[--brief | --full | <topic> | --brief <topic>]"
5
5
  allowed-tools:
6
6
  - Read
7
+ - Bash
7
8
  ---
8
9
  <objective>
9
10
  Display pp-core help at the tier the user asked for: brief (one-line refresher), default (one-page tour), full (complete reference), a single topic section, or a compact scoped lookup of one topic (`--brief <topic>`: signature + one-line summary).
10
11
 
11
- Output ONLY the reference content of the chosen tier. Do NOT add:
12
+ Output ONLY the reference content of the chosen tier. For a business user or a business engineer, help.md prints one audience header line first; that line is part of the output. Do NOT add:
12
13
  - Project-specific analysis
13
14
  - Git status or file context
14
15
  - Next-step suggestions
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pp-core",
3
- "version": "1.22.0",
3
+ "version": "1.23.1",
4
4
  "description": "PP Core — a meta-prompting, context engineering, and spec-driven development system for AI coding agents. Loads pp-core's operating context into every Gemini CLI session.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -18,7 +18,7 @@ const { isSemverNewer } = require('../pp-core/bin/lib/semver-compare.cjs');
18
18
  // The previous `require('../package.json').name` (#378) resolved to undefined
19
19
  // in the installed tree (only a {"type":"commonjs"} marker ships), so the
20
20
  // background check never reported updates.
21
- const { checkLatestVersion } = require('../gsd-core/bin/check-latest-version.cjs');
21
+ const { checkLatestVersion } = require('../pp-core/bin/check-latest-version.cjs');
22
22
  const { PACKAGE_NAME } = require('../pp-core/bin/lib/package-identity.cjs');
23
23
  // Authoritative list of managed hooks — shared with tests to retire source-grep
24
24
  // assertions (pending-migration-to-typed-ir [#455]).
@@ -133,7 +133,7 @@ process.stdin.on('end', () => {
133
133
  const isGsdActive = fs.existsSync(path.join(cwd, '.planning', 'STATE.md'));
134
134
 
135
135
  // On CRITICAL with active GSD project, auto-record session state as a
136
- // breadcrumb for /gsd:resume-work (#1974). Fire-and-forget subprocess —
136
+ // breadcrumb for /pp-resume-work (#1974). Fire-and-forget subprocess —
137
137
  // doesn't block the hook or the agent. Fires ONCE per CRITICAL session,
138
138
  // guarded by warnData.criticalRecorded to prevent repeated overwrites
139
139
  // of the "crash moment" record on every debounce cycle.
@@ -165,8 +165,8 @@ process.stdin.on('end', () => {
165
165
  message = isGsdActive
166
166
  ? `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
167
167
  'Context is nearly exhausted. Do NOT start new complex work or write handoff files — ' +
168
- 'GSD state is already tracked in STATE.md. Inform the user so they can run ' +
169
- '/gsd:pause-work at the next natural stopping point.'
168
+ 'pp-core state is already tracked in STATE.md. Inform the user so they can run ' +
169
+ '/pp-pause-work at the next natural stopping point.'
170
170
  : `CONTEXT CRITICAL: Usage at ${usedPct}%. Remaining: ${remaining}%. ` +
171
171
  'Context is nearly exhausted. Inform the user that context is low and ask how they ' +
172
172
  'want to proceed. Do NOT autonomously save state or write handoff files unless the user asks.';
@@ -9,6 +9,59 @@ const os = require('os');
9
9
  const { isSemverNewer } = require('../pp-core/bin/lib/semver-compare.cjs');
10
10
  const { PACKAGE_NAME, updateCacheFileName } = require('../pp-core/bin/lib/package-identity.cjs');
11
11
 
12
+ // --- Audience wording (MSG-06, D-18, D-20) ----------------------------------
13
+ // The light catalog entry. When it cannot load (missing, broken, or its slice
14
+ // missing) every text below stays today's developer string.
15
+ let hookMessages = null;
16
+ try {
17
+ hookMessages = require('../pp-core/bin/lib/hook-messages.cjs');
18
+ } catch (e) {
19
+ hookMessages = null;
20
+ }
21
+
22
+ /** The audience for this process; developer when the catalog is unavailable or throws. */
23
+ function hookAudience() {
24
+ if (!hookMessages) return 'developer';
25
+ try {
26
+ return hookMessages.audienceOf(process.env);
27
+ } catch (e) {
28
+ return 'developer';
29
+ }
30
+ }
31
+
32
+ /** The catalog text for `code`, or `literal` (today's developer string) on any failure. */
33
+ function hookText(code, params, audience, literal) {
34
+ if (!hookMessages || audience === 'developer') return literal;
35
+ try {
36
+ const out = hookMessages.msg(code, params, audience, literal);
37
+ return typeof out === 'string' && out !== '' ? out : literal;
38
+ } catch (e) {
39
+ return literal;
40
+ }
41
+ }
42
+
43
+ /** The display word for a canonical value, or the value itself on any failure. */
44
+ function hookLabel(domain, value, audience) {
45
+ if (!hookMessages || audience === 'developer') return value;
46
+ try {
47
+ const out = hookMessages.label(domain, value, audience);
48
+ return typeof out === 'string' && out !== '' ? out : value;
49
+ } catch (e) {
50
+ return value;
51
+ }
52
+ }
53
+
54
+ /** The display text for a STATE.md status, or the status itself on any failure. */
55
+ function hookStatusWord(status, audience) {
56
+ if (!hookMessages || audience === 'developer') return status;
57
+ try {
58
+ const out = hookMessages.statusWord(status, audience);
59
+ return typeof out === 'string' && out !== '' ? out : status;
60
+ } catch (e) {
61
+ return status;
62
+ }
63
+ }
64
+
12
65
  // --- Config + last-command readers ------------------------------------------
13
66
 
14
67
  /**
@@ -241,7 +294,8 @@ function renderProgressBar(percent) {
241
294
  * Progress bar is opt-in: appended to the milestone segment only when
242
295
  * progress.percent is present in frontmatter; absent → empty string.
243
296
  */
244
- function formatGsdState(s) {
297
+ function formatGsdState(s, opts) {
298
+ const audience = (opts && opts.audience) || 'developer';
245
299
  const parts = [];
246
300
 
247
301
  // Milestone segment: version + name + (opt-in) progress bar
@@ -261,24 +315,41 @@ function formatGsdState(s) {
261
315
  // Scene 1: an orchestrator is mid-flight on this phase.
262
316
  // stage = whichever lifecycle status was written by the orchestrator
263
317
  // (discussing / planning / executing / verifying)
264
- const stage = s.status || '';
265
- parts.push(stage ? `Phase ${s.activePhase} ${stage}` : `Phase ${s.activePhase}`);
318
+ const stage = s.status ? hookStatusWord(s.status, audience) : '';
319
+ parts.push(stage
320
+ ? hookText('hook.statusline.phase_stage', { number: s.activePhase, stage }, audience, `Phase ${s.activePhase} ${s.status}`)
321
+ : hookText('hook.statusline.phase', { number: s.activePhase }, audience, `Phase ${s.activePhase}`));
266
322
  } else if (s.nextAction && phasesStr) {
267
323
  // Scene 2: idle + a recommended next command is visible to the user.
268
324
  // Surfaces "what to run next" without the user opening STATE.md.
269
- parts.push(`next ${s.nextAction} ${phasesStr}`);
325
+ parts.push(hookText(
326
+ 'hook.statusline.next',
327
+ { action: hookLabel('state.next_action', s.nextAction, audience), numbers: phasesStr },
328
+ audience,
329
+ `next ${s.nextAction} ${phasesStr}`
330
+ ));
270
331
  } else if (Number(s.percent) === 100 || (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases)) {
271
332
  // Scene 3: milestone complete (every phase done).
272
- parts.push('milestone complete');
333
+ parts.push(hookText('hook.statusline.milestone_complete', {}, audience, 'milestone complete'));
273
334
  } else {
274
335
  // Backward-compatible default — preserved EXACTLY for STATE.md files that
275
336
  // don't carry the new lifecycle fields. Identical output to v1.38.x and
276
337
  // earlier so no existing project's status-line changes shape.
277
- if (s.status) parts.push(s.status);
338
+ if (s.status) parts.push(hookStatusWord(s.status, audience));
278
339
  if (s.phaseNum && s.phaseTotal) {
279
340
  const phase = s.phaseName
280
- ? `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
281
- : `ph ${s.phaseNum}/${s.phaseTotal}`;
341
+ ? hookText(
342
+ 'hook.statusline.phase_named',
343
+ { name: s.phaseName, num: s.phaseNum, total: s.phaseTotal },
344
+ audience,
345
+ `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
346
+ )
347
+ : hookText(
348
+ 'hook.statusline.phase_count',
349
+ { num: s.phaseNum, total: s.phaseTotal },
350
+ audience,
351
+ `ph ${s.phaseNum}/${s.phaseTotal}`
352
+ );
282
353
  parts.push(phase);
283
354
  }
284
355
  }
@@ -393,7 +464,9 @@ function runStatusline() {
393
464
  }
394
465
 
395
466
  // GSD state (milestone · status · phase) — shown when no todo task
396
- const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {});
467
+ // The audience is resolved once per run, never inside the formatter (Pitfall 7).
468
+ const audience = hookAudience();
469
+ const gsdStateStr = task ? '' : formatGsdState(readGsdState(dir) || {}, { audience });
397
470
 
398
471
  // GSD update available?
399
472
  // Read only the per-package shared cache file (#607). The legacy
@@ -406,12 +479,12 @@ function runStatusline() {
406
479
  const cache = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
407
480
  const { showUpdate, staleWarning } = evaluateUpdateCache(cache);
408
481
  if (showUpdate) {
409
- gsdUpdate = '\x1b[33m⬆ /gsd:update\x1b[0m │ ';
482
+ gsdUpdate = '\x1b[33m' + hookText('hook.statusline.update', {}, audience, '⬆ /pp-update') + '\x1b[0m │ ';
410
483
  }
411
484
  if (staleWarning === 'dev') {
412
- gsdUpdate += '\x1b[33m⚠ dev install — re-run installer to sync hooks\x1b[0m │ ';
485
+ gsdUpdate += '\x1b[33m' + hookText('hook.statusline.dev_install', {}, audience, '⚠ dev install — re-run installer to sync hooks') + '\x1b[0m │ ';
413
486
  } else if (staleWarning === 'stale') {
414
- gsdUpdate += '\x1b[31m⚠ stale hooks — run /gsd:update\x1b[0m │ ';
487
+ gsdUpdate += '\x1b[31m' + hookText('hook.statusline.stale', {}, audience, '⚠ stale hooks — run /pp-update') + '\x1b[0m │ ';
415
488
  }
416
489
  } catch (e) {}
417
490
  }
@@ -556,7 +629,8 @@ module.exports = {
556
629
  * Render the statusline from an already-parsed hook input object. Exported for
557
630
  * testing without feeding stdin. Returns the rendered string.
558
631
  */
559
- function renderStatusline(data) {
632
+ function renderStatusline(data, opts) {
633
+ const audience = (opts && opts.audience) || hookAudience();
560
634
  const model = data.model?.display_name || 'Claude';
561
635
  const dir = data.workspace?.current_dir || process.cwd();
562
636
  const dirname = path.basename(dir);
@@ -575,7 +649,7 @@ function renderStatusline(data) {
575
649
  if (cfgPos != null) position = cfgPos;
576
650
  } catch (e) { /* swallow */ }
577
651
 
578
- const gsdStateStr = formatGsdState(readGsdState(dir) || {});
652
+ const gsdStateStr = formatGsdState(readGsdState(dir) || {}, { audience });
579
653
  const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
580
654
  return formatDevBadge(readDevBuild()) + composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, position });
581
655
  }