@aikdna/kdna-cli 0.19.2 → 0.20.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.
@@ -0,0 +1,875 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { error, EXIT } = require('./_common');
5
+ const {
6
+ loadWorkPack,
7
+ validateWorkPackManifest,
8
+ checkWorkPackStructure,
9
+ inspectWorkPack,
10
+ } = require('@aikdna/kdna-core');
11
+
12
+ // ── Helpers ─────────────────────────────────────────────────────────
13
+
14
+ function uid(prefix) {
15
+ return `${prefix}_${crypto.randomBytes(4).toString('hex')}`;
16
+ }
17
+
18
+ function timestamp() {
19
+ return new Date().toISOString();
20
+ }
21
+
22
+ function readJsonSafe(p) {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ // ── Validate ────────────────────────────────────────────────────────
31
+
32
+ function cmdWorkpackValidate(target, args = []) {
33
+ const abs = path.resolve(target);
34
+ if (!fs.existsSync(abs)) error(`Work Pack directory not found: ${abs}`);
35
+ if (!fs.statSync(abs).isDirectory())
36
+ error(`Not a directory: ${abs}. Work Packs must be directories.`);
37
+
38
+ const jsonMode = args.includes('--json');
39
+ const schemaOnly = args.includes('--schema-only');
40
+
41
+ const { manifest } = loadWorkPack(abs);
42
+ if (!manifest) {
43
+ if (jsonMode) {
44
+ console.log(JSON.stringify({ valid: false, errors: [`workpack.json not found in ${abs}`] }));
45
+ } else {
46
+ error(`workpack.json not found in ${abs}`);
47
+ }
48
+ process.exit(EXIT.VALIDATION_FAILED);
49
+ }
50
+
51
+ const schemaResult = validateWorkPackManifest(manifest);
52
+ const structResult = schemaOnly
53
+ ? { complete: true, missing: [] }
54
+ : checkWorkPackStructure(manifest, abs);
55
+ const valid = schemaResult.valid && structResult.complete;
56
+
57
+ if (jsonMode) {
58
+ console.log(
59
+ JSON.stringify(
60
+ {
61
+ valid,
62
+ level: valid ? (structResult.complete ? 'L1' : 'L0') : 'INVALID',
63
+ schema: { valid: schemaResult.valid, errors: schemaResult.errors },
64
+ structure: structResult.complete
65
+ ? { complete: true }
66
+ : { complete: false, missing: structResult.missing },
67
+ },
68
+ null,
69
+ 2,
70
+ ),
71
+ );
72
+ } else {
73
+ if (valid) {
74
+ console.log(`✓ Valid: ${manifest.name} v${manifest.version}`);
75
+ console.log(` Level: L1 — structurally complete`);
76
+ } else {
77
+ if (!schemaResult.valid) {
78
+ console.error(`✗ Schema validation failed for ${manifest.name}:`);
79
+ schemaResult.errors.forEach((e) => console.error(` ${e}`));
80
+ }
81
+ if (!structResult.complete) {
82
+ console.error(`✗ Structural completeness — missing files:`);
83
+ structResult.missing.forEach((f) => console.error(` ${f}`));
84
+ }
85
+ }
86
+ }
87
+ process.exit(valid ? EXIT.OK : EXIT.VALIDATION_FAILED);
88
+ }
89
+
90
+ // ── Inspect ─────────────────────────────────────────────────────────
91
+
92
+ function cmdWorkpackInspect(target, args = []) {
93
+ const abs = path.resolve(target);
94
+ if (!fs.existsSync(abs)) error(`Work Pack directory not found: ${abs}`);
95
+ const { manifest } = loadWorkPack(abs);
96
+ if (!manifest) error(`workpack.json not found in ${abs}`);
97
+ const jsonMode = args.includes('--json');
98
+ const info = inspectWorkPack(manifest, abs);
99
+
100
+ if (jsonMode) {
101
+ console.log(JSON.stringify(info, null, 2));
102
+ return;
103
+ }
104
+
105
+ console.log(`${info.name} v${info.version}`);
106
+ console.log(` Description: ${info.description}`);
107
+ console.log(` Status: ${info.status}`);
108
+ console.log(` Access: ${info.access}`);
109
+ console.log(` License: ${info.license}`);
110
+ console.log(` Format: ${info.format_version}\n`);
111
+
112
+ console.log('KDNA:');
113
+ console.log(` Mode: ${info.kdna.mode}`);
114
+ for (const a of info.kdna.assets) {
115
+ console.log(` • ${a.name} @ ${a.version} [${a.role}]`);
116
+ }
117
+ console.log('');
118
+
119
+ if (info.skills.length) {
120
+ console.log('Skills:');
121
+ for (const s of info.skills) {
122
+ const flags = [];
123
+ if (s.required) flags.push('required');
124
+ if (s.fallback) flags.push(`fallback:${s.fallback}`);
125
+ console.log(
126
+ ` • ${s.name}${s.type !== 'unspecified' ? ` (${s.type})` : ''} ${flags.length ? `[${flags.join(', ')}]` : ''}`,
127
+ );
128
+ }
129
+ console.log('');
130
+ }
131
+
132
+ if (info.templates?.task || info.templates?.output) {
133
+ console.log('Templates:');
134
+ if (info.templates.task) console.log(` Task: ${info.templates.task}`);
135
+ if (info.templates.output) console.log(` Output: ${info.templates.output}`);
136
+ console.log('');
137
+ }
138
+
139
+ console.log('Quality & Safety:');
140
+ console.log(` Review Gates: ${info.review_gates}`);
141
+ console.log(` Risk Policy: ${info.has_risk_policy ? '✓' : '✗'}`);
142
+ console.log(` Trace Policy: ${info.has_trace_policy ? '✓' : '✗'}`);
143
+ console.log(` Eval Cases: ${info.has_evals ? '✓' : '✗'}`);
144
+ console.log(` Structural: ${info.structural_complete ? 'complete ✓' : 'incomplete ✗'}`);
145
+ if (info.missing_files.length) {
146
+ info.missing_files.forEach((f) => console.log(` Missing: ${f}`));
147
+ }
148
+ }
149
+
150
+ // ── Explain ─────────────────────────────────────────────────────────
151
+
152
+ function cmdWorkpackExplain(target) {
153
+ const abs = path.resolve(target);
154
+ if (!fs.existsSync(abs)) error(`Work Pack directory not found: ${abs}`);
155
+ const { manifest } = loadWorkPack(abs);
156
+ if (!manifest) error(`workpack.json not found in ${abs}`);
157
+ const info = inspectWorkPack(manifest, abs);
158
+ const lines = [];
159
+
160
+ lines.push(`${info.name} is a KDNA Work Pack — a packaged AI work capability.`);
161
+ lines.push('');
162
+ lines.push(
163
+ `It combines ${info.kdna.assets.length} KDNA judgment asset(s) with ${info.skills.length} skill(s), ${info.review_gates} review gate(s), and quality controls to perform: "${info.description}"`,
164
+ );
165
+ lines.push('');
166
+
167
+ const primary = info.kdna.assets.find((a) => a.role === 'primary');
168
+ const constraints = info.kdna.assets.filter((a) => a.role === 'constraint');
169
+ if (primary)
170
+ lines.push(
171
+ `The primary judgment framework is "${primary.name}" — it defines the core standards for this task.`,
172
+ );
173
+ if (constraints.length) {
174
+ lines.push(
175
+ `${constraints.map((a) => `"${a.name}"`).join(' and ')} provides additional safety or quality boundaries.`,
176
+ );
177
+ }
178
+ lines.push('');
179
+
180
+ if (info.skills.length) {
181
+ const req = info.skills.filter((s) => s.required);
182
+ const opt = info.skills.filter((s) => !s.required);
183
+ if (req.length) lines.push(`Required skills: ${req.map((s) => s.name).join(', ')}.`);
184
+ if (opt.length) lines.push(`Optional skills: ${opt.map((s) => s.name).join(', ')}.`);
185
+ lines.push('');
186
+ }
187
+
188
+ lines.push(`${info.review_gates} review gate(s) check output quality.`);
189
+ if (info.has_risk_policy)
190
+ lines.push('Risk policy configured — high-risk actions may be blocked.');
191
+ if (info.has_trace_policy)
192
+ lines.push('Trace policy ensures all judgment decisions are auditable.');
193
+ lines.push('');
194
+ lines.push(`Status: ${info.status}.`);
195
+
196
+ console.log(lines.join('\n'));
197
+ }
198
+
199
+ // ── Plan ────────────────────────────────────────────────────────────
200
+
201
+ /**
202
+ * kdna workpack plan <path> [--input <file|text>] [--json]
203
+ *
204
+ * Generate a dry-run execution plan showing what WOULD happen without
205
+ * actually invoking an LLM or external tools.
206
+ */
207
+ function cmdWorkpackPlan(target, args = []) {
208
+ const abs = path.resolve(target);
209
+ if (!fs.existsSync(abs)) error(`Work Pack directory not found: ${abs}`);
210
+
211
+ const { manifest } = loadWorkPack(abs);
212
+ if (!manifest) error(`workpack.json not found in ${abs}`);
213
+
214
+ const jsonMode = args.includes('--json');
215
+
216
+ // Resolve input
217
+ const inputIdx = args.indexOf('--input');
218
+ let input = null;
219
+ let inputSource = null;
220
+ if (inputIdx >= 0) {
221
+ const val = args[inputIdx + 1];
222
+ if (val && !val.startsWith('--')) {
223
+ const maybeFile = path.resolve(val);
224
+ if (fs.existsSync(maybeFile) && fs.statSync(maybeFile).isFile()) {
225
+ input = fs.readFileSync(maybeFile, 'utf8').slice(0, 500);
226
+ inputSource = val;
227
+ } else {
228
+ input = val;
229
+ inputSource = '<inline>';
230
+ }
231
+ }
232
+ }
233
+
234
+ // Build plan
235
+ const sessionId = uid('wp_ses');
236
+ const plan = buildPlan(manifest, abs, sessionId, input, inputSource);
237
+
238
+ if (jsonMode) {
239
+ console.log(JSON.stringify(plan, null, 2));
240
+ return;
241
+ }
242
+
243
+ console.log(`Work Pack Plan: ${manifest.name} v${manifest.version}`);
244
+ console.log(`Session: ${sessionId}`);
245
+ console.log(`Mode: dry-run`);
246
+ console.log('');
247
+
248
+ if (input) {
249
+ console.log(`Input: ${input.slice(0, 120)}${input.length > 120 ? '...' : ''}`);
250
+ console.log('');
251
+ }
252
+
253
+ console.log(`KDNA Assets (${plan.kdna_assets.length}):`);
254
+ for (const a of plan.kdna_assets) {
255
+ console.log(` • ${a.name} @ ${a.version} [${a.role}] — ${a.status}`);
256
+ }
257
+ console.log('');
258
+
259
+ console.log(`Skills (${plan.skills.length}):`);
260
+ for (const s of plan.skills) {
261
+ const icon = s.available ? (s.fallback_used ? '⚠' : '✓') : '✗';
262
+ const fb = s.fallback_used ? ` → fallback: ${s.fallback_used}` : '';
263
+ console.log(` ${icon} ${s.name} [${s.required ? 'required' : 'optional'}]${fb}`);
264
+ }
265
+ console.log('');
266
+
267
+ console.log(`Review Gates (${plan.review_gates.length}):`);
268
+ for (const g of plan.review_gates) {
269
+ const criteriaCount = g.criteria_count || '?';
270
+ console.log(` • ${g.name} (${criteriaCount} criteria)`);
271
+ }
272
+ console.log('');
273
+
274
+ console.log(`Risk Checks (${plan.risk_checks.length}):`);
275
+ for (const r of plan.risk_checks) {
276
+ console.log(` • ${r.timing}: ${r.description}`);
277
+ }
278
+ console.log('');
279
+
280
+ console.log(
281
+ `Trace: ${plan.trace_enabled ? 'enabled' : 'disabled'} (${plan.trace_fields.length} fields)`,
282
+ );
283
+ console.log(
284
+ `Conflicts: ${plan.kdna_assets.length > 1 ? 'will be exposed if detected' : 'N/A (single asset)'}`,
285
+ );
286
+ console.log('');
287
+
288
+ console.log('Execution Phases:');
289
+ for (const p of plan.phases) {
290
+ console.log(` ${p.order}. ${p.name} → ${p.expected_duration || 'instant'}`);
291
+ }
292
+ }
293
+
294
+ function buildPlan(manifest, rootDir, sessionId, input, inputSource) {
295
+ const kdnaAssets = [];
296
+ if (manifest.kdna?.mode === 'single') {
297
+ kdnaAssets.push({
298
+ name: manifest.kdna.asset.name,
299
+ version: manifest.kdna.asset.version,
300
+ role: manifest.kdna.asset.role,
301
+ digest: manifest.kdna.asset.digest || null,
302
+ status: 'resolved',
303
+ });
304
+ } else if (manifest.kdna?.mode === 'cluster') {
305
+ for (const a of manifest.kdna.assets || []) {
306
+ kdnaAssets.push({
307
+ name: a.name,
308
+ version: a.version,
309
+ role: a.role,
310
+ digest: a.digest || null,
311
+ status: 'resolved',
312
+ });
313
+ }
314
+ }
315
+
316
+ const skills = (manifest.skills || []).map((s) => {
317
+ const fallbackAvail = s.fallback ? true : false;
318
+ return {
319
+ name: s.name,
320
+ type: s.type || 'unspecified',
321
+ required: s.required !== false,
322
+ available: false, // dry-run cannot verify
323
+ fallback_used: !s.required && s.fallback ? s.fallback : null,
324
+ mcp_server: s.mcp_server || null,
325
+ };
326
+ });
327
+
328
+ const reviewGates = (manifest.review_gates || []).map((gp) => {
329
+ const gatePath = path.resolve(rootDir, gp);
330
+ const gate = readJsonSafe(gatePath);
331
+ return {
332
+ name: gate?.name || path.basename(gp, '.json'),
333
+ path: gp,
334
+ criteria_count: gate?.criteria?.length || 0,
335
+ exists: !!gate,
336
+ };
337
+ });
338
+
339
+ let riskPolicy = null;
340
+ if (manifest.risk_policy) {
341
+ const rp = readJsonSafe(path.resolve(rootDir, manifest.risk_policy));
342
+ riskPolicy = rp;
343
+ }
344
+
345
+ const riskChecks = [
346
+ { timing: 'pre-skill', description: 'Check prohibited actions before any skill invocation' },
347
+ { timing: 'post-exec', description: 'Assess risk levels after agent execution' },
348
+ { timing: 'pre-output', description: 'Final risk check before output rendering' },
349
+ ];
350
+
351
+ let traceEnabled = false;
352
+ let traceFields = [];
353
+ if (manifest.trace_policy) {
354
+ const tp = readJsonSafe(path.resolve(rootDir, manifest.trace_policy));
355
+ traceEnabled = !!tp;
356
+ traceFields = tp?.record || [];
357
+ }
358
+
359
+ const phases = [
360
+ { order: 1, name: 'Input Normalization', expected_duration: 'instant' },
361
+ { order: 2, name: 'Work Pack Resolution', expected_duration: 'instant' },
362
+ {
363
+ order: 3,
364
+ name: 'KDNA Loading',
365
+ expected_duration: kdnaAssets.length > 1 ? 'brief' : 'instant',
366
+ },
367
+ { order: 4, name: 'Skill Binding', expected_duration: 'instant' },
368
+ { order: 5, name: 'Agent Execution', expected_duration: 'model-dependent' },
369
+ { order: 6, name: 'Review Gate Execution', expected_duration: `${reviewGates.length} gates` },
370
+ { order: 7, name: 'Risk Policy Enforcement', expected_duration: 'instant' },
371
+ { order: 8, name: 'Output Processing', expected_duration: 'instant' },
372
+ { order: 9, name: 'Trace Generation', expected_duration: 'instant' },
373
+ { order: 10, name: 'Report Generation', expected_duration: 'instant' },
374
+ ];
375
+
376
+ return {
377
+ session_id: sessionId,
378
+ workpack: { name: manifest.name, version: manifest.version },
379
+ mode: 'dry-run',
380
+ input: input ? { source: inputSource, preview: input.slice(0, 200) } : null,
381
+ kdna_assets: kdnaAssets,
382
+ skills,
383
+ review_gates: reviewGates,
384
+ risk_checks: riskChecks,
385
+ risk_policy_loaded: !!riskPolicy,
386
+ risk_levels: riskPolicy?.levels?.map((l) => l.level) || ['low', 'medium', 'high', 'critical'],
387
+ trace_enabled: traceEnabled,
388
+ trace_fields: traceFields,
389
+ phases,
390
+ limitations: [
391
+ 'This is a dry-run plan. Skills are not actually invoked.',
392
+ 'KDNA assets are not actually loaded — references are resolved syntactically.',
393
+ 'LLM is not called — agent execution is simulated.',
394
+ ],
395
+ created_at: timestamp(),
396
+ };
397
+ }
398
+
399
+ // ── Run ─────────────────────────────────────────────────────────────
400
+
401
+ /**
402
+ * kdna workpack run <path> --input <file> [--dry-run] [--json]
403
+ *
404
+ * Execute a Work Pack. With --dry-run, simulates execution without LLM or external tools.
405
+ */
406
+ function cmdWorkpackRun(target, args = []) {
407
+ const abs = path.resolve(target);
408
+ if (!fs.existsSync(abs)) error(`Work Pack directory not found: ${abs}`);
409
+
410
+ const { manifest } = loadWorkPack(abs);
411
+ if (!manifest) error(`workpack.json not found in ${abs}`);
412
+
413
+ const dryRun = args.includes('--dry-run');
414
+ const jsonMode = args.includes('--json');
415
+
416
+ const inputIdx = args.indexOf('--input');
417
+ let input = null;
418
+ if (inputIdx >= 0) {
419
+ const val = args[inputIdx + 1];
420
+ if (val && !val.startsWith('--')) {
421
+ const maybeFile = path.resolve(val);
422
+ if (fs.existsSync(maybeFile) && fs.statSync(maybeFile).isFile()) {
423
+ input = fs.readFileSync(maybeFile, 'utf8');
424
+ } else {
425
+ input = val;
426
+ }
427
+ }
428
+ }
429
+ if (!input) error('Usage: kdna workpack run <path> --input <file|text> [--dry-run] [--json]');
430
+
431
+ const sessionId = uid('wp_ses');
432
+ const runId = uid('wp_run');
433
+
434
+ if (dryRun) {
435
+ const plan = buildPlan(manifest, abs, sessionId, input, '--input');
436
+ const dryRunResult = buildDryRunResult(manifest, plan, sessionId, runId, input);
437
+
438
+ if (jsonMode) {
439
+ console.log(JSON.stringify(dryRunResult, null, 2));
440
+ return;
441
+ }
442
+
443
+ console.log(`Work Pack Dry Run: ${manifest.name} v${manifest.version}`);
444
+ console.log(`Session: ${sessionId} Run: ${runId}`);
445
+ console.log('');
446
+ console.log(`Status: ${dryRunResult.run.status}`);
447
+ console.log(`Overall Gate: ${dryRunResult.run.overall_gate_result}`);
448
+ console.log(
449
+ `Gates: ${dryRunResult.run.review_gate_results.map((g) => `${g.gate_name}=${g.result}`).join(', ')}`,
450
+ );
451
+ console.log(`Risk Events: ${dryRunResult.run.risk_events.length}`);
452
+ console.log(`Conflicts: ${dryRunResult.run.conflicts.length}`);
453
+ console.log(`Skill Fallbacks: ${dryRunResult.run.skill_fallbacks.length}`);
454
+ if (dryRunResult.limitations.length) {
455
+ console.log(`\nLimitations:`);
456
+ dryRunResult.limitations.forEach((l) => console.log(` ⚠ ${l}`));
457
+ }
458
+ } else {
459
+ error('Real execution (without --dry-run) requires an LLM backend. Coming in Phase 5.2.');
460
+ }
461
+ }
462
+
463
+ function buildDryRunResult(manifest, plan, sessionId, runId, input) {
464
+ // Simulate gate results — in dry-run, gates pass by default
465
+ const gateResults = plan.review_gates.map((g) => ({
466
+ gate_name: g.name,
467
+ result: 'pass',
468
+ timestamp: timestamp(),
469
+ criteria_results: [],
470
+ reasoning: '[dry-run] Gates are simulated. No real judgment was executed.',
471
+ }));
472
+
473
+ const riskEvents = [
474
+ {
475
+ risk_event_id: uid('risk'),
476
+ timestamp: timestamp(),
477
+ level: 'low',
478
+ source: 'runtime_guard',
479
+ reason: '[dry-run] Risk assessment is simulated.',
480
+ action: 'proceed_with_note',
481
+ status: 'confirmed',
482
+ },
483
+ ];
484
+
485
+ const limitations = [...plan.limitations];
486
+
487
+ // Check for skill fallbacks
488
+ const fallbacks = [];
489
+ for (const s of plan.skills) {
490
+ if (s.fallback_used) {
491
+ fallbacks.push({
492
+ skill: s.name,
493
+ status: 'unavailable',
494
+ fallback: { skill: s.fallback_used, status: 'used', reason: 'primary_skill_unavailable' },
495
+ required: s.required,
496
+ impact: 'reduced_confidence',
497
+ confidence_reduction: `${s.name} was unavailable. ${s.fallback_used} was used as fallback — reduced confidence.`,
498
+ trace_note: `${s.name} not executed. ${s.fallback_used} used as fallback.`,
499
+ });
500
+ }
501
+ }
502
+
503
+ return {
504
+ session: {
505
+ session_id: sessionId,
506
+ workpack: plan.workpack,
507
+ state: 'completed',
508
+ mode: 'dry-run',
509
+ created_at: timestamp(),
510
+ },
511
+ run: {
512
+ session_id: sessionId,
513
+ run_id: runId,
514
+ mode: 'dry-run',
515
+ status: 'completed',
516
+ overall_gate_result: 'pass',
517
+ review_gate_results: gateResults,
518
+ risk_events: riskEvents,
519
+ conflicts: [],
520
+ skill_fallbacks: fallbacks,
521
+ unresolved_questions: [],
522
+ started_at: timestamp(),
523
+ completed_at: timestamp(),
524
+ },
525
+ trace: {
526
+ trace_id: uid('wp_trc'),
527
+ session_id: sessionId,
528
+ trace_version: '0.2',
529
+ workpack_identity: plan.workpack,
530
+ kdna_assets_loaded: plan.kdna_assets.map((a) => ({
531
+ name: a.name,
532
+ version: a.version,
533
+ role: a.role,
534
+ })),
535
+ entries: [
536
+ { timestamp: timestamp(), type: 'phase_transition', data: { phase: 'resolution' } },
537
+ {
538
+ timestamp: timestamp(),
539
+ type: 'phase_transition',
540
+ data: { phase: 'kdna_loading', assets_count: plan.kdna_assets.length },
541
+ },
542
+ {
543
+ timestamp: timestamp(),
544
+ type: 'phase_transition',
545
+ data: { phase: 'skill_binding', skills_bound: plan.skills.length },
546
+ },
547
+ {
548
+ timestamp: timestamp(),
549
+ type: 'phase_transition',
550
+ data: { phase: 'completed', note: '[dry-run] No real execution performed.' },
551
+ },
552
+ ],
553
+ conflict_log: [],
554
+ skill_fallback_log: fallbacks,
555
+ integrity: { append_only: true, signed: false, digest: null },
556
+ generated_at: timestamp(),
557
+ },
558
+ limitations,
559
+ };
560
+ }
561
+
562
+ // ── Report ──────────────────────────────────────────────────────────
563
+
564
+ /**
565
+ * kdna workpack report <session-id|path> [--json]
566
+ *
567
+ * Generate or display a Work Pack session report.
568
+ * If given a session ID, looks up the run directory.
569
+ * If given a path, reads the report from that directory.
570
+ */
571
+ function cmdWorkpackReport(target, args = []) {
572
+ const jsonMode = args.includes('--json');
573
+
574
+ // Accept either a session ID or a path
575
+ let reportData = null;
576
+
577
+ // Try as a path first
578
+ const abs = path.resolve(target);
579
+ if (fs.existsSync(abs)) {
580
+ if (fs.statSync(abs).isDirectory()) {
581
+ const runResultPath = path.join(abs, 'run-result.json');
582
+ const tracePath = path.join(abs, 'judgment-trace.json');
583
+ if (fs.existsSync(runResultPath)) {
584
+ reportData = buildReportFromRun(abs, runResultPath, tracePath);
585
+ }
586
+ }
587
+ }
588
+
589
+ if (!reportData) {
590
+ // Generate a skeleton report
591
+ reportData = {
592
+ session_id: target,
593
+ workpack: { name: 'unknown', version: 'unknown' },
594
+ generated_at: timestamp(),
595
+ summary: {
596
+ status: 'unknown',
597
+ overall_gate_result: 'unknown',
598
+ total_gates: 0,
599
+ gates_passed: 0,
600
+ gates_failed: 0,
601
+ risk_events_total: 0,
602
+ risk_events_high: 0,
603
+ risk_events_critical: 0,
604
+ conflicts_detected: 0,
605
+ skill_fallbacks_used: 0,
606
+ unresolved_questions_count: 0,
607
+ duration_ms: 0,
608
+ limitations: ['No run data found for this session.'],
609
+ },
610
+ judgment_report: {},
611
+ review_report: [],
612
+ risk_report: {},
613
+ conflict_report: [],
614
+ skill_fallbacks: [],
615
+ output_available: false,
616
+ output_path: null,
617
+ };
618
+ }
619
+
620
+ if (jsonMode) {
621
+ console.log(JSON.stringify(reportData, null, 2));
622
+ return;
623
+ }
624
+
625
+ console.log(`Work Pack Report: ${reportData.workpack.name} v${reportData.workpack.version}`);
626
+ console.log(`Session: ${reportData.session_id}`);
627
+ console.log(`Generated: ${reportData.generated_at}`);
628
+ console.log('');
629
+ console.log(`Status: ${reportData.summary.status}`);
630
+ console.log(`Gate Result: ${reportData.summary.overall_gate_result}`);
631
+ console.log(
632
+ `Gates: ${reportData.summary.gates_passed}/${reportData.summary.total_gates} passed`,
633
+ );
634
+ console.log(
635
+ `Risk Events: ${reportData.summary.risk_events_total} (${reportData.summary.risk_events_high} high)`,
636
+ );
637
+ console.log(`Conflicts: ${reportData.summary.conflicts_detected}`);
638
+ console.log(`Fallbacks: ${reportData.summary.skill_fallbacks_used}`);
639
+ console.log(
640
+ `Output: ${reportData.output_available ? reportData.output_path : 'not available'}`,
641
+ );
642
+ if (reportData.summary.limitations?.length) {
643
+ console.log(`\nLimitations:`);
644
+ reportData.summary.limitations.forEach((l) => console.log(` ⚠ ${l}`));
645
+ }
646
+ }
647
+
648
+ function buildReportFromRun(dirPath, runResultPath, tracePath) {
649
+ const runResult = readJsonSafe(runResultPath);
650
+ const trace = readJsonSafe(tracePath);
651
+ // Build a basic report from existing data — full implementation in Phase 5.2
652
+ return {
653
+ session_id: runResult?.session_id || path.basename(dirPath),
654
+ workpack: { name: 'code-review', version: '0.1.0' },
655
+ generated_at: timestamp(),
656
+ summary: {
657
+ status: runResult?.status || 'unknown',
658
+ overall_gate_result: runResult?.overall_gate_result || 'unknown',
659
+ total_gates: (runResult?.review_gate_results || []).length,
660
+ gates_passed: (runResult?.review_gate_results || []).filter((g) => g.result === 'pass')
661
+ .length,
662
+ gates_failed: (runResult?.review_gate_results || []).filter((g) => g.result !== 'pass')
663
+ .length,
664
+ risk_events_total: (runResult?.risk_events || []).length,
665
+ risk_events_high: 0,
666
+ risk_events_critical: 0,
667
+ conflicts_detected: (runResult?.conflicts || []).length,
668
+ skill_fallbacks_used: (runResult?.skill_fallbacks || []).length,
669
+ unresolved_questions_count: (runResult?.unresolved_questions || []).length,
670
+ duration_ms: 0,
671
+ limitations: trace?.entries?.find(
672
+ (e) => e.type === 'phase_transition' && e.data?.phase === 'completed' && e.data?.note,
673
+ )
674
+ ? [
675
+ trace.entries.find(
676
+ (e) => e.type === 'phase_transition' && e.data?.phase === 'completed',
677
+ ).data.note,
678
+ ]
679
+ : [],
680
+ },
681
+ review_report: runResult?.review_gate_results || [],
682
+ risk_report: { events: runResult?.risk_events || [], highest_risk_level: 'low' },
683
+ conflict_report: runResult?.conflicts || [],
684
+ skill_fallbacks: runResult?.skill_fallbacks || [],
685
+ output_available: !!runResult?.output_path,
686
+ output_path: runResult?.output_path || null,
687
+ };
688
+ }
689
+
690
+ // ── Init ────────────────────────────────────────────────────────────
691
+
692
+ function cmdWorkpackInit(name, argsArr = []) {
693
+ const domainIdx = argsArr.indexOf('--domain');
694
+ const domain = domainIdx >= 0 ? argsArr[domainIdx + 1] : 'code_review';
695
+
696
+ if (!name) error('Usage: kdna workpack init <name> [--domain <domain>]');
697
+
698
+ const dir = path.resolve(name);
699
+ if (fs.existsSync(dir)) error(`Directory "${name}" already exists.`);
700
+
701
+ const dirs = [
702
+ 'kdna',
703
+ 'skills',
704
+ 'templates',
705
+ 'review-gates',
706
+ 'risk',
707
+ 'trace',
708
+ 'evals',
709
+ 'examples',
710
+ ];
711
+ for (const d of dirs) fs.mkdirSync(path.join(dir, d), { recursive: true });
712
+
713
+ const manifest = {
714
+ format: 'kdna-workpack',
715
+ format_version: '0.1',
716
+ name,
717
+ version: '0.1.0',
718
+ description: `A KDNA Work Pack for ${name.replace(/-/g, ' ')}.`,
719
+ status: 'draft',
720
+ access: 'open',
721
+ license: 'Apache-2.0',
722
+ kdna: { mode: 'single', asset: { name: domain, version: '^1.0.0', role: 'primary' } },
723
+ skills: [{ name: 'analyze_input', type: 'analysis', required: true }],
724
+ templates: { task: 'templates/task-template.md', output: 'templates/output-template.md' },
725
+ review_gates: ['review-gates/quality-gate.json'],
726
+ risk_policy: 'risk/risk-policy.json',
727
+ trace_policy: 'trace/trace-policy.json',
728
+ evals: 'evals/cases.jsonl',
729
+ };
730
+ fs.writeFileSync(path.join(dir, 'workpack.json'), JSON.stringify(manifest, null, 2) + '\n');
731
+ fs.writeFileSync(
732
+ path.join(dir, 'review-gates/quality-gate.json'),
733
+ JSON.stringify(
734
+ {
735
+ name: 'quality-gate',
736
+ description: 'Checks whether the output meets quality standards.',
737
+ criteria: [{ id: 'completeness', description: 'Output covers all required aspects' }],
738
+ results: {
739
+ pass: 'All criteria satisfied',
740
+ redo: 'Issues found',
741
+ block: 'Critical issues',
742
+ human_review: 'Ambiguous',
743
+ },
744
+ },
745
+ null,
746
+ 2,
747
+ ) + '\n',
748
+ );
749
+ fs.writeFileSync(
750
+ path.join(dir, 'risk/risk-policy.json'),
751
+ JSON.stringify(
752
+ {
753
+ levels: [
754
+ { level: 'low', label: 'Low', action: 'proceed_with_note', description: 'Minor issues' },
755
+ {
756
+ level: 'medium',
757
+ label: 'Medium',
758
+ action: 'flag_and_continue',
759
+ description: 'Notable issues',
760
+ },
761
+ {
762
+ level: 'high',
763
+ label: 'High',
764
+ action: 'require_confirmation',
765
+ description: 'Requires confirmation',
766
+ },
767
+ { level: 'critical', label: 'Critical', action: 'block', description: 'Block execution' },
768
+ ],
769
+ },
770
+ null,
771
+ 2,
772
+ ) + '\n',
773
+ );
774
+ fs.writeFileSync(
775
+ path.join(dir, 'trace/trace-policy.json'),
776
+ JSON.stringify(
777
+ {
778
+ record: [
779
+ 'workpack_identity',
780
+ 'kdna_assets_loaded',
781
+ 'review_gate_results',
782
+ 'risk_events',
783
+ 'final_output_path',
784
+ ],
785
+ integrity: { sign_trace: false, include_timestamps: true },
786
+ },
787
+ null,
788
+ 2,
789
+ ) + '\n',
790
+ );
791
+ fs.writeFileSync(
792
+ path.join(dir, 'skills/skill-bindings.json'),
793
+ JSON.stringify([{ name: 'analyze_input', type: 'analysis', required: true }], null, 2) + '\n',
794
+ );
795
+ fs.writeFileSync(
796
+ path.join(dir, 'kdna/references.json'),
797
+ JSON.stringify(
798
+ {
799
+ kdna_assets: [{ name: domain, version: '^1.0.0', role: 'primary' }],
800
+ routing: { strategy: 'role_based' },
801
+ conflict_resolution: 'expose_all',
802
+ },
803
+ null,
804
+ 2,
805
+ ) + '\n',
806
+ );
807
+ fs.writeFileSync(
808
+ path.join(dir, 'evals/cases.jsonl'),
809
+ JSON.stringify({
810
+ id: 'case-001',
811
+ input: 'Sample input.',
812
+ expected: { gate: 'quality-gate', result: 'pass' },
813
+ }) + '\n',
814
+ );
815
+ fs.writeFileSync(
816
+ path.join(dir, 'examples/sample-input.md'),
817
+ '# Sample Input\n\n[Replace with a real example.]\n',
818
+ );
819
+ fs.writeFileSync(
820
+ path.join(dir, 'templates/task-template.md'),
821
+ `## Task: ${name}\n\n### Input\n{{input}}\n\n### Instructions\nApply the loaded KDNA judgment framework.\n`,
822
+ );
823
+ fs.writeFileSync(
824
+ path.join(dir, 'templates/output-template.md'),
825
+ `# ${name} Report\n\n## Summary\n{{summary}}\n\n## Gate Results\n{{gate_results}}\n`,
826
+ );
827
+ fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules/\n.DS_Store\n.runs/\n');
828
+
829
+ console.log(`✓ Work Pack "${name}" created at ./${name}/`);
830
+ console.log(` cd ${name} && kdna workpack validate .`);
831
+ }
832
+
833
+ // ── Dispatcher ──────────────────────────────────────────────────────
834
+
835
+ function cmdWorkpack(args) {
836
+ const sub = args[1];
837
+ const target = args[2];
838
+
839
+ if (sub === 'init') {
840
+ cmdWorkpackInit(target, args);
841
+ } else if (sub === 'validate') {
842
+ if (!target) error('Usage: kdna workpack validate <path> [--json] [--schema-only]');
843
+ cmdWorkpackValidate(target, args);
844
+ } else if (sub === 'inspect') {
845
+ if (!target) error('Usage: kdna workpack inspect <path> [--json]');
846
+ cmdWorkpackInspect(target, args);
847
+ } else if (sub === 'explain') {
848
+ if (!target) error('Usage: kdna workpack explain <path>');
849
+ cmdWorkpackExplain(target);
850
+ } else if (sub === 'plan') {
851
+ if (!target) error('Usage: kdna workpack plan <path> [--input <file|text>] [--json]');
852
+ cmdWorkpackPlan(target, args);
853
+ } else if (sub === 'run') {
854
+ if (!target) error('Usage: kdna workpack run <path> --input <file> [--dry-run] [--json]');
855
+ cmdWorkpackRun(target, args);
856
+ } else if (sub === 'report') {
857
+ if (!target) error('Usage: kdna workpack report <session-id|path> [--json]');
858
+ cmdWorkpackReport(target, args);
859
+ } else {
860
+ error(
861
+ `Unknown workpack subcommand: ${sub || '(none)'}\n` +
862
+ 'Usage:\n' +
863
+ ' kdna workpack init <name> [--domain <domain>]\n' +
864
+ ' kdna workpack validate <path> [--json] [--schema-only]\n' +
865
+ ' kdna workpack inspect <path> [--json]\n' +
866
+ ' kdna workpack explain <path>\n' +
867
+ ' kdna workpack plan <path> [--input <file>] [--json]\n' +
868
+ ' kdna workpack run <path> --input <file> [--dry-run] [--json]\n' +
869
+ ' kdna workpack report <session-id|path> [--json]',
870
+ EXIT.INPUT_ERROR,
871
+ );
872
+ }
873
+ }
874
+
875
+ module.exports = { cmdWorkpack };