@pareto4/pp-core 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/install.js +116 -0
  3. package/commands/pp-build-handoff.md +46 -0
  4. package/commands/pp-discovery-to-build.md +40 -0
  5. package/gemini-extension.json +1 -1
  6. package/package.json +2 -1
  7. package/pareto-skills/_shared/create-project-board.sh +195 -0
  8. package/pareto-skills/_shared/pp-claude-md-protocol.md +306 -0
  9. package/pareto-skills/_shared/sync-phase-status.sh +139 -0
  10. package/pareto-skills/pp-brand/SKILL.md +644 -0
  11. package/pareto-skills/pp-brand/assets/crew/ParetoAgent.inline.html +120 -0
  12. package/pareto-skills/pp-brand/assets/crew/ParetoAgent.tsx +239 -0
  13. package/pareto-skills/pp-brand/assets/crew/build.svg +11 -0
  14. package/pareto-skills/pp-brand/assets/crew/deploy.svg +11 -0
  15. package/pareto-skills/pp-brand/assets/crew/discovery.svg +11 -0
  16. package/pareto-skills/pp-brand/assets/crew/plan.svg +11 -0
  17. package/pareto-skills/pp-brand/assets/crew/run.svg +11 -0
  18. package/pareto-skills/pp-brand/assets/crew/validate.svg +11 -0
  19. package/pareto-skills/pp-brand/assets/pareto-brand-preview.html +547 -0
  20. package/pareto-skills/pp-obsidian-notes/SKILL.md +127 -0
  21. package/pareto-skills/pp-obsidian-notes/templates/CLAUDE.md +29 -0
  22. package/pareto-skills/pp-obsidian-notes/templates/client-context.md +116 -0
  23. package/pareto-skills/pp-obsidian-notes/templates/meeting-note.md +66 -0
  24. package/pareto-skills/pp-obsidian-notes/templates/pareto-context.md +102 -0
  25. package/pareto-skills/pp-obsidian-notes/templates/proposal.md +117 -0
  26. package/pareto-skills/pp-project-setup/SKILL.md +1364 -0
  27. package/pp-core/bin/lib/intel.cjs +1 -1
  28. package/pp-core/bin/lib/io.cjs +120 -0
  29. package/pp-core/bin/lib/profile-pipeline-command-router.cjs +1 -1
  30. package/pp-core/bin/lib/roadmap-parser.cjs +1 -1
  31. package/pp-core/bin/lib/runtime-artifact-conversion.cjs +2 -2
  32. package/pp-core/bin/lib/surface.cjs +1 -1
  33. package/pp-core/bin/lib/worktree-safety.cjs +2 -2
  34. package/pp-core/bin/pp-tools.cjs +23 -0
  35. package/pp-core/references/ui-brand.md +73 -11
  36. package/pp-core/templates/codebase/structure.md +5 -5
  37. package/pp-core/templates/spec.md +7 -7
  38. package/pp-core/workflows/add-backlog.md +1 -1
  39. package/pp-core/workflows/autonomous.md +4 -4
  40. package/pp-core/workflows/debug.md +1 -1
  41. package/pp-core/workflows/discovery-to-build.md +598 -0
  42. package/pp-core/workflows/execute-phase.md +9 -1
  43. package/pp-core/workflows/plan-phase.md +14 -12
  44. package/pp-core/workflows/pr-branch.md +5 -4
  45. package/pp-core/workflows/reapply-patches.md +1 -1
  46. package/pp-core/workflows/ship.md +1 -1
  47. package/pp-core/workflows/thread.md +1 -1
  48. package/pp-core/workflows/verify-work.md +6 -5
