@mmerterden/multi-agent-pipeline 14.2.1 → 15.0.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.
- package/CHANGELOG.md +118 -6
- package/README.md +20 -9
- package/README.tr.md +150 -0
- package/docs/FIGMA_PIPELINE.md +3 -3
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0009-claude-stack-skills-plugin-only.md +31 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +24 -9
- package/docs/ecosystem.md +237 -0
- package/docs/features.md +5 -5
- package/index.js +2 -0
- package/install/_codex-agents.mjs +11 -2
- package/install/_common.mjs +65 -1
- package/install/_dev-only-files.mjs +0 -1
- package/install/_platform-filter.mjs +73 -7
- package/install/_plugin-skills.mjs +33 -8
- package/install/claude.mjs +144 -59
- package/install/codex.mjs +37 -7
- package/install/copilot.mjs +36 -11
- package/install/index.mjs +6 -2
- package/install/templates/codex-instructions.md +1 -1
- package/install/templates/copilot-instructions.md +15 -12
- package/package.json +1 -2
- package/pipeline/commands/multi-agent/SKILL.md +4 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/channels/SKILL.md +16 -5
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +186 -0
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -4
- package/pipeline/commands/multi-agent/dev/SKILL.md +10 -23
- package/pipeline/commands/multi-agent/dev-autopilot/SKILL.md +10 -2
- package/pipeline/commands/multi-agent/dev-local/SKILL.md +10 -24
- package/pipeline/commands/multi-agent/dev-local-autopilot/SKILL.md +10 -3
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +19 -4
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/jira/SKILL.md +13 -2
- package/pipeline/commands/multi-agent/language/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/local/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/local-autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/log/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +81 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/{ship → resume-local}/SKILL.md +13 -9
- package/pipeline/commands/multi-agent/setup/SKILL.md +5 -5
- package/pipeline/commands/multi-agent/stack/SKILL.md +55 -43
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +3 -3
- package/pipeline/commands/multi-agent/sync/SKILL.md +18 -11
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/update/SKILL.md +1 -1
- package/pipeline/lib/extract-conventions.sh +44 -15
- package/pipeline/lib/fetch-figma-annotations.sh +8 -1
- package/pipeline/lib/fetch-fortify.sh +23 -8
- package/pipeline/lib/figma-screenshot.sh +11 -1
- package/pipeline/lib/issue-fetcher.sh +77 -10
- package/pipeline/lib/md2confluence-v3.py +16 -2
- package/pipeline/lib/parse-complaints.sh +306 -0
- package/pipeline/lib/plan-todos.sh +5 -2
- package/pipeline/lib/post-pr-review.sh +8 -6
- package/pipeline/lib/shadow-git.sh +50 -9
- package/pipeline/lib/submodule-detector.sh +8 -1
- package/pipeline/multi-agent-refs/_input-parser.md +1 -1
- package/pipeline/multi-agent-refs/channels/confluence.md +3 -0
- package/pipeline/multi-agent-refs/channels/issue-comment.md +2 -2
- package/pipeline/multi-agent-refs/channels/jira.md +13 -2
- package/pipeline/multi-agent-refs/channels/pr-review-actions.md +1 -1
- package/pipeline/multi-agent-refs/channels/pr.md +20 -0
- package/pipeline/multi-agent-refs/channels/wiki.md +4 -4
- package/pipeline/multi-agent-refs/complaint-analysis-template.md +99 -0
- package/pipeline/multi-agent-refs/component-dispatch.md +6 -6
- package/pipeline/multi-agent-refs/cross-cli-contract.md +16 -16
- package/pipeline/multi-agent-refs/features/external-context-injection.md +1 -1
- package/pipeline/multi-agent-refs/features/stack-skill-routing.md +5 -5
- package/pipeline/multi-agent-refs/features/worktree-finalize.md +1 -1
- package/pipeline/multi-agent-refs/generate-issue.md +3 -3
- package/pipeline/multi-agent-refs/issue-jira-triad.md +3 -3
- package/pipeline/multi-agent-refs/payload-contracts.md +67 -0
- package/pipeline/multi-agent-refs/phases/modes.md +20 -0
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +2 -2
- package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +7 -7
- package/pipeline/multi-agent-refs/phases/phase-2-planning.md +5 -5
- package/pipeline/multi-agent-refs/phases/phase-3-dev.md +3 -3
- package/pipeline/multi-agent-refs/phases/phase-4-review.md +12 -12
- package/pipeline/multi-agent-refs/phases/phase-5-test.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-6-commit.md +8 -40
- package/pipeline/multi-agent-refs/phases/phase-7-report.md +5 -3
- package/pipeline/multi-agent-refs/phases.md +6 -0
- package/pipeline/multi-agent-refs/rules.md +2 -0
- package/pipeline/multi-agent-refs/tracker-contract.md +1 -1
- package/pipeline/multi-agent-refs/wiki-capture.md +2 -2
- package/pipeline/preferences-template.json +13 -5
- package/pipeline/rules/figma-pipeline.md +2 -2
- package/pipeline/schemas/agent-state.schema.json +1 -1
- package/pipeline/schemas/complaint-analysis-spec.schema.json +216 -0
- package/pipeline/schemas/migrations/prefs-2.5.0-to-2.6.0.mjs +46 -0
- package/pipeline/schemas/prefs.schema.json +277 -67
- package/pipeline/schemas/token-budget.json +2 -2
- package/pipeline/scripts/_stack-routing.mjs +79 -0
- package/pipeline/scripts/audit-log-rotate.sh +14 -1
- package/pipeline/scripts/build-skills-index.mjs +11 -0
- package/pipeline/scripts/build-stack-plugins.mjs +35 -60
- package/pipeline/scripts/check-derived-drift.mjs +65 -29
- package/pipeline/scripts/diff-explain.mjs +41 -3
- package/pipeline/scripts/diff-risk-score.mjs +72 -8
- package/pipeline/scripts/gc-worktrees.sh +4 -1
- package/pipeline/scripts/gen-mode-dispatch.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/scripts/learning-curve.mjs +8 -2
- package/pipeline/scripts/match-skills.mjs +8 -2
- package/pipeline/scripts/migrate-prefs.mjs +28 -20
- package/pipeline/scripts/output-quality-check.sh +15 -4
- package/pipeline/scripts/phase-tracker.sh +33 -12
- package/pipeline/scripts/phase0-exit-gate.mjs +3 -2
- package/pipeline/scripts/pre-commit-check.sh +69 -22
- package/pipeline/scripts/render-agent-log-cost.sh +8 -3
- package/pipeline/scripts/render-cost-summary.sh +42 -22
- package/pipeline/scripts/render-work-summary.sh +47 -13
- package/pipeline/scripts/review-scope.mjs +1 -1
- package/pipeline/scripts/run-aggregator.mjs +43 -14
- package/pipeline/scripts/scan-agent-config.sh +1 -1
- package/pipeline/scripts/skill-conformance.mjs +165 -30
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
- package/pipeline/scripts/smoke-schema-validation.sh +5 -1
- package/pipeline/scripts/test-gap-rules/android.json +25 -0
- package/pipeline/scripts/test-gap-rules/ios.json +34 -0
- package/pipeline/scripts/test-gap-rules/node.json +29 -0
- package/pipeline/scripts/test-gap-rules/python.json +25 -0
- package/pipeline/scripts/test-gap-scan.mjs +45 -6
- package/pipeline/scripts/uninstall.mjs +196 -14
- package/pipeline/scripts/update-issue-progress.sh +12 -16
- package/pipeline/scripts/validate-complaint-doc.mjs +229 -0
- package/pipeline/scripts/validate-reviewer.mjs +9 -3
- package/pipeline/scripts/worktree-finalize.sh +23 -2
- package/pipeline/skills/.skill-manifest.json +156 -108
- package/pipeline/skills/.skills-index.json +459 -13
- package/pipeline/skills/shared/README.md +15 -11
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +49 -0
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-dev/SKILL.md +4 -17
- package/pipeline/skills/shared/core/multi-agent-dev-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-dev-local/SKILL.md +5 -18
- package/pipeline/skills/shared/core/multi-agent-dev-local-autopilot/SKILL.md +8 -0
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-local/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-local-autopilot/SKILL.md +4 -0
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +83 -0
- package/pipeline/skills/shared/core/{multi-agent-ship → multi-agent-resume-local}/SKILL.md +10 -6
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +79 -22
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +1 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +1 -1
- package/pipeline/skills/shared/external/ios-coding-standard/modules/_TEMPLATE.yml +2 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +368 -33
- package/pipeline/skills/shared/external/ios-coding-standard/references/swiftlint.draft.yml +1 -2
- package/pipeline/skills/shared/external/ios-coding-standard/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +75 -0
- package/pipeline/skills/shared/external/ios-module-structure/modules/_TEMPLATE.yml +131 -0
- package/pipeline/skills/shared/external/ios-module-structure/references/rules.yml +559 -0
- package/pipeline/skills/shared/external/ios-module-structure/scripts/check_structure.py +765 -0
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +302 -0
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +156 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +108 -0
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +176 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +865 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +335 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +344 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +130 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +264 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +298 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +529 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +187 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +171 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +184 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +26 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +173 -0
- package/pipeline/skills/skills-index.md +9 -5
|
@@ -59,8 +59,8 @@ Run every step automatically:
|
|
|
59
59
|
```
|
|
60
60
|
Step 1: PLATFORM Detect macOS / Linux / Windows (Git Bash / WSL); export PLATFORM env
|
|
61
61
|
Step 1.5: DETECT Compare timestamps, find stale targets
|
|
62
|
-
Step 2: COPILOT Claude Code -> Copilot CLI (instructions +
|
|
63
|
-
Step 2b: CODEX Claude Code -> Codex CLI (1 router skill +
|
|
62
|
+
Step 2: COPILOT Claude Code -> Copilot CLI (instructions + 51 sub-command skills)
|
|
63
|
+
Step 2b: CODEX Claude Code -> Codex CLI (1 router skill + 51 specs as refs + 8 agent TOML)
|
|
64
64
|
Step 3: REPO Claude Code -> pipeline repo (genericized, personal data scrub, bash -n on all sh)
|
|
65
65
|
Step 3c: PLUGINS pipeline shared/external -> multi-agent-plugins marketplace (rebuild knowledge/,
|
|
66
66
|
bump changed plugins' patch version, commit + push the plugins repo)
|
|
@@ -127,13 +127,13 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
127
127
|
- `~/.claude/CLAUDE.md`, `~/.claude/rules/`, `~/.claude/knowledge/`
|
|
128
128
|
- `~/.claude/scripts/` - EXCEPT `pre-commit-check.sh`, `agent-guard.sh`, `agent-guard.py`, and `build-stack-plugins.mjs` (generic, synced)
|
|
129
129
|
- `~/.claude/settings.json`
|
|
130
|
-
- **Any `~/.claude/commands/multi-agent/*.md` whose frontmatter has `local-only: true`** - these are user/repo-specific alias wrappers that delegate to a private marketplace plugin (e.g. corporate `ai-ios-
|
|
130
|
+
- **Any `~/.claude/commands/multi-agent/*.md` whose frontmatter has `local-only: true`** - these are user/repo-specific alias wrappers that delegate to a private marketplace plugin (e.g. corporate `ai-ios-toolkit` skills exposed as `multi-agent:<name>`). Syncing them would leak the private plugin/skill names into the public pipeline. Filter before copy: skip every source file containing `local-only: true`, and after copy assert none reached `pipeline/commands/`.
|
|
131
131
|
```bash
|
|
132
132
|
# backstop: no local-only wrapper may exist in the synced target
|
|
133
133
|
grep -rl "^local-only: true" ~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
134
134
|
&& { echo "ABORT: local-only wrapper leaked into pipeline/commands"; exit 1; } || true
|
|
135
135
|
# backstop: no corporate marketplace/skill reference in the synced tree
|
|
136
|
-
grep -rniE "ai-ios-engineering
|
|
136
|
+
grep -rniE "ai-ios(-engineering)?-toolkit:(create-ui-component|evolve-ui-component|fix-bug|branch-and-pr|backlog|code-connect|figma-utility|resource-utility|component-wiki|component-docs|figma-setup)" \
|
|
137
137
|
~/multi-agent-pipeline/pipeline/commands/ 2>/dev/null \
|
|
138
138
|
&& { echo "ABORT: corporate reference leaked into pipeline/commands"; exit 1; } || true
|
|
139
139
|
```
|
|
@@ -176,7 +176,7 @@ If nothing is stale → report "All targets up to date" and stop.
|
|
|
176
176
|
Unlike the Copilot step, this one does **not** hand-copy files. The Codex tree is a
|
|
177
177
|
*transform* of the Claude tree, not a mirror of it, and the transform is real work:
|
|
178
178
|
|
|
179
|
-
- the
|
|
179
|
+
- the 51 sub-command specs become reference files, because Codex silently truncates
|
|
180
180
|
its skills block (see `cross-cli-contract.md` 2.6 for the measurement)
|
|
181
181
|
- every `$HOME/.claude/...` reference to a CLI-owned tree is retargeted, with
|
|
182
182
|
`agents/<persona>.md` becoming `.toml` and the dispatcher becoming the router skill
|
|
@@ -233,13 +233,20 @@ node "$HOME/multi-agent-pipeline/pipeline/scripts/build-stack-plugins.mjs" --dry
|
|
|
233
233
|
|
|
234
234
|
# 2. Apply: rebuild each stack plugin's knowledge/ from shared/external,
|
|
235
235
|
# regenerate plugin.json skills[], bump the patch version of any plugin
|
|
236
|
-
# whose skill set changed. Cross-stack skills -> ai-common-
|
|
236
|
+
# whose skill set changed. Cross-stack skills -> ai-common-toolkit;
|
|
237
237
|
# Apple/Xcode-only skills -> iOS plugin only.
|
|
238
238
|
node "$HOME/multi-agent-pipeline/pipeline/scripts/build-stack-plugins.mjs"
|
|
239
239
|
```
|
|
240
240
|
|
|
241
241
|
**Version-based management:** the generator bumps a plugin's `version` (patch) only when its skill set actually changed. Idempotent - a no-op run bumps nothing. Consumers pick up new versions via `/multi-agent:update` (which runs `claude marketplace update multi-agent-plugins`).
|
|
242
242
|
|
|
243
|
+
**Help + README freshness (mandatory when the generator reported any change):**
|
|
244
|
+
|
|
245
|
+
1. Every plugin ships a `tools/help` skill that renders its catalog LIVE from `plugin.json`, so it needs no per-skill edits - but verify each changed plugin still registers it (`grep -c "./skills/tools/help" plugins/<name>/.claude-plugin/plugin.json` = 1) and that a new plugin gets one before publish.
|
|
246
|
+
2. Plugin README tables sync through `python3 tools/bump.py <plugin> --set <version>`; run `python3 tools/bump.py --check` and treat DRIFT as a blocker.
|
|
247
|
+
3. `python3 tools/validate.py` must be clean (it also enforces the CHANGELOG heading for every bumped version - write the entry, do not backdate).
|
|
248
|
+
4. When the pipeline's command surface changed in the same sync (new/renamed `/multi-agent:*` commands), confirm `commands/multi-agent/help/SKILL.md` and both pipeline READMEs (`README.md`, `README.tr.md`) were updated in Step 3 before pushing here.
|
|
249
|
+
|
|
243
250
|
**Publish (only if the generator reported bumps):**
|
|
244
251
|
|
|
245
252
|
```bash
|
|
@@ -484,21 +491,21 @@ When invoked with the `release` argument:
|
|
|
484
491
|
> Codex takes the Step 2b path instead; see that section.
|
|
485
492
|
|
|
486
493
|
This runs on the Claude <-> Copilot axis. Codex is NOT synced here: it receives the
|
|
487
|
-
same
|
|
494
|
+
same 51 specs as reference files rather than as peer skills, via Step 2b - see
|
|
488
495
|
`cross-cli-contract.md` 2.6 for why the parity axis differs per host.
|
|
489
496
|
|
|
490
497
|
| Claude Code | Copilot CLI |
|
|
491
498
|
|-------------|-------------|
|
|
492
499
|
| `~/.claude/commands/multi-agent/{cmd}/SKILL.md` | `~/.copilot/skills/multi-agent-{cmd}/SKILL.md` |
|
|
493
500
|
|
|
494
|
-
**
|
|
501
|
+
**51 commands are synced** (canonical inventory - must match `cross-cli-contract.md` section 1; drift = contract violation):
|
|
495
502
|
|
|
496
503
|
```
|
|
497
|
-
analysis, analysis-resolve, autopilot, build-optimize, channels, create-jira, design-check, dev,
|
|
504
|
+
analysis, analysis-resolve, autopilot, build-optimize, channels, complaint-analysis, create-jira, design-check, dev,
|
|
498
505
|
dev-autopilot, dev-local, dev-local-autopilot, diff-explain, forget, garbage-collect,
|
|
499
506
|
help, ios-coding-standard, issue, jira, kill, language, local,
|
|
500
|
-
local-autopilot, log, manual-test, prune-logs, purge, refactor, resume, review, review-issue, review-jira,
|
|
501
|
-
routines, save, scan, search, setup,
|
|
507
|
+
local-autopilot, log, manual-test, prune-logs, prune-prompts, purge, refactor, resume, review, review-issue, review-jira,
|
|
508
|
+
routines, save, scan, search, setup, resume-local, stack, status, store-ready, sync, test, test-accessibility,
|
|
502
509
|
test-dark-mode, test-dynamic-type, test-screenshots, testflight-validation, uninstall, update
|
|
503
510
|
```
|
|
504
511
|
|
|
@@ -40,7 +40,7 @@ Unchanged from before the merge, on the iOS path:
|
|
|
40
40
|
|---|---|---|
|
|
41
41
|
| **1. Static** | `ios_app_store_audit` (18 rules, real ITMS codes) | an `.xcarchive` |
|
|
42
42
|
| **2. Authoritative** | `ios_testflight_validate` → `altool --validate-app` | an `.ipa` + credentials |
|
|
43
|
-
| **3. Policy** | `app-store-review` skill vs repo source | repo checkout |
|
|
43
|
+
| **3. Policy** | `ai-ios-toolkit:app-store-review` skill vs repo source | repo checkout |
|
|
44
44
|
|
|
45
45
|
A skipped gate is never folded into the pass count, and the run never uploads. The
|
|
46
46
|
report lands in `~/StoreChecks/ios-<repo>-<branch>-<timestamp>/report.md`.
|
|
@@ -28,6 +28,8 @@ Tokens in the OS credential store are NEVER removed by either mode.
|
|
|
28
28
|
- `~/.claude/CLAUDE.md` (user customisations)
|
|
29
29
|
- `~/.claude/rules/` (user-owned; installed write-if-missing, never overwritten)
|
|
30
30
|
- User content OUTSIDE the `<!-- multi-agent-pipeline:begin/end -->` markers (`copilot-instructions.md`)
|
|
31
|
+
- Local-only alias wrappers (`local-only: true` frontmatter) under `commands/multi-agent/` (user-authored, exist nowhere else)
|
|
32
|
+
- User-authored skill dirs and agent files (only pipeline-shipped names are removed)
|
|
31
33
|
|
|
32
34
|
Settings (`multi-agent-preferences.json`) and the log root (`~/.claude/logs/multi-agent/`) are kept by default and removed only when the user picks full cleanup (`--all-data`).
|
|
33
35
|
|
|
@@ -101,7 +101,7 @@ Update the pipeline in one command. Existing preferences are preserved; only ski
|
|
|
101
101
|
```
|
|
102
102
|
Current: v3.6.0
|
|
103
103
|
-> git pull origin main
|
|
104
|
-
-> node install.js --all (
|
|
104
|
+
-> node install.js --all (50 commands, 80 scripts, 203 skills)
|
|
105
105
|
-> migrate-prefs.mjs (0 changes - already v2.1.0)
|
|
106
106
|
|
|
107
107
|
✓ Updated: v3.6.0 → v3.7.0
|
|
@@ -140,9 +140,31 @@ run_find() {
|
|
|
140
140
|
if [ "${#roots[@]}" -eq 0 ]; then
|
|
141
141
|
return 0
|
|
142
142
|
fi
|
|
143
|
+
# No explicit -print anywhere in this expression means find falls back to
|
|
144
|
+
# its own implicit print on the WHOLE expression - which fires for every
|
|
145
|
+
# true branch of the -prune OR-chain too, including the pruned directories
|
|
146
|
+
# themselves (their contents are still correctly skipped; the dir path
|
|
147
|
+
# itself was not). An explicit -print, implicit-AND'd onto the caller's own
|
|
148
|
+
# criteria as the OR-chain's last branch, only fires on that branch.
|
|
143
149
|
find "${roots[@]}" \
|
|
144
150
|
"${SKIP_PRUNE_ARGS[@]}" \
|
|
145
|
-
"$@" 2>/dev/null || true
|
|
151
|
+
"$@" -print 2>/dev/null || true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
# Count how many of the newline-separated paths in $1 contain a line matching
|
|
155
|
+
# grep pattern $2. `xargs grep -l` here used to word-split on whitespace by
|
|
156
|
+
# default, so any path with a space in it was silently mis-scanned as several
|
|
157
|
+
# nonexistent partial paths (a false-negative on real matches, not an error -
|
|
158
|
+
# stderr was already redirected to /dev/null). Reading line-by-line handles
|
|
159
|
+
# that correctly since each line is one whole path regardless of embedded
|
|
160
|
+
# spaces.
|
|
161
|
+
count_grep_hits() {
|
|
162
|
+
local list="$1" pattern="$2" n=0 f
|
|
163
|
+
while IFS= read -r f; do
|
|
164
|
+
[ -z "$f" ] && continue
|
|
165
|
+
grep -lq "$pattern" "$f" 2>/dev/null && n=$((n + 1))
|
|
166
|
+
done <<< "$list"
|
|
167
|
+
printf '%s' "$n"
|
|
146
168
|
}
|
|
147
169
|
|
|
148
170
|
# ---- confidence classifier ----------------------------------------------------
|
|
@@ -849,8 +871,15 @@ bucket_localization_key() {
|
|
|
849
871
|
case "$PLATFORM" in
|
|
850
872
|
ios)
|
|
851
873
|
local strings_files xcstrings_files
|
|
852
|
-
|
|
853
|
-
|
|
874
|
+
# An earlier version ANDed -name onto the FRONT of the prune chain
|
|
875
|
+
# (`-name X SKIP_PRUNE_ARGS...`), which requires every -prune branch to
|
|
876
|
+
# ALSO be named "Localizable.strings" - a directory prune can never
|
|
877
|
+
# satisfy that, so no prune ever fired and the search walked into
|
|
878
|
+
# Pods/.build/node_modules looking for vendored copies. Prunes must
|
|
879
|
+
# stay their own independent OR-chain, ahead of (not ANDed with) the
|
|
880
|
+
# real search criteria. The xcstrings search had no prune chain at all.
|
|
881
|
+
strings_files=$(find "$REPO_PATH" "${SKIP_PRUNE_ARGS[@]}" -name 'Localizable.strings' -type f -print 2>/dev/null | head -5 || true)
|
|
882
|
+
xcstrings_files=$(find "$REPO_PATH" "${SKIP_PRUNE_ARGS[@]}" -name 'Localizable.xcstrings' -type f -print 2>/dev/null | head -3 || true)
|
|
854
883
|
if [ -n "$strings_files" ]; then
|
|
855
884
|
keys=$(printf '%s\n' "$strings_files" | while IFS= read -r f; do
|
|
856
885
|
[ -n "$f" ] && grep -hEo '^"[^"]+"' "$f" 2>/dev/null | tr -d '"'
|
|
@@ -918,9 +947,9 @@ bucket_di_registration() {
|
|
|
918
947
|
fi
|
|
919
948
|
files=$(run_find -type f -name '*.swift' 2>/dev/null || true)
|
|
920
949
|
if [ -n "$files" ]; then
|
|
921
|
-
resolver_count=$(
|
|
922
|
-
factory_count=$(
|
|
923
|
-
swinject_count=$(
|
|
950
|
+
resolver_count=$(count_grep_hits "$files" 'Resolver.register')
|
|
951
|
+
factory_count=$(count_grep_hits "$files" '@Injected\|Container.shared')
|
|
952
|
+
swinject_count=$(count_grep_hits "$files" 'Swinject\|Container()')
|
|
924
953
|
fi
|
|
925
954
|
manual_count=${manual_count:-0}; resolver_count=${resolver_count:-0}; factory_count=${factory_count:-0}; swinject_count=${swinject_count:-0}
|
|
926
955
|
|
|
@@ -941,9 +970,9 @@ bucket_di_registration() {
|
|
|
941
970
|
files=$(run_find -type f -name '*.kt' 2>/dev/null || true)
|
|
942
971
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
943
972
|
local hilt_count koin_count dagger_count
|
|
944
|
-
hilt_count=$(
|
|
945
|
-
koin_count=$(
|
|
946
|
-
dagger_count=$(
|
|
973
|
+
hilt_count=$(count_grep_hits "$files" '@Module\|@InstallIn\|@HiltViewModel')
|
|
974
|
+
koin_count=$(count_grep_hits "$files" 'koinViewModel\|module {')
|
|
975
|
+
dagger_count=$(count_grep_hits "$files" '@Component\|@Subcomponent')
|
|
947
976
|
hilt_count=${hilt_count:-0}; koin_count=${koin_count:-0}; dagger_count=${dagger_count:-0}
|
|
948
977
|
if [ "$hilt_count" -ge "$koin_count" ] && [ "$hilt_count" -ge "$dagger_count" ] && [ "$hilt_count" -gt 0 ]; then
|
|
949
978
|
emit_bucket "Hilt" "@Module @InstallIn(SingletonComponent::class)" "$(confidence_for "$hilt_count")" "[]" '["Koin","Dagger"]'
|
|
@@ -959,9 +988,9 @@ bucket_di_registration() {
|
|
|
959
988
|
files=$(run_find -type f -name '*.py' 2>/dev/null || true)
|
|
960
989
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
961
990
|
local depends_count dishka_count di_count
|
|
962
|
-
depends_count=$(
|
|
963
|
-
dishka_count=$(
|
|
964
|
-
di_count=$(
|
|
991
|
+
depends_count=$(count_grep_hits "$files" 'Depends(')
|
|
992
|
+
dishka_count=$(count_grep_hits "$files" 'dishka')
|
|
993
|
+
di_count=$(count_grep_hits "$files" 'dependency_injector')
|
|
965
994
|
depends_count=${depends_count:-0}; dishka_count=${dishka_count:-0}; di_count=${di_count:-0}
|
|
966
995
|
if [ "$depends_count" -ge "$dishka_count" ] && [ "$depends_count" -ge "$di_count" ] && [ "$depends_count" -gt 0 ]; then
|
|
967
996
|
emit_bucket "FastAPI Depends" "def handler(svc: FooService = Depends(get_service))" "$(confidence_for "$depends_count")" "[]" '["dishka","dependency_injector"]'
|
|
@@ -977,9 +1006,9 @@ bucket_di_registration() {
|
|
|
977
1006
|
files=$(run_find -type f \( -name '*.tsx' -o -name '*.ts' \) 2>/dev/null || true)
|
|
978
1007
|
if [ -z "$files" ]; then empty_bucket; return; fi
|
|
979
1008
|
local context_count zustand_count hook_count
|
|
980
|
-
context_count=$(
|
|
981
|
-
zustand_count=$(
|
|
982
|
-
hook_count=$(
|
|
1009
|
+
context_count=$(count_grep_hits "$files" 'createContext\|\.Provider')
|
|
1010
|
+
zustand_count=$(count_grep_hits "$files" "from 'zustand'\\|from \"zustand\"")
|
|
1011
|
+
hook_count=$(count_grep_hits "$files" 'export function use[A-Z]')
|
|
983
1012
|
context_count=${context_count:-0}; zustand_count=${zustand_count:-0}; hook_count=${hook_count:-0}
|
|
984
1013
|
if [ "$context_count" -ge "$zustand_count" ] && [ "$context_count" -ge "$hook_count" ] && [ "$context_count" -gt 0 ]; then
|
|
985
1014
|
emit_bucket "React Context Provider" "const FooContext = createContext<Foo>(...)" "$(confidence_for "$context_count")" "[]" '["zustand","custom-hook-factory"]'
|
|
@@ -124,10 +124,17 @@ trap 'rm -rf "$TMPDIR_A"' EXIT INT TERM
|
|
|
124
124
|
# curl one /nodes batch (token via -K config so it never lands in argv).
|
|
125
125
|
curl_nodes() {
|
|
126
126
|
local out="$1" ids="$2" code
|
|
127
|
+
# curl already writes "000" via -w on a total connection failure (and
|
|
128
|
+
# still exits non-zero for it, which the inner `|| echo "000"` used to
|
|
129
|
+
# catch too - so `code` became the literal string "000000", never
|
|
130
|
+
# matching a plain "000" comparison anywhere downstream). `|| true` keeps
|
|
131
|
+
# a failing curl from tripping any caller's `set -e` without re-printing
|
|
132
|
+
# into $code; `:-000` only fills in when curl fails before -w ever runs.
|
|
127
133
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
128
134
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
129
135
|
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
130
|
-
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids" ||
|
|
136
|
+
"$FIGMA_API/files/$FILE_KEY/nodes?ids=$ids") || true
|
|
137
|
+
code="${code:-000}"
|
|
131
138
|
printf '%s' "$code"
|
|
132
139
|
}
|
|
133
140
|
|
|
@@ -202,18 +202,33 @@ fortify_get() {
|
|
|
202
202
|
return 1
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
case
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
# Check fortify_get's actual exit status, not a string pattern over its
|
|
206
|
+
# output - the old `case ... HTTP_AUTH_FAILED|*HTTP_4*)` only recognized
|
|
207
|
+
# auth failures and 4xx. A dropped connection, timeout, or 5xx made
|
|
208
|
+
# fortify_get return "...\nHTTP_000"/"...\nHTTP_5xx", which matched neither
|
|
209
|
+
# arm, so the case fell through, VERSION_JSON stayed unparseable garbage,
|
|
210
|
+
# and the Python block below silently treated that as "0 issues found" -
|
|
211
|
+
# the security gate passing precisely when it couldn't check anything.
|
|
212
|
+
if ! VERSION_JSON=$(fortify_get "projectVersions/$VERSION_ID"); then
|
|
213
|
+
echo "ERR: Fortify fetch failed for projectVersions/$VERSION_ID: ${VERSION_JSON:-no response}" >&2
|
|
214
|
+
exit 3
|
|
215
|
+
fi
|
|
211
216
|
|
|
212
217
|
ISSUES_JSON=""
|
|
213
218
|
if [ -n "$ISSUE_ID" ]; then
|
|
214
|
-
ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?q=issueInstanceId:\"$ISSUE_ID\"&limit=20"
|
|
219
|
+
if ! ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?q=issueInstanceId:\"$ISSUE_ID\"&limit=20"); then
|
|
220
|
+
echo "ERR: Fortify issues fetch failed for issue $ISSUE_ID: ${ISSUES_JSON:-no response}" >&2
|
|
221
|
+
exit 3
|
|
222
|
+
fi
|
|
215
223
|
else
|
|
216
|
-
|
|
224
|
+
# filter=ANALYSIS!=NotAnIssue,ANALYSIS!=FalsePositive (%21%3D is `!=`) is
|
|
225
|
+
# meant to EXCLUDE suppressed findings. It was encoded as %3D%3D (`==`),
|
|
226
|
+
# which instead selected only suppressed findings - the opposite of the
|
|
227
|
+
# gate's intent whenever SSC honors the filter.
|
|
228
|
+
if ! ISSUES_JSON=$(fortify_get "projectVersions/$VERSION_ID/issues?limit=200&filter=ANALYSIS%21%3DNotAnIssue,ANALYSIS%21%3DFalsePositive&filterset=Critical%2CHigh"); then
|
|
229
|
+
echo "ERR: Fortify issues fetch failed for version $VERSION_ID: ${ISSUES_JSON:-no response}" >&2
|
|
230
|
+
exit 3
|
|
231
|
+
fi
|
|
217
232
|
fi
|
|
218
233
|
|
|
219
234
|
# Issue details are fetched per-issue lazily - only for the first 10 to keep
|
|
@@ -180,10 +180,20 @@ curl_figma() {
|
|
|
180
180
|
for attempt in 1 2 3; do
|
|
181
181
|
# Token header goes through a curl config via process substitution so it
|
|
182
182
|
# never appears in argv (argv is visible to ps).
|
|
183
|
+
# curl already writes "000" via -w on a total connection failure (and
|
|
184
|
+
# still exits non-zero for it, which the inner `|| echo "000"` used to
|
|
185
|
+
# catch too - so `code` became the literal string "000000", matching
|
|
186
|
+
# neither the `000)` retry branch below nor handle_http_code's "000)
|
|
187
|
+
# network timeout" case, just the generic catch-all). `|| true` on its
|
|
188
|
+
# own line keeps `set -e` from aborting on curl's exit status without
|
|
189
|
+
# re-printing anything into $code; the `:-000` fallback only fires for
|
|
190
|
+
# the rarer case where curl fails before -w ever runs and $code is
|
|
191
|
+
# genuinely empty.
|
|
183
192
|
code=$(curl -sS -o "$out" -w '%{http_code}' \
|
|
184
193
|
--max-time "$HTTP_TIMEOUT" --connect-timeout 5 \
|
|
185
194
|
-K <(printf 'header = "X-Figma-Token: %s"\n' "$FIGMA_TOKEN") \
|
|
186
|
-
"$url" ||
|
|
195
|
+
"$url") || true
|
|
196
|
+
code="${code:-000}"
|
|
187
197
|
case "$code" in
|
|
188
198
|
2*) printf '%s' "$code"; return 0 ;;
|
|
189
199
|
401|403) printf '%s' "$code"; return 0 ;;
|
|
@@ -12,7 +12,15 @@
|
|
|
12
12
|
# "title": "...",
|
|
13
13
|
# "type": "Bug" | "Task" | "Story" | "Issue",
|
|
14
14
|
# "status": "...",
|
|
15
|
-
# "description":"...", // raw body (truncated to 4000 chars)
|
|
15
|
+
# "description":"...", // raw body (truncated to 4000 chars); this
|
|
16
|
+
# // issue's OWN description - never auto-substituted
|
|
17
|
+
# "parentKey": "PROJ-1" | null, // jira only; set when own description is empty
|
|
18
|
+
# // AND a parent link exists (single hop, no chasing
|
|
19
|
+
# // the parent's parent)
|
|
20
|
+
# "parentDescription": "..." | null, // jira only; the parent's description (truncated
|
|
21
|
+
# // 4000 chars), only when own is empty and parent
|
|
22
|
+
# // has content - a CANDIDATE, not applied automatically;
|
|
23
|
+
# // the caller must confirm before using it as "description"
|
|
16
24
|
# "url": "https://...",
|
|
17
25
|
# "host": "jira.example.com" | "github.com",
|
|
18
26
|
# "owner": "org" (gh) | null,
|
|
@@ -21,11 +29,17 @@
|
|
|
21
29
|
# "maturity": {
|
|
22
30
|
# "score": 0..100,
|
|
23
31
|
# "blockers": ["status_closed", "description_empty"],
|
|
24
|
-
# "warnings": ["short_description", "no_repro_steps"],
|
|
32
|
+
# "warnings": ["short_description", "no_repro_steps", "description_empty_parent_available"],
|
|
25
33
|
# "summary": "Description boş - pipeline başlatılamaz." (human-readable, tr|en)
|
|
26
34
|
# }
|
|
27
35
|
# }
|
|
28
36
|
#
|
|
37
|
+
# "description_empty_parent_available": own description is empty but parentDescription is
|
|
38
|
+
# non-empty. This is a WARNING, not a blocker - the caller must ask the user (or, in
|
|
39
|
+
# autopilot, auto-accept per the same "warnings auto-continue" rule) whether to proceed
|
|
40
|
+
# using parentDescription as the working description. See jira/SKILL.md's maturity
|
|
41
|
+
# section for the exact question wording.
|
|
42
|
+
#
|
|
29
43
|
# Required env:
|
|
30
44
|
# ACCOUNT_JIRA_TOKEN_KEY keychain service name (optional)
|
|
31
45
|
# ACCOUNT_JIRA_HOST e.g. jira.example.com (optional)
|
|
@@ -51,8 +65,16 @@ detect_kind() {
|
|
|
51
65
|
case "$in" in
|
|
52
66
|
*atlassian.net/browse/*|*jira*/browse/*|http*://*/browse/*) echo "jira-url"; return ;;
|
|
53
67
|
*github.com/*/issues/*) echo "gh-url"; return ;;
|
|
54
|
-
[A-Z]*-[0-9]*) echo "jira-id"; return ;;
|
|
55
68
|
esac
|
|
69
|
+
# A bare Jira key is PROJECT-NUMBER, anchored end-to-end. The case glob this
|
|
70
|
+
# replaced ([A-Z]*-[0-9]*) matches whenever that shape appears ANYWHERE in a
|
|
71
|
+
# longer string (case patterns match the whole string, but `*` is greedy
|
|
72
|
+
# enough to swallow surrounding words) - "Add iOS-16 support" contains
|
|
73
|
+
# "S-16" and satisfied it, silently misrouting a free-text task description
|
|
74
|
+
# into a Jira lookup instead of the freetext path below.
|
|
75
|
+
if [[ "$in" =~ ^[A-Z][A-Z0-9]*-[0-9]+$ ]]; then
|
|
76
|
+
echo "jira-id"; return
|
|
77
|
+
fi
|
|
56
78
|
# Bare issue number ("316" or "#316") must resolve BEFORE the repo#N glob:
|
|
57
79
|
# `*\#[0-9]*` also matches "#316" with an empty repo prefix, which used to
|
|
58
80
|
# misroute it to gh-short and build https://github.com/<owner>//issues/316.
|
|
@@ -140,7 +162,7 @@ fetch_jira() {
|
|
|
140
162
|
return 1
|
|
141
163
|
fi
|
|
142
164
|
curl -sf -K <(jira_auth_cfg "$token") \
|
|
143
|
-
"https://$JIRA_HOST/rest/api/2/issue/$key?fields=summary,status,issuetype,description,priority,resolution,fixVersions" \
|
|
165
|
+
"https://$JIRA_HOST/rest/api/2/issue/$key?fields=summary,status,issuetype,description,priority,resolution,fixVersions,parent" \
|
|
144
166
|
|| { echo "ERR: Jira fetch failed for $key" >&2; return 1; }
|
|
145
167
|
}
|
|
146
168
|
|
|
@@ -197,6 +219,8 @@ descriptor = {
|
|
|
197
219
|
"resolution": resolution or None,
|
|
198
220
|
"fixVersions": fix_versions,
|
|
199
221
|
"description": desc,
|
|
222
|
+
"parentKey": fields.get("parentKey") or None,
|
|
223
|
+
"parentDescription": fields.get("parentDescription") or None,
|
|
200
224
|
"host": fields.get("host") or None,
|
|
201
225
|
"url": fields.get("url") or None,
|
|
202
226
|
"owner": fields.get("owner") or None,
|
|
@@ -213,6 +237,9 @@ title = (descriptor.get("title") or "").strip()
|
|
|
213
237
|
status = (descriptor.get("status") or "").lower()
|
|
214
238
|
itype = (descriptor.get("type") or "").lower()
|
|
215
239
|
|
|
240
|
+
parent_desc = (descriptor.get("parentDescription") or "").strip()
|
|
241
|
+
parent_key = descriptor.get("parentKey") or ""
|
|
242
|
+
|
|
216
243
|
blockers, warnings = [], []
|
|
217
244
|
closed = {"done","closed","cancelled","canceled","resolved"}
|
|
218
245
|
if status in closed:
|
|
@@ -221,7 +248,14 @@ if status in closed:
|
|
|
221
248
|
if resolution:
|
|
222
249
|
blockers.append("already_resolved")
|
|
223
250
|
if not desc.strip():
|
|
224
|
-
|
|
251
|
+
# A parent with actual content is a candidate, not an auto-fix - surface it
|
|
252
|
+
# as a warning so the caller asks the user before substituting it in (or,
|
|
253
|
+
# under autopilot, auto-accepts per the same "warnings auto-continue" rule).
|
|
254
|
+
# No parent / an equally-empty parent keeps the hard blocker as before.
|
|
255
|
+
if parent_desc:
|
|
256
|
+
warnings.append("description_empty_parent_available")
|
|
257
|
+
else:
|
|
258
|
+
blockers.append("description_empty")
|
|
225
259
|
elif len(desc.strip()) < 80:
|
|
226
260
|
warnings.append("short_description")
|
|
227
261
|
if len(title) < 10:
|
|
@@ -239,6 +273,7 @@ labels_tr = {
|
|
|
239
273
|
"status_closed": "Issue kapalı veya iptal edilmiş",
|
|
240
274
|
"already_resolved": "Issue zaten resolved/merged ({})".format(resolution or " - "),
|
|
241
275
|
"description_empty": "Açıklama boş",
|
|
276
|
+
"description_empty_parent_available":"Açıklama boş ama parent'ta ({}) içerik var - oradan devam edilsin mi?".format(parent_key or " - "),
|
|
242
277
|
"short_description": "Açıklama çok kısa (<80 karakter)",
|
|
243
278
|
"short_title": "Başlık çok kısa (<10 karakter)",
|
|
244
279
|
"no_repro_steps": "Bug için repro adımları yok",
|
|
@@ -248,6 +283,7 @@ labels_en = {
|
|
|
248
283
|
"status_closed": "Issue is closed/cancelled",
|
|
249
284
|
"already_resolved": "Issue already resolved/merged ({})".format(resolution or " - "),
|
|
250
285
|
"description_empty": "Description is empty",
|
|
286
|
+
"description_empty_parent_available":"Description is empty but the parent ({}) has content - continue from there?".format(parent_key or " - "),
|
|
251
287
|
"short_description": "Description too short (<80 chars)",
|
|
252
288
|
"short_title": "Title too short (<10 chars)",
|
|
253
289
|
"no_repro_steps": "Bug missing reproduction steps",
|
|
@@ -309,21 +345,52 @@ description = (f.get("description") or "")
|
|
|
309
345
|
priority = _name(f.get("priority"))
|
|
310
346
|
resolution = _name(f.get("resolution"))
|
|
311
347
|
fixversions = ",".join((v or {}).get("name", "") for v in (f.get("fixVersions") or []))
|
|
312
|
-
|
|
348
|
+
parentkey = (f.get("parent") or {}).get("key", "") or ""
|
|
349
|
+
parts = [title, itype, status, description, priority, resolution, fixversions, parentkey]
|
|
313
350
|
parts = [v.replace("\x1f", " ") for v in parts]
|
|
314
351
|
sys.stdout.write("\x1f".join(parts))
|
|
315
352
|
')
|
|
316
|
-
IFS=$'\x1f' read -r -d '' title itype status description priority resolution fixversions <<< "$parsed" || true
|
|
353
|
+
IFS=$'\x1f' read -r -d '' title itype status description priority resolution fixversions parentkey <<< "$parsed" || true
|
|
354
|
+
# `<<<` appends a trailing newline to its input; `read -d ''` has no
|
|
355
|
+
# delimiter to stop on, so that newline folds into whichever field
|
|
356
|
+
# comes last - now parentkey. Strip it, or a genuinely-empty parent
|
|
357
|
+
# (no fallback needed) or a real key gets a stray \n appended, which
|
|
358
|
+
# breaks the parent lookup's URL and misreports parentKey in output.
|
|
359
|
+
parentkey="${parentkey%$'\n'}"
|
|
317
360
|
else
|
|
318
361
|
title="(unfetched)"; itype="Task"; status="Unknown"; description=""
|
|
319
|
-
priority=""; resolution=""; fixversions=""
|
|
362
|
+
priority=""; resolution=""; fixversions=""; parentkey=""
|
|
363
|
+
fi
|
|
364
|
+
# Development sub-tasks are frequently filed with an empty description while
|
|
365
|
+
# the real requirements live on the parent (story/epic). Single hop only -
|
|
366
|
+
# if the parent is also empty/unreachable, description stays empty and
|
|
367
|
+
# description_empty blocks below, same as before this existed. This fetcher
|
|
368
|
+
# only surfaces the parent's description as a CANDIDATE (parentDescription)
|
|
369
|
+
# - it never substitutes it into "description" itself. Whether to actually
|
|
370
|
+
# use it is a decision for the caller to put in front of the user (or
|
|
371
|
+
# auto-accept under autopilot), same as any other maturity warning.
|
|
372
|
+
parentdesc=""
|
|
373
|
+
if [ -z "$(printf '%s' "$description" | tr -d '[:space:]')" ] && [ -n "${parentkey:-}" ]; then
|
|
374
|
+
praw=$(fetch_jira "$parentkey") || praw=""
|
|
375
|
+
if [ -n "$praw" ]; then
|
|
376
|
+
pdesc=$(printf '%s' "$praw" | python3 -c '
|
|
377
|
+
import json, sys
|
|
378
|
+
d = json.load(sys.stdin)
|
|
379
|
+
f = d.get("fields", {}) or {}
|
|
380
|
+
sys.stdout.write((f.get("description") or "").replace("\x1f", " "))
|
|
381
|
+
')
|
|
382
|
+
if [ -n "$(printf '%s' "$pdesc" | tr -d '[:space:]')" ]; then
|
|
383
|
+
parentdesc="$pdesc"
|
|
384
|
+
fi
|
|
385
|
+
fi
|
|
320
386
|
fi
|
|
321
387
|
branch=$(branch_for "$KEY" "$itype" "$title")
|
|
322
388
|
emit_descriptor \
|
|
323
389
|
"kind=jira" "key=$KEY" "title=$title" "type=$itype" "status=$status" \
|
|
324
390
|
"description=$description" "host=$JIRA_HOST" \
|
|
325
391
|
"url=https://$JIRA_HOST/browse/$KEY" "branchHint=$branch" \
|
|
326
|
-
"priority=$priority" "resolution=$resolution" "fixVersions=$fixversions"
|
|
392
|
+
"priority=$priority" "resolution=$resolution" "fixVersions=$fixversions" \
|
|
393
|
+
"parentKey=$parentkey" "parentDescription=$parentdesc"
|
|
327
394
|
;;
|
|
328
395
|
|
|
329
396
|
jira-url)
|
|
@@ -335,7 +402,7 @@ sys.stdout.write("\x1f".join(parts))
|
|
|
335
402
|
gh-url)
|
|
336
403
|
OWNER=$(printf '%s' "$INPUT" | sed -E 's#https?://github.com/##; s#/.*##')
|
|
337
404
|
REPO=$(printf '%s' "$INPUT" | sed -E 's#https?://github.com/[^/]+/##; s#/.*##')
|
|
338
|
-
NUM=$(printf '%s' "$INPUT" | sed -E 's#.*/issues/##; s#[
|
|
405
|
+
NUM=$(printf '%s' "$INPUT" | sed -E 's#.*/issues/##; s#[/?#].*##')
|
|
339
406
|
raw=$(fetch_github "$OWNER" "$REPO" "$NUM") || raw=""
|
|
340
407
|
if [ -n "$raw" ]; then
|
|
341
408
|
# Single python3 pass - see jira-id branch for rationale on the
|
|
@@ -137,7 +137,7 @@ MERMAID_FENCE_OPEN_RE = re.compile(r"^```mermaid\s*$")
|
|
|
137
137
|
def mermaid_macro(body: str) -> str:
|
|
138
138
|
return (
|
|
139
139
|
'<ac:structured-macro ac:name="mermaid">'
|
|
140
|
-
f'<ac:plain-text-body><![CDATA[\n{body}\n]]></ac:plain-text-body>'
|
|
140
|
+
f'<ac:plain-text-body><![CDATA[\n{cdata_escape(body)}\n]]></ac:plain-text-body>'
|
|
141
141
|
'</ac:structured-macro>'
|
|
142
142
|
)
|
|
143
143
|
|
|
@@ -205,6 +205,20 @@ def esc(s: str) -> str:
|
|
|
205
205
|
return html.escape(s, quote=False)
|
|
206
206
|
|
|
207
207
|
|
|
208
|
+
def cdata_escape(s: str) -> str:
|
|
209
|
+
"""Make `s` safe to embed inside a CDATA section.
|
|
210
|
+
|
|
211
|
+
A code fence or Mermaid diagram can legitimately contain the literal
|
|
212
|
+
three-character sequence "]]>" (a real XML CDATA example, a shell case
|
|
213
|
+
pattern, ...), which is the CDATA terminator - embedding it unescaped
|
|
214
|
+
closes the section early and leaves the rest of the storage-format XML
|
|
215
|
+
unparsed as markup. Splitting it across a close/reopen pair is the
|
|
216
|
+
standard CDATA-in-CDATA escape: it renders identically, but is no longer
|
|
217
|
+
a literal terminator to the XML parser.
|
|
218
|
+
"""
|
|
219
|
+
return s.replace("]]>", "]]]]><![CDATA[>")
|
|
220
|
+
|
|
221
|
+
|
|
208
222
|
@dataclass
|
|
209
223
|
class InlineContext:
|
|
210
224
|
attachments_available: set[str]
|
|
@@ -388,7 +402,7 @@ def markdown_to_storage(
|
|
|
388
402
|
out.append(
|
|
389
403
|
'<ac:structured-macro ac:name="code">'
|
|
390
404
|
f'<ac:parameter ac:name="language">{esc(code_lang)}</ac:parameter>'
|
|
391
|
-
f'<ac:plain-text-body><![CDATA[{body}]]></ac:plain-text-body>'
|
|
405
|
+
f'<ac:plain-text-body><![CDATA[{cdata_escape(body)}]]></ac:plain-text-body>'
|
|
392
406
|
'</ac:structured-macro>'
|
|
393
407
|
)
|
|
394
408
|
i += 1
|