prompt-contract 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,8 +7,7 @@
7
7
 
8
8
  English · [简体中文](README.zh-CN.md)
9
9
 
10
- ![PromptContract playground running the self-contained demo: a vague one-line request enhanced into a structured task spec, with six rule assertions passing](docs/assets/hero-playground.png)
11
- <sub>The playground running its self-contained demo (`?demo=1`) against the local mock upstream — actual pipeline output, six rule assertions passing.</sub>
10
+ ![PromptContract a vague one-line request becomes a structured task spec (goal, scope, acceptance criteria, non-goals), verified by six hard rules via `prompt-prompt-contract check`. Real-LLM latency P50 2.3s; engine adds 0.005ms. CLI · MCP · Playground — zero dependencies, BYOK, offline-capable.](docs/assets/hero-cover.png)
12
11
 
13
12
  **PromptContract is a deterministic prompt-contract layer for AI coding agents.** It compiles a vague one-line request into a structured task specification — goal, scope, constraints, acceptance criteria — and verifies the result against six hard rules before you ever see it.
14
13
 
@@ -17,7 +16,7 @@ It is *not* a smarter brain: your model does the thinking, PromptContract makes
17
16
  ## Why PromptContract
18
17
 
19
18
  - **Stable agent inputs** — a profile plus hard constraints turn "a website for my dog" into goal / scope / acceptance criteria / explicit non-goals. Reduced retries and scope drift are hypotheses measured by the task-level harness, not current product claims.
20
- - **Six deterministic guardrails** — language consistency, enhanced-text-only, length & completeness, expand-don't-answer, no hallucinated tech. Every enhancement can be asserted with `contract check`; the same spec drives templates and tests.
19
+ - **Six deterministic guardrails** — language consistency, enhanced-text-only, length & completeness, expand-don't-answer, no hallucinated tech. Every enhancement can be asserted with `prompt-prompt-contract check`; the same spec drives templates and tests.
21
20
  - **One engine, three surfaces** — a CLI, an MCP server (agent-invoked **tool** + user-invoked **slash prompts**), and a browser playground. All share one zero-dependency core.
22
21
  - **Private by architecture** — bring your own key, no server in the middle, no telemetry, offline-capable via Ollama.
23
22
 
@@ -31,16 +30,21 @@ node packages/cli/bin/contract.js "帮我做一个展示我家狗的网站" \
31
30
  node packages/playground/serve.js # → http://127.0.0.1:8123/ (or ?demo=1 for the self-running demo)
32
31
  ```
33
32
 
34
- Install: `npm i -g prompt-contract` — or zero-install: `npx prompt-contract "your vague idea"`.
33
+ Install: `npm i -g prompt-contract` — or zero-install: `npx prompt-prompt-prompt-contract "your vague idea"`.
35
34
 
36
35
  ## Your real model
37
36
 
38
37
  ```bash
39
- export CONTRACT_API_KEY=sk-xxx CONTRACT_MODEL=gpt-4o-mini # any OpenAI-compatible endpoint (DeepSeek, Qwen, GLM, vLLM…)
40
- contract "A website for my dog"
38
+ export CONTRACT_API_KEY=sk-xxx CONTRACT_MODEL=gpt-4o-mini # any OpenAI-compatible endpoint (vLLM, OpenRouter, your gateway…)
39
+ prompt-prompt-contract "A website for my dog"
40
+
41
+ export CONTRACT_PROVIDER=deepseek CONTRACT_API_KEY=sk-… # vendor presets carry the base URL + a suggested model
42
+ export CONTRACT_PROVIDER=qwen # also: glm, moonshot, groq, openrouter, lmstudio (keyless)
43
+ export CONTRACT_PROVIDER=anthropic CONTRACT_API_KEY=sk-ant-… # native Messages API — thinking deltas dropped in transport
44
+ prompt-prompt-contract "帮我写一封请假邮件"
41
45
 
42
46
  export CONTRACT_PROVIDER=ollama CONTRACT_MODEL=qwen3:4b # fully local/offline; keep_alive pins the model in RAM
