@hone-ai/cli 1.7.0 → 1.8.0
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/hone-cli.js +198 -3
- package/lib/auto-detect.js +47 -12
- package/lib/eval-ab-testing.js +113 -0
- package/package.json +3 -2
- package/schema/metadata.schema.json +134 -0
package/hone-cli.js
CHANGED
|
@@ -1274,7 +1274,8 @@ program
|
|
|
1274
1274
|
|
|
1275
1275
|
try {
|
|
1276
1276
|
const health = await axios.get(`${config.apiUrl}/health`);
|
|
1277
|
-
|
|
1277
|
+
const ver = health.data.version ? ` (v${health.data.version})` : '';
|
|
1278
|
+
console.log(`✓ API health: ${health.data.status}${ver}`);
|
|
1278
1279
|
} catch (e) {
|
|
1279
1280
|
console.error(`✗ API health: ${e.message}`);
|
|
1280
1281
|
}
|
|
@@ -3927,12 +3928,12 @@ program
|
|
|
3927
3928
|
.description('Validate .github/pipeline/<STORY-ID>/metadata.yml against the framework JSON schema. Implements SC-010 §10 (metadata.yml as wire protocol).')
|
|
3928
3929
|
.option('--all', 'validate every metadata.yml in .github/pipeline/')
|
|
3929
3930
|
.option('--repo-root <path>', 'repo root (default: process.cwd())')
|
|
3930
|
-
.option('--schema <path>', 'override schema path (default:
|
|
3931
|
+
.option('--schema <path>', 'override schema path (default: bundled metadata.schema.json)')
|
|
3931
3932
|
.option('--json', 'emit findings as JSON')
|
|
3932
3933
|
.action((storyId, opts) => {
|
|
3933
3934
|
const { validateMetadata, validateAllMetadata } = require('./lib/validate-metadata');
|
|
3934
3935
|
const repoRoot = opts.repoRoot || process.cwd();
|
|
3935
|
-
const schemaPath = opts.schema || require('node:path').join(
|
|
3936
|
+
const schemaPath = opts.schema || require('node:path').join(__dirname, 'schema', 'metadata.schema.json');
|
|
3936
3937
|
|
|
3937
3938
|
if (opts.all) {
|
|
3938
3939
|
const result = validateAllMetadata({ repoRoot, schemaPath });
|
|
@@ -4234,6 +4235,200 @@ program
|
|
|
4234
4235
|
process.exit(results.failed + results.errors > 0 ? 1 : 0);
|
|
4235
4236
|
});
|
|
4236
4237
|
|
|
4238
|
+
// ── HC-041: Run Story (Orchestrator) ─────────────────────────────────────────
|
|
4239
|
+
program
|
|
4240
|
+
.command('run-story <storyId>')
|
|
4241
|
+
.description('Run the SDLC pipeline for a story via the orchestrator')
|
|
4242
|
+
.option('--mode <mode>', 'Execution mode: interactive or batch', 'interactive')
|
|
4243
|
+
.option('--repo <name>', 'Repository name override (default: directory name)')
|
|
4244
|
+
.option('--branch <name>', 'Git branch (default: current)')
|
|
4245
|
+
.option('--status', 'Show status of the latest workflow run for this story')
|
|
4246
|
+
.option('--kill', 'Kill the latest workflow run for this story')
|
|
4247
|
+
.option('--approve <step>', 'Approve a paused gate (e.g., --approve step_0)')
|
|
4248
|
+
.option('--format <fmt>', 'Output format: pretty or json', 'pretty')
|
|
4249
|
+
.option('--poll-interval <s>', 'Poll interval in seconds', '5')
|
|
4250
|
+
.action(async (storyIdOrRunId, opts) => {
|
|
4251
|
+
const config = getConfig();
|
|
4252
|
+
const client = api(config);
|
|
4253
|
+
|
|
4254
|
+
// Resolve: if it looks like a UUID, use as runId directly.
|
|
4255
|
+
// Otherwise treat as storyId — will be used for POST /orchestrate body.
|
|
4256
|
+
const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(storyIdOrRunId);
|
|
4257
|
+
const storyId = isUuid ? null : storyIdOrRunId;
|
|
4258
|
+
const runId = isUuid ? storyIdOrRunId : null;
|
|
4259
|
+
|
|
4260
|
+
// Status mode
|
|
4261
|
+
if (opts.status) {
|
|
4262
|
+
const statusId = runId || storyIdOrRunId;
|
|
4263
|
+
try {
|
|
4264
|
+
const { data } = await client.get(`/orchestrate/${statusId}`);
|
|
4265
|
+
if (opts.format === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
4266
|
+
console.log('');
|
|
4267
|
+
console.log(`Workflow: ${data.runId}`);
|
|
4268
|
+
console.log(`Story: ${data.storyId}`);
|
|
4269
|
+
console.log(`Status: ${data.status}`);
|
|
4270
|
+
console.log(`Mode: ${data.mode}`);
|
|
4271
|
+
console.log(`Tokens: ${(data.totalTokens || 0).toLocaleString()}`);
|
|
4272
|
+
if (data.errorMessage) console.log(`Error: ${data.errorMessage}`);
|
|
4273
|
+
console.log('');
|
|
4274
|
+
console.log('Steps:');
|
|
4275
|
+
for (const s of (data.steps || [])) {
|
|
4276
|
+
const icon = s.status === 'completed' ? '✓' : s.status === 'running' ? '⏳' : s.status === 'failed' ? '✗' : '⬜';
|
|
4277
|
+
const gate = s.gateResult ? ` (${s.gateResult})` : '';
|
|
4278
|
+
console.log(` ${icon} ${s.stepKey} (${s.agent}) ${s.status}${gate}`);
|
|
4279
|
+
}
|
|
4280
|
+
if (data.awaitingApproval) {
|
|
4281
|
+
console.log('');
|
|
4282
|
+
console.log(`Awaiting approval at ${data.currentStep}.`);
|
|
4283
|
+
console.log(`Approve: hone run-story ${data.runId} --approve ${data.currentStep}`);
|
|
4284
|
+
}
|
|
4285
|
+
console.log('');
|
|
4286
|
+
} catch (e) {
|
|
4287
|
+
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
4288
|
+
process.exit(1);
|
|
4289
|
+
}
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
|
|
4293
|
+
// Kill mode
|
|
4294
|
+
if (opts.kill) {
|
|
4295
|
+
const killId = runId || storyIdOrRunId;
|
|
4296
|
+
try {
|
|
4297
|
+
const { data } = await client.post(`/orchestrate/${killId}/kill`, { reason: 'CLI kill' });
|
|
4298
|
+
console.log(`Workflow ${data.runId} killed.`);
|
|
4299
|
+
} catch (e) {
|
|
4300
|
+
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
4301
|
+
process.exit(1);
|
|
4302
|
+
}
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
|
|
4306
|
+
// Approve mode
|
|
4307
|
+
if (opts.approve) {
|
|
4308
|
+
const approveId = runId || storyIdOrRunId;
|
|
4309
|
+
try {
|
|
4310
|
+
const { data } = await client.post(`/orchestrate/${approveId}/approve`, { stepKey: opts.approve });
|
|
4311
|
+
console.log(`Gate ${opts.approve} approved. Workflow resuming.`);
|
|
4312
|
+
console.log(`Poll: hone run-story ${approveId} --status`);
|
|
4313
|
+
} catch (e) {
|
|
4314
|
+
console.error(`Failed: ${e.response?.data?.error || e.message}`);
|
|
4315
|
+
process.exit(1);
|
|
4316
|
+
}
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
// Start workflow (storyId required, runId not accepted for starting)
|
|
4321
|
+
if (isUuid) {
|
|
4322
|
+
console.error('Cannot start workflow with a runId. Use a story ID like HC-042.');
|
|
4323
|
+
console.error('To check status: hone run-story <runId> --status');
|
|
4324
|
+
process.exit(1);
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4327
|
+
const repoName = opts.repo || path.basename(process.cwd());
|
|
4328
|
+
let branch = opts.branch;
|
|
4329
|
+
if (!branch) {
|
|
4330
|
+
try { branch = execSync('git symbolic-ref --short HEAD', { encoding: 'utf8' }).trim(); } catch { branch = null; }
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
try {
|
|
4334
|
+
const { data } = await client.post('/orchestrate', {
|
|
4335
|
+
storyId: storyIdOrRunId, repoName, branch, mode: opts.mode, config: {},
|
|
4336
|
+
});
|
|
4337
|
+
|
|
4338
|
+
console.log('');
|
|
4339
|
+
console.log(`Workflow started: ${data.runId}`);
|
|
4340
|
+
console.log(`Mode: ${data.mode} | Story: ${data.storyId}`);
|
|
4341
|
+
console.log(`Poll: ${data.pollUrl}`);
|
|
4342
|
+
console.log('');
|
|
4343
|
+
|
|
4344
|
+
// Poll loop
|
|
4345
|
+
const interval = parseInt(opts.pollInterval, 10) * 1000;
|
|
4346
|
+
while (true) {
|
|
4347
|
+
await new Promise(r => setTimeout(r, interval));
|
|
4348
|
+
|
|
4349
|
+
let status;
|
|
4350
|
+
try {
|
|
4351
|
+
const { data: pollData } = await client.get(`/orchestrate/${data.runId}`);
|
|
4352
|
+
status = pollData;
|
|
4353
|
+
} catch (e) {
|
|
4354
|
+
console.error(`Poll error: ${e.message}`);
|
|
4355
|
+
continue;
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
if (status.status === 'completed') {
|
|
4359
|
+
console.log(`✓ Workflow completed! Total tokens: ${(status.totalTokens || 0).toLocaleString()}`);
|
|
4360
|
+
process.exit(0);
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
if (status.status === 'failed' || status.status === 'killed') {
|
|
4364
|
+
console.error(`✗ Workflow ${status.status}: ${status.errorMessage || 'unknown'}`);
|
|
4365
|
+
process.exit(1);
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
if (status.status === 'paused' && status.awaitingApproval) {
|
|
4369
|
+
const step = status.steps.find(s => s.stepKey === status.currentStep);
|
|
4370
|
+
console.log(`⏳ Step ${status.currentStep} (${step?.agent || 'unknown'}) — awaiting approval`);
|
|
4371
|
+
|
|
4372
|
+
// Interactive prompt
|
|
4373
|
+
if (process.stdin.isTTY) {
|
|
4374
|
+
const readline = require('readline');
|
|
4375
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
4376
|
+
const answer = await new Promise(resolve => {
|
|
4377
|
+
rl.question(' Approve? [y/n/kill] ', resolve);
|
|
4378
|
+
});
|
|
4379
|
+
rl.close();
|
|
4380
|
+
|
|
4381
|
+
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
4382
|
+
try {
|
|
4383
|
+
await client.post(`/orchestrate/${data.runId}/approve`, { stepKey: status.currentStep });
|
|
4384
|
+
console.log(' Approved — workflow resuming...');
|
|
4385
|
+
} catch (e) {
|
|
4386
|
+
console.error(` Approve failed: ${e.response?.data?.error || e.message}`);
|
|
4387
|
+
}
|
|
4388
|
+
} else if (answer.toLowerCase() === 'kill') {
|
|
4389
|
+
try {
|
|
4390
|
+
await client.post(`/orchestrate/${data.runId}/kill`, { reason: 'CLI kill' });
|
|
4391
|
+
console.log(' Workflow killed.');
|
|
4392
|
+
process.exit(0);
|
|
4393
|
+
} catch (e) {
|
|
4394
|
+
console.error(` Kill failed: ${e.message}`);
|
|
4395
|
+
}
|
|
4396
|
+
} else {
|
|
4397
|
+
console.log(` Skipped. Resume later: hone run-story ${data.runId} --status`);
|
|
4398
|
+
process.exit(0);
|
|
4399
|
+
}
|
|
4400
|
+
} else {
|
|
4401
|
+
// Non-interactive (CI): exit with code 2 (paused, needs approval)
|
|
4402
|
+
console.log(` Non-interactive mode. Approve with: hone run-story ${data.runId} --approve ${status.currentStep}`);
|
|
4403
|
+
process.exit(2);
|
|
4404
|
+
}
|
|
4405
|
+
continue;
|
|
4406
|
+
}
|
|
4407
|
+
|
|
4408
|
+
// Still running
|
|
4409
|
+
const currentStep = status.steps.find(s => s.status === 'running');
|
|
4410
|
+
if (currentStep) {
|
|
4411
|
+
process.stdout.write(` Running: ${currentStep.stepKey} (${currentStep.agent})...\r`);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4414
|
+
} catch (e) {
|
|
4415
|
+
if (e.response?.status === 429) {
|
|
4416
|
+
console.error(`Max concurrent workflows reached. ${e.response.data.error}`);
|
|
4417
|
+
} else {
|
|
4418
|
+
console.error(`Failed to start: ${e.response?.data?.error || e.message}`);
|
|
4419
|
+
}
|
|
4420
|
+
process.exit(1);
|
|
4421
|
+
}
|
|
4422
|
+
});
|
|
4423
|
+
|
|
4424
|
+
// SIGINT handler for run-story
|
|
4425
|
+
process.on('SIGINT', () => {
|
|
4426
|
+
console.log('\n\nWorkflow continues on the server.');
|
|
4427
|
+
console.log('Resume: hone run-story <storyId> --status');
|
|
4428
|
+
console.log('Kill: hone run-story <storyId> --kill');
|
|
4429
|
+
process.exit(0);
|
|
4430
|
+
});
|
|
4431
|
+
|
|
4237
4432
|
// ── CLI setup ─────────────────────────────────────────────────────────────────
|
|
4238
4433
|
program
|
|
4239
4434
|
.name('hone')
|
package/lib/auto-detect.js
CHANGED
|
@@ -174,23 +174,58 @@ function detectE2EConvention(signals) {
|
|
|
174
174
|
* we recommend. Returns either status:'ok' or status:'drift' with an
|
|
175
175
|
* actionable suggested fix.
|
|
176
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* Check if configuredPattern is a superset of recommendedPattern.
|
|
179
|
+
* A broader configured pattern is intentional (not drift).
|
|
180
|
+
* Only flag when configured is NARROWER than recommended.
|
|
181
|
+
*/
|
|
182
|
+
function isPatternBroader(configured, recommended) {
|
|
183
|
+
try {
|
|
184
|
+
const recRe = new RegExp(recommended);
|
|
185
|
+
const cfgRe = new RegExp(configured);
|
|
186
|
+
// Test a set of sample strings that the recommended pattern matches
|
|
187
|
+
// If configured matches all of them, it's at least as broad
|
|
188
|
+
const samples = [];
|
|
189
|
+
// Generate simple test strings from recommended pattern components
|
|
190
|
+
const recStr = recommended.replace(/\\/g, '');
|
|
191
|
+
if (recStr.includes('|')) {
|
|
192
|
+
// Pattern has alternations — extract them
|
|
193
|
+
const alts = recommended.split('|').map(a => a.replace(/[()^$]/g, ''));
|
|
194
|
+
for (const alt of alts) {
|
|
195
|
+
// Generate a plausible match for each alternative
|
|
196
|
+
const sample = alt.replace(/\[0-9\]\+/g, '123').replace(/\[A-Z\]\+?/g, 'A').replace(/\[A-Za-z0-9\]\+/g, 'abc');
|
|
197
|
+
samples.push(sample);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// If configured matches everything recommended matches, it's broader or equal
|
|
201
|
+
return samples.length > 0 && samples.every(s => cfgRe.test(s));
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
177
207
|
function checkPatternDrift({ configured, recommended }) {
|
|
178
208
|
const findings = [];
|
|
179
209
|
if (configured?.story_id_pattern && configured.story_id_pattern !== recommended.story.pattern) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
210
|
+
// Only flag if configured is NARROWER — broader is intentional
|
|
211
|
+
if (!isPatternBroader(configured.story_id_pattern, recommended.story.pattern)) {
|
|
212
|
+
findings.push({
|
|
213
|
+
key: 'story_id_pattern',
|
|
214
|
+
configured: configured.story_id_pattern,
|
|
215
|
+
recommended: recommended.story.pattern,
|
|
216
|
+
reason: `repo looks like ${recommended.story.shape} (${recommended.story.confidence} confidence)`,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
186
219
|
}
|
|
187
220
|
if (configured?.e2e_spec_pattern && configured.e2e_spec_pattern !== recommended.e2e.pattern) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
221
|
+
if (!isPatternBroader(configured.e2e_spec_pattern, recommended.e2e.pattern)) {
|
|
222
|
+
findings.push({
|
|
223
|
+
key: 'e2e_spec_pattern',
|
|
224
|
+
configured: configured.e2e_spec_pattern,
|
|
225
|
+
recommended: recommended.e2e.pattern,
|
|
226
|
+
reason: `detected ${recommended.e2e.framework} under ${recommended.e2e.dir}/e2e/ (${recommended.e2e.confidence} confidence)`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
194
229
|
}
|
|
195
230
|
|
|
196
231
|
if (findings.length === 0) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* eval-ab-testing.js — HC-019k Agent A/B testing.
|
|
4
|
+
*
|
|
5
|
+
* Compare two prompt variants using the same eval scenarios.
|
|
6
|
+
* Reports which variant performs better across all checks.
|
|
7
|
+
*
|
|
8
|
+
* Usage: provide two versions of an agent prompt (A and B),
|
|
9
|
+
* run the same deterministic evals against both, compare results.
|
|
10
|
+
*
|
|
11
|
+
* Pure helper — no I/O, no LLM calls.
|
|
12
|
+
*/
|
|
13
|
+
const { runScenario } = require('./eval-runner');
|
|
14
|
+
const { wrapDeterministic } = require('./eval-three-valued');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Run A/B comparison for a set of scenarios against two prompt variants.
|
|
18
|
+
*
|
|
19
|
+
* @param {Array<object>} scenarios — eval scenarios (filtered for one agent)
|
|
20
|
+
* @param {string} promptA — current prompt text (control)
|
|
21
|
+
* @param {string} promptB — new prompt text (variant)
|
|
22
|
+
* @param {object} [opts]
|
|
23
|
+
* @param {string} [opts.labelA='A (current)']
|
|
24
|
+
* @param {string} [opts.labelB='B (variant)']
|
|
25
|
+
* @returns {{ agent, labelA, labelB, scenarios: Array, summary }}
|
|
26
|
+
*/
|
|
27
|
+
function comparePrompts(scenarios, promptA, promptB, opts = {}) {
|
|
28
|
+
const { labelA = 'A (current)', labelB = 'B (variant)' } = opts;
|
|
29
|
+
const results = [];
|
|
30
|
+
|
|
31
|
+
for (const scenario of scenarios) {
|
|
32
|
+
const resultA = wrapDeterministic(runScenario(scenario, promptA));
|
|
33
|
+
const resultB = wrapDeterministic(runScenario(scenario, promptB));
|
|
34
|
+
|
|
35
|
+
const winner =
|
|
36
|
+
resultA.verdict === 'pass' && resultB.verdict !== 'pass' ? 'A' :
|
|
37
|
+
resultB.verdict === 'pass' && resultA.verdict !== 'pass' ? 'B' :
|
|
38
|
+
resultA.checks_passed > resultB.checks_passed ? 'A' :
|
|
39
|
+
resultB.checks_passed > resultA.checks_passed ? 'B' :
|
|
40
|
+
'tie';
|
|
41
|
+
|
|
42
|
+
results.push({
|
|
43
|
+
id: scenario.id,
|
|
44
|
+
name: scenario.name || scenario.id,
|
|
45
|
+
a: { verdict: resultA.verdict, checks_passed: resultA.checks_passed, checks: resultA.checks },
|
|
46
|
+
b: { verdict: resultB.verdict, checks_passed: resultB.checks_passed, checks: resultB.checks },
|
|
47
|
+
winner,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const aWins = results.filter(r => r.winner === 'A').length;
|
|
52
|
+
const bWins = results.filter(r => r.winner === 'B').length;
|
|
53
|
+
const ties = results.filter(r => r.winner === 'tie').length;
|
|
54
|
+
|
|
55
|
+
const aTotal = results.reduce((s, r) => s + r.a.checks_passed, 0);
|
|
56
|
+
const bTotal = results.reduce((s, r) => s + r.b.checks_passed, 0);
|
|
57
|
+
const maxChecks = results.reduce((s, r) => s + r.a.checks, 0);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
agent: scenarios[0]?.evalAgent || scenarios[0]?.agent || 'unknown',
|
|
61
|
+
labelA,
|
|
62
|
+
labelB,
|
|
63
|
+
scenarios: results,
|
|
64
|
+
summary: {
|
|
65
|
+
total: results.length,
|
|
66
|
+
a_wins: aWins,
|
|
67
|
+
b_wins: bWins,
|
|
68
|
+
ties,
|
|
69
|
+
a_score: maxChecks > 0 ? Math.round((aTotal / maxChecks) * 100) : 0,
|
|
70
|
+
b_score: maxChecks > 0 ? Math.round((bTotal / maxChecks) * 100) : 0,
|
|
71
|
+
recommendation: aWins > bWins ? 'keep_a' : bWins > aWins ? 'use_b' : 'no_difference',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Format A/B comparison results.
|
|
78
|
+
*/
|
|
79
|
+
function formatComparison(result, format = 'pretty') {
|
|
80
|
+
if (format === 'json') return JSON.stringify(result, null, 2);
|
|
81
|
+
|
|
82
|
+
const lines = ['', 'Hone AI — A/B Prompt Comparison', '================================', ''];
|
|
83
|
+
lines.push(`Agent: ${result.agent}`);
|
|
84
|
+
lines.push(`Variant A: ${result.labelA}`);
|
|
85
|
+
lines.push(`Variant B: ${result.labelB}`);
|
|
86
|
+
lines.push('');
|
|
87
|
+
|
|
88
|
+
lines.push(' Scenario A B Winner');
|
|
89
|
+
lines.push(' -------- - - ------');
|
|
90
|
+
for (const s of result.scenarios) {
|
|
91
|
+
const name = (s.name || s.id).padEnd(32).slice(0, 32);
|
|
92
|
+
const a = `${s.a.checks_passed}/${s.a.checks}`.padStart(6);
|
|
93
|
+
const b = `${s.b.checks_passed}/${s.b.checks}`.padStart(6);
|
|
94
|
+
const winner = s.winner === 'tie' ? ' tie' : s.winner === 'A' ? ' <-A' : ' B->';
|
|
95
|
+
lines.push(` ${name} ${a} ${b} ${winner}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
lines.push('');
|
|
99
|
+
lines.push('----------------------------------');
|
|
100
|
+
lines.push(`Score: A=${result.summary.a_score}% | B=${result.summary.b_score}%`);
|
|
101
|
+
lines.push(`Wins: A=${result.summary.a_wins} | B=${result.summary.b_wins} | Ties=${result.summary.ties}`);
|
|
102
|
+
|
|
103
|
+
const rec = result.summary.recommendation;
|
|
104
|
+
const msg = rec === 'keep_a' ? 'Keep current prompt (A wins)' :
|
|
105
|
+
rec === 'use_b' ? 'Switch to variant B (B wins)' :
|
|
106
|
+
'No significant difference';
|
|
107
|
+
lines.push(`Recommendation: ${msg}`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { comparePrompts, formatComparison };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hone-ai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Hone AI — Enterprise SDLC Pipeline CLI",
|
|
5
5
|
"main": "hone-cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"bin/",
|
|
11
11
|
"hone-cli.js",
|
|
12
12
|
"lib/",
|
|
13
|
-
"!lib/*.test.js"
|
|
13
|
+
"!lib/*.test.js",
|
|
14
|
+
"schema/"
|
|
14
15
|
],
|
|
15
16
|
"scripts": {
|
|
16
17
|
"test": "echo \"No tests yet\" && exit 0",
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hone.ai/schema/metadata.schema.json",
|
|
4
|
+
"title": "Hone SDLC Pipeline Story Metadata",
|
|
5
|
+
"description": "Schema for .github/pipeline/<STORY-ID>/metadata.yml. Implements SC-010 §10 (metadata.yml as wire protocol) — fields read by 3+ agents must validate against this schema before agents run, otherwise typos silently degrade to no-op behavior.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": true,
|
|
8
|
+
"required": ["story_id", "title", "branch", "base", "steps"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"story_id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"minLength": 1,
|
|
13
|
+
"description": "Story identifier. Hone-server convention: SC-NNN, H-NNN, RP-NNN, AU-NNN, SR-NNN, HC-NNN; OptionsFlow convention: E-NNN-X. Adopters may use any pattern."
|
|
14
|
+
},
|
|
15
|
+
"title": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"minLength": 1,
|
|
18
|
+
"description": "Human-readable story title."
|
|
19
|
+
},
|
|
20
|
+
"issue": {
|
|
21
|
+
"type": ["integer", "string", "null"],
|
|
22
|
+
"description": "GitHub issue reference. Modern convention: integer issue number or null. Legacy convention: full GitHub issue URL string. Both accepted."
|
|
23
|
+
},
|
|
24
|
+
"branch": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"minLength": 1,
|
|
27
|
+
"description": "Git branch name. Convention: feat/<STORY-ID>-<slug> | fix/<STORY-ID>-<slug> | chore/..."
|
|
28
|
+
},
|
|
29
|
+
"base": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"minLength": 1,
|
|
32
|
+
"description": "Base branch the story merges to. Convention: develop. Stacked story chains (story B branched off feature/A) may use feat/...-style base names. Adopter override schemas may tighten the enum."
|
|
33
|
+
},
|
|
34
|
+
"captured_at": {
|
|
35
|
+
"type": ["string", "null"],
|
|
36
|
+
"description": "ISO date when the story was captured (YYYY-MM-DD). May be null."
|
|
37
|
+
},
|
|
38
|
+
"type": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"enum": ["feature", "enhancement", "bug", "bug_fix", "chore", "refactor", "docs", "meta-epic", "fix"],
|
|
41
|
+
"description": "Story type per SC-001 classifier vocabulary. 'bug_fix' is a legacy alias for 'bug'/'fix'; both accepted."
|
|
42
|
+
},
|
|
43
|
+
"priority": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Adopter priority. Optional. No canonical enum — different stories use P0/P1/P2/P3, high/medium/low, M, low-medium, etc. Adopters with strict policies override via --schema."
|
|
46
|
+
},
|
|
47
|
+
"phase": {
|
|
48
|
+
"type": ["string", "number"],
|
|
49
|
+
"description": "Story lifecycle phase. Modern convention: string ('backlog', 'in-progress', 'blocked', 'done', 'completed'). Legacy convention: numeric phase identifier (e.g. 6.2, 2.1). Both accepted; adopter override schema may tighten."
|
|
50
|
+
},
|
|
51
|
+
"story_type": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Free-form story type label (legacy field; prefer 'type' for new stories)."
|
|
54
|
+
},
|
|
55
|
+
"fast_track": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"description": "True if story is on the fast-track pipeline (skip steps 0-3 gates). Per SC-001 classifier output."
|
|
58
|
+
},
|
|
59
|
+
"hot_fix": {
|
|
60
|
+
"type": "boolean",
|
|
61
|
+
"description": "True if story is on the hot-fix pipeline. Per SC-001 classifier output."
|
|
62
|
+
},
|
|
63
|
+
"fix_for": {
|
|
64
|
+
"type": ["string", "null"],
|
|
65
|
+
"description": "Story ID this fixes (for regression-test policy per H-030 + SC-009 §Guardrail-before-fix). Null when this story is not a fix."
|
|
66
|
+
},
|
|
67
|
+
"parent_story": {
|
|
68
|
+
"type": ["string", "null"],
|
|
69
|
+
"description": "Parent story ID for sub-stories or follow-ups. Null at top level."
|
|
70
|
+
},
|
|
71
|
+
"sibling_pipelines": {
|
|
72
|
+
"type": ["array", "null"],
|
|
73
|
+
"items": { "type": "string" },
|
|
74
|
+
"description": "Story IDs sharing this pipeline. Used for multi-story epics."
|
|
75
|
+
},
|
|
76
|
+
"author": {
|
|
77
|
+
"type": ["string", "null"]
|
|
78
|
+
},
|
|
79
|
+
"created": {
|
|
80
|
+
"type": ["string", "null"]
|
|
81
|
+
},
|
|
82
|
+
"base_sha": {
|
|
83
|
+
"type": ["string", "number", "null"],
|
|
84
|
+
"description": "Base commit SHA. Permissive type — hex-like SHA strings (92192e7) get parsed as numbers by js-yaml. Adopters who want strict SHA strings can override schema."
|
|
85
|
+
},
|
|
86
|
+
"steps": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"additionalProperties": false,
|
|
89
|
+
"description": "Pipeline step status table. Wire-protocol object — no extra step IDs allowed.",
|
|
90
|
+
"properties": {
|
|
91
|
+
"step_0": { "$ref": "#/$defs/step" },
|
|
92
|
+
"step_1": { "$ref": "#/$defs/step" },
|
|
93
|
+
"step_2": { "$ref": "#/$defs/step" },
|
|
94
|
+
"step_3a": { "$ref": "#/$defs/step" },
|
|
95
|
+
"step_3b": { "$ref": "#/$defs/step" },
|
|
96
|
+
"step_4": { "$ref": "#/$defs/step" },
|
|
97
|
+
"step_5": { "$ref": "#/$defs/step" },
|
|
98
|
+
"step_5b": { "$ref": "#/$defs/step" },
|
|
99
|
+
"step_5c": { "$ref": "#/$defs/step" }
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
"cross_validation": {
|
|
103
|
+
"type": "object",
|
|
104
|
+
"additionalProperties": true,
|
|
105
|
+
"description": "Cross-validation findings per H-035 + SC-001..SC-005."
|
|
106
|
+
},
|
|
107
|
+
"self_applied_classifier": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": true,
|
|
110
|
+
"description": "SC-001 classifier output recorded for the story. Tracks the classification decision so future readers can audit the routing."
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
"$defs": {
|
|
114
|
+
"step": {
|
|
115
|
+
"type": "object",
|
|
116
|
+
"additionalProperties": true,
|
|
117
|
+
"description": "Per-step status. The wire-protocol fields (status, agent) are validated; adopter and historical extension fields (acceptance_criteria_count, acs_met, automation_rationale, bug_caught_in_step, etc.) are passed through. Set additionalProperties: false in your adopter override schema for stricter enforcement.",
|
|
118
|
+
"properties": {
|
|
119
|
+
"status": {
|
|
120
|
+
"type": "string",
|
|
121
|
+
"description": "Step status. Hone-server uses 'completed'/'in_progress'/'skipped'; older files may use 'complete'/'in-progress' — both accepted. Adopter override schemas may tighten the enum."
|
|
122
|
+
},
|
|
123
|
+
"agent": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "Agent ID (story-groomer, implementation-planner, unit-test-writer, e2e-qa-planner, e2e-test-spec-writer, code-builder, code-reviewer, delivery-architect, etc.)."
|
|
126
|
+
},
|
|
127
|
+
"artifact": {
|
|
128
|
+
"type": ["string", "null"],
|
|
129
|
+
"description": "Path or filename of the step's artifact (e.g., step-0-grooming.md). May be null if the step is pending."
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|