Add Motion: the go-to-market operating system on top of the ledger

The ledger answers which contracted capacity is sold, to whom, at what
margin. It says nothing about the motion — the repeatable practice that
turns a customer conversation into a scoped deployment, and turns that
deployment into something the next one reuses.

Motion is deliberately not a parallel entity tree. DEMAND_STAGES already
is the motion, so Motion binds reusable artefacts to the stages of a
demand deal that already exists: an engagement hangs off one deal,
cascade deleted, one per deal by unique constraint.

Nine closed kinds, each declaring which stages it serves, and a starter
library of twelve templates covering all eight open stages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-17 18:27:03 -07:00
parent 99d165b5e5
commit 516685526c
61 changed files with 13013 additions and 29 deletions
+73 -1
View File
@@ -9,7 +9,7 @@
*
* Design rules, learned from tool surfaces that went wrong:
*
* **Keep it small.** Nine tools, each doing one thing. A sprawling tool list
* **Keep it small.** Ten tools, each doing one thing. A sprawling tool list
* degrades model performance more than it adds capability; anything genuinely
* niche belongs behind `pig_search` or the HTTP API.
*
@@ -22,6 +22,7 @@
* and quote back to a human. A wall of raw JSON forces the model to re-derive
* meaning that the server already knows.
*/
import { DEMAND_STAGES, MOTION_KINDS } from '@pig/core';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
@@ -542,6 +543,77 @@ export function createPigMcpServer(options: PigMcpOptions): McpServer {
},
);
// ----------------------------------------------------------- motion library
server.registerTool(
'pig_motion_library',
{
title: 'Search the motion library',
description:
'Find reusable go-to-market practice: discovery guides, qualification frameworks, POC ' +
'structures, proposal blocks, pricing inputs, reference architectures, case studies, ' +
'technical narratives and deployment playbooks, each bound to the demand stage it ' +
'serves. Use it before writing a proposal or scoping a POC from scratch — a template ' +
'with a usage count is language that has already survived a customer.',
inputSchema: {
kind: z
.enum(MOTION_KINDS)
.optional()
.describe('Restrict to one kind of template. Omit for all nine.'),
stage: z
.enum(DEMAND_STAGES)
.optional()
.describe('Restrict to templates serving one demand stage, e.g. proposal.'),
query: z.string().optional().describe('Matched against title, summary and slug'),
},
},
async (input) => {
const params = new URLSearchParams();
if (input.kind) params.set('kind', input.kind);
if (input.stage) params.set('stage', input.stage);
if (input.query) params.set('q', input.query);
const { templates, truncated } = await api.request<{
templates: {
id: string;
kind: string;
slug: string;
version: number;
title: string;
summary: string;
stage: string;
visibility: string;
usageCount: number;
updatedAt: string;
}[];
truncated: boolean;
}>(`/api/motion/templates?${params}`);
if (templates.length === 0) {
return ok(
'No template matches that request. Note that this key sees shared templates and ' +
"the caller's own private drafts, so a colleague's draft will not appear.",
);
}
const lines = [
`${templates.length}${truncated ? '+' : ''} template(s), newest version of each:\n`,
];
for (const t of templates.slice(0, 25)) {
lines.push(
`${t.title}${t.kind}, ${t.stage} stage, v${t.version}` +
`${t.visibility === 'private' ? ' [private draft]' : ''}`,
` ${t.summary}`,
// Usage is the difference between practice and a document nobody
// opened, and it is the field a model should rank on.
` Instantiated ${t.usageCount} time(s) · id: ${t.id}`,
'',
);
}
return ok(lines.join('\n'));
},
);
// ------------------------------------------------------------- log activity
server.registerTool(