@shahmarasy/prodo 0.1.3 → 0.1.4

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 (45) hide show
  1. package/dist/agents.js +4 -2
  2. package/dist/artifacts.d.ts +1 -0
  3. package/dist/artifacts.js +265 -31
  4. package/dist/cli.js +80 -3
  5. package/dist/init-tui.d.ts +3 -0
  6. package/dist/init-tui.js +28 -1
  7. package/dist/init.d.ts +1 -0
  8. package/dist/init.js +9 -3
  9. package/dist/normalize.js +55 -7
  10. package/dist/providers/openai-provider.js +2 -1
  11. package/dist/settings.d.ts +1 -0
  12. package/dist/settings.js +2 -1
  13. package/dist/templates.d.ts +1 -1
  14. package/dist/templates.js +2 -0
  15. package/dist/utils.d.ts +1 -0
  16. package/dist/utils.js +13 -0
  17. package/dist/validator.js +0 -4
  18. package/dist/workflow-commands.js +2 -1
  19. package/package.json +1 -1
  20. package/presets/fintech/preset.json +48 -1
  21. package/presets/fintech/prompts/prd.md +99 -2
  22. package/presets/marketplace/preset.json +51 -1
  23. package/presets/marketplace/prompts/prd.md +140 -2
  24. package/presets/saas/preset.json +53 -1
  25. package/presets/saas/prompts/prd.md +150 -2
  26. package/src/agents.ts +4 -2
  27. package/src/artifacts.ts +323 -28
  28. package/src/cli.ts +97 -6
  29. package/src/init-tui.ts +30 -1
  30. package/src/init.ts +11 -4
  31. package/src/normalize.ts +55 -7
  32. package/src/providers/openai-provider.ts +2 -1
  33. package/src/settings.ts +3 -2
  34. package/src/templates.ts +2 -0
  35. package/src/utils.ts +14 -0
  36. package/src/validator.ts +0 -4
  37. package/src/workflow-commands.ts +2 -1
  38. package/templates/commands/prodo-fix.md +46 -0
  39. package/templates/commands/prodo-normalize.md +118 -23
  40. package/templates/commands/prodo-prd.md +138 -17
  41. package/templates/commands/prodo-stories.md +153 -17
  42. package/templates/commands/prodo-techspec.md +167 -17
  43. package/templates/commands/prodo-validate.md +184 -26
  44. package/templates/commands/prodo-wireframe.md +188 -17
  45. package/templates/commands/prodo-workflow.md +200 -17
package/src/cli.ts CHANGED
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { loadAgentCommandSet, resolveAgent } from "./agents";
6
6
  import { resolveAi, type SupportedAi } from "./agent-command-installer";
7
- import { listArtifactTypes } from "./artifact-registry";
7
+ import { listArtifactDefinitions, listArtifactTypes } from "./artifact-registry";
8
8
  import { generateArtifact } from "./artifacts";
9
9
  import { runDoctor } from "./doctor";
10
10
  import { UserError } from "./errors";
@@ -30,10 +30,11 @@ const dynamicImport = new Function("specifier", "return import(specifier)") as (
30
30
  specifier: string
31
31
  ) => Promise<unknown>;
32
32
 
