@onthink/prompt-observer 0.1.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/LICENSE +21 -0
- package/PROMPT_OBSERVER.md +104 -0
- package/README.md +104 -0
- package/bin/prompt-observer.mjs +603 -0
- package/package.json +40 -0
- package/schema/event.schema.json +140 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Danial Hasanzade
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Prompt Observer Contract
|
|
2
|
+
|
|
3
|
+
Follow this contract after every user-requested task. Complete the task first, then create and persist one observation event.
|
|
4
|
+
|
|
5
|
+
## Non-negotiable rules
|
|
6
|
+
|
|
7
|
+
1. Use only information visible in the conversation, tool results, file changes, and verification output.
|
|
8
|
+
2. Never store the full user prompt, the full assistant response, system instructions, private reasoning, chain-of-thought, credentials, access tokens, or other secrets.
|
|
9
|
+
3. `prompt_summary` must be a short, redacted description of the request, not a quotation or close reproduction.
|
|
10
|
+
4. Do not invent the model name, token counts, or cost. Use `null` values with `source: "unavailable"` unless the platform explicitly reports them. Use `source: "estimated"` only when a deterministic tokenizer or pricing tool produced the value, and name that tool in `estimation_method`.
|
|
11
|
+
5. Base execution signals only on observed results. Do not claim a file changed or a test passed without evidence.
|
|
12
|
+
6. Use schema version `1.0` and conform to `.prompt-observer/event.schema.json`.
|
|
13
|
+
|
|
14
|
+
## Required workflow
|
|
15
|
+
|
|
16
|
+
After completing a task:
|
|
17
|
+
|
|
18
|
+
1. Evaluate the prompt on each 0–10 dimension in the event schema.
|
|
19
|
+
2. Create a unique event ID and an ISO-8601 UTC timestamp.
|
|
20
|
+
3. Write the event temporarily to `.prompt-observer/pending/<event_id>.json`.
|
|
21
|
+
4. Run:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
node .prompt-observer/prompt-observer.mjs log .prompt-observer/pending/<event_id>.json
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
5. After a successful log, remove only the temporary event file you created.
|
|
28
|
+
6. End the user-facing answer with exactly three concise English lines:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
Prompt Insight: <average>/10 — <short strength>
|
|
32
|
+
Main weakness: <highest-impact weakness, or "No material weakness detected">
|
|
33
|
+
Next time: <one actionable improvement>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Do not paste the complete JSON event into the user-facing response when persistence succeeds.
|
|
37
|
+
|
|
38
|
+
## No-filesystem fallback
|
|
39
|
+
|
|
40
|
+
If writing files or running the logger is unavailable, do not pretend the event was saved. Return the same three-line insight, followed by one fenced `json` block containing the complete valid event. This is the only fallback; do not retry with unsafe shell commands or external services.
|
|
41
|
+
|
|
42
|
+
## Scoring guidance
|
|
43
|
+
|
|
44
|
+
- `intent_clarity`: Is the desired outcome explicit and understandable?
|
|
45
|
+
- `context_sufficiency`: Is enough relevant background provided for the task?
|
|
46
|
+
- `scope_definition`: Are boundaries, affected areas, and exclusions clear?
|
|
47
|
+
- `constraints_quality`: Are technical, safety, compatibility, and style constraints usable?
|
|
48
|
+
- `acceptance_criteria`: Is completion objectively recognizable?
|
|
49
|
+
- `verification_plan`: Are tests or other verification expectations stated or clearly inferable?
|
|
50
|
+
- `ambiguity_risk`: Estimate the risk that a capable agent would choose the wrong interpretation.
|
|
51
|
+
|
|
52
|
+
Score only prompt quality. Do not lower a score because implementation was difficult when the request itself was clear.
|
|
53
|
+
|
|
54
|
+
## Event example
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"schema_version": "1.0",
|
|
59
|
+
"event_id": "evt-20260901-7f3a92c1",
|
|
60
|
+
"timestamp": "2026-09-01T12:00:00.000Z",
|
|
61
|
+
"task_type": "coding",
|
|
62
|
+
"prompt_summary": "Implement a dependency-free prompt observation kit with structured local logging.",
|
|
63
|
+
"intent_clarity": 9,
|
|
64
|
+
"context_sufficiency": 8,
|
|
65
|
+
"scope_definition": 9,
|
|
66
|
+
"constraints_quality": 9,
|
|
67
|
+
"acceptance_criteria": 8,
|
|
68
|
+
"verification_plan": 7,
|
|
69
|
+
"ambiguity_risk": "low",
|
|
70
|
+
"strengths": [
|
|
71
|
+
"The requested deliverables and runtime constraints are explicit."
|
|
72
|
+
],
|
|
73
|
+
"weaknesses": [
|
|
74
|
+
{
|
|
75
|
+
"category": "missing_verification",
|
|
76
|
+
"severity": "low",
|
|
77
|
+
"message": "The exact expected report contents were not fully enumerated."
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"improvement_suggestions": [
|
|
81
|
+
"List the required report sections and one expected example."
|
|
82
|
+
],
|
|
83
|
+
"execution_signals": {
|
|
84
|
+
"result_status": "completed",
|
|
85
|
+
"files_changed": [
|
|
86
|
+
"PROMPT_OBSERVER.md"
|
|
87
|
+
],
|
|
88
|
+
"tests_run": [
|
|
89
|
+
{
|
|
90
|
+
"name": "node --test",
|
|
91
|
+
"status": "passed"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
"usage": {
|
|
96
|
+
"model": null,
|
|
97
|
+
"input_tokens": null,
|
|
98
|
+
"output_tokens": null,
|
|
99
|
+
"cost_usd": null,
|
|
100
|
+
"source": "unavailable",
|
|
101
|
+
"estimation_method": null
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
package/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Prompt Observer
|
|
2
|
+
|
|
3
|
+
Prompt Observer is a dependency-free observability kit for professional coding-agent workflows. It gives an agent a clear contract: after each task, evaluate the prompt, show a short insight, and save a privacy-safe structured event locally.
|
|
4
|
+
|
|
5
|
+
It is designed for Vibe Coding workflows and works without a browser extension, a local model, or an additional AI API. The agent already completing the task creates the observation.
|
|
6
|
+
|
|
7
|
+
## What it records
|
|
8
|
+
|
|
9
|
+
- Prompt-quality dimensions: clarity, context, scope, constraints, acceptance criteria, and verification plan
|
|
10
|
+
- Actionable weaknesses and improvement suggestions
|
|
11
|
+
- Observed execution signals: result status, changed files, and test outcomes
|
|
12
|
+
- Model, token, and cost information only when the platform explicitly provides it
|
|
13
|
+
|
|
14
|
+
Raw prompts, raw responses, private reasoning, system instructions, and secrets are prohibited from the event format.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js 20 or newer
|
|
19
|
+
- A coding agent that can read project instructions
|
|
20
|
+
- Filesystem access by the agent for automatic event persistence
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
Clone this repository and initialize Prompt Observer in a target project:
|
|
25
|
+
|
|
26
|
+
```powershell
|
|
27
|
+
git clone <your-repository-url>
|
|
28
|
+
cd prompt-observer
|
|
29
|
+
node bin/prompt-observer.mjs init "E:\path\to\your-project"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Then add the following one-line instruction to the target project's existing agent-instruction file:
|
|
33
|
+
|
|
34
|
+
```md
|
|
35
|
+
Read and follow `.prompt-observer/PROMPT_OBSERVER.md` after every user-requested task.
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Prompt Observer does not modify vendor-specific instruction files in v1. Add the line to whichever project-instruction mechanism your agent already uses.
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
User request → Coding agent completes the task → Agent evaluates the prompt
|
|
44
|
+
→ Validated JSONL event → Local Markdown report
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each initialized project receives:
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
.prompt-observer/
|
|
51
|
+
PROMPT_OBSERVER.md Agent contract
|
|
52
|
+
event.schema.json Versioned event schema
|
|
53
|
+
prompt-observer.mjs Portable local CLI
|
|
54
|
+
events.jsonl Generated append-only event log
|
|
55
|
+
report.md Generated analysis report
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The generated log and report are excluded by the local `.prompt-observer/.gitignore`; the contract and schema can safely be committed.
|
|
59
|
+
|
|
60
|
+
## Commands
|
|
61
|
+
|
|
62
|
+
Run these commands inside an initialized project:
|
|
63
|
+
|
|
64
|
+
```powershell
|
|
65
|
+
# Check an event before saving it
|
|
66
|
+
node .prompt-observer/prompt-observer.mjs validate .prompt-observer/pending/example.json
|
|
67
|
+
|
|
68
|
+
# Validate and append an event to the local JSONL log
|
|
69
|
+
node .prompt-observer/prompt-observer.mjs log .prompt-observer/pending/example.json
|
|
70
|
+
|
|
71
|
+
# Generate the aggregate Markdown report
|
|
72
|
+
node .prompt-observer/prompt-observer.mjs report .
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The report includes average prompt health, recurring weaknesses, ambiguity risk, execution outcomes, verification trends, and platform-reported usage totals.
|
|
76
|
+
|
|
77
|
+
## No-filesystem fallback
|
|
78
|
+
|
|
79
|
+
When an agent cannot write files, the contract requires it to show the three-line insight and return a complete valid JSON event in a fenced `json` block. It must state that the event was not persisted. A later extension or integration can capture that output automatically.
|
|
80
|
+
|
|
81
|
+
## npm package — coming soon
|
|
82
|
+
|
|
83
|
+
The project is configured for npm packaging, but it is **not published yet**. The final package name will be chosen before release, preferably as a scoped name such as `@your-npm-username/prompt-observer`.
|
|
84
|
+
|
|
85
|
+
After publication, the intended installation flow will be:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
npx @your-npm-username/prompt-observer init .
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Before publishing, replace `@your-npm-username` with your real npm scope and configure the repository URL in `package.json`.
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```powershell
|
|
96
|
+
npm run check
|
|
97
|
+
npm test
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`prepublishOnly` runs both checks automatically before `npm publish`. The package uses only Node.js built-ins; JSONL is the source of truth for v1, while SQLite export is intentionally deferred to a later release.
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appendFile,
|
|
5
|
+
copyFile,
|
|
6
|
+
mkdir,
|
|
7
|
+
open,
|
|
8
|
+
readFile,
|
|
9
|
+
stat,
|
|
10
|
+
writeFile,
|
|
11
|
+
} from "node:fs/promises";
|
|
12
|
+
import { dirname, basename, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const SCHEMA_VERSION = "1.0";
|
|
16
|
+
const SCORE_FIELDS = [
|
|
17
|
+
"intent_clarity",
|
|
18
|
+
"context_sufficiency",
|
|
19
|
+
"scope_definition",
|
|
20
|
+
"constraints_quality",
|
|
21
|
+
"acceptance_criteria",
|
|
22
|
+
"verification_plan",
|
|
23
|
+
];
|
|
24
|
+
const TASK_TYPES = new Set([
|
|
25
|
+
"coding",
|
|
26
|
+
"debugging",
|
|
27
|
+
"planning",
|
|
28
|
+
"review",
|
|
29
|
+
"testing",
|
|
30
|
+
"documentation",
|
|
31
|
+
"refactoring",
|
|
32
|
+
"other",
|
|
33
|
+
]);
|
|
34
|
+
const RISK_LEVELS = new Set(["low", "medium", "high"]);
|
|
35
|
+
const WEAKNESS_CATEGORIES = new Set([
|
|
36
|
+
"missing_context",
|
|
37
|
+
"ambiguous_goal",
|
|
38
|
+
"undefined_scope",
|
|
39
|
+
"missing_constraints",
|
|
40
|
+
"missing_acceptance_criteria",
|
|
41
|
+
"missing_verification",
|
|
42
|
+
"missing_output_format",
|
|
43
|
+
"conflicting_requirements",
|
|
44
|
+
"other",
|
|
45
|
+
]);
|
|
46
|
+
const SEVERITIES = new Set(["low", "medium", "high"]);
|
|
47
|
+
const RESULT_STATUSES = new Set(["completed", "partial", "blocked"]);
|
|
48
|
+
const TEST_STATUSES = new Set(["passed", "failed", "not_run", "unknown"]);
|
|
49
|
+
const USAGE_SOURCES = new Set(["platform_reported", "estimated", "unavailable"]);
|
|
50
|
+
const ROOT_KEYS = new Set([
|
|
51
|
+
"schema_version",
|
|
52
|
+
"event_id",
|
|
53
|
+
"timestamp",
|
|
54
|
+
"task_type",
|
|
55
|
+
"prompt_summary",
|
|
56
|
+
...SCORE_FIELDS,
|
|
57
|
+
"ambiguity_risk",
|
|
58
|
+
"strengths",
|
|
59
|
+
"weaknesses",
|
|
60
|
+
"improvement_suggestions",
|
|
61
|
+
"execution_signals",
|
|
62
|
+
"usage",
|
|
63
|
+
]);
|
|
64
|
+
const BANNED_KEYS = new Set([
|
|
65
|
+
"prompt",
|
|
66
|
+
"raw_prompt",
|
|
67
|
+
"full_prompt",
|
|
68
|
+
"user_prompt",
|
|
69
|
+
"response",
|
|
70
|
+
"raw_response",
|
|
71
|
+
"full_response",
|
|
72
|
+
"assistant_response",
|
|
73
|
+
"reasoning",
|
|
74
|
+
"chain_of_thought",
|
|
75
|
+
"system_prompt",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
function isObject(value) {
|
|
79
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function addUnknownKeyErrors(value, allowed, path, errors) {
|
|
83
|
+
for (const key of Object.keys(value)) {
|
|
84
|
+
if (!allowed.has(key)) errors.push(`${path}.${key} is not allowed`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateShortText(value, path, errors, { max = 500 } = {}) {
|
|
89
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
90
|
+
errors.push(`${path} must be a non-empty string`);
|
|
91
|
+
} else if (value.length > max) {
|
|
92
|
+
errors.push(`${path} must be at most ${max} characters`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function validateTextArray(value, path, errors, maxItems = 10) {
|
|
97
|
+
if (!Array.isArray(value)) {
|
|
98
|
+
errors.push(`${path} must be an array`);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (value.length > maxItems) errors.push(`${path} must contain at most ${maxItems} items`);
|
|
102
|
+
value.forEach((item, index) => validateShortText(item, `${path}[${index}]`, errors));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function findBannedKeys(value, path, errors) {
|
|
106
|
+
if (Array.isArray(value)) {
|
|
107
|
+
value.forEach((item, index) => findBannedKeys(item, `${path}[${index}]`, errors));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (!isObject(value)) return;
|
|
111
|
+
for (const [key, child] of Object.entries(value)) {
|
|
112
|
+
const normalized = key.toLowerCase().replaceAll("-", "_");
|
|
113
|
+
if (BANNED_KEYS.has(normalized)) {
|
|
114
|
+
errors.push(`${path}.${key} is prohibited because raw prompt, response, or private reasoning must not be stored`);
|
|
115
|
+
}
|
|
116
|
+
findBannedKeys(child, `${path}.${key}`, errors);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findLikelySecrets(value, errors) {
|
|
121
|
+
const serialized = JSON.stringify(value);
|
|
122
|
+
const patterns = [
|
|
123
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/i, "private key material"],
|
|
124
|
+
[/\bsk-[A-Za-z0-9_-]{16,}\b/, "an API key"],
|
|
125
|
+
[/\bgh[pousr]_[A-Za-z0-9]{20,}\b/, "a GitHub token"],
|
|
126
|
+
[/\bBearer\s+[A-Za-z0-9._~+/=-]{12,}/i, "a bearer token"],
|
|
127
|
+
];
|
|
128
|
+
for (const [pattern, label] of patterns) {
|
|
129
|
+
if (pattern.test(serialized)) errors.push(`event appears to contain ${label}; redact it before logging`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function validateEvent(event) {
|
|
134
|
+
const errors = [];
|
|
135
|
+
if (!isObject(event)) return ["event must be a JSON object"];
|
|
136
|
+
|
|
137
|
+
addUnknownKeyErrors(event, ROOT_KEYS, "$", errors);
|
|
138
|
+
findBannedKeys(event, "$", errors);
|
|
139
|
+
findLikelySecrets(event, errors);
|
|
140
|
+
|
|
141
|
+
for (const key of ROOT_KEYS) {
|
|
142
|
+
if (!(key in event)) errors.push(`$.${key} is required`);
|
|
143
|
+
}
|
|
144
|
+
if (errors.some((error) => error.endsWith(" is required"))) return errors;
|
|
145
|
+
|
|
146
|
+
if (event.schema_version !== SCHEMA_VERSION) {
|
|
147
|
+
errors.push(`$.schema_version must equal ${SCHEMA_VERSION}`);
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
typeof event.event_id !== "string" ||
|
|
151
|
+
!/^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/.test(event.event_id)
|
|
152
|
+
) {
|
|
153
|
+
errors.push("$.event_id must be 8-128 safe identifier characters");
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
typeof event.timestamp !== "string" ||
|
|
157
|
+
!event.timestamp.includes("T") ||
|
|
158
|
+
Number.isNaN(Date.parse(event.timestamp))
|
|
159
|
+
) {
|
|
160
|
+
errors.push("$.timestamp must be a valid ISO-8601 date-time");
|
|
161
|
+
}
|
|
162
|
+
if (!TASK_TYPES.has(event.task_type)) errors.push("$.task_type is invalid");
|
|
163
|
+
validateShortText(event.prompt_summary, "$.prompt_summary", errors);
|
|
164
|
+
|
|
165
|
+
for (const field of SCORE_FIELDS) {
|
|
166
|
+
if (!Number.isInteger(event[field]) || event[field] < 0 || event[field] > 10) {
|
|
167
|
+
errors.push(`$.${field} must be an integer from 0 to 10`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!RISK_LEVELS.has(event.ambiguity_risk)) errors.push("$.ambiguity_risk is invalid");
|
|
171
|
+
validateTextArray(event.strengths, "$.strengths", errors);
|
|
172
|
+
validateTextArray(event.improvement_suggestions, "$.improvement_suggestions", errors);
|
|
173
|
+
|
|
174
|
+
if (!Array.isArray(event.weaknesses)) {
|
|
175
|
+
errors.push("$.weaknesses must be an array");
|
|
176
|
+
} else {
|
|
177
|
+
if (event.weaknesses.length > 10) errors.push("$.weaknesses must contain at most 10 items");
|
|
178
|
+
event.weaknesses.forEach((weakness, index) => {
|
|
179
|
+
const path = `$.weaknesses[${index}]`;
|
|
180
|
+
if (!isObject(weakness)) {
|
|
181
|
+
errors.push(`${path} must be an object`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
addUnknownKeyErrors(weakness, new Set(["category", "severity", "message"]), path, errors);
|
|
185
|
+
if (!WEAKNESS_CATEGORIES.has(weakness.category)) errors.push(`${path}.category is invalid`);
|
|
186
|
+
if (!SEVERITIES.has(weakness.severity)) errors.push(`${path}.severity is invalid`);
|
|
187
|
+
validateShortText(weakness.message, `${path}.message`, errors);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const signals = event.execution_signals;
|
|
192
|
+
if (!isObject(signals)) {
|
|
193
|
+
errors.push("$.execution_signals must be an object");
|
|
194
|
+
} else {
|
|
195
|
+
addUnknownKeyErrors(signals, new Set(["result_status", "files_changed", "tests_run"]), "$.execution_signals", errors);
|
|
196
|
+
if (!RESULT_STATUSES.has(signals.result_status)) {
|
|
197
|
+
errors.push("$.execution_signals.result_status is invalid");
|
|
198
|
+
}
|
|
199
|
+
validateTextArray(signals.files_changed, "$.execution_signals.files_changed", errors, 500);
|
|
200
|
+
if (!Array.isArray(signals.tests_run)) {
|
|
201
|
+
errors.push("$.execution_signals.tests_run must be an array");
|
|
202
|
+
} else {
|
|
203
|
+
if (signals.tests_run.length > 100) {
|
|
204
|
+
errors.push("$.execution_signals.tests_run must contain at most 100 items");
|
|
205
|
+
}
|
|
206
|
+
signals.tests_run.forEach((test, index) => {
|
|
207
|
+
const path = `$.execution_signals.tests_run[${index}]`;
|
|
208
|
+
if (!isObject(test)) {
|
|
209
|
+
errors.push(`${path} must be an object`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
addUnknownKeyErrors(test, new Set(["name", "status"]), path, errors);
|
|
213
|
+
validateShortText(test.name, `${path}.name`, errors);
|
|
214
|
+
if (!TEST_STATUSES.has(test.status)) errors.push(`${path}.status is invalid`);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const usage = event.usage;
|
|
220
|
+
if (!isObject(usage)) {
|
|
221
|
+
errors.push("$.usage must be an object");
|
|
222
|
+
} else {
|
|
223
|
+
const usageKeys = new Set([
|
|
224
|
+
"model",
|
|
225
|
+
"input_tokens",
|
|
226
|
+
"output_tokens",
|
|
227
|
+
"cost_usd",
|
|
228
|
+
"source",
|
|
229
|
+
"estimation_method",
|
|
230
|
+
]);
|
|
231
|
+
addUnknownKeyErrors(usage, usageKeys, "$.usage", errors);
|
|
232
|
+
for (const key of usageKeys) {
|
|
233
|
+
if (!(key in usage)) errors.push(`$.usage.${key} is required`);
|
|
234
|
+
}
|
|
235
|
+
if (usage.model !== null && (typeof usage.model !== "string" || usage.model.length > 200)) {
|
|
236
|
+
errors.push("$.usage.model must be null or a string up to 200 characters");
|
|
237
|
+
}
|
|
238
|
+
for (const field of ["input_tokens", "output_tokens"]) {
|
|
239
|
+
if (usage[field] !== null && (!Number.isInteger(usage[field]) || usage[field] < 0)) {
|
|
240
|
+
errors.push(`$.usage.${field} must be null or a non-negative integer`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (usage.cost_usd !== null && (typeof usage.cost_usd !== "number" || usage.cost_usd < 0)) {
|
|
244
|
+
errors.push("$.usage.cost_usd must be null or a non-negative number");
|
|
245
|
+
}
|
|
246
|
+
if (!USAGE_SOURCES.has(usage.source)) errors.push("$.usage.source is invalid");
|
|
247
|
+
if (
|
|
248
|
+
usage.estimation_method !== null &&
|
|
249
|
+
(typeof usage.estimation_method !== "string" || usage.estimation_method.trim() === "" || usage.estimation_method.length > 300)
|
|
250
|
+
) {
|
|
251
|
+
errors.push("$.usage.estimation_method must be null or a non-empty string up to 300 characters");
|
|
252
|
+
}
|
|
253
|
+
const metrics = [usage.model, usage.input_tokens, usage.output_tokens, usage.cost_usd];
|
|
254
|
+
if (usage.source === "unavailable" && metrics.some((value) => value !== null)) {
|
|
255
|
+
errors.push("$.usage metrics must all be null when source is unavailable");
|
|
256
|
+
}
|
|
257
|
+
if (usage.source === "unavailable" && usage.estimation_method !== null) {
|
|
258
|
+
errors.push("$.usage.estimation_method must be null when source is unavailable");
|
|
259
|
+
}
|
|
260
|
+
if (usage.source === "estimated") {
|
|
261
|
+
if (typeof usage.estimation_method !== "string" || usage.estimation_method.trim() === "") {
|
|
262
|
+
errors.push("$.usage.estimation_method is required when source is estimated");
|
|
263
|
+
}
|
|
264
|
+
if (usage.input_tokens === null && usage.output_tokens === null && usage.cost_usd === null) {
|
|
265
|
+
errors.push("$.usage estimated source requires at least one estimated numeric metric");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (usage.source === "platform_reported" && usage.estimation_method !== null) {
|
|
269
|
+
errors.push("$.usage.estimation_method must be null when source is platform_reported");
|
|
270
|
+
}
|
|
271
|
+
if (usage.source === "platform_reported" && metrics.every((value) => value === null)) {
|
|
272
|
+
errors.push("$.usage platform_reported source requires at least one reported metric");
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return errors;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function exists(path) {
|
|
280
|
+
try {
|
|
281
|
+
await stat(path);
|
|
282
|
+
return true;
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error.code === "ENOENT") return false;
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function findResourceRoot() {
|
|
290
|
+
const selfDirectory = dirname(fileURLToPath(import.meta.url));
|
|
291
|
+
const candidates = [resolve(selfDirectory, ".."), selfDirectory];
|
|
292
|
+
for (const candidate of candidates) {
|
|
293
|
+
const contract = join(candidate, "PROMPT_OBSERVER.md");
|
|
294
|
+
const schema = (await exists(join(candidate, "schema", "event.schema.json")))
|
|
295
|
+
? join(candidate, "schema", "event.schema.json")
|
|
296
|
+
: join(candidate, "event.schema.json");
|
|
297
|
+
if ((await exists(contract)) && (await exists(schema))) {
|
|
298
|
+
return { root: candidate, contract, schema };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
throw new Error("Could not locate PROMPT_OBSERVER.md and event.schema.json next to the CLI.");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function copyUnlessPresent(source, destination, actions) {
|
|
305
|
+
if (await exists(destination)) {
|
|
306
|
+
actions.push(`kept ${destination}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await copyFile(source, destination);
|
|
310
|
+
actions.push(`created ${destination}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function writeUnlessPresent(destination, content, actions) {
|
|
314
|
+
if (await exists(destination)) {
|
|
315
|
+
actions.push(`kept ${destination}`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
await writeFile(destination, content, "utf8");
|
|
319
|
+
actions.push(`created ${destination}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export async function initProject(targetPath) {
|
|
323
|
+
const target = resolve(targetPath ?? ".");
|
|
324
|
+
const observerDirectory = join(target, ".prompt-observer");
|
|
325
|
+
const resources = await findResourceRoot();
|
|
326
|
+
const selfPath = fileURLToPath(import.meta.url);
|
|
327
|
+
const actions = [];
|
|
328
|
+
|
|
329
|
+
await mkdir(observerDirectory, { recursive: true });
|
|
330
|
+
await mkdir(join(observerDirectory, "pending"), { recursive: true });
|
|
331
|
+
await copyUnlessPresent(resources.contract, join(observerDirectory, "PROMPT_OBSERVER.md"), actions);
|
|
332
|
+
await copyUnlessPresent(resources.schema, join(observerDirectory, "event.schema.json"), actions);
|
|
333
|
+
await copyUnlessPresent(selfPath, join(observerDirectory, "prompt-observer.mjs"), actions);
|
|
334
|
+
await writeUnlessPresent(
|
|
335
|
+
join(observerDirectory, ".gitignore"),
|
|
336
|
+
["events.jsonl", "report.md", "pending/", "*.tmp", ""].join("\n"),
|
|
337
|
+
actions,
|
|
338
|
+
);
|
|
339
|
+
const eventsFile = join(observerDirectory, "events.jsonl");
|
|
340
|
+
const file = await open(eventsFile, "a");
|
|
341
|
+
await file.close();
|
|
342
|
+
actions.push((await stat(eventsFile)).size === 0 ? `ready ${eventsFile}` : `kept ${eventsFile}`);
|
|
343
|
+
|
|
344
|
+
return { target, observerDirectory, actions };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function resolveObserverDirectory(targetPath) {
|
|
348
|
+
if (targetPath) {
|
|
349
|
+
const resolved = resolve(targetPath);
|
|
350
|
+
const candidate = basename(resolved) === ".prompt-observer" ? resolved : join(resolved, ".prompt-observer");
|
|
351
|
+
if (await exists(candidate)) return candidate;
|
|
352
|
+
throw new Error(`Prompt Observer is not initialized at ${resolved}. Run init first.`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let current = resolve(".");
|
|
356
|
+
while (true) {
|
|
357
|
+
const candidate = join(current, ".prompt-observer");
|
|
358
|
+
if (await exists(candidate)) return candidate;
|
|
359
|
+
const parent = dirname(current);
|
|
360
|
+
if (parent === current) break;
|
|
361
|
+
current = parent;
|
|
362
|
+
}
|
|
363
|
+
throw new Error("No .prompt-observer directory found from the current directory upward. Run init first.");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function readEvents(eventsPath) {
|
|
367
|
+
if (!(await exists(eventsPath))) return [];
|
|
368
|
+
const contents = await readFile(eventsPath, "utf8");
|
|
369
|
+
const events = [];
|
|
370
|
+
for (const [index, line] of contents.split(/\r?\n/).entries()) {
|
|
371
|
+
if (line.trim() === "") continue;
|
|
372
|
+
let event;
|
|
373
|
+
try {
|
|
374
|
+
event = JSON.parse(line);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error(`Invalid JSON in ${eventsPath} at line ${index + 1}: ${error.message}`);
|
|
377
|
+
}
|
|
378
|
+
const validationErrors = validateEvent(event);
|
|
379
|
+
if (validationErrors.length > 0) {
|
|
380
|
+
throw new Error(`Invalid event in ${eventsPath} at line ${index + 1}:\n- ${validationErrors.join("\n- ")}`);
|
|
381
|
+
}
|
|
382
|
+
events.push(event);
|
|
383
|
+
}
|
|
384
|
+
return events;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export async function logEvent(eventFile, targetPath) {
|
|
388
|
+
if (!eventFile) throw new Error("An event JSON file is required.");
|
|
389
|
+
const eventPath = resolve(eventFile);
|
|
390
|
+
let event;
|
|
391
|
+
try {
|
|
392
|
+
event = JSON.parse(await readFile(eventPath, "utf8"));
|
|
393
|
+
} catch (error) {
|
|
394
|
+
throw new Error(`Could not read event JSON from ${eventPath}: ${error.message}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const validationErrors = validateEvent(event);
|
|
398
|
+
if (validationErrors.length > 0) {
|
|
399
|
+
throw new Error(`Event validation failed:\n- ${validationErrors.join("\n- ")}`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const observerDirectory = await resolveObserverDirectory(targetPath);
|
|
403
|
+
const eventsPath = join(observerDirectory, "events.jsonl");
|
|
404
|
+
const existingEvents = await readEvents(eventsPath);
|
|
405
|
+
if (existingEvents.some((item) => item.event_id === event.event_id)) {
|
|
406
|
+
throw new Error(`Event ID ${event.event_id} already exists; refusing to create a duplicate.`);
|
|
407
|
+
}
|
|
408
|
+
await appendFile(eventsPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
409
|
+
return { eventId: event.event_id, eventsPath };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function average(values) {
|
|
413
|
+
if (values.length === 0) return null;
|
|
414
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function formatAverage(value) {
|
|
418
|
+
return value === null ? "N/A" : value.toFixed(2);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function tableRows(counts, preferredOrder) {
|
|
422
|
+
return preferredOrder.map((key) => `| ${key} | ${counts.get(key) ?? 0} |`).join("\n");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export function buildReport(events) {
|
|
426
|
+
const dimensionAverages = new Map();
|
|
427
|
+
for (const field of SCORE_FIELDS) {
|
|
428
|
+
dimensionAverages.set(field, average(events.map((event) => event[field])));
|
|
429
|
+
}
|
|
430
|
+
const overallScores = events.map((event) => average(SCORE_FIELDS.map((field) => event[field])));
|
|
431
|
+
const overallAverage = average(overallScores.filter((value) => value !== null));
|
|
432
|
+
|
|
433
|
+
const weaknessCounts = new Map();
|
|
434
|
+
const severityCounts = new Map();
|
|
435
|
+
const riskCounts = new Map();
|
|
436
|
+
const resultCounts = new Map();
|
|
437
|
+
const testCounts = new Map();
|
|
438
|
+
let eventsWithTests = 0;
|
|
439
|
+
let unavailableUsage = 0;
|
|
440
|
+
let totalInputTokens = 0;
|
|
441
|
+
let totalOutputTokens = 0;
|
|
442
|
+
let totalCost = 0;
|
|
443
|
+
let hasInputTokens = false;
|
|
444
|
+
let hasOutputTokens = false;
|
|
445
|
+
let hasCost = false;
|
|
446
|
+
|
|
447
|
+
for (const event of events) {
|
|
448
|
+
riskCounts.set(event.ambiguity_risk, (riskCounts.get(event.ambiguity_risk) ?? 0) + 1);
|
|
449
|
+
const status = event.execution_signals.result_status;
|
|
450
|
+
resultCounts.set(status, (resultCounts.get(status) ?? 0) + 1);
|
|
451
|
+
if (event.execution_signals.tests_run.length > 0) eventsWithTests += 1;
|
|
452
|
+
for (const test of event.execution_signals.tests_run) {
|
|
453
|
+
testCounts.set(test.status, (testCounts.get(test.status) ?? 0) + 1);
|
|
454
|
+
}
|
|
455
|
+
for (const weakness of event.weaknesses) {
|
|
456
|
+
weaknessCounts.set(weakness.category, (weaknessCounts.get(weakness.category) ?? 0) + 1);
|
|
457
|
+
severityCounts.set(weakness.severity, (severityCounts.get(weakness.severity) ?? 0) + 1);
|
|
458
|
+
}
|
|
459
|
+
if (event.usage.source === "unavailable") unavailableUsage += 1;
|
|
460
|
+
if (event.usage.input_tokens !== null) {
|
|
461
|
+
totalInputTokens += event.usage.input_tokens;
|
|
462
|
+
hasInputTokens = true;
|
|
463
|
+
}
|
|
464
|
+
if (event.usage.output_tokens !== null) {
|
|
465
|
+
totalOutputTokens += event.usage.output_tokens;
|
|
466
|
+
hasOutputTokens = true;
|
|
467
|
+
}
|
|
468
|
+
if (event.usage.cost_usd !== null) {
|
|
469
|
+
totalCost += event.usage.cost_usd;
|
|
470
|
+
hasCost = true;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const topWeaknesses = [...weaknessCounts.entries()]
|
|
475
|
+
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
|
476
|
+
.map(([category, count]) => `| ${category} | ${count} |`)
|
|
477
|
+
.join("\n");
|
|
478
|
+
const generatedAt = new Date().toISOString();
|
|
479
|
+
|
|
480
|
+
return `# Prompt Observer Report
|
|
481
|
+
|
|
482
|
+
Generated: ${generatedAt}
|
|
483
|
+
|
|
484
|
+
## Overview
|
|
485
|
+
|
|
486
|
+
- Events: ${events.length}
|
|
487
|
+
- Average prompt health: ${formatAverage(overallAverage)}/10
|
|
488
|
+
- Events with tests: ${eventsWithTests}
|
|
489
|
+
- Events with unavailable usage: ${unavailableUsage}
|
|
490
|
+
|
|
491
|
+
## Quality dimensions
|
|
492
|
+
|
|
493
|
+
| Dimension | Average |
|
|
494
|
+
| --- | ---: |
|
|
495
|
+
${SCORE_FIELDS.map((field) => `| ${field} | ${formatAverage(dimensionAverages.get(field))} |`).join("\n")}
|
|
496
|
+
|
|
497
|
+
## Weaknesses
|
|
498
|
+
|
|
499
|
+
| Category | Count |
|
|
500
|
+
| --- | ---: |
|
|
501
|
+
${topWeaknesses || "| None recorded | 0 |"}
|
|
502
|
+
|
|
503
|
+
### Severity
|
|
504
|
+
|
|
505
|
+
| Severity | Count |
|
|
506
|
+
| --- | ---: |
|
|
507
|
+
${tableRows(severityCounts, ["high", "medium", "low"])}
|
|
508
|
+
|
|
509
|
+
## Ambiguity risk
|
|
510
|
+
|
|
511
|
+
| Risk | Count |
|
|
512
|
+
| --- | ---: |
|
|
513
|
+
${tableRows(riskCounts, ["high", "medium", "low"])}
|
|
514
|
+
|
|
515
|
+
## Execution outcomes
|
|
516
|
+
|
|
517
|
+
| Status | Count |
|
|
518
|
+
| --- | ---: |
|
|
519
|
+
${tableRows(resultCounts, ["completed", "partial", "blocked"])}
|
|
520
|
+
|
|
521
|
+
## Verification
|
|
522
|
+
|
|
523
|
+
| Test status | Count |
|
|
524
|
+
| --- | ---: |
|
|
525
|
+
${tableRows(testCounts, ["passed", "failed", "not_run", "unknown"])}
|
|
526
|
+
|
|
527
|
+
## Reported usage
|
|
528
|
+
|
|
529
|
+
- Input tokens: ${hasInputTokens ? totalInputTokens : "N/A"}
|
|
530
|
+
- Output tokens: ${hasOutputTokens ? totalOutputTokens : "N/A"}
|
|
531
|
+
- Cost (USD): ${hasCost ? totalCost.toFixed(6) : "N/A"}
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export async function generateReport(targetPath) {
|
|
536
|
+
const observerDirectory = await resolveObserverDirectory(targetPath ?? ".");
|
|
537
|
+
const eventsPath = join(observerDirectory, "events.jsonl");
|
|
538
|
+
const events = await readEvents(eventsPath);
|
|
539
|
+
const report = buildReport(events);
|
|
540
|
+
const reportPath = join(observerDirectory, "report.md");
|
|
541
|
+
await writeFile(reportPath, report, "utf8");
|
|
542
|
+
return { eventCount: events.length, reportPath, report };
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function printHelp() {
|
|
546
|
+
process.stdout.write(`Prompt Observer ${SCHEMA_VERSION}
|
|
547
|
+
|
|
548
|
+
Usage:
|
|
549
|
+
prompt-observer init <target-path>
|
|
550
|
+
prompt-observer log <event-file> [--target <target-path>]
|
|
551
|
+
prompt-observer report [target-path]
|
|
552
|
+
prompt-observer validate <event-file>
|
|
553
|
+
prompt-observer help
|
|
554
|
+
`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function optionValue(args, name) {
|
|
558
|
+
const index = args.indexOf(name);
|
|
559
|
+
if (index === -1) return undefined;
|
|
560
|
+
if (!args[index + 1]) throw new Error(`${name} requires a value.`);
|
|
561
|
+
return args[index + 1];
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
export async function runCli(args) {
|
|
565
|
+
const [command, ...rest] = args;
|
|
566
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
567
|
+
printHelp();
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (command === "init") {
|
|
571
|
+
const result = await initProject(rest[0] ?? ".");
|
|
572
|
+
process.stdout.write(`Initialized Prompt Observer at ${result.observerDirectory}\n${result.actions.join("\n")}\n`);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (command === "validate") {
|
|
576
|
+
if (!rest[0]) throw new Error("validate requires an event JSON file.");
|
|
577
|
+
const event = JSON.parse(await readFile(resolve(rest[0]), "utf8"));
|
|
578
|
+
const errors = validateEvent(event);
|
|
579
|
+
if (errors.length > 0) throw new Error(`Event validation failed:\n- ${errors.join("\n- ")}`);
|
|
580
|
+
process.stdout.write(`Valid event: ${event.event_id}\n`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (command === "log") {
|
|
584
|
+
const target = optionValue(rest, "--target");
|
|
585
|
+
const result = await logEvent(rest[0], target);
|
|
586
|
+
process.stdout.write(`Logged ${result.eventId} to ${result.eventsPath}\n`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (command === "report") {
|
|
590
|
+
const result = await generateReport(rest[0] ?? ".");
|
|
591
|
+
process.stdout.write(`Generated ${result.reportPath} from ${result.eventCount} event(s).\n`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
throw new Error(`Unknown command: ${command}`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const isDirectExecution = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));
|
|
598
|
+
if (isDirectExecution) {
|
|
599
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
600
|
+
process.stderr.write(`Error: ${error.message}\n`);
|
|
601
|
+
process.exitCode = 1;
|
|
602
|
+
});
|
|
603
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@onthink/prompt-observer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A dependency-free prompt observability contract and CLI for coding agents.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"coding-agents",
|
|
8
|
+
"prompt-engineering",
|
|
9
|
+
"observability",
|
|
10
|
+
"vibe-coding"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"prompt-observer": "./bin/prompt-observer.mjs"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test",
|
|
18
|
+
"check": "node --check bin/prompt-observer.mjs",
|
|
19
|
+
"prepublishOnly": "npm run check && npm test"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/daniialHZ/prompt-observer.git"
|
|
27
|
+
},
|
|
28
|
+
"bugs": {
|
|
29
|
+
"url": "https://github.com/daniialHZ/prompt-observer/issues"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/daniialHZ/prompt-observer#readme",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"files": [
|
|
34
|
+
"bin/",
|
|
35
|
+
"schema/",
|
|
36
|
+
"PROMPT_OBSERVER.md",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://prompt-observer.local/schema/event-1.0.json",
|
|
4
|
+
"title": "Prompt Observer Event",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"schema_version",
|
|
9
|
+
"event_id",
|
|
10
|
+
"timestamp",
|
|
11
|
+
"task_type",
|
|
12
|
+
"prompt_summary",
|
|
13
|
+
"intent_clarity",
|
|
14
|
+
"context_sufficiency",
|
|
15
|
+
"scope_definition",
|
|
16
|
+
"constraints_quality",
|
|
17
|
+
"acceptance_criteria",
|
|
18
|
+
"verification_plan",
|
|
19
|
+
"ambiguity_risk",
|
|
20
|
+
"strengths",
|
|
21
|
+
"weaknesses",
|
|
22
|
+
"improvement_suggestions",
|
|
23
|
+
"execution_signals",
|
|
24
|
+
"usage"
|
|
25
|
+
],
|
|
26
|
+
"properties": {
|
|
27
|
+
"schema_version": { "const": "1.0" },
|
|
28
|
+
"event_id": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$"
|
|
31
|
+
},
|
|
32
|
+
"timestamp": { "type": "string", "format": "date-time" },
|
|
33
|
+
"task_type": {
|
|
34
|
+
"enum": [
|
|
35
|
+
"coding",
|
|
36
|
+
"debugging",
|
|
37
|
+
"planning",
|
|
38
|
+
"review",
|
|
39
|
+
"testing",
|
|
40
|
+
"documentation",
|
|
41
|
+
"refactoring",
|
|
42
|
+
"other"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"prompt_summary": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
46
|
+
"intent_clarity": { "$ref": "#/$defs/score" },
|
|
47
|
+
"context_sufficiency": { "$ref": "#/$defs/score" },
|
|
48
|
+
"scope_definition": { "$ref": "#/$defs/score" },
|
|
49
|
+
"constraints_quality": { "$ref": "#/$defs/score" },
|
|
50
|
+
"acceptance_criteria": { "$ref": "#/$defs/score" },
|
|
51
|
+
"verification_plan": { "$ref": "#/$defs/score" },
|
|
52
|
+
"ambiguity_risk": { "enum": ["low", "medium", "high"] },
|
|
53
|
+
"strengths": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": { "$ref": "#/$defs/shortText" },
|
|
56
|
+
"maxItems": 10
|
|
57
|
+
},
|
|
58
|
+
"weaknesses": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"maxItems": 10,
|
|
61
|
+
"items": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"additionalProperties": false,
|
|
64
|
+
"required": ["category", "severity", "message"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"category": {
|
|
67
|
+
"enum": [
|
|
68
|
+
"missing_context",
|
|
69
|
+
"ambiguous_goal",
|
|
70
|
+
"undefined_scope",
|
|
71
|
+
"missing_constraints",
|
|
72
|
+
"missing_acceptance_criteria",
|
|
73
|
+
"missing_verification",
|
|
74
|
+
"missing_output_format",
|
|
75
|
+
"conflicting_requirements",
|
|
76
|
+
"other"
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"severity": { "enum": ["low", "medium", "high"] },
|
|
80
|
+
"message": { "$ref": "#/$defs/shortText" }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"improvement_suggestions": {
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": { "$ref": "#/$defs/shortText" },
|
|
87
|
+
"maxItems": 10
|
|
88
|
+
},
|
|
89
|
+
"execution_signals": {
|
|
90
|
+
"type": "object",
|
|
91
|
+
"additionalProperties": false,
|
|
92
|
+
"required": ["result_status", "files_changed", "tests_run"],
|
|
93
|
+
"properties": {
|
|
94
|
+
"result_status": { "enum": ["completed", "partial", "blocked"] },
|
|
95
|
+
"files_changed": {
|
|
96
|
+
"type": "array",
|
|
97
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
98
|
+
"maxItems": 500
|
|
99
|
+
},
|
|
100
|
+
"tests_run": {
|
|
101
|
+
"type": "array",
|
|
102
|
+
"maxItems": 100,
|
|
103
|
+
"items": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"additionalProperties": false,
|
|
106
|
+
"required": ["name", "status"],
|
|
107
|
+
"properties": {
|
|
108
|
+
"name": { "type": "string", "minLength": 1, "maxLength": 500 },
|
|
109
|
+
"status": { "enum": ["passed", "failed", "not_run", "unknown"] }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"usage": {
|
|
116
|
+
"type": "object",
|
|
117
|
+
"additionalProperties": false,
|
|
118
|
+
"required": [
|
|
119
|
+
"model",
|
|
120
|
+
"input_tokens",
|
|
121
|
+
"output_tokens",
|
|
122
|
+
"cost_usd",
|
|
123
|
+
"source",
|
|
124
|
+
"estimation_method"
|
|
125
|
+
],
|
|
126
|
+
"properties": {
|
|
127
|
+
"model": { "type": ["string", "null"], "maxLength": 200 },
|
|
128
|
+
"input_tokens": { "type": ["integer", "null"], "minimum": 0 },
|
|
129
|
+
"output_tokens": { "type": ["integer", "null"], "minimum": 0 },
|
|
130
|
+
"cost_usd": { "type": ["number", "null"], "minimum": 0 },
|
|
131
|
+
"source": { "enum": ["platform_reported", "estimated", "unavailable"] },
|
|
132
|
+
"estimation_method": { "type": ["string", "null"], "maxLength": 300 }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
"$defs": {
|
|
137
|
+
"score": { "type": "integer", "minimum": 0, "maximum": 10 },
|
|
138
|
+
"shortText": { "type": "string", "minLength": 1, "maxLength": 500 }
|
|
139
|
+
}
|
|
140
|
+
}
|