feat(skills): port operator desk patterns as generic skills

Add four curated skills that capture desk operating patterns without any
vendor, counterparty, or person names:

- operator-approval-loop: never-silent approval contract with hashed
  drafts, epoch-keyed decisions, an idempotent delivery ledger, a
  pre-draft baseline gate, and optional time-boxed auto-approval.
- counterparty-channel-discipline: channel classes, mention gating,
  silent observation, a per-channel strict prompt template, and a
  data-shaped communication autonomy policy.
- master-agreement-generator: template plus per-counterparty spec,
  role-selected clauses, rolling Schedule A appended by written notice,
  a pinned signature page, and a dependency-free Node build script.
- esign-field-placement: calibrated Location panel placement, recipient
  ownership, screenshot evidence, and a save-as-draft default.

Register the skills in a new operator-desk-patterns install module and
capability, add contract tests, and refresh catalog counts (286 to 290).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016UHxskuJiMLydwLrxDRqwy
This commit is contained in:
Affaan Mustafa
2026-09-02 07:19:32 -04:00
co-authored by Claude Fable 5.1
parent ca185ef5f7
commit 125826365e
25 changed files with 1309 additions and 16 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
{
"name": "ecc",
"source": "./",
"description": "Harness-native ECC operator layer - 68 agents, 286 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
"description": "Harness-native ECC operator layer - 68 agents, 290 skills, 94 legacy command shims, reusable hooks, rules, selective install profiles, and production-ready workflows for Claude Code, Codex, OpenCode, Cursor, and related agent harnesses",
"version": "2.2.1",
"author": {
"name": "Affaan Mustafa",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "ecc",
"version": "2.2.1",
"description": "Harness-native ECC plugin for engineering teams - 68 agents, 286 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
"description": "Harness-native ECC plugin for engineering teams - 68 agents, 290 skills, 94 legacy command shims, reusable hooks, rules, MCP conventions, and operator workflows for Claude Code plus adjacent agent harnesses",
"author": {
"name": "Affaan Mustafa",
"url": "https://x.com/affaanmustafa"
+2 -2
View File
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Instructions
This is a **production-ready AI coding plugin** providing 68 specialized agents, 286 skills, 94 commands, and automated hook workflows for software development.
This is a **production-ready AI coding plugin** providing 68 specialized agents, 290 skills, 94 commands, and automated hook workflows for software development.
**Version:** 2.2.1
@@ -154,7 +154,7 @@ Troubleshoot failures: check test isolation → verify mocks → fix implementat
```
agents/ — 68 specialized subagents
skills/ — 286 workflow skills and domain knowledge
skills/ — 290 workflow skills and domain knowledge
commands/ — 94 slash commands
hooks/ — Trigger-based automations
rules/ — Always-follow guidelines (common + per-language)
+2 -2
View File
@@ -162,12 +162,12 @@ Instead of rebuilding that process in every prompt, you install it once and make
ECC is MIT-licensed open source. It works best with Claude Code today, has a supported Codex sync path, and provides capability-limited adapters for Cursor, OpenCode, Gemini, Zed, GitHub Copilot, Antigravity, Qwen, and other harnesses. See the [support status matrix](#platform-support) before assuming feature parity.
Access to 68 agents, 286 skills, and 94 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work.
Access to 68 agents, 290 skills, and 94 legacy command shims, plus hooks, rules, memory, continuous learning, and AgentShield security scanning. The agents are specialized for planning, review, build repair, security, architecture, and domain work.
| Included | Count | What it gives you |
| ---------------- | ----------: | ------------------------------------------------------------------------------------ |
| Agents | 68 agents | Planning, review, build repair, security, architecture, and domain work |
| Skills | 286 skills | TDD, research, security, docs, frontend, data, ML, operations, and more |
| Skills | 290 skills | TDD, research, security, docs, frontend, data, ML, operations, and more |
| Commands | 94 commands | Convenient entry points while ECC moves to a skills-first surface |
| Hooks and memory | Runtime | Enforcement, session summaries, continuous learning, instincts, and context controls |
| Rules | Selective | Always-loaded standards you choose by language or project |
+1 -1
View File
@@ -196,7 +196,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
**完成!** 你现在可以使用 68 个代理、286 个技能和 94 个命令。
**完成!** 你现在可以使用 68 个代理、290 个技能和 94 个命令。
### multi-* 命令需要额外配置
+2 -2
View File
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — Agent Talimatları
Bu, yazılım geliştirme için 68 özel agent, 286 skill, 94 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
Bu, yazılım geliştirme için 68 özel agent, 290 skill, 94 command ve otomatik hook iş akışları sağlayan **üretime hazır bir AI kodlama eklentisidir**.
**Sürüm:** 2.2.1
@@ -142,7 +142,7 @@ Başarısızlık sorunlarını giderin: test izolasyonunu kontrol edin → mockl
```
agents/ — 68 özel subagent
skills/ — 286 iş akışı skillleri ve alan bilgisi
skills/ — 290 iş akışı skillleri ve alan bilgisi
commands/ — 94 slash command
hooks/ — Tetikleyici tabanlı otomasyonlar
rules/ — Her zaman uyulması gereken kurallar (ortak + dile özel)
+2 -2
View File
@@ -1,6 +1,6 @@
# Everything Claude Code (ECC) — 智能体指令
这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、286 项技能、94 条命令以及自动化钩子工作流,用于软件开发。
这是一个**生产就绪的 AI 编码插件**,提供 68 个专业代理、290 项技能、94 条命令以及自动化钩子工作流,用于软件开发。
**版本:** 2.2.1
@@ -147,7 +147,7 @@
```
agents/ — 68 个专业子代理
skills/ — 286 个工作流技能和领域知识
skills/ — 290 个工作流技能和领域知识
commands/ — 94 个斜杠命令
hooks/ — 基于触发的自动化
rules/ — 始终遵循的指导方针(通用 + 每种语言)
+3 -3
View File
@@ -260,7 +260,7 @@ Copy-Item -Recurse rules/typescript "$HOME/.claude/rules/"
/plugin list ecc@ecc
```
**搞定!** 你现在可以使用 68 个智能体、286 项技能和 94 个命令了。
**搞定!** 你现在可以使用 68 个智能体、290 项技能和 94 个命令了。
***
@@ -1174,7 +1174,7 @@ opencode
|---------|---------------|----------|--------|
| 智能体 | PASS: 68 个 | PASS: 12 个 | **Claude Code 领先** |
| 命令 | PASS: 94 个 | PASS: 35 个 | **Claude Code 领先** |
| 技能 | PASS: 286 项 | PASS: 37 项 | **Claude Code 领先** |
| 技能 | PASS: 290 项 | PASS: 37 项 | **Claude Code 领先** |
| 钩子 | PASS: 8 种事件类型 | PASS: 11 种事件 | **OpenCode 更多!** |
| 规则 | PASS: 29 条 | PASS: 13 条指令 | **Claude Code 领先** |
| MCP 服务器 | PASS: 14 个 | PASS: 完整 | **完全对等** |
@@ -1282,7 +1282,7 @@ ECC 是**第一个最大化利用每个主要 AI 编码工具的插件**。以
|---------|-----------------------|------------|-----------|----------|
| **智能体** | 68 | 共享 (AGENTS.md) | 共享 (AGENTS.md) | 12 |
| **命令** | 94 | 共享 | 基于指令 | 35 |
| **技能** | 286 | 共享 | 10 (原生格式) | 37 |
| **技能** | 290 | 共享 | 10 (原生格式) | 37 |
| **钩子事件** | 8 种类型 | 15 种类型 | SessionStart1 种类型) | 11 种类型 |
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 |
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
+9 -1
View File
@@ -194,10 +194,18 @@
"prediction-market-skills"
]
},
{
"id": "capability:operator-desk-patterns",
"family": "capability",
"description": "Operator desk patterns for agents that draft, gate, and paper external counterparty interactions.",
"modules": [
"operator-desk-patterns"
]
},
{
"id": "capability:ito-compute",
"family": "capability",
"description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.",
"description": "Authenticated It\u00f4 GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.",
"modules": [
"ito-compute"
]
+28 -1
View File
@@ -611,10 +611,37 @@
"cost": "medium",
"stability": "beta"
},
{
"id": "operator-desk-patterns",
"kind": "skills",
"description": "Generic operator desk patterns: never-silent approval loop, counterparty channel discipline, master agreement generation with a rolling schedule, and deterministic e-signature field placement.",
"paths": [
"skills/operator-approval-loop",
"skills/counterparty-channel-discipline",
"skills/master-agreement-generator",
"skills/esign-field-placement"
],
"targets": [
"claude",
"claude-project",
"cursor",
"antigravity",
"codex",
"opencode",
"codebuddy",
"joycode",
"qwen",
"zed"
],
"dependencies": [],
"defaultInstall": false,
"cost": "light",
"stability": "beta"
},
{
"id": "ito-compute",
"kind": "skills",
"description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.",
"description": "Authenticated It\u00f4 GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.",
"paths": [
"skills/ito-compute",
"skills/ito-inference",
+1
View File
@@ -88,6 +88,7 @@
"operator-workflows",
"optimization-workflows",
"prediction-market-skills",
"operator-desk-patterns",
"ito-compute",
"nasiko-control-plane",
"social-distribution",
+4
View File
@@ -181,6 +181,7 @@
"skills/cost-tracking/",
"skills/council/",
"skills/council-multi-model/",
"skills/counterparty-channel-discipline/",
"skills/cpp-coding-standards/",
"skills/cpp-testing/",
"skills/crosspost/",
@@ -210,6 +211,7 @@
"skills/energy-procurement/",
"skills/enterprise-agent-ops/",
"skills/error-handling/",
"skills/esign-field-placement/",
"skills/eval-harness/",
"skills/evm-token-decimals/",
"skills/exa-search/",
@@ -399,6 +401,7 @@
"skills/loop-design-check/",
"skills/mailtrap-email-integration/",
"skills/marketing-campaign/",
"skills/master-agreement-generator/",
"skills/ml-adoption-playbook/",
"skills/motion-advanced/",
"skills/motion-foundations/",
@@ -406,6 +409,7 @@
"skills/nextjs-turbopack/",
"skills/nuxt4-patterns/",
"skills/openclaw-persona-forge/",
"skills/operator-approval-loop/",
"skills/opensource-pipeline/",
"skills/orch-add-feature/",
"skills/orch-build-mvp/",
@@ -0,0 +1,141 @@
---
name: counterparty-channel-discipline
description: Per-channel strict prompts, mention gating, silent observation, and a communication autonomy policy for agents that sit in shared channels with external counterparties. Use when an agent joins group chats, shared channels, or DMs where outsiders can read every message and you need it to speak only when addressed, never leak internal context, and route risky content to draft-only approval.
---
# Counterparty Channel Discipline
An agent in a shared channel is speaking for the company. Every message is
counterparty-visible. This skill defines how to classify channels, when the
agent may speak, what it may never say, and which content goes to an operator
before it is sent.
## When to Use
- The agent is added to a group chat or shared channel that includes
customers, suppliers, investors, or partners.
- The agent handles DMs from unknown or partially known parties.
- You have seen an agent answer a message it was not asked, mention internal
tooling in front of a counterparty, or quote one counterparty's terms to
another.
- You need a written policy for what the agent sends on its own and what it
drafts for approval.
## How It Works
### Channel classes
| Class | Who can read | Agent behaviour |
| --- | --- | --- |
| Internal ops | Only the team | Full context, tool talk allowed, economics discussed here. Default reply target for anything internal. |
| Counterparty-visible | Team plus outsiders | Strict prompt. Speak only when addressed or when you own a direct answer. No internal references. |
| Quiet | Anyone | Observe only. Never post unless an operator instructs it for that channel. |
Assign every channel to a class in configuration. Unknown channels default to
quiet.
### Mention gating and silent observation
- `require_mention: true` on every counterparty-visible group. The agent
responds only when a message matches a mention pattern: its handle, a reply
to one of its messages, or its name in plain text.
- `observe_unmentioned_group_messages: true`. Unmentioned messages are read
into context so the agent knows the thread, but they never trigger a reply.
- `group_policy: allowlist` and `dm_policy: allowlist`. Groups and DMs not on
the allowlist are quiet channels.
- `allow_bots: mentions`. Other bots can address the agent by mention; their
unmentioned traffic is ignored.
- `never_silent_ack: true` in internal channels only. In counterparty
channels an interim acknowledgement is noise, so it is off there.
Configuration shape: [references/channel-policy.example.yaml](references/channel-policy.example.yaml).
### Strict prompt for counterparty-visible channels
Each counterparty-visible channel gets its own system prompt built from one
template with the channel name filled in. The rules, in priority order:
1. Speak only when addressed (mention, reply, or name) or when you own a
direct answer the group needs. Otherwise observe.
2. Never mention internal tools, sessions, production changes, config, or
capabilities.
3. Never say you cannot see or search something. Ask one concise clarifying
question instead.
4. No interim acknowledgements when you can answer directly. When you have
nothing material, stay silent.
5. Short plain professional sentences. No emojis. No em dashes.
6. Quote only verified facts, inventory, and prices from the record.
7. Deal economics and internal discussion happen in the internal ops channel,
never here.
8. Never reveal one counterparty's identity, terms, or pricing to another.
9. Anything that makes or changes a commitment is filed for operator approval
(see the operator-approval-loop skill), not sent.
Template: [references/strict-prompt.template.md](references/strict-prompt.template.md).
### Communication autonomy policy
Default is auto: the agent sends routine traffic without asking. The policy
lists exceptions about risk, not permission.
| Bucket | Meaning | Examples |
| --- | --- | --- |
| auto | Send without asking | Scheduling, logistics, holding replies, apologies for delay, asking a supplier for information we lack, relationship maintenance with no commercial ask, chasing something owed, internal coordination |
| draft_only by tier | Operator reads first because a wrong send is expensive | Tier-1 investors, tier-1 clients with executed contracts |
| draft_only by content class | Operator reads first regardless of recipient | Prices or rates, contractual language that could read as acceptance, legal or due-diligence threads, public posts, unverified claims, technical specs not measured by us |
| frozen | No outbound at all, inbound may be answered only if listed | A closing round, a standing cold-outreach hold, simulation or test channels |
| never | Hard stop, not an approval gate | Signing contracts or e-signature envelopes, moving money, entering passwords or one-time codes, publishing packages, revealing one party's pricing to another |
Write the policy as data, not prose in a prompt, so monitors and the approval
tool can enforce it mechanically.
### Cross-channel leakage checks
Before any send in a counterparty channel, check the draft against the other
counterparties' names, terms, and prices held in the ledger. A match blocks
the send and files it for approval with the match named.
## Examples
### A group message not addressed to the agent
```text
[#acme-shared] buyer: can someone confirm the rack count for phase 2?
```
The agent reads it (observation), does not reply, and if the answer is owed
by the team it files an inbound obligation in the internal ops channel:
`Open obligation: #acme-shared asks for phase 2 rack count. Owner needed.`
### Addressed, answer known and verified
```text
[#acme-shared] buyer: @desk what start dates do you have open in October?
agent: 6, 13 and 20 October are open. Which works for you?
```
No acknowledgement first, no "let me check", no tool mention.
### Addressed, answer requires a commitment
The buyer asks for a rate. Rates are a draft-only content class. The agent
says nothing in the shared channel beyond a single clarifying question if one
is needed, files a draft with the proposed rate for operator approval, and
posts the filing notice in the internal ops channel.
### Cannot see the referenced document
```text
buyer: see the attached spec sheet, does it match?
agent: Which revision of the spec sheet should I compare against, the one dated 12 August?
```
Not: "I can't open attachments."
## Invariants to test
- An unmentioned group message never produces an outbound message.
- A draft containing another counterparty's name or terms is blocked.
- Content matching a draft-only class is filed, not sent, even in auto mode.
- The strict prompt for each channel contains the channel name and all nine
rules; a prompt generated for one channel is never reused for another.
@@ -0,0 +1,67 @@
# Channel discipline policy. Placeholder names; adapt to your channels.
internal_channels:
- "#ops"
- "#desk-internal"
quiet_channels:
- "#announcements"
- "#random"
platforms:
chat:
group_policy: allowlist
dm_policy: allowlist
require_mention: true
observe_unmentioned_group_messages: true
allow_bots: mentions
mention_patterns:
- "@desk"
- "\\bdesk\\b"
- "\\bops bot\\b"
never_silent_ack:
internal: true
counterparty_visible: false
groups:
"#acme-shared":
class: counterparty_visible
system_prompt_template: strict-prompt.template.md
counterparty: acme-supplier
"#globex-shared":
class: counterparty_visible
system_prompt_template: strict-prompt.template.md
counterparty: globex-buyer
"#ops":
class: internal
autonomy:
default: auto
draft_only:
tiers:
investors_tier1: ["investor-a", "investor-b"]
clients_tier1: ["client-a"]
content_classes:
- prices_or_rates
- contractual
- legal_or_dd
- public_posts
- unverified_claims
- technical_specs
auto_examples:
- scheduling and logistics
- acknowledgements and holding replies
- asking a supplier for information we do not have
- chasing something we are owed
- internal team coordination
frozen:
- name: closing-round-network
reason: inbound may be answered, nothing outbound
- name: simulation-channels
channels: ["#sim-alpha", "#sim-beta"]
reason: test agents; never let their numbers enter a real record
never:
- signing contracts or e-signature envelopes
- moving money, wires, card entry
- entering passwords, one-time codes, or verification codes
- publishing packages
- revealing one counterparty's pricing or terms to another
@@ -0,0 +1,21 @@
# Strict prompt template (counterparty-visible channel)
Fill `{{CHANNEL_NAME}}` per channel. Generate one prompt per channel; never share a prompt across channels.
```text
You are the desk agent in a shared channel with counterparties present
({{CHANNEL_NAME}}). Every message is counterparty-visible.
Speak only when addressed (by mention, reply, or by name) or when you own a
direct answer the group needs; otherwise observe.
STRICT RULES (highest priority):
- Never mention internal tools, sessions, production changes, config, or capabilities.
- Never say you cannot see or search something. Ask one concise clarifying question instead.
- No interim acknowledgements when you can answer directly. When you have nothing material, stay silent.
- Short plain professional sentences. No emojis. No em dashes.
- Quote only verified inventory, facts, and prices from the record.
- Deal economics and internal discussion happen in the internal ops channel, never here.
- Never reveal one counterparty's identity, terms, or pricing to another.
- Anything that makes or changes a commitment (price, term, acceptance, legal language) is filed for operator approval, not sent.
```
+131
View File
@@ -0,0 +1,131 @@
---
name: esign-field-placement
description: Deterministic method for placing signature, date, and text fields in a web e-signature composer through a browser automation session, using a fixed signature page, numeric Location panel coordinates instead of drag, and a save-as-draft default. Use when automating envelope preparation for generated agreements and you need repeatable field positions, correct per-recipient ownership, and a hard gate before anything is sent or signed.
---
# E-Signature Field Placement
Dragging fields onto a PDF in a browser is not repeatable. Reading and
writing the composer's numeric Location panel is. This skill describes the
method for placing fields deterministically, assigning them to the right
recipient, and stopping before send unless an operator says otherwise.
## When to Use
- You generate agreements from a template (see master-agreement-generator)
and prepare envelopes for them in a web e-signature composer.
- Field positions drift between runs, or fields land on the wrong recipient.
- You need screenshots and a draft envelope for operator review before send.
- The automation runs through an attached browser session (remote debugging
port) rather than a vendor API.
## How It Works
### Preconditions
- The document's signature page is on its own page with a fixed layout: our
block first (By, Name, Title, Email, Date), then the counterparty block.
The generator guarantees this with a page break before the block.
- The browser session is already signed in by a human. The automation never
enters credentials, one-time codes, or verification codes. If the composer
redirects to a login page, print `LOGGED OUT` and exit non-zero.
### Recipients
1. Enable signing order.
2. Recipient 1: our signer (name, email).
3. Recipient 2: the counterparty signer from the spec.
4. Optional cc: added as "receives a copy", never as a signer.
5. Subject and message come from arguments; subject is trimmed to the
composer's limit.
### Calibration
Coordinates in the Location panel are in document units; screen pixels are
scaled. Calibrate once per envelope:
1. Scroll to the signature page (last page).
2. Drag one signature field from the palette to an arbitrary spot on the
page, then select it.
3. Open the Location panel and read its x,y. Together with the known screen
position where the field was dropped, this gives the page origin and the
scale factor.
4. From then on compute every target y as
`(screen_top_of_line - page_origin) / scale` and set positions through
the panel's numeric inputs.
### Placing fields
For each field, in this order:
1. Select the recipient who owns the field first. Fields placed while a
recipient is selected belong to that recipient. Place all of our fields,
then switch to the counterparty and place theirs.
2. Drag the field type from the palette to a neutral drop spot (not its final
position).
3. If it is a text field over a blank entity line (name, title, email to be
completed at signing), set the font size small (8 point) through the
Formatting panel so it fits the line.
4. Set x and y through the Location panel inputs: click, select all, type
the integer, tab out. Never nudge by drag.
5. Click on empty canvas to deselect before the next field.
Our block gets a signature and a date. The counterparty block gets a
signature, a date, and optional text fields for name, title, and email when
the spec left them blank. Page-1 entity blanks (legal name, jurisdiction,
address) take additional small text fields at coordinates supplied as
arguments.
### Evidence
Before any send decision, deselect all fields and capture a screenshot of the
signature page (and page 1 if fields were placed there). Store it with the
envelope subject in the file name. The operator reviews this image.
### Hard gate
- Default action is save as draft (Actions, then Save and Close). Print
`DRAFT SAVED: <subject>`.
- Sending requires an explicit operator instruction for this envelope.
Print `SENT: <subject>` only after the composer confirms.
- A `--stop` mode ends the run after placement with nothing saved, for dry
runs.
- The automation never signs, never declines, never voids, and never opens
a counterparty's signing link.
- Every argument is plain text; no credentials or tokens are passed.
Checklist: [references/placement-checklist.md](references/placement-checklist.md).
## Examples
### Dry run for a new counterparty
```text
prepare-envelope --docx "out/Acme MASTER.docx" --cp-name "A. Person" \
--cp-email signer@example.com --subject "Master Agreement: Acme" \
--message "Please review and sign." --blank-title --stop
-> screenshot env_sig.png written, STOPPED before send: Master Agreement: Acme
```
### Draft for operator review
Same arguments with `--draft` instead of `--stop`. The operator opens the
draft in the composer, checks the screenshot, and either sends it by hand or
instructs the automation to send.
### Session expired
```text
LOGGED OUT
exit status 2
```
The operator re-authenticates in the browser; the automation is re-run.
## Invariants to test
- Two runs on the same document produce identical Location panel values.
- Every counterparty field is owned by recipient 2, every one of ours by
recipient 1.
- With no `--draft` or explicit send instruction, the envelope is not sent.
- A logged-out session exits non-zero before touching the composer.
@@ -0,0 +1,32 @@
# Placement checklist
Before placing
- [ ] Signature page is the last page and starts on its own page.
- [ ] Browser session is signed in by a human; no login page visible.
- [ ] Spec says which counterparty blanks (name, title, email) need text fields.
Recipients
- [ ] Signing order enabled.
- [ ] Recipient 1 is our signer, recipient 2 is the counterparty, cc is "receives a copy".
- [ ] Subject within the composer limit; message is plain text.
Calibration
- [ ] One signature field dropped, selected, Location panel read.
- [ ] Page origin and scale computed from that reading.
Fields (per recipient, our block first)
- [ ] Recipient selected before placing their fields.
- [ ] Field dragged to a neutral spot, then positioned by Location panel inputs.
- [ ] Text fields over blank lines set to 8 point.
- [ ] Canvas clicked to deselect between fields.
Evidence and gate
- [ ] Signature page screenshot captured with all fields deselected.
- [ ] Page-1 screenshot captured if fields were placed there.
- [ ] Action is save as draft unless the operator explicitly instructed send.
- [ ] No sign, decline, void, or signing-link open performed by automation.
+161
View File
@@ -0,0 +1,161 @@
---
name: master-agreement-generator
description: Generate counterparty master agreements from one template plus a per-counterparty JSON spec, with role-selected clauses, a rolling Schedule A appended by written notice instead of re-signing, and a signature page pinned to its own page for stable e-sign geometry. Use when you need to issue the same framework agreement to many counterparties, add deals to an executed agreement without a new signature, or keep contract documents reproducible from source.
---
# Master Agreement Generator
One master template, one small spec per counterparty, one build step. The
executed agreement covers every future opportunity; each opportunity is added
to a rolling schedule by a dated written notice. Nobody re-signs.
## 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. Missing signature fields
render as blank lines to be completed at signing. See
[references/spec.example.json](references/spec.example.json).
### 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
```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, writes
`out/<file> MASTER.md`, and converts to `.docx` with pandoc when pandoc is on
`PATH`. Without pandoc it writes the markdown and reports that docx was
skipped, exit code 0. Keep the generated files out of version control; the
template and specs are the source.
### Signature page geometry
The template ends the body with an OpenXML page break so the signature block
always starts a fresh page:
````markdown
```{=openxml}
<w:p><w:r><w:br w:type="page"/></w:r></w:p>
```
````
The signature page layout (our block, then the counterparty block, each with
By, Name, Title, Email, Date) never changes between counterparties. That is
what lets e-signature field placement use fixed coordinates; see the
esign-field-placement skill.
### Schedule A append workflow
The schedule is rolling. Adding an entry is a notice, not an amendment:
1. Agree the opportunity terms in the shared channel or by email.
2. Send a dated written Schedule A notice in that same channel or by email.
It names the protected counterparty or lot, our role for that entry
(introducer, or principal), the commercial terms, and the fee (standard
unless a different percentage, fixed amount, or per-unit rate is stated).
3. The entry takes effect on the notice date unless the counterparty objects
within the objection window (for example ten business days) with dated
written evidence of a substantive pre-existing relationship.
4. Record the entry in the tracked spec's `schedule` array and rebuild the
document so the source of truth matches what was noticed.
5. Each entry carries its own protection period (for example twelve months
from its notice date).
Schedule A notices are draft-only content (contractual). File them for
operator approval before sending; see operator-approval-loop.
## Examples
### Notice text
```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 markdown, attach the rebuilt document to the record.
### Counterparty fills its own details at signing
Omit `legal`, `juris`, `addr`, `signer`, `title`, `email` from the spec. The
build renders blank lines and the e-sign envelope places small text fields
over them for the counterparty to complete.
@@ -0,0 +1,85 @@
# MASTER AGREEMENT: MUTUAL NON-DISCLOSURE, {{FEE_TITLE}} AND NON-CIRCUMVENTION
**Execution copy. Our fields are complete; {{CP_SHORT}} fields are completed at signing where blank. One signature covers every introduction and every transaction between the parties; each opportunity is logged on Schedule A by written notice. No re-signing.**
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 written notice in the parties' shared channel or by email identifying the Protected Counterparty and, where applicable, the terms and any fee that differs from the standard fee. An 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.] Where Schedule A states a different fee for an entry, that fee applies to that entry instead.
**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; electronic signatures and counterparts.]
## Schedule A (rolling)
Entries are added by Schedule A notice as defined in Section 1. 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: _________________________________
&nbsp;
**{{CP_SIGBLOCK}}**
By: _________________________________
Name: {{CP_SIGNER}}
Title: {{CP_TITLE}}
Email: {{CP_EMAIL}}
Date: _________________________________
@@ -0,0 +1,16 @@
{
"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"
}
@@ -0,0 +1,151 @@
#!/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>
*
* Writes "<out_dir>/<spec.file> MASTER.md" and, when pandoc is on PATH,
* the matching .docx. No dependencies. Node >= 18.
*/
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
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 renderScheduleRows(rows) {
if (!Array.isArray(rows) || rows.length === 0) {
return EMPTY_SCHEDULE_ROW;
}
return rows.map(row => `| ${row.map(cell => String(cell)).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`);
}
}
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 output;
}
function pandocAvailable() {
const probe = spawnSync('pandoc', ['--version'], { encoding: 'utf8' });
return !probe.error && probe.status === 0;
}
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);
fs.mkdirSync(outDir, { recursive: true });
const base = path.join(outDir, `${spec.file} MASTER`);
const mdPath = `${base}.md`;
fs.writeFileSync(mdPath, markdown, 'utf8');
const result = { markdown: mdPath, docx: null, docxSkipped: false };
const canConvert = options.pandoc === undefined ? pandocAvailable() : options.pandoc;
if (!canConvert) {
result.docxSkipped = true;
return result;
}
const docxPath = `${base}.docx`;
const converted = spawnSync('pandoc', [mdPath, '-o', docxPath], { encoding: 'utf8' });
if (converted.error || converted.status !== 0) {
throw new Error(`pandoc failed: ${converted.stderr || converted.error?.message || 'unknown error'}`);
}
result.docx = docxPath;
return result;
}
function main(argv) {
const [templatePath, specPath, outDir] = argv;
if (!templatePath || !specPath || !outDir) {
console.error('usage: build-agreement.js <template.md> <spec.json> <out_dir>');
return 2;
}
try {
const result = build(templatePath, specPath, outDir);
console.log(`wrote ${result.markdown}`);
if (result.docxSkipped) {
console.log('docx skipped: pandoc not found on PATH');
} else {
console.log(`wrote ${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 };
+157
View File
@@ -0,0 +1,157 @@
---
name: operator-approval-loop
description: Never-silent operator approval contract for agent-drafted outbound messages, with hashed drafts, epoch-keyed decisions, an idempotent delivery ledger, and a pre-draft baseline gate. Use when an agent drafts messages to external counterparties and a human operator must approve, reject, or steer each send before it leaves.
---
# Operator Approval Loop
An agent that talks to external counterparties should never send on its own
judgment and should never go quiet. This skill defines the contract that
makes both true: every outbound draft is filed as an obligation, an operator
decides on the exact text, and a delivery ledger proves what went out.
## When to Use
- An agent drafts replies to customers, suppliers, investors, or partners in
a shared channel, email, or chat, and a human must approve before send.
- You need an audit trail that links each sent message to the exact draft
text, the operator who approved it, and the decision time.
- You have seen a stale approval release a rewritten draft, or two workers
deliver the same approved message twice.
- Drafts keep re-asking counterparties for facts the ledger already holds.
## How It Works
### Objects
| Object | Meaning |
| --- | --- |
| Obligation | One thing we owe a counterparty. Status moves `drafted`, then `approved` or `rejected`, then `sent`. Carries `direction`, `counterparty`, `channel`, and an `updated_at` epoch. |
| Draft | Sidecar row holding the exact draft text, a sha256 of that text, origin coordinates (platform, channel, thread, user), and priority (P0 to P3). One per obligation, replaced on re-file. |
| Decision | An operator's approve or reject, recorded with the operator id, a nonce, and the draft epoch it was made against. |
| Delivery | Ledger row proving one send or notice for one (obligation, decision) pair. |
The reference schema is in [references/approval-ledger.sql](references/approval-ledger.sql).
### Filing a draft
1. Clean inputs. Strip control characters, collapse whitespace in single-line
fields, and enforce length caps (draft, summary, context, counterparty).
Empty or oversized fields are refused, not truncated silently.
2. Run the baseline gate (below). It may refuse the filing.
3. Hash the draft text with sha256. The hash prefix goes into the summary so
the approval panel shows which text it is approving.
4. Upsert. If an open drafted obligation already exists for the same
(counterparty, channel), replace the draft sidecar and advance the
obligation's `updated_at`. That advance is the epoch rotation: any
decision keyed to the old epoch can no longer release the new text.
Otherwise insert a new obligation with status `drafted`.
5. Acknowledge in the origin channel that a draft is filed and awaiting
approval. Never-silent means the requester always sees that something
happened, even when the answer is "waiting on the operator".
### Baseline gate
Before any draft is filed, query the current baseline for the counterparty
(a temporal ledger, contract store, or CRM):
- Signed or delivered contract on record: refuse the filing with the evidence
and a recommendation. Asking a counterparty about specs after signing is the
exact failure this gate exists to stop.
- Operator override: `force_despite_signed_contract` lets the filing through
and stamps `[BASELINE_OVERRIDE_SIGNED_CONTRACT]` into the draft context.
- Gate service unreachable: the filing proceeds and the context is stamped
`[BASELINE_CHECK_UNAVAILABLE]`. The panel sees that the guard was off.
Failures never silently disable the gate.
- When facts are available, attach the freshest few to the context as a
`[BASELINE FACTS: ...]` digest so the draft lands with current truth.
### Deciding
The approval panel lists obligations with status `drafted` and direction
`we_owe_them`. Approve or reject writes a decision row carrying the draft
epoch (`draft_updated_ts`) and flips the obligation status in the same
transaction. A decision whose epoch does not match the current `updated_at`
is stale and must not release anything.
### Delivering
A delivery monitor scans for decided obligations with no delivery row:
1. Send the exact `draft_text` from the sidecar. Never re-derive text from
the summary or regenerate it.
2. Insert the delivery row keyed by (obligation_id, decision_id). The UNIQUE
constraint makes this idempotent: if the insert conflicts, someone else
delivered, so stop without sending.
3. On success flip the obligation to `sent`.
4. In internal channels append a receipt footer:
`approved by <operator> · receipt <decision_id> · draft sha256 <prefix>`.
Rejected decisions produce a reject notice through the same ledger. Legacy
obligations that have no draft sidecar are reported for a manual notice, not
delivered.
### Time-boxed auto-approval (optional)
A draft may carry `auto_send_after` (epoch seconds). A sweep approves drafts
whose deadline passed with no decision, recording operator `auto-ttl`, then
delivery proceeds through the normal path. Operator actions always win: a
decision flips status before the sweep sees it, and a re-file rotates the
epoch and moves or clears the deadline. The sweep re-checks status and epoch
inside the write transaction so a race resolves as a no-op. Drafts without a
deadline stay hard-gated forever.
### Signal linkage
A draft can name the inbound obligation it answers (`signal_obligation_id`).
This is the only truthful link for latency measurement (inbound signal to
drafted response) and lets the SLA scan treat that inbound item as answered.
Reject the filing if the referenced row does not exist.
## Examples
### File a draft
```text
file_request(
draft="Thanks, we can hold the slot until Friday. Which start date works?",
counterparty="acme-supplier",
context="reply to delivery window question",
origin_platform="slack", origin_channel="#acme-shared",
origin_thread="1712345678.000100", priority="P1",
signal_obligation_id=412)
-> {obligation_id: 431, draft_sha256: "9f2c...", refiled: false}
```
Origin channel sees: `Draft filed for approval (P1, sha 9f2c8a1b). Waiting on operator.`
### Re-file after a steer
The operator asks for a shorter draft. Filing again for the same
(counterparty, channel) returns `refiled: true`, the sidecar text and hash
change, and `updated_at` advances. An approve clicked on the old panel row
carries the old epoch and is ignored.
### Gate refusal
```text
DeskApprovalError: baseline gate refused this draft: the ledger shows a
signed contract for 'acme-supplier'. Evidence: master agreement executed
2026-08-14. Recommendation: do not ask. Re-file with
force_despite_signed_contract=true if this is genuinely a new thread.
```
### Delivery footer in an internal channel
```text
Confirmed for Friday, start date 2026-09-08.
approved by operator-a · receipt 118 · draft sha256 9f2c8a1b2d3e4f50
```
## Invariants to test
- Same (counterparty, channel) filed twice yields one obligation, two epochs.
- A decision with a stale epoch never results in a delivery row.
- Two concurrent deliverers produce exactly one delivery row and one send.
- Gate unavailable stamps the marker; gate signed refuses without force.
- Auto-ttl never fires against text the operator has since re-filed.
@@ -0,0 +1,71 @@
-- Reference schema for the operator approval loop.
-- SQLite dialect; adapt types for other engines.
CREATE TABLE IF NOT EXISTS obligations (
id INTEGER PRIMARY KEY,
counterparty TEXT NOT NULL,
source TEXT NOT NULL, -- origin platform
channel TEXT NOT NULL,
direction TEXT NOT NULL, -- 'we_owe_them' | 'they_owe_us' | 'none'
status TEXT NOT NULL, -- 'open' | 'drafted' | 'approved' | 'rejected' | 'sent' | 'closed'
summary TEXT NOT NULL,
opened_ts INTEGER NOT NULL,
last_touch_ts INTEGER NOT NULL,
updated_at INTEGER NOT NULL -- decision epoch; advances on every re-file
);
-- Exact draft text plus origin coordinates. One per obligation; replaced on re-file.
CREATE TABLE IF NOT EXISTS obligation_drafts (
obligation_id INTEGER PRIMARY KEY REFERENCES obligations(id),
draft_text TEXT NOT NULL,
context TEXT,
origin_platform TEXT NOT NULL,
origin_channel TEXT NOT NULL,
origin_thread TEXT,
origin_user TEXT,
priority TEXT NOT NULL DEFAULT 'P2', -- P0..P3
draft_sha256 TEXT NOT NULL,
created_ts INTEGER NOT NULL,
updated_ts INTEGER NOT NULL,
auto_send_after INTEGER, -- NULL = hard gate
signal_obligation_id INTEGER REFERENCES obligations(id)
);
-- Operator (or auto-ttl) decisions, keyed to the draft epoch they were made against.
CREATE TABLE IF NOT EXISTS obligation_decisions (
id INTEGER PRIMARY KEY,
obligation_id INTEGER NOT NULL REFERENCES obligations(id),
decision TEXT NOT NULL CHECK (decision IN ('approve', 'reject')),
operator TEXT NOT NULL,
decided_ts INTEGER NOT NULL,
nonce TEXT NOT NULL UNIQUE,
draft_updated_ts INTEGER NOT NULL -- must equal obligations.updated_at to be valid
);
-- Delivery ledger. UNIQUE(obligation_id, decision_id) makes delivery idempotent.
CREATE TABLE IF NOT EXISTS obligation_deliveries (
id INTEGER PRIMARY KEY,
obligation_id INTEGER NOT NULL REFERENCES obligations(id),
decision_id INTEGER NOT NULL REFERENCES obligation_decisions(id),
kind TEXT NOT NULL CHECK (kind IN ('draft_sent', 'reject_notice', 'manual_notice')),
coordinate TEXT NOT NULL, -- where it landed: message id, email id, thread ts
delivered_ts INTEGER NOT NULL,
UNIQUE(obligation_id, decision_id)
);
-- State table
-- drafted -> approved (operator approve, or auto-ttl sweep when auto_send_after passed)
-- drafted -> rejected (operator reject)
-- drafted -> drafted (re-file: new draft text, new sha, updated_at advances)
-- approved -> sent (delivery monitor sends exact draft_text, ledger row inserted)
-- rejected -> rejected (reject notice ledgered, no send)
-- Pending deliveries: decided, has a draft sidecar, no ledger row yet.
-- SELECT o.id, dec.id, d.draft_text
-- FROM obligations o
-- JOIN obligation_decisions dec ON dec.obligation_id = o.id
-- JOIN obligation_drafts d ON d.obligation_id = o.id
-- WHERE o.status IN ('approved', 'rejected')
-- AND NOT EXISTS (SELECT 1 FROM obligation_deliveries del
-- WHERE del.obligation_id = o.id AND del.decision_id = dec.id)
-- ORDER BY dec.decided_ts ASC;
+91
View File
@@ -0,0 +1,91 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const os = require('os');
const path = require('path');
const repoRoot = path.resolve(__dirname, '..', '..');
const scriptPath = path.join(repoRoot, 'skills/master-agreement-generator/scripts/build-agreement.js');
const templatePath = path.join(repoRoot, 'skills/master-agreement-generator/references/master-template.example.md');
const specPath = path.join(repoRoot, 'skills/master-agreement-generator/references/spec.example.json');
const builder = require(scriptPath);
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed += 1;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
const template = fs.readFileSync(templatePath, 'utf8');
const exampleSpec = JSON.parse(fs.readFileSync(specPath, 'utf8'));
console.log('\n=== build-agreement ===\n');
test('renders every placeholder from the example spec', () => {
const output = builder.render(template, exampleSpec);
assert.ok(!/\{\{[A-Z_]+\}\}/.test(output), 'placeholders remain');
assert.match(output, /Acme Compute Ltd/);
assert.match(output, /\*\*ACME COMPUTE LTD\*\*/);
assert.match(output, /SOURCING FEE/);
assert.match(output, /\| 1 \| 2026-08-20 \| Lot A \(16 nodes\) \| introducer \| 12 months \| standard \|/);
assert.match(output, /the Data Processing Addendum dated 2026-09-01; amendable/);
});
test('renders the empty schedule placeholder row and blank lines when fields are omitted', () => {
const output = builder.render(template, { file: 'X', short: 'Xco', role: 'buyer', date: 'January 1, 2030' });
assert.ok(output.includes(builder.EMPTY_SCHEDULE_ROW));
assert.match(output, new RegExp(`Name: ${builder.BLANK}`));
assert.match(output, /\*\*XCO\*\*/);
assert.match(output, /January 1, 2030/);
assert.ok(!output.includes('; amendable') || output.includes('matter; amendable'), 'supplement separator must be empty');
});
test('selects the role clause by spec.role', () => {
for (const role of ['buyer', 'supplier', 'mutual']) {
const values = builder.buildValues({ file: 'X', short: 'Xco', role });
assert.strictEqual(values.FEE_TITLE, builder.ROLE_CLAUSES[role].title);
assert.ok(!values.ROLE_CLAUSE.includes('{cp}'), 'counterparty short name not substituted');
}
assert.match(builder.buildValues({ file: 'X', short: 'Xco', role: 'mutual' }).ROLE_CLAUSE, /Each Party may introduce/);
});
test('rejects unknown roles and missing required fields', () => {
assert.throws(() => builder.buildValues({ file: 'X', short: 'Xco', role: 'partner' }), /unknown role "partner"/);
assert.throws(() => builder.buildValues({ short: 'Xco', role: 'buyer' }), /spec\.file is required/);
});
test('build writes markdown and reports docx skipped when pandoc is unavailable', () => {
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-build-agreement-'));
try {
const result = builder.build(templatePath, specPath, outDir, { pandoc: false, now: new Date('2030-01-01T00:00:00Z') });
assert.ok(fs.existsSync(result.markdown));
assert.strictEqual(path.basename(result.markdown), 'AcmeSupplier MASTER.md');
assert.strictEqual(result.docxSkipped, true);
assert.strictEqual(result.docx, null);
} finally {
fs.rmSync(outDir, { recursive: true, force: true });
}
});
test('main returns usage exit code without arguments', () => {
const originalError = console.error;
console.error = () => {};
try {
assert.strictEqual(builder.main([]), 2);
} finally {
console.error = originalError;
}
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);
+129
View File
@@ -0,0 +1,129 @@
'use strict';
/**
* Contract tests for the generic desk-pattern skills: operator approval loop,
* counterparty channel discipline, master agreement generator, and e-sign
* field placement. They must stay vendor-neutral and free of local paths.
*/
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const repoRoot = path.resolve(__dirname, '..', '..');
const SKILLS = [
'operator-approval-loop',
'counterparty-channel-discipline',
'master-agreement-generator',
'esign-field-placement',
];
const REQUIRED_SECTIONS = ['## When to Use', '## How It Works', '## Examples'];
const FORBIDDEN_WORDS = [
'ito', 'itô', 'hermes', 'docusign', 'pluto', 'stellon', 'mayfield',
'affaan', 'alejandro', 'graphiti', 'itomarkets',
];
const EM_DASH = '—';
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(`${name}`);
passed += 1;
} catch (error) {
console.log(`${name}`);
console.log(` Error: ${error.message}`);
failed += 1;
}
}
function walk(dir, acc = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walk(full, acc);
} else {
acc.push(full);
}
}
return acc;
}
console.log('\n=== Desk pattern skills ===\n');
for (const skill of SKILLS) {
const skillDir = path.join(repoRoot, 'skills', skill);
const skillPath = path.join(skillDir, 'SKILL.md');
test(`${skill}: SKILL.md has name and description frontmatter`, () => {
assert.ok(fs.existsSync(skillPath), `${skill}/SKILL.md is missing`);
const source = fs.readFileSync(skillPath, 'utf8');
const frontmatter = source.match(/^---\n([\s\S]*?)\n---/);
assert.ok(frontmatter, 'frontmatter missing');
const keys = frontmatter[1].split('\n').map(line => line.split(':')[0]);
assert.deepStrictEqual(keys, ['name', 'description']);
assert.match(frontmatter[1], new RegExp(`^name: ${skill}$`, 'm'));
assert.match(frontmatter[1], /^description: .*Use when/m);
});
test(`${skill}: SKILL.md has the required sections`, () => {
const source = fs.readFileSync(skillPath, 'utf8');
for (const section of REQUIRED_SECTIONS) {
assert.ok(source.includes(section), `missing ${section}`);
}
});
test(`${skill}: files contain no em dashes, vendor names, or local paths`, () => {
for (const file of walk(skillDir)) {
const relative = path.relative(repoRoot, file);
const source = fs.readFileSync(file, 'utf8');
assert.ok(!source.includes(EM_DASH), `${relative} contains an em dash`);
assert.ok(!/\/Users\//.test(source), `${relative} contains a /Users/ path`);
for (const word of FORBIDDEN_WORDS) {
const pattern = new RegExp(`(^|[^a-z])${word}([^a-z]|$)`, 'i');
assert.ok(!pattern.test(source), `${relative} mentions "${word}"`);
}
}
});
}
test('operator-approval-loop ships the ledger schema with the idempotency key', () => {
const sql = fs.readFileSync(path.join(repoRoot, 'skills/operator-approval-loop/references/approval-ledger.sql'), 'utf8');
assert.match(sql, /UNIQUE\(obligation_id, decision_id\)/);
assert.match(sql, /draft_sha256/);
assert.match(sql, /auto_send_after/);
const skill = fs.readFileSync(path.join(repoRoot, 'skills/operator-approval-loop/SKILL.md'), 'utf8');
assert.match(skill, /BASELINE_CHECK_UNAVAILABLE/);
assert.match(skill, /exact `draft_text`/);
});
test('counterparty-channel-discipline ships a policy example and a strict prompt template', () => {
const policy = fs.readFileSync(path.join(repoRoot, 'skills/counterparty-channel-discipline/references/channel-policy.example.yaml'), 'utf8');
assert.match(policy, /require_mention: true/);
assert.match(policy, /observe_unmentioned_group_messages: true/);
assert.match(policy, /default: auto/);
const template = fs.readFileSync(path.join(repoRoot, 'skills/counterparty-channel-discipline/references/strict-prompt.template.md'), 'utf8');
assert.match(template, /\{\{CHANNEL_NAME\}\}/);
assert.match(template, /Never reveal one counterparty/);
});
test('master-agreement-generator template pins the signature page with a page break', () => {
const template = fs.readFileSync(path.join(repoRoot, 'skills/master-agreement-generator/references/master-template.example.md'), 'utf8');
assert.match(template, /w:br w:type="page"/);
assert.match(template, /\{\{SCHEDULE_ROWS\}\}/);
const spec = JSON.parse(fs.readFileSync(path.join(repoRoot, 'skills/master-agreement-generator/references/spec.example.json'), 'utf8'));
assert.strictEqual(spec.role, 'supplier');
});
test('esign-field-placement defaults to draft and forbids credential entry', () => {
const skill = fs.readFileSync(path.join(repoRoot, 'skills/esign-field-placement/SKILL.md'), 'utf8');
assert.match(skill, /save as draft/i);
assert.match(skill, /never\s+enters credentials/i);
assert.match(skill, /Never nudge by drag/);
assert.match(skill, /LOGGED OUT/);
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
process.exit(failed > 0 ? 1 : 0);