43
- contract "帮我写一封请假邮件"
47
+ prompt-prompt-prompt-contract "帮我写一封请假邮件"
44
48
  ```
45
49
 
46
50
  Or write `~/.prompt-contract/config.json` once: `{ "provider": "openai", "baseUrl": "…", "apiKey": "…", "model": "…" }`.
@@ -69,7 +73,7 @@ Community profiles are the main contribution surface — a PR adding `profiles/<
69
73
  ## Quality gates — and their honest limits
70
74
 
71
75
  ```bash
72
- npm test # 60 tests: engine units + SSE/ndjson streaming + CLI/MCP e2e against a local mock
76
+ npm test # 102 tests (serial): engine units + SSE/ndjson streaming + CLI/MCP e2e against a local mock
73
77
  npm run eval # 9 deterministic cases over the six hard-constraint assertions
74
78
  npm run eval:tasks -- --format-only # validate the coding-agent task fixture format gate
75
79
  npm run bench # engine overhead: P50 ≈ 0.005ms (budget < 5ms)
@@ -82,20 +86,20 @@ npm run bench # engine overhead: P50 ≈ 0.005ms (budget < 5ms)
82
86
  ```
83
87
  raw input → script/scenario detect → profile + hard constraints + strength (+ optional context)
84
88
  → single streaming LLM call (small fast model by default; the model does the thinking)
85
- → deterministic cleaning (quotes/fences/length clamp, empty → llm_error)
89
+ → deterministic cleaning (reasoning-block strip, quotes/fences/length clamp, empty → llm_error)
86
90
  → { enhanced, original, meta } — original always preserved, one-key revert in every surface
87
91
  ```
88
92
 
89
93
  ## Status & roadmap — stated honestly
90
94
 
91
- - **Shipped:** engine, CLI (`contract` / `check` / `doctor` / `profiles` / `spike-0`), MCP server (tool + zero-key prompts), playground, 3 profiles, eval cases, CI matrix.
92
- - **Gated:** `contract watch` (global-hotkey resident mode) remains intentionally unavailable. `contract spike-0` measures macOS capture/clipboard safety and dry-run focus eligibility, but never pastes or unlocks watch by itself; see [docs/SPIKE-0.md](docs/SPIKE-0.md).
95
+ - **Shipped:** engine, CLI (`prompt-contract` / `check` / `doctor` / `profiles` / `spike-0` / `watch`), MCP server (tool + zero-key prompts), playground, 3 profiles, eval cases, CI matrix.
96
+ - **Resident mode:** `prompt-prompt-contract watch` — select text anywhere on macOS, press ⌥B, and the enhanced prompt replaces your selection: clipboard backed up and restored, focus re-validated before pasting, gated by `prompt-prompt-contract spike-0` evidence (decision D7). See [docs/WATCH.md](docs/WATCH.md). `prompt-prompt-contract spike-0` itself remains a dry-run diagnostic and never pastes; see [docs/SPIKE-0.md](docs/SPIKE-0.md).
93
97
  - **Open validation:** task-level outcome evaluation remains an evidence-gathering task. The harness and curated fixture set exist, but PromptContract's downstream effectiveness is still a hypothesis until a declared runner produces reviewed results.
94
98
  - Deferred: animated demo asset, IDE plugins, LLM-as-judge as *one* scorer inside the task-level eval.
95
99
 
96
100
  ## Layout
97
101
 
98
- `packages/core` (engine, browser-safe, zero deps) · `packages/providers` (OpenAI-compatible SSE + Ollama `keep_alive`) · `packages/cli` · `packages/mcp-server` · `packages/playground` · `profiles/` · `eval/` · `mock/` · `docs/ACCEPTANCE.md` · `docs/SPIKE-0.md` (requirements → implementation → acceptance + evidence boundaries)
102
+ docs/ACCEPTANCE.md` · `docs/SPIKE-0.md` · `docs/WATCH.md` (requirements → implementation → acceptance + evidence boundaries)
99
103
 
100
104
  ## Contributing & License
101
105
 
package/README.zh-CN.md CHANGED
@@ -6,16 +6,16 @@
6
6
 
7
7
  [English](README.md) · 简体中文
8
8
 
