mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-08 07:37:48 +02:00
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:
co-authored by
Claude Fable 5.1
parent
ca185ef5f7
commit
125826365e
@@ -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,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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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/ — 始终遵循的指导方针(通用 + 每种语言)
|
||||
|
||||
@@ -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 种类型 | SessionStart(1 种类型) | 11 种类型 |
|
||||
| **钩子脚本** | 20+ 个脚本 | 16 个脚本 (DRY 适配器) | 1 个 SessionStart 引导脚本 | 插件钩子 |
|
||||
| **规则** | 34 (通用 + 语言) | 34 (YAML 前页) | 基于指令 | 13 条指令 |
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -88,6 +88,7 @@
|
||||
"operator-workflows",
|
||||
"optimization-workflows",
|
||||
"prediction-market-skills",
|
||||
"operator-desk-patterns",
|
||||
"ito-compute",
|
||||
"nasiko-control-plane",
|
||||
"social-distribution",
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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: _________________________________
|
||||
|
||||
|
||||
|
||||
**{{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 };
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user