@shahmarasy/prodo 0.1.3 → 0.1.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.
Files changed (205) hide show
  1. package/README.md +201 -97
  2. package/bin/prodo.cjs +6 -6
  3. package/dist/agents/agent-registry.d.ts +13 -0
  4. package/dist/agents/agent-registry.js +79 -0
  5. package/dist/agents/anthropic/index.d.ts +9 -0
  6. package/dist/agents/anthropic/index.js +55 -0
  7. package/dist/agents/base.d.ts +25 -0
  8. package/dist/agents/base.js +71 -0
  9. package/dist/agents/google/index.d.ts +9 -0
  10. package/dist/agents/google/index.js +53 -0
  11. package/dist/agents/mock/index.d.ts +11 -0
  12. package/dist/agents/mock/index.js +26 -0
  13. package/dist/agents/openai/index.d.ts +9 -0
  14. package/dist/agents/openai/index.js +57 -0
  15. package/dist/agents/system-prompts.d.ts +3 -0
  16. package/dist/agents/system-prompts.js +32 -0
  17. package/dist/agents.js +4 -2
  18. package/dist/artifacts.d.ts +1 -0
  19. package/dist/artifacts.js +265 -31
  20. package/dist/cli/agent-command-installer.d.ts +4 -0
  21. package/dist/cli/agent-command-installer.js +148 -0
  22. package/dist/cli/agent-ids.d.ts +15 -0
  23. package/dist/cli/agent-ids.js +49 -0
  24. package/dist/cli/doctor.d.ts +1 -0
  25. package/dist/cli/doctor.js +144 -0
  26. package/dist/cli/fix-tui.d.ts +4 -0
  27. package/dist/cli/fix-tui.js +79 -0
  28. package/dist/cli/index.d.ts +9 -0
  29. package/dist/cli/index.js +465 -0
  30. package/dist/cli/init-tui.d.ts +23 -0
  31. package/dist/cli/init-tui.js +176 -0
  32. package/dist/cli/init.d.ts +11 -0
  33. package/dist/cli/init.js +334 -0
  34. package/dist/cli/normalize-interactive.d.ts +8 -0
  35. package/dist/cli/normalize-interactive.js +167 -0
  36. package/dist/cli/preset-loader.d.ts +4 -0
  37. package/dist/cli/preset-loader.js +210 -0
  38. package/dist/cli.js +80 -3
  39. package/dist/core/artifact-registry.d.ts +11 -0
  40. package/dist/core/artifact-registry.js +49 -0
  41. package/dist/core/artifacts.d.ts +10 -0
  42. package/dist/core/artifacts.js +892 -0
  43. package/dist/core/clean.d.ts +10 -0
  44. package/dist/core/clean.js +74 -0
  45. package/dist/core/consistency.d.ts +8 -0
  46. package/dist/core/consistency.js +328 -0
  47. package/dist/core/constants.d.ts +7 -0
  48. package/dist/core/constants.js +64 -0
  49. package/dist/core/errors.d.ts +3 -0
  50. package/dist/core/errors.js +10 -0
  51. package/dist/core/fix.d.ts +31 -0
  52. package/dist/core/fix.js +188 -0
  53. package/dist/core/hook-executor.d.ts +1 -0
  54. package/dist/core/hook-executor.js +175 -0
  55. package/dist/core/markdown.d.ts +16 -0
  56. package/dist/core/markdown.js +81 -0
  57. package/dist/core/normalize.d.ts +8 -0
  58. package/dist/core/normalize.js +125 -0
  59. package/dist/core/normalized-brief.d.ts +48 -0
  60. package/dist/core/normalized-brief.js +182 -0
  61. package/dist/core/output-index.d.ts +13 -0
  62. package/dist/core/output-index.js +55 -0
  63. package/dist/core/paths.d.ts +17 -0
  64. package/dist/core/paths.js +80 -0
  65. package/dist/core/project-config.d.ts +14 -0
  66. package/dist/core/project-config.js +69 -0
  67. package/dist/core/registry.d.ts +13 -0
  68. package/dist/core/registry.js +115 -0
  69. package/dist/core/settings.d.ts +7 -0
  70. package/dist/core/settings.js +35 -0
  71. package/dist/core/template-engine.d.ts +3 -0
  72. package/dist/core/template-engine.js +43 -0
  73. package/dist/core/template-resolver.d.ts +15 -0
  74. package/dist/core/template-resolver.js +46 -0
  75. package/dist/core/templates.d.ts +33 -0
  76. package/dist/core/templates.js +440 -0
  77. package/dist/core/terminology.d.ts +21 -0
  78. package/dist/core/terminology.js +143 -0
  79. package/dist/core/tracing.d.ts +21 -0
  80. package/dist/core/tracing.js +74 -0
  81. package/dist/core/types.d.ts +35 -0
  82. package/dist/core/types.js +5 -0
  83. package/dist/core/utils.d.ts +7 -0
  84. package/dist/core/utils.js +66 -0
  85. package/dist/core/validate.d.ts +10 -0
  86. package/dist/core/validate.js +226 -0
  87. package/dist/core/validator.d.ts +5 -0
  88. package/dist/core/validator.js +76 -0
  89. package/dist/core/version.d.ts +1 -0
  90. package/dist/core/version.js +30 -0
  91. package/dist/core/workflow-commands.d.ts +7 -0
  92. package/dist/core/workflow-commands.js +29 -0
  93. package/dist/i18n/en.json +45 -0
  94. package/dist/i18n/index.d.ts +5 -0
  95. package/dist/i18n/index.js +63 -0
  96. package/dist/i18n/tr.json +45 -0
  97. package/dist/init-tui.d.ts +3 -0
  98. package/dist/init-tui.js +28 -1
  99. package/dist/init.d.ts +1 -0
  100. package/dist/init.js +9 -3
  101. package/dist/normalize.js +55 -7
  102. package/dist/providers/index.d.ts +2 -1
  103. package/dist/providers/index.js +20 -6
  104. package/dist/providers/mock-provider.d.ts +1 -1
  105. package/dist/providers/mock-provider.js +7 -6
  106. package/dist/providers/openai-provider.d.ts +1 -1
  107. package/dist/providers/openai-provider.js +3 -2
  108. package/dist/settings.d.ts +1 -0
  109. package/dist/settings.js +2 -1
  110. package/dist/skills/engine.d.ts +10 -0
  111. package/dist/skills/engine.js +75 -0
  112. package/dist/skills/fix-skill.d.ts +2 -0
  113. package/dist/skills/fix-skill.js +38 -0
  114. package/dist/skills/generate-artifact-skill.d.ts +2 -0
  115. package/dist/skills/generate-artifact-skill.js +32 -0
  116. package/dist/skills/generate-pipeline-skill.d.ts +2 -0
  117. package/dist/skills/generate-pipeline-skill.js +45 -0
  118. package/dist/skills/normalize-skill.d.ts +2 -0
  119. package/dist/skills/normalize-skill.js +29 -0
  120. package/dist/skills/types.d.ts +28 -0
  121. package/dist/skills/types.js +2 -0
  122. package/dist/skills/validate-skill.d.ts +2 -0
  123. package/dist/skills/validate-skill.js +29 -0
  124. package/dist/templates.d.ts +1 -1
  125. package/dist/templates.js +2 -0
  126. package/dist/utils.d.ts +1 -0
  127. package/dist/utils.js +13 -0
  128. package/dist/validator.js +0 -4
  129. package/dist/workflow-commands.js +2 -1
  130. package/package.json +74 -45
  131. package/presets/fintech/preset.json +48 -1
  132. package/presets/fintech/prompts/prd.md +99 -2
  133. package/presets/marketplace/preset.json +51 -1
  134. package/presets/marketplace/prompts/prd.md +140 -2
  135. package/presets/saas/preset.json +53 -1
  136. package/presets/saas/prompts/prd.md +150 -2
  137. package/src/agents/agent-registry.ts +93 -0
  138. package/src/agents/anthropic/index.ts +86 -0
  139. package/src/agents/anthropic/manifest.json +7 -0
  140. package/src/agents/base.ts +77 -0
  141. package/src/agents/google/index.ts +79 -0
  142. package/src/agents/google/manifest.json +7 -0
  143. package/src/agents/mock/index.ts +32 -0
  144. package/src/agents/mock/manifest.json +7 -0
  145. package/src/agents/openai/index.ts +83 -0
  146. package/src/agents/openai/manifest.json +7 -0
  147. package/src/agents/system-prompts.ts +35 -0
  148. package/src/{agent-command-installer.ts → cli/agent-command-installer.ts} +164 -164
  149. package/src/{agents.ts → cli/agent-ids.ts} +58 -56
  150. package/src/{doctor.ts → cli/doctor.ts} +157 -137
  151. package/src/cli/fix-tui.ts +111 -0
  152. package/src/{cli.ts → cli/index.ts} +459 -319
  153. package/src/{init-tui.ts → cli/init-tui.ts} +208 -179
  154. package/src/{init.ts → cli/init.ts} +398 -391
  155. package/src/cli/normalize-interactive.ts +241 -0
  156. package/src/{preset-loader.ts → cli/preset-loader.ts} +237 -237
  157. package/src/{artifact-registry.ts → core/artifact-registry.ts} +69 -69
  158. package/src/{artifacts.ts → core/artifacts.ts} +1081 -777
  159. package/src/core/clean.ts +88 -0
  160. package/src/{consistency.ts → core/consistency.ts} +374 -303
  161. package/src/{constants.ts → core/constants.ts} +72 -72
  162. package/src/{errors.ts → core/errors.ts} +7 -7
  163. package/src/core/fix.ts +253 -0
  164. package/src/{hook-executor.ts → core/hook-executor.ts} +196 -196
  165. package/src/{markdown.ts → core/markdown.ts} +93 -73
  166. package/src/core/normalize.ts +145 -0
  167. package/src/{normalized-brief.ts → core/normalized-brief.ts} +227 -206
  168. package/src/{output-index.ts → core/output-index.ts} +59 -59
  169. package/src/{paths.ts → core/paths.ts} +75 -71
  170. package/src/{project-config.ts → core/project-config.ts} +78 -78
  171. package/src/{registry.ts → core/registry.ts} +119 -119
  172. package/src/{settings.ts → core/settings.ts} +35 -34
  173. package/src/core/template-engine.ts +45 -0
  174. package/src/{template-resolver.ts → core/template-resolver.ts} +54 -54
  175. package/src/{templates.ts → core/templates.ts} +452 -450
  176. package/src/core/terminology.ts +177 -0
  177. package/src/core/tracing.ts +110 -0
  178. package/src/{types.ts → core/types.ts} +46 -46
  179. package/src/{utils.ts → core/utils.ts} +64 -50
  180. package/src/{validate.ts → core/validate.ts} +252 -246
  181. package/src/{validator.ts → core/validator.ts} +92 -96
  182. package/src/{version.ts → core/version.ts} +24 -24
  183. package/src/{workflow-commands.ts → core/workflow-commands.ts} +32 -31
  184. package/src/i18n/en.json +45 -0
  185. package/src/i18n/index.ts +58 -0
  186. package/src/i18n/tr.json +45 -0
  187. package/src/providers/index.ts +29 -12
  188. package/src/providers/mock-provider.ts +200 -199
  189. package/src/providers/openai-provider.ts +88 -87
  190. package/src/skills/engine.ts +94 -0
  191. package/src/skills/fix-skill.ts +38 -0
  192. package/src/skills/generate-artifact-skill.ts +32 -0
  193. package/src/skills/generate-pipeline-skill.ts +49 -0
  194. package/src/skills/normalize-skill.ts +29 -0
  195. package/src/skills/types.ts +36 -0
  196. package/src/skills/validate-skill.ts +29 -0
  197. package/templates/commands/prodo-fix.md +46 -0
  198. package/templates/commands/prodo-normalize.md +118 -23
  199. package/templates/commands/prodo-prd.md +138 -17
  200. package/templates/commands/prodo-stories.md +153 -17
  201. package/templates/commands/prodo-techspec.md +167 -17
  202. package/templates/commands/prodo-validate.md +184 -26
  203. package/templates/commands/prodo-wireframe.md +188 -17
  204. package/templates/commands/prodo-workflow.md +200 -17
  205. package/src/normalize.ts +0 -89