9
+ ![PromptContract——模糊的一句话被编译成结构化任务规范(目标 / 范围 / 验收标准 / 明确不做的事),六条硬规则经 `prompt-prompt-contract check` 校验。真实 LLM 端到端 P50 2.3s,引擎附加开销 0.005ms。CLI · MCP · Playground——零依赖、BYOK、可离线。](docs/assets/hero-cover.png)
10
+
9
11
  **PromptContract 是面向 AI 编码 agent 的确定性 prompt 契约层**:把模糊的一句话编译成结构化任务规范(目标 / 范围 / 约束 / 验收标准),并在你看到结果之前用六条硬规则完成校验。
10
12
 
11
13
  它不是「更聪明的大脑」:思考由你的模型完成,PromptContract 让 agent 的输入变得稳定、可审查、可跨模型与跨工具迁移。
12
14
 
13
- ![Playground 自运行演示:模糊一句话被增强为结构化任务规范,六条规则断言全部通过](docs/assets/hero-playground.png)
14
-
15
15
  ## 为什么
16
16
 
17
17
  - **稳定的 agent 输入**——profile + 硬约束把「帮我做个网站」编译成目标 / 范围 / 验收标准 / 明确不做的事;重试与范围漂移是否减少,仍须由任务级 harness 验证,当前不作产品结论
18
- - **六条确定性护栏**——语言一致性、只输出增强文本、长度与完整性、扩写而非回答、无幻觉技术栈;`contract check` 随时可断言,模板与测试共用同一份规格
18
+ - **六条确定性护栏**——语言一致性、只输出增强文本、长度与完整性、扩写而非回答、无幻觉技术栈;`prompt-prompt-contract check` 随时可断言,模板与测试共用同一份规格
19
19
  - **一个引擎、三个形态**——CLI、MCP server(agent 调用的 tool + 用户调用的斜杠 prompts)、浏览器 Playground,共享同一个零依赖内核
20
20
  - **隐私即架构**——BYOK、无中间服务、零遥测,Ollama 全本地可用
21
21
 
@@ -29,16 +29,21 @@ node packages/cli/bin/contract.js "帮我做一个展示我家狗的网站" \
29
29
  node packages/playground/serve.js # → http://127.0.0.1:8123/(?demo=1 为自运行演示)
30
30
  ```
31
31
 
32
- 安装:`npm i -g prompt-contract`;或零安装体验:`npx prompt-contract "你的模糊想法"`。
32
+ 安装:`npm i -g prompt-contract`;或零安装体验:`npx prompt-prompt-prompt-contract "你的模糊想法"`。
33
33
 
34
34
  ## 接入真实模型
35
35
 
36
36
  ```bash
37
- export CONTRACT_API_KEY=sk-xxx CONTRACT_MODEL=gpt-4o-mini # 任意 OpenAI 兼容端点(DeepSeek/Qwen/GLM/vLLM…)
38
- contract "A website for my dog"
37
+ export CONTRACT_API_KEY=sk-xxx CONTRACT_MODEL=gpt-4o-mini # 任意 OpenAI 兼容端点(vLLM、OpenRouter、自家网关…)
38
+ prompt-prompt-contract "A website for my dog"
39
+
40
+ export CONTRACT_PROVIDER=deepseek CONTRACT_API_KEY=sk-… # 厂商预设:自带 base URL 与建议默认模型
41
+ export CONTRACT_PROVIDER=qwen # 另有 glm、moonshot、groq、openrouter、lmstudio(免 key)
42
+ export CONTRACT_PROVIDER=anthropic CONTRACT_API_KEY=sk-ant-… # Anthropic 原生 Messages API——思考增量在传输层即被丢弃
43
+ prompt-prompt-contract "帮我写一封请假邮件"
39
44
 
40
45
  export CONTRACT_PROVIDER=ollama CONTRACT_MODEL=qwen3:4b # 全本地;keep_alive 把模型钉在内存
41
- contract "帮我写一封请假邮件"
46
+ prompt-prompt-prompt-contract "帮我写一封请假邮件"
42
47
  ```
43
48
 
44
49
  或一次写入 `~/.prompt-contract/config.json`。
@@ -53,7 +58,7 @@ contract "帮我写一封请假邮件"
53
58
  ## 质量门禁与诚实边界
54
59
 
55
60
  ```bash
