claude-dev-env 8.14.0 → 8.15.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 (123) hide show
  1. package/.agents/agents/test_agent_frontmatter.py +0 -72
  2. package/.agents/agents-archived/code-quality-agent.md +16 -16
  3. package/.agents/skills/_shared/end-of-run-gotcha-recommendations.md +5 -21
  4. package/.agents/skills/e-code-review/SKILL.md +1 -3
  5. package/.agents/skills/e-simplify/SKILL.md +6 -6
  6. package/.agents/skills/eli5/SKILL.md +4 -14
  7. package/.agents/skills/eli5/reference/task-seeds.md +3 -4
  8. package/.agents/skills/everything-search/SKILL.md +3 -3
  9. package/.agents/skills/fresh-branch/SKILL.md +2 -3
  10. package/.agents/skills/grok-spawn/SKILL.md +3 -8
  11. package/.agents/skills/issue-tracker/SKILL.md +4 -6
  12. package/.agents/skills/orchestrator-refresh/SKILL.md +7 -7
  13. package/.agents/skills/plugin-eval-standalone-skill/SKILL.md +3 -3
  14. package/.agents/skills/privacy-hygiene/SKILL.md +6 -10
  15. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  16. package/.agents/skills/pull-request/SKILL.md +11 -14
  17. package/.agents/skills/pull-request/reference/publication-tasks.md +2 -4
  18. package/.agents/skills/recovering-codex-startup/SKILL.md +3 -6
  19. package/.agents/skills/recovering-codex-startup/reference/task-seeds.md +1 -1
  20. package/.agents/skills/repairing-hook-boundaries/SKILL.md +4 -7
  21. package/.agents/skills/repairing-hook-boundaries/reference/task-seeds.md +1 -2
  22. package/.agents/skills/second-account-workers/.claude/CLAUDE.md +1 -0
  23. package/.agents/skills/second-account-workers/AGENTS.md +1 -0
  24. package/.agents/skills/second-account-workers/SKILL.md +65 -0
  25. package/.agents/skills/syncing-submodules/SKILL.md +7 -17
  26. package/.agents/skills/syncing-submodules/reference/run-tasks.md +1 -1
  27. package/.agents/skills/task-build/SKILL.md +1 -1
  28. package/.agents/skills/task-build/reference/tool-routing.md +1 -1
  29. package/.agents/skills/team-advisor/SKILL.md +2 -4
  30. package/.agents/skills/usage-pause/SKILL.md +1 -1
  31. package/.agents/skills/windows-scheduled-task/SKILL.md +1 -1
  32. package/AGENTS.md +1 -1
  33. package/_shared/advisor/advisor-protocol.md +4 -4
  34. package/_shared/advisor/reference/consult-format.md +1 -1
  35. package/_shared/advisor/reference/identity.md +2 -2
  36. package/_shared/advisor/reference/warm-up.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  38. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -16
  39. package/bin/ever-shipped-skills.mjs +1 -0
  40. package/bin/install.mjs +1 -0
  41. package/bin/install.prompt-context.test.mjs +46 -0
  42. package/bin/install.test.mjs +3 -2
  43. package/commands/sr-loop.md +2 -2
  44. package/docs/CODE_RULES.md +2 -2
  45. package/docs/DJANGO_PATTERNS.md +5 -5
  46. package/docs/REACT_PATTERNS.md +6 -6
  47. package/docs/TEST_QUALITY.md +10 -28
  48. package/docs/agent-spawn-protocol.md +4 -4
  49. package/docs/codex-compatibility.md +4 -33
  50. package/docs/references/team-advisor-skill.md +0 -1
  51. package/docs/second-claude-account.md +4 -0
  52. package/docs/worker-completion-gate.md +0 -1
  53. package/hooks/advisory/migration_safety_advisor.py +3 -2
  54. package/hooks/advisory/refactor_guard.py +3 -3
  55. package/hooks/hooks.json +0 -5
  56. package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +1 -2
  57. package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +1 -2
  58. package/package.json +1 -1
  59. package/rules/explore-thoroughly.md +0 -1
  60. package/rules/filesystem-search.md +1 -1
  61. package/rules/git-workflow.md +5 -5
  62. package/rules/long-horizon-autonomy.md +1 -5
  63. package/rules/re-stage-before-commit.md +0 -2
  64. package/rules/research-mode.md +1 -1
  65. package/rules/shell-invocation.md +1 -1
  66. package/scripts/claude_account_worker.py +347 -0
  67. package/scripts/claude_account_worker_process.py +169 -0
  68. package/scripts/claude_account_worker_report.py +170 -0
  69. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +139 -0
  70. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +8 -2
  71. package/scripts/grok_headless_runner.py +2 -5
  72. package/scripts/policy_lint/config/constants.py +1 -0
  73. package/scripts/policy_lint/engine.py +5 -2
  74. package/scripts/repository_checks/config/constants.py +1 -1
  75. package/scripts/spawn_grok_batch.py +12 -1
  76. package/scripts/subprocess_stream_text.py +28 -0
  77. package/scripts/task_usage_events.py +387 -0
  78. package/scripts/task_usage_report.py +391 -0
  79. package/scripts/test_claude_account_worker.py +331 -0
  80. package/scripts/test_spawn_grok_batch.py +22 -0
  81. package/scripts/test_task_usage_events.py +47 -0
  82. package/scripts/test_task_usage_report.py +603 -0
  83. package/scripts/tests/test_banned_prose_words.py +1 -1
  84. package/scripts/tests/test_policy_lint_rules_archive.py +16 -0
  85. package/scripts/tests/test_policy_lint_rules_engine.py +2 -2
  86. package/scripts/tests/test_rules.py +9 -9
  87. package/system-prompts/software-engineer.xml +15 -30
  88. package/audit-rubrics/audit-categories.json +0 -700
  89. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +0 -90
  90. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +0 -36
  91. package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +0 -35
  92. package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +0 -35
  93. package/audit-rubrics/category_rubrics/category-e-dead-code.md +0 -40
  94. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +0 -41
  95. package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +0 -38
  96. package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +0 -40
  97. package/audit-rubrics/category_rubrics/category-i-concurrency.md +0 -38
  98. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +0 -59
  99. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +0 -66
  100. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +0 -48
  101. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +0 -44
  102. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +0 -54
  103. package/audit-rubrics/category_rubrics/category-p-name-vs-behavior-contract.md +0 -59
  104. package/audit-rubrics/category_rubrics/category-q-cross-surface-claims.md +0 -46
  105. package/audit-rubrics/prompts/category-a-api-contracts.md +0 -407
  106. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +0 -401
  107. package/audit-rubrics/prompts/category-c-resource-cleanup.md +0 -420
  108. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +0 -414
  109. package/audit-rubrics/prompts/category-e-dead-code.md +0 -434
  110. package/audit-rubrics/prompts/category-f-silent-failures.md +0 -432
  111. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +0 -383
  112. package/audit-rubrics/prompts/category-h-security-boundaries.md +0 -423
  113. package/audit-rubrics/prompts/category-i-concurrency.md +0 -429
  114. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +0 -458
  115. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +0 -338
  116. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +0 -131
  117. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +0 -129
  118. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +0 -139
  119. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +0 -89
  120. package/audit-rubrics/prompts/category-q-cross-surface-claims.md +0 -51
  121. package/hooks/hooks_constants/style_reminder_prompt_constants.py +0 -9
  122. package/hooks/session/style_reminder_prompt.py +0 -41
  123. package/hooks/session/test_style_reminder_prompt.py +0 -32
