forge-workflow 0.0.4 → 0.0.6

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 (248) hide show
  1. package/.claude/commands/dev.md +345 -340
  2. package/.claude/commands/plan.md +566 -521
  3. package/.claude/commands/premerge.md +186 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +448 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +212 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +90 -48
  10. package/.claude/commands/validate.md +288 -282
  11. package/.claude/commands/verify.md +269 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +121 -105
  14. package/.claude/scripts/greptile-resolve.sh +558 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +342 -337
  17. package/.cline/workflows/plan.md +563 -518
  18. package/.cline/workflows/premerge.md +183 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +445 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +209 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +87 -45
  25. package/.cline/workflows/validate.md +285 -279
  26. package/.cline/workflows/verify.md +266 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +345 -340
  29. package/.codex/skills/plan/SKILL.md +566 -521
  30. package/.codex/skills/premerge/SKILL.md +186 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +448 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +212 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +90 -48
  37. package/.codex/skills/validate/SKILL.md +288 -282
  38. package/.codex/skills/verify/SKILL.md +269 -221
  39. package/.cursor/commands/dev.md +342 -337
  40. package/.cursor/commands/plan.md +563 -518
  41. package/.cursor/commands/premerge.md +183 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +445 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +209 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +87 -45
  48. package/.cursor/commands/validate.md +285 -279
  49. package/.cursor/commands/verify.md +266 -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 +347 -342
  54. package/.github/prompts/plan.prompt.md +568 -523
  55. package/.github/prompts/premerge.prompt.md +188 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +450 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +214 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +92 -50
  62. package/.github/prompts/validate.prompt.md +290 -284
  63. package/.github/prompts/verify.prompt.md +271 -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 +346 -341
  67. package/.kilocode/workflows/plan.md +567 -522
  68. package/.kilocode/workflows/premerge.md +187 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +449 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +213 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +91 -49
  75. package/.kilocode/workflows/validate.md +289 -283
  76. package/.kilocode/workflows/verify.md +270 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +345 -340
  79. package/.opencode/commands/plan.md +566 -521
  80. package/.opencode/commands/premerge.md +186 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +448 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +212 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +90 -48
  87. package/.opencode/commands/validate.md +288 -282
  88. package/.opencode/commands/verify.md +269 -221
  89. package/.roo/commands/dev.md +346 -341
  90. package/.roo/commands/plan.md +567 -522
  91. package/.roo/commands/premerge.md +187 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +449 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +213 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +91 -49
  98. package/.roo/commands/validate.md +289 -283
  99. package/.roo/commands/verify.md +270 -222
  100. package/AGENTS.md +272 -175
  101. package/CLAUDE.md +110 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +317 -313
  104. package/bin/forge-preflight.js +322 -309
  105. package/bin/forge.js +4765 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +612 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +653 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +50 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/_registry.js +134 -0
  132. package/lib/commands/clean.js +181 -0
  133. package/lib/commands/dev.js +571 -513
  134. package/lib/commands/plan.js +692 -692
  135. package/lib/commands/push.js +196 -0
  136. package/lib/commands/recommend.js +119 -119
  137. package/lib/commands/ship.js +377 -377
  138. package/lib/commands/status.js +378 -378
  139. package/lib/commands/sync.js +55 -0
  140. package/lib/commands/team.js +37 -0
  141. package/lib/commands/test.js +207 -0
  142. package/lib/commands/validate.js +602 -602
  143. package/lib/commands/worktree.js +310 -0
  144. package/lib/context-merge.js +359 -359
  145. package/lib/dep-guard/analyzer.js +294 -294
  146. package/lib/dep-guard/behavior-detector.js +98 -98
  147. package/lib/dep-guard/contract-detector.js +162 -162
  148. package/lib/dep-guard/import-detector.js +498 -498
  149. package/lib/dep-guard/path-utils.js +13 -13
  150. package/lib/dep-guard/rubric.js +120 -120
  151. package/lib/dep-guard/task-parser.js +318 -318
  152. package/lib/detect-agent.js +191 -191
  153. package/lib/detect-worktree.js +47 -47
  154. package/lib/docs-command.js +51 -0
  155. package/lib/docs-copy.js +50 -0
  156. package/lib/file-hash.js +26 -26
  157. package/lib/freshness-token.js +148 -0
  158. package/lib/greptile-match.js +80 -0
  159. package/lib/husky-migration.js +450 -0
  160. package/lib/lefthook-check.js +65 -0
  161. package/lib/pat-setup.js +207 -0
  162. package/lib/plugin-catalog.js +350 -350
  163. package/lib/plugin-manager.js +166 -166
  164. package/lib/plugin-recommender.js +141 -141
  165. package/lib/project-discovery.js +491 -491
  166. package/lib/reset.js +309 -0
  167. package/lib/setup-action-log.js +139 -139
  168. package/lib/setup-summary-renderer.js +106 -106
  169. package/lib/setup-utils.js +96 -0
  170. package/lib/setup.js +192 -192
  171. package/lib/smart-merge.js +64 -0
  172. package/lib/symlink-utils.js +81 -0
  173. package/lib/task-ownership.js +117 -0
  174. package/lib/workflow-profiles.js +197 -197
  175. package/package.json +131 -128
  176. package/scripts/beads-context.sh +426 -0
  177. package/scripts/beads-context.test.js +567 -0
  178. package/scripts/behavioral-judge.sh +378 -0
  179. package/scripts/benchmark.js +85 -0
  180. package/scripts/branch-protection.js +183 -0
  181. package/scripts/check-agents.js +172 -0
  182. package/scripts/check-forge-token.js +98 -0
  183. package/scripts/commitlint.js +42 -0
  184. package/scripts/conflict-detect.sh +323 -0
  185. package/scripts/dep-guard-analyze.js +71 -0
  186. package/scripts/dep-guard.sh +789 -0
  187. package/scripts/eval_win.py +249 -0
  188. package/scripts/file-index.sh +493 -0
  189. package/scripts/forge-team/index.sh +86 -0
  190. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  191. package/scripts/forge-team/lib/claim.sh +256 -0
  192. package/scripts/forge-team/lib/dashboard.sh +341 -0
  193. package/scripts/forge-team/lib/epic.sh +332 -0
  194. package/scripts/forge-team/lib/hooks.sh +253 -0
  195. package/scripts/forge-team/lib/identity.sh +235 -0
  196. package/scripts/forge-team/lib/sync-github.sh +317 -0
  197. package/scripts/forge-team/lib/verify.sh +284 -0
  198. package/scripts/forge-team/lib/workload.sh +296 -0
  199. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  200. package/scripts/forge-team/tests/claim.test.sh +179 -0
  201. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  202. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  203. package/scripts/forge-team/tests/epic.test.sh +176 -0
  204. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  205. package/scripts/forge-team/tests/identity.test.sh +176 -0
  206. package/scripts/forge-team/tests/integration.test.sh +371 -0
  207. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  208. package/scripts/forge-team/tests/verify.test.sh +314 -0
  209. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  210. package/scripts/forge-team/tests/workload.test.sh +209 -0
  211. package/scripts/github-beads-sync/comment.mjs +64 -0
  212. package/scripts/github-beads-sync/config.mjs +148 -0
  213. package/scripts/github-beads-sync/github-api.mjs +131 -0
  214. package/scripts/github-beads-sync/index.mjs +332 -0
  215. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  216. package/scripts/github-beads-sync/mapping.mjs +78 -0
  217. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  218. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  219. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  220. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  221. package/scripts/github-beads-sync.config.json +26 -0
  222. package/scripts/improve-command.js +375 -0
  223. package/scripts/lib/eval-runner.js +268 -0
  224. package/scripts/lib/eval-schema.js +135 -0
  225. package/scripts/lib/eval-storage.js +78 -0
  226. package/scripts/lib/grading.js +203 -0
  227. package/scripts/lib/jsonl-lock.sh +48 -0
  228. package/scripts/lib/sanitize.sh +116 -0
  229. package/scripts/lib/transcript-parser.js +63 -0
  230. package/scripts/lint.js +47 -0
  231. package/scripts/migrate-to-bun-test.js +412 -0
  232. package/scripts/pr-coordinator.sh +706 -0
  233. package/scripts/run-command-eval.js +236 -0
  234. package/scripts/smart-status.sh +809 -0
  235. package/scripts/sync-commands.js +571 -0
  236. package/scripts/sync-utils.sh +455 -0
  237. package/scripts/test-dashboard.js +123 -0
  238. package/scripts/test.js +46 -0
  239. package/scripts/validate.sh +94 -0
  240. package/skills/parallel-deep-research/SKILL.md +108 -108
  241. package/skills/parallel-deep-research/evals/README.md +27 -27
  242. package/skills/parallel-deep-research/evals/evals.json +62 -62
  243. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  244. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  245. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  246. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  247. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  248. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -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,48 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/lib/jsonl-lock.sh — Atomic JSONL append with file locking
