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
|
@@ -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
|
+
}
|