in2-agent-server 0.20260821.185429
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/README.md +369 -0
- package/agents/plan-requirement.md +32 -0
- package/dist/campfire/configItem.js +201 -0
- package/dist/campfire/configItemDescription.js +379 -0
- package/dist/campfire/deriveFromBundle.js +1013 -0
- package/dist/cli.js +465 -0
- package/dist/core/betaFeedback.js +493 -0
- package/dist/core/chapterInstructions.js +473 -0
- package/dist/core/claudeAssets.js +61 -0
- package/dist/core/gate.js +201 -0
- package/dist/core/ids.js +9 -0
- package/dist/core/install.js +60 -0
- package/dist/core/operatorQa.js +462 -0
- package/dist/core/orgCallMeter.js +178 -0
- package/dist/core/parcels.js +1235 -0
- package/dist/core/paths.js +105 -0
- package/dist/core/phases.js +225 -0
- package/dist/core/planFile.js +251 -0
- package/dist/core/planFragments.js +119 -0
- package/dist/core/readsLog.js +398 -0
- package/dist/core/registration.js +24 -0
- package/dist/core/result.js +38 -0
- package/dist/core/resume.js +233 -0
- package/dist/core/revisionChain.js +351 -0
- package/dist/core/richText.js +61 -0
- package/dist/core/semanticHash.js +89 -0
- package/dist/core/sessionOrg.js +60 -0
- package/dist/core/sfQuery.js +19 -0
- package/dist/core/spawn.js +182 -0
- package/dist/core/supervisorContract.js +230 -0
- package/dist/core/telemetry.js +1297 -0
- package/dist/core/telemetryContext.js +154 -0
- package/dist/core/telemetrySpool.js +244 -0
- package/dist/core/transcriptUpload.js +516 -0
- package/dist/core/update.js +363 -0
- package/dist/core/viewer.js +117 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/writeGrants.js +93 -0
- package/dist/core/writebackManifest.js +92 -0
- package/dist/direct/applyLoop.js +2516 -0
- package/dist/direct/batchReport.js +510 -0
- package/dist/direct/comparatorNoise.js +75 -0
- package/dist/direct/containers.js +85 -0
- package/dist/direct/contamination.js +221 -0
- package/dist/direct/crud.js +56 -0
- package/dist/direct/destructive.js +168 -0
- package/dist/direct/knownTokens.js +94 -0
- package/dist/direct/lint.js +382 -0
- package/dist/direct/metadataSchema.generated.json +1 -0
- package/dist/direct/metadataSchema.js +179 -0
- package/dist/direct/operatorPage.js +349 -0
- package/dist/direct/orgRepo.js +1531 -0
- package/dist/direct/orgRepoSpawnCount.js +21 -0
- package/dist/direct/plannedScope.js +119 -0
- package/dist/direct/preImageTargets.js +63 -0
- package/dist/direct/refVerify.js +191 -0
- package/dist/direct/registry.js +129 -0
- package/dist/direct/removals.js +262 -0
- package/dist/direct/rollback.js +1513 -0
- package/dist/direct/rollbackRun.js +1360 -0
- package/dist/direct/sessionProjection.js +126 -0
- package/dist/direct/snapshotComparability.js +165 -0
- package/dist/direct/sourceTree.js +113 -0
- package/dist/direct/unstatedChanges.js +106 -0
- package/dist/direct/waveAssembly.js +556 -0
- package/dist/direct/waveCounts.js +19 -0
- package/dist/direct/waveDerivation.js +152 -0
- package/dist/direct/waveGrade.js +185 -0
- package/dist/index.js +90 -0
- package/dist/orgread/customFieldSnapshot.js +48 -0
- package/dist/orgread/fieldMetadata.js +178 -0
- package/dist/orgread/genericSnapshot.js +61 -0
- package/dist/orgread/layoutSnapshot.js +151 -0
- package/dist/orgread/salesforceOrgReader.js +184 -0
- package/dist/orgread/snapshotTypes.js +9 -0
- package/dist/projection/bridge.js +26 -0
- package/dist/projection/diff.js +141 -0
- package/dist/projection/mapmd.js +213 -0
- package/dist/projection/nameAmbiguity.js +106 -0
- package/dist/projection/precedent.js +136 -0
- package/dist/projection/project.js +214 -0
- package/dist/projection/scope.js +110 -0
- package/dist/projection/types.js +5 -0
- package/dist/tools/analyze.js +322 -0
- package/dist/tools/auth.js +915 -0
- package/dist/tools/campfire.js +1742 -0
- package/dist/tools/closeBatch.js +154 -0
- package/dist/tools/closeText.js +35 -0
- package/dist/tools/direct.js +1613 -0
- package/dist/tools/feedback.js +318 -0
- package/dist/tools/operator.js +287 -0
- package/dist/tools/org.js +739 -0
- package/dist/tools/phase.js +249 -0
- package/dist/tools/projection.js +172 -0
- package/dist/tools/session.js +313 -0
- package/dist/tools/update.js +82 -0
- package/dist/tools/waves.js +793 -0
- package/package.json +48 -0
- package/reference/campfire-configuration-items-guide.md +710 -0
- package/reference/deployability-cribs.md +119 -0
- package/reference/operator-voice.md +141 -0
- package/reference/org-read-cribs.md +34 -0
- package/reference/scoped-dml-interim.md +68 -0
- package/reference/supervisor-playbook.md +313 -0
- package/reference/templates.md +233 -0
- package/skills/in2-delivery/SKILL.md +322 -0
- package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
- package/skills/in2-delivery/references/deployability-cribs.md +119 -0
- package/skills/in2-delivery/references/operator-voice.md +141 -0
- package/skills/in2-delivery/references/org-read-cribs.md +34 -0
- package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
- package/skills/in2-delivery/references/templates.md +233 -0
- package/skills/plan-requirement/SKILL.md +188 -0
- package/viewer/index.html +899 -0
- package/viewer/server.mjs +433 -0
package/README.md
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# cc-mcp-server
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) stdio server that turns Campfire Salesforce requirements into verified
|
|
4
|
+
org changes, driven by a Claude Code supervisor.
|
|
5
|
+
|
|
6
|
+
**Standalone.** No `workspace:*` dependencies — it resolves nothing outside published npm packages,
|
|
7
|
+
so it publishes to the public npm registry as **`in2-agent-server`** (bins: `in2-agent-server`,
|
|
8
|
+
`in2`), stamped and shipped by `scripts/release.mjs`.
|
|
9
|
+
|
|
10
|
+
- **What the tools are and how they compose:** [`docs/TOOL-SURFACE-AUDIT.md`](docs/TOOL-SURFACE-AUDIT.md)
|
|
11
|
+
— full inventory, the happy-path flow, the failure loops.
|
|
12
|
+
- **How the supervisor runs a batch:** [`harness/supervisor-playbook.md`](harness/supervisor-playbook.md) — the deep reference.
|
|
13
|
+
- **What actually reaches a production session:** [`src/core/supervisorContract.ts`](src/core/supervisorContract.ts)
|
|
14
|
+
— shipped as the server's MCP `instructions`, so it arrives regardless of the session's cwd.
|
|
15
|
+
|
|
16
|
+
## The loop
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
activate_session redeem a Campfire code: auth the org, bind it, grant write, import reqs
|
|
20
|
+
→ project_org read the org slice — ground truth; asserted API names are untrusted
|
|
21
|
+
→ (analyzers) one per requirement, writing requirements/<extId>/plan.json
|
|
22
|
+
→ direct_lint schema + lemma + referential tiers, locally, in milliseconds
|
|
23
|
+
→ direct_apply checkOnly → deploy → re-project → three-way diff → verdict + evidence
|
|
24
|
+
→ campfire_config_item_plan / campfire_write_config_items
|
|
25
|
+
→ close_batch state the terminal result, write the evidence, ship everything
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Carry the **`sessionId`** and nothing else: the session owns the target org, the Campfire org, the
|
|
29
|
+
authored trees and the evidence bundles. Author each wave to `<session>/authored/<wave>/`, then both
|
|
30
|
+
`direct_lint {sessionId, wave}` and `direct_apply {sessionId, wave}` derive that path themselves.
|
|
31
|
+
|
|
32
|
+
### Chapters
|
|
33
|
+
|
|
34
|
+
The loop has nine named chapters — `activation`, `intake`, `partitioning`, `parallel-analysis`,
|
|
35
|
+
`clarification`, `scope-approval`, `delivery`, `review-results`, `writeback-close` — and
|
|
36
|
+
`advance_phase {sessionId, completed, entering}` marks each boundary. Three things ride on one call,
|
|
37
|
+
and they are bound together because any two without the third is worse than the whole:
|
|
38
|
+
|
|
39
|
+
- **The position is recorded**, appended to `<session>/phases.jsonl`. Everything else in a session
|
|
40
|
+
directory says what a run *produced*; nothing else says where it *got to*, and a chapter entered
|
|
41
|
+
but never finished leaves no other trace.
|
|
42
|
+
- **The evidence so far is shipped** — outbox, parcels, transcript. `close_batch` is the only other
|
|
43
|
+
flush and it runs at the end, so before this a run that died mid-flight shipped *nothing*: its
|
|
44
|
+
events sat in the local outbox with no scheduled drain, and its transcript, which has no outbox at
|
|
45
|
+
all, was simply lost.
|
|
46
|
+
- **The entering chapter's expanded instructions come back.** The resident contract is a skeleton on
|
|
47
|
+
measured evidence that clients truncate it at ~2 KB, so per-step doctrine increasingly arrives
|
|
48
|
+
here — at the moment it applies, rather than an hour early in the playbook.
|
|
49
|
+
|
|
50
|
+
Small batches legitimately skip `partitioning`, `parallel-analysis` and `clarification`; the rest are
|
|
51
|
+
load-bearing and skipping one is refused by name. Ending a run early is legal from anywhere.
|
|
52
|
+
**`advance_phase` never replaces `close_batch`** — entering the last chapter is not closing the batch.
|
|
53
|
+
|
|
54
|
+
`resume_session {sessionId}` picks up a run a previous session left unfinished: it reconstructs the
|
|
55
|
+
position from the ledger plus the artifacts, reports where the two disagree rather than reconciling
|
|
56
|
+
them, and **checks the org auth instead of assuming it** — an activation-minted token lasts about a
|
|
57
|
+
day, so the commonest resume has intact artifacts and a dead org, which is a card for the operator
|
|
58
|
+
and never a re-authentication.
|
|
59
|
+
|
|
60
|
+
## Tools
|
|
61
|
+
|
|
62
|
+
| Group | Tools |
|
|
63
|
+
|-------|-------|
|
|
64
|
+
| Session + auth | `activate_session`, `create_session`, `import_requirements`, `org_login` |
|
|
65
|
+
| Grounding | `project_org`, `sf_read`, `list_components`, `describe_object` |
|
|
66
|
+
| Delivery | `plan_waves`, `direct_lint`, `direct_apply` |
|
|
67
|
+
| History | `direct_history` |
|
|
68
|
+
| Rollback | `direct_rollback` |
|
|
69
|
+
| Operator | `ask_operator`, `resolve_blocker` |
|
|
70
|
+
| Run structure | `advance_phase`, `resume_session`, `close_batch` |
|
|
71
|
+
| Self-update | `update_server` (read-only unless `apply: true` — `npm install -g`s the published version, which the NEXT conversation runs; the current session keeps its loaded build) |
|
|
72
|
+
| Campfire writeback | `read_campfire_requirements`, `update_campfire_requirements`, `campfire_config_item_plan`, `campfire_write_config_items`, `create_campfire_deployment_tasks`, `create_campfire_requirements`, `mirror_requirements` |
|
|
73
|
+
|
|
74
|
+
`direct_apply` verdicts: `LINT_BLOCKED` | `REJECTED_BY_ORG` | `VALIDATED` (dry run) | `DIFF_VERIFIED`
|
|
75
|
+
| `DELIVERED_WITH_DRIFT` | `AMBIGUOUS_DELIVERY`. `DIFF_VERIFIED` is mechanical read-back proof of
|
|
76
|
+
metadata **shape** — never runtime behavior.
|
|
77
|
+
|
|
78
|
+
## Org history
|
|
79
|
+
|
|
80
|
+
Each state-changing verdict (`DIFF_VERIFIED`, `DELIVERED_WITH_DRIFT`, `AMBIGUOUS_DELIVERY`) overlays
|
|
81
|
+
its post-image and its deployed source XML onto a per-org mirror at
|
|
82
|
+
`.local/cc-mcp/orgrepo/<orgId>/` and commits — **only if the tree changed**, so a re-delivery of
|
|
83
|
+
unchanged content records nothing. The other three verdicts never reach it. `direct_history` reads
|
|
84
|
+
it back; no operator-facing git concept exists above that tool.
|
|
85
|
+
|
|
86
|
+
The commit is a **post-verdict side effect**: a mirror that cannot write lands a note and never
|
|
87
|
+
fails, delays, or masks a delivery. Materialization is deterministic — fixed identity, timestamps
|
|
88
|
+
from the bundle's mutation window, a total sort key of (window start, session id, wave) — so any
|
|
89
|
+
machine holding the same evidence bundles rebuilds byte-identical history. That also means **SHAs
|
|
90
|
+
are display aliases**: a rebuild rewrites every one, so nothing may persist them (address a wave by
|
|
91
|
+
`(sessionId, wave)`).
|
|
92
|
+
|
|
93
|
+
Two honest limits, both surfaced rather than papered over. The mirror covers **what this install
|
|
94
|
+
delivered** — a client admin's edit in Setup and another consultant's wave from another machine are
|
|
95
|
+
both invisible until IN2 next touches the component, when it is reported as "changed outside this
|
|
96
|
+
machine's history". And it is a **rebuildable cache**, derived entirely from the evidence bundles:
|
|
97
|
+
losing it loses nothing the bundles cannot re-materialize.
|
|
98
|
+
|
|
99
|
+
### Rebuilding, and uniting two machines
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
in2 rebuild-mirror <orgId> this machine's bundles only
|
|
103
|
+
in2 rebuild-mirror <orgId> --fetch also pull other machines' parcels for that org
|
|
104
|
+
in2 rebuild-mirror <orgId> --fetch --batch <id> fetch under one named batch's grant
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A rebuild is always **from scratch, never an append** — which is what lets a foreign wave older than
|
|
108
|
+
everything local take its correct place in history without a merge: the sort key is total, so it
|
|
109
|
+
simply sorts first. A wave present both as a local bundle and as this machine's own uploaded parcel
|
|
110
|
+
is replayed once. Every commit carries a `Materializer:` trailer, and a mirror holding a version
|
|
111
|
+
other than the running build's is reported and fully re-derived rather than appended to.
|
|
112
|
+
|
|
113
|
+
The result always states its **scope**, because a local-only history and an org-wide one look
|
|
114
|
+
identical from the outside: `--fetch` that could not run (offline, no live grant, route down) is
|
|
115
|
+
never a failure — the rebuild proceeds on local bundles and says that waves run elsewhere are
|
|
116
|
+
absent, and that their absence is not evidence that none exist.
|
|
117
|
+
|
|
118
|
+
## Rollback
|
|
119
|
+
|
|
120
|
+
`direct_rollback {sessionId, wave}` undoes a delivered wave, addressed by that rebuild-stable pair
|
|
121
|
+
and never by a SHA. It classifies every selected component before anything runs and changes nothing
|
|
122
|
+
until it is called again with `mode: "execute"`; execution goes through the same apply loop, so a
|
|
123
|
+
rollback earns its own evidence bundle and its commit is marked `Revert-Of: <session>/<wave>`.
|
|
124
|
+
|
|
125
|
+
**Partial by default** — a component this refuses never blocks the rest of the selection. What each
|
|
126
|
+
class gets: a component the wave *created* is deleted; one it *modified* is restored from the
|
|
127
|
+
bundle's `pre/source/` image (ack-gated when that image declared an unread facet); one it *deleted*
|
|
128
|
+
under 15 days ago is **carded for a Setup undelete, never re-created** — the undelete restores the
|
|
129
|
+
field *and its data*, a re-create restores the definition and destroys every value while reporting
|
|
130
|
+
success; a delete past the window is a re-create behind a per-component ack naming the loss; a
|
|
131
|
+
component that is live again is reported and left alone; and an undo the Metadata API cannot perform
|
|
132
|
+
(a picklist value set moved in either direction, a created RecordType, a granted CMT access) is
|
|
133
|
+
refused, naming the limitation. Acks are one key per component (`ackComponents:
|
|
134
|
+
["CustomField:Account.Region__c"]`) — there is no bulk yes, one key accepts every reason listed
|
|
135
|
+
against that component (the plan prints them all, and says how many the key covers), and a key
|
|
136
|
+
matching no classified component is reported rather than dropped.
|
|
137
|
+
|
|
138
|
+
**No ack releases an in-window field** (AE4). The class decides that, not the gate: whatever else is
|
|
139
|
+
true of the component — including an R16 divergence, which for a delete means the freed API name has
|
|
140
|
+
been re-occupied — it is carded, and `rollbackWork` throws rather than let one reach the deploy. When
|
|
141
|
+
the preflight does find the name occupied, the card says so, because that is what blocks the
|
|
142
|
+
rename-back after the undelete.
|
|
143
|
+
|
|
144
|
+
The undelete window is re-checked at execution, per field, and `mode: "verify"` is the wave that
|
|
145
|
+
follows the operator's undelete: it reads the restored field under both its own and its `_del`
|
|
146
|
+
name and diffs it against the wave's pre-image, reporting what the undelete did not restore.
|
|
147
|
+
|
|
148
|
+
## Read provenance and the contamination report
|
|
149
|
+
|
|
150
|
+
Every org-reading call appends one record to `<session>/reads.jsonl` — tool, org id, requested
|
|
151
|
+
scope, the component keys it returned (`Type:FullName`), and an **access pattern**: `enumeration`
|
|
152
|
+
(listed the family), `targeted` (named the component), `body` (read its definition). A read whose
|
|
153
|
+
component identity cannot be derived appends an **opaque** record rather than nothing. Identifiers
|
|
154
|
+
only: raw query text stays in `<session>/sf-read/`, and nothing here reaches the event tables.
|
|
155
|
+
|
|
156
|
+
At batch end that read-set is intersected with what **prior sessions** delivered into the same org,
|
|
157
|
+
per the mirror, and the graded result lands in `<session>/CONTAMINATION.md` (also on demand via
|
|
158
|
+
`direct_history {action: "contamination"}`). It answers measurement validity — did a run scored
|
|
159
|
+
against a corpus read the previous arm's work? — and it is **positive-evidence-only**:
|
|
160
|
+
|
|
161
|
+
- Enumeration overlap is reported, never as an accusation. Every run lists shared objects.
|
|
162
|
+
- A targeted or body read of a predecessor-delivered component is named individually.
|
|
163
|
+
- The opaque-read count is printed beside every result, because it bounds any claim made from it.
|
|
164
|
+
- An empty mirror reports **"no prior arms recorded on this machine"** — never "clean".
|
|
165
|
+
|
|
166
|
+
> The certified/compiler tier (14 tools: `compile_requirement`, `lower_contract`,
|
|
167
|
+
> `approve_requirement`, `apply`, `verify`, `record_analysis`, `settle_dispositions`, …) was removed
|
|
168
|
+
> on 2026-07-26 — no users, zero calls in telemetry, and the only reason this package depended on
|
|
169
|
+
> the workspace. Recover from git history if a signed evidence chain is ever genuinely needed.
|
|
170
|
+
|
|
171
|
+
## Build and test
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
pnpm --filter in2-agent-server build # tsc -> dist/
|
|
175
|
+
pnpm --filter in2-agent-server test # vitest
|
|
176
|
+
pnpm --filter in2-agent-server release # stamp version from the commit, stage payload, npm publish
|
|
177
|
+
pnpm --filter in2-agent-server stage:payload # just the staging step (skills/agents into the package)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`release` derives the version from the release commit's timestamp (`0.YYYYMMDD.HHMMSS`, semver-legal);
|
|
181
|
+
the repo's `package.json` stays `0.1.0` so a checkout is recognizably not a release. `release:dry`
|
|
182
|
+
does everything but publish.
|
|
183
|
+
|
|
184
|
+
## Register with Claude Code
|
|
185
|
+
|
|
186
|
+
Consultant machines never do this by hand — `in2 setup` (from the published package) registers the
|
|
187
|
+
server, sets the env gates on the registration (`CC_MCP_LAUNCH_URL`, `CC_MCP_DIRECT_WRITE=1`,
|
|
188
|
+
`CC_MCP_CAMPFIRE_WRITE=1`, `CC_MCP_BETA=1`, and `CC_MCP_SF_ENTRY` on Windows), installs the skills
|
|
189
|
+
and agents, and probes the server. For a dev checkout, register the built dist directly:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
claude mcp add cc-mcp --scope user -- node <repo>/apps/cc-mcp-server/dist/index.js
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Use an explicit Node **>= 24** binary rather than PATH `node` — Claude Code's PATH may resolve an
|
|
196
|
+
older nvm default, which breaks `project_org`'s live reader (jsforce/undici; live-caught on v20.20.2).
|
|
197
|
+
|
|
198
|
+
Server env is fixed at spawn, so a gate the run needs must be set **before** the session starts;
|
|
199
|
+
the server logs every gate's state on connect for exactly this reason.
|
|
200
|
+
|
|
201
|
+
## Safety model
|
|
202
|
+
|
|
203
|
+
- **Write authority is scoped to the org an activation authenticated.** `CC_MCP_DIRECT_WRITE=1` is a
|
|
204
|
+
*capability* ("this install may write metadata"), not an *authority* ("to this org"). Authority
|
|
205
|
+
comes from a grant minted by `activate_session` only after the sf CLI authenticated an org **and**
|
|
206
|
+
it matched the org the launch service independently named. A grant names one Org ID, is recorded
|
|
207
|
+
in `direct-write-grants.json` (0600, identity only — never a token), and expires
|
|
208
|
+
(`CC_MCP_GRANT_TTL_DAYS`, default 30).
|
|
209
|
+
|
|
210
|
+
This exists because the alternative does not scale: an Org ID is unknowable until someone logs in,
|
|
211
|
+
so a customer can never be pre-listed — which pushed internal testing to
|
|
212
|
+
`CC_MCP_DIRECT_ALLOWLIST=*`. That wildcard makes **every** org the machine's sf CLI holds
|
|
213
|
+
writable; on a consultant's laptop that includes their own production org and their other
|
|
214
|
+
clients'. Grants replace it for customers, and the plugin's `run-server.sh` explicitly unsets it.
|
|
215
|
+
|
|
216
|
+
- **Check order, and it matters:** consultant-org refusal → activation grant → wildcard → env
|
|
217
|
+
allowlist. The consultant org (VF-partial, `00DRu00000Mpj50MAB`) is refused **first, by Org ID**,
|
|
218
|
+
so no grant and no allowlist setting can reach it.
|
|
219
|
+
|
|
220
|
+
- **Campfire writes ride a separate gate.** The consultant org is REAL and SHARED, so
|
|
221
|
+
`CC_MCP_CAMPFIRE_WRITE` is deliberately distinct from metadata-write authority: permission to
|
|
222
|
+
deploy to a client org must never imply permission to write the shared org.
|
|
223
|
+
|
|
224
|
+
- **Fail-closed reads.** An org-read error is never reported as "does not exist" — including a
|
|
225
|
+
component the org lists but cannot read (orphaned metadata), which records as absent *with a
|
|
226
|
+
reason* rather than aborting the projection.
|
|
227
|
+
|
|
228
|
+
- **Idempotent Campfire writes.** Every row is keyed by `External_ID__c`; the batch writer derives
|
|
229
|
+
each key from the component, so re-running updates instead of duplicating.
|
|
230
|
+
|
|
231
|
+
- **Observability never participates.** Campfire-started sessions record `batchId` and immutable
|
|
232
|
+
requirement revision/content hashes, and every reported outcome is read from what the delivery
|
|
233
|
+
loop already produced. No reporting failure — no database, no grant, no network — can fail a
|
|
234
|
+
boot, a request, or a tool, and no telemetry code path writes to an org or adds a step to the
|
|
235
|
+
run. Local evidence remains the authority on what was delivered.
|
|
236
|
+
|
|
237
|
+
## Operational telemetry
|
|
238
|
+
|
|
239
|
+
Local timing remains in `.local/cc-mcp/telemetry/<runId>.jsonl`. After a Campfire activation, the
|
|
240
|
+
server also binds `batchId` + install/process/session/version and appends authenticated v2 events to
|
|
241
|
+
`.local/cc-mcp/telemetry-outbox/<runId>.jsonl` before network delivery. The outbox is chmod 600
|
|
242
|
+
where supported, deletes only acknowledged event IDs, and has hard byte/age caps. A
|
|
243
|
+
full/unwritable outbox produces one bounded warning and never blocks a tool.
|
|
244
|
+
|
|
245
|
+
Four cadences ship it, because a process that dies mid-batch must not take its ledger with it:
|
|
246
|
+
|
|
247
|
+
- **every 20 seconds**, while the process lives;
|
|
248
|
+
- **at each `deployment.wave_completed`** — a wave boundary is rare and its payload is small, and it
|
|
249
|
+
is the last durable moment before the org round-trip a run is most likely not to return from;
|
|
250
|
+
- **at `close_batch`**, the explicit end, which now drains the whole outbox rather than sending one
|
|
251
|
+
200-event batch. A real run emits far more than that, so a single send left the remainder to a
|
|
252
|
+
timer that was about to stop ticking;
|
|
253
|
+
- **at server start**, which drains whatever a PREVIOUS run stranded on this machine. Everything in
|
|
254
|
+
the outbox at that instant belongs to a prior process, so the recovery window is the next session
|
|
255
|
+
on that machine rather than "some later run happens to live long enough for its timer".
|
|
256
|
+
|
|
257
|
+
Both drains loop past the 200-event delivery cap and stop on the first pass that makes no progress,
|
|
258
|
+
so an unreachable ledger or an expired grant costs exactly one POST. Whatever they could not ship
|
|
259
|
+
stays on disk and is reported — at startup only when something was actually recovered, at close as a
|
|
260
|
+
count of what is still queued, because "could not ship" and "shipped" must not look alike.
|
|
261
|
+
|
|
262
|
+
Events a run left behind are therefore delayed, not lost. Previously they were durable but nothing
|
|
263
|
+
ever performed the replay, and a killed run's whole batch sat on disk until the age cap dropped it.
|
|
264
|
+
|
|
265
|
+
The batch-scoped bearer grant is stored separately under
|
|
266
|
+
`.local/cc-mcp/telemetry-grants/<batchId>.json` (chmod 600). It authorizes ingest for one
|
|
267
|
+
batch/install only and is never written to a session, evidence bundle, repository, argv, or log.
|
|
268
|
+
The server stores only its hash. Remote delivery is fail-open; `CC_MCP_TELEMETRY_REMOTE=0` opts out
|
|
269
|
+
without disabling the local timing log.
|
|
270
|
+
|
|
271
|
+
`scripts/telemetry-report.mjs` accepts either a local JSONL file or the JSON returned by the
|
|
272
|
+
protected central batch-detail endpoint. Agent/operator gap classifications remain estimates: the
|
|
273
|
+
MCP server cannot see exact model time, native client tools, or question-card time.
|
|
274
|
+
|
|
275
|
+
### The org-call meter
|
|
276
|
+
|
|
277
|
+
Each `deployment.wave_completed` carries where the wave's **org time** went: `orgCallCount` /
|
|
278
|
+
`orgCallMs`, split by kind into `orgCliCount` / `orgCliMs` and `orgApiCount` / `orgApiMs`, plus
|
|
279
|
+
`orgFirstCallMs` and a per-phase `orgCallsByPhase` (`preProjection`, `postProjection`,
|
|
280
|
+
`preImageRetrieve`, `checkOnly`, `destructiveCheckOnly`, `deploy`, `destructive`, `facetUpdate`,
|
|
281
|
+
`historyProbe`, `changeScan`, `other`).
|
|
282
|
+
|
|
283
|
+
A Windows run was observed taking up to ~8 minutes per wave against a Mac's ~34s median, and nothing
|
|
284
|
+
in the ledger could apportion it.
|
|
285
|
+
|
|
286
|
+
**The split by kind is the point.** The intuitive culprit — many expensive `sf` CLI spawns — is not
|
|
287
|
+
what happens: the direct tier spawns `sf` exactly **three** times per wave (retrieve, deploy,
|
|
288
|
+
destructive deploy), the org mirror spawns `git` only, and every projection read goes through
|
|
289
|
+
jsforce, already batched at the API cap of 10 with 5 in flight. A projection makes **tens** of
|
|
290
|
+
round-trips, so counting spawns alone would measure the three cheapest events in the wave. CLI cost
|
|
291
|
+
is process startup — and on Windows an `sf.cmd` shim resolved to its JS entry and run under this
|
|
292
|
+
process's node; API cost is network latency. They have opposite fixes.
|
|
293
|
+
|
|
294
|
+
**`orgFirstCallMs` separates startup from latency.** A cost concentrated in the first call is
|
|
295
|
+
startup — connection setup, module load, an on-access AV scan of a freshly touched binary. A cost
|
|
296
|
+
spread evenly is latency. Steady state is derivable as `(orgCallMs - orgFirstCallMs) / (orgCallCount
|
|
297
|
+
- 1)`; a first call far above it points at the machine, one in line with it points at the link.
|
|
298
|
+
|
|
299
|
+
Counts and milliseconds only, and the phase names are a closed set the ingest validator enforces:
|
|
300
|
+
argv and SOQL are where org content lives, and the ledger is shape-only by contract. The meter is
|
|
301
|
+
cleared at each wave's entry, counts failures as well as successes (a call that died still cost the
|
|
302
|
+
wall clock), and excludes the mirror's local git — folding that in would make the number mean two
|
|
303
|
+
things. Absent on a wave that never reached the org, and on installs predating it.
|
|
304
|
+
|
|
305
|
+
## Provenance parcels
|
|
306
|
+
|
|
307
|
+
Every wave that changes the org gzips its evidence bundle and the session's `reads.jsonl` into one
|
|
308
|
+
parcel and posts it to `/api/parcel/v1` under the same batch grant telemetry and transcripts use.
|
|
309
|
+
Entries sit at their real session paths (`<sessionId>/direct/<wave>/…`, `<sessionId>/reads.jsonl`)
|
|
310
|
+
so a fetched parcel unpacks exactly where a rebuild expects to find it. **On by default**;
|
|
311
|
+
`CC_MCP_PARCEL_UPLOAD=0` opts out and says so once on stderr.
|
|
312
|
+
|
|
313
|
+
The client sends exactly one descriptive header, `x-in2-parcel-wave`. It does **not** state the
|
|
314
|
+
org: parcels are fetchable by another machine, so the ledger treats `orgId` as a permission
|
|
315
|
+
boundary and resolves it from the grant's own batch. Org, attempt and verdict travel inside the tar
|
|
316
|
+
in `parcel.json`, which is where a rebuild reads them.
|
|
317
|
+
|
|
318
|
+
The default is disclosed rather than assumed: most of a parcel restructures content the transcript
|
|
319
|
+
already carries, but `pre/source/` and the pre-image JSON are org state the tools retrieved and the
|
|
320
|
+
model never read, so the install prompt names it explicitly (`startingPrompt` in campfire-launch,
|
|
321
|
+
guarded by a test).
|
|
322
|
+
|
|
323
|
+
The same two routes read back. `GET /api/parcel/v1` lists what this grant's org holds and
|
|
324
|
+
`GET /api/parcel/v1/blob?parcelId=` returns a short-lived signed URL — again with no org on the way
|
|
325
|
+
out, so the client checks the org the ledger names in the response and skips a grant bound
|
|
326
|
+
elsewhere. `in2 rebuild-mirror <orgId> --fetch` is the consumer: it stages each parcel under
|
|
327
|
+
`.local/cc-mcp/parcel-fetch/<orgId>/` (deliberately **outside** the session workspace, so a fetched
|
|
328
|
+
bundle is never counted as this machine's own work) and hands the unpacked bundle directories to the
|
|
329
|
+
rebuild. The grant is never sent to the storage host the signed URL points at, and every tar entry
|
|
330
|
+
path is checked before a byte is written.
|
|
331
|
+
|
|
332
|
+
Upload is fail-open in the same sense as the org mirror: the apply loop only awaits the local
|
|
333
|
+
tar-and-spool step, the network flush is fire-and-forget, and nothing here can fail, delay, or mask
|
|
334
|
+
a delivery that landed. Failures land in `.local/cc-mcp/parcel-outbox` (chmod 700, byte/age capped)
|
|
335
|
+
and replay on the next flush — after each wave and once more before the process exits, so a parcel
|
|
336
|
+
does not outlive the grant that is the only thing able to attribute it. A parcel still queued at
|
|
337
|
+
exit gets a line on stderr; one the server permanently rejects is dropped with its reason. Every
|
|
338
|
+
parcel's compressed and raw size is logged and recorded on the wave's verdict, which is the input
|
|
339
|
+
the deferred retention decision needs.
|
|
340
|
+
|
|
341
|
+
## Env
|
|
342
|
+
|
|
343
|
+
| Var | Meaning |
|
|
344
|
+
|-----|---------|
|
|
345
|
+
| `CC_MCP_WORKSPACE_ROOT` | Session workspace root (default `<repo>/.local/cc-mcp/sessions`) |
|
|
346
|
+
| `CC_MCP_ORGREPO_ROOT` | Org-mirror root (default `<workspace>/../orgrepo`, i.e. `.local/cc-mcp/orgrepo`) |
|
|
347
|
+
| `CC_MCP_OPERATOR` | Operator identity for the mirror's `Operator:` trailer (default: the session's authenticated username) |
|
|
348
|
+
| `CC_MCP_DIRECT_WRITE` | `1` opens metadata writes (`direct_apply`); consultant org hard-refused regardless |
|
|
349
|
+
| `CC_MCP_DIRECT_ALLOWLIST` | **Internal dev only.** Comma-separated Org IDs (default T-Pinto); `*` allows every authenticated org. Must NOT ship — on a consultant's machine that includes their own production org. Customer installs get authority from an activation grant instead. |
|
|
350
|
+
| `CC_MCP_GRANT_TTL_DAYS` | Lifetime of an activation grant (default 30). A grant outlives a session, not the engagement. |
|
|
351
|
+
| `CC_MCP_CAMPFIRE_WRITE` | `1` opens LIVE writes to the Campfire consultant org |
|
|
352
|
+
| `CC_MCP_CAMPFIRE_ALLOWLIST` | Comma-separated Org IDs allowed for Campfire writes (default VF-partial) |
|
|
353
|
+
| `CC_MCP_TELEMETRY` | `0` disables per-call timing to `.local/cc-mcp/telemetry/<runId>.jsonl` — and, because it short-circuits the whole sink, also stops transcript, parcel, operator-Q&A and beta-feedback upload. One switch, all egress |
|
|
354
|
+
| `CC_MCP_TELEMETRY_REMOTE` | `0` disables durable remote delivery while retaining local timing |
|
|
355
|
+
| `CC_MCP_TRANSCRIPT_UPLOAD` | `0` stops the full Claude Code session text shipping at exit. **On by default**; timings and outcomes still ship |
|
|
356
|
+
| `CC_MCP_PARCEL_UPLOAD` | `0` stops each state-changing wave's provenance parcel shipping — the evidence bundle (including the pre-image the tools retrieved from the org) plus the session's read log. **On by default**; named in the install disclosure |
|
|
357
|
+
| `CC_MCP_OPERATOR_QA_UPLOAD` | `0` sends the transcript without a queryable table of what each question card asked and what the operator answered. **On by default**, and `CC_MCP_TRANSCRIPT_UPLOAD=0` stops it too — the Q&A words are part of the session text that install declined |
|
|
358
|
+
| `CC_MCP_BETA` | `1` turns on the beta feedback channel: registers `report_feedback` and `record_operator_feedback`, and appends a beta section to the supervisor contract. **OFF by default — the one inverted switch in this family**, because an install that never opted in must look exactly like one that predates the feature. `in2 setup` writes it into the registration env, so it travels with the install across `npm install -g` updates |
|
|
359
|
+
| `CC_MCP_FEEDBACK_OUTBOX` | Beta feedback outbox directory (default `.local/cc-mcp/feedback-outbox`) — its own directory beside the telemetry one, which is what keeps a prose record structurally unable to reach the shape-only endpoint |
|
|
360
|
+
| `CC_MCP_PARCEL_OUTBOX` | Parcel outbox directory (default `.local/cc-mcp/parcel-outbox`) |
|
|
361
|
+
| `CC_MCP_PARCEL_STAGE` | Where `rebuild-mirror --fetch` unpacks fetched parcels (default `.local/cc-mcp/parcel-fetch`); kept outside the session workspace on purpose |
|
|
362
|
+
| `CC_MCP_LAUNCH_URL` | Launch origin used for activation and authenticated telemetry delivery — activation + ingest only; release checks go to the npm registry, not here |
|
|
363
|
+
| `CC_MCP_REPO_ROOT` | Override monorepo root resolution (rarely needed) |
|
|
364
|
+
|
|
365
|
+
## Claude Code harness
|
|
366
|
+
|
|
367
|
+
`harness/supervisor-playbook.md` is the supervisor workflow and `harness/skills/plan-requirement/SKILL.md` is the
|
|
368
|
+
per-requirement analyzer skill. `pnpm sync:plugin` generates the plugin's copies from these, so the
|
|
369
|
+
harness stays the single source; `pnpm check:plugin` fails when they drift.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-requirement
|
|
3
|
+
description: "Analyze ONE Salesforce requirement (or a coupled group, or one fragment of an oversized requirement) against the supervisor's shared projection and analysis brief: decompose it into deliverable components, author the unblocked ones, lint them, and write plan.json. Use for every parallel requirement-analysis Task the IN2 MCP supervisor fans out — REQUIRES the in2 MCP server connected (all org access rides mcp__in2__* tools; there is no Bash). Never use on the bare/CLI harness, whose analyzers need sf + lint.mjs via Bash. Reads the org read-only and never writes to it."
|
|
4
|
+
tools: Read, Write, Edit, Glob, Grep, TodoWrite, mcp__in2__describe_object, mcp__in2__list_components, mcp__in2__sf_read, mcp__in2__direct_lint, mcp__in2__validate_components, mcp__in2__report_feedback
|
|
5
|
+
---
|
|
6
|
+
<!-- GENERATED from apps/cc-mcp-server/harness/agents/plan-requirement.md by scripts/sync-plugin-skills.mjs.
|
|
7
|
+
source-sha256: 46ae1e6e3df54b5b
|
|
8
|
+
Edit that source file, then run `npm run sync:plugin`. Do not edit this copy. -->
|
|
9
|
+
|
|
10
|
+
# IN2 requirement analyzer
|
|
11
|
+
|
|
12
|
+
Read `skills/plan-requirement/SKILL.md` — it ships with this install at a fixed path and is the
|
|
13
|
+
complete contract for this job. Follow it exactly; nothing below replaces it.
|
|
14
|
+
|
|
15
|
+
Your Task prompt names the requirement(s) or fragment you own, the session, and the absolute path
|
|
16
|
+
to `analysis/brief.md`. Read your section(s) of the brief FIRST, then the requirement prose.
|
|
17
|
+
|
|
18
|
+
## Why your tool list is short
|
|
19
|
+
|
|
20
|
+
You have six IN2 tools and the file tools, and that is deliberate — it is not a restriction added
|
|
21
|
+
on top of the skill, it is the skill's own "Never" list made mechanical:
|
|
22
|
+
|
|
23
|
+
- `describe_object`, `list_components`, `sf_read` — read the org when the projection falls short.
|
|
24
|
+
- `direct_lint` — lint the files you authored, before you write your plan.
|
|
25
|
+
- `validate_components` — ask the org whether it would ACCEPT what you authored. A checkOnly, so it
|
|
26
|
+
changes nothing; scoped to the components your own plan.json owns.
|
|
27
|
+
- `report_feedback` — beta installs only; file stumbles before you return.
|
|
28
|
+
|
|
29
|
+
`project_org`, `direct_apply` and every writer are the supervisor's, after the operator approves.
|
|
30
|
+
You were never permitted to call them, so they are not loaded, and the ~21 tool definitions you
|
|
31
|
+
will never use are not in your context either. If a tool you expected is missing, that is this
|
|
32
|
+
allowlist and not a broken install — say so in your returned summary rather than working around it.
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Campfire Configuration Item derivation — the executable form of VFP's
|
|
2
|
+
// "Campfire Configuration Items - Common Types Guide" (v12, 2025-08-08, re-synced against VFP's
|
|
3
|
+
// 2026-08-19 revision; a copy lives at harness/reference/campfire-configuration-items-guide.md).
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS: a Configuration Item is not a free-text note, it is a DEPLOYMENT MANIFEST
|
|
6
|
+
// ROW. Campfire composes `ClickDeploy_Text__c` from Type + Configuration Item API Name +
|
|
7
|
+
// Object API Name + Folder API Name, and that text is what gets pasted into ClickDeploy to move
|
|
8
|
+
// the work between orgs. So the fullName the direct tier carries internally
|
|
9
|
+
// ("CF63_Intake_Request__c.CF63_Notes__c", "unfiled$public/My_Template",
|
|
10
|
+
// "Obj__c-My Layout") must be SPLIT into the right fields — writing the composite into the API
|
|
11
|
+
// Name field produces a manifest row that silently fails to resolve in ClickDeploy.
|
|
12
|
+
//
|
|
13
|
+
// Campfire's Type picklist is ALSO not the SDR registry vocabulary: FlexiPage -> LightningPage,
|
|
14
|
+
// CustomApplication -> App, CustomTab -> Tab, WorkflowAlert -> WorkflowEmailAlert,
|
|
15
|
+
// VisualforcePage -> ApexPage, SharingRules -> SharingCriteriaRule | SharingOwnerRule.
|
|
16
|
+
//
|
|
17
|
+
// The mappings below were validated against the org's ACTUAL restricted picklist (vfppartial,
|
|
18
|
+
// 2026-07-25), which corrected the guide's prose in two places: it writes "Approval Process"
|
|
19
|
+
// and "Email Templates", but the only values the picklist accepts are `ApprovalProcess` and
|
|
20
|
+
// `EmailTemplate`. Type__c is RESTRICTED, so a prose-faithful value would have been rejected on
|
|
21
|
+
// save. The picklist also has `Queue`, which the guide never mentions. Where the guide and the
|
|
22
|
+
// org disagree, the ORG wins — it is the thing that has to accept the write.
|
|
23
|
+
// Keyed by the metadata type name the direct tier uses (SDR registry names).
|
|
24
|
+
export const CI_RULES = {
|
|
25
|
+
// --- object + children -------------------------------------------------------------
|
|
26
|
+
CustomObject: { campfireType: "CustomObject", shape: "bare", note: "Custom Metadata Types and Custom Settings are ALSO Type=CustomObject." },
|
|
27
|
+
CustomField: { campfireType: "CustomField", shape: "objectChild", note: "Object API Name may be an object, a __mdt type, or a Custom Setting." },
|
|
28
|
+
StandardField: { campfireType: "StandardField", shape: "objectChild" },
|
|
29
|
+
RecordType: { campfireType: "RecordType", shape: "objectChild" },
|
|
30
|
+
ListView: { campfireType: "ListView", shape: "objectChild" },
|
|
31
|
+
CompactLayout: { campfireType: "CompactLayout", shape: "objectChild" },
|
|
32
|
+
ValidationRule: { campfireType: "ValidationRule", shape: "objectChild" },
|
|
33
|
+
FieldSet: { campfireType: "FieldSet", shape: "objectChild" },
|
|
34
|
+
WebLink: { campfireType: "WebLink", shape: "objectChild", note: "Custom buttons." },
|
|
35
|
+
Layout: { campfireType: "Layout", shape: "layout", note: "API Name is the layout's LABEL, not an api name. Package layouts need the package prefix." },
|
|
36
|
+
// --- actions -----------------------------------------------------------------------
|
|
37
|
+
QuickAction: {
|
|
38
|
+
campfireType: "QuickAction",
|
|
39
|
+
shape: "objectChildOptional",
|
|
40
|
+
note: "Object-specific action -> Object API Name set. GLOBAL action -> Object API Name must be BLANK."
|
|
41
|
+
},
|
|
42
|
+
// --- automation --------------------------------------------------------------------
|
|
43
|
+
// No static note. "Flows deploy INACTIVE" was printed on every Flow row as though it were a fact
|
|
44
|
+
// about THIS deployment; CF76 verified both flows landed Active and the note was simply false.
|
|
45
|
+
// A per-type string can only say what is USUALLY true, and a manifest row is a statement about
|
|
46
|
+
// what happened. The observed status is read from the bundle's post projection and attached to the
|
|
47
|
+
// delivery's `deliveredConfiguration` in deriveFromBundle (see observedFlowFacets), where the
|
|
48
|
+
// description generator projects it — never assumed, and never taken from the submitted XML.
|
|
49
|
+
Flow: { campfireType: "Flow", shape: "bare" },
|
|
50
|
+
WorkflowAlert: { campfireType: "WorkflowEmailAlert", shape: "objectChild" },
|
|
51
|
+
WorkflowFieldUpdate: { campfireType: "WorkflowFieldUpdate", shape: "objectChild" },
|
|
52
|
+
WorkflowOutboundMessage: { campfireType: "WorkflowOutboundMessage", shape: "objectChild" },
|
|
53
|
+
WorkflowRule: { campfireType: "WorkflowRule", shape: "objectChild" },
|
|
54
|
+
WorkflowTask: { campfireType: "WorkflowTask", shape: "objectChild" },
|
|
55
|
+
AssignmentRule: { campfireType: "AssignmentRule", shape: "objectChild" },
|
|
56
|
+
ApprovalProcess: {
|
|
57
|
+
campfireType: "ApprovalProcess",
|
|
58
|
+
shape: "objectChild",
|
|
59
|
+
note: "Guide prose says \"Approval Process\" but the restricted picklist value is ApprovalProcess (verified live). Its Workflow Field Updates / Email Alerts must ship in the same deployment."
|
|
60
|
+
},
|
|
61
|
+
DuplicateRule: { campfireType: "DuplicateRule", shape: "objectChild" },
|
|
62
|
+
MatchingRule: { campfireType: "MatchingRule", shape: "objectChild" },
|
|
63
|
+
// --- metadata records --------------------------------------------------------------
|
|
64
|
+
CustomMetadata: {
|
|
65
|
+
campfireType: "CustomMetadata",
|
|
66
|
+
shape: "objectChild",
|
|
67
|
+
note: "The RECORD. Object API Name is the __mdt type. (The TYPE itself is Type=CustomObject.) DLRS rollups use Object API Name dlrs__LookupRollupSummary2."
|
|
68
|
+
},
|
|
69
|
+
// --- access ------------------------------------------------------------------------
|
|
70
|
+
PermissionSet: { campfireType: "PermissionSet", shape: "bare" },
|
|
71
|
+
PermissionSetGroup: { campfireType: "PermissionSetGroup", shape: "bare", note: "Package PSGs need the package prefix." },
|
|
72
|
+
MutingPermissionSet: { campfireType: "MutingPermissionSet", shape: "bare" },
|
|
73
|
+
Profile: { campfireType: "Profile", shape: "bare" },
|
|
74
|
+
CustomPermission: { campfireType: "CustomPermission", shape: "bare" },
|
|
75
|
+
Group: { campfireType: "Group", shape: "bare", note: "Public groups." },
|
|
76
|
+
// Live-caught 2026-08-14 (Renner QA, ses_09b70d19): three delivered Roles were dropped from the
|
|
77
|
+
// writeback because this row was missing — the run called it "a mapping-table gap in my tooling"
|
|
78
|
+
// and it was. Role IS a legal restricted-picklist value (re-verified live 2026-08-18, 165 values).
|
|
79
|
+
// VFP's 2026-08-19 guide revision then ADDED a Role section that agrees with this row on both
|
|
80
|
+
// counts: Type=Role, and "the Role Name" — the field its screenshot rings on the Role Edit page,
|
|
81
|
+
// which sits beside Label and is the developer name. `bare` + DeveloperName is what they specify.
|
|
82
|
+
Role: { campfireType: "Role", shape: "bare", note: "The fullName is the role's DeveloperName (e.g. Broker), not its label — the guide's \"Role Name\" field, not the Label beside it." },
|
|
83
|
+
Queue: { campfireType: "Queue", shape: "bare", note: "Not covered by the guide, but Queue IS a valid Type picklist value (verified live)." },
|
|
84
|
+
SharingCriteriaRule: { campfireType: "SharingCriteriaRule", shape: "objectChild" },
|
|
85
|
+
SharingOwnerRule: { campfireType: "SharingOwnerRule", shape: "objectChild" },
|
|
86
|
+
SharingSet: { campfireType: "SharingSet", shape: "bare" },
|
|
87
|
+
// --- UI ----------------------------------------------------------------------------
|
|
88
|
+
CustomTab: { campfireType: "Tab", shape: "bare", note: "Spaces become underscores; a custom-object tab ends in __c." },
|
|
89
|
+
FlexiPage: { campfireType: "LightningPage", shape: "bare", note: "Lightning Page ASSIGNMENTS are not deployable — ship the App instead." },
|
|
90
|
+
CustomApplication: { campfireType: "App", shape: "bare", note: "Also the carrier for Lightning Page assignments." },
|
|
91
|
+
PathAssistant: { campfireType: "PathAssistant", shape: "bare" },
|
|
92
|
+
UiFormatSpecificationSet: { campfireType: "UiFormatSpecificationSet", shape: "bare", note: "Conditional field formatting." },
|
|
93
|
+
// --- content / folders --------------------------------------------------------------
|
|
94
|
+
EmailTemplate: { campfireType: "EmailTemplate", shape: "folderDoc", note: "Guide prose says \"Email Templates\" but the restricted picklist value is EmailTemplate (verified live). Standard public folder is unfiled$public." },
|
|
95
|
+
EmailTemplateFolder: { campfireType: "EmailTemplateFolder", shape: "bare" },
|
|
96
|
+
// The SDR registry deploys CLASSIC email folders as type "EmailFolder" — the key direct_apply
|
|
97
|
+
// records in the evidence bundle — so without this alias the row lands UNMAPPED and forces an
|
|
98
|
+
// operator round-trip (live-caught SM301: guide + operator resolved it to EmailTemplateFolder;
|
|
99
|
+
// both EmailFolder and EmailTemplateFolder are legal picklist values, the guide's is used).
|
|
100
|
+
EmailFolder: { campfireType: "EmailTemplateFolder", shape: "bare" },
|
|
101
|
+
Report: { campfireType: "Report", shape: "folderDoc", note: "Deploy the folder too." },
|
|
102
|
+
ReportFolder: { campfireType: "ReportFolder", shape: "bare" },
|
|
103
|
+
ReportType: { campfireType: "ReportType", shape: "bare" },
|
|
104
|
+
Dashboard: { campfireType: "Dashboard", shape: "folderDoc", note: "Deploy the folder too." },
|
|
105
|
+
DashboardFolder: { campfireType: "DashboardFolder", shape: "bare" },
|
|
106
|
+
// --- integrations -------------------------------------------------------------------
|
|
107
|
+
// Same Renner QA run dropped a delivered Named Credential for the same reason. NamedCredential
|
|
108
|
+
// is a legal picklist value (verified live 2026-08-18), and VFP's 2026-08-19 guide revision
|
|
109
|
+
// added a Named Credential section that specifies exactly this: Type=NamedCredential, API Name =
|
|
110
|
+
// "The Name of the Named Credential", no Object and no Folder field — i.e. `bare`.
|
|
111
|
+
//
|
|
112
|
+
// The guide is still silent on ExternalCredential, and it is NOT in the picklist, so a named
|
|
113
|
+
// credential's paired external credential still lands unmapped — fail-closed is correct there
|
|
114
|
+
// until Campfire adds the value AND the guide says how to key it.
|
|
115
|
+
NamedCredential: { campfireType: "NamedCredential", shape: "bare" },
|
|
116
|
+
// --- code ---------------------------------------------------------------------------
|
|
117
|
+
ApexClass: { campfireType: "ApexClass", shape: "bare" },
|
|
118
|
+
ApexTrigger: { campfireType: "ApexTrigger", shape: "bare" },
|
|
119
|
+
ApexPage: { campfireType: "ApexPage", shape: "bare", note: "Visualforce pages." },
|
|
120
|
+
CustomLabel: { campfireType: "CustomLabel", shape: "bare" },
|
|
121
|
+
CustomNotificationType: { campfireType: "CustomNotificationType", shape: "bare" },
|
|
122
|
+
GlobalValueSet: { campfireType: "GlobalValueSet", shape: "bare" },
|
|
123
|
+
StaticResource: { campfireType: "StaticResource", shape: "bare" },
|
|
124
|
+
Document: { campfireType: "Document", shape: "folderDoc" },
|
|
125
|
+
DocumentFolder: { campfireType: "DocumentFolder", shape: "bare" }
|
|
126
|
+
};
|
|
127
|
+
const splitFirst = (value, sep) => {
|
|
128
|
+
const i = value.indexOf(sep);
|
|
129
|
+
return i < 0 ? null : [value.slice(0, i), value.slice(i + sep.length)];
|
|
130
|
+
};
|
|
131
|
+
// Derive the Campfire CI field set from what the direct tier already knows about a delivered
|
|
132
|
+
// component. Fail-closed: an unmapped type returns an error rather than a guessed Type__c,
|
|
133
|
+
// because a wrong picklist value writes a manifest row that cannot deploy.
|
|
134
|
+
export function deriveConfigItem(typeName, fullName) {
|
|
135
|
+
const rule = CI_RULES[typeName];
|
|
136
|
+
if (!rule) {
|
|
137
|
+
return {
|
|
138
|
+
error: `No Campfire Configuration Item mapping for metadata type "${typeName}". Campfire's Type picklist is NOT the SDR registry vocabulary — check harness/reference/campfire-configuration-items-guide.md and ask the operator rather than guessing a Type value (a wrong Type produces a ClickDeploy row that will not resolve). Note: Queue is not covered by the guide.`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const base = { type: rule.campfireType, ...(rule.note ? { note: rule.note } : {}) };
|
|
142
|
+
switch (rule.shape) {
|
|
143
|
+
case "bare":
|
|
144
|
+
return { ...base, apiName: fullName };
|
|
145
|
+
case "objectChild": {
|
|
146
|
+
const parts = splitFirst(fullName, ".");
|
|
147
|
+
if (!parts) {
|
|
148
|
+
return {
|
|
149
|
+
error: `${typeName} "${fullName}" needs an Object.Child fullName — Campfire stores the object in its OWN field (Object API Name) and the child alone in Configuration Item API Name.`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const [objectApiName, apiName] = parts;
|
|
153
|
+
// CustomMetadata records: the guide wants the __mdt TYPE in Object API Name, while the
|
|
154
|
+
// metadata fullName drops the suffix (CF66_Readiness_Rule.Low_Risk).
|
|
155
|
+
const obj = typeName === "CustomMetadata" && !objectApiName.endsWith("__mdt") ? `${objectApiName}__mdt` : objectApiName;
|
|
156
|
+
return { ...base, apiName, objectApiName: obj };
|
|
157
|
+
}
|
|
158
|
+
case "objectChildOptional": {
|
|
159
|
+
const parts = splitFirst(fullName, ".");
|
|
160
|
+
if (!parts)
|
|
161
|
+
return { ...base, apiName: fullName }; // global action — Object API Name stays blank
|
|
162
|
+
return { ...base, apiName: parts[1], objectApiName: parts[0] };
|
|
163
|
+
}
|
|
164
|
+
case "layout": {
|
|
165
|
+
const parts = splitFirst(fullName, "-");
|
|
166
|
+
if (!parts) {
|
|
167
|
+
return { error: `Layout "${fullName}" needs an "Object-Layout Label" fullName so the object can go in Object API Name.` };
|
|
168
|
+
}
|
|
169
|
+
return { ...base, apiName: parts[1], objectApiName: parts[0] };
|
|
170
|
+
}
|
|
171
|
+
case "folderDoc": {
|
|
172
|
+
const parts = splitFirst(fullName, "/");
|
|
173
|
+
if (!parts) {
|
|
174
|
+
return {
|
|
175
|
+
error: `${typeName} "${fullName}" needs a "Folder/Name" fullName — Campfire stores the folder in Report / Dashboard Folder API Name (use unfiled$public for the standard public folder).`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return { ...base, apiName: parts[1], folderApiName: parts[0] };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
export function isConfigItemError(v) {
|
|
183
|
+
return "error" in v;
|
|
184
|
+
}
|
|
185
|
+
// Deployment dependencies the guide calls out. These are not deployable facts about ONE
|
|
186
|
+
// component — they are things that must ride along, so they belong on the operator's radar as
|
|
187
|
+
// Deployment Tasks or as extra Configuration Items.
|
|
188
|
+
export const DEPLOYMENT_DEPENDENCY_RULES = [
|
|
189
|
+
"PermissionSet/Profile granting FLS on a custom field: the field must be in the deployment or already exist in the target org.",
|
|
190
|
+
"Report/Dashboard: its folder must be deployed too.",
|
|
191
|
+
"LightningPage embedding a screen flow or list view: that flow/list view must be in the deployment. Certinia components (Cards/Highlights panels) are NOT deployable and must be re-created by hand with the exact same Name.",
|
|
192
|
+
"Flow referencing custom fields or a custom object: those must be in the deployment or already exist.",
|
|
193
|
+
"FieldSet: its member fields must be in the deployment or already exist.",
|
|
194
|
+
"ApprovalProcess: its Workflow Field Updates and Email Alerts must be in the deployment or already exist.",
|
|
195
|
+
// Not "flows deploy INACTIVE". A Flow deploys with the status its XML DECLARES: author
|
|
196
|
+
// <status>Active</status> and it lands active, which is what CF76's two flows did while the
|
|
197
|
+
// manifest and this rule both insisted otherwise. Omit the element and it lands Draft.
|
|
198
|
+
"Flow: it deploys with the <status> its XML declares — state Active explicitly to have it land active, and verify after deploy, since org settings can still force Draft. Raise an activation Deployment Task only when the deployed status is not Active.",
|
|
199
|
+
"API-name CHANGES are not deployable: ClickDeploy treats a renamed component as a NEW one and leaves the original behind.",
|
|
200
|
+
"Certinia data-backed components (navigation menus, card panels, highlights) are data, not metadata — track them as a Pre-Deployment Deployment Task and move them by data export/import."
|
|
201
|
+
];
|