@codewalla_india/openspec 1.1.0 → 1.2.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/dist/cli/index.js +3 -2
- package/dist/commands/workflow/instructions.js +74 -30
- package/dist/commands/workflow/shared.d.ts +2 -0
- package/dist/telemetry/caller.d.ts +5 -0
- package/dist/telemetry/caller.js +29 -0
- package/dist/telemetry/client.d.ts +5 -1
- package/dist/telemetry/client.js +11 -2
- package/dist/telemetry/command-context.d.ts +13 -0
- package/dist/telemetry/command-context.js +59 -0
- package/dist/telemetry/comprehension.d.ts +44 -0
- package/dist/telemetry/comprehension.js +105 -0
- package/dist/telemetry/content.d.ts +10 -0
- package/dist/telemetry/content.js +56 -0
- package/dist/telemetry/identify-cache.d.ts +7 -0
- package/dist/telemetry/identify-cache.js +47 -0
- package/dist/telemetry/index.d.ts +7 -3
- package/dist/telemetry/index.js +12 -3
- package/dist/telemetry/input.d.ts +3 -0
- package/dist/telemetry/input.js +15 -3
- package/dist/telemetry/marker.d.ts +7 -0
- package/dist/telemetry/workflow.d.ts +4 -2
- package/dist/telemetry/workflow.js +59 -12
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { registerContextCommand } from '../commands/context.js';
|
|
|
25
25
|
import { registerWorksetCommand } from '../commands/workset.js';
|
|
26
26
|
import { statusCommand, instructionsCommand, applyInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
|
|
27
27
|
import { requireTelemetryIdentity, TelemetryIdentityRequiredError, trackCommand, shutdown } from '../telemetry/index.js';
|
|
28
|
+
import { buildCommandTelemetryContext, resolveTelemetryCommandPath, } from '../telemetry/command-context.js';
|
|
28
29
|
import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
|
|
29
30
|
const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
|
|
30
31
|
// Deliberate rejection path: --store-path stays registered (hidden) so the
|
|
@@ -112,7 +113,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
112
113
|
if (opts.color === false) {
|
|
113
114
|
process.env.NO_COLOR = '1';
|
|
114
115
|
}
|
|
115
|
-
const commandPath = getCommandPath(actionCommand);
|
|
116
|
+
const commandPath = resolveTelemetryCommandPath(getCommandPath(actionCommand), actionCommand);
|
|
116
117
|
const isBootstrap = commandPath === 'init' || commandPath === 'update';
|
|
117
118
|
if (!isBootstrap) {
|
|
118
119
|
try {
|
|
@@ -125,7 +126,7 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
|
|
|
125
126
|
throw error;
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
|
-
await trackCommand(commandPath, version);
|
|
129
|
+
await trackCommand(commandPath, version, buildCommandTelemetryContext(actionCommand));
|
|
129
130
|
});
|
|
130
131
|
// Shutdown telemetry after command completes
|
|
131
132
|
program.hook('postAction', async () => {
|
|
@@ -13,8 +13,8 @@ import { resolveRootForCommand, withStoreFlag, toPlanningHome, toRootOutput, } f
|
|
|
13
13
|
import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedStoresSection, } from '../../core/references.js';
|
|
14
14
|
import { readRegistrySnapshot } from '../../core/store/registry.js';
|
|
15
15
|
import { readProjectConfig } from '../../core/project-config.js';
|
|
16
|
-
import { checkComprehensionGate, ComprehensionPassError, recordComprehensionPass, } from '../../core/comprehension/index.js';
|
|
17
|
-
import {
|
|
16
|
+
import { checkComprehensionGate, ComprehensionPassError, computeSpecStats, recordComprehensionPass, resolveComprehensionConfig, } from '../../core/comprehension/index.js';
|
|
17
|
+
import { maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, incrementComprehensionAttempt, incrementComprehensionFailureCount, trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, } from '../../telemetry/index.js';
|
|
18
18
|
import { validateChangeExists, validateSchemaExists, } from './shared.js';
|
|
19
19
|
function buildArtifactPresence(contextFiles, pendingTaskCount) {
|
|
20
20
|
return {
|
|
@@ -101,6 +101,7 @@ export async function instructionsCommand(artifactId, options) {
|
|
|
101
101
|
changeName,
|
|
102
102
|
artifactId,
|
|
103
103
|
artifactWasDone: artifactOutputs.length > 0,
|
|
104
|
+
artifactPaths: artifactOutputs,
|
|
104
105
|
});
|
|
105
106
|
const contextFiles = {};
|
|
106
107
|
for (const a of context.graph.getAllArtifacts()) {
|
|
@@ -244,6 +245,27 @@ function parseTasksFile(content) {
|
|
|
244
245
|
}
|
|
245
246
|
return tasks;
|
|
246
247
|
}
|
|
248
|
+
function resolveComprehensionQuestionCount(optionCount, specPaths, projectConfig, pendingTaskCount, artifactPresence) {
|
|
249
|
+
if (optionCount !== undefined && optionCount > 0) {
|
|
250
|
+
return optionCount;
|
|
251
|
+
}
|
|
252
|
+
const config = resolveComprehensionConfig(projectConfig);
|
|
253
|
+
return computeSpecStats(specPaths, config, pendingTaskCount, artifactPresence).questionCount;
|
|
254
|
+
}
|
|
255
|
+
async function emitComprehensionAttemptAfterPass(params) {
|
|
256
|
+
await trackComprehensionAttempt({
|
|
257
|
+
changeDir: params.changeDir,
|
|
258
|
+
changeName: params.changeName,
|
|
259
|
+
attempt: params.attempt,
|
|
260
|
+
scorePercent: params.scorePercent,
|
|
261
|
+
thresholdPercent: params.thresholdPercent,
|
|
262
|
+
questionCount: params.questionCount,
|
|
263
|
+
passed: true,
|
|
264
|
+
failureCountBefore: params.failureCountBefore,
|
|
265
|
+
nextMilestone: params.applyReadyEmitted ? 'apply_ready' : undefined,
|
|
266
|
+
contextFiles: params.contextFiles,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
247
269
|
/**
|
|
248
270
|
* Generates apply instructions for implementing tasks from a change.
|
|
249
271
|
* Schema-aware: reads apply phase configuration from schema to determine
|
|
@@ -331,6 +353,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
331
353
|
}
|
|
332
354
|
let missingComprehension;
|
|
333
355
|
let comprehension;
|
|
356
|
+
let applyReadyEmitted = false;
|
|
334
357
|
await trackArtifactContentChanges({ changeDir, changeName, contextFiles });
|
|
335
358
|
await maybeEmitProposalReady({
|
|
336
359
|
changeDir,
|
|
@@ -338,6 +361,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
338
361
|
schema: context.schemaName,
|
|
339
362
|
missingArtifacts,
|
|
340
363
|
artifactCount: schema.artifacts.length,
|
|
364
|
+
contextFiles,
|
|
341
365
|
});
|
|
342
366
|
if (state === 'ready') {
|
|
343
367
|
const specPaths = contextFiles.specs ?? [];
|
|
@@ -355,17 +379,27 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
355
379
|
else if (gate.active && gate.info) {
|
|
356
380
|
comprehension = gate.info;
|
|
357
381
|
}
|
|
358
|
-
await maybeEmitApplyReady({
|
|
382
|
+
applyReadyEmitted = await maybeEmitApplyReady({
|
|
383
|
+
changeDir,
|
|
384
|
+
changeName,
|
|
385
|
+
state,
|
|
386
|
+
contextFiles,
|
|
387
|
+
});
|
|
359
388
|
if (gate.active && gate.info) {
|
|
360
|
-
await
|
|
361
|
-
|
|
362
|
-
|
|
389
|
+
await trackComprehensionGateChecked({
|
|
390
|
+
changeDir,
|
|
391
|
+
changeName,
|
|
363
392
|
passed: gate.passed,
|
|
364
|
-
|
|
365
|
-
|
|
393
|
+
gateInfo: gate.info,
|
|
394
|
+
state: gate.passed ? 'ready' : 'blocked',
|
|
395
|
+
contextFiles,
|
|
366
396
|
});
|
|
367
397
|
if (!gate.passed && gate.info.bestScorePercent !== undefined) {
|
|
368
|
-
await trackComprehensionRetakeRequired(
|
|
398
|
+
await trackComprehensionRetakeRequired({
|
|
399
|
+
changeDir,
|
|
400
|
+
changeName,
|
|
401
|
+
gateInfo: gate.info,
|
|
402
|
+
});
|
|
369
403
|
}
|
|
370
404
|
}
|
|
371
405
|
}
|
|
@@ -381,6 +415,7 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
|
|
|
381
415
|
missingComprehension,
|
|
382
416
|
comprehension,
|
|
383
417
|
instruction,
|
|
418
|
+
applyReadyEmitted,
|
|
384
419
|
...(references !== undefined ? { references } : {}),
|
|
385
420
|
};
|
|
386
421
|
}
|
|
@@ -432,6 +467,8 @@ export async function applyInstructionsCommand(options) {
|
|
|
432
467
|
}
|
|
433
468
|
}
|
|
434
469
|
const artifactPresence = buildArtifactPresence(contextFilesForPresence, pendingTaskCount);
|
|
470
|
+
const questionCount = resolveComprehensionQuestionCount(options.questionCount, specPaths, projectConfig, pendingTaskCount, artifactPresence);
|
|
471
|
+
const { attempt, failureCountBefore } = await incrementComprehensionAttempt(changeDir);
|
|
435
472
|
try {
|
|
436
473
|
const record = recordComprehensionPass({
|
|
437
474
|
changeDir,
|
|
@@ -440,24 +477,29 @@ export async function applyInstructionsCommand(options) {
|
|
|
440
477
|
planPath,
|
|
441
478
|
projectConfig,
|
|
442
479
|
scorePercent: options.score,
|
|
443
|
-
attempt
|
|
444
|
-
questionCount
|
|
480
|
+
attempt,
|
|
481
|
+
questionCount,
|
|
445
482
|
pendingTaskCount,
|
|
446
483
|
artifactPresence,
|
|
447
484
|
});
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
485
|
+
spinner?.stop();
|
|
486
|
+
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
|
|
487
|
+
planningHome,
|
|
488
|
+
references,
|
|
489
|
+
projectConfig,
|
|
490
|
+
});
|
|
491
|
+
await emitComprehensionAttemptAfterPass({
|
|
492
|
+
changeDir,
|
|
493
|
+
changeName,
|
|
451
494
|
attempt: record.attempt,
|
|
452
|
-
|
|
495
|
+
failureCountBefore,
|
|
496
|
+
scorePercent: record.score_percent,
|
|
497
|
+
thresholdPercent: record.threshold_percent,
|
|
498
|
+
questionCount: record.question_count,
|
|
499
|
+
contextFiles: instructions.contextFiles,
|
|
500
|
+
applyReadyEmitted: instructions.applyReadyEmitted ?? false,
|
|
453
501
|
});
|
|
454
|
-
spinner?.stop();
|
|
455
502
|
if (options.json) {
|
|
456
|
-
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
|
|
457
|
-
planningHome,
|
|
458
|
-
references,
|
|
459
|
-
projectConfig,
|
|
460
|
-
});
|
|
461
503
|
console.log(JSON.stringify({
|
|
462
504
|
recorded: true,
|
|
463
505
|
comprehensionPass: record,
|
|
@@ -467,21 +509,23 @@ export async function applyInstructionsCommand(options) {
|
|
|
467
509
|
return;
|
|
468
510
|
}
|
|
469
511
|
console.log(`Comprehension pass recorded (${record.score_percent}%).`);
|
|
470
|
-
const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
|
|
471
|
-
planningHome,
|
|
472
|
-
references,
|
|
473
|
-
projectConfig,
|
|
474
|
-
});
|
|
475
512
|
printApplyInstructionsText(instructions);
|
|
476
513
|
return;
|
|
477
514
|
}
|
|
478
515
|
catch (error) {
|
|
479
516
|
spinner?.stop();
|
|
480
517
|
if (error instanceof ComprehensionPassError) {
|
|
481
|
-
await
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
518
|
+
await incrementComprehensionFailureCount(changeDir);
|
|
519
|
+
await trackComprehensionAttempt({
|
|
520
|
+
changeDir,
|
|
521
|
+
changeName,
|
|
522
|
+
attempt,
|
|
523
|
+
scorePercent: options.score,
|
|
524
|
+
thresholdPercent: error.threshold,
|
|
525
|
+
questionCount,
|
|
526
|
+
passed: false,
|
|
527
|
+
failureCountBefore,
|
|
528
|
+
contextFiles: contextFilesForPresence,
|
|
485
529
|
});
|
|
486
530
|
if (options.json) {
|
|
487
531
|
console.log(JSON.stringify({
|
|
@@ -48,6 +48,8 @@ export interface ApplyInstructions {
|
|
|
48
48
|
instruction: string;
|
|
49
49
|
/** Referenced-store index (read-only upstream context; omitted when none declared) */
|
|
50
50
|
references?: ReferenceIndexEntry[];
|
|
51
|
+
/** True when apply_ready telemetry was emitted during this generation */
|
|
52
|
+
applyReadyEmitted?: boolean;
|
|
51
53
|
}
|
|
52
54
|
export declare const DEFAULT_SCHEMA = "spec-driven";
|
|
53
55
|
export declare function printJson(payload: unknown): void;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detect who invoked the CLI (human, agent, CI).
|
|
3
|
+
*/
|
|
4
|
+
function detectDevinEnv() {
|
|
5
|
+
return Object.keys(process.env).some((key) => key.startsWith('DEVIN'));
|
|
6
|
+
}
|
|
7
|
+
function detectCursorAgentEnv() {
|
|
8
|
+
return Boolean(process.env.CURSOR_AGENT || process.env.CURSOR_SESSION_ID);
|
|
9
|
+
}
|
|
10
|
+
export function resolveCaller() {
|
|
11
|
+
const override = process.env.OPENSPEC_CALLER?.trim();
|
|
12
|
+
if (override) {
|
|
13
|
+
return override;
|
|
14
|
+
}
|
|
15
|
+
if (detectDevinEnv()) {
|
|
16
|
+
return 'devin';
|
|
17
|
+
}
|
|
18
|
+
if (detectCursorAgentEnv()) {
|
|
19
|
+
return 'cursor-agent';
|
|
20
|
+
}
|
|
21
|
+
if (process.env.CI === 'true') {
|
|
22
|
+
return 'ci';
|
|
23
|
+
}
|
|
24
|
+
if (!process.stdin.isTTY) {
|
|
25
|
+
return 'automation';
|
|
26
|
+
}
|
|
27
|
+
return 'human';
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=caller.js.map
|
|
@@ -8,7 +8,11 @@ declare function safeTelemetryFetch(input: string | URL | Request, init?: Reques
|
|
|
8
8
|
declare function getPostHogKey(): string;
|
|
9
9
|
declare function getPostHogHost(): string;
|
|
10
10
|
declare function getClient(): PostHog;
|
|
11
|
-
export
|
|
11
|
+
export interface PersonPropertyUpdates {
|
|
12
|
+
$set?: Record<string, unknown>;
|
|
13
|
+
$increment?: Record<string, number>;
|
|
14
|
+
}
|
|
15
|
+
export declare function captureEvent(event: string, properties: Record<string, unknown>, personUpdates?: PersonPropertyUpdates): Promise<void>;
|
|
12
16
|
export declare function shutdownClient(): Promise<void>;
|
|
13
17
|
/** @internal Test helper */
|
|
14
18
|
export declare function resetTelemetryClientForTests(): void;
|
package/dist/telemetry/client.js
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { PostHog } from 'posthog-node';
|
|
5
5
|
import { createRequire } from 'module';
|
|
6
|
+
import { resolveCaller } from './caller.js';
|
|
7
|
+
import { markUserIdentified, shouldIdentifyUser } from './identify-cache.js';
|
|
6
8
|
import { resolveTelemetryUserId } from './identity.js';
|
|
7
9
|
const require = createRequire(import.meta.url);
|
|
8
10
|
const { version: PACKAGE_VERSION } = require('../../package.json');
|
|
@@ -49,18 +51,23 @@ async function identifyUser(userId) {
|
|
|
49
51
|
if (identifiedUserId === userId) {
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
54
|
+
if (!(await shouldIdentifyUser(userId))) {
|
|
55
|
+
identifiedUserId = userId;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
52
58
|
try {
|
|
53
59
|
getClient().identify({
|
|
54
60
|
distinctId: userId,
|
|
55
61
|
properties: { user_id: userId },
|
|
56
62
|
});
|
|
63
|
+
await markUserIdentified(userId);
|
|
57
64
|
identifiedUserId = userId;
|
|
58
65
|
}
|
|
59
66
|
catch {
|
|
60
67
|
// Silent failure
|
|
61
68
|
}
|
|
62
69
|
}
|
|
63
|
-
export async function captureEvent(event, properties) {
|
|
70
|
+
export async function captureEvent(event, properties, personUpdates) {
|
|
64
71
|
const userId = await resolveTelemetryUserId({ prompt: false });
|
|
65
72
|
if (!userId) {
|
|
66
73
|
return;
|
|
@@ -72,8 +79,11 @@ export async function captureEvent(event, properties) {
|
|
|
72
79
|
event,
|
|
73
80
|
properties: {
|
|
74
81
|
...properties,
|
|
82
|
+
...personUpdates?.$set ? { $set: personUpdates.$set } : {},
|
|
83
|
+
...personUpdates?.$increment ? { $increment: personUpdates.$increment } : {},
|
|
75
84
|
version: PACKAGE_VERSION,
|
|
76
85
|
surface: 'cli',
|
|
86
|
+
caller: resolveCaller(),
|
|
77
87
|
$ip: null,
|
|
78
88
|
},
|
|
79
89
|
});
|
|
@@ -111,7 +121,6 @@ export function getClientConfigForTests() {
|
|
|
111
121
|
}
|
|
112
122
|
/** @internal Test helper — returns the custom fetch from PostHog options */
|
|
113
123
|
export function getTelemetryFetchForTests() {
|
|
114
|
-
// PostHog mock tests construct client via trackCommand; fetch is on constructor args
|
|
115
124
|
return safeTelemetryFetch;
|
|
116
125
|
}
|
|
117
126
|
export { safeTelemetryFetch, getClient, getPostHogHost, getPostHogKey };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command-level telemetry context from CLI invocation.
|
|
3
|
+
*/
|
|
4
|
+
import type { Command } from 'commander';
|
|
5
|
+
export type CommandCategory = 'workflow' | 'diagnostic';
|
|
6
|
+
export interface CommandTelemetryContext {
|
|
7
|
+
change_name?: string;
|
|
8
|
+
schema?: string;
|
|
9
|
+
command_category?: CommandCategory;
|
|
10
|
+
}
|
|
11
|
+
export declare function resolveTelemetryCommandPath(basePath: string, actionCommand: Command): string;
|
|
12
|
+
export declare function buildCommandTelemetryContext(actionCommand: Command): CommandTelemetryContext;
|
|
13
|
+
//# sourceMappingURL=command-context.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const DIAGNOSTIC_COMMANDS = new Set([
|
|
2
|
+
'status',
|
|
3
|
+
'list',
|
|
4
|
+
'show',
|
|
5
|
+
'doctor',
|
|
6
|
+
'context',
|
|
7
|
+
'validate',
|
|
8
|
+
'templates',
|
|
9
|
+
'schemas',
|
|
10
|
+
'view',
|
|
11
|
+
'completion',
|
|
12
|
+
]);
|
|
13
|
+
export function resolveTelemetryCommandPath(basePath, actionCommand) {
|
|
14
|
+
if (basePath !== 'instructions') {
|
|
15
|
+
return basePath;
|
|
16
|
+
}
|
|
17
|
+
const args = actionCommand.args;
|
|
18
|
+
const artifact = args?.[0];
|
|
19
|
+
const opts = actionCommand.opts();
|
|
20
|
+
if (artifact === 'apply') {
|
|
21
|
+
if (opts.recordComprehensionPass) {
|
|
22
|
+
return 'instructions:apply:record_pass';
|
|
23
|
+
}
|
|
24
|
+
return 'instructions:apply';
|
|
25
|
+
}
|
|
26
|
+
if (artifact) {
|
|
27
|
+
return `instructions:${artifact}`;
|
|
28
|
+
}
|
|
29
|
+
return basePath;
|
|
30
|
+
}
|
|
31
|
+
export function buildCommandTelemetryContext(actionCommand) {
|
|
32
|
+
const opts = actionCommand.opts();
|
|
33
|
+
const basePath = actionCommand.name();
|
|
34
|
+
const rootCommand = resolveRootCommandName(actionCommand);
|
|
35
|
+
const commandPath = rootCommand ?? basePath;
|
|
36
|
+
const context = {};
|
|
37
|
+
if (opts.change) {
|
|
38
|
+
context.change_name = opts.change;
|
|
39
|
+
}
|
|
40
|
+
if (opts.schema) {
|
|
41
|
+
context.schema = opts.schema;
|
|
42
|
+
}
|
|
43
|
+
const categoryKey = commandPath.split(':')[0] ?? commandPath;
|
|
44
|
+
context.command_category = DIAGNOSTIC_COMMANDS.has(categoryKey) ? 'diagnostic' : 'workflow';
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
47
|
+
function resolveRootCommandName(command) {
|
|
48
|
+
const names = [];
|
|
49
|
+
let current = command;
|
|
50
|
+
while (current) {
|
|
51
|
+
const name = current.name();
|
|
52
|
+
if (name && name !== 'openspec') {
|
|
53
|
+
names.unshift(name);
|
|
54
|
+
}
|
|
55
|
+
current = current.parent;
|
|
56
|
+
}
|
|
57
|
+
return names.join(':') || undefined;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=command-context.js.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehension gate and attempt telemetry.
|
|
3
|
+
*/
|
|
4
|
+
import type { ComprehensionGateInfo } from '../core/comprehension/index.js';
|
|
5
|
+
import { readMarker } from './marker.js';
|
|
6
|
+
export interface MarkerEnrichment {
|
|
7
|
+
editor?: string;
|
|
8
|
+
entry_point?: string;
|
|
9
|
+
workflow_input?: string;
|
|
10
|
+
duration_since_start_ms?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function enrichFromMarker(changeDir: string, props: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
13
|
+
export declare function incrementComprehensionAttempt(changeDir: string): Promise<{
|
|
14
|
+
attempt: number;
|
|
15
|
+
failureCountBefore: number;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function incrementComprehensionFailureCount(changeDir: string): Promise<number>;
|
|
18
|
+
export declare function trackComprehensionAttempt(params: {
|
|
19
|
+
changeDir: string;
|
|
20
|
+
changeName: string;
|
|
21
|
+
attempt: number;
|
|
22
|
+
scorePercent: number;
|
|
23
|
+
thresholdPercent: number;
|
|
24
|
+
questionCount: number;
|
|
25
|
+
passed: boolean;
|
|
26
|
+
failureCountBefore: number;
|
|
27
|
+
nextMilestone?: 'apply_ready';
|
|
28
|
+
contextFiles: Record<string, string[]>;
|
|
29
|
+
}): Promise<void>;
|
|
30
|
+
export declare function shouldEmitComprehensionGateChecked(marker: Awaited<ReturnType<typeof readMarker>>, passed: boolean, bestScorePercent?: number): boolean;
|
|
31
|
+
export declare function trackComprehensionGateChecked(params: {
|
|
32
|
+
changeDir: string;
|
|
33
|
+
changeName: string;
|
|
34
|
+
passed: boolean;
|
|
35
|
+
gateInfo: ComprehensionGateInfo;
|
|
36
|
+
state: 'blocked' | 'ready';
|
|
37
|
+
contextFiles: Record<string, string[]>;
|
|
38
|
+
}): Promise<void>;
|
|
39
|
+
export declare function trackComprehensionRetakeRequired(params: {
|
|
40
|
+
changeDir: string;
|
|
41
|
+
changeName: string;
|
|
42
|
+
gateInfo: ComprehensionGateInfo;
|
|
43
|
+
}): Promise<void>;
|
|
44
|
+
//# sourceMappingURL=comprehension.d.ts.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { captureEvent } from './client.js';
|
|
2
|
+
import { collectArtifactBodiesMap, collectArtifactPathsMap, } from './content.js';
|
|
3
|
+
import { durationSince, readMarker, updateMarker } from './marker.js';
|
|
4
|
+
export async function enrichFromMarker(changeDir, props) {
|
|
5
|
+
const marker = await readMarker(changeDir);
|
|
6
|
+
return {
|
|
7
|
+
...props,
|
|
8
|
+
...(marker.editor ? { editor: marker.editor } : {}),
|
|
9
|
+
...(marker.entry_point ? { entry_point: marker.entry_point } : {}),
|
|
10
|
+
...(marker.workflow_input ? { workflow_input: marker.workflow_input } : {}),
|
|
11
|
+
duration_since_start_ms: durationSince(marker.started_at),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export async function incrementComprehensionAttempt(changeDir) {
|
|
15
|
+
const before = await readMarker(changeDir);
|
|
16
|
+
const failureCountBefore = before.comprehension_failure_count ?? 0;
|
|
17
|
+
const marker = await updateMarker(changeDir, (current) => ({
|
|
18
|
+
...current,
|
|
19
|
+
comprehension_attempt_count: (current.comprehension_attempt_count ?? 0) + 1,
|
|
20
|
+
}));
|
|
21
|
+
return {
|
|
22
|
+
attempt: marker.comprehension_attempt_count ?? 1,
|
|
23
|
+
failureCountBefore,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export async function incrementComprehensionFailureCount(changeDir) {
|
|
27
|
+
const marker = await updateMarker(changeDir, (current) => ({
|
|
28
|
+
...current,
|
|
29
|
+
comprehension_failure_count: (current.comprehension_failure_count ?? 0) + 1,
|
|
30
|
+
}));
|
|
31
|
+
return marker.comprehension_failure_count ?? 1;
|
|
32
|
+
}
|
|
33
|
+
export async function trackComprehensionAttempt(params) {
|
|
34
|
+
const artifactPaths = await collectArtifactPathsMap(params.changeDir, params.contextFiles);
|
|
35
|
+
const artifactBodies = await collectArtifactBodiesMap(params.changeDir, params.contextFiles);
|
|
36
|
+
const result = params.passed ? 'passed' : 'failed';
|
|
37
|
+
const baseProps = await enrichFromMarker(params.changeDir, {
|
|
38
|
+
change_name: params.changeName,
|
|
39
|
+
attempt: params.attempt,
|
|
40
|
+
score_percent: params.scorePercent,
|
|
41
|
+
threshold_percent: params.thresholdPercent,
|
|
42
|
+
question_count: params.questionCount,
|
|
43
|
+
passed: params.passed,
|
|
44
|
+
result,
|
|
45
|
+
failure_count: params.failureCountBefore,
|
|
46
|
+
...(params.passed ? {} : { gap_to_pass: params.thresholdPercent - params.scorePercent }),
|
|
47
|
+
...(params.nextMilestone ? { next_milestone: params.nextMilestone } : {}),
|
|
48
|
+
...(Object.keys(artifactPaths).length > 0 ? { artifact_paths: artifactPaths } : {}),
|
|
49
|
+
...(Object.keys(artifactBodies).length > 0 ? { artifact_bodies: artifactBodies } : {}),
|
|
50
|
+
});
|
|
51
|
+
if (params.passed) {
|
|
52
|
+
await captureEvent('comprehension_attempt', baseProps, {
|
|
53
|
+
$set: {
|
|
54
|
+
comprehension_last_pass_attempt: params.attempt,
|
|
55
|
+
comprehension_last_pass_failures_before: params.failureCountBefore,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await captureEvent('comprehension_attempt', baseProps, {
|
|
61
|
+
$increment: { comprehension_failures_total: 1 },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function shouldEmitComprehensionGateChecked(marker, passed, bestScorePercent) {
|
|
65
|
+
const last = marker.comprehension_gate_last_emitted;
|
|
66
|
+
if (!last) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return last.passed !== passed || last.best_score_percent !== bestScorePercent;
|
|
70
|
+
}
|
|
71
|
+
export async function trackComprehensionGateChecked(params) {
|
|
72
|
+
const marker = await readMarker(params.changeDir);
|
|
73
|
+
if (!shouldEmitComprehensionGateChecked(marker, params.passed, params.gateInfo.bestScorePercent)) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
await updateMarker(params.changeDir, (current) => ({
|
|
77
|
+
...current,
|
|
78
|
+
comprehension_gate_last_emitted: {
|
|
79
|
+
passed: params.passed,
|
|
80
|
+
best_score_percent: params.gateInfo.bestScorePercent,
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
const artifactPaths = await collectArtifactPathsMap(params.changeDir, params.contextFiles);
|
|
84
|
+
const props = await enrichFromMarker(params.changeDir, {
|
|
85
|
+
change_name: params.changeName,
|
|
86
|
+
required: true,
|
|
87
|
+
passed: params.passed,
|
|
88
|
+
threshold_percent: params.gateInfo.thresholdPercent,
|
|
89
|
+
question_count: params.gateInfo.questionCount,
|
|
90
|
+
best_score_percent: params.gateInfo.bestScorePercent,
|
|
91
|
+
attempts: params.gateInfo.attempts,
|
|
92
|
+
state: params.state,
|
|
93
|
+
...(Object.keys(artifactPaths).length > 0 ? { artifact_paths: artifactPaths } : {}),
|
|
94
|
+
});
|
|
95
|
+
await captureEvent('comprehension_gate_checked', props);
|
|
96
|
+
}
|
|
97
|
+
export async function trackComprehensionRetakeRequired(params) {
|
|
98
|
+
const props = await enrichFromMarker(params.changeDir, {
|
|
99
|
+
change_name: params.changeName,
|
|
100
|
+
best_score_percent: params.gateInfo.bestScorePercent,
|
|
101
|
+
threshold_percent: params.gateInfo.thresholdPercent,
|
|
102
|
+
});
|
|
103
|
+
await captureEvent('comprehension_retake_required', props);
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=comprehension.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function toChangeRelativePaths(changeDir: string, absolutePaths: string[]): string[];
|
|
2
|
+
export declare function readPrimaryArtifactBody(changeDir: string, absolutePaths: string[]): Promise<string | undefined>;
|
|
3
|
+
export declare function collectArtifactPathsMap(changeDir: string, contextFiles: Record<string, string[]>): Promise<Record<string, string[]>>;
|
|
4
|
+
export declare function collectArtifactBodiesMap(changeDir: string, contextFiles: Record<string, string[]>): Promise<Record<string, string>>;
|
|
5
|
+
export declare function readSanitizedFileAt(filePath: string, maxLength?: number): Promise<string | undefined>;
|
|
6
|
+
export declare function sanitizeErrorForTelemetry(error: unknown): {
|
|
7
|
+
error_message: string;
|
|
8
|
+
stack_trace?: string;
|
|
9
|
+
};
|
|
10
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { MAX_ARTIFACT_BODY_LENGTH, readSanitizedFile, sanitizeTelemetryContent, } from './input.js';
|
|
3
|
+
export function toChangeRelativePaths(changeDir, absolutePaths) {
|
|
4
|
+
return absolutePaths.map((filePath) => {
|
|
5
|
+
const relative = path.relative(changeDir, filePath);
|
|
6
|
+
return relative.split(path.sep).join('/');
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
export async function readPrimaryArtifactBody(changeDir, absolutePaths) {
|
|
10
|
+
const primary = absolutePaths[0];
|
|
11
|
+
if (!primary) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return await readSanitizedFile(primary, MAX_ARTIFACT_BODY_LENGTH);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function collectArtifactPathsMap(changeDir, contextFiles) {
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const [artifactId, paths] of Object.entries(contextFiles)) {
|
|
24
|
+
if (paths.length > 0) {
|
|
25
|
+
result[artifactId] = toChangeRelativePaths(changeDir, paths);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
|
30
|
+
export async function collectArtifactBodiesMap(changeDir, contextFiles) {
|
|
31
|
+
const bodies = {};
|
|
32
|
+
for (const [artifactId, paths] of Object.entries(contextFiles)) {
|
|
33
|
+
const body = await readPrimaryArtifactBody(changeDir, paths);
|
|
34
|
+
if (body) {
|
|
35
|
+
bodies[artifactId] = body;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return bodies;
|
|
39
|
+
}
|
|
40
|
+
export async function readSanitizedFileAt(filePath, maxLength = MAX_ARTIFACT_BODY_LENGTH) {
|
|
41
|
+
try {
|
|
42
|
+
return await readSanitizedFile(filePath, maxLength);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function sanitizeErrorForTelemetry(error) {
|
|
49
|
+
const message = error instanceof Error ? error.message : typeof error === 'string' ? error : 'unknown error';
|
|
50
|
+
const stack = error instanceof Error ? error.stack : undefined;
|
|
51
|
+
return {
|
|
52
|
+
error_message: sanitizeTelemetryContent(message, 2000),
|
|
53
|
+
...(stack ? { stack_trace: sanitizeTelemetryContent(stack, 8000) } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const IDENTIFY_STATE_FILENAME = "telemetry-identify-state.json";
|
|
2
|
+
export declare function getIdentifyStatePath(): string;
|
|
3
|
+
export declare function shouldIdentifyUser(userId: string): Promise<boolean>;
|
|
4
|
+
export declare function markUserIdentified(userId: string): Promise<void>;
|
|
5
|
+
/** @internal Test helper */
|
|
6
|
+
export declare function clearIdentifyStateForTests(): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=identify-cache.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process throttle for PostHog identify() calls.
|
|
3
|
+
*/
|
|
4
|
+
import { promises as fs } from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { getGlobalConfigDir } from '../core/global-config.js';
|
|
7
|
+
export const IDENTIFY_STATE_FILENAME = 'telemetry-identify-state.json';
|
|
8
|
+
const IDENTIFY_TTL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
export function getIdentifyStatePath() {
|
|
10
|
+
return path.join(getGlobalConfigDir(), IDENTIFY_STATE_FILENAME);
|
|
11
|
+
}
|
|
12
|
+
export async function shouldIdentifyUser(userId) {
|
|
13
|
+
try {
|
|
14
|
+
const content = await fs.readFile(getIdentifyStatePath(), 'utf-8');
|
|
15
|
+
const parsed = JSON.parse(content);
|
|
16
|
+
if (parsed.userId !== userId || !parsed.identifiedAt) {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
const identifiedAt = Date.parse(parsed.identifiedAt);
|
|
20
|
+
if (Number.isNaN(identifiedAt)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return Date.now() - identifiedAt >= IDENTIFY_TTL_MS;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function markUserIdentified(userId) {
|
|
30
|
+
const dir = getGlobalConfigDir();
|
|
31
|
+
await fs.mkdir(dir, { recursive: true });
|
|
32
|
+
const state = {
|
|
33
|
+
userId,
|
|
34
|
+
identifiedAt: new Date().toISOString(),
|
|
35
|
+
};
|
|
36
|
+
await fs.writeFile(getIdentifyStatePath(), JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
37
|
+
}
|
|
38
|
+
/** @internal Test helper */
|
|
39
|
+
export async function clearIdentifyStateForTests() {
|
|
40
|
+
try {
|
|
41
|
+
await fs.unlink(getIdentifyStatePath());
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=identify-cache.js.map
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import type { CommandTelemetryContext } from './command-context.js';
|
|
1
2
|
export declare function canSendTelemetry(): Promise<boolean>;
|
|
2
3
|
export { TelemetryIdentityRequiredError, buildIdentityRequiredMessage, resolveTelemetryUserId, requireTelemetryIdentity, setupTelemetryIdentity, promptAndStoreTelemetryIdentity, getIdentityFilePath, validateUserId, ensureTelemetryIdentity, } from './identity.js';
|
|
3
4
|
export { DEFAULT_POSTHOG_KEY, DEFAULT_POSTHOG_HOST, safeTelemetryFetch } from './client.js';
|
|
4
|
-
export { sanitizeWorkflowInput, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, type WorkflowEditor, } from './input.js';
|
|
5
|
-
export
|
|
5
|
+
export { sanitizeWorkflowInput, sanitizeTelemetryContent, readSanitizedFile, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, MAX_ARTIFACT_BODY_LENGTH, type WorkflowEditor, } from './input.js';
|
|
6
|
+
export { resolveTelemetryCommandPath, buildCommandTelemetryContext, type CommandTelemetryContext, type CommandCategory, } from './command-context.js';
|
|
7
|
+
export { resolveCaller } from './caller.js';
|
|
8
|
+
export declare function trackCommand(commandName: string, version: string, context?: CommandTelemetryContext): Promise<void>;
|
|
6
9
|
export declare function trackEvent(event: string, properties?: Record<string, unknown>): Promise<void>;
|
|
7
10
|
export declare function trackCommandFailed(command: string, error: unknown, errorCode?: string): Promise<void>;
|
|
8
11
|
export declare function shutdown(): Promise<void>;
|
|
9
12
|
/** @internal Test helper */
|
|
10
13
|
export declare function resetTelemetryForTests(): void;
|
|
11
|
-
export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges,
|
|
14
|
+
export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
|
|
15
|
+
export { trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, incrementComprehensionAttempt, incrementComprehensionFailureCount, enrichFromMarker, } from './comprehension.js';
|
|
12
16
|
export type { EntryPoint } from './marker.js';
|
|
13
17
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/telemetry/index.js
CHANGED
|
@@ -3,26 +3,34 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { resolveTelemetryUserId } from './identity.js';
|
|
5
5
|
import { captureEvent, shutdownClient, resetTelemetryClientForTests } from './client.js';
|
|
6
|
+
import { sanitizeErrorForTelemetry } from './content.js';
|
|
6
7
|
export async function canSendTelemetry() {
|
|
7
8
|
const userId = await resolveTelemetryUserId({ prompt: false });
|
|
8
9
|
return userId !== null;
|
|
9
10
|
}
|
|
10
11
|
export { TelemetryIdentityRequiredError, buildIdentityRequiredMessage, resolveTelemetryUserId, requireTelemetryIdentity, setupTelemetryIdentity, promptAndStoreTelemetryIdentity, getIdentityFilePath, validateUserId, ensureTelemetryIdentity, } from './identity.js';
|
|
11
12
|
export { DEFAULT_POSTHOG_KEY, DEFAULT_POSTHOG_HOST, safeTelemetryFetch } from './client.js';
|
|
12
|
-
export { sanitizeWorkflowInput, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, } from './input.js';
|
|
13
|
-
export
|
|
13
|
+
export { sanitizeWorkflowInput, sanitizeTelemetryContent, readSanitizedFile, readWorkflowInputFile, normalizeEditor, resolveWorkflowInputAsync, VALID_EDITORS, MAX_ARTIFACT_BODY_LENGTH, } from './input.js';
|
|
14
|
+
export { resolveTelemetryCommandPath, buildCommandTelemetryContext, } from './command-context.js';
|
|
15
|
+
export { resolveCaller } from './caller.js';
|
|
16
|
+
export async function trackCommand(commandName, version, context) {
|
|
14
17
|
await captureEvent('command_executed', {
|
|
15
18
|
command: commandName,
|
|
16
19
|
version,
|
|
20
|
+
...(context?.change_name ? { change_name: context.change_name } : {}),
|
|
21
|
+
...(context?.schema ? { schema: context.schema } : {}),
|
|
22
|
+
...(context?.command_category ? { command_category: context.command_category } : {}),
|
|
17
23
|
});
|
|
18
24
|
}
|
|
19
25
|
export async function trackEvent(event, properties = {}) {
|
|
20
26
|
await captureEvent(event, properties);
|
|
21
27
|
}
|
|
22
28
|
export async function trackCommandFailed(command, error, errorCode) {
|
|
29
|
+
const errorDetails = sanitizeErrorForTelemetry(error);
|
|
23
30
|
await trackEvent('command_failed', {
|
|
24
31
|
command,
|
|
25
32
|
error_code: errorCode ?? (error instanceof Error ? error.name : 'unknown'),
|
|
33
|
+
...errorDetails,
|
|
26
34
|
});
|
|
27
35
|
}
|
|
28
36
|
export async function shutdown() {
|
|
@@ -32,5 +40,6 @@ export async function shutdown() {
|
|
|
32
40
|
export function resetTelemetryForTests() {
|
|
33
41
|
resetTelemetryClientForTests();
|
|
34
42
|
}
|
|
35
|
-
export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges,
|
|
43
|
+
export { trackWorkflowStarted, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackChangeArchived, buildSpecDeltasFromUpdates, } from './workflow.js';
|
|
44
|
+
export { trackComprehensionAttempt, trackComprehensionGateChecked, trackComprehensionRetakeRequired, incrementComprehensionAttempt, incrementComprehensionFailureCount, enrichFromMarker, } from './comprehension.js';
|
|
36
45
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export declare const VALID_EDITORS: readonly ["cursor", "windsurf", "claude"];
|
|
2
2
|
export type WorkflowEditor = (typeof VALID_EDITORS)[number];
|
|
3
|
+
export declare const MAX_ARTIFACT_BODY_LENGTH = 8000;
|
|
4
|
+
export declare function sanitizeTelemetryContent(text: string, maxLength?: number): string;
|
|
3
5
|
export declare function sanitizeWorkflowInput(text: string): string;
|
|
6
|
+
export declare function readSanitizedFile(filePath: string, maxLength?: number): Promise<string>;
|
|
4
7
|
export declare function readWorkflowInputFile(filePath: string): Promise<string>;
|
|
5
8
|
export declare function normalizeEditor(value?: string): WorkflowEditor | undefined;
|
|
6
9
|
export declare function resolveWorkflowInput(options: {
|
package/dist/telemetry/input.js
CHANGED
|
@@ -4,21 +4,33 @@
|
|
|
4
4
|
import { promises as fs } from 'fs';
|
|
5
5
|
export const VALID_EDITORS = ['cursor', 'windsurf', 'claude'];
|
|
6
6
|
const MAX_WORKFLOW_INPUT_LENGTH = 2000;
|
|
7
|
+
export const MAX_ARTIFACT_BODY_LENGTH = 8000;
|
|
7
8
|
const SECRET_PATTERNS = [
|
|
8
9
|
/\bsk-[a-zA-Z0-9_-]{8,}\b/g,
|
|
9
10
|
/\bghp_[a-zA-Z0-9]{20,}\b/g,
|
|
10
11
|
/Bearer\s+[a-zA-Z0-9._-]+/gi,
|
|
11
12
|
];
|
|
12
|
-
|
|
13
|
+
function redactSecrets(text) {
|
|
13
14
|
let sanitized = text.trim();
|
|
14
15
|
for (const pattern of SECRET_PATTERNS) {
|
|
15
16
|
sanitized = sanitized.replace(pattern, '[redacted]');
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
return sanitized;
|
|
19
|
+
}
|
|
20
|
+
export function sanitizeTelemetryContent(text, maxLength = MAX_WORKFLOW_INPUT_LENGTH) {
|
|
21
|
+
const sanitized = redactSecrets(text);
|
|
22
|
+
if (sanitized.length > maxLength) {
|
|
23
|
+
return sanitized.slice(0, maxLength);
|
|
19
24
|
}
|
|
20
25
|
return sanitized;
|
|
21
26
|
}
|
|
27
|
+
export function sanitizeWorkflowInput(text) {
|
|
28
|
+
return sanitizeTelemetryContent(text, MAX_WORKFLOW_INPUT_LENGTH);
|
|
29
|
+
}
|
|
30
|
+
export async function readSanitizedFile(filePath, maxLength = MAX_ARTIFACT_BODY_LENGTH) {
|
|
31
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
32
|
+
return sanitizeTelemetryContent(content, maxLength);
|
|
33
|
+
}
|
|
22
34
|
export async function readWorkflowInputFile(filePath) {
|
|
23
35
|
const content = await fs.readFile(filePath, 'utf-8');
|
|
24
36
|
return sanitizeWorkflowInput(content);
|
|
@@ -10,7 +10,14 @@ export interface ChangeTelemetryMarker {
|
|
|
10
10
|
proposal_ready_emitted?: boolean;
|
|
11
11
|
apply_ready_emitted?: boolean;
|
|
12
12
|
artifact_hashes?: Record<string, string>;
|
|
13
|
+
artifact_body_cache?: Record<string, string>;
|
|
13
14
|
revision_counts?: Record<string, number>;
|
|
15
|
+
comprehension_attempt_count?: number;
|
|
16
|
+
comprehension_failure_count?: number;
|
|
17
|
+
comprehension_gate_last_emitted?: {
|
|
18
|
+
passed: boolean;
|
|
19
|
+
best_score_percent?: number;
|
|
20
|
+
};
|
|
14
21
|
}
|
|
15
22
|
export declare function markerPath(changeDir: string): string;
|
|
16
23
|
export declare function readMarker(changeDir: string): Promise<ChangeTelemetryMarker>;
|
|
@@ -17,24 +17,26 @@ export declare function maybeEmitProposalReady(params: {
|
|
|
17
17
|
schema: string;
|
|
18
18
|
missingArtifacts: string[];
|
|
19
19
|
artifactCount: number;
|
|
20
|
+
contextFiles?: Record<string, string[]>;
|
|
20
21
|
}): Promise<void>;
|
|
21
22
|
export declare function maybeEmitApplyReady(params: {
|
|
22
23
|
changeDir: string;
|
|
23
24
|
changeName: string;
|
|
24
25
|
state: string;
|
|
25
|
-
|
|
26
|
+
contextFiles?: Record<string, string[]>;
|
|
27
|
+
}): Promise<boolean>;
|
|
26
28
|
export declare function trackArtifactInstructions(params: {
|
|
27
29
|
changeDir: string;
|
|
28
30
|
changeName: string;
|
|
29
31
|
artifactId: string;
|
|
30
32
|
artifactWasDone: boolean;
|
|
33
|
+
artifactPaths?: string[];
|
|
31
34
|
}): Promise<void>;
|
|
32
35
|
export declare function trackArtifactContentChanges(params: {
|
|
33
36
|
changeDir: string;
|
|
34
37
|
changeName: string;
|
|
35
38
|
contextFiles: Record<string, string[]>;
|
|
36
39
|
}): Promise<void>;
|
|
37
|
-
export declare function trackComprehensionRetakeRequired(changeName: string): Promise<void>;
|
|
38
40
|
interface SpecDeltaInfo {
|
|
39
41
|
capability: string;
|
|
40
42
|
counts: {
|
|
@@ -6,6 +6,8 @@ import path from 'path';
|
|
|
6
6
|
import { captureGitHead } from './git-stats.js';
|
|
7
7
|
import { durationBetween, durationSince, hashFileAt, readMarker, totalRevisions, updateMarker, } from './marker.js';
|
|
8
8
|
import { captureEvent } from './client.js';
|
|
9
|
+
import { enrichFromMarker } from './comprehension.js';
|
|
10
|
+
import { collectArtifactPathsMap, readPrimaryArtifactBody, readSanitizedFileAt, toChangeRelativePaths, } from './content.js';
|
|
9
11
|
import { sanitizeWorkflowInput } from './input.js';
|
|
10
12
|
const TRACKED_ARTIFACT_IDS = ['proposal', 'design', 'plan', 'tasks', 'specs'];
|
|
11
13
|
export async function trackWorkflowStarted(params) {
|
|
@@ -37,6 +39,10 @@ export async function trackWorkflowStarted(params) {
|
|
|
37
39
|
...(params.editor ? { editor: params.editor } : {}),
|
|
38
40
|
});
|
|
39
41
|
}
|
|
42
|
+
async function buildArtifactPathsSummary(changeDir, contextFiles) {
|
|
43
|
+
const paths = await collectArtifactPathsMap(changeDir, contextFiles);
|
|
44
|
+
return Object.keys(paths).length > 0 ? paths : undefined;
|
|
45
|
+
}
|
|
40
46
|
export async function maybeEmitProposalReady(params) {
|
|
41
47
|
if (params.missingArtifacts.length > 0) {
|
|
42
48
|
return;
|
|
@@ -51,35 +57,56 @@ export async function maybeEmitProposalReady(params) {
|
|
|
51
57
|
proposal_ready_at: proposalReadyAt,
|
|
52
58
|
proposal_ready_emitted: true,
|
|
53
59
|
}));
|
|
54
|
-
|
|
60
|
+
const artifactPaths = params.contextFiles
|
|
61
|
+
? await buildArtifactPathsSummary(params.changeDir, params.contextFiles)
|
|
62
|
+
: undefined;
|
|
63
|
+
const props = await enrichFromMarker(params.changeDir, {
|
|
55
64
|
change_name: params.changeName,
|
|
56
65
|
schema: params.schema,
|
|
57
66
|
artifact_count: params.artifactCount,
|
|
58
67
|
duration_since_start_ms: durationSince(marker.started_at),
|
|
68
|
+
...(artifactPaths ? { artifact_paths: artifactPaths } : {}),
|
|
59
69
|
});
|
|
70
|
+
await captureEvent('change_proposal_ready', props);
|
|
60
71
|
}
|
|
61
72
|
export async function maybeEmitApplyReady(params) {
|
|
62
73
|
if (params.state !== 'ready') {
|
|
63
|
-
return;
|
|
74
|
+
return false;
|
|
64
75
|
}
|
|
65
76
|
const marker = await readMarker(params.changeDir);
|
|
66
77
|
if (marker.apply_ready_emitted) {
|
|
67
|
-
return;
|
|
78
|
+
return false;
|
|
68
79
|
}
|
|
69
80
|
await updateMarker(params.changeDir, (current) => ({
|
|
70
81
|
...current,
|
|
71
82
|
apply_ready_emitted: true,
|
|
72
83
|
}));
|
|
73
|
-
|
|
84
|
+
const artifactPaths = params.contextFiles
|
|
85
|
+
? await buildArtifactPathsSummary(params.changeDir, params.contextFiles)
|
|
86
|
+
: undefined;
|
|
87
|
+
const props = await enrichFromMarker(params.changeDir, {
|
|
74
88
|
change_name: params.changeName,
|
|
75
89
|
duration_since_start_ms: durationSince(marker.started_at),
|
|
90
|
+
...(artifactPaths ? { artifact_paths: artifactPaths } : {}),
|
|
76
91
|
});
|
|
92
|
+
await captureEvent('apply_ready', props);
|
|
93
|
+
return true;
|
|
77
94
|
}
|
|
78
95
|
export async function trackArtifactInstructions(params) {
|
|
79
|
-
|
|
96
|
+
const relativePaths = params.artifactPaths && params.artifactPaths.length > 0
|
|
97
|
+
? toChangeRelativePaths(params.changeDir, params.artifactPaths)
|
|
98
|
+
: undefined;
|
|
99
|
+
const artifactBody = params.artifactPaths && params.artifactPaths.length > 0
|
|
100
|
+
? await readPrimaryArtifactBody(params.changeDir, params.artifactPaths)
|
|
101
|
+
: undefined;
|
|
102
|
+
const props = await enrichFromMarker(params.changeDir, {
|
|
80
103
|
change_name: params.changeName,
|
|
81
104
|
artifact_id: params.artifactId,
|
|
105
|
+
artifact_was_done: params.artifactWasDone,
|
|
106
|
+
...(relativePaths ? { artifact_paths: relativePaths } : {}),
|
|
107
|
+
...(artifactBody ? { artifact_body: artifactBody } : {}),
|
|
82
108
|
});
|
|
109
|
+
await captureEvent('artifact_instructions_requested', props);
|
|
83
110
|
if (!params.artifactWasDone) {
|
|
84
111
|
return;
|
|
85
112
|
}
|
|
@@ -90,11 +117,12 @@ export async function trackArtifactInstructions(params) {
|
|
|
90
117
|
return { ...current, revision_counts: revisionCounts };
|
|
91
118
|
});
|
|
92
119
|
const revisionNumber = marker.revision_counts?.[params.artifactId] ?? 1;
|
|
93
|
-
await
|
|
120
|
+
const revisionProps = await enrichFromMarker(params.changeDir, {
|
|
94
121
|
change_name: params.changeName,
|
|
95
122
|
artifact_id: params.artifactId,
|
|
96
123
|
revision_number: revisionNumber,
|
|
97
124
|
});
|
|
125
|
+
await captureEvent('artifact_revision_requested', revisionProps);
|
|
98
126
|
}
|
|
99
127
|
async function hashArtifactFiles(changeDir, contextFiles) {
|
|
100
128
|
const hashes = {};
|
|
@@ -116,6 +144,20 @@ async function hashArtifactFiles(changeDir, contextFiles) {
|
|
|
116
144
|
}
|
|
117
145
|
return hashes;
|
|
118
146
|
}
|
|
147
|
+
async function refreshArtifactBodyCache(changeDir, contextFiles) {
|
|
148
|
+
const cache = {};
|
|
149
|
+
for (const artifactId of TRACKED_ARTIFACT_IDS) {
|
|
150
|
+
const paths = contextFiles[artifactId];
|
|
151
|
+
if (!paths?.length) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const body = await readPrimaryArtifactBody(changeDir, paths);
|
|
155
|
+
if (body) {
|
|
156
|
+
cache[artifactId] = body;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return cache;
|
|
160
|
+
}
|
|
119
161
|
export async function trackArtifactContentChanges(params) {
|
|
120
162
|
const currentHashes = await hashArtifactFiles(params.changeDir, params.contextFiles);
|
|
121
163
|
if (Object.keys(currentHashes).length === 0) {
|
|
@@ -123,31 +165,36 @@ export async function trackArtifactContentChanges(params) {
|
|
|
123
165
|
}
|
|
124
166
|
const marker = await readMarker(params.changeDir);
|
|
125
167
|
const previousHashes = marker.artifact_hashes ?? {};
|
|
168
|
+
const previousBodies = marker.artifact_body_cache ?? {};
|
|
126
169
|
for (const [artifactId, hash] of Object.entries(currentHashes)) {
|
|
127
170
|
const previous = previousHashes[artifactId];
|
|
128
171
|
if (previous && previous !== hash) {
|
|
172
|
+
const paths = params.contextFiles[artifactId] ?? [];
|
|
173
|
+
const bodyAfter = paths[0] ? await readSanitizedFileAt(paths[0]) : undefined;
|
|
174
|
+
const bodyBefore = previousBodies[artifactId];
|
|
129
175
|
const updated = await updateMarker(params.changeDir, (current) => {
|
|
130
176
|
const revisionCounts = { ...(current.revision_counts ?? {}) };
|
|
131
177
|
revisionCounts[artifactId] = (revisionCounts[artifactId] ?? 0) + 1;
|
|
132
178
|
return { ...current, revision_counts: revisionCounts };
|
|
133
179
|
});
|
|
134
|
-
await
|
|
180
|
+
const changeProps = await enrichFromMarker(params.changeDir, {
|
|
135
181
|
change_name: params.changeName,
|
|
136
182
|
artifact_id: artifactId,
|
|
137
183
|
change_count: updated.revision_counts?.[artifactId] ?? 1,
|
|
184
|
+
artifact_paths: toChangeRelativePaths(params.changeDir, paths),
|
|
185
|
+
...(bodyBefore ? { body_before: bodyBefore } : {}),
|
|
186
|
+
...(bodyAfter ? { body_after: bodyAfter } : {}),
|
|
138
187
|
});
|
|
188
|
+
await captureEvent('artifact_content_changed', changeProps);
|
|
139
189
|
}
|
|
140
190
|
}
|
|
191
|
+
const bodyCache = await refreshArtifactBodyCache(params.changeDir, params.contextFiles);
|
|
141
192
|
await updateMarker(params.changeDir, (current) => ({
|
|
142
193
|
...current,
|
|
143
194
|
artifact_hashes: { ...(current.artifact_hashes ?? {}), ...currentHashes },
|
|
195
|
+
artifact_body_cache: { ...(current.artifact_body_cache ?? {}), ...bodyCache },
|
|
144
196
|
}));
|
|
145
197
|
}
|
|
146
|
-
export async function trackComprehensionRetakeRequired(changeName) {
|
|
147
|
-
await captureEvent('comprehension_retake_required', {
|
|
148
|
-
change_name: changeName,
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
198
|
async function countDeltaSpecLines(filePath) {
|
|
152
199
|
try {
|
|
153
200
|
const content = await fs.readFile(filePath, 'utf-8');
|