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.
Files changed (115) hide show
  1. package/README.md +369 -0
  2. package/agents/plan-requirement.md +32 -0
  3. package/dist/campfire/configItem.js +201 -0
  4. package/dist/campfire/configItemDescription.js +379 -0
  5. package/dist/campfire/deriveFromBundle.js +1013 -0
  6. package/dist/cli.js +465 -0
  7. package/dist/core/betaFeedback.js +493 -0
  8. package/dist/core/chapterInstructions.js +473 -0
  9. package/dist/core/claudeAssets.js +61 -0
  10. package/dist/core/gate.js +201 -0
  11. package/dist/core/ids.js +9 -0
  12. package/dist/core/install.js +60 -0
  13. package/dist/core/operatorQa.js +462 -0
  14. package/dist/core/orgCallMeter.js +178 -0
  15. package/dist/core/parcels.js +1235 -0
  16. package/dist/core/paths.js +105 -0
  17. package/dist/core/phases.js +225 -0
  18. package/dist/core/planFile.js +251 -0
  19. package/dist/core/planFragments.js +119 -0
  20. package/dist/core/readsLog.js +398 -0
  21. package/dist/core/registration.js +24 -0
  22. package/dist/core/result.js +38 -0
  23. package/dist/core/resume.js +233 -0
  24. package/dist/core/revisionChain.js +351 -0
  25. package/dist/core/richText.js +61 -0
  26. package/dist/core/semanticHash.js +89 -0
  27. package/dist/core/sessionOrg.js +60 -0
  28. package/dist/core/sfQuery.js +19 -0
  29. package/dist/core/spawn.js +182 -0
  30. package/dist/core/supervisorContract.js +230 -0
  31. package/dist/core/telemetry.js +1297 -0
  32. package/dist/core/telemetryContext.js +154 -0
  33. package/dist/core/telemetrySpool.js +244 -0
  34. package/dist/core/transcriptUpload.js +516 -0
  35. package/dist/core/update.js +363 -0
  36. package/dist/core/viewer.js +117 -0
  37. package/dist/core/workspace.js +92 -0
  38. package/dist/core/writeGrants.js +93 -0
  39. package/dist/core/writebackManifest.js +92 -0
  40. package/dist/direct/applyLoop.js +2516 -0
  41. package/dist/direct/batchReport.js +510 -0
  42. package/dist/direct/comparatorNoise.js +75 -0
  43. package/dist/direct/containers.js +85 -0
  44. package/dist/direct/contamination.js +221 -0
  45. package/dist/direct/crud.js +56 -0
  46. package/dist/direct/destructive.js +168 -0
  47. package/dist/direct/knownTokens.js +94 -0
  48. package/dist/direct/lint.js +382 -0
  49. package/dist/direct/metadataSchema.generated.json +1 -0
  50. package/dist/direct/metadataSchema.js +179 -0
  51. package/dist/direct/operatorPage.js +349 -0
  52. package/dist/direct/orgRepo.js +1531 -0
  53. package/dist/direct/orgRepoSpawnCount.js +21 -0
  54. package/dist/direct/plannedScope.js +119 -0
  55. package/dist/direct/preImageTargets.js +63 -0
  56. package/dist/direct/refVerify.js +191 -0
  57. package/dist/direct/registry.js +129 -0
  58. package/dist/direct/removals.js +262 -0
  59. package/dist/direct/rollback.js +1513 -0
  60. package/dist/direct/rollbackRun.js +1360 -0
  61. package/dist/direct/sessionProjection.js +126 -0
  62. package/dist/direct/snapshotComparability.js +165 -0
  63. package/dist/direct/sourceTree.js +113 -0
  64. package/dist/direct/unstatedChanges.js +106 -0
  65. package/dist/direct/waveAssembly.js +556 -0
  66. package/dist/direct/waveCounts.js +19 -0
  67. package/dist/direct/waveDerivation.js +152 -0
  68. package/dist/direct/waveGrade.js +185 -0
  69. package/dist/index.js +90 -0
  70. package/dist/orgread/customFieldSnapshot.js +48 -0
  71. package/dist/orgread/fieldMetadata.js +178 -0
  72. package/dist/orgread/genericSnapshot.js +61 -0
  73. package/dist/orgread/layoutSnapshot.js +151 -0
  74. package/dist/orgread/salesforceOrgReader.js +184 -0
  75. package/dist/orgread/snapshotTypes.js +9 -0
  76. package/dist/projection/bridge.js +26 -0
  77. package/dist/projection/diff.js +141 -0
  78. package/dist/projection/mapmd.js +213 -0
  79. package/dist/projection/nameAmbiguity.js +106 -0
  80. package/dist/projection/precedent.js +136 -0
  81. package/dist/projection/project.js +214 -0
  82. package/dist/projection/scope.js +110 -0
  83. package/dist/projection/types.js +5 -0
  84. package/dist/tools/analyze.js +322 -0
  85. package/dist/tools/auth.js +915 -0
  86. package/dist/tools/campfire.js +1742 -0
  87. package/dist/tools/closeBatch.js +154 -0
  88. package/dist/tools/closeText.js +35 -0
  89. package/dist/tools/direct.js +1613 -0
  90. package/dist/tools/feedback.js +318 -0
  91. package/dist/tools/operator.js +287 -0
  92. package/dist/tools/org.js +739 -0
  93. package/dist/tools/phase.js +249 -0
  94. package/dist/tools/projection.js +172 -0
  95. package/dist/tools/session.js +313 -0
  96. package/dist/tools/update.js +82 -0
  97. package/dist/tools/waves.js +793 -0
  98. package/package.json +48 -0
  99. package/reference/campfire-configuration-items-guide.md +710 -0
  100. package/reference/deployability-cribs.md +119 -0
  101. package/reference/operator-voice.md +141 -0
  102. package/reference/org-read-cribs.md +34 -0
  103. package/reference/scoped-dml-interim.md +68 -0
  104. package/reference/supervisor-playbook.md +313 -0
  105. package/reference/templates.md +233 -0
  106. package/skills/in2-delivery/SKILL.md +322 -0
  107. package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
  108. package/skills/in2-delivery/references/deployability-cribs.md +119 -0
  109. package/skills/in2-delivery/references/operator-voice.md +141 -0
  110. package/skills/in2-delivery/references/org-read-cribs.md +34 -0
  111. package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
  112. package/skills/in2-delivery/references/templates.md +233 -0
  113. package/skills/plan-requirement/SKILL.md +188 -0
  114. package/viewer/index.html +899 -0
  115. package/viewer/server.mjs +433 -0
