@danmoisan/drm-copilot-mcp 1.0.21 → 1.0.23

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 (43) hide show
  1. package/out/mcp-server.js +1730 -139
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/MEMORY.md +5 -1
  4. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_commit_push_memory_before_pr.md +48 -2
  5. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_no_sendmessage_tool.md +35 -0
  6. package/resources/claude-customizations/.claude/agent-memory/epic-orchestrator/feedback_worktree_isolation_branches_from_main.md +45 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +257 -0
  8. package/resources/claude-customizations/.claude/agents/parallel-planner.md +183 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +23 -11
  10. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +259 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +499 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate-helpers.ps1 +302 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +359 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +244 -0
  15. package/resources/claude-customizations/.claude/lib/bash/compute-cohorts.sh +143 -0
  16. package/resources/claude-customizations/.claude/lib/bash/compute-concurrency-batches.sh +122 -0
  17. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +330 -0
  18. package/resources/claude-customizations/.claude/lib/bash/parallel-common.sh +238 -0
  19. package/resources/claude-customizations/.claude/lib/bash/parallel-items-validate.sh +244 -0
  20. package/resources/claude-customizations/.claude/lib/bash/parallel-manifest-validate.sh +187 -0
  21. package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-emit.sh +340 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-yaml-scan.sh +335 -0
  23. package/resources/claude-customizations/.claude/lib/bash/validate-parallel-manifest.sh +134 -0
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +379 -0
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +491 -0
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +490 -0
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +429 -0
  28. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +366 -0
  29. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +184 -0
  30. package/resources/claude-customizations/.claude/rules/shell.md +7 -2
  31. package/resources/claude-customizations/.claude/settings.json +28 -0
  32. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +152 -0
  33. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +93 -0
  34. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +971 -0
  35. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +461 -0
  36. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +176 -0
  37. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +56 -0
  38. package/resources/claude-customizations/config/blast-radius.json +16 -0
  39. package/resources/claude-customizations/config/orchestration-routing.json +355 -0
  40. package/resources/claude-customizations/pack-manifests/core.json +32 -1
  41. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  42. package/resources/config/orchestration-routing.json +22 -0
  43. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +29 -0
