@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
|
@@ -0,0 +1,243 @@
|
|
|
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 { sanitizeWorkflowInput } from './input.js';
|
|
10
|
+
const TRACKED_ARTIFACT_IDS = ['proposal', 'design', 'plan', 'tasks', 'specs'];
|
|
11
|
+
export async function trackWorkflowStarted(params) {
|
|
12
|
+
const gitHead = await captureGitHead(params.projectRoot);
|
|
13
|
+
const startedAt = new Date().toISOString();
|
|
14
|
+
const workflowInput = params.workflowInput
|
|
15
|
+
? sanitizeWorkflowInput(params.workflowInput)
|
|
16
|
+
: undefined;
|
|
17
|
+
const description = params.description
|
|
18
|
+
? sanitizeWorkflowInput(params.description)
|
|
19
|
+
: undefined;
|
|
20
|
+
const goal = params.goal ? sanitizeWorkflowInput(params.goal) : undefined;
|
|
21
|
+
await updateMarker(params.changeDir, (current) => ({
|
|
22
|
+
...current,
|
|
23
|
+
started_at: startedAt,
|
|
24
|
+
entry_point: params.entryPoint,
|
|
25
|
+
...(workflowInput ? { workflow_input: workflowInput } : {}),
|
|
26
|
+
...(params.editor ? { editor: params.editor } : {}),
|
|
27
|
+
...(gitHead ? { git_head_at_start: gitHead } : {}),
|
|
28
|
+
}));
|
|
29
|
+
await captureEvent('workflow_started', {
|
|
30
|
+
change_name: params.changeName,
|
|
31
|
+
schema: params.schema,
|
|
32
|
+
entry_point: params.entryPoint,
|
|
33
|
+
store_selected: params.storeSelected,
|
|
34
|
+
...(workflowInput ? { workflow_input: workflowInput } : {}),
|
|
35
|
+
...(description ? { description } : {}),
|
|
36
|
+
...(goal ? { goal } : {}),
|
|
37
|
+
...(params.editor ? { editor: params.editor } : {}),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export async function maybeEmitProposalReady(params) {
|
|
41
|
+
if (params.missingArtifacts.length > 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const marker = await readMarker(params.changeDir);
|
|
45
|
+
if (marker.proposal_ready_emitted) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const proposalReadyAt = new Date().toISOString();
|
|
49
|
+
await updateMarker(params.changeDir, (current) => ({
|
|
50
|
+
...current,
|
|
51
|
+
proposal_ready_at: proposalReadyAt,
|
|
52
|
+
proposal_ready_emitted: true,
|
|
53
|
+
}));
|
|
54
|
+
await captureEvent('change_proposal_ready', {
|
|
55
|
+
change_name: params.changeName,
|
|
56
|
+
schema: params.schema,
|
|
57
|
+
artifact_count: params.artifactCount,
|
|
58
|
+
duration_since_start_ms: durationSince(marker.started_at),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export async function maybeEmitApplyReady(params) {
|
|
62
|
+
if (params.state !== 'ready') {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const marker = await readMarker(params.changeDir);
|
|
66
|
+
if (marker.apply_ready_emitted) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await updateMarker(params.changeDir, (current) => ({
|
|
70
|
+
...current,
|
|
71
|
+
apply_ready_emitted: true,
|
|
72
|
+
}));
|
|
73
|
+
await captureEvent('apply_ready', {
|
|
74
|
+
change_name: params.changeName,
|
|
75
|
+
duration_since_start_ms: durationSince(marker.started_at),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export async function trackArtifactInstructions(params) {
|
|
79
|
+
await captureEvent('artifact_instructions_requested', {
|
|
80
|
+
change_name: params.changeName,
|
|
81
|
+
artifact_id: params.artifactId,
|
|
82
|
+
});
|
|
83
|
+
if (!params.artifactWasDone) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const marker = await updateMarker(params.changeDir, (current) => {
|
|
87
|
+
const revisionCounts = { ...(current.revision_counts ?? {}) };
|
|
88
|
+
const next = (revisionCounts[params.artifactId] ?? 0) + 1;
|
|
89
|
+
revisionCounts[params.artifactId] = next;
|
|
90
|
+
return { ...current, revision_counts: revisionCounts };
|
|
91
|
+
});
|
|
92
|
+
const revisionNumber = marker.revision_counts?.[params.artifactId] ?? 1;
|
|
93
|
+
await captureEvent('artifact_revision_requested', {
|
|
94
|
+
change_name: params.changeName,
|
|
95
|
+
artifact_id: params.artifactId,
|
|
96
|
+
revision_number: revisionNumber,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function hashArtifactFiles(changeDir, contextFiles) {
|
|
100
|
+
const hashes = {};
|
|
101
|
+
for (const artifactId of TRACKED_ARTIFACT_IDS) {
|
|
102
|
+
const paths = contextFiles[artifactId];
|
|
103
|
+
if (!paths?.length) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const parts = [];
|
|
107
|
+
for (const filePath of paths) {
|
|
108
|
+
const hash = await hashFileAt(filePath);
|
|
109
|
+
if (hash) {
|
|
110
|
+
parts.push(hash);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (parts.length > 0) {
|
|
114
|
+
hashes[artifactId] = parts.join(':');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return hashes;
|
|
118
|
+
}
|
|
119
|
+
export async function trackArtifactContentChanges(params) {
|
|
120
|
+
const currentHashes = await hashArtifactFiles(params.changeDir, params.contextFiles);
|
|
121
|
+
if (Object.keys(currentHashes).length === 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const marker = await readMarker(params.changeDir);
|
|
125
|
+
const previousHashes = marker.artifact_hashes ?? {};
|
|
126
|
+
for (const [artifactId, hash] of Object.entries(currentHashes)) {
|
|
127
|
+
const previous = previousHashes[artifactId];
|
|
128
|
+
if (previous && previous !== hash) {
|
|
129
|
+
const updated = await updateMarker(params.changeDir, (current) => {
|
|
130
|
+
const revisionCounts = { ...(current.revision_counts ?? {}) };
|
|
131
|
+
revisionCounts[artifactId] = (revisionCounts[artifactId] ?? 0) + 1;
|
|
132
|
+
return { ...current, revision_counts: revisionCounts };
|
|
133
|
+
});
|
|
134
|
+
await captureEvent('artifact_content_changed', {
|
|
135
|
+
change_name: params.changeName,
|
|
136
|
+
artifact_id: artifactId,
|
|
137
|
+
change_count: updated.revision_counts?.[artifactId] ?? 1,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
await updateMarker(params.changeDir, (current) => ({
|
|
142
|
+
...current,
|
|
143
|
+
artifact_hashes: { ...(current.artifact_hashes ?? {}), ...currentHashes },
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
export async function trackComprehensionRetakeRequired(changeName) {
|
|
147
|
+
await captureEvent('comprehension_retake_required', {
|
|
148
|
+
change_name: changeName,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async function countDeltaSpecLines(filePath) {
|
|
152
|
+
try {
|
|
153
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
154
|
+
return content.split('\n').length;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export async function trackChangeArchived(params) {
|
|
161
|
+
const marker = await readMarker(params.changeDir);
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
for (const delta of params.specDeltas) {
|
|
164
|
+
await captureEvent('spec_delta_applied', {
|
|
165
|
+
change_name: params.changeName,
|
|
166
|
+
capability: delta.capability,
|
|
167
|
+
requirements_added: delta.counts.added,
|
|
168
|
+
requirements_modified: delta.counts.modified,
|
|
169
|
+
requirements_removed: delta.counts.removed,
|
|
170
|
+
requirements_renamed: delta.counts.renamed,
|
|
171
|
+
});
|
|
172
|
+
if (delta.deltaSpecLinesChanged !== undefined) {
|
|
173
|
+
await captureEvent('spec_delta_lines_changed', {
|
|
174
|
+
change_name: params.changeName,
|
|
175
|
+
capability: delta.capability,
|
|
176
|
+
lines_added: delta.deltaSpecLinesChanged,
|
|
177
|
+
lines_removed: 0,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let implementation;
|
|
182
|
+
if (marker.git_head_at_start) {
|
|
183
|
+
const { diffStatsSince } = await import('./git-stats.js');
|
|
184
|
+
const stats = await diffStatsSince(marker.git_head_at_start, params.projectRoot);
|
|
185
|
+
if (stats) {
|
|
186
|
+
implementation = stats;
|
|
187
|
+
await captureEvent('implementation_changed', {
|
|
188
|
+
change_name: params.changeName,
|
|
189
|
+
files_changed: stats.files_changed,
|
|
190
|
+
lines_added: stats.lines_added,
|
|
191
|
+
lines_removed: stats.lines_removed,
|
|
192
|
+
lines_changed: stats.lines_changed,
|
|
193
|
+
spec_capabilities_count: params.specDeltas.length,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const specsSummary = params.specDeltas.map((d) => ({
|
|
198
|
+
capability: d.capability,
|
|
199
|
+
requirements_added: d.counts.added,
|
|
200
|
+
requirements_modified: d.counts.modified,
|
|
201
|
+
requirements_removed: d.counts.removed,
|
|
202
|
+
requirements_renamed: d.counts.renamed,
|
|
203
|
+
delta_spec_lines_changed: d.deltaSpecLinesChanged,
|
|
204
|
+
}));
|
|
205
|
+
await captureEvent('change_archived', {
|
|
206
|
+
change_name: params.changeName,
|
|
207
|
+
schema: params.schema,
|
|
208
|
+
specs_updated: params.specsUpdated,
|
|
209
|
+
tasks_complete: params.tasksComplete,
|
|
210
|
+
entry_point: marker.entry_point ?? 'manual',
|
|
211
|
+
...(marker.workflow_input ? { workflow_input: marker.workflow_input } : {}),
|
|
212
|
+
...(marker.editor ? { editor: marker.editor } : {}),
|
|
213
|
+
duration_since_start_ms: durationSince(marker.started_at),
|
|
214
|
+
duration_proposal_to_ready_ms: durationBetween(marker.started_at, marker.proposal_ready_at),
|
|
215
|
+
duration_ready_to_archive_ms: durationBetween(marker.proposal_ready_at, now),
|
|
216
|
+
total_revisions: totalRevisions(marker),
|
|
217
|
+
revision_counts: marker.revision_counts ?? {},
|
|
218
|
+
specs: specsSummary,
|
|
219
|
+
...(implementation ? { implementation } : {}),
|
|
220
|
+
...(params.totals
|
|
221
|
+
? {
|
|
222
|
+
requirements_added: params.totals.added,
|
|
223
|
+
requirements_modified: params.totals.modified,
|
|
224
|
+
requirements_removed: params.totals.removed,
|
|
225
|
+
requirements_renamed: params.totals.renamed,
|
|
226
|
+
}
|
|
227
|
+
: {}),
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
export async function buildSpecDeltasFromUpdates(specUpdates) {
|
|
231
|
+
const deltas = [];
|
|
232
|
+
for (const update of specUpdates) {
|
|
233
|
+
const capability = path.basename(path.dirname(update.source));
|
|
234
|
+
const deltaSpecLinesChanged = await countDeltaSpecLines(update.source);
|
|
235
|
+
deltas.push({
|
|
236
|
+
capability,
|
|
237
|
+
counts: update.counts,
|
|
238
|
+
deltaSpecLinesChanged,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return deltas;
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=workflow.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codewalla_india/openspec",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.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
|
+
}
|
|
@@ -15,7 +15,12 @@ artifacts:
|
|
|
15
15
|
- **Capabilities**: Identify which specs will be created or modified:
|
|
16
16
|
- **New Capabilities**: List capabilities being introduced. Each becomes a new `specs/<name>/spec.md`. Use kebab-case names (e.g., `user-auth`, `data-export`).
|
|
17
17
|
- **Modified Capabilities**: List existing capabilities whose REQUIREMENTS are changing. Only include if spec-level behavior changes (not just implementation details). Each needs a delta spec file. Check `openspec/specs/` for existing spec names. Leave empty if no requirement changes.
|
|
18
|
-
- **Impact**: Affected code, APIs, dependencies, or systems.
|
|
18
|
+
- **Impact**: Affected code, APIs, dependencies, or systems. If importing from
|
|
19
|
+
Jira, record ticket key(s) here (e.g., `Jira: CW-1234`) for traceability.
|
|
20
|
+
|
|
21
|
+
When importing from Jira: name capabilities by behavioral domain (e.g., `ui`,
|
|
22
|
+
`user-auth`), not by ticket key. Reference the ticket in Impact, not in
|
|
23
|
+
capability names.
|
|
19
24
|
|
|
20
25
|
IMPORTANT: The Capabilities section is critical. It creates the contract between
|
|
21
26
|
proposal and specs phases. Research existing specs before filling this in.
|
|
@@ -38,6 +43,9 @@ artifacts:
|
|
|
38
43
|
- New capabilities: use the exact kebab-case name from the proposal (specs/<capability>/spec.md).
|
|
39
44
|
- Modified capabilities: use the existing spec folder name from openspec/specs/<capability>/ when creating the delta spec at specs/<capability>/spec.md.
|
|
40
45
|
|
|
46
|
+
When Jira acceptance criteria exist: translate them into requirements under the
|
|
47
|
+
capability folders listed in the proposal. Never create specs/<ticket-key>/.
|
|
48
|
+
|
|
41
49
|
Delta operations (use ## headers):
|
|
42
50
|
- **ADDED Requirements**: New capabilities
|
|
43
51
|
- **MODIFIED Requirements**: Changed behavior - MUST include full updated content
|