56
- npm test # 60 项:引擎单测 + SSE/ndjson 流式 + CLI/MCP 端到端(对本地 mock)
61
+ npm test # 102 项(串行):引擎单测 + SSE/ndjson 流式 + CLI/MCP 端到端(对本地 mock)
57
62
  npm run eval # 9 个确定性用例(六条硬约束断言)
58
63
  npm run eval:tasks -- --format-only # 校验 coding-agent 任务 fixture 的格式门禁
59
64
  npm run bench # 引擎自身开销 P50 ≈ 0.005ms(预算 <5ms)
@@ -63,8 +68,8 @@ npm run bench # 引擎自身开销 P50 ≈ 0.005ms(预算 <5ms)
63
68
 
64
69
  ## 状态与路线图(诚实版)
65
70
 
66
- - **已交付**:引擎、CLI(含 `spike-0`)、MCP server(tool + 零 key prompts)、Playground、3 个 profiles、eval 用例、CI 矩阵
67
- - **被门控**:`contract watch`(全局热键常驻)仍不可用。`contract spike-0` 只测 macOS 取词、剪贴板恢复和焦点校验的 dry-run,不发送粘贴,也不会自行解锁 watch;见 [docs/SPIKE-0.md](docs/SPIKE-0.md)
71
+ - **已交付**:引擎、CLI(含 `prompt-prompt-contract spike-0`、`prompt-prompt-contract watch` 常驻模式)、MCP server(tool + 零 key prompts)、Playground、3 个 profiles、eval 用例、CI 矩阵
72
+ - **常驻模式**:`prompt-prompt-contract watch`——在 macOS 任意应用选中一段粗糙 prompt,按 ⌥B,增强结果原地替换选区:剪贴板先备份后恢复、回贴前焦点复验(漂移即放弃)、以 `prompt-prompt-contract spike-0` 证据为启动门控(决策 D7);详见 [docs/WATCH.md](docs/WATCH.md)。`prompt-prompt-contract spike-0` 本身仍是 dry-run 诊断,永不发送粘贴;见 [docs/SPIKE-0.md](docs/SPIKE-0.md)
68
73
  - **开放验证**:任务级效果评测的 harness 与任务 fixture 已交付,但还没有声明 runner 产生并复核结果;长期价值在此之前仍是假设
69
74
  - **推迟**:动画 demo 资产、IDE 插件、LLM-as-judge(作为任务级评测中的评分器之一)
70
75
 
package/package.json CHANGED
@@ -1,21 +1,22 @@
1
1
  {
2
2
  "name": "prompt-contract",
3
- "version": "0.2.0",
4
- "description": "pc — one-key prompt enhancement: turn vague ideas into structured task specs anywhere you type to an AI. CLI + MCP server. Zero dependencies, BYOK, offline-capable (Ollama).",
3
+ "version": "0.3.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "description": "One-key prompt enhancement — turn vague ideas into structured task specs anywhere you type to an AI. CLI + MCP server. Zero dependencies, BYOK, offline-capable (Ollama).",
5
8
  "license": "Apache-2.0",
6
9
  "type": "module",
7
10
  "engines": {
8
11
  "node": ">=20"
9
12
  },
10
13
  "bin": {
11
- "contract": "./packages/cli/bin/contract.js",
12
14
  "prompt-contract": "./packages/cli/bin/contract.js",
13
- "prompt-contract-mcp": "./packages/mcp-server/bin/prompt-contract-mcp.js",
14
- "pb": "./packages/cli/bin/contract.js"
15
+ "contract": "./packages/cli/bin/contract.js",
16
+ "prompt-contract-mcp": "./packages/mcp-server/bin/prompt-contract-mcp.js"
15
17
  },
16
18
  "keywords": [
17
19
  "prompt",
18
- "prompt-contract",
19
20
  "prompt-engineering",
20
21
  "llm",
21
22
  "cli",
@@ -36,7 +37,7 @@
36
37
  "packages/",
37
38
  "profiles/",
38
39
  "README.md",
39
- "README.en.md",
40
+ "README.zh-CN.md",
40
41
  "LICENSE"
41
42
  ]
42
43
  }
