pan-wizard 3.12.5 → 3.14.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.
Files changed (53) hide show
  1. package/README.md +5 -5
  2. package/agents/pan-hardener.md +1 -0
  3. package/agents/pan-meta-reviewer.md +1 -0
  4. package/agents/pan-planner.md +16 -0
  5. package/agents/pan-reviewer.md +1 -0
  6. package/bin/install-lib.cjs +8 -0
  7. package/commands/pan/audit-deployment.md +8 -8
  8. package/commands/pan/focus-auto.md +10 -6
  9. package/commands/pan/hygiene.md +69 -0
  10. package/commands/pan/milestone-done.md +3 -2
  11. package/hooks/dist/pan-context-monitor.js +24 -12
  12. package/hooks/dist/pan-statusline.js +7 -1
  13. package/package.json +5 -5
  14. package/pan-wizard-core/bin/lib/config.cjs +14 -1
  15. package/pan-wizard-core/bin/lib/constants.cjs +40 -0
  16. package/pan-wizard-core/bin/lib/core.cjs +6 -2
  17. package/pan-wizard-core/bin/lib/doc-lint.cjs +86 -1
  18. package/pan-wizard-core/bin/lib/focus.cjs +48 -2
  19. package/pan-wizard-core/bin/lib/hud.cjs +19 -3
  20. package/pan-wizard-core/bin/lib/hygiene.cjs +447 -0
  21. package/pan-wizard-core/bin/lib/knowledge.cjs +30 -14
  22. package/pan-wizard-core/bin/lib/learn-index.cjs +17 -0
  23. package/pan-wizard-core/bin/lib/memory.cjs +146 -3
  24. package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
  25. package/pan-wizard-core/bin/lib/phase-remove.cjs +1 -1
  26. package/pan-wizard-core/bin/lib/phase.cjs +4 -4
  27. package/pan-wizard-core/bin/lib/review-deep.cjs +3 -1
  28. package/pan-wizard-core/bin/lib/skill-align.cjs +364 -0
  29. package/pan-wizard-core/bin/lib/verify.cjs +182 -61
  30. package/pan-wizard-core/bin/pan-tools.cjs +57 -1
  31. package/pan-wizard-core/learnings/index.json +262 -10
  32. package/pan-wizard-core/learnings/internal/external-research.md +13 -1
  33. package/pan-wizard-core/learnings/universal/adversarial-verification.md +45 -0
  34. package/pan-wizard-core/learnings/universal/audit-convergence.md +33 -0
  35. package/pan-wizard-core/learnings/universal/autonomous-loop.md +4 -4
  36. package/pan-wizard-core/learnings/universal/external-tool-truth.md +21 -0
  37. package/pan-wizard-core/learnings/universal/fix-campaigns.md +45 -0
  38. package/pan-wizard-core/learnings/universal/flaky-triage.md +33 -0
  39. package/pan-wizard-core/learnings/universal/golden-sets.md +33 -0
  40. package/pan-wizard-core/learnings/universal/harness-isolation.md +21 -0
  41. package/pan-wizard-core/learnings/universal/integration-verification.md +33 -0
  42. package/pan-wizard-core/learnings/universal/live-path-honesty.md +45 -0
  43. package/pan-wizard-core/learnings/universal/mcp-security.md +21 -0
  44. package/pan-wizard-core/learnings/universal/migration-safety.md +21 -0
  45. package/pan-wizard-core/learnings/universal/service-security.md +21 -0
  46. package/pan-wizard-core/learnings/universal/single-source-of-truth.md +33 -0
  47. package/pan-wizard-core/learnings/universal/test-integrity.md +21 -0
  48. package/pan-wizard-core/learnings/universal/workaround-catalog.md +21 -0
  49. package/pan-wizard-core/references/model-profiles.md +4 -1
  50. package/pan-wizard-core/workflows/exec-phase.md +26 -3
  51. package/pan-wizard-core/workflows/plan-phase.md +1 -0
  52. package/scripts/release-check.js +29 -14
  53. package/scripts/run-tests.cjs +44 -0
@@ -5,7 +5,7 @@
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
  const { execFileSync } = require('child_process');
