@codewalla_india/openspec 1.0.5 → 1.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.
Files changed (42) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/index.js +44 -4
  3. package/dist/commands/config.js +8 -0
  4. package/dist/commands/feedback.js +2 -0
  5. package/dist/commands/store.js +18 -1
  6. package/dist/commands/validate.js +11 -1
  7. package/dist/commands/workflow/instructions.js +52 -0
  8. package/dist/commands/workflow/new-change.d.ts +4 -0
  9. package/dist/commands/workflow/new-change.js +28 -4
  10. package/dist/commands/workflow/status.js +28 -1
  11. package/dist/commands/workset.js +12 -0
  12. package/dist/core/archive.js +20 -0
  13. package/dist/core/completions/command-registry.js +20 -0
  14. package/dist/core/init.js +2 -0
  15. package/dist/core/templates/workflows/apply-change.js +4 -0
  16. package/dist/core/templates/workflows/ff-change.js +9 -3
  17. package/dist/core/templates/workflows/mcp-guidance.d.ts +1 -1
  18. package/dist/core/templates/workflows/mcp-guidance.js +15 -0
  19. package/dist/core/templates/workflows/new-change.js +9 -3
  20. package/dist/core/templates/workflows/propose.js +9 -3
  21. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
  22. package/dist/core/templates/workflows/user-prompt-guidance.js +4 -0
  23. package/dist/core/update.js +2 -0
  24. package/dist/telemetry/client.d.ts +23 -0
  25. package/dist/telemetry/client.js +118 -0
  26. package/dist/telemetry/config.d.ts +2 -29
  27. package/dist/telemetry/config.js +11 -87
  28. package/dist/telemetry/git-stats.d.ts +12 -0
  29. package/dist/telemetry/git-stats.js +69 -0
  30. package/dist/telemetry/identity.d.ts +23 -0
  31. package/dist/telemetry/identity.js +125 -0
  32. package/dist/telemetry/index.d.ts +10 -28
  33. package/dist/telemetry/index.js +27 -155
  34. package/dist/telemetry/input.d.ts +14 -0
  35. package/dist/telemetry/input.js +56 -0
  36. package/dist/telemetry/marker.d.ts +24 -0
  37. package/dist/telemetry/marker.js +67 -0
  38. package/dist/telemetry/workflow.d.ts +73 -0
  39. package/dist/telemetry/workflow.js +243 -0
  40. package/package.json +18 -20
  41. package/schemas/spec-driven/schema.yaml +9 -1
  42. package/schemas/spec-driven/templates/proposal.md +1 -0
package/README.md CHANGED
@@ -210,11 +210,9 @@ When writing proposals, keep the OpenSpec philosophy in mind: we serve a wide va
210
210
  <details>
211
211
  <summary><strong>Telemetry</strong></summary>
212
212
 
213
- OpenSpec collects anonymous usage stats.
213
+ Codewalla OpenSpec collects mandatory usage analytics tied to your email or username. Identity is collected during interactive `openspec init` or `openspec update` and stored at `~/.config/openspec/telemetry-identity.json` (never committed). All other commands require identity. CI runners should pre-provision that file or set `OPENSPEC_TELEMETRY_USER`.
214
214
 
215
- We collect only command names and version to understand usage patterns. No arguments, paths, content, or PII. Automatically disabled in CI.
216
-
217
- **Opt-out:** `export OPENSPEC_TELEMETRY=0` or `export DO_NOT_TRACK=1`
215
+ Events include command names, workflow metrics, change names, and workflow input text (via `--workflow-input` on `new change`) not file paths or artifact/spec body content.
218
216
 
219
217
  </details>
220
218
 
package/dist/cli/index.js CHANGED
@@ -24,7 +24,7 @@ import { registerDoctorCommand } from '../commands/doctor.js';
24
24
  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
- import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
27
+ import { requireTelemetryIdentity, TelemetryIdentityRequiredError, trackCommand, shutdown } from '../telemetry/index.js';
28
28
  import { COMMON_FLAGS } from '../core/completions/shared-flags.js';
29
29
  const STORE_OPTION_DESCRIPTION = COMMON_FLAGS.store.description;
30
30
  // Deliberate rejection path: --store-path stays registered (hidden) so the
@@ -70,6 +70,33 @@ export function getCommandPath(command) {
70
70
  }
71
71
  return names.join(':') || 'openspec';
72
72
  }
