@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.
- package/README.md +2 -4
- package/dist/cli/index.js +44 -4
- package/dist/commands/config.js +8 -0
- package/dist/commands/feedback.js +2 -0
- package/dist/commands/store.js +18 -1
- package/dist/commands/validate.js +11 -1
- package/dist/commands/workflow/instructions.js +52 -0
- package/dist/commands/workflow/new-change.d.ts +4 -0
- package/dist/commands/workflow/new-change.js +28 -4
- package/dist/commands/workflow/status.js +28 -1
- package/dist/commands/workset.js +12 -0
- package/dist/core/archive.js +20 -0
- package/dist/core/completions/command-registry.js +20 -0
- package/dist/core/init.js +2 -0
- package/dist/core/templates/workflows/apply-change.js +4 -0
- package/dist/core/templates/workflows/ff-change.js +9 -3
- package/dist/core/templates/workflows/mcp-guidance.d.ts +1 -1
- package/dist/core/templates/workflows/mcp-guidance.js +15 -0
- package/dist/core/templates/workflows/new-change.js +9 -3
- package/dist/core/templates/workflows/propose.js +9 -3
- package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
- package/dist/core/templates/workflows/user-prompt-guidance.js +4 -0
- package/dist/core/update.js +2 -0
- package/dist/telemetry/client.d.ts +23 -0
- package/dist/telemetry/client.js +118 -0
- package/dist/telemetry/config.d.ts +2 -29
- package/dist/telemetry/config.js +11 -87
- package/dist/telemetry/git-stats.d.ts +12 -0
- package/dist/telemetry/git-stats.js +69 -0
- package/dist/telemetry/identity.d.ts +23 -0
- package/dist/telemetry/identity.js +125 -0
- package/dist/telemetry/index.d.ts +10 -28
- package/dist/telemetry/index.js +27 -155
- package/dist/telemetry/input.d.ts +14 -0
- package/dist/telemetry/input.js +56 -0
- package/dist/telemetry/marker.d.ts +24 -0
- package/dist/telemetry/marker.js +67 -0
- package/dist/telemetry/workflow.d.ts +73 -0
- package/dist/telemetry/workflow.js +243 -0
- package/package.json +18 -20
- package/schemas/spec-driven/schema.yaml +9 -1
- 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
|
|
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
|
-
|
|
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 {
|
|
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())
|
package/dist/commands/config.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/store.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
package/dist/commands/workset.js
CHANGED
|
@@ -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
|
}
|
package/dist/core/archive.js
CHANGED
|
@@ -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**
|