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 +16 -12
- package/README.zh-CN.md +15 -10
- package/package.json +8 -7
- package/packages/cli/bin/contract.js +41 -24
- package/packages/cli/src/spike-0.js +23 -5
- package/packages/cli/src/watch.js +609 -0
- package/packages/core/src/clean.js +23 -0
- package/packages/core/src/node.js +30 -6
- package/packages/core/src/rules.js +1 -1
- package/packages/mcp-server/src/server.js +4 -1
- package/packages/providers/src/anthropic.js +84 -0
- package/packages/providers/src/openai.js +3 -2
- package/packages/cli/test/cli.test.js +0 -89
- package/packages/cli/test/spike-0.test.js +0 -260
- package/packages/core/bench/bench.js +0 -48
- package/packages/core/package.json +0 -11
- package/packages/core/test/clean.test.js +0 -44
- package/packages/core/test/lang.test.js +0 -21
- package/packages/core/test/pipeline.test.js +0 -85
- package/packages/core/test/profile.test.js +0 -40
- package/packages/core/test/rules.test.js +0 -53
- package/packages/mcp-server/package.json +0 -10
- package/packages/mcp-server/test/mcp.test.js +0 -167
- package/packages/providers/package.json +0 -7
- package/packages/providers/test/providers.test.js +0 -64
package/README.md
CHANGED
|
@@ -7,8 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
English · [简体中文](README.zh-CN.md)
|
|
9
9
|
|
|
10
|
-

|
|
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 (
|
|
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 #
|
|
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
|
-
- **
|
|
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
|
-
|
|
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
|
+

|
|
10
|
+
|
|
9
11
|
**PromptContract 是面向 AI 编码 agent 的确定性 prompt 契约层**:把模糊的一句话编译成结构化任务规范(目标 / 范围 / 约束 / 验收标准),并在你看到结果之前用六条硬规则完成校验。
|
|
10
12
|
|
|
11
13
|
它不是「更聪明的大脑」:思考由你的模型完成,PromptContract 让 agent 的输入变得稳定、可审查、可跨模型与跨工具迁移。
|
|
12
14
|
|
|
13
|
-

|
|
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 兼容端点(
|
|
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 #
|
|
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
|
|
67
|
-
-
|
|
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.
|
|
4
|
-
"
|
|
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
|
-
"
|
|
14
|
-
"
|
|
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.
|
|
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
|
-
*
|
|
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.
|
|
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
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
313
|
-
|
|
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
|
|
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
|
|
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
|
|