@@ -2,11 +2,13 @@
2
2
  /**
3
3
  * contract — PromptContract CLI (PRD §4 P0). Physical file kept as contract.js for path stability.
4
4
  * One-shot enhance (arg or stdin), profiles, check (rule assertions), doctor.
5
- * `contract watch` is intentionally gated by decision D7 (Spike-0 first) — see docs/SPIKE-0.md.
5
+ * One-shot enhance (arg or stdin), profiles, check (rule assertions), doctor,
6
+ * spike-0 (capture diagnostic), watch (resident hotkey mode — see docs/WATCH.md).
6
7
  */
7
8
  import { enhance, checkRules, PromptContractError, normalizeError } from '../../core/src/index.js';
8
9
  import { loadProfiles, loadProfile, resolveConfig } from '../../core/src/node.js';
9
10
  import { createOpenAIProvider } from '../../providers/src/openai.js';
11
+ import { createAnthropicProvider } from '../../providers/src/anthropic.js';
10
12
  import { createOllamaProvider } from '../../providers/src/ollama.js';
11
13
  import { readFileSync, existsSync, writeFileSync } from 'node:fs';
12
14
  import { basename } from 'node:path';
@@ -18,30 +20,32 @@ import {
18
20
  resolveSpikeTargets,
19
21
  runSpike0,
20
22
  } from '../src/spike-0.js';
23
+ import { runWatch } from '../src/watch.js';
21
24
 
22
- // `contract` was the command name before the prompt-contract rename; warn while the alias ships.
25
+ // `prompt-contract` was the command name before the prompt-contract rename; warn while the alias ships.
23
26
  if (basename(process.argv[1] || '') === 'contract') {
24
- process.stderr.write('[deprecated] this CLI is now `contract` (prompt-contract); the `contract` command will be removed in a future release.\n');
27
+ process.stderr.write('[deprecated] this CLI is now `prompt-contract` (prompt-contract); the `prompt-contract` command will be removed in a future release.\n');
25
28
  }
26
29
 
27
- const VERSION = '0.2.0';
28
- const USAGE = `contract — one-key prompt enhancement (PromptContract v${VERSION})
30
+ const VERSION = '0.3.0';
31
+ const USAGE = `prompt-contract — one-key prompt enhancement (PromptContract v${VERSION})
29
32
 
30
33
  Usage:
31
- contract "build me a website for my dog" enhance a prompt (prints enhanced text to stdout)
32
- cat prompt.txt | contract enhance from stdin
33
- contract profiles list built-in profiles
34
- contract check --original "..." --enhanced "..."
34
+ prompt-prompt-prompt-contract "build me a website for my dog" enhance a prompt (prints enhanced text to stdout)
35
+ cat prompt.txt | prompt-contract enhance from stdin
36
+ prompt-prompt-contract profiles list built-in profiles
37
+ prompt-prompt-prompt-contract check --original "..." --enhanced "..."
35
38
  run the six hard-constraint rule assertions
36
- contract doctor verify config, provider reachability, profiles
37
- contract spike-0 macOS-only capture/restore compatibility diagnostic (dry-run)
38
- contract watch NOT BUILT gated by decision D7 (Spike-0 first); see docs/SPIKE-0.md
39
+ prompt-prompt-contract doctor verify config, provider reachability, profiles
40
+ prompt-prompt-prompt-contract spike-0 macOS-only capture/restore compatibility diagnostic (dry-run)
41
+ prompt-prompt-prompt-contract watch resident mode: select text hotkey enhanced text replaces it (macOS; docs/WATCH.md)
39
42
 
40
43
  Options:
41
44
  -p, --profile <name> scenario profile (default: coding-agent)
42
45
  -s, --strength <mode> polish | standard (default) | expand
43
46
  -m, --model <model> model override
44
- --provider <name> openai (default) | ollama
47
+ --provider <name> openai (default) | anthropic | ollama | vendor presets:
48
+ deepseek qwen glm moonshot groq openrouter lmstudio
45
49
  --base-url <url> OpenAI-compatible base URL (or CONTRACT_BASE_URL)