@@ -0,0 +1,152 @@
1
+ // What `plan_waves` DERIVED, kept where the next call can check itself against it.
2
+ //
3
+ // THE DEFECT THIS EXISTS FOR. `plan_waves` is advisory — deliberately, and that stays true: a
4
+ // supervisor who splits a wave further for a reason the planner cannot see is doing the right
5
+ // thing, and hand-authoring a wave is legitimate. What was missing is that NOTHING NOTICED when the
6
+ // submitted wave bore no relation to any derived one. The planner ran, returned N waves, and the
7
+ // supervisor then passed `direct_apply` a component list of its own; the two were never compared,
8
+ // so a hand-order that was a mistake looked exactly like a hand-order that was a judgement call.
9
+ //
10
+ // LIVE CASE (the Windows run): the supervisor hand-listed every component rather than lifting the
11
+ // planner's waves, and the wave count inflated. Each extra wave costs two deploys — a ~45s checkOnly
12
+ // and the real one — plus two projections. That is the single biggest lever on run time
13
+ // (`direct_apply` is 44.6% of all time spent inside tools) being pushed the wrong way, silently.
14
+ //
15
+ // SO: WARN, NEVER REFUSE. The submitted `waveComponents` are compared against every derived wave and
16
+ // the divergence is NAMED. It cannot block — the planner has no standing to overrule an operator who
17
+ // knows something it does not — but it can make the deviation visible at the moment it costs money,
18
+ // which is the one thing nothing did before.
19
+ //
20
+ // It is also FAIL-OPEN in every direction. No derivation on disk (the supervisor never ran
21
+ // plan_waves, or ran it before this shipped) means no claim: silence, not a warning. An unreadable
22
+ // file is the same. A wave that cannot compare itself to a plan still deploys — this is a note on a
23
+ // deploy, never a gate in front of one.
24
+ import path from "node:path";
25
+ import { promises as fs } from "node:fs";
26
+ import { sessionDir, writeJsonAtomic } from "../core/workspace.js";
27
+ /** Where a session's derivation lives. One file per session — the planner's latest answer. */
28
+ export function derivationFile(sessionId) {
29
+ return path.join(sessionDir(sessionId), "analysis", "wave-derivation.json");
30
+ }
31
+ /**
32
+ * The comparison key. `path` is part of identity, not decoration: two revisions of one component
33
+ * are two different things to deploy, and a wave that swapped one for the other has diverged even
34
+ * though the type and name match.
35
+ */
36
+ const keyOf = (c) => `${c.typeName}:${c.fullName}${c.path ? `@${c.path}` : ""}`;
37
+ /** Persist the planner's answer. FAIL-OPEN: a session that cannot record it still gets its plan. */
38
+ export async function writeWaveDerivation(sessionId, waves, groundedAgainst) {
39
+ try {
40
+ const derivation = {
41
+ schemaVersion: "wave-derivation-v1",
42
+ derivedAt: new Date().toISOString(),
43
+ waves,
44
+ groundedAgainst
45
+ };
46
+ await writeJsonAtomic(derivationFile(sessionId), derivation);
47
+ return true;
48
+ }
49
+ catch {
50
+ // Bookkeeping never blocks a plan. The consequence is one missing warning later, not a failure.
51
+ return false;
52
+ }
53
+ }
54
+ /** Read it back, or null for "no claim to make" — never a throw, never a partial answer. */
55
+ export async function readWaveDerivation(sessionId) {
56
+ try {
57
+ const parsed = JSON.parse(await fs.readFile(derivationFile(sessionId), "utf8"));
58
+ if (parsed?.schemaVersion !== "wave-derivation-v1" || !Array.isArray(parsed.waves))
59
+ return null;
60
+ return parsed;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ /**
67
+ * Compare one submitted wave against the whole derivation.
68
+ *
69
+ * "Corresponds to a derived wave" is deliberately generous: the nearest wave is the one sharing the
70
+ * most components, and an exact match on that wave's contents is a clean pass. Everything else is
71
+ * DESCRIBED rather than judged — which components the planner never ordered at all, which it put in
72
+ * other waves, and which of the nearest wave's own members were left out. That is the information a
73
+ * supervisor needs to say "yes, I meant that" in one line, or to notice they did not.
74
+ *
75
+ * A submission that touches NO derived component at all gets `nearestWave: null` — the honest
76
+ * answer, and the shape of the live defect: a hand-listed batch that corresponds to nothing.
77
+ */
78
+ export function compareToDerivation(submitted, derivation) {
79
+ const want = new Set(submitted.map(keyOf));
80
+ // Where the planner put each component. Built over the whole derivation, because "you deployed
81
+ // this early" is only answerable by knowing which OTHER wave it belongs to.
82
+ const waveOf = new Map();
83
+ derivation.waves.forEach((wave, i) => {
84
+ for (const c of wave)
85
+ if (!waveOf.has(keyOf(c)))
86
+ waveOf.set(keyOf(c), i);
87
+ });
88
+ let nearest = -1;
89
+ let bestOverlap = 0;
90
+ derivation.waves.forEach((wave, i) => {
91
+ const overlap = wave.filter((c) => want.has(keyOf(c))).length;
92
+ if (overlap > bestOverlap) {
93
+ bestOverlap = overlap;
94
+ nearest = i;
95
+ }
96
+ });
97
+ const unplanned = [...want].filter((k) => !waveOf.has(k)).sort();
98
+ if (nearest < 0) {
99
+ return {
100
+ nearestWave: null,
101
+ unplanned,
102
+ missingFromWave: [],
103
+ fromOtherWaves: [...want]
104
+ .filter((k) => waveOf.has(k))
105
+ .map((k) => ({ component: k, derivedWave: waveOf.get(k) + 1 }))
106
+ .sort((a, b) => a.component.localeCompare(b.component)),
107
+ matches: false
108
+ };
109
+ }
110
+ const nearestKeys = new Set(derivation.waves[nearest].map(keyOf));
111
+ const missingFromWave = [...nearestKeys].filter((k) => !want.has(k)).sort();
112
+ const fromOtherWaves = [...want]
113
+ .filter((k) => !nearestKeys.has(k) && waveOf.has(k))
114
+ .map((k) => ({ component: k, derivedWave: waveOf.get(k) + 1 }))
115
+ .sort((a, b) => a.component.localeCompare(b.component));
116
+ return {
117
+ nearestWave: nearest + 1,
118
+ unplanned,
119
+ missingFromWave,
120
+ fromOtherWaves,
121
+ matches: unplanned.length === 0 && missingFromWave.length === 0 && fromOtherWaves.length === 0
122
+ };
123
+ }
124
+ /**
125
+ * The warning text, or null when there is nothing to say.
126
+ *
127
+ * Written to be actionable in one read: what diverged, what it costs, and the fact that proceeding
128
+ * is allowed. The last part is not a courtesy — a warning that reads like a refusal gets worked
129
+ * around, and working around this one means hand-copying files past the ownership checks.
130
+ */
131
+ export function renderDivergence(report, derivation) {
132
+ if (report.matches)
133
+ return null;
134
+ const lines = [];
135
+ if (report.nearestWave === null) {
136
+ lines.push(`WAVE NOT DERIVED — this component list corresponds to NO wave plan_waves derived (it ordered ${derivation.waves.length} wave(s) at ${derivation.derivedAt}).`);
137
+ }
138
+ else {
139
+ lines.push(`WAVE DIVERGES FROM THE DERIVED PLAN — closest is derived wave ${report.nearestWave} of ${derivation.waves.length} (derived ${derivation.derivedAt}), and this submission is not it:`);
140
+ }
141
+ if (report.unplanned.length > 0) {
142
+ lines.push(` ${report.unplanned.length} component(s) plan_waves ordered into NO wave — check they are in a plan.json and were not left unplaced or refused:`, ...report.unplanned.map((c) => ` ${c}`));
143
+ }
144
+ if (report.fromOtherWaves.length > 0) {
145
+ lines.push(` ${report.fromOtherWaves.length} component(s) the planner put in a DIFFERENT wave — merging across a dependency edge is the one merge that is never safe:`, ...report.fromOtherWaves.map((f) => ` ${f.component} (derived wave ${f.derivedWave})`));
146
+ }
147
+ if (report.missingFromWave.length > 0) {
148
+ lines.push(` ${report.missingFromWave.length} component(s) of derived wave ${report.nearestWave} are NOT in this submission — they will need a wave of their own:`, ...report.missingFromWave.map((c) => ` ${c}`));
149
+ }
150
+ lines.push("", "NOT A REFUSAL, and nothing here has been changed: hand-ordering is legitimate and the planner cannot see", "every reason to split. What it can see is the cost — every extra wave is two org round-trips (a ~45s", "checkOnly and the deploy) plus two projections, and a batch listed by hand rather than lifted from", "plan_waves is how a 4-wave plan becomes a 7-wave run. If this split was deliberate, say so when you", "report the wave. If it was not, re-run plan_waves and pass its wave list through unedited.");
151
+ return lines.join("\n");
152
+ }
@@ -0,0 +1,185 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ /** `edit|CustomField:PV_Asset__c.PV_Critical__c` — the join key coverage is decided on. */
4
+ function scopeKey(op, typeName, fullName) {
5
+ return `${op}|${typeName}:${fullName}`;
6
+ }
7
+ const DELIVERED = new Set(["DIFF_VERIFIED", "DELIVERED_WITH_DRIFT"]);
8
+ /**
9
+ * Read every bundle this session left on disk.
10
+ *
11
+ * Deliberately the SAME directory scan both graders used separately, so a report that disagreed
12
+ * with the terminal event about how many waves delivered is no longer expressible.
13
+ */
14
+ export function readWaveBundles(directDir) {
15
+ if (!existsSync(directDir))
16
+ return [];
17
+ const bundles = [];
18
+ for (const entry of readdirSync(directDir, { withFileTypes: true })) {
19
+ if (!entry.isDirectory())
20
+ continue;
21
+ const bundle = path.join(directDir, entry.name);
22
+ const verdictFile = path.join(bundle, "verdict.json");
23
+ if (!existsSync(verdictFile))
24
+ continue;
25
+ let verdict;
26
+ try {
27
+ verdict = String(JSON.parse(readFileSync(verdictFile, "utf8")).verdict ?? "UNKNOWN");
28
+ }
29
+ catch {
30
+ // An unreadable bundle is not a verdict; skip rather than mis-grade the batch.
31
+ continue;
32
+ }
33
+ bundles.push({ wave: entry.name, verdict, ...readRequest(bundle) });
34
+ }
35
+ return bundles.sort((a, b) => a.wave.localeCompare(b.wave));
36
+ }
37
+ /**
38
+ * `dryRunOnly` and the touched scope, from the request the caller actually made.
39
+ *
40
+ * FAILS CLOSED on an unreadable request: `dryRunOnly: false` means the bundle grades as a real
41
+ * wave, so a refusal surfaces rather than being written off as practice. The two directions are not
42
+ * symmetric — a wrong `true` hides a wave that did not deliver, which is the failure the grade
43
+ * exists to catch, while a wrong `false` costs one over-reported wave a reader can check.
44
+ */
45
+ function readRequest(bundle) {
46
+ try {
47
+ const request = JSON.parse(readFileSync(path.join(bundle, "request.json"), "utf8"));
48
+ return {
49
+ dryRunOnly: request.dryRunOnly === true,
50
+ scope: [
51
+ ...(request.edits ?? []).map((e) => scopeKey("edit", e.typeName, e.fullName)),
52
+ ...(request.deletes ?? []).map((d) => scopeKey("delete", d.typeName, d.fullName)),
53
+ ...(request.facetUpdates ?? []).map((f) => scopeKey("facet", "CustomObject", f.objectApiName))
54
+ ]
55
+ };
56
+ }
57
+ catch {
58
+ return { dryRunOnly: false, scope: null };
59
+ }
60
+ }
61
+ /**
62
+ * Grade the batch from its bundles. Pure — every input is already on disk and read.
63
+ *
64
+ * THE RULE FOR A BLOCKED PRACTICE RUN, which is the only genuinely contestable call here.
65
+ *
66
+ * A refused practice check is, in a normal run, exactly what a human should look at before the real
67
+ * wave — it is the mechanism working. But in PV Arm B it was a DELIBERATE demonstration of the
68
+ * block, and the two fields the org refused to delete there were deleted for real by `w4` minutes
69
+ * later. Blessing all rejected dry runs would hide the first case; counting all of them would keep
70
+ * reporting the second as unfinished business that finished.
71
+ *
72
+ * So the batch's own evidence decides: a blocked practice run stops being attention-worthy when a
73
+ * DELIVERING wave in the same batch performed the SAME OPERATION on EVERY component it named — the
74
+ * org accepted, and verified, precisely what it had refused in practice.
75
+ *
76
+ * Four limits, stated rather than papered over:
77
+ * - This is a SCOPE claim, not a causal one. It establishes that the same operation on the same
78
+ * components succeeded in this batch; it does not prove the reason for the refusal was
79
+ * understood. That is why a covered run is still REPORTED, by name, with its covering waves
80
+ * named too — the grade drops the alarm, the report never drops the fact.
81
+ * - The scope key is `(operation, typeName:fullName)`, which is COMPONENT identity — and under the
82
+ * component revision model one batch may deliver two STATES of one component, in two waves, with
83
+ * the same key. So a practice run blocked on an early state of a component is cleared by a later
84
+ * state of it delivering, even though the two waves carried different files. That is the widest
85
+ * this claim reaches, and it is ACCEPTED rather than fixed: narrowing the key to a revision would
86
+ * mean importing the revision model into a module that is deliberately synchronous and
87
+ * dependency-free (`finalizeBatch` may run inside a signal handler, where async work does not
88
+ * survive), to sharpen an alarm that is already only advisory and already names its own evidence.
89
+ * A reader who wants the stronger claim has the covering wave's name and the delivery log.
90
+ * - It makes NO temporal claim. "Later" is not derivable here: bundle directories sort by label,
91
+ * not by time, and the delivery log carries an `orderingUnreliable` flag precisely because
92
+ * position is not evidence of when. Same-batch coverage needs no ordering to be true.
93
+ * - Operation kind must MATCH. A practice run refused a delete; an unrelated later edit of the
94
+ * same field is not that delete succeeding.
95
+ */
96
+ export function gradeWaves(bundles) {
97
+ const delivering = bundles.filter((b) => !b.dryRunOnly && DELIVERED.has(b.verdict));
98
+ const deliveredScope = new Map();
99
+ for (const b of delivering) {
100
+ for (const key of b.scope ?? []) {
101
+ const waves = deliveredScope.get(key) ?? [];
102
+ waves.push(b.wave);
103
+ deliveredScope.set(key, waves);
104
+ }
105
+ }
106
+ const waves = bundles.map((bundle) => {
107
+ if (!bundle.dryRunOnly) {
108
+ const delivered = DELIVERED.has(bundle.verdict);
109
+ return {
110
+ ...bundle,
111
+ classification: delivered ? "delivered" : "problem",
112
+ resolvedBy: null,
113
+ needsAttention: !delivered
114
+ };
115
+ }
116
+ if (bundle.verdict === "VALIDATED") {
117
+ return { ...bundle, classification: "practice-accepted", resolvedBy: null, needsAttention: false };
118
+ }
119
+ // Every other dry-run verdict is a BLOCK — the org refused it, our lint refused it, or it is a
120
+ // verdict this vocabulary does not know. The unknown case lands here rather than with the
121
+ // accepted ones on purpose: a verdict nobody recognises is not evidence that anything passed.
122
+ const resolvedBy = coveringWaves(bundle.scope, deliveredScope);
123
+ return {
124
+ ...bundle,
125
+ classification: "practice-blocked",
126
+ resolvedBy,
127
+ needsAttention: resolvedBy === null
128
+ };
129
+ });
130
+ const deliveredWaves = waves.filter((w) => w.classification === "delivered").length;
131
+ const problemWaves = waves.filter((w) => w.needsAttention).length;
132
+ const practiceRuns = waves.length - deliveredWaves - problemWaves;
133
+ return {
134
+ result: gradeResult(waves.length, deliveredWaves, problemWaves),
135
+ waves,
136
+ deliveredWaves,
137
+ problemWaves,
138
+ practiceRuns,
139
+ reason: gradeReason(waves.length, deliveredWaves, problemWaves, practiceRuns)
140
+ };
141
+ }
142
+ /** The waves that covered every component this bundle named, or null when any went uncovered. */
143
+ function coveringWaves(scope, deliveredScope) {
144
+ // A scope that could not be read establishes nothing, and an EMPTY scope is vacuously covered —
145
+ // both would silently clear a block. Neither is allowed to.
146
+ if (!scope || scope.length === 0)
147
+ return null;
148
+ const covering = new Set();
149
+ for (const key of scope) {
150
+ const waves = deliveredScope.get(key);
151
+ if (!waves)
152
+ return null;
153
+ for (const wave of waves)
154
+ covering.add(wave);
155
+ }
156
+ return [...covering].sort();
157
+ }
158
+ /**
159
+ * COMPLETE / NEEDS_ATTENTION / FAILED / NO_WAVES.
160
+ *
161
+ * `NO_WAVES` covers two runs that look different and mean the same thing: one that never deployed,
162
+ * and one whose only bundles were accepted practice runs. Neither changed the org, so neither has a
163
+ * DELIVERY result to state, and `COMPLETE` on a run that delivered nothing would call a batch
164
+ * finished that never started. `finalizeBatch` sends no terminal event in that case, exactly as it
165
+ * has always done for a read-only run.
166
+ */
167
+ function gradeResult(total, delivered, problems) {
168
+ if (total === 0)
169
+ return "NO_WAVES";
170
+ if (problems === 0)
171
+ return delivered === 0 ? "NO_WAVES" : "COMPLETE";
172
+ return delivered === 0 ? "FAILED" : "NEEDS_ATTENTION";
173
+ }
174
+ /** The counted status line — never a narrative, never an org's error text, bounded well under 200. */
175
+ function gradeReason(total, delivered, problems, practice) {
176
+ const practiceNote = practice > 0 ? `; ${practice} practice run(s), none needing review` : "";
177
+ if (total === 0)
178
+ return "no wave verdicts on disk";
179
+ if (problems === 0) {
180
+ return delivered === 0
181
+ ? `no wave delivered — ${practice} practice run(s) only, all accepted`
182
+ : `all ${delivered} wave(s) delivered${practiceNote}`;
183
+ }
184
+ return `${problems} of ${total} wave(s) need review${practiceNote}`;
185
+ }
package/dist/index.js ADDED
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { log } from "./core/result.js";
5
+ import { Telemetry } from "./core/telemetry.js";
6
+ import { supervisorInstructions } from "./core/supervisorContract.js";
7
+ import { betaEnabled } from "./core/betaFeedback.js";
8
+ import { REPO_ROOT, WORKSPACE_ROOT } from "./core/paths.js";
9
+ import { campfireAllowlistedOrgIds, campfireWriteEnabled, directAllowlistOpen, directWriteEnabled } from "./core/gate.js";
10
+ import { activeGrants } from "./core/writeGrants.js";
11
+ import { registerSessionTools } from "./tools/session.js";
12
+ import { registerActivationTools, registerAuthTools } from "./tools/auth.js";
13
+ import { registerOperatorTools } from "./tools/operator.js";
14
+ import { registerCampfireTools } from "./tools/campfire.js";
15
+ import { registerOrgTools } from "./tools/org.js";
16
+ import { registerProjectionTools } from "./tools/projection.js";
17
+ import { registerDirectTools } from "./tools/direct.js";
18
+ import { registerUpdateTools } from "./tools/update.js";
19
+ import { registerAnalyzeTools } from "./tools/analyze.js";
20
+ import { registerWaveTools } from "./tools/waves.js";
21
+ import { registerPhaseTools } from "./tools/phase.js";
22
+ import { registerCloseBatchTools } from "./tools/closeBatch.js";
23
+ import { registerFeedbackTools } from "./tools/feedback.js";
24
+ import { instrumentRegistration, registeredToolNames } from "./core/registration.js";
25
+ import { installClaudeAssets } from "./core/claudeAssets.js";
26
+ import { installedVersion, isNpmInstall } from "./core/install.js";
27
+ const server = new McpServer({ name: "cc-mcp-server", version: "0.1.0" },
28
+ // Shipped to the model on connect regardless of cwd — the only channel that does not depend on
29
+ // the operator opening a particular directory or describing the workflow themselves. Carries the
30
+ // beta feedback section only when CC_MCP_BETA is on; an install that is not in beta ships a
31
+ // contract with no trace of the channel.
32
+ { instructions: supervisorInstructions() });
33
+ // Telemetry wraps registerTool ONCE, before any registration, so every tool — present and
34
+ // future — is timed without each module having to remember. Never throws; CC_MCP_TELEMETRY=0
35
+ // disables it.
36
+ const telemetry = new Telemetry();
37
+ instrumentRegistration(server, telemetry);
38
+ // Default surface = the direct working tier and everything it needs. Fixed for the life of the
39
+ // process — the tool surface no longer swaps live (the hot-swap updater is gone with the move to
40
+ // npm distribution; an update lands for the NEXT conversation).
41
+ registerSessionTools(server);
42
+ registerAuthTools(server);
43
+ registerActivationTools(server);
44
+ registerOperatorTools(server);
45
+ registerCampfireTools(server);
46
+ registerOrgTools(server);
47
+ registerProjectionTools(server);
48
+ registerDirectTools(server);
49
+ registerUpdateTools(server);
50
+ registerAnalyzeTools(server);
51
+ registerWaveTools(server);
52
+ registerPhaseTools(server);
53
+ registerCloseBatchTools(server);
54
+ // Registers nothing unless CC_MCP_BETA is on — the flag lives inside the registrar.
55
+ registerFeedbackTools(server);
56
+ // Keep ~/.claude/skills and ~/.claude/agents in step with THIS build. Best-effort by contract and
57
+ // awaited before the transport connects, so a session never races its own instructions.
58
+ await installClaudeAssets();
59
+ log(`repo root: ${REPO_ROOT}`);
60
+ log(`workspace root: ${WORKSPACE_ROOT}`);
61
+ log(`telemetry: ${process.env.CC_MCP_TELEMETRY === "0" ? "disabled (CC_MCP_TELEMETRY=0)" : `run ${telemetry.runId}`}`);
62
+ // Which orgs this install can actually write to is the highest-consequence fact about it, so it is
63
+ // stated plainly at every start rather than left to be inferred from env vars.
64
+ const grants = await activeGrants().catch(() => []);
65
+ log(`direct write: ${directWriteEnabled()
66
+ ? directAllowlistOpen()
67
+ ? "ENABLED, allowlist WIDE OPEN (CC_MCP_DIRECT_ALLOWLIST=*) — every authenticated org is writable; consultant org still refused. INTERNAL DEV ONLY, must not ship."
68
+ : "ENABLED, authority from activation grants + per-org allowlist"
69
+ : "closed (default)"}`);
70
+ log(grants.length > 0
71
+ ? `activation grants: ${grants.map((g) => `${g.orgId} (${g.alias}, expires ${g.expiresAt.slice(0, 10)})`).join("; ")}`
72
+ : "activation grants: none — no org is writable by grant until a Campfire code is redeemed with activate_session");
73
+ // Stated at start for the same reason as direct write, and for one more: this gate is checked at
74
+ // the END of a run. A closed Campfire gate costs nothing until writeback, and by then the session
75
+ // has spent half an hour and the fix (different server env) needs a restart the operator cannot
76
+ // perform. Saying it here turns a late, expensive refusal into something the supervisor can card
77
+ // on turn one — while re-registering is still cheap.
78
+ log(`campfire write: ${campfireWriteEnabled()
79
+ ? `ENABLED for allowlisted consultant org(s) [${campfireAllowlistedOrgIds().join(", ")}]`
80
+ : "CLOSED (default) — writeback will refuse CAMPFIRE_WRITE_NOT_AUTHORIZED at the END of the run. Set CC_MCP_CAMPFIRE_WRITE=1 and restart if this session is expected to write Configuration Items back."}`);
81
+ log(`version: ${installedVersion()}${isNpmInstall() ? " (npm — update_server / in2 update install the next version)" : " (checkout — updates come from git, not npm)"}`);
82
+ // Beta mode changes the tool surface AND the operating contract, so it is stated at every start for
83
+ // the same reason direct write is: a machine in beta must never be mistakable for one that is not.
84
+ // The announcement itself (what leaves, and how to stop it) is announceBeta's, called from Telemetry
85
+ // beside the other content-channel lines; this is the one-word status beside the other capabilities.
86
+ log(`beta: ${betaEnabled() ? "ON (CC_MCP_BETA) — report_feedback and record_operator_feedback are registered" : "off (default)"}`);
87
+ log(`tool surface: direct tier only (${registeredToolNames().length} tools)`);
88
+ const transport = new StdioServerTransport();
89
+ await server.connect(transport);
90
+ log("cc-mcp-server ready (stdio)");
@@ -0,0 +1,48 @@
1
+ // Dedicated CustomField snapshot. Ported from sf-config-verifier
2
+ // (src/snapshots/customFieldSnapshot.ts).
3
+ //
4
+ // Carries EVERY facet the Metadata API returned, coercing the typed ones at the READ boundary so a
5
+ // comparison never has to care that the API hands back XML leaves as strings.
6
+ //
7
+ // It used to carry a ~16-facet allowlist instead, and that was a defect, not a design. Any facet an
8
+ // author stated that fell outside the list — <unique>, <externalId>, <caseSensitive>, and 40-odd
9
+ // others the schema knows — was dropped by THIS reader before the comparison ever ran, so the
10
+ // intent check reported it as intended-but-absent: false drift on a field the org had stored
11
+ // exactly as asked. Worse, the evidence could not distinguish the two explanations, because a facet
12
+ // our reader discarded and a facet the org omitted look identical downstream. A snapshot that
13
+ // silently narrows what it observed cannot be the substrate for a drift verdict.
14
+ //
15
+ // The shared normalizer does the coercion (fieldMetadata.normalizeCustomFieldMetadata — the same
16
+ // one the generic CustomField path and the intent side already use, so all three compare
17
+ // like-with-like); this reader adds only what is specific to reading ONE named field.
18
+ import { normalizeCustomFieldMetadata, valueSetDefaultValue } from "./fieldMetadata.js";
19
+ import { snapshot } from "./snapshotTypes.js";
20
+ export async function readCustomFieldSnapshot(org, fullName) {
21
+ // Upstream destructured `[result]` and dereferenced it directly, which throws if the reader hands
22
+ // back an empty array. cc-mcp compiles with noUncheckedIndexedAccess and surfaced it; an empty
23
+ // response is treated as not-found, which is what the caller can actually act on.
24
+ const rows = await org.metadataRead("CustomField", [fullName]);
25
+ const result = rows[0] ?? { typeName: "CustomField", fullName, found: false, errors: ["metadataRead returned no rows"] };
26
+ const normalized = result.found ? normalizeCustomFieldMetadata(result.metadata) : null;
27
+ let metadata = null;
28
+ if (normalized) {
29
+ // The wire carries the UNQUALIFIED field name (`CF63_Notes__c`); this reader answers for the
30
+ // name it was asked about (`CF63_Intake__c.CF63_Notes__c`), which is the key every caller holds.
31
+ metadata = { ...normalized, fullName };
32
+ const defaultValue = asDefaultValue(normalized.defaultValue) ?? valueSetDefaultValue(normalized.valueSet);
33
+ if (defaultValue !== undefined)
34
+ metadata.defaultValue = defaultValue;
35
+ }
36
+ const state = { metadata };
37
+ return snapshot("customField", "CustomField", fullName, "metadata.read", state, result.errors);
38
+ }
39
+ // EXT-2040: the CustomField <defaultValue> element is a formula-syntax STRING (e.g. "false" for a
40
+ // checkbox default-unchecked, `"West"` for a text literal, TODAY() for a date). Some XML parsers
41
+ // coerce a bare `<defaultValue>false</defaultValue>` to a boolean and `<defaultValue>5</defaultValue>`
42
+ // to a number (the same coercion class) — normalize both back to the string form so a comparison on
43
+ // the stated literal matches byte-for-byte. A Picklist carries NO top-level <defaultValue>; its
44
+ // default is derived from the valueSet per-value <default> flag (valueSetDefaultValue, at the call
45
+ // site above).
46
+ function asDefaultValue(v) {
47
+ return typeof v === "string" ? v : typeof v === "boolean" || typeof v === "number" ? String(v) : undefined;
48
+ }
@@ -0,0 +1,178 @@
1
+ /**
2
+ * CustomField metadata normalization — the record of how Salesforce's Metadata API ACTUALLY
3
+ * serializes a field, as opposed to what you asked it to store.
4
+ *
5
+ * Ported verbatim from sf-config-verifier (src/snapshots/customFieldMetadata.ts). Every rule below
6
+ * was caught against a live org, usually as a family that scored zero after a byte-perfect deploy.
7
+ * The EXT ids are load-bearing: they are the only record of why each line exists, so keep them with
8
+ * the code they explain.
9
+ *
10
+ * This is not cosmetic. direct/applyLoop.ts compares pre- and post-deploy reads through these
11
+ * functions, so a missing rule here does not produce a slightly-off report — it produces a false
12
+ * failure on correct work, or a false green on incorrect work.
13
+ *
14
+ * Applied by BOTH the dedicated customField snapshot and the generic-metadata path
15
+ * (picklistDependency / formula / rollupSummary / lookupRelationship / masterDetailRelationship all
16
+ * resolve to a CustomField). Keeping it in one place fixes F11: a picklist value set was normalized
17
+ * only on the dedicated path, so an EXT-531-style `picklistDependency` family (which routes through
18
+ * the generic verifier) saw a raw `<valueSet>` and failed `$.metadata.valueSet.values` — a
19
+ * false-fail.
20
+ */
21
+ /**
22
+ * Normalize a parsed `<valueSet>` so callers can read a flat `valueSet.values: string[]` (the
23
+ * inline picklist value API names, in document order) instead of navigating
24
+ * `valueSetDefinition.value[].fullName` — which is also brittle because a single <value> parses to
25
+ * an object, not an array. The raw structure is preserved; `values`, normalized `restricted`, and a
26
+ * global-value-set `valueSetName` are added on top. Returns the input unchanged for non-picklist
27
+ * (non-object) values.
28
+ */
29
+ export function normalizeValueSet(raw) {
30
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
31
+ return raw;
32
+ const vs = raw;
33
+ const out = { ...vs };
34
+ if (typeof vs.valueSetName === "string")
35
+ out.valueSetName = vs.valueSetName;
36
+ if (vs.restricted !== undefined)
37
+ out.restricted = vs.restricted === true || vs.restricted === "true";
38
+ const def = vs.valueSetDefinition;
39
+ const rawValues = def?.value;
40
+ const valueList = rawValues === undefined ? [] : Array.isArray(rawValues) ? rawValues : [rawValues];
41
+ const values = valueList
42
+ .map((v) => (v && typeof v === "object" ? v.fullName : v))
43
+ .filter((v) => typeof v === "string");
44
+ if (values.length > 0)
45
+ out.values = values;
46
+ // D5 (registry v11, live-caught EXT-15346): canonicalize the dependent-picklist valid-for matrix
47
+ // so a valueSettings comparison (sorted [{value, controllingFieldValues[]}]) can hold under a
48
+ // strict deep equality. Wire shape per entry: {valueName, controllingFieldValue: string|string[]}
49
+ // with the usual single-element object/array collapse. Entries without a valueName are dropped
50
+ // (never guessed).
51
+ const rawSettings = vs.valueSettings;
52
+ if (rawSettings !== undefined) {
53
+ const settingsList = Array.isArray(rawSettings) ? rawSettings : [rawSettings];
54
+ const canonical = settingsList
55
+ .flatMap((entry) => {
56
+ if (!entry || typeof entry !== "object")
57
+ return [];
58
+ const record = entry;
59
+ if (typeof record.valueName !== "string")
60
+ return [];
61
+ const rawControlling = record.controllingFieldValue;
62
+ const controllingFieldValues = (rawControlling === undefined ? [] : Array.isArray(rawControlling) ? rawControlling : [rawControlling])
63
+ .filter((c) => typeof c === "string")
64
+ .sort();
65
+ return [{ value: record.valueName, controllingFieldValues }];
66
+ })
67
+ .sort((a, b) => a.value.localeCompare(b.value));
68
+ if (canonical.length > 0)
69
+ out.valueSettings = canonical;
70
+ }
71
+ return out;
72
+ }
73
+ /**
74
+ * EXT-2040 (the Picklist wire shape): a Picklist default is NOT a top-level CustomField
75
+ * `<defaultValue>` on the wire — the Metadata API carries it as the per-value `<default>` flag
76
+ * inside `valueSet.valueSetDefinition.value[]` (exactly where the sf-cli-wrapper deploys it).
77
+ * Return the default-flagged value's API name (fullName) so the snapshot can project it to
78
+ * `$.metadata.defaultValue`; undefined when no value is flagged (a global value set, or simply no
79
+ * default — never fabricated). Handles the single-`<value>` object collapse and the parser handing
80
+ * the flag back as the string 'true' or a real boolean.
81
+ */
82
+ export function valueSetDefaultValue(raw) {
83
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
84
+ return undefined;
85
+ const def = raw.valueSetDefinition;
86
+ const rawValues = def?.value;
87
+ const valueList = rawValues === undefined ? [] : Array.isArray(rawValues) ? rawValues : [rawValues];
88
+ for (const v of valueList) {
89
+ if (!v || typeof v !== "object")
90
+ continue;
91
+ const entry = v;
92
+ if ((entry.default === true || entry.default === "true") && typeof entry.fullName === "string")
93
+ return entry.fullName;
94
+ }
95
+ return undefined;
96
+ }
97
+ /**
98
+ * Named numeric CustomField facets. XML leaves parse as STRINGS ("0"); the dedicated customField
99
+ * snapshot coerced these through a private asNumber of its own, but the generic-metadata path
100
+ * (formula / rollupSummary / picklistDependency / lookupRelationship / masterDetailRelationship)
101
+ * did not — so an equality check on `$.metadata.scale` compared 0 against "0" and false-failed
102
+ * (live-caught EXT-15347, a Number formula field with scale 0 scoring the family to 0 after a
103
+ * byte-perfect deploy). Both paths now share THIS, which is what keeps them from drifting apart
104
+ * again. Coerce at the read boundary, named keys only — never blanket-coerce (the
105
+ * customObjectSnapshot rule).
106
+ *
107
+ * This is EVERY member `@salesforce/types` declares as a number on CustomField (queried against
108
+ * src/direct/metadataSchema.generated.json), not the subset some family happened to hit. The old
109
+ * five-key list was that subset, and a snapshot is only as trustworthy as its narrowest coercion.
110
+ */
111
+ const NUMERIC_FACET_KEYS = [
112
+ "length",
113
+ "precision",
114
+ "relationshipOrder",
115
+ "scale",
116
+ "startingNumber",
117
+ "visibleLines"
118
+ ];
119
+ /**
120
+ * Named boolean CustomField facets — same read-boundary rule as the numerics, same provenance
121
+ * (every member the schema artifact declares `boolean` on CustomField).
122
+ *
123
+ * `required` was the only one of these ever coerced, because it was the only one the dedicated
124
+ * snapshot bothered to project. Widening the snapshot to carry every facet the org returns makes
125
+ * the rest reachable, and an uncoerced `unique: "true"` would land in the comparison substrate as a
126
+ * STRING — the exact class of defect EXT-966 and EXT-15347 exist for.
127
+ *
128
+ * `defaultValue` is deliberately NOT here: it is an xsd:string carrying formula syntax, so a
129
+ * checkbox default is legitimately the STRING "false" (see asDefaultValue in customFieldSnapshot).
130
+ * Coercing it would undo EXT-2040 rather than help it.
131
+ */
132
+ const BOOLEAN_FACET_KEYS = [
133
+ "caseSensitive",
134
+ "deprecated",
135
+ "escapeMarkup",
136
+ "externalId",
137
+ "isAIPredictionField",
138
+ "isConvertLeadDisabled",
139
+ "isFilteringDisabled",
140
+ "isNameField",
141
+ "isSortingDisabled",
142
+ "populateExistingRows",
143
+ "reparentableMasterDetail",
144
+ "required",
145
+ "restrictedAdminField",
146
+ "stripMarkup",
147
+ "trackFeedHistory",
148
+ "trackHistory",
149
+ "trackTrending",
150
+ "translateData",
151
+ "unique",
152
+ "writeRequiresMasterRead"
153
+ ];
154
+ /** Apply CustomField-level normalization (valueSet flattening + typed facet coercion) to a raw metadata object. */
155
+ export function normalizeCustomFieldMetadata(md) {
156
+ if (!md || typeof md !== "object")
157
+ return md ?? null;
158
+ const out = { ...md };
159
+ if (md.valueSet !== undefined)
160
+ out.valueSet = normalizeValueSet(md.valueSet);
161
+ for (const key of NUMERIC_FACET_KEYS) {
162
+ const v = out[key];
163
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)))
164
+ out[key] = Number(v);
165
+ }
166
+ for (const key of BOOLEAN_FACET_KEYS) {
167
+ if (out[key] === "true")
168
+ out[key] = true;
169
+ else if (out[key] === "false")
170
+ out[key] = false;
171
+ }
172
+ // LongTextArea omits <required> from the wire entirely — the platform forbids required long text
173
+ // areas — so the guaranteed value is projected instead of undefined (live-caught EXT-15345: the
174
+ // customField family false-failed `required == false` against an absent leaf).
175
+ if (out.required === undefined && out.type === "LongTextArea")
176
+ out.required = false;
177
+ return out;
178
+ }