sdd-cli 0.1.30 → 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];
@@ -24,5 +24,6 @@ export declare function runDigitalHumanReview(appDir: string, context?: Lifecycl
24
24
  export declare function convertFindingsToUserStories(findings: ReviewerFinding[]): UserStory[];
25
25
  export declare function storiesToDiagnostics(stories: UserStory[]): string[];
26
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;
27
28
  export declare function writeDigitalReviewReport(appDir: string, review: DigitalReviewResult): string | null;
28
29
  export {};
@@ -7,6 +7,7 @@ exports.runDigitalHumanReview = runDigitalHumanReview;
7
7
  exports.convertFindingsToUserStories = convertFindingsToUserStories;
8
8
  exports.storiesToDiagnostics = storiesToDiagnostics;
9
9
  exports.writeUserStoriesBacklog = writeUserStoriesBacklog;
10
+ exports.appendDigitalReviewRound = appendDigitalReviewRound;
10
11
  exports.writeDigitalReviewReport = writeDigitalReviewReport;
11
12
  const fs_1 = __importDefault(require("fs"));
12
13
  const path_1 = __importDefault(require("path"));
@@ -355,6 +356,30 @@ function writeUserStoriesBacklog(appDir, stories) {
355
356
  fs_1.default.writeFileSync(mdPath, `${lines.join("\n")}\n`, "utf-8");
356
357
  return jsonPath;
357
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
+ }
358
383
  function writeDigitalReviewReport(appDir, review) {
359
384
  if (!fs_1.default.existsSync(appDir)) {
360
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,46 +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
- let stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
461
- const initialReviewReport = (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
462
- const initialStoriesPath = (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
463
- if (initialReviewReport) {
464
- printWhy(`Digital-review report: ${initialReviewReport}`);
465
- }
466
- if (initialStoriesPath) {
467
- printWhy(`Digital-review user stories: ${initialStoriesPath} (${stories.length} stories)`);
468
- }
469
- if (!review.passed) {
470
- 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}).`);
471
483
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue: ${issue}`));
472
- }
473
- for (let attempt = 1; attempt <= maxReviewAttempts && !review.passed; attempt += 1) {
474
484
  const storyDiagnostics = (0, digital_reviewers_1.storiesToDiagnostics)(stories);
475
- const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, [...review.diagnostics, ...storyDiagnostics, "Implement all user stories from digital review backlog."], intent.domain);
485
+ const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, [...review.diagnostics, ...storyDiagnostics, "Implement all prioritized user stories before next review."], intent.domain);
476
486
  if (!repair.attempted || !repair.applied) {
477
- printWhy(`Digital-review repair attempt ${attempt} skipped: ${repair.reason || "unknown reason"}`);
487
+ printWhy(`Iteration ${round}: repair skipped (${repair.reason || "unknown reason"}).`);
478
488
  break;
479
489
  }
480
- printWhy(`Digital-review repair attempt ${attempt} applied (${repair.fileCount} files).`);
490
+ printWhy(`Iteration ${round}: repair applied (${repair.fileCount} files). Re-validating lifecycle.`);
481
491
  lifecycle = (0, app_lifecycle_1.runAppLifecycle)(projectRoot, activeProject, {
482
492
  goalText: text,
483
493
  intentSignals: intent.signals,
484
494
  intentDomain: intent.domain,
485
495
  intentFlow: intent.flow
486
496
  });
487
- lifecycle.summary.forEach((line) => printWhy(`Lifecycle (digital-review retry ${attempt}): ${line}`));
497
+ lifecycle.summary.forEach((line) => printWhy(`Lifecycle (iteration ${round}): ${line}`));
488
498
  if (!lifecycle.qualityPassed) {
499
+ printWhy("Quality gates failed after story implementation. Applying one quality-repair pass.");
489
500
  const qualityRepair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, lifecycle.qualityDiagnostics, intent.domain);
490
501
  if (qualityRepair.attempted && qualityRepair.applied) {
491
- printWhy(`Quality regression repaired after digital review (${qualityRepair.fileCount} files). Re-validating delivery.`);
492
502
  lifecycle = (0, app_lifecycle_1.runAppLifecycle)(projectRoot, activeProject, {
493
503
  goalText: text,
494
504
  intentSignals: intent.signals,
@@ -498,7 +508,7 @@ async function runHello(input, runQuestions) {
498
508
  }
499
509
  }
500
510
  if (!lifecycle.qualityPassed) {
501
- printWhy("Delivery regressed below lifecycle quality gates during digital-review iteration.");
511
+ printWhy(`Iteration ${round}: lifecycle quality still failing.`);
502
512
  continue;
503
513
  }
504
514
  review = (0, digital_reviewers_1.runDigitalHumanReview)(appDir, {
@@ -510,16 +520,20 @@ async function runHello(input, runQuestions) {
510
520
  stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
511
521
  (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
512
522
  (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
513
- if (!review.passed) {
514
- review.diagnostics.forEach((issue) => printWhy(`Reviewer issue (retry ${attempt}): ${issue}`));
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}).`);
515
530
  }
516
531
  }
517
- if (!review.passed) {
518
- printWhy("Digital-review quality bar not met after refinement attempts.");
532
+ if (!deliveryApproved) {
533
+ printWhy("Digital-review quality bar not met after configured iterations.");
519
534
  printRecoveryNext(activeProject, "finish", text);
520
535
  return;
521
536
  }
522
- printWhy(`Digital reviewers approved delivery quality (${review.summary}).`);
523
537
  }
524
538
  (0, local_metrics_1.recordActivationMetric)("completed", {
525
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.30",
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",