33
- function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | undefined {
33
+ function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "validate" | "normalize" | "fix" | undefined {
34
34
  if (forcedCommand === "prodo-init") return "init";
35
35
  if (forcedCommand === "prodo-validate") return "validate";
36
36
  if (forcedCommand === "prodo-normalize") return "normalize";
37
+ if (forcedCommand === "prodo-fix") return "fix";
37
38
  if (forcedCommand === "prodo-prd") return "prd";
38
39
  if (forcedCommand === "prodo-workflow") return "workflow";
39
40
  if (forcedCommand === "prodo-wireframe") return "wireframe";
@@ -44,7 +45,7 @@ function mapForcedCommand(forcedCommand: string): ArtifactType | "init" | "valid
44
45
 
45
46
  async function runArtifactCommand(
46
47
  type: ArtifactType,
47
- opts: { from?: string; out?: string; agent?: string },
48
+ opts: { from?: string; out?: string; agent?: string; revisionType?: "default" | "fix" },
48
49
  cwd: string,
49
50
  log: (message: string) => void,
50
51
  options?: { suggestValidate?: boolean }
@@ -56,7 +57,8 @@ async function runArtifactCommand(
56
57
  cwd,
57
58
  normalizedBriefOverride: opts.from,
58
59
  outPath: opts.out,
59
- agent
60
+ agent,
61
+ revisionType: opts.revisionType
60
62
  });
61
63
  const agentMsg = agent ? ` [agent=${agent}]` : "";
62
64
  log(`${type.toUpperCase()} generated${agentMsg}: ${file}`);
@@ -116,13 +118,15 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
116
118
  .command("init [target]")
117
119
  .option("--ai <name>", "agent integration: codex | gemini-cli | claude-cli")
118
120
  .option("--lang <code>", "document language (e.g. en, tr)")
121
+ .option("--author <name>", "document author name")
119
122
  .option("--preset <name>", "preset to install during initialization")
120
123
  .action(async (target, opts) => {
121
124
  const projectRoot = path.resolve(cwd, target ?? ".");
122
125
  const selected = await gatherInitSelections({
123
126
  projectRoot,
124
127
  aiInput: opts.ai,
125
- langInput: opts.lang
128
+ langInput: opts.lang,
129
+ authorInput: opts.author
126
130
  });
127
131
  const selectedAi = selected.ai as SupportedAi | undefined;
128
132
 
@@ -133,6 +137,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
133
137
  const result = await runInit(projectRoot, {
134
138
  ai: selectedAi,
135
139
  lang: selected.lang,
140
+ author: selected.author,
136
141
  preset: opts.preset,
137
142
  script: selected.script
138
143
  });
@@ -141,7 +146,8 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
141
146
  projectRoot,
142
147
  settingsPath: result.settingsPath,
143
148
  ai: selectedAi,
144
- lang: selected.lang
149
+ lang: selected.lang,
150
+ author: selected.author
145
151
  });
146
152
  return;
147
153
  }
@@ -149,6 +155,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
149
155
  const result = await runInit(projectRoot, {
150
156
  ai: selectedAi,
151
157
  lang: selected.lang,
158
+ author: selected.author,
152
159
  preset: opts.preset,
153
160
  script: selected.script
154
161
  });
@@ -161,6 +168,7 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
161
168
  out("No agent selected. Use `prodo generate` for end-to-end generation.");
162
169
  }
163
170
  out(`Settings file: ${result.settingsPath}`);
171
+ out(`Author: ${selected.author}`);
164
172
  out("Next: edit brief.md.");
165
173
  });
166
174
 
@@ -198,6 +206,56 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
198
206
  });
199
207
  });
200
208
 
209
+ program
210
+ .command("fix", { hidden: true })
211
+ .description("Advanced: auto-regenerate affected artifacts from validation findings")
212
+ .option("--agent <name>", "agent profile: codex | gemini-cli | claude-cli")
213
+ .option("--strict", "treat validation warnings as errors")
214
+ .option("--report <path>", "validation report output path")
215
+ .action(async (opts) => {
216
+ if (opts.agent) resolveAgent(opts.agent);
217
+ await withBriefReadOnlyGuard(cwd, async () => {
218
+ await runHookPhase(cwd, "before_validate", out);
219
+ const initial = await runValidate(cwd, {
220
+ strict: Boolean(opts.strict),
221
+ report: opts.report
222
+ });
223
+ out(`Validation report written to: ${initial.reportPath}`);
224
+ await runHookPhase(cwd, "after_validate", out);
225
+
226
+ if (initial.pass) {
227
+ out("No blocking issues found. Nothing to fix.");
228
+ return;
229
+ }
230
+
231
+ const targets = await resolveFixTargets(cwd, artifactTypes, initial.issues);
232
+ out(`Validation failed. Regenerating impacted artifacts: ${targets.join(", ")}`);
233
+
234
+ await runHookPhase(cwd, "before_normalize", out);
235
+ const normalizedPath = await runNormalize({ cwd });
236
+ out(`Normalized brief refreshed: ${normalizedPath}`);
237
+ await runHookPhase(cwd, "after_normalize", out);
238
+
239
+ for (const type of targets) {
240
+ await runArtifactCommand(type, { from: normalizedPath, agent: opts.agent, revisionType: "fix" }, cwd, out, {
241
+ suggestValidate: false
242
+ });
243
+ }
244
+
245
+ await runHookPhase(cwd, "before_validate", out);
246
+ const final = await runValidate(cwd, {
247
+ strict: Boolean(opts.strict),
248
+ report: opts.report
249
+ });
250
+ out(`Validation report written to: ${final.reportPath}`);
251
+ if (!final.pass) {
252
+ throw new UserError("Fix completed but validation is still failing. Review report and retry.");
253
+ }
254
+ out("Fix pipeline completed. Validation passed.");
255
+ await runHookPhase(cwd, "after_validate", out);
256
+ });
257
+ });
258
+
201
259
  program
