forge-workflow 0.0.3 → 0.0.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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Eval set schema validator and loader.
3
+ *
4
+ * Supports three assertion types:
5
+ * - standard : { type, check }
6
+ * - hard-gate : { type, precondition, check }
7
+ * - contract : { type, producer, consumer, check }
8
+ */
9
+
10
+ const fs = require('fs');
11
+
12
+ const VALID_ASSERTION_TYPES = ['standard', 'hard-gate', 'contract'];
13
+
14
+ // ── assertion-level required fields by type ────────────────────────────
15
+ const ASSERTION_REQUIRED_FIELDS = {
16
+ standard: ['check'],
17
+ 'hard-gate': ['precondition', 'check'],
18
+ contract: ['producer', 'consumer', 'check'],
19
+ };
20
+
21
+ // ── helpers ────────────────────────────────────────────────────────────
22
+
23
+ function requireNonEmptyString(obj, field, prefix) {
24
+ if (typeof obj[field] !== 'string' || obj[field].length === 0) {
25
+ throw new Error(`${prefix}missing required field: ${field}`);
26
+ }
27
+ }
28
+
29
+ function validateAssertion(assertion, queryName, index) {
30
+ const prefix = `query "${queryName}", assertion ${index}`;
31
+
32
+ if (!assertion || typeof assertion.type !== 'string') {
33
+ throw new Error(`${prefix}: missing required field: type`);
34
+ }
35
+
36
+ if (!VALID_ASSERTION_TYPES.includes(assertion.type)) {
37
+ throw new Error(`${prefix}: unknown assertion type: "${assertion.type}"`);
38
+ }
39
+
40
+ const requiredFields = ASSERTION_REQUIRED_FIELDS[assertion.type];
41
+ for (const field of requiredFields) {
42
+ if (typeof assertion[field] !== 'string' || assertion[field].length === 0) {
43
+ throw new Error(`${prefix} (${assertion.type}): missing required field: ${field}`);
44
+ }
45
+ }
46
+ }
47
+
48
+ function validateQuery(query, index, seenNames) {
49
+ // name must exist before we can use it in error messages
50
+ if (typeof query.name !== 'string' || query.name.length === 0) {
51
+ throw new Error(`query at index ${index}: missing required field: name`);
52
+ }
53
+
54
+ const name = query.name;
55
+
56
+ if (seenNames.has(name)) {
57
+ throw new Error(`duplicate query name: "${name}"`);
58
+ }
59
+ seenNames.add(name);
60
+
61
+ requireNonEmptyString(query, 'prompt', `query "${name}": `);
62
+
63
+ // assertions: must be a non-empty array
64
+ if (!Array.isArray(query.assertions)) {
65
+ throw new Error(`query "${name}": missing required field: assertions`);
66
+ }
67
+ if (query.assertions.length === 0) {
68
+ throw new Error(`query "${name}": assertions must be a non-empty array`);
69
+ }
70
+
71
+ for (let i = 0; i < query.assertions.length; i++) {
72
+ validateAssertion(query.assertions[i], name, i);
73
+ }
74
+
75
+ // Normalise optional fields
76
+ if (query.setup === undefined) query.setup = null;
77
+ if (query.teardown === undefined) query.teardown = null;
78
+ }
79
+
80
+ // ── public API ─────────────────────────────────────────────────────────
81
+
82
+ /**
83
+ * Validate an in-memory eval-set object.
84
+ * Returns the (possibly normalised) object on success, throws on failure.
85
+ */
86
+ function validateEvalSet(data) {
87
+ if (!data || typeof data !== 'object') {
88
+ throw new Error('eval set must be a non-null object');
89
+ }
90
+
91
+ requireNonEmptyString(data, 'command', '');
92
+ requireNonEmptyString(data, 'description', '');
93
+
94
+ if (!Array.isArray(data.queries)) {
95
+ throw new Error('missing required field: queries');
96
+ }
97
+ if (data.queries.length === 0) {
98
+ throw new Error('queries must be a non-empty array');
99
+ }
100
+
101
+ const seenNames = new Set();
102
+ for (let i = 0; i < data.queries.length; i++) {
103
+ validateQuery(data.queries[i], i, seenNames);
104
+ }
105
+
106
+ return data;
107
+ }
108
+
109
+ /**
110
+ * Load an eval-set from a `.eval.json` file on disk.
111
+ * Returns the validated object or throws with a descriptive error.
112
+ */
113
+ function loadEvalSet(filePath) {
114
+ if (!fs.existsSync(filePath)) {
115
+ throw new Error(`eval set file not found: ${filePath}`);
116
+ }
117
+
118
+ let raw;
119
+ try {
120
+ raw = fs.readFileSync(filePath, 'utf-8');
121
+ } catch (err) {
122
+ throw new Error(`failed to read eval set file: ${err.message}`);
123
+ }
124
+
125
+ let data;
126
+ try {
127
+ data = JSON.parse(raw);
128
+ } catch (_err) {
129
+ throw new Error(`invalid JSON in eval set file: ${filePath}`);
130
+ }
131
+
132
+ return validateEvalSet(data);
133
+ }
134
+
135
+ module.exports = { loadEvalSet, validateEvalSet };
@@ -0,0 +1,78 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_BASE_PATH = '.forge/eval-logs';
5
+
6
+ /**
7
+ * Normalize a command name: strip leading slash, replace remaining slashes with dashes.
8
+ */
9
+ function normalizeCommand(command) {
10
+ return command.replace(/^\//, '').replace(/\//g, '-');
11
+ }
12
+
13
+ /**
14
+ * Save an eval result as timestamped JSON.
15
+ *
16
+ * @param {object} result - Eval result with at minimum: { command, overall_score, results, timestamp }
17
+ * @param {string} [_basePath] - Directory to write to (defaults to .forge/eval-logs)
18
+ * @returns {string} The file path that was written
19
+ */
20
+ function saveEvalResult(result, _basePath) {
21
+ const basePath = _basePath || DEFAULT_BASE_PATH;
22
+ const command = normalizeCommand(result.command);
23
+ const date = new Date(result.timestamp);
24
+
25
+ const yyyy = date.getUTCFullYear();
26
+ const mm = String(date.getUTCMonth() + 1).padStart(2, '0');
27
+ const dd = String(date.getUTCDate()).padStart(2, '0');
28
+ const hh = String(date.getUTCHours()).padStart(2, '0');
29
+ const min = String(date.getUTCMinutes()).padStart(2, '0');
30
+
31
+ const fileName = `${yyyy}-${mm}-${dd}-${hh}-${min}-${command}.json`;
32
+ const filePath = path.join(basePath, fileName);
33
+
34
+ fs.mkdirSync(basePath, { recursive: true });
35
+ fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf8');
36
+
37
+ return filePath;
38
+ }
39
+
40
+ /**
41
+ * Load eval history for a specific command.
42
+ *
43
+ * @param {string} command - Command name (e.g., '/status' or 'status')
44
+ * @param {string} [_basePath] - Directory to read from (defaults to .forge/eval-logs)
45
+ * @returns {object[]} Array of eval results sorted by timestamp (newest first)
46
+ */
47
+ function loadEvalHistory(command, _basePath) {
48
+ const basePath = _basePath || DEFAULT_BASE_PATH;
49
+ const normalized = normalizeCommand(command);
50
+
51
+ if (!fs.existsSync(basePath)) {
52
+ return [];
53
+ }
54
+
55
+ const suffix = `-${normalized}.json`;
56
+ const files = fs.readdirSync(basePath).filter((f) => f.endsWith(suffix));
57
+
58
+ const results = [];
59
+ for (const file of files) {
60
+ const filePath = path.join(basePath, file);
61
+ try {
62
+ const content = fs.readFileSync(filePath, 'utf8');
63
+ results.push(JSON.parse(content));
64
+ } catch (_err) {
65
+ // History is best-effort context only; skip corrupted entries.
66
+ }
67
+ }
68
+
69
+ results.sort((a, b) => {
70
+ const ta = new Date(a.timestamp).getTime();
71
+ const tb = new Date(b.timestamp).getTime();
72
+ return tb - ta;
73
+ });
74
+
75
+ return results;
76
+ }
77
+
78
+ module.exports = { DEFAULT_BASE_PATH, saveEvalResult, loadEvalHistory };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Grading orchestrator — invoke grader agent + collect results.
3
+ *
4
+ * Takes a parsed transcript (from transcript-parser.js) and an array of
5
+ * assertion objects, invokes the grader agent (via `claude -p` or an
6
+ * injectable function), parses the grader's JSON response, and computes
7
+ * per-query and overall scores.
8
+ */
9
+
10
+ const { execFileSync } = require('child_process');
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // buildGraderPrompt
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * Build the prompt string sent to the command-grader agent.
18
+ *
19
+ * @param {{ messages: Array, toolCalls: Array, result: object|null }} transcript
20
+ * @param {Array<{ type: string, check: string, [key: string]: any }>} assertions
21
+ * @returns {string}
22
+ */
23
+ function buildGraderPrompt(transcript, assertions) {
24
+ // Serialize transcript messages into readable text
25
+ const transcriptLines = [];
26
+ for (const msg of transcript.messages) {
27
+ if (msg.text) {
28
+ transcriptLines.push(msg.text);
29
+ }
30
+ for (const tc of msg.toolCalls) {
31
+ transcriptLines.push(`[Tool Call] ${tc.name}: ${JSON.stringify(tc.input)}`);
32
+ }
33
+ }
34
+ const transcriptText = transcriptLines.join('\n');
35
+
36
+ // Serialize assertions
37
+ const assertionsDef = JSON.stringify(assertions, null, 2);
38
+
39
+ return [
40
+ '## Transcript',
41
+ '',
42
+ '<transcript>',
43
+ transcriptText,
44
+ '</transcript>',
45
+ '',
46
+ '## Assertions',
47
+ '',
48
+ assertionsDef,
49
+ '',
50
+ 'Grade each assertion against the transcript above. Return your response as a JSON object with a "results" array containing one entry per assertion, each with "assertion", "pass" (boolean), and "reasoning" (string).',
51
+ ].join('\n');
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // parseGraderResponse
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * Parse the grader's text response and extract the results array.
60
+ *
61
+ * Handles:
62
+ * - Raw JSON
63
+ * - JSON wrapped in markdown ```json code blocks
64
+ * - JSON embedded in leading/trailing prose
65
+ *
66
+ * @param {string} responseText
67
+ * @returns {Array<{ assertion: object, pass: boolean, reasoning: string }>}
68
+ * @throws {Error} if no valid JSON with a results array can be extracted
69
+ */
70
+ function parseGraderResponse(responseText) {
71
+ // Strategy 1: markdown code block
72
+ const codeBlockMatch = responseText.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
73
+ if (codeBlockMatch) {
74
+ try {
75
+ const parsed = JSON.parse(codeBlockMatch[1].trim());
76
+ if (parsed && Array.isArray(parsed.results)) {
77
+ return parsed.results;
78
+ }
79
+ } catch (_e) {
80
+ // fall through to next strategy
81
+ }
82
+ }
83
+
84
+ // Strategy 2: find the outermost { ... } containing "results"
85
+ const firstBrace = responseText.indexOf('{');
86
+ const lastBrace = responseText.lastIndexOf('}');
87
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
88
+ const candidate = responseText.slice(firstBrace, lastBrace + 1);
89
+ try {
90
+ const parsed = JSON.parse(candidate);
91
+ if (parsed && Array.isArray(parsed.results)) {
92
+ return parsed.results;
93
+ }
94
+ } catch (_e) {
95
+ // fall through
96
+ }
97
+ }
98
+
99
+ throw new Error('Failed to parse grader response: no valid JSON with results array found');
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Default invokeGrader (calls `claude -p` with the grader agent)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ /**
107
+ * Default grader invocation via `claude -p`.
108
+ *
109
+ * @param {string} prompt
110
+ * @param {{ timeout?: number }} options
111
+ * @returns {Promise<string>}
112
+ */
113
+ async function defaultInvokeGrader(prompt, options = {}) {
114
+ const timeout = options.timeout || 120_000;
115
+ const result = execFileSync(
116
+ 'claude',
117
+ ['-p', prompt, '--agent', 'command-grader', '--output-format', 'text', '--no-session-persistence'],
118
+ {
119
+ encoding: 'utf-8',
120
+ timeout,
121
+ maxBuffer: 10 * 1024 * 1024,
122
+ }
123
+ );
124
+ return result;
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // gradeTranscript
129
+ // ---------------------------------------------------------------------------
130
+
131
+ /**
132
+ * Orchestrate grading: build prompt, invoke grader, parse response, score.
133
+ *
134
+ * @param {{ messages: Array, toolCalls: Array, result: object|null }} transcript
135
+ * @param {Array<{ type: string, check: string, [key: string]: any }>} assertions
136
+ * @param {{ timeout?: number, _invokeGrader?: Function }} [options]
137
+ * @returns {Promise<{ assertions: Array<{ type: string, check: string, pass: boolean, reasoning: string }>, score: number }>}
138
+ */
139
+ async function gradeTranscript(transcript, assertions, options = {}) {
140
+ // Empty assertions → perfect score
141
+ if (assertions.length === 0) {
142
+ return { assertions: [], score: 1.0 };
143
+ }
144
+
145
+ const invokeGrader = options._invokeGrader || defaultInvokeGrader;
146
+
147
+ const prompt = buildGraderPrompt(transcript, assertions);
148
+
149
+ let responseText;
150
+ try {
151
+ responseText = await invokeGrader(prompt, { timeout: options.timeout });
152
+ } catch (_err) {
153
+ // Grader invocation failed entirely — mark all as error
154
+ return {
155
+ assertions: assertions.map((a) => ({
156
+ ...a,
157
+ pass: false,
158
+ reasoning: 'Grader returned malformed response',
159
+ })),
160
+ score: 0,
161
+ };
162
+ }
163
+
164
+ let graderResults;
165
+ try {
166
+ graderResults = parseGraderResponse(responseText);
167
+ } catch (_err) {
168
+ // Malformed response — mark all as error
169
+ return {
170
+ assertions: assertions.map((a) => ({
171
+ ...a,
172
+ pass: false,
173
+ reasoning: 'Grader returned malformed response',
174
+ })),
175
+ score: 0,
176
+ };
177
+ }
178
+
179
+ // Merge grader results back onto the original assertions
180
+ const scoredAssertions = assertions.map((assertion, i) => {
181
+ const graded = graderResults[i];
182
+ if (graded && typeof graded.pass === 'boolean') {
183
+ return {
184
+ ...assertion,
185
+ pass: graded.pass,
186
+ reasoning: graded.reasoning || '',
187
+ };
188
+ }
189
+ // Missing or malformed entry for this assertion
190
+ return {
191
+ ...assertion,
192
+ pass: false,
193
+ reasoning: 'Grader returned malformed response',
194
+ };
195
+ });
196
+
197
+ const passed = scoredAssertions.filter((a) => a.pass).length;
198
+ const score = passed / scoredAssertions.length;
199
+
200
+ return { assertions: scoredAssertions, score };
201
+ }
202
+
203
+ module.exports = { gradeTranscript, buildGraderPrompt, parseGraderResponse };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Parse NDJSON output from `claude -p --output-format stream-json`.
3
+ *
4
+ * Extracts assistant text content, tool calls (name + input), and final result.
5
+ *
6
+ * @param {string} ndjsonString - Raw NDJSON string (one JSON object per line)
7
+ * @returns {{ messages: Array, toolCalls: Array, result: object|null }}
8
+ */
9
+ function parseTranscript(ndjsonString) {
10
+ const transcript = {
11
+ messages: [],
12
+ toolCalls: [],
13
+ result: null,
14
+ };
15
+
16
+ if (!ndjsonString || ndjsonString.trim() === '') {
17
+ return transcript;
18
+ }
19
+
20
+ const lines = ndjsonString.split('\n');
21
+
22
+ for (const line of lines) {
23
+ const trimmed = line.trim();
24
+ if (trimmed === '') continue;
25
+
26
+ let event;
27
+ try {
28
+ event = JSON.parse(trimmed);
29
+ } catch (_err) {
30
+ // Malformed line — skip silently
31
+ continue;
32
+ }
33
+
34
+ if (event.type === 'assistant') {
35
+ const message = {
36
+ role: 'assistant',
37
+ text: '',
38
+ toolCalls: [],
39
+ };
40
+
41
+ const content = event.message && event.message.content;
42
+ if (Array.isArray(content)) {
43
+ for (const block of content) {
44
+ if (block.type === 'text') {
45
+ message.text += block.text;
46
+ } else if (block.type === 'tool_use') {
47
+ const toolCall = { name: block.name, input: block.input };
48
+ message.toolCalls.push(toolCall);
49
+ transcript.toolCalls.push(toolCall);
50
+ }
51
+ }
52
+ }
53
+
54
+ transcript.messages.push(message);
55
+ } else if (event.type === 'result') {
56
+ transcript.result = event.result || null;
57
+ }
58
+ }
59
+
60
+ return transcript;
61
+ }
62
+
63
+ module.exports = { parseTranscript };
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Cross-platform ESLint runner for lefthook pre-push hook.
4
+ * Delegates to the project's package manager: <pkg> run lint
5
+ * Works on Windows CMD, PowerShell, macOS, Linux.
6
+ */
7
+
8
+ const { spawnSync } = require('node:child_process');
9
+ const fs = require('node:fs');
10
+
11
+ // On Windows, package manager CLIs are .cmd files — shell: true resolves them
12
+ const isWindows = process.platform === 'win32';
13
+
14
+ // Detect package manager from lock files (same priority as forge.js and test.js)
15
+ function detectPackageManager() {
16
+ if (fs.existsSync('bun.lockb') || fs.existsSync('bun.lock')) return 'bun';
17
+ if (fs.existsSync('pnpm-lock.yaml')) return 'pnpm';
18
+ if (fs.existsSync('yarn.lock')) return 'yarn';
19
+ return 'npm';
20
+ }
21
+
22
+ const pkgManager = detectPackageManager();
23
+ console.log(`🔍 Running ESLint (${pkgManager} run lint)...`);
24
+
25
+ const result = spawnSync(pkgManager, ['run', 'lint'], { stdio: 'inherit', shell: isWindows });
26
+
27
+ if (result.error) {
28
+ console.error('');
29
+ console.error(`❌ Failed to run ${pkgManager} run lint: ${result.error.message}`);
30
+ console.error(` Is '${pkgManager}' installed and on PATH?`);
31
+ console.error('');
32
+ process.exit(1);
33
+ }
34
+
35
+ if (result.status !== 0) {
36
+ console.error('');
37
+ console.error('❌ ESLint errors found. Fix them before pushing.');
38
+ console.error('');
39
+ console.error(`To see errors: ${pkgManager} run lint`);
40
+ console.error(`To auto-fix: ${pkgManager} run lint -- --fix`);
41
+ console.error('');
42
+ console.error('Fix the failing checks. Do not bypass hooks.');
43
+ console.error('');
44
+ process.exit(1);
45
+ }
46
+
47
+ console.log('✅ ESLint check passed (no errors)');