46
50
  --api-key <key> API key (or CONTRACT_API_KEY; local ollama needs none)
47
51
  --context <text> background context to assemble into the prompt
@@ -54,19 +58,30 @@ Options:
54
58
  --setup-delay-ms <ms> Spike-0 delay before each target capture
55
59
  --no-prompt Spike-0 do not wait for target/app setup
56
60
  --output <path> write Spike-0 JSON report to a file
61
+ --hotkey <spec> watch trigger combo: ctrl=⌃ alt/option=⌥ cmd=⌘ shift=⇧ + key,
62
+ e.g. "ctrl+alt+b" = hold ⌃⌥ and press B (default alt+b; persist via "hotkey"
63
+ in ~/.prompt-contract/config.json)
64
+ --trigger <name> watch trigger source: hotkey (default) | stdin (each Enter = trigger, q quits)
65
+ --report <path> Spike-0 JSON report satisfying the watch evidence gate (D7)
66
+ --force run watch without Spike-0 evidence (at your own risk)
67
+ --dry-run watch: capture + enhance but never paste
68
+ --paste-delay-ms <ms> watch: wait between ⌘V and clipboard restore (default: 1000)
69
+ --cooldown-ms <ms> watch: minimum gap between cycles (default: 800)
57
70
  --json machine-readable output {original, enhanced, meta, rules}
58
71
  --no-stream buffer instead of streaming progress