202
260
  .command("normalize", { hidden: true })
203
261
  .description("Advanced: normalize brief without full pipeline")
@@ -294,6 +352,8 @@ export async function runCli(options: RunOptions = {}): Promise<number> {
294
352
  await program.parseAsync(["node", "prodo", "normalize", ...argv.slice(2)]);
295
353
  } else if (forced === "validate") {
296
354
  await program.parseAsync(["node", "prodo", "validate", ...argv.slice(2)]);
355
+ } else if (forced === "fix") {
356
+ await program.parseAsync(["node", "prodo", "fix", ...argv.slice(2)]);
297
357
  } else {
298
358
  await program.parseAsync(["node", "prodo", forced, ...argv.slice(2)]);
299
359
  }
@@ -317,3 +377,34 @@ if (require.main === module) {
317
377
  process.exitCode = code;
318
378
  });
319
379
  }
380
+
381
+ async function resolveFixTargets(
382
+ cwd: string,
383
+ artifactTypes: ArtifactType[],
384
+ issues: Array<{ artifactType?: ArtifactType }>
385
+ ): Promise<ArtifactType[]> {
386
+ const direct = new Set<ArtifactType>(
387
+ issues
388
+ .map((issue) => issue.artifactType)
389
+ .filter(
390
+ (artifactType): artifactType is ArtifactType =>
391
+ typeof artifactType === "string" && artifactTypes.includes(artifactType)
392
+ )
393
+ );
394
+ if (direct.size === 0) return artifactTypes;
395
+
396
+ const defs = await listArtifactDefinitions(cwd);
397
+ let changed = true;
398
+ while (changed) {
399
+ changed = false;
400
+ for (const def of defs) {
401
+ const needsRefresh = def.upstream.some((upstream) => direct.has(upstream));
402
+ if (needsRefresh && !direct.has(def.name)) {
403
+ direct.add(def.name);
404
+ changed = true;
405
+ }
406
+ }
407
+ }
408
+
409
+ return artifactTypes.filter((artifactType) => direct.has(artifactType));
410
+ }
package/src/init-tui.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import os from "node:os";
2
3
  import { resolveAi, type SupportedAi } from "./agent-command-installer";
3
4
  import { UserError } from "./errors";
4
5
  import { fileExists } from "./utils";
@@ -7,6 +8,7 @@ export type InitSelections = {
7
8
  ai?: SupportedAi;
8
9
  script: "sh" | "ps";
9
10
  lang: "tr" | "en";
11
+ author: string;
10
12
  interactive: boolean;
11
13
  };
12
14
 
@@ -14,6 +16,7 @@ type GatherInitUiOptions = {
14
16
  projectRoot: string;
15
17
  aiInput?: string;
16
18
  langInput?: string;
19
+ authorInput?: string;
17
20
  };
18
21
 
19
22
  type ClackPrompts = typeof import("@clack/prompts");
@@ -93,17 +96,31 @@ function modelChoiceFromAi(ai?: SupportedAi): "codex" | "gemini" | "auto-detect"
93
96
  return "auto-detect";
94
97
  }
95
98
 
