@plures/praxis 1.2.12 → 1.2.41
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 +63 -0
- package/dist/browser/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
- package/dist/browser/{chunk-K377RW4V.js → chunk-FCEH7WMH.js} +1 -1
- package/dist/browser/{engine-YJZV4SLD.js → engine-65QDGCAN.js} +1 -1
- package/dist/browser/index.d.ts +104 -2
- package/dist/browser/index.js +181 -5
- package/dist/browser/integrations/svelte.d.ts +2 -2
- package/dist/browser/integrations/svelte.js +2 -2
- package/dist/browser/{reactive-engine.svelte-9aS0kTa8.d.ts → reactive-engine.svelte-Cqd8Mod2.d.ts} +56 -1
- package/dist/node/{chunk-PRPQO6R5.js → chunk-32YFEEML.js} +1 -1
- package/dist/node/{chunk-VOMLVI6V.js → chunk-BBP2F7TT.js} +70 -1
- package/dist/node/{chunk-5RH7UAQC.js → chunk-PTH6MD6P.js} +1 -0
- package/dist/node/cli/index.cjs +1553 -839
- package/dist/node/cli/index.js +39 -2
- package/dist/node/cloud/index.d.cts +1 -1
- package/dist/node/cloud/index.d.ts +1 -1
- package/dist/node/components/index.d.cts +2 -2
- package/dist/node/components/index.d.ts +2 -2
- package/dist/node/conversations-KQBXTP3N.js +596 -0
- package/dist/node/{engine-2DQBKBJC.js → engine-7CXQV6RC.js} +1 -1
- package/dist/node/index.cjs +408 -3
- package/dist/node/index.d.cts +308 -7
- package/dist/node/index.d.ts +308 -7
- package/dist/node/index.js +336 -6
- package/dist/node/integrations/svelte.cjs +70 -1
- package/dist/node/integrations/svelte.d.cts +3 -3
- package/dist/node/integrations/svelte.d.ts +3 -3
- package/dist/node/integrations/svelte.js +2 -2
- package/dist/node/{protocol-Qek7ebBl.d.ts → protocol-BocKczNv.d.cts} +1 -1
- package/dist/node/{protocol-Qek7ebBl.d.cts → protocol-BocKczNv.d.ts} +1 -1
- package/dist/node/{reactive-engine.svelte-CRNqHlbv.d.ts → reactive-engine.svelte-CGe8SpVE.d.cts} +57 -2
- package/dist/node/{reactive-engine.svelte-BFIZfawz.d.cts → reactive-engine.svelte-D-xTDxT5.d.ts} +57 -2
- package/dist/node/{terminal-adapter-B-UK_Vdz.d.ts → terminal-adapter-CvIvgTo4.d.ts} +1 -1
- package/dist/node/{terminal-adapter-BQSIF5bf.d.cts → terminal-adapter-Db-snPJ3.d.cts} +1 -1
- package/dist/node/{validate-CNHUULQE.js → validate-EN3M4FUR.js} +1 -1
- package/dist/node/{verify-KLJRXVJS.js → verify-7VZRP2WS.js} +2 -2
- package/docs/BOT_UPDATE_POLICY.md +125 -0
- package/docs/DOGFOODING_CHECKLIST.md +254 -0
- package/docs/DOGFOODING_INDEX.md +169 -0
- package/docs/DOGFOODING_QUICK_START.md +140 -0
- package/docs/KNO_ENG_EXTRACTION_PLAN.md +577 -0
- package/docs/PLURES_TOOLS_INVENTORY.md +170 -0
- package/docs/README.md +12 -0
- package/docs/TESTING_BOT_WORKFLOWS.md +154 -0
- package/docs/conversations/INTEGRATION_POINTS.md +719 -0
- package/docs/conversations/README.md +168 -0
- package/docs/core/extending-praxis-core.md +604 -0
- package/docs/core/praxis-core-api.md +385 -0
- package/docs/decision-ledger/contract-index.json +2 -2
- package/docs/decision-ledger/decisions/2026-02-01-monorepo-organization.md +130 -0
- package/docs/examples/DOGFOODING_WORKFLOW_EXAMPLE.md +295 -0
- package/docs/examples/README.md +41 -0
- package/docs/workflows/pr-overlap-guard.md +50 -0
- package/package.json +7 -2
- package/src/__tests__/chronicle.test.ts +512 -0
- package/src/__tests__/conversations.test.ts +312 -0
- package/src/__tests__/edge-cases.test.ts +1 -1
- package/src/__tests__/engine-dx.test.ts +355 -0
- package/src/cli/commands/conversations.ts +252 -0
- package/src/cli/index.ts +73 -0
- package/src/conversations/README.md +230 -0
- package/src/conversations/candidate.schema.json +123 -0
- package/src/conversations/candidates.ts +114 -0
- package/src/conversations/capture.ts +56 -0
- package/src/conversations/classify.ts +110 -0
- package/src/conversations/conversation.schema.json +106 -0
- package/src/conversations/emitters/fs.ts +65 -0
- package/src/conversations/emitters/github.ts +115 -0
- package/src/conversations/gate.ts +102 -0
- package/src/conversations/index.ts +28 -0
- package/src/conversations/normalize.ts +51 -0
- package/src/conversations/redact.ts +57 -0
- package/src/conversations/types.ts +96 -0
- package/src/core/chronicle/chronicle.ts +227 -0
- package/src/core/chronicle/context.ts +80 -0
- package/src/core/chronicle/index.ts +53 -0
- package/src/core/chronicle/mcp.ts +135 -0
- package/src/core/chronicle/types.ts +61 -0
- package/src/core/engine.ts +99 -1
- package/src/core/pluresdb/index.ts +22 -0
- package/src/core/pluresdb/store.ts +162 -5
- package/src/core/rules.ts +12 -0
- package/src/dsl/index.ts +6 -0
- package/src/index.ts +18 -0
- package/src/integrations/pluresdb.ts +22 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Praxis CLI - Conversations Commands
|
|
3
|
+
*
|
|
4
|
+
* Commands for the praxis-conversations subsystem
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import {
|
|
9
|
+
loadConversation,
|
|
10
|
+
captureConversation,
|
|
11
|
+
redactConversation,
|
|
12
|
+
normalizeConversation,
|
|
13
|
+
classifyConversation,
|
|
14
|
+
generateCandidate,
|
|
15
|
+
applyGates,
|
|
16
|
+
candidatePassed,
|
|
17
|
+
emitToFS,
|
|
18
|
+
emitToGitHub,
|
|
19
|
+
type Conversation,
|
|
20
|
+
type Candidate,
|
|
21
|
+
} from '../../conversations/index.js';
|
|
22
|
+
|
|
23
|
+
interface CaptureOptions {
|
|
24
|
+
input?: string;
|
|
25
|
+
output?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PushOptions {
|
|
29
|
+
input: string;
|
|
30
|
+
output?: string;
|
|
31
|
+
skipRedaction?: boolean;
|
|
32
|
+
skipNormalization?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ClassifyOptions {
|
|
36
|
+
input: string;
|
|
37
|
+
output?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface EmitOptions {
|
|
41
|
+
input: string;
|
|
42
|
+
emitter: 'fs' | 'github';
|
|
43
|
+
outputDir?: string;
|
|
44
|
+
owner?: string;
|
|
45
|
+
repo?: string;
|
|
46
|
+
token?: string;
|
|
47
|
+
dryRun?: boolean;
|
|
48
|
+
commitIntent?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Capture command: Capture a conversation from input
|
|
53
|
+
*/
|
|
54
|
+
export async function captureCommand(options: CaptureOptions): Promise<void> {
|
|
55
|
+
console.log('📝 Capturing conversation...');
|
|
56
|
+
|
|
57
|
+
let conversation: Conversation;
|
|
58
|
+
|
|
59
|
+
if (options.input) {
|
|
60
|
+
// Load from file
|
|
61
|
+
const content = await fs.readFile(options.input, 'utf-8');
|
|
62
|
+
conversation = loadConversation(content);
|
|
63
|
+
console.log(`✓ Loaded conversation from ${options.input}`);
|
|
64
|
+
} else {
|
|
65
|
+
// Create a sample conversation for demo
|
|
66
|
+
conversation = captureConversation({
|
|
67
|
+
turns: [
|
|
68
|
+
{ role: 'user', content: 'Hello, I have a question about the feature' },
|
|
69
|
+
{ role: 'assistant', content: 'Sure, I\'d be happy to help!' },
|
|
70
|
+
],
|
|
71
|
+
metadata: {
|
|
72
|
+
source: 'cli',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
console.log('✓ Created sample conversation');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.output) {
|
|
79
|
+
await fs.writeFile(
|
|
80
|
+
options.output,
|
|
81
|
+
JSON.stringify(conversation, null, 2),
|
|
82
|
+
'utf-8'
|
|
83
|
+
);
|
|
84
|
+
console.log(`✓ Saved to ${options.output}`);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(JSON.stringify(conversation, null, 2));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Push command: Process conversation through pipeline (capture -> redact -> normalize)
|
|
92
|
+
*/
|
|
93
|
+
export async function pushCommand(options: PushOptions): Promise<void> {
|
|
94
|
+
console.log('🔄 Processing conversation through pipeline...');
|
|
95
|
+
|
|
96
|
+
// Load conversation
|
|
97
|
+
const content = await fs.readFile(options.input, 'utf-8');
|
|
98
|
+
let conversation = loadConversation(content);
|
|
99
|
+
console.log(`✓ Loaded conversation ${conversation.id}`);
|
|
100
|
+
|
|
101
|
+
// Redact
|
|
102
|
+
if (!options.skipRedaction) {
|
|
103
|
+
conversation = redactConversation(conversation);
|
|
104
|
+
console.log('✓ Redacted PII');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Normalize
|
|
108
|
+
if (!options.skipNormalization) {
|
|
109
|
+
conversation = normalizeConversation(conversation);
|
|
110
|
+
console.log('✓ Normalized content');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Save result
|
|
114
|
+
if (options.output) {
|
|
115
|
+
await fs.writeFile(
|
|
116
|
+
options.output,
|
|
117
|
+
JSON.stringify(conversation, null, 2),
|
|
118
|
+
'utf-8'
|
|
119
|
+
);
|
|
120
|
+
console.log(`✓ Saved to ${options.output}`);
|
|
121
|
+
} else {
|
|
122
|
+
console.log(JSON.stringify(conversation, null, 2));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Classify command: Classify a conversation and generate candidate
|
|
128
|
+
*/
|
|
129
|
+
export async function classifyCommand(options: ClassifyOptions): Promise<void> {
|
|
130
|
+
console.log('🏷️ Classifying conversation...');
|
|
131
|
+
|
|
132
|
+
// Load conversation
|
|
133
|
+
const content = await fs.readFile(options.input, 'utf-8');
|
|
134
|
+
let conversation = loadConversation(content);
|
|
135
|
+
console.log(`✓ Loaded conversation ${conversation.id}`);
|
|
136
|
+
|
|
137
|
+
// Ensure conversation is processed
|
|
138
|
+
if (!conversation.redacted) {
|
|
139
|
+
conversation = redactConversation(conversation);
|
|
140
|
+
console.log('✓ Applied redaction');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!conversation.normalized) {
|
|
144
|
+
conversation = normalizeConversation(conversation);
|
|
145
|
+
console.log('✓ Applied normalization');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Classify
|
|
149
|
+
conversation = classifyConversation(conversation);
|
|
150
|
+
console.log(`✓ Classified as: ${conversation.classification?.category} (confidence: ${conversation.classification?.confidence?.toFixed(2)})`);
|
|
151
|
+
|
|
152
|
+
// Generate candidate
|
|
153
|
+
const candidate = generateCandidate(conversation);
|
|
154
|
+
if (!candidate) {
|
|
155
|
+
console.error('✗ Failed to generate candidate');
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log(`✓ Generated candidate: ${candidate.title}`);
|
|
160
|
+
|
|
161
|
+
// Apply gates
|
|
162
|
+
const gatedCandidate = applyGates(candidate);
|
|
163
|
+
console.log(`\n📋 Gate Results:`);
|
|
164
|
+
for (const gate of gatedCandidate.gateStatus?.gates || []) {
|
|
165
|
+
const icon = gate.passed ? '✓' : '✗';
|
|
166
|
+
console.log(` ${icon} ${gate.name}: ${gate.message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const passed = candidatePassed(gatedCandidate);
|
|
170
|
+
console.log(`\n${passed ? '✓ All gates passed' : '✗ Some gates failed'}`);
|
|
171
|
+
|
|
172
|
+
// Save result
|
|
173
|
+
if (options.output) {
|
|
174
|
+
await fs.writeFile(
|
|
175
|
+
options.output,
|
|
176
|
+
JSON.stringify(gatedCandidate, null, 2),
|
|
177
|
+
'utf-8'
|
|
178
|
+
);
|
|
179
|
+
console.log(`✓ Saved candidate to ${options.output}`);
|
|
180
|
+
} else {
|
|
181
|
+
console.log('\n' + JSON.stringify(gatedCandidate, null, 2));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Emit command: Emit a candidate to a destination
|
|
187
|
+
*/
|
|
188
|
+
export async function emitCommand(options: EmitOptions): Promise<void> {
|
|
189
|
+
console.log('📤 Emitting candidate...');
|
|
190
|
+
|
|
191
|
+
// Load candidate
|
|
192
|
+
const content = await fs.readFile(options.input, 'utf-8');
|
|
193
|
+
const candidate = JSON.parse(content) as Candidate;
|
|
194
|
+
console.log(`✓ Loaded candidate ${candidate.id}`);
|
|
195
|
+
|
|
196
|
+
// Check gates
|
|
197
|
+
if (!candidatePassed(candidate)) {
|
|
198
|
+
console.error('✗ Candidate did not pass gates, refusing to emit');
|
|
199
|
+
console.error(` Reason: ${candidate.gateStatus?.reason}`);
|
|
200
|
+
process.exit(1);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let result: Candidate;
|
|
204
|
+
|
|
205
|
+
if (options.emitter === 'fs') {
|
|
206
|
+
const outputDir = options.outputDir || './output/candidates';
|
|
207
|
+
result = await emitToFS(candidate, {
|
|
208
|
+
outputDir,
|
|
209
|
+
dryRun: options.dryRun,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (result.emissionResult?.success) {
|
|
213
|
+
console.log(`✓ Emitted to filesystem: ${result.emissionResult.externalId}`);
|
|
214
|
+
} else {
|
|
215
|
+
console.error(`✗ Emission failed: ${result.emissionResult?.error}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
} else if (options.emitter === 'github') {
|
|
219
|
+
if (!options.owner || !options.repo) {
|
|
220
|
+
console.error('✗ GitHub emitter requires --owner and --repo');
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// CRITICAL: Display commit_intent gate status
|
|
225
|
+
if (!options.commitIntent) {
|
|
226
|
+
console.error('');
|
|
227
|
+
console.error('⛔ GATE BLOCKED: commit_intent=false');
|
|
228
|
+
console.error('');
|
|
229
|
+
console.error('The GitHub emitter is HARD GATED by the --commit-intent flag.');
|
|
230
|
+
console.error('This prevents accidental issue creation.');
|
|
231
|
+
console.error('');
|
|
232
|
+
console.error('To emit to GitHub, add: --commit-intent');
|
|
233
|
+
console.error('');
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
result = await emitToGitHub(candidate, {
|
|
238
|
+
owner: options.owner,
|
|
239
|
+
repo: options.repo,
|
|
240
|
+
token: options.token,
|
|
241
|
+
dryRun: options.dryRun,
|
|
242
|
+
commitIntent: options.commitIntent,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (result.emissionResult?.success) {
|
|
246
|
+
console.log(`✓ Emitted to GitHub: ${result.emissionResult.externalId}`);
|
|
247
|
+
} else {
|
|
248
|
+
console.error(`✗ Emission failed: ${result.emissionResult?.error}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -301,4 +301,77 @@ program
|
|
|
301
301
|
}
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
// Conversations commands (praxis-conversations subsystem)
|
|
305
|
+
const conversationsCmd = program
|
|
306
|
+
.command('conversations')
|
|
307
|
+
.description('Conversation ingestion subsystem (capture -> redact -> normalize -> classify -> emit)');
|
|
308
|
+
|
|
309
|
+
conversationsCmd
|
|
310
|
+
.command('capture')
|
|
311
|
+
.description('Capture a conversation from input')
|
|
312
|
+
.option('-i, --input <file>', 'Input conversation file')
|
|
313
|
+
.option('-o, --output <file>', 'Output file for captured conversation')
|
|
314
|
+
.action(async (options) => {
|
|
315
|
+
try {
|
|
316
|
+
const { captureCommand } = await import('./commands/conversations.js');
|
|
317
|
+
await captureCommand(options);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('Error capturing conversation:', error);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
conversationsCmd
|
|
325
|
+
.command('push')
|
|
326
|
+
.description('Process conversation through pipeline (redact -> normalize)')
|
|
327
|
+
.requiredOption('-i, --input <file>', 'Input conversation file')
|
|
328
|
+
.option('-o, --output <file>', 'Output file for processed conversation')
|
|
329
|
+
.option('--skip-redaction', 'Skip PII redaction', false)
|
|
330
|
+
.option('--skip-normalization', 'Skip normalization', false)
|
|
331
|
+
.action(async (options) => {
|
|
332
|
+
try {
|
|
333
|
+
const { pushCommand } = await import('./commands/conversations.js');
|
|
334
|
+
await pushCommand(options);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Error processing conversation:', error);
|
|
337
|
+
process.exit(1);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
conversationsCmd
|
|
342
|
+
.command('classify')
|
|
343
|
+
.description('Classify conversation and generate candidate')
|
|
344
|
+
.requiredOption('-i, --input <file>', 'Input conversation file')
|
|
345
|
+
.option('-o, --output <file>', 'Output file for candidate')
|
|
346
|
+
.action(async (options) => {
|
|
347
|
+
try {
|
|
348
|
+
const { classifyCommand } = await import('./commands/conversations.js');
|
|
349
|
+
await classifyCommand(options);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('Error classifying conversation:', error);
|
|
352
|
+
process.exit(1);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
conversationsCmd
|
|
357
|
+
.command('emit')
|
|
358
|
+
.description('Emit candidate to destination (fs or github)')
|
|
359
|
+
.requiredOption('-i, --input <file>', 'Input candidate file')
|
|
360
|
+
.requiredOption('-e, --emitter <type>', 'Emitter type (fs, github)')
|
|
361
|
+
.option('--output-dir <dir>', 'Output directory (for fs emitter)', './output/candidates')
|
|
362
|
+
.option('--owner <owner>', 'GitHub repository owner (for github emitter)')
|
|
363
|
+
.option('--repo <repo>', 'GitHub repository name (for github emitter)')
|
|
364
|
+
.option('--token <token>', 'GitHub token (for github emitter)')
|
|
365
|
+
.option('--dry-run', 'Dry run mode (no actual emission)', false)
|
|
366
|
+
.option('--commit-intent', 'REQUIRED: Explicit commit intent for GitHub emission', false)
|
|
367
|
+
.action(async (options) => {
|
|
368
|
+
try {
|
|
369
|
+
const { emitCommand } = await import('./commands/conversations.js');
|
|
370
|
+
await emitCommand(options);
|
|
371
|
+
} catch (error) {
|
|
372
|
+
console.error('Error emitting candidate:', error);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
304
377
|
program.parse();
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Praxis Conversations Subsystem
|
|
2
|
+
|
|
3
|
+
The praxis-conversations subsystem provides a deterministic-first conversation ingestion pipeline for capturing, processing, and emitting conversation data.
|
|
4
|
+
|
|
5
|
+
## Pipeline
|
|
6
|
+
|
|
7
|
+
The subsystem follows a deterministic pipeline:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
capture → redact → normalize → classify → candidates → gate → emit
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Pipeline Stages
|
|
14
|
+
|
|
15
|
+
1. **Capture**: Capture conversations from various sources (CLI, API, files)
|
|
16
|
+
2. **Redact**: Remove PII (email, phone, IP addresses) using deterministic patterns
|
|
17
|
+
3. **Normalize**: Normalize whitespace, code blocks, and formatting
|
|
18
|
+
4. **Classify**: Classify conversations using keyword-based rules (no LLM)
|
|
19
|
+
5. **Candidates**: Generate emission candidates (GitHub issues, docs, etc.)
|
|
20
|
+
6. **Gate**: Apply quality gates before emission
|
|
21
|
+
7. **Emit**: Emit to destinations (filesystem, GitHub)
|
|
22
|
+
|
|
23
|
+
## Schemas
|
|
24
|
+
|
|
25
|
+
### Conversation Schema
|
|
26
|
+
|
|
27
|
+
See `conversation.schema.json` for the full schema.
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"id": "uuid",
|
|
32
|
+
"timestamp": "ISO 8601",
|
|
33
|
+
"turns": [
|
|
34
|
+
{
|
|
35
|
+
"role": "user|assistant|system",
|
|
36
|
+
"content": "text",
|
|
37
|
+
"timestamp": "ISO 8601"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"metadata": {
|
|
41
|
+
"source": "github-copilot|cli|web",
|
|
42
|
+
"userId": "string",
|
|
43
|
+
"sessionId": "string",
|
|
44
|
+
"tags": ["tag1", "tag2"]
|
|
45
|
+
},
|
|
46
|
+
"redacted": false,
|
|
47
|
+
"normalized": false,
|
|
48
|
+
"classified": false,
|
|
49
|
+
"classification": {
|
|
50
|
+
"category": "bug-report|feature-request|question|...",
|
|
51
|
+
"confidence": 0.85,
|
|
52
|
+
"tags": ["tag1", "tag2"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Candidate Schema
|
|
58
|
+
|
|
59
|
+
See `candidate.schema.json` for the full schema.
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"id": "uuid",
|
|
64
|
+
"conversationId": "uuid",
|
|
65
|
+
"type": "github-issue|github-pr|documentation|...",
|
|
66
|
+
"title": "Short title",
|
|
67
|
+
"body": "Full body content",
|
|
68
|
+
"metadata": {
|
|
69
|
+
"priority": "low|medium|high|critical",
|
|
70
|
+
"labels": ["bug", "priority:high"],
|
|
71
|
+
"assignees": ["username"]
|
|
72
|
+
},
|
|
73
|
+
"gateStatus": {
|
|
74
|
+
"passed": true,
|
|
75
|
+
"gates": [...]
|
|
76
|
+
},
|
|
77
|
+
"emitted": false
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## CLI Usage
|
|
82
|
+
|
|
83
|
+
### Capture a Conversation
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Capture from file
|
|
87
|
+
praxis conversations capture -i conversation.json -o captured.json
|
|
88
|
+
|
|
89
|
+
# Create sample conversation
|
|
90
|
+
praxis conversations capture -o sample.json
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Process Through Pipeline
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
# Redact and normalize
|
|
97
|
+
praxis conversations push -i conversation.json -o processed.json
|
|
98
|
+
|
|
99
|
+
# Skip redaction
|
|
100
|
+
praxis conversations push -i conversation.json --skip-redaction
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Classify and Generate Candidate
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Classify conversation and generate candidate
|
|
107
|
+
praxis conversations classify -i conversation.json -o candidate.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Emit to Destination
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Emit to filesystem
|
|
114
|
+
praxis conversations emit -i candidate.json -e fs --output-dir ./output
|
|
115
|
+
|
|
116
|
+
# Emit to GitHub (dry run)
|
|
117
|
+
praxis conversations emit -i candidate.json -e github \
|
|
118
|
+
--owner myorg --repo myrepo --dry-run
|
|
119
|
+
|
|
120
|
+
# Emit to GitHub (REQUIRES --commit-intent)
|
|
121
|
+
praxis conversations emit -i candidate.json -e github \
|
|
122
|
+
--owner myorg --repo myrepo --token $GITHUB_TOKEN --commit-intent
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## GitHub Emitter Gate
|
|
126
|
+
|
|
127
|
+
**CRITICAL**: The GitHub emitter is **HARD GATED** by the `--commit-intent` flag.
|
|
128
|
+
|
|
129
|
+
This prevents accidental issue creation. You **MUST** explicitly pass `--commit-intent` to emit to GitHub:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
praxis conversations emit -i candidate.json -e github \
|
|
133
|
+
--owner myorg --repo myrepo --commit-intent
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Without `--commit-intent`, the command will fail with:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
⛔ GATE BLOCKED: commit_intent=false
|
|
140
|
+
|
|
141
|
+
The GitHub emitter is HARD GATED by the --commit-intent flag.
|
|
142
|
+
This prevents accidental issue creation.
|
|
143
|
+
|
|
144
|
+
To emit to GitHub, add: --commit-intent
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Programmatic Usage
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import {
|
|
151
|
+
captureConversation,
|
|
152
|
+
redactConversation,
|
|
153
|
+
normalizeConversation,
|
|
154
|
+
classifyConversation,
|
|
155
|
+
generateCandidate,
|
|
156
|
+
applyGates,
|
|
157
|
+
candidatePassed,
|
|
158
|
+
emitToFS,
|
|
159
|
+
emitToGitHub,
|
|
160
|
+
} from '@plures/praxis/conversations';
|
|
161
|
+
|
|
162
|
+
// Capture
|
|
163
|
+
const conversation = captureConversation({
|
|
164
|
+
turns: [
|
|
165
|
+
{ role: 'user', content: 'I found a bug...' },
|
|
166
|
+
{ role: 'assistant', content: 'Thanks for reporting!' },
|
|
167
|
+
],
|
|
168
|
+
metadata: { source: 'cli' },
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Process
|
|
172
|
+
let processed = redactConversation(conversation);
|
|
173
|
+
processed = normalizeConversation(processed);
|
|
174
|
+
processed = classifyConversation(processed);
|
|
175
|
+
|
|
176
|
+
// Generate candidate
|
|
177
|
+
const candidate = generateCandidate(processed);
|
|
178
|
+
|
|
179
|
+
// Gate
|
|
180
|
+
const gated = applyGates(candidate);
|
|
181
|
+
if (candidatePassed(gated)) {
|
|
182
|
+
// Emit
|
|
183
|
+
const result = await emitToFS(gated, {
|
|
184
|
+
outputDir: './output/candidates',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
console.log('Emitted:', result.emissionResult?.externalId);
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## Classification Categories
|
|
192
|
+
|
|
193
|
+
The classifier uses deterministic keyword matching to identify:
|
|
194
|
+
|
|
195
|
+
- `bug-report`: bugs, errors, crashes
|
|
196
|
+
- `feature-request`: features, enhancements
|
|
197
|
+
- `question`: questions, help requests
|
|
198
|
+
- `documentation`: docs, guides, examples
|
|
199
|
+
- `performance`: performance, optimization
|
|
200
|
+
|
|
201
|
+
## Quality Gates
|
|
202
|
+
|
|
203
|
+
Before emission, candidates pass through these gates:
|
|
204
|
+
|
|
205
|
+
1. **minimum-length**: Content must be >= 50 characters
|
|
206
|
+
2. **valid-title**: Title must be 10-200 characters
|
|
207
|
+
3. **not-duplicate**: Not a duplicate (stub, would check existing issues)
|
|
208
|
+
4. **has-metadata**: Must have labels and priority
|
|
209
|
+
|
|
210
|
+
## Testing
|
|
211
|
+
|
|
212
|
+
Run tests:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npm test src/__tests__/conversations.test.ts
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Test fixtures are in `test/fixtures/conversations/`.
|
|
219
|
+
|
|
220
|
+
## Examples
|
|
221
|
+
|
|
222
|
+
See `test/fixtures/conversations/` for example conversations:
|
|
223
|
+
|
|
224
|
+
- `bug-report.json`: Bug report conversation
|
|
225
|
+
- `feature-request.json`: Feature request conversation
|
|
226
|
+
- `question.json`: Question conversation
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://praxis.dev/schemas/candidate.json",
|
|
4
|
+
"title": "Candidate",
|
|
5
|
+
"description": "Schema for a conversation candidate for emission (GitHub issue, documentation, etc.)",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["id", "conversationId", "type", "title", "body", "metadata"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Unique identifier for the candidate"
|
|
12
|
+
},
|
|
13
|
+
"conversationId": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Reference to the source conversation ID"
|
|
16
|
+
},
|
|
17
|
+
"type": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"enum": ["github-issue", "github-pr", "documentation", "feature-request", "bug-report"],
|
|
20
|
+
"description": "Type of candidate to emit"
|
|
21
|
+
},
|
|
22
|
+
"title": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "Title or subject for the candidate"
|
|
25
|
+
},
|
|
26
|
+
"body": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"description": "Main content body for the candidate"
|
|
29
|
+
},
|
|
30
|
+
"metadata": {
|
|
31
|
+
"type": "object",
|
|
32
|
+
"description": "Candidate metadata",
|
|
33
|
+
"properties": {
|
|
34
|
+
"priority": {
|
|
35
|
+
"type": "string",
|
|
36
|
+
"enum": ["low", "medium", "high", "critical"],
|
|
37
|
+
"description": "Priority level"
|
|
38
|
+
},
|
|
39
|
+
"labels": {
|
|
40
|
+
"type": "array",
|
|
41
|
+
"items": { "type": "string" },
|
|
42
|
+
"description": "Labels to apply (for GitHub issues/PRs)"
|
|
43
|
+
},
|
|
44
|
+
"assignees": {
|
|
45
|
+
"type": "array",
|
|
46
|
+
"items": { "type": "string" },
|
|
47
|
+
"description": "Assignees (for GitHub issues/PRs)"
|
|
48
|
+
},
|
|
49
|
+
"source": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"description": "Source information",
|
|
52
|
+
"properties": {
|
|
53
|
+
"conversationId": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
},
|
|
56
|
+
"timestamp": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"format": "date-time"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"gateStatus": {
|
|
65
|
+
"type": "object",
|
|
66
|
+
"description": "Gating decision results",
|
|
67
|
+
"properties": {
|
|
68
|
+
"passed": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"description": "Whether the candidate passed gating"
|
|
71
|
+
},
|
|
72
|
+
"reason": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Reason for gating decision"
|
|
75
|
+
},
|
|
76
|
+
"gates": {
|
|
77
|
+
"type": "array",
|
|
78
|
+
"items": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"name": {
|
|
82
|
+
"type": "string"
|
|
83
|
+
},
|
|
84
|
+
"passed": {
|
|
85
|
+
"type": "boolean"
|
|
86
|
+
},
|
|
87
|
+
"message": {
|
|
88
|
+
"type": "string"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"description": "Individual gate results"
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"emitted": {
|
|
97
|
+
"type": "boolean",
|
|
98
|
+
"description": "Whether the candidate has been emitted",
|
|
99
|
+
"default": false
|
|
100
|
+
},
|
|
101
|
+
"emissionResult": {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"description": "Result of emission attempt",
|
|
104
|
+
"properties": {
|
|
105
|
+
"success": {
|
|
106
|
+
"type": "boolean"
|
|
107
|
+
},
|
|
108
|
+
"timestamp": {
|
|
109
|
+
"type": "string",
|
|
110
|
+
"format": "date-time"
|
|
111
|
+
},
|
|
112
|
+
"externalId": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"description": "External ID (e.g., GitHub issue number)"
|
|
115
|
+
},
|
|
116
|
+
"error": {
|
|
117
|
+
"type": "string",
|
|
118
|
+
"description": "Error message if emission failed"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|