@@ -0,0 +1,139 @@
1
+ """Named values for the second-account Claude worker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dev_env_scripts_constants.claude_account_constants import (
6
+ CHOICE_MAIN,
7
+ CHOICE_SECOND,
8
+ CHOICE_WAIT,
9
+ CREDENTIALS_FILE_NAME,
10
+ JSON_ACCOUNT_KEY,
11
+ JSON_CONFIG_DIRECTORY_KEY,
12
+ JSON_REASON_KEY,
13
+ MAIN_CLAUDE_HOME_DIRECTORY_NAME,
14
+ )
15
+ from dev_env_scripts_constants.shared_tree_constants import CLAUDE_CONFIG_DIR_ENV_VAR
16
+
17
+ CLAUDE_BINARY_NAME: str = "claude"
18
+
19
+ DEFAULT_PERMISSION_MODE: str = "auto"
20
+
21
+ DEFAULT_TIMEOUT_MINUTES: int = 60
22
+
23
+ MINIMUM_TIMEOUT_MINUTES: int = 1
24
+
25
+ WAIT_EXIT_CODE: int = 3
26
+
27
+ TIMEOUT_EXIT_CODE: int = 124
28
+
29
+ MISSING_BINARY_EXIT_CODE: int = 127
30
+
31
+ LAUNCH_FAILURE_EXIT_CODE: int = 1
32
+
33
+ WAIT_DURATION_SECONDS: float = 0.0
34
+
35
+ STDOUT_TAIL_CHARACTER_LIMIT: int = 4000
36
+
37
+ DRAIN_GRACE_TIMEOUT_SECONDS: int = 10
38
+
39
+ DRAIN_ATTEMPT_LIMIT: int = 2
40
+
41
+ DURATION_DECIMAL_PLACES: int = 3
42
+
43
+ REPORT_INDENT: int = 2
44
+
45
+ UTF8_ENCODING: str = "utf-8"
46
+
47
+ UTF8_DECODE_ERRORS: str = "replace"
48
+
49
+ CLI_DESCRIPTION: str = "Run one Claude worker using the account picker."
50
+
51
+ PROMPT_FILE_FLAG: str = "--prompt-file"
52
+
53
+ CWD_FLAG: str = "--cwd"
54
+
55
+ REPORT_FILE_FLAG: str = "--report-file"
56
+
57
+ MODEL_FLAG: str = "--model"
58
+
59
+ PERMISSION_MODE_FLAG: str = "--permission-mode"
60
+
61
+ TIMEOUT_MINUTES_FLAG: str = "--timeout-minutes"
62
+
63
+ OUTPUT_FORMAT_FLAG: str = "--output-format"
64
+
65
+ OUTPUT_FORMAT_JSON: str = "json"
66
+
67
+ SINGLE_PROMPT_FLAG: str = "-p"
68
+
69
+ SECONDS_PER_MINUTE: int = 60
70
+
71
+ JSON_RESULT_KEY: str = "result"
72
+
73
+ JSON_IS_ERROR_KEY: str = "is_error"
74
+
75
+ REPORT_ACCOUNT_KEY: str = JSON_ACCOUNT_KEY
76
+
77
+ REPORT_REASON_KEY: str = JSON_REASON_KEY
78
+
79
+ REPORT_EXIT_CODE_KEY: str = "exit_code"
80
+
81
+ REPORT_DURATION_SECONDS_KEY: str = "duration_seconds"
82
+
83
+ REPORT_RESULT_KEY: str = JSON_RESULT_KEY
84
+
85
+ REPORT_IS_ERROR_KEY: str = JSON_IS_ERROR_KEY
86
+
87
+ SUMMARY_LINE_TEMPLATE: str = "account={account} exit_code={exit_code} report={report_file}"
88
+
89
+ INVALID_TIMEOUT_MESSAGE: str = "--timeout-minutes must be at least 1"
90
+
91
+
92
+ __all__ = [
93
+ "CHOICE_MAIN",
94
+ "CHOICE_SECOND",
95
+ "CHOICE_WAIT",
96
+ "CLAUDE_BINARY_NAME",
97
+ "CLAUDE_CONFIG_DIR_ENV_VAR",
98
+ "CLI_DESCRIPTION",
99
+ "CREDENTIALS_FILE_NAME",
100
+ "CWD_FLAG",
101
+ "DEFAULT_PERMISSION_MODE",
102
+ "DEFAULT_TIMEOUT_MINUTES",
103
+ "DRAIN_ATTEMPT_LIMIT",
104
+ "DRAIN_GRACE_TIMEOUT_SECONDS",
105
+ "DURATION_DECIMAL_PLACES",
106
+ "INVALID_TIMEOUT_MESSAGE",
107
+ "JSON_ACCOUNT_KEY",
108
+ "JSON_CONFIG_DIRECTORY_KEY",
109
+ "JSON_IS_ERROR_KEY",
110
+ "JSON_REASON_KEY",
111
+ "JSON_RESULT_KEY",
112
+ "LAUNCH_FAILURE_EXIT_CODE",
113
+ "MAIN_CLAUDE_HOME_DIRECTORY_NAME",
114
+ "MISSING_BINARY_EXIT_CODE",
115
+ "MINIMUM_TIMEOUT_MINUTES",
116
+ "MODEL_FLAG",
117
+ "OUTPUT_FORMAT_FLAG",
118
+ "OUTPUT_FORMAT_JSON",
119
+ "PERMISSION_MODE_FLAG",
120
+ "PROMPT_FILE_FLAG",
121
+ "REPORT_ACCOUNT_KEY",
122
+ "REPORT_DURATION_SECONDS_KEY",
123
+ "REPORT_EXIT_CODE_KEY",
124
+ "REPORT_FILE_FLAG",
125
+ "REPORT_INDENT",
126
+ "REPORT_IS_ERROR_KEY",
127
+ "REPORT_REASON_KEY",
128
+ "REPORT_RESULT_KEY",
129
+ "SECONDS_PER_MINUTE",
130
+ "SINGLE_PROMPT_FLAG",
131
+ "STDOUT_TAIL_CHARACTER_LIMIT",
132
+ "SUMMARY_LINE_TEMPLATE",
133
+ "TIMEOUT_EXIT_CODE",
134
+ "TIMEOUT_MINUTES_FLAG",
135
+ "UTF8_DECODE_ERRORS",
136
+ "UTF8_ENCODING",
137
+ "WAIT_DURATION_SECONDS",
138
+ "WAIT_EXIT_CODE",
139
+ ]
@@ -616,9 +616,15 @@ ADVISOR_CLI_RESUME_FLAG: str = "--resume"
616
616
  ADVISOR_BIND_PROMPT_TEMPLATE: str = (
617
617
  "You are the unique worker advisor for role {role_name}. "
618
618
  "Answer only. Open with exactly one of: ENDORSE | CORRECTION | PLAN | STOP. "
619
- "Pre-dispatch: ENDORSE this worker assignment if safe, else CORRECTION."
619
+ "Pre-dispatch consult: the full worker assignment follows. "
620
+ "Hunt for missing requirements, wrong assumptions, unsafe actions, and unclear "
621
+ "completion criteria. ENDORSE when the worker can start as written; "
622
+ "otherwise open with CORRECTION, PLAN, or STOP and name the fix.\n"
623
+ "(Advisor: please keep your guidance under 80 words, I need a focused "
624
+ "starting point.)\n\n"
625
+ "Worker assignment:\n{assignment_text}"
620
626
  )
621
- """Prompt body used for the pre-dispatch advisor bind."""
627
+ """Prompt body for the pre-dispatch advisor consult; carries the assembled worker prompt."""
622
628
 
623
629
  ADVISOR_VERDICT_PROMPT_TEMPLATE: str = (
624
630
  "You are the unique worker advisor for role {role_name}, session {session_id}. "
@@ -73,6 +73,7 @@ from dev_env_scripts_constants.grok_worker_constants import (
73
73
  )
74
74
  from shared_tree_paths import resolve_shared_process_tree_scripts_directory
75
75
  from grok_worker_preflight import poteto_mode_skill_path
76
+ from subprocess_stream_text import decode_stream_text
76
77
 
77
78
  _shared_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
78
79
  __file__,
@@ -200,11 +201,7 @@ def _classify_completion(
200
201
 
201
202
 
202
203
  def _normalize_stream(stream_payload: str | bytes | None) -> str:
203
- if stream_payload is None:
204
- return ""
205
- if isinstance(stream_payload, bytes):
206
- return stream_payload.decode(UTF8_ENCODING, errors=UTF8_DECODE_ERRORS)
207
- return stream_payload
204
+ return decode_stream_text(stream_payload, encoding=UTF8_ENCODING, errors=UTF8_DECODE_ERRORS)
208
205
 
209
206
 
210
207
  def _resolve_returncode(process: subprocess.Popen[str]) -> int:
@@ -7,6 +7,7 @@ UTF8_ENCODING = "utf-8"
7
7
  PATH_SEPARATOR = "/"
8
8
  ARCHIVED_SKILLS_DIRECTORY_NAME = "skill-archive"
9
9
  ARCHIVED_HOOKS_DIRECTORY_NAME = "hooks-archived"
10
+ ARCHIVED_AUDIT_RUBRICS_DIRECTORY_NAME = "audit-rubrics-archived"
10
11
  NUL_BYTE = b"\0"
11
12
  ALL_GIT_ROOT_ARGUMENTS = ("rev-parse", "--show-toplevel")
12
13
  ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "--verify", "HEAD")
@@ -4,6 +4,7 @@ from collections.abc import Iterable, Sequence
4
4
  from pathlib import Path, PurePosixPath
5
5
 
6
6
  from .config.constants import (
7
+ ARCHIVED_AUDIT_RUBRICS_DIRECTORY_NAME,
7
8
  ARCHIVED_HOOKS_DIRECTORY_NAME,
8
9
  ARCHIVED_SKILLS_DIRECTORY_NAME,
9
10
  )
@@ -30,7 +31,8 @@ def lint(
30
31
  """Run the selected rules on one source selection.
