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.
- package/.claude/commands/dev.md +340 -314
- package/.claude/commands/plan.md +521 -478
- package/.claude/commands/premerge.md +176 -179
- package/.claude/commands/research.md +42 -42
- package/.claude/commands/review.md +442 -442
- package/.claude/commands/rollback.md +721 -721
- package/.claude/commands/ship.md +164 -134
- package/.claude/commands/sonarcloud.md +152 -152
- package/.claude/commands/status.md +48 -77
- package/.claude/commands/validate.md +282 -237
- package/.claude/commands/verify.md +221 -221
- package/.claude/rules/greptile-review-process.md +285 -285
- package/.claude/rules/workflow.md +105 -105
- package/.claude/scripts/greptile-resolve.sh +526 -526
- package/.claude/scripts/load-env.sh +32 -32
- package/.cline/workflows/dev.md +337 -311
- package/.cline/workflows/plan.md +518 -475
- package/.cline/workflows/premerge.md +173 -176
- package/.cline/workflows/research.md +39 -39
- package/.cline/workflows/review.md +439 -439
- package/.cline/workflows/rollback.md +718 -718
- package/.cline/workflows/ship.md +161 -131
- package/.cline/workflows/sonarcloud.md +146 -146
- package/.cline/workflows/status.md +45 -74
- package/.cline/workflows/validate.md +279 -234
- package/.cline/workflows/verify.md +218 -218
- package/.codex/config.toml +11 -11
- package/.codex/skills/dev/SKILL.md +340 -314
- package/.codex/skills/plan/SKILL.md +521 -478
- package/.codex/skills/premerge/SKILL.md +176 -179
- package/.codex/skills/research/SKILL.md +42 -42
- package/.codex/skills/review/SKILL.md +442 -442
- package/.codex/skills/rollback/SKILL.md +721 -721
- package/.codex/skills/ship/SKILL.md +164 -134
- package/.codex/skills/sonarcloud/SKILL.md +149 -149
- package/.codex/skills/status/SKILL.md +48 -77
- package/.codex/skills/validate/SKILL.md +282 -237
- package/.codex/skills/verify/SKILL.md +221 -221
- package/.cursor/commands/dev.md +337 -311
- package/.cursor/commands/plan.md +518 -475
- package/.cursor/commands/premerge.md +173 -176
- package/.cursor/commands/research.md +39 -39
- package/.cursor/commands/review.md +439 -439
- package/.cursor/commands/rollback.md +718 -718
- package/.cursor/commands/ship.md +161 -131
- package/.cursor/commands/sonarcloud.md +146 -146
- package/.cursor/commands/status.md +45 -74
- package/.cursor/commands/validate.md +279 -234
- package/.cursor/commands/verify.md +218 -218
- package/.cursor/rules/permissions-guidance.mdc +37 -37
- package/.forge/hooks/check-tdd.js +240 -240
- package/.github/PLUGIN_TEMPLATE.json +32 -32
- package/.github/prompts/dev.prompt.md +342 -316
- package/.github/prompts/plan.prompt.md +523 -480
- package/.github/prompts/premerge.prompt.md +178 -181
- package/.github/prompts/research.prompt.md +44 -44
- package/.github/prompts/review.prompt.md +444 -444
- package/.github/prompts/rollback.prompt.md +723 -723
- package/.github/prompts/ship.prompt.md +166 -136
- package/.github/prompts/sonarcloud.prompt.md +151 -151
- package/.github/prompts/status.prompt.md +50 -79
- package/.github/prompts/validate.prompt.md +284 -239
- package/.github/prompts/verify.prompt.md +223 -223
- package/.github/workflows/beads-to-github.yml +56 -0
- package/.github/workflows/github-to-beads.yml +97 -0
- package/.kilocode/workflows/dev.md +341 -315
- package/.kilocode/workflows/plan.md +522 -479
- package/.kilocode/workflows/premerge.md +177 -180
- package/.kilocode/workflows/research.md +43 -43
- package/.kilocode/workflows/review.md +443 -443
- package/.kilocode/workflows/rollback.md +722 -722
- package/.kilocode/workflows/ship.md +165 -135
- package/.kilocode/workflows/sonarcloud.md +150 -150
- package/.kilocode/workflows/status.md +49 -78
- package/.kilocode/workflows/validate.md +283 -238
- package/.kilocode/workflows/verify.md +222 -222
- package/.mcp.json.example +12 -12
- package/.opencode/commands/dev.md +340 -314
- package/.opencode/commands/plan.md +521 -478
- package/.opencode/commands/premerge.md +176 -179
- package/.opencode/commands/research.md +42 -42
- package/.opencode/commands/review.md +442 -442
- package/.opencode/commands/rollback.md +721 -721
- package/.opencode/commands/ship.md +164 -134
- package/.opencode/commands/sonarcloud.md +149 -149
- package/.opencode/commands/status.md +48 -77
- package/.opencode/commands/validate.md +282 -237
- package/.opencode/commands/verify.md +221 -221
- package/.roo/commands/dev.md +341 -315
- package/.roo/commands/plan.md +522 -479
- package/.roo/commands/premerge.md +177 -180
- package/.roo/commands/research.md +43 -43
- package/.roo/commands/review.md +443 -443
- package/.roo/commands/rollback.md +722 -722
- package/.roo/commands/ship.md +165 -135
- package/.roo/commands/sonarcloud.md +150 -150
- package/.roo/commands/status.md +49 -78
- package/.roo/commands/validate.md +283 -238
- package/.roo/commands/verify.md +222 -222
- package/AGENTS.md +175 -169
- package/CLAUDE.md +100 -99
- package/LICENSE +21 -21
- package/README.md +429 -414
- package/bin/forge-cmd.js +313 -313
- package/bin/{forge-validate.js → forge-preflight.js} +309 -303
- package/bin/forge.js +4596 -4232
- package/docs/AGENT_INSTALL_PROMPT.md +342 -342
- package/docs/BEADS_GITHUB_SYNC.md +251 -0
- package/docs/ENHANCED_ONBOARDING.md +602 -602
- package/docs/EXAMPLES.md +482 -482
- package/docs/GREPTILE_SETUP.md +400 -400
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
- package/docs/ROADMAP.md +359 -359
- package/docs/SETUP.md +663 -632
- package/docs/TOOLCHAIN.md +630 -630
- package/docs/VALIDATION.md +363 -363
- package/install.sh +40 -1058
- package/lefthook.yml +39 -39
- package/lib/agents/README.md +198 -198
- package/lib/agents/claude.plugin.json +28 -28
- package/lib/agents/cline.plugin.json +22 -22
- package/lib/agents/codex.plugin.json +19 -19
- package/lib/agents/copilot.plugin.json +24 -24
- package/lib/agents/cursor.plugin.json +25 -25
- package/lib/agents/kilocode.plugin.json +22 -22
- package/lib/agents/opencode.plugin.json +20 -20
- package/lib/agents/roo.plugin.json +23 -23
- package/lib/agents-config.js +2112 -2112
- package/lib/beads-health-check.js +143 -0
- package/lib/beads-setup.js +341 -0
- package/lib/beads-sync-scaffold.js +260 -0
- package/lib/commands/dev.js +513 -513
- package/lib/commands/plan.js +692 -692
- package/lib/commands/recommend.js +119 -119
- package/lib/commands/ship.js +377 -377
- package/lib/commands/status.js +378 -378
- package/lib/commands/validate.js +602 -602
- package/lib/context-merge.js +359 -359
- package/lib/dep-guard/analyzer.js +294 -294
- package/lib/dep-guard/behavior-detector.js +98 -98
- package/lib/dep-guard/contract-detector.js +162 -162
- package/lib/dep-guard/import-detector.js +498 -498
- package/lib/dep-guard/path-utils.js +13 -13
- package/lib/dep-guard/rubric.js +120 -120
- package/lib/dep-guard/task-parser.js +318 -318
- package/lib/detect-agent.js +191 -0
- package/lib/detect-worktree.js +47 -0
- package/lib/file-hash.js +26 -0
- package/lib/husky-migration.js +450 -0
- package/lib/lefthook-check.js +65 -0
- package/lib/pat-setup.js +207 -0
- package/lib/plugin-catalog.js +350 -350
- package/lib/plugin-manager.js +166 -166
- package/lib/plugin-recommender.js +141 -141
- package/lib/project-discovery.js +491 -491
- package/lib/setup-action-log.js +139 -0
- package/lib/setup-summary-renderer.js +106 -0
- package/lib/setup-utils.js +96 -0
- package/lib/setup.js +192 -118
- package/lib/smart-merge.js +64 -0
- package/lib/symlink-utils.js +81 -0
- package/lib/workflow-profiles.js +197 -197
- package/package.json +131 -129
- package/scripts/beads-context.sh +291 -0
- package/scripts/beads-context.test.js +563 -0
- package/scripts/behavioral-judge.sh +378 -0
- package/scripts/benchmark.js +85 -0
- package/scripts/branch-protection.js +183 -0
- package/scripts/check-agents.js +172 -0
- package/scripts/commitlint.js +42 -0
- package/scripts/conflict-detect.sh +323 -0
- package/scripts/dep-guard-analyze.js +71 -0
- package/scripts/dep-guard.sh +811 -0
- package/scripts/eval_win.py +249 -0
- package/scripts/file-index.sh +399 -0
- package/scripts/github-beads-sync/comment.mjs +64 -0
- package/scripts/github-beads-sync/config.mjs +148 -0
- package/scripts/github-beads-sync/github-api.mjs +131 -0
- package/scripts/github-beads-sync/index.mjs +332 -0
- package/scripts/github-beads-sync/label-mapper.mjs +54 -0
- package/scripts/github-beads-sync/mapping.mjs +78 -0
- package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
- package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
- package/scripts/github-beads-sync/run-bd.mjs +159 -0
- package/scripts/github-beads-sync/sanitize.mjs +121 -0
- package/scripts/github-beads-sync.config.json +26 -0
- package/scripts/improve-command.js +375 -0
- package/scripts/lib/eval-runner.js +229 -0
- package/scripts/lib/eval-schema.js +135 -0
- package/scripts/lib/eval-storage.js +78 -0
- package/scripts/lib/grading.js +203 -0
- package/scripts/lib/transcript-parser.js +63 -0
- package/scripts/lint.js +47 -0
- package/scripts/migrate-to-bun-test.js +412 -0
- package/scripts/run-command-eval.js +236 -0
- package/scripts/smart-status.sh +782 -0
- package/scripts/sync-commands.js +571 -0
- package/scripts/sync-utils.sh +460 -0
- package/scripts/test-dashboard.js +123 -0
- package/scripts/test.js +44 -0
- package/scripts/validate.sh +94 -0
- package/skills/parallel-deep-research/SKILL.md +108 -108
- package/skills/parallel-deep-research/evals/README.md +27 -27
- package/skills/parallel-deep-research/evals/evals.json +62 -62
- package/skills/sonarcloud-analysis/SKILL.md +171 -171
- package/skills/sonarcloud-analysis/evals/README.md +27 -27
- package/skills/sonarcloud-analysis/evals/evals.json +50 -50
- package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
- 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 };
|
package/scripts/lint.js
ADDED
|
@@ -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)');
|