3
+ #
4
+ # Provides atomic_jsonl_append() for concurrent-safe JSONL file writes.
5
+ # Cross-platform: uses flock on Linux/WSL/Git Bash, mkdir-based fallback on macOS.
6
+ #
7
+ # Usage (source this file):
8
+ # source scripts/lib/jsonl-lock.sh
9
+ # atomic_jsonl_append "/path/to/file.jsonl" '{"key":"value"}'
10
+ #
11
+ # Exit codes: 0=success, 1=lock timeout or write failure
12
+ # Lock timeout: 5 seconds
13
+
14
+ # atomic_jsonl_append <jsonl_file> <json_line>
15
+ # Appends a JSON line to a JSONL file with file locking.
16
+ # Lock file: <jsonl_file>.lock (flock) or <jsonl_file>.lock.d (mkdir)
17
+ # Timeout: 5 seconds
18
+ # Exit codes: 0=success, 1=lock timeout or write failure
19
+ atomic_jsonl_append() {
20
+ local jsonl_file="$1"
21
+ local json_line="$2"
22
+
23
+ mkdir -p "$(dirname "$jsonl_file")"
24
+
25
+ if command -v flock &>/dev/null; then
26
+ # flock-based locking (Linux, WSL, Git Bash)
27
+ local lock_file="${jsonl_file}.lock"
28
+ (
29
+ flock -w 5 200 || { echo "Error: JSONL lock timeout after 5s" >&2; return 1; }
30
+ printf '%s\n' "$json_line" >> "$jsonl_file"
31
+ ) 200>"$lock_file"
32
+ else
33
+ # mkdir-based fallback (macOS, systems without flock)
34
+ local lock_file="${jsonl_file}.lock.d"
35
+ local attempts=0
36
+ while ! mkdir "$lock_file" 2>/dev/null; do
37
+ attempts=$((attempts + 1))
38
+ if [[ $attempts -ge 50 ]]; then # 50 * 0.1s = 5s timeout
39
+ echo "Error: JSONL lock timeout after 5s" >&2
40
+ return 1
41
+ fi
42
+ sleep 0.1
43
+ done
44
+ # RETURN trap handles cleanup on both success and failure
45
+ trap 'rmdir "$lock_file" 2>/dev/null; trap - RETURN' RETURN
46
+ printf '%s\n' "$json_line" >> "$jsonl_file"
47
+ fi
48
+ }
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env bash
2
+ # sanitize.sh — Shared input sanitization and validation library.
3
+ #
4
+ # Extracted from file-index.sh, dep-guard.sh, beads-context.sh, sync-utils.sh
5
+ # to eliminate duplication. Source this file; do NOT run it directly.
6
+ #
7
+ # Functions:
8
+ # sanitize <string> Strip shell-injection patterns (OWASP A03)
9
+ # sanitize_config_value <string> Strip injection + pipes, trim whitespace
10
+ # validate_branch_name <string> Validate git branch name format
11
+ # validate_pr_number <string> Validate GitHub PR number (digits only)
12
+ # validate_label_name <string> Validate GitHub label format
13
+ #
14
+ # This file does NOT set errexit/pipefail — callers manage their own shell options.
15
+ # OWASP A03: All inputs validated and shell-injection patterns stripped.
16
+
17
+ # Guard against double-sourcing
18
+ if [[ -n "${_SANITIZE_LIB_LOADED:-}" ]]; then
19
+ return 0 2>/dev/null || true
20
+ fi
21
+ _SANITIZE_LIB_LOADED=1
22
+
23
+ # ── sanitize ──────────────────────────────────────────────────────────────
24
+ # Sanitize a string: strip shell-injection patterns (OWASP A03).
25
+ # Removes: double quotes, $(...), backticks, semicolons, and newlines.
26
+ # Usage: clean="$(sanitize "$raw")"
27
+ sanitize() {
28
+ local val="$1"
29
+ # Remove double quotes
30
+ val="${val//\"/}"
31
+ # Remove $(...) command substitution patterns (loop handles nested)
32
+ val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
33
+ # Remove backtick command substitution
34
+ val="${val//\`/}"
35
+ # Remove semicolons (command chaining)
36
+ val="${val//;/}"
37
+ # Replace newlines with spaces
38
+ val="$(printf '%s' "$val" | tr '\n' ' ')"
39
+ printf '%s' "$val"
40
+ }
41
+
42
+ # ── sanitize_config_value ────────────────────────────────────────────────
43
+ # Sanitize a config value: strip shell-injection patterns + pipes (OWASP A03).
44
+ # Removes: backticks, $(...), semicolons, pipes, newlines. Trims whitespace.
45
+ # Usage: clean="$(sanitize_config_value "$raw")"
46
+ sanitize_config_value() {
47
+ local val="$1"
48
+ # Remove backtick command substitution
49
+ val="${val//\`/}"
50
+ # Remove $(...) command substitution patterns (loop handles nested)
51
+ val="$(printf '%s' "$val" | sed -e ':loop' -e 's/\$([^()]*)//g' -e 't loop')"
52
+ # Remove semicolons (command chaining)
53
+ val="${val//;/}"
54
+ # Remove pipes (command chaining)
55
+ val="${val//|/}"
56
+ # Collapse newlines to spaces
57
+ val="$(printf '%s' "$val" | tr '\n' ' ')"
58
+ # Trim leading/trailing whitespace
59
+ val="$(printf '%s' "$val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
60
+ printf '%s' "$val"
61
+ }
62
+
63
+ # ── validate_branch_name ─────────────────────────────────────────────────
64
+ # Validate a git branch name.
65
+ # Allowed: alphanumeric, dots, hyphens, underscores, forward slashes, @
66
+ # Usage: validate_branch_name "feat/my-feature" || exit 1
67
+ validate_branch_name() {
68
+ local name="${1:-}"
69
+ if [[ -z "$name" ]]; then
70
+ echo "Error: branch name cannot be empty" >&2
71
+ return 1
72
+ fi
73
+ if [[ ! "$name" =~ ^[a-zA-Z0-9._/@-]+$ ]]; then
74
+ echo "Error: invalid branch name format: $name" >&2
75
+ return 1
76
+ fi
77
+ # Git rejects: double dots (..), trailing .lock, leading/trailing slash, leading hyphen
78
+ if [[ "$name" == *..* ]] || [[ "$name" == *.lock ]] || [[ "$name" == /* ]] || [[ "$name" == */ ]] || [[ "$name" == -* ]]; then
79
+ echo "Error: invalid branch name (contains .., .lock suffix, leading/trailing /, or leading -): $name" >&2
80
+ return 1
81
+ fi
82
+ return 0
83
+ }
84
+
85
+ # ── validate_pr_number ───────────────────────────────────────────────────
86
+ # Validate a GitHub PR number (digits only).
87
+ # Usage: validate_pr_number "42" || exit 1
88
+ validate_pr_number() {
89
+ local num="${1:-}"
90
+ if [[ -z "$num" ]]; then
91
+ echo "Error: PR number cannot be empty" >&2
92
+ return 1
93
+ fi
94
+ if [[ ! "$num" =~ ^[0-9]+$ ]]; then
95
+ echo "Error: invalid PR number format: $num" >&2
96
+ return 1
97
+ fi
98
+ return 0
99
+ }
100
+
101
+ # ── validate_label_name ──────────────────────────────────────────────────
102
+ # Validate a GitHub label name.
103
+ # Allowed: alphanumeric, dots, hyphens, underscores, forward slashes
104
+ # Usage: validate_label_name "forge/has-deps" || exit 1
105
+ validate_label_name() {
106
+ local name="${1:-}"
107
+ if [[ -z "$name" ]]; then
108
+ echo "Error: label name cannot be empty" >&2
109
+ return 1
110
+ fi
111
+ if [[ ! "$name" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
112
+ echo "Error: invalid label name format: $name" >&2
113
+ return 1
114
+ fi
115
+ return 0
116
+ }
@@ -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)');