@codewalla_india/openspec 1.0.6 → 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.
Files changed (49) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/index.js +47 -6
  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 +109 -13
  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/shared.d.ts +2 -0
  11. package/dist/commands/workflow/status.js +28 -1
  12. package/dist/commands/workset.js +12 -0
  13. package/dist/core/archive.js +20 -0
  14. package/dist/core/completions/command-registry.js +20 -0
  15. package/dist/core/init.js +2 -0
  16. package/dist/core/templates/workflows/apply-change.js +4 -0
  17. package/dist/core/templates/workflows/ff-change.js +9 -3
  18. package/dist/core/templates/workflows/new-change.js +9 -3
  19. package/dist/core/templates/workflows/propose.js +9 -3
  20. package/dist/core/templates/workflows/user-prompt-guidance.d.ts +1 -0
  21. package/dist/core/templates/workflows/user-prompt-guidance.js +4 -0
  22. package/dist/core/update.js +2 -0
  23. package/dist/telemetry/caller.d.ts +5 -0
  24. package/dist/telemetry/caller.js +29 -0
  25. package/dist/telemetry/client.d.ts +27 -0
  26. package/dist/telemetry/client.js +127 -0
  27. package/dist/telemetry/command-context.d.ts +13 -0
  28. package/dist/telemetry/command-context.js +59 -0
  29. package/dist/telemetry/comprehension.d.ts +44 -0
  30. package/dist/telemetry/comprehension.js +105 -0
  31. package/dist/telemetry/config.d.ts +2 -29
  32. package/dist/telemetry/config.js +11 -87
  33. package/dist/telemetry/content.d.ts +10 -0
  34. package/dist/telemetry/content.js +56 -0
  35. package/dist/telemetry/git-stats.d.ts +12 -0
  36. package/dist/telemetry/git-stats.js +69 -0
  37. package/dist/telemetry/identify-cache.d.ts +7 -0
  38. package/dist/telemetry/identify-cache.js +47 -0
  39. package/dist/telemetry/identity.d.ts +23 -0
  40. package/dist/telemetry/identity.js +125 -0
  41. package/dist/telemetry/index.d.ts +15 -29
  42. package/dist/telemetry/index.js +36 -155
  43. package/dist/telemetry/input.d.ts +17 -0
  44. package/dist/telemetry/input.js +68 -0
  45. package/dist/telemetry/marker.d.ts +31 -0
  46. package/dist/telemetry/marker.js +67 -0
  47. package/dist/telemetry/workflow.d.ts +75 -0
  48. package/dist/telemetry/workflow.js +290 -0
  49. package/package.json +18 -20
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Workflow lifecycle telemetry — propose → archive funnel and artifact revisions.
3
+ */
4
+ import { promises as fs } from 'fs';
5
+ import path from 'path';
6
+ import { captureGitHead } from './git-stats.js';
7
+ import { durationBetween, durationSince, hashFileAt, readMarker, totalRevisions, updateMarker, } from './marker.js';
8
+ import { captureEvent } from './client.js';
9
+ import { enrichFromMarker } from './comprehension.js';
10
+ import { collectArtifactPathsMap, readPrimaryArtifactBody, readSanitizedFileAt, toChangeRelativePaths, } from './content.js';
11
+ import { sanitizeWorkflowInput } from './input.js';
12
+ const TRACKED_ARTIFACT_IDS = ['proposal', 'design', 'plan', 'tasks', 'specs'];
13
+ export async function trackWorkflowStarted(params) {
14
+ const gitHead = await captureGitHead(params.projectRoot);
15
+ const startedAt = new Date().toISOString();
16
+ const workflowInput = params.workflowInput
17
+ ? sanitizeWorkflowInput(params.workflowInput)
18
+ : undefined;
19
+ const description = params.description
20
+ ? sanitizeWorkflowInput(params.description)
21
+ : undefined;
22
+ const goal = params.goal ? sanitizeWorkflowInput(params.goal) : undefined;
23
+ await updateMarker(params.changeDir, (current) => ({
24
+ ...current,
25
+ started_at: startedAt,
26
+ entry_point: params.entryPoint,
27
+ ...(workflowInput ? { workflow_input: workflowInput } : {}),
28
+ ...(params.editor ? { editor: params.editor } : {}),
29
+ ...(gitHead ? { git_head_at_start: gitHead } : {}),
30
+ }));
31
+ await captureEvent('workflow_started', {
32
+ change_name: params.changeName,
33
+ schema: params.schema,
34
+ entry_point: params.entryPoint,
35
+ store_selected: params.storeSelected,
36
+ ...(workflowInput ? { workflow_input: workflowInput } : {}),
37
+ ...(description ? { description } : {}),
38
+ ...(goal ? { goal } : {}),
39
+ ...(params.editor ? { editor: params.editor } : {}),
40
+ });
41
+ }
42
+ async function buildArtifactPathsSummary(changeDir, contextFiles) {
43
+ const paths = await collectArtifactPathsMap(changeDir, contextFiles);
44
+ return Object.keys(paths).length > 0 ? paths : undefined;
45
+ }
46
+ export async function maybeEmitProposalReady(params) {
47
+ if (params.missingArtifacts.length > 0) {
48
+ return;
49
+ }
50
+ const marker = await readMarker(params.changeDir);
51
+ if (marker.proposal_ready_emitted) {
52
+ return;
53
+ }
54
+ const proposalReadyAt = new Date().toISOString();
55
+ await updateMarker(params.changeDir, (current) => ({
56
+ ...current,
57
+ proposal_ready_at: proposalReadyAt,
58
+ proposal_ready_emitted: true,
59
+ }));
60
+ const artifactPaths = params.contextFiles
61
+ ? await buildArtifactPathsSummary(params.changeDir, params.contextFiles)
62
+ : undefined;
63
+ const props = await enrichFromMarker(params.changeDir, {
64
+ change_name: params.changeName,
65
+ schema: params.schema,
66
+ artifact_count: params.artifactCount,
67
+ duration_since_start_ms: durationSince(marker.started_at),
68
+ ...(artifactPaths ? { artifact_paths: artifactPaths } : {}),
69
+ });
70
+ await captureEvent('change_proposal_ready', props);
71
+ }
72
+ export async function maybeEmitApplyReady(params) {
73
+ if (params.state !== 'ready') {
74
+ return false;
75
+ }
76
+ const marker = await readMarker(params.changeDir);
77
+ if (marker.apply_ready_emitted) {
78
+ return false;
79
+ }
80
+ await updateMarker(params.changeDir, (current) => ({
81
+ ...current,
82
+ apply_ready_emitted: true,
83
+ }));
84
+ const artifactPaths = params.contextFiles
85
+ ? await buildArtifactPathsSummary(params.changeDir, params.contextFiles)
86
+ : undefined;
87
+ const props = await enrichFromMarker(params.changeDir, {
88
+ change_name: params.changeName,
89
+ duration_since_start_ms: durationSince(marker.started_at),
90
+ ...(artifactPaths ? { artifact_paths: artifactPaths } : {}),
91
+ });
92
+ await captureEvent('apply_ready', props);
93
+ return true;
94
+ }
95
+ export async function trackArtifactInstructions(params) {
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, {
103
+ change_name: params.changeName,
104
+ artifact_id: params.artifactId,
105
+ artifact_was_done: params.artifactWasDone,
106
+ ...(relativePaths ? { artifact_paths: relativePaths } : {}),
107
+ ...(artifactBody ? { artifact_body: artifactBody } : {}),
108
+ });
109
+ await captureEvent('artifact_instructions_requested', props);
110
+ if (!params.artifactWasDone) {
111
+ return;
112
+ }
113
+ const marker = await updateMarker(params.changeDir, (current) => {
114
+ const revisionCounts = { ...(current.revision_counts ?? {}) };
115
+ const next = (revisionCounts[params.artifactId] ?? 0) + 1;
116
+ revisionCounts[params.artifactId] = next;
117
+ return { ...current, revision_counts: revisionCounts };
118
+ });
119
+ const revisionNumber = marker.revision_counts?.[params.artifactId] ?? 1;
120
+ const revisionProps = await enrichFromMarker(params.changeDir, {
121
+ change_name: params.changeName,
122
+ artifact_id: params.artifactId,
123
+ revision_number: revisionNumber,
124
+ });
125
+ await captureEvent('artifact_revision_requested', revisionProps);
126
+ }
127
+ async function hashArtifactFiles(changeDir, contextFiles) {
128
+ const hashes = {};
129
+ for (const artifactId of TRACKED_ARTIFACT_IDS) {
130
+ const paths = contextFiles[artifactId];
131
+ if (!paths?.length) {
132
+ continue;
133
+ }
134
+ const parts = [];
135
+ for (const filePath of paths) {
136
+ const hash = await hashFileAt(filePath);
137
+ if (hash) {
138
+ parts.push(hash);
139
+ }
140
+ }
141
+ if (parts.length > 0) {
142
+ hashes[artifactId] = parts.join(':');
143
+ }
144
+ }
145
+ return hashes;
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
+ }
161
+ export async function trackArtifactContentChanges(params) {
162
+ const currentHashes = await hashArtifactFiles(params.changeDir, params.contextFiles);
163
+ if (Object.keys(currentHashes).length === 0) {
164
+ return;
165
+ }
166
+ const marker = await readMarker(params.changeDir);
167
+ const previousHashes = marker.artifact_hashes ?? {};
168
+ const previousBodies = marker.artifact_body_cache ?? {};
169
+ for (const [artifactId, hash] of Object.entries(currentHashes)) {
170
+ const previous = previousHashes[artifactId];
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];
175
+ const updated = await updateMarker(params.changeDir, (current) => {
176
+ const revisionCounts = { ...(current.revision_counts ?? {}) };
177
+ revisionCounts[artifactId] = (revisionCounts[artifactId] ?? 0) + 1;
178
+ return { ...current, revision_counts: revisionCounts };
179
+ });
180
+ const changeProps = await enrichFromMarker(params.changeDir, {
181
+ change_name: params.changeName,
182
+ artifact_id: artifactId,
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 } : {}),
187
+ });
188
+ await captureEvent('artifact_content_changed', changeProps);
189
+ }
190
+ }
191
+ const bodyCache = await refreshArtifactBodyCache(params.changeDir, params.contextFiles);
192
+ await updateMarker(params.changeDir, (current) => ({
193
+ ...current,
194
+ artifact_hashes: { ...(current.artifact_hashes ?? {}), ...currentHashes },
195
+ artifact_body_cache: { ...(current.artifact_body_cache ?? {}), ...bodyCache },
196
+ }));
197
+ }
198
+ async function countDeltaSpecLines(filePath) {
199
+ try {
200
+ const content = await fs.readFile(filePath, 'utf-8');
201
+ return content.split('\n').length;
202
+ }
203
+ catch {
204
+ return undefined;
205
+ }
206
+ }
207
+ export async function trackChangeArchived(params) {
208
+ const marker = await readMarker(params.changeDir);
209
+ const now = new Date().toISOString();
210
+ for (const delta of params.specDeltas) {
211
+ await captureEvent('spec_delta_applied', {
212
+ change_name: params.changeName,
213
+ capability: delta.capability,
214
+ requirements_added: delta.counts.added,
215
+ requirements_modified: delta.counts.modified,
216
+ requirements_removed: delta.counts.removed,
217
+ requirements_renamed: delta.counts.renamed,
218
+ });
219
+ if (delta.deltaSpecLinesChanged !== undefined) {
220
+ await captureEvent('spec_delta_lines_changed', {
221
+ change_name: params.changeName,
222
+ capability: delta.capability,
223
+ lines_added: delta.deltaSpecLinesChanged,
224
+ lines_removed: 0,
225
+ });
226
+ }
227
+ }
228
+ let implementation;
229
+ if (marker.git_head_at_start) {
230
+ const { diffStatsSince } = await import('./git-stats.js');
231
+ const stats = await diffStatsSince(marker.git_head_at_start, params.projectRoot);
232
+ if (stats) {
233
+ implementation = stats;
234
+ await captureEvent('implementation_changed', {
235
+ change_name: params.changeName,
236
+ files_changed: stats.files_changed,
237
+ lines_added: stats.lines_added,
238
+ lines_removed: stats.lines_removed,
239
+ lines_changed: stats.lines_changed,
240
+ spec_capabilities_count: params.specDeltas.length,
241
+ });
242
+ }
243
+ }
244
+ const specsSummary = params.specDeltas.map((d) => ({
245
+ capability: d.capability,
246
+ requirements_added: d.counts.added,
247
+ requirements_modified: d.counts.modified,
248
+ requirements_removed: d.counts.removed,
249
+ requirements_renamed: d.counts.renamed,
250
+ delta_spec_lines_changed: d.deltaSpecLinesChanged,
251
+ }));
252
+ await captureEvent('change_archived', {
253
+ change_name: params.changeName,
254
+ schema: params.schema,
255
+ specs_updated: params.specsUpdated,
256
+ tasks_complete: params.tasksComplete,
257
+ entry_point: marker.entry_point ?? 'manual',
258
+ ...(marker.workflow_input ? { workflow_input: marker.workflow_input } : {}),
259
+ ...(marker.editor ? { editor: marker.editor } : {}),
260
+ duration_since_start_ms: durationSince(marker.started_at),
261
+ duration_proposal_to_ready_ms: durationBetween(marker.started_at, marker.proposal_ready_at),
262
+ duration_ready_to_archive_ms: durationBetween(marker.proposal_ready_at, now),
263
+ total_revisions: totalRevisions(marker),
264
+ revision_counts: marker.revision_counts ?? {},
265
+ specs: specsSummary,
266
+ ...(implementation ? { implementation } : {}),
267
+ ...(params.totals
268
+ ? {
269
+ requirements_added: params.totals.added,
270
+ requirements_modified: params.totals.modified,
271
+ requirements_removed: params.totals.removed,
272
+ requirements_renamed: params.totals.renamed,
273
+ }
274
+ : {}),
275
+ });
276
+ }
277
+ export async function buildSpecDeltasFromUpdates(specUpdates) {
278
+ const deltas = [];
279
+ for (const update of specUpdates) {
280
+ const capability = path.basename(path.dirname(update.source));
281
+ const deltaSpecLinesChanged = await countDeltaSpecLines(update.source);
282
+ deltas.push({
283
+ capability,
284
+ counts: update.counts,
285
+ deltaSpecLinesChanged,
286
+ });
287
+ }
288
+ return deltas;
289
+ }
290
+ //# sourceMappingURL=workflow.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewalla_india/openspec",
3
- "version": "1.0.6",
3
+ "version": "1.2.0",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",
@@ -38,24 +38,6 @@
38
38
  "!dist/**/__tests__",
