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,89 @@
1
+ // Stable content fingerprinting — the one thing cc-mcp used @in2/contracts for.
2
+ //
3
+ // @in2/contracts is a 3,191-line platform-wide schema package (zod schemas for the certified
4
+ // pipeline's neutral protocol). The direct tier used exactly one function out of it. Carrying the
5
+ // whole package — and zod with it — to get a sha256 of a sorted object was not a dependency, it was
6
+ // an accident of where the code happened to live.
7
+ //
8
+ // Ported VERBATIM rather than simplified, deliberately. These hashes are truncated into session ids
9
+ // (`ses_844e89ff…`) that are already persisted on disk, so any behavioural drift here silently
10
+ // orphans existing sessions. semanticHash.test.ts pins the exact digests to catch that.
11
+ import { createHash } from "node:crypto";
12
+ /** Recursively sort object keys and drop undefined values, so key order cannot change a digest. */
13
+ export function sortJson(value) {
14
+ if (Array.isArray(value))
15
+ return value.map(sortJson);
16
+ if (value && typeof value === "object") {
17
+ const entries = Object.entries(value)
18
+ .filter(([, v]) => typeof v !== "undefined")
19
+ .sort(([a], [b]) => a.localeCompare(b));
20
+ const out = {};
21
+ for (const [k, v] of entries)
22
+ out[k] = sortJson(v);
23
+ return out;
24
+ }
25
+ return value;
26
+ }
27
+ export function stableStringify(value) {
28
+ return JSON.stringify(sortJson(value));
29
+ }
30
+ const NEUTRAL_PROTOCOL_SCHEMA_VERSION = "1.0.0";
31
+ // Upstream shape table, kept intact. cc-mcp never constructs these records, so in practice this
32
+ // only ever matters at depth 0 (where stripping is unconditional). It is retained anyway because
33
+ // the cost is a dead lookup and the benefit is digest-for-digest identity with the original.
34
+ const NEUTRAL_PROTOCOL_RECORD_SHAPES = [
35
+ ["requirementId", "currentRevisionId", "createdAt"],
36
+ ["revisionId", "requirementId", "revisionNumber", "source"],
37
+ ["batchId", "currentRevisionId", "sourceType"],
38
+ ["revisionId", "batchId", "requirementRevisionIds"],
39
+ ["subrequirementId", "currentRevisionId", "sourceRequirementIds"],
40
+ ["revisionId", "subrequirementId", "acceptanceConditions"],
41
+ ["resultId", "sessionId", "requirementRevisionId", "proposedSubrequirements"],
42
+ ["revisionId", "sessionId", "analyzerResultIds", "canonicalSubrequirements"],
43
+ ["sessionId", "requirementBatchRevisionId", "state", "executionMode"],
44
+ ["eventId", "sessionId", "sequence", "payload"],
45
+ ["planId", "planVersionId", "sessionId", "expectedOutcomes"],
46
+ ["approvalId", "sessionId", "coveredPlanVersionIds"],
47
+ ["observationId", "sessionId", "scope", "values"],
48
+ ["requestId", "sessionId", "executionUnit", "allowedOperationScope"],
49
+ ["receiptId", "requestId", "componentResults", "terminalTransportStatus"],
50
+ ["requestId", "subrequirementId", "expectedOutcomes", "observationScope"],
51
+ ["resultId", "requestId", "postObservationId", "comparisons"],
52
+ ["sessionId", "subrequirementId", "basisReferences", "settledAt"],
53
+ ["snapshotId", "sessionId", "counts", "rates"],
54
+ ["benchmarkId", "revisionId", "frozenDenominatorSubrequirementIds"],
55
+ ["attemptId", "benchmarkRevisionId", "engine"],
56
+ ["envelopeId", "payload", "redaction", "subjectReferences"]
57
+ ];
58
+ function isNeutralProtocolRecord(record) {
59
+ return (record.schemaVersion === NEUTRAL_PROTOCOL_SCHEMA_VERSION &&
60
+ NEUTRAL_PROTOCOL_RECORD_SHAPES.some((shape) => shape.every((key) => Object.hasOwn(record, key))));
61
+ }
62
+ function semanticIdentityPayloadAtDepth(value, depth) {
63
+ if (Array.isArray(value))
64
+ return value.map((entry) => semanticIdentityPayloadAtDepth(entry, depth + 1));
65
+ if (value && typeof value === "object") {
66
+ const record = value;
67
+ const protocolRecord = depth === 0 || isNeutralProtocolRecord(record);
68
+ const output = {};
69
+ for (const [key, child] of Object.entries(record)) {
70
+ // Drop the envelope's own bookkeeping so stamping last run's hash onto a record does not
71
+ // change what the record means — the "semantic" in the name.
72
+ if (protocolRecord && (key === "nonFunctionalMetadata" || key === "contentHash"))
73
+ continue;
74
+ output[key] = semanticIdentityPayloadAtDepth(child, depth + 1);
75
+ }
76
+ return output;
77
+ }
78
+ return value;
79
+ }
80
+ export function semanticIdentityPayload(value) {
81
+ return semanticIdentityPayloadAtDepth(value, 0);
82
+ }
83
+ export function sha256Json(value) {
84
+ return createHash("sha256").update(stableStringify(value)).digest("hex");
85
+ }
86
+ /** Stable 64-char hex fingerprint of a value, independent of key order. */
87
+ export function semanticContentHash(value) {
88
+ return sha256Json(semanticIdentityPayload(value));
89
+ }
@@ -0,0 +1,60 @@
1
+ // Resolve WHICH org a tool acts on, from the session rather than from a caller-supplied alias.
2
+ //
3
+ // The session is the source of truth for an engagement: it owns the org binding that
4
+ // activate_session verified, the imported requirements, the analysis, and the evidence. Tools that
5
+ // took a bare `targetOrg` were asking the model to re-state something the session already knows,
6
+ // and an alias is the weakest possible form of that answer — `org_login`'s own description says an
7
+ // alias is "operator-controlled and can be repointed".
8
+ //
9
+ // So the alias is treated as a POINTER, and this module is where the pointer is checked against
10
+ // what the session recorded. A repointed alias is the failure this exists to name: without the
11
+ // check it surfaces as a confusing "org not allowlisted" (the grant no longer matches), which sends
12
+ // someone hunting through gate config for what is actually a changed alias.
13
+ import { GateError } from "./gate.js";
14
+ import { defaultOrgIdResolver } from "./gate.js";
15
+ export class SessionOrgError extends Error {
16
+ code;
17
+ constructor(code, message) {
18
+ super(message);
19
+ this.code = code;
20
+ this.name = "SessionOrgError";
21
+ }
22
+ }
23
+ function bindingFor(session, role) {
24
+ return role === "target" ? session.targetOrg : session.campfireOrg ?? null;
25
+ }
26
+ /**
27
+ * The org this session is bound to for `role`, with the alias re-checked against the recorded id.
28
+ *
29
+ * `verify: false` skips the live check for read-only callers that would rather not pay an
30
+ * `sf org display` round-trip; writes always verify, because a repointed alias on a write is the
31
+ * case where being wrong is expensive.
32
+ */
33
+ export async function resolveSessionOrg(session, role, opts = {}) {
34
+ const binding = bindingFor(session, role);
35
+ if (!binding?.alias) {
36
+ throw new SessionOrgError("ORG_NOT_BOUND", role === "target"
37
+ ? `Session ${session.sessionId} has no target org bound. Redeem a Campfire code with activate_session, or bind one with create_session { targetOrgAlias, targetOrgId }.`
38
+ : `Session ${session.sessionId} has no Campfire (consultant) org bound. Import from Campfire first — import_requirements { campfire: { consultantOrg, names } } records it.`);
39
+ }
40
+ if (opts.verify === false)
41
+ return binding;
42
+ // Verification compares the alias against a RECORDED id. Without one there is nothing to compare,
43
+ // and quietly returning would turn "unverified" into "verified" — so it refuses instead.
44
+ if (!binding.orgId) {
45
+ throw new SessionOrgError("ORG_NOT_BOUND", `Session ${session.sessionId} has alias ${JSON.stringify(binding.alias)} for its ${role} org but no recorded Org ID, so the binding cannot be verified. Refusing fail-closed.`);
46
+ }
47
+ const resolver = opts.resolveOrgId ?? defaultOrgIdResolver;
48
+ const live = await resolver(binding.alias);
49
+ if (!live) {
50
+ // Deliberately a GateError: the caller's fail-closed handling for "we cannot tell which org
51
+ // this is" already exists and is the correct response here too.
52
+ throw new GateError("ORG_ID_UNRESOLVED", `Session ${session.sessionId} is bound to alias ${JSON.stringify(binding.alias)} (org ${binding.orgId}), but that alias does not resolve — it may have been logged out. Re-authenticate it, then retry. Refusing fail-closed.`);
53
+ }
54
+ if (live.slice(0, 15) !== binding.orgId.slice(0, 15)) {
55
+ throw new SessionOrgError("ALIAS_REPOINTED", `Alias ${JSON.stringify(binding.alias)} now points at org ${live}, but session ${session.sessionId} was bound to ${binding.orgId}. Refusing — an alias is operator-controlled and can be repointed, and acting on the new one would apply this session's work to a different org than it was approved against.`);
56
+ }
57
+ // Return the session's recorded id, not the freshly resolved one: they are equal here, and using
58
+ // the recorded value keeps the session the single authority.
59
+ return binding;
60
+ }
@@ -0,0 +1,19 @@
1
+ // `sf data query --json` exits 0 after silently capping results at SF_ORG_MAX_QUERY_LIMIT
2
+ // (default 10,000): a truncated read is indistinguishable from a complete one by exit code, and
3
+ // every call site that trusted `result.records` alone would report a short list as the whole
4
+ // answer. The envelope's `done` and `totalSize` are the only truncation signals the CLI gives.
5
+ //
6
+ // Returns a human-readable verdict when the read is provably incomplete, null otherwise. The
7
+ // guard acts only on a POSITIVE signal — an envelope that omits both counters (older plugin
8
+ // shapes, some tooling-API arms) makes no claim, and refusing it would fail-closed on every read.
9
+ export function sfQueryTruncation(parsed) {
10
+ const result = parsed?.result;
11
+ const records = result?.records ?? [];
12
+ const totalSize = result?.totalSize;
13
+ if (result?.done === false || (typeof totalSize === "number" && totalSize > records.length)) {
14
+ return (`query returned ${records.length} of ${typeof totalSize === "number" ? totalSize : "an unreported total"} records — ` +
15
+ `sf data query silently caps at SF_ORG_MAX_QUERY_LIMIT (default 10,000) and still exits 0. ` +
16
+ `The result is INCOMPLETE and must not be read as the full answer.`);
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,182 @@
1
+ import { execFile, spawn } from "node:child_process";
2
+ import { existsSync, openSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { recordOrgCall } from "./orgCallMeter.js";
5
+ // Environment for spawned `sf` CLI children. Two hazards, both live-caught:
6
+ // (1) .env.local names (SF_CLIENT_ID etc.) collide with sf CLI config vars and
7
+ // break auth resolution (documented in the writeback runbook);
8
+ // (2) the CLI's update check / telemetry do network I/O at startup and have been
9
+ // observed stalling a child past its kill timeout inside the MCP server.
10
+ export function sfCliEnv() {
11
+ const env = { ...process.env };
12
+ delete env.SF_CLIENT_ID;
13
+ delete env.SF_CLIENT_SECRET;
14
+ delete env.SF_LOGIN_URL;
15
+ delete env.SF_REDIRECT_URI;
16
+ delete env.NODE_ENV;
17
+ env.SF_DISABLE_AUTOUPDATE = "true";
18
+ env.SF_AUTOUPDATE_DISABLE = "true";
19
+ env.SF_SKIP_NEW_VERSION_CHECK = "true";
20
+ env.SF_DISABLE_TELEMETRY = "true";
21
+ return env;
22
+ }
23
+ // On Windows, `sf` is not an executable. Every way it lands on a machine — the private npm prefix
24
+ // this payload installs, a consultant's own `npm i -g`, or Salesforce's Windows installer — puts a
25
+ // cmd shim (sf.cmd) on PATH, and a cmd shim cannot be spawned directly: CreateProcess resolves only
26
+ // .exe/.com, and Node refuses .cmd/.bat without a shell outright (CVE-2024-27980). shell:true is
27
+ // not the fix — cmd.exe would re-tokenise every argument, and sf arguments carry SOQL with quotes
28
+ // and spaces. So the shim is resolved to the JavaScript entry it wraps and run with THIS process's
29
+ // node, which changes nothing about how sf behaves and everything about whether it starts.
30
+ //
31
+ // CC_MCP_SF_ENTRY (set by the generated Windows launcher) short-circuits the scan; without it the
32
+ // PATH is walked for sf.exe (runnable as-is) or a parseable sf.cmd.
33
+ export function jsEntryFromCmdShim(shimText, shimDir) {
34
+ // npm's cmd-shim invokes `"%_prog%" "%dp0%\node_modules\<pkg>\bin\run.js" %*` (older shims spell
35
+ // it %~dp0); oclif's Windows installer invokes `"%~dp0\..\client\bin\node.exe" "...\run.js"`.
36
+ // Both name the entry as a "-quoted .js path relative to the shim's own directory.
37
+ const m = /"%(?:~)?dp0%?\\?([^"]+\.js)"/i.exec(shimText);
38
+ if (!m)
39
+ return undefined;
40
+ return path.resolve(shimDir, m[1]);
41
+ }
42
+ let cachedSf = null;
43
+ function resolveWindowsSf() {
44
+ const entry = process.env.CC_MCP_SF_ENTRY?.trim();
45
+ if (entry && existsSync(entry))
46
+ return { command: process.execPath, prefixArgs: [entry] };
47
+ for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
48
+ if (!dir)
49
+ continue;
50
+ const exe = path.join(dir, "sf.exe");
51
+ if (existsSync(exe))
52
+ return { command: exe, prefixArgs: [] };
53
+ const shim = path.join(dir, "sf.cmd");
54
+ if (!existsSync(shim))
55
+ continue;
56
+ try {
57
+ const target = jsEntryFromCmdShim(readFileSync(shim, "utf8"), dir);
58
+ if (target && existsSync(target))
59
+ return { command: process.execPath, prefixArgs: [target] };
60
+ }
61
+ catch {
62
+ // Unreadable or unparseable shim — keep scanning; a later PATH entry may carry a usable one.
63
+ }
64
+ }
65
+ // Nothing resolved: return the bare name and let the spawn fail with ENOENT, which every caller
66
+ // already surfaces as a clear "sf is not available" error.
67
+ return { command: "sf", prefixArgs: [] };
68
+ }
69
+ export function sfInvocation(args) {
70
+ // CC_MCP_SF_ENTRY is honored on EVERY platform, not only Windows. `in2 setup` installs
71
+ // @salesforce/cli into a private prefix (~/.in2-agent/tools) when the machine has no sf, and the
72
+ // server's spawn environment — fixed by the MCP client, not by setup's shell — has no reason to
73
+ // carry that directory on PATH. The registered entry is the one channel that reliably reaches
74
+ // this process, so a bare-name resolution here would pass setup's own probe and then ENOENT on
75
+ // the first real deploy (adversarial review, 2026-08-21).
76
+ const entry = process.env.CC_MCP_SF_ENTRY?.trim();
77
+ if (entry && existsSync(entry))
78
+ return { command: process.execPath, args: [entry, ...args] };
79
+ if (process.platform !== "win32")
80
+ return { command: "sf", args };
81
+ if (!cachedSf)
82
+ cachedSf = resolveWindowsSf();
83
+ return { command: cachedSf.command, args: [...cachedSf.prefixArgs, ...args] };
84
+ }
85
+ const MAX_BUFFER = 64 * 1024 * 1024;
86
+ // Fire-and-forget child that OUTLIVES this call. `sf org login web` blocks until the human
87
+ // finishes the browser round-trip, which an MCP tool call cannot wait on (a client timeout
88
+ // would kill the login mid-flow and leave no auth file). So we detach it, tee its output to a
89
+ // log the status tool can read, and let the CLI's own localhost callback listener do its job.
90
+ // Returns the pid; the AUTHORITY on whether login succeeded is always a fresh `sf org display`.
91
+ export function spawnDetached(command, args, options) {
92
+ if (command === "sf")
93
+ ({ command, args } = sfInvocation(args));
94
+ const fd = openSync(options.logFile, "a");
95
+ const child = spawn(command, args, {
96
+ env: options.env ?? process.env,
97
+ detached: true,
98
+ stdio: ["ignore", fd, fd]
99
+ });
100
+ child.unref();
101
+ return { pid: child.pid };
102
+ }
103
+ export function runProcess(command, args, options) {
104
+ // METERED HERE, before resolution, for the same reason resolution itself is centralised here:
105
+ // this is the one funnel every `sf` launch passes through, and on Windows the resolved command is
106
+ // `node` (an sf.cmd shim cannot be spawned directly — see sfInvocation), so a meter reading the
107
+ // RESOLVED name would count zero sf spawns on precisely the platform whose spawn cost is the
108
+ // question. The caller's own word is the reliable signal.
109
+ const meterAsSf = command === "sf";
110
+ // Centralised here so nine call sites across five modules keep saying `runProcess("sf", …)` and
111
+ // never learn what sf resolves to on this platform.
112
+ if (command === "sf")
113
+ ({ command, args } = sfInvocation(args));
114
+ const startedAt = Date.now();
115
+ return new Promise((resolve) => {
116
+ // `encoding: "buffer"` makes execFile hand back Buffers, which is the only way to keep an exact
117
+ // byte count; the text form is then decoded from the same bytes so every existing caller sees
118
+ // precisely what it saw before. Off by default so the common path allocates one string, not two.
119
+ const binary = options.binary === true;
120
+ const child = execFile(command, args, {
121
+ cwd: options.cwd,
122
+ env: options.env ?? process.env,
123
+ timeout: options.timeoutMs,
124
+ killSignal: "SIGKILL",
125
+ maxBuffer: MAX_BUFFER,
126
+ ...(binary ? { encoding: "buffer" } : { encoding: "utf8" })
127
+ }, (error, rawStdout, rawStderr) => {
128
+ const durationMs = Date.now() - startedAt;
129
+ const stdoutBuffer = binary ? rawStdout : undefined;
130
+ const stdout = binary ? (stdoutBuffer?.toString("utf8") ?? "") : (rawStdout ?? "");
131
+ const stderr = binary
132
+ ? (rawStderr?.toString("utf8") ?? "")
133
+ : (rawStderr ?? "");
134
+ // Every outcome is metered, including the failures. A spawn that died ENOENT or was killed
135
+ // at its timeout still consumed the wall clock the operator waited through, and a meter
136
+ // that counted only successes would under-report exactly the runs that hurt.
137
+ if (meterAsSf)
138
+ recordOrgCall("cli", durationMs);
139
+ if (!error) {
140
+ resolve({
141
+ exitCode: 0,
142
+ stdout,
143
+ stderr,
144
+ timedOut: false,
145
+ durationMs,
146
+ ...(stdoutBuffer ? { stdoutBuffer } : {})
147
+ });
148
+ return;
149
+ }
150
+ const err = error;
151
+ const timedOut = err.killed === true && durationMs >= options.timeoutMs - 50;
152
+ const spawnError = typeof err.code === "string" ? `${err.code}: ${err.message}` : undefined;
153
+ resolve({
154
+ exitCode: typeof err.code === "number" ? err.code : null,
155
+ stdout,
156
+ stderr,
157
+ timedOut,
158
+ spawnError,
159
+ durationMs,
160
+ ...(stdoutBuffer ? { stdoutBuffer } : {})
161
+ });
162
+ });
163
+ if (options.input !== undefined) {
164
+ // EPIPE IS NOT AN EXCEPTION HERE — it is the child having exited first.
165
+ //
166
+ // A child that dies before reading its stdin (a broken repo, a bad flag, ENOENT) leaves the
167
+ // write end of the pipe with no reader, and Node raises `EPIPE` on the stream. With no
168
+ // `error` listener that is an UNHANDLED 'error' EVENT, which crashes the whole MCP server —
169
+ // measured, not theorised: a 500 KB write to `git cat-file --batch` outside a repository
170
+ // exits the process with an uncaught EPIPE. It only bites once the payload exceeds the OS
171
+ // pipe buffer (~16-64 KB), so the small stdin writes this helper started with never hit it
172
+ // and the batched blob reads can.
173
+ //
174
+ // Swallowed rather than surfaced: the child's own exit code and stderr are the real answer
175
+ // and are already reported through `RunResult`. A stdin that could not be delivered shows up
176
+ // there as the failure it caused, which is what every caller already handles — and telemetry,
177
+ // the mirror and the deploy path are all fail-open by contract.
178
+ child.stdin?.on("error", () => undefined);
179
+ child.stdin?.end(options.input);
180
+ }
181
+ });
182
+ }
@@ -0,0 +1,230 @@
1
+ // THE OPERATING CONTRACT — shipped as the MCP server's `instructions`.
2
+ //
3
+ // Why this exists as code rather than only as a document on disk. The playbook has three delivery
4
+ // shapes and only ONE of them is guaranteed:
5
+ // - MCP `instructions` (this file) — pushed to the model on connect, from the server, whatever
6
+ // the cwd. Always arrives.
7
+ // - a bundled SKILL (in2-agent-plugin, and ~/.claude/skills after install) — arrives when the
8
+ // model's own skill selection picks it up, which depends on the description matching.
9
+ // - reference/supervisor-playbook.md on disk — arrives only if the model obeys a read pointer.
10
+ // The file used to be named harness/CLAUDE.md, which implied a fourth channel that never fires: a
11
+ // CLAUDE.md is loaded only when the session's working directory is that folder, and the real user
12
+ // is non-technical, opens a session wherever they happen to be, and pastes one line ("activate my
13
+ // session with code X"). They will never `cd` into a harness directory, and must never be expected
14
+ // to describe the workflow themselves — every instruction they would have to type is a defect here.
15
+ //
16
+ // Keep it COMPACT: it is present in every context window this server touches.
17
+ //
18
+ // AND ASSUME IT ARRIVES TRUNCATED. Live-caught CF79: the Claude Code client cut this text at
19
+ // roughly 2.5 KB — mid-sentence in step 2 of the loop — so ~80% of the contract never reached the
20
+ // model, which reconstructed the rest from tool descriptions. A later cost audit observed a live
21
+ // client cutting at byte 2,050. Two consequences, both load-bearing: the first ~2 KB must carry the
22
+ // loop skeleton plus a pointer to the full playbook ON DISK, and the playbook must actually ship
23
+ // (build-artifact stages harness/supervisor-playbook.md as reference/supervisor-playbook.md; see
24
+ // SUPERVISOR_PLAYBOOK_PATH).
25
+ //
26
+ // THE ORDERING RULE THAT FOLLOWS FROM THAT, and the reason this file reads the way it does:
27
+ // EVERYTHING PAST THE FIRST ~2 KB MUST BE ASSUMED NOT TO ARRIVE. So the text is ordered by
28
+ // criticality — skeleton and pointer, then entry and session identity, then the loop, then the
29
+ // unenforced judgement rules — and no paragraph anywhere in it may be the ONLY statement of a rule
30
+ // the run depends on. Anything a refusal, a runtime note or a tool description already says at the
31
+ // moment it matters belongs there and not here: a duplicate placed at offset 20,000 of a text that
32
+ // gets cut at 2,050 is not redundancy, it is the copy truncation deletes first.
33
+ import { OPERATOR_VOICE_PATH, SUPERVISOR_PLAYBOOK_PATH } from "./paths.js";
34
+ import { betaEnabled } from "./betaFeedback.js";
35
+ export const SUPERVISOR_INSTRUCTIONS = `You are the Supervisor for IN2 delivery batches: Salesforce configuration work that arrives as
36
+ Campfire requirements or an explicit IN2 request. These tools turn requirements into verified org
37
+ changes. The operator is usually NON-TECHNICAL: they will paste a short line such as
38
+ "activate my session with code X" and nothing else. Everything below is YOUR job to run — never ask
39
+ them to name a tool, a step, an API name, or a metadata type, and never make them describe the
40
+ process. They decide business meaning and give the final go-ahead; that is all.
41
+
42
+ FULL PLAYBOOK ON DISK — Read ${SUPERVISOR_PLAYBOOK_PATH} at the start of every delivery run. Some
43
+ clients TRUNCATE these instructions; that file is the complete contract (full loop rationale,
44
+ live-caught lemmas, refusal handling, the successor-batch fast path and the blind-run rule that
45
+ gates it) and it is the authority if anything below appears cut off.
46
+
47
+ THE LOOP, IN ONE LINE: read every requirement -> \`project_org\` -> write the brief -> fan out one
48
+ analyzer per requirement -> \`plan_waves\` -> clarify -> ONE approval card -> \`direct_apply\` once
49
+ per wave -> report -> Campfire writeback -> \`close_batch\`. Expanded below.
50
+
51
+ AT EVERY BOUNDARY OF THAT LOOP, \`advance_phase {sessionId, completed, entering}\`: it RETURNS the
52
+ chapter's full instructions (arriving when they apply) plus a progress line to relay, and ships
53
+ the run's evidence so far. It never replaces \`close_batch\`.
54
+
55
+ START: a pasted activation code -> \`activate_session {code}\`, which authenticates their org (no
56
+ manual sf login) and imports the requirements they picked in Campfire. Any other start ->
57
+ \`create_session\` then \`import_requirements\`. An unauthenticated org -> \`org_login\`, a browser
58
+ flow THEY complete. Never ask for a password or token, and never re-authenticate an org yourself
59
+ when a session dies: CARD them with the fix, then \`close_batch\`, then stop — in that order.
60
+ \`close_batch\` makes no org round-trip, so it still works with every token on the machine dead, and
61
+ a run you walk away from instead reads as ABANDONED with no record that it ever ended.
62
+
63
+ PROVIDER BOUNDARY: an IN2 delivery runs on IN2's tools alone, activation through close_batch.
64
+ Never substitute another Salesforce MCP's tools for any part of it — reads, deploys, verification,
65
+ rollback, update, diagnostics — and never treat another MCP's output as IN2 evidence. If another
66
+ provider may have changed the org, re-run \`project_org\` before continuing.
67
+
68
+ CARRY THE sessionId, NOT AN ORG. Every org-touching tool derives its org from the session, bound
69
+ and verified when the session was created, so you never pass a target org to a write. The read-only
70
+ tools take an optional \`targetOrg\` ONLY to look at a different org than the session's.
71
+
72
+ THE LOOP:
73
+ 1. Read EVERY requirement yourself first and form the batch model: the shared objects, the
74
+ cross-requirement dependency ladder, and any ambiguity only visible across requirements.
75
+ 2. \`project_org\` over the touched scope. It is ground truth: requirement-asserted API names are
76
+ UNTRUSTED, and a component that already exists is a modify, never a clobbering create.
77
+
78
+ YOU ARE THE ONLY ONE WHO CAN WIDEN IT. Analyzers READ the projection and cannot extend it, so
79
+ anything you fail to project is not missing to them — it is INVISIBLE. Enumerate every metadata
80
+ TYPE the batch touches, not just the objects: tabs, apps, profiles, queues, groups, flows,
81
+ approval processes, email templates, permission sets.
82
+ 3. WRITE THE BRIEF, THEN FAN OUT — every Task in ONE MESSAGE, so wall clock is the slowest
83
+ analyzer, not the sum. The brief is <sessionDir>/analysis/brief.md: a shared preamble plus one
84
+ section per requirement. It carries DECISIONS and POINTERS — never paraphrases of what an
85
+ analyzer can read itself — and amendments are dated appends, so a re-dispatched analyzer reads
86
+ the CURRENT brief rather than a patched prompt. Before fan-out you MAY ask ONE early card of 1-3
87
+ questions (gate in the playbook; the half you must not lose is that each question NAMES the
88
+ requirements it unlocks).
89
+ Delegate ONE analyzer per requirement by default, as \`subagent_type: "plan-requirement"\`;
90
+ group 2-3 onto one ONLY on real coupling, never topical similarity, and a grouped analyzer
91
+ still writes ONE plan.json PER requirement. Each Task prompt is a fixed stub: read the
92
+ plan-requirement skill (the tool list names its path), you own <extId>(s), session <sessionId>,
93
+ read your brief section(s) FIRST. Each returns requirements/<extId>/plan.json listing what it
94
+ OWNS — the ownership that maps Configuration Items to requirements at writeback.
95
+ 4. Consolidate: dedupe ownership, then \`plan_waves {sessionId}\` for the deploy order.
96
+
97
+ A component this batch CHANGES AFTER CREATING IT is two entries in plan.json — the same
98
+ component, \`intent: "create"\` then \`intent: "update"\`, each naming its own \`authoredFile\`.
99
+ Never one file plus a wave you assemble by hand: that path skips the ownership and drop checks
100
+ entirely. The planner names the split and never writes it; what the second state must contain is
101
+ yours to author. A component the batch DELETES is an entry with \`intent: "delete"\` and no file:
102
+ plan_waves orders it but never executes it, so that call is yours and \`dryRunOnly\` goes first.
103
+ A component nobody projected comes back "not verifiable" rather than guessed — answer that with
104
+ a project_org pass, never by deploying past it.
105
+
106
+ Then clarify what GENUINELY FORKS the work (see CLARIFYING below). After answers,
107
+ \`record_operator_answers\` FIRST; then \`resolve_blocker\` when the answer is an edit you can
108
+ make, and a re-dispatch only when the reply changes WHICH components exist. Then ONE approval
109
+ card: the wave plan, the assumptions in force verbatim, and what verification will not prove.
110
+ 5. Execute wave by wave only after approval, ONE call per wave:
111
+ \`direct_apply {sessionId, wave, waveComponents}\` — that wave's component list from plan_waves,
112
+ assembled into <session>/authored/<wave> and deployed in the one call. THE WAVE TREE IS A BUILD
113
+ ARTIFACT rebuilt every call: a wave-specific fix goes in the requirement's own file under
114
+ requirements/<extId>/authored/, and an edit inside the wave tree is overwritten, never deployed.
115
+ \`direct_lint\` is the same assembly without a deploy.
116
+ 6. Report per requirement, and ALWAYS close with a user-flow verification checklist: what a human
117
+ must click to confirm behavior, because verification proves metadata shape, never runtime.
118
+ 7. Ask before any Campfire writeback. A WRITEBACK IS THE CONFIGURATION ITEMS — nothing else.
119
+ \`campfire_config_item_plan {sessionId}\` to PREVIEW the manifest, show it, then write it with
120
+ ONE \`campfire_write_config_items {sessionId}\` call, the only Configuration Item writer there
121
+ is. Deployment Tasks are a SEPARATE step under the same approval, through
122
+ \`create_campfire_deployment_tasks\`. The run NEVER writes the requirement record itself — not
123
+ notes, not Build Status, not Org Deployed To: this program automates configuration, deployment
124
+ is someone else's process, and a run that hands back manual work has not completed a build.
125
+ Analyzed inline, so no plan.json exists? Pass \`ownership: [{typeName, fullName, extId}]\` —
126
+ both tools refuse an unattributed plan.
127
+ 8. END THE RUN with ONE \`close_batch {sessionId}\` call, as the LAST tool call — after the final
128
+ wave and any writeback, and EQUALLY when the run ended badly (rejected, blocked, stopped early).
129
+ Nothing else states a batch's result or ships its evidence, so a run you do not close reads as
130
+ ABANDONED on the IN2 dashboard however well it went. It returns \`operatorPage\` — the results
131
+ page already rendered — so PASTE IT VERBATIM and write only what sits BELOW THE WHOLE BLOCK,
132
+ after its \`=== END OF BLOCK\` fence: the manual items, the Campfire count, rollback, and the
133
+ technical section. Never recompose or re-count it.
134
+
135
+ OPERATOR VOICE — two surfaces. Cards, progress lines and the end-results page speak Salesforce and
136
+ business outcomes, never harness internals; \`direct_apply\` returns \`operatorSummary\`, so relay
137
+ that string rather than the raw verdict.
138
+ Read ${OPERATOR_VOICE_PATH} before composing the approval card, and before writing the sections
139
+ beneath the rendered results page. Same rigor, reader's language; never soften a real failure.
140
+
141
+ CLARIFYING IS A PHASE, NOT ONE TOOL CALL, AND IT HAPPENS EARLY. Ask everything the batch genuinely
142
+ raises, back to back, BEFORE executing: what is minimised is the OPERATOR'S attention window, not
143
+ your call count. A question whose recommendation you would follow anyway is an assumption to state,
144
+ not a round-trip to spend. While executing, work around what you can and stop only when no progress
145
+ is possible. \`ask_operator\` only RECORDS a card — mirror every card through the native
146
+ AskUserQuestion UI, or the operator never sees it and cannot answer.
147
+
148
+ HARD RULES — the judgement calls no check can make for you:
149
+ - Never guess an API name. Project first; if the projection contradicts the requirement, ask.
150
+ - DELIVER IT, DO NOT DELEGATE IT. A Deployment Task hands work BACK to the consultant this system
151
+ exists to save, so it is a LAST resort, never a safe-looking default. Raise one only when the
152
+ thing genuinely cannot be deployed (data rows, an API-name change, an external system).
153
+ - READ INTENT ACROSS ALL THREE SURFACES: the Solution Design Summary (HOW), the user story, and the
154
+ acceptance criteria (what it is FOR). The Summary is hand-written and sometimes asks for the
155
+ impossible, or for what the other two contradict. Work out what was INTENDED, propose the
156
+ SMALLEST correction that achieves it, and recommend that — never refuse the requirement, never
157
+ silently substitute your own reading. A CORRECTION mechanism, not a licence to add: improving the
158
+ design is the consultant's job.
159
+ - ADDITIONS: BENIGN IS FINE, OBLIGATION-CREATING IS NOT. An obvious additive detail on a brand-new
160
+ component is welcome judgement. Forbidden is an addition that CREATES OBLIGATIONS: a reference to
161
+ a component that must then exist, a flag whose value implies further setup, anything that makes
162
+ more work necessary to succeed. NEVER PUT INVENTED SCOPE TO THE OPERATOR either — if you added it
163
+ and it is now a problem, remove it rather than escalating it as though the gap were theirs.
164
+ - NEVER MAKE SOMETHING REQUIRED THAT WAS NOT REQUESTED. A required field nobody asked for changes
165
+ data entry for every user of that object forever. Absolute violation, however benign it looks.
166
+ - "I could not verify that" is only TRUE AFTER YOU LOOKED. Not having checked is not the same as
167
+ not being able to check, and reporting the second when you mean the first is a false statement
168
+ about the org.
169
+ - DIFF_VERIFIED proves metadata SHAPE, never runtime behavior — say so on the approval card. A
170
+ CONTENT REMOVED note means the component lost content between its pre- and post-image: read the
171
+ pre-image and fix the file rather than acking a drop you have not looked at. Benign drift is not
172
+ worth the operator's attention; a real deviation is, as INFORMATION with a recommendation.
173
+ - UNDOING A WAVE IS \`direct_rollback {sessionId, wave}\`, never by hand and never by a sha — and a
174
+ custom field deleted under 15 days ago is not a deploy at all. Relay the tool's Setup undelete
175
+ card: an undelete restores the field AND ITS DATA, while a metadata re-create restores the
176
+ definition, reports success, and destroys every value in it.
177
+ - A Configuration Item is a DEPLOYMENT MANIFEST ROW, one per configurable component (a Workflow
178
+ file yields its alerts, not itself). Pass \`{typeName, fullName, extId}\`, split by the tool.
179
+ - Report refusals and failures verbatim. Refusals are fail-closed: nothing deployed, nothing to undo.`;
180
+ /**
181
+ * BETA ONLY — appended to the contract above when `CC_MCP_BETA` is on, and absent otherwise.
182
+ *
183
+ * Server-generated and gated on the flag DIRECTLY rather than on tool presence, unlike the analyzer
184
+ * skills. The skills are static files installed once and shared by every run on the machine, so they
185
+ * have to condition on what they can see at runtime ("if report_feedback is among your tools"). This
186
+ * text is composed per connection by the process that already knows the answer, so an install that
187
+ * is not in beta ships a contract with no trace of the channel — which is the property that makes
188
+ * turning beta off a real off.
189
+ *
190
+ * THE WORDING OF THE QUESTIONS IS NOT RESTATED HERE. It used to be, on a redundancy argument: the
191
+ * contract arrives on connect and truncates (CF79), so state the frozen questions in both channels.
192
+ * That argument does not survive its own premise — this section begins ~7 KB into a text observed
193
+ * cut at byte 2,050, so a client that truncates deletes this copy FIRST, before it deletes
194
+ * `record_operator_feedback`'s description, which is generated from the same frozen constants
195
+ * (`OPERATOR_QUESTIONS` / `OPERATOR_OPTIONS`), is shorter, and is gated on exactly the same flag.
196
+ * What survives here is what neither tool description carries: the four obligations named as
197
+ * obligations, and the fatigue budget, which is a budget on BEHAVIOUR rather than a question wording.
198
+ */
199
+ export const BETA_FEEDBACK_SECTION = `
200
+
201
+ BETA — YOU ALSO REPORT ON THE RUN ITSELF. \`report_feedback\` and \`record_operator_feedback\` are
202
+ yours, and FOUR things are expected on EVERY run, including runs that go perfectly. Both tools
203
+ carry their exact wording, placement rules and guardrails; follow those, never a paraphrase.
204
+
205
+ - STUMBLE? SAY SO IN THE MOMENT with \`report_feedback\`, while the exact error text is still in
206
+ front of you, and QUOTE it.
207
+ - AT CLOSE, FILE THE WRAP-UP, EVERY RUN — \`report_feedback {wrapUp: {...}}\`. A clean run with
208
+ empty lists is the denominator the whole trend rests on. Never invent friction to look useful.
209
+ - ONE CARD BEFORE EXECUTION, the moment they approve the plan and BEFORE THE FIRST DEPLOY: the
210
+ manual-time estimate. ASK IT THERE AND NOWHERE ELSE, and it rides AFTER the approval card, never
211
+ on it. ASK IT PLAINLY — never tell the operator why it is asked at that moment.
212
+ - THEN THE CLOSE CARDS, after the final writebacks and \`close_batch\`. Put their ACTUAL WORDS in
213
+ \`openAnswer\` — verbatim, never your summary of them. If they would rather not answer, do not
214
+ press: \`declined: true\` is a recorded skip and is itself data. The relational card goes out
215
+ ONLY WHEN THE ACTIVATION SAID IT WAS DUE; IF IT DID NOT SAY SO, DO NOT ASK.
216
+ - THE FATIGUE BUDGET IS PART OF THE INSTRUMENT, not a nicety: ONE card before execution, AT MOST
217
+ THREE at close, and the relational question only when it is due. A consultant trained to
218
+ speed-tap cards poisons the longitudinal record this channel exists to build, so an extra
219
+ question is not a free data point — it is a withdrawal from every answer after it. Do not invent
220
+ additional feedback questions of your own.`;
221
+ /**
222
+ * The contract this process actually ships, composed at connect time.
223
+ *
224
+ * A FUNCTION rather than a second constant because the flag is read from the environment, and a
225
+ * module-level ternary would freeze the answer at import — connect time is when the process that
226
+ * knows the flag actually composes the contract, so that is when it is read.
227
+ */
228
+ export function supervisorInstructions(env = process.env) {
229
+ return betaEnabled(env) ? `${SUPERVISOR_INSTRUCTIONS}${BETA_FEEDBACK_SECTION}` : SUPERVISOR_INSTRUCTIONS;
230
+ }