@@ -1,196 +1,196 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { spawn } from "node:child_process";
4
- import yaml from "js-yaml";
5
- import { UserError } from "./errors";
6
- import { fileExists } from "./utils";
7
-
8
- type HookItem = {
9
- command?: string;
10
- optional?: boolean;
11
- enabled?: boolean;
12
- description?: string;
13
- prompt?: string;
14
- extension?: string;
15
- condition?: string;
16
- timeout_ms?: number;
17
- retry?: number;
18
- retry_delay_ms?: number;
19
- };
20
-
21
- type HooksConfig = {
22
- hooks?: Record<string, HookItem[]>;
23
- };
24
-
25
- function hooksPath(cwd: string): string {
26
- return path.join(cwd, ".prodo", "hooks.yml");
27
- }
28
-
29
- async function runShellCommand(
30
- command: string,
31
- cwd: string,
32
- timeoutMs: number
33
- ): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
34
- const parsed = parseCommand(command);
35
- if (!parsed) {
36
- return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
37
- }
38
- return new Promise((resolve) => {
39
- const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
40
- let stdout = "";
41
- let stderr = "";
42
- let timedOut = false;
43
- const timer = setTimeout(() => {
44
- timedOut = true;
45
- child.kill();
46
- }, Math.max(1000, timeoutMs));
47
-
48
- child.stdout.on("data", (chunk) => {
49
- stdout += chunk.toString();
50
- });
51
- child.stderr.on("data", (chunk) => {
52
- stderr += chunk.toString();
53
- });
54
- child.on("close", (code) => {
55
- clearTimeout(timer);
56
- resolve({ code: code ?? 1, stdout, stderr, timedOut });
57
- });
58
- });
59
- }
60
-
61
- function parseCommand(command: string): { bin: string; args: string[] } | null {
62
- const src = command.trim();
63
- if (!src) return null;
64
- const out: string[] = [];
65
- let current = "";
66
- let quote: '"' | "'" | null = null;
67
- let escaping = false;
68
- for (let i = 0; i < src.length; i += 1) {
69
- const ch = src[i];
70
- if (escaping) {
71
- current += ch;
72
- escaping = false;
73
- continue;
74
- }
75
- if (ch === "\\") {
76
- escaping = true;
77
- continue;
78
- }
79
- if (quote) {
80
- if (ch === quote) {
81
- quote = null;
82
- } else {
83
- current += ch;
84
- }
85
- continue;
86
- }
87
- if (ch === "'" || ch === '"') {
88
- quote = ch;
89
- continue;
90
- }
91
- if (/\s/.test(ch)) {
92
- if (current.length > 0) {
93
- out.push(current);
94
- current = "";
95
- }
96
- continue;
97
- }
98
- current += ch;
99
- }
100
- if (escaping || quote) return null;
101
- if (current.length > 0) out.push(current);
102
- if (out.length === 0) return null;
103
- return { bin: out[0], args: out.slice(1) };
104
- }
105
-
106
- function toPositiveInt(value: unknown, fallback: number): number {
107
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
108
- const normalized = Math.floor(value);
109
- return normalized > 0 ? normalized : fallback;
110
- }
111
-
112
- function toNonNegativeInt(value: unknown, fallback: number): number {
113
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
114
- const normalized = Math.floor(value);
115
- return normalized >= 0 ? normalized : fallback;
116
- }
117
-
118
- async function sleep(ms: number): Promise<void> {
119
- await new Promise((resolve) => setTimeout(resolve, ms));
120
- }
121
-
122
- async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
123
- const trimmed = condition.trim();
124
- if (!trimmed) return true;
125
- const result = await runShellCommand(trimmed, cwd, 10_000);
126
- return !result.timedOut && result.code === 0;
127
- }
128
-
129
- async function readHooks(cwd: string): Promise<HooksConfig | null> {
130
- const file = hooksPath(cwd);
131
- if (!(await fileExists(file))) return null;
132
- try {
133
- const raw = await fs.readFile(file, "utf8");
134
- const parsed = yaml.load(raw) as HooksConfig;
135
- return parsed ?? null;
136
- } catch {
137
- return null;
138
- }
139
- }
140
-
141
- export async function runHookPhase(
142
- cwd: string,
143
- phaseKey: string,
144
- log: (message: string) => void
145
- ): Promise<void> {
146
- const config = await readHooks(cwd);
147
- const phaseHooks = config?.hooks?.[phaseKey];
148
- if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
149
-
150
- for (const hook of phaseHooks) {
151
- if (hook?.enabled === false) continue;
152
- const command = typeof hook?.command === "string" ? hook.command.trim() : "";
153
- if (!command) continue;
154
- if (typeof hook.condition === "string" && hook.condition.trim()) {
155
- const pass = await evaluateCondition(hook.condition, cwd);
156
- if (!pass) {
157
- log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
158
- continue;
159
- }
160
- }
161
-
162
- const label = hook.extension || hook.description || command;
163
- if (hook.optional) {
164
- log(`[Hook:optional:${phaseKey}] ${label}`);
165
- if (hook.prompt) log(` Prompt: ${hook.prompt}`);
166
- log(` To run manually: ${command}`);
167
- continue;
168
- }
169
-
170
- const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
171
- const retries = toNonNegativeInt(hook.retry, 0);
172
- const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
173
- const attempts = 1 + retries;
174
-
175
- let lastDetail = "";
176
- for (let attempt = 1; attempt <= attempts; attempt += 1) {
177
- log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
178
- const result = await runShellCommand(command, cwd, timeoutMs);
179
- if (!result.timedOut && result.code === 0) {
180
- lastDetail = "";
181
- break;
182
- }
183
-
184
- const stderr = result.stderr.trim();
185
- const stdout = result.stdout.trim();
186
- lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
187
- if (attempt < attempts) {
188
- await sleep(retryDelayMs);
189
- }
190
- }
191
-
192
- if (lastDetail) {
193
- throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
194
- }
195
- }
196
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import yaml from "js-yaml";
5
+ import { UserError } from "./errors";
6
+ import { fileExists } from "./utils";
7
+
8
+ type HookItem = {
9
+ command?: string;
10
+ optional?: boolean;
11
+ enabled?: boolean;
12
+ description?: string;
13
+ prompt?: string;
14
+ extension?: string;
15
+ condition?: string;
16
+ timeout_ms?: number;
17
+ retry?: number;
18
+ retry_delay_ms?: number;
19
+ };
20
+
21
+ type HooksConfig = {
22
+ hooks?: Record<string, HookItem[]>;
23
+ };
24
+
25
+ function hooksPath(cwd: string): string {
26
+ return path.join(cwd, ".prodo", "hooks.yml");
27
+ }
28
+
29
+ async function runShellCommand(
30
+ command: string,
31
+ cwd: string,
32
+ timeoutMs: number
33
+ ): Promise<{ code: number; stdout: string; stderr: string; timedOut: boolean }> {
34
+ const parsed = parseCommand(command);
35
+ if (!parsed) {
36
+ return { code: 1, stdout: "", stderr: "Invalid hook command syntax.", timedOut: false };
37
+ }
38
+ return new Promise((resolve) => {
39
+ const child = spawn(parsed.bin, parsed.args, { cwd, shell: false, stdio: ["ignore", "pipe", "pipe"] });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ let timedOut = false;
43
+ const timer = setTimeout(() => {
44
+ timedOut = true;
45
+ child.kill();
46
+ }, Math.max(1000, timeoutMs));
47
+
48
+ child.stdout.on("data", (chunk) => {
49
+ stdout += chunk.toString();
50
+ });
51
+ child.stderr.on("data", (chunk) => {
52
+ stderr += chunk.toString();
53
+ });
54
+ child.on("close", (code) => {
55
+ clearTimeout(timer);
56
+ resolve({ code: code ?? 1, stdout, stderr, timedOut });
57
+ });
58
+ });
59
+ }
60
+
61
+ function parseCommand(command: string): { bin: string; args: string[] } | null {
62
+ const src = command.trim();
63
+ if (!src) return null;
64
+ const out: string[] = [];
65
+ let current = "";
66
+ let quote: '"' | "'" | null = null;
67
+ let escaping = false;
68
+ for (let i = 0; i < src.length; i += 1) {
69
+ const ch = src[i];
70
+ if (escaping) {
71
+ current += ch;
72
+ escaping = false;
73
+ continue;
74
+ }
75
+ if (ch === "\\") {
76
+ escaping = true;
77
+ continue;
78
+ }
79
+ if (quote) {
80
+ if (ch === quote) {
81
+ quote = null;
82
+ } else {
83
+ current += ch;
84
+ }
85
+ continue;
86
+ }
87
+ if (ch === "'" || ch === '"') {
88
+ quote = ch;
89
+ continue;
90
+ }
91
+ if (/\s/.test(ch)) {
92
+ if (current.length > 0) {
93
+ out.push(current);
94
+ current = "";
95
+ }
96
+ continue;
97
+ }
98
+ current += ch;
99
+ }
100
+ if (escaping || quote) return null;
101
+ if (current.length > 0) out.push(current);
102
+ if (out.length === 0) return null;
103
+ return { bin: out[0], args: out.slice(1) };
104
+ }
105
+
106
+ function toPositiveInt(value: unknown, fallback: number): number {
107
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
108
+ const normalized = Math.floor(value);
109
+ return normalized > 0 ? normalized : fallback;
110
+ }
111
+
112
+ function toNonNegativeInt(value: unknown, fallback: number): number {
113
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
114
+ const normalized = Math.floor(value);
115
+ return normalized >= 0 ? normalized : fallback;
116
+ }
117
+
118
+ async function sleep(ms: number): Promise<void> {
119
+ await new Promise((resolve) => setTimeout(resolve, ms));
120
+ }
121
+
122
+ async function evaluateCondition(condition: string, cwd: string): Promise<boolean> {
123
+ const trimmed = condition.trim();
124
+ if (!trimmed) return true;
125
+ const result = await runShellCommand(trimmed, cwd, 10_000);
126
+ return !result.timedOut && result.code === 0;
127
+ }
128
+
129
+ async function readHooks(cwd: string): Promise<HooksConfig | null> {
130
+ const file = hooksPath(cwd);
131
+ if (!(await fileExists(file))) return null;
132
+ try {
133
+ const raw = await fs.readFile(file, "utf8");
134
+ const parsed = yaml.load(raw) as HooksConfig;
135
+ return parsed ?? null;
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ export async function runHookPhase(
142
+ cwd: string,
143
+ phaseKey: string,
144
+ log: (message: string) => void
145
+ ): Promise<void> {
146
+ const config = await readHooks(cwd);
147
+ const phaseHooks = config?.hooks?.[phaseKey];
148
+ if (!Array.isArray(phaseHooks) || phaseHooks.length === 0) return;
149
+
150
+ for (const hook of phaseHooks) {
151
+ if (hook?.enabled === false) continue;
152
+ const command = typeof hook?.command === "string" ? hook.command.trim() : "";
153
+ if (!command) continue;
154
+ if (typeof hook.condition === "string" && hook.condition.trim()) {
155
+ const pass = await evaluateCondition(hook.condition, cwd);
156
+ if (!pass) {
157
+ log(`[Hook:skipped:${phaseKey}] condition=false for ${command}`);
158
+ continue;
159
+ }
160
+ }
161
+
162
+ const label = hook.extension || hook.description || command;
163
+ if (hook.optional) {
164
+ log(`[Hook:optional:${phaseKey}] ${label}`);
165
+ if (hook.prompt) log(` Prompt: ${hook.prompt}`);
166
+ log(` To run manually: ${command}`);
167
+ continue;
168
+ }
169
+
170
+ const timeoutMs = toPositiveInt(hook.timeout_ms, 30_000);
171
+ const retries = toNonNegativeInt(hook.retry, 0);
172
+ const retryDelayMs = toNonNegativeInt(hook.retry_delay_ms, 500);
173
+ const attempts = 1 + retries;
174
+
175
+ let lastDetail = "";
176
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
177
+ log(`[Hook:mandatory:${phaseKey}] Running (attempt ${attempt}/${attempts}): ${command}`);
178
+ const result = await runShellCommand(command, cwd, timeoutMs);
179
+ if (!result.timedOut && result.code === 0) {
180
+ lastDetail = "";
181
+ break;
182
+ }
183
+
184
+ const stderr = result.stderr.trim();
185
+ const stdout = result.stdout.trim();
186
+ lastDetail = result.timedOut ? `Timed out after ${timeoutMs}ms` : stderr || stdout || "unknown error";
187
+ if (attempt < attempts) {
188
+ await sleep(retryDelayMs);
189
+ }
190
+ }
191
+
192
+ if (lastDetail) {
193
+ throw new UserError(`Mandatory hook failed (${phaseKey}): ${command}\n${lastDetail}`);
194
+ }
195
+ }
196
+ }
@@ -1,73 +1,93 @@
1
- export type MarkdownSection = {
2
- heading: string;
3
- headingKey: string;
4
- level: number;
5
- textLines: string[];
6
- listItems: string[];
7
- };
8
-
9
- function normalizeText(input: string): string {
10
- return input.trim().replace(/\s+/g, " ");
11
- }
12
-
13
- export function normalizeHeadingKey(heading: string): string {
14
- return normalizeText(heading)
15
- .toLowerCase()
16
- .replace(/[^a-z0-9\s]/g, " ")
17
- .replace(/\s+/g, " ")
18
- .trim();
19
- }
20
-
21
- export function parseMarkdownSections(markdown: string): MarkdownSection[] {
22
- const sections: MarkdownSection[] = [];
23
- let current: MarkdownSection | null = null;
24
-
25
- for (const rawLine of markdown.split(/\r?\n/)) {
26
- const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
27
- if (headingMatch) {
28
- const title = normalizeText(headingMatch[2]);
29
- current = {
30
- heading: title,
31
- headingKey: normalizeHeadingKey(title),
32
- level: headingMatch[1].length,
33
- textLines: [],
34
- listItems: []
35
- };
36
- sections.push(current);
37
- continue;
38
- }
39
-
40
- if (!current) continue;
41
- const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
42
- if (listMatch) {
43
- const item = normalizeText(listMatch[1]);
44
- if (item.length > 0 && !current.listItems.includes(item)) current.listItems.push(item);
45
- if (item.length > 0) current.textLines.push(item);
46
- continue;
47
- }
48
-
49
- const text = normalizeText(rawLine);
50
- if (text.length > 0) current.textLines.push(text);
51
- }
52
-
53
- return sections;
54
- }
55
-
56
- export function extractRequiredHeadings(content: string): string[] {
57
- const sections = parseMarkdownSections(content);
58
- return sections
59
- .filter((section) => section.level === 2)
60
- .map((section) => `## ${section.heading}`)
61
- .filter((heading) => heading.length > 3);
62
- }
63
-
64
- export function sectionTextMap(content: string): Map<string, string> {
65
- const sections = parseMarkdownSections(content);
66
- const mapped = new Map<string, string>();
67
- for (const section of sections) {
68
- const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
69
- mapped.set(`## ${section.heading}`, parts.join("\n").trim());
70
- }
71
- return mapped;
72
- }
73
-
1
+ export type MarkdownSection = {
2
+ heading: string;
3
+ headingKey: string;
4
+ level: number;
5
+ textLines: string[];
6
+ listItems: string[];
7
+ };
8
+
9
+ function normalizeText(input: string): string {
10
+ return input.trim().replace(/\s+/g, " ");
11
+ }
12
+
13
+ export function normalizeHeadingKey(heading: string): string {
14
+ return normalizeText(heading)
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9\s]/g, " ")
17
+ .replace(/\s+/g, " ")
18
+ .trim();
19
+ }
20
+
21
+ export function parseMarkdownSections(markdown: string): MarkdownSection[] {
22
+ const sections: MarkdownSection[] = [];
23
+ let current: MarkdownSection | null = null;
24
+
25
+ for (const rawLine of markdown.split(/\r?\n/)) {
26
+ const headingMatch = rawLine.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
27
+ if (headingMatch) {
28
+ const title = normalizeText(headingMatch[2]);
29
+ current = {
30
+ heading: title,
31
+ headingKey: normalizeHeadingKey(title),
32
+ level: headingMatch[1].length,
33
+ textLines: [],
34
+ listItems: []
35
+ };
36
+ sections.push(current);
37
+ continue;
38
+ }
39
+
40
+ if (!current) continue;
41
+ const listMatch = rawLine.match(/^\s*(?:[-*+]|\d+\.)\s+(.+?)\s*$/);
42
+ if (listMatch) {
43
+ const item = normalizeText(listMatch[1]);
44
+ if (item.length > 0 && !current.listItems.includes(item)) current.listItems.push(item);
45
+ if (item.length > 0) current.textLines.push(item);
46
+ continue;
47
+ }
48
+
49
+ const text = normalizeText(rawLine);
50
+ if (text.length > 0) current.textLines.push(text);
51
+ }
52
+
53
+ return sections;
54
+ }
55
+
56
+ export function extractRequiredHeadings(content: string): string[] {
57
+ const sections = parseMarkdownSections(content);
58
+ return sections
59
+ .filter((section) => section.level === 2)
60
+ .map((section) => `## ${section.heading}`)
61
+ .filter((heading) => heading.length > 3);
62
+ }
63
+
64
+ export type TaggedLine = {
65
+ contractId: string;
66
+ line: string;
67
+ };
68
+
69
+ export function taggedLinesByContract(body: string): TaggedLine[] {
70
+ const lines = body
71
+ .split(/\r?\n/)
72
+ .map((line) => line.trim())
73
+ .filter((line) => line.length > 0);
74
+ const tagged: TaggedLine[] = [];
75
+ for (const line of lines) {
76
+ const matches = line.match(/\[([GFC][0-9]+)\]/g) ?? [];
77
+ for (const match of matches) {
78
+ tagged.push({ contractId: match.slice(1, -1), line });
79
+ }
80
+ }
81
+ return tagged;
82
+ }
83
+
84
+ export function sectionTextMap(content: string): Map<string, string> {
85
+ const sections = parseMarkdownSections(content);
86
+ const mapped = new Map<string, string>();
87
+ for (const section of sections) {
88
+ const parts = [...section.listItems, ...section.textLines].filter((item) => item.length > 0);
89
+ mapped.set(`## ${section.heading}`, parts.join("\n").trim());
90
+ }
91
+ return mapped;
92
+ }
93
+