@mmerterden/multi-agent-pipeline 20.9.0 → 20.11.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 (28) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/docs/facts.json +5 -4
  3. package/install/catalog-history.json +1 -1
  4. package/manifest.json +31 -28
  5. package/package.json +1 -1
  6. package/pipeline/commands/multi-agent/SKILL.md +1 -1
  7. package/pipeline/commands/multi-agent/help/SKILL.md +2 -2
  8. package/pipeline/commands/multi-agent/setup/SKILL.md +12 -13
  9. package/pipeline/commands/multi-agent/store-ready/SKILL.md +63 -19
  10. package/pipeline/lib/credential-inventory.sh +3 -2
  11. package/pipeline/lib/repo-profile-derive.mjs +21 -12
  12. package/pipeline/multi-agent-refs/features/doctor.md +17 -0
  13. package/pipeline/multi-agent-refs/features/repo-profile.md +1 -1
  14. package/pipeline/multi-agent-refs/keychain.md +2 -0
  15. package/pipeline/multi-agent-refs/outside-the-pipeline.md +1 -0
  16. package/pipeline/multi-agent-refs/setup/app-store-connect.md +106 -20
  17. package/pipeline/multi-agent-refs/setup/google-play.md +70 -0
  18. package/pipeline/preferences-template.json +3 -1
  19. package/pipeline/schemas/prefs.schema.json +9 -1
  20. package/pipeline/schemas/repo-profile.schema.json +32 -1
  21. package/pipeline/scripts/doctor.mjs +91 -1
  22. package/pipeline/scripts/pre-commit-check.sh +11 -2
  23. package/pipeline/scripts/probe-store-key.mjs +306 -0
  24. package/pipeline/scripts/write-store-config.mjs +293 -0
  25. package/pipeline/skills/.skill-manifest.json +4 -4
  26. package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +1 -1
  27. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +58 -6
  28. package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +11 -7
@@ -19,7 +19,7 @@ Every role is `{ value, source, evidence, confidence }`. `source` is `derived`,
19
19
  | `commit.format` | last 200 non-merge, non-bot subjects: ticket `prefix` / `suffix` / `none` by majority, its style (`[KEY-123]`, `KEY-123:`), whether a conventional scope is required. A trailing `(#123)` is the host's PR number and is ignored |
20
20
  | `repo.workBranch`, `repo.defaultBranch` | `pull_request: branches:` targets in workflows, else `push: branches:` targets, else the most active of the usual integration branches, remote or local; `origin/HEAD` |
21
21
  | `build`, `test`, `lint` | CI `run:` steps, exact flags kept, continuations joined; `high` when a pull request triggers the step or it appears twice; `-testPlan` values and `*.xctestplan` files; `package.json` scripts as a low-confidence fallback |
22
- | `accessors.*`, `di.*` | top-level types a generated localization / testing-id / token tree declares, ranked by `<Root>.<Seg>.<Seg>` usage outside it; the registrar suffix is the compound most files of the commonest `Registrar` / `Configurator` / `Assembly` / `Container` / `Module` family end in, read from the file names; its common method; the inject attribute. `--conventions <file>` or `--with-conventions <platform>` folds in `extract-conventions.sh` buckets |
22
+ | `accessors.*`, `di.*` | top-level types a generated localization / testing-id / token tree declares, ranked by `<Root>.<Seg>.<Seg>` usage outside it; the registrar suffix is the compound most files of the commonest `Registrar` / `Configurator` / `Assembly` / `Container` / `Module` family end in, read from the file names; its commonest registration method (`registerMethod`) and, apart from it, its commonest `validate*` / `verify*` / `check*` method (`validateMethod`); the inject attribute (`@Inject`, `@Injected`, `@Dependency` and similar keyed wrappers). `--conventions <file>` or `--with-conventions <platform>` folds in `extract-conventions.sh` buckets |
23
23
  | `module`, `mock` | repeated module manifests as a glob; a module validator script or step; mock framework imports or a fixture tree; a `Custom*` tree beside a generated one. `mock.system` and `di.injectAttribute` count only files of the repo's dominant language and carry `file:line` evidence; otherwise they are `null` |