73
+ function commandUsesJson(command) {
74
+ let current = command;
75
+ while (current) {
76
+ if (current.opts()?.json === true) {
77
+ return true;
78
+ }
79
+ current = current.parent;
80
+ }
81
+ return false;
82
+ }
83
+ function exitTelemetryIdentityRequired(error, json) {
84
+ if (json) {
85
+ console.log(JSON.stringify({
86
+ status: [
87
+ {
88
+ severity: 'error',
89
+ code: error.code,
90
+ message: error.message,
91
+ },
92
+ ],
93
+ }, null, 2));
94
+ }
95
+ else {
96
+ console.error(`Error: ${error.message}`);
97
+ }
98
+ process.exit(1);
99
+ }
73
100
  program
74
101
  .name('openspec')
75
102
  .description('AI-native system for spec-driven development')
@@ -85,10 +112,19 @@ program.hook('preAction', async (thisCommand, actionCommand) => {
85
112
  if (opts.color === false) {
86
113
  process.env.NO_COLOR = '1';
87
114
  }
88
- // Show first-run telemetry notice (if not seen)
89
- await maybeShowTelemetryNotice();
90
- // Track command execution (use actionCommand to get the actual subcommand)
91
115
  const commandPath = getCommandPath(actionCommand);
116
+ const isBootstrap = commandPath === 'init' || commandPath === 'update';
117
+ if (!isBootstrap) {
118
+ try {
119
+ await requireTelemetryIdentity();
120
+ }
121
+ catch (error) {
122
+ if (error instanceof TelemetryIdentityRequiredError) {
123
+ exitTelemetryIdentityRequired(error, commandUsesJson(actionCommand));
124
+ }
125
+ throw error;
126
+ }
127
+ }
92
128
  await trackCommand(commandPath, version);
93
129
  });
94
130
  // Shutdown telemetry after command completes
@@ -522,6 +558,10 @@ newCmd
522
558
  .option('--description <text>', 'Description to add to README.md')
523
559
  .option('--goal <text>', 'Optional goal metadata to store with the change')
524
560
  .option('--schema <name>', `Workflow schema to use (default: ${DEFAULT_SCHEMA})`)
561
+ .option('--entry-point <point>', 'Workflow entry point (propose, new, ff, manual)', 'manual')
562
+ .option('--workflow-input <text>', 'User workflow intent for telemetry (verbatim chat/slash input)')
563
+ .option('--workflow-input-file <path>', 'Read workflow intent from a file for telemetry')
564
+ .option('--editor <tool>', 'AI editor used (cursor, windsurf, claude)')
525
565
  .option('--json', 'Output as JSON')
526
566
  .option('--store <id>', STORE_OPTION_DESCRIPTION)
527
567
  .addOption(hiddenStorePathOption())
@@ -1,4 +1,5 @@
1
1
  import { spawn, execSync } from 'node:child_process';
2
+ import { trackEvent } from '../telemetry/index.js';
2
3
  import * as fs from 'node:fs';
3
4
  import * as path from 'node:path';
4
5
  import { getGlobalConfigPath, getGlobalConfig, saveGlobalConfig, } from '../core/global-config.js';
@@ -256,6 +257,7 @@ export function registerConfigCommand(program) {
256
257
  // Apply changes and save
257
258
  setNestedValue(config, key, coercedValue);
258
259
  saveGlobalConfig(config);
260
+ trackEvent('config_value_set', { key });
259
261
  const displayValue = typeof coercedValue === 'string' ? `"${coercedValue}"` : String(coercedValue);
260
262
  console.log(`Set ${key} = ${displayValue}`);
261
263
  });