@@ -0,0 +1,244 @@
1
+ #!/usr/bin/env bash
2
+ # parallel-items-validate.sh: sourceable bash port of the shared work-item
3
+ # validators in scripts/dev_tools/_parallel_state_common.py -- validate_items,
4
+ # validate_item_record, _validate_merge_status, and validate_blast_radius_block.
5
+ # It reads the node table populated by parallel-yaml-emit.sh and appends errors
6
+ # to the PC_ERRORS accumulator owned by parallel-common.sh.
7
+ #
8
+ # Message order is part of the contract, not an implementation detail. Within
9
+ # one item the order is issue_num, feature_folder, state, kind (when required),
10
+ # merge_status, then the blast-radius block in field order. Across items the
11
+ # order is positional, and every duplicate issue_num is reported after the
12
+ # whole per-entry pass, in ascending key order, so the sequence is reproducible
13
+ # regardless of the order the entries appeared in.
14
+ #
15
+ # The Python module remains the repository authority; this file must reproduce
16
+ # its output byte for byte for every fixture in tests/fixtures/parallel_manifest_bash.
17
+
18
+ # Resolve this file's own directory so its dependencies source regardless of
19
+ # the caller's working directory.
20
+ PI_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
21
+ # shellcheck source=.claude/lib/bash/parallel-common.sh
22
+ # shellcheck disable=SC1091
23
+ source "$PI_LIB_DIR/parallel-common.sh"
24
+ # shellcheck source=.claude/lib/bash/parallel-yaml-emit.sh
25
+ # shellcheck disable=SC1091
26
+ source "$PI_LIB_DIR/parallel-yaml-emit.sh"
27
+
28
+ pi_repr_at() {
29
+ # Echo the Python repr of the value stored at a node path.
30
+ #
31
+ # Args: $1 = node path. An unknown path renders as None, matching the
32
+ # Python code's mapping.get default.
33
+ local path="$1" type value
34
+ type=$(yp_type_of "$path")
35
+ value=$(yp_value_of "$path")
36
+ pc_repr "$type" "$value"
37
+ }
38
+
39
+ pi_is_string_list() {
40
+ # Return 0 when a node is a sequence whose every entry is a non-empty string.
41
+ #
42
+ # An empty sequence satisfies the predicate: every blast-radius collection
43
+ # other than paths is legitimately empty. Mirrors is_string_list.
44
+ #
45
+ # Args: $1 = node path.
46
+ local path="$1"
47
+ [[ $(yp_type_of "$path") == seq ]] || return 1
48
+ local count index entry_type entry_value
49
+ count=$(yp_count_of "$path")
50
+ # A single blank entry fails the whole list, because it would silently
51
+ # widen a radius and under-report contention.
52
+ for ((index = 0; index < count; index++)); do
53
+ entry_type=$(yp_type_of "${path}[${index}]")
54
+ entry_value=$(yp_value_of "${path}[${index}]")
55
+ pc_is_non_empty_string "$entry_type" "$entry_value" || return 1
56
+ done
57
+ return 0
58
+ }
59
+
60
+ pi_validate_blast_radius_block() {
61
+ # Validate one blast_radius block against spec invariant 9.
62
+ #
63
+ # Args: $1 = node path of the block, $2 = item-scoped context prefix.
64
+ local path="$1" context="$2"
65
+ if [[ $(yp_type_of "$path") != map ]]; then
66
+ pc_error_add "$context blast_radius must be an object."
67
+ return 0
68
+ fi
69
+ local field
70
+ local -a radius_fields=()
71
+ read -ra radius_fields <<<"$PC_BLAST_RADIUS_LIST_FIELDS"
72
+ # Report every malformed collection field rather than stopping at the
73
+ # first, so one validation pass tells the author everything to fix.
74
+ for field in "${radius_fields[@]}"; do
75
+ if ! pi_is_string_list "${path}.${field}"; then
76
+ pc_error_add "$context blast_radius.$field must be a list of non-empty strings."
77
+ fi
78
+ done
79
+
80
+ local source_type source_value
81
+ source_type=$(yp_type_of "${path}.source")
82
+ source_value=$(yp_value_of "${path}.source")
83
+ if [[ $source_type != str ]] || ! pc_enum_members_contains "$PC_VALID_SOURCES" "$source_value"; then
84
+ pc_error_add "$(pc_enum_error "$context" "blast_radius.source" "$PC_VALID_SOURCES" "$(pi_repr_at "${path}.source")")"
85
+ fi
86
+
87
+ local computed_type computed_value
88
+ computed_type=$(yp_type_of "${path}.computed_at")
89
+ computed_value=$(yp_value_of "${path}.computed_at")
90
+ if ! pc_is_non_empty_string "$computed_type" "$computed_value"; then
91
+ pc_error_add "$context blast_radius.computed_at must be a non-empty string."
92
+ fi
93
+ }
94
+
95
+ pi_validate_merge_status() {
96
+ # Validate merge_status membership and its agreement with item state.
97
+ #
98
+ # Absence is the backward-compatible case: an item with no merge_status is
99
+ # treated as not_started and yields no error, so the check is presence
100
+ # gated rather than requirement gated.
101
+ #
102
+ # Args: $1 = item node path, $2 = item-scoped context, $3 = state repr,
103
+ # $4 = raw state value.
104
+ local path="$1" context="$2" state_repr="$3" state_value="$4"
105
+ yp_has "${path}.merge_status" || return 0
106
+ local status_type status_value
107
+ status_type=$(yp_type_of "${path}.merge_status")
108
+ status_value=$(yp_value_of "${path}.merge_status")
109
+ # An out-of-enum value short-circuits the consistency rule, because
110
+ # agreement with state is meaningless for a value that is not a status.
111
+ if [[ $status_type != str ]] || ! pc_enum_members_contains "$PC_VALID_MERGE_STATUS" "$status_value"; then
112
+ pc_error_add "$(pc_enum_error "$context" "merge_status" "$PC_VALID_MERGE_STATUS" "$(pi_repr_at "${path}.merge_status")")"
113
+ return 0
114
+ fi
115
+
116
+ # Invariant 8 pins the two terminal families to their item states; every
117
+ # other status places no constraint on state.
118
+ if pc_contains_word "$PC_MERGED_MERGE_STATUSES" "$status_value" && [[ $state_value != merged ]]; then
119
+ pc_error_add "$context merge_status $(pc_repr_string "$status_value") requires state 'merged'; found: $state_repr."
120
+ return 0
121
+ fi
122
+ if pc_contains_word "$PC_BLOCKED_MERGE_STATUSES" "$status_value" && [[ $state_value != blocked ]]; then
123
+ pc_error_add "$context merge_status $(pc_repr_string "$status_value") requires state 'blocked'; found: $state_repr."
124
+ fi
125
+ }
126
+
127
+ pi_validate_item_record() {
128
+ # Validate one work-item record against spec invariants 5 through 9.
129
+ #
130
+ # Args: $1 = item node path, $2 = item-scoped context prefix, $3 = 1 when
131
+ # kind is required (the manifest and planner surfaces carry it).
132
+ local path="$1" context="$2" require_kind="$3"
133
+ if [[ $(yp_type_of "$path") != map ]]; then
134
+ pc_error_add "$context must be an object."
135
+ return 0
136
+ fi
137
+
138
+ local issue_type issue_value
139
+ issue_type=$(yp_type_of "${path}.issue_num")
140
+ issue_value=$(yp_value_of "${path}.issue_num")
141
+ if ! pc_is_positive_integer "$issue_type" "$issue_value"; then
142
+ pc_error_add "$context issue_num must be a positive integer; found: $(pi_repr_at "${path}.issue_num")."
143
+ fi
144
+
145
+ local folder_type folder_value
146
+ folder_type=$(yp_type_of "${path}.feature_folder")
147
+ folder_value=$(yp_value_of "${path}.feature_folder")
148
+ if ! pc_is_non_empty_string "$folder_type" "$folder_value"; then
149
+ pc_error_add "$context feature_folder must be a non-empty string."
150
+ fi
151
+
152
+ local state_type state_value state_repr
153
+ state_type=$(yp_type_of "${path}.state")
154
+ state_value=$(yp_value_of "${path}.state")
155
+ state_repr=$(pi_repr_at "${path}.state")
156
+ if [[ $state_type != str ]] || ! pc_enum_members_contains "$PC_VALID_ITEM_STATES" "$state_value"; then
157
+ pc_error_add "$(pc_enum_error "$context" "state" "$PC_VALID_ITEM_STATES" "$state_repr")"
158
+ fi
159
+
160
+ if ((require_kind == 1)); then
161
+ local kind_type kind_value
162
+ kind_type=$(yp_type_of "${path}.kind")
163
+ kind_value=$(yp_value_of "${path}.kind")
164
+ if [[ $kind_type != str ]] || ! pc_enum_members_contains "$PC_VALID_KINDS" "$kind_value"; then
165
+ pc_error_add "$(pc_enum_error "$context" "kind" "$PC_VALID_KINDS" "$(pi_repr_at "${path}.kind")")"
166
+ fi
167
+ fi
168
+
169
+ pi_validate_merge_status "$path" "$context" "$state_repr" "$state_value"
170
+ pi_validate_blast_radius_block "${path}.blast_radius" "$context"
171
+ }
172
+
173
+ pi_validate_items() {
174
+ # Validate the items collection, including issue_num uniqueness.
175
+ #
176
+ # Args: $1 = node path of the items collection, $2 = surface prefix,
177
+ # $3 = 1 when kind is required on every entry.
178
+ local path="$1" context="$2" require_kind="$3"
179
+ if [[ $(yp_type_of "$path") != seq ]]; then
180
+ pc_error_add "$context items must be a list."
181
+ return 0
182
+ fi
183
+
184
+ local count index entry_path item_ctx issue_type issue_value
185
+ local seen=""
186
+ local -a duplicates=()
187
+ count=$(yp_count_of "$path")
188
+ # Validate each entry in place, and in the same pass accumulate the primary
189
+ # keys so uniqueness is decided without a second traversal.
190
+ for ((index = 0; index < count; index++)); do
191
+ entry_path="${path}[${index}]"
192
+ item_ctx=$(pc_item_context "$context" "$index")
193
+ pi_validate_item_record "$entry_path" "$item_ctx" "$require_kind"
194
+ [[ $(yp_type_of "$entry_path") == map ]] || continue
195
+ issue_type=$(yp_type_of "${entry_path}.issue_num")
196
+ issue_value=$(yp_value_of "${entry_path}.issue_num")
197
+ [[ $issue_type == int ]] || continue
198
+ if pc_contains_word "$seen" "$issue_value"; then
199
+ duplicates+=("$issue_value")
200
+ fi
201
+ seen="$seen $issue_value"
202
+ done
203
+
204
+ ((${#duplicates[@]} > 0)) || return 0
205
+ # Report duplicates in ascending numeric order, de-duplicated, so a key
206
+ # repeated three times still yields exactly one message.
207
+ local ordered duplicate
208
+ ordered=$(printf '%s\n' "${duplicates[@]}" | sort -n -u)
209
+ while IFS= read -r duplicate; do
210
+ [[ -n $duplicate ]] || continue
211
+ pc_error_add "$context has duplicate items[].issue_num: $duplicate."
212
+ done <<<"$ordered"
213
+ }
214
+
215
+ pi_scan_prohibited_keys() {
216
+ # Reject prohibited keys per manifest invariant M7.
217
+ #
218
+ # Args: $1 = surface prefix, $2 = space-separated keys rejected at any
219
+ # nesting level, $3 = space-separated keys rejected at the document root
220
+ # only. Deep results come first, in document order, then the top-level
221
+ # results in the order the third argument lists them.
222
+ local context="$1" deep_keys="$2" top_keys="$3"
223
+ local index path key total=${#YP_ORDER[@]}
224
+ # Walk the node table in document order so the message sequence reproduces
225
+ # Python's depth-first, insertion-ordered traversal exactly.
226
+ for ((index = 0; index < total; index++)); do
227
+ path="${YP_ORDER[index]}"
228
+ key="${YP_KEY[$path]-}"
229
+ [[ -n $key ]] || continue
230
+ if pc_contains_word "$deep_keys" "$key"; then
231
+ pc_error_add "$context carries prohibited key '$key' at ${YP_PARENT[$path]}."
232
+ fi
233
+ done
234
+
235
+ # The shallow pass exists because manifest M7 bans integration_branch at
236
+ # the top level only, where a nested occurrence is legitimate child data.
237
+ local -a top_key_list=()
238
+ read -ra top_key_list <<<"$top_keys"
239
+ for key in "${top_key_list[@]}"; do
240
+ if yp_has "$key"; then
241
+ pc_error_add "$context carries prohibited key '$key' at $PC_ROOT_PATH."
242
+ fi
243
+ done
244
+ }
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ # parallel-manifest-validate.sh: sourceable bash port of
3
+ # scripts/dev_tools/parallel_manifest_contract.py. Validates a parallel-run
4
+ # manifest document against invariants M1 through M7 and exposes the two
5
+ # default-resolving accessors that every consumer uses instead of reading
6
+ # `mode` and `max_concurrency` directly.
7
+ #
8
+ # Section order is load-bearing and mirrors the Python module exactly: the M1
9
+ # frontmatter checks short-circuit with a single error; then run identity
10
+ # (M2 parallel, M3 mode, M4 max_concurrency, M5 created_at) in schema field
11
+ # order; then the M7 prohibited-key scan, deep results in document order
12
+ # followed by the top-level results; then the M6 items collection.
13
+ #
14
+ # Every error string begins with the literal prefix `Parallel manifest` and
15
+ # ends with a period. The Python module remains the repository authority.
16
+ #
17
+ # Out-of-subset input. When the YAML scanner refuses a construct it does not
18
+ # model, this module returns exit status 2 with the refusal detail in
19
+ # PM_SUBSET_DETAIL and emits no validation errors. Refusing to answer is
20
+ # deliberate: a guessed parse could disagree with the Python authority
21
+ # silently, whereas an explicit refusal is visible to the caller.
22
+ #
23
+ # shellcheck disable=SC2034
24
+ # SC2034 is disabled file-wide because PM_SUBSET_DETAIL is written here and
25
+ # read by the entry point validate-parallel-manifest.sh, which shellcheck
26
+ # analyses as a separate file and therefore cannot see the use.
27
+
28
+ # Resolve this file's own directory so its dependencies source regardless of
29
+ # the caller's working directory.
30
+ PM_LIB_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
31
+ # shellcheck source=.claude/lib/bash/parallel-items-validate.sh
32
+ # shellcheck disable=SC1091
33
+ source "$PM_LIB_DIR/parallel-items-validate.sh"
34
+
35
+ # Literal context prefix for every error this module and its helpers emit.
36
+ PM_CONTEXT="Parallel manifest"
37
+
38
+ # Documented default run mode when the manifest omits `mode` (invariant M3).
39
+ PM_DEFAULT_MODE="closed"
40
+
41
+ # Documented default fan-out when the manifest omits `max_concurrency` (M4).
42
+ PM_DEFAULT_MAX_CONCURRENCY=4
43
+
44
+ # Inclusive bounds on a present `max_concurrency` (invariant M4, A7).
45
+ PM_MIN_CONCURRENCY=1
46
+ PM_MAX_CONCURRENCY=8
47
+
48
+ # Keys the manifest rejects at any nesting level (invariant M7).
49
+ PM_DEEP_PROHIBITED_KEYS="depends_on"
50
+
51
+ # Keys the manifest rejects at the document root only (invariant M7).
52
+ PM_TOP_LEVEL_PROHIBITED_KEYS="integration_branch"
53
+
54
+ # Detail recorded when the parser refuses an out-of-subset construct.
55
+ PM_SUBSET_DETAIL=""
56
+
57
+ pm_parse_manifest() {
58
+ # Parse manifest text into the shared node table (invariant M1).
59
+ #
60
+ # Args: $1 = raw manifest document text.
61
+ # Returns 0 when the frontmatter parsed into a mapping. Returns 1 after
62
+ # appending the single M1 error to PC_ERRORS for a missing, unterminated,
63
+ # unparseable, or non-mapping block. Returns 2 without appending anything
64
+ # when the document uses a construct outside the supported subset.
65
+ local text="$1"
66
+ if ! yp_extract_frontmatter_body "$text" "$PM_CONTEXT"; then
67
+ pc_error_add "$YP_FENCE_ERROR"
68
+ return 1
69
+ fi
70
+ if yp_parse_body; then
71
+ return 0
72
+ fi
73
+ # Routing table for the three parser failure modes: a sequence or empty
74
+ # root is M1's non-mapping case, a malformed line is M1's YAML-error case,
75
+ # and an unsupported construct makes the module refuse to answer.
76
+ case "$YP_STATUS" in
77
+ not_a_mapping)
78
+ pc_error_add "$PM_CONTEXT frontmatter must be a mapping."
79
+ return 1
80
+ ;;
81
+ yaml_error)
82
+ pc_error_add "$PM_CONTEXT frontmatter is not valid YAML: $YP_DETAIL."
83
+ return 1
84
+ ;;
85
+ *)
86
+ PM_SUBSET_DETAIL="$YP_DETAIL"
87
+ return 2
88
+ ;;
89
+ esac
90
+ }
91
+
92
+ pm_validate_identity() {
93
+ # Validate run identity fields against invariants M2 through M5.
94
+ #
95
+ # The mode and max_concurrency checks are presence gated because both keys
96
+ # are optional: absence is the documented authoring shape that the
97
+ # accessors resolve, so it must contribute no error.
98
+ local slug_type slug_value
99
+ slug_type=$(yp_type_of "parallel")
100
+ slug_value=$(yp_value_of "parallel")
101
+ if ! pc_is_non_empty_string "$slug_type" "$slug_value"; then
102
+ pc_error_add "$PM_CONTEXT parallel must be a non-empty string."
103
+ fi
104
+
105
+ if yp_has "mode"; then
106
+ local mode_type mode_value
107
+ mode_type=$(yp_type_of "mode")
108
+ mode_value=$(yp_value_of "mode")
109
+ if [[ $mode_type != str ]] || ! pc_enum_members_contains "$PC_VALID_MODES" "$mode_value"; then
110
+ pc_error_add "$(pc_enum_error "$PM_CONTEXT" "mode" "$PC_VALID_MODES" "$(pi_repr_at "mode")")"
111
+ fi
112
+ fi
113
+
114
+ if yp_has "max_concurrency"; then
115
+ local cap_type cap_value
116
+ cap_type=$(yp_type_of "max_concurrency")
117
+ cap_value=$(yp_value_of "max_concurrency")
118
+ if ! pc_in_bounded_range "$cap_type" "$cap_value" "$PM_MIN_CONCURRENCY" "$PM_MAX_CONCURRENCY"; then
119
+ pc_error_add "$PM_CONTEXT max_concurrency must be an integer from $PM_MIN_CONCURRENCY through $PM_MAX_CONCURRENCY; found: $(pi_repr_at "max_concurrency")."
120
+ fi
121
+ fi
122
+
123
+ local created_type created_value
124
+ created_type=$(yp_type_of "created_at")
125
+ created_value=$(yp_value_of "created_at")
126
+ if ! pc_is_non_empty_string "$created_type" "$created_value"; then
127
+ pc_error_add "$PM_CONTEXT created_at must be a non-empty string."
128
+ fi
129
+ }
130
+
131
+ pm_validate_text() {
132
+ # Validate a manifest document against invariants M1 to M7.
133
+ #
134
+ # Args: $1 = raw manifest document text, authored with LF, CRLF, or CR
135
+ # line endings.
136
+ # Returns 0 after populating PC_ERRORS with the complete error list, which
137
+ # is empty for a valid manifest. Returns 2 without populating PC_ERRORS
138
+ # when the document uses a construct outside the supported subset.
139
+ local text="$1" parse_status=0
140
+ pc_errors_reset
141
+ PM_SUBSET_DETAIL=""
142
+ pm_parse_manifest "$text" || parse_status=$?
143
+ # An out-of-subset refusal propagates so the caller can distinguish it from
144
+ # a validation verdict; an M1 failure returns the single-element error list
145
+ # already accumulated, because no field check is meaningful without a
146
+ # mapping to read fields from.
147
+ if ((parse_status == 2)); then
148
+ return 2
149
+ fi
150
+ if ((parse_status != 0)); then
151
+ return 0
152
+ fi
153
+
154
+ pm_validate_identity
155
+ pi_scan_prohibited_keys "$PM_CONTEXT" "$PM_DEEP_PROHIBITED_KEYS" "$PM_TOP_LEVEL_PROHIBITED_KEYS"
156
+ # The manifest carries `kind` on every item, unlike the orchestrator
157
+ # checkpoint, so the shared item validator is asked to require it (S1).
158
+ pi_validate_items "items" "$PM_CONTEXT" 1
159
+ return 0
160
+ }
161
+
162
+ pm_manifest_mode() {
163
+ # Echo the run mode declared by the parsed manifest (invariant M3).
164
+ #
165
+ # A present but non-string value resolves to the default so this accessor
166
+ # always returns a mode; the malformed value is reported separately by
167
+ # pm_validate_text.
168
+ if [[ $(yp_type_of "mode") == str ]]; then
169
+ yp_value_of "mode"
170
+ else
171
+ printf '%s' "$PM_DEFAULT_MODE"
172
+ fi
173
+ }
174
+
175
+ pm_manifest_max_concurrency() {
176
+ # Echo the fan-out cap declared by the parsed manifest (invariant M4).
177
+ #
178
+ # A present but non-integer value resolves to the default so this accessor
179
+ # always returns an integer; the malformed value is reported separately by
180
+ # pm_validate_text. Booleans are classified as `bool` by the scanner and
181
+ # therefore never satisfy the integer test.
182
+ if [[ $(yp_type_of "max_concurrency") == int ]]; then
183
+ yp_value_of "max_concurrency"
184
+ else
185
+ printf '%s' "$PM_DEFAULT_MAX_CONCURRENCY"
186
+ fi
187
+ }