references/master-template.example.md
# MASTER AGREEMENT: MUTUAL NON-DISCLOSURE, {{FEE_TITLE}} AND NON-CIRCUMVENTION
**Template draft for review, not an execution copy. Complete all bracketed directives and party fields and obtain the required legal and operator review before preparing any execution document. Schedule A notices apply only when authorized by the executed agreement.**
This Master Agreement (the **Agreement**) is entered into as of **{{DATE}}** between **[OUR LEGAL NAME]**, a [our jurisdiction and form], at [our address] (**Us**), and **{{CP_LEGAL}}**, a {{CP_JURIS}}, at {{CP_ADDR}} (**{{CP_SHORT}}**). Each is a **Party**.
## 1. Definitions
- **Transaction:** [define the covered dealings between {{CP_SHORT}} and a Protected Counterparty, including renewals and replacements].
- **Contract Value:** [define the base the fee is computed on].
- **Protected Counterparty:** [a party or lot first identified in writing by the introducing Party in a Schedule A notice, together with affiliates and nominees].
- **Schedule A notice:** a dated, approved, counterparty-specific written notice delivered through the notice channel authorized by the executed agreement, identifying the Protected Counterparty and the terms and fee that the agreement permits to be stated by notice. It contains no internal negotiation detail or third-party economics. An authorized entry takes effect on the notice date unless {{CP_SHORT}} objects within [objection window] with dated written evidence of a substantive pre-existing relationship.
- **Protection Period:** [period] from each Schedule A notice, for that entry.
## 2. Standing arrangement
{{ROLE_CLAUSE}} [Independent-introducer language: no authority to bind, not a party to the Transaction unless a Schedule A entry says otherwise, direct contact permitted provided economics are preserved.]
## 3. Fee
{{FEE_CLAUSE}}
**Standard Fee.** [Insert the counsel-approved fee schedule.] A different fee may be recorded by Schedule A notice only to the extent the executed agreement expressly authorizes that variation; otherwise obtain the required signed amendment first.
**Payment.** [When the fee is due relative to funds received.]
**Reporting.** [What documents the paying Party sends and when.]
## 4. Non-circumvention, both directions
[Mutual non-circumvention covenant limited to counterparties first introduced by the other Party under this Agreement, with the usual carve-outs for pre-existing and independently sourced relationships.]
## 5. Mutual non-disclosure
[Definition of Confidential Information, exclusions, permitted disclosures, compelled disclosure, return or destruction, no publicity, survival.]
## 6. No commitment; term
[No obligation to transact; term and renewal; survival of Protection Periods and confidentiality.]
## 7. General
[Liability cap and carve-outs; injunctive relief; governing law and forum; assignment; notices by email to the signature page addresses; entire agreement on its subject matter; {{SUPPLEMENT_CLAUSE}}amendable only in a signed writing, except for Schedule A entries expressly authorized by this Agreement to be added by notice without changing its standing terms; changes outside that notice authority require the agreed amendment procedure; electronic signatures and counterparts.]
## Schedule A (rolling)
Only entries within the executed agreement's express notice authority are added by Schedule A notice as defined in Section 1; a notice does not itself authorize an amendment to standing terms. Each entry states the Protected Counterparty or lot, the introducing Party's role, the commercial terms, and the fee (standard unless stated).
<!-- markdownlint-disable MD055 MD056 -->
| # | Date | Protected Counterparty or lot | Role | Terms | Fee |
|---|---|---|---|---|---|
{{SCHEDULE_ROWS}}
<!-- markdownlint-enable MD055 MD056 -->
```{=openxml}
<w:p><w:r><w:br w:type="page"/></w:r></w:p>
```
## Signatures
**[OUR LEGAL NAME]**
By: _________________________________
Name: [our signer]
Title: [our signer title]
Email: [our signer email]
Date: _________________________________
**{{CP_SIGBLOCK}}**
By: _________________________________
Name: {{CP_SIGNER}}
Title: {{CP_TITLE}}
Email: {{CP_EMAIL}}
Date: _________________________________
references/spec.example.json
{
"file": "AcmeSupplier",
"short": "Acme",
"role": "supplier",
"date": "September 2, 2026",
"legal": "Acme Compute Ltd",
"juris": "England and Wales company",
"addr": "1 Example Street, London",
"signer": "A. Person",
"title": "Director",
"email": "signer@example.com",
"schedule": [
["1", "2026-08-20", "Lot A (16 nodes)", "introducer", "12 months", "standard"]
],
"supplement": "the Data Processing Addendum dated 2026-09-01"
}
scripts/build-agreement.js
#!/usr/bin/env node
'use strict';
/**
* Build a counterparty master agreement from a template and a JSON spec.
*
* Usage: node build-agreement.js <template.md> <spec.json> <out_dir> [--require-docx | --markdown-only]
*
* Writes draft Markdown and requires matching DOCX unless --markdown-only is explicit.
* No Node dependencies. DOCX conversion requires installed pandoc. Node >= 18.
*/
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const DRAFT_NOTICE = '**DRAFT: For review only. Not an execution copy or authorization to send.**';
const CONVERTER_OPTIONS = { encoding: 'utf8', timeout: 10000, maxBuffer: 1024 * 1024 };
const BLANK = '______________________________';
const EMPTY_SCHEDULE_ROW = '| | | *(no entries at signing)* | | | |';
const ROLE_CLAUSES = {
buyer: {
title: 'REFERRAL FEE',
role: '{cp} appoints Us on a non-exclusive basis to source and introduce counterparties for {cp}\'s requirements, and {cp} pays Us the fee in Section 3 on each Transaction with a Protected Counterparty.',
fee: '{cp} pays Us a referral fee on each Transaction between {cp} (or its affiliates) and a Protected Counterparty introduced by Us.',
},
supplier: {
title: 'SOURCING FEE',
role: '{cp} offers capacity to Us and to buyers We introduce, and pays Us the fee in Section 3 on each Transaction with a Protected Counterparty; where We elect to buy as principal for an entry, We contract directly with {cp} on the terms stated on Schedule A.',
fee: '{cp} pays Us a sourcing fee on each Transaction between {cp} (or its affiliates) and a Protected Counterparty introduced by Us.',
},
mutual: {
title: 'REFERRAL AND SOURCING FEE',
role: 'Each Party may introduce the other to counterparties. The Party that closes a Transaction with a Protected Counterparty introduced by the other pays the fee in Section 3; where We supply {cp} as principal, Our economics are in Our price and no fee is payable on that entry.',
fee: 'The Party that closes a Transaction with a Protected Counterparty first introduced by the other Party pays the introducing Party the fee below.',
},
};
function defaultDate(now = new Date()) {
return now.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
}
function encodeScheduleCell(cell) {
const entity = character => `&#${character.codePointAt(0)};`;
// Entities keep data out of Markdown/HTML syntax, including smart punctuation.
// Preserve single internal spaces and ordinary dates/example text as written.
return String(cell).replace(/\r\n|\r|\n/g, ' ')
.replace(/[\\|`*_{}[\]<>!&#~^$'"@]/g, entity)
.replace(/-{2,}|\.{3,}/g, run => [...run].map(entity).join(''))
.replace(/^ +| +$| {2,}|[^\S ]/gu, run => [...run].map(entity).join(''));
}
function renderScheduleRows(rows) {
if (rows === undefined) {
return EMPTY_SCHEDULE_ROW;
}
if (!Array.isArray(rows)) {
throw new Error('spec.schedule must be an array of six-cell rows');
}
if (rows.length === 0) return EMPTY_SCHEDULE_ROW;
return Array.from(rows, (row, rowIndex) => {
if (!Object.hasOwn(rows, rowIndex) || !Array.isArray(row) || row.length !== 6) {
throw new Error(`spec.schedule[${rowIndex}] must be a dense six-cell array`);
}
const cells = Array.from(row, (cell, cellIndex) => {
if (!Object.hasOwn(row, cellIndex) ||
!((typeof cell === 'string' && !/\p{Surrogate}/u.test(cell)) ||
(typeof cell === 'number' && Number.isFinite(cell)))) {
throw new Error(`spec.schedule[${rowIndex}][${cellIndex}] must be valid Unicode text or a finite number`);
}
return encodeScheduleCell(cell);
});
return `| ${cells.join(' | ')} |`;
}).join('\n');
}
function buildValues(spec, now) {
if (!spec || typeof spec !== 'object') {
throw new Error('spec must be an object');
}
for (const key of ['file', 'short', 'role']) {
if (typeof spec[key] !== 'string' || spec[key].trim() === '') {
throw new Error(`spec.${key} is required`);
}
}
// Reject path syntax on every host, including Windows paths supplied on POSIX.
if (/[<>:"/\\|?*\p{Cc}]/u.test(spec.file) ||
/[. ]$/.test(spec.file) ||
/^(con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(spec.file)) {
throw new Error('spec.file must be a portable filename without path components or control characters');
}
const clauses = ROLE_CLAUSES[spec.role];
if (!clauses) {
throw new Error(`unknown role "${spec.role}"; expected one of ${Object.keys(ROLE_CLAUSES).join(', ')}`);
}
const cp = spec.short;
const fill = text => text.split('{cp}').join(cp);
const supplement = typeof spec.supplement === 'string' && spec.supplement.trim() ? `${spec.supplement.trim()}; ` : '';
return {
FEE_TITLE: clauses.title,
CP_SHORT: cp,
DATE: spec.date || defaultDate(now),
CP_LEGAL: spec.legal || BLANK,
CP_JURIS: spec.juris || BLANK,
CP_ADDR: spec.addr || BLANK,
ROLE_CLAUSE: fill(clauses.role),
FEE_CLAUSE: fill(clauses.fee),
SCHEDULE_ROWS: renderScheduleRows(spec.schedule),
SUPPLEMENT_CLAUSE: supplement,
CP_SIGBLOCK: (spec.legal || cp).toUpperCase(),
CP_SIGNER: spec.signer || BLANK,
CP_TITLE: spec.title || BLANK,
CP_EMAIL: spec.email || BLANK,
};
}
function render(template, spec, now) {
const values = buildValues(spec, now);
let output = template;
for (const [key, value] of Object.entries(values)) {
output = output.split(`{{${key}}}`).join(value);
}
const leftover = output.match(/\{\{[A-Z_]+\}\}/g);
if (leftover) {
throw new Error(`template has unfilled placeholders: ${[...new Set(leftover)].join(', ')}`);
}
return `${DRAFT_NOTICE}\n\n${output}`;
}
function pandocAvailable() {
const probe = spawnSync('pandoc', ['--version'], CONVERTER_OPTIONS);
return !probe.error && probe.status === 0;
}
function outputPaths(outDir, file) {
const root = path.resolve(outDir);
const destinations = ['md', 'docx'].map(extension => path.resolve(root, `${file} MASTER.${extension}`));
for (const destination of destinations) {
if (path.dirname(destination) !== root) {
throw new Error('spec.file must keep generated files directly inside the output directory');
}
// lstat also detects dangling links. Check BOTH outputs before the first write,
// even when conversion is disabled. The caller must control this directory;
// these checks do not isolate concurrent hostile filesystem changes.
let stat;
try {
stat = fs.lstatSync(destination);
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (stat?.isSymbolicLink()) {
throw new Error('output destination must not be a symlink');
}
}
return { root, mdPath: destinations[0], docxPath: destinations[1] };
}
function build(templatePath, specPath, outDir, options = {}) {
const template = fs.readFileSync(templatePath, 'utf8');
const spec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
const markdown = render(template, spec, options.now);
const { root, mdPath, docxPath } = outputPaths(outDir, spec.file);
fs.mkdirSync(root, { recursive: true });
fs.writeFileSync(mdPath, markdown, 'utf8');
// Generated DOCX is replaceable output. Never leave a stale or partial copy
// beside a newly built Markdown draft, including explicit Markdown-only builds.
fs.rmSync(docxPath, { force: true });
const result = { markdown: mdPath, docx: null, docxSkipped: false, documentStatus: 'draft' };
if (options.markdownOnly === true) {
result.docxSkipped = true;
return result;
}
const canConvert = options.pandoc === undefined ? pandocAvailable() : options.pandoc;
if (!canConvert) {
throw new Error('DOCX required: pandoc unavailable; use --markdown-only for an explicit Markdown-only draft');
}
try {
const converted = spawnSync('pandoc', [mdPath, '-o', docxPath], CONVERTER_OPTIONS);
if (converted.error || converted.status !== 0) {
throw new Error('pandoc conversion failed; DOCX unavailable');
}
const artifact = fs.lstatSync(docxPath);
if (!artifact.isFile() || artifact.size === 0) {
throw new Error('pandoc did not produce a nonempty regular DOCX artifact');
}
} catch (error) {
fs.rmSync(docxPath, { force: true });
if (error.code === 'ENOENT') throw new Error('pandoc did not produce a DOCX artifact');
throw error;
}
result.docx = docxPath;
return result;
}
function main(argv) {
const [templatePath, specPath, outDir, ...flags] = argv;
if (!templatePath || !specPath || !outDir ||
flags.some(flag => !['--require-docx', '--markdown-only'].includes(flag)) ||
flags.length > 1) {
console.error('usage: build-agreement.js <template.md> <spec.json> <out_dir> [--require-docx | --markdown-only]');
return 2;
}
try {
const result = build(templatePath, specPath, outDir, { markdownOnly: flags.includes('--markdown-only') });
console.log(`wrote ${result.documentStatus} ${result.markdown}`);
if (result.docxSkipped) {
console.log('docx skipped: explicit Markdown-only draft; no e-sign input produced');
} else {
console.log(`wrote ${result.documentStatus} ${result.docx}`);
}
return 0;
} catch (error) {
console.error(`build-agreement: ${error.message}`);
return 1;
}
}
if (require.main === module) {
process.exit(main(process.argv.slice(2)));
}
module.exports = { ROLE_CLAUSES, EMPTY_SCHEDULE_ROW, BLANK, buildValues, render, renderScheduleRows, build, main };
SKILL.md
---
name: master-agreement-generator
description: Generate review drafts of counterparty master agreements from one template plus a JSON spec, with role-selected clauses and a Schedule A workflow limited to the executed agreement's notice authority. Use when you need reproducible drafting and separately reviewed execution preparation.
---
# Master Agreement Generator
One master template, one small spec per counterparty, one draft build step.
The generator always labels output **DRAFT**, including documents generated
from a completed template. A successful conversion proves artifact generation,
not legal completeness, authority to contract, or readiness to send or sign.
An executed agreement may permit designated opportunities to be added by notice;
that authority must be established before using the Schedule A workflow.
## When to Use
- You issue a framework agreement (NDA, referral or sourcing fee,
non-circumvention, master services) to many counterparties with the same
terms and a few party-specific fields.
- Deals are added over time and re-papering each one is the bottleneck.
- Documents must be reproducible from tracked source, diffable, and free of
hand edits.
- Signature fields are placed by automation and need a stable page layout.
## How It Works
### Template
A single markdown template with `{{PLACEHOLDER}}` fields. Every party-specific
value is a placeholder; everything else is fixed text. A skeleton lives at
[references/master-template.example.md](references/master-template.example.md).
Replace its generic sentences with your counsel-approved clauses.
Placeholders the reference script fills:
| Placeholder | Source |
| --- | --- |
| `{{DATE}}` | `spec.date`, default today |
| `{{CP_SHORT}}` | `spec.short` |
| `{{CP_LEGAL}}`, `{{CP_JURIS}}`, `{{CP_ADDR}}` | spec fields, or a blank line when the counterparty completes them at signing |
| `{{ROLE_CLAUSE}}`, `{{FEE_TITLE}}`, `{{FEE_CLAUSE}}` | selected by `spec.role` from the role table |
| `{{SCHEDULE_ROWS}}` | `spec.schedule`, or one "no entries at signing" row |
| `{{SUPPLEMENT_CLAUSE}}` | `spec.supplement`, rendered with a trailing separator or empty |
| `{{CP_SIGBLOCK}}`, `{{CP_SIGNER}}`, `{{CP_TITLE}}`, `{{CP_EMAIL}}` | signature block fields, blanks when unknown |
### Spec
One JSON file per counterparty:
```json
{
"file": "AcmeSupplier",
"short": "Acme",
"role": "supplier",
"legal": "Acme Compute Ltd",
"juris": "England and Wales company",
"addr": "1 Example Street, London",
"signer": "A. Person",
"title": "Director",
"email": "signer@example.com",
"schedule": [["1", "2026-09-01", "Lot A (16 nodes)", "introducer", "12 months", "standard"]],
"supplement": "the Data Processing Addendum dated 2026-09-01"
}
```
Only `file`, `short`, and `role` are required for a draft. Missing signature fields
render as blank lines for review and completion. See
[references/spec.example.json](references/spec.example.json).
`file` must be a nonempty portable filename, such as `AcmeSupplier` or
`Acme Supplier`, without directory components. The builder rejects either path
separator, drive/UNC syntax, control characters, Windows-reserved punctuation
or device names, and trailing dots or spaces. Invalid names are rejected without
sanitizing or renaming them, before creating output or invoking pandoc.
Omit `schedule` or use `[]` for the “no entries at signing” placeholder. A supplied
schedule must otherwise be a dense array of six-cell arrays, in this order:
number, date, protected counterparty or lot, role, terms, fee. Each cell must be
a valid Unicode string or finite number; empty strings are allowed for intentional blanks.
Nulls, booleans, objects, nested cell arrays, missing cells and non-finite numbers
are rejected with a row/cell index before any artifact write or pandoc activity.
Unpaired UTF-16 surrogates are also rejected rather than replaced during UTF-8
output; valid supplementary characters, such as emoji, remain supported.
Cells are plain text, not Markdown or HTML. The builder encodes syntax characters
so literal pipes, backslashes, backticks and markup stay in their original fields.
Each CRLF, bare CR or LF becomes a space; text around line breaks is retained.
Other whitespace and literal punctuation are preserved in the rendered cells.
The source spec is not modified. An ordinary valid schedule retains its six
columns; malformed input is never silently replaced with an empty schedule.
### Role table
`spec.role` selects three strings: the standing-arrangement clause, the fee
section title, and the fee clause opener.
| Role | Who pays | Shape of the clause |
| --- | --- | --- |
| buyer | The counterparty pays on transactions with introduced parties | Counterparty appoints us on a non-exclusive basis to source and introduce |
| supplier | The counterparty pays on transactions with introduced parties; where we buy as principal we contract on the schedule terms | Counterparty offers capacity to us and to buyers we introduce |
| mutual | Whoever closes with the other's introduction pays | Each party may introduce; the closing party pays |
Unknown roles are rejected at build time.
### Build
Use operator-reviewed templates and specs only. Ordinary template substitutions
outside Schedule A are markup-capable, not a sanitizer for untrusted documents.
Pandoc can read referenced local or remote resources; this generator does not
sandbox the converter's filesystem or network access. Review those references
and run conversion in your own appropriately restricted environment. The focused
tests use a synthetic converter and do not certify real DOCX layout or isolation.
```sh
node skills/master-agreement-generator/scripts/build-agreement.js \
skills/master-agreement-generator/references/master-template.example.md \
specs/AcmeSupplier.json \
out/
```
The script fills placeholders, renders the schedule table, and writes
`out/<file> MASTER.md` with a mandatory DRAFT notice. By default (or with
`--require-docx`) it requires installed pandoc to produce a nonempty regular
`.docx` artifact. Missing pandoc, failed conversion or missing/empty output
returns exit code 1. Each pandoc probe or conversion is bounded to ten seconds.
Unknown, duplicate or conflicting flags return exit code 2.
Use `--markdown-only` explicitly for a successful Markdown-only draft. This mode
never probes or invokes pandoc, returns `docxSkipped: true` from the library,
and provides no DOCX for an e-sign workflow. Library callers must pass
`{ markdownOnly: true }`; `{ pandoc: false }` alone now fails the DOCX requirement.
The result always reports `documentStatus: 'draft'`. Existing generated DOCX is
removed when rebuilding its Markdown, and failed conversion leaves no partial
DOCX, so an earlier artifact cannot masquerade as the current output. Keep
both generated files out of version control; the template and specs are source.
There is no execution-copy mode. The example deliberately contains unresolved
bracketed drafting directives; filling `{{PLACEHOLDER}}` tokens does not complete
those legal provisions. Before preparing an execution document, obtain separate
review of the completed clauses, party details, authorized signer, commercial
terms and exact document version. Preserve the draft and the reviewed execution
copy as distinct records. Even a successful DOCX conversion does not authorize an
upload, send or signature. See the esign-field-placement approval workflow.
Both output destinations must be direct children of the resolved output directory.
An existing symlink at either destination, including a dangling link, is rejected
before either artifact is written, even when DOCX conversion is disabled. Ordinary
regular files can be rebuilt. Use an output directory you control; these checks
do not provide isolation against concurrent hostile filesystem changes. Returned
artifact paths are absolute.
### Signature page geometry
The template ends the body with an OpenXML page break so the signature block
requests a fresh page in a compatible DOCX renderer:
````markdown
```{=openxml}
<w:p><w:r><w:br w:type="page"/></w:r></w:p>
```
````
The signature page structure (our block, then the counterparty block, each with
By, Name, Title, Email, Date) is consistent, but pagination can change with text,
fonts, renderer or format. Inspect the actual reviewed document and its page
geometry before placing fields; see the esign-field-placement skill.
### Schedule A append workflow
Use the executed agreement's actual authority and notice requirements:
1. Review opportunity economics and negotiation strategy in an **internal**
negotiation/approval channel. Obtain commitment approval before sending
contractual content. A shared counterparty channel is not an internal channel.
2. Confirm that the proposed entry, role, terms, fee and effective date fall
within the agreement's express Schedule A notice authority. Changes to
standing terms, or variations outside that authority, require the applicable
amendment procedure; a notice cannot create its own exception.
3. Draft an approved, counterparty-specific dated notice for the recipient and
notice channel authorized by the executed agreement. Include useful business
content: the protected counterparty or lot, authorized role, commercial terms
and applicable fee. Exclude internal margins, negotiation strategy, other
parties' economics, system traces, raw errors and internal filing notices.
4. File the exact notice for operator approval before sending; see
operator-approval-loop. Preserve silence in the counterparty channel while
approval or participation authority is absent. Approval is distinct from
evidence that an authorized sender actually delivered the notice.
5. Record the authorized delivery evidence, effective date and any objection
under the executed agreement's actual requirements. Example periods are not
defaults. Keep the executed document immutable; update the tracked schedule
record and rebuild a **draft consolidated view** for internal review, with a
reference to the executed version and approved notice. This rebuild does not
replace the signed agreement or prove legal effect.
## Examples
### Notice text
Illustrative draft only: use these terms and dates solely when the executed
agreement authorizes them and the operator approves this exact recipient notice.
```text
Schedule A notice, 2026-09-02
Agreement: Master Agreement dated 2026-08-14 between Us and Acme
Entry 2: Lot B, 8 nodes, region EU-West
Role: introducer
Terms: 6 month term, start no later than 2026-10-01
Fee: standard
This entry takes effect today unless you object within ten business days
with dated written evidence of a prior relationship with the counterparty.
```
### Adding the entry to the spec
```json
"schedule": [
["1", "2026-08-20", "Lot A (16 nodes)", "introducer", "12 months", "standard"],
["2", "2026-09-02", "Lot B (8 nodes, EU-West)", "introducer", "6 months", "standard"]
]
```
Rebuild, diff the draft Markdown, and attach the consolidated draft to the
internal record alongside the unchanged executed document and notice evidence.
### Counterparty fills its own details at signing
For drafting, omit `legal`, `juris`, `addr`, `signer`, `title`, `email` from the
spec to render blank lines. A separately reviewed execution workflow must decide
which details may be completed by the counterparty and verify the actual fields;
the generator does not create or approve an e-sign envelope.