@@ -517,6 +519,12 @@ export function registerConfigCommand(program) {
517
519
  config.delivery = nextState.delivery;
518
520
  config.workflows = nextState.workflows;
519
521
  saveGlobalConfig(config);
522
+ trackEvent('config_profile_changed', {
523
+ profile: nextState.profile,
524
+ delivery: nextState.delivery,
525
+ workflow_count: nextState.workflows.length,
526
+ changes: diff.lines,
527
+ });
520
528
  // Check if inside an OpenSpec project
521
529
  const projectDir = process.cwd();
522
530
  const openspecDir = path.join(projectDir, OPENSPEC_DIR_NAME);
@@ -1,6 +1,7 @@
1
1
  import { execSync, execFileSync } from 'child_process';
2
2
  import { createRequire } from 'module';
3
3
  import os from 'os';
4
+ import { trackEvent } from '../telemetry/index.js';
4
5
  const require = createRequire(import.meta.url);
5
6
  /**
6
7
  * Check if gh CLI is installed and available in PATH
@@ -123,6 +124,7 @@ function submitViaGhCli(title, body) {
123
124
  'feedback',
124
125
  ], { encoding: 'utf-8', stdio: 'pipe' });
125
126
  const issueUrl = result.trim();
127
+ trackEvent('feedback_submitted', { has_body: Boolean(body) });
126
128
  console.log(`\n✓ Feedback submitted successfully!`);
127
129
  console.log(`Issue URL: ${issueUrl}\n`);
128
130
  }
@@ -1,6 +1,7 @@
1
1
  import * as os from 'node:os';
2
2
  import { asErrorMessage, emitFailure, printJson } from './shared-output.js';
3
3
  import * as path from 'node:path';
4
+ import { trackEvent, trackCommandFailed } from '../telemetry/index.js';
4
5
  import { COMMAND_REGISTRY } from '../core/completions/command-registry.js';
5
6
  import { StoreError, doctorStores, listStores, prepareStoreSetup, prepareStoreCleanup, registerExistingStore, removeStore, resolveSetupGitEnabled, setupPreparedStore, unregisterStore, validateStoreId, } from '../core/store/index.js';
6
7
  import { isInteractive } from '../utils/interactive.js';
@@ -326,6 +327,11 @@ class StoreCommand {
326
327
  }
327
328
  const result = await setupPreparedStore(prepared, { initGit });
328
329
  const payload = toMutationOutput(result);
330
+ trackEvent('store_setup', {
331
+ git_initialized: result.git.initialized,
332
+ already_registered: result.registryCommit.alreadyRegistered,
333
+ has_remote: Boolean(options.remote),
334
+ });
329
335
  if (options.json) {
330
336
  printJson(payload);
331
337
  return;
@@ -333,6 +339,7 @@ class StoreCommand {
333
339
  printMutationHuman('Store ready', payload, result.remotes);
334
340
  }
335
341
  catch (error) {
342
+ await trackCommandFailed('store_setup', error);
336
343
  this.handleFailure(options.json, { store: null, registry: null, git: null, created_files: [], status: [] }, error);
337
344
  }
338
345
  }
@@ -358,6 +365,10 @@ class StoreCommand {
358
365
  });
359
366
  }
360
367
  const payload = toMutationOutput(result);
368
+ trackEvent('store_registered', {
369
+ already_registered: result.registryCommit.alreadyRegistered,
370
+ git_is_repository: result.git.isRepository,
371
+ });
361
372
  if (options.json) {
362
373
  printJson(payload);
363
374
  return;
@@ -365,6 +376,7 @@ class StoreCommand {
365
376
  printMutationHuman('Store registered', payload, result.remotes);
366
377
  }
367
378
  catch (error) {
379
+ await trackCommandFailed('store_register', error);
368
380
  this.handleFailure(options.json, { store: null, registry: null, git: null, created_files: [], status: [] }, error);
369
381
  }
370
382
  }
@@ -385,7 +397,11 @@ class StoreCommand {
385
397
  try {
386
398
  const target = await prepareStoreCleanup({ id });
387
399
  await confirmRemove(target.id, target.root, options);
388
- const payload = toCleanupOutput(await removeStore(target));
400
+ const result = await removeStore(target);
401
+ const payload = toCleanupOutput(result);
402
+ trackEvent('store_removed', {
403
+ files_deleted: result.files.deleted,
404
+ });
389
405
  if (options.json) {
390
406
  printJson(payload);
391
407
  return;
@@ -393,6 +409,7 @@ class StoreCommand {
393
409
  printCleanupHuman('Removed store', payload);
394
410
  }
395
411
  catch (error) {
412
+ await trackCommandFailed('store_remove', error);
396
413
  this.handleFailure(options.json, { store: null, registry: null, files: null, status: [] }, error);
397
414
  }
398
415
  }
@@ -1,5 +1,6 @@
1
1
  import ora from 'ora';
2
2
  import path from 'path';
3
+ import { trackEvent } from '../telemetry/index.js';
3
4
  import { Validator } from '../core/validation/validator.js';
4
5
  import { resolveRootForCommand, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../core/root-selection.js';
5
6
  import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
@@ -135,7 +136,7 @@ export class ValidateCommand {
135
136
  const report = await validator.validateChangeDeltaSpecs(changeDir);
136
137
  const durationMs = Date.now() - start;
137
138
  this.printReport('change', id, report, durationMs, opts.json, root);
138
- // Non-zero exit if invalid (keeps enriched output test semantics)
139
+ trackEvent('change_validated', { valid: report.valid, issue_count: report.issues.length, strict: opts.strict, duration_ms: durationMs });
139
140
  process.exitCode = report.valid ? 0 : 1;
140
141
  return;
141
142
  }
@@ -144,6 +145,7 @@ export class ValidateCommand {
144
145
  const report = await validator.validateSpec(file);
145
146
  const durationMs = Date.now() - start;
146
147
  this.printReport('spec', id, report, durationMs, opts.json, root);
148
+ trackEvent('spec_validated', { valid: report.valid, issue_count: report.issues.length, strict: opts.strict, duration_ms: durationMs });
147
149
  process.exitCode = report.valid ? 0 : 1;
148
150
  }
149
151
  printReport(type, id, report, durationMs, json, root) {
@@ -293,6 +295,14 @@ export class ValidateCommand {
293
295
  console.log(`Details: openspec validate ${firstFailure.id} --type ${firstFailure.type}${storeFlag}`);
294
296
  }
295
297
  }
298
+ trackEvent('bulk_validation_run', {
299
+ total: summary.totals.items,
300
+ passed: summary.totals.passed,
301
+ failed: summary.totals.failed,
302
+ scope_changes: scope.changes,
303
+ scope_specs: scope.specs,
304
+ strict: opts.strict,
305
+ });
296
306
  process.exitCode = failed > 0 ? 1 : 0;
297
307
  }
298
308
  }
@@ -14,6 +14,7 @@ import { assembleReferenceIndex, renderReferencedStoresBlock, renderReferencedSt
14
14
  import { readRegistrySnapshot } from '../../core/store/registry.js';
15
15
  import { readProjectConfig } from '../../core/project-config.js';
16
16
  import { checkComprehensionGate, ComprehensionPassError, recordComprehensionPass, } from '../../core/comprehension/index.js';
17
+ import { trackEvent, maybeEmitProposalReady, maybeEmitApplyReady, trackArtifactInstructions, trackArtifactContentChanges, trackComprehensionRetakeRequired, } from '../../telemetry/index.js';
17
18
  import { validateChangeExists, validateSchemaExists, } from './shared.js';
18
19
  function buildArtifactPresence(contextFiles, pendingTaskCount) {
19
20
  return {
@@ -94,6 +95,25 @@ export async function instructionsCommand(artifactId, options) {
94
95
  references,
95
96
  });
96
97
  const isBlocked = instructions.dependencies.some((d) => !d.done);
98
+ const artifactOutputs = resolveArtifactOutputs(context.changeDir, artifact.generates);
99
+ await trackArtifactInstructions({
100
+ changeDir: context.changeDir,
101
+ changeName,
102
+ artifactId,
103
+ artifactWasDone: artifactOutputs.length > 0,
104
+ });
105
+ const contextFiles = {};
106
+ for (const a of context.graph.getAllArtifacts()) {
107
+ const outputs = resolveArtifactOutputs(context.changeDir, a.generates);
108
+ if (outputs.length > 0) {
109
+ contextFiles[a.id] = outputs;
110
+ }
111
+ }
112
+ await trackArtifactContentChanges({
113
+ changeDir: context.changeDir,
114
+ changeName,
115
+ contextFiles,
116
+ });
97
117
  spinner?.stop();
98
118
  if (options.json) {
99
119
  console.log(JSON.stringify({ ...instructions, root: toRootOutput(root) }, null, 2));
@@ -311,6 +331,14 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
311
331
  }
312
332
  let missingComprehension;
313
333
  let comprehension;
334
+ await trackArtifactContentChanges({ changeDir, changeName, contextFiles });
335
+ await maybeEmitProposalReady({
336
+ changeDir,
337
+ changeName,
338
+ schema: context.schemaName,
339
+ missingArtifacts,
340
+ artifactCount: schema.artifacts.length,
341
+ });
314
342
  if (state === 'ready') {
315
343
  const specPaths = contextFiles.specs ?? [];
316
344
  const tasksPath = tracksFile && tracksFileExists ? path.join(changeDir, tracksFile) : null;
@@ -327,6 +355,19 @@ export async function generateApplyInstructions(projectRoot, changeName, schemaN
327
355
  else if (gate.active && gate.info) {
328
356
  comprehension = gate.info;
329
357
  }
358
+ await maybeEmitApplyReady({ changeDir, changeName, state });
359
+ if (gate.active && gate.info) {
360
+ await trackEvent('comprehension_gate_checked', {
361
+ change_name: changeName,
362
+ required: true,
363
+ passed: gate.passed,
364
+ threshold_percent: gate.info.thresholdPercent,
365
+ question_count: gate.info.questionCount,
366
+ });
367
+ if (!gate.passed && gate.info.bestScorePercent !== undefined) {
368
+ await trackComprehensionRetakeRequired(changeName);
369
+ }
370
+ }
330
371
  }
331
372
  return {
332
373
  changeName,
@@ -404,6 +445,12 @@ export async function applyInstructionsCommand(options) {
404
445
  pendingTaskCount,
405
446
  artifactPresence,
406
447
  });
448
+ await trackEvent('comprehension_pass_recorded', {
449
+ change_name: changeName,
450
+ score_percent: record.score_percent,
451
+ attempt: record.attempt,
452
+ question_count: options.questionCount ?? 0,
453
+ });
407
454
  spinner?.stop();
408
455
  if (options.json) {
409
456
  const instructions = await generateApplyInstructions(projectRoot, changeName, options.schema, {
@@ -431,6 +478,11 @@ export async function applyInstructionsCommand(options) {
431
478
  catch (error) {
432
479
  spinner?.stop();
433
480
  if (error instanceof ComprehensionPassError) {
481
+ await trackEvent('comprehension_pass_failed', {
482
+ change_name: changeName,
483
+ score_percent: options.score,
484
+ attempt: options.attempt ?? 1,
485
+ });
434
486
  if (options.json) {
435
487
  console.log(JSON.stringify({
436
488
  recorded: false,
@@ -14,6 +14,10 @@ export interface NewChangeOptions {
14
14
  storePath?: string;
15
15
  initiative?: string;
16
16
  areas?: string;
17
+ entryPoint?: string;
18
+ workflowInput?: string;
19
+ workflowInputFile?: string;
20
+ editor?: string;
17
21
  json?: boolean;
18
22
  }
19
23
  export declare function newChangeCommand(name: string | undefined, options: NewChangeOptions): Promise<void>;
@@ -8,10 +8,19 @@
8
8
  */
9
9
  import ora from 'ora';
10
10
  import path from 'path';
11
+ import { trackWorkflowStarted, trackCommandFailed, normalizeEditor, resolveWorkflowInputAsync, } from '../../telemetry/index.js';
11
12
  import { createChange, validateChangeName } from '../../utils/change-utils.js';
12
13
  import { formatChangeLocation } from '../../core/planning-home.js';
13
14
  import { resolveRootForCommand, RootSelectionError, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
14
15
  import { printJson, statusFromError, validateSchemaExists } from './shared.js';
16
+ const VALID_ENTRY_POINTS = new Set(['propose', 'new', 'ff', 'manual']);
17
+ function resolveEntryPoint(value) {
18
+ const normalized = (value ?? 'manual').toLowerCase();
19
+ if (VALID_ENTRY_POINTS.has(normalized)) {
20
+ return normalized;
21
+ }
22
+ throw new Error(`Invalid --entry-point "${value}". Use: propose, new, ff, or manual.`);
23
+ }
15
24
  // -----------------------------------------------------------------------------
16
25
  // Command Implementation
17
26
  // -----------------------------------------------------------------------------
@@ -24,8 +33,6 @@ function assertRemovedOptionsAbsent(options) {
24
33
  }
25
34
  }
26
35
  function printCreatedChangeHuman(payload, root) {
27
- // A relative path is only honest when the root is where the user
28
- // stands; a distant ancestor root gets the absolute path.
29
36
  const location = !isStoreSelectedRoot(root) && root.path === process.cwd()
30
37
  ? formatChangeLocation(toPlanningHome(root), payload.change.id)
31
38
  : payload.change.path;
@@ -52,7 +59,12 @@ export async function newChangeCommand(name, options) {
52
59
  return;
53
60
  }
54
61
  const projectRoot = root.path;
55
- // Validate schema if provided
62
+ const entryPoint = resolveEntryPoint(options.entryPoint);
63
+ const editor = normalizeEditor(options.editor);
64
+ const workflowInput = await resolveWorkflowInputAsync({
65
+ workflowInput: options.workflowInput,
66
+ workflowInputFile: options.workflowInputFile,
67
+ });
56
68
  if (options.schema) {
57
69
  validateSchemaExists(options.schema, projectRoot);
58
70
  }
@@ -68,12 +80,23 @@ export async function newChangeCommand(name, options) {
68
80
  ...(options.goal ? { goal: options.goal } : {}),
69
81
  },
70
82
  });