59
72
  -h, --help show this help`;
60
73
 
61
74
  function parseArgs(argv) {
62
75
  const flags = { _: [] };
63
- const needsValue = new Set(['--profile', '-p', '--strength', '-s', '--model', '-m', '--provider', '--base-url', '--api-key', '--context', '--max-chars', '--timeout', '--app', '--iterations', '--settle-ms', '--pause-ms', '--setup-delay-ms', '--output', '--original', '--enhanced']);
76
+ const needsValue = new Set(['--profile', '-p', '--strength', '-s', '--model', '-m', '--provider', '--base-url', '--api-key', '--context', '--max-chars', '--timeout', '--app', '--iterations', '--settle-ms', '--pause-ms', '--setup-delay-ms', '--output', '--original', '--enhanced', '--hotkey', '--trigger', '--report', '--paste-delay-ms', '--cooldown-ms']);
64
77
  const camel = (k) => k.replace(/^--?/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase());
65
78
  for (let i = 0; i < argv.length; i++) {
66
79
  const a = argv[i];
67
80
  if (a === '--json') flags.json = true;
68
81
  else if (a === '--no-stream') flags.noStream = true;
69
82
  else if (a === '--no-prompt') flags.noPrompt = true;
83
+ else if (a === '--dry-run') flags.dryRun = true;
84
+ else if (a === '--force') flags.force = true;
70
85
  else if (a === '--help' || a === '-h') flags.help = true;
71
86
  else if (a === '--version') flags.version = true;
72
87
  else if (needsValue.has(a)) flags[camel(a)] = argv[++i];
@@ -77,9 +92,9 @@ function parseArgs(argv) {
77
92
  }
78
93
 
79
94
  function buildProvider(cfg) {
80
- return cfg.provider === 'ollama'
81
- ? createOllamaProvider({ baseUrl: cfg.baseUrl })
82
- : createOpenAIProvider({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
95
+ if (cfg.provider === 'ollama') return createOllamaProvider({ baseUrl: cfg.baseUrl });
96
+ if (cfg.provider === 'anthropic') return createAnthropicProvider({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
97
+ return createOpenAIProvider({ baseUrl: cfg.baseUrl, apiKey: cfg.apiKey });
83
98
  }
84
99
 
85
100
  function makeProvider(cfg, { warm = false } = {}) {
@@ -133,7 +148,7 @@ async function cmdEnhance(flags) {
133
148
  if (!flags.noStream) {
134
149
  process.stderr.write(`\n— ${res.meta.profile} · ${res.meta.model} · ${res.meta.ms}ms · ${res.meta.chars} chars\n`);
135
150
  if (!rules.pass) {
136
- process.stderr.write('rule assertions (advisory — run `contract check` for gate mode):\n');
151
+ process.stderr.write('rule assertions (advisory — run `prompt-prompt-prompt-contract check` for gate mode):\n');
137
152
  for (const r of rules.results.filter((r) => !r.pass)) {
138
153
  process.stderr.write(` [FAIL] ${r.title}${r.detail ? ` — ${r.detail}` : ''}\n`);
139
154
  }
@@ -168,7 +183,7 @@ function cmdCheck(flags) {
168
183
  }
169
184
  }
170
185
  if (original === undefined || enhanced === undefined) {
171
- process.stderr.write('contract check requires --original and --enhanced (or a JSON {original, enhanced} line on stdin)\n');
186
+ process.stderr.write('prompt-prompt-contract check requires --original and --enhanced (or a JSON {original, enhanced} line on stdin)\n');
172
187
  return 2;
173
188
  }
174
189
  const rules = checkRules(original, enhanced, { maxChars: flags.maxChars ? parseInt(flags.maxChars, 10) : 800 });
@@ -204,7 +219,7 @@ async function cmdDoctor(flags) {
204
219
 
205
220
  async function cmdSpike0(flags) {
206
221
  if (!isMacOS) {
207
- process.stderr.write('contract spike-0 is macOS-only: requires pbpaste, pbcopy, and Accessibility-backed System Events.\n');
222
+ process.stderr.write('prompt-prompt-contract spike-0 is macOS-only: requires pbpaste, pbcopy, and Accessibility-backed System Events.\n');
208
223
  return 2;
209
224
  }
210
225
 
@@ -216,7 +231,7 @@ async function cmdSpike0(flags) {
216
231
  const pauseMs = flags.pauseMs === undefined ? 0 : Number.parseInt(flags.pauseMs, 10);
217
232
  const setupDelayMs = flags.setupDelayMs === undefined ? 0 : Number.parseInt(flags.setupDelayMs, 10);
218
233
  if (!Number.isInteger(iterations) || iterations < 1 || !Number.isInteger(settleMs) || settleMs < 0 || !Number.isInteger(pauseMs) || pauseMs < 0 || !Number.isInteger(setupDelayMs) || setupDelayMs < 0) {
219
- process.stderr.write('contract spike-0 requires non-negative integer --settle-ms/--pause-ms/--setup-delay-ms and positive integer --iterations\n');
234
+ process.stderr.write('prompt-prompt-contract spike-0 requires non-negative integer --settle-ms/--pause-ms/--setup-delay-ms and positive integer --iterations\n');
220
235
  return 2;
221
236
  }
222
237
 
@@ -237,6 +252,10 @@ async function cmdSpike0(flags) {
237
252
  return report.decision.pass ? 0 : 1;
238
253
  }
239
254
 
255
+ async function cmdWatch(flags) {
256
+ return runWatch(flags);
257
+ }
258
+
240
259
  async function main() {
241
260
  const flags = parseArgs(process.argv.slice(2));
242
261
  if (flags.help) { process.stdout.write(USAGE + '\n'); return 0; }
@@ -249,9 +268,7 @@ async function main() {
249
268
  case 'check': return cmdCheck(flags);
250
269
  case 'doctor': return await cmdDoctor(flags);
251
270
  case 'spike-0': return await cmdSpike0(flags);
252
- case 'watch':
253
- process.stderr.write('contract watch is gated by decision D7: run `contract spike-0` and review its evidence before implementing watch.\nThe watch implementation remains intentionally unavailable in this Spike-0-only change.\n');
254
- return 2;
271
+ case 'watch': return await cmdWatch(flags);
255
272
  default:
256
273
  // treat unknown first word as prompt text
257
274
  flags._.unshift(cmd);
@@ -36,6 +36,9 @@ export const APP_TARGETS = Object.freeze({
36
36
  });
37
37
 
38
38
  const COPY_SCRIPT = 'tell application "System Events" to keystroke "c" using {command down}';
39
+ // Used only by prompt-contract watch. The Spike-0 diagnostic itself never issues ⌘V; its
40
+ // safety contract is enforced in captureSelectedText/validatePasteBackDryRun.
41
+ const PASTE_SCRIPT = 'tell application "System Events" to keystroke "v" using {command down}';
39
42
 
40
43
  // The report deliberately excludes AXValue: it can contain the user's prompt or
41
44
  // other sensitive text. These attributes are enough to conservatively detect a
@@ -212,6 +215,12 @@ export function createMacOSAdapter({ run = runCommand } = {}) {
212
215
  if (result.code !== 0) throw commandError('osascript-copy', result);
213
216
  },
214
217
 
218
+ async pasteSelection() {
219
+ const result = await run('/usr/bin/osascript', ['-e', PASTE_SCRIPT]);
220
+ if (result.error) throw result.error;
221
+ if (result.code !== 0) throw commandError('osascript-paste', result);
222
+ },
223
+
215
224
  async getFocusIdentity() {
216
225
  const result = await run('/usr/bin/osascript', ['-e', CONTEXT_SCRIPT], { timeoutMs: 5000 });
217
226
  if (result.error) throw result.error;
@@ -275,7 +284,7 @@ function fingerprintForContext(context) {
275
284
  return createHash('sha256').update(fields.join('\u001f')).digest('hex').slice(0, 16);
276
285
  }
277
286
 
278
- function sameFocusIdentity(left, right) {
287
+ export function sameFocusIdentity(left, right) {
279
288
  if (!left || !right) return false;
280
289
  const appStable = normalized(left.processName) === normalized(right.processName)
281
290
  && normalized(left.bundleId) === normalized(right.bundleId)
@@ -309,8 +318,17 @@ export async function captureSelectedText(adapter, { settleMs = 75 } = {}) {
309
318
  clipboardUntouched = false;
310
319
  await adapter.copySelection();
311
320
  await adapter.sleep(settleMs);
312
- const copiedText = await adapter.readClipboard();
313
- selectedText = typeof copiedText === 'string' && copiedText.trim() ? copiedText : null;
321
+ let copiedText = await adapter.readClipboard();
322
+ // ⌘C is delivered asynchronously: a read that still shows the pre-copy
323
+ // clipboard means the copy has not landed — poll briefly instead of
324
+ // mistaking the stale clipboard for the selection. A read identical to the
325
+ // pre-copy clipboard after polling reports an empty selection (the
326
+ // clipboard fallback then covers the intentional selection==clipboard case).
327
+ for (let polls = 0; copiedText === originalClipboard && polls < 6; polls++) {
328
+ await adapter.sleep(60);
329
+ copiedText = await adapter.readClipboard();
330
+ }
331
+ selectedText = typeof copiedText === 'string' && copiedText.trim() && copiedText !== originalClipboard ? copiedText : null;
314
332
  contextAfterCapture = await adapter.getFocusIdentity();
315
333
  } catch (captureError) {
316
334
  error = captureError;
@@ -457,7 +475,7 @@ export function buildCompatibilityReport({
457
475
  pass: reasons.length === 0,
458
476
  reasons,
459
477
  watchGate: 'closed',
460
- note: 'A dry-run cannot prove actual paste landing; contract watch remains gated until a separate implementation decision.',
478
+ note: 'A dry-run cannot prove actual paste landing; this report is evidence for prompt-contract watch, never an authorization by itself.',
461
479
  },
462
480
  };
463
481
  }
@@ -575,7 +593,7 @@ export function formatSpike0Summary(report) {
575
593
  `capture ${summary.captureSuccesses}/${summary.attempts} (${summary.captureSuccessRate})`,
576
594
  `clipboard restore ${summary.clipboardRestoreSuccesses}/${summary.attempts} (${summary.clipboardRestoreSuccessRate})`,
577
595
  `dry-run paste-back eligibility ${summary.dryRunPasteBackSuccesses}/${summary.attempts} (${summary.dryRunPasteBackRate})`,
578
- report.decision.reasons.length ? `reasons: ${report.decision.reasons.join('; ')}` : 'thresholds met; contract watch remains gated',
596
+ report.decision.reasons.length ? `reasons: ${report.decision.reasons.join('; ')}` : 'thresholds met; evidence usable by prompt-contract watch',
579
597
  ].join('\n');
580
598
  }
581
599