24
24
  | `hooks[]` | `.githooks/`, `scripts/git-hooks/`, `.husky/`, pre-commit and lefthook configs; side effects `amends-commit`, `stages-files`, `writes-files`, `network`; the script that sets `core.hooksPath` |
25
25
  | `requiredChecks[]`, `docs.authoritative[]`, `inRepoSkills[]` | workflow jobs with a `required` comment (always low); the documents `claude-md-links.mjs` admits from the repo's `CLAUDE.md` files (same bounds, evidence is the CLAUDE.md that points at it); `.claude/skills/*/SKILL.md` with `trigger: explicit` when `disable-model-invocation: true` |
@@ -163,6 +163,8 @@ The shell driver auto-delegates to `~/.claude/scripts/keychain.py` for determini
163
163
  | `vercel` | Vercel deploys | `${USER}_Vercel_Access_Token` | Access Token | Vercel -> Account Settings -> Tokens. Read by `vercel-deploy.sh`; never passed on argv |
164
164
  | `appstore_connect_key_id` | App Store Connect | `${USER}_AppStoreConnect_Key_Id` | Identifier, not a secret | App Store Connect -> Users and Access -> Integrations -> App Store Connect API |
165
165
  | `appstore_connect_issuer_id` | App Store Connect | `${USER}_AppStoreConnect_Issuer_Id` | Identifier, not a secret | Same page as the key id; one issuer id per team |
166
+ | `appstore_connect_private_key` | App Store Connect | `${USER}_AppStoreConnect_Private_Key` | Full PEM text of `AuthKey_<keyId>.p8` | Same page as the key id; downloadable once. Stored from the file via stdin (`set appstore_connect_private_key - < AuthKey_<keyId>.p8`), probed first with `probe-store-key.mjs --asc` |
167
+ | `google_play` | Google Play | `${USER}_GooglePlay_Access_Json` | Service-account JSON, as issued | Play Console -> Setup -> API access; invite the account with app permissions and enable the Google Play Android Developer API and the Google Play Developer Reporting API. Probed first with `probe-store-key.mjs --play` |
166
168
  | `appstore_connect_apple_id` | App Store Connect | `${USER}_AppStoreConnect_Apple_Id` | Email address | The Apple ID itself; no generation step |
167
169
  | `appstore_connect_password_item` | App Store Connect | `${USER}_AppStoreConnect_Password_Item` | Keychain ITEM NAME, not a password | `xcrun altool --store-password-in-keychain-item`; map only the item NAME |
168
170
  | - | Git Identity | Stored in preferences JSON | Not Keychain | Written to preferences by setup Step 4 |
@@ -104,6 +104,7 @@ another stack's toolkit in its place.
104
104
  | `design_*` (`design_visual_compare`, `design_ui_geometry`, `design_report`, ...) | a built screen has to be compared against its design |
105
105
  | `ios_app_store_audit` | a package is heading for review |
106
106
  | `ios_testflight_validate` | validating a build before upload |
107
+ | `store_*` (`store_status`, `store_asc_*`, `store_play_*`, `store_search` / `store_call`) | a question about App Store Connect or Google Play data: builds and build numbers, reviews, subscriptions, tracks, vitals. Keys come from the Keychain via the store reference file `/multi-agent:setup` writes; `store_status` says which store resolves. Writes return a preview until `confirm: true` and are refused unattended |
107
108
 
108
109
  Registered at user scope by the installer, and preserved by uninstall - the tools are
109
110
  useful with no pipeline at all. If it is not registered the tools simply are not
@@ -1,5 +1,12 @@
1
1
  # App Store Connect Onboarding (setup Step 3b)
2
2
 