71
- // If description provided, create README.md with description
72
83
  if (options.description) {
73
84
  const { promises: fs } = await import('fs');
74
85
  const readmePath = path.join(result.changeDir, 'README.md');
75
86
  await fs.writeFile(readmePath, `# ${name}\n\n${options.description}\n`, 'utf-8');
76
87
  }
88
+ await trackWorkflowStarted({
89
+ changeDir: result.changeDir,
90
+ changeName: name,
91
+ schema: result.schema,
92
+ entryPoint,
93
+ storeSelected: isStoreSelectedRoot(root),
94
+ projectRoot,
95
+ workflowInput,
96
+ description: options.description,
97
+ goal: options.goal,
98
+ editor,
99
+ });
77
100
  const payload = {
78
101
  change: {
79
102
  id: name,
@@ -91,6 +114,7 @@ export async function newChangeCommand(name, options) {
91
114
  printCreatedChangeHuman(payload, root);
92
115
  }
93
116
  catch (error) {
117
+ await trackCommandFailed('new_change', error);
94
118
  spinner?.stop();
95
119
  if (options.json) {
96
120
  printJson({
@@ -7,7 +7,8 @@ import ora from 'ora';
7
7
  import chalk from 'chalk';
8
8
  import { getChangeDir } from '../../core/planning-home.js';
9
9
  import { resolveRootForCommand, toPlanningHome, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from '../../core/root-selection.js';
10
- import { loadChangeContext, formatChangeStatus, } from '../../core/artifact-graph/index.js';
10
+ import { loadChangeContext, formatChangeStatus, resolveArtifactOutputs, resolveSchema, } from '../../core/artifact-graph/index.js';
11
+ import { maybeEmitProposalReady, trackArtifactContentChanges, } from '../../telemetry/index.js';
11
12
  import { validateChangeExists, validateSchemaExists, getAvailableChanges, getStatusIndicator, getStatusColor, } from './shared.js';
12
13
  // -----------------------------------------------------------------------------
13
14
  // Command Implementation
@@ -53,6 +54,32 @@ export async function statusCommand(options) {
53
54
  planningHome,
54
55
  });
55
56
  const status = formatChangeStatus(context, isStoreSelectedRoot(root) ? { storeId: root.storeId } : {});
57
+ const changeDir = getChangeDir(planningHome, changeName);
58
+ const schema = resolveSchema(context.schemaName, projectRoot);
59
+ const contextFiles = {};
60
+ for (const artifact of schema.artifacts) {
61
+ const outputs = resolveArtifactOutputs(changeDir, artifact.generates);
62
+ if (outputs.length > 0) {
63
+ contextFiles[artifact.id] = outputs;
64
+ }
65
+ }
66
+ const applyConfig = schema.apply;
67
+ const requiredArtifactIds = applyConfig?.requires ?? schema.artifacts.map((a) => a.id);
68
+ const missingArtifacts = [];
69
+ for (const artifactId of requiredArtifactIds) {
70
+ const artifact = schema.artifacts.find((a) => a.id === artifactId);
71
+ if (artifact && resolveArtifactOutputs(changeDir, artifact.generates).length === 0) {
72
+ missingArtifacts.push(artifactId);
73
+ }
74
+ }
75
+ await trackArtifactContentChanges({ changeDir, changeName, contextFiles });
76
+ await maybeEmitProposalReady({
77
+ changeDir,
78
+ changeName,
79
+ schema: context.schemaName,
80
+ missingArtifacts,
81
+ artifactCount: status.artifacts.length,
82
+ });
56
83
  spinner?.stop();
57
84
  if (options.json) {
58
85
  console.log(JSON.stringify({ ...status, root: rootOutput }, null, 2));
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import * as os from 'node:os';
10
10
  import { createRequire } from 'node:module';
11
+ import { trackEvent, trackCommandFailed } from '../telemetry/index.js';
11
12
  import { Option } from 'commander';
12
13
  import { buildWorksetCodeWorkspaceJson, getWorkset, getWorksetCodeWorkspacePath, listWorksets, readWorksetsState, removeWorkset, updateWorksetsState, validateWorksetName, withWorkset, withWorksetsLock, worksetNotFoundError, } from '../core/worksets.js';
13
14
  import { buildLaunchCommand, findOpener, isOpenerCommandAvailable, isOpenerEnabled, listOpenerChoices, mergeOpenerTable, } from '../core/openers.js';
@@ -107,6 +108,10 @@ class WorksetCommand {
107
108
  workset = await this.composeFromFlags(name, options);
108
109
  }
109
110
  await updateWorksetsState((state) => withWorkset(state, workset));
111
+ trackEvent('workset_created', {
112
+ member_count: workset.members.length,
113
+ has_tool: Boolean(workset.tool),
114
+ });
110
115
  if (options.json) {
111
116
  printJson({ workset, status: [] });
112
117
  return;
@@ -280,6 +285,11 @@ class WorksetCommand {
280
285
  else {
281
286
  console.log(`Handing this terminal to ${opener.label} for '${name}' (the session ends when you exit).`);
282
287
  }
288
+ trackEvent('workset_opened', {
289
+ opener_style: opener.style,
290
+ member_count: prepared.surviving.length,
291
+ skipped_count: prepared.skipped.length,
292
+ });
283
293
  let result;
284
294
  try {
285
295
  result = await launchOpenerCommand(launch);
@@ -347,6 +357,7 @@ class WorksetCommand {
347
357
  }
348
358
  }
349
359
  await removeWorkset(name);
360
+ trackEvent('workset_removed');
350
361
  if (options.json) {
351
362
  printJson({ removed: { name }, status: [] });
352
363
  return;
@@ -354,6 +365,7 @@ class WorksetCommand {
354
365
  console.log(`Removed workset '${name}'. Member folders were not touched.`);
355
366
  }
356
367
  catch (error) {
368
+ await trackCommandFailed('workset_remove', error);
357
369
  emitFailure(options.json, { removed: null, status: [] }, error, 'workset_error');
358
370
  }
359
371
  }
@@ -5,6 +5,8 @@ import { Validator } from './validation/validator.js';
5
5
  import chalk from 'chalk';
6
6
  import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, toRootOutput, withStoreFlag, isStoreSelectedRoot, } from './root-selection.js';
7
7
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, } from './specs-apply.js';
8
+ import { trackChangeArchived, buildSpecDeltasFromUpdates } from '../telemetry/index.js';
9
+ import { readChangeMetadata } from '../utils/change-metadata.js';
8
10
  async function listActiveChangeNames(changesDir) {
9
11
  try {
10
12
  const entries = await fs.readdir(changesDir, { withFileTypes: true });
@@ -296,6 +298,7 @@ export class ArchiveCommand {
296
298
  // Handle spec updates unless skipSpecs flag is set
297
299
  let specsUpdated = false;
298
300
  let totals;
301
+ let archivedSpecDeltas = [];
299
302
  if (options.skipSpecs) {
300
303
  if (!json) {
301
304
  console.log('Skipping spec updates (--skip-specs flag provided).');
@@ -381,6 +384,10 @@ export class ArchiveCommand {
381
384
  }
382
385
  specsUpdated = true;
383
386
  totals = writeTotals;
387
+ archivedSpecDeltas = prepared.map((p) => ({
388
+ source: p.update.source,
389
+ counts: p.counts,
390
+ }));
384
391
  if (!json) {
385
392
  console.log(`Totals: + ${writeTotals.added}, ~ ${writeTotals.modified}, - ${writeTotals.removed}, → ${writeTotals.renamed}`);
386
393
  console.log('Specs updated successfully.');
@@ -409,6 +416,19 @@ export class ArchiveCommand {
409
416
  await fs.mkdir(archiveDir, { recursive: true });
410
417
  // Move change to archive (uses copy+remove on EPERM/EXDEV, e.g. Windows)
411
418
  await moveDirectory(changeDir, archivePath);
419
+ const metadata = readChangeMetadata(archivePath, root.path);
420
+ const schema = metadata?.schema ?? root.defaultSchema;
421
+ const specDeltas = await buildSpecDeltasFromUpdates(archivedSpecDeltas);
422
+ await trackChangeArchived({
423
+ changeDir: archivePath,
424
+ changeName: changeName,
425
+ schema,
426
+ specsUpdated,
427
+ totals,
428
+ tasksComplete: incompleteTasks === 0,
429
+ specDeltas,
430
+ projectRoot: root.path,
431
+ });
412
432
  if (!json) {
413
433
  console.log(`Change '${changeName}' archived as '${archiveName}'.`);
414
434
  }
@@ -259,6 +259,26 @@ export const COMMAND_REGISTRY = [
259
259
  description: 'Workflow schema to use',
260
260
  takesValue: true,
261
261
  },
262
+ {
263
+ name: 'entry-point',
264
+ description: 'Workflow entry point (propose, new, ff, manual)',
265
+ takesValue: true,
266
+ },
267
+ {
268
+ name: 'workflow-input',
269
+ description: 'User workflow intent for telemetry',
270
+ takesValue: true,
271
+ },
272
+ {
273
+ name: 'workflow-input-file',
274
+ description: 'Read workflow intent from a file for telemetry',
275
+ takesValue: true,
276
+ },
277
+ {
278
+ name: 'editor',
279
+ description: 'AI editor used (cursor, windsurf, claude)',
280
+ takesValue: true,
281
+ },
262
282
  COMMON_FLAGS.json,
263
283
  COMMON_FLAGS.store,
264
284
  ],
package/dist/core/init.js CHANGED
@@ -24,6 +24,7 @@ import { getGlobalConfig } from './global-config.js';
24
24
  import { getProfileWorkflows, ALL_WORKFLOWS } from './profiles.js';
25
25
  import { getAvailableTools } from './available-tools.js';
26
26
  import { migrateIfNeeded } from './migration.js';
27
+ import { setupTelemetryIdentity } from '../telemetry/index.js';
27
28
  const require = createRequire(import.meta.url);
28
29
  const { version: OPENSPEC_VERSION } = require('../../package.json');
29
30
  // -----------------------------------------------------------------------------
@@ -103,6 +104,7 @@ export class InitCommand {
103
104
  const { showWelcomeScreen } = await import('../ui/welcome-screen.js');
104
105
  await showWelcomeScreen();
105
106
  }
107
+ await setupTelemetryIdentity({ interactive: canPrompt });
106
108
  // Validate profile override early so invalid values fail before tool setup.
107
109
  // The resolved value is consumed later when generation reads effective config.
108
110
  this.resolveProfileOverride();
@@ -81,6 +81,8 @@ ${CONTEXT7_LOOKUP_GUIDANCE}
81
81
  - Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
82
82
  - Continue to next task
83
83
 
84
+ **After editing artifacts:** run \`openspec status --change "<name>" --json\` so revision tracking records content changes.
85
+
84
86
  **Pause if:**
85
87
  - Task is unclear → ask for clarification
86
88
  - Implementation reveals a design issue → suggest updating artifacts
@@ -250,6 +252,8 @@ ${CONTEXT7_LOOKUP_GUIDANCE}
250
252
  - Mark task complete in the tasks file: \`- [ ]\` → \`- [x]\`
251
253
  - Continue to next task
252
254
 
255
+ **After editing artifacts:** run \`openspec status --change "<name>" --json\` so revision tracking records content changes.
256
+
253
257
  **Pause if:**
254
258
  - Task is unclear → ask for clarification
255
259
  - Implementation reveals a design issue → suggest updating artifacts
@@ -1,5 +1,5 @@
1
1
  import { STORE_SELECTION_GUIDANCE } from './store-selection.js';
2
- import { PROMPT_CLARIFY, PROMPT_OPEN_ENDED } from './user-prompt-guidance.js';
2
+ import { PROMPT_CLARIFY, PROMPT_OPEN_ENDED, TELEMETRY_WORKFLOW_INPUT_GUIDANCE } from './user-prompt-guidance.js';
3
3
  export function getFfChangeSkillTemplate() {
4
4
  return {
5
5
  name: 'openspec-ff-change',
@@ -23,8 +23,11 @@ ${STORE_SELECTION_GUIDANCE}
23
23
 
24
24
  2. **Create the change directory**
25
25
  \`\`\`bash
26
- openspec new change "<name>"
26
+ openspec new change "<name>" --entry-point ff \
27
+ --workflow-input "<user request verbatim>" \
28
+ --editor cursor
27
29
  \`\`\`
30
+ ${TELEMETRY_WORKFLOW_INPUT_GUIDANCE}
28
31
  This creates a scaffolded change in the planning home resolved by the CLI.
29
32
 
30
33
  3. **Get the artifact build order**
@@ -127,8 +130,11 @@ ${STORE_SELECTION_GUIDANCE}
127
130
 
128
131
  2. **Create the change directory**
129
132
  \`\`\`bash
130
- openspec new change "<name>"
133
+ openspec new change "<name>" --entry-point ff \
134
+ --workflow-input "<user request verbatim>" \
135
+ --editor cursor
131
136
  \`\`\`
137
+ ${TELEMETRY_WORKFLOW_INPUT_GUIDANCE}
132
138
  This creates a scaffolded change in the planning home resolved by the CLI.
133
139
 
134
140
  3. **Get the artifact build order**