31
32
 
32
33
  Repository and diff selections check active code. File and editor
33
- selections include archived skills and archived hooks.
34
+ selections include archived skills, archived hooks, and archived
35
+ audit rubrics.
34
36
 
35
37
  Args:
36
38
  request: Repository root, source, and rule sets.
@@ -61,6 +63,7 @@ def _is_runtime_path(path: PurePosixPath) -> bool:
61
63
  return (
62
64
  path.parts[0] != ARCHIVED_SKILLS_DIRECTORY_NAME
63
65
  and ARCHIVED_HOOKS_DIRECTORY_NAME not in path.parts
66
+ and ARCHIVED_AUDIT_RUBRICS_DIRECTORY_NAME not in path.parts
64
67
  )
65
68
 
66
69
 
@@ -252,7 +255,7 @@ def _scoped_diagnostics(
252
255
  all_diagnostics: Iterable[Diagnostic],
253
256
  document: Document,
254
257
  ) -> tuple[Diagnostic, ...]:
255
- if document.changed_lines is None or not document.changed_lines:
258
+ if document.changed_lines is None:
256
259
  return tuple(all_diagnostics)
257
260
  return tuple(
258
261
  each_diagnostic
@@ -118,7 +118,7 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
118
118
  "f9e409c5c16ecfdd5883da4f3ad045b4af5677740c9e703ad2d47ba231718350",
119
119
  ),