3
+ <!-- toc -->
4
+ - [API key](#api-key)
5
+ - [Apple ID + app-specific password](#apple-id-app-specific-password)
6
+ - [Multi-provider accounts](#multi-provider-accounts)
7
+ - [Verify + expiry](#verify-expiry)
8
+ <!-- /toc -->
9
+
3
10
  Loaded on demand by `/multi-agent:setup` Step 3b (iOS only, optional). The SKILL.md carries the step intro; this file is the full flow.
4
11
 
5
12
  Runs inside Step 3 alongside the other missing credentials, not as a late add-on:
@@ -7,24 +14,25 @@ a user who already has an App Store Connect credential in their keychain gets it
7
14
  mapped by Step 1 discovery like any other token, and only the genuinely missing
8
15
  pieces reach this flow.
9
16
 
10
- Three of the four entries do not go through the normal Token Save Flow, because
11
- what they hold is not a pasteable secret:
17
+ None of the five entries goes through the clipboard Token Save Flow: four hold
18
+ something that is not a pasteable secret, and the one secret arrives as a file.
12
19
 
13
20
  | Entry | Holds | Flow |
14
21
  |---|---|---|
15
22
  | `appstore_connect_key_id` | an identifier | plain value, not a secret; still mapped so it is read through the mapping layer |
16
23
  | `appstore_connect_issuer_id` | an identifier | same |
17
- | `appstore_connect_apple_id` | an email address | same |
24
+ | `appstore_connect_private_key` | the full PEM text of `AuthKey_<keyId>.p8` | probed from the file, then stored from the file through stdin; never pasted, never on argv |
25
+ | `appstore_connect_apple_id` | an email address | plain value, not a secret |
18
26
  | `appstore_connect_password_item` | a keychain ITEM NAME | the password lives in Apple's own keychain item, referenced as `-p @keychain:<item>` and never read by the pipeline |
19
27
 
20
- All four entries are **iOS-only and optional**: skip them all and the pipeline
28
+ All five entries are **iOS-only and optional**: skip them all and the pipeline
21
29
  still works, it just reports Gate 2 of `/multi-agent:store-ready` as `SKIPPED`,
22
30
  never as a pass.
23
31
 
24
32
  The tiers mirror the Figma 3-tier shape: Tier 1 = API key (`appstore_connect_key_id`
25
- + `appstore_connect_issuer_id`), Tier 2 = Apple ID + app-specific password
26
- (`appstore_connect_apple_id` + `appstore_connect_password_item`), Tier 3 = nothing
27
- configured.
33
+ + `appstore_connect_issuer_id` + `appstore_connect_private_key`), Tier 2 = Apple ID
34
+ + app-specific password (`appstore_connect_apple_id` + `appstore_connect_password_item`),
35
+ Tier 3 = nothing configured.
28
36
 
29
37
  Ask which tier to configure (picker): **API key** / **Apple ID + app-specific
30
38
  password** / **Skip**. Lead with the second when the user says they cannot create
@@ -32,16 +40,89 @@ an API key: creating one needs an Admin or App Manager role in App Store Connect
32
40
  while an app-specific password is generated by the account holder at
33
41
  `appleid.apple.com` with no team permission at all.
34
42
 
35
- **API key.** The private key is a FILE and is never copied into the credential
36
- store. It must sit in a directory `altool` already searches:
43
+ ## API key
37
44
 
38
- ```bash
39
- ls ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null \
40
- || echo "MISSING: put AuthKey_<keyId>.p8 in ~/.appstoreconnect/private_keys/"
41
- ```
45
+ Every part of the key lives in the Keychain, the `.p8` included. The toolkit's
46
+ `store_asc_*` tools and `ios_testflight_validate` read all three items through the
47
+ store reference file (`~/.config/multi-agent-toolkit/store.json`). Called with
48
+ neither `api_key_id` nor `apple_id`, `ios_testflight_validate` uses that same key
49
+ and hands `altool` a private temporary copy that exists only for the length of the
50
+ call, so no copy under `~/.appstoreconnect/private_keys/` is needed.
51
+
52
+ Map before storing. The three entries in `prefs.global.keychainMapping`
53
+ (`appstore_connect_key_id`, `appstore_connect_issuer_id`,
54
+ `appstore_connect_private_key`) point at their standard names from
55
+ `refs/keychain.md`, or keep an existing mapping when re-onboarding, as Token Save
56
+ Flow Step D does for any key. `set` resolves a logical key through the mapping,
57
+ so every write below goes through the logical key and lands in the item the
58
+ mapping names; an unmapped key would be stored under the bare logical name.
59
+
60
+ 1. **Key id and issuer id.** App Store Connect -> Users and Access -> Integrations
61
+ -> App Store Connect API. Both are identifiers; store them as plain values:
62
+
63
+ ```bash
64
+ ~/.claude/lib/credential-store.sh set appstore_connect_key_id "<key id>"
65
+ ~/.claude/lib/credential-store.sh set appstore_connect_issuer_id "<issuer id>"
66
+ ```
67
+
68
+ 2. **Locate the `.p8`.** Ask for the path to `AuthKey_<keyId>.p8` as a path, never
69
+ its contents. Apple lets the file be downloaded once, so it is usually in
70
+ `~/Downloads` or `~/.appstoreconnect/private_keys/`:
71
+
72
+ ```bash
73
+ ls ~/Downloads/AuthKey_*.p8 ~/.appstoreconnect/private_keys/AuthKey_*.p8 2>/dev/null
74
+ ```
75
+
76
+ 3. **Validate before saving.** A key that is saved first and tried later fails at
77
+ the first store call, long after setup. Probe it against the file:
78
+
79
+ ```bash
80
+ node "$HOME/.claude/scripts/probe-store-key.mjs" --asc \
81
+ --key-id "<key id>" --issuer-id "<issuer id>" --p8 "<path to AuthKey_<keyId>.p8>"
82
+ ```
83
+
84
+ It signs a 10-minute ES256 token, reads `GET /v1/apps?limit=1` and prints one
85
+ line, `{ok, store, status, hint}`, with no key material in it. Exit 0 = accepted.
86
+ On exit 1 show the `hint` and stop this tier: `401` means the key id or issuer
87
+ id does not belong to this `.p8`, the key was revoked, or the clock is off;
88
+ `403` means the key's role cannot read apps. Exit 2 is a usage problem (wrong
89
+ path, not a private key). Nothing is saved on a failed probe.
90
+
91
+ 4. **Store the key from the file.** A file does not go through the clipboard;
92
+ stdin keeps it off argv and out of shell history:
42
93
 
43
- **Apple ID + app-specific password.** Use Apple's own keychain helper. The secret
44
- never enters chat and never becomes a shell argument, per the Token Save Flow rule:
94
+ ```bash
95
+ ~/.claude/lib/credential-store.sh set appstore_connect_private_key - < "<path to AuthKey_<keyId>.p8>"
96
+ ```
97
+
98
+ 5. **Write the store reference file.**
99
+
100
+ ```bash
101
+ node "$HOME/.claude/scripts/write-store-config.mjs"
102
+ ```
103
+
104
+ It names the three Keychain items in `~/.config/multi-agent-toolkit/store.json`
105
+ (0600, directory 0700), after checking each item exists without reading it.
106
+ `store_status` in the toolkit then reports App Store Connect as configured from
107
+ the Keychain.
108
+
109
+ 6. **Offer to delete the `.p8` from disk** (picker, default keep). The key is in
110
+ the Keychain now, and a plain file in `~/Downloads` is the copy most likely to
111
+ leak. Apple does not let it be downloaded again, so the user decides:
112
+
113
+ - `question` / `description` in `outputLanguage`, for example "The key is now in
114
+ the Keychain. Delete the .p8 file from disk?"
115
+ - `header`: `P8 file`
116
+ - options: `{label: "Keep file", description: "<outputLanguage: leave the file where it is>"}`
117
+ (first, default) / `{label: "Delete file", description: "<outputLanguage: remove it; the Keychain copy is the only one left>"}`
118
+
119
+ On **Delete file**, `rm "<path>"` that one file and nothing else. On **Keep
120
+ file**, say where it is.
121
+
122
+ ## Apple ID + app-specific password
123
+
124
+ Use Apple's own keychain helper. The secret never enters chat and never becomes a
125
+ shell argument, per the Token Save Flow rule:
45
126
 
46
127
  ```bash
47
128
  # the user exports AC_PASSWORD_ONCE in their own shell, for this one command
@@ -51,15 +132,20 @@ xcrun altool --store-password-in-keychain-item "<item-name>" \
51
132
 
52
133
  Then map only `<item-name>` as `appstore_connect_password_item`.
53
134
 
54
- **Multi-provider accounts.** A corporate Apple ID often belongs to several
135
+ ## Multi-provider accounts
136
+
137
+ A corporate Apple ID often belongs to several
55
138
  providers, and `altool` fails opaquely without one. Resolve it once with
56
139
  `ios_testflight_validate({list_providers: true, <credentials just configured>})`
57
140
  and store the answer under
58
141
  `prefs.projects[<key>].appStoreConnect.providerPublicId` - per-project, since a
59
142
  user can ship for more than one team.
60
143
 
61
- **Verify + expiry.** Re-run the `list_providers` probe and report the resolved
62
- tier. A credential that resolves but is rejected (401/403) follows the
63
- Expired-token decision in `refs/keychain.md` Rule 1 - Regenerate / Use a
64
- different token / Skip and continue - never a silent drop.
144
+ ## Verify + expiry
65
145
 
146
+ For the API key, `store_status({probe: true})` makes one read-only call and says
147
+ whether it worked; for either tier, re-run the `list_providers` probe and report
148
+ the resolved tier. A credential that resolves but is rejected (401/403) follows the
149
+ Expired-token decision in `refs/keychain.md` Rule 1 - Regenerate / Use a
150
+ different token / Skip and continue - never a silent drop. Regenerating the API
151
+ key repeats steps 2 to 5 with the new file.
@@ -0,0 +1,70 @@
1
+ # Google Play Onboarding (setup Step 3d)
2
+
3
+ Loaded on demand by `/multi-agent:setup` Step 3d (Android only, optional). The SKILL.md carries the step intro; this file is the full flow.
4
+
5
+ Runs inside Step 3, only when the target repo is Android, the same way Step 3b
6
+ runs only for iOS. One entry, `google_play`: a Google Cloud service-account JSON,
7
+ stored exactly as Google issued it. The toolkit's `store_play_*` tools
8
+ authenticate with it through the store reference file, and it is what lets
9
+ `/multi-agent:store-ready` hand Android Gate 2 to Google instead of reporting it
10
+ `SKIPPED`.
11
+
12
+ ## Ask
13
+
14
+ Picker: **Add Google Play** / **Skip**. `question` and `description` in
15
+ `outputLanguage`; `label` and `header` (`Google Play`) English. Skip writes
16
+ nothing.
17
+
18
+ ## What the account needs
19
+
20
+ Say this before asking for the file; a key that authenticates but reaches nothing
21
+ is the common failure.
22
+
23
+ 1. Play Console -> Setup -> API access: link a Google Cloud project, create (or
24
+ pick) a service account and download its JSON key.
25
+ 2. Play Console -> Users and permissions: invite the service account's
26
+ `client_email` with permissions for the apps it should read.
27
+ 3. In that Google Cloud project, enable the **Google Play Android Developer API**
28
+ and the **Google Play Developer Reporting API**.
29
+
30
+ ## Validate before saving
31
+
32
+ Ask for the JSON's **path**, never its contents; a file does not go through the
33
+ clipboard or the chat.
34
+
35
+ ```bash
36
+ node "$HOME/.claude/scripts/probe-store-key.mjs" --play --service-account "<path to service-account.json>"
37
+ ```
38
+
39
+ It exchanges a signed assertion at `oauth2.googleapis.com` for the
40
+ `androidpublisher` and `playdeveloperreporting` scopes, reads one page of
41
+ `apps:search` on the reporting API, and prints `{ok, store, status, hint}` with no
42
+ key material. Exit 0 = accepted. On exit 1 show the `hint` and save nothing:
43
+
44
+ | Status | Meaning | Fix |
45
+ |---|---|---|
46
+ | `403` with `SERVICE_DISABLED` | one of the two APIs is off in the account's Cloud project | enable both APIs (step 3 above), probe again |
47
+ | any other `403` | the account is not invited in Play Console, or holds no app permission | step 2 above |
48
+ | `400` / `401` at the token step | the key was deleted or disabled, or the clock is off | generate a new key for the account |
49
+
50
+ Exit 2 is a usage problem (wrong path, not a service-account file).
51
+
52
+ ## Save
53
+
54
+ Map `keychainMapping.google_play` first: the standard name
55
+ `${USER}_GooglePlay_Access_Json`, or the existing mapping when re-onboarding
56
+ (Token Save Flow Step D). `set` resolves a logical key through the mapping, and an
57
+ unmapped one would be stored under the bare logical name. Then:
58
+
59
+ ```bash
60
+ ~/.claude/lib/credential-store.sh set google_play - < "<path to service-account.json>"
61
+ node "$HOME/.claude/scripts/write-store-config.mjs"
62
+ ```
63
+
64
+ The writer names the item in `~/.config/multi-agent-toolkit/store.json` (0600,
65
+ directory 0700) after checking it exists without reading it. `store_status` in the
66
+ toolkit then reports Google Play as configured from the Keychain, and
67
+ `store_status({probe: true})` makes one read-only call to confirm it.
68
+
69
+ The JSON stays on disk unless the user asks otherwise: Google issues a new key on
70
+ request, so the file is not the only copy the way a `.p8` is.
@@ -16,7 +16,9 @@
16
16
  "usage_ingest": null,
17
17
  "firebase": null,
18
18
  "jenkins": null,
19
- "vercel": null
19
+ "vercel": null,
20
+ "appstore_connect_private_key": null,
21
+ "google_play": null
20
22
  },
21
23
  "tokenScripts": {},
22
24
  "platformIdentityRouting": {},
@@ -208,7 +208,15 @@
208
208
  },
209
209
  "appstore_connect_issuer_id": {
210
210
  "type": ["string", "null"],
211
- "description": "App Store Connect API issuer ID. Required alongside appstore_connect_key_id; either alone leaves Tier 1 unusable and the resolver says so instead of silently dropping to Tier 2. The .p8 private key is a FILE and is never stored here - it belongs at ~/.appstoreconnect/private_keys/AuthKey_<keyId>.p8, one of the directories altool searches."
211
+ "description": "App Store Connect API issuer ID. Required alongside appstore_connect_key_id; either alone leaves Tier 1 unusable and the resolver says so instead of silently dropping to Tier 2. The .p8 private key is stored in the Keychain as well, under appstore_connect_private_key; the store reference file written by write-store-config.mjs names all three items so the toolkit's store_* tools and ios_testflight_validate read the key from there."
212
+ },
213
+ "appstore_connect_private_key": {
214
+ "type": ["string", "null"],
215
+ "description": "Keychain item holding the App Store Connect API private key: the full PEM text of AuthKey_<keyId>.p8. Stored from the file through credential-store.sh stdin so the key never reaches argv or the clipboard. The toolkit reads it through the store reference file and writes a short-lived 0600 copy only for tools that read a key from disk."
216
+ },
217
+ "google_play": {
218
+ "type": ["string", "null"],
219
+ "description": "Keychain item holding the Google Play service-account JSON, stored exactly as Google issued it. Read by the toolkit's store_play_* tools through the store reference file. The account must be invited in Play Console with app permissions, and the Google Play Android Developer API and the Google Play Developer Reporting API must be enabled in its Google Cloud project."
212
220
  },
213
221
  "appstore_connect_apple_id": {
214
222
  "type": ["string", "null"],
@@ -752,7 +752,38 @@
752
752
  },
753
753
  "registerMethod": {
754
754
  "type": "object",
755
- "description": "Method a registrar implements.",
755
+ "description": "Method a registrar implements to perform the registration.",
756
+ "additionalProperties": false,
757
+ "required": ["value", "source", "evidence", "confidence"],
758
+ "properties": {
759
+ "value": {
760
+ "anyOf": [
761
+ {
762
+ "type": "string",
763
+ "minLength": 1
764
+ },
765
+ {
766
+ "type": "null"
767
+ }
768
+ ]
769
+ },
770
+ "source": {
771
+ "$ref": "#/$defs/source"
772
+ },
773
+ "evidence": {
774
+ "$ref": "#/$defs/evidence"
775
+ },
776
+ "confidence": {
777
+ "$ref": "#/$defs/confidence"
778
+ },
779
+ "note": {
780
+ "type": "string"
781
+ }
782
+ }
783
+ },
784
+ "validateMethod": {
785
+ "type": "object",
786
+ "description": "Method a registrar implements to check that what its units read is wired (validate, verify, check). Optional: profiles derived before it existed omit it.",
756
787
  "additionalProperties": false,
757
788
  "required": ["value", "source", "evidence", "confidence"],
758
789
  "properties": {
@@ -49,7 +49,7 @@ import {
49
49
  rmSync,
50
50
  realpathSync,
51
51
  } from "node:fs";
52
- import { join, dirname } from "node:path";
52
+ import { basename, join, dirname } from "node:path";
53
53
  import { homedir } from "node:os";
54
54
  import { execFileSync } from "node:child_process";
55
55
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -92,6 +92,7 @@ const CHECK_IDS = [
92
92
  "hook-coverage",
93
93
  "credential-mapping",
94
94
  "credential-liveness",
95
+ "store-reference",
95
96
  "embedded-credentials",
96
97
  "task-tools",
97
98
  "mcp-registration",
@@ -584,6 +585,94 @@ function checkCredentials(inv) {
584
585
  ok("credential-liveness");
585
586
  }
586
587
 
588
+ // The toolkit's store_* tools find the App Store Connect and Google Play keys
589
+ // through a reference file naming Keychain services, written by
590
+ // write-store-config.mjs from keychainMapping. A mapping the file does not
591
+ // reflect means the keys sit in the Keychain and every store call still says
592
+ // "not onboarded". Nothing mapped is nothing claimed, so that case is OK.
593
+ const STORE_OF_KEY = {
594
+ appstore_connect_key_id: "appStoreConnect",
595
+ appstore_connect_issuer_id: "appStoreConnect",
596
+ appstore_connect_private_key: "appStoreConnect",
597
+ google_play: "googlePlay",
598
+ };
599
+ const STORE_LABELS = { appStoreConnect: "App Store Connect", googlePlay: "Google Play" };
600
+
601
+ // The host tree this doctor was installed into (~/.claude, ~/.copilot or
602
+ // ~/.codex); run from a checkout, the Claude tree.
603
+ const HOST_TREE = [".claude", ".copilot", ".codex"].includes(basename(dirname(HERE)))
604
+ ? dirname(HERE)
605
+ : CLAUDE;
606
+
607
+ function checkStoreReference() {
608
+ const prefsFile = join(CLAUDE, "multi-agent-preferences.json");
609
+ const km = readJson(prefsFile)?.global?.keychainMapping || {};
610
+ const mapped = Object.keys(STORE_OF_KEY).filter((k) => typeof km[k] === "string" && km[k].trim());
611
+ if (!mapped.length) {
612
+ ok("store-reference");
613
+ return;
614
+ }
615
+ const writer = join(HOST_TREE, "scripts", "write-store-config.mjs");
616
+ if (!existsSync(writer)) {
617
+ skip("store-reference", "write-store-config.mjs is not installed");
618
+ return;
619
+ }
620
+ let res;
621
+ try {
622
+ const out = execFileSync(process.execPath, [writer, "--check"], {
623
+ encoding: "utf8",
624
+ stdio: ["ignore", "pipe", "ignore"],
625
+ env: {
626
+ ...process.env,
627
+ MULTI_AGENT_PREFS: prefsFile,
628
+ CRED_STORE: join(HOST_TREE, "lib", "credential-store.sh"),
629
+ },
630
+ });
631
+ res = JSON.parse(out);
632
+ } catch (e) {
633
+ try {
634
+ res = JSON.parse(String(e.stdout || ""));
635
+ } catch {
636
+ res = null;
637
+ }
638
+ }
639
+ if (!res || res.error) {
640
+ skip("store-reference", res?.error || "write-store-config.mjs --check produced no verdict");
641
+ return;
642
+ }
643
+ const fix = `run node "$HOME/${basename(HOST_TREE)}/scripts/write-store-config.mjs"`;
644
+ if (res.state !== "current") {
645
+ const ready = Object.entries(res.stores || {})
646
+ .filter(([, v]) => v.included)
647
+ .map(([k]) => STORE_LABELS[k] || k);
648
+ report(
649
+ "store-reference",
650
+ "WARN",
651
+ `store keys are in the Keychain but the store reference file ${res.state === "missing" ? "is missing" : "is stale"} (${res.path}), so store_* tools cannot find ${ready.join(" and ") || "them"}`,
652
+ fix,
653
+ );
654
+ return;
655
+ }
656
+ const partial = Object.entries(res.stores || {}).filter(
657
+ ([store, v]) => !v.included && mapped.some((k) => STORE_OF_KEY[k] === store),
658
+ );
659
+ if (partial.length) {
660
+ report(
661
+ "store-reference",
662
+ "INFO",
663
+ partial
664
+ .map(
665
+ ([store, v]) =>
666
+ `${STORE_LABELS[store] || store} is partly onboarded, missing ${v.missing.join(", ")}`,
667
+ )
668
+ .join("; "),
669
+ `run ${hostCommand("setup", HERE)}`,
670
+ );
671
+ return;
672
+ }
673
+ ok("store-reference");
674
+ }
675
+
587
676
  function checkEmbeddedCredentials() {
588
677
  // The value never enters a shell variable or this process's output. What is
589
678
  // reported is the repo, the config key, the host, a shape label and a length
@@ -1079,6 +1168,7 @@ function main() {
1079
1168
  checkIdentity();
1080
1169
  checkHookCoverage();
1081
1170
  checkCredentials(credentialInventory());
1171
+ checkStoreReference();
1082
1172
  checkEmbeddedCredentials();
1083
1173
  checkTaskTools();
1084
1174
  checkMcpRegistration();
@@ -294,6 +294,11 @@ $folded"
294
294
  # +0.25. Clipping keeps one unusual pair from outweighing the rest. An
295
295
  # all-caps segment of up to five letters inside a token that also has
296
296
  # lowercase letters is an acronym (URL, HTTP, JSON) and is not scored.
297
+ # Version markers (v1, V2), digit runs and "/" separate segments too, so
298
+ # a versioned API name
299
+ # (ReportingV1beta1QueryCrashRateMetricSetRequest) or a short REST path
300
+ # (/v1/inAppPurchaseOfferCodes) is read as the words it is made of; a
301
+ # token that is mostly digits is never word-shaped.
297
302
  BEGIN {
298
303
  rows = "06655352403757150767433232 52015000500600400421400010 " \
299
304
  "60306006504400600515500040 50026021600313500420301030 " \
@@ -322,13 +327,17 @@ $folded"
322
327
  pair_score += PAIR[substr(l, j - 1, 2)];
323
328
  }
324
329
  }
325
- function wordlike(t, n, i, c, p, nx, seg, nseg, total, maxlen, split_here, has_lower) {
326
- if (t !~ /^[A-Za-z_-]+$/) return 0;
330
+ function wordlike(t, n, i, c, p, nx, seg, nseg, total, maxlen, split_here, has_lower, digits) {
331
+ if (t !~ /^[A-Za-z0-9_\/-]+$/) return 0;
332
+ digits = t; gsub(/[^0-9]/, "", digits);
333
+ if (length(digits) * 8 > length(t)) return 0;
334
+ gsub(/[vV][0-9]+/, "_", t);
327
335
  has_lower = (t ~ /[a-z]/);
328
336
  pairs = 0; pair_score = 0;
329
337
  n = length(t); seg = ""; nseg = 0; total = 0; maxlen = 0;
330
338
  for (i = 1; i <= n + 1; i++) {
331
339
  c = (i <= n) ? substr(t, i, 1) : "_";
340
+ if (c ~ /[0-9\/]/) c = "_";
332
341
  split_here = 0;
333
342
  if (c == "_" || c == "-") split_here = 1;
334
343
  else if (seg != "" && c ~ /[A-Z]/) {