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.
- package/README.md +5 -5
- package/agents/pan-hardener.md +1 -0
- package/agents/pan-meta-reviewer.md +1 -0
- package/agents/pan-planner.md +16 -0
- package/agents/pan-reviewer.md +1 -0
- package/bin/install-lib.cjs +8 -0
- package/commands/pan/audit-deployment.md +8 -8
- package/commands/pan/focus-auto.md +10 -6
- package/commands/pan/hygiene.md +69 -0
- package/commands/pan/milestone-done.md +3 -2
- package/hooks/dist/pan-context-monitor.js +24 -12
- package/hooks/dist/pan-statusline.js +7 -1
- package/package.json +5 -5
- package/pan-wizard-core/bin/lib/config.cjs +14 -1
- package/pan-wizard-core/bin/lib/constants.cjs +40 -0
- package/pan-wizard-core/bin/lib/core.cjs +6 -2
- package/pan-wizard-core/bin/lib/doc-lint.cjs +86 -1
- package/pan-wizard-core/bin/lib/focus.cjs +48 -2
- package/pan-wizard-core/bin/lib/hud.cjs +19 -3
- package/pan-wizard-core/bin/lib/hygiene.cjs +447 -0
- package/pan-wizard-core/bin/lib/knowledge.cjs +30 -14
- package/pan-wizard-core/bin/lib/learn-index.cjs +17 -0
- package/pan-wizard-core/bin/lib/memory.cjs +146 -3
- package/pan-wizard-core/bin/lib/optimize.cjs +2 -2
- package/pan-wizard-core/bin/lib/phase-remove.cjs +1 -1
- package/pan-wizard-core/bin/lib/phase.cjs +4 -4
- package/pan-wizard-core/bin/lib/review-deep.cjs +3 -1
- package/pan-wizard-core/bin/lib/skill-align.cjs +364 -0
- package/pan-wizard-core/bin/lib/verify.cjs +182 -61
- package/pan-wizard-core/bin/pan-tools.cjs +57 -1
- package/pan-wizard-core/learnings/index.json +262 -10
- package/pan-wizard-core/learnings/internal/external-research.md +13 -1
- package/pan-wizard-core/learnings/universal/adversarial-verification.md +45 -0
- package/pan-wizard-core/learnings/universal/audit-convergence.md +33 -0
- package/pan-wizard-core/learnings/universal/autonomous-loop.md +4 -4
- package/pan-wizard-core/learnings/universal/external-tool-truth.md +21 -0
- package/pan-wizard-core/learnings/universal/fix-campaigns.md +45 -0
- package/pan-wizard-core/learnings/universal/flaky-triage.md +33 -0
- package/pan-wizard-core/learnings/universal/golden-sets.md +33 -0
- package/pan-wizard-core/learnings/universal/harness-isolation.md +21 -0
- package/pan-wizard-core/learnings/universal/integration-verification.md +33 -0
- package/pan-wizard-core/learnings/universal/live-path-honesty.md +45 -0
- package/pan-wizard-core/learnings/universal/mcp-security.md +21 -0
- package/pan-wizard-core/learnings/universal/migration-safety.md +21 -0
- package/pan-wizard-core/learnings/universal/service-security.md +21 -0
- package/pan-wizard-core/learnings/universal/single-source-of-truth.md +33 -0
- package/pan-wizard-core/learnings/universal/test-integrity.md +21 -0
- package/pan-wizard-core/learnings/universal/workaround-catalog.md +21 -0
- package/pan-wizard-core/references/model-profiles.md +4 -1
- package/pan-wizard-core/workflows/exec-phase.md +26 -3
- package/pan-wizard-core/workflows/plan-phase.md +1 -0
- package/scripts/release-check.js +29 -14
- 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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
354
|
-
for (const exp of
|
|
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
|
-
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1278
|
-
|
|
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>)'); }
|