sdd-cli 0.1.29 → 0.1.31

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
@@ -55,7 +55,7 @@ sdd-cli hello "create a calculator app"
55
55
  ## Global Flags
56
56
 
57
57
  - `--approve`, `--improve`, `--parallel`
58
- - `--non-interactive`, `--dry-run`, `--beginner`, `--from-step`
58
+ - `--non-interactive`, `--dry-run`, `--beginner`, `--from-step`, `--iterations`
59
59
  - `--project`, `--output`, `--scope`, `--metrics-local`
60
60
  - `--provider`, `--gemini`, `--model`
61
61
 
package/dist/cli.js CHANGED
@@ -85,6 +85,7 @@ program
85
85
  .option("--metrics-local", "Enable local opt-in telemetry snapshots in workspace/metrics")
86
86
  .option("--provider <name>", "AI provider: gemini|codex|auto", (0, providers_1.defaultProviderPreference)())
87
87
  .option("--model <name>", "AI model id (for providers that support model override)")
88
+ .option("--iterations <n>", "Autopilot improvement iterations (1-10)", "1")
88
89
  .option("--gemini", "Shortcut for --provider gemini");
89
90
  program.hook("preAction", (thisCommand, actionCommand) => {
90
91
  const config = (0, config_1.ensureConfig)();
@@ -109,7 +110,8 @@ program.hook("preAction", (thisCommand, actionCommand) => {
109
110
  : typeof opts.provider === "string"
110
111
  ? opts.provider
111
112
  : config.ai.preferred_cli,
112
- model: typeof opts.model === "string" ? opts.model : config.ai.model
113
+ model: typeof opts.model === "string" ? opts.model : config.ai.model,
114
+ iterations: Number.parseInt(typeof opts.iterations === "string" ? opts.iterations : "1", 10)
113
115
  });
114
116
  process.env.SDD_GEMINI_MODEL = typeof opts.model === "string" ? opts.model : config.ai.model;
115
117
  const commandPath = typeof actionCommand.name === "function"
@@ -492,7 +494,7 @@ function normalizeArgv(argv) {
492
494
  if (args.length === 0) {
493
495
  return argv;
494
496
  }
495
- const valueFlags = new Set(["--from-step", "--project", "--output", "--scope", "--provider", "--model"]);
497
+ const valueFlags = new Set(["--from-step", "--project", "--output", "--scope", "--provider", "--model", "--iterations"]);
496
498
  let positionalIndex = -1;
497
499
  for (let i = 0; i < args.length; i += 1) {
498
500
  const token = args[i];
@@ -4,6 +4,14 @@ type ReviewerFinding = {
4
4
  severity: "high" | "medium";
5
5
  message: string;
6
6
  };
7
+ export type UserStory = {
8
+ id: string;
9
+ priority: "P0" | "P1";
10
+ persona: string;
11
+ story: string;
12
+ acceptanceCriteria: string[];
13
+ sourceReviewer: string;
14
+ };
7
15
  export type DigitalReviewResult = {
8
16
  passed: boolean;
9
17
  findings: ReviewerFinding[];
@@ -13,5 +21,9 @@ export type DigitalReviewResult = {
13
21
  summary: string;
14
22
  };
15
23
  export declare function runDigitalHumanReview(appDir: string, context?: LifecycleContext): DigitalReviewResult;
24
+ export declare function convertFindingsToUserStories(findings: ReviewerFinding[]): UserStory[];
25
+ export declare function storiesToDiagnostics(stories: UserStory[]): string[];
26
+ export declare function writeUserStoriesBacklog(appDir: string, stories: UserStory[]): string | null;
27
+ export declare function appendDigitalReviewRound(appDir: string, round: number, review: DigitalReviewResult, stories: UserStory[]): string | null;
16
28
  export declare function writeDigitalReviewReport(appDir: string, review: DigitalReviewResult): string | null;
17
29
  export {};
@@ -4,6 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runDigitalHumanReview = runDigitalHumanReview;
7
+ exports.convertFindingsToUserStories = convertFindingsToUserStories;
8
+ exports.storiesToDiagnostics = storiesToDiagnostics;
9
+ exports.writeUserStoriesBacklog = writeUserStoriesBacklog;
10
+ exports.appendDigitalReviewRound = appendDigitalReviewRound;
7
11
  exports.writeDigitalReviewReport = writeDigitalReviewReport;
8
12
  const fs_1 = __importDefault(require("fs"));
9
13
  const path_1 = __importDefault(require("path"));
@@ -90,6 +94,30 @@ function hasUserFlowDocs(root, readme) {
90
94
  }
91
95
  return Boolean(findDoc(root, ["user-flow.md", "ux-notes.md", "experience.md"]));
92
96
  }
97
+ function hasAccessibilityEvidence(root, readme) {
98
+ if (/\ba11y\b|\baccessibility\b|\bwcag\b|\bkeyboard\b/.test(readme)) {
99
+ return true;
100
+ }
101
+ return Boolean(findDoc(root, ["accessibility.md", "a11y.md"]));
102
+ }
103
+ function hasPerformanceEvidence(root, readme) {
104
+ if (/\bperformance\b|\blatency\b|\bthroughput\b|\bp95\b|\bp99\b/.test(readme)) {
105
+ return true;
106
+ }
107
+ return Boolean(findDoc(root, ["performance.md", "performance-budget.md", "scalability.md"]));
108
+ }
109
+ function hasSupportEvidence(root, readme) {
110
+ if (/\btroubleshoot\b|\bsupport\b|\bfaq\b/.test(readme)) {
111
+ return true;
112
+ }
113
+ return Boolean(findDoc(root, ["troubleshooting.md", "support.md", "faq.md"]));
114
+ }
115
+ function hasApiContracts(root) {
116
+ return Boolean(findDoc(root, ["openapi.yaml", "openapi.yml", "api-contract.md", "api.md"]));
117
+ }
118
+ function hasReleaseNotes(root) {
119
+ return Boolean(findDoc(root, ["release-notes.md", "changelog.md"]));
120
+ }
93
121
  function parseThreshold() {
94
122
  const raw = Number.parseInt(process.env.SDD_DIGITAL_REVIEW_MIN_SCORE ?? "", 10);
95
123
  if (!Number.isFinite(raw)) {
@@ -179,6 +207,41 @@ function runDigitalHumanReview(appDir, context) {
179
207
  message: "User experience flow is unclear. Add user-flow/UX notes and acceptance of critical journeys."
180
208
  });
181
209
  }
210
+ if (!hasAccessibilityEvidence(appDir, readme)) {
211
+ findings.push({
212
+ reviewer: "accessibility_tester",
213
+ severity: "medium",
214
+ message: "Accessibility evidence missing. Add keyboard/contrast/screen-reader validation notes."
215
+ });
216
+ }
217
+ if (!hasPerformanceEvidence(appDir, readme)) {
218
+ findings.push({
219
+ reviewer: "performance_engineer",
220
+ severity: "medium",
221
+ message: "Performance expectations are unclear. Add performance budget and baseline measurements."
222
+ });
223
+ }
224
+ if (!hasSupportEvidence(appDir, readme)) {
225
+ findings.push({
226
+ reviewer: "support_agent",
227
+ severity: "medium",
228
+ message: "Support/troubleshooting guidance is missing for operators and end users."
229
+ });
230
+ }
231
+ if (!hasApiContracts(appDir)) {
232
+ findings.push({
233
+ reviewer: "integrator_partner",
234
+ severity: "medium",
235
+ message: "API contract/documentation missing. Add OpenAPI or API contract document for integrators."
236
+ });
237
+ }
238
+ if (!hasReleaseNotes(appDir)) {
239
+ findings.push({
240
+ reviewer: "release_manager",
241
+ severity: "medium",
242
+ message: "Release notes/changelog missing. Add release documentation for change visibility."
243
+ });
244
+ }
182
245
  if (hasSecretLeak(appDir)) {
183
246
  findings.push({
184
247
  reviewer: "security_reviewer",
@@ -227,6 +290,96 @@ function runDigitalHumanReview(appDir, context) {
227
290
  summary
228
291
  };
229
292
  }
293
+ function slugReviewer(reviewer) {
294
+ return reviewer.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
295
+ }
296
+ function acceptanceCriteriaFromFinding(finding) {
297
+ const base = finding.message.replace(/\.$/, "");
298
+ return [
299
+ `Given the generated app, when quality review runs, then ${base.toLowerCase()}.`,
300
+ "Given CI validation, when documentation/tests are checked, then evidence is discoverable and actionable."
301
+ ];
302
+ }
303
+ function convertFindingsToUserStories(findings) {
304
+ const deduped = new Map();
305
+ for (const finding of findings) {
306
+ const key = `${finding.reviewer}::${finding.message}`.toLowerCase();
307
+ if (!deduped.has(key)) {
308
+ deduped.set(key, finding);
309
+ }
310
+ }
311
+ let index = 1;
312
+ return [...deduped.values()].map((finding) => {
313
+ const id = `US-${String(index).padStart(3, "0")}`;
314
+ index += 1;
315
+ const persona = slugReviewer(finding.reviewer);
316
+ const priority = finding.severity === "high" ? "P0" : "P1";
317
+ return {
318
+ id,
319
+ priority,
320
+ persona,
321
+ sourceReviewer: finding.reviewer,
322
+ story: `As a ${persona}, I need ${finding.message.toLowerCase()} so that the delivery is production-ready.`,
323
+ acceptanceCriteria: acceptanceCriteriaFromFinding(finding)
324
+ };
325
+ });
326
+ }
327
+ function storiesToDiagnostics(stories) {
328
+ return stories.map((story) => `[UserStory:${story.id}][${story.priority}] ${story.story}`);
329
+ }
330
+ function writeUserStoriesBacklog(appDir, stories) {
331
+ if (!fs_1.default.existsSync(appDir)) {
332
+ return null;
333
+ }
334
+ const deployDir = path_1.default.join(appDir, "deploy");
335
+ fs_1.default.mkdirSync(deployDir, { recursive: true });
336
+ const jsonPath = path_1.default.join(deployDir, "digital-review-user-stories.json");
337
+ fs_1.default.writeFileSync(jsonPath, JSON.stringify({
338
+ generatedAt: new Date().toISOString(),
339
+ count: stories.length,
340
+ stories
341
+ }, null, 2), "utf-8");
342
+ const mdPath = path_1.default.join(deployDir, "digital-review-user-stories.md");
343
+ const lines = [
344
+ "# Digital Review User Stories",
345
+ "",
346
+ ...stories.flatMap((story) => [
347
+ `## ${story.id} (${story.priority})`,
348
+ `- Persona: ${story.persona}`,
349
+ `- Source reviewer: ${story.sourceReviewer}`,
350
+ `- Story: ${story.story}`,
351
+ "- Acceptance criteria:",
352
+ ...story.acceptanceCriteria.map((criterion) => ` - ${criterion}`),
353
+ ""
354
+ ])
355
+ ];
356
+ fs_1.default.writeFileSync(mdPath, `${lines.join("\n")}\n`, "utf-8");
357
+ return jsonPath;
358
+ }
359
+ function appendDigitalReviewRound(appDir, round, review, stories) {
360
+ if (!fs_1.default.existsSync(appDir)) {
361
+ return null;
362
+ }
363
+ const deployDir = path_1.default.join(appDir, "deploy");
364
+ fs_1.default.mkdirSync(deployDir, { recursive: true });
365
+ const reportPath = path_1.default.join(deployDir, "digital-review-rounds.json");
366
+ const existing = fs_1.default.existsSync(reportPath)
367
+ ? JSON.parse(fs_1.default.readFileSync(reportPath, "utf-8"))
368
+ : { rounds: [] };
369
+ const rounds = Array.isArray(existing.rounds) ? existing.rounds : [];
370
+ rounds.push({
371
+ round,
372
+ generatedAt: new Date().toISOString(),
373
+ summary: review.summary,
374
+ passed: review.passed,
375
+ score: review.score,
376
+ threshold: review.threshold,
377
+ findings: review.findings,
378
+ stories
379
+ });
380
+ fs_1.default.writeFileSync(reportPath, JSON.stringify({ rounds }, null, 2), "utf-8");
381
+ return reportPath;
382
+ }
230
383
  function writeDigitalReviewReport(appDir, review) {
231
384
  if (!fs_1.default.existsSync(appDir)) {
232
385
  return null;
@@ -128,6 +128,11 @@ async function runHello(input, runQuestions) {
128
128
  const dryRun = runtimeFlags.dryRun;
129
129
  const beginnerMode = runtimeFlags.beginner;
130
130
  const provider = runtimeFlags.provider;
131
+ const iterations = runtimeFlags.iterations;
132
+ if (!Number.isInteger(iterations) || iterations < 1 || iterations > 10) {
133
+ (0, errors_1.printError)("SDD-1005", "Invalid --iterations value. Use an integer between 1 and 10.");
134
+ return;
135
+ }
131
136
  console.log("Hello from sdd-cli.");
132
137
  console.log(`Workspace: ${workspace.root}`);
133
138
  if (beginnerMode) {
@@ -136,6 +141,7 @@ async function runHello(input, runQuestions) {
136
141
  if (autoGuidedMode) {
137
142
  printWhy("Auto-guided mode active: using current workspace defaults.");
138
143
  printWhy(`AI provider preference: ${provider ?? "gemini"}`);
144
+ printWhy(`Iterations configured: ${iterations}`);
139
145
  }
140
146
  else {
141
147
  const useWorkspace = await (0, prompt_1.confirm)("Use this workspace path? (y/n) ");
@@ -449,40 +455,50 @@ async function runHello(input, runQuestions) {
449
455
  const digitalReviewDisabled = lifecycleDisabled || process.env.SDD_DISABLE_AI_AUTOPILOT === "1" || process.env.SDD_DISABLE_DIGITAL_REVIEW === "1";
450
456
  if (!digitalReviewDisabled) {
451
457
  const appDir = path_1.default.join(projectRoot, "generated-app");
452
- const parsedReviewAttempts = Number.parseInt(process.env.SDD_DIGITAL_REVIEW_MAX_ATTEMPTS ?? "", 10);
453
- const maxReviewAttempts = Number.isFinite(parsedReviewAttempts) && parsedReviewAttempts > 0 ? parsedReviewAttempts : 3;
454
- let review = (0, digital_reviewers_1.runDigitalHumanReview)(appDir, {
455
- goalText: text,
456
- intentSignals: intent.signals,
457
- intentDomain: intent.domain,
458
- intentFlow: intent.flow
459
- });
460
- const initialReviewReport = (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
461
- if (initialReviewReport) {
462
- printWhy(`Digital-review report: ${initialReviewReport}`);
463
- }
464
- if (!review.passed) {
465
- printWhy(`Digital human reviewers found delivery issues (${review.summary}). Applying targeted refinements.`);
458
+ let deliveryApproved = false;
459
+ for (let round = 1; round <= iterations; round += 1) {
460
+ printWhy(`Iteration ${round}/${iterations}: running multi-persona digital review.`);
461
+ let review = (0, digital_reviewers_1.runDigitalHumanReview)(appDir, {
462
+ goalText: text,
463
+ intentSignals: intent.signals,
464
+ intentDomain: intent.domain,
465
+ intentFlow: intent.flow
466
+ });
467
+ let stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
468
+ const reviewPath = (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
469
+ const storiesPath = (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
470
+ (0, digital_reviewers_1.appendDigitalReviewRound)(appDir, round, review, stories);
471
+ if (reviewPath) {
472
+ printWhy(`Digital-review report: ${reviewPath}`);
473
+ }
474
+ if (storiesPath) {
475
+ printWhy(`Digital-review user stories: ${storiesPath} (${stories.length} stories)`);
476
+ }
477
+ if (review.passed) {
478
+ printWhy(`Iteration ${round}: digital reviewers approved (${review.summary}).`);
479
+ deliveryApproved = true;
480
+ continue;
481
+ }
482
+ printWhy(`Iteration ${round}: reviewers requested improvements (${review.summary}).`);
466
483
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue: ${issue}`));
467
- }
468
- for (let attempt = 1; attempt <= maxReviewAttempts && !review.passed; attempt += 1) {
469
- const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, review.diagnostics, intent.domain);
484
+ const storyDiagnostics = (0, digital_reviewers_1.storiesToDiagnostics)(stories);
485
+ const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, [...review.diagnostics, ...storyDiagnostics, "Implement all prioritized user stories before next review."], intent.domain);
470
486
  if (!repair.attempted || !repair.applied) {
471
- printWhy(`Digital-review repair attempt ${attempt} skipped: ${repair.reason || "unknown reason"}`);
487
+ printWhy(`Iteration ${round}: repair skipped (${repair.reason || "unknown reason"}).`);
472
488
  break;
473
489
  }
474
- printWhy(`Digital-review repair attempt ${attempt} applied (${repair.fileCount} files).`);
490
+ printWhy(`Iteration ${round}: repair applied (${repair.fileCount} files). Re-validating lifecycle.`);
475
491
  lifecycle = (0, app_lifecycle_1.runAppLifecycle)(projectRoot, activeProject, {
476
492
  goalText: text,
477
493
  intentSignals: intent.signals,
478
494
  intentDomain: intent.domain,
479
495
  intentFlow: intent.flow
480
496
  });
481
- lifecycle.summary.forEach((line) => printWhy(`Lifecycle (digital-review retry ${attempt}): ${line}`));
497
+ lifecycle.summary.forEach((line) => printWhy(`Lifecycle (iteration ${round}): ${line}`));
482
498
  if (!lifecycle.qualityPassed) {
499
+ printWhy("Quality gates failed after story implementation. Applying one quality-repair pass.");
483
500
  const qualityRepair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, lifecycle.qualityDiagnostics, intent.domain);
484
501
  if (qualityRepair.attempted && qualityRepair.applied) {
485
- printWhy(`Quality regression repaired after digital review (${qualityRepair.fileCount} files). Re-validating delivery.`);
486
502
  lifecycle = (0, app_lifecycle_1.runAppLifecycle)(projectRoot, activeProject, {
487
503
  goalText: text,
488
504
  intentSignals: intent.signals,
@@ -492,7 +508,7 @@ async function runHello(input, runQuestions) {
492
508
  }
493
509
  }
494
510
  if (!lifecycle.qualityPassed) {
495
- printWhy("Delivery regressed below lifecycle quality gates during digital-review iteration.");
511
+ printWhy(`Iteration ${round}: lifecycle quality still failing.`);
496
512
  continue;
497
513
  }
498
514
  review = (0, digital_reviewers_1.runDigitalHumanReview)(appDir, {
@@ -501,17 +517,23 @@ async function runHello(input, runQuestions) {
501
517
  intentDomain: intent.domain,
502
518
  intentFlow: intent.flow
503
519
  });
520
+ stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
504
521
  (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
505
- if (!review.passed) {
506
- review.diagnostics.forEach((issue) => printWhy(`Reviewer issue (retry ${attempt}): ${issue}`));
522
+ (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
523
+ (0, digital_reviewers_1.appendDigitalReviewRound)(appDir, round, review, stories);
524
+ if (review.passed) {
525
+ printWhy(`Iteration ${round}: delivery improved and approved (${review.summary}).`);
526
+ deliveryApproved = true;
527
+ }
528
+ else {
529
+ printWhy(`Iteration ${round}: additional improvements still required (${review.summary}).`);
507
530
  }
508
531
  }
509
- if (!review.passed) {
510
- printWhy("Digital-review quality bar not met after refinement attempts.");
532
+ if (!deliveryApproved) {
533
+ printWhy("Digital-review quality bar not met after configured iterations.");
511
534
  printRecoveryNext(activeProject, "finish", text);
512
535
  return;
513
536
  }
514
- printWhy(`Digital reviewers approved delivery quality (${review.summary}).`);
515
537
  }
516
538
  (0, local_metrics_1.recordActivationMetric)("completed", {
517
539
  project: activeProject,
@@ -12,6 +12,7 @@ export type RuntimeFlags = {
12
12
  metricsLocal?: boolean;
13
13
  provider?: string;
14
14
  model?: string;
15
+ iterations: number;
15
16
  };
16
17
  export declare function setFlags(next: Partial<RuntimeFlags>): void;
17
18
  export declare function getFlags(): RuntimeFlags;
@@ -15,7 +15,8 @@ const flags = {
15
15
  scope: undefined,
16
16
  metricsLocal: false,
17
17
  provider: process.env.SDD_AI_PROVIDER_DEFAULT ?? "gemini",
18
- model: process.env.SDD_AI_MODEL_DEFAULT
18
+ model: process.env.SDD_AI_MODEL_DEFAULT,
19
+ iterations: 1
19
20
  };
20
21
  function setFlags(next) {
21
22
  if ("approve" in next) {
@@ -57,6 +58,10 @@ function setFlags(next) {
57
58
  if ("model" in next) {
58
59
  flags.model = typeof next.model === "string" ? next.model : flags.model;
59
60
  }
61
+ if ("iterations" in next) {
62
+ const raw = Number(next.iterations);
63
+ flags.iterations = Number.isFinite(raw) ? Math.trunc(raw) : flags.iterations;
64
+ }
60
65
  }
61
66
  function getFlags() {
62
67
  return { ...flags };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-cli",
3
- "version": "0.1.29",
3
+ "version": "0.1.31",
4
4
  "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
5
5
  "keywords": [
6
6
  "cli",