120
120
  (
121
- "packages/claude-dev-env/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md",
121
+ "packages/claude-dev-env/audit-rubrics-archived/prompts/category-n-test-name-scenario-verifier.md",
122
122
  "home-path",
123
123
  "6b98ff7f0398f06452f19c4a93f3d69186401dab40aaa7077b394ef59b3e730e",
124
124
  ),
@@ -290,6 +290,7 @@ def bind_unique_worker_advisor(
290
290
  *,
291
291
  advisor_spec: AdvisorSpec,
292
292
  role_name: str,
293
+ assignment_text: str,
293
294
  all_used_session_ids: set[str],
294
295
  ) -> tuple[str, str]:
295
296
  """Bind one unique advisor session for a worker; refuse duplicates and sentinels.
@@ -303,6 +304,7 @@ def bind_unique_worker_advisor(
303
304
  Args:
304
305
  advisor_spec: Lead-supplied launcher/model/effort.
305
306
  role_name: Worker role used in the bind prompt.
307
+ assignment_text: Assembled worker prompt the advisor reviews before dispatch.
306
308
  all_used_session_ids: Sessions already issued in this batch.
307
309
 
308
310
  Returns:
@@ -317,7 +319,9 @@ def bind_unique_worker_advisor(
317
319
  )
318
320
  if advisor_spec.launcher == PENDING_BIND_SENTINEL:
319
321
  raise ValueError(f"advisor.launcher is {PENDING_BIND_SENTINEL}")
320
- prompt_text = ADVISOR_BIND_PROMPT_TEMPLATE.format(role_name=role_name)
322
+ prompt_text = ADVISOR_BIND_PROMPT_TEMPLATE.format(
323
+ role_name=role_name, assignment_text=assignment_text
324
+ )
321
325
  session_id, advisor_body_text, returncode = batch_invoke_advisor(
322
326
  launcher=advisor_spec.launcher,
323
327
  model=advisor_spec.model,
@@ -898,6 +902,7 @@ def _bind_worker_advisor_session(
898
902
  *,
899
903
  advisor_spec: AdvisorSpec,
900
904
  role_name: str,
905
+ assignment_text: str,
901
906
  all_used_session_ids: set[str],
902
907
  session_id_lock: object | None,
903
908
  ) -> tuple[str, str]:
@@ -906,11 +911,13 @@ def _bind_worker_advisor_session(
906
911
  return bind_unique_worker_advisor(
907
912
  advisor_spec=advisor_spec,
908
913
  role_name=role_name,
914
+ assignment_text=assignment_text,
909
915
  all_used_session_ids=all_used_session_ids,
910
916
  )
911
917
  return bind_unique_worker_advisor(
912
918
  advisor_spec=advisor_spec,
913
919
  role_name=role_name,
920
+ assignment_text=assignment_text,
914
921
  all_used_session_ids=all_used_session_ids,
915
922
  )
916
923
 
@@ -990,6 +997,10 @@ def _maybe_bind_advisor_for_worker(
990
997
  advisor_session_id, pre_signal = _bind_worker_advisor_session(
991
998
  advisor_spec=advisor_spec,
992
999
  role_name=worker_spec.role_name,
1000
+ assignment_text=assemble_worker_prompt(
1001
+ all_prompt_part_paths=worker_spec.all_prompt_part_paths,
1002
+ tool_profile=worker_spec.tool_profile,
1003
+ ),
993
1004
  all_used_session_ids=used_ids,
994
1005
  session_id_lock=session_id_lock,
995
1006
  )
@@ -0,0 +1,28 @@
1
+ """Decode a subprocess stream text payload captured in text or bytes mode."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def decode_stream_text(
7
+ stream_payload: str | bytes | None,
8
+ *,
9
+ encoding: str,
10
+ errors: str,
11
+ ) -> str:
12
+ """Decode a captured subprocess stream text payload, whichever mode captured it.
13
+
14
+ Args:
15
+ stream_payload: The captured stream content, or None when nothing
16
+ was captured.
17
+ encoding: The text encoding to use when the payload is bytes.
18
+ errors: The decode error handler to use when the payload is bytes.
19
+
20
+ Returns:
21
+ The decoded text, an empty string when stream_payload is None, or
22
+ stream_payload unchanged when it is already text.
23
+ """
24
+ if stream_payload is None:
25
+ return ""
26
+ if isinstance(stream_payload, bytes):
27
+ return stream_payload.decode(encoding, errors=errors)
28
+ return stream_payload