@jenga-ai/agent 1.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/LICENSE +201 -0
- package/README.md +340 -0
- package/agents/ai_engineer.md +113 -0
- package/agents/developer.md +236 -0
- package/agents/scrum-master.md +349 -0
- package/agents/scrutiny-agent.md +137 -0
- package/agents/solution-assessor.md +185 -0
- package/agents/tester.md +339 -0
- package/bin/jenga.js +70 -0
- package/hooks/copilot_session_end.sh +29 -0
- package/hooks/on_session_end.sh +238 -0
- package/hooks/prompt_router.sh +11 -0
- package/hooks/prompt_router_helper.js +52 -0
- package/hooks/session_end_helper.js +29 -0
- package/hooks/session_end_watcher.sh +24 -0
- package/lib/commands/attach.js +47 -0
- package/lib/commands/init.js +207 -0
- package/lib/commands/start.js +16 -0
- package/lib/commands/status.js +53 -0
- package/lib/config-schema.js +72 -0
- package/lib/inject-settings.js +61 -0
- package/lib/mirror.js +244 -0
- package/lib/resolve-project-dir.sh +47 -0
- package/mcp/execute-ticket/index.js +10 -0
- package/mcp/execute-ticket/package.json +5 -0
- package/mcp/help/index.js +79 -0
- package/mcp/help/package.json +14 -0
- package/mcp/router/README.md +19 -0
- package/mcp/router/embedder.js +23 -0
- package/mcp/router/index.js +204 -0
- package/mcp/router/matcher.js +87 -0
- package/mcp/router/package-lock.json +1048 -0
- package/mcp/router/package.json +11 -0
- package/mcp/router/skill-index.js +104 -0
- package/package.json +47 -0
- package/scripts/board_resolver.sh +46 -0
- package/scripts/e25_s01_extract_board_graph.py +292 -0
- package/scripts/e25_s01_generate_synthetic_board.py +90 -0
- package/scripts/measurement-10x.json +50 -0
- package/scripts/measurement-10x.txt +4 -0
- package/scripts/measurement-real.json +50 -0
- package/scripts/measurement-real.txt +4 -0
- package/scripts/postinstall.js +165 -0
- package/scripts/todo_cleanup.sh +22 -0
- package/scripts/todo_manager.sh +86 -0
- package/scripts/validate-board.sh +190 -0
- package/scripts/validate-story-format.sh +53 -0
- package/skills/brainstorm/SKILL.md +47 -0
- package/skills/btw/SKILL.md +42 -0
- package/skills/commit/SKILL.md +29 -0
- package/skills/commit/assets/user_instructions_template.md +22 -0
- package/skills/continue/SKILL.md +29 -0
- package/skills/convert/SKILL.md +124 -0
- package/skills/convert/convert_cli.py +235 -0
- package/skills/convert/tests/sample.csv +4 -0
- package/skills/convert/tests/sample.json +5 -0
- package/skills/convert/tests/sample.jsonl +3 -0
- package/skills/convert/tests/sample.yaml +18 -0
- package/skills/convert/tests/sample_obj.csv +2 -0
- package/skills/convert/tests/sample_obj.json +9 -0
- package/skills/deep-dive/SKILL.md +167 -0
- package/skills/do/SKILL.md +88 -0
- package/skills/do/assets/sender_template.json +12 -0
- package/skills/doc/SKILL.md +314 -0
- package/skills/doc/assets/path-objectives.yaml +38 -0
- package/skills/doc-sync/SKILL.md +167 -0
- package/skills/doc-sync/assets/default_excludes.txt +21 -0
- package/skills/doc-sync/assets/doc_targets.md +14 -0
- package/skills/dooo/SKILL.md +60 -0
- package/skills/error/SKILL.md +29 -0
- package/skills/evaluate/SKILL.md +45 -0
- package/skills/evaluate/assets/evaluation_invokation_template.yml +3 -0
- package/skills/evaluate/assets/evaluation_rapport_template.md +24 -0
- package/skills/examplify/SKILL.md +42 -0
- package/skills/help/SKILL.md +36 -0
- package/skills/improve/SKILL.md +55 -0
- package/skills/index/scripts/board-index +4 -0
- package/skills/index/scripts/board_index.py +615 -0
- package/skills/index/scripts/smoke_test.sh +86 -0
- package/skills/init/SKILL.md +44 -0
- package/skills/init/assets/.gitignore_template +15 -0
- package/skills/init/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/init/assets/directory_structure.txt +13 -0
- package/skills/init/assets/test-config_template.json +4 -0
- package/skills/init/assets/workflow_template.json +30 -0
- package/skills/init/scripts/init.sh +48 -0
- package/skills/jbp/SKILL.md +25 -0
- package/skills/jenga/SKILL.md +68 -0
- package/skills/lgtm/SKILL.md +21 -0
- package/skills/mirror-public/SKILL.md +237 -0
- package/skills/mirror-public/assets/config.json +5 -0
- package/skills/mirror-public/scripts/mirror.sh +374 -0
- package/skills/pi-plan/SKILL.md +62 -0
- package/skills/pi-plan/assets/epic.json +7 -0
- package/skills/pi-plan/assets/story_template.md +18 -0
- package/skills/proceed/SKILL.md +29 -0
- package/skills/publish/SKILL.md +351 -0
- package/skills/publish/adapters/droplet.md +200 -0
- package/skills/publish/adapters/mobile-ios.md +114 -0
- package/skills/publish/adapters/npm-ci.md +223 -0
- package/skills/publish/adapters/npm.md +121 -0
- package/skills/publish/assets/ExportOptions.plist.template +19 -0
- package/skills/publish/assets/ci-contract.md +111 -0
- package/skills/publish/assets/ownership-matrix.md +17 -0
- package/skills/publish/assets/publish.example.json +85 -0
- package/skills/publish/assets/publish.example.npm-ci.json +40 -0
- package/skills/publish/assets/publish.example.npm.json +41 -0
- package/skills/publish/assets/secrets-guide.md +104 -0
- package/skills/publish/schemas/fixtures/npm-ci-minimal.json +17 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-empty-secrets.json +18 -0
- package/skills/publish/schemas/fixtures/npm-ci-with-workflow-path.json +18 -0
- package/skills/publish/schemas/publish.schema.json +428 -0
- package/skills/publish/scripts/check_target_config.sh +96 -0
- package/skills/publish/scripts/droplet_pipeline.sh +208 -0
- package/skills/publish/scripts/generate_release_notes.sh +200 -0
- package/skills/publish/scripts/ios_pipeline.sh +486 -0
- package/skills/publish/scripts/npm_ci_pipeline.sh +225 -0
- package/skills/publish/scripts/npm_pipeline.sh +249 -0
- package/skills/publish/scripts/publish_common.sh +253 -0
- package/skills/publish/scripts/publish_deploy.sh +538 -0
- package/skills/publish/scripts/reconcile_tags.sh +135 -0
- package/skills/publish/scripts/run_gates.sh +616 -0
- package/skills/publish/scripts/setup_wizard.sh +394 -0
- package/skills/publish/scripts/show_history.sh +95 -0
- package/skills/publish/scripts/suggest_semver_bump.sh +105 -0
- package/skills/publish/scripts/validate_config.sh +163 -0
- package/skills/publish/scripts/validate_droplet_env.sh +45 -0
- package/skills/publish/scripts/validate_ios_env.sh +68 -0
- package/skills/publish/scripts/validate_npm_ci_env.sh +71 -0
- package/skills/publish/scripts/validate_npm_env.sh +22 -0
- package/skills/publish/scripts/write_ledger_entry.sh +126 -0
- package/skills/publish/wizards/droplet.md +275 -0
- package/skills/publish/wizards/mobile-ios.md +157 -0
- package/skills/publish/wizards/npm-ci.md +240 -0
- package/skills/publish/wizards/npm.md +224 -0
- package/skills/reconcile/SKILL.md +93 -0
- package/skills/reconcile/assets/report_format.md +44 -0
- package/skills/reconcile-origin/SKILL.md +75 -0
- package/skills/reconcile-origin/scripts/reconcile-origin.sh +372 -0
- package/skills/redo/SKILL.md +70 -0
- package/skills/route/SKILL.md +180 -0
- package/skills/self-sync/SKILL.md +73 -0
- package/skills/self-sync/scripts/run.js +136 -0
- package/skills/skillify/SKILL.md +68 -0
- package/skills/skillify/assets/init-new/SKILL.md +35 -0
- package/skills/skillify/assets/init-new/assets/.gitignore_template +15 -0
- package/skills/skillify/assets/init-new/assets/PROJECT_SUMMARY_template.md +13 -0
- package/skills/skillify/assets/init-new/assets/directory_structure.txt +10 -0
- package/skills/skillify/assets/init-new/assets/test-config_template.json +4 -0
- package/skills/skillify/assets/init-new/assets/workflow_template.json +17 -0
- package/skills/skillify/assets/init-new/scripts/init.sh +48 -0
- package/skills/skillify/assets/init-old/SKILL.md +124 -0
- package/skills/spinoff/SKILL.md +48 -0
- package/skills/status/SKILL.md +33 -0
- package/skills/status/assets/output_format.md +41 -0
- package/skills/todo/SKILL.md +46 -0
- package/skills/todo/assets/todo_handoff_template.md +22 -0
- package/skills/todo/assets/todo_template.md +3 -0
- package/skills/train/SKILL.md +116 -0
- package/skills/train/assets/dashboard-templates/classifiers.html +106 -0
- package/skills/train/assets/dashboard-templates/nlp.html +102 -0
- package/skills/train/assets/dashboard-templates/transformers.html +98 -0
- package/skills/train/assets/results-parsers/__init__.py +9 -0
- package/skills/train/assets/results-parsers/classifiers.py +84 -0
- package/skills/train/assets/results-parsers/nlp.py +88 -0
- package/skills/train/assets/results-parsers/reporter.py +154 -0
- package/skills/train/assets/results-parsers/transformers.py +120 -0
- package/skills/train/train_cli.py +786 -0
- package/templates/EXECUTION_PLAN_TEMPLATE.md +43 -0
- package/templates/EXECUTION_SUMMARY_TEMPLATE.md +50 -0
- package/templates/JENGA_CONFIG_TEMPLATE.json +23 -0
- package/templates/PROBLEM_RAPPORT_TEMPLATE.md +88 -0
- package/templates/SCRUM_BOARD_SCHEMA.md +311 -0
- package/templates/SKILL.md +16 -0
- package/templates/SKILL_TEMPLATE.md +28 -0
- package/templates/USER_INSTRUCTIONS_TEMPLATE.md +22 -0
- package/templates/copilot-instructions.md.tpl +55 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# `npm-ci` Adapter
|
|
2
|
+
|
|
3
|
+
The `npm-ci` adapter drives the `/publish deploy` flow for npm package targets.
|
|
4
|
+
It publishes to npmjs.com via **GitHub Actions OIDC Trusted Publishers** — no
|
|
5
|
+
`NPM_TOKEN` is stored anywhere. The adapter generates a GitHub Actions workflow
|
|
6
|
+
that runs `npm publish --provenance`, commits it to the repository, and triggers
|
|
7
|
+
it via `gh workflow run`. The local machine never publishes directly to npm.
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
Publish an npm package from a GitHub repository to npmjs.com by:
|
|
12
|
+
|
|
13
|
+
1. Generating a GitHub Actions workflow that authenticates to npm using a
|
|
14
|
+
short-lived OIDC token issued by GitHub's token service.
|
|
15
|
+
2. Committing the workflow file to `<workflow_path>` (default:
|
|
16
|
+
`.github/workflows/npm-publish.yml`) in the repository.
|
|
17
|
+
3. Triggering the workflow via `gh workflow run`.
|
|
18
|
+
|
|
19
|
+
No `NPM_TOKEN` is created, stored, or referenced. All authorisation is handled
|
|
20
|
+
through the npmjs.com Trusted Publisher link established between the package and
|
|
21
|
+
the repository.
|
|
22
|
+
|
|
23
|
+
## Trust Boundary
|
|
24
|
+
|
|
25
|
+
`publish.json` holds **only** configuration values — no secrets of any kind.
|
|
26
|
+
The trust chain is OIDC-based:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
publish.json GitHub Actions npmjs.com
|
|
30
|
+
────────────── ────────────────────────── ─────────────────────────────
|
|
31
|
+
github_repo: "owner/name" ← OIDC token issued per run ← Trusted Publisher link
|
|
32
|
+
npm.package_name: "pkg" scoped to this package (package ↔ repo + workflow)
|
|
33
|
+
npm.access: "public"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
GitHub's token service issues a short-lived OIDC token for each workflow run.
|
|
37
|
+
The npmjs.com Trusted Publisher configuration — a one-time link between the
|
|
38
|
+
package name and the repository + workflow file path — is the sole source of
|
|
39
|
+
authorisation. Once that link is in place, any workflow run originating from
|
|
40
|
+
the linked repository and workflow path may publish the package without a stored
|
|
41
|
+
token.
|
|
42
|
+
|
|
43
|
+
No secret values are committed to `publish.json` or the repository. Any
|
|
44
|
+
`secrets` block present in the target config is ignored by this adapter.
|
|
45
|
+
|
|
46
|
+
## Invocation Contract
|
|
47
|
+
|
|
48
|
+
### Inputs from `publish.json`
|
|
49
|
+
|
|
50
|
+
An `npm-ci` target must define:
|
|
51
|
+
|
|
52
|
+
- `name`
|
|
53
|
+
- `type: npm-ci`
|
|
54
|
+
- `npm.package_name` — name of the npm package to publish
|
|
55
|
+
- `npm.access` — `"public"` or `"restricted"`
|
|
56
|
+
- `npm.registry` — registry URL (default: `https://registry.npmjs.org`)
|
|
57
|
+
- `npm.dist_tag` — npm dist-tag (e.g. `latest`, `beta`)
|
|
58
|
+
- `github_repo` — **required**; `owner/name` identifying the GitHub repository
|
|
59
|
+
- `workflow_path` — **optional**; path to the workflow file to generate and
|
|
60
|
+
trigger; defaults to `.github/workflows/npm-publish.yml`
|
|
61
|
+
- `secrets` — **not required**; any `secrets` block present is ignored by this
|
|
62
|
+
adapter
|
|
63
|
+
|
|
64
|
+
The `npm.*` fields are read through the shared `npmSettings` schema, reused
|
|
65
|
+
across all npm-targeting adapters.
|
|
66
|
+
|
|
67
|
+
### Required environment and tools
|
|
68
|
+
|
|
69
|
+
The adapter requires the following tools at invocation time:
|
|
70
|
+
|
|
71
|
+
- `jq` — to read `publish.json` target config
|
|
72
|
+
- `git` — to commit the generated workflow file
|
|
73
|
+
- `gh` — GitHub CLI, authenticated via `gh auth login`, to trigger the workflow
|
|
74
|
+
via `gh workflow run`
|
|
75
|
+
|
|
76
|
+
The `gh` CLI must be authenticated before running `/publish deploy` against an
|
|
77
|
+
`npm-ci` target.
|
|
78
|
+
|
|
79
|
+
### Pipeline entrypoint
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bash skills/publish/scripts/npm_ci_pipeline.sh \
|
|
83
|
+
--target <name> \
|
|
84
|
+
--config <path-to-publish.json> \
|
|
85
|
+
[--dry-run]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Execution Phases
|
|
89
|
+
|
|
90
|
+
Run the adapter phases in this exact order:
|
|
91
|
+
|
|
92
|
+
1. **`validate`** — verify that `gh` CLI is authenticated (`gh auth status`),
|
|
93
|
+
that `github_repo` resolves to an accessible repository, and that all
|
|
94
|
+
required `npm.*` fields (`package_name`, `access`, `registry`, `dist_tag`)
|
|
95
|
+
are present and non-empty. Abort with exit 4 on any failure.
|
|
96
|
+
|
|
97
|
+
2. **`generate-workflow`** — render the GitHub Actions workflow YAML in memory.
|
|
98
|
+
The generated workflow sets:
|
|
99
|
+
```yaml
|
|
100
|
+
permissions:
|
|
101
|
+
id-token: write
|
|
102
|
+
contents: read
|
|
103
|
+
```
|
|
104
|
+
and runs `npm publish --provenance` to attach a build provenance attestation
|
|
105
|
+
to the published package. In `--dry-run` mode, print the rendered YAML to
|
|
106
|
+
stdout and exit 0 without writing any file.
|
|
107
|
+
|
|
108
|
+
3. **`commit-workflow`** — write the generated YAML to `<workflow_path>`
|
|
109
|
+
(creating the `.github/workflows/` directory if needed) and commit it with:
|
|
110
|
+
```
|
|
111
|
+
git add <workflow_path>
|
|
112
|
+
git commit -m "chore(publish): update npm CI workflow for target <name>"
|
|
113
|
+
```
|
|
114
|
+
If the file already exists and the content is unchanged (`git diff --quiet`),
|
|
115
|
+
skip the commit — the operation is idempotent.
|
|
116
|
+
|
|
117
|
+
4. **`trigger`** — invoke:
|
|
118
|
+
```bash
|
|
119
|
+
gh workflow run <workflow_filename> --ref <branch>
|
|
120
|
+
```
|
|
121
|
+
where `<workflow_filename>` is the basename of `<workflow_path>`. After
|
|
122
|
+
dispatching, retrieve the run URL via
|
|
123
|
+
`gh run list --workflow <workflow_filename> --limit 1 --json url`.
|
|
124
|
+
|
|
125
|
+
5. **`triggered`** — print the workflow run URL to stdout. The caller
|
|
126
|
+
(`publish_deploy.sh`) writes the ledger entry with
|
|
127
|
+
`platform_state: "triggered"`.
|
|
128
|
+
|
|
129
|
+
## State Machine
|
|
130
|
+
|
|
131
|
+
```
|
|
132
|
+
start
|
|
133
|
+
│
|
|
134
|
+
▼
|
|
135
|
+
validate ──── exit 4 (config/env invalid)
|
|
136
|
+
│
|
|
137
|
+
▼
|
|
138
|
+
generate-workflow ──── [--dry-run] → print YAML → exit 0
|
|
139
|
+
│
|
|
140
|
+
▼
|
|
141
|
+
commit-workflow
|
|
142
|
+
│
|
|
143
|
+
▼
|
|
144
|
+
trigger ──── exit 3 (gh workflow run failed)
|
|
145
|
+
│
|
|
146
|
+
▼
|
|
147
|
+
triggered → exit 0 (success)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Terminal states:
|
|
151
|
+
|
|
152
|
+
- **success** — all five phases complete; workflow run URL printed; exit 0.
|
|
153
|
+
- **dry-run exit** — adapter exits after `generate-workflow` without writing,
|
|
154
|
+
committing, or triggering; exit 0.
|
|
155
|
+
- **failure** — adapter exits with a non-zero code at the failing phase (see
|
|
156
|
+
Exit Codes).
|
|
157
|
+
|
|
158
|
+
## Dry-Run Behaviour
|
|
159
|
+
|
|
160
|
+
When `--dry-run` is passed:
|
|
161
|
+
|
|
162
|
+
- The `validate` phase runs normally; config and environment errors still
|
|
163
|
+
surface.
|
|
164
|
+
- The `generate-workflow` phase runs normally; the rendered workflow YAML is
|
|
165
|
+
printed to stdout.
|
|
166
|
+
- The `commit-workflow` phase is **skipped** — no file is written, no git
|
|
167
|
+
commit is made.
|
|
168
|
+
- The `trigger` phase is **skipped** — `gh workflow run` is not called.
|
|
169
|
+
- The adapter exits 0 after printing the YAML.
|
|
170
|
+
- The ledger entry written by `publish_deploy.sh` will have
|
|
171
|
+
`platform_state: "dry-run"`.
|
|
172
|
+
|
|
173
|
+
## Exit Codes
|
|
174
|
+
|
|
175
|
+
| Code | Meaning |
|
|
176
|
+
|------|-----------------------------------------------------------------------------|
|
|
177
|
+
| `0` | Pipeline completed successfully, or `--dry-run` finished cleanly |
|
|
178
|
+
| `3` | Workflow trigger failure (`gh workflow run` exited non-zero) |
|
|
179
|
+
| `4` | Config or environment invalid (missing required field, `gh` not authed, |
|
|
180
|
+
| | `github_repo` not found, or required `npm.*` field absent) |
|
|
181
|
+
|
|
182
|
+
## Output Artefacts
|
|
183
|
+
|
|
184
|
+
A successful non-dry-run produces:
|
|
185
|
+
|
|
186
|
+
- The workflow file written to `<workflow_path>` in the working tree (default:
|
|
187
|
+
`.github/workflows/npm-publish.yml`) and committed to the repository
|
|
188
|
+
- A git commit: `chore(publish): update npm CI workflow for target <name>`
|
|
189
|
+
- A workflow run URL printed to stdout
|
|
190
|
+
- A history entry in `project/logs/publish-history.json` written by
|
|
191
|
+
`publish_deploy.sh` with `platform_state: "triggered"`
|
|
192
|
+
|
|
193
|
+
## Post-Deploy Manual Steps
|
|
194
|
+
|
|
195
|
+
After a successful deploy trigger, the adapter prints the workflow run URL.
|
|
196
|
+
The operator should:
|
|
197
|
+
|
|
198
|
+
1. Open the printed run URL to monitor the publish job in GitHub Actions.
|
|
199
|
+
2. Verify the package appears on npmjs.com with the expected version and
|
|
200
|
+
provenance badge (the shield icon confirming `npm publish --provenance`).
|
|
201
|
+
3. Confirm the dist-tag (`latest`, `beta`, or other) is correctly applied on
|
|
202
|
+
the npmjs.com package page.
|
|
203
|
+
|
|
204
|
+
The adapter does not wait for the CI run to complete — monitoring is the
|
|
205
|
+
operator's responsibility.
|
|
206
|
+
|
|
207
|
+
## First-run prerequisite
|
|
208
|
+
|
|
209
|
+
The npmjs.com Trusted Publisher link between the package and the repository must
|
|
210
|
+
be established **before** the first `/publish deploy` invocation against an
|
|
211
|
+
`npm-ci` target. This one-time setup step is out of scope for the adapter itself
|
|
212
|
+
and is covered by the setup wizard (see S03).
|
|
213
|
+
|
|
214
|
+
Minimum pre-requisites:
|
|
215
|
+
|
|
216
|
+
- The package must already exist on npmjs.com (or be publishable under your
|
|
217
|
+
npm account).
|
|
218
|
+
- A Trusted Publisher must be configured on npmjs.com linking the package to
|
|
219
|
+
`github_repo` + `workflow_path`.
|
|
220
|
+
- The GitHub repository must have Actions enabled.
|
|
221
|
+
|
|
222
|
+
Once the Trusted Publisher link is in place, the adapter can publish on every
|
|
223
|
+
invocation without any stored token.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# `npm` Adapter
|
|
2
|
+
|
|
3
|
+
The `npm` adapter drives the `/publish deploy` flow for npm registry targets.
|
|
4
|
+
It is a prompt/template contract for the agent layer and delegates concrete execution to
|
|
5
|
+
`skills/publish/scripts/npm_pipeline.sh`.
|
|
6
|
+
|
|
7
|
+
## Invocation Contract
|
|
8
|
+
|
|
9
|
+
### Inputs from `publish.json`
|
|
10
|
+
|
|
11
|
+
An `npm` target must define:
|
|
12
|
+
|
|
13
|
+
- `name`
|
|
14
|
+
- `type: npm`
|
|
15
|
+
- `platform: npm-registry`
|
|
16
|
+
- `checks.pre[]` / `checks.post[]`
|
|
17
|
+
- `secrets.NPM_TOKEN` (env-var reference; `NODE_AUTH_TOKEN` is also accepted)
|
|
18
|
+
- `npm.package_name`
|
|
19
|
+
- `npm.access` (`public` or `restricted`)
|
|
20
|
+
- `npm.registry` (optional; defaults to `https://registry.npmjs.org`)
|
|
21
|
+
- `npm.dist_tag` (optional; defaults to `latest`)
|
|
22
|
+
|
|
23
|
+
### Required environment variables
|
|
24
|
+
|
|
25
|
+
The adapter uses env-var references only and never stores secret values in repo files.
|
|
26
|
+
The required env vars are:
|
|
27
|
+
|
|
28
|
+
- `NPM_TOKEN` (or `NODE_AUTH_TOKEN` — either satisfies the auth requirement)
|
|
29
|
+
|
|
30
|
+
Validate them before execution with:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bash skills/publish/scripts/validate_npm_env.sh <path-to-publish.json>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Pipeline entrypoint
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bash skills/publish/scripts/npm_pipeline.sh <target> <path-to-publish.json> [--dry-run] [--non-interactive]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Execution Phases
|
|
43
|
+
|
|
44
|
+
Run the adapter phases in this exact order:
|
|
45
|
+
|
|
46
|
+
1. `validate`
|
|
47
|
+
2. `gates`
|
|
48
|
+
3. `pack`
|
|
49
|
+
4. `publish`
|
|
50
|
+
5. `tag`
|
|
51
|
+
|
|
52
|
+
Quality gates run inside the `gates` phase and must include `npm test`. If the consumer `package.json` declares a `build` script, `npm run build` also runs as a gate. Once control enters the adapter, external publish-side commands are restricted to direct `npm` and `git` invocations.
|
|
53
|
+
|
|
54
|
+
## State Machine
|
|
55
|
+
|
|
56
|
+
The adapter records these transitions in `project/logs/publish-history.json`:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
idle → validating → gating → packing → publishing → tagging → published
|
|
60
|
+
└───────────────→ failed
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### State meanings
|
|
64
|
+
|
|
65
|
+
- `idle` — conceptual pre-start state before a history row is appended
|
|
66
|
+
- `validating` — token presence and `publish.json` npm block are being verified
|
|
67
|
+
- `gating` — `npm test` (and `npm run build` when present) are running
|
|
68
|
+
- `packing` — `npm pack` is generating the tarball for inspection
|
|
69
|
+
- `publishing` — `npm publish` is running against the configured `registry` with the resolved `access` and `dist_tag`
|
|
70
|
+
- `tagging` — `git tag v<version>` is being created and (optionally) pushed
|
|
71
|
+
- `published` — publish and tag both succeeded; package is visible on the registry
|
|
72
|
+
- `failed` — any validation/gate/publish/tag failure; `completed_at` must be set
|
|
73
|
+
|
|
74
|
+
## Dry-run Mode
|
|
75
|
+
|
|
76
|
+
`--dry-run` is mandatory for safe CI validation without live npm credentials.
|
|
77
|
+
|
|
78
|
+
Behavior:
|
|
79
|
+
|
|
80
|
+
- The `publish` phase runs `npm publish --dry-run` instead of a live publish. No package is written to the registry; npm prints the tarball contents and would-be metadata.
|
|
81
|
+
- All `git tag` / `git push` commands are printed with a `[DRY RUN]` prefix and are **not** executed.
|
|
82
|
+
- State-machine writes still happen so reviewers can inspect the planned flow.
|
|
83
|
+
- Token presence is still validated so dry-runs surface auth misconfiguration early.
|
|
84
|
+
- The adapter exits `0` after simulating a successful publish.
|
|
85
|
+
|
|
86
|
+
## Exit Codes
|
|
87
|
+
|
|
88
|
+
| Code | Meaning |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `0` | Pipeline completed successfully or dry-run finished successfully |
|
|
91
|
+
| `2` | Missing `NPM_TOKEN` / `NODE_AUTH_TOKEN`, config validation failure, or quality-gate failure (`npm test` / `npm run build`) |
|
|
92
|
+
| `3` | `npm publish` failure, `git tag` failure, or history-update failure |
|
|
93
|
+
|
|
94
|
+
### Success condition
|
|
95
|
+
|
|
96
|
+
`npm publish` exits `0` and the package is retrievable from the configured registry at the newly-published `version` and `dist_tag`.
|
|
97
|
+
|
|
98
|
+
### Failure conditions
|
|
99
|
+
|
|
100
|
+
- **Missing token** — `NPM_TOKEN` (and `NODE_AUTH_TOKEN`) unset → exit `2`
|
|
101
|
+
- **Gate failure** — `npm test` or `npm run build` non-zero → exit `2`
|
|
102
|
+
- **Publish error** — `npm publish` non-zero (network, 403, version conflict, etc.) → exit `3`
|
|
103
|
+
|
|
104
|
+
## Output Artefacts
|
|
105
|
+
|
|
106
|
+
A successful run produces:
|
|
107
|
+
|
|
108
|
+
- A published package on the configured npm registry (npmjs.com by default), reachable at `https://www.npmjs.com/package/<package_name>/v/<version>`
|
|
109
|
+
- A local (and optionally pushed) git tag `v<version>` matching the published `version`
|
|
110
|
+
- A history row appended to `project/logs/publish-history.json`
|
|
111
|
+
|
|
112
|
+
## Post-deploy manual steps
|
|
113
|
+
|
|
114
|
+
After a successful publish, the deploy flow must print:
|
|
115
|
+
|
|
116
|
+
✅ Publish complete. Next manual steps:
|
|
117
|
+
1. Verify the package on the registry: https://www.npmjs.com/package/<package_name>/v/<version>
|
|
118
|
+
2. Push the git tag if not already pushed: `git push origin v<version>`
|
|
119
|
+
3. Draft release notes (GitHub Releases or your changelog of choice)
|
|
120
|
+
|
|
121
|
+
Print the same block in `--non-interactive` mode so CI logs capture it.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>method</key>
|
|
6
|
+
<string>{{EXPORT_METHOD}}</string>
|
|
7
|
+
<key>teamID</key>
|
|
8
|
+
<string>{{TEAM_ID}}</string>
|
|
9
|
+
<key>signingStyle</key>
|
|
10
|
+
<string>manual</string>
|
|
11
|
+
<key>signingCertificate</key>
|
|
12
|
+
<string>{{SIGNING_IDENTITY}}</string>
|
|
13
|
+
<key>provisioningProfiles</key>
|
|
14
|
+
<dict>
|
|
15
|
+
<key>{{BUNDLE_ID}}</key>
|
|
16
|
+
<string>{{PROVISIONING_PROFILE_UUID}}</string>
|
|
17
|
+
</dict>
|
|
18
|
+
</dict>
|
|
19
|
+
</plist>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# `/publish` deploy contract
|
|
2
|
+
|
|
3
|
+
This document defines the CI-safe execution contract for the scaffolded `/publish` workflow. Interactive flows added later must wrap this contract and must not weaken it.
|
|
4
|
+
|
|
5
|
+
## Validation order
|
|
6
|
+
|
|
7
|
+
Every `/publish` invocation must perform these steps before command-specific work:
|
|
8
|
+
|
|
9
|
+
1. Resolve config path (`--config <path>` or default `project/configs/publish.json`)
|
|
10
|
+
2. Run `bash skills/publish/scripts/validate_config.sh <resolved-path>`
|
|
11
|
+
3. Abort immediately with exit code `4` if the file is missing, invalid JSON, or fails validation
|
|
12
|
+
4. For the iOS adapter, run `bash skills/publish/scripts/validate_ios_env.sh <resolved-path>` before build/upload work begins
|
|
13
|
+
|
|
14
|
+
## Inputs
|
|
15
|
+
|
|
16
|
+
| Purpose | Flag | Environment Variable | Required in non-interactive mode? | Notes |
|
|
17
|
+
|---|---|---|---|---|
|
|
18
|
+
| Select deploy target | `--target <name>` | `PUBLISH_TARGET` | Yes | Flag takes precedence over env var |
|
|
19
|
+
| Skip confirmation prompts | `--yes` | `PUBLISH_YES` | Yes | Env var truthy values: `1`, `true`, `yes`, `on` |
|
|
20
|
+
| Override config path | `--config <path>` | — | No | Defaults to `project/configs/publish.json` |
|
|
21
|
+
| Reuse prepared release notes | `--release-notes <path>` | — | No | Reserved for later release-note flow |
|
|
22
|
+
| Dry-run execution | `--dry-run` | `PUBLISH_DRY_RUN` | No | Prints allowlisted `xcodebuild` / `xcrun` commands without executing them |
|
|
23
|
+
|
|
24
|
+
## Required contract
|
|
25
|
+
|
|
26
|
+
For a CI or agent-driven deploy, the caller must provide:
|
|
27
|
+
|
|
28
|
+
- a valid target via `--target` or `PUBLISH_TARGET`
|
|
29
|
+
- confirmation bypass via `--yes` or truthy `PUBLISH_YES`
|
|
30
|
+
- a valid config file at the provided path or the default path
|
|
31
|
+
- the required iOS credential environment variables when the `mobile-ios` adapter is used
|
|
32
|
+
|
|
33
|
+
If any of these are missing in a non-interactive execution context, the command must fail fast rather than prompting.
|
|
34
|
+
|
|
35
|
+
## Mandatory global pre-deploy gates
|
|
36
|
+
|
|
37
|
+
`build` and `test` are mandatory global gates for `/publish deploy`.
|
|
38
|
+
|
|
39
|
+
- They run first on every `pre` gate pass.
|
|
40
|
+
- They cannot be disabled, removed, or overridden by target configuration.
|
|
41
|
+
- If `publish.json` also lists `build` or `test` under target-specific pre-gates, the runner ignores those duplicates and keeps the mandatory global versions.
|
|
42
|
+
- If either mandatory gate fails, the gate runner surfaces the full command output and exits with code `2` without continuing to later gates.
|
|
43
|
+
|
|
44
|
+
## Configurable target gates
|
|
45
|
+
|
|
46
|
+
Per-target gates may add optional checks such as:
|
|
47
|
+
|
|
48
|
+
- `lint`
|
|
49
|
+
- `type-check`
|
|
50
|
+
- `custom-script`
|
|
51
|
+
- `smoke-test`
|
|
52
|
+
- `ping`
|
|
53
|
+
|
|
54
|
+
These run only after the mandatory global gates succeed.
|
|
55
|
+
|
|
56
|
+
## Optional contract
|
|
57
|
+
|
|
58
|
+
These inputs remain optional in non-interactive mode:
|
|
59
|
+
|
|
60
|
+
- `--config <path>` when the default config exists
|
|
61
|
+
- `--release-notes <path>`
|
|
62
|
+
- `--from-tag` and `--to-ref` on `/publish release-notes`
|
|
63
|
+
|
|
64
|
+
## Exit codes
|
|
65
|
+
|
|
66
|
+
| Code | Meaning | Current story coverage |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `0` | Success | Config/env validation succeeds, dry-runs complete, and later deploy steps finish |
|
|
69
|
+
| `1` | User abort | Reserved for interactive confirmation flow |
|
|
70
|
+
| `2` | Quality gate failure | Implemented by the gate runner |
|
|
71
|
+
| `3` | Deploy failure | Used by the iOS adapter pipeline for build/export/upload failures |
|
|
72
|
+
| `4` | Config or environment invalid | Implemented by validation scripts |
|
|
73
|
+
|
|
74
|
+
## Precedence rules
|
|
75
|
+
|
|
76
|
+
1. Command-line flags win over environment variables.
|
|
77
|
+
2. Environment variables are only fallbacks where explicitly supported.
|
|
78
|
+
3. Missing/invalid config always stops execution before target resolution or deploy steps.
|
|
79
|
+
4. Missing required iOS env vars stop execution before any `xcodebuild` or `xcrun` command runs.
|
|
80
|
+
|
|
81
|
+
## Interactive vs non-interactive behavior
|
|
82
|
+
|
|
83
|
+
- Interactive mode offers: `[r] Retry after fixing / [a] Abort deploy`
|
|
84
|
+
- Retries are capped at 3 attempts per failed gate
|
|
85
|
+
- `--non-interactive` disables prompts and aborts immediately on any gate failure
|
|
86
|
+
- Every gate failure is appended to `project/logs/publish-history.json`
|
|
87
|
+
- `--dry-run` still records adapter state transitions so the planned pipeline can be reviewed safely
|
|
88
|
+
|
|
89
|
+
## Example CI invocations
|
|
90
|
+
|
|
91
|
+
### Flag-driven
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
publish deploy --target staging-appstore --yes --dry-run
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Mixed flag + env var
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
PUBLISH_YES=true publish deploy --target staging-appstore --dry-run
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Fully env-driven target selection
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
PUBLISH_TARGET=staging-appstore PUBLISH_YES=1 publish deploy --dry-run
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Scope notes
|
|
110
|
+
|
|
111
|
+
This story set does not perform live Apple API calls during verification. The iOS adapter is structured around allowlisted `xcodebuild` and `xcrun` commands and relies on caller-provided credentials and a local Apple toolchain.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Publish Agent Ownership Matrix
|
|
2
|
+
|
|
3
|
+
The `/publish` skill is initiated by the operator running the CLI, but each action has a defined workflow owner inside the multi-agent system.
|
|
4
|
+
|
|
5
|
+
| Action | Developer | Tester | Scrum Master |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| `/publish setup` | Initiates and owns | May run to configure test targets | Never |
|
|
8
|
+
| `/publish deploy --target staging` | Initiates | May run as part of test cycle | Never directly |
|
|
9
|
+
| `/publish deploy --target production` | Initiates with explicit confirmation | Reviews ledger entry post-deploy | Reviews `/status` only |
|
|
10
|
+
| `/publish history` | Reads for context | Reads for test baseline | Reads in `/status` review |
|
|
11
|
+
| `/publish release-notes` | Generates and reviews draft | May read draft for test context | Reviews content for sprint summary |
|
|
12
|
+
| Gate failure resolution | Owns rework | Validates rework | Escalates if blocked |
|
|
13
|
+
| Ledger entry disputes | Never modifies | Never modifies | Flags as rapport; human resolves |
|
|
14
|
+
|
|
15
|
+
## Trust boundary
|
|
16
|
+
|
|
17
|
+
The `mobile-ios` adapter may only execute direct `xcodebuild` and `xcrun` commands. Arbitrary shell commands are not permitted inside the adapter pipeline.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaults": {
|
|
4
|
+
"history_file": "project/logs/publish-history.json",
|
|
5
|
+
"mandatory_checks": [
|
|
6
|
+
"build",
|
|
7
|
+
"test"
|
|
8
|
+
],
|
|
9
|
+
"optional_checks": [
|
|
10
|
+
"lint",
|
|
11
|
+
"type-check",
|
|
12
|
+
"custom-script",
|
|
13
|
+
"smoke-test",
|
|
14
|
+
"ping"
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
"targets": [
|
|
18
|
+
{
|
|
19
|
+
"name": "staging-appstore",
|
|
20
|
+
"type": "mobile-ios",
|
|
21
|
+
"platform": "ios-app-store",
|
|
22
|
+
"checks": {
|
|
23
|
+
"pre": [
|
|
24
|
+
"lint",
|
|
25
|
+
"type-check"
|
|
26
|
+
],
|
|
27
|
+
"post": [
|
|
28
|
+
"smoke-test"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"secrets": {
|
|
32
|
+
"app_store_connect_api_key_id": "$APP_STORE_CONNECT_API_KEY_ID",
|
|
33
|
+
"app_store_connect_issuer_id": "$APP_STORE_CONNECT_ISSUER_ID",
|
|
34
|
+
"app_store_connect_private_key_path": "$APP_STORE_CONNECT_PRIVATE_KEY_PATH",
|
|
35
|
+
"code_sign_identity": "$CODE_SIGN_IDENTITY",
|
|
36
|
+
"provisioning_profile_uuid": "$PROVISIONING_PROFILE_UUID"
|
|
37
|
+
},
|
|
38
|
+
"ios": {
|
|
39
|
+
"project_path": "ios/ExampleApp.xcodeproj",
|
|
40
|
+
"scheme": "ExampleApp",
|
|
41
|
+
"configuration": "Release",
|
|
42
|
+
"archive_path": "build/ios/staging/ExampleApp.xcarchive",
|
|
43
|
+
"export_path": "build/ios/staging/export",
|
|
44
|
+
"export_method": "ad-hoc",
|
|
45
|
+
"bundle_id": "com.example.myapp.staging",
|
|
46
|
+
"team_id": "ABCDE12345",
|
|
47
|
+
"app_store_app_id": "1234567890",
|
|
48
|
+
"provider_short_name": "exampleco"
|
|
49
|
+
},
|
|
50
|
+
"notes": "Scaffold example only. Replace paths and identifiers with your real iOS project values."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "production-appstore",
|
|
54
|
+
"type": "mobile-ios",
|
|
55
|
+
"platform": "ios-app-store",
|
|
56
|
+
"checks": {
|
|
57
|
+
"pre": [
|
|
58
|
+
"lint",
|
|
59
|
+
"type-check"
|
|
60
|
+
],
|
|
61
|
+
"post": []
|
|
62
|
+
},
|
|
63
|
+
"secrets": {
|
|
64
|
+
"app_store_connect_api_key_id": "$APP_STORE_CONNECT_API_KEY_ID",
|
|
65
|
+
"app_store_connect_issuer_id": "$APP_STORE_CONNECT_ISSUER_ID",
|
|
66
|
+
"app_store_connect_private_key_path": "$APP_STORE_CONNECT_PRIVATE_KEY_PATH",
|
|
67
|
+
"code_sign_identity": "$CODE_SIGN_IDENTITY",
|
|
68
|
+
"provisioning_profile_uuid": "$PROVISIONING_PROFILE_UUID"
|
|
69
|
+
},
|
|
70
|
+
"ios": {
|
|
71
|
+
"workspace_path": "ios/ExampleApp.xcworkspace",
|
|
72
|
+
"scheme": "ExampleApp",
|
|
73
|
+
"configuration": "Release",
|
|
74
|
+
"archive_path": "build/ios/production/ExampleApp.xcarchive",
|
|
75
|
+
"export_path": "build/ios/production/export",
|
|
76
|
+
"export_method": "app-store",
|
|
77
|
+
"bundle_id": "com.example.myapp",
|
|
78
|
+
"team_id": "ABCDE12345",
|
|
79
|
+
"app_store_app_id": "1234567890",
|
|
80
|
+
"provider_short_name": "exampleco"
|
|
81
|
+
},
|
|
82
|
+
"notes": "Production upload example using App Store export mode."
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaults": {
|
|
4
|
+
"history_file": "project/logs/publish-history.json",
|
|
5
|
+
"mandatory_checks": [
|
|
6
|
+
"build",
|
|
7
|
+
"test"
|
|
8
|
+
],
|
|
9
|
+
"optional_checks": [
|
|
10
|
+
"lint",
|
|
11
|
+
"type-check",
|
|
12
|
+
"smoke-test"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"targets": [
|
|
16
|
+
{
|
|
17
|
+
"name": "npm-ci-jenga-agent",
|
|
18
|
+
"type": "npm-ci",
|
|
19
|
+
"platform": "npm-ci-oidc",
|
|
20
|
+
"github_repo": "samwelmunga/jenga-npm",
|
|
21
|
+
"workflow_path": ".github/workflows/npm-publish.yml",
|
|
22
|
+
"checks": {
|
|
23
|
+
"pre": [
|
|
24
|
+
"lint",
|
|
25
|
+
"type-check"
|
|
26
|
+
],
|
|
27
|
+
"post": [
|
|
28
|
+
"smoke-test"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
31
|
+
"npm": {
|
|
32
|
+
"package_name": "jenga-agent",
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org",
|
|
35
|
+
"dist_tag": "latest"
|
|
36
|
+
},
|
|
37
|
+
"notes": "Scaffold example for a CI-only npm publish via GitHub Actions OIDC (Trusted Publishers). No NPM_TOKEN is required — npmjs.com authenticates the publish using a short-lived OIDC token issued by GitHub's token service. The one-time setup step is: on npmjs.com, go to the package Settings → Publishing → Trusted Publishers → Add publisher → GitHub Actions → enter github_repo and workflow_path → Save. Once configured, only the linked workflow can publish this package — local npm publish with a token will be rejected."
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"defaults": {
|
|
4
|
+
"history_file": "project/logs/publish-history.json",
|
|
5
|
+
"mandatory_checks": [
|
|
6
|
+
"build",
|
|
7
|
+
"test"
|
|
8
|
+
],
|
|
9
|
+
"optional_checks": [
|
|
10
|
+
"lint",
|
|
11
|
+
"type-check",
|
|
12
|
+
"smoke-test"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"targets": [
|
|
16
|
+
{
|
|
17
|
+
"name": "npm-public",
|
|
18
|
+
"type": "npm",
|
|
19
|
+
"platform": "npm-registry",
|
|
20
|
+
"checks": {
|
|
21
|
+
"pre": [
|
|
22
|
+
"lint",
|
|
23
|
+
"type-check"
|
|
24
|
+
],
|
|
25
|
+
"post": [
|
|
26
|
+
"smoke-test"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"secrets": {
|
|
30
|
+
"NPM_TOKEN": "$NPM_TOKEN"
|
|
31
|
+
},
|
|
32
|
+
"npm": {
|
|
33
|
+
"package_name": "jenga-agent",
|
|
34
|
+
"access": "public",
|
|
35
|
+
"registry": "https://registry.npmjs.org",
|
|
36
|
+
"dist_tag": "latest"
|
|
37
|
+
},
|
|
38
|
+
"notes": "Scaffold example for a public npm registry publish. Field guide: `package_name` = the name that will appear in the registry (supports @scope/name); `access` = \"public\" for openly-installable packages or \"restricted\" for private packages; `registry` = target registry URL (default https://registry.npmjs.org, override for private registries or GitHub Packages); `dist_tag` = npm dist-tag applied to this publish (default \"latest\", use \"next\" or \"beta\" for pre-releases). The `secrets.NPM_TOKEN` entry maps to the `NPM_TOKEN` env var loaded at publish time — never hard-code the token value here."
|
|
39
|
+
}
|
|
40
|
+
]
|
|
41
|
+
}
|