99
+ function defaultAuthorName(authorInput?: string): string {
100
+ const explicit = (authorInput ?? "").trim();
101
+ if (explicit.length > 0) return explicit;
102
+ try {
103
+ const username = os.userInfo().username.trim();
104
+ if (username.length > 0) return username;
105
+ } catch {
106
+ // ignore lookup errors and continue with fallback
107
+ }
108
+ return "Product Author";
109
+ }
110
+
96
111
  export async function gatherInitSelections(options: GatherInitUiOptions): Promise<InitSelections> {
97
112
  const clack = await loadClack();
98
113
  const defaultLang = normalizeLang(options.langInput);
99
114
  const fallbackScript: "sh" | "ps" = process.platform === "win32" ? "ps" : "sh";
100
115
  const parsedAi = resolveAi(options.aiInput);
116
+ const defaultAuthor = defaultAuthorName(options.authorInput);
101
117
 
102
118
  if (!isInteractiveTerminal()) {
103
119
  return {
104
120
  ai: parsedAi,
105
121
  script: fallbackScript,
106
122
  lang: defaultLang,
123
+ author: defaultAuthor,
107
124
  interactive: false
108
125
  };
109
126
  }
@@ -153,6 +170,16 @@ export async function gatherInitSelections(options: GatherInitUiOptions): Promis
153
170
  throw new UserError("Initialization cancelled.");
154
171
  }
155
172
 
173
+ const author = await clack.text({
174
+ message: "Author name",
175
+ placeholder: "Shahmarasy",
176
+ defaultValue: defaultAuthor
177
+ });
178
+ if (clack.isCancel(author)) {
179
+ clack.cancel("Initialization cancelled.");
180
+ throw new UserError("Initialization cancelled.");
181
+ }
182
+
156
183
  let selectedAi: SupportedAi | undefined;
157
184
  if (model === "codex") selectedAi = "codex";
158
185
  else if (model === "gemini") selectedAi = "gemini-cli";
@@ -162,6 +189,7 @@ export async function gatherInitSelections(options: GatherInitUiOptions): Promis
162
189
  ai: selectedAi,
163
190
  script: fallbackScript,
164
191
  lang,
192
+ author: String(author).trim() || defaultAuthor,
165
193
  interactive: true
166
194
  };
167
195
  }
@@ -171,9 +199,10 @@ export function finishInitInteractive(summary: {
171
199
  settingsPath: string;
172
200
  ai?: SupportedAi;
173
201
  lang: "tr" | "en";
202
+ author: string;
174
203
  }): Promise<void> {
175
204
  const aiText = summary.ai ?? "none";
176
205
  return loadClack().then((clack) => clack.outro(
177
- `Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`
206
+ `Scaffold complete.\nAI: ${aiText}\nLanguage: ${summary.lang}\nAuthor: ${summary.author}\nSettings: ${summary.settingsPath}\nNext: edit brief.md`
178
207
  ));
179
208
  }
package/src/init.ts CHANGED
@@ -9,6 +9,7 @@ import { briefPath, outputDirPath, outputIndexPath, prodoPath } from "./paths";
9
9
  import { applyConfiguredPresets } from "./preset-loader";
10
10
  import { syncRegistry } from "./registry";
11
11
  import { writeSettings } from "./settings";
12
+ import { extractRequiredHeadingsFromTemplate } from "./template-resolver";
12
13
  import { buildWorkflowCommands } from "./workflow-commands";
