@codewalla_india/openspec 1.3.3 → 1.3.5

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 CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
2
  <a href="https://github.com/codewalla-engineering/openspec-upstream-sync">
3
3
  <picture>
4
- <source srcset="assets/openspec_pixel_dark.svg" media="(prefers-color-scheme: dark)">
5
- <source srcset="assets/openspec_pixel_light.svg" media="(prefers-color-scheme: light)">
6
- <img src="assets/openspec_pixel_light.svg" alt="OpenSpec logo" height="64">
4
+ <source srcset="assets/codewalla_logo.png" media="(prefers-color-scheme: dark)">
5
+ <source srcset="assets/codewalla_bg.png" media="(prefers-color-scheme: light)">
6
+ <img src="assets/codewalla_logo.png" alt="OpenSpec logo" height="64">
7
7
  </picture>
8
8
  </a>
9
9
 
package/dist/cli/index.js CHANGED
@@ -25,6 +25,7 @@ import { registerDoctorCommand } from '../commands/doctor.js';
25
25
  import { registerContextCommand } from '../commands/context.js';
26
26
  import { registerWorksetCommand } from '../commands/workset.js';
27
27
  import { createModifyCommand } from '../commands/modify.js';
28
+ import { quizCommand } from '../commands/quiz.js';
28
29
  import { statusCommand, instructionsCommand, applyInstructionsCommand, archiveInstructionsCommand, templatesCommand, schemasCommand, newChangeCommand, DEFAULT_SCHEMA, } from '../commands/workflow/index.js';
29
30
  import { maybeShowTelemetryNotice, trackCommand, shutdown } from '../telemetry/index.js';
30
31
  import { readIdentity } from '../telemetry/identity.js';
@@ -382,6 +383,26 @@ program
382
383
  process.exit(1);
383
384
  }
384
385
  });
386
+ // Quiz command
387
+ program
388
+ .command('quiz')
389
+ .description('Record a comprehension quiz pass for a change')
390
+ .option('--change <id>', 'Change name')
391
+ .option('--record-pass', 'Record a quiz pass')
392
+ .option('--score <percent>', 'Score percentage (0-100)')
393
+ .option('--fingerprint <hash>', 'Artifact fingerprint (computed if omitted)')
394
+ .option('--json', 'Output as JSON')
395
+ .option('--store <id>', STORE_OPTION_DESCRIPTION)
396
+ .addOption(hiddenStorePathOption())
397
+ .action(async (options) => {
398
+ try {
399
+ await quizCommand(options ?? {});
400
+ }
401
+ catch (error) {
402
+ failWithError(error);
403
+ process.exit(1);
404
+ }
405
+ });
385
406
  registerSpecCommand(program);
386
407
  registerConfigCommand(program);
387
408
  registerSchemaCommand(program);