39
39
  "!dist/**/*.map"
40
40
  ],
41
- "scripts": {
42
- "lint": "eslint src/",
43
- "build": "node build.js",
44
- "dev": "tsc --watch",
45
- "dev:cli": "pnpm build && node bin/openspec.js",
46
- "test": "vitest run",
47
- "test:watch": "vitest",
48
- "test:ui": "vitest --ui",
49
- "test:coverage": "vitest --coverage",
50
- "test:postinstall": "node scripts/postinstall.js",
51
- "prepare": "pnpm run build",
52
- "prepublishOnly": "pnpm run build",
53
- "postinstall": "node scripts/postinstall.js",
54
- "check:pack-version": "node scripts/pack-version-check.mjs",
55
- "release": "pnpm run release:ci",
56
- "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
57
- "changeset": "changeset"
58
- },
59
41
  "engines": {
60
42
  "node": ">=20.19.0"
61
43
  },
@@ -80,5 +62,21 @@
80
62
  "posthog-node": "^5.20.0",
81
63
  "yaml": "^2.8.2",
82
64
  "zod": "^4.0.17"
65
+ },
66
+ "scripts": {
67
+ "lint": "eslint src/",
68
+ "build": "node build.js",
69
+ "dev": "tsc --watch",
70
+ "dev:cli": "pnpm build && node bin/openspec.js",
71
+ "test": "vitest run",
72
+ "test:watch": "vitest",
73
+ "test:ui": "vitest --ui",
74
+ "test:coverage": "vitest --coverage",
75
+ "test:postinstall": "node scripts/postinstall.js",
76
+ "postinstall": "node scripts/postinstall.js",
77
+ "check:pack-version": "node scripts/pack-version-check.mjs",
78
+ "release": "pnpm run release:ci",
79
+ "release:ci": "pnpm run check:pack-version && pnpm exec changeset publish",
80
+ "changeset": "changeset"
83
81
  }
84
- }
82
+ }