@@ -0,0 +1,306 @@
1
+ <!--
2
+ pp-claude-md-protocol.md
3
+
4
+ Canonical pp-core ↔ GitHub protocol injected into a project's CLAUDE.md.
5
+
6
+ Used by:
7
+ - pareto-build-handoff (Step 6b: CLAUDE.md update)
8
+ - pareto-push-issues (Step 5.5: CLAUDE.md injection)
9
+
10
+ To inject: append the block below `## GitHub Board Sync` (if a section of
11
+ that name already exists, replace it — idempotent).
12
+
13
+ Single source of truth — edit here, not in the skills. Every repo that
14
+ runs build-handoff or push-issues gets the same protocol.
15
+ -->
16
+
17
+ ## GitHub Board Sync
18
+
19
+ Requirements are tracked as GitHub issues on a project board. The mapping
20
+ between pp-core requirement IDs (EPIC-xxx, US-xxx, FR-xxx) and GitHub issue
21
+ numbers lives in `.planning/github-issues.json`.
22
+
23
+ **Status flow:**
24
+
25
+ ```
26
+ Backlog → Discuss → Planned → In Progress → Verification → PR Raised → Done
27
+ ```
28
+
29
+ Every phase runs on its own branch and merges to `main` via a pull request
30
+ after verification passes. Stories only close when their phase PR is merged.
31
+
32
+ ### Per-phase lifecycle
33
+
34
+ **1. Phase starts (executing begins)**
35
+
36
+ Create a branch from `main` named `phase/{N}-{slug}` (slug from ROADMAP.md
37
+ phase title, kebab-case). All phase commits — code AND `.planning/` artifacts
38
+ — land on this branch.
39
+
40
+ ```bash
41
+ git checkout main && git pull
42
+ git checkout -b "phase/${N}-${SLUG}"
43
+ ```
44
+
45
+ Move each story issue covered by the phase to **In Progress**:
46
+
47
+ ```bash
48
+ gh issue edit {number} --add-label "in-progress,phase:{N}" \
49
+ --remove-label "verification" -R {repo}
50
+ ```
51
+
52
+ Also set the `Phase` custom field on the board item to the phase number.
53
+
54
+ **2. Phase implementation complete (SUMMARY.md written)**
55
+
56
+ Push the branch (first push uses `-u origin HEAD`) and move the stories to
57
+ **Verification**:
58
+
59
+ ```bash
60
+ git push -u origin HEAD
61
+ gh issue edit {number} --add-label "verification" \
62
+ --remove-label "in-progress" -R {repo}
63
+ ```
64
+
65
+ **3. Verification passes (VERIFICATION.md result = PASS)**
66
+
67
+ Open a pull request from the phase branch to `main`. The PR body MUST use
68
+ `Closes #N` for every story issue so GitHub auto-closes them on merge.
69
+
70
+ ```bash
71
+ PR_BODY=$(cat <<EOF
72
+ # Phase ${N}: ${PHASE_TITLE}
73
+
74
+ ## Summary
75
+ ${SUMMARY_FROM_SUMMARY_MD}
76
+
77
+ ## Requirements delivered
78
+ Closes #${ISSUE1} — ${US_ID_1}: ${STORY_TITLE_1}
79
+ Closes #${ISSUE2} — ${US_ID_2}: ${STORY_TITLE_2}
80
+
81
+ ## Verification
82
+ VERIFICATION.md result: PASS
83
+ - ${KEY_OUTCOMES}
84
+
85
+ ## Phase artifacts
86
+ - \`.planning/phases/${N}-${SLUG}/PLAN.md\`
87
+ - \`.planning/phases/${N}-${SLUG}/SUMMARY.md\`
88
+ - \`.planning/phases/${N}-${SLUG}/VERIFICATION.md\`
89
+
90
+ ---
91
+ Generated by pp-core phase execution.
92
+ EOF
93
+ )
94
+
95
+ gh pr create \
96
+ --base main \
97
+ --head "phase/${N}-${SLUG}" \
98
+ --title "Phase ${N}: ${PHASE_TITLE}" \
99
+ --body "$PR_BODY" \
100
+ --label "phase:${N}"
101
+ ```
102
+
103
+ **Do not close the story issues at this step.** They stay in Verification
104
+ until the PR merges. Keeping them open until merge is what makes the board
105
+ reflect *merged* reality, not just *verified* intent.
106
+
107
+ If verification fails: fix the code on the same phase branch, re-run
108
+ verification, update the PR (it auto-updates from new commits). No new
109
+ branch.
110
+
111
+ **4. PR merged**
112
+
113
+ GitHub auto-closes linked issues. The sync protocol then reconciles:
114
+
115
+ ```bash
116
+ # Sync loop — run after merge (or via /pareto-sync-board)
117
+ gh issue edit {number} --remove-label "in-progress,verification" -R {repo}
118
+ ```
119
+
120
+ Update `.planning/github-issues.json`: set each story's `status` to `"Done"`
121
+ and each EPIC's `status` based on whether all its stories are now closed.
122
+
123
+ If the merged phase closed ALL stories under an EPIC, also close the EPIC
124
+ issue with a delivery note:
125
+
126
+ ```bash
127
+ gh issue close {epic_number} -R {repo} \
128
+ -c "All stories delivered across phases {list}."
129
+ ```
130
+
131
+ ### Status map for .planning/github-issues.json
132
+
133
+ ```json
134
+ "status_map": {
135
+ "Backlog": "Backlog",
136
+ "discussing": "Discuss",
137
+ "planned": "Planned",
138
+ "executing": "In Progress",
139
+ "verifying": "Verification",
140
+ "pr_open": "PR Raised",
141
+ "complete": "Done"
142
+ }
143
+ ```
144
+
145
+ ### Filtered code-only PRs (optional)
146
+
147
+ If the team wants PR diffs without `.planning/` noise, run `/pp-pr-branch`
148
+ after Step 3 to produce a filtered branch. The filtered branch becomes the
149
+ PR head instead of `phase/{N}-{slug}`. The original phase branch stays as
150
+ the auditable work record.
151
+
152
+ ### MANDATORY: Board sync after pp-core phase transitions
153
+
154
+ **These instructions are project-level overrides.** When `.planning/github-issues.json`
155
+ exists, you MUST run the sync script and issue updates at each lifecycle transition
156
+ below. Sync is non-blocking — failures must never stop the pp-core workflow (always
157
+ append `|| true`).
158
+
159
+ **After `/pp-discuss-phase` completes** (CONTEXT.md written):
160
+
161
+ Move stories to **Discuss** and comment on each with key decisions:
162
+
163
+ ```bash
164
+ [ -f ".planning/github-issues.json" ] && bash "$HOME/.claude/skills/pareto/_shared/sync-phase-status.sh" "${PHASE_NUMBER}" "Discuss" 2>&1 || true
165
+ ```
166
+
167
+ Comment on each story issue for this phase with key decisions that affect it:
168
+
169
+ ```bash
170
+ if [ -f ".planning/github-issues.json" ]; then
171
+ REPO=$(jq -r '.repo' .planning/github-issues.json)
172
+ # For each story in the phase, add a comment with relevant decisions from CONTEXT.md
173
+ jq -r --argjson p "${PHASE_NUMBER}" '.issues | to_entries[] | select(.value.phase == $p and .value.type == "story") | "\(.key) \(.value.number)"' .planning/github-issues.json | while read REQ_ID ISSUE_NUM; do
174
+ gh issue comment "$ISSUE_NUM" -R "$REPO" --body "## Phase ${PHASE_NUMBER} — Context gathered
175
+
176
+ Decisions from \`/pp-discuss-phase\` that affect ${REQ_ID}:
177
+
178
+ ${DECISIONS_RELEVANT_TO_THIS_STORY}
179
+
180
+ ---
181
+ _Auto-posted by pp-core discuss-phase_" 2>/dev/null || true
182
+ done
183
+ fi
184
+ ```
185
+
186
+ Replace `${DECISIONS_RELEVANT_TO_THIS_STORY}` with the actual decisions from CONTEXT.md
187
+ that are relevant to each story. Extract from `<decisions>` section.
188
+
189
+ **After `/pp-plan-phase` completes planning** (plans pass all gates, STATE.md updated):
190
+
191
+ Move stories to **Planned** and comment with plan assignment:
192
+
193
+ ```bash
194
+ [ -f ".planning/github-issues.json" ] && bash "$HOME/.claude/skills/pareto/_shared/sync-phase-status.sh" "${PHASE_NUMBER}" "Planned" 2>&1 || true
195
+ ```
196
+
197
+ Then comment on each story issue with which plan covers it:
198
+
199
+ ```bash
200
+ if [ -f ".planning/github-issues.json" ]; then
201
+ REPO=$(jq -r '.repo' .planning/github-issues.json)
202
+ # For each story, find which PLAN.md references its requirement ID and comment
203
+ jq -r --argjson p "${PHASE_NUMBER}" '.issues | to_entries[] | select(.value.phase == $p and .value.type == "story") | "\(.key) \(.value.number)"' .planning/github-issues.json | while read REQ_ID ISSUE_NUM; do
204
+ gh issue comment "$ISSUE_NUM" -R "$REPO" --body "## Phase ${PHASE_NUMBER} — Planned
205
+
206
+ Covered by: \`${PLAN_FILE}\` (Wave ${WAVE})
207
+ Key tasks: ${TASK_SUMMARY}
208
+
209
+ ---
210
+ _Auto-posted by pp-core plan-phase_" 2>/dev/null || true
211
+ done
212
+ fi
213
+ ```
214
+
215
+ Replace `${PLAN_FILE}`, `${WAVE}`, and `${TASK_SUMMARY}` with actual values from the
216
+ PLAN.md that contains this story's requirement ID in its `requirements` frontmatter field.
217
+
218
+ **After `/pp-execute-phase` begins execution** (branch created, STATE.md shows phase active):
219
+
220
+ ```bash
221
+ [ -f ".planning/github-issues.json" ] && bash "$HOME/.claude/skills/pareto/_shared/sync-phase-status.sh" "${PHASE_NUMBER}" "In Progress" --label-add "in-progress" --label-remove "verification" 2>&1 || true
222
+ ```
223
+
224
+ **After `/pp-execute-phase` verification passes** (VERIFICATION.md written, ROADMAP.md updated):
225
+
226
+ Move stories to **Verification**:
227
+
228
+ ```bash
229
+ [ -f ".planning/github-issues.json" ] && bash "$HOME/.claude/skills/pareto/_shared/sync-phase-status.sh" "${PHASE_NUMBER}" "Verification" --label-add "verification" --label-remove "in-progress" 2>&1 || true
230
+ ```
231
+
232
+ **After PR is created** (push branch, create PR):
233
+
234
+ Push the branch and create a PR, then move stories to **PR Raised**:
235
+
236
+ ```bash
237
+ # 1. Push the phase branch
238
+ git push -u origin HEAD 2>&1 || true
239
+ ```
240
+
241
+ ```bash
242
+ # 2. Build PR body with Closes links for every story in this phase
243
+ if [ -f ".planning/github-issues.json" ]; then
244
+ REPO=$(jq -r '.repo' .planning/github-issues.json)
245
+ CLOSES_LINES=$(jq -r --argjson p "${PHASE_NUMBER}" '
246
+ .issues | to_entries[]
247
+ | select(.value.phase == $p and .value.type == "story")
248
+ | "Closes #\(.value.number) — \(.key): \(.value.title)"
249
+ ' .planning/github-issues.json)
250
+
251
+ gh pr create \
252
+ --base main \
253
+ --head "phase/${PHASE_NUMBER}-${PHASE_SLUG}" \
254
+ --title "Phase ${PHASE_NUMBER}: ${PHASE_NAME}" \
255
+ --body "$(cat <<EOF
256
+ ## Summary
257
+
258
+ ${SUMMARY_FROM_VERIFICATION_MD}
259
+
260
+ ## Requirements delivered
261
+
262
+ ${CLOSES_LINES}
263
+
264
+ ## Verification
265
+
266
+ VERIFICATION.md result: PASS
267
+
268
+ ## Phase artifacts
269
+
270
+ - \`.planning/phases/${PADDED_PHASE}-${PHASE_SLUG}/PLAN.md\`
271
+ - \`.planning/phases/${PADDED_PHASE}-${PHASE_SLUG}/SUMMARY.md\`
272
+ - \`.planning/phases/${PADDED_PHASE}-${PHASE_SLUG}/VERIFICATION.md\`
273
+
274
+ ---
275
+ Generated by pp-core phase execution.
276
+ EOF
277
+ )" 2>/dev/null || true
278
+ fi
279
+ ```
280
+
281
+ ```bash
282
+ # 4. Move stories to PR Raised
283
+ [ -f ".planning/github-issues.json" ] && bash "$HOME/.claude/skills/pareto/_shared/sync-phase-status.sh" "${PHASE_NUMBER}" "PR Raised" 2>&1 || true
284
+ ```
285
+
286
+ Stories stay in **PR Raised** until the PR is merged. GitHub auto-closes
287
+ the linked issues via `Closes #N`, which moves them to **Done** on the board.
288
+
289
+ After PR merge, clean up labels:
290
+
291
+ ```bash
292
+ [ -f ".planning/github-issues.json" ] && bash "$HOME/.claude/skills/pareto/_shared/sync-phase-status.sh" "${PHASE_NUMBER}" "Done" --label-remove "in-progress,verification" 2>&1 || true
293
+ ```
294
+
295
+ If any sync is missed or the board drifts, use the recovery steps below.
296
+
297
+ ### Recovery
298
+
299
+ If any step is missed, run `/pareto-sync-board` to reconcile. It:
300
+ - Reads `.planning/STATE.md`, `ROADMAP.md`, and every phase directory
301
+ - Checks `gh pr list --head phase/{N}-*` for PR state
302
+ - Only closes stories whose phase PR shows state `MERGED`
303
+ - Rewrites `github-issues.json` to match live board state
304
+
305
+ If `.planning/github-issues.json` is missing entirely, run
306
+ `/pareto-push-issues` to rebuild it from `.planning/REQUIREMENTS.md`.
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bash
2
+ # sync-phase-status.sh
3
+ #
4
+ # Move GitHub issues for a pp-core phase to a target board status.
5
+ # Reads .planning/github-issues.json to find issues mapped to the phase,
6
+ # then updates labels and board column.
7
+ #
8
+ # Used by pp-core workflows (plan-phase, execute-phase) at lifecycle transitions.
9
+ # Non-blocking — failures are logged but never stop the pp-core workflow.
10
+ #
11
+ # Usage:
12
+ # bash sync-phase-status.sh <phase_number> <target_status> [--label-add X] [--label-remove Y]
13
+ #
14
+ # Examples:
15
+ # bash sync-phase-status.sh 4 "To Do"
16
+ # bash sync-phase-status.sh 4 "In Progress" --label-add in-progress --label-remove verification
17
+ # bash sync-phase-status.sh 4 "Verification" --label-add verification --label-remove in-progress
18
+ #
19
+ # Environment:
20
+ # GITHUB_ISSUES_JSON — path to mapping file (default: .planning/github-issues.json)
21
+ #
22
+ # Exit code: always 0 (non-blocking). Errors go to stderr.
23
+
24
+ set -uo pipefail
25
+
26
+ PHASE="$1"
27
+ TARGET_STATUS="$2"
28
+ shift 2
29
+
30
+ LABEL_ADD=""
31
+ LABEL_REMOVE=""
32
+
33
+ while [[ $# -gt 0 ]]; do
34
+ case "$1" in
35
+ --label-add) LABEL_ADD="$2"; shift 2 ;;
36
+ --label-remove) LABEL_REMOVE="$2"; shift 2 ;;
37
+ *) shift ;;
38
+ esac
39
+ done
40
+
41
+ MAPPING="${GITHUB_ISSUES_JSON:-.planning/github-issues.json}"
42
+
43
+ if [ ! -f "$MAPPING" ]; then
44
+ echo "sync-phase-status: no mapping file at $MAPPING — skipping board sync" >&2
45
+ exit 0
46
+ fi
47
+
48
+ # Extract repo and board info
49
+ REPO=$(jq -r '.repo // empty' "$MAPPING" 2>/dev/null)
50
+ BOARD_NUM=$(jq -r '.board.number // empty' "$MAPPING" 2>/dev/null)
51
+ BOARD_OWNER=$(jq -r '.board.owner // empty' "$MAPPING" 2>/dev/null)
52
+
53
+ if [ -z "$REPO" ]; then
54
+ echo "sync-phase-status: no repo in mapping file — skipping" >&2
55
+ exit 0
56
+ fi
57
+
58
+ # Get all issue numbers for this phase (stories only — EPICs are derived)
59
+ ISSUE_NUMBERS=$(jq -r --arg p "$PHASE" '
60
+ [.issues | to_entries[]
61
+ | select(.value.phase == ($p | tonumber) and .value.type == "story")
62
+ | .value.number] | .[]
63
+ ' "$MAPPING" 2>/dev/null)
64
+
65
+ if [ -z "$ISSUE_NUMBERS" ]; then
66
+ echo "sync-phase-status: no stories mapped to phase $PHASE — skipping" >&2
67
+ exit 0
68
+ fi
69
+
70
+ COUNT=$(echo "$ISSUE_NUMBERS" | wc -l | tr -d ' ')
71
+ echo "sync-phase-status: moving $COUNT stories for phase $PHASE → $TARGET_STATUS"
72
+
73
+ # Update labels on each issue
74
+ for NUM in $ISSUE_NUMBERS; do
75
+ LABEL_CMD="gh issue edit $NUM -R $REPO"
76
+ [ -n "$LABEL_ADD" ] && LABEL_CMD="$LABEL_CMD --add-label $LABEL_ADD"
77
+ [ -n "$LABEL_REMOVE" ] && LABEL_CMD="$LABEL_CMD --remove-label $LABEL_REMOVE"
78
+
79
+ if [ -n "$LABEL_ADD" ] || [ -n "$LABEL_REMOVE" ]; then
80
+ eval "$LABEL_CMD" 2>/dev/null || echo " warn: label update failed for #$NUM" >&2
81
+ fi
82
+ done
83
+
84
+ # Update board column (if board info available)
85
+ if [ -n "$BOARD_NUM" ] && [ -n "$BOARD_OWNER" ]; then
86
+ # Resolve board project ID and Status field
87
+ PROJECT_ID=$(gh project view "$BOARD_NUM" --owner "$BOARD_OWNER" --format json --jq '.id' 2>/dev/null)
88
+
89
+ if [ -z "$PROJECT_ID" ]; then
90
+ echo "sync-phase-status: could not resolve project board — skipping column update" >&2
91
+ exit 0
92
+ fi
93
+
94
+ STATUS_FIELD_ID=$(gh project field-list "$BOARD_NUM" --owner "$BOARD_OWNER" --format json \
95
+ --jq '.fields[] | select(.name=="Status") | .id' 2>/dev/null)
96
+
97
+ OPTION_ID=$(gh project field-list "$BOARD_NUM" --owner "$BOARD_OWNER" --format json \
98
+ --jq ".fields[] | select(.name==\"Status\") | .options[] | select(.name==\"$TARGET_STATUS\") | .id" 2>/dev/null)
99
+
100
+ if [ -z "$STATUS_FIELD_ID" ] || [ -z "$OPTION_ID" ]; then
101
+ echo "sync-phase-status: could not resolve Status field or option '$TARGET_STATUS' — skipping column update" >&2
102
+ exit 0
103
+ fi
104
+
105
+ # Get all board items and match by issue number (--limit 200 to avoid default 30 cap)
106
+ ITEMS_JSON=$(gh project item-list "$BOARD_NUM" --owner "$BOARD_OWNER" --format json --limit 200 2>/dev/null)
107
+
108
+ for NUM in $ISSUE_NUMBERS; do
109
+ ITEM_ID=$(echo "$ITEMS_JSON" | jq -r --argjson n "$NUM" '
110
+ .items[] | select(.content.number == $n) | .id
111
+ ' 2>/dev/null)
112
+
113
+ if [ -n "$ITEM_ID" ] && [ "$ITEM_ID" != "null" ]; then
114
+ gh project item-edit \
115
+ --project-id "$PROJECT_ID" \
116
+ --id "$ITEM_ID" \
117
+ --field-id "$STATUS_FIELD_ID" \
118
+ --single-select-option-id "$OPTION_ID" 2>/dev/null \
119
+ && echo " ✓ #$NUM → $TARGET_STATUS" \
120
+ || echo " warn: board update failed for #$NUM" >&2
121
+ else
122
+ echo " warn: #$NUM not found on board — skipping column update" >&2
123
+ fi
124
+ done
125
+ fi
126
+
127
+ # Update status in the mapping file
128
+ jq --arg p "$PHASE" --arg s "$TARGET_STATUS" '
129
+ .issues |= with_entries(
130
+ if (.value.phase == ($p | tonumber) and .value.type == "story")
131
+ then .value.status = $s
132
+ else .
133
+ end
134
+ )
135
+ | .last_synced = (now | strftime("%Y-%m-%d"))
136
+ ' "$MAPPING" > "${MAPPING}.tmp" && mv "${MAPPING}.tmp" "$MAPPING"
137
+
138
+ echo "sync-phase-status: done — $COUNT stories updated in mapping"
139
+ exit 0