8
- const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, error } = require('./core.cjs');
8
+ const { safeReadFile, normalizePhaseName, execGit, findPhaseInternal, getMilestoneInfo, toPosix, output, error, escapeRegex } = require('./core.cjs');
9
9
  const { extractFrontmatter, parseMustHavesBlock } = require('./frontmatter.cjs');
10
10
  const { writeStateMd, readStateSafe } = require('./state.cjs');
11
11
  const {
@@ -317,32 +317,22 @@ function cmdVerifyCommits(cwd, hashes, raw) {
317
317
  * @param {boolean} raw - If true, output raw value instead of JSON
318
318
  * @returns {void}
319
319
  */
320
- function cmdVerifyArtifacts(cwd, planFilePath, raw) {
321
- if (!planFilePath) { error('plan file path required'); }
322
- const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
323
- const content = safeReadFile(fullPath);
324
- if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
325
-
326
- const artifacts = parseMustHavesBlock(content, 'artifacts');
327
- if (artifacts.length === 0) {
328
- output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
329
- return;
330
- }
331
-
320
+ /**
321
+ * Pure substance check of must_haves.artifacts against disk — no output/exit,
322
+ * so `verify reconcile` can compose it. Returns {all_passed, passed, total, artifacts}.
323
+ */
324
+ function checkArtifacts(cwd, planContent) {
325
+ const artifacts = parseMustHavesBlock(planContent, 'artifacts');
332
326
  const results = [];
333
327
  for (const artifact of artifacts) {
334
328
  if (typeof artifact === 'string') continue; // skip simple string items
335
329
  const artPath = artifact.path;
336
330
  if (!artPath) continue;
337
-
338
- const artFullPath = path.join(cwd, artPath);
339
- const fileContent = safeReadFile(artFullPath);
331
+ const fileContent = safeReadFile(path.join(cwd, artPath));
340
332
  const exists = fileContent !== null;
341
333
  const check = { path: artPath, exists, issues: [], passed: false };
342
-
343
334
  if (exists) {
344
335
  const lineCount = fileContent.split('\n').length;
345
-
346
336
  if (artifact.min_lines && lineCount < artifact.min_lines) {
347
337
  check.issues.push(`Only ${lineCount} lines, need ${artifact.min_lines}`);
348
338
  }
@@ -350,8 +340,8 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
350
340
  check.issues.push(`Missing pattern: ${artifact.contains}`);
351
341
  }
352
342
  if (artifact.exports) {
353
- const exports = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
354
- for (const exp of exports) {
343
+ const exps = Array.isArray(artifact.exports) ? artifact.exports : [artifact.exports];
344
+ for (const exp of exps) {
355
345
  if (!fileContent.includes(exp)) check.issues.push(`Missing export: ${exp}`);
356
346
  }
357
347
  }
@@ -359,43 +349,42 @@ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
359
349
  } else {
360
350
  check.issues.push('File not found');
361
351
  }
362
-
363
352
  results.push(check);
364
353
  }
365
-
366
354
  const passed = results.filter(r => r.passed).length;
367
- output({
368
- all_passed: passed === results.length,
369
- passed,
370
- total: results.length,
371
- artifacts: results,
372
- }, raw, passed === results.length ? 'valid' : 'invalid');
355
+ return { all_passed: passed === results.length, passed, total: results.length, artifacts: results };
373
356
  }
374
357
 
375
- /**
376
- * Verify must_haves.key_links from a plan.md: source-to-target references and patterns.
377
- * @param {string} cwd - Working directory path
378
- * @param {string} planFilePath - Path to the plan.md file containing key link specs
379
- * @param {boolean} raw - If true, output raw value instead of JSON
380
- * @returns {void}
381
- */
382
- function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
358
+ function cmdVerifyArtifacts(cwd, planFilePath, raw) {
383
359
  if (!planFilePath) { error('plan file path required'); }
384
360
  const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
385
361
  const content = safeReadFile(fullPath);
386
362
  if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
387
-
388
- const keyLinks = parseMustHavesBlock(content, 'key_links');
389
- if (keyLinks.length === 0) {
390
- output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
363
+ const r = checkArtifacts(cwd, content);
364
+ if (r.total === 0) {
365
+ output({ error: 'No must_haves.artifacts found in frontmatter', path: planFilePath }, raw);
391
366
  return;
392
367
  }
368
+ output(r, raw, r.all_passed ? 'valid' : 'invalid');
369
+ }
393
370
 
371
+ /**
372
+ * Verify must_haves.key_links from a plan.md: source-to-target references and patterns.
373
+ * @param {string} cwd - Working directory path
374
+ * @param {string} planFilePath - Path to the plan.md file containing key link specs
375
+ * @param {boolean} raw - If true, output raw value instead of JSON
376
+ * @returns {void}
377
+ */
378
+ /**
379
+ * Pure wiring check of must_haves.key_links against disk — no output/exit.
380
+ * Returns {all_verified, verified, total, links}.
381
+ */
382
+ function checkKeyLinks(cwd, planContent) {
383
+ const keyLinks = parseMustHavesBlock(planContent, 'key_links');
394
384
  const results = [];
395
385
  for (const link of keyLinks) {
396
386
  if (typeof link === 'string') continue;
397
387
  const check = { from: link.from, to: link.to, via: link.via || '', verified: false, detail: '' };
398
-
399
388
  const sourceContent = safeReadFile(path.join(cwd, link.from || ''));
400
389
  if (!sourceContent) {
401
390
  check.detail = 'Source file not found';
@@ -415,29 +404,142 @@ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
415
404
  }
416
405
  }
417
406
  } catch {
418
- // Regex compilation failed -- report the invalid pattern to the caller
419
407
  check.detail = `Invalid regex pattern: ${link.pattern}`;
420
408
  }
409
+ } else if (sourceContent.includes(link.to || '')) {
410
+ check.verified = true;
411
+ check.detail = 'Target referenced in source';
421
412
  } else {
422
- // No pattern: just check source references target
423
- if (sourceContent.includes(link.to || '')) {
424
- check.verified = true;
425
- check.detail = 'Target referenced in source';
426
- } else {
427
- check.detail = 'Target not referenced in source';
428
- }
413
+ check.detail = 'Target not referenced in source';
429
414
  }
430
-
431
415
  results.push(check);
432
416
  }
433
-
434
417
  const verified = results.filter(r => r.verified).length;
435
- output({
436
- all_verified: verified === results.length,
437
- verified,
438
- total: results.length,
439
- links: results,
440
- }, raw, verified === results.length ? 'valid' : 'invalid');
418
+ return { all_verified: verified === results.length, verified, total: results.length, links: results };
419
+ }
420
+
421
+ function cmdVerifyKeyLinks(cwd, planFilePath, raw) {
422
+ if (!planFilePath) { error('plan file path required'); }
423
+ const fullPath = path.isAbsolute(planFilePath) ? planFilePath : path.join(cwd, planFilePath);
424
+ const content = safeReadFile(fullPath);
425
+ if (!content) { output({ error: 'File not found', path: planFilePath }, raw); return; }
426
+ const r = checkKeyLinks(cwd, content);
427
+ if (r.total === 0) {
428
+ output({ error: 'No must_haves.key_links found in frontmatter', path: planFilePath }, raw);
429
+ return;
430
+ }
431
+ output(r, raw, r.all_verified ? 'valid' : 'invalid');
432
+ }
433
+
434
+ // ─── Reconcile: cross-check a written verification verdict against the
435
+ // mechanical signals (ADR-0036 review — closes the rubber-stamp gap). ─────────
436
+
437
+ function findPhaseDir(cwd, phaseNum) {
438
+ const base = phasesPath(cwd);
439
+ let entries;
440
+ try { entries = fs.readdirSync(base, { withFileTypes: true }); } catch { return null; }
441
+ const re = new RegExp('^0*' + String(phaseNum).replace(/[^0-9A-Za-z.]/g, '') + '-');
442
+ for (const e of entries) {
443
+ if (e.isDirectory() && re.test(e.name)) return path.join(base, e.name);
444
+ }
445
+ return null;
446
+ }
447
+
448
+ /**
449
+ * Re-derive the mechanical signals for a phase and check them against the
450
+ * verdict written in its -verification.md. A verification that CLAIMS a pass
451
+ * while artifacts fail substance checks or key-links are unwired is a
452
+ * contradiction (a rubber stamp) — reported deterministically, never trusted.
453
+ * When no must_haves are declared there are no mechanical signals to reconcile,
454
+ * so the verdict is passed through (reconciled: true, with a note).
455
+ */
456
+ function reconcilePhase(cwd, phaseNum) {
457
+ const base = { phase: String(phaseNum), found: false, reconciled: true, contradictions: [] };
458
+ const dir = findPhaseDir(cwd, phaseNum);
459
+ if (!dir) return { ...base, note: 'phase directory not found' };
460
+ let files;
461
+ try { files = fs.readdirSync(dir); } catch { return { ...base, note: 'phase directory unreadable' }; }
462
+ const verFile = files.find(f => isVerificationFile(f));
463
+ if (!verFile) return { ...base, note: 'no verification.md — absence is covered by the verification gate, not reconcile' };
464
+ const verRaw = safeReadFile(path.join(dir, verFile)) || '';
465
+ const sm = verRaw.match(/^status:\s*([A-Za-z_-]+)/m);
466
+ const status = sm ? sm[1].toLowerCase() : 'unknown';
467
+ const claimsPass = /^(pass|passed|verified|complete|verified_pass)$/.test(status);
468
+ const planFile = files.find(f => isPlanFile(f));
469
+ const planContent = planFile ? (safeReadFile(path.join(dir, planFile)) || '') : '';
470
+ const artifacts = checkArtifacts(cwd, planContent);
471
+ const keyLinks = checkKeyLinks(cwd, planContent);
472
+ const signals = artifacts.total + keyLinks.total;
473
+ const contradictions = [];
474
+ if (claimsPass) {
475
+ if (artifacts.total > 0 && !artifacts.all_passed) {
476
+ contradictions.push(`verification status "${status}" but ${artifacts.total - artifacts.passed}/${artifacts.total} artifact substance check(s) FAIL`);
477
+ }
478
+ if (keyLinks.total > 0 && !keyLinks.all_verified) {
479
+ contradictions.push(`verification status "${status}" but ${keyLinks.total - keyLinks.verified}/${keyLinks.total} key-link(s) UNWIRED`);
480
+ }
481
+ }
482
+ return {
483
+ phase: String(phaseNum), found: true, verification_status: status, claims_pass: claimsPass,
484
+ mechanical_signals: signals, artifacts, key_links: keyLinks, contradictions,
485
+ reconciled: contradictions.length === 0,
486
+ note: signals === 0 ? 'no must_haves declared — mechanical reconciliation unavailable; verdict trusted' : undefined,
487
+ };
488
+ }
489
+
490
+ function cmdVerifyReconcile(cwd, phaseNum, raw) {
491
+ if (!phaseNum) { error('Usage: verify reconcile <phase>'); }
492
+ const r = reconcilePhase(cwd, phaseNum);
493
+ output(r, raw, r.reconciled ? 'valid' : 'invalid');
494
+ process.exit(r.reconciled ? 0 : 1);
495
+ }
496
+
497
+ // ─── Stub / fake-return scanner (ADR-0036 review — closes the hardcoded
498
+ // "return {ok:true}" / "not implemented" gap the old anti-pattern grep missed,
499
+ // which only blocked the literal `return {}` and `placeholder`/`coming soon`). ─
500
+ const STUB_PATTERNS = [
501
+ { re: /\bnot[\s_-]?implemented\b/i, marker: 'not-implemented', severity: 'high' },
502
+ { re: /\bNotImplemented(Error)?\b/, marker: 'NotImplemented', severity: 'high' },
503
+ { re: /throw\s+new\s+\w*Error\s*\(\s*['"`][^'"`]*\b(unimplemented|not\s+implemented|stub|todo)\b/i, marker: 'throw-stub', severity: 'high' },
504
+ { re: /\bres(ponse)?\.status\(\s*501\s*\)/, marker: 'http-501', severity: 'high' },
505
+ { re: /\b(coming\s+soon|placeholder)\b/i, marker: 'placeholder', severity: 'medium' },
506
+ { re: /return\s*(\{\s*\}|\[\s*\])\s*;?\s*(\/\/.*)?$/, marker: 'empty-return', severity: 'medium' },
507
+ { re: /return\s*\{\s*ok\s*:\s*true\s*\}\s*;?\s*(\/\/.*)?$/, marker: 'fake-ok-return', severity: 'medium' },
508
+ { re: /\b(TODO|FIXME|XXX|HACK)\b/, marker: 'todo-marker', severity: 'low' },
509
+ ];
510
+ const STUB_CODE_EXT = /\.(js|cjs|mjs|jsx|ts|tsx|py|go|rb|java|php|rs|c|cc|cpp|h|hpp|cs|kt|swift|scala)$/i;
511
+
512
+ /**
513
+ * Scan source files for stub / fake-implementation markers. Defaults to the
514
+ * git-changed files (so it gates a handoff), or a caller-supplied file list.
515
+ * `high`-severity markers are the blocking set; TODO markers are informational.
516
+ * @returns {{scanned, findings: Array, blocking: number, total: number}}
517
+ */
518
+ function scanStubs(cwd, opts = {}) {
519
+ let files = Array.isArray(opts.files) ? opts.files : getChangedFiles(cwd);
520
+ files = (files || []).filter(f => STUB_CODE_EXT.test(f));
521
+ const findings = [];
522
+ for (const rel of files) {
523
+ const content = safeReadFile(path.join(cwd, rel));
524
+ if (content === null) continue;
525
+ const lines = content.split(/\r?\n/);
526
+ for (let i = 0; i < lines.length; i++) {
527
+ for (const { re, marker, severity } of STUB_PATTERNS) {
528
+ if (re.test(lines[i])) {
529
+ findings.push({ file: toPosix(rel), line: i + 1, marker, severity, text: lines[i].trim().slice(0, 160) });
530
+ break; // one finding per line
531
+ }
532
+ }
533
+ }
534
+ }
535
+ const blocking = findings.filter(f => f.severity === 'high').length;
536
+ return { scanned: files.length, findings, blocking, total: findings.length };
537
+ }
538
+
539
+ function cmdVerifyStubs(cwd, opts = {}, raw) {
540
+ const r = scanStubs(cwd, opts);
541
+ output(r, raw, r.blocking === 0 ? 'valid' : 'invalid');
542
+ if (opts.gate) process.exit(r.blocking > 0 ? 1 : 0);
441
543
  }
442
544
 
443
545
  /**
@@ -995,12 +1097,12 @@ function syncRequirementCheckboxes(cwd) {
995
1097
  let fixed = 0;
996
1098
  for (const phaseNum of completedPhases) {
997
1099
  const reqMatch = roadmapContent.match(
998
- new RegExp(`Phase\\s+${phaseNum.replace(/\./g, '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
1100
+ new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i')
999
1101
  );
1000
1102
  if (!reqMatch) continue;
1001
1103
  const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(id => id.trim()).filter(Boolean);
1002
1104
  for (const reqId of reqIds) {
1003
- const escaped = reqId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1105
+ const escaped = escapeRegex(reqId);
1004
1106
  const re = new RegExp(`(- \\[) (\\]\\s*\\*\\*${escaped}\\*\\*)`, 'gi');
1005
1107
  const before = reqContent;
1006
1108
  reqContent = reqContent.replace(re, '$1x$2');
@@ -1186,6 +1288,7 @@ function cmdValidateHealth(cwd, options, raw) {
1186
1288
  // Check 10 (optional): full validation — run tests and build
1187
1289
  let testStatus;
1188
1290
  let buildStatus;
1291
+ let memoryBudget;
1189
1292
  if (options.full) {
1190
1293
  testStatus = runFullTestCheck(cwd);
1191
1294
  buildStatus = runFullBuildCheck(cwd);
@@ -1195,6 +1298,14 @@ function cmdValidateHealth(cwd, options, raw) {
1195
1298
  if (buildStatus.pass === false) {
1196
1299
  addIssue('error', 'BUILD_FAIL', `Build failed (exit code ${buildStatus.exitCode})`, 'Fix build errors');
1197
1300
  }
1301
+ // Memory-load budget (ADR-0036 acceptance signal): keep per-agent memory
1302
+ // injection bounded as logs grow. Read-only, non-blocking.
1303
+ memoryBudget = require('./memory.cjs').memoryLoadBudget(cwd);
1304
+ if (memoryBudget.status === 'critical') {
1305
+ addIssue('warning', 'MEM_BUDGET', memoryBudget.advisory, "Run 'pan-tools memory compact <agent>' or scope injection with 'memory select'");
1306
+ } else if (memoryBudget.status === 'warning') {
1307
+ addIssue('info', 'MEM_BUDGET', memoryBudget.advisory, "Run 'pan-tools memory compact <agent>' or scope injection with 'memory select'");
1308
+ }
1198
1309
  }
1199
1310
 
1200
1311
  // Determine overall status from error/warning counts
@@ -1252,6 +1363,7 @@ function cmdValidateHealth(cwd, options, raw) {
1252
1363
  if (options.full) {
1253
1364
  result.test_status = testStatus;
1254
1365
  result.build_status = buildStatus;
1366
+ result.memory_budget = memoryBudget;
1255
1367
  }
1256
1368
  if (options.drift) {
1257
1369
  result.drift_status = driftResult;
@@ -1274,8 +1386,11 @@ function runFullTestCheck(cwd) {
1274
1386
  stdio: ['pipe', 'pipe', 'pipe'],
1275
1387
  encoding: 'utf-8',
1276
1388
  });
1277
- const testMatch = result.match(/# tests (\d+)/);
1278
- const passMatch = result.match(/# pass (\d+)/);
1389
+ // Match both TAP ("# tests N") and spec-reporter ("ℹ tests N") summaries —
1390
+ // modern node --test defaults to the spec reporter, which the old "# "-only
1391
+ // regex silently missed (returning tests: null).
1392
+ const testMatch = result.match(/[#ℹ]\s*tests\s+(\d+)/);
1393
+ const passMatch = result.match(/[#ℹ]\s*pass\s+(\d+)/);
1279
1394
  return {
1280
1395
  pass: true,
1281
1396
  exitCode: 0,
@@ -1334,6 +1449,12 @@ module.exports = {
1334
1449
  cmdVerifyCommits,
1335
1450
  cmdVerifyArtifacts,
1336
1451
  cmdVerifyKeyLinks,
1452
+ checkArtifacts,
1453
+ checkKeyLinks,
1454
+ reconcilePhase,
1455
+ cmdVerifyReconcile,
1456
+ scanStubs,
1457
+ cmdVerifyStubs,
1337
1458
  cmdValidateConsistency,
1338
1459
  cmdValidateHealth,
1339
1460
  cmdPreflight,
@@ -205,6 +205,8 @@ const cost = require('./lib/cost.cjs');
205
205
  const preview = require('./lib/preview.cjs');
206
206
  const reviewDeep = require('./lib/review-deep.cjs');
207
207
  const knowledge = require('./lib/knowledge.cjs');
208
+ const skillAlign = require('./lib/skill-align.cjs');
209
+ const hygiene = require('./lib/hygiene.cjs');
208
210
  const whatif = require('./lib/whatif.cjs');
209
211
  const bridge = require('./lib/bridge.cjs');
210
212
  const optimize = require('./lib/optimize.cjs');
@@ -522,6 +524,10 @@ async function main() {
522
524
  verify.cmdVerifyArtifacts(cwd, args[2], raw);
523
525
  } else if (subcommand === 'key-links') {
524
526
  verify.cmdVerifyKeyLinks(cwd, args[2], raw);
527
+ } else if (subcommand === 'reconcile') {
528
+ verify.cmdVerifyReconcile(cwd, args[2], raw);
529
+ } else if (subcommand === 'stubs') {
530
+ verify.cmdVerifyStubs(cwd, { gate: args.includes('--gate') }, raw);
525
531
  } else {
526
532
  error('Unknown verify subcommand. Available: plan-structure, phase-completeness, references, commits, artifacts, key-links');
527
533
  }
@@ -910,8 +916,16 @@ async function main() {
910
916
  memory.cmdMemoryList(cwd, raw);
911
917
  } else if (subcommand === 'compact') {
912
918
  memory.cmdMemoryCompact(cwd, args[2], args[3], raw);
919
+ } else if (subcommand === 'select') {
920
+ memory.cmdMemorySelect(cwd, args[2], {
921
+ cue: getArgValue(args, '--cue'),
922
+ tokenBudget: getArgValue(args, '--token-budget'),
923
+ recencyFloor: getArgValue(args, '--recency-floor'),
924
+ }, raw);
925
+ } else if (subcommand === 'budget') {
926
+ memory.cmdMemoryBudget(cwd, raw);
913
927
  } else {
914
- error('Unknown memory subcommand. Available: read, append, list, compact');
928
+ error('Unknown memory subcommand. Available: read, append, list, compact, select, budget');
915
929
  }
916
930
  break;
917
931
  }
@@ -970,6 +984,7 @@ async function main() {
970
984
  const maxSources = getArgValue(args, '--max-sources');
971
985
  knowledge.cmdKnowledgeAsk(cwd, question, {
972
986
  max_sources: maxSources ? Number(maxSources) : undefined,
987
+ recall_cue: getArgValue(args, '--recall-cue'),
973
988
  }, raw);
974
989
  } else if (subcommand === 'discuss') {
975
990
  const phaseNum = args[2];
@@ -989,6 +1004,41 @@ async function main() {
989
1004
  break;
990
1005
  }
991
1006
 
1007
+ case 'skills': {
1008
+ const subcommand = args[1];
1009
+ const skillRoot = getArgValue(args, '--source-root') || skillAlign.resolveSkillRoot();
1010
+ if (subcommand === 'index') {
1011
+ skillAlign.cmdSkillsIndex(skillRoot, raw);
1012
+ } else if (subcommand === 'align') {
1013
+ skillAlign.cmdSkillsAlign(skillRoot, {
1014
+ draft: getArgValue(args, '--draft'),
1015
+ draftFile: getArgValue(args, '--draft-file'),
1016
+ topK: getArgValue(args, '--top'),
1017
+ minScore: getArgValue(args, '--min-score'),
1018
+ tokenBudget: getArgValue(args, '--token-budget'),
1019
+ }, raw);
1020
+ } else {
1021
+ error('Unknown skills subcommand. Available: index, align');
1022
+ }
1023
+ break;
1024
+ }
1025
+
1026
+ case 'hygiene': {
1027
+ const subcommand = args[1];
1028
+ const hygieneOpts = {
1029
+ traceAgeDays: getArgValue(args, '--trace-age-days'),
1030
+ apply: args.includes('--apply'),
1031
+ };
1032
+ if (subcommand === 'scan') {
1033
+ hygiene.cmdHygieneScan(cwd, hygieneOpts, raw);
1034
+ } else if (subcommand === 'clean') {
1035
+ hygiene.cmdHygieneClean(cwd, hygieneOpts, raw);
1036
+ } else {
1037
+ error('Unknown hygiene subcommand. Available: scan, clean [--apply] [--trace-age-days N]');
1038
+ }
1039
+ break;
1040
+ }
1041
+
992
1042
  case 'review-deep': {
993
1043
  const subcommand = args[1];
994
1044
  const phaseNum = args[2];
@@ -1257,6 +1307,12 @@ async function main() {
1257
1307
  docLint.cmdDocLintCounts(cwd, dir, { raw, exclude });
1258
1308
  break;
1259
1309
  }
1310
+ if (subcommand === 'flags') {
1311
+ const docDirs = [];
1312
+ for (let k = 0; k < args.length; k++) if (args[k] === '--doc-dir') docDirs.push(args[k + 1]);
1313
+ docLint.cmdDocLintFlags(cwd, { docDirs: docDirs.length ? docDirs : undefined }, raw);
1314
+ break;
1315
+ }
1260
1316
  // Default: lint a directory
1261
1317
  const dir = args[1];
1262
1318
  if (!dir || dir.startsWith('--')) { error('doc-lint <dir> required (or doc-lint schema-check <path>, doc-lint counts <dir>)'); }