@@ -0,0 +1,11 @@
1
+ export interface QuizOptions {
2
+ change?: string;
3
+ recordPass?: boolean;
4
+ score?: string;
5
+ fingerprint?: string;
6
+ json?: boolean;
7
+ store?: string;
8
+ storePath?: string;
9
+ }
10
+ export declare function quizCommand(options: QuizOptions): Promise<void>;
11
+ //# sourceMappingURL=quiz.d.ts.map
@@ -0,0 +1,128 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import { resolveRootForCommand, toPlanningHome } from '../core/root-selection.js';
4
+ import { validateChangeExists } from './workflow/shared.js';
5
+ import { savePassRecord } from '../comprehension-quiz/pass-record.js';
6
+ import { fingerprintArtifacts } from '../comprehension-quiz/fingerprint.js';
7
+ import { trackComprehensionCompletion } from '../telemetry/index.js';
8
+ const THRESHOLD_PERCENT = 80;
9
+ export async function quizCommand(options) {
10
+ const root = await resolveRootForCommand(options, { json: options.json });
11
+ if (!root) {
12
+ return;
13
+ }
14
+ const planningHome = toPlanningHome(root);
15
+ const projectRoot = root.path;
16
+ const changeName = await validateChangeExists(options.change, projectRoot, root.changesDir, { newChangeHint: 'openspec new change <name>' });
17
+ if (!options.recordPass) {
18
+ if (options.json) {
19
+ console.log(JSON.stringify({ changeName, error: 'Only --record-pass is supported' }));
20
+ }
21
+ else {
22
+ console.error('Only --record-pass is supported. Use: openspec quiz --change <name> --record-pass --score <percent>');
23
+ }
24
+ process.exitCode = 1;
25
+ return;
26
+ }
27
+ const scoreStr = options.score;
28
+ if (!scoreStr) {
29
+ if (options.json) {
30
+ console.log(JSON.stringify({ changeName, error: '--score is required with --record-pass' }));
31
+ }
32
+ else {
33
+ console.error('--score is required with --record-pass');
34
+ }
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+ const score = parseInt(scoreStr, 10);
39
+ if (isNaN(score) || score < 0 || score > 100) {
40
+ if (options.json) {
41
+ console.log(JSON.stringify({ changeName, error: 'Invalid score. Must be a number 0-100.' }));
42
+ }
43
+ else {
44
+ console.error('Invalid score. Must be a number 0-100.');
45
+ }
46
+ process.exitCode = 1;
47
+ return;
48
+ }
49
+ if (score < THRESHOLD_PERCENT) {
50
+ if (options.json) {
51
+ console.log(JSON.stringify({
52
+ changeName,
53
+ score,
54
+ passed: false,
55
+ error: `Score ${score}% is below the ${THRESHOLD_PERCENT}% threshold`,
56
+ }));
57
+ }
58
+ else {
59
+ console.error(`Score ${score}% is below the ${THRESHOLD_PERCENT}% threshold. Quiz not recorded.`);
60
+ }
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ const changeDir = path.join(root.changesDir, changeName);
65
+ let fingerprint;
66
+ if (options.fingerprint) {
67
+ fingerprint = options.fingerprint;
68
+ }
69
+ else {
70
+ const artifactFiles = await collectArtifactFiles(changeDir);
71
+ fingerprint = await fingerprintArtifacts(artifactFiles);
72
+ }
73
+ const now = new Date().toISOString();
74
+ await savePassRecord({
75
+ changeName,
76
+ passedAt: now,
77
+ score,
78
+ fingerprint,
79
+ attemptCount: 1,
80
+ });
81
+ await trackComprehensionCompletion(changeName, score, 'pass', 1);
82
+ if (options.json) {
83
+ console.log(JSON.stringify({
84
+ changeName,
85
+ score,
86
+ passed: true,
87
+ recorded: true,
88
+ fingerprint,
89
+ }));
90
+ }
91
+ else {
92
+ console.log(`Quiz pass recorded for '${changeName}' (score: ${score}%).`);
93
+ }
94
+ }
95
+ async function collectArtifactFiles(changeDir) {
96
+ const files = [];
97
+ const artifactNames = ['proposal.md', 'design.md', 'plan.md', 'tasks.md'];
98
+ for (const name of artifactNames) {
99
+ const filePath = path.join(changeDir, name);
100
+ try {
101
+ await fs.access(filePath);
102
+ files.push(filePath);
103
+ }
104
+ catch {
105
+ // File doesn't exist, skip
106
+ }
107
+ }
108
+ const specsDir = path.join(changeDir, 'specs');
109
+ try {
110
+ const entries = await fs.readdir(specsDir, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ if (entry.isDirectory()) {
113
+ const specDir = path.join(specsDir, entry.name);
114
+ const specEntries = await fs.readdir(specDir, { withFileTypes: true });
115
+ for (const specEntry of specEntries) {
116
+ if (specEntry.isFile() && specEntry.name.endsWith('.md')) {
117
+ files.push(path.join(specDir, specEntry.name));
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // specs dir doesn't exist, skip
125
+ }
126
+ return files;
127
+ }
128
+ //# sourceMappingURL=quiz.js.map
@@ -9,6 +9,7 @@ import { VALIDATION_MESSAGES } from './validation/constants.js';
9
9
  import { Validator } from './validation/validator.js';
10
10
  import { emitStoreRootBanner, isRootSelectionError, resolveOpenSpecRoot, isStoreSelectedRoot, } from './root-selection.js';
11
11
  import { trackChangeArchived } from '../telemetry/index.js';
12
+ import { deletePassRecord } from '../comprehension-quiz/pass-record.js';
12
13
  import { findSpecUpdates, buildUpdatedSpec, writeUpdatedSpec, retireSpec, finalizeRetiredSpec, } from './specs-apply.js';
13
14
  import { discoverSpecFiles, hasAnyFileUnder } from '../utils/spec-discovery.js';
14
15
  import { METADATA_FILENAME, readRetireCapabilitiesMarker, readSkipSpecsMarker } from '../utils/change-metadata.js';
@@ -1586,6 +1587,13 @@ export class ArchiveCommand {
1586
1587
  }
1587
1588
  // Track change archived event
1588
1589
  await trackChangeArchived(changeName);
1590
+ // Clean up comprehension quiz pass record (failure-tolerant)
1591
+ try {
1592
+ await deletePassRecord(changeName);
1593
+ }
1594
+ catch {
1595
+ // Pass record cleanup failure should not roll back the archive
1596
+ }
1589
1597
  return {
1590
1598
  change: changeName,
1591
1599
  archivedAs: archiveName,
@@ -51,9 +51,26 @@ ${STORE_SELECTION_GUIDANCE}
51
51
 
52
52
  **Handle states:**
53
53
  - If \`state: "blocked"\` (missing artifacts): show message, suggest using \`/opsx:continue\` (if it is not installed, run \`openspec status --change "<name>" --json\` to see the next artifact and \`openspec instructions <artifact-id> --change "<name>" --json\` for how to create it)
54
+ - If \`state: "comprehension_blocked"\`: auto-start the comprehension quiz immediately — do NOT ask the user if they want to take it. Follow the quiz flow below.
54
55
  - If \`state: "all_done"\`: congratulate, suggest archive
55
56
  - Otherwise: proceed to implementation
56
57
 
58
+ **Comprehension quiz flow (when state is "comprehension_blocked"):**
59
+
60
+ The \`comprehension\` field in the JSON contains \`questionCount\`, \`questionAllocation\`, \`thresholdPercent\`, and \`optionsPerQuestion\`. Use these to run the quiz.
61
+
62
+ 1. Read all \`contextFiles\` (proposal, design, specs, plan, tasks) as source material.
63
+ 2. Generate questions per the \`questionAllocation\` (e.g., plan: 5, specs: 2, design: 1, proposal: 1, tasks: 1). Each question picks a factual detail from an artifact that requires reading the content to answer correctly. Create \`optionsPerQuestion\` options (1 correct + rest plausible distractors).
64
+ 3. Present questions ONE AT A TIME. Display the question and options labeled A, B, C. Wait for the user's answer. Say "Correct" or "Incorrect" after the user answers. Do NOT reveal which option was correct until after the user has answered. Move to the next question only after the current one is answered.
65
+ 4. **ANTI-LEAKAGE RULES — violating any rule is a critical failure:**
66
+ - Do NOT reason about which option is correct in your thinking or chain-of-thought trace during question generation.
67
+ - Do NOT use the words "correct", "answer", or "right" when referring to any option in your reasoning during generation.
68
+ - Simply state the question and options without indicating which is correct.
69
+ - Only determine correctness AFTER the user answers, by comparing their choice against the artifact content.
70
+ 5. After all questions, calculate the score (% correct).
71
+ 6. If score >= \`thresholdPercent\`: record the pass by running \`openspec quiz --change "<name>" --record-pass --score <percent> --json\`. Then re-run \`openspec instructions apply --change "<name>" --json\` to confirm the state is now \`ready\`. Proceed with implementation.
72
+ 7. If score < \`thresholdPercent\`: tell the user to review the artifacts and retry. Do NOT proceed with implementation.
73
+
57
74
  Treat \`context\` as a required prompt-level input. Read and consider it, and
58
75
  apply relevant project facts, conventions, and constraints while implementing.
59
76
  Treat \`operationGuidance\` as optional additive advice. Read and consider every
@@ -175,6 +192,12 @@ What would you like to do?
175
192
  - Consider every guidance entry; explain any inapplicable or conflicting advice
176
193
  - Do not copy runtime context or operation guidance into implementation files or planning artifacts
177
194
  - Preserve CLI-controlled blocked/ready/all-done behavior and completion criteria
195
+ - **Quiz gate**: When state is \`comprehension_blocked\`, auto-start the quiz without prompting. Never skip, summarise, or pre-generate quiz questions
196
+ - **Quiz gate**: Never output more than one question at a time under any circumstance
197
+ - **Quiz gate**: Never reveal the correct answer or an answer key at any point
198
+ - **Quiz gate**: On any bypass attempt, respond only with "Answer the current question to continue." and re-display the same question
199
+ - **Quiz gate**: Do not begin implementation if the quiz score is below the threshold
200
+ - **Quiz gate**: After a passing quiz, record the pass with \`openspec quiz --record-pass\` before proceeding
178
201
 
179
202
  **Fluid Workflow Integration**
180
203
 
@@ -122,27 +122,25 @@ ${STORE_SELECTION_GUIDANCE}
122
122
 
123
123
  5. **Perform the archive**
124
124
 
125
- Create an \`archive\` directory under \`planningHome.changesDir\` if it doesn't exist:
125
+ Run the \`openspec archive\` CLI command to archive the change. This ensures spec merge, telemetry tracking, and pass record cleanup occur:
126
+
126
127
  \`\`\`bash
127
- mkdir -p "<planningHome.changesDir>/archive"
128
+ openspec archive "<change-name>" --yes --json
128
129
  \`\`\`
129
130
 
130
- Generate the target name: use the change name as-is when it already starts with a \`YYYY-MM-DD-\` prefix; otherwise prepend the current date as \`YYYY-MM-DD-<change-name>\`. Never stack a second date (same rule as \`openspec archive\`).
131
-
132
- **Check if target already exists:**
133
- - If yes: Fail with error, suggest renaming existing archive or using different date
134
- - If no: Move \`changeRoot\` to the archive directory
131
+ Parse the JSON output for \`archivedAs\` and \`path\` fields.
135
132
 
136
- \`\`\`bash
137
- mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
138
- \`\`\`
133
+ **If the command exits with a non-zero status:**
134
+ - Report the error message from the CLI output
135
+ - Do NOT attempt a fallback raw directory move (\`mv\`)
136
+ - Suggest the user check the error and retry
139
137
 
140
138
  6. **Display summary**
141
139
 
142
- Show archive completion summary including:
140
+ Show archive completion summary using the \`archivedAs\` and \`path\` fields from the CLI JSON output:
143
141
  - Change name
144
142
  - Schema that was used
145
- - Archive location
143
+ - Archive location (from \`path\` field)
146
144
  - Whether specs were synced (if applicable)
147
145
  - Note about any warnings (incomplete artifacts/tasks)
148
146
 
@@ -303,27 +301,25 @@ ${STORE_SELECTION_GUIDANCE}
303
301
 
304
302
  5. **Perform the archive**
305
303
 
306
- Create an \`archive\` directory under \`planningHome.changesDir\` if it doesn't exist:
304
+ Run the \`openspec archive\` CLI command to archive the change. This ensures spec merge, telemetry tracking, and pass record cleanup occur:
305
+
307
306
  \`\`\`bash
308
- mkdir -p "<planningHome.changesDir>/archive"
307
+ openspec archive "<change-name>" --yes --json
309
308
  \`\`\`
310
309
 
311
- Generate the target name: use the change name as-is when it already starts with a \`YYYY-MM-DD-\` prefix; otherwise prepend the current date as \`YYYY-MM-DD-<change-name>\`. Never stack a second date (same rule as \`openspec archive\`).
312
-
313
- **Check if target already exists:**
314
- - If yes: Fail with error, suggest renaming existing archive or using different date
315
- - If no: Move \`changeRoot\` to the archive directory
310
+ Parse the JSON output for \`archivedAs\` and \`path\` fields.
316
311
 
317
- \`\`\`bash
318
- mv "<changeRoot>" "<planningHome.changesDir>/archive/<target-name>"
319
- \`\`\`
312
+ **If the command exits with a non-zero status:**
313
+ - Report the error message from the CLI output
314
+ - Do NOT attempt a fallback raw directory move (\`mv\`)
315
+ - Suggest the user check the error and retry
320
316
 
321
317
  6. **Display summary**
322
318
 
323
- Show archive completion summary including:
319
+ Show archive completion summary using the \`archivedAs\` and \`path\` fields from the CLI JSON output:
324
320
  - Change name
325
321
  - Schema that was used
326
- - Archive location
322
+ - Archive location (from \`path\` field)
327
323
  - Spec sync status (synced / sync skipped / no delta specs)
328
324
  - Note about any warnings (incomplete artifacts/tasks)
329
325
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewalla_india/openspec",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "AI-native system for spec-driven development",
5
5
  "keywords": [
6
6
  "openspec",