13
14
  import {
14
15
  NORMALIZED_BRIEF_TEMPLATE,
@@ -283,7 +284,7 @@ function summarizeParity(items: AssetManifestItem[]): ScaffoldManifest["parity_s
283
284
 
284
285
  export async function runInit(
285
286
  cwd: string,
286
- options?: { ai?: SupportedAi; lang?: string; preset?: string; script?: "sh" | "ps" }
287
+ options?: { ai?: SupportedAi; lang?: string; author?: string; preset?: string; script?: "sh" | "ps" }
287
288
  ): Promise<{ installedAgentFiles: string[]; settingsPath: string }> {
288
289
  const root = prodoPath(cwd);
289
290
  const artifactDefs = await listArtifactDefinitions(cwd);
@@ -325,15 +326,20 @@ export async function runInit(
325
326
  const scriptType = options?.script ?? (process.platform === "win32" ? "ps" : "sh");
326
327
  await fs.writeFile(
327
328
  path.join(root, "init-options.json"),
328
- `${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`,
329
+ `${JSON.stringify({ ai: options?.ai ?? null, lang: options?.lang ?? "en", author: options?.author ?? null, preset: options?.preset ?? null, script: scriptType }, null, 2)}\n`,
329
330
  "utf8"
330
331
  );
331
332
 
332
333
  await copyDirIfMissing(path.join(projectScaffoldTemplates, "artifacts"), path.join(root, "templates"), copiedAssets);
333
334
  for (const artifact of artifactDefs) {
335
+ const markdownTemplatePath = path.join(root, "templates", `${artifact.name}.md`);
336
+ const templateHeadings =
337
+ (await fileExists(markdownTemplatePath))
338
+ ? extractRequiredHeadingsFromTemplate(await fs.readFile(markdownTemplatePath, "utf8"))
339
+ : [];
334
340
  const schema = {
335
341
  ...schemaTemplate(artifact.name),
336
- x_required_headings: artifact.required_headings
342
+ x_required_headings: templateHeadings.length > 0 ? templateHeadings : artifact.required_headings
337
343
  };
338
344
  await writeFileIfMissing(path.join(root, "schemas", `${artifact.name}.yaml`), yaml.dump(schema));
339
345
  await writeFileIfMissing(path.join(root, "prompts", `${artifact.name}.md`), `${promptTemplate(artifact.name, options?.lang ?? "en")}\n`);
@@ -385,7 +391,8 @@ export async function runInit(
385
391
  await syncRegistry(cwd);
386
392
  const settingsPath = await writeSettings(cwd, {
387
393
  lang: (options?.lang ?? "en").trim() || "en",
388
- ai: options?.ai
394
+ ai: options?.ai,
395
+ author: (options?.author ?? "").trim() || undefined
389
396
  });
390
397
  return { installedAgentFiles, settingsPath };
391
398
  }
package/src/normalize.ts CHANGED
@@ -17,6 +17,53 @@ type NormalizeOptions = {
17
17
  out?: string;
18
18
  };
19
19
 
20
+ function normalizedKey(value: string): string {
21
+ return value
22
+ .normalize("NFD")
23
+ .replace(/[\u0300-\u036f]/g, "")
24
+ .replace(/ı/g, "i")
25
+ .replace(/İ/g, "I")
26
+ .toLowerCase()
27
+ .replace(/[^a-z0-9]+/g, " ")
28
+ .trim();
29
+ }
30
+
31
+ function extractBriefProductName(rawBrief: string): string | undefined {
32
+ const lines = rawBrief.split(/\r?\n/);
33
+ for (let index = 0; index < lines.length; index += 1) {
34
+ const headingMatch = lines[index].match(/^\s*#{1,6}\s+(.+?)\s*$/);
35
+ if (!headingMatch) continue;
36
+ const headingKey = normalizedKey(headingMatch[1]);
37
+ const isProductHeading =
38
+ headingKey === "product name" ||
39
+ headingKey === "project name" ||
40
+ headingKey === "urun adi" ||
41
+ headingKey === "urun ismi";
42
+ if (!isProductHeading) continue;
43
+
44
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
45
+ const rawLine = lines[cursor].trim();
46
+ if (!rawLine) continue;
47
+ if (/^\s*#{1,6}\s+/.test(rawLine)) break;
48
+ const cleaned = rawLine.replace(/^\s*[-*]\s*/, "").trim();
49
+ if (cleaned.length > 0) return cleaned;
50
+ }
51
+ }
52
+ return undefined;
53
+ }
54
+
55
+ function preserveOriginalProductName(
56
+ parsed: Record<string, unknown>,
57
+ rawBrief: string
58
+ ): Record<string, unknown> {
59
+ const briefProductName = extractBriefProductName(rawBrief);
60
+ if (!briefProductName) return parsed;
61
+ const generated = typeof parsed.product_name === "string" ? parsed.product_name : "";
62
+ if (!generated.trim()) return { ...parsed, product_name: briefProductName };
63
+ if (normalizedKey(generated) !== normalizedKey(briefProductName)) return parsed;
64
+ return { ...parsed, product_name: briefProductName };
65
+ }
66
+
20
67
  function extractJsonObject(raw: string): Record<string, unknown> {
21
68
  const trimmed = raw.trim();
22
69
  const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
@@ -61,17 +108,18 @@ export async function runNormalize(options: NormalizeOptions): Promise<string> {
61
108
  );
62
109
 
63
110
  const parsed = extractJsonObject(generated.body);
111
+ const preserved = preserveOriginalProductName(parsed, rawBrief);
64
112
  const withContracts = {
65
- ...parsed,
113
+ ...preserved,
66
114
  contracts:
67
- parsed.contracts ??
115
+ preserved.contracts ??
68
116
  buildContractsFromArrays({
69
- goals: Array.isArray(parsed.goals) ? parsed.goals.filter((x): x is string => typeof x === "string") : [],
70
- core_features: Array.isArray(parsed.core_features)
71
- ? parsed.core_features.filter((x): x is string => typeof x === "string")
117
+ goals: Array.isArray(preserved.goals) ? preserved.goals.filter((x): x is string => typeof x === "string") : [],
118
+ core_features: Array.isArray(preserved.core_features)
119
+ ? preserved.core_features.filter((x): x is string => typeof x === "string")
72
120
  : [],
73
- constraints: Array.isArray(parsed.constraints)
74
- ? parsed.constraints.filter((x): x is string => typeof x === "string")
121
+ constraints: Array.isArray(preserved.constraints)
122
+ ? preserved.constraints.filter((x): x is string => typeof x === "string")
75
123
  : []
76
124
  })
77
125
  };
@@ -37,7 +37,8 @@ export class OpenAIProvider implements LLMProvider {
37
37
  const system =
38
38
  mode === "normalize"
39
39
  ? `You normalize messy human product briefs into strict JSON.
40
- Return valid JSON only, no markdown. Include confidence scores (0..1) for critical fields.`
40
+ Return valid JSON only, no markdown. Include confidence scores (0..1) for critical fields.
41
+ Preserve source language and Unicode characters exactly; never transliterate Turkish letters to ASCII.`
41
42
  : mode === "semantic_consistency"
42
43
  ? `You detect semantic inconsistencies between paired artifacts.
43
44
  Return valid JSON only: { "issues": [{level, code, check, contract_id, file, message, suggestion}] }.`
package/src/settings.ts CHANGED
@@ -5,6 +5,7 @@ import { fileExists } from "./utils";
5
5
  export type ProdoSettings = {
6
6
  lang: string;
7
7
  ai?: string;
8
+ author?: string;
8
9
  };
9
10
 
10
11
  const DEFAULT_SETTINGS: ProdoSettings = {
@@ -19,7 +20,8 @@ export async function readSettings(cwd: string): Promise<ProdoSettings> {
19
20
  const parsed = JSON.parse(raw) as Partial<ProdoSettings>;
20
21
  return {
21
22
  lang: typeof parsed.lang === "string" && parsed.lang.trim() ? parsed.lang.trim() : "en",
22
- ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined
23
+ ai: typeof parsed.ai === "string" && parsed.ai.trim() ? parsed.ai.trim() : undefined,
24
+ author: typeof parsed.author === "string" && parsed.author.trim() ? parsed.author.trim() : undefined
23
25
  };
24
26
  } catch {
25
27
  return { ...DEFAULT_SETTINGS };
@@ -31,4 +33,3 @@ export async function writeSettings(cwd: string, settings: ProdoSettings): Promi
31
33
  await fs.writeFile(path, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
32
34
  return path;
33
35
  }
34
-
package/src/templates.ts CHANGED
@@ -347,6 +347,8 @@ Return JSON object with keys:
347
347
  Rules:
348
348
  - do NOT invent missing critical content
349
349
  - keep wording concise and concrete
350
+ - preserve original language and Unicode characters exactly from brief
351
+ - never transliterate Turkish letters (ç, ğ, ı, İ, ö, ş, ü) into ASCII
350
352
  - if critical field is missing, return empty and low confidence (<0.7)
351
353
  - assign deterministic IDs: goals => G1..Gn, features => F1..Fn, constraints => C1..Cn
352
354
  - input files are read-only; never modify, summarize, or rewrite \`brief.md\` in-place
package/src/utils.ts CHANGED
@@ -23,6 +23,20 @@ export function timestampSlug(date = new Date()): string {
23
23
  return date.toISOString().replace(/[:.]/g, "-");
24
24
  }
25
25
 
26
+ function pad2(value: number): string {
27
+ return String(value).padStart(2, "0");
28
+ }
29
+
30
+ export function artifactFileStamp(date = new Date()): string {
31
+ const year = date.getFullYear();
32
+ const month = pad2(date.getMonth() + 1);
33
+ const day = pad2(date.getDate());
34
+ const hour = pad2(date.getHours());
35
+ const minute = pad2(date.getMinutes());
36
+ const second = pad2(date.getSeconds());
37
+ return `${year}${month}${day}-${hour}${minute}${second}`;
38
+ }
39
+
26
40
  export async function listFilesSortedByMtime(dirPath: string): Promise<string[]> {
27
41
  const exists = await fileExists(dirPath);
28
42
  if (!exists) return [];
package/src/validator.ts CHANGED
@@ -59,10 +59,6 @@ export async function validateSchema(
59
59
  }
60
60
 
61
61
  const sections = sectionTextMap(doc.body);
62
- const trMode = String((doc.frontmatter as Record<string, unknown>).language ?? "").toLowerCase().startsWith("tr");
63
- if (trMode) {
64
- return { issues, requiredHeadings: [] };
65
- }
66
62
  for (const heading of requiredHeadings) {
67
63
  if (!doc.body.includes(heading)) {
68
64
  issues.push({
@@ -11,7 +11,8 @@ const BASE_WORKFLOW_COMMANDS: WorkflowCommand[] = [
11
11
  { name: "prodo-wireframe", cliSubcommand: "wireframe", description: "Generate wireframe artifact." },
12
12
  { name: "prodo-stories", cliSubcommand: "stories", description: "Generate stories artifact." },
13
13
  { name: "prodo-techspec", cliSubcommand: "techspec", description: "Generate technical specification artifact." },
14
- { name: "prodo-validate", cliSubcommand: "validate", description: "Run schema and cross-artifact consistency validation." }
14
+ { name: "prodo-validate", cliSubcommand: "validate", description: "Run schema and cross-artifact consistency validation." },
15
+ { name: "prodo-fix", cliSubcommand: "fix", description: "Auto-fix artifacts based on validation report and brief." }
15
16
  ];
16
17
 
17
18
  export const WORKFLOW_COMMANDS: WorkflowCommand[] = BASE_WORKFLOW_COMMANDS;
@@ -0,0 +1,46 @@
1
+ ---
2
+ description: Fix failing artifacts using latest validation report and brief-aligned regeneration.
3
+ agent-role: "Recovery Engineer"
4
+ ---
5
+
6
+ ## User Input
7
+
8
+ ```text
9
+ $ARGUMENTS
10
+ ```
11
+
12
+ ## Execution Policy
13
+
14
+ - Execute-first, diagnose-second.
15
+ - Do not run shell commands or CLI commands from inside the agent.
16
+ - Never invoke `prodo-fix`, `prodo fix`, or any `prodo ...` command in shell.
17
+ - Input files are read-only; never modify `brief.md`.
18
+ - Write outputs only under `product-docs/` and `.prodo/`.
19
+ - Do not print full artifact bodies in chat.
20
+
21
+ ## Execution
22
+
23
+ 1. Minimum prerequisites only:
24
+ - `.prodo/` exists
25
+ - `brief.md` exists
26
+ - `.prodo/briefs/normalized-brief.json` exists (refresh if stale)
27
+ - Validation report exists in `product-docs/reports/`
28
+
29
+ 2. Execute fix process immediately:
30
+ - Read latest validation report.
31
+ - Identify failing artifact types and regenerate only impacted chain.
32
+ - Use normalized brief + active templates for regeneration.
33
+
34
+ 3. Verify result:
35
+ - Re-run validation.
36
+ - Confirm report status is PASS.
37
+ - Confirm `brief.md` remained unchanged.
38
+
39
+ 4. Diagnose only on failure:
40
+ - Inspect internal hooks/scripts only after fix attempt fails.
41
+ - Return concise root cause + next repair action.
42
+
43
+ ## Handoff
44
+
45
+ If pass: suggest next command `/prodo-validate`.
46
+ If fail: suggest